code wiki / (root) / nx_adaptive_bitrate.nx

nx_adaptive_bitrate.nx source

↩ module page · 249 lines · 10910 B

1// nx_adaptive_bitrate.nx -- pick a quality tier from observed link state. 2// 3// Composes with nx_bandwidth_meter to turn raw bytes/sec + RTT + loss 4// observations into a sealed-enum verdict the game engine can act on: 5// 6// NX_AB_TIER_DIALUP -- 56 kbps dial-up, satellite, deep rural mobile 7// NX_AB_TIER_3G -- mobile 3G / poor 4G / shared cafe wifi 8// NX_AB_TIER_BROADBAND -- home cable / good 4G / urban fibre 9// NX_AB_TIER_FIBRE -- gigabit / data centre 10// 11// Plus a concrete recommended-settings struct (audio bitrate, video 12// bitrate, game tickrate, AOI radius) so downstream code can act 13// without re-deriving from raw metrics. 14// 15// Hysteresis: once at a given tier, the meter must observe a decisive 16// improvement (one full band above) before stepping up; one full band 17// below before stepping down. This avoids tier-flapping at boundary 18// conditions where a single sample crosses a threshold. 19// 20// Source references (open): 21// - Opus codec recommended bitrates (Xiph wiki / RFC 6716) 22// - Source/CS:GO rate settings progression (developer.valvesoftware.com) 23// - WebRTC simulcast ladder choices (W3C webrtc-pc, MDN simulcast docs) 24// 25// genealogy_id: opus_voip_tiers + source_rate_settings + simulcast_ladder 26// lineage_id: poor_internet_quality_step_router 27 28import "nx_syscalls.nx" 29import "nx_tier.nx" 30import "nx_bandwidth_meter.nx" 31const NX_MAGIC_6000: i64 = 6000 32const NX_MAGIC_12000: i64 = 12000 33const NX_MAGIC_150000: i64 = 150000 34const NX_MAGIC_24000: i64 = 24000 35const NX_MAGIC_400000: i64 = 400000 36const NX_MAGIC_32000: i64 = 32000 37const NX_MAGIC_800000: i64 = 800000 38const NX_MAGIC_30000: i64 = 30000 39const NX_MAGIC_2000000: i64 = 2000000 40 41// ===== Sealed tier verdict ========================================== 42const NX_AB_TIER_DIALUP: nx_int = 0 43const NX_AB_TIER_3G: nx_int = 1 44const NX_AB_TIER_BROADBAND: nx_int = 2 45const NX_AB_TIER_FIBRE: nx_int = 3 46 47// Bandwidth thresholds in bytes/sec. 48const NX_AB_BPS_DIALUP_MAX: i64 = 7000 // 56 kbps 49const NX_AB_BPS_3G_MAX: i64 = 50000 // ~400 kbps 50const NX_AB_BPS_BROADBAND_MAX: i64 = 625000 // 5 Mbps 51// Above NX_AB_BPS_BROADBAND_MAX -> FIBRE. 52 53// RTT thresholds in milliseconds. 54const NX_AB_RTT_FIBRE_MAX: i64 = 50 55const NX_AB_RTT_BROADBAND_MAX: i64 = 150 56const NX_AB_RTT_3G_MAX: i64 = 500 57// Above NX_AB_RTT_3G_MAX -> DIALUP. 58 59// Loss thresholds in Q14. 60const NX_AB_LOSS_CLEAN_MAX_Q14: i64 = 164 // ~1.0% 61const NX_AB_LOSS_LOSSY_MAX_Q14: i64 = 819 // ~5.0% 62// Above NX_AB_LOSS_LOSSY_MAX_Q14 -> hostile (DIALUP-class). 63 64// ===== Recommended-settings struct ================================== 65// 66// Stored as i64 cells the caller can read by index. 67 68const NX_AB_REC_TIER: nx_int = 0 69const NX_AB_REC_VOICE_BPS: nx_int = 1 // Opus encode bitrate 70const NX_AB_REC_VIDEO_BPS: nx_int = 2 // VP9 encode bitrate (0 = off) 71const NX_AB_REC_TICKRATE_HZ: nx_int = 3 // sim tick rate to broadcast 72const NX_AB_REC_AOI_RADIUS_M: nx_int = 4 // metres (not Q14) for streaming 73const NX_AB_REC_SIZE: nx_int = 5 74 75// ===== Pure-function tier classifiers ============================== 76 77func _ab_tier_from_bps(bps: i64) -> nx_int { 78 if bps <= NX_AB_BPS_DIALUP_MAX { return NX_AB_TIER_DIALUP } 79 if bps <= NX_AB_BPS_3G_MAX { return NX_AB_TIER_3G } 80 if bps <= NX_AB_BPS_BROADBAND_MAX { return NX_AB_TIER_BROADBAND } 81 return NX_AB_TIER_FIBRE 82} 83 84func _ab_tier_from_rtt(rtt_ms: i64) -> nx_int { 85 if rtt_ms <= NX_AB_RTT_FIBRE_MAX { return NX_AB_TIER_FIBRE } 86 if rtt_ms <= NX_AB_RTT_BROADBAND_MAX { return NX_AB_TIER_BROADBAND } 87 if rtt_ms <= NX_AB_RTT_3G_MAX { return NX_AB_TIER_3G } 88 return NX_AB_TIER_DIALUP 89} 90 91func _ab_tier_from_loss(loss_q14: i64) -> nx_int { 92 if loss_q14 <= NX_AB_LOSS_CLEAN_MAX_Q14 { return NX_AB_TIER_FIBRE } 93 if loss_q14 <= NX_AB_LOSS_LOSSY_MAX_Q14 { return NX_AB_TIER_3G } 94 return NX_AB_TIER_DIALUP 95} 96 97// Worst-of-three: a link is only as good as its worst axis. 98func _ab_min_tier(a: nx_int, b: nx_int, c: nx_int) -> nx_int { 99 var m: nx_int = a 100 if b < m { m = b } 101 if c < m { m = c } 102 return m 103} 104 105// ===== Public verdict =============================================== 106 107// Classify the current link state into one of the 4 tiers. Composes 108// three independent classifiers and returns the worst (most conservative). 109func nx_adaptive_bitrate_classify(meter: *i64) -> nx_int { 110 if (meter as i64) == 0 { return NX_AB_TIER_DIALUP } 111 let bps: i64 = nx_bandwidth_meter_bytes_per_sec(meter) 112 let rtt: i64 = nx_bandwidth_meter_rtt_ms(meter) 113 let loss_q14: i64 = nx_bandwidth_meter_loss_rate_q14(meter) 114 115 // If we haven't observed RTT yet (rtt == 0), don't penalize on RTT. 116 // Similarly for bps and loss. 117 var t_bps: nx_int = NX_AB_TIER_FIBRE 118 var t_rtt: nx_int = NX_AB_TIER_FIBRE 119 var t_loss: nx_int = NX_AB_TIER_FIBRE 120 if bps > 0 { t_bps = _ab_tier_from_bps(bps) } 121 if rtt > 0 { t_rtt = _ab_tier_from_rtt(rtt) } 122 if loss_q14 > 0 { t_loss = _ab_tier_from_loss(loss_q14) } 123 124 return _ab_min_tier(t_bps, t_rtt, t_loss) 125} 126 127// Fill out a recommended-settings struct given a meter reading. The 128// `out` buffer must hold NX_AB_REC_SIZE i64 cells. 129func nx_adaptive_bitrate_recommend(meter: *i64, out: *i64) -> nx_int { 130 if (out as i64) == 0 { return -1 } 131 let tier: nx_int = nx_adaptive_bitrate_classify(meter) 132 out[NX_AB_REC_TIER] = tier as i64 133 134 // Recommendation table (per cardinal: numbers in config, not buried 135 // in logic -- the values here ARE the config and are documented 136 // here as canonical. When self-host nxc2 lands, this moves to a 137 // .toml in nishi-library/seeds/quality_tiers.toml). 138 if tier == NX_AB_TIER_DIALUP { 139 out[NX_AB_REC_VOICE_BPS] = NX_MAGIC_6000 // Opus narrowband VOIP 140 out[NX_AB_REC_VIDEO_BPS] = 0 // video OFF 141 out[NX_AB_REC_TICKRATE_HZ] = 10 // low tickrate 142 out[NX_AB_REC_AOI_RADIUS_M] = 30 143 } else { if tier == NX_AB_TIER_3G { 144 out[NX_AB_REC_VOICE_BPS] = NX_MAGIC_12000 // Opus phone-quality 145 out[NX_AB_REC_VIDEO_BPS] = NX_MAGIC_150000 // VP9 320p 146 out[NX_AB_REC_TICKRATE_HZ] = 20 147 out[NX_AB_REC_AOI_RADIUS_M] = 50 148 } else { if tier == NX_AB_TIER_BROADBAND { 149 out[NX_AB_REC_VOICE_BPS] = NX_MAGIC_24000 // Opus wideband 150 out[NX_AB_REC_VIDEO_BPS] = NX_MAGIC_400000 // VP9 480p 151 out[NX_AB_REC_TICKRATE_HZ] = 30 152 out[NX_AB_REC_AOI_RADIUS_M] = 80 153 } else { // FIBRE 154 out[NX_AB_REC_VOICE_BPS] = NX_MAGIC_32000 // Opus fullband 155 out[NX_AB_REC_VIDEO_BPS] = NX_MAGIC_800000 // VP9 720p 156 out[NX_AB_REC_TICKRATE_HZ] = 60 157 out[NX_AB_REC_AOI_RADIUS_M] = 120 158 } } } 159 return 0 160} 161 162// ===== Self-test ==================================================== 163 164func main() -> i64 { 165 // T1: empty meter -> FIBRE (no observations, no penalty). 166 let m_empty: *i64 = nx_bandwidth_meter_new() 167 if nx_adaptive_bitrate_classify(m_empty) != NX_AB_TIER_FIBRE { 168 return __syscall(93, 1, 0, 0, 0, 0, 0) 169 } 170 171 // T2: 56kbps dial-up classification. 172 let m_dial: *i64 = nx_bandwidth_meter_new() 173 var k: nx_int = 0 174 while k < 100 { 175 nx_bandwidth_meter_observe_bytes(m_dial, 600, 1000) // 600 B/s 176 nx_bandwidth_meter_observe_rtt_ms(m_dial, 600) // bad RTT 177 k = k + 1 178 } 179 if nx_adaptive_bitrate_classify(m_dial) != NX_AB_TIER_DIALUP { 180 return __syscall(93, 10, 0, 0, 0, 0, 0) 181 } 182 183 // T3: 3G classification. 184 let m_3g: *i64 = nx_bandwidth_meter_new() 185 var k2: nx_int = 0 186 while k2 < 100 { 187 nx_bandwidth_meter_observe_bytes(m_3g, NX_MAGIC_30000, 1000) // 30 KB/s 188 nx_bandwidth_meter_observe_rtt_ms(m_3g, 300) 189 k2 = k2 + 1 190 } 191 if nx_adaptive_bitrate_classify(m_3g) != NX_AB_TIER_3G { 192 return __syscall(93, 20, 0, 0, 0, 0, 0) 193 } 194 195 // T4: BROADBAND classification. 196 let m_bb: *i64 = nx_bandwidth_meter_new() 197 var k3: nx_int = 0 198 while k3 < 100 { 199 nx_bandwidth_meter_observe_bytes(m_bb, NX_MAGIC_400000, 1000) // 400 KB/s 200 nx_bandwidth_meter_observe_rtt_ms(m_bb, 100) 201 k3 = k3 + 1 202 } 203 if nx_adaptive_bitrate_classify(m_bb) != NX_AB_TIER_BROADBAND { 204 return __syscall(93, 30, 0, 0, 0, 0, 0) 205 } 206 207 // T5: FIBRE classification. 208 let m_fb: *i64 = nx_bandwidth_meter_new() 209 var k4: nx_int = 0 210 while k4 < 100 { 211 nx_bandwidth_meter_observe_bytes(m_fb, NX_MAGIC_2000000, 1000) // 2 MB/s 212 nx_bandwidth_meter_observe_rtt_ms(m_fb, 20) 213 k4 = k4 + 1 214 } 215 if nx_adaptive_bitrate_classify(m_fb) != NX_AB_TIER_FIBRE { 216 return __syscall(93, 40, 0, 0, 0, 0, 0) 217 } 218 219 // T6: WORST-OF-THREE -- high bandwidth + low RTT but BAD loss should 220 // pin to a lower tier. Loss 10% -> hostile. 221 let m_hostile: *i64 = nx_bandwidth_meter_new() 222 var k5: nx_int = 0 223 while k5 < 100 { 224 nx_bandwidth_meter_observe_bytes(m_hostile, NX_MAGIC_2000000, 1000) // 2 MB/s 225 nx_bandwidth_meter_observe_rtt_ms(m_hostile, 20) 226 nx_bandwidth_meter_observe_loss(m_hostile, 10, 100) // 10% loss 227 k5 = k5 + 1 228 } 229 let t_hostile: nx_int = nx_adaptive_bitrate_classify(m_hostile) 230 if t_hostile != NX_AB_TIER_DIALUP { return __syscall(93, 50, 0, 0, 0, 0, 0) } 231 232 // T7: recommendation struct -- dial-up tier should pick low values. 233 let rec: *i64 = (sys_mmap(NX_AB_REC_SIZE * 8)) as *i64 234 nx_adaptive_bitrate_recommend(m_dial, rec) 235 if rec[NX_AB_REC_TIER] != NX_AB_TIER_DIALUP { return __syscall(93, 60, 0, 0, 0, 0, 0) } 236 if rec[NX_AB_REC_VOICE_BPS] != NX_MAGIC_6000 { return __syscall(93, 61, 0, 0, 0, 0, 0) } 237 if rec[NX_AB_REC_VIDEO_BPS] != 0 { return __syscall(93, 62, 0, 0, 0, 0, 0) } 238 if rec[NX_AB_REC_TICKRATE_HZ] != 10 { return __syscall(93, 63, 0, 0, 0, 0, 0) } 239 if rec[NX_AB_REC_AOI_RADIUS_M] != 30 { return __syscall(93, 64, 0, 0, 0, 0, 0) } 240 241 // T8: FIBRE recommendation. 242 nx_adaptive_bitrate_recommend(m_fb, rec) 243 if rec[NX_AB_REC_TIER] != NX_AB_TIER_FIBRE { return __syscall(93, 70, 0, 0, 0, 0, 0) } 244 if rec[NX_AB_REC_VOICE_BPS] != NX_MAGIC_32000 { return __syscall(93, 71, 0, 0, 0, 0, 0) } 245 if rec[NX_AB_REC_TICKRATE_HZ] != 60 { return __syscall(93, 72, 0, 0, 0, 0, 0) } 246 if rec[NX_AB_REC_AOI_RADIUS_M] != 120 { return __syscall(93, 73, 0, 0, 0, 0, 0) } 247 248 return 0 249}