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}