detached eba94e9

fix(session): drain release eventfd first, build generations atomically, make post-submit failures sticky

Released drains before checking the point, finishPending skips old
generations, newGeneration commits only on full success and records each
retired pipeline's own generation, Close always completes and joins errors.
Adds Target.Waiting for GPU-bound deferral.
commit
eba94e937596462f3e120881796b6dc5784136e5
author
brice · · 2026-10-01 17:40 UTC
parent
8c0d565
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
1 file · +110 -49
M internal/presentation/session/target.go +110 -49
@@ -8,6 +8,8 @@ import (
88 "image"
99 "path/filepath"
1010
11+ "github.com/bnema/purego-vulkan/vulkan"
12+
1113 "github.com/bnema/nefergui/internal/layout"
1214 "github.com/bnema/nefergui/internal/presentation/buffers"
1315 "github.com/bnema/nefergui/internal/presentation/syncobj"
@@ -77,7 +79,11 @@ type Target struct {
7779
7880 list render.ListBuffer
7981 pipeline *vkdevice.Pipeline
82+ pipelineGen uint64 // pool generation the current pipeline belongs to
8083 retiredPipelines []retiredPipelines
84+ genPending bool // Pool.Resize done, images of the new generation not yet built
85+ waiting bool // the last Draw found work that only GPU progress unblocks
86+ broken error // sticky: a failure after Submit left the pipeline inconsistent
8187 acquire uint32
8288 acquireFD int
8389 formats []vkdevice.Format
@@ -131,51 +137,67 @@ func (t *Target) Resize(width, height int32) { t.wantW, t.wantH = width, height
131137 // Size reports the physical size of the current image generation.
132138 func (t *Target) Size() (int32, int32) { return t.width, t.height }
133139
134-func (t *Target) newGeneration() error {
140+// newGeneration builds the pipeline and every image of the current pool
141+// generation into locals and commits them only when all succeeded, so a
142+// failure leaves the previous size, pipeline and slots intact.
143+func (t *Target) newGeneration() (err error) {
135144 mods, err := t.Device.ExportableModifiers()
136145 if err != nil {
137146 return err
138147 }
139- if t.selected, err = vkdevice.ChooseModifier(t.formats, t.transparent, mods); err != nil {
148+ selected, err := vkdevice.ChooseModifier(t.formats, t.transparent, mods)
149+ if err != nil {
140150 return err
141151 }
142152 pipeline, err := t.Device.NewListPipeline(t.wantW, t.wantH)
143153 if err != nil {
144154 return err
145155 }
146- if t.pipeline != nil {
147- t.retiredPipelines = append(t.retiredPipelines, retiredPipelines{generation: t.Pool.Generation - 1, list: t.pipeline})
148- }
149- t.pipeline = pipeline
150- t.width, t.height = t.wantW, t.wantH
156+ fresh := make(map[uint64]*Slot)
157+ defer func() {
158+ if err != nil {
159+ for _, slot := range fresh {
160+ _ = t.closeSlot(slot)
161+ }
162+ pipeline.Close()
163+ }
164+ }()
151165 for _, b := range t.Pool.Buffers {
152166 if b.Generation != t.Pool.Generation {
153167 continue
154168 }
155- slot, err := t.newSlot(b)
169+ slot, err := t.newSlot(b, t.wantW, t.wantH, selected)
156170 if err != nil {
157171 return err
158172 }
159- t.Slots[b.ID] = slot
173+ fresh[b.ID] = slot
174+ }
175+ if t.pipeline != nil {
176+ t.retiredPipelines = append(t.retiredPipelines, retiredPipelines{generation: t.pipelineGen, list: t.pipeline})
177+ }
178+ t.pipeline, t.pipelineGen, t.selected = pipeline, t.Pool.Generation, selected
179+ t.width, t.height = t.wantW, t.wantH
180+ for id, slot := range fresh {
181+ t.Slots[id] = slot
160182 }
161183 return nil
162184 }
163185
164-func (t *Target) newSlot(b *buffers.Buffer) (_ *Slot, err error) {
186+func (t *Target) newSlot(b *buffers.Buffer, width, height int32, selected vkdevice.Modifier) (_ *Slot, err error) {
165187 slot := &Slot{Buffer: b, First: true, ReleaseFD: -1, ReleaseExport: -1}
166188 defer func() {
167189 if err != nil {
168- t.closeSlot(slot)
190+ _ = t.closeSlot(slot)
169191 }
170192 }()
171- if slot.Image, err = t.Device.AllocateImage(t.width, t.height, t.selected); err != nil {
193+ if slot.Image, err = t.Device.AllocateImage(width, height, selected); err != nil {
172194 return nil, err
173195 }
174196 if slot.Frame, err = t.Device.NewFrame(slot.Image); err != nil {
175197 return nil, err
176198 }
177199 if t.capture {
178- if slot.Frame.Readback, err = t.Device.NewReadback(t.width, t.height); err != nil {
200+ if slot.Frame.Readback, err = t.Device.NewReadback(width, height); err != nil {
179201 return nil, err
180202 }
181203 }
@@ -192,7 +214,7 @@ func (t *Target) newSlot(b *buffers.Buffer) (_ *Slot, err error) {
192214 }
193215
194216 // closeSlot frees a slot whose GPU work has completed (or never started).
195-func (t *Target) closeSlot(slot *Slot) {
217+func (t *Target) closeSlot(slot *Slot) error {
196218 if slot.Frame != nil {
197219 slot.Frame.Close()
198220 }
@@ -205,10 +227,12 @@ func (t *Target) closeSlot(slot *Slot) {
205227 if slot.Image != nil {
206228 slot.Image.Close()
207229 }
230+ var err error
208231 if slot.ReleaseHandle != 0 {
209- _ = t.Node.Destroy(slot.ReleaseHandle)
232+ err = t.Node.Destroy(slot.ReleaseHandle)
210233 }
211234 closeFD(&slot.ReleaseExport)
235+ return err
212236 }
213237
214238 func closeFD(fd *int) {
@@ -219,8 +243,12 @@ func closeFD(fd *int) {
219243 }
220244
221245 // Draw prepares and presents one frame. It returns false, touching no GPU
222-// state, when no image is ready; the caller retries after Released.
246+// state, when no image is ready; the caller retries after Released, or after a
247+// short delay while Waiting reports GPU progress is needed.
223248 func (t *Target) Draw(display []layout.Command, scale float64, out *Output) (bool, error) {
249+ if t.broken != nil {
250+ return false, t.broken
251+ }
224252 if t.closed {
225253 return false, errors.New("session: target closed")
226254 }
@@ -231,36 +259,51 @@ func (t *Target) Draw(display []layout.Command, scale float64, out *Output) (boo
231259 _ = unix.Close(r.ReleaseFD)
232260 }
233261 t.reported = t.reported[:0]
234- if t.width != t.wantW || t.height != t.wantH {
235- if t.width != 0 {
262+ if t.genPending || t.width != t.wantW || t.height != t.wantH {
263+ if !t.genPending && t.width != 0 {
236264 if err := t.Pool.Resize(3); err != nil {
237265 return false, err
238266 }
239267 }
268+ t.genPending = true
240269 if err := t.newGeneration(); err != nil {
241270 return false, err
242271 }
272+ t.genPending = false
243273 }
244274 if err := t.retire(); err != nil {
245275 return false, err
246276 }
247- if _, err := t.finishPending(); err != nil {
277+ pending, err := t.finishPending()
278+ if err != nil {
248279 return false, err
249280 }
250- b, slot, _, err := beginReady(t.Pool, t.Slots, func(s *Slot) (bool, error) { return s.Frame.Ready() })
251- if err != nil || b == nil {
281+ b, slot, waiting, err := beginReady(t.Pool, t.Slots, func(s *Slot) (bool, error) { return s.Frame.Ready() })
282+ if err != nil {
252283 return false, err
253284 }
285+ t.waiting = pending || waiting
286+ if b == nil {
287+ return false, nil
288+ }
254289 ok, err := t.submit(b, slot, display, scale, out)
255- if !ok || err != nil {
256- if b.State == buffers.Rendering {
257- b.State = buffers.Available // nothing was submitted
290+ if err != nil {
291+ // Before Submit nothing reached the GPU and the buffer is reusable.
292+ if t.broken == nil && b.State == buffers.Rendering {
293+ b.State = buffers.Available
258294 }
259295 return false, err
260296 }
261- return true, nil
297+ t.waiting = false
298+ return ok, nil
262299 }
263300
301+// Waiting reports that the last Draw found a frame blocked only on GPU
302+// progress (an image still being drawn, or a release wait not yet installed),
303+// which no event announces: the caller should retry after a short delay. It is
304+// false when every image is owned by the compositor; Released unblocks that.
305+func (t *Target) Waiting() bool { return t.waiting }
306+
264307 func (t *Target) submit(b *buffers.Buffer, slot *Slot, display []layout.Command, scale float64, out *Output) (bool, error) {
265308 frame, err := t.Preparer.Prepare(display, scale, int(t.width), int(t.height))
266309 if err != nil {
@@ -290,33 +333,37 @@ func (t *Target) submit(b *buffers.Buffer, slot *Slot, display []layout.Command,
290333 // From here the buffer is committed to the pipeline; failures are fatal.
291334 fd, err := slot.Frame.Signal.Export()
292335 if err != nil {
293- return false, fatal(err)
336+ return false, t.fail(err)
294337 }
295338 acquire, err := t.Pool.Acquire(b)
296339 if err != nil {
297340 _ = unix.Close(fd)
298- return false, fatal(err)
341+ return false, t.fail(err)
299342 }
300343 if err = t.Node.AcquirePoint(fd, t.acquire, acquire); err != nil {
301344 _ = unix.Close(fd)
302- return false, fatal(err)
345+ return false, t.fail(err)
303346 }
304347 release, err := t.Pool.Commit(b)
305348 if err != nil {
306- return false, fatal(err)
349+ return false, t.fail(err)
307350 }
308351 if err = t.Node.EventFD(slot.ReleaseHandle, release, slot.ReleaseFD); err != nil {
309- return false, fatal(err)
352+ return false, t.fail(err)
310353 }
311354 if err = t.Pool.Own(b); err != nil {
312- return false, fatal(err)
355+ return false, t.fail(err)
313356 }
314357 t.fill(out, slot, acquire, release)
315358 return true, nil
316359 }
317360
318-// fatal marks an error after which the buffer cannot be returned to Available.
319-func fatal(err error) error { return fmt.Errorf("session: presentation pipeline broken: %w", err) }
361+// fail records an error after Submit: the buffer cannot go back to Available
362+// and every later Draw returns this error.
363+func (t *Target) fail(err error) error {
364+ t.broken = fmt.Errorf("session: presentation pipeline broken: %w", err)
365+ return t.broken
366+}
320367
321368 func (t *Target) fill(out *Output, slot *Slot, acquire, release uint64) {
322369 b := slot.Buffer
@@ -342,7 +389,12 @@ func (t *Target) fill(out *Output, slot *Slot, acquire, release uint64) {
342389 func (t *Target) Released(id uint64) error {
343390 slot := t.Slots[id]
344391 if slot == nil {
345- return nil
392+ return nil // retired: its descriptors are gone
393+ }
394+ // Drain first: a notification that lands after the drain is seen by the
395+ // Signaled check below or leaves the eventfd readable for a later no-op call.
396+ if err := drainEventFD(slot.ReleaseFD); err != nil {
397+ return err
346398 }
347399 b := slot.Buffer
348400 if b.State != buffers.CompositorOwned {
@@ -353,13 +405,18 @@ func (t *Target) Released(id uint64) error {
353405 return err
354406 }
355407 if !signaled {
356- return nil // still armed; spurious call
408+ return nil // stale notification; the registration for the live point is still armed
357409 }
410+ return t.release(slot, b.ReleasePoint)
411+}
412+
413+// drainEventFD resets a non-blocking eventfd; an empty one is fine.
414+func drainEventFD(fd int) error {
358415 var counter [8]byte
359- if _, err = unix.Read(slot.ReleaseFD, counter[:]); err != nil && err != unix.EAGAIN {
416+ if _, err := unix.Read(fd, counter[:]); err != nil && err != unix.EAGAIN {
360417 return fmt.Errorf("session: read release eventfd: %w", err)
361418 }
362- return t.release(slot, b.ReleasePoint)
419+ return nil
363420 }
364421
365422 func (t *Target) release(slot *Slot, point uint64) error {
@@ -406,7 +463,8 @@ func (t *Target) release(slot *Slot, point uint64) error {
406463 func (t *Target) finishPending() (bool, error) {
407464 pending := false
408465 for _, slot := range t.Slots {
409- if slot.PendingWait == nil {
466+ // Old generations never return to Available; retire frees them.
467+ if slot.PendingWait == nil || slot.Buffer.Generation != t.Pool.Generation {
410468 continue
411469 }
412470 ready, err := slot.Frame.Ready()
@@ -446,14 +504,17 @@ func (t *Target) retire() error {
446504 eventFD := slot.ReleaseFD
447505 slot.ReleaseFD = -1
448506 wasPresented := slot.Presented
449- t.closeSlot(slot)
507+ err = t.closeSlot(slot)
450508 if wasPresented {
451509 t.pending = append(t.pending, Retired{Buffer: id, ReleaseFD: eventFD})
452510 } else if eventFD >= 0 {
453511 _ = unix.Close(eventFD)
454512 }
455513 delete(t.Slots, id)
456- if err = t.Pool.Remove(b); err != nil {
514+ if rerr := t.Pool.Remove(b); rerr != nil {
515+ return errors.Join(err, rerr)
516+ }
517+ if err != nil {
457518 return err
458519 }
459520 }
@@ -470,7 +531,6 @@ func (t *Target) retire() error {
470531 i++
471532 continue
472533 }
473- old.rect.Close()
474534 old.list.Close()
475535 t.retiredPipelines = append(t.retiredPipelines[:i], t.retiredPipelines[i+1:]...)
476536 }
@@ -495,15 +555,17 @@ func (t *Target) Readback(id uint64) (*image.NRGBA, error) {
495555 return img, nil
496556 }
497557
498-// Close waits for the GPU, then frees every descriptor and image. The
499-// compositor's imports keep their own kernel references.
558+// Close waits for the GPU, then frees every descriptor and image, always
559+// completing the teardown. The compositor's imports keep their own kernel
560+// references. It returns the joined errors of the steps that failed.
500561 func (t *Target) Close() error {
501562 if t == nil || t.closed {
502563 return nil
503564 }
504565 t.closed = true
566+ var errs []error
505567 if t.Device != nil && t.Device.Logical != 0 && t.Device.Dispatch != nil {
506- _ = t.Device.Dispatch.DeviceWaitIdle(t.Device.Logical)
568+ errs = append(errs, vulkan.Check(t.Device.Dispatch.DeviceWaitIdle(t.Device.Logical)))
507569 }
508570 for _, r := range t.reported {
509571 _ = unix.Close(r.ReleaseFD)
@@ -514,11 +576,10 @@ func (t *Target) Close() error {
514576 t.reported, t.pending = nil, nil
515577 for id, slot := range t.Slots {
516578 closeFD(&slot.ReleaseFD)
517- t.closeSlot(slot)
579+ errs = append(errs, t.closeSlot(slot))
518580 delete(t.Slots, id)
519581 }
520582 for _, old := range t.retiredPipelines {
521- old.rect.Close()
522583 old.list.Close()
523584 }
524585 t.retiredPipelines = nil
@@ -526,10 +587,10 @@ func (t *Target) Close() error {
526587 closeFD(&t.acquireFD)
527588 if t.Node != nil {
528589 if t.acquire != 0 {
529- _ = t.Node.Destroy(t.acquire)
590+ errs = append(errs, t.Node.Destroy(t.acquire))
530591 }
531- _ = t.Node.Close()
592+ errs = append(errs, t.Node.Close())
532593 }
533594 t.Device.Close()
534- return nil
595+ return errors.Join(errs...)
535596 }
@@ -8,6 +8,8 @@ import (
8 "image"8 "image"
9 "path/filepath"9 "path/filepath"
10 10
11+ "github.com/bnema/purego-vulkan/vulkan"
12+
11 "github.com/bnema/nefergui/internal/layout"13 "github.com/bnema/nefergui/internal/layout"
12 "github.com/bnema/nefergui/internal/presentation/buffers"14 "github.com/bnema/nefergui/internal/presentation/buffers"
13 "github.com/bnema/nefergui/internal/presentation/syncobj"15 "github.com/bnema/nefergui/internal/presentation/syncobj"
@@ -77,7 +79,11 @@ type Target struct {
77 79
78 list render.ListBuffer80 list render.ListBuffer
79 pipeline *vkdevice.Pipeline81 pipeline *vkdevice.Pipeline
82+ pipelineGen uint64 // pool generation the current pipeline belongs to
80 retiredPipelines []retiredPipelines83 retiredPipelines []retiredPipelines
84+ genPending bool // Pool.Resize done, images of the new generation not yet built
85+ waiting bool // the last Draw found work that only GPU progress unblocks
86+ broken error // sticky: a failure after Submit left the pipeline inconsistent
81 acquire uint3287 acquire uint32
82 acquireFD int88 acquireFD int
83 formats []vkdevice.Format89 formats []vkdevice.Format
@@ -131,51 +137,67 @@ func (t *Target) Resize(width, height int32) { t.wantW, t.wantH = width, height
131 // Size reports the physical size of the current image generation.137 // Size reports the physical size of the current image generation.
132 func (t *Target) Size() (int32, int32) { return t.width, t.height }138 func (t *Target) Size() (int32, int32) { return t.width, t.height }
133 139
134-func (t *Target) newGeneration() error {140+// newGeneration builds the pipeline and every image of the current pool
141+// generation into locals and commits them only when all succeeded, so a
142+// failure leaves the previous size, pipeline and slots intact.
143+func (t *Target) newGeneration() (err error) {
135 mods, err := t.Device.ExportableModifiers()144 mods, err := t.Device.ExportableModifiers()
136 if err != nil {145 if err != nil {
137 return err146 return err
138 }147 }
139- if t.selected, err = vkdevice.ChooseModifier(t.formats, t.transparent, mods); err != nil {148+ selected, err := vkdevice.ChooseModifier(t.formats, t.transparent, mods)
149+ if err != nil {
140 return err150 return err
141 }151 }
142 pipeline, err := t.Device.NewListPipeline(t.wantW, t.wantH)152 pipeline, err := t.Device.NewListPipeline(t.wantW, t.wantH)
143 if err != nil {153 if err != nil {
144 return err154 return err
145 }155 }
146- if t.pipeline != nil {156+ fresh := make(map[uint64]*Slot)
147- t.retiredPipelines = append(t.retiredPipelines, retiredPipelines{generation: t.Pool.Generation - 1, list: t.pipeline})157+ defer func() {
148- }158+ if err != nil {
149- t.pipeline = pipeline159+ for _, slot := range fresh {
150- t.width, t.height = t.wantW, t.wantH160+ _ = t.closeSlot(slot)
161+ }
162+ pipeline.Close()
163+ }
164+ }()
151 for _, b := range t.Pool.Buffers {165 for _, b := range t.Pool.Buffers {
152 if b.Generation != t.Pool.Generation {166 if b.Generation != t.Pool.Generation {
153 continue167 continue
154 }168 }
155- slot, err := t.newSlot(b)169+ slot, err := t.newSlot(b, t.wantW, t.wantH, selected)
156 if err != nil {170 if err != nil {
157 return err171 return err
158 }172 }
159- t.Slots[b.ID] = slot173+ fresh[b.ID] = slot
174+ }
175+ if t.pipeline != nil {
176+ t.retiredPipelines = append(t.retiredPipelines, retiredPipelines{generation: t.pipelineGen, list: t.pipeline})
177+ }
178+ t.pipeline, t.pipelineGen, t.selected = pipeline, t.Pool.Generation, selected
179+ t.width, t.height = t.wantW, t.wantH
180+ for id, slot := range fresh {
181+ t.Slots[id] = slot
160 }182 }
161 return nil183 return nil
162 }184 }
163 185
164-func (t *Target) newSlot(b *buffers.Buffer) (_ *Slot, err error) {186+func (t *Target) newSlot(b *buffers.Buffer, width, height int32, selected vkdevice.Modifier) (_ *Slot, err error) {
165 slot := &Slot{Buffer: b, First: true, ReleaseFD: -1, ReleaseExport: -1}187 slot := &Slot{Buffer: b, First: true, ReleaseFD: -1, ReleaseExport: -1}
166 defer func() {188 defer func() {
167 if err != nil {189 if err != nil {
168- t.closeSlot(slot)190+ _ = t.closeSlot(slot)
169 }191 }
170 }()192 }()
171- if slot.Image, err = t.Device.AllocateImage(t.width, t.height, t.selected); err != nil {193+ if slot.Image, err = t.Device.AllocateImage(width, height, selected); err != nil {
172 return nil, err194 return nil, err
173 }195 }
174 if slot.Frame, err = t.Device.NewFrame(slot.Image); err != nil {196 if slot.Frame, err = t.Device.NewFrame(slot.Image); err != nil {
175 return nil, err197 return nil, err
176 }198 }
177 if t.capture {199 if t.capture {
178- if slot.Frame.Readback, err = t.Device.NewReadback(t.width, t.height); err != nil {200+ if slot.Frame.Readback, err = t.Device.NewReadback(width, height); err != nil {
179 return nil, err201 return nil, err
180 }202 }
181 }203 }
@@ -192,7 +214,7 @@ func (t *Target) newSlot(b *buffers.Buffer) (_ *Slot, err error) {
192 }214 }
193 215
194 // closeSlot frees a slot whose GPU work has completed (or never started).216 // closeSlot frees a slot whose GPU work has completed (or never started).
195-func (t *Target) closeSlot(slot *Slot) {217+func (t *Target) closeSlot(slot *Slot) error {
196 if slot.Frame != nil {218 if slot.Frame != nil {
197 slot.Frame.Close()219 slot.Frame.Close()
198 }220 }
@@ -205,10 +227,12 @@ func (t *Target) closeSlot(slot *Slot) {
205 if slot.Image != nil {227 if slot.Image != nil {
206 slot.Image.Close()228 slot.Image.Close()
207 }229 }
230+ var err error
208 if slot.ReleaseHandle != 0 {231 if slot.ReleaseHandle != 0 {
209- _ = t.Node.Destroy(slot.ReleaseHandle)232+ err = t.Node.Destroy(slot.ReleaseHandle)
210 }233 }
211 closeFD(&slot.ReleaseExport)234 closeFD(&slot.ReleaseExport)
235+ return err
212 }236 }
213 237
214 func closeFD(fd *int) {238 func closeFD(fd *int) {
@@ -219,8 +243,12 @@ func closeFD(fd *int) {
219 }243 }
220 244
221 // Draw prepares and presents one frame. It returns false, touching no GPU245 // Draw prepares and presents one frame. It returns false, touching no GPU
222-// state, when no image is ready; the caller retries after Released.246+// state, when no image is ready; the caller retries after Released, or after a
247+// short delay while Waiting reports GPU progress is needed.
223 func (t *Target) Draw(display []layout.Command, scale float64, out *Output) (bool, error) {248 func (t *Target) Draw(display []layout.Command, scale float64, out *Output) (bool, error) {
249+ if t.broken != nil {
250+ return false, t.broken
251+ }
224 if t.closed {252 if t.closed {
225 return false, errors.New("session: target closed")253 return false, errors.New("session: target closed")
226 }254 }
@@ -231,36 +259,51 @@ func (t *Target) Draw(display []layout.Command, scale float64, out *Output) (boo
231 _ = unix.Close(r.ReleaseFD)259 _ = unix.Close(r.ReleaseFD)
232 }260 }
233 t.reported = t.reported[:0]261 t.reported = t.reported[:0]
234- if t.width != t.wantW || t.height != t.wantH {262+ if t.genPending || t.width != t.wantW || t.height != t.wantH {
235- if t.width != 0 {263+ if !t.genPending && t.width != 0 {
236 if err := t.Pool.Resize(3); err != nil {264 if err := t.Pool.Resize(3); err != nil {
237 return false, err265 return false, err
238 }266 }
239 }267 }
268+ t.genPending = true
240 if err := t.newGeneration(); err != nil {269 if err := t.newGeneration(); err != nil {
241 return false, err270 return false, err
242 }271 }
272+ t.genPending = false
243 }273 }
244 if err := t.retire(); err != nil {274 if err := t.retire(); err != nil {
245 return false, err275 return false, err
246 }276 }
247- if _, err := t.finishPending(); err != nil {277+ pending, err := t.finishPending()
278+ if err != nil {
248 return false, err279 return false, err
249 }280 }
250- b, slot, _, err := beginReady(t.Pool, t.Slots, func(s *Slot) (bool, error) { return s.Frame.Ready() })281+ b, slot, waiting, err := beginReady(t.Pool, t.Slots, func(s *Slot) (bool, error) { return s.Frame.Ready() })
251- if err != nil || b == nil {282+ if err != nil {
252 return false, err283 return false, err
253 }284 }
285+ t.waiting = pending || waiting
286+ if b == nil {
287+ return false, nil
288+ }
254 ok, err := t.submit(b, slot, display, scale, out)289 ok, err := t.submit(b, slot, display, scale, out)
255- if !ok || err != nil {290+ if err != nil {
256- if b.State == buffers.Rendering {291+ // Before Submit nothing reached the GPU and the buffer is reusable.
257- b.State = buffers.Available // nothing was submitted292+ if t.broken == nil && b.State == buffers.Rendering {
293+ b.State = buffers.Available
258 }294 }
259 return false, err295 return false, err
260 }296 }
261- return true, nil297+ t.waiting = false
298+ return ok, nil
262 }299 }
263 300
301+// Waiting reports that the last Draw found a frame blocked only on GPU
302+// progress (an image still being drawn, or a release wait not yet installed),
303+// which no event announces: the caller should retry after a short delay. It is
304+// false when every image is owned by the compositor; Released unblocks that.
305+func (t *Target) Waiting() bool { return t.waiting }
306+
264 func (t *Target) submit(b *buffers.Buffer, slot *Slot, display []layout.Command, scale float64, out *Output) (bool, error) {307 func (t *Target) submit(b *buffers.Buffer, slot *Slot, display []layout.Command, scale float64, out *Output) (bool, error) {
265 frame, err := t.Preparer.Prepare(display, scale, int(t.width), int(t.height))308 frame, err := t.Preparer.Prepare(display, scale, int(t.width), int(t.height))
266 if err != nil {309 if err != nil {
@@ -290,33 +333,37 @@ func (t *Target) submit(b *buffers.Buffer, slot *Slot, display []layout.Command,
290 // From here the buffer is committed to the pipeline; failures are fatal.333 // From here the buffer is committed to the pipeline; failures are fatal.
291 fd, err := slot.Frame.Signal.Export()334 fd, err := slot.Frame.Signal.Export()
292 if err != nil {335 if err != nil {
293- return false, fatal(err)336+ return false, t.fail(err)
294 }337 }
295 acquire, err := t.Pool.Acquire(b)338 acquire, err := t.Pool.Acquire(b)
296 if err != nil {339 if err != nil {
297 _ = unix.Close(fd)340 _ = unix.Close(fd)
298- return false, fatal(err)341+ return false, t.fail(err)
299 }342 }
300 if err = t.Node.AcquirePoint(fd, t.acquire, acquire); err != nil {343 if err = t.Node.AcquirePoint(fd, t.acquire, acquire); err != nil {
301 _ = unix.Close(fd)344 _ = unix.Close(fd)
302- return false, fatal(err)345+ return false, t.fail(err)
303 }346 }
304 release, err := t.Pool.Commit(b)347 release, err := t.Pool.Commit(b)
305 if err != nil {348 if err != nil {
306- return false, fatal(err)349+ return false, t.fail(err)
307 }350 }
308 if err = t.Node.EventFD(slot.ReleaseHandle, release, slot.ReleaseFD); err != nil {351 if err = t.Node.EventFD(slot.ReleaseHandle, release, slot.ReleaseFD); err != nil {
309- return false, fatal(err)352+ return false, t.fail(err)
310 }353 }
311 if err = t.Pool.Own(b); err != nil {354 if err = t.Pool.Own(b); err != nil {
312- return false, fatal(err)355+ return false, t.fail(err)
313 }356 }
314 t.fill(out, slot, acquire, release)357 t.fill(out, slot, acquire, release)
315 return true, nil358 return true, nil
316 }359 }
317 360
318-// fatal marks an error after which the buffer cannot be returned to Available.361+// fail records an error after Submit: the buffer cannot go back to Available
319-func fatal(err error) error { return fmt.Errorf("session: presentation pipeline broken: %w", err) }362+// and every later Draw returns this error.
363+func (t *Target) fail(err error) error {
364+ t.broken = fmt.Errorf("session: presentation pipeline broken: %w", err)
365+ return t.broken
366+}
320 367
321 func (t *Target) fill(out *Output, slot *Slot, acquire, release uint64) {368 func (t *Target) fill(out *Output, slot *Slot, acquire, release uint64) {
322 b := slot.Buffer369 b := slot.Buffer
@@ -342,7 +389,12 @@ func (t *Target) fill(out *Output, slot *Slot, acquire, release uint64) {
342 func (t *Target) Released(id uint64) error {389 func (t *Target) Released(id uint64) error {
343 slot := t.Slots[id]390 slot := t.Slots[id]
344 if slot == nil {391 if slot == nil {
345- return nil392+ return nil // retired: its descriptors are gone
393+ }
394+ // Drain first: a notification that lands after the drain is seen by the
395+ // Signaled check below or leaves the eventfd readable for a later no-op call.
396+ if err := drainEventFD(slot.ReleaseFD); err != nil {
397+ return err
346 }398 }
347 b := slot.Buffer399 b := slot.Buffer
348 if b.State != buffers.CompositorOwned {400 if b.State != buffers.CompositorOwned {
@@ -353,13 +405,18 @@ func (t *Target) Released(id uint64) error {
353 return err405 return err
354 }406 }
355 if !signaled {407 if !signaled {
356- return nil // still armed; spurious call408+ return nil // stale notification; the registration for the live point is still armed
357 }409 }
410+ return t.release(slot, b.ReleasePoint)
411+}
412+
413+// drainEventFD resets a non-blocking eventfd; an empty one is fine.
414+func drainEventFD(fd int) error {
358 var counter [8]byte415 var counter [8]byte
359- if _, err = unix.Read(slot.ReleaseFD, counter[:]); err != nil && err != unix.EAGAIN {416+ if _, err := unix.Read(fd, counter[:]); err != nil && err != unix.EAGAIN {
360 return fmt.Errorf("session: read release eventfd: %w", err)417 return fmt.Errorf("session: read release eventfd: %w", err)
361 }418 }
362- return t.release(slot, b.ReleasePoint)419+ return nil
363 }420 }
364 421
365 func (t *Target) release(slot *Slot, point uint64) error {422 func (t *Target) release(slot *Slot, point uint64) error {
@@ -406,7 +463,8 @@ func (t *Target) release(slot *Slot, point uint64) error {
406 func (t *Target) finishPending() (bool, error) {463 func (t *Target) finishPending() (bool, error) {
407 pending := false464 pending := false
408 for _, slot := range t.Slots {465 for _, slot := range t.Slots {
409- if slot.PendingWait == nil {466+ // Old generations never return to Available; retire frees them.
467+ if slot.PendingWait == nil || slot.Buffer.Generation != t.Pool.Generation {
410 continue468 continue
411 }469 }
412 ready, err := slot.Frame.Ready()470 ready, err := slot.Frame.Ready()
@@ -446,14 +504,17 @@ func (t *Target) retire() error {
446 eventFD := slot.ReleaseFD504 eventFD := slot.ReleaseFD
447 slot.ReleaseFD = -1505 slot.ReleaseFD = -1
448 wasPresented := slot.Presented506 wasPresented := slot.Presented
449- t.closeSlot(slot)507+ err = t.closeSlot(slot)
450 if wasPresented {508 if wasPresented {
451 t.pending = append(t.pending, Retired{Buffer: id, ReleaseFD: eventFD})509 t.pending = append(t.pending, Retired{Buffer: id, ReleaseFD: eventFD})
452 } else if eventFD >= 0 {510 } else if eventFD >= 0 {
453 _ = unix.Close(eventFD)511 _ = unix.Close(eventFD)
454 }512 }
455 delete(t.Slots, id)513 delete(t.Slots, id)
456- if err = t.Pool.Remove(b); err != nil {514+ if rerr := t.Pool.Remove(b); rerr != nil {
515+ return errors.Join(err, rerr)
516+ }
517+ if err != nil {
457 return err518 return err
458 }519 }
459 }520 }
@@ -470,7 +531,6 @@ func (t *Target) retire() error {
470 i++531 i++
471 continue532 continue
472 }533 }
473- old.rect.Close()
474 old.list.Close()534 old.list.Close()
475 t.retiredPipelines = append(t.retiredPipelines[:i], t.retiredPipelines[i+1:]...)535 t.retiredPipelines = append(t.retiredPipelines[:i], t.retiredPipelines[i+1:]...)
476 }536 }
@@ -495,15 +555,17 @@ func (t *Target) Readback(id uint64) (*image.NRGBA, error) {
495 return img, nil555 return img, nil
496 }556 }
497 557
498-// Close waits for the GPU, then frees every descriptor and image. The558+// Close waits for the GPU, then frees every descriptor and image, always
499-// compositor's imports keep their own kernel references.559+// completing the teardown. The compositor's imports keep their own kernel
560+// references. It returns the joined errors of the steps that failed.
500 func (t *Target) Close() error {561 func (t *Target) Close() error {
501 if t == nil || t.closed {562 if t == nil || t.closed {
502 return nil563 return nil
503 }564 }
504 t.closed = true565 t.closed = true
566+ var errs []error
505 if t.Device != nil && t.Device.Logical != 0 && t.Device.Dispatch != nil {567 if t.Device != nil && t.Device.Logical != 0 && t.Device.Dispatch != nil {
506- _ = t.Device.Dispatch.DeviceWaitIdle(t.Device.Logical)568+ errs = append(errs, vulkan.Check(t.Device.Dispatch.DeviceWaitIdle(t.Device.Logical)))
507 }569 }
508 for _, r := range t.reported {570 for _, r := range t.reported {
509 _ = unix.Close(r.ReleaseFD)571 _ = unix.Close(r.ReleaseFD)
@@ -514,11 +576,10 @@ func (t *Target) Close() error {
514 t.reported, t.pending = nil, nil576 t.reported, t.pending = nil, nil
515 for id, slot := range t.Slots {577 for id, slot := range t.Slots {
516 closeFD(&slot.ReleaseFD)578 closeFD(&slot.ReleaseFD)
517- t.closeSlot(slot)579+ errs = append(errs, t.closeSlot(slot))
518 delete(t.Slots, id)580 delete(t.Slots, id)
519 }581 }
520 for _, old := range t.retiredPipelines {582 for _, old := range t.retiredPipelines {
521- old.rect.Close()
522 old.list.Close()583 old.list.Close()
523 }584 }
524 t.retiredPipelines = nil585 t.retiredPipelines = nil
@@ -526,10 +587,10 @@ func (t *Target) Close() error {
526 closeFD(&t.acquireFD)587 closeFD(&t.acquireFD)
527 if t.Node != nil {588 if t.Node != nil {
528 if t.acquire != 0 {589 if t.acquire != 0 {
529- _ = t.Node.Destroy(t.acquire)590+ errs = append(errs, t.Node.Destroy(t.acquire))
530 }591 }
531- _ = t.Node.Close()592+ errs = append(errs, t.Node.Close())
532 }593 }
533 t.Device.Close()594 t.Device.Close()
534- return nil595+ return errors.Join(errs...)
535 }596 }

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 1

Tags 12

↑↓ moveenter openesc close