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 firstRetry, secondRetry := h.step(), h.step()
52 if firstRetry || secondRetry || h.draws != 1 || len(h.submits) != 3 || h.submits[2] != 1 {
53 t.Fatalf("retry redrew: draws=%d submits=%v", h.draws, h.submits)
54 }
55 h.blocked = false
56 if !h.step() || h.draws != 1 || h.s.NeedsRetry(false, true) {
57 t.Fatalf("release did not commit cleanly: draws=%d submits=%v", h.draws, h.submits)
58 }
59 if h.step() || len(h.submits) != 4 {
60 t.Fatalf("clean schedule submitted again: %v", h.submits)
61 }
62}
63
64func TestScheduleInvalidateReplacesChangedFrame(t *testing.T) {
65 h := newHarness(t)
66 h.blocked = true
67 h.step()
68 h.s.Invalidate()
69 h.blocked = false
70 if !h.step() || h.draws != 2 || h.submits[len(h.submits)-1] != 2 {
71 t.Fatalf("stale frame submitted: draws=%d submits=%v", h.draws, h.submits)
72 }
73}
74
75func TestScheduleNoOpInputKeepsUnsubmittedFrame(t *testing.T) {
76 h := newHarness(t)
77 h.blocked = true
78 h.step()
79 h.s.Invalidate()
80 h.changed = false
81 if h.step() || h.draws != 2 || h.submits[len(h.submits)-1] != 1 {
82 t.Fatalf("no-op lost held frame: draws=%d submits=%v", h.draws, h.submits)
83 }
84 h.blocked = false
85 if !h.step() || h.draws != 2 || h.submits[len(h.submits)-1] != 1 {
86 t.Fatalf("release lost held frame: draws=%d submits=%v", h.draws, h.submits)
87 }
88 if h.s.NeedsRetry(false, true) {
89 t.Fatal("committed schedule did not go idle")
90 }
91}
92
93func TestScheduleDrawUnchangedGoesIdle(t *testing.T) {
94 h := newHarness(t)
95 h.changed = false
96 if h.step() || h.draws != 1 || len(h.submits) != 0 {
97 t.Fatalf("unchanged draw: draws=%d submits=%v", h.draws, h.submits)
98 }
99 if h.s.NeedsRetry(false, true) {
100 t.Fatal("idle schedule asks for retry (busy loop)")
101 }
102 for i := 0; i < 3; i++ {
103 h.step()
104 }
105 if h.draws != 1 {
106 t.Fatalf("idle loop redrew %d times", h.draws)
107 }
108 h.s.Invalidate()
109 h.changed = true
110 if !h.step() || h.draws != 2 {
111 t.Fatalf("wake after idle: draws=%d", h.draws)
112 }
113}
114
115func TestScheduleWaitsForFrameCallback(t *testing.T) {
116 h := newHarness(t)
117 h.ready = false
118 if h.step() || h.draws != 0 || len(h.submits) != 0 || h.s.NeedsRetry(false, false) {
119 t.Fatalf("drew before frame callback: draws=%d", h.draws)
120 }
121 h.ready = true
122 if !h.step() || h.draws != 1 {
123 t.Fatalf("frame callback did not release draw: draws=%d", h.draws)
124 }
125 // A prepared frame waiting for a callback is woken by the event, not polled.
126 h.s.Invalidate()
127 h.blocked = true
128 h.step()
129 if h.s.NeedsRetry(false, false) || !h.s.NeedsRetry(false, true) {
130 t.Fatal("retry must follow frame readiness")
131 }
132}
133
134func TestSchedulePendingGPUForcesRetry(t *testing.T) {
135 h := newHarness(t)
136 h.changed = false
137 h.step()
138 if !h.s.NeedsRetry(true, false) {
139 t.Fatal("pending GPU work must schedule a bounded retry")
140 }
141}
142
143func TestScheduleErrorsPropagate(t *testing.T) {
144 h := newHarness(t)
145 h.drawErr = errors.New("draw")
146 if _, err := h.s.Step(true, func() (render.Frame, bool, error) { return render.Frame{}, false, h.drawErr }, nil); !errors.Is(err, h.drawErr) {
147 t.Fatalf("draw err = %v", err)
148 }
149 h.drawErr = nil
150 h.submitErr = errors.New("submit")
151 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) {
152 t.Fatalf("submit error: ok=%v err=%v", ok, err)
153 }
154}