Coverage for /home/antoine/projects/xpra-git/dist/python3/lib64/python/xpra/server/window/video_scoring.py : 100%
Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# -*- coding: utf-8 -*-
2# This file is part of Xpra.
3# Copyright (C) 2013-2020 Antoine Martin <antoine@xpra.org>
4# Xpra is released under the terms of the GNU GPL v2, or, at your option, any
5# later version. See the file COPYING for details.
7from xpra.util import envint
8from xpra.codecs.codec_constants import LOSSY_PIXEL_FORMATS
9from xpra.log import Logger
11scorelog = Logger("score")
13GPU_BIAS = envint("XPRA_GPU_BIAS", 100)
14MIN_FPS_COST = envint("XPRA_MIN_FPS_COST", 4)
16#any colourspace convertion will lose at least some quality (due to rounding)
17#(so add 0.2 to the value we get from calculating the degradation using get_subsampling_divs)
18SUBSAMPLING_QUALITY_LOSS = {
19 "NV12" : 186,
20 "YUV420P" : 186, #1.66 + 0.2
21 "YUV422P" : 153, #1.33 + 0.2
22 "YUV444P" : 120, #1.00 + 0.2
23 }
26def get_quality_score(csc_format, csc_spec, encoder_spec, scaling,
27 target_quality : int=100, min_quality : int=0) -> int:
28 quality = encoder_spec.quality
29 div = SUBSAMPLING_QUALITY_LOSS.get(csc_format, 100)
30 quality = quality*100//div
32 if csc_spec:
33 #csc_spec.quality is the upper limit (up to 100):
34 quality += csc_spec.quality
35 quality /= 2.0
37 if scaling==(1, 1) and csc_format not in ("NV12", "YUV420P", "YUV422P") and target_quality==100 and encoder_spec.has_lossless_mode:
38 #we want lossless!
39 qscore = quality + 80
40 else:
41 #how far are we from the current quality heuristics?
42 qscore = 100-abs(target_quality - quality)
43 if min_quality>=quality:
44 #if this encoder's quality is lower than the min_quality
45 #then it isn't very suitable, discount its score:
46 mqs = (min_quality - quality) // 2
47 qscore = max(0, qscore - mqs)
48 #when downscaling, YUV420P should always win:
49 if csc_format in ("YUV420P", "NV12") and scaling!=(1, 1):
50 qscore *= 2.0
51 return int(qscore)
53def get_speed_score(csc_format, csc_spec, encoder_spec, scaling,
54 target_speed : int=100, min_speed : int=0) -> int:
55 #when subsampling, add the speed gains to the video encoder
56 #which now has less work to do:
57 mult = {
58 "NV12" : 100,
59 "YUV420P" : 100,
60 "YUV422P" : 80,
61 }.get(csc_format, 60)
62 #score based on speed:
63 speed = int(encoder_spec.speed*mult//100)
64 #the encoder speed matters less
65 #when the target speed is low:
66 ts = min(100, max(1, target_speed))
67 sscore = (50-ts//2) + speed*100//(100+ts)
68 if csc_spec:
69 #if there is a csc step,
70 #then we lose some performance,
71 #but less if the csc is fast
72 sscore = sscore - 20 - (100-csc_spec.speed)//2
73 #when already downscaling, favour YUV420P subsampling:
74 if csc_format in ("YUV420P", "NV12") and scaling!=(1, 1):
75 sscore += 25
76 if min_speed>=speed:
77 #if this encoder's speed is lower than the min_speed
78 #then it isn't very suitable, discount its score:
79 mss = (min_speed - speed) // 2
80 sscore = max(0, sscore - mss)
81 return max(0, min(100, sscore))
83def get_pipeline_score(enc_in_format, csc_spec, encoder_spec,
84 width : int, height : int, scaling,
85 target_quality : int, min_quality : int,
86 target_speed : int, min_speed : int,
87 current_csce, current_ve,
88 score_delta : int, ffps : int, detection=True):
89 """
90 Given an optional csc step (csc_format and csc_spec), and
91 and a required encoding step (encoder_spec and width/height),
92 we calculate a score of how well this matches our requirements:
93 * our quality target "self._currend_quality"
94 * our speed target "self._current_speed"
95 * how expensive it would be to switch to this pipeline option
96 Note: we know the current pipeline settings, so the "switching
97 cost" will be lower for pipelines that share components with the
98 current one.
100 Can be called from any thread.
101 """
102 def clamp(v):
103 return max(0, min(100, v))
104 qscore = clamp(get_quality_score(enc_in_format, csc_spec, encoder_spec, scaling, target_quality, min_quality))
105 sscore = clamp(get_speed_score(enc_in_format, csc_spec, encoder_spec, scaling, target_speed, min_speed))
107 #multiplier for setup_cost:
108 #(lose points if we have less than N fps)
109 setup_cost_mult = int(detection)*(1+max(0, MIN_FPS_COST-ffps))
111 #how well the codec deals with larger screen sizes:
112 sizescore = 100
113 pixels = width*height
114 if scaling!=(1, 1):
115 n, d = scaling
116 pixels = pixels*n*n//d//d
117 if pixels>=1048576:
118 #high size efficiency means sizescore stays high even with high number of mpixels,
119 #ie: 1MPixels -> sizescore = 100
120 #ie: 8MPixels -> sizescore = size_efficiency
121 sdisc = 100-encoder_spec.size_efficiency
122 sizescore = max(0, 100-pixels*sdisc//1048576//4)
124 #runtime codec adjustements:
125 runtime_score = 100
126 #score for "edge resistance" via setup cost:
127 ecsc_score = 100
129 csc_width = 0
130 csc_height = 0
131 if csc_spec:
132 #OR the masks so we have a chance of making it work
133 width_mask = csc_spec.width_mask & encoder_spec.width_mask
134 height_mask = csc_spec.height_mask & encoder_spec.height_mask
135 csc_width = width & width_mask
136 csc_height = height & height_mask
137 if enc_in_format=="RGB":
138 #converting to "RGB" is often a waste of CPU
139 #(can only get selected because the csc step will do scaling,
140 # but even then, the YUV subsampling are better options)
141 ecsc_score = 1
142 elif current_csce is None or current_csce.get_dst_format()!=enc_in_format or \
143 type(current_csce)!=csc_spec.codec_class or \
144 current_csce.get_src_width()!=csc_width or current_csce.get_src_height()!=csc_height:
145 #if we have to change csc, account for new csc setup cost:
146 ecsc_score = max(0, 80 - int(csc_spec.setup_cost*setup_cost_mult*80//100))
147 else:
148 ecsc_score = 80
149 ecsc_score += csc_spec.score_boost
150 runtime_score *= csc_spec.get_runtime_factor()
152 csc_scaling = scaling
153 encoder_scaling = (1, 1)
154 if scaling!=(1,1) and not csc_spec.can_scale:
155 #csc cannot take care of scaling, so encoder will have to:
156 encoder_scaling = scaling
157 csc_scaling = (1, 1)
158 if scaling!=(1, 1):
159 #if we are (down)scaling, we should prefer lossy pixel formats:
160 v = LOSSY_PIXEL_FORMATS.get(enc_in_format, 1)
161 qscore *= (v/2)
162 enc_width, enc_height = get_encoder_dimensions(encoder_spec, csc_width, csc_height, scaling)
163 else:
164 #not using csc at all!
165 ecsc_score = 100
166 width_mask = encoder_spec.width_mask
167 height_mask = encoder_spec.height_mask
168 enc_width = width & width_mask
169 enc_height = height & height_mask
170 csc_scaling = None
171 encoder_scaling = scaling
173 if encoder_scaling!=(1,1) and not encoder_spec.can_scale:
174 #we need the encoder to scale but it cannot do it, fail it:
175 scorelog("scaling (%s) not supported by %s", encoder_scaling, encoder_spec)
176 return None
178 if enc_width<encoder_spec.min_w or enc_height<encoder_spec.min_h:
179 scorelog("video size %ix%i out of range for %s, min %ix%i", enc_width, enc_height, encoder_spec.codec_type, encoder_spec.min_w, encoder_spec.min_h)
180 return None
181 elif enc_width>encoder_spec.max_w or enc_height>encoder_spec.max_h:
182 scorelog("video size %ix%i out of range for %s, max %ix%i", enc_width, enc_height, encoder_spec.codec_type, encoder_spec.max_w, encoder_spec.max_h)
183 return None
185 ee_score = 100
186 if current_ve is None or current_ve.get_type()!=encoder_spec.codec_type or \
187 current_ve.get_src_format()!=enc_in_format or \
188 current_ve.get_width()!=enc_width or current_ve.get_height()!=enc_height:
189 #account for new encoder setup cost:
190 ee_score = 100 - int(encoder_spec.setup_cost*setup_cost_mult)
191 ee_score += encoder_spec.score_boost
192 #edge resistance score: average of csc and encoder score:
193 er_score = (ecsc_score + ee_score) // 2
194 #gpu vs cpu
195 gpu_score = max(0, GPU_BIAS-50)*encoder_spec.gpu_cost//50
196 cpu_score = max(0, 50-GPU_BIAS)*encoder_spec.cpu_cost//50
197 score = int((qscore+sscore+er_score+sizescore+score_delta+gpu_score+cpu_score)*runtime_score//100//5)
198 scorelog("get_pipeline_score(%-7s, %-24r, %-24r, %5i, %5i) quality: %3i, speed: %3i, setup: %4i - %4i runtime: %3i scaling: %s / %s, encoder dimensions=%sx%s, sizescore=%3i, client score delta=%3i, cpu score=%3i, gpu score=%3i, score=%3i",
199 enc_in_format, csc_spec, encoder_spec, width, height,
200 qscore, sscore, ecsc_score, ee_score, runtime_score, scaling, encoder_scaling, enc_width, enc_height, sizescore, score_delta,
201 cpu_score, gpu_score, score)
202 return score, scaling, csc_scaling, csc_width, csc_height, csc_spec, enc_in_format, encoder_scaling, enc_width, enc_height, encoder_spec
204def get_encoder_dimensions(encoder_spec, width : int, height : int, scaling=(1,1)):
205 """
206 Given a csc and encoder specs and dimensions, we calculate
207 the dimensions that we would use as output.
208 Taking into account:
209 * applications can require scaling (see "scaling" attribute)
210 * we scale fullscreen and maximize windows when at high speed
211 and low quality.
212 * we do not bother scaling small dimensions
213 * the encoder may not support all dimensions
214 (see width and height masks)
215 """
216 v, u = scaling
217 enc_width = int(width * v / u) & encoder_spec.width_mask
218 enc_height = int(height * v / u) & encoder_spec.height_mask
219 return enc_width, enc_height