xpra icon
Bug tracker and wiki

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


Ticket #620: encoding-scoring-v2.patch

File encoding-scoring-v2.patch, 17.4 KB (added by Antoine Martin, 7 years ago)

choose the encoding using a scoring mechanism

  • xpra/server/window_source.py

     
    150150        #just here for clarity:
    151151        self._current_quality = 50
    152152        self._current_speed = 50
     153        self._want_alpha = False
    153154        self._lossless_threshold_base = 85
    154155        self._lossless_threshold_pixel_boost = 20
    155156        self._small_as_rgb = MAX_PIXELS_PREFER_RGB
     
    388389        self.update_encoding_options()
    389390
    390391    def update_encoding_options(self, force_reload=False):
    391         #choose which encoding options methods to use:
    392         want_alpha = self.is_tray or (self.has_alpha and self.supports_transparency)
    393         if self.strict or self.encoding=="png/L":
    394             if self.encoding=="rgb":
    395                 #choose between rgb32 and rgb24 already
    396                 #as alpha support does not change without going through this code
    397                 if want_alpha and "rgb32" in self.common_encodings:
    398                     self.get_encoding_options = self.get_rgb32_options
    399                 else:
    400                     assert "rgb24" in self.common_encodings
    401                     self.get_encoding_options = self.get_rgb24_options
    402             else:
    403                 self.get_encoding_options = self.get_strict_options
    404         elif want_alpha:
    405             #always use transparent encoding for tray or for transparent windows:
    406             self.get_encoding_options = self.get_transparent_encoding_options
    407         else:
    408             self.get_encoding_options = self.get_encoding_options_default
     392        self._want_alpha = self.is_tray or (self.has_alpha and self.supports_transparency)
    409393        self._lossless_threshold_base = min(95, 75+self._current_speed/5)
    410394        self._lossless_threshold_pixel_boost = 20
    411395        #calculate the threshold for using rgb
     
    413397        smult = max(0.25, (self._current_speed-50)/5.0)
    414398        qmult = max(0, self._current_quality/20.0)
    415399        self._small_as_rgb = int(MAX_PIXELS_PREFER_RGB * smult * qmult * (1 + int(self.is_OR)*2))
     400        self.get_best_encoding = self.get_best_encoding_impl()
    416401
    417     def get_rgb32_options(self, *args):
    418         return ["rgb32"]
     402    def get_best_encoding_impl(self):
     403        #choose which encoding options methods to use:
     404        #first the easy ones (when there is no choice):
     405        if self.strict or self.encoding=="png/L":
     406            if self.encoding=="rgb":
     407                #choose between rgb32 and rgb24 already
     408                #as alpha support does not change without going through this code
     409                if self._want_alpha and "rgb32" in self.common_encodings:
     410                    return self.encoding_is_rgb32
     411                else:
     412                    assert "rgb24" in self.common_encodings
     413                    return self.encoding_is_rgb24
     414            return self.get_strict_encoding
     415        if self._want_alpha:
     416            if self.encoding in ("png/P", "png/L", "png", "webp"):
     417                #chosen encoding does alpha, stick to it:
     418                #(prevents alpha bleeding as different encoders may encode it differently)
     419                return self.get_strict_encoding
     420            #TODO: choose an alpha encoding and keep it?
     421            #TODO: stick to specified encoding when non-video
     422        return self.get_encoding_by_score
    419423
    420     def get_rgb24_options(self, *args):
    421         return ["rgb24"]
    422424
    423     def get_strict_options(self, *args):
    424         return [self.encoding]
     425    def encoding_is_rgb32(self, *args):
     426        return "rgb32"
    425427
    426     def get_transparent_encoding_options(self, batching, pixel_count, ww, wh, speed, quality, current_encoding):
    427         current_encoding = {"rgb" : "rgb32"}.get(current_encoding, current_encoding)
    428         WITH_ALPHA = ["rgb32", "png", "rgb24"]
    429         if current_encoding in ("png/P", "png/L"):
    430             #only allow the use of those encodings for transparency
    431             #if they are already selected as current encoding
    432             WITH_ALPHA.append(current_encoding)
    433         #webp is shockingly bad at high res and low speed:
    434         max_webp = 1024*1024*(200-quality)/100*speed/100
    435         can_webp = 16384<pixel_count<max_webp
    436         if can_webp:
    437             WITH_ALPHA.append("webp")
    438         options = []
    439         if current_encoding in WITH_ALPHA:
    440             #current selection is valid
    441             options.append(current_encoding)
    442         if (speed>=75 and quality>=75) or pixel_count<=MAX_PIXELS_PREFER_RGB:
    443             #rgb is the fastest, and also worth using on small regions:
    444             options.append("rgb32")
    445         if not self.is_tray and can_webp:
    446             #see note above about webp
    447             options.append("webp")
    448         if speed<75:
    449             #png is a bit slow!
    450             options.append("png")
    451         #add fallback options:
    452         v = options+WITH_ALPHA
    453         if current_encoding in WITH_ALPHA:
    454             #current selection is valid:
    455             options.insert(0, current_encoding)
    456         #log.info("transparent_encoding_options%s=%s", (current_encoding, pixel_count, speed, quality), v)
    457         return v
     428    def encoding_is_rgb24(self, *args):
     429        return "rgb24"
    458430
    459     def get_encoding_options_default(self, batching, pixel_count, ww, wh, speed, quality, current_encoding):
    460         current_encoding = {"rgb" : "rgb24"}.get(current_encoding, current_encoding)
    461         if current_encoding in ("rgb24", "rgb32", "png"):
    462             #the user has selected a lossless encoding:
    463             quality = 100
    464         options = []
    465         #use sliding scale for lossless threshold
    466         #(high speed favours switching to lossy sooner)
    467         #take into account how many pixels need to be encoder:
    468         #fewer pixels means we switch to lossless more easily
    469         lossless_q = self._lossless_threshold_base + self._lossless_threshold_pixel_boost * pixel_count / (ww*wh)
    470         #avoid large areas (too slow), especially at low speed and high quality:
    471         max_webp = 1024*1024 * (200-quality)/100 * speed/100
    472         #log.info("get_encoding_options%s lossless_q=%s, smult=%s, max_rgb=%s, max_webp=%s", (batching, pixel_count, ww, wh, speed, quality, current_encoding), lossless_q, smult, max_rgb, max_webp)
    473         if quality<lossless_q:
    474             #add lossy options
    475             ALL_OPTIONS = ["jpeg", "png/P", "png/L", "rgb24"]
    476             if pixel_count<self._small_as_rgb:
    477                 #high speed and high quality, rgb is still good
    478                 options.append("rgb24")
    479             if speed>20:
    480                 #at medium to high speed, jpeg is always good
    481                 options.append("jpeg")
    482             if speed>30 and 16384<pixel_count<max_webp:
    483                 #medium speed: webp compresses well (just a bit slow)
    484                 options.append("webp")
    485         else:
    486             #lossless options:
    487             ALL_OPTIONS = ["png", "rgb24"]
    488             if speed>75 or pixel_count<self._small_as_rgb:
    489                 #high speed, rgb is very good:
    490                 options.append("rgb24")
    491             if 16384<pixel_count<max_webp:
    492                 #don't enable webp for "true" lossless (q>99) unless speed is high enough
    493                 #because webp forces speed=100 for true lossless mode
    494                 #also avoid very small and very large areas (both slow)
    495                 if quality<100 or speed>=50:
    496                     options.append("webp")
    497             #always allow png
    498             options.append("png")
    499         if current_encoding in ALL_OPTIONS:
    500             #current selection is valid:
    501             options.insert(0, current_encoding)
    502         return options+[x for x in ALL_OPTIONS if x not in options]
     431    def get_strict_encoding(self, *args):
     432        return self.encoding
    503433
    504434
    505     def get_best_encoding(self, batching, pixel_count, ww, wh, speed, quality, current_encoding, fallback=[]):
    506         options = self.get_encoding_options(batching, pixel_count, ww, wh, speed, quality, current_encoding)
    507         if current_encoding is None:
    508             #add all the fallbacks so something will match
    509             options += [x for x in fallback+self.common_encodings if x is not None and x not in options]
    510         e = self.do_get_best_encoding(options, current_encoding, fallback)
    511         return e or self.common_encodings[0]
     435    def get_encoding_by_score(self, pixel_count, ww, wh, speed, quality, current_encoding=None, default_scores={}):
     436        #lossless_q = self._lossless_threshold_base + self._lossless_threshold_pixel_boost * pixel_count / (ww*wh)
     437        #TODO: only assume good speed for rgb if we have lz4!
     438        #webp is very bad at certain sizes: very big (slow) and very small (inefficient)
     439        max_webp = 1024*1024*(200-quality)/100*speed/100
     440        scores = {
     441                  #has alpha, bad for speed, good for quality (lossless)
     442                  "png"     : self._want_alpha*500 - speed + quality*2,
     443                  #no alpha, avoid at low speed, but very good at high speed
     444                  "jpeg"    : -self._want_alpha*500 + 3*(speed-20) - quality,
     445                  #has alpha, good speed and quality
     446                  "rgb32"   : -self._want_alpha*500 + 3*(speed-20) - quality,
     447                  #no alpha, good speed and quality
     448                  "rgb24"   : -self._want_alpha*500 + 2*speed + quality + (pixel_count<self._small_as_rgb)*100,
     449                  #has alpha, ok for speed, good for quality (lossless)
     450                  # for "true" lossless (q>99), avoid unless the speed is high enough
     451                  #because webp forces speed=100 for true lossless mode
     452                  "webp"    : self._want_alpha*500 + quality*2 - (quality>=100)*(100-speed) - (not bool(16384<pixel_count<max_webp)) * 1000
     453                  }
     454        for e in (x for x in self.common_encodings if x in scores.keys()):
     455            scores[e] = default_scores.get(e, 0) + scores.get(e) + (current_encoding==e)*50
     456        #log("get_encoding_by_score%s scores=%s", (self, pixel_count, ww, wh, speed, quality, current_encoding, default_scores), scores)
     457        winner = scores[sorted(scores)[-1]]
     458        return [e for e,s in scores.items() if s==winner][0]
    512459
    513     def do_get_best_encoding(self, options, current_encoding, fallback=[]):
    514         #non-video encodings: stick to what we have if we can:
    515         if current_encoding in options:
    516             return current_encoding
    517         return self.pick_encoding(options + fallback)
     460    def get_refresh_encoding(self, pixel_count, ww, wh, speed, quality, current_encoding):
     461        default_scores = {}
     462        for e in self.auto_refresh_encodings:
     463            default_scores[e] = 100
     464        return self.get_encoding_by_score(pixel_count, ww, wh, speed, quality, current_encoding, default_scores)
    518465
    519     def pick_encoding(self, encodings):
    520         """ choose an encoding from the list, or use the fallback """
    521         for encoding in (e for e in encodings if e in self.common_encodings):
    522             return encoding
    523         log.warn("cannot find an encoding to use! (from %s, common=%s)" % (encodings, self.common_encodings))
    524         #slow fallback
    525         e = self.common_encodings
    526         if e=="rgb":
    527             #TODO: rgb24 vs rgb32
    528             e = "rgb24"
    529         return e
    530466
    531 
    532467    def unmap(self):
    533468        self.cancel_damage()
    534469        self.statistics.reset()
     
    758693            if actual_encoding is None:
    759694                q = options.get("quality") or self._current_quality
    760695                s = options.get("speed") or self._current_speed
    761                 actual_encoding = self.get_best_encoding(False, w*h, ww, wh, s, q, self.encoding)
     696                actual_encoding = self.get_best_encoding(w*h, ww, wh, s, q, self.encoding)
    762697            if self.must_encode_full_frame(window, actual_encoding):
    763698                x, y = 0, 0
    764699                w, h = ww, wh
     
    905840        window.acknowledge_changes()
    906841        self.do_send_delayed_regions(damage_time, window, regions, coding, options)
    907842
    908     def do_send_delayed_regions(self, damage_time, window, regions, coding, options, exclude_region=None, fallback=[]):
     843    def do_send_delayed_regions(self, damage_time, window, regions, coding, options, exclude_region=None, get_best_encoding=None):
    909844        ww,wh = window.get_dimensions()
    910845        speed = options.get("speed") or self._current_speed
    911846        quality = options.get("quality") or self._current_quality
     847        get_best_encoding = get_best_encoding or self.get_best_encoding
    912848        def get_encoding(pixel_count):
    913             return self.get_best_encoding(True, pixel_count, ww, wh, speed, quality, coding, fallback)
     849            return get_best_encoding(pixel_count, ww, wh, speed, quality, coding)
    914850
    915851        def send_full_window_update():
    916852            actual_encoding = get_encoding(ww*wh)
     
    11641100            now = time.time()
    11651101            refreshlog("timer_full_refresh() after %ims, regions=%s", 1000.0*(time.time()-ret), regions)
    11661102            #choose an encoding:
    1167             ww, wh = window.get_dimensions()
    11681103            encoding = self.auto_refresh_encodings[0]
    1169             encodings = self.get_encoding_options(False, ww*wh, ww, wh, AUTO_REFRESH_SPEED, AUTO_REFRESH_QUALITY, encoding)
    1170             refresh_encodings = [x for x in self.auto_refresh_encodings if x in encodings]
    1171             if refresh_encodings:
    1172                 encoding = refresh_encodings[0]
    11731104            options = self.get_refresh_options()
    1174             WindowSource.do_send_delayed_regions(self, now, window, regions, encoding, options, exclude_region=self.get_refresh_exclude())
     1105            WindowSource.do_send_delayed_regions(self, now, window, regions, encoding, options, exclude_region=self.get_refresh_exclude(), get_best_encoding=self.get_refresh_encoding)
    11751106        return False
    11761107
    11771108    def get_refresh_exclude(self):
  • xpra/server/window_video_source.py

     
    253253        self.non_video_encodings = [x for x in PREFERED_ENCODING_ORDER if x in nv_common]
    254254        log("set_client_properties(%s) csc_modes=%s, full_csc_modes=%s, video_scaling=%s, video_subregion=%s, uses_swscale=%s, non_video_encodings=%s", properties, self.csc_modes, self.full_csc_modes, self.supports_video_scaling, self.supports_video_subregion, self.uses_swscale, self.non_video_encodings)
    255255
     256    def get_best_encoding_impl(self):
     257        return self.get_best_encoding
    256258
    257     def get_best_encoding(self, batching, pixel_count, ww, wh, speed, quality, current_encoding, fallback=[]):
     259
     260    def get_best_encoding(self, pixel_count, ww, wh, speed, quality, current_encoding, fallback=[]):
    258261        """
    259262            decide whether we send a full window update using the video encoder,
    260263            or if a separate small region(s) is a better choice
     
    262265        def nonvideo(s=speed, q=quality):
    263266            s = max(0, min(100, s))
    264267            q = max(0, min(100, q))
    265             return WindowSource.get_best_encoding(self, batching, pixel_count, ww, wh, s, q, current_encoding, fallback)
     268            return WindowSource.get_encoding_by_score(self, pixel_count, ww, wh, s, q, current_encoding)
    266269
    267270        if current_encoding not in self.video_encodings:
    268271            #not doing video, bail out:
     
    303306        factors = (max(1, (speed-75)/5.0),                      #speed multiplier
    304307                   1 + int(self.is_OR)*2,                       #OR windows tend to be static
    305308                   max(1, 10-self._sequence),                   #gradual discount the first 9 frames, as the window may be temporary
    306                    1 + int(not batching)*2,                     #if we're not batching, allow more pixels
    307309                   1.0 / (int(bool(self._video_encoder)) + 1)   #if we have a video encoder already, make it more likely we'll use it:
    308310                   )
    309311        max_nvp = int(reduce(operator.mul, factors, MAX_NONVIDEO_PIXELS))
     
    319321            return nonvideo()
    320322        return current_encoding
    321323
    322     def do_get_best_encoding(self, options, current_encoding, fallback):
    323         #video encodings: always pick from the ordered list of options
    324         #rather than sticking with the current encoding:
    325         return self.pick_encoding(options + fallback)
    326324
    327 
    328325    def unmap(self):
    329326        WindowSource.cancel_damage(self)
    330327        self.cleanup_codecs()
     
    378375        now = time.time()
    379376        encoding = self.auto_refresh_encodings[0]
    380377        options = self.get_refresh_options()
    381         WindowSource.do_send_delayed_regions(self, now, window, regions, encoding, options)
     378        WindowSource.do_send_delayed_regions(self, now, window, regions, encoding, options, get_best_encoding=self.get_refresh_encoding)
    382379
    383380    def remove_refresh_region(self, region):
    384381        #override so we can update the subregion timers / regions tracking:
     
    420417        #so we can ensure we don't use the video encoder when we don't want to:
    421418
    422419        def send_nonvideo(regions=regions, encoding=coding, exclude_region=None):
    423             WindowSource.do_send_delayed_regions(self, damage_time, window, regions, encoding, options, exclude_region=exclude_region, fallback=self.non_video_encodings)
     420            WindowSource.do_send_delayed_regions(self, damage_time, window, regions, encoding, options, exclude_region=exclude_region, get_best_encoding=self.get_encoding_by_score)
    424421
    425422        if self.is_tray:
    426423            sublog("BUG? video for tray - don't use video region!")