refactor(outputkit): share capture request bookkeeping and the shown rule between drm and headless
- commit
- 1e618e62d09628513f0a34e03fe435f3b878b547
- author
- brice · · 2026-10-08 05:47 UTC
- parent
- a315432
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
7 files · +238 -116
M internal/adapters/drm/output.go +17 -48
| @@ -1044,9 +1044,9 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||
| 1044 | 1044 | cursorScale := -1.0 // not loaded yet | |
| 1045 | 1045 | var cursorTransform ports.BufferTransform | |
| 1046 | 1046 | frame := 0 | |
| 1047 | - | var requestStorage [capture.MaxRequests]ports.CaptureRequest | |
| 1048 | - | requests := requestStorage[:0] | |
| 1047 | + | var requests capture.Requests | |
| 1049 | 1048 | ctx, cancelCaptures := context.WithCancel(ctx) | |
| 1049 | + | requests.Init(ctx, captured) | |
| 1050 | 1050 | pipeline := capture.NewPipeline(ctx, captured) | |
| 1051 | 1051 | pipeline.Security = o.Security | |
| 1052 | 1052 | if o.NewCaptureRenderer != nil { | |
| @@ -1056,19 +1056,7 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||
| 1056 | 1056 | defer func() { | |
| 1057 | 1057 | cancelCaptures() | |
| 1058 | 1058 | pipeline.Close(r) | |
| 1059 | - | for _, q := range requests { | |
| 1060 | - | if q.ID != 0 || q.Dst.File != nil { | |
| 1061 | - | capture.Fail(ctx, q, fmt.Errorf("output stopped"), captured) | |
| 1062 | - | } | |
| 1063 | - | } | |
| 1064 | - | for { | |
| 1065 | - | select { | |
| 1066 | - | case q := <-captures: | |
| 1067 | - | capture.Fail(ctx, q, fmt.Errorf("output stopped"), captured) | |
| 1068 | - | default: | |
| 1069 | - | return | |
| 1070 | - | } | |
| 1071 | - | } | |
| 1059 | + | requests.Stop(ctx, captures) | |
| 1072 | 1060 | }() | |
| 1073 | 1061 | clk := o.clock | |
| 1074 | 1062 | if clk == nil { | |
| @@ -1119,12 +1107,8 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||
| 1119 | 1107 | o.cursor.image, o.cursor.later = false, nil | |
| 1120 | 1108 | } | |
| 1121 | 1109 | } | |
| 1122 | - | if o.protected && len(requests) > 0 { | |
| 1123 | - | for _, q := range requests { | |
| 1124 | - | capture.Fail(ctx, q, fmt.Errorf("session protected"), captured) | |
| 1125 | - | } | |
| 1126 | - | clear(requests) | |
| 1127 | - | requests = requestStorage[:0] | |
| 1110 | + | if o.protected && requests.Len() > 0 { | |
| 1111 | + | requests.FailPending(capture.ErrSessionProtected) | |
| 1128 | 1112 | } | |
| 1129 | 1113 | if o.protected && enabled && !o.frame.pendingCommit() && !o.securityPrepared && !o.protectBackoffActive() { | |
| 1130 | 1114 | if err := o.prepareSecurity(ctx, r); err != nil { | |
| @@ -1200,9 +1184,9 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||
| 1200 | 1184 | gapTimer.Stop() | |
| 1201 | 1185 | } | |
| 1202 | 1186 | var holdDue <-chan time.Time | |
| 1203 | - | if len(requests) > 0 { | |
| 1187 | + | if requests.Len() > 0 { | |
| 1204 | 1188 | var wait time.Duration | |
| 1205 | - | requests, wait = pipeline.Expire(scene, requests, time.Now()) | |
| 1189 | + | requests.List, wait = pipeline.Expire(scene, requests.List, time.Now()) | |
| 1206 | 1190 | if wait > 0 { | |
| 1207 | 1191 | holdTimer.Reset(wait) | |
| 1208 | 1192 | holdDue = holdTimer.C | |
| @@ -1271,17 +1255,11 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||
| 1271 | 1255 | continue | |
| 1272 | 1256 | case q := <-captures: | |
| 1273 | 1257 | o.observeSecurity() | |
| 1258 | + | var refuse error | |
| 1274 | 1259 | if o.protected || !enabled || o.off || o.wantOff { | |
| 1275 | - | capture.Fail(ctx, q, fmt.Errorf("output off"), captured) | |
| 1276 | - | } else if len(requests) == cap(requests) { | |
| 1277 | - | capture.Fail(ctx, q, fmt.Errorf("output capture batch full"), captured) | |
| 1278 | - | } else { | |
| 1279 | - | q.Since = time.Now() | |
| 1280 | - | requests = append(requests, q) | |
| 1281 | - | // Its indicator may already be on screen; else it waits for the | |
| 1282 | - | // scene that shows it. | |
| 1283 | - | dirty = dirty || haveScene && capture.IndicatorShown(scene, q) | |
| 1260 | + | refuse = capture.ErrOutputOff | |
| 1284 | 1261 | } | |
| 1262 | + | dirty = requests.Admit(q, refuse, scene, haveScene) || dirty | |
| 1285 | 1263 | case ack := <-o.seatDisable: | |
| 1286 | 1264 | o.bypassForSeatDisable() | |
| 1287 | 1265 | // The seat is about to be disabled: no frame may select the | |
| @@ -1421,12 +1399,8 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||
| 1421 | 1399 | if o.protected && !o.securityPrepared { | |
| 1422 | 1400 | continue | |
| 1423 | 1401 | } | |
| 1424 | - | if len(requests) > 0 && (o.protected || !enabled || o.off || o.wantOff) { | |
| 1425 | - | for _, q := range requests { | |
| 1426 | - | capture.Fail(ctx, q, fmt.Errorf("output off"), captured) | |
| 1427 | - | } | |
| 1428 | - | clear(requests) | |
| 1429 | - | requests = requestStorage[:0] | |
| 1402 | + | if requests.Len() > 0 && (o.protected || !enabled || o.off || o.wantOff) { | |
| 1403 | + | requests.FailPending(capture.ErrOutputOff) | |
| 1430 | 1404 | } | |
| 1431 | 1405 | if !enabled || o.frame.pendingCommit() { | |
| 1432 | 1406 | continue | |
| @@ -1476,20 +1450,15 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||
| 1476 | 1450 | continue | |
| 1477 | 1451 | } | |
| 1478 | 1452 | start := time.Now() | |
| 1479 | - | direct, err := o.submitFrame(ctx, r, scene, drawn, seen, requests, pipeline) | |
| 1453 | + | direct, err := o.submitFrame(ctx, r, scene, drawn, seen, requests.List, pipeline) | |
| 1480 | 1454 | var fatal renderError | |
| 1481 | 1455 | if errors.As(err, &fatal) || errors.Is(err, errSecurityScene) { | |
| 1482 | - | // A failed render or epoch rejection did not hand these requests to the worker. | |
| 1483 | - | for _, q := range requests { | |
| 1484 | - | if capture.Handed(q) { | |
| 1485 | - | continue // already owned by the worker, or answered | |
| 1486 | - | } | |
| 1487 | - | capture.Fail(ctx, q, err, captured) | |
| 1488 | - | } | |
| 1489 | - | clear(requests) // held ones included: their frame is gone | |
| 1456 | + | // A failed render or epoch rejection did not hand these requests | |
| 1457 | + | // to the worker; held ones go too: their frame is gone. | |
| 1458 | + | requests.FailPending(err) | |
| 1490 | 1459 | } | |
| 1491 | 1460 | // Requests still waiting for their indicator stay; the rest is gone. | |
| 1492 | - | requests = capture.Waiting(requests, scene) | |
| 1461 | + | requests.List = capture.Waiting(requests.List, scene) | |
| 1493 | 1462 | if errors.As(err, &fatal) { | |
| 1494 | 1463 | return err | |
| 1495 | 1464 | } | |
| @@ -1044,9 +1044,9 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||||
| 1044 | cursorScale := -1.0 // not loaded yet | 1044 | cursorScale := -1.0 // not loaded yet | ||
| 1045 | var cursorTransform ports.BufferTransform | 1045 | var cursorTransform ports.BufferTransform | ||
| 1046 | frame := 0 | 1046 | frame := 0 | ||
| 1047 | - | var requestStorage [capture.MaxRequests]ports.CaptureRequest | 1047 | + | var requests capture.Requests |
| 1048 | - | requests := requestStorage[:0] | |||
| 1049 | ctx, cancelCaptures := context.WithCancel(ctx) | 1048 | ctx, cancelCaptures := context.WithCancel(ctx) | ||
| 1049 | + | requests.Init(ctx, captured) | |||
| 1050 | pipeline := capture.NewPipeline(ctx, captured) | 1050 | pipeline := capture.NewPipeline(ctx, captured) | ||
| 1051 | pipeline.Security = o.Security | 1051 | pipeline.Security = o.Security | ||
| 1052 | if o.NewCaptureRenderer != nil { | 1052 | if o.NewCaptureRenderer != nil { | ||
| @@ -1056,19 +1056,7 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||||
| 1056 | defer func() { | 1056 | defer func() { | ||
| 1057 | cancelCaptures() | 1057 | cancelCaptures() | ||
| 1058 | pipeline.Close(r) | 1058 | pipeline.Close(r) | ||
| 1059 | - | for _, q := range requests { | 1059 | + | requests.Stop(ctx, captures) |
| 1060 | - | if q.ID != 0 || q.Dst.File != nil { | |||
| 1061 | - | capture.Fail(ctx, q, fmt.Errorf("output stopped"), captured) | |||
| 1062 | - | } | |||
| 1063 | - | } | |||
| 1064 | - | for { | |||
| 1065 | - | select { | |||
| 1066 | - | case q := <-captures: | |||
| 1067 | - | capture.Fail(ctx, q, fmt.Errorf("output stopped"), captured) | |||
| 1068 | - | default: | |||
| 1069 | - | return | |||
| 1070 | - | } | |||
| 1071 | - | } | |||
| 1072 | }() | 1060 | }() | ||
| 1073 | clk := o.clock | 1061 | clk := o.clock | ||
| 1074 | if clk == nil { | 1062 | if clk == nil { | ||
| @@ -1119,12 +1107,8 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||||
| 1119 | o.cursor.image, o.cursor.later = false, nil | 1107 | o.cursor.image, o.cursor.later = false, nil | ||
| 1120 | } | 1108 | } | ||
| 1121 | } | 1109 | } | ||
| 1122 | - | if o.protected && len(requests) > 0 { | 1110 | + | if o.protected && requests.Len() > 0 { |
| 1123 | - | for _, q := range requests { | 1111 | + | requests.FailPending(capture.ErrSessionProtected) |
| 1124 | - | capture.Fail(ctx, q, fmt.Errorf("session protected"), captured) | |||
| 1125 | - | } | |||
| 1126 | - | clear(requests) | |||
| 1127 | - | requests = requestStorage[:0] | |||
| 1128 | } | 1112 | } | ||
| 1129 | if o.protected && enabled && !o.frame.pendingCommit() && !o.securityPrepared && !o.protectBackoffActive() { | 1113 | if o.protected && enabled && !o.frame.pendingCommit() && !o.securityPrepared && !o.protectBackoffActive() { | ||
| 1130 | if err := o.prepareSecurity(ctx, r); err != nil { | 1114 | if err := o.prepareSecurity(ctx, r); err != nil { | ||
| @@ -1200,9 +1184,9 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||||
| 1200 | gapTimer.Stop() | 1184 | gapTimer.Stop() | ||
| 1201 | } | 1185 | } | ||
| 1202 | var holdDue <-chan time.Time | 1186 | var holdDue <-chan time.Time | ||
| 1203 | - | if len(requests) > 0 { | 1187 | + | if requests.Len() > 0 { |
| 1204 | var wait time.Duration | 1188 | var wait time.Duration | ||
| 1205 | - | requests, wait = pipeline.Expire(scene, requests, time.Now()) | 1189 | + | requests.List, wait = pipeline.Expire(scene, requests.List, time.Now()) |
| 1206 | if wait > 0 { | 1190 | if wait > 0 { | ||
| 1207 | holdTimer.Reset(wait) | 1191 | holdTimer.Reset(wait) | ||
| 1208 | holdDue = holdTimer.C | 1192 | holdDue = holdTimer.C | ||
| @@ -1271,17 +1255,11 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||||
| 1271 | continue | 1255 | continue | ||
| 1272 | case q := <-captures: | 1256 | case q := <-captures: | ||
| 1273 | o.observeSecurity() | 1257 | o.observeSecurity() | ||
| 1258 | + | var refuse error | |||
| 1274 | if o.protected || !enabled || o.off || o.wantOff { | 1259 | if o.protected || !enabled || o.off || o.wantOff { | ||
| 1275 | - | capture.Fail(ctx, q, fmt.Errorf("output off"), captured) | 1260 | + | refuse = capture.ErrOutputOff |
| 1276 | - | } else if len(requests) == cap(requests) { | |||
| 1277 | - | capture.Fail(ctx, q, fmt.Errorf("output capture batch full"), captured) | |||
| 1278 | - | } else { | |||
| 1279 | - | q.Since = time.Now() | |||
| 1280 | - | requests = append(requests, q) | |||
| 1281 | - | // Its indicator may already be on screen; else it waits for the | |||
| 1282 | - | // scene that shows it. | |||
| 1283 | - | dirty = dirty || haveScene && capture.IndicatorShown(scene, q) | |||
| 1284 | } | 1261 | } | ||
| 1262 | + | dirty = requests.Admit(q, refuse, scene, haveScene) || dirty | |||
| 1285 | case ack := <-o.seatDisable: | 1263 | case ack := <-o.seatDisable: | ||
| 1286 | o.bypassForSeatDisable() | 1264 | o.bypassForSeatDisable() | ||
| 1287 | // The seat is about to be disabled: no frame may select the | 1265 | // The seat is about to be disabled: no frame may select the | ||
| @@ -1421,12 +1399,8 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||||
| 1421 | if o.protected && !o.securityPrepared { | 1399 | if o.protected && !o.securityPrepared { | ||
| 1422 | continue | 1400 | continue | ||
| 1423 | } | 1401 | } | ||
| 1424 | - | if len(requests) > 0 && (o.protected || !enabled || o.off || o.wantOff) { | 1402 | + | if requests.Len() > 0 && (o.protected || !enabled || o.off || o.wantOff) { |
| 1425 | - | for _, q := range requests { | 1403 | + | requests.FailPending(capture.ErrOutputOff) |
| 1426 | - | capture.Fail(ctx, q, fmt.Errorf("output off"), captured) | |||
| 1427 | - | } | |||
| 1428 | - | clear(requests) | |||
| 1429 | - | requests = requestStorage[:0] | |||
| 1430 | } | 1404 | } | ||
| 1431 | if !enabled || o.frame.pendingCommit() { | 1405 | if !enabled || o.frame.pendingCommit() { | ||
| 1432 | continue | 1406 | continue | ||
| @@ -1476,20 +1450,15 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||||
| 1476 | continue | 1450 | continue | ||
| 1477 | } | 1451 | } | ||
| 1478 | start := time.Now() | 1452 | start := time.Now() | ||
| 1479 | - | direct, err := o.submitFrame(ctx, r, scene, drawn, seen, requests, pipeline) | 1453 | + | direct, err := o.submitFrame(ctx, r, scene, drawn, seen, requests.List, pipeline) |
| 1480 | var fatal renderError | 1454 | var fatal renderError | ||
| 1481 | if errors.As(err, &fatal) || errors.Is(err, errSecurityScene) { | 1455 | if errors.As(err, &fatal) || errors.Is(err, errSecurityScene) { | ||
| 1482 | - | // A failed render or epoch rejection did not hand these requests to the worker. | 1456 | + | // A failed render or epoch rejection did not hand these requests |
| 1483 | - | for _, q := range requests { | 1457 | + | // to the worker; held ones go too: their frame is gone. |
| 1484 | - | if capture.Handed(q) { | 1458 | + | requests.FailPending(err) |
| 1485 | - | continue // already owned by the worker, or answered | |||
| 1486 | - | } | |||
| 1487 | - | capture.Fail(ctx, q, err, captured) | |||
| 1488 | - | } | |||
| 1489 | - | clear(requests) // held ones included: their frame is gone | |||
| 1490 | } | 1459 | } | ||
| 1491 | // Requests still waiting for their indicator stay; the rest is gone. | 1460 | // Requests still waiting for their indicator stay; the rest is gone. | ||
| 1492 | - | requests = capture.Waiting(requests, scene) | 1461 | + | requests.List = capture.Waiting(requests.List, scene) |
| 1493 | if errors.As(err, &fatal) { | 1462 | if errors.As(err, &fatal) { | ||
| 1494 | return err | 1463 | return err | ||
| 1495 | } | 1464 | } | ||
M internal/adapters/drm/report.go +4 -8
| @@ -3,6 +3,7 @@ package drm | |||
| 3 | 3 | import ( | |
| 4 | 4 | "maps" | |
| 5 | 5 | ||
| 6 | + | "github.com/bnema/neferwl/internal/adapters/outputkit/surfaces" | |
| 6 | 7 | "github.com/bnema/neferwl/internal/ports" | |
| 7 | 8 | ) | |
| 8 | 9 | ||
| @@ -28,14 +29,9 @@ func (o *Output) shownBy(s ports.Scene, seen map[ports.WindowID]uint64) map[port | |||
| 28 | 29 | if o.showsScratch == nil { | |
| 29 | 30 | o.showsScratch = make(map[ports.WindowID]uint64) | |
| 30 | 31 | } | |
| 31 | - | clear(o.showsScratch) | |
| 32 | - | for id, seq := range seen { | |
| 33 | - | // A closed window drawn from its kept content is not shown to its | |
| 34 | - | // client: the empty content has no frame to present. | |
| 35 | - | if s.Shows(id) && (o.kept == nil || !o.kept(id)) { | |
| 36 | - | o.showsScratch[id] = seq | |
| 37 | - | } | |
| 38 | - | } | |
| 32 | + | // A closed window drawn from its kept content is not shown to its | |
| 33 | + | // client: the empty content has no frame to present. | |
| 34 | + | surfaces.FillShown(o.showsScratch, seen, s, o.kept) | |
| 39 | 35 | if !maps.Equal(o.showsSnapshot, o.showsScratch) { | |
| 40 | 36 | o.showsSnapshot = maps.Clone(o.showsScratch) | |
| 41 | 37 | } | |
| @@ -3,6 +3,7 @@ package drm | |||||
| 3 | import ( | 3 | import ( | ||
| 4 | "maps" | 4 | "maps" | ||
| 5 | 5 | ||||
| 6 | + | "github.com/bnema/neferwl/internal/adapters/outputkit/surfaces" | |||
| 6 | "github.com/bnema/neferwl/internal/ports" | 7 | "github.com/bnema/neferwl/internal/ports" | ||
| 7 | ) | 8 | ) | ||
| 8 | 9 | ||||
| @@ -28,14 +29,9 @@ func (o *Output) shownBy(s ports.Scene, seen map[ports.WindowID]uint64) map[port | |||||
| 28 | if o.showsScratch == nil { | 29 | if o.showsScratch == nil { | ||
| 29 | o.showsScratch = make(map[ports.WindowID]uint64) | 30 | o.showsScratch = make(map[ports.WindowID]uint64) | ||
| 30 | } | 31 | } | ||
| 31 | - | clear(o.showsScratch) | 32 | + | // A closed window drawn from its kept content is not shown to its |
| 32 | - | for id, seq := range seen { | 33 | + | // client: the empty content has no frame to present. |
| 33 | - | // A closed window drawn from its kept content is not shown to its | 34 | + | surfaces.FillShown(o.showsScratch, seen, s, o.kept) |
| 34 | - | // client: the empty content has no frame to present. | |||
| 35 | - | if s.Shows(id) && (o.kept == nil || !o.kept(id)) { | |||
| 36 | - | o.showsScratch[id] = seq | |||
| 37 | - | } | |||
| 38 | - | } | |||
| 39 | if !maps.Equal(o.showsSnapshot, o.showsScratch) { | 35 | if !maps.Equal(o.showsSnapshot, o.showsScratch) { | ||
| 40 | o.showsSnapshot = maps.Clone(o.showsScratch) | 36 | o.showsSnapshot = maps.Clone(o.showsScratch) | ||
| 41 | } | 37 | } | ||
M internal/adapters/headless/output.go +27 -60
| @@ -2,7 +2,6 @@ package headless | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| 5 | - | "errors" | |
| 6 | 5 | "fmt" | |
| 7 | 6 | "image" | |
| 8 | 7 | "image/png" | |
| @@ -93,9 +92,9 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||
| 93 | 92 | var scene ports.Scene | |
| 94 | 93 | haveScene, dirty := false, false | |
| 95 | 94 | frame := 0 | |
| 96 | - | var requestStorage [capture.MaxRequests]ports.CaptureRequest | |
| 97 | - | requests := requestStorage[:0] | |
| 95 | + | var requests capture.Requests | |
| 98 | 96 | ctx, cancelCaptures := context.WithCancel(ctx) | |
| 97 | + | requests.Init(ctx, opts.Captured) | |
| 99 | 98 | pipeline := capture.NewPipeline(ctx, opts.Captured) | |
| 100 | 99 | pipeline.Security = opts.Security | |
| 101 | 100 | if opts.NewCaptureRenderer != nil { | |
| @@ -106,23 +105,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||
| 106 | 105 | pipeline.EndGate(capture.ErrFrameNotPresented) | |
| 107 | 106 | cancelCaptures() | |
| 108 | 107 | pipeline.Close(r) | |
| 109 | - | for _, q := range requests { | |
| 110 | - | if capture.Handed(q) { | |
| 111 | - | continue // already answered, or owned by a worker | |
| 112 | - | } | |
| 113 | - | capture.Fail(ctx, q, fmt.Errorf("output stopped"), opts.Captured) | |
| 114 | - | } | |
| 115 | - | for { | |
| 116 | - | select { | |
| 117 | - | case q, ok := <-incoming: | |
| 118 | - | if !ok { | |
| 119 | - | return | |
| 120 | - | } | |
| 121 | - | capture.Fail(ctx, q, fmt.Errorf("output stopped"), opts.Captured) | |
| 122 | - | default: | |
| 123 | - | return | |
| 124 | - | } | |
| 125 | - | } | |
| 108 | + | requests.Stop(ctx, incoming) | |
| 126 | 109 | }() | |
| 127 | 110 | var want ports.CursorChange | |
| 128 | 111 | cursorScale := -1.0 // not loaded yet | |
| @@ -132,16 +115,10 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||
| 132 | 115 | var reports presented.Queue | |
| 133 | 116 | var security ports.SecurityState | |
| 134 | 117 | securityKnown := false | |
| 135 | - | failRequests := func(reason string) { | |
| 118 | + | failRequests := func(reason error) { | |
| 136 | 119 | // Whatever this frame had handed to a worker is failed with it. | |
| 137 | - | pipeline.EndGate(capture.GateVerdict(errors.New(reason))) | |
| 138 | - | for _, q := range requests { | |
| 139 | - | if !capture.Handed(q) { | |
| 140 | - | capture.Fail(ctx, q, fmt.Errorf("%s", reason), opts.Captured) | |
| 141 | - | } | |
| 142 | - | } | |
| 143 | - | clear(requests) | |
| 144 | - | requests = requestStorage[:0] | |
| 120 | + | pipeline.EndGate(capture.GateVerdict(reason)) | |
| 121 | + | requests.FailPending(reason) | |
| 145 | 122 | } | |
| 146 | 123 | // A transition is its own frame, never a locker-buffer-dependent frame. | |
| 147 | 124 | // Keep its epoch through the fence wait and report only actual completion. | |
| @@ -194,7 +171,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||
| 194 | 171 | } | |
| 195 | 172 | // Capture reply backpressure must not postpone the first black | |
| 196 | 173 | // completion/proof. Capture drainage is a separate parent barrier. | |
| 197 | - | failRequests("session protected") | |
| 174 | + | failRequests(capture.ErrSessionProtected) | |
| 198 | 175 | } | |
| 199 | 176 | } | |
| 200 | 177 | acceptScene := func(s ports.Scene) { | |
| @@ -245,9 +222,9 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||
| 245 | 222 | // Requests wait for a scene that shows their indicator, at most | |
| 246 | 223 | // capture.HoldFor (see capture.Hold). | |
| 247 | 224 | var holdDue <-chan time.Time | |
| 248 | - | if len(requests) > 0 { | |
| 225 | + | if requests.Len() > 0 { | |
| 249 | 226 | var wait time.Duration | |
| 250 | - | requests, wait = pipeline.Expire(scene, requests, time.Now()) | |
| 227 | + | requests.List, wait = pipeline.Expire(scene, requests.List, time.Now()) | |
| 251 | 228 | if wait > 0 { | |
| 252 | 229 | holdTimer.Reset(wait) | |
| 253 | 230 | holdDue = holdTimer.C | |
| @@ -300,25 +277,19 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||
| 300 | 277 | } | |
| 301 | 278 | if err := checkSecurity(); err != nil { | |
| 302 | 279 | // q has left incoming but is not yet owned by requests/workers. | |
| 303 | - | capture.Fail(ctx, q, fmt.Errorf("output stopped"), opts.Captured) | |
| 280 | + | capture.Fail(ctx, q, capture.ErrOutputStopped, opts.Captured) | |
| 304 | 281 | if ctx.Err() != nil { | |
| 305 | 282 | return nil | |
| 306 | 283 | } | |
| 307 | 284 | return err | |
| 308 | 285 | } | |
| 286 | + | var refuse error | |
| 309 | 287 | if opts.protected() { | |
| 310 | - | capture.Fail(ctx, q, fmt.Errorf("session protected"), opts.Captured) | |
| 288 | + | refuse = capture.ErrSessionProtected | |
| 311 | 289 | } else if scene.Off { | |
| 312 | - | capture.Fail(ctx, q, fmt.Errorf("output off"), opts.Captured) | |
| 313 | - | } else if len(requests) == cap(requests) { | |
| 314 | - | capture.Fail(ctx, q, fmt.Errorf("output capture batch full"), opts.Captured) | |
| 315 | - | } else { | |
| 316 | - | q.Since = time.Now() | |
| 317 | - | requests = append(requests, q) | |
| 318 | - | // Its indicator may already be on screen; else it waits for the | |
| 319 | - | // scene that shows it. | |
| 320 | - | dirty = dirty || haveScene && capture.IndicatorShown(scene, q) | |
| 290 | + | refuse = capture.ErrOutputOff | |
| 321 | 291 | } | |
| 292 | + | dirty = requests.Admit(q, refuse, scene, haveScene) || dirty | |
| 322 | 293 | case <-retry: | |
| 323 | 294 | if err := checkSecurity(); err != nil { | |
| 324 | 295 | if ctx.Err() != nil { | |
| @@ -419,14 +390,10 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||
| 419 | 390 | return err | |
| 420 | 391 | } | |
| 421 | 392 | if opts.protected() { | |
| 422 | - | failRequests("session protected") | |
| 393 | + | failRequests(capture.ErrSessionProtected) | |
| 423 | 394 | } | |
| 424 | - | if scene.Off && len(requests) > 0 { | |
| 425 | - | for _, q := range requests { | |
| 426 | - | capture.Fail(ctx, q, fmt.Errorf("output off"), opts.Captured) | |
| 427 | - | } | |
| 428 | - | clear(requests) | |
| 429 | - | requests = requestStorage[:0] | |
| 395 | + | if scene.Off && requests.Len() > 0 { | |
| 396 | + | requests.FailPending(capture.ErrOutputOff) | |
| 430 | 397 | } | |
| 431 | 398 | if !haveScene || !dirty || scene.Off { | |
| 432 | 399 | // Contents not drawn are still read: report them. An output | |
| @@ -458,7 +425,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||
| 458 | 425 | // Requests the scene cannot serve (exclusion of another session, a | |
| 459 | 426 | // workspace that moved) are failed here; see capture.Pipeline.Split. | |
| 460 | 427 | // A request whose indicator the scene does not show yet stays held. | |
| 461 | - | ready, _ := pipeline.Hold(scene, requests, time.Now()) | |
| 428 | + | ready, _ := pipeline.Hold(scene, requests.List, time.Now()) | |
| 462 | 429 | if len(scene.CaptureIndicators) > 0 { | |
| 463 | 430 | // The captures of this frame are delivered only once its frame, | |
| 464 | 431 | // with the indicator, is presented (capture.Pipeline.EndGate). | |
| @@ -470,7 +437,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||
| 470 | 437 | // A hidden workspace: a child renderer draws it. The display's | |
| 471 | 438 | // fences do not cover the child, so wait for it before reporting. | |
| 472 | 439 | if opts.Security != nil && opts.Security.Snapshot() != scene.Security { | |
| 473 | - | failRequests("security epoch changed") | |
| 440 | + | failRequests(capture.ErrEpochChanged) | |
| 474 | 441 | continue | |
| 475 | 442 | } | |
| 476 | 443 | pipeline.SubmitHidden(scene, drawn, hidden) | |
| @@ -518,7 +485,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||
| 518 | 485 | // Exclude requests: the frame without the excluded surfaces is | |
| 519 | 486 | // drawn first (same renderer, queue order), then the displayed one. | |
| 520 | 487 | if opts.Security != nil && opts.Security.Snapshot() != scene.Security { | |
| 521 | - | failRequests("security epoch changed") | |
| 488 | + | failRequests(capture.ErrEpochChanged) | |
| 522 | 489 | continue | |
| 523 | 490 | } | |
| 524 | 491 | xdone, renderErr := r.Render(pipeline.ExcludedScene(scene), drawn) | |
| @@ -539,14 +506,14 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||
| 539 | 506 | return fmt.Errorf("render excluded frame: %w", renderErr) | |
| 540 | 507 | } | |
| 541 | 508 | if opts.Security != nil && opts.Security.Snapshot() != scene.Security { | |
| 542 | - | failRequests("security epoch changed") | |
| 509 | + | failRequests(capture.ErrEpochChanged) | |
| 543 | 510 | continue | |
| 544 | 511 | } | |
| 545 | 512 | pipeline.SubmitScoped(scene.Security, r, excluded) | |
| 546 | 513 | clear(excluded) | |
| 547 | 514 | } | |
| 548 | 515 | if opts.Security != nil && opts.Security.Snapshot() != scene.Security { | |
| 549 | - | failRequests("security epoch changed") | |
| 516 | + | failRequests(capture.ErrEpochChanged) | |
| 550 | 517 | continue | |
| 551 | 518 | } | |
| 552 | 519 | done, err := r.Render(scene, drawn) | |
| @@ -571,13 +538,13 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||
| 571 | 538 | // Do not capture or report a frame that crossed a transition during | |
| 572 | 539 | // Render/fence wait. The next loop establishes the new black proof. | |
| 573 | 540 | if opts.Security != nil && opts.Security.Snapshot() != scene.Security { | |
| 574 | - | failRequests("security epoch changed") | |
| 541 | + | failRequests(capture.ErrEpochChanged) | |
| 575 | 542 | continue | |
| 576 | 543 | } | |
| 577 | 544 | pipeline.SubmitScoped(scene.Security, r, normal) | |
| 578 | 545 | // The indicated frame is presented: release what it gated. | |
| 579 | 546 | pipeline.EndGate(nil) | |
| 580 | - | requests = capture.Waiting(requests, scene) | |
| 547 | + | requests.List = capture.Waiting(requests.List, scene) | |
| 581 | 548 | frame++ | |
| 582 | 549 | opts.report(&reports, flipInfo(scene, drawn, table.Kept), seen) | |
| 583 | 550 | if opts.ScreenshotDir != "" && !opts.protected() { | |
| @@ -671,14 +638,14 @@ func (opts Options) flush(q *presented.Queue) { | |||
| 671 | 638 | // flipInfo describes a drawn frame as a flip at the current CLOCK_MONOTONIC | |
| 672 | 639 | // time (software clock, refresh unknown), so presentation feedback and | |
| 673 | 640 | // frame callbacks follow headless frames. | |
| 674 | - | func flipInfo(scene ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent, kept func(ports.WindowID) bool) *ports.FlipInfo { | |
| 641 | + | func flipInfo(scene ports.Scene, contents map[ports.WindowID]ports.SurfaceContent, kept func(ports.WindowID) bool) *ports.FlipInfo { | |
| 675 | 642 | var ts unix.Timespec | |
| 676 | 643 | _ = unix.ClockGettime(unix.CLOCK_MONOTONIC, &ts) | |
| 677 | 644 | shows := map[ports.WindowID]uint64{} | |
| 678 | - | for id, c := range surfaces { | |
| 645 | + | for id, c := range contents { | |
| 679 | 646 | // A closed window drawn from its kept content is not shown to | |
| 680 | 647 | // its client. | |
| 681 | - | if scene.Shows(id) && !kept(id) { | |
| 648 | + | if surfaces.Shown(scene, id, kept) { | |
| 682 | 649 | shows[id] = c.Seq | |
| 683 | 650 | } | |
| 684 | 651 | } | |
| @@ -2,7 +2,6 @@ package headless | |||||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
| 5 | - | "errors" | |||
| 6 | "fmt" | 5 | "fmt" | ||
| 7 | "image" | 6 | "image" | ||
| 8 | "image/png" | 7 | "image/png" | ||
| @@ -93,9 +92,9 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||||
| 93 | var scene ports.Scene | 92 | var scene ports.Scene | ||
| 94 | haveScene, dirty := false, false | 93 | haveScene, dirty := false, false | ||
| 95 | frame := 0 | 94 | frame := 0 | ||
| 96 | - | var requestStorage [capture.MaxRequests]ports.CaptureRequest | 95 | + | var requests capture.Requests |
| 97 | - | requests := requestStorage[:0] | |||
| 98 | ctx, cancelCaptures := context.WithCancel(ctx) | 96 | ctx, cancelCaptures := context.WithCancel(ctx) | ||
| 97 | + | requests.Init(ctx, opts.Captured) | |||
| 99 | pipeline := capture.NewPipeline(ctx, opts.Captured) | 98 | pipeline := capture.NewPipeline(ctx, opts.Captured) | ||
| 100 | pipeline.Security = opts.Security | 99 | pipeline.Security = opts.Security | ||
| 101 | if opts.NewCaptureRenderer != nil { | 100 | if opts.NewCaptureRenderer != nil { | ||
| @@ -106,23 +105,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||||
| 106 | pipeline.EndGate(capture.ErrFrameNotPresented) | 105 | pipeline.EndGate(capture.ErrFrameNotPresented) | ||
| 107 | cancelCaptures() | 106 | cancelCaptures() | ||
| 108 | pipeline.Close(r) | 107 | pipeline.Close(r) | ||
| 109 | - | for _, q := range requests { | 108 | + | requests.Stop(ctx, incoming) |
| 110 | - | if capture.Handed(q) { | |||
| 111 | - | continue // already answered, or owned by a worker | |||
| 112 | - | } | |||
| 113 | - | capture.Fail(ctx, q, fmt.Errorf("output stopped"), opts.Captured) | |||
| 114 | - | } | |||
| 115 | - | for { | |||
| 116 | - | select { | |||
| 117 | - | case q, ok := <-incoming: | |||
| 118 | - | if !ok { | |||
| 119 | - | return | |||
| 120 | - | } | |||
| 121 | - | capture.Fail(ctx, q, fmt.Errorf("output stopped"), opts.Captured) | |||
| 122 | - | default: | |||
| 123 | - | return | |||
| 124 | - | } | |||
| 125 | - | } | |||
| 126 | }() | 109 | }() | ||
| 127 | var want ports.CursorChange | 110 | var want ports.CursorChange | ||
| 128 | cursorScale := -1.0 // not loaded yet | 111 | cursorScale := -1.0 // not loaded yet | ||
| @@ -132,16 +115,10 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||||
| 132 | var reports presented.Queue | 115 | var reports presented.Queue | ||
| 133 | var security ports.SecurityState | 116 | var security ports.SecurityState | ||
| 134 | securityKnown := false | 117 | securityKnown := false | ||
| 135 | - | failRequests := func(reason string) { | 118 | + | failRequests := func(reason error) { |
| 136 | // Whatever this frame had handed to a worker is failed with it. | 119 | // Whatever this frame had handed to a worker is failed with it. | ||
| 137 | - | pipeline.EndGate(capture.GateVerdict(errors.New(reason))) | 120 | + | pipeline.EndGate(capture.GateVerdict(reason)) |
| 138 | - | for _, q := range requests { | 121 | + | requests.FailPending(reason) |
| 139 | - | if !capture.Handed(q) { | |||
| 140 | - | capture.Fail(ctx, q, fmt.Errorf("%s", reason), opts.Captured) | |||
| 141 | - | } | |||
| 142 | - | } | |||
| 143 | - | clear(requests) | |||
| 144 | - | requests = requestStorage[:0] | |||
| 145 | } | 122 | } | ||
| 146 | // A transition is its own frame, never a locker-buffer-dependent frame. | 123 | // A transition is its own frame, never a locker-buffer-dependent frame. | ||
| 147 | // Keep its epoch through the fence wait and report only actual completion. | 124 | // Keep its epoch through the fence wait and report only actual completion. | ||
| @@ -194,7 +171,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||||
| 194 | } | 171 | } | ||
| 195 | // Capture reply backpressure must not postpone the first black | 172 | // Capture reply backpressure must not postpone the first black | ||
| 196 | // completion/proof. Capture drainage is a separate parent barrier. | 173 | // completion/proof. Capture drainage is a separate parent barrier. | ||
| 197 | - | failRequests("session protected") | 174 | + | failRequests(capture.ErrSessionProtected) |
| 198 | } | 175 | } | ||
| 199 | } | 176 | } | ||
| 200 | acceptScene := func(s ports.Scene) { | 177 | acceptScene := func(s ports.Scene) { | ||
| @@ -245,9 +222,9 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||||
| 245 | // Requests wait for a scene that shows their indicator, at most | 222 | // Requests wait for a scene that shows their indicator, at most | ||
| 246 | // capture.HoldFor (see capture.Hold). | 223 | // capture.HoldFor (see capture.Hold). | ||
| 247 | var holdDue <-chan time.Time | 224 | var holdDue <-chan time.Time | ||
| 248 | - | if len(requests) > 0 { | 225 | + | if requests.Len() > 0 { |
| 249 | var wait time.Duration | 226 | var wait time.Duration | ||
| 250 | - | requests, wait = pipeline.Expire(scene, requests, time.Now()) | 227 | + | requests.List, wait = pipeline.Expire(scene, requests.List, time.Now()) |
| 251 | if wait > 0 { | 228 | if wait > 0 { | ||
| 252 | holdTimer.Reset(wait) | 229 | holdTimer.Reset(wait) | ||
| 253 | holdDue = holdTimer.C | 230 | holdDue = holdTimer.C | ||
| @@ -300,25 +277,19 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||||
| 300 | } | 277 | } | ||
| 301 | if err := checkSecurity(); err != nil { | 278 | if err := checkSecurity(); err != nil { | ||
| 302 | // q has left incoming but is not yet owned by requests/workers. | 279 | // q has left incoming but is not yet owned by requests/workers. | ||
| 303 | - | capture.Fail(ctx, q, fmt.Errorf("output stopped"), opts.Captured) | 280 | + | capture.Fail(ctx, q, capture.ErrOutputStopped, opts.Captured) |
| 304 | if ctx.Err() != nil { | 281 | if ctx.Err() != nil { | ||
| 305 | return nil | 282 | return nil | ||
| 306 | } | 283 | } | ||
| 307 | return err | 284 | return err | ||
| 308 | } | 285 | } | ||
| 286 | + | var refuse error | |||
| 309 | if opts.protected() { | 287 | if opts.protected() { | ||
| 310 | - | capture.Fail(ctx, q, fmt.Errorf("session protected"), opts.Captured) | 288 | + | refuse = capture.ErrSessionProtected |
| 311 | } else if scene.Off { | 289 | } else if scene.Off { | ||
| 312 | - | capture.Fail(ctx, q, fmt.Errorf("output off"), opts.Captured) | 290 | + | refuse = capture.ErrOutputOff |
| 313 | - | } else if len(requests) == cap(requests) { | |||
| 314 | - | capture.Fail(ctx, q, fmt.Errorf("output capture batch full"), opts.Captured) | |||
| 315 | - | } else { | |||
| 316 | - | q.Since = time.Now() | |||
| 317 | - | requests = append(requests, q) | |||
| 318 | - | // Its indicator may already be on screen; else it waits for the | |||
| 319 | - | // scene that shows it. | |||
| 320 | - | dirty = dirty || haveScene && capture.IndicatorShown(scene, q) | |||
| 321 | } | 291 | } | ||
| 292 | + | dirty = requests.Admit(q, refuse, scene, haveScene) || dirty | |||
| 322 | case <-retry: | 293 | case <-retry: | ||
| 323 | if err := checkSecurity(); err != nil { | 294 | if err := checkSecurity(); err != nil { | ||
| 324 | if ctx.Err() != nil { | 295 | if ctx.Err() != nil { | ||
| @@ -419,14 +390,10 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||||
| 419 | return err | 390 | return err | ||
| 420 | } | 391 | } | ||
| 421 | if opts.protected() { | 392 | if opts.protected() { | ||
| 422 | - | failRequests("session protected") | 393 | + | failRequests(capture.ErrSessionProtected) |
| 423 | } | 394 | } | ||
| 424 | - | if scene.Off && len(requests) > 0 { | 395 | + | if scene.Off && requests.Len() > 0 { |
| 425 | - | for _, q := range requests { | 396 | + | requests.FailPending(capture.ErrOutputOff) |
| 426 | - | capture.Fail(ctx, q, fmt.Errorf("output off"), opts.Captured) | |||
| 427 | - | } | |||
| 428 | - | clear(requests) | |||
| 429 | - | requests = requestStorage[:0] | |||
| 430 | } | 397 | } | ||
| 431 | if !haveScene || !dirty || scene.Off { | 398 | if !haveScene || !dirty || scene.Off { | ||
| 432 | // Contents not drawn are still read: report them. An output | 399 | // Contents not drawn are still read: report them. An output | ||
| @@ -458,7 +425,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||||
| 458 | // Requests the scene cannot serve (exclusion of another session, a | 425 | // Requests the scene cannot serve (exclusion of another session, a | ||
| 459 | // workspace that moved) are failed here; see capture.Pipeline.Split. | 426 | // workspace that moved) are failed here; see capture.Pipeline.Split. | ||
| 460 | // A request whose indicator the scene does not show yet stays held. | 427 | // A request whose indicator the scene does not show yet stays held. | ||
| 461 | - | ready, _ := pipeline.Hold(scene, requests, time.Now()) | 428 | + | ready, _ := pipeline.Hold(scene, requests.List, time.Now()) |
| 462 | if len(scene.CaptureIndicators) > 0 { | 429 | if len(scene.CaptureIndicators) > 0 { | ||
| 463 | // The captures of this frame are delivered only once its frame, | 430 | // The captures of this frame are delivered only once its frame, | ||
| 464 | // with the indicator, is presented (capture.Pipeline.EndGate). | 431 | // with the indicator, is presented (capture.Pipeline.EndGate). | ||
| @@ -470,7 +437,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||||
| 470 | // A hidden workspace: a child renderer draws it. The display's | 437 | // A hidden workspace: a child renderer draws it. The display's | ||
| 471 | // fences do not cover the child, so wait for it before reporting. | 438 | // fences do not cover the child, so wait for it before reporting. | ||
| 472 | if opts.Security != nil && opts.Security.Snapshot() != scene.Security { | 439 | if opts.Security != nil && opts.Security.Snapshot() != scene.Security { | ||
| 473 | - | failRequests("security epoch changed") | 440 | + | failRequests(capture.ErrEpochChanged) |
| 474 | continue | 441 | continue | ||
| 475 | } | 442 | } | ||
| 476 | pipeline.SubmitHidden(scene, drawn, hidden) | 443 | pipeline.SubmitHidden(scene, drawn, hidden) | ||
| @@ -518,7 +485,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||||
| 518 | // Exclude requests: the frame without the excluded surfaces is | 485 | // Exclude requests: the frame without the excluded surfaces is | ||
| 519 | // drawn first (same renderer, queue order), then the displayed one. | 486 | // drawn first (same renderer, queue order), then the displayed one. | ||
| 520 | if opts.Security != nil && opts.Security.Snapshot() != scene.Security { | 487 | if opts.Security != nil && opts.Security.Snapshot() != scene.Security { | ||
| 521 | - | failRequests("security epoch changed") | 488 | + | failRequests(capture.ErrEpochChanged) |
| 522 | continue | 489 | continue | ||
| 523 | } | 490 | } | ||
| 524 | xdone, renderErr := r.Render(pipeline.ExcludedScene(scene), drawn) | 491 | xdone, renderErr := r.Render(pipeline.ExcludedScene(scene), drawn) | ||
| @@ -539,14 +506,14 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||||
| 539 | return fmt.Errorf("render excluded frame: %w", renderErr) | 506 | return fmt.Errorf("render excluded frame: %w", renderErr) | ||
| 540 | } | 507 | } | ||
| 541 | if opts.Security != nil && opts.Security.Snapshot() != scene.Security { | 508 | if opts.Security != nil && opts.Security.Snapshot() != scene.Security { | ||
| 542 | - | failRequests("security epoch changed") | 509 | + | failRequests(capture.ErrEpochChanged) |
| 543 | continue | 510 | continue | ||
| 544 | } | 511 | } | ||
| 545 | pipeline.SubmitScoped(scene.Security, r, excluded) | 512 | pipeline.SubmitScoped(scene.Security, r, excluded) | ||
| 546 | clear(excluded) | 513 | clear(excluded) | ||
| 547 | } | 514 | } | ||
| 548 | if opts.Security != nil && opts.Security.Snapshot() != scene.Security { | 515 | if opts.Security != nil && opts.Security.Snapshot() != scene.Security { | ||
| 549 | - | failRequests("security epoch changed") | 516 | + | failRequests(capture.ErrEpochChanged) |
| 550 | continue | 517 | continue | ||
| 551 | } | 518 | } | ||
| 552 | done, err := r.Render(scene, drawn) | 519 | done, err := r.Render(scene, drawn) | ||
| @@ -571,13 +538,13 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||||
| 571 | // Do not capture or report a frame that crossed a transition during | 538 | // Do not capture or report a frame that crossed a transition during | ||
| 572 | // Render/fence wait. The next loop establishes the new black proof. | 539 | // Render/fence wait. The next loop establishes the new black proof. | ||
| 573 | if opts.Security != nil && opts.Security.Snapshot() != scene.Security { | 540 | if opts.Security != nil && opts.Security.Snapshot() != scene.Security { | ||
| 574 | - | failRequests("security epoch changed") | 541 | + | failRequests(capture.ErrEpochChanged) |
| 575 | continue | 542 | continue | ||
| 576 | } | 543 | } | ||
| 577 | pipeline.SubmitScoped(scene.Security, r, normal) | 544 | pipeline.SubmitScoped(scene.Security, r, normal) | ||
| 578 | // The indicated frame is presented: release what it gated. | 545 | // The indicated frame is presented: release what it gated. | ||
| 579 | pipeline.EndGate(nil) | 546 | pipeline.EndGate(nil) | ||
| 580 | - | requests = capture.Waiting(requests, scene) | 547 | + | requests.List = capture.Waiting(requests.List, scene) |
| 581 | frame++ | 548 | frame++ | ||
| 582 | opts.report(&reports, flipInfo(scene, drawn, table.Kept), seen) | 549 | opts.report(&reports, flipInfo(scene, drawn, table.Kept), seen) | ||
| 583 | if opts.ScreenshotDir != "" && !opts.protected() { | 550 | if opts.ScreenshotDir != "" && !opts.protected() { | ||
| @@ -671,14 +638,14 @@ func (opts Options) flush(q *presented.Queue) { | |||||
| 671 | // flipInfo describes a drawn frame as a flip at the current CLOCK_MONOTONIC | 638 | // flipInfo describes a drawn frame as a flip at the current CLOCK_MONOTONIC | ||
| 672 | // time (software clock, refresh unknown), so presentation feedback and | 639 | // time (software clock, refresh unknown), so presentation feedback and | ||
| 673 | // frame callbacks follow headless frames. | 640 | // frame callbacks follow headless frames. | ||
| 674 | - | func flipInfo(scene ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent, kept func(ports.WindowID) bool) *ports.FlipInfo { | 641 | + | func flipInfo(scene ports.Scene, contents map[ports.WindowID]ports.SurfaceContent, kept func(ports.WindowID) bool) *ports.FlipInfo { |
| 675 | var ts unix.Timespec | 642 | var ts unix.Timespec | ||
| 676 | _ = unix.ClockGettime(unix.CLOCK_MONOTONIC, &ts) | 643 | _ = unix.ClockGettime(unix.CLOCK_MONOTONIC, &ts) | ||
| 677 | shows := map[ports.WindowID]uint64{} | 644 | shows := map[ports.WindowID]uint64{} | ||
| 678 | - | for id, c := range surfaces { | 645 | + | for id, c := range contents { |
| 679 | // A closed window drawn from its kept content is not shown to | 646 | // A closed window drawn from its kept content is not shown to | ||
| 680 | // its client. | 647 | // its client. | ||
| 681 | - | if scene.Shows(id) && !kept(id) { | 648 | + | if surfaces.Shown(scene, id, kept) { |
| 682 | shows[id] = c.Seq | 649 | shows[id] = c.Seq | ||
| 683 | } | 650 | } | ||
| 684 | } | 651 | } | ||
A internal/adapters/outputkit/capture/requests.go +95 -0
| @@ -0,0 +1,95 @@ | |||
| 1 | + | package capture | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "errors" | |
| 6 | + | "time" | |
| 7 | + | ||
| 8 | + | "github.com/bnema/neferwl/internal/ports" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | // Reasons an output owner fails a request with. | |
| 12 | + | var ( | |
| 13 | + | ErrOutputStopped = errors.New("output stopped") | |
| 14 | + | ErrOutputOff = errors.New("output off") | |
| 15 | + | ErrSessionProtected = errors.New("session protected") | |
| 16 | + | ErrBatchFull = errors.New("output capture batch full") | |
| 17 | + | ErrEpochChanged = errors.New("security epoch changed") | |
| 18 | + | ) | |
| 19 | + | ||
| 20 | + | // Requests are the capture requests an output owner collected for its next | |
| 21 | + | // frame, in fixed storage (MaxRequests): no allocation. Its owner goroutine | |
| 22 | + | // is the only user. List is the live slice the pipeline calls take and | |
| 23 | + | // return; assign their result back to it. | |
| 24 | + | type Requests struct { | |
| 25 | + | storage [MaxRequests]ports.CaptureRequest | |
| 26 | + | List []ports.CaptureRequest | |
| 27 | + | ctx context.Context | |
| 28 | + | replies chan<- ports.CaptureDone | |
| 29 | + | } | |
| 30 | + | ||
| 31 | + | // Init binds the requests to the owner's context and reply channel. | |
| 32 | + | func (r *Requests) Init(ctx context.Context, replies chan<- ports.CaptureDone) { | |
| 33 | + | r.List, r.ctx, r.replies = r.storage[:0], ctx, replies | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | // Len is the number of requests held. | |
| 37 | + | func (r *Requests) Len() int { return len(r.List) } | |
| 38 | + | ||
| 39 | + | // Admit takes q for the next frame, or fails it with refuse when that is not | |
| 40 | + | // nil, or when the batch is full. It reports whether q needs a new frame: its | |
| 41 | + | // indicator is already on the current scene. | |
| 42 | + | func (r *Requests) Admit(q ports.CaptureRequest, refuse error, scene ports.Scene, haveScene bool) (redraw bool) { | |
| 43 | + | if refuse == nil && len(r.List) == cap(r.List) { | |
| 44 | + | refuse = ErrBatchFull | |
| 45 | + | } | |
| 46 | + | if refuse != nil { | |
| 47 | + | Fail(r.ctx, q, refuse, r.replies) | |
| 48 | + | return false | |
| 49 | + | } | |
| 50 | + | q.Since = time.Now() | |
| 51 | + | r.List = append(r.List, q) | |
| 52 | + | // Its indicator may already be on screen; else it waits for the scene | |
| 53 | + | // that shows it. | |
| 54 | + | return haveScene && IndicatorShown(scene, q) | |
| 55 | + | } | |
| 56 | + | ||
| 57 | + | // FailPending fails every request not yet handed to the capture worker and | |
| 58 | + | // empties the list. | |
| 59 | + | func (r *Requests) FailPending(err error) { | |
| 60 | + | for _, q := range r.List { | |
| 61 | + | if !Handed(q) { | |
| 62 | + | Fail(r.ctx, q, err, r.replies) | |
| 63 | + | } | |
| 64 | + | } | |
| 65 | + | r.Reset() | |
| 66 | + | } | |
| 67 | + | ||
| 68 | + | // Reset empties the list without answering: its requests were answered or | |
| 69 | + | // handed over. | |
| 70 | + | func (r *Requests) Reset() { | |
| 71 | + | clear(r.List) | |
| 72 | + | r.List = r.storage[:0] | |
| 73 | + | } | |
| 74 | + | ||
| 75 | + | // Stop fails what the owner still holds, then what is queued on incoming, | |
| 76 | + | // with ErrOutputStopped. ctx is the context replies are sent under. It | |
| 77 | + | // returns once incoming is empty or closed. | |
| 78 | + | func (r *Requests) Stop(ctx context.Context, incoming <-chan ports.CaptureRequest) { | |
| 79 | + | for _, q := range r.List { | |
| 80 | + | if !Handed(q) { | |
| 81 | + | Fail(ctx, q, ErrOutputStopped, r.replies) | |
| 82 | + | } | |
| 83 | + | } | |
| 84 | + | for { | |
| 85 | + | select { | |
| 86 | + | case q, ok := <-incoming: | |
| 87 | + | if !ok { | |
| 88 | + | return | |
| 89 | + | } | |
| 90 | + | Fail(ctx, q, ErrOutputStopped, r.replies) | |
| 91 | + | default: | |
| 92 | + | return | |
| 93 | + | } | |
| 94 | + | } | |
| 95 | + | } | |
| @@ -0,0 +1,95 @@ | |||||
| 1 | + | package capture | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "errors" | |||
| 6 | + | "time" | |||
| 7 | + | ||||
| 8 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | // Reasons an output owner fails a request with. | |||
| 12 | + | var ( | |||
| 13 | + | ErrOutputStopped = errors.New("output stopped") | |||
| 14 | + | ErrOutputOff = errors.New("output off") | |||
| 15 | + | ErrSessionProtected = errors.New("session protected") | |||
| 16 | + | ErrBatchFull = errors.New("output capture batch full") | |||
| 17 | + | ErrEpochChanged = errors.New("security epoch changed") | |||
| 18 | + | ) | |||
| 19 | + | ||||
| 20 | + | // Requests are the capture requests an output owner collected for its next | |||
| 21 | + | // frame, in fixed storage (MaxRequests): no allocation. Its owner goroutine | |||
| 22 | + | // is the only user. List is the live slice the pipeline calls take and | |||
| 23 | + | // return; assign their result back to it. | |||
| 24 | + | type Requests struct { | |||
| 25 | + | storage [MaxRequests]ports.CaptureRequest | |||
| 26 | + | List []ports.CaptureRequest | |||
| 27 | + | ctx context.Context | |||
| 28 | + | replies chan<- ports.CaptureDone | |||
| 29 | + | } | |||
| 30 | + | ||||
| 31 | + | // Init binds the requests to the owner's context and reply channel. | |||
| 32 | + | func (r *Requests) Init(ctx context.Context, replies chan<- ports.CaptureDone) { | |||
| 33 | + | r.List, r.ctx, r.replies = r.storage[:0], ctx, replies | |||
| 34 | + | } | |||
| 35 | + | ||||
| 36 | + | // Len is the number of requests held. | |||
| 37 | + | func (r *Requests) Len() int { return len(r.List) } | |||
| 38 | + | ||||
| 39 | + | // Admit takes q for the next frame, or fails it with refuse when that is not | |||
| 40 | + | // nil, or when the batch is full. It reports whether q needs a new frame: its | |||
| 41 | + | // indicator is already on the current scene. | |||
| 42 | + | func (r *Requests) Admit(q ports.CaptureRequest, refuse error, scene ports.Scene, haveScene bool) (redraw bool) { | |||
| 43 | + | if refuse == nil && len(r.List) == cap(r.List) { | |||
| 44 | + | refuse = ErrBatchFull | |||
| 45 | + | } | |||
| 46 | + | if refuse != nil { | |||
| 47 | + | Fail(r.ctx, q, refuse, r.replies) | |||
| 48 | + | return false | |||
| 49 | + | } | |||
| 50 | + | q.Since = time.Now() | |||
| 51 | + | r.List = append(r.List, q) | |||
| 52 | + | // Its indicator may already be on screen; else it waits for the scene | |||
| 53 | + | // that shows it. | |||
| 54 | + | return haveScene && IndicatorShown(scene, q) | |||
| 55 | + | } | |||
| 56 | + | ||||
| 57 | + | // FailPending fails every request not yet handed to the capture worker and | |||
| 58 | + | // empties the list. | |||
| 59 | + | func (r *Requests) FailPending(err error) { | |||
| 60 | + | for _, q := range r.List { | |||
| 61 | + | if !Handed(q) { | |||
| 62 | + | Fail(r.ctx, q, err, r.replies) | |||
| 63 | + | } | |||
| 64 | + | } | |||
| 65 | + | r.Reset() | |||
| 66 | + | } | |||
| 67 | + | ||||
| 68 | + | // Reset empties the list without answering: its requests were answered or | |||
| 69 | + | // handed over. | |||
| 70 | + | func (r *Requests) Reset() { | |||
| 71 | + | clear(r.List) | |||
| 72 | + | r.List = r.storage[:0] | |||
| 73 | + | } | |||
| 74 | + | ||||
| 75 | + | // Stop fails what the owner still holds, then what is queued on incoming, | |||
| 76 | + | // with ErrOutputStopped. ctx is the context replies are sent under. It | |||
| 77 | + | // returns once incoming is empty or closed. | |||
| 78 | + | func (r *Requests) Stop(ctx context.Context, incoming <-chan ports.CaptureRequest) { | |||
| 79 | + | for _, q := range r.List { | |||
| 80 | + | if !Handed(q) { | |||
| 81 | + | Fail(ctx, q, ErrOutputStopped, r.replies) | |||
| 82 | + | } | |||
| 83 | + | } | |||
| 84 | + | for { | |||
| 85 | + | select { | |||
| 86 | + | case q, ok := <-incoming: | |||
| 87 | + | if !ok { | |||
| 88 | + | return | |||
| 89 | + | } | |||
| 90 | + | Fail(ctx, q, ErrOutputStopped, r.replies) | |||
| 91 | + | default: | |||
| 92 | + | return | |||
| 93 | + | } | |||
| 94 | + | } | |||
| 95 | + | } | |||
A internal/adapters/outputkit/capture/requests_test.go +63 -0
| @@ -0,0 +1,63 @@ | |||
| 1 | + | package capture | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "testing" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/neferwl/internal/ports" | |
| 8 | + | "github.com/stretchr/testify/require" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | func TestRequestsAdmitRefuseAndFull(t *testing.T) { | |
| 12 | + | replies := make(chan ports.CaptureDone, MaxRequests+2) | |
| 13 | + | var r Requests | |
| 14 | + | r.Init(context.Background(), replies) | |
| 15 | + | scene := ports.Scene{OutputWidth: 10, OutputHeight: 10} | |
| 16 | + | ||
| 17 | + | require.False(t, r.Admit(ports.CaptureRequest{ID: 1}, ErrOutputOff, scene, true)) | |
| 18 | + | require.ErrorIs(t, (<-replies).Err, ErrOutputOff) | |
| 19 | + | require.Zero(t, r.Len()) | |
| 20 | + | ||
| 21 | + | // A request without an indicator is ready at once on a known scene. | |
| 22 | + | require.True(t, r.Admit(ports.CaptureRequest{ID: 2}, nil, scene, true)) | |
| 23 | + | require.False(t, r.Admit(ports.CaptureRequest{ID: 3}, nil, scene, false), "no scene yet") | |
| 24 | + | require.False(t, r.List[0].Since.IsZero()) | |
| 25 | + | for i := r.Len(); i < MaxRequests; i++ { | |
| 26 | + | r.Admit(ports.CaptureRequest{ID: uint64(10 + i)}, nil, scene, true) | |
| 27 | + | } | |
| 28 | + | require.False(t, r.Admit(ports.CaptureRequest{ID: 99}, nil, scene, true)) | |
| 29 | + | done := <-replies | |
| 30 | + | require.Equal(t, uint64(99), done.ID) | |
| 31 | + | require.ErrorIs(t, done.Err, ErrBatchFull) | |
| 32 | + | require.Equal(t, MaxRequests, r.Len()) | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | func TestRequestsFailPendingSkipsHanded(t *testing.T) { | |
| 36 | + | replies := make(chan ports.CaptureDone, 4) | |
| 37 | + | var r Requests | |
| 38 | + | r.Init(context.Background(), replies) | |
| 39 | + | r.Admit(ports.CaptureRequest{ID: 1}, nil, ports.Scene{}, false) | |
| 40 | + | r.Admit(ports.CaptureRequest{ID: 2}, nil, ports.Scene{}, false) | |
| 41 | + | r.List[0] = ports.CaptureRequest{} // handed to the worker | |
| 42 | + | r.FailPending(ErrEpochChanged) | |
| 43 | + | require.Zero(t, r.Len()) | |
| 44 | + | require.Len(t, replies, 1) | |
| 45 | + | done := <-replies | |
| 46 | + | require.Equal(t, uint64(2), done.ID) | |
| 47 | + | require.ErrorIs(t, done.Err, ErrEpochChanged) | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | func TestRequestsStopDrainsIncoming(t *testing.T) { | |
| 51 | + | replies := make(chan ports.CaptureDone, 4) | |
| 52 | + | var r Requests | |
| 53 | + | r.Init(context.Background(), replies) | |
| 54 | + | r.Admit(ports.CaptureRequest{ID: 1}, nil, ports.Scene{}, false) | |
| 55 | + | incoming := make(chan ports.CaptureRequest, 2) | |
| 56 | + | incoming <- ports.CaptureRequest{ID: 2} | |
| 57 | + | close(incoming) | |
| 58 | + | r.Stop(context.Background(), incoming) | |
| 59 | + | require.Len(t, replies, 2) | |
| 60 | + | for range 2 { | |
| 61 | + | require.ErrorIs(t, (<-replies).Err, ErrOutputStopped) | |
| 62 | + | } | |
| 63 | + | } | |
| @@ -0,0 +1,63 @@ | |||||
| 1 | + | package capture | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "testing" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 8 | + | "github.com/stretchr/testify/require" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | func TestRequestsAdmitRefuseAndFull(t *testing.T) { | |||
| 12 | + | replies := make(chan ports.CaptureDone, MaxRequests+2) | |||
| 13 | + | var r Requests | |||
| 14 | + | r.Init(context.Background(), replies) | |||
| 15 | + | scene := ports.Scene{OutputWidth: 10, OutputHeight: 10} | |||
| 16 | + | ||||
| 17 | + | require.False(t, r.Admit(ports.CaptureRequest{ID: 1}, ErrOutputOff, scene, true)) | |||
| 18 | + | require.ErrorIs(t, (<-replies).Err, ErrOutputOff) | |||
| 19 | + | require.Zero(t, r.Len()) | |||
| 20 | + | ||||
| 21 | + | // A request without an indicator is ready at once on a known scene. | |||
| 22 | + | require.True(t, r.Admit(ports.CaptureRequest{ID: 2}, nil, scene, true)) | |||
| 23 | + | require.False(t, r.Admit(ports.CaptureRequest{ID: 3}, nil, scene, false), "no scene yet") | |||
| 24 | + | require.False(t, r.List[0].Since.IsZero()) | |||
| 25 | + | for i := r.Len(); i < MaxRequests; i++ { | |||
| 26 | + | r.Admit(ports.CaptureRequest{ID: uint64(10 + i)}, nil, scene, true) | |||
| 27 | + | } | |||
| 28 | + | require.False(t, r.Admit(ports.CaptureRequest{ID: 99}, nil, scene, true)) | |||
| 29 | + | done := <-replies | |||
| 30 | + | require.Equal(t, uint64(99), done.ID) | |||
| 31 | + | require.ErrorIs(t, done.Err, ErrBatchFull) | |||
| 32 | + | require.Equal(t, MaxRequests, r.Len()) | |||
| 33 | + | } | |||
| 34 | + | ||||
| 35 | + | func TestRequestsFailPendingSkipsHanded(t *testing.T) { | |||
| 36 | + | replies := make(chan ports.CaptureDone, 4) | |||
| 37 | + | var r Requests | |||
| 38 | + | r.Init(context.Background(), replies) | |||
| 39 | + | r.Admit(ports.CaptureRequest{ID: 1}, nil, ports.Scene{}, false) | |||
| 40 | + | r.Admit(ports.CaptureRequest{ID: 2}, nil, ports.Scene{}, false) | |||
| 41 | + | r.List[0] = ports.CaptureRequest{} // handed to the worker | |||
| 42 | + | r.FailPending(ErrEpochChanged) | |||
| 43 | + | require.Zero(t, r.Len()) | |||
| 44 | + | require.Len(t, replies, 1) | |||
| 45 | + | done := <-replies | |||
| 46 | + | require.Equal(t, uint64(2), done.ID) | |||
| 47 | + | require.ErrorIs(t, done.Err, ErrEpochChanged) | |||
| 48 | + | } | |||
| 49 | + | ||||
| 50 | + | func TestRequestsStopDrainsIncoming(t *testing.T) { | |||
| 51 | + | replies := make(chan ports.CaptureDone, 4) | |||
| 52 | + | var r Requests | |||
| 53 | + | r.Init(context.Background(), replies) | |||
| 54 | + | r.Admit(ports.CaptureRequest{ID: 1}, nil, ports.Scene{}, false) | |||
| 55 | + | incoming := make(chan ports.CaptureRequest, 2) | |||
| 56 | + | incoming <- ports.CaptureRequest{ID: 2} | |||
| 57 | + | close(incoming) | |||
| 58 | + | r.Stop(context.Background(), incoming) | |||
| 59 | + | require.Len(t, replies, 2) | |||
| 60 | + | for range 2 { | |||
| 61 | + | require.ErrorIs(t, (<-replies).Err, ErrOutputStopped) | |||
| 62 | + | } | |||
| 63 | + | } | |||
M internal/adapters/outputkit/surfaces/surfaces.go +18 -0
| @@ -98,3 +98,21 @@ func (t *Table) Kept(id ports.WindowID) bool { | |||
| 98 | 98 | _, ok := t.kept[id] | |
| 99 | 99 | return ok | |
| 100 | 100 | } | |
| 101 | + | ||
| 102 | + | // Shown is the presentation rule both output backends report with: id was | |
| 103 | + | // shown to its client when scene draws it and its content is not kept. A | |
| 104 | + | // nil kept counts nothing as kept. | |
| 105 | + | func Shown(scene ports.Scene, id ports.WindowID, kept func(ports.WindowID) bool) bool { | |
| 106 | + | return scene.Shows(id) && (kept == nil || !kept(id)) | |
| 107 | + | } | |
| 108 | + | ||
| 109 | + | // FillShown clears dst and fills it with the windows of seen that scene | |
| 110 | + | // showed to their client (Shown), at their seen Seq. | |
| 111 | + | func FillShown(dst, seen map[ports.WindowID]uint64, scene ports.Scene, kept func(ports.WindowID) bool) { | |
| 112 | + | clear(dst) | |
| 113 | + | for id, seq := range seen { | |
| 114 | + | if Shown(scene, id, kept) { | |
| 115 | + | dst[id] = seq | |
| 116 | + | } | |
| 117 | + | } | |
| 118 | + | } | |
| @@ -98,3 +98,21 @@ func (t *Table) Kept(id ports.WindowID) bool { | |||||
| 98 | _, ok := t.kept[id] | 98 | _, ok := t.kept[id] | ||
| 99 | return ok | 99 | return ok | ||
| 100 | } | 100 | } | ||
| 101 | + | ||||
| 102 | + | // Shown is the presentation rule both output backends report with: id was | |||
| 103 | + | // shown to its client when scene draws it and its content is not kept. A | |||
| 104 | + | // nil kept counts nothing as kept. | |||
| 105 | + | func Shown(scene ports.Scene, id ports.WindowID, kept func(ports.WindowID) bool) bool { | |||
| 106 | + | return scene.Shows(id) && (kept == nil || !kept(id)) | |||
| 107 | + | } | |||
| 108 | + | ||||
| 109 | + | // FillShown clears dst and fills it with the windows of seen that scene | |||
| 110 | + | // showed to their client (Shown), at their seen Seq. | |||
| 111 | + | func FillShown(dst, seen map[ports.WindowID]uint64, scene ports.Scene, kept func(ports.WindowID) bool) { | |||
| 112 | + | clear(dst) | |||
| 113 | + | for id, seq := range seen { | |||
| 114 | + | if Shown(scene, id, kept) { | |||
| 115 | + | dst[id] = seq | |||
| 116 | + | } | |||
| 117 | + | } | |||
| 118 | + | } | |||
M internal/adapters/outputkit/surfaces/surfaces_test.go +14 -0
| @@ -122,3 +122,17 @@ func TestKeptChildrenFencesDroppedOnACopy(t *testing.T) { | |||
| 122 | 122 | t.Fatal("wayland's slice was modified") | |
| 123 | 123 | } | |
| 124 | 124 | } | |
| 125 | + | ||
| 126 | + | // Both backends report a window shown only when the scene draws it and its | |
| 127 | + | // content is not kept. | |
| 128 | + | func TestFillShown(t *testing.T) { | |
| 129 | + | kept := func(id ports.WindowID) bool { return id == 2 } | |
| 130 | + | dst := map[ports.WindowID]uint64{9: 9} | |
| 131 | + | FillShown(dst, map[ports.WindowID]uint64{1: 3, 2: 4, 3: 5}, shows(1, 2), kept) | |
| 132 | + | if len(dst) != 1 || dst[1] != 3 { | |
| 133 | + | t.Fatalf("shown %v", dst) | |
| 134 | + | } | |
| 135 | + | if !Shown(shows(2), 2, nil) { | |
| 136 | + | t.Fatal("nil kept counts nothing as kept") | |
| 137 | + | } | |
| 138 | + | } | |
| @@ -122,3 +122,17 @@ func TestKeptChildrenFencesDroppedOnACopy(t *testing.T) { | |||||
| 122 | t.Fatal("wayland's slice was modified") | 122 | t.Fatal("wayland's slice was modified") | ||
| 123 | } | 123 | } | ||
| 124 | } | 124 | } | ||
| 125 | + | ||||
| 126 | + | // Both backends report a window shown only when the scene draws it and its | |||
| 127 | + | // content is not kept. | |||
| 128 | + | func TestFillShown(t *testing.T) { | |||
| 129 | + | kept := func(id ports.WindowID) bool { return id == 2 } | |||
| 130 | + | dst := map[ports.WindowID]uint64{9: 9} | |||
| 131 | + | FillShown(dst, map[ports.WindowID]uint64{1: 3, 2: 4, 3: 5}, shows(1, 2), kept) | |||
| 132 | + | if len(dst) != 1 || dst[1] != 3 { | |||
| 133 | + | t.Fatalf("shown %v", dst) | |||
| 134 | + | } | |||
| 135 | + | if !Shown(shows(2), 2, nil) { | |||
| 136 | + | t.Fatal("nil kept counts nothing as kept") | |||
| 137 | + | } | |||
| 138 | + | } | |||