1//go:build linux
2
3package session
4
5import (
6 "errors"
7 "testing"
8
9 "github.com/bnema/nefergui/internal/render"
10)
11
12// scheduleHarness plays the app and session sides of frameSchedule.
13type scheduleHarness struct {
14 t *testing.T
15 s *frameSchedule
16 draws int
17 submits []int // frame id (Quads length) of each submit attempt
18 changed bool
19 blocked bool
20 ready bool
21 drawErr error
22 submitErr error
23}
24
25func newHarness(t *testing.T) *scheduleHarness {
26 return &scheduleHarness{t: t, s: newFrameSchedule(), changed: true, ready: true}
27}
28
29func (h *scheduleHarness) step() bool {
30 h.t.Helper()
31 ok, err := h.s.Step(h.ready, func() (render.Frame, bool, error) {
32 h.draws++
33 return render.Frame{Quads: make([]render.Quad, h.draws)}, h.changed, h.drawErr
34 }, func(f render.Frame) (bool, error) {
35 h.submits = append(h.submits, len(f.Quads))
36 return !h.blocked, h.submitErr
37 })
38 if err != nil && h.drawErr == nil && h.submitErr == nil {
39 h.t.Fatal(err)
40 }
41 return ok
42}
43
44func TestScheduleBlockedFrameKeepsPreparedAndRetries(t *testing.T) {
45 h := newHarness(t)
46 h.blocked = true
47 if h.step() || h.draws != 1 || !h.s.NeedsRetry(false, true) {
48 t.Fatalf("blocked: draws=%d retry=%v", h.draws, h.s.NeedsRetry(false, true))
49 }
50 // Retries resubmit the same prepared frame without redrawing.
51 if h.step() || h.step() || h.draws != 1 || len(h.submits) != 3 || h.submits[2] != 1 {
52 t.Fatalf("retry redrew: draws=%d submits=%v", h.draws, h.submits)
53 }
54 h.blocked = false
55 if !h.step() || h.draws != 1 || h.s.NeedsRetry(false, true) {
56 t.Fatalf("release did not commit cleanly: draws=%d submits=%v", h.draws, h.submits)
57 }
58 if h.step() || len(h.submits) != 4 {
59 t.Fatalf("clean schedule submitted again: %v", h.submits)
60 }
61}
62
63func TestScheduleInvalidateReplacesChangedFrame(t *testing.T) {
64 h := newHarness(t)
65 h.blocked = true
66 h.step()
67 h.s.Invalidate()
68 h.blocked = false
69 if !h.step() || h.draws != 2 || h.submits[len(h.submits)-1] != 2 {
70 t.Fatalf("stale frame submitted: draws=%d submits=%v", h.draws, h.submits)
71 }
72}
73
74func TestScheduleNoOpInputKeepsUnsubmittedFrame(t *testing.T) {
75 h := newHarness(t)
76 h.blocked = true
77 h.step()
78 h.s.Invalidate()
79 h.changed = false
80 if h.step() || h.draws != 2 || h.submits[len(h.submits)-1] != 1 {
81 t.Fatalf("no-op lost held frame: draws=%d submits=%v", h.draws, h.submits)
82 }
83 h.blocked = false
84 if !h.step() || h.draws != 2 || h.submits[len(h.submits)-1] != 1 {
85 t.Fatalf("release lost held frame: draws=%d submits=%v", h.draws, h.submits)
86 }
87 if h.s.NeedsRetry(false, true) {
88 t.Fatal("committed schedule did not go idle")
89 }
90}
91
92func TestScheduleDrawUnchangedGoesIdle(t *testing.T) {
93 h := newHarness(t)
94 h.changed = false
95 if h.step() || h.draws != 1 || len(h.submits) != 0 {
96 t.Fatalf("unchanged draw: draws=%d submits=%v", h.draws, h.submits)
97 }
98 if h.s.NeedsRetry(false, true) {
99 t.Fatal("idle schedule asks for retry (busy loop)")
100 }
101 for i := 0; i < 3; i++ {
102 h.step()
103 }
104 if h.draws != 1 {
105 t.Fatalf("idle loop redrew %d times", h.draws)
106 }
107 h.s.Invalidate()
108 h.changed = true
109 if !h.step() || h.draws != 2 {
110 t.Fatalf("wake after idle: draws=%d", h.draws)
111 }
112}
113
114func TestScheduleWaitsForFrameCallback(t *testing.T) {
115 h := newHarness(t)
116 h.ready = false
117 if h.step() || h.draws != 0 || len(h.submits) != 0 || h.s.NeedsRetry(false, false) {
118 t.Fatalf("drew before frame callback: draws=%d", h.draws)
119 }
120 h.ready = true
121 if !h.step() || h.draws != 1 {
122 t.Fatalf("frame callback did not release draw: draws=%d", h.draws)
123 }
124 // A prepared frame waiting for a callback is woken by the event, not polled.
125 h.s.Invalidate()
126 h.blocked = true
127 h.step()
128 if h.s.NeedsRetry(false, false) || !h.s.NeedsRetry(false, true) {
129 t.Fatal("retry must follow frame readiness")
130 }
131}
132
133func TestSchedulePendingGPUForcesRetry(t *testing.T) {
134 h := newHarness(t)
135 h.changed = false
136 h.step()
137 if !h.s.NeedsRetry(true, false) {
138 t.Fatal("pending GPU work must schedule a bounded retry")
139 }
140}
141
142func TestScheduleErrorsPropagate(t *testing.T) {
143 h := newHarness(t)
144 h.drawErr = errors.New("draw")
145 if _, err := h.s.Step(true, func() (render.Frame, bool, error) { return render.Frame{}, false, h.drawErr }, nil); !errors.Is(err, h.drawErr) {
146 t.Fatalf("draw err = %v", err)
147 }
148 h.drawErr = nil
149 h.submitErr = errors.New("submit")
150 if ok, err := h.s.Step(true, func() (render.Frame, bool, error) { return render.Frame{}, true, nil }, func(render.Frame) (bool, error) { return true, h.submitErr }); ok || !errors.Is(err, h.submitErr) {
151 t.Fatalf("submit error: ok=%v err=%v", ok, err)
152 }
153}