Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
totaam committed Nov 20, 2016
1 parent 81f5b36 commit 6abfae0
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 28 deletions.
45 changes: 40 additions & 5 deletions src/xpra/client/gl/gl_window_backing_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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])
Expand Down Expand Up @@ -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)
Expand All @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)

Expand Down
18 changes: 10 additions & 8 deletions src/xpra/server/window/motion.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -102,18 +102,20 @@ def calculate_distances(array1, array2, int min_score=0, int max_distance=1000):
assert distances!=NULL
with nogil:
memset(<void*> 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:
Expand Down
6 changes: 4 additions & 2 deletions src/xpra/server/window/window_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down
74 changes: 61 additions & 13 deletions src/xpra/server/window/window_video_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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 dec<self.statistics.damage_events_count:
self.scroll_data[0] = self.statistics.damage_events_count
#quick check: does the vertical position of the two rectangles intersect at all?
if y<sy<(y+h) or y<sy+sh<(y+h):
#quick check: same for horizontal
if x<sx<(x+w) or x<sx+sw<(x+w):
#it definitey intersects
#remove any lines that have been updated
#by zeroing out their checksums
rem = 0
assert len(csums)==sh
for i in range(sh):
if y<sy+i<y+h:
rem += 1
csums[i] = 0
nonzero = len([True for v in csums if v!=0])
scrolllog("removed %i lines checksums from intersection of scroll area %i+%i and draw packet %i+%i, remains %i out of %i", rem, sy, sh, y, h, nonzero, sh)
#if less then half has been invalidated.. drop it
if nonzero<=sh//2:
self.scroll_data = None
return packet


def encode_scrolling(self, image, distances, old_csums, csums, options):
tstart = time.time()
try:
del options["av-sync"]
except:
pass
scrolllog("encode_scrolling(%s, {..}, [], [], %s) window-dimensions=%s", image, options, self.window_dimensions)
x, y, w, h = image.get_geometry()[:4]
yscroll_values = []
Expand Down Expand Up @@ -1458,7 +1501,8 @@ def encode_scrolling(self, image, distances, old_csums, csums, options):
#things have actually moved
#aggregate consecutive lines into rectangles:
cl = consecutive_lines(lines)
scrolllog(" scroll groups for distance=%i : %s=%s", s, lines, cl)
#scrolllog(" scroll groups for distance=%i : %s=%s", s, lines, cl)
scrolllog(" scroll groups for distance=%i : %s", s, cl)
for start,count in cl:
#new rectangle
scrolls.append((x, y+start-s, w, count, 0, s))
Expand Down Expand Up @@ -1502,6 +1546,7 @@ def encode_scrolling(self, image, distances, old_csums, csums, options):
self.free_image_wrapper(sub)
scrolllog("non-scroll encoding took %ims", (time.time()-non_start)*1000)
assert flush==0
self.last_scroll_time = time.time()
return None

def video_fallback(self, image, options, order=PREFERED_ENCODING_ORDER):
Expand Down Expand Up @@ -1536,22 +1581,25 @@ def do_video_encode(self, encoding, image, options):
videolog.warn("image pixel format changed from %s to %s", self.pixel_format, src_format)
self.pixel_format = src_format

#check for scrolling, unless there's already a b-frame pending, or in strict mode:
# or when a true video region exists and detection is turned off
if self.supports_scrolling and not self.b_frame_flush_timer and not STRICT_MODE and (self.video_subregion.detection or not self.subregion_is_video()):
test_scrolling = self.supports_scrolling and not STRICT_MODE
if test_scrolling and self.b_frame_flush_timer:
scrolllog("not testing scrolling: b_frame_flush_timer=%s", self.b_frame_flush_timer)
test_scrolling = False
self.scroll_data = None
else:
lsd = self.scroll_data
try:
start = time.time()
lsd = self.scroll_data
pixels = image.get_pixels()
stride = image.get_rowstride()
width = image.get_width()
height = image.get_height()
csums = CRC_Image(pixels, width, height, stride)
if csums:
self.scroll_data = (width, height, csums)
scrolllog("updated scroll data")
self.scroll_data = [self.statistics.damage_events_count, x, y, width, height, csums]
scrolllog("updated scroll data, previously set: %s", bool(lsd))
if lsd and csums:
lw, lh, lcsums = lsd
lw, lh, lcsums = lsd[-3:]
if lw!=width or lh!=height:
scrolllog("scroll data size mismatch: %ix%i vs %ix%i", lw, lh, width, height)
else:
Expand Down Expand Up @@ -1743,7 +1791,7 @@ def do_flush_video_encoder(self):
client_options["scaled_size"] = scaled_size
client_options["flush-encoder"] = True
videolog("do_flush_video_encoder %s : (%s %s bytes, %s)", flush_data, len(data or ()), type(data), client_options)
packet = self.make_draw_packet(x, y, w, h, encoding, Compressed(encoding, data), 0, client_options)
packet = self.make_draw_packet(x, y, w, h, encoding, Compressed(encoding, data), 0, client_options, {})
self.queue_damage_packet(packet)
#check for more delayed frames since we want to support multiple b-frames:
if not self.b_frame_flush_timer and client_options.get("delayed", 0)>0:
Expand Down

0 comments on commit 6abfae0

Please sign in to comment.