Ticket #1426: scroll-cythonized-v4.patch
File scroll-cythonized-v4.patch, 34.6 KB (added by , 4 years ago) |
---|
-
setup.py
2174 2174 ["xpra/server/window/region.pyx"], 2175 2175 **O3_pkgconfig)) 2176 2176 cython_add(Extension("xpra.server.window.motion", 2177 ["xpra/server/window/motion.pyx" ],2177 ["xpra/server/window/motion.pyx", "xpra/server/window/xxhash.c"], 2178 2178 **O3_pkgconfig)) 2179 2179 2180 2180 -
unittests/unit/server/motion_test.py
11 11 from xpra.util import envbool 12 12 try: 13 13 from xpra.server.window import motion 14 log = motion.log #@UndefinedVariable 14 15 except ImportError: 15 16 motion = None 16 17 … … 20 21 21 22 class TestMotion(unittest.TestCase): 22 23 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) 26 28 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 27 39 def test_match_distance(self): 28 40 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) 31 44 linecount = sum(line_defs.values()) 32 45 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) 33 46 for N in (1, 10, 100): … … 36 49 37 50 a = [1]*N 38 51 t(a, a, 0, N) 39 for M in range(N):40 t(a, a, M, N-M)41 52 42 53 #from a1 to a2: shift by 2, get 2 hits 43 54 t([3, 4, 5, 6], [1, 2, 3, 4], 2, 2) … … 54 65 def test_calculate_distances(self): 55 66 array1 = [crc32(str(x)) for x in (1234, "abc", 99999)] 56 67 array2 = array1[:] 57 d = self.calculate_distances(array1, array2, 1) 68 d = self.calculate_distances(array1, array2, 1)[0] 58 69 assert len(d)==1 and sum(d[0].values())==len(array1), "expected %i matches but got %s" % (len(array1), d[0]) 59 70 60 71 array1 = range(1, 5) 61 72 array2 = range(2, 6) 62 d = self.calculate_distances(array1, array2) 73 d = self.calculate_distances(array1, array2)[0] 63 74 assert len(d)==1, "expected 1 match but got: %s" % len(d) 64 75 common = set(array1).intersection(set(array2)) 65 76 assert -1 in d, "expected distance of -1 but got: %s" % d.keys() … … 81 92 start = time.time() 82 93 array1 = range(N) 83 94 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] 85 96 end = time.time() 86 97 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)) 88 99 89 100 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 91 106 #W, H, BPP = 2, 4, 4 92 107 LEN = W * H * BPP 93 108 import numpy as np 94 109 try: 95 na1 = np.random.randint(2 **63-1, size=LEN//8, dtype="int64")110 na1 = np.random.randint(255, size=LEN, dtype="uint8") 96 111 except TypeError as e: 97 112 #older numpy version may not have dtype argument.. 98 113 #and may not accept 64-bit values … … 110 125 #older versions of numpy (ie: centos7) 111 126 return a.tostring() 112 127 buf1 = tobytes(na1) 113 ov1 = motion.CRC_Image(buf1, W, H, W*BPP, BPP)114 assert len(ov1)==H128 #push first image: 129 sd = motion.ScrollData(0, 0, W, H) 115 130 #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) 118 137 buf2 = tobytes(na2) 119 138 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) 121 140 end = time.time() 122 141 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))) 125 143 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)) 127 149 end = time.time() 128 150 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, {}) 131 153 linecount = sum(line_defs.values()) 132 assert linecount>0, "could not find distance %i " % N154 assert linecount>0, "could not find distance %i in %s" % (N, line_defs) 133 155 assert linecount == (H-N), "expected to match %i lines but got %i" % (H-N, linecount) 134 156 if False: 135 157 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))) 138 160 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, )) 143 163 144 164 def test_csum_data(self): 145 165 a1=[ … … 181 201 17157005122993541799, 5218869126146608853, 13274228147453099388, 16342723934713827717, 2435034235422505275, 3689766606612767057, 13721141386368216492, 14859793948180065358, 182 202 ] 183 203 #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() 186 209 wh = len(a1) 187 print("best match: %s" % ((scroll, count),))210 log("best match: %s" % ((scroll, count),)) 188 211 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 191 214 scrolls = [] 192 215 def h(v): 193 216 return hex(v).lstrip("0x").rstrip("L") 194 217 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]))) 196 219 for scroll, line_defs in raw_scroll.items(): 197 220 if scroll==0: 198 221 continue -
xpra/server/window/motion.pyx
9 9 10 10 import os 11 11 import time 12 import struct 12 13 import collections 13 14 14 from xpra.util import envbool 15 from xpra.util import envbool, repr_ellipsized, csv 15 16 from xpra.log import Logger 16 17 log = Logger("encoding", "scroll") 17 18 18 19 from xpra.buffers.membuf cimport memalign, object_as_buffer 20 from xpra.server.window.region import rectangle 19 21 22 20 23 cdef int DEBUG = envbool("XPRA_SCROLL_DEBUG", False) 21 hashfn = None22 if envbool("XPRA_XXHASH", True):23 try:24 import xxhash25 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")33 24 34 25 35 from libc.stdint cimport int32_t, uint8_t, uint16_t, int16_t, uint32_t, int64_t26 from libc.stdint cimport uint8_t, uint16_t, int16_t, uint64_t, uintptr_t 36 27 37 28 cdef extern from "string.h": 38 29 void free(void * ptr) nogil 39 30 void *memset(void * ptr, int value, size_t num) nogil 31 void *memcpy(void * destination, void * source, size_t num) nogil 40 32 33 cdef 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) 41 36 42 def CRC_Image(pixels, unsigned int width, unsigned int height, unsigned int rowstride, unsigned char bpp=4):43 global hashfn44 if not hashfn:45 return None46 cdef uint8_t *buf = NULL47 cdef Py_ssize_t buf_len = 048 assert object_as_buffer(pixels, <const void**> &buf, &buf_len)==049 assert buf_len>=0 and (<unsigned int> buf_len)>=rowstride*height, "buffer is too small for %ix%i" % (rowstride, height)50 cdef unsigned int i51 cdef size_t row_len = width*bpp52 f = hashfn53 crcs = []54 for i in range(height):55 crcs.append(f(buf[:row_len]))56 buf += rowstride57 return crcs58 37 59 60 38 DEF MAXINT64 = 2**63 61 39 DEF MAXUINT64 = 2**64 62 40 DEF MASK64 = 2**64-1 63 cdef inline castint64(v):64 if v>=MAXINT64:65 return v-MAXUINT6466 #assert v>=0, "invalid int to cast: %s" % v67 return v68 41 69 cdef inline da(int64_t *a, uint16_t l):70 return [a[i] for i in range(l)]42 def h(v): 43 return hex(v).lstrip("0x").rstrip("L") 71 44 72 cdef inline dd(uint16_t *d, uint16_t l):73 return [d[i] for i in range(l)]45 cdef inline uint64_t hashtoint64(s): 46 return <uint64_t> struct.unpack("@L", s)[0] 74 47 75 cdef inline ds(int16_t *d, uint16_t l):76 return [d[i] for i in range(l)]48 cdef da(uint64_t *a, uint16_t l): 49 return repr_ellipsized(csv(h(a[i]) for i in range(l))) 77 50 51 cdef dd(uint16_t *d, uint16_t l): 52 return repr_ellipsized(csv(h(d[i]) for i in range(l))) 78 53 79 assert sizeof(int64_t)==64//8, "uint64_t is not 64-bit: %i!" % sizeof(int64_t)80 54 55 assert sizeof(uint64_t)==64//8, "uint64_t is not 64-bit: %i!" % sizeof(uint64_t) 81 56 82 cdef class ScrollDistances:83 57 58 cdef class ScrollData: 59 84 60 cdef object __weakref__ 85 61 #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 90 70 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 105 76 106 77 def __repr__(self): 107 return "ScrollDistances(%i )" % self.l78 return "ScrollDistances(%ix%i)" % (self.width, self.height) 108 79 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 113 160 cdef uint16_t y1, y2 114 161 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 116 167 with nogil: 117 memset(self.distances, 0, 2* self.l*sizeof(uint16_t))168 memset(self.distances, 0, 2*l*sizeof(uint16_t)) 118 169 for y2 in range(l): 119 170 #miny = max(0, y2-max_distance): 120 171 if y2>max_distance: … … 133 184 if a1[y1]==a2v: 134 185 #distance = y1-y2 135 186 self.distances[l-(y1-y2)] += 1 187 matches += 1 136 188 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)) 138 193 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 """ 140 200 DEF MAX_MATCHES = 20 201 if self.a1==NULL or self.a2==NULL: 202 return None 141 203 cdef uint16_t m_arr[MAX_MATCHES] #number of hits 142 204 cdef int16_t s_arr[MAX_MATCHES] #scroll distance 143 205 cdef int16_t i … … 147 209 cdef int16_t low = 0 148 210 cdef int16_t matches 149 211 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): 151 214 with nogil: 152 215 for i in range(2*l): 153 216 matches = distances[i] 154 217 if matches>low and matches>min_hits: 155 218 #add this candidate match to the arrays: 219 #find the lowest score index and replace it: 156 220 for j in range(MAX_MATCHES): 157 221 if m_arr[j]==low: 158 222 break 159 223 m_arr[j] = matches 160 224 s_arr[j] = i-l 161 #find the new lowest value :225 #find the new lowest value we have: 162 226 low = matches 163 227 for j in range(MAX_MATCHES): 164 228 if m_arr[j]<low: … … 165 229 low = m_arr[j] 166 230 if low==0: 167 231 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))170 232 #first collect the list of distances sorted by highest number of matches: 171 233 #(there can be more than one distance value for each match count): 172 234 scroll_hits = {} … … 178 240 #return a dict with the scroll distance as key, 179 241 #and the list of matching lines in a dictionary: 180 242 # {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: 206 272 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 211 276 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 216 285 cdef char swap = 0 217 286 if distance<0: 218 287 #swap order: 219 288 swap = 1 220 a1 = self.a2289 a1 = tmp 221 290 a2 = self.a1 222 291 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) 226 293 cdef uint16_t i, start = 0, count = 0 227 294 line_defs = collections.OrderedDict() 228 for i in range(self. l-distance):295 for i in range(self.height-distance): 229 296 #if DEBUG: 230 297 # log("%i: a1=%i / a2=%i", i, a1[i], a2[i+distance]) 231 298 if a1[i]!=0 and a1[i]==a2[i+distance]: … … 250 317 if count>0: 251 318 #last few lines ended as a match: 252 319 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) 255 322 return line_defs 256 323 257 324 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 258 355 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 260 359 cdef int d = 0 261 360 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. l266 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 267 366 268 367 def __dealloc__(self): 368 self.free() 369 370 def free(self): 269 371 cdef void* ptr = <void*> self.distances 270 372 if ptr: 271 373 self.distances = NULL … … 278 380 if ptr: 279 381 self.a2 = NULL 280 382 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
14 14 from xpra.codecs.codec_constants import TransientCodecException, RGB_FORMATS, PIXEL_SUBSAMPLING 15 15 from xpra.server.window.window_source import WindowSource, STRICT_MODE, AUTO_REFRESH_SPEED, AUTO_REFRESH_QUALITY 16 16 from xpra.server.window.region import rectangle, merge_all #@UnresolvedImport 17 from xpra.server.window.motion import scroll_distances, CRC_Image#@UnresolvedImport17 from xpra.server.window.motion import ScrollData #@UnresolvedImport 18 18 from xpra.server.window.video_subregion import VideoSubregion, VIDEO_SUBREGION 19 19 from xpra.server.window.video_scoring import get_pipeline_score 20 20 from xpra.codecs.loader import PREFERED_ENCODING_ORDER, EDGE_ENCODING_ORDER … … 1425 1425 #overriden so we can invalidate the scroll data: 1426 1426 #log.error("make_draw_packet%s", (x, y, w, h, coding, "..", outstride, client_options) 1427 1427 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: 1431 1430 if client_options.get("scaled_size") or client_options.get("quality", 100)<20: 1432 #don't scroll low quality content, better to refresh it1431 #don't scroll very low quality content, better to refresh it 1433 1432 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 s elf.scroll_data = None1433 sd.free() 1435 1434 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) 1453 1436 return packet 1454 1437 1455 1438 1456 def encode_scrolling(self, image, distances, options):1439 def encode_scrolling(self, image, options={}): 1457 1440 start = time.time() 1458 1441 try: 1459 1442 del options["av-sync"] … … 1461 1444 pass 1462 1445 #tells make_data_packet not to invalidate the scroll data: 1463 1446 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)) 1465 1448 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() 1468 1450 scrolllog(" will send scroll data=%s, non-scroll=%s", raw_scroll, non_scroll) 1469 1451 flush = len(non_scroll) 1470 1452 assert raw_scroll, "failed to detect scroll values" … … 1573 1555 x, y, w, h = image.get_geometry()[:4] 1574 1556 src_format = image.get_pixel_format() 1575 1557 stride = image.get_rowstride() 1576 img_data = None1577 1558 if self.pixel_format!=src_format: 1578 1559 videolog.warn("image pixel format changed from %s to %s", self.pixel_format, src_format) 1579 1560 self.pixel_format = src_format … … 1585 1566 rgb_format = image.get_pixel_format() #ie: BGRA 1586 1567 stride = image.get_rowstride() 1587 1568 img = Image.frombuffer("RGBA", (w, h), memoryview_to_bytes(img_data), "raw", rgb_format.replace("BGRX", "BGRA"), stride) 1569 img.convert("RGB") 1588 1570 kwargs = {} 1589 1571 if SAVE_VIDEO_FRAMES=="jpeg": 1590 1572 kwargs = { … … 1597 1579 videolog("do_present_fbo: saving %4ix%-4i pixels, %7i bytes to %s", w, h, (stride*h), filename) 1598 1580 img.save(filename, SAVE_VIDEO_FRAMES, **kwargs) 1599 1581 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") 1622 1599 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) 1641 1609 1642 1610 def video_fallback(): 1643 1611 videolog.warn("using non-video fallback encoding")