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.patch

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

this cythonized version of the code is super fast, but also not visually correct (yet)

  • xpra/server/window/motion.pyx

     
    11# coding=utf8
    22# This file is part of Xpra.
    3 # Copyright (C) 2016 Antoine Martin <antoine@devloop.org.uk>
     3# Copyright (C) 2016-2017 Antoine Martin <antoine@devloop.org.uk>
    44# Xpra is released under the terms of the GNU GPL v2, or, at your option, any
    55# later version. See the file COPYING for details.
    66
     
    99
    1010import os
    1111import time
     12import collections
    1213
    1314from xpra.util import envbool
    1415from xpra.log import Logger
    15 logger = Logger("encoding")
     16log = Logger("encoding", "scroll")
    1617
    17 import zlib
     18
     19cdef int DEBUG = envbool("XPRA_SCROLL_DEBUG", False)
    1820hashfn = None
    1921if envbool("XPRA_XXHASH", True):
    2022    try:
     
    2224        def hashfn(x):
    2325            return xxhash.xxh64(x).intdigest()
    2426    except ImportError as e:
    25         logger.warn("Warning: xxhash python bindings not found")
     27        log.warn("Warning: xxhash python bindings not found")
    2628else:
    27     logger.warn("Warning: xxhash disabled")
     29    log.warn("Warning: xxhash disabled")
    2830if hashfn is None:
    29     logger.warn(" no scrolling detection")
     31    log.warn(" no scrolling detection")
    3032
    3133
    32 cdef extern from "math.h":
    33     double log(double x)
     34from libc.stdint cimport int32_t, uint8_t, uint16_t, int16_t, uint32_t, int64_t
    3435
    35 from libc.stdint cimport int32_t, uint8_t, uint32_t, int64_t
    36 
    37 cdef extern from "stdlib.h":
    38     int abs(int number)
    39 
    4036cdef extern from "string.h":
    4137    void free(void * ptr) nogil
    4238    void *memset(void * ptr, int value, size_t num) nogil
    43     int memcmp(const void *a1, const void *a2, size_t size)
    4439
    4540cdef extern from "../../buffers/memalign.h":
    4641    void *xmemalign(size_t size) nogil
     
    7671    #assert v>=0, "invalid int to cast: %s" % v
    7772    return v
    7873
    79 def calculate_distances(array1, array2, int min_score=0, int max_distance=1000):
    80     #print("calculate_distances(..)")
    81     assert len(array1)==len(array2)
    82     cdef int l = len(array1)
    83     cdef int i, y1, y2, miny, maxy, d
    84     #we want fast array access,
    85     #so cache both arrays in C arrays:
    86     assert sizeof(int64_t)==64//8, "uint64_t is not 64-bit: %i!" % sizeof(int64_t)
    87     cdef size_t asize = l*(sizeof(int64_t))
    88     cdef int64_t *a1 = NULL
    89     cdef int64_t *a2 = NULL
    90     cdef int64_t a2v = 0
    91     cdef int32_t *distances = NULL
    92     #print("calculate_distances(%s, %s, %i, %i)" % (array1, array2, elen, min_score))
    93     try:
    94         a1 = <int64_t*> xmemalign(asize)
    95         a2 = <int64_t*> xmemalign(asize)
    96         assert a1!=NULL and a2!=NULL, "failed to allocate %i bytes of scroll array memory" % asize
    97         for i in range(l):
    98             a1[i] = castint64(array1[i])
    99             a2[i] = castint64(array2[i])
     74cdef inline da(int64_t *a, uint16_t l):
     75    return [a[i] for i in range(l)]
     76
     77cdef inline dd(uint16_t *d, uint16_t l):
     78    return [d[i] for i in range(l)]
     79
     80cdef inline ds(int16_t *d, uint16_t l):
     81    return [d[i] for i in range(l)]
     82
     83
     84assert sizeof(int64_t)==64//8, "uint64_t is not 64-bit: %i!" % sizeof(int64_t)
     85
     86
     87cdef class ScrollDistances:
     88
     89    cdef object __weakref__
     90    #for each distance, keep track of the hit count:
     91    cdef uint16_t* distances
     92    cdef uint16_t l
     93    cdef int64_t *a1
     94    cdef int64_t *a2
     95
     96    def init(self, array1, array2, uint16_t max_distance=1000):
     97        assert len(array1)==len(array2)
     98        assert len(array1)<2**15 and len(array1)>0, "invalid array length: %i" % len(array1)
     99        self.l = len(array1)
     100        self.distances = <uint16_t*> xmemalign(2*self.l*sizeof(uint16_t))
     101        cdef size_t asize = self.l*(sizeof(int64_t))
     102        self.a1 = <int64_t*> xmemalign(asize)
     103        self.a2 = <int64_t*> xmemalign(asize)
     104        assert self.distances!=NULL and self.a1!=NULL and self.a2!=NULL, "scroll memory allocation failed"
     105        for i in range(self.l):
     106            self.a1[i] = castint64(array1[i])
     107            self.a2[i] = castint64(array2[i])
    100108        #now compare all the values
    101         distances = <int32_t*> xmemalign(2*l*sizeof(int32_t))
    102         assert distances!=NULL
     109        self.calculate(max_distance)
     110
     111    def __repr__(self):
     112        return "ScrollDistances(%i)" % self.l
     113
     114    cdef calculate(self, uint16_t max_distance=1000):
     115        cdef int64_t *a1 = self.a1
     116        cdef int64_t *a2 = self.a2
     117        cdef uint16_t l = self.l
     118        cdef uint16_t y1, y2
     119        cdef uint16_t miny=0, maxy=0
     120        cdef int64_t a2v
    103121        with nogil:
    104             memset(<void*> distances, 0, 2*l*sizeof(int32_t))
     122            memset(<void*> self.distances, 0, 2*self.l*sizeof(uint16_t))
    105123            for y2 in range(l):
    106                 miny = max(0, y2-max_distance)
    107                 maxy = min(l, y2+max_distance)
     124                #miny = max(0, y2-max_distance):
     125                if y2>max_distance:
     126                    miny = y2-max_distance
     127                else:
     128                    miny = 0
     129                #maxy = min(l, y2+max_distance)
     130                if y2+max_distance<l:
     131                    maxy = y2+max_distance
     132                else:
     133                    maxy = l
    108134                a2v = a2[y2]
    109135                if a2v==0:
    110136                    continue
     
    111137                for y1 in range(miny, maxy):
    112138                    if a1[y1]==a2v:
    113139                        #distance = y1-y2
    114                         distances[l+y1-y2] += 1
    115         r = {}
    116         for i in range(2*l):
    117             d = distances[i]
    118             if abs(d)>=min_score:
    119                 r[i-l] = d
    120         return r
    121     finally:
    122         if a1!=NULL:
    123             free(a1)
    124         if a2!=NULL:
    125             free(a2)
    126         if distances!=NULL:
    127             free(distances)
     140                        self.distances[l-(y1-y2)] += 1
     141        if DEBUG:
     142            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))
    128143
    129 def match_distance(array1, array2, int distance):
    130     assert len(array1)==len(array2)
    131     l = len(array1)
    132     if distance>=0:
    133         return [i for i,v in enumerate(array1) if (i+distance)<l and array2[i+distance]==v]
    134     distance = abs(distance)
    135     return [i+distance for i,v in enumerate(array2) if (i+distance)<l and array1[i+distance]==v]
     144    def get_best_scroll_values(self, uint16_t min_hits=2):
     145        DEF MAX_MATCHES = 20
     146        cdef uint16_t m_arr[MAX_MATCHES]    #number of hits
     147        cdef int16_t s_arr[MAX_MATCHES]     #scroll distance
     148        cdef int16_t i
     149        cdef uint8_t j
     150        memset(<void*> &m_arr, 0, MAX_MATCHES*sizeof(uint16_t))
     151        memset(<void*> &s_arr, 0, MAX_MATCHES*sizeof(int16_t))
     152        cdef int16_t low = 0
     153        cdef int16_t matches
     154        cdef uint16_t* distances = self.distances
     155        cdef uint16_t l = self.l
     156        with nogil:
     157            for i in range(2*l):
     158                matches = distances[i]
     159                if matches>low and matches>min_hits:
     160                    #add this candidate match to the arrays:
     161                    for j in range(MAX_MATCHES):
     162                        if m_arr[j]==low:
     163                            break
     164                    m_arr[j] = matches
     165                    s_arr[j] = i-l
     166                    #find the new lowest value:
     167                    low = matches
     168                    for j in range(MAX_MATCHES):
     169                        if m_arr[j]<low:
     170                            low = m_arr[j]
     171                            if low==0:
     172                                break
     173        if DEBUG:
     174            log("get_best_scroll_values: arrays: matches=%s, scroll=%s", dd(m_arr, MAX_MATCHES), ds(s_arr, MAX_MATCHES))
     175        #first collect the list of distances sorted by highest number of matches:
     176        #(there can be more than one distance value for each match count):
     177        scroll_hits = {}
     178        for i in range(MAX_MATCHES):
     179            if m_arr[i]>min_hits:
     180                scroll_hits.setdefault(m_arr[i], []).append(s_arr[i])
     181        if DEBUG:
     182            log("scroll hits=%s", scroll_hits)
     183        #return a dict with the scroll distance as key,
     184        #and the list of matching lines in a dictionary:
     185        # {line-start : count, ..}
     186        #this is destructive as we clear the checksums after use
     187        scrolls = collections.OrderedDict()
     188        cdef uint16_t m
     189        #starting with the highest matches
     190        for m in reversed(sorted(scroll_hits.keys())):
     191            v = scroll_hits[m]
     192            for scroll in v:
     193                #find matching lines:
     194                line_defs = self.match_distance(scroll)
     195                if line_defs:
     196                    scrolls[scroll] = line_defs
     197        return scrolls
    136198
    137 def consecutive_lines(line_numbers):
    138     #print("line_numbers_to_rectangles(%s)" % (line_numbers, ))
    139     #aggregates consecutive lines:
    140     #[1,2,3,4,8,9] -> [(1,3), (8,1)]
    141     assert len(line_numbers)>0
    142     if len(line_numbers)==1:
    143         return [(line_numbers[0], 1)]
    144     cdef int start = line_numbers[0]
    145     cdef int last = start
    146     cdef int line = 0
    147     r = []
    148     for line in line_numbers[1:]:
    149         if line!=last+1:
    150             #new rectangle
    151             r.append((start, last-start+1))
    152             start = line
    153         last = line
    154     if last!=line_numbers[0]:
    155         r.append((start, last-start+1))
    156     return r
     199    def get_remaining_areas(self):
     200        #all the lines which have not been zeroed out
     201        #when we matched them with match_distance
     202        cdef int64_t *a2 = self.a2
     203        cdef uint16_t i, start = 0, count = 0
     204        line_defs = collections.OrderedDict()
     205        for i in range(self.l):
     206            if a2[i]!=0:
     207                if count==0:
     208                    start = i
     209                count += 1
     210            elif count>0:
     211                line_defs[start] = count
     212                count = 0
     213        if count>0:
     214            line_defs[start] = count
     215        return line_defs
     216
     217    def match_distance(self, int16_t distance):
     218        """ find the lines that match the given scroll distance """
     219        cdef int64_t *a1 = self.a1
     220        cdef int64_t *a2 = self.a2
     221        cdef char swap = 0
     222        if distance<0:
     223            #swap order:
     224            swap = 1
     225            a1 = self.a2
     226            a2 = self.a1
     227            distance = -distance
     228        if DEBUG:
     229            log("match_distance(%i) l=%i, a1=%s, a2=%s", distance, self.l, da(a1, self.l), da(a2, self.l))
     230        assert distance<self.l, "invalid distance %i for size %i" % (distance, self.l)
     231        cdef uint16_t i, start = 0, count = 0
     232        line_defs = collections.OrderedDict()
     233        for i in range(self.l-distance):
     234            #if DEBUG:
     235            #    log("%i: a1=%i / a2=%i", i, a1[i], a2[i+distance])
     236            if a1[i]!=0 and a1[i]==a2[i+distance]:
     237                #if DEBUG:
     238                #    log("match at %i: %i", i, a1[i])
     239                if count==0:
     240                    #first match
     241                    start = i
     242                count += 1
     243                #mark the target line as dealt with:
     244                if swap:
     245                    a1[i] = 0
     246                else:
     247                    a2[i+distance] = 0
     248            elif count>0:
     249                #we had a match
     250                line_defs[start] = count
     251                count = 0
     252        if count>0:
     253            #last few lines ended as a match:
     254            line_defs[start] = count
     255        if DEBUG:
     256            log("match_distance(%i)=%s", distance, line_defs)
     257        return line_defs
     258
     259
     260    def get_best_match(self):
     261        cdef int16_t max = 0
     262        cdef int d = 0
     263        cdef unsigned int i
     264        for i in range(2*self.l):
     265            if self.distances[i]>max:
     266                max = self.distances[i]
     267                d = i-self.l
     268        return d, max
     269
     270    def __dealloc__(self):
     271        cdef void* ptr = <void*> self.distances
     272        if ptr:
     273            self.distances = NULL
     274            free(ptr)
     275        ptr = <void*> self.a1
     276        if ptr:
     277            self.a1 = NULL
     278            free(ptr)
     279        ptr = <void*> self.a2
     280        if ptr:
     281            self.a2 = NULL
     282            free(ptr)
     283       
     284
     285def scroll_distances(array1, array2, unsigned int min_score=0, uint16_t max_distance=1000):
     286    cdef ScrollDistances sd = ScrollDistances()
     287    sd.init(array1, array2, max_distance)
     288    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 match_distance, consecutive_lines, calculate_distances, CRC_Image #@UnresolvedImport
     17from xpra.server.window.motion import scroll_distances, CRC_Image #@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
     
    14531453        return packet
    14541454
    14551455
    1456     def encode_scrolling(self, image, distances, old_csums, csums, options):
     1456    def encode_scrolling(self, image, distances, options):
    14571457        start = time.time()
    14581458        try:
    14591459            del options["av-sync"]
     
    14601460        except:
    14611461            pass
    14621462        #tells make_data_packet not to invalidate the scroll data:
    1463         scrolllog("encode_scrolling(%s, {..}, [], [], %s) window-dimensions=%s", image, options, self.window_dimensions)
     1463        scrolllog("encode_scrolling(%s, %s, [], [], %s) window-dimensions=%s", image, distances, options, self.window_dimensions)
    14641464        x, y, w, h = image.get_geometry()[:4]
    1465         yscroll_values = []
    1466         max_scroll_regions = 50
    1467         #process distances with the highest score first:
    1468         for hits in reversed(sorted(distances.values())):
    1469             for scroll in (d for d,v in distances.items() if v==hits):
    1470                 assert scroll<h
    1471                 yscroll_values.append(scroll)
    1472             if len(yscroll_values)>=max_scroll_regions:
    1473                 break
    1474         assert yscroll_values
    1475         #always add zero=no-change so we can drop those updates!
    1476         if 0 not in yscroll_values and 0 in distances:
    1477             #(but do this last so we don't end up cutting too many rectangles)
    1478             yscroll_values.append(0)
    1479         scrolllog(" will send scroll packets for yscroll=%s", yscroll_values)
    1480         #keep track of the lines we have handled already:
    1481         #(the same line may be available from multiple scroll directions)
    1482         handled = set()
     1465        raw_scroll = distances.get_best_scroll_values()
     1466        non_scroll = distances.get_remaining_areas()
     1467        scrolllog(" will send scroll data=%s, non-scroll=%s", raw_scroll, non_scroll)
     1468        flush = len(non_scroll)
     1469        assert raw_scroll, "failed to detect scroll values"
     1470        #convert to a screen rectangle list for the client:
    14831471        scrolls = []
    1484         max_scrolls = 1000
    1485         for s in yscroll_values:
    1486             #find all the lines that scroll by this much:
    1487             slines = match_distance(old_csums, csums, s)
    1488             assert slines, "no lines matching distance %i" % s
    1489             #remove any lines we have already handled:
    1490             lines = [v+s for v in slines if ((v+s) not in handled and v not in handled)]
    1491             if not lines:
    1492                 continue
    1493             #and them to the handled set so we don't try to paint them again:
    1494             handled = handled.union(set(lines))
    1495             if s==0:
    1496                 scrolllog(" %i lines have not changed", len(lines))
    1497             else:
    1498                 #things have actually moved
    1499                 #aggregate consecutive lines into rectangles:
    1500                 cl = consecutive_lines(lines)
    1501                 #scrolllog(" scroll groups for distance=%i : %s=%s", s, lines, cl)
    1502                 scrolllog(" scroll groups for distance=%i : %s", s, cl)
    1503                 for sy,sh in cl:
    1504                     #new rectangle
    1505                     scrolls.append((x, y+sy-s, w, sh, 0, s))
    1506                     if len(scrolls)>max_scrolls:
    1507                         break
    1508                 if len(scrolls)>max_scrolls:
    1509                     break
    1510 
    1511         non_scroll = []
    1512         remaining = set(range(h))-handled
    1513         if remaining:
    1514             damaged_lines = sorted(list(remaining))
    1515             non_scroll = consecutive_lines(damaged_lines)
    1516             scrolllog(" non scroll: %i packets: %s", len(non_scroll), non_scroll)
    1517         flush = len(non_scroll)
    1518         #send as scroll paints packets:
    1519         if scrolls:
    1520             client_options = options.copy()
    1521             if flush>0 and self.supports_flush:
    1522                 client_options["flush"] = flush
    1523             coding = "scroll"
    1524             end = time.time()
    1525             packet = self.make_draw_packet(x, y, w, h, coding, LargeStructure(coding, scrolls), 0, client_options, options)
    1526             self.queue_damage_packet(packet)
    1527             compresslog("compress: %5.1fms for %4ix%-4i pixels at %4i,%-4i for wid=%-5i using %6s as %3i rectangles  (%5iKB)           , sequence %5i, client_options=%s",
    1528                  (end-start)*1000.0, w, h, x, y, self.wid, coding, len(scrolls), w*h*4/1024, self._damage_packet_sequence, client_options)
     1472        for scroll, line_defs in raw_scroll.items():
     1473            for line, count in line_defs.items():
     1474                scrolls.append((x, y+line, w, count, 0, scroll))
     1475        assert len(scrolls)>0
     1476        client_options = options.copy()
     1477        try:
     1478            del client_options["scroll"]
     1479        except:
     1480            pass
     1481        if flush>0 and self.supports_flush:
     1482            client_options["flush"] = flush
     1483        coding = "scroll"
     1484        end = time.time()
     1485        packet = self.make_draw_packet(x, y, w, h, coding, LargeStructure(coding, scrolls), 0, client_options, options)
     1486        self.queue_damage_packet(packet)
     1487        compresslog("compress: %5.1fms for %4ix%-4i pixels at %4i,%-4i for wid=%-5i using %6s as %3i rectangles  (%5iKB)           , sequence %5i, client_options=%s",
     1488             (end-start)*1000.0, w, h, x, y, self.wid, coding, len(scrolls), w*h*4/1024, self._damage_packet_sequence, client_options)
    15291489        #send the rest as rectangles:
    15301490        if non_scroll:
    15311491            nsstart = time.time()
     
    15341494            else:
    15351495                #slower but can be lossless:
    15361496                encoding = self.get_video_fallback_encoding(PREFERED_ENCODING_ORDER)
     1497            client_options = options.copy()
    15371498            if encoding:
    15381499                encode_fn = self._encoders[encoding]
    1539                 for sy, sh in non_scroll:
     1500                for sy, sh in non_scroll.items():
     1501                    #s = time.time()
    15401502                    sub = image.get_sub_image(0, sy, w, sh)
    1541                     flush -= 1
     1503                    #scrolllog("sub_image %i pixels: %.1fms", (w*sh), (time.time()-s)*1000)
    15421504                    ret = encode_fn(encoding, sub, options)
    15431505                    if not ret:
    15441506                        self.free_image_wrapper(sub)
     
    15461508                        return None
    15471509                    coding, data, client_options, outw, outh, outstride, _ = ret
    15481510                    assert data
    1549                     client_options = options.copy()
    1550                     if flush>0 and self.supports_flush:
     1511                    flush -= 1
     1512                    if self.supports_flush and flush>0:
    15511513                        client_options["flush"] = flush
    15521514                    packet = self.make_draw_packet(sub.get_x(), sub.get_y(), outw, outh, coding, data, outstride, client_options, options)
    15531515                    self.queue_damage_packet(packet)
     
    15581520                    compresslog("compress: %5.1fms for %4ix%-4i pixels at %4i,%-4i for wid=%-5i using %6s with ratio %5.1f%%  (%5iKB to %5iKB), sequence %5i, client_options=%s",
    15591521                         (end-nsstart)*1000.0, w, sh, 0, sy, self.wid, coding, 100.0*csize/psize, psize/1024, csize/1024, self._damage_packet_sequence, client_options)
    15601522                scrolllog("non-scroll encoding using %s (quality=%i, speed=%i) took %ims for %i rectangles", encoding, self._current_quality, self._current_speed, (time.time()-nsstart)*1000, len(non_scroll))
     1523            else:
     1524                #we can't send the non-scroll areas, ouch!
     1525                flush = 0
    15611526        assert flush==0
    15621527        self.last_scroll_time = time.time()
    15631528        scrolllog("scroll encoding total time: %ims", (self.last_scroll_time-start)*1000)
     
    16351600                if csums:
    16361601                    self.scroll_data = rectangle(x, y, w, h), csums
    16371602                    options["scroll"] = True
    1638                     scrolllog("updated scroll data, previously set: %s", bool(lsd))
     1603                    scrolllog("updated scroll data with %s, previously set: %s", self.scroll_data[0], (lsd or [None])[0])
    16391604                if lsd and csums:
    16401605                    rect, lcsums = lsd
    16411606                    if rect.x!=x or rect.y!=y:
     
    16461611                    else:
    16471612                        #same size, try to find scrolling value
    16481613                        assert len(csums)==len(lcsums), "mismatch between checksums lists: %i vs %i items!" % (len(csums), len(lcsums))
    1649                         distances = calculate_distances(csums, lcsums, 2, 1000)
    1650                         if len(distances)==0:
     1614                        max_distance = min(1000, (100-SCROLL_MIN_PERCENT)*h//100)
     1615                        distances = scroll_distances(lcsums, csums, 2, max_distance)
     1616                        scroll, count = distances.get_best_match()
     1617                        if count==0:
    16511618                            scrolllog("no scroll distances found")
    16521619                        else:
    1653                             best = max(distances.values())
    1654                             scroll = distances.keys()[distances.values().index(best)]
    16551620                            end = time.time()
    1656                             best_pct = int(100*best/h)
    1657                             scrolllog("best scroll guess took %ims, matches %i%% of %i lines: %s", (end-start)*1000, best_pct, h, scroll)
     1621                            match_pct = int(100*count/h)
     1622                            scrolllog("best scroll guess took %ims, matches %i%% of %i lines: %s", (end-start)*1000, match_pct, h, scroll)
    16581623                            #if enough scrolling is detected, use scroll encoding for this frame:
    1659                             if best_pct>=SCROLL_MIN_PERCENT:
    1660                                 return self.encode_scrolling(image, distances, lcsums, csums, options)
     1624                            if match_pct>=SCROLL_MIN_PERCENT:
     1625                                return self.encode_scrolling(image, distances, options)
    16611626            except Exception:
    16621627                scrolllog.error("Error during scrolling detection!", exc_info=True)
    16631628
  • unittests/unit/server/motion_test.py

     
    11#!/usr/bin/env python
    22# This file is part of Xpra.
    3 # Copyright (C) 2011-2014 Antoine Martin <antoine@devloop.org.uk>
     3# Copyright (C) 2016-2017 Antoine Martin <antoine@devloop.org.uk>
    44# Xpra is released under the terms of the GNU GPL v2, or, at your option, any
    55# later version. See the file COPYING for details.
    66
     
    2020
    2121class TestMotion(unittest.TestCase):
    2222
     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)
     26
    2327        def test_match_distance(self):
    2428                def t(a1, a2, distance, matches):
    25                         lines = motion.match_distance(a1, a2, distance)
    26                         assert len(lines)==matches, "expected %i matches for distance=%i but got %i for a1=%s, a2=%s, result=%s" % (matches, distance, len(lines), a1, a2, lines)
     29                        sd = motion.scroll_distances(a1, a2)
     30                        line_defs = sd.match_distance(distance)
     31                        linecount = sum(line_defs.values())
     32                        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)
    2733                for N in (1, 10, 100):
    28                         a = range(N)
     34                        a = range(1, N+1)
    2935                        t(a, a, 0, N)           #identity: all match
    3036
    31                         a = [0]*N
     37                        a = [1]*N
    3238                        t(a, a, 0, N)
    3339                        for M in range(N):
    3440                                t(a, a, M, N-M)
    3541
     42                #from a1 to a2: shift by 2, get 2 hits
     43                t([3, 4, 5, 6], [1, 2, 3, 4], 2, 2)
    3644                #from a2 to a1: shift by -2, get 2 hits
    37                 t([0, 1, 2, 3], [2, 3, 4, 5], -2, 2)
     45                t([1, 2, 3, 4], [3, 4, 5, 6], -2, 2)
    3846                N = 100
    39                 a1 = range(N)
    40                 for M in (1, 2, 10, 100):
     47                S = 1
     48                a1 = range(S, S+N)
     49                for M in (2, 3, 10, 100):
    4150                        a2 = range(M, M+N)
    42                         t(a1, a2, -M, N-abs(M))
    43                         a2 = range(-M, N-M)
    44                         t(a1, a2, M, N-abs(M))
    45                 for M in (-10, -20):
    46                         a2 = range(M, N+M)
    47                         t(a1, a2, -M, N-abs(M))
     51                        t(a1, a2, S-M, S+N-M)
     52                        t(a2, a1, M-S, S+N-M)
    4853
    49         def test_consecutive_lines(self):
    50                 def f(v):
    51                         try:
    52                                 motion.consecutive_lines(v)
    53                         except:
    54                                 pass
    55                         else:
    56                                 raise Exception("consecutive_lines should have failed for value %s" % v)
    57                 f(None)
    58                 f("")
    59                 f([])
    60                 f(1)
    61                 def t(v, e):
    62                         r = motion.consecutive_lines(v)
    63                         assert r==e, "expected %s but got %s for input=%s" % (e, r, v)
    64                 t([5, 10], [(5, 1), (10, 1)])
    65                 t([5], [(5, 1)])
    66                 t([1,2,3], [(1,3)])
    67                 t(range(100), [(0, 100),])
    68                 t([1,2,3,100,200,201], [(1,3), (100, 1), (200, 2)])
    69 
    7054        def test_calculate_distances(self):
    7155                array1 = [crc32(str(x)) for x in (1234, "abc", 99999)]
    72                 array2 = array1
    73                 d = motion.calculate_distances(array1, array2, 1)
    74                 assert len(d)==1 and d[0]==3
     56                array2 = array1[:]
     57                d = self.calculate_distances(array1, array2, 1)
     58                assert len(d)==1 and sum(d[0].values())==len(array1), "expected %i matches but got %s" % (len(array1), d[0])
    7559
    76                 array1 = range(0, 3)
    77                 array2 = range(1, 4)
    78                 d = motion.calculate_distances(array1, array2, 1)
    79                 assert len(d)==1, "expected 1 match but got: %s" % d
    80                 assert d.get(1)==2, "expected distance of 1 with 2 hits but got: %s" % d
     60                array1 = range(1, 5)
     61                array2 = range(2, 6)
     62                d = self.calculate_distances(array1, array2)
     63                assert len(d)==1, "expected 1 match but got: %s" % len(d)
     64                common = set(array1).intersection(set(array2))
     65                assert -1 in d, "expected distance of -1 but got: %s" % d.keys()
     66                linecount = sum(d.get(-1).values())
     67                assert linecount==len(common), "expected %i hits but got: %s" % (len(common), linecount)
    8168                def cdf(v1, v2):
    8269                        try:
    83                                 motion.calculate_distances(v1, v2, 1)
     70                                self.calculate_distances(v1, v2, 1)
    8471                        except:
    8572                                return
    8673                        raise Exception("calculate_distances should have failed for values: %s" % (v1, v2))
     
    8875                cdf([], None)
    8976                cdf(None, [])
    9077                cdf([1, 2], [1])
    91                 assert len(motion.calculate_distances([], [], 1))==0
    9278
    9379                #performance:
    9480                N = 4096
     
    9581                start = time.time()
    9682                array1 = range(N)
    9783                array2 = [N*2-x*2 for x in range(N)]
    98                 d = motion.calculate_distances(array1, array2, 1)
     84                d = self.calculate_distances(array1, array2, 1)
    9985                end = time.time()
    10086                if SHOW_PERF:
    10187                        print("calculate_distances %4i^2 in %5.1f ms" % (N, (end-start)*1000))
     
    137123                                print("\nCRC_Image %ix%i (%.1fMB) in %4.2f ms" % (W, H, len(buf2)//1024//1024, 1000.0*(end-start)))
    138124                        assert len(ov2)==H
    139125                        start = time.time()
    140                         distances = motion.calculate_distances(ov1, ov2, min_score=1)
     126                        distances = self.calculate_distances(ov1, ov2, min_hits=1)
    141127                        end = time.time()
    142128                        if SHOW_PERF:
    143129                                print("calculate_distances %4i^2 in %5.2f ms" % (H, 1000.0*(end-start)))
    144                         linecount = distances.get(N, 0)
     130                        line_defs = distances.get(-N, 0)
     131                        linecount = sum(line_defs.values())
    145132                        assert linecount>0, "could not find distance %i" % N
    146133                        assert linecount == (H-N), "expected to match %i lines but got %i" % (H-N, linecount)
    147134                if False: