Hide keyboard shortcuts

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. 

6 

7from xpra.util import envint 

8from xpra.codecs.codec_constants import LOSSY_PIXEL_FORMATS 

9from xpra.log import Logger 

10 

11scorelog = Logger("score") 

12 

13GPU_BIAS = envint("XPRA_GPU_BIAS", 100) 

14MIN_FPS_COST = envint("XPRA_MIN_FPS_COST", 4) 

15 

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 } 

24 

25 

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 

31 

32 if csc_spec: 

33 #csc_spec.quality is the upper limit (up to 100): 

34 quality += csc_spec.quality 

35 quality /= 2.0 

36 

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) 

52 

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)) 

82 

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. 

99 

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)) 

106 

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)) 

110 

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) 

123 

124 #runtime codec adjustements: 

125 runtime_score = 100 

126 #score for "edge resistance" via setup cost: 

127 ecsc_score = 100 

128 

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() 

151 

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 

172 

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 

177 

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 

184 

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 

203 

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