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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
1 file · +110 -49
M internal/presentation/session/target.go +110 -49
| @@ -8,6 +8,8 @@ import ( | |||
| 8 | 8 | "image" | |
| 9 | 9 | "path/filepath" | |
| 10 | 10 | ||
| 11 | + | "github.com/bnema/purego-vulkan/vulkan" | |
| 12 | + | ||
| 11 | 13 | "github.com/bnema/nefergui/internal/layout" | |
| 12 | 14 | "github.com/bnema/nefergui/internal/presentation/buffers" | |
| 13 | 15 | "github.com/bnema/nefergui/internal/presentation/syncobj" | |
| @@ -77,7 +79,11 @@ type Target struct { | |||
| 77 | 79 | ||
| 78 | 80 | list render.ListBuffer | |
| 79 | 81 | pipeline *vkdevice.Pipeline | |
| 82 | + | pipelineGen uint64 // pool generation the current pipeline belongs to | |
| 80 | 83 | 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 | 87 | acquire uint32 | |
| 82 | 88 | acquireFD int | |
| 83 | 89 | formats []vkdevice.Format | |
| @@ -131,51 +137,67 @@ func (t *Target) Resize(width, height int32) { t.wantW, t.wantH = width, height | |||
| 131 | 137 | // Size reports the physical size of the current image generation. | |
| 132 | 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 | 144 | mods, err := t.Device.ExportableModifiers() | |
| 136 | 145 | if err != nil { | |
| 137 | 146 | 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 | 150 | return err | |
| 141 | 151 | } | |
| 142 | 152 | pipeline, err := t.Device.NewListPipeline(t.wantW, t.wantH) | |
| 143 | 153 | if err != nil { | |
| 144 | 154 | return err | |
| 145 | 155 | } | |
| 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 | + | }() | |
| 151 | 165 | for _, b := range t.Pool.Buffers { | |
| 152 | 166 | if b.Generation != t.Pool.Generation { | |
| 153 | 167 | continue | |
| 154 | 168 | } | |
| 155 | - | slot, err := t.newSlot(b) | |
| 169 | + | slot, err := t.newSlot(b, t.wantW, t.wantH, selected) | |
| 156 | 170 | if err != nil { | |
| 157 | 171 | return err | |
| 158 | 172 | } | |
| 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 | |
| 160 | 182 | } | |
| 161 | 183 | 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 | 187 | slot := &Slot{Buffer: b, First: true, ReleaseFD: -1, ReleaseExport: -1} | |
| 166 | 188 | defer func() { | |
| 167 | 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 | 194 | return nil, err | |
| 173 | 195 | } | |
| 174 | 196 | if slot.Frame, err = t.Device.NewFrame(slot.Image); err != nil { | |
| 175 | 197 | return nil, err | |
| 176 | 198 | } | |
| 177 | 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 | 201 | 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 | 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 | 218 | if slot.Frame != nil { | |
| 197 | 219 | slot.Frame.Close() | |
| 198 | 220 | } | |
| @@ -205,10 +227,12 @@ func (t *Target) closeSlot(slot *Slot) { | |||
| 205 | 227 | if slot.Image != nil { | |
| 206 | 228 | slot.Image.Close() | |
| 207 | 229 | } | |
| 230 | + | var err error | |
| 208 | 231 | if slot.ReleaseHandle != 0 { | |
| 209 | - | _ = t.Node.Destroy(slot.ReleaseHandle) | |
| 232 | + | err = t.Node.Destroy(slot.ReleaseHandle) | |
| 210 | 233 | } | |
| 211 | 234 | closeFD(&slot.ReleaseExport) | |
| 235 | + | return err | |
| 212 | 236 | } | |
| 213 | 237 | ||
| 214 | 238 | func closeFD(fd *int) { | |
| @@ -219,8 +243,12 @@ func closeFD(fd *int) { | |||
| 219 | 243 | } | |
| 220 | 244 | ||
| 221 | 245 | // 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 | 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 | 252 | if t.closed { | |
| 225 | 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 | 259 | _ = unix.Close(r.ReleaseFD) | |
| 232 | 260 | } | |
| 233 | 261 | 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 { | |
| 236 | 264 | if err := t.Pool.Resize(3); err != nil { | |
| 237 | 265 | return false, err | |
| 238 | 266 | } | |
| 239 | 267 | } | |
| 268 | + | t.genPending = true | |
| 240 | 269 | if err := t.newGeneration(); err != nil { | |
| 241 | 270 | return false, err | |
| 242 | 271 | } | |
| 272 | + | t.genPending = false | |
| 243 | 273 | } | |
| 244 | 274 | if err := t.retire(); err != nil { | |
| 245 | 275 | return false, err | |
| 246 | 276 | } | |
| 247 | - | if _, err := t.finishPending(); err != nil { | |
| 277 | + | pending, err := t.finishPending() | |
| 278 | + | if err != nil { | |
| 248 | 279 | return false, err | |
| 249 | 280 | } | |
| 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 { | |
| 252 | 283 | return false, err | |
| 253 | 284 | } | |
| 285 | + | t.waiting = pending || waiting | |
| 286 | + | if b == nil { | |
| 287 | + | return false, nil | |
| 288 | + | } | |
| 254 | 289 | 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 | |
| 258 | 294 | } | |
| 259 | 295 | return false, err | |
| 260 | 296 | } | |
| 261 | - | return true, nil | |
| 297 | + | 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 | 307 | func (t *Target) submit(b *buffers.Buffer, slot *Slot, display []layout.Command, scale float64, out *Output) (bool, error) { | |
| 265 | 308 | frame, err := t.Preparer.Prepare(display, scale, int(t.width), int(t.height)) | |
| 266 | 309 | if err != nil { | |
| @@ -290,33 +333,37 @@ func (t *Target) submit(b *buffers.Buffer, slot *Slot, display []layout.Command, | |||
| 290 | 333 | // From here the buffer is committed to the pipeline; failures are fatal. | |
| 291 | 334 | fd, err := slot.Frame.Signal.Export() | |
| 292 | 335 | if err != nil { | |
| 293 | - | return false, fatal(err) | |
| 336 | + | return false, t.fail(err) | |
| 294 | 337 | } | |
| 295 | 338 | acquire, err := t.Pool.Acquire(b) | |
| 296 | 339 | if err != nil { | |
| 297 | 340 | _ = unix.Close(fd) | |
| 298 | - | return false, fatal(err) | |
| 341 | + | return false, t.fail(err) | |
| 299 | 342 | } | |
| 300 | 343 | if err = t.Node.AcquirePoint(fd, t.acquire, acquire); err != nil { | |
| 301 | 344 | _ = unix.Close(fd) | |
| 302 | - | return false, fatal(err) | |
| 345 | + | return false, t.fail(err) | |
| 303 | 346 | } | |
| 304 | 347 | release, err := t.Pool.Commit(b) | |
| 305 | 348 | if err != nil { | |
| 306 | - | return false, fatal(err) | |
| 349 | + | return false, t.fail(err) | |
| 307 | 350 | } | |
| 308 | 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 | 354 | if err = t.Pool.Own(b); err != nil { | |
| 312 | - | return false, fatal(err) | |
| 355 | + | return false, t.fail(err) | |
| 313 | 356 | } | |
| 314 | 357 | t.fill(out, slot, acquire, release) | |
| 315 | 358 | return true, nil | |
| 316 | 359 | } | |
| 317 | 360 | ||
| 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 | + | } | |
| 320 | 367 | ||
| 321 | 368 | func (t *Target) fill(out *Output, slot *Slot, acquire, release uint64) { | |
| 322 | 369 | b := slot.Buffer | |
| @@ -342,7 +389,12 @@ func (t *Target) fill(out *Output, slot *Slot, acquire, release uint64) { | |||
| 342 | 389 | func (t *Target) Released(id uint64) error { | |
| 343 | 390 | slot := t.Slots[id] | |
| 344 | 391 | 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 | |
| 346 | 398 | } | |
| 347 | 399 | b := slot.Buffer | |
| 348 | 400 | if b.State != buffers.CompositorOwned { | |
| @@ -353,13 +405,18 @@ func (t *Target) Released(id uint64) error { | |||
| 353 | 405 | return err | |
| 354 | 406 | } | |
| 355 | 407 | if !signaled { | |
| 356 | - | return nil // still armed; spurious call | |
| 408 | + | 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 | 415 | 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 | 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 | 422 | func (t *Target) release(slot *Slot, point uint64) error { | |
| @@ -406,7 +463,8 @@ func (t *Target) release(slot *Slot, point uint64) error { | |||
| 406 | 463 | func (t *Target) finishPending() (bool, error) { | |
| 407 | 464 | pending := false | |
| 408 | 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 | 468 | continue | |
| 411 | 469 | } | |
| 412 | 470 | ready, err := slot.Frame.Ready() | |
| @@ -446,14 +504,17 @@ func (t *Target) retire() error { | |||
| 446 | 504 | eventFD := slot.ReleaseFD | |
| 447 | 505 | slot.ReleaseFD = -1 | |
| 448 | 506 | wasPresented := slot.Presented | |
| 449 | - | t.closeSlot(slot) | |
| 507 | + | err = t.closeSlot(slot) | |
| 450 | 508 | if wasPresented { | |
| 451 | 509 | t.pending = append(t.pending, Retired{Buffer: id, ReleaseFD: eventFD}) | |
| 452 | 510 | } else if eventFD >= 0 { | |
| 453 | 511 | _ = unix.Close(eventFD) | |
| 454 | 512 | } | |
| 455 | 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 | 518 | return err | |
| 458 | 519 | } | |
| 459 | 520 | } | |
| @@ -470,7 +531,6 @@ func (t *Target) retire() error { | |||
| 470 | 531 | i++ | |
| 471 | 532 | continue | |
| 472 | 533 | } | |
| 473 | - | old.rect.Close() | |
| 474 | 534 | old.list.Close() | |
| 475 | 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 | 555 | return img, nil | |
| 496 | 556 | } | |
| 497 | 557 | ||
| 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. | |
| 500 | 561 | func (t *Target) Close() error { | |
| 501 | 562 | if t == nil || t.closed { | |
| 502 | 563 | return nil | |
| 503 | 564 | } | |
| 504 | 565 | t.closed = true | |
| 566 | + | var errs []error | |
| 505 | 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 | 570 | for _, r := range t.reported { | |
| 509 | 571 | _ = unix.Close(r.ReleaseFD) | |
| @@ -514,11 +576,10 @@ func (t *Target) Close() error { | |||
| 514 | 576 | t.reported, t.pending = nil, nil | |
| 515 | 577 | for id, slot := range t.Slots { | |
| 516 | 578 | closeFD(&slot.ReleaseFD) | |
| 517 | - | t.closeSlot(slot) | |
| 579 | + | errs = append(errs, t.closeSlot(slot)) | |
| 518 | 580 | delete(t.Slots, id) | |
| 519 | 581 | } | |
| 520 | 582 | for _, old := range t.retiredPipelines { | |
| 521 | - | old.rect.Close() | |
| 522 | 583 | old.list.Close() | |
| 523 | 584 | } | |
| 524 | 585 | t.retiredPipelines = nil | |
| @@ -526,10 +587,10 @@ func (t *Target) Close() error { | |||
| 526 | 587 | closeFD(&t.acquireFD) | |
| 527 | 588 | if t.Node != nil { | |
| 528 | 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 | 594 | t.Device.Close() | |
| 534 | - | return nil | |
| 595 | + | return errors.Join(errs...) | |
| 535 | 596 | } | |
| @@ -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.ListBuffer | 80 | list render.ListBuffer | ||
| 79 | pipeline *vkdevice.Pipeline | 81 | pipeline *vkdevice.Pipeline | ||
| 82 | + | pipelineGen uint64 // pool generation the current pipeline belongs to | |||
| 80 | retiredPipelines []retiredPipelines | 83 | 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 uint32 | 87 | acquire uint32 | ||
| 82 | acquireFD int | 88 | acquireFD int | ||
| 83 | formats []vkdevice.Format | 89 | 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 err | 146 | 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 err | 150 | 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 err | 154 | 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 = pipeline | 159 | + | for _, slot := range fresh { |
| 150 | - | t.width, t.height = t.wantW, t.wantH | 160 | + | _ = 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 | continue | 167 | 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 err | 171 | return err | ||
| 158 | } | 172 | } | ||
| 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 | |||
| 160 | } | 182 | } | ||
| 161 | return nil | 183 | 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, err | 194 | 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, err | 197 | 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, err | 201 | 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 GPU | 245 | // 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, err | 265 | 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, err | 270 | 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, err | 275 | return false, err | ||
| 246 | } | 276 | } | ||
| 247 | - | if _, err := t.finishPending(); err != nil { | 277 | + | pending, err := t.finishPending() |
| 278 | + | if err != nil { | |||
| 248 | return false, err | 279 | 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, err | 283 | 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 submitted | 292 | + | if t.broken == nil && b.State == buffers.Rendering { |
| 293 | + | b.State = buffers.Available | |||
| 258 | } | 294 | } | ||
| 259 | return false, err | 295 | return false, err | ||
| 260 | } | 296 | } | ||
| 261 | - | return true, nil | 297 | + | 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, nil | 358 | 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.Buffer | 369 | 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 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 | |||
| 346 | } | 398 | } | ||
| 347 | b := slot.Buffer | 399 | 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 err | 405 | return err | ||
| 354 | } | 406 | } | ||
| 355 | if !signaled { | 407 | if !signaled { | ||
| 356 | - | return nil // still armed; spurious call | 408 | + | 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]byte | 415 | 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 := false | 464 | 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 | continue | 468 | 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.ReleaseFD | 504 | eventFD := slot.ReleaseFD | ||
| 447 | slot.ReleaseFD = -1 | 505 | slot.ReleaseFD = -1 | ||
| 448 | wasPresented := slot.Presented | 506 | 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 err | 518 | return err | ||
| 458 | } | 519 | } | ||
| 459 | } | 520 | } | ||
| @@ -470,7 +531,6 @@ func (t *Target) retire() error { | |||||
| 470 | i++ | 531 | i++ | ||
| 471 | continue | 532 | 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, nil | 555 | return img, nil | ||
| 496 | } | 556 | } | ||
| 497 | 557 | ||||
| 498 | - | // Close waits for the GPU, then frees every descriptor and image. The | 558 | + | // 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 nil | 563 | return nil | ||
| 503 | } | 564 | } | ||
| 504 | t.closed = true | 565 | 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, nil | 576 | 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 = nil | 585 | 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 nil | 595 | + | return errors.Join(errs...) |
| 535 | } | 596 | } | ||