xpra icon
Bug tracker and wiki

This bug tracker and wiki are being discontinued
please use https://github.com/Xpra-org/xpra instead.


Ticket #1426: scroll-cythonized-v4.patch

File scroll-cythonized-v4.patch, 34.6 KB (added by Antoine Martin, 4 years ago)

avoid back and forth: stay in cython

  • setup.py

     
    21742174                ["xpra/server/window/region.pyx"],
    21752175                **O3_pkgconfig))
    21762176    cython_add(Extension("xpra.server.window.motion",
    2177                 ["xpra/server/window/motion.pyx"],
     2177                ["xpra/server/window/motion.pyx", "xpra/server/window/xxhash.c"],
    21782178                **O3_pkgconfig))
    21792179
    21802180
  • unittests/unit/server/motion_test.py

     
    1111from xpra.util import envbool
    1212try:
    1313        from xpra.server.window import motion
     14        log = motion.log                #@UndefinedVariable
    1415except ImportError:
    1516        motion = None
    1617
     
    2021
    2122class TestMotion(unittest.TestCase):
    2223
    23         def calculate_distances(self, array1, array2, min_hits=2, max_distance=1):
    24                 sd = motion.scroll_distances(array1, array2, max_distance)
    25                 return sd.get_best_scroll_values(min_hits)
     24        def calculate_distances(self, array1, array2, min_hits=2, max_distance=1000):
     25                assert len(array1)==len(array2)
     26                rect = (0, 0, 1, len(array1))
     27                return self.do_calculate_distances(rect, array1, array2, min_hits, max_distance)
    2628
     29        def do_calculate_distances(self, rect, array1, array2, min_hits=2, max_distance=1000):
     30                sd = motion.ScrollData(*rect)
     31                sd._test_update(array1)
     32                sd._test_update(array2)
     33                sd.calculate(max_distance)
     34                return sd.get_scroll_values(min_hits)
     35
     36        def test_simple(self):
     37                self.calculate_distances([0], [1], 0, 100)
     38
    2739        def test_match_distance(self):
    2840                def t(a1, a2, distance, matches):
    29                         sd = motion.scroll_distances(a1, a2)
    30                         line_defs = sd.match_distance(distance)
     41                        scrolls = self.calculate_distances(a1, a2, 0)[0]
     42                        line_defs = scrolls.get(distance)
     43                        assert line_defs, "distance %i not found in scroll data: %s for a1=%s, a2=%s" % (distance, scrolls, a1, a2)
    3144                        linecount = sum(line_defs.values())
    3245                        assert linecount==matches, "expected %i matches for distance=%i but got %i for a1=%s, a2=%s, result=%s" % (matches, distance, linecount, a1, a2, line_defs)
    3346                for N in (1, 10, 100):
     
    3649
    3750                        a = [1]*N
    3851                        t(a, a, 0, N)
    39                         for M in range(N):
    40                                 t(a, a, M, N-M)
    4152
    4253                #from a1 to a2: shift by 2, get 2 hits
    4354                t([3, 4, 5, 6], [1, 2, 3, 4], 2, 2)
     
    5465        def test_calculate_distances(self):
    5566                array1 = [crc32(str(x)) for x in (1234, "abc", 99999)]
    5667                array2 = array1[:]
    57                 d = self.calculate_distances(array1, array2, 1)
     68                d = self.calculate_distances(array1, array2, 1)[0]
    5869                assert len(d)==1 and sum(d[0].values())==len(array1), "expected %i matches but got %s" % (len(array1), d[0])
    5970
    6071                array1 = range(1, 5)
    6172                array2 = range(2, 6)
    62                 d = self.calculate_distances(array1, array2)
     73                d = self.calculate_distances(array1, array2)[0]
    6374                assert len(d)==1, "expected 1 match but got: %s" % len(d)
    6475                common = set(array1).intersection(set(array2))
    6576                assert -1 in d, "expected distance of -1 but got: %s" % d.keys()
     
    8192                start = time.time()
    8293                array1 = range(N)
    8394                array2 = [N*2-x*2 for x in range(N)]
    84                 d = self.calculate_distances(array1, array2, 1)
     95                d = self.calculate_distances(array1, array2, 1)[0]
    8596                end = time.time()
    8697                if SHOW_PERF:
    87                         print("calculate_distances %4i^2 in %5.1f ms" % (N, (end-start)*1000))
     98                        log.info("calculate_distances %4i^2 in %5.1f ms" % (N, (end-start)*1000))
    8899
    89100        def test_detect_motion(self):
    90                 W, H, BPP = 1920, 1080, 4
     101                self.do_test_detect_motion(5, 5)
     102                self.do_test_detect_motion(1920, 1080)
     103
     104        def do_test_detect_motion(self, W, H):
     105                BPP = 4
    91106                #W, H, BPP = 2, 4, 4
    92107                LEN = W * H * BPP
    93108                import numpy as np
    94109                try:
    95                         na1 = np.random.randint(2**63-1, size=LEN//8, dtype="int64")
     110                        na1 = np.random.randint(255, size=LEN, dtype="uint8")
    96111                except TypeError as e:
    97112                        #older numpy version may not have dtype argument..
    98113                        #and may not accept 64-bit values
     
    110125                                #older versions of numpy (ie: centos7)
    111126                                return a.tostring()
    112127                buf1 = tobytes(na1)
    113                 ov1 = motion.CRC_Image(buf1, W, H, W*BPP, BPP)
    114                 assert len(ov1)==H
     128                #push first image:
     129                sd = motion.ScrollData(0, 0, W, H)
    115130                #make a new "image" shifted N lines:
    116                 for N in (1, 20, 100):
    117                         na2 = np.roll(na1, -N*W*BPP//8)
     131                for N in (1, 2, 20, 100):
     132                        if N>H//2:
     133                                break
     134                        sd.update(buf1, 0, 0, W, H, W*BPP, BPP)
     135                        log("picture of height %i scrolled by %i", H, N)
     136                        na2 = np.roll(na1, -N*W*BPP)
    118137                        buf2 = tobytes(na2)
    119138                        start = time.time()
    120                         ov2 = motion.CRC_Image(buf2, W, H, W*BPP, BPP)
     139                        sd.update(buf2, 0, 0, W, H, W*BPP, BPP)
    121140                        end = time.time()
    122141                        if SHOW_PERF:
    123                                 print("\nCRC_Image %ix%i (%.1fMB) in %4.2f ms" % (W, H, len(buf2)//1024//1024, 1000.0*(end-start)))
    124                         assert len(ov2)==H
     142                                log.info("hashed image %ix%i (%.1fMB) in %4.2f ms" % (W, H, len(buf2)//1024//1024, 1000.0*(end-start)))
    125143                        start = time.time()
    126                         distances = self.calculate_distances(ov1, ov2, min_hits=1)
     144                        sd.calculate()
     145                        sd_data = sd.get_scroll_values(1)
     146                        scrolls, non_scrolls = sd_data
     147                        log("scroll values=%s", dict(scrolls))
     148                        log("non scroll values=%s", dict(non_scrolls))
    127149                        end = time.time()
    128150                        if SHOW_PERF:
    129                                 print("calculate_distances %4i^2 in %5.2f ms" % (H, 1000.0*(end-start)))
    130                         line_defs = distances.get(-N, {})
     151                                log.info("calculated distances %4i^2 in %5.2f ms" % (H, 1000.0*(end-start)))
     152                        line_defs = scrolls.get(-N, {})
    131153                        linecount = sum(line_defs.values())
    132                         assert linecount>0, "could not find distance %i" % N
     154                        assert linecount>0, "could not find distance %i in %s" % (N, line_defs)
    133155                        assert linecount == (H-N), "expected to match %i lines but got %i" % (H-N, linecount)
    134156                if False:
    135157                        import binascii
    136                         print("na1:\n%s" % binascii.hexlify(tobytes(na1)))
    137                         print("na2:\n%s" % binascii.hexlify(tobytes(na2)))
     158                        log("na1:\n%s" % binascii.hexlify(tobytes(na1)))
     159                        log("na2:\n%s" % binascii.hexlify(tobytes(na2)))
    138160                        np.set_printoptions(threshold=np.inf)
    139                         print("na1:\n%s" % (na1, ))
    140                         print("na2:\n%s" % (na2, ))
    141                         print("ov1:\n%s" % (ov1, ))
    142                         print("ov2:\n%s" % (ov2, ))
     161                        log("na1:\n%s" % (na1, ))
     162                        log("na2:\n%s" % (na2, ))
    143163
    144164        def test_csum_data(self):
    145165                a1=[
     
    181201                        17157005122993541799, 5218869126146608853, 13274228147453099388, 16342723934713827717, 2435034235422505275, 3689766606612767057, 13721141386368216492, 14859793948180065358,
    182202                        ]
    183203                #distances = motion.scroll_distances(a1[100:400], a2[100:400], 2, 1000)
    184                 distances = motion.scroll_distances(a1, a2, 2, 1000)
    185                 scroll, count = distances.get_best_match()
     204                sd = motion.ScrollData(0, 0, 1050, len(a1))
     205                sd._test_update(a1)
     206                sd._test_update(a2)
     207                sd.calculate(1000)
     208                scroll, count = sd.get_best_match()
    186209                wh = len(a1)
    187                 print("best match: %s" % ((scroll, count),))
     210                log("best match: %s" % ((scroll, count),))
    188211                x, y, w, h = 0, 0, 1050, 1151
    189                 raw_scroll = distances.get_best_scroll_values()
    190                 #non_scroll = distances.get_remaining_areas()
     212                raw_scroll, non_scroll = sd.get_scroll_values()
     213                assert len(non_scroll)>0
    191214                scrolls = []
    192215                def h(v):
    193216                        return hex(v).lstrip("0x").rstrip("L")
    194217                for i in range(wh):
    195                         print("%2i:     %16s    %16s" % (i, h(a1[i]), h(a2[i])))
     218                        log("%2i:       %16s    %16s" % (i, h(a1[i]), h(a2[i])))
    196219                for scroll, line_defs in raw_scroll.items():
    197220                        if scroll==0:
    198221                                continue
  • xpra/server/window/motion.pyx

     
    99
    1010import os
    1111import time
     12import struct
    1213import collections
    1314
    14 from xpra.util import envbool
     15from xpra.util import envbool, repr_ellipsized, csv
    1516from xpra.log import Logger
    1617log = Logger("encoding", "scroll")
    1718
    1819from xpra.buffers.membuf cimport memalign, object_as_buffer
     20from xpra.server.window.region import rectangle
    1921
     22
    2023cdef int DEBUG = envbool("XPRA_SCROLL_DEBUG", False)
    21 hashfn = None
    22 if envbool("XPRA_XXHASH", True):
    23     try:
    24         import xxhash
    25         def hashfn(x):
    26             return xxhash.xxh64(x).intdigest()
    27     except ImportError as e:
    28         log.warn("Warning: xxhash python bindings not found")
    29 else:
    30     log.warn("Warning: xxhash disabled")
    31 if hashfn is None:
    32     log.warn(" no scrolling detection")
    3324
    3425
    35 from libc.stdint cimport int32_t, uint8_t, uint16_t, int16_t, uint32_t, int64_t
     26from libc.stdint cimport uint8_t, uint16_t, int16_t, uint64_t, uintptr_t
    3627
    3728cdef extern from "string.h":
    3829    void free(void * ptr) nogil
    3930    void *memset(void * ptr, int value, size_t num) nogil
     31    void *memcpy(void * destination, void * source, size_t num) nogil
    4032
     33cdef extern from "xxhash.h":
     34    ctypedef unsigned long long XXH64_hash_t
     35    XXH64_hash_t XXH64(const void* input, size_t length, unsigned long long seed)
    4136
    42 def CRC_Image(pixels, unsigned int width, unsigned int height, unsigned int rowstride, unsigned char bpp=4):
    43     global hashfn
    44     if not hashfn:
    45         return None
    46     cdef uint8_t *buf = NULL
    47     cdef Py_ssize_t buf_len = 0
    48     assert object_as_buffer(pixels, <const void**> &buf, &buf_len)==0
    49     assert buf_len>=0 and (<unsigned int> buf_len)>=rowstride*height, "buffer is too small for %ix%i" % (rowstride, height)
    50     cdef unsigned int i
    51     cdef size_t row_len = width*bpp
    52     f = hashfn
    53     crcs = []
    54     for i in range(height):
    55         crcs.append(f(buf[:row_len]))
    56         buf += rowstride
    57     return crcs
    5837
    59 
    6038DEF MAXINT64 = 2**63
    6139DEF MAXUINT64 = 2**64
    6240DEF MASK64 = 2**64-1
    63 cdef inline castint64(v):
    64     if v>=MAXINT64:
    65         return v-MAXUINT64
    66     #assert v>=0, "invalid int to cast: %s" % v
    67     return v
    6841
    69 cdef inline da(int64_t *a, uint16_t l):
    70     return [a[i] for i in range(l)]
     42def h(v):
     43    return hex(v).lstrip("0x").rstrip("L")
    7144
    72 cdef inline dd(uint16_t *d, uint16_t l):
    73     return [d[i] for i in range(l)]
     45cdef inline uint64_t hashtoint64(s):
     46    return <uint64_t> struct.unpack("@L", s)[0]
    7447
    75 cdef inline ds(int16_t *d, uint16_t l):
    76     return [d[i] for i in range(l)]
     48cdef da(uint64_t *a, uint16_t l):
     49    return repr_ellipsized(csv(h(a[i]) for i in range(l)))
    7750
     51cdef dd(uint16_t *d, uint16_t l):
     52    return repr_ellipsized(csv(h(d[i]) for i in range(l)))
    7853
    79 assert sizeof(int64_t)==64//8, "uint64_t is not 64-bit: %i!" % sizeof(int64_t)
    8054
     55assert sizeof(uint64_t)==64//8, "uint64_t is not 64-bit: %i!" % sizeof(uint64_t)
    8156
    82 cdef class ScrollDistances:
    8357
     58cdef class ScrollData:
     59
    8460    cdef object __weakref__
    8561    #for each distance, keep track of the hit count:
    86     cdef uint16_t* distances
    87     cdef uint16_t l
    88     cdef int64_t *a1
    89     cdef int64_t *a2
     62    cdef uint16_t *distances
     63    cdef uint64_t *a1        #checksums of reference picture
     64    cdef uint64_t *a2        #checksums of latest picture
     65    cdef uint8_t matched
     66    cdef int16_t x
     67    cdef int16_t y
     68    cdef int16_t width
     69    cdef int16_t height
    9070
    91     def init(self, array1, array2, uint16_t max_distance=1000):
    92         assert len(array1)==len(array2)
    93         assert len(array1)<2**15 and len(array1)>0, "invalid array length: %i" % len(array1)
    94         self.l = len(array1)
    95         self.distances = <uint16_t*> memalign(2*self.l*sizeof(uint16_t))
    96         cdef size_t asize = self.l*(sizeof(int64_t))
    97         self.a1 = <int64_t*> memalign(asize)
    98         self.a2 = <int64_t*> memalign(asize)
    99         assert self.distances!=NULL and self.a1!=NULL and self.a2!=NULL, "scroll memory allocation failed"
    100         for i in range(self.l):
    101             self.a1[i] = castint64(array1[i])
    102             self.a2[i] = castint64(array2[i])
    103         #now compare all the values
    104         self.calculate(max_distance)
     71    def __cinit__(self, int16_t x=0, int16_t y=0, int16_t width=0, int16_t height=0):
     72        self.x = x
     73        self.y = y
     74        self.width = width
     75        self.height = height
    10576
    10677    def __repr__(self):
    107         return "ScrollDistances(%i)" % self.l
     78        return "ScrollDistances(%ix%i)" % (self.width, self.height)
    10879
    109     cdef calculate(self, uint16_t max_distance=1000):
    110         cdef int64_t *a1 = self.a1
    111         cdef int64_t *a2 = self.a2
    112         cdef uint16_t l = self.l
     80    #only used by the unit tests:
     81    def _test_update(self, arr):
     82        if self.a1:
     83            free(self.a1)
     84            self.a1 = NULL
     85        if self.a2:
     86            self.a1 = self.a2
     87            self.a2 = NULL
     88        cdef uint16_t l = len(arr)
     89        cdef size_t asize = l*(sizeof(uint64_t))
     90        self.a2 = <uint64_t*> memalign(asize)
     91        assert self.a2!=NULL, "checksum memory allocation failed"
     92        for i,v in enumerate(arr):
     93            self.a2[i] = <uint64_t> abs(v)
     94
     95    def update(self, pixels, int16_t x, int16_t y, int16_t width, int16_t height, int16_t rowstride, uint8_t bpp=4):
     96        """
     97            Add a new image to compare with,
     98            checksum its rows into a2,
     99            and push existing values (if we had any) into a1.
     100        """
     101        if DEBUG:
     102            log("%s.update%s a1=%#x, a2=%#x, distances=%#x, current size: %ix%i", self, (repr_ellipsized(pixels), x, y, width, height, rowstride, bpp), <uintptr_t> self.a1, <uintptr_t> self.a2, <uintptr_t> self.distances, self.width, self.height)
     103        assert width>0 and height>0, "invalid dimensions: %ix%i" % (width, height)
     104        #this is a new picture, shift a2 into a1 if we have it:
     105        if self.a1:
     106            free(self.a1)
     107            self.a1 = NULL
     108        if self.a2:
     109            self.a1 = self.a2
     110            self.a2 = NULL
     111        #scroll area can move within the window:
     112        self.x = x
     113        self.y = y
     114        #but cannot change size (checksums would not match):
     115        if height!=self.height or width!=self.width:
     116            log("new image size: %ix%i (was %ix%i), clearing reference checksums", width, height, self.width, self.height)
     117            if self.a1:
     118                free(self.a1)
     119                self.a1 = NULL
     120            if self.distances:
     121                free(self.distances)
     122                self.distances = NULL
     123            self.width = width
     124            self.height = height
     125        #allocate new checksum array:
     126        assert self.a2==NULL
     127        cdef size_t asize = height*(sizeof(uint64_t))
     128        self.a2 = <uint64_t*> memalign(asize)
     129        assert self.a2!=NULL, "checksum memory allocation failed"
     130        #checksum each line of the pixel array:
     131        cdef uint8_t *buf = NULL
     132        cdef Py_ssize_t buf_len = 0
     133        assert object_as_buffer(pixels, <const void**> &buf, &buf_len)==0
     134        assert buf_len>=0 and (<unsigned int> buf_len)>=rowstride*height, "buffer length=%i is too small for %ix%i" % (buf_len, rowstride, height)
     135        cdef size_t row_len = width*bpp
     136        assert row_len<=rowstride, "invalid row length: %ix%i=%i but rowstride is %i" % (width, bpp, width*bpp, rowstride)
     137        cdef uint64_t *a2 = self.a2
     138        cdef unsigned long long seed = 0
     139        cdef uint16_t i
     140        for i in range(height):
     141            a2[i] = <uint64_t> XXH64(buf, row_len, seed)
     142            #import xxhash
     143            #a2[i] = <uint64_t> abs(xxhash.xxh64(buf[:row_len]).intdigest())
     144            buf += rowstride
     145
     146    def calculate(self, uint16_t max_distance=1000):
     147        """
     148            Find all the scroll distances
     149            that would move lines from a1 to a2.
     150            The same lines may be accounted for multiple times.
     151            The result is stored in the "distances" array.
     152        """
     153        if DEBUG:
     154            log("calculate(%i) a1=%#x, a2=%#x, distances=%#x", max_distance, <uintptr_t> self.a1, <uintptr_t> self.a2, <uintptr_t> self.distances)
     155        if self.a1==NULL or self.a2==NULL:
     156            return
     157        cdef uint64_t *a1 = self.a1
     158        cdef uint64_t *a2 = self.a2
     159        cdef uint16_t l = self.height
    113160        cdef uint16_t y1, y2
    114161        cdef uint16_t miny=0, maxy=0
    115         cdef int64_t a2v
     162        cdef uint64_t a2v
     163        if self.distances==NULL:
     164            self.distances = <uint16_t*> memalign(2*l*sizeof(uint16_t))
     165            assert self.distances!=NULL, "distance memory allocation failed"
     166        cdef uint16_t matches = 0
    116167        with nogil:
    117             memset(self.distances, 0, 2*self.l*sizeof(uint16_t))
     168            memset(self.distances, 0, 2*l*sizeof(uint16_t))
    118169            for y2 in range(l):
    119170                #miny = max(0, y2-max_distance):
    120171                if y2>max_distance:
     
    133184                    if a1[y1]==a2v:
    134185                        #distance = y1-y2
    135186                        self.distances[l-(y1-y2)] += 1
     187                        matches += 1
    136188        if DEBUG:
    137             log("ScrollDistance: l=%i, calculate(%s, %s, %i)=%s", self.l, da(self.a1, self.l), da(self.a2, self.l), max_distance, dd(self.distances, self.l*2))
     189            log("ScrollDistance: height=%i, calculate:", l)
     190            log(" a1=%s", da(self.a1, l))
     191            log(" a2=%s", da(self.a2, l))
     192            log(" %i matches, distances=%s", matches, dd(self.distances, l*2))
    138193
    139     def get_best_scroll_values(self, uint16_t min_hits=2):
     194    def get_scroll_values(self, uint16_t min_hits=2):
     195        """
     196            Return two dictionaries that describe how to go from a1 to a2.
     197            * scrolls dictionary contains scroll definitions
     198            * non-scrolls dictionary is everything else (that will need to be repainted)
     199        """
    140200        DEF MAX_MATCHES = 20
     201        if self.a1==NULL or self.a2==NULL:
     202            return None
    141203        cdef uint16_t m_arr[MAX_MATCHES]    #number of hits
    142204        cdef int16_t s_arr[MAX_MATCHES]     #scroll distance
    143205        cdef int16_t i
     
    147209        cdef int16_t low = 0
    148210        cdef int16_t matches
    149211        cdef uint16_t* distances = self.distances
    150         cdef uint16_t l = self.l
     212        cdef uint16_t l = self.height
     213        #find the best values (highest match count):
    151214        with nogil:
    152215            for i in range(2*l):
    153216                matches = distances[i]
    154217                if matches>low and matches>min_hits:
    155218                    #add this candidate match to the arrays:
     219                    #find the lowest score index and replace it:
    156220                    for j in range(MAX_MATCHES):
    157221                        if m_arr[j]==low:
    158222                            break
    159223                    m_arr[j] = matches
    160224                    s_arr[j] = i-l
    161                     #find the new lowest value:
     225                    #find the new lowest value we have:
    162226                    low = matches
    163227                    for j in range(MAX_MATCHES):
    164228                        if m_arr[j]<low:
     
    165229                            low = m_arr[j]
    166230                            if low==0:
    167231                                break
    168         if DEBUG:
    169             log("get_best_scroll_values: arrays: matches=%s, scroll=%s", dd(m_arr, MAX_MATCHES), ds(s_arr, MAX_MATCHES))
    170232        #first collect the list of distances sorted by highest number of matches:
    171233        #(there can be more than one distance value for each match count):
    172234        scroll_hits = {}
     
    178240        #return a dict with the scroll distance as key,
    179241        #and the list of matching lines in a dictionary:
    180242        # {line-start : count, ..}
    181         #this is destructive as we clear the checksums after use in match_distance()
    182         scrolls = collections.OrderedDict()
    183         cdef uint16_t m
    184         #starting with the highest matches
    185         for m in reversed(sorted(scroll_hits.keys())):
    186             v = scroll_hits[m]
    187             for scroll in v:
    188                 #find matching lines:
    189                 line_defs = self.match_distance(scroll)
    190                 if line_defs:
    191                     scrolls[scroll] = line_defs
    192         return scrolls
    193 
    194     def get_remaining_areas(self):
    195         #all the lines which have not been zeroed out
    196         #when we matched them in match_distance
    197         cdef int64_t *a2 = self.a2
    198         cdef uint16_t i, start = 0, count = 0
    199         line_defs = collections.OrderedDict()
    200         for i in range(self.l):
    201             if a2[i]!=0:
    202                 if count==0:
    203                     start = i
    204                 count += 1
    205             elif count>0:
     243        #use a temporary buffer which we modify:
     244        #(where we clear the values we have already matched)
     245        cdef size_t asize = l*(sizeof(uint64_t))
     246        cdef uint64_t *tmp = <uint64_t*> memalign(asize)
     247        assert self.a2!=NULL, "checksum memory allocation failed"
     248        cdef uint16_t start = 0, count = 0
     249        try:
     250            memcpy(tmp, self.a2, asize)
     251            scrolls = collections.OrderedDict()
     252            #starting with the highest matches
     253            for i in reversed(sorted(scroll_hits.keys())):
     254                v = scroll_hits[i]
     255                for scroll in v:
     256                    #find matching lines:
     257                    line_defs = self.match_distance(tmp, scroll)
     258                    if line_defs:
     259                        scrolls[scroll] = line_defs
     260            #same for the unmatched lines:
     261            #all the lines in tmp which have not been zeroed out by match_distance()
     262            line_defs = collections.OrderedDict()
     263            for i in range(l):
     264                if tmp[i]!=0:
     265                    if count==0:
     266                        start = i
     267                    count += 1
     268                elif count>0:
     269                    line_defs[start] = count
     270                    count = 0
     271            if count>0:
    206272                line_defs[start] = count
    207                 count = 0
    208         if count>0:
    209             line_defs[start] = count
    210         return line_defs
     273        finally:
     274            free(tmp)
     275        return scrolls, line_defs
    211276
    212     def match_distance(self, int16_t distance):
    213         """ find the lines that match the given scroll distance """
    214         cdef int64_t *a1 = self.a1
    215         cdef int64_t *a2 = self.a2
     277    cdef match_distance(self, uint64_t *tmp, int16_t distance):
     278        """
     279            find the lines that match the given scroll distance,
     280            return a dictionary with the starting line as key
     281            and the number of matching lines as value
     282        """
     283        cdef uint64_t *a1 = self.a1
     284        cdef uint64_t *a2 = tmp         #this is a copy of a2 we can modify
    216285        cdef char swap = 0
    217286        if distance<0:
    218287            #swap order:
    219288            swap = 1
    220             a1 = self.a2
     289            a1 = tmp
    221290            a2 = self.a1
    222291            distance = -distance
    223         if DEBUG:
    224             log("match_distance(%i) l=%i, a1=%s, a2=%s", distance, self.l, da(a1, self.l), da(a2, self.l))
    225         assert distance<self.l, "invalid distance %i for size %i" % (distance, self.l)
     292        assert distance<self.height, "invalid distance %i for size %i" % (distance, self.height)
    226293        cdef uint16_t i, start = 0, count = 0
    227294        line_defs = collections.OrderedDict()
    228         for i in range(self.l-distance):
     295        for i in range(self.height-distance):
    229296            #if DEBUG:
    230297            #    log("%i: a1=%i / a2=%i", i, a1[i], a2[i+distance])
    231298            if a1[i]!=0 and a1[i]==a2[i+distance]:
     
    250317        if count>0:
    251318            #last few lines ended as a match:
    252319            line_defs[start] = count
    253         if DEBUG:
    254             log("match_distance(%i)=%s", distance, line_defs)
     320        #if DEBUG:
     321        #    log("match_distance(%i)=%s", distance, line_defs)
    255322        return line_defs
    256323
    257324
     325    def invalidate(self, int16_t x, int16_t y, int16_t w, int16_t h):
     326        if self.a2==NULL:
     327            #nothing to invalidate!
     328            return
     329        #do they intersect?
     330        rect = rectangle(self.x, self.y, self.width, self.height)
     331        inter = rect.intersection(x, y, w, h)
     332        if not inter:
     333            return
     334        #remove any lines that have been updated
     335        #by zeroing out their checksums:
     336        assert len(inter)<=self.height
     337        assert inter.y>=rect.y and inter.y+inter.height<=rect.y+rect.height
     338        #the array indexes are relative to rect.y:
     339        cdef int start_y = inter.y-rect.y
     340        cdef int i
     341        for i in range(start_y, start_y+inter.height):
     342            self.a2[i] = 0
     343        cdef uint16_t nonzero = 0
     344        for i in range(self.height):
     345            if self.a2[i]!=0:
     346                nonzero += 1
     347        log("invalidated %i lines checksums from intersection of scroll area %s and %s rectangle %s, remains %i", inter.height, rect, (x, y, w, h), nonzero)
     348        #if more than half has already been invalidated, drop it completely:
     349        if nonzero<=rect.height//2:
     350            log("invalidating whole scroll data as only %i of it remains valid", 100*nonzero//rect.height)
     351            free(self.a2)
     352            self.a2 = NULL
     353
     354
    258355    def get_best_match(self):
    259         cdef int16_t max = 0
     356        if self.a1==NULL or self.a2==NULL:
     357            return 0, 0
     358        cdef int16_t max_hits = 0
    260359        cdef int d = 0
    261360        cdef unsigned int i
    262         for i in range(2*self.l):
    263             if self.distances[i]>max:
    264                 max = self.distances[i]
    265                 d = i-self.l
    266         return d, max
     361        for i in range(2*self.height):
     362            if self.distances[i]>max_hits:
     363                max_hits = self.distances[i]
     364                d = i-self.height
     365        return d, max_hits
    267366
    268367    def __dealloc__(self):
     368        self.free()
     369
     370    def free(self):
    269371        cdef void* ptr = <void*> self.distances
    270372        if ptr:
    271373            self.distances = NULL
     
    278380        if ptr:
    279381            self.a2 = NULL
    280382            free(ptr)
    281        
    282 
    283 def scroll_distances(array1, array2, unsigned int min_score=0, uint16_t max_distance=1000):
    284     cdef ScrollDistances sd = ScrollDistances()
    285     sd.init(array1, array2, max_distance)
    286     return sd
  • xpra/server/window/window_video_source.py

     
    1414from xpra.codecs.codec_constants import TransientCodecException, RGB_FORMATS, PIXEL_SUBSAMPLING
    1515from xpra.server.window.window_source import WindowSource, STRICT_MODE, AUTO_REFRESH_SPEED, AUTO_REFRESH_QUALITY
    1616from xpra.server.window.region import rectangle, merge_all          #@UnresolvedImport
    17 from xpra.server.window.motion import scroll_distances, CRC_Image #@UnresolvedImport
     17from xpra.server.window.motion import ScrollData                    #@UnresolvedImport
    1818from xpra.server.window.video_subregion import VideoSubregion, VIDEO_SUBREGION
    1919from xpra.server.window.video_scoring import get_pipeline_score
    2020from xpra.codecs.loader import PREFERED_ENCODING_ORDER, EDGE_ENCODING_ORDER
     
    14251425        #overriden so we can invalidate the scroll data:
    14261426        #log.error("make_draw_packet%s", (x, y, w, h, coding, "..", outstride, client_options)
    14271427        packet = WindowSource.make_draw_packet(self, x, y, w, h, coding, data, outstride, client_options)
    1428         lsd = self.scroll_data
    1429         if lsd and not options.get("scroll"):
    1430             rect, csums = lsd
     1428        sd = self.scroll_data
     1429        if sd and not options.get("scroll") and False:
    14311430            if client_options.get("scaled_size") or client_options.get("quality", 100)<20:
    1432                 #don't scroll low quality content, better to refresh it
     1431                #don't scroll very low quality content, better to refresh it
    14331432                scrolllog("low quality %s update, invalidating all scroll data (scaled_size=%s, quality=%s)", coding, client_options.get("scaled_size"), client_options.get("quality", 100))
    1434                 self.scroll_data = None
     1433                sd.free()
    14351434            else:
    1436                 #do they intersect?
    1437                 inter = rect.intersection(x, y, w, h)
    1438                 if inter:
    1439                     #remove any lines that have been updated
    1440                     #by zeroing out their checksums:
    1441                     assert len(csums)==rect.height
    1442                     assert inter.y>=rect.y and inter.y+inter.height<=rect.y+rect.height
    1443                     #the array indexes are relative to rect.y:
    1444                     start_y = inter.y-rect.y
    1445                     for iy in range(start_y, start_y+inter.height):
    1446                         csums[iy] = 0
    1447                     nonzero = len([True for v in csums if v!=0])
    1448                     scrolllog("removed %i lines checksums from intersection of scroll area %s and %s draw packet %s, remains %i", inter.height, rect, coding, (x, y, w, h), nonzero)
    1449                     #if more than half has already been invalidated, drop it completely:
    1450                     if nonzero<=rect.height//2:
    1451                         scrolllog("invalidating whole scroll data as only %i of it remains valid", 100*nonzero//rect.height)
    1452                         self.scroll_data = None
     1435                sd.invalidate(x, y, w, h)
    14531436        return packet
    14541437
    14551438
    1456     def encode_scrolling(self, image, distances, options):
     1439    def encode_scrolling(self, image, options={}):
    14571440        start = time.time()
    14581441        try:
    14591442            del options["av-sync"]
     
    14611444            pass
    14621445        #tells make_data_packet not to invalidate the scroll data:
    14631446        ww, wh = self.window_dimensions
    1464         scrolllog("encode_scrolling(%s, %s, [], [], %s) window-dimensions=%s", image, distances, options, (ww, wh))
     1447        scrolllog("encode_scrolling(%s, %s) window-dimensions=%s", image, options, (ww, wh))
    14651448        x, y, w, h = image.get_geometry()[:4]
    1466         raw_scroll = distances.get_best_scroll_values()
    1467         non_scroll = distances.get_remaining_areas()
     1449        raw_scroll, non_scroll = self.scroll_data.get_scroll_values()
    14681450        scrolllog(" will send scroll data=%s, non-scroll=%s", raw_scroll, non_scroll)
    14691451        flush = len(non_scroll)
    14701452        assert raw_scroll, "failed to detect scroll values"
     
    15731555        x, y, w, h = image.get_geometry()[:4]
    15741556        src_format = image.get_pixel_format()
    15751557        stride = image.get_rowstride()
    1576         img_data = None
    15771558        if self.pixel_format!=src_format:
    15781559            videolog.warn("image pixel format changed from %s to %s", self.pixel_format, src_format)
    15791560            self.pixel_format = src_format
     
    15851566            rgb_format = image.get_pixel_format() #ie: BGRA
    15861567            stride = image.get_rowstride()
    15871568            img = Image.frombuffer("RGBA", (w, h), memoryview_to_bytes(img_data), "raw", rgb_format.replace("BGRX", "BGRA"), stride)
     1569            img.convert("RGB")
    15881570            kwargs = {}
    15891571            if SAVE_VIDEO_FRAMES=="jpeg":
    15901572                kwargs = {
     
    15971579            videolog("do_present_fbo: saving %4ix%-4i pixels, %7i bytes to %s", w, h, (stride*h), filename)
    15981580            img.save(filename, SAVE_VIDEO_FRAMES, **kwargs)
    15991581
    1600         test_scrolling = self.supports_scrolling and not STRICT_MODE
    1601         if test_scrolling and self.b_frame_flush_timer:
    1602             scrolllog("not testing scrolling: b_frame_flush_timer=%s", self.b_frame_flush_timer)
    1603             test_scrolling = False
    1604             self.scroll_data = None
    1605         else:
    1606             lsd = self.scroll_data
    1607             try:
    1608                 start = time.time()
    1609                 img_data = img_data or image.get_pixels()
    1610                 csums = CRC_Image(img_data, w, h, stride)
    1611                 if csums:
    1612                     self.scroll_data = rectangle(x, y, w, h), csums
    1613                     options["scroll"] = True
    1614                     scrolllog("updated scroll data with %s, previously set: %s", self.scroll_data[0], (lsd or [None])[0])
    1615                 if lsd and csums:
    1616                     rect, lcsums = lsd
    1617                     if rect.x!=x or rect.y!=y:
    1618                         #TODO: just adjust the scrolling packet for this offset
    1619                         scrolllog("scroll data position mismatch: %s vs %i,%i", rect, x, y)
    1620                     elif rect.width!=w or rect.height!=h:
    1621                         scrolllog("scroll data size mismatch: %s vs %ix%i", rect, w, h)
     1582        if self.supports_scrolling and not STRICT_MODE:
     1583            if self.b_frame_flush_timer and self.scroll_data:
     1584                scrolllog("not testing scrolling: b_frame_flush_timer=%s", self.b_frame_flush_timer)
     1585                self.scroll_data.free()
     1586                self.scroll_data = None
     1587            else:
     1588                try:
     1589                    start = time.time()
     1590                    if not self.scroll_data:
     1591                        self.scroll_data = ScrollData()
     1592                        scrolllog("new scroll data: %s", self.scroll_data)
     1593                    self.scroll_data.update(image.get_pixels(), x, y, w, h, image.get_rowstride(), len(src_format))
     1594                    max_distance = min(1000, (100-SCROLL_MIN_PERCENT)*h//100)
     1595                    self.scroll_data.calculate(max_distance)
     1596                    scroll, count = self.scroll_data.get_best_match()
     1597                    if count==0:
     1598                        scrolllog("no scroll distances found")
    16221599                    else:
    1623                         #same size, try to find scrolling value
    1624                         assert len(csums)==len(lcsums), "mismatch between checksums lists: %i vs %i items!" % (len(csums), len(lcsums))
    1625                         #no point in searching for a large scroll distance,
    1626                         #if we require a high match percentage:
    1627                         max_distance = min(1000, (100-SCROLL_MIN_PERCENT)*h//100)
    1628                         distances = scroll_distances(lcsums, csums, 2, max_distance)
    1629                         scroll, count = distances.get_best_match()
    1630                         if count==0:
    1631                             scrolllog("no scroll distances found")
    1632                         else:
    1633                             end = time.time()
    1634                             match_pct = int(100*count/h)
    1635                             scrolllog("best scroll guess took %ims, matches %i%% of %i lines: %s", (end-start)*1000, match_pct, h, scroll)
    1636                             #if enough scrolling is detected, use scroll encoding for this frame:
    1637                             if match_pct>=SCROLL_MIN_PERCENT:
    1638                                 return self.encode_scrolling(image, distances, options)
    1639             except Exception:
    1640                 scrolllog.error("Error during scrolling detection!", exc_info=True)
     1600                        end = time.time()
     1601                        match_pct = int(100*count/h)
     1602                        scrolllog("best scroll guess took %ims, matches %i%% of %i lines: %s", (end-start)*1000, match_pct, h, scroll)
     1603                        #if enough scrolling is detected, use scroll encoding for this frame:
     1604                        if match_pct>=SCROLL_MIN_PERCENT:
     1605                            return self.encode_scrolling(image, options)
     1606                except Exception:
     1607                    scrolllog.error("Error during scrolling detection")
     1608                    scrolllog.error(" with image=%s, options=%s", image, options, exc_info=True)
    16411609
    16421610        def video_fallback():
    16431611            videolog.warn("using non-video fallback encoding")