1#!/usr/bin/python3
2
3import ctypes
4import fcntl
5import os
6import pykms
7import selectors
8import sys
9import time
10
11bar_width = 20
12bar_speed = 8
13
14class Timer(object):
15    timers = []
16
17    def __init__(self, timeout, callback, data):
18        self.timeout = time.clock_gettime(time.CLOCK_MONOTONIC) + timeout
19        self.callback = callback
20        self.data = data
21
22        print("adding timer %f" % self.timeout)
23        self.timers.append(self)
24        self.timers.sort(key=lambda timer: timer.timeout)
25
26    @classmethod
27    def fire(_class):
28        clk = time.clock_gettime(time.CLOCK_MONOTONIC)
29        while len(_class.timers) > 0:
30            timer = _class.timers[0]
31            if timer.timeout > clk:
32                break
33
34            del _class.timers[0]
35            print("fireing timer %f" % timer.timeout)
36            timer.callback(timer.data)
37
38    @classmethod
39    def next_timeout(_class):
40        clk = time.clock_gettime(time.CLOCK_MONOTONIC)
41        if len(_class.timers) == 0 or _class.timers[0].timeout < clk:
42            return None
43
44        return _class.timers[0].timeout - clk
45
46
47class Timeline(object):
48
49    class sw_sync_create_fence_data(ctypes.Structure):
50        _fields_ = [
51            ('value', ctypes.c_uint32),
52            ('name', ctypes.c_char * 32),
53            ('fence', ctypes.c_int32),
54        ]
55
56    SW_SYNC_IOC_CREATE_FENCE = (3 << 30) | (ctypes.sizeof(sw_sync_create_fence_data) << 16) | (ord('W') << 8) | (0 << 0)
57    SW_SYNC_IOC_INC = (1 << 30) | (ctypes.sizeof(ctypes.c_uint32) << 16) | (ord('W') << 8) | (1 << 0)
58
59    class SWSync(object):
60        def __init__(self, fd):
61            self.fd = fd
62        def __del__(self):
63            os.close(self.fd)
64
65    def __init__(self):
66        self.value = 0
67        try:
68            self.fd = os.open('/sys/kernel/debug/sync/sw_sync', 0);
69        except:
70            raise RuntimeError('Failed to open sw_sync file')
71
72    def close(self):
73        os.close(self.fd)
74
75    def create_fence(self, value):
76        data = self.sw_sync_create_fence_data(value = value);
77        print("ioctl number %u" % self.SW_SYNC_IOC_CREATE_FENCE)
78        ret = fcntl.ioctl(self.fd, self.SW_SYNC_IOC_CREATE_FENCE, data);
79        if ret < 0:
80            raise RuntimeError('Failed to create fence')
81
82        return self.SWSync(data.fence)
83
84    def signal(self, value):
85        fcntl.ioctl(self.fd, self.SW_SYNC_IOC_INC, ctypes.c_uint32(value))
86        self.value += value
87
88
89class FlipHandler():
90    def __init__(self, crtc, width, height):
91        super().__init__()
92        self.crtc = crtc
93        self.timeline = Timeline()
94        self.bar_xpos = 0
95        self.front_buf = 0
96        self.fb1 = pykms.DumbFramebuffer(crtc.card, width, height, "XR24");
97        self.fb2 = pykms.DumbFramebuffer(crtc.card, width, height, "XR24");
98        self.flips = 0
99        self.flips_last = 0
100        self.frame_last = 0
101        self.time_last = 0
102
103    def handle_page_flip(self, frame, time):
104        if self.time_last == 0:
105            self.frame_last = frame
106            self.time_last = time
107
108        # Verify that the page flip hasn't completed before the timeline got
109        # signaled.
110        if self.timeline.value < 2 * self.flips - 1:
111            raise RuntimeError('Page flip %u for fence %u complete before timeline (%u)!' %
112                               (self.flips, 2 * self.flips - 1, self.timeline.value))
113
114        self.flips += 1
115
116        # Print statistics every 5 seconds.
117        time_delta = time - self.time_last
118        if time_delta >= 5:
119            frame_delta = frame - self.frame_last
120            flips_delta = self.flips - self.flips_last
121            print("Frame rate: %f (%u/%u frames in %f s)" %
122                  (frame_delta / time_delta, flips_delta, frame_delta, time_delta))
123
124            self.frame_last = frame
125            self.flips_last = self.flips
126            self.time_last = time
127
128        # Draw the color bar on the back buffer.
129        if self.front_buf == 0:
130            fb = self.fb2
131        else:
132            fb = self.fb1
133
134        self.front_buf = self.front_buf ^ 1
135
136        current_xpos = self.bar_xpos;
137        old_xpos = (current_xpos + (fb.width - bar_width - bar_speed)) % (fb.width - bar_width);
138        new_xpos = (current_xpos + bar_speed) % (fb.width - bar_width);
139
140        self.bar_xpos = new_xpos
141
142        pykms.draw_color_bar(fb, old_xpos, new_xpos, bar_width)
143
144        # Flip the buffers with an in fence located in the future. The atomic
145        # commit is asynchronous and returns immediately, but the flip should
146        # not complete before the fence gets signaled.
147        print("flipping with fence @%u, timeline is @%u" % (2 * self.flips - 1, self.timeline.value))
148        fence = self.timeline.create_fence(2 * self.flips - 1)
149        req = pykms.AtomicReq(self.crtc.card)
150        req.add(self.crtc.primary_plane, { 'FB_ID': fb.id, 'IN_FENCE_FD': fence.fd })
151        req.commit()
152        del fence
153
154        # Arm a timer to signal the fence in 0.5s.
155        def timeline_signal(timeline):
156            print("signaling timeline @%u" % timeline.value)
157            timeline.signal(2)
158
159        Timer(0.5, timeline_signal, self.timeline)
160
161
162def main(argv):
163    if len(argv) > 1:
164        conn_name = argv[1]
165    else:
166        conn_name = ''
167
168    card = pykms.Card()
169    if not card.has_atomic:
170        raise RuntimeError('This test requires atomic update support')
171
172    res = pykms.ResourceManager(card)
173    conn = res.reserve_connector(conn_name)
174    crtc = res.reserve_crtc(conn)
175    mode = conn.get_default_mode()
176
177    flip_handler = FlipHandler(crtc, mode.hdisplay, mode.vdisplay)
178
179    fb = flip_handler.fb1
180    pykms.draw_color_bar(fb, fb.width - bar_width - bar_speed, bar_speed, bar_width)
181    mode_blob = mode.blob(card)
182
183    req = pykms.AtomicReq(card)
184    req.add(conn, 'CRTC_ID', crtc.id)
185    req.add(crtc, { 'ACTIVE': 1, 'MODE_ID': mode_blob.id })
186    req.add(crtc.primary_plane, {
187                'FB_ID': fb.id,
188                'CRTC_ID': crtc.id,
189                'SRC_X': 0 << 16,
190                'SRC_Y': 0 << 16,
191                'SRC_W': fb.width << 16,
192                'SRC_H': fb.height << 16,
193                'CRTC_X': 0,
194                'CRTC_Y': 0,
195                'CRTC_W': fb.width,
196                'CRTC_H': fb.height,
197    })
198    ret = req.commit(flip_handler, allow_modeset = True)
199    if ret < 0:
200        raise RuntimeError('Atomic mode set failed with %d' % ret)
201
202    def bye():
203        # Signal the timeline to complete all pending page flips
204        flip_handler.timeline.signal(100)
205        exit(0)
206
207    def readdrm(fileobj, mask):
208        for ev in card.read_events():
209            if ev.type == pykms.DrmEventType.FLIP_COMPLETE:
210                flip_handler.handle_page_flip(ev.seq, ev.time)
211
212    def readkey(fileobj, mask):
213        sys.stdin.readline()
214        bye()
215
216    sel = selectors.DefaultSelector()
217    sel.register(card.fd, selectors.EVENT_READ, readdrm)
218    sel.register(sys.stdin, selectors.EVENT_READ, readkey)
219
220    while True:
221        timeout = Timer.next_timeout()
222        print("--> timeout %s" % repr(timeout))
223        try:
224            events = sel.select(timeout)
225        except KeyboardInterrupt:
226            bye()
227        for key, mask in events:
228            callback = key.data
229            callback(key.fileobj, mask)
230
231        Timer.fire()
232
233if __name__ == '__main__':
234    main(sys.argv)
235