From 6abfae00757cfb30b95aedc43b0e08a9c6535b4f Mon Sep 17 00:00:00 2001 From: Antoine Martin Date: Sun, 20 Nov 2016 16:57:02 +0000 Subject: [PATCH] #1232: * make it easier to debug opengl paints: add XPRA_OPENGL_SAVE_BUFFERS to save the current contents of the FBO to file * keep track of the last time we used scroll encoding * don't use aggressive automatic scaling or b-frames when we have used scroll encoding recently * if the window contents have been damaged again, zero out the checksum for those lines so we don't try to scroll them - but can still scroll the rest git-svn-id: https://xpra.org/svn/Xpra/trunk@14465 3bb7dfac-3a0b-4e04-842a-767bc560f471 --- src/xpra/client/gl/gl_window_backing_base.py | 45 +++++++++-- src/xpra/server/window/motion.pyx | 18 +++-- src/xpra/server/window/window_source.py | 6 +- src/xpra/server/window/window_video_source.py | 74 +++++++++++++++---- 4 files changed, 115 insertions(+), 28 deletions(-) diff --git a/src/xpra/client/gl/gl_window_backing_base.py b/src/xpra/client/gl/gl_window_backing_base.py index c24979848a..52596048e0 100644 --- a/src/xpra/client/gl/gl_window_backing_base.py +++ b/src/xpra/client/gl/gl_window_backing_base.py @@ -17,6 +17,11 @@ SCROLL_ENCODING = envbool("XPRA_SCROLL_ENCODING", True) PAINT_FLUSH = envbool("XPRA_PAINT_FLUSH", True) +SAVE_BUFFERS = os.environ.get("XPRA_OPENGL_SAVE_BUFFERS") +if SAVE_BUFFERS not in ("png", "jpeg"): + log.warn("invalid value for XPRA_OPENGL_SAVE_BUFFERS: must be 'png' or 'jpeg'") + SAVE_BUFFERS = None + from xpra.gtk_common.gtk_util import color_parse, is_realized @@ -439,6 +444,8 @@ def clear_fbo(): log("glClear error", exc_info=True) log.warn("Warning: failed to clear FBO") log.warn(" %r", e) + if getattr(e, "err", None)==1286: + raise Exception("OpenGL error '%r' likely caused by buggy drivers" % e) # Define empty tmp FBO glBindTexture(GL_TEXTURE_RECTANGLE_ARB, self.textures[TEX_TMP_FBO]) @@ -609,6 +616,9 @@ def present_fbo(self, x, y, w, h, flush=0): self.do_present_fbo() def do_present_fbo(self): + bw, bh = self.size + ww, wh = self.render_size + self.gl_marker("Presenting FBO on screen") # Change state to target screen instead of our FBO glBindFramebuffer(GL_FRAMEBUFFER, 0) @@ -623,8 +633,6 @@ def do_present_fbo(self): # Draw FBO texture on screen self.set_rgb_paint_state() - bw, bh = self.size - ww, wh = self.render_size rect_count = len(self.pending_fbo_paint) if self.glconfig.is_double_buffered() or bw!=ww or bh!=wh: @@ -644,6 +652,33 @@ def do_present_fbo(self): glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE) + if SAVE_BUFFERS: + glBindFramebuffer(GL_READ_FRAMEBUFFER, self.offscreen_fbo) + glBindTexture(GL_TEXTURE_RECTANGLE_ARB, self.textures[TEX_FBO]) + glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_RECTANGLE_ARB, self.textures[TEX_FBO], 0) + glReadBuffer(GL_COLOR_ATTACHMENT0) + glViewport(0, 0, bw, bh) + from OpenGL.GL import glGetTexImage + size = bw*bh*4 + import numpy + data = numpy.empty(size) + img_data = glGetTexImage(GL_TEXTURE_RECTANGLE_ARB, 0, GL_BGRA, GL_UNSIGNED_BYTE, data) + from PIL import Image, ImageOps + img = Image.frombuffer("RGBA", (bw, bh), img_data, "raw", "BGRA", bw*4) + img = ImageOps.flip(img) + kwargs = {} + if SAVE_BUFFERS=="jpeg": + kwargs = { + "quality" : 0, + "optimize" : False, + } + t = time.time() + tstr = time.strftime("%H-%M-%S", time.localtime(t)) + filename = "./W%i-FBO-%s.%03i.%s" % (self.wid, tstr, (t*1000)%1000, SAVE_BUFFERS) + log("do_present_fbo: saving %4ix%-4i pixels, %7i bytes to %s", bw, bh, size, filename) + img.save(filename, SAVE_BUFFERS, **kwargs) + glBindFramebuffer(GL_READ_FRAMEBUFFER, 0) + #viewport for painting to window: glViewport(0, 0, ww, wh) if ww!=bw or wh!=bh: @@ -854,7 +889,7 @@ def do_paint_rgb(self, rgb_format, img_data, x, y, width, height, rowstride, opt fire_paint_callbacks(callbacks) except Exception as e: log("Error in %s paint of %i bytes, options=%s)", rgb_format, len(img_data), options) - fire_paint_callbacks(callbacks, False, "opengl %s paint error: %s" % (rgb_format, e)) + fire_paint_callbacks(callbacks, False, "OpenGL %s paint error: %s" % (rgb_format, e)) def do_video_paint(self, img, x, y, enc_width, enc_height, width, height, options, callbacks): #copy so the data will be usable (usually a str) @@ -890,9 +925,9 @@ def gl_paint_planar(self, flush, encoding, img, x, y, enc_width, enc_height, wid fire_paint_callbacks(callbacks, True) return except GLError as e: - message = "gl_paint_planar error: %r" % e + message = "OpenGL %s paint error: %r" % (encoding, e) except Exception as e: - message = "gl_paint_planar error: %s" % e + message = "OpenGL %s paint error: %s" % (encoding, e) log.error("%s.gl_paint_planar(..) error: %s", self, e, exc_info=True) fire_paint_callbacks(callbacks, False, message) diff --git a/src/xpra/server/window/motion.pyx b/src/xpra/server/window/motion.pyx index f0cc9e7b98..f7994d6a2b 100644 --- a/src/xpra/server/window/motion.pyx +++ b/src/xpra/server/window/motion.pyx @@ -87,7 +87,7 @@ def calculate_distances(array1, array2, int min_score=0, int max_distance=1000): cdef size_t asize = l*(sizeof(int64_t)) cdef int64_t *a1 = NULL cdef int64_t *a2 = NULL - cdef int64_t a1v = 0 + cdef int64_t a2v = 0 cdef int32_t *distances = NULL #print("calculate_distances(%s, %s, %i, %i)" % (array1, array2, elen, min_score)) try: @@ -102,18 +102,20 @@ def calculate_distances(array1, array2, int min_score=0, int max_distance=1000): assert distances!=NULL with nogil: memset( distances, 0, 2*l*sizeof(int32_t)) - for y1 in range(l): - miny = max(0, y1-max_distance) - maxy = min(l, y1+max_distance) - a1v = a1[y1] - for y2 in range(miny, maxy): - if a1v==a2[y2]: + for y2 in range(l): + miny = max(0, y2-max_distance) + maxy = min(l, y2+max_distance) + a2v = a2[y2] + if a2v==0: + continue + for y1 in range(miny, maxy): + if a1[y1]==a2v: #distance = y1-y2 distances[l+y1-y2] += 1 r = {} for i in range(2*l): d = distances[i] - if min_score<=0 or abs(d)>=min_score: + if abs(d)>=min_score: r[i-l] = d return r finally: diff --git a/src/xpra/server/window/window_source.py b/src/xpra/server/window/window_source.py index f933fd52dc..bc9892c801 100644 --- a/src/xpra/server/window/window_source.py +++ b/src/xpra/server/window/window_source.py @@ -1633,10 +1633,12 @@ def full_quality_refresh(self, damage_options={}): self.damage(0, 0, w, h, options=new_options) def get_refresh_options(self): - return {"optimize" : False, + return { + "optimize" : False, "auto_refresh" : True, #not strictly an auto-refresh, just makes sure we won't trigger one "quality" : AUTO_REFRESH_QUALITY, - "speed" : AUTO_REFRESH_SPEED} + "speed" : AUTO_REFRESH_SPEED, + } def queue_damage_packet(self, packet, damage_time=0, process_damage_time=0): """ diff --git a/src/xpra/server/window/window_video_source.py b/src/xpra/server/window/window_video_source.py index 48e1dbc302..a3dcb71178 100644 --- a/src/xpra/server/window/window_video_source.py +++ b/src/xpra/server/window/window_video_source.py @@ -119,6 +119,7 @@ def init_vars(self): self.encode_from_queue_timer = None self.encode_from_queue_due = 0 self.scroll_data = None + self.last_scroll_time = 0 def set_auto_refresh_delay(self, d): WindowSource.set_auto_refresh_delay(self, d) @@ -465,6 +466,7 @@ def cancel_damage(self): self.free_encode_queue_images() self.video_subregion.cancel_refresh_timer() self.scroll_data = None + self.last_scroll_time = 0 WindowSource.cancel_damage(self) #we must clean the video encoder to ensure #we will resend a key frame because we may be missing a frame @@ -480,6 +482,7 @@ def full_quality_refresh(self, damage_options={}): #keep the region, but cancel the refresh: self.video_subregion.cancel_refresh_timer() self.scroll_data = None + self.last_scroll_time = 0 if self.non_video_encodings: #refresh the whole window in one go: damage_options["novideo"] = True @@ -1130,6 +1133,7 @@ def calculate_scaling(self, width, height, max_w=4096, max_h=4096): q = self._current_quality s = self._current_speed actual_scaling = self.scaling + now = time.time() def get_min_required_scaling(): if width<=max_w and height<=max_h: return (1, 1) #no problem @@ -1152,7 +1156,8 @@ def get_min_required_scaling(): elif actual_scaling is None and (width>max_w or height>max_h): #most encoders can't deal with that! actual_scaling = get_min_required_scaling() - elif actual_scaling is None and not self.is_shadow and self.statistics.damage_events_count>50 and (time.time()-self.statistics.last_resized)>0.5: + elif actual_scaling is None and not self.is_shadow and self.statistics.damage_events_count>50 \ + and (now-self.statistics.last_resized>0.5) and (now-self.last_scroll_time)>5: #no scaling window attribute defined, so use heuristics to enable: if self.matches_video_subregion(width, height): ffps = self.video_subregion.fps @@ -1162,11 +1167,10 @@ def get_min_required_scaling(): else: sc = (self.scaling_control+25) else: - #no the video region, so much less aggressive scaling: + #not the video region, so much less aggressive scaling: sc = max(0, (self.scaling_control-50)//2) #calculate full frames per second (measured in pixels vs window size): ffps = 0 - now = time.time() stime = now-5 #only look at the last 5 seconds max lde = [x for x in list(self.statistics.last_damage_events) if x[0]>stime] if len(lde)>10: @@ -1409,7 +1413,7 @@ def setup_pipeline_option(self, width, height, src_format, def get_video_encoder_options(self, encoding, width, height): #tweaks for "real" video: - if self.matches_video_subregion(width, height) and self.subregion_is_video(): + if self.matches_video_subregion(width, height) and self.subregion_is_video() and (time.time()-self.last_scroll_time)>5: return { "source" : "video", #could take av-sync into account here to choose the number of b-frames: @@ -1418,8 +1422,47 @@ def get_video_encoder_options(self, encoding, width, height): return {} + def make_draw_packet(self, x, y, w, h, coding, data, outstride, client_options={}): + #overriden so we can invalidate the scroll data: + #log.error("make_draw_packet%s", (x, y, w, h, coding, "..", outstride, client_options) + packet = WindowSource.make_draw_packet(self, x, y, w, h, coding, data, outstride, client_options) + lsd = self.scroll_data + if lsd and coding!="scroll": + dec, sx, sy, sw, sh, csums = lsd + if client_options.get("scaled_size") or client_options.get("quality", 100)<20: + #don't scroll low quality content, better to refresh it + self.scroll_data = None + #if the image contents have actually changed since we collected the checksums + #(skip other parts of the same damage event, and refresh via timer): + elif dec0: