From 0a50f9b832cef0a06c7e5219e7906aab23943c66 Mon Sep 17 00:00:00 2001 From: Greg Pomerantz Date: Fri, 30 Oct 2026 16:08:48 -0400 Subject: [PATCH] 120Hz: fix the ALooper wake-fd race (root cause of slot drops) + phase-slip-robust referee MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ROOT CAUSE of the 8-12% slot drops (uniform dec=1/0/1/0: locked, no wake seen, dirty, no render): AOSP Looper::pollInner (system/core/libutils/ Looper.cpp) drains the wake fd in the SAME epoll batch (up to 16 fds) as any ready input fd, and pollOnce returns the input's ident — the POLL_WAKE result is a local that gets dropped. ALooper_wake's byte is consumed WITHOUT PollEvent::Wake ever reaching Slint, unrecoverable. With motion events arriving 1-4 ms after the edge (always for a real 120-240 Hz finger; ~1 in 5 frames for 20 Hz adb input) the wake is lost and the locked decision (saw_wake && slot-guard) drops the dirty frame for that slot. Fix (vendor backend): a dedicated eventfd registered with the MAIN looper under ALOOPER_POLL_CALLBACK. The Looper invokes the C trampoline INSIDE pollInner (before pollOnce returns, regardless of which ident is handed back), so delivery can never be swallowed by a co-batched input response. - frame_callback: note the edge, write the eventfd, then ALooper_wake (secondary). The fd is leaked: it belongs to the main looper for the process lifetime. - trampoline: DRAINS the eventfd (level-triggered — without the drain the counter stays >0, the fd stays readable and the loop busy-spins on POLL_CALLBACK at ~10 kHz: 1812 spin iterations measured) and publishes the edge into delivered_edge. - main loop: saw_wake |= (delivered_edge newer than last seen). - DIRTY BACKSTOP: the 6 ms poll cap was never a backstop — timeout iterations have saw_wake=false so the locked decision never rendered (the 20-40 ms silences). Now: dirty && last render >= 2 slots old renders regardless of wake state. Measured on the phone (Pixel 9 Pro, 120 Hz, sltest, identical capture): buildC (before): 127 drops + 12 stalls in 1142 frames buildE (after): 8 drops + 0 stalls in 1266 frames Phase (p10-p90): 0.72-1.00 ms in all windows. Remaining 8/1266 (0.63%) are slip-and-recover pairs where the UI thread is DESCHEDULED 6-8 ms while sleeping in epoll (wakes arrive on time, cb-lat 0.5 ms) — EAS scheduling variance, no app-side contention in sltest. Referee fixes found while judging this data (tools/jank_eval.py): - NEAREST-OBSERVED-EDGE slot grid: the 120 Hz panel clock phase-slips after idle blanking (re-locks at arbitrary phase), invalidating any grid anchored at ftns[0]. An entire drag window read late=230/ dropped=24 while per-cycle tracing showed presents at edge+2.0 ms. slot_of_ftn assigns each present to the nearest observed hardware edge in a time-continuous slot domain (slip-immune: a constant delta shifts all slots, differences preserved; stall-accurate: ftn times are CLOCK_MONOTONIC). - tools/jank_capture.py: capture is INVALID unless the screen is Awake AND the keyguard is down (a run on a sleeping phone silently injects no input: 49 IT lines, 1 present). --- .../__pycache__/jank_capture.cpython-311.pyc | Bin 0 -> 27640 bytes tools/__pycache__/jank_eval.cpython-311.pyc | Bin 24897 -> 26863 bytes tools/jank_capture.py | 29 +++- tools/jank_eval.py | 49 ++++++- .../Cargo.toml | 4 + .../lib.rs | 27 +++- .../vsync.rs | 136 ++++++++++++++++-- .../i-slint-renderer-skia/wgpu_30_surface.rs | 17 ++- 8 files changed, 241 insertions(+), 21 deletions(-) create mode 100644 tools/__pycache__/jank_capture.cpython-311.pyc diff --git a/tools/__pycache__/jank_capture.cpython-311.pyc b/tools/__pycache__/jank_capture.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7d0b4c7da6db18a432c8bbd2bfb21749b26a0e4c GIT binary patch literal 27640 zcmd6QeQ;D)mS2Co{%*C@`u;+Ck`PFs4}yE#v{f|%xq?AnZ&b8s$xQY&>6AO2P;s*B%8-A;talSH zRjK56?(3HNwFJw~)Nbw5>+{~b-}l^e?>*<-bFaRilVjE3;{V&9pNV#BG=D}P>C2va z`Rb8Qqj^swXijJZt)QFHp3t(V?u3p#^(XY~$(`WX({RGTp2ibK_B5R^v8VZj8BhI; zW!8Gas@1slXKd-LqUpTS4(zE4k)!fTeYhPTYc-lr@KY}J*NI%WDI<@(bg0aypmpbd zjQV|opK@uHS~^cSk$-;LHCm8<%X|u)U{J?CQ79M%({-(2zN9-*Bs2(?OWG5~I*pq< zQ<9D;ShLDmD%i3@*wrB>bwW;7h^(3;L~d4yyftY!vqI#r2~m(0qHs-!qO1_bYeJM{ zg(zJUqAV*!`I-WG+91>cRtZ~#jc}_L z>hZ-3n*eLvwZi7JtgS^m@~HV3jq*vlVKiua-5C2dZk@kj&}gh@T;8+e6RtV`yy$M3 zyU73dg=@U%p7FR&&$xNFS7`S8n(^S<+PCqC0z3uhQ^EPI2s!5y{Vi7Ou-DB`_-1Ea z2rX*gPb9ucU?-Zqu z5RzMJvQDEk%_!B}yx%SGr!V^5yl-++a{Hww-j5ubr{)n7k(zjy*EMr7K=CPiRuca7 zyk|z(EU`j2c3G`Ft9>T2(uinoMtS)2zVj_pvvV!djNdK!`Q~OpbfIJ&P3=t`{K@8K zziUcKv+q@_b+KaJ^mJ87%9)W#Yy(cH}vdbc$)Np zNT>~2J52bzz^WGs;c{PaP599`!$Y6|rAhcn(KpL`ediIJvfJJQl0hM7T=U+E>85l| zM%~itw!iNUd|zF3C;RJGuQ;{)NG@ zv4Jrq9qPdsm5R9MJQ6xBNO{JEzNL^gYbU~=G%t!Mlz@(gWU?=0((IoW-7bMuh1H?q z{Itu@yJsZyN|(e>vyM;w6s{~q5cE1p5h}X)7YET$M9Sx+i{1%@r?i{UQPI0ZAf^w* zi;6YzgTsyM^>1Z(;|~lT8DNBuPOK1KOH0eED81-zRt63??@ISw(6f&!mp!mHH%soB zNj5TM*PDG~WMUO&jpd@Iw?3qCd#F*Hv
*Zhp1C?;v`?cYB>HZVHadnBoOcmY9f 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 literal 0 HcmV?d00001 diff --git a/tools/__pycache__/jank_eval.cpython-311.pyc b/tools/__pycache__/jank_eval.cpython-311.pyc index e520faaceadcc77b1981f244d0b70c1c3f77dcf4..14a625ebc48dc548d12cf78a9a95f8d1fc88ece2 100644 GIT binary patch literal 26863 zcmcJ2d2kzNb|)GKagyKxiWGGsv zmHoZ18z4YR_Dm{iH6Oaa{=WC!@4fH6_kH@E!a|!4*X6JO$GMYxbh`geFPY1rJ$&^K zcz8!A=uYbdPS8(qr#bekKdooKhSLW2YdmdazoyeB_FHhefc=_JoAGOyuuNJ{TRELx z=QW*Uxe|WC_%Wx`eS$yrI&EL}-L&kxLnyeWJ6$N40gD6+pi{5{77I4O62T5wDmVbk zghD`<;1ZnIxYOlAG2T`QC4dz|DPW~{wNQ3mPx*=tjo_G5N9-oA157N33SM+&;jlAfc z^16fW(Freqq1q*IG}$rn7UwNAaw zHhl2#aem;~U@z*7I!?LKv%#}&0QY$e1&>?w^1%Q zvWlqV?v=5izv~r#%;%jDc%Q_+oOO%BB|>pgy5jfneZ2#He9$-PMVC%aHM8cYUaFg{ z=ezi^i9kTCtDEGTc%W*nZ#hzr)?&@q#n(+Xu|R$@An-@1OI~bkJW{XX=)3IpU_fFF zPy5}IqrUO!z%-^nKt$M;Y_Bj>%}CtEPkIHP+t0T(u`vR~tSEwaBIxGpvW3PJ37%zH zH1PfCmwnwm2lzUSxtxjC$a|Z|n|bf#H2M-5N^Xq)E4(=Em+Cv!A=t%`i-Bpspw`aq z_nZxgR6mUJ;8_%s-cc-%PxJtfi1MLyfwBCeOKN4i^D3wf3yI%zq@{gn)@vwf3bJXA z=`-P*^l8(57b_7PvP6G8Y`UV$rv0LK!pEjN8}kT4b9+H~PO;f2b@F^ulR7gcej|EO z3?7miF)t5EJX+qni*Igj-iXf92+){M#9-ITSZ|{anZ{UFGewy)1DqG#L7F5xT3bMV zP@aq4Mt(QnMz20$!i!F@^TOeQ^bhJsGY~(! zScG68Fu_lPF7ciTwH$DfW5kn#M%gGnFLQCm)3VXDhoE^WL^j5aOK(e00+)|5 z|Ls@8pwCZ}|2DSlE2o>g7|hB7I^C~P&+W4QU#p(^WkwnA%bctqgEUZY7FKwD6W{@rbd&fvi8+`s?+B6QbF_pHk(SS(xrnHIi_q35FRoaLFBBi;B zG&d*P4x^!xQC9iHxFGu6st}+)+QZmBV5Yv zc&yV~%TtcxHxFDt@LpN$=x?2R_f-7o&C|C|-`sF(!>2V1?jMc+?)akn_PIOfZnxZN zQ7StVj$KK|F2%7c<#0W8xZWJPJ``(9IMyW{>ok;{wLXK=5bOS}fp-Vu-8Y}R_1sP8 zEoZ_}mvq!Aj=HZ?c4yQT=~Rs6U!mB#C(n~V#+P?=!JN4u>+<_b;LcNWov0_-EtrvD z5h56_>4MbC+9m5`$ex1!H}tDBLAsC8tDj`&POmOs9?Z`P^OO@SWiGEY%Vv*Y3IW>^ zem|sVjG=;1+x`q|g%s!ufV*ov4P1j5`4Ve`hzYhPH9uO9QV!)c3r*k-{C#y0-~hzc zcTlA>8(u<|VJ@r}bh2LNLYQ10{bjw@ED*@mryrI3^_^Ym>ar{F`w2qx28FFnj4j``l0alxI%je!$@U=hR;{3@~yb+BQ3# zeWeG6LUaMANc}1n0HCaf39mn0AVJ9WVjhNkQ}ue$iBD-xkf?99!lxa%B^Imkg&I?O z1z<+^OS3Jq?dp|lSEB9Te{JTqm=F^lT8h*xKPrnKU)Z+DE$SEbcb-X9Z%bBh! z5J6m{E!A!DTWq9 z$bwc9)Ep#LtD-EMaF5FCUeBl2S+evjy6|l-OPDId+$;!^pbH^!BE?K|zH|Yktw2!H$k7ZVN8(z13~d|(mmCH6d^#=Khj`L9fUh&e zXh10%SZIq}jJjjoyl3v!*vWYJtrrr`EeKzBFA<#j!toMzUD!Lm_7@j$%X_tut9!>^ zb^V8dzYbm7J620k==?7wI4Zznkx{rnz$`+2^E?+EL9R950_J+jf zoypBRXLPJZDa-TTD?Z(yzq?DULxOsfxDG!e@x8Q3boD2mT7q7w9R3w`A2W5Mw8 z-jPGYY2zqnwAhFsCQZEpHuWqU7-{FSa*9OhcT&WU0KP`q{=P_Ou}5}Xef8R_(WBpg zedhI)vodmpk^H3r@cX9U_uSp`+4&!zzt{a)Ako^NZ0*PBNE8hu0WAYb%K!+!&HjU) zC^y?b*B|Yk9h_^N>3OW@inct~Wx!1TFYQHgVn@-S{XnX0E+~~ zkPwU^q|aSpoC6m1qAy{Stj{I#=dwA=%k*)fLd4d~hP>ERE_OXJwqb5CFE&vOHMT+4 zFJlG;a)Dqv&%TPKcs4%0%&a5@LDVjDeMfx6L~Ee7E7(dw=;NMmU(J)E!4G;b@vSXK z_)DUDiY-G&eOPk4Cm5-{1RYG16qpu0L{YK2l+*>Nwz>!F0qNeIUHGf|Gi|layJ>;z zA7_a0Or)0<^C}_UeFSPmtlsOJ$1_zO@+oFb1`?^Sur@JbNpd!=pYo**o`94#NIrks zgyprrU7bT_Hiyy$D!WVTrOWucB93{Ho{e?! z5XBI3RvyInWJaVh^Gv`-vs)*Y=8AC$HxTd4AFckf{5W>3bXLZ)>%y2xPt3Id`64$e z0j+TJOa9bSnq~7-N@;#l{>w^f{+3c2Wj&}9=BCyLCd^_^o!h8^k*&f_({pIZZfI}D zNm9y88cn!kYC&x!HPqJ)G4MElc;9f}v6Ij=XZ3?5*EF*E1#Ov|KR$StI9tiK$;{-g6*yCJDQ-L?Pyh%rqIig%8WlqmusTtWXzEBhB)&ycWq z>e%6-0p1N27D;{?;f>87C@_6fOh}~pt(p%QDu|Qb3FxN+p7T(gH_iM! zq8qAqhC)M3q9;E(0h0sdR3`s1a-t&Bl;>&@C?}RJw3N~`KJl+@ZzAE7uSJEtwHeC* zBRQZ^z?-e%rL#V~A{T+AVbF~+Xdfr8&V>5q9%|ghND?7Euf?_V{gH$Pq z?(wX>;8I&XDhypXm8Gk_qqPO94-(wF+PCp)_Q^Q4EYz;Z;*-7+LC$DLE(QzX_{%<00Wx7yR^pd1cA^3(aMH@0~ znN>t!RPu@!kv+?D)C;j1B9D4K zZd$ESJ(;b66{hFl;hqB{hYn*cb9iX5M_qrMqDF=0m^Jz(Fv)B;Xhsqm>QM0pVKk`o zoW(>&uaChh0gDp&b%2H#T8$NpqQWvny@TSg5wBJNjEj8!=6zVC+JD zv#Nq1&da1|o$iah8HGf_s1My7Ogq%y5oU=|S+Iq1TTMN2MBGd?p9e^Dm(v^+goIoo z-2mgj04fuz8LF<77y{(u8Wtv+aK#Ep*GqxZ=O9k zcQA7B)3OEk!m-7g+t1%#cW2!~5h6N^qixY(toz2L`CakqxcmLhiPBodS*uuTq5q?- zqC%|g#yHT%OA_{t&`7S?5N~^TXKd#W3@A&->~piuIcLQAX~&{*VR})z{c2+CuH@ET zO2aO-h}{sY`@SEV2~pvX&K4k3yCdR`Y@0ctipRvlGgp?S{|q6}jOm$P%m6kFW3^`V zEeAtn`VWYK=sIG(*$|T|8fMwzQcSQWVx~FlFDoK|i5EPD{pH1ZD*OAE64V<*f6i=@ zFsIcr^3h1Mq_v)m7In3%Tvpqt*`qMWj(LOABGeLWf@`+85g4}A*+Up$5Yo5^20kou zJZxIoX{#Fj*yLJnR8uJoV|N+5BoNd!L6!xMmi9Be2gDsDGfVk3nYS*p5UWoc2{RK_ z#cuRaJ(m$mN&Xaj0n$dQF2q>Ky=j985Ha{v? z$Bw>R5G%ky?T#8|2WA~}j)(&b+UkYU1<&ou_s@KICO(8wit%^>Bl%LSI@W!&W;USM z*D1zzD;IAhSJPNMaRWpB9NC@1Z$q#xHo1jAx(NqEc^6R(#G28d)#JR%VZT3#&F zNPH7CF)nNj8wB(9vRn~1NMuR7@`Xo?crrGvVRNG(Y{vLE&z94epDm*qaT%K9+>($O z<}rBmyMb+PZLWG9WwzwTTzbyMw=8!HG=tW#Eo{vbVJ*;B0Dpd5lyV?!mre29Ii9JM zPPW5vh4L;b?*$uM#yVt&Y&cJ}07_!nHe-{YY2!E$OsctV|6fUt!)N)w-glAF%!?B6!^ub_UVvnJZEjaYf3<3~+O? zH?DA+Pv*g-eGc-hs=?r zF34bnw%MyXTzv>0$x47 zV6#fNL3d!Yrt8@^pB3xrP1Rfew~PYwCwqM}zxTIAkNyeQ&?A~-dFo3VE~Jb5>REud z4NJOWHUT&WX+3S2Enq~+NJMGYq^=Bq= z59-FWk=&n{BD8*$SVRdrDTa}K)SvaG^%H@#QSkaDkz_!vwj}hXji`#0Hlg#xphzlM zaVsS?1g3*b&_r{zrHww}aw~&v;&F;|lE5hf&jU!S05Y;?&hN9&YMNC?Zlj535&J47 zTZeA?n)IrV9Xdy`VyuekqpMTa!iamW0+L|W%)XSpB)TJ3GY`Gzda_LyM_seeN2S=d z`O6VKeO^VX&{c0s3HzF)eGQ@%yP!ffZDe597~vvqDGQ{(=*HQxXmIvr#j+N6(y}(r zB`nqITY`l_7Zf!{kKM4uN^aO=qtFkUq@w?|JvSZ}+zUvX_tShgfB zTNKNdl+7{I^RR-CyXIfN@%m$(#njBetUbcbJ`>p&)jxEWVf-z-KeaAyy6wEP z@#xVT<`{Rw7CScYjBkQHWTEtdbF1RqnyRRbwargQjj7`DsOhmuSKRoxfV$egCgpIx z`P}vAVrB?b=9oF|iU*a7#)P9O>1a|MO|bMA%{;eKUe8KIV;DPcaBn!sAqG=lAIE@N zc`bKT>D5KKGv%aYi|0t%OX>2tA;pupxhlj)ZYwWgnVZT>n5$t^m`OF)w8K7Y4d6ttcBV8y`qDH2Ll)+}opP+S8!tf}pu{@&4_7X0U3cXvgP-UXz8h=QBVL{)on zgi7)PK)n@w1G&P5iF}Etn0rx>`30*C>m`c$A^~QjBmTu$SAC(%q^LS+qcr99hyr^g zYX#PD{_!+70;UGdmCBOFC?$yyYO7FNNHiw-SC+Vq#%hX}H-vZz5kI07-55Z`Z5Yu6 z*?PzJmMzvX@4VrBtSdBieMull7>h#Uo3bC$k&<@%La+3ag`uc_RI$Y`pp%D~{d&Z;2%LduR}5E?Oawl=9WP zpi#6wecHMd`Vbf=rnx|A?z(fi@lPH%<-KJFPpmCYGh=HJ46aW6 z72~fYTuSbn^l!}g5=ck5EbI!GhgS&(e60vqf|sq%TT|t5DS26q^6SSq+rDs>Tqsv9 z(N0E$)`Zu}X0be%$H;53hUfC>FpIk`yx#B1GC2Y5Q6&!k zinVcQIkw3qT8<^M>!~^ZIxV!aRFzlPJ8R(YNcvmEj8T z%-&piKwh1nHq6U>kW_ctl{c&S{J28VN~1euAxL6NeGhE8Gh{i2WwDn=N4OgOr;UYx z9W9%AHp$h3QWmURY{4B9ijAxMxhLGT}!pj9q}vTmdhLA*18H#q!o~DZ24`<%yIw8@j%D&FMsJy55H{peU+8ga7hw8#oU@Jj~eDYc`+OEM#u`eRM;w8(OQk~nPiIudqk#JNr+tD zZnDLp3ls&*GGh=HhGqC5$2Ls|Fj=tI3IbQK>o}R=>NH0`QlVoLRc6-ieSSc=kkDqZ(P>yLFMB4Y1!$j^ECia@Ank;6lPe>67BUls$L+7B zog;8})O1wd$t9y7)MR9PYs#1t$;Qq$%|JgjHa(%*C&=*Dxn$DI7!kF-NUR=ASI11N z<1obgVF-m;Olo0hnQf8fO?y1X)g5Tal`Qs}n?S}jAZs>MZ5=GRYJSCTJM0sw-vu+R zT8|vNTaF5B=n+V%u0K?0>+0&tn6hg_##-Lr6)NVN_VB|824RDQ#t}X{%q+;1Wxi(* z9Mt)AO>LZyf8VToL<>A-3<;TPc}i>G`4>Zm=GL*0zPVW>@jPUMrTz@|@$BLGw2`*b zq>Z$VCgdP_Ib$miS$WO6+x1O>YD)jcLTWtNT&YBw(MQ{F*7c2{xqU2DK|TPQ@%h~J zT;!*f)4dYUGvW@WEi9Ct%oL3ttZG7fEPzkL#Sx@szf<^SzGTank^NK#ZVGBd_Y|DI zC@&;%3u%4ed|E##Nt+Ns?Zj_~s)^a6?OgSW{~qc70Dlq*cF^b*>1;(gdc302*e0cD zy;9WnQAa%Z{>~3~E{GrOnHfkqT{HV(YM$v!Rc?5!+h(f!k^mN1#>i-NW2&e$+8!N^ zZl60JF{CVoZ!G7WDIJJ!1Hk?1@M78R=kGkP zz5ym2`;(6SiUYa1Qf=*ur9EN-h?)Unn*rim7CKU`?Th>F>hJDbEJ|3KBIami?0fOS zRMmRLvOZD}U4?BjkpfjM)-iwi#^w19H&E&d>&7n$Kp9rHDth6@j@jNwcjQ9KNjkE^ ztq&_U#@ph;LU*E~Ia$#>Z-j}wE!y_WiuI%rjvrUolxlFwQJTBoMsci7Im;Dib!t^b zY|H$)r~x^|s^5Ah`b^5TIyMY_;HiXbQ?xJTTJz4(TSM{gxU_I|v25|^;)TzKe_VOD z`p0W-kKFYn8oQO6eF;}j($%A|J5^p8>wfEbENxwDVi)EQMh8-^b)UO7J#cM`A5FMw zldf9DRZHgjo!7&WFcf&Pl6hOylq#-xr|7MsIEN|?zrQ^eN)$IFiyM^U2E>K?Ug2(d zN7uyMaqgz)R$aX9*4FvJLe+!v4yC*!RgALF+oJ`k;>vfNZ#m;7@uTGIyKv$5@SV!V z>N{)RA6fJyHtkgS&P4I9WbrPgc$ZrLk32Auosp7Keg?S6w zWZn8W7wwPq&-OorLt+2y%X2SB^w8M8J&gTH$8J{Ks)(1|Ts{BtjhExc66LkY(z=EA zPd$qpZ;#&@Ulebjzq2<{+IhDpQL;BukSYS!>*tiBPPlhpc=ts-ESy}Fe)kM8QipQZ zZ5RbgNk_~Cmj@dj;+GbyKaeB)Qe|tDvd!`K*u{wDp`{Rw;cScr*$)HQ8RfZMcc*SC zL>n%#>Ki+uJKY^GOO#@k9DZy-N){{C)Vf%D=hWiSJE!mJZ=XRC8~5f#_+rgErKCQ( zD!MAZ`Uy>YWh0@z+!Fs`vRS{2qky!=VB++UllH#%6+@rm-}{qNA6o{!)Ev} zKQ<5!H9WPiFZcCJG|V9PcMKu|p1-K7j?=*3cxx*K9HVrAv(I5}NYQ<4x*mzQDWykn zFB%rRKQpP%_l~K*Doi+zCLKo=cBjhM#YSTnVxu=+B4pbcnC$~Xk)i7#bRD=C^y;q) z6OL_3$2NuCsq(e4V=;H^*o_w{^=<}oP;?TCPTUKk`m4h11~%HmR#F8o)9zUNjdhe7 z{Rt51W+m7{$+zH6IBJuQT7}(T8W7=^Myp~QfP%&R{=p9q;t}`V&FU{;YE6w&xNE@- z5P@Z2@gx%2p_AfFd(fz+J;=ng=ZoUX$U()v^*tf}y_=J_CKcmWRYeZMo4v~M#MCk8 zprm=D@{QHx^?kik&|hCIJR7NutcFUs;F>iIr3Rgg;LF=7_c0QGl0}7`Z`M4K)h4L_ z>~|a=gU)}FUH*@mg+ig|n(nMlaQ=4QZnqT}i}Ns+FpQ-_*%L6jgmNmMu<9(F8HD_F zHldWId{Rj&gv#%%*K%wsz9*GpwNUjP*w!p7+uCob(>h`OcVJstQ?<=EJfVenq53a6m`b3Me}wYmanAt)h-$N2{m^%JwZ8D6n5ep{^h$fz+hmn z(Y5I$<0buN!wb4g9H)B$Sb;ak&EZ_ulpwEIKEur*$H0;)2^M5{T$(pzRr^$aCGra^V7B0=G zVxf?^x6&ja1M^#z|TI4c>H?0V-kzELHUJ+g)mm|DoMYvsFh49v#aHh-( zSIGJRCs$xA_QPlUu?is3iU#F=-&)y4_<)V%Yb(7 zLS6Ql1AeQz+mh{WSI?uN-Hqf`OMbI#9gcvJG5C+y)#IEJIyXZi1p&^iAq_(_-$Smh zsyhyxX0)Rjd{8KO>3EQdE6rjJ282)d*MDzf*0iBV&qHCDXRe^xO^TAQ{x@3tZ9N%~ zVPr6e3ueiDnyXG+rci>C>kT-+VKekaY$KK}vv#KZw9Gl=f_P54r4cFUWeZx~AX}ig zMDUa5vupuXk=;gc*DiVefZ)9}IpkKu)aXeEU}2@NRW`tL|xfU#$OdZ{wClZ{vH zz-ZK*VWEr?ELy!Ixu%M9Srw>Y1qUF$jF2T)udq$FJ((W}g-@aX1^I1~|Ax_Lix(`l z_0m&5Ev9(qvc_|a)1o2fmK3sO$7NT@#LnLEJzHqdh1g*YII<&DFv>z7eev&%7*R#F zW@hya8Jlr_LO*sr7D&@$W4_Dj0_+~f2`k@lHn{6#jhtyq&^tLrLpfbKgwZV=$7_JL zWb{**>5}Lp&z*At+HZwD9JDt8b%1S@=?M&QLWf-y$`eWa0m9REZTU|-MADpJBu1!u z=x{;Jp@$}n>JzI!-l`f%mlpAwxtJV?{Gsy9X(>w{ObEtSf9!Y#AR}p6dGtc$Ld2AE zz-=2#UM#h+@P4ebTdNtsVi$|UzDOt%N)?wxkIolFIjrkaj*>SIUO%X;-WhiT;9hVq zns58m=Y*p(>F87(os^^}+Jk+x{WtpOZ9tYu*9-}Ibu;b-?Kk0QNjh2-M+@aQ5RoG? z7NhSt-f}4GcEv9M;Knxl+Rv=&bE0^6vUs;ryc=o-?5gjlf>tSTN@-iPKY*Y3)?JkO zi?jiZ`}(1?BI#^c=vcgX&#O2OCY%S8&V#V~!sW(x-4@;U&d#@X#!e*4YLaC&s-L`K z*`$3{-Fsm#p1Y!0Dw!Z2D%G~T&^8?$^0l>XQp$5>qY2K44~<+B%;Aux9}*|2Gb;gZ zbF5D#il@5qi%ob8Rjur>%>LR6#QloU&FB_Z!kkFz1Jx!L{MUTlP&Lj*RE#D7aq zZvv!klkUqSs{SWb+5u%IToFmzM9zfmt*zfxaPh~4iApX0KESV2=G7>(_@5}H3t$hC zjz{+i{275i2k3u9fz0FJkq1wqLRHtLIy(MqmTe8nCaLWiADmH3=xan^XdUU@c;Dvl zb#=A!TX#SaTE`ChLNHGF%4;t27h?Uq77waToVL|sHvsb_0^2lzZ5lz2Ohb=|I?o}-gE)FV3Piu7vm11n7?2MjJb8h=r z?P@-+t9?KY&1lfPHlzW-+K}|IG*{qRpl7XUV3s_#Ihhz3 zYelu$X7H-2&Oe~1f7amKNUcaIF`?=gFWhbWZ2Inn&#v67P3#y{{q$cLttiO-}iV>|Ie((=A3F+7mwa8`Rv5qqo2KS&z#sckfeLWXj*7TSgD$8 z^`*HXV>+mN@g<@8%+~slorjS`JEtpc=xCQhJm1jJfL&#tH2Fj2a2dpn9tv+08>5vR4 z`2%Siw(SMsnmgeeOc_fo1r0jedSyd9?!|q7WcZ2c4^4N2_l`fb7Dl^c7Z$k1>U##o+MBTU zCat~DSz7zKFRbgd1cw*#?@xz+dg5m%?)NIkPUWW3E7qQbwI^xqp;UcHRjE`p;a(6I zdlt9f-E_~C8+~OabxOl&+$pR52g9=`=1xR=W1DkRcWA$WpImwhId=6Ear_5FKkUXcO;!VNT^x| zXS$Vls;KzQ^ViSMPR>oj$WvJHd138?!rBExqOdVp2t7DwWjbfThn4VjUUc2tr&JCk zDhHC41G9sXo~VnuClg1FPwMFXOdXvco{KH_8E)vguer~^wAkjX(M{^E!&Fhpn-kY3 zX8m*ih~c55Xl_th^|JDcC*cs14uS0xBfsc!O}lN(YO?P`ElrBI)_cJuJ9!?0bI{6WRyD_~&Dj55^`He;_e7l^mN& zymBG=$^~UDws@|FOMH7ogjG!>`^l>hipf{L_%ye`EetPK+DXBWtCDiNcNIUgfF(XC^ax1!hE=WAol~A zQVw7s5hP;@4SIsGshDh$#`qSca$5|0{uag(#_dVtcEz|IW-e^#i?l`BV%^x*C($8j ze_Fe$-)j1~)!1KX{dqA5=ppeYa}h14_pkJT`N#8+JLR2a=bb!R+wd_E5VcpYC%dfY z$H5_5*_H81ml5W41MP2~JK!>Xq+mGDvJ*>kx0x+Jtr=QN6V6C3xQ6Yq%a08U7YJsS zlR0dWO=4M|hIg3;1~EwYWq}rk6J5DFTWoi7mCz;!uGs2b3(IX zC3iug7+Sg#9B8J!voO^SjI=(aW6UX&;K(j1%CV$a1&xqJwxg9Q@K#CU@d~|(9=(;L zpN2l)w%of9C36g+fW@U&K?n>9SsQ&plxFiu*p5Aj&1G}uOb1Y-Tk3P zK4jj-e6G4ewxvu%P8C^GCa>wUNTEvP!zYH1^z|I?%yZZx7cO>ygevB%j9kZDC6nTHui+@VRt3Y>Z`yI6f2hMcHDauf=iNApe%(JGx zFl`Lr#8m8PBVQF8+jYRS$m|?bb$bDmjH@XL`B|~UyV%J?Yy$w>bjF-ipkQOd?$0UG zF9;M8pe3p5#j^!Z5adOVD$pLF56=@|#TmuZa@Cr4;DVT-SOmIh9Mqz}W^@m6sDUfy z!{MdsDW;!0njScsRGrr6j?M=ToQ+Bj_A`ACVOHLrD5*}CR9}4#JJB~i%s*Wc{8K4f zQbH}fwD3}j4Knc4!sA}k|j0q(~%CgX^lSad|1NAS0zg7k|lKu&Fn+ z)!UWo?RV<|?i&Fj{g7)*ZVV`P9A>!|BF)RZWV$8DfCR`i0dA%ff2x z*;BVD#OfY6H_i+^G+L39{Qws`8k26GP;5ukR~0UH+~w}p{KWLxu4NzYm*qxFZQK&S z@L}!3jt^SmEz!nkqw>A|5J!Mji&dcO>g5npbdW-dNWdxDnHoIsrP*LV1cq)f4S~fI zgz!etqI;&7Q|vGQm7Pw=*chKk*gKN;4#nQ_7i*Q1rO*DDQsAKJ^%Jk_z|@okG0cY}B3dqKs1FkwHKv>&`buGmi~ z#uF=z_(TwyKtM;Tbox&%SESoligMOFQ7f0)!gXvDaa6%=` zW9?c}_~){y{PZi>BDZAtmFmzszk;DR-ypq|t5CEYdmh8h$q97;%U3TvJ2KDtt#B zhZg&su)<*gy>eI7bB4v;}ALy;*b}j+2#1OC>BK@70h4;LWYKw#K$T zEUk)dyR|beeCS^kems#V?MasQTs?p@0L{pYlDimk7;Nlw_K5wV(R^)xv{otPBS#}g zV;2&}>ZGw+$=r-!rY$2Q)NCUokpG72tJ3C?5&T~WBO~HJP%G~vu${mI0)It7hjtZ- z45aNI{O>E!!C`<%Y>$1~OadN;zR1+^G!a$dqMDvM2+%o(IS#`lEUCf;Ilr=Tp>{oW zBNdJ|FrqX}%p^Mp>uMZ~ilb_#C%k(^JL2KiNxumQ_QY|Ijq5q%V;zA#I&Qn7+x9Pe zr*t(+{{4l`71=n~pp>^KY#m8k#|-+TJkmbbsjTWq*taF^+h#0}jW!ORk6BP@9YnZH*#R0S+JigruQt^w2$MU0$KqN>JbDK zIdH#zHFpxDItT7>a?oyM!A70Y5eYpotX2%GQ%1{7@74Zm{WI(zW{0r~!E9jtf9OGu A2LJ#7 literal 24897 zcmb_^3vg3cnx-CC*3*_{%TH{$wh6WYzkm=zFkrBG1TYvINC?C&d}SlBCHr2Phx#gU zy0ROYaaTkSsfY|ziyqQ#CS>DG_0+m&XRDY@&n!J#Q*|$;u4uKxZf176cG;Smq9-#| zz1gbW{r+<$S&~hXp4#i{U+3O?&VSzj>-^{Zq`26o!_}ky-G#r{tkeBBddOVHbl}Us zM&Ny&pgXM-^nzhre_GFe4W|w4*Ld20!U2 z+~5Uo&?`>({5~n@8{xY)b@B&7yfh*P#>d+zGEb4GJ;8w3ZnGWtdIX90Ois2*KEcaR zdc{$GJTNBlEys@c4R#zH>Nq+0YX4q-+~@a78+pm=`Yh3OG5P$Gip8wqaYMen576ZD)L_wvVj1_wL#^&C7*F9Twb_l$XbeksTY z&wKfhR}8QM+63moY+6uasKLL0P!TR)P_o(54BGSt=nxjo?+#E zqoW&npC6U;idXrby+a32_VJf}DC%XtMVgj^$jB=oU%%iLef}{d<#Bbj^Zo$ed;I9J zV|~56G#&`@d`s|hfFB9?CGW^|(02)~A|h9RPy)W-d46YimlpAyH+b3W^|vxan>0j6 zPwY9|M~LZSh_Z=MII5=Pneg(0PnsO}TxEE)^qX1w!#zWNne32$^1Mg#w)y;RtYAC& z4bQiC@)PI>U%R)RpY)_#Zg3wz?g@^Z*D!5im4jpTu{yd`DY(pq~rra1OzJS2{B))HO=-?5Qx|bgn z0~4%<0(zToxf~ELN<6xdDxDsMljj3|FB>QH(mOGYVxv>iSX(zz+;l5ZPmGB^mWS31 zqJm-rEUJLl!c(0v#aS0=}OBfjAH)$Kgr)~1aL ziEr%OxTUkRlNUtKSl3JZR)jZp0cdYFHQF%BJ=170no%QE&`dM(BFc>h+}hb8@!dOl z&j^i^jrExw8W+dH5}3m zddElGf?g?z(&o+Xwl)c$=nHkJbk&cBkbEJ`wu}D2Wq;dPARu&T!E+wLh6(8POh_-W z9tZ{kSRMcr41Wpqj*Zz#~G!}MoR~RSqiSx$;LRy`uxF^ zX-o`EPquO?6C=PWV_-U%(vPR~6B1R5=J!{dJ0|?Wjy;}{i_?=Gpub}sAjD$8Cv=Q? z#>YVoJE-raj@+S?vQ2tK$vaH6Ky(39BmSjvfEiuV=9t-=EG~PhGn!svAfkWlD1Z0J zwIi_x)lrvl)J613yW^=&Z+$80aK8KUwU=)jV?96I|H1xv&&`)_y?oPo%lUEn{J055u|X!;+J@jejH zJw1C55mO{$hiT_S7kT*>jc{SUbZESi*aA9+*G9ir81qVL_A=oG9SV%H87O$igB~!} zpm*FWjRYpagc!e`_Mh{C@(TR9tJ-YTh~fob5PY0y2=#LR(V@QWV5;aZpD^ti2SX2t zpw?cGh)H}Ic)@LfgLts$y`YgRk_P;Ufs!*0!Z1xud;CFP$ivv{gy$ltNASES2oB;G zq!G`gmk|JxA0~VuDkHuTwGN8YOg=zUFgz_pi_&BYBp?tB_#tjkRY0OIdqe>k#WAnQ zA8T#rk7`t%(SL?bYg2{@Oyoe%@!nHC$NTvG$B&-q@8yS%pBOr@9b^&NBI_(w;aS_F z1FnGLwt`|Y1Z`~^wzdh0-`2hvS3BQNasZVX1kb~dQd@$lX&pI=o+Gq~LG-MhArXr( zPM%b>ys3Nge{qR4J4G6ft%j6w-0K%f}0&>)~jmix_Q@p z*S&Jp*{0gt6ZUq6YZuFaByXZ*iD?R7Am-}d#vt$2~iVDS9*{1MTOzV(hXDS>qPu871Of)Qx=jE4BK$SkJ%B60(opT{e^#owc@0Mq9zJg}Y@2u;Fxg*4- zLbmL*>kd^Ea(2e`wfSL6LDKMuL5av9nXs^K!xcL9rZ6ySg0Z$_t-oWgz69dpHc7Of9@9_s(JPuX@7_ag=$zO4Of?*JoH z(XQF)>>~vj3f5L^G%zxXqE}MUizMiHUWhUw-z45S=pBbS2kI&0)hJc_>}rTaMYrK! zdJ_O0Zpn0bvZ9vt?yqSG{1bth-d~w*ku7gsefw&3)A!z(c_SvogvXW=t&|^@$A{*( zEa(>u3x@mK)P^mIhAsG_mYhleT23V_r$C$S4uz|U4n>C$`ukolAh8tRG}nW$K5b_( z2Y|N)6VT)|Z3}dkvVN+pQ8D_)@(yUKkE5^fj)ETwQugNG)0?YJv zRJF%Haz4$and-|l!L%T<_u%opCl2=<=a2OD^k;;t3}I$^pp+JxhCs;EAF>+Zo`Xk@ z_Vygsbo0v)e|RY{Et188q$LPTU;y+Qs-IsIboQbo-oTjXnLLlT6iYKAkYM!2mNrt3 z!7fHey?7nev}ovD=U}VgiEEJt3|7S0hh_rsi9ED5ru!4;=m86Yt)XpX9Ks6AH&b`0 zx6`mX5I-WzhX|r8Xoz+m>)6D5F2jOA0w3vc)02qF+KRNhG5-V#)6P>1jQGS6DB4I( z!YKG0P_fcvG8{ovAiqqy_cX-5fB^ZD@KVO-($5nzxD#WyY6GU+kblW~~AqtxF*ujt5Ap)apewk{BXts~yK+iUKP^s~kW9ASbgjbaou$ zFQfhW77sH-vAQtzpIXIBbsF=C=R9LjxtZYB|KBs?&!<2YWJD<~%xQuh&8F6(6bE_! ze9AECOBqK3Qpza#{3#QZaQ`Mv+_Q>Rh?pu$%XDq@V34lh|EgGx2hmAy^iwIL$3G^K zOa>aQU7A>EW8-m{Oc&w{_`DbY(&qr6V{V|0>V@(`-SW~?B?tay=0beY($oYsccRo5bdvWffYN?4B zVw>XCarfQo`Gz~I?ygc-ZH#T2_bhbX8=d#edr1E5`QD|OOHm;zByH}<+PU@7mn1NESrrh~|Yk85Hs&6E(%MJkgL73?Zb*rx5<~{5W<{*5$G6 zpl{iHK&6P%P@bT;Cb@`F8KY+oWOk5@Oyiz&-f@^=Xt4M9K?xZg!r-SV*v6<18}-|H z*m-#9;(no}Wuk2o(qwA~6dN{j_l*ZYs|4RApF}20jK<3%hB&EfnW@m(-o@y|p1yrY zq2^Ft>G^OuFb(nboHip_S)iyi!vK&&*)I74)7s>2=36#wA%erkRVxghAQ>>%L9F!= zrGSpcibRB@W0ns<+^NX7>B2paHD5bxW(nF4zl*rVRKj*0O!z>qZH$J|Cf9WIu z=3I%+RvZb=)<#dNmQ|Q))itq!4~k+%-#12jqQ=?%vyM4O#PQ+ae8YU%{K&oPJ7@2n zjrSuWls8H?6|1~{MYXJ9azext*)nrzxk!-J%`Rc!zoUDn_??n>obQyr<9c5=3lVrs zH;7+Bzoh2~IeOJKmteeBoIA{eS=L`T9m0lLGN7$uvL}QMNR`~Ii-!7aF=PmzvN>Edw+?SD7a10Dm0a{zjckhNlAA1k2v_FiCOE3` z+ySI}@!9GA|FpsKykYRP=7~lHi$`duAF-aa5xiD%%^5_0Wv)nZq$J`RHDWo*l8a@w zfJ7E*;#k7UYu&KD0IM@n8qoo>2}@L#ys<#cEqxW{f_}y*gH6EnWAy4r*`6)TJ@}0{ z3QFgY?QgA?ttp^Ox4f9rk7_~;x97y( zLn*_#&Xgh8nI)<-Gc!z(5uZb<&>C6|8BQ;sTJ6-39!lEGcf5euW`hE$V@x{fL7^B874Zx zdB|M;GG3({2+i|bHPCk{DcKFO4J0UTA=dT4{lN3{s)w$h*Dk)kcpR^k<3q}cQ|j?k z%IVkDf!7t!$fJP~WncvD#eUNe5#0i7uay3Bt3{-S(kw705fZ64WgPPcMdC*(tJgn0 z;iYw}6bIWukZI_xWg=Mv(tMRhzPO*BnOMuAqotIIjdgJ?;n0gBQKXdKm*Ozy(I9|b ztc}Ejle%rZ-vYhSA2<$iOTY(MP(H7DvPrZJZsA z24`PaEOodOmb$oJwKQl?DtnhKb4P{9bHGh=EE?q zvqw+d-Wzwv&ygU1ch&syJF6Ggs{B^9?uA6%3yTeE-Ajslhid6gSh^KUH(R-RT*b%T z*WbMH=2IPK>SSQn9?{Qki|mOS9y`l1BMa^ieGA;3z})~|tM0=I_u+?v>Ksfs2O}Jd zPlIgv{h{f@k@>ZEe0O~d`a2gF8W)bM{I&$Y4KY=>kZ=pX9Q7#|{OV{RF&a=Nr_|9Y z)hQ;NVuVZDor--`dg&-;j`iT*jA{nwd+uCPs@hdYN5aveI6AatqOXy#{xycnJNkF5 z@7UOaqCwWhF^ZO7g^PW?x~Tr_S|XeATtbsgSYSs-JV`%Si`d9-`6Uz>RL@`@hPki_ z3I`Wo9vyFl0=~sKzV(_dciC~)n$Oz=j^v`e7`BySj2NyFCYtr>D`Ji4M-750UvdbN zJuZFa<0<6gFh--^OA=bxoQK`UuonS)4z~}|s$%+jc20ts$q|t0zCn)oEwB}o)gqhU zYJ>Ed(;k^}K@BaMbP3knvKB5hGkL>IvM6!4U51Etx|8`~hR3-6mu@9d#4qn7KxW=TiG+S(S9)E(%2(9`)n}d^*E>S_V4C-aY{qhnAf1rPG99H6?&#Vuu(SZkm^oJ<8m-Fpx zt&ag?POW(?OyN!DdKL$tHO?~a{vw;ApVVdBqJem}NPLuB6No7fkBtY;dB$70oK_*0 zrL_f>zS)`G60%Tj_tl~Z!>-zJT9!~vyr*eESv3E#$<+SqJ8TFMjkM^EX_MQQA zX1{6ZrUa}}G|M=X^r$6TZQ!aToDy>teUm9G*}Ry)6Jy>Yi8m*-7!oWlI{rH}DsW{OadF<55GN34 zh9d7ktAi0|`~gbc@7sQ0i*3H{yy1MRvzm7OngD3Iv;5uAwa~0QC$k5|QXhRKS+(l@ zH-7L&d_=8UpQu_t>xdX515v~+d-v+KtFiLgH|E~Zxb$OZ8I}|-&0UK6ROcF4=m+jp z-mRRsstsKc#>N>3qT!XjKlJ^PU0t^$v2F)msqU8(?w23-s?LFgb0E#IsNu6W@u`nD zsOz>S)@@ywQrB%;EL+^8^1BlJF2qsYXA|zTzZ^dI%Mrgi97qf^RyF(#R>djR?eUTi z_s$#d9JqTRUJ@@^;1-0%u1`mms&3V>GvU~&IClOo=G>r(Wx5fXh?$G%BgtrAet^|6 zGKP8Z|13*DxCk1fS?0j2jAwtB@f1tg8n(f-WtDBY%L&lw2y6P1?fKMPNDIB<2HCta z|B@|Vn;vc!?2i*nqn3Q!(6^3`VKEaWEeRKg9fBUq9!~t1;@=f6)4q-AM!7SXgyHhA zJ6sX26!hULK^LyZe@!kw`?^x(=gau_$#5++!P+Ie1S6)a!gaaaNv@-{HY{c!|Bvvm zT`XpOc(v?!MvK(v=cm`n#m|VjdRfepXT+>q7PI;-k6bTT6Auipk=F=CX;bJ{*@-8n zW3xpSr(FIFoVD^A)DW$p#a?QQT$>W(v1T&3F6Uo<9yw(ef@CcBRw1Ajfu?oaHD@S;S#ibCTx+L1c%0^ z;fhu4*9#@#wf{0|gOOhXBqo=zyuYq$Z$gfVW z0uD!hZ(|;m3p$}bm%_s~*u*$MZFo)ixx749%eBBr)PvCrgn9Wnp#j(%Zx<{TgIceZ z-7Da(WAw1L0CN+=yf%xiz+MS#^)kfLWhGr1+j9)tI$)!kH{M`3UW(Rlk)k=bG_qM*C#;uEX!Q+wY{r=w$SBKopFj{jkre_~v%daU$RW{#1Nsxrm^i}98 z09~fDf5}cM?nOLtBS6SLce7OvVm zc|PPIVHN(Y@QM|>Lsni0Ok*p7)csWvIlX(kzp{`yc6?mw4wdq0C-$`WdiPhx_D!Rq zDq7;wzO-GKo|u$&!live>V5)sT1ScKqAdVCE$KIfY*_8P=%f7jPzhU8Z;{}eK86Ko zAnY1Ni6^8?I?d0Xr88{jGjEw6zlP1aJzb~`Ay+ypZIb|{$@45CD$bO|bqHitL@B(l{mvQng-g*40Us`Q=@?+RAi&k$LnEYn6?`x?c*COQ!5 zfx)d8!Ge3anwKp%Ezf1kT~=7`%&7mQL@=c5P)oE4s zHTg42u^u2@lj2ULxHH-lcg4Hzy5}o@v^u^z;)w&&A3ot`wMh@m!4pT0^c;Vc$MDN6 z40MMqSR92uh0#@%9+2k#qkM1AP*2Fo^E9k8`w6;R?V7%pRn;`jZ8r)82Z3{vNR6B} z&rV_w3RE^}dLqT0gL`&s%aSEle2L=GazM(QR@LGEP0Q3w=Sy*7;Ibr+(|3ap_Ih6z z5P8R-O=96wg0})~bWV`Uec38Qm9UmGG&^H5e!D#jp`quX(E5Xxi{= z0#Hf0$hl}^vZO4!DS9sY{M^Ne5w5rqX?EM(w&=NQ-I4B$k=A`%if_Jo<<=E)%$*;Y zpIT^I2rJcns$*Zmu}^XAduU1WjqyG6hUlfprHCoHVWVQ%7%>4v%>c3I;_H%~n-=yg z8W#5~l&F@rh&fsvdoeziEOADRNmoUrD9%MMJ$AvO4GsIsjVssJ+(2Q=tc|}W5a~&l zS4O99Y@O}JtekpWUXRt3%K6F#w_3h6QJ#LCbhzF`FQ-6lX)SvMRRj`UQA*lvlJXwqkT#Ds`vYU&>!!KOY;K@pJ_FOH}idz7X^x~%-4>qe^`;@l* zYW;yk{Q;%^fL47Nu^Z+`@2w{T_@epIx?Re;UCGk&=+t!!nxuYpTp!&BzwLdG;gz#* z_Vu~fBZg#2>3ikTeI>i3grPKD~H4+0Z=S^`lks zRST7ioByzWq5i2+S6&lcm+U))khS!S^pe2P?aeXi=C)hg;wOOO#NxhW#j4mTctTdJ zgD0f58J>{VW;B?!`B#==r6e2+0^t6*2_6kC_gl25MdM=6pA;=l{fYHK=o*!I^UWxZcuSBr4HSa_A_7_o7`-@-L&Jcn#xR(&n z6Ct2qWnd^Bu${?bA}sNfgr)7*CZnnSpRfn1o%SHL(_)}A+Hh@~VsE_7#fEO$ZrK#B zQIkeM`?D7;YnqMif5L$I4)>0UoN}*mLUV*0#YW$^tzr1RybWJ%tcQ@JZqYT9&;n*) z!U~BMUyZrGH^MUg4sNnW!43!))_e(DC=wj^i}USFw4oUBOSJgsb%OKv_4iBjS11H* z1HNCrVmh~$PN+DqzhAj5Rn`6K6;stvDxvo60u2E9RNY^-Vv4$SKK00F^$Pi{SuqvQ zQWa=of-8R|C*1-KtW1r|%2XsYEpM~D9#}hTf{yeYYzpi0OQwgL1HPE?r3zAHYrp75 z*E3;wgv7|qMZzVigbzc4oI^O z=1T}h6u1q*UH$A7Sn>mo{9y(~4n|?5v%`Zs$3csp zNM@Wnlk3C-|H`8K=KO}Enq9N!8eSugh#kY+M3&%^4GdLEh|qM z%-SZ|R^YH^&2v~QpF1O)*g_jqPJORH%Xy;vn}0Sv`Qmpu9a9{KupVTFXzIl(SDtD` zaz6+EOGy*-U#0KNHN*(N5AiNNGLip7ge39|$r0UmYmP&Z@-K$mE7kt5C{Yz# zWWpMg(}mR|M=^Gqx+yPYxTu~d_{Gl=4d&hd$s#dH|RICSIEGEIMxH+mvk)i`pbJTpp9D50S{wR1I z{}AJfo4;_DN9-DB%OxcgW`wK=0pe;jAp1oRo-`AHUZgIPC?2orNG9sv&QR6z4qzYt zD@7-4GrCw!tY&$ZWJq9HlD9i8SP5*$ie_qr85v_8h2Z+d%lNLj+25g1BY}?ratmXo zJfTL0YE~`|K7Bz+C_@yeC+4}i7s7sD{DqrXs$0)WH^k=tlMzH2Ax@vUi1f39>NuL< z+K$LPf>gk*3yE3SGP@f7nPkgIUsyG=<`XJrCAEd^w4o=D$QF?HQRA2%Yhi|PaW0XC z>;cK6FyDOQ9)@j1(2a9pC;K&F9mW}_4qA5o7;iqxz74zN(l{}?WtZ&A&*6DpzKvS8 zU^TIrJSu;T&isgo7rVk`u*KppzXh;&&EBNz(n;K9!xiJJy32aK?p0s}Qz{FW%jL3l z)DU*d2JmGQ_;OLWLUyg}c~MaQ%3O=&1*{Hfmtd9)o{R!k)|JYFM@1at&YwTQjJXS0P*_*9p~#UnAEe zel1Fkx3|mkYlGu)h#=K1-?GVwLmkSI-g|3hlNlQ}G0hF52JCvZ!KZ}LEduG?myDn? zo|m^@QKSlmHY_J&Bj>#%xqLmlWA*QlK;i+g>(aSNyKioeb;Smvu4qrRDEpF8IY#92 z1Q;U3oALi8`BY|YtcI|D&ZzAj-S^F=)Aiq6-i#;j6XYUQ0i-Rdt~SNh7SjW0w^qtN zL=Z&+57!9n#!cHK*ln>v7RMlTrwm)9kcl4)1j(4&!i=Hacp;n{ z?-qlEd>R0sH2Y+?c!?g3tvsJH96b~&UB7-kf8^lck)ENw2Y5WRmTIm!FHllu>U|R- zSYZPx8x9Q$!nbzZcP?caglms~?BLOqQ^ZjYxU4QV6yL0tH7ClNXU%XF z-a?|7=XNx7Rbc;Ui)`3g*H^+yf22ji}vRQ;$5Vs`D_+MC0- zhCd!zX!y6yf7ra(@ag)WuD=(!ANcpugRcMZ!e=i$?E2H~Ki~edH~-|#UmhD&hE9Aw z^xC7L*OW77)uGoDL$9mHh7-qzf9V-j#?F5}=6^KiR|1pj*i>R{O7(~dkEqm1xRLLh z`lANg#;A{Miioh!i;(+chaQzede)V`Vt^fb!=tut?cqV=L&u{%L)wG#>TAmI_@h_H z(~nbx?TUV{;U6)A@uR&4JfPVW`wg$qrvvywn-n`EuSI(!24>0BJA?XIuTt_Iv?YQX zoQDB4@5O`j%8t{E`xf^pL#GQqVRIS!Gn*7k1bL6^H|9NVrIQMlBJV-YQr?>ruPR5T z9u7SmQii8;KhPkV*3&4NW@gWPD3xJqeCBlT=`eoQ!R^^>{%orrFtnP;B|E((bC47r z7MDITs&#v)3cB|!trq%x+bKt4{p-+3)-Cb7r+`S+pL|ULBxmFIyZH8?qnqU zA-+EO1%bZ=K=hyBXP@{-+QVn~2^DK(iIQ#Z?DTDfl!kb}gHMe9Bm8rUwvNbEdIgSe z@jdrKcXub9f`;W=?f5f%$GKH4Ne&VSk!vSo+e_9ejqUc0Qx^8wgB^-Cf66z|Y5j4c+4A5n~dLtufxF9{HP z5fcDRvn8gIadow1dQSXneEyFV<&RNx=HzN|&GbAqxJ=kb@+`1cXKI4U8F!Lfudo}d zj5B+l8Y)dR>%JNObEnQ^Q*7&1ZbO3Gpl}seK4e)I<0W8 zsoZM`?llDRqb!8!PigK`oBI;YeGjIwM5AzLRPIcIJA(j=9qG{UEjBGyD9yXn=3R;A zT@OyI6jiZdU2x7;H^F@uf&3yY?p=IdY3@;*dlJn( z!20S+#a+Wn(0u2+cfb4KgvuRCaEBD`P#RG^eN@)(#jQ5?CYpPfRT?W1k&W?2rMfeQ z-MRDYRIV$*btzm|va~F+Gtw35f-uUZ4HoFf>}5xYXwaAc2)Qx;0I{WmH41c3m^D`R z9HhfM?iU<`0ZsQ=vEd)fjIGFjX-DMO#J2=V%1vL;wXOGUg?9F5=4EGV4VWNHyE0f# zBrPwy3hIkvf-aack#H1Nj`qBC^l7fgV}ANVjSJYamKdo8CAMa8=lCd}%R{UY1)?OI{Gzckp9 z6Bmm;g)wBil;5ij%f{-r9n)R;Il(v73a@?4$sGHeW92a-J0QSRLx5>4%$-?2n)-3V zl5gDml^$YR%H+opd{W4oJ@ZubBj-?8BSa1{ctqNW-OERCW*HouUgF!^+Z%C?L;5s` zQJ;t%%(S-#r^B@33@>x~2+$OF-a9@Svge$-7iz(gdLuZX?Ywu{!D;NszwPOB`C40z zDK2;wvgO~P9OB@b4S>F=&Auxda{X< zmoFWk%1$MvGgR{=A)<&(9MmKscJP`xYy2U@k7Xsp56$Ce8GfoKaF}qe^yf>TV^%?e zP#sdj%2V8+-Pe0z-(1PiW>S=E?ev}S6(FF0Dlm%mm?GZ|q^oV2FMC=b9qitkj%Ha`&VSHup#0OOV z^tqxm?3j=R&$O#jilc*N#s5MPuw4sT597pJY)|F_IO-JsY~&YaV_TxwF{kWAQq8tP zavnPYj2)gR!2@5%&eRd#qR<3^GX%&R3`<)Ec%X5t97OXq?>e$*p+w>c zywEgEZ0qxqNs!q~TUNojK`@Z~Z+tXSkCXH;qkC+1C9J$^ZA@4j zXZ9qyqIZsd?`U*iylLjB%B@Rq>lAKX=Ix>RGG&vOK8z-~QH2}Ld^qy4X(5=bfh9m^ zaznK<^^s}^2>lcAi!|25^L3PXYl7RVa9iQt_HOsJ?%7>)yWr+#w!a;w*!7Ps_BqRY z)wGy~z0TPDtXS%>^>&V|38r;8HR#=vYw$*^PFSiHOZDw0a;mzy;ns%OhR2T5sQX4u z{6)plraIaZj<(Mon;tnfsg5lP$Cl3>yB;}qsg9n6qi3e?vArVdRqb^NdmT1W=O4cY zMOrCoQY~u}mbHpy?S~_G&(BZYxp?iqPSsZU()7;4@ybPe1%%l zktpey*`G9*kn=}tva&uFjv6CHk)mI@_;{u2YDu_S;7f(NzCpFw{R2Tbl(ZyDTHe|}(=!uHTAkRCQ8Tj#LMGDMcj4g8y*E!QHd5^E zyBL6X)X4{Xe|B2k1`n8=_sT2e%9DTFyu1?9^ z8r88357W>c9)=1Gb3+k-;YHW(kfXZ?`7JIW` zy-g1X#w@6=B~)3k%|VY@P^WXcX6#S7ZThCCx_od+zfF&2@hk}H*XrRGmIa6PI?fRZ eJu=oP#+oE&ndyCN-`o3U*dOQ#SBuxIh5sKv%5<3k diff --git a/tools/jank_capture.py b/tools/jank_capture.py index 144bff2..a5d2968 100644 --- a/tools/jank_capture.py +++ b/tools/jank_capture.py @@ -53,7 +53,8 @@ LINE_RES = [ r"render=(\d+)us anim=(\d) to=(\d+)ms .*dec=([\d/]+) pcpu=(\d+)us " r"rcpu=(\d+)us vsw=(\d+) nvol=(\d+)")), ("VSYNC", re.compile(r"^VSYNC ftn=(\d+) mon=(\d+)")), - ("PRES", re.compile(r"^PRES f=(\d+)\w* mon=(\d+) pre_us=(\d+) precpu=(\d+)us")), + ("PRES", re.compile(r"^PRES f=(\d+)\w* mon=(\d+) pre_us=(\d+)")), + ("NOPRES",re.compile(r"^NOPRES f=(\d+) mon=(\d+) why=(\S+)")), ("EW", re.compile(r"^EW land=(-?\d+)ms")), ("SLOWEV",re.compile(r"^SLOWEV (\S+) (\d+)us cpu=(\d+)us")), ("FR", re.compile(r"^FR gap=([\d.]+) mon=(\d+)")), @@ -151,7 +152,9 @@ def miss_report(parsed, period_us, max_episodes=12, ctx_ms=33): elif kind == "VSYNC": tag = f"VSYNC ftn={grp[0]} (+{(mon - int(grp[0]))/1e6:.2f}ms cb latency)" elif kind == "PRES": - tag = f"PRES f={grp[0]} pre={grp[2]} precpu={grp[3]}" + tag = f"PRES f={grp[0]} pre={grp[2]}us" + elif kind == "NOPRES": + tag = f"NOPRES f={grp[0]} why={grp[2]}" elif kind == "FR": tag = f"FR gap={grp[0]}ms" elif kind == "SLOWEV": @@ -195,6 +198,7 @@ def capture(app, drags, tag, tag_file, outdir, settle_after): time.sleep(3) if not resumed(app, rpt): rpt["valid"] = False + screen_awake(rpt) # before: screen must be Awake for input to land if launch != "COLD": rpt["notes"].append(f"launch was {launch}, not COLD (leftover process?)") @@ -210,6 +214,7 @@ def capture(app, drags, tag, tag_file, outdir, settle_after): # 5. state + stop resumed(app, rpt) + screen_awake(rpt) adb_shell(f"am force-stop {app}") time.sleep(1) @@ -232,6 +237,26 @@ def capture(app, drags, tag, tag_file, outdir, settle_after): print(f" valid: {rpt['valid']}") return 0 if rpt["valid"] else 1 +def screen_awake(rpt): + """The capture is INVALID unless the screen is Awake: with the screen + off, `input swipe` injects nothing and the app idles — a whole run can + be silently wasted (buildD 2026-10-30: 49 IT lines, 1 present, screen + asleep the entire time).""" + out = adb_shell("dumpsys power | grep -E 'mWakefulness=' | head -1").stdout.strip() + awake = "Awake" in out + kg = adb_shell("dumpsys window | grep -m1 'isKeyguardShowing'").stdout.strip() + keyguard_up = "true" in kg + key = "awake_after_run" if "awake_after_run" in rpt else "awake_before_run" + if keyguard_up: + rpt["valid"] = False + rpt["notes"].append(f"keyguard showing ({key}): input would be eaten by the lock screen") + return False + rpt[key] = awake + if not awake or keyguard_up: + rpt["valid"] = False + rpt["notes"].append(f"screen NOT awake ({key}): {out or 'no mWakefulness line'}") + return awake + def resumed(app, rpt): out = adb_shell("dumpsys activity activities | grep ResumedActivity").stdout ok = app in out diff --git a/tools/jank_eval.py b/tools/jank_eval.py index 1bd8e57..bb66548 100644 --- a/tools/jank_eval.py +++ b/tools/jank_eval.py @@ -30,7 +30,7 @@ Exit: 0 = PASS, 1 = FAIL, 2 = no usable data. Self-test: --selftest (synthesizes log FILES and runs the same main() path end-to-end; the tool must classify each case or exit non-zero). """ -import argparse, math, os, re, sys, tempfile +import argparse, bisect, math, os, re, sys, tempfile from collections import defaultdict PRES_RE = re.compile(r"^PRES f=(\d+)\w* mon=(\d+)") @@ -82,6 +82,38 @@ def anchor_phase(monots, period): if ang < 0: ang += 2*math.pi return int(ang/(2*math.pi)*period) +def slot_of_ftn(m, ftns, P): + """Assign a present to the slot of the NEAREST OBSERVED hardware edge, + in a TIME-CONTINUOUS slot domain. (2026-10-30: the cumulative grid + anchored at ftns[0] is WRONG after a display phase-slip — the 120 Hz + panel clock re-locks at an arbitrary phase after idle blanking, so every + present after the slip measures ~5-6 ms 'late' against the stale grid + while landing perfectly against the real edges. Measured on the phone: + an entire drag window (w1) read late=230/dropped=24 while per-cycle + tracing showed presents at edge +2.0 ms.) + + The slot of present m is: round((ftn_i − ftns[0])/P) where ftn_i is the + nearest observed edge before m (plus one when m is in that edge's second + half). A phase-slip Δ

P: + x = (m - ftns[0]) / P + s = int(x + 0.5) if x >= 0 else -int(-x + 0.5) + return s, (m - ftns[0]) - s * P + base = (ftns[i] - ftns[0]) / P + s = int(base + 0.5) if base >= 0 else -int(-base + 0.5) + if d >= P / 2: + s += 1 + return s, d + def slot_of(m, P, grid): """grid = ('edge', ftn0) or ('anchor', anchor). Returns (slot, phase_ns). edge: phase in [0, P) from the vsync edge (hardware truth) @@ -148,11 +180,15 @@ def explicit_windows(pres, t0, spec): out.append((name or f"w{len(out)}", idx0, idx1)) return out -def evaluate(pres, P, grid, windows, suspect=False): +def evaluate(pres, P, grid, windows, suspect=False, ftns=None): """suspect=True (fitted grid, no VSYNC edges): drops/doubles are reported for information but excluded from the verdict — a fitted grid can't prove - them. Stalls and late-phase remain meaningful.""" + them. Stalls and late-phase remain meaningful. + ftns: observed hardware vsync edges — when given, slots/phases are + assigned relative to the NEAREST observed edge (phase-slip robust); see + slot_of_ftn.""" t0 = pres[0][1] + use_ftn = ftns is not None and len(ftns) > 10 any_fail = False total = 0 for name, i0, i1 in windows: @@ -167,7 +203,10 @@ def evaluate(pres, P, grid, windows, suspect=False): phases = [] for k in range(len(seg)): f, m = seg[k] - s, p = slot_of(m, P, grid) + if use_ftn: + s, p = slot_of_ftn(m, ftns, P) + else: + s, p = slot_of(m, P, grid) phases.append(p) if prev_s is not None: gap = s - prev_s @@ -304,7 +343,7 @@ def run_file(path, period_ms, windows_spec): wins = explicit_windows(pres, t0, windows_spec) if windows_spec else auto_windows(pres, P) if not wins: print("no active windows found"); return False, suspect - return evaluate(pres, P, grid, wins, suspect=suspect), suspect + return evaluate(pres, P, grid, wins, suspect=suspect, ftns=ftns or None), suspect def main(): ap = argparse.ArgumentParser() diff --git a/vendor/i-slint-backend-android-activity-1.18.1/Cargo.toml b/vendor/i-slint-backend-android-activity-1.18.1/Cargo.toml index 23ef7f3..c177aa0 100644 --- a/vendor/i-slint-backend-android-activity-1.18.1/Cargo.toml +++ b/vendor/i-slint-backend-android-activity-1.18.1/Cargo.toml @@ -101,6 +101,10 @@ default-features = false [target.'cfg(target_os = "android")'.dependencies.jni] version = "0.22" +# eventfd for the vsync delivery fd (vsync.rs wake-fd-race fix) +[target.'cfg(target_os = "android")'.dependencies.libc] +version = "0.2" + [target.'cfg(target_os = "android")'.dependencies.ndk-08] version = "0.8.0" features = ["rwh_06"] diff --git a/vendor/i-slint-backend-android-activity-1.18.1/lib.rs b/vendor/i-slint-backend-android-activity-1.18.1/lib.rs index ea59c9a..ec1ac8a 100644 --- a/vendor/i-slint-backend-android-activity-1.18.1/lib.rs +++ b/vendor/i-slint-backend-android-activity-1.18.1/lib.rs @@ -126,6 +126,11 @@ impl i_slint_core::platform::Platform for AndroidPlatform { // the frame held by the render-ahead pipeline (otherwise the last // content change would never be displayed). let mut prev_locked = false; + // Edge (ns) of the last vsync delivered to this thread via the + // eventfd trampoline (vsync.rs). A value newer than this at + // decision time means a wake was delivered THIS iteration — immune + // to the ALooper wake-fd swallow (see vsync.rs note). + let mut last_delivered_edge = 0u64; loop { let mut timeout = ctx.duration_until_next_timer_update(); // While animating, the vsync thread wakes this loop at each display refresh @@ -260,6 +265,17 @@ impl i_slint_core::platform::Platform for AndroidPlatform { let t_poll1 = stg::now_us(); let __poll_cpu = stg::thread_cpu_us().saturating_sub(__cpu_p0); let lv1 = stg::lv_snapshot(); + // Wake-fd-race fix (2026-10-30): the ALooper wake byte can be + // CONSUMED by Looper::pollInner while pollOnce returns a + // co-batched input ident (system/core/libutils/Looper.cpp — the + // POLL_WAKE result is a dropped local; see vsync.rs). The + // dedicated eventfd trampoline runs inside poll_events and + // publishes the delivered edge there — check it NOW, after. + let __delivered = vsync.take_delivered_edge(); + if __delivered > last_delivered_edge { + saw_wake = true; + last_delivered_edge = __delivered; + } // Unconditionally update timers and animations on every event-loop wakeup ctx.update_timers_and_animations(); @@ -328,7 +344,16 @@ impl i_slint_core::platform::Platform for AndroidPlatform { __now_ns.saturating_sub(__e) / 1000, __now_ns.saturating_sub(__last) / 1000); } - let __accept = saw_wake && (__p == 0 || __sw > __sn); + // DIRTY BACKSTOP (2026-10-30): the 6 ms poll cap exists to + // bound chain-breaks — but a timeout iteration has + // saw_wake=false, so `saw_wake && guard` never rendered and + // the "backstop" backstopped nothing (the 20-40 ms silences). + // If the window is dirty and the last render is >=2 slots + // old, render NOW regardless of wake state. + let __dirty = self.window.pending_redraw.get(); + let __age = __now_ns.saturating_sub(__last); + let __backstop = __dirty && __last != 0 && __p != 0 && __age >= 2 * __p; + let __accept = (saw_wake && (__p == 0 || __sw > __sn)) || __backstop; if __accept { i_slint_renderer_skia::wgpu_30_surface::vkprobe::LAST_RENDER_START_NS .store(__now_ns, std::sync::atomic::Ordering::Relaxed); diff --git a/vendor/i-slint-backend-android-activity-1.18.1/vsync.rs b/vendor/i-slint-backend-android-activity-1.18.1/vsync.rs index 04c7e9e..1aa557e 100644 --- a/vendor/i-slint-backend-android-activity-1.18.1/vsync.rs +++ b/vendor/i-slint-backend-android-activity-1.18.1/vsync.rs @@ -5,7 +5,8 @@ //! Optimized for ultra-low latency: self-re-entrant Choreographer callbacks wake //! the main loop directly on vsync without thread round-trip serialization delay. -use std::sync::atomic::{AtomicBool, Ordering}; +use std::os::fd::FromRawFd; +use std::sync::atomic::{AtomicBool, AtomicI32, AtomicU64, Ordering}; use std::sync::{Arc, Condvar, Mutex}; use std::thread::JoinHandle; @@ -13,13 +14,22 @@ use crate::android_activity::AndroidAppWaker; use crate::ndk::looper::{ForeignLooper, ThreadLooper}; mod ffi { - use core::ffi::{c_long, c_void}; + use core::ffi::{c_int, c_long, c_void}; #[repr(C)] pub struct AChoreographer { _unused: [u8; 0], } + #[repr(C)] + pub struct ALooper { + _unused: [u8; 0], + } pub type FrameCallback = unsafe extern "C" fn(frame_time_nanos: c_long, data: *mut c_void); + pub type FdCallback = + unsafe extern "C" fn(fd: c_int, events: c_int, data: *mut c_void) -> c_int; + + pub const ALOOPER_POLL_CALLBACK: c_int = -1; + pub const ALOOPER_EVENT_INPUT: c_int = 0x0001; #[link(name = "android")] unsafe extern "C" { @@ -29,9 +39,36 @@ mod ffi { callback: FrameCallback, data: *mut c_void, ); + pub fn ALooper_forThread() -> *mut ALooper; + pub fn ALooper_addFd( + looper: *mut ALooper, + fd: c_int, + ident: c_int, + events: c_int, + callback: Option, + data: *mut c_void, + ) -> c_int; } } +/// ALooper wake-fd race (2026-10-30, ROOT CAUSE of the 120 Hz slot drops): +/// `Looper::pollInner` (system/core/libutils/Looper.cpp) drains the wake fd +/// in the SAME epoll batch as any ready input fd, and `pollOnce` returns the +/// input's ident — the `POLL_WAKE` result is a local that gets dropped. The +/// `ALooper_wake` byte is consumed WITHOUT `PollEvent::Wake` ever reaching +/// Slint, and it cannot be recovered (the byte is gone). With motion events +/// arriving 1-4 ms after the edge (always, for a real finger at 120-240 Hz +/// input rate; ~1 in 5 frames for 20 Hz adb input) the wake is lost and the +/// locked render decision (`saw_wake && slot-guard`) drops the dirty frame +/// for that slot — measured: 8-12% of slots dropped, uniform `dec=1/0/1/0`. +/// +/// Fix: a dedicated eventfd registered with the main looper under +/// `ALOOPER_POLL_CALLBACK`. The Looper invokes its C callback INSIDE +/// `pollInner` (before pollOnce returns, and regardless of which ident +/// pollOnce hands back), so the delivery can never be swallowed by an input +/// response. The trampoline publishes the edge ns into `delivered_edge`; the +/// main loop treats "new edge delivered" exactly like `saw_wake`. + struct Shared { state: Mutex, cond: Condvar, @@ -39,6 +76,35 @@ struct Shared { looper: Mutex>, main_waker: AndroidAppWaker, choreographer: *mut ffi::AChoreographer, + /// Dedicated vsync-delivery eventfd (see the wake-fd race note above). + /// Raw fd (>=0 once registered; -1 = unavailable). The fd is leaked on + /// purpose: it is registered with the MAIN looper, which outlives the + /// driver (same lifetime convention as the forgotten Timer). + evfd_raw: AtomicI32, + /// Edge (CLOCK_MONOTONIC ns) of the latest vsync DELIVERED to the main + /// thread via the eventfd trampoline. The main loop swaps it out each + /// iteration; a value > last-seen means "saw a wake this iteration" — + /// immune to the ALooper wake-fd swallow. + delivered_edge: AtomicU64, +} + +/// Trampoline: runs on the MAIN thread inside ALooper_pollOnce (see the +/// race note above). Publishes the edge that `frame_callback` just wrote. +unsafe extern "C" fn vsync_fd_trampoline(fd: i32, _events: i32, data: *mut core::ffi::c_void) -> i32 { + // DRAIN FIRST (level-triggered): the eventfd is a counter; if we never + // read it, it stays readable forever and ALooper_pollOnce returns + // POLL_CALLBACK on EVERY poll — the main loop busy-spins at ~10 kHz + // (measured: 1812 spin iterations per run, wake→render latency stretched + // to +5.5 ms phase in the affected window). + let mut counter: u64 = 0; + libc::read(fd, &mut counter as *mut u64 as *mut core::ffi::c_void, 8); + let shared = &*(data as *const Shared); + let edge = i_slint_renderer_skia::wgpu_30_surface::vkprobe::VSYNC_EDGE_NS + .load(Ordering::Acquire); + if edge > 0 { + shared.delivered_edge.store(edge, Ordering::Release); + } + 1 // keep the fd registered } unsafe impl Send for Shared {} @@ -76,7 +142,36 @@ impl VsyncDriver { looper: Mutex::new(None), main_waker: main_waker.clone(), choreographer: std::ptr::null_mut(), + evfd_raw: AtomicI32::new(-1), + delivered_edge: AtomicU64::new(0), }); + // Register the delivery eventfd with the MAIN looper (this runs on + // the main thread, from run_event_loop). ALOOPER_POLL_CALLBACK makes + // the Looper invoke the trampoline itself inside pollInner. + unsafe { + let evfd = libc::eventfd(0, libc::EFD_NONBLOCK); + if evfd >= 0 { + let looper = ffi::ALooper_forThread(); + let r = ffi::ALooper_addFd( + looper, + evfd, + ffi::ALOOPER_POLL_CALLBACK, + ffi::ALOOPER_EVENT_INPUT, + Some(vsync_fd_trampoline), + Arc::as_ptr(&shared) as *mut core::ffi::c_void, + ); + if r >= 0 { + log::info!("VSYNC EVFD registered fd={evfd}"); + shared.evfd_raw.store(evfd, Ordering::Release); + // Leak: the fd belongs to the main looper for the life of + // the process (the driver's drop must not close it). + std::mem::forget(std::os::fd::OwnedFd::from_raw_fd(evfd)); + } else { + log::error!("VSYNC EVFD ALooper_addFd failed"); + libc::close(evfd); + } + } + } let handle = std::thread::Builder::new() .name("slint vsync".into()) .spawn({ @@ -112,6 +207,15 @@ impl VsyncDriver { self.shared.driving.load(Ordering::Acquire) } + /// Consume the latest vsync edge delivered to the main thread via the + /// eventfd trampoline (0 = nothing new this iteration). Call ONCE per + /// main-loop iteration, after poll_events. + pub fn take_delivered_edge(&self) -> u64 { + self.shared.delivered_edge.swap(0, Ordering::AcqRel) + } + + /// (edge delivery happens in `frame_callback`, which owns &Shared) + /// Push (animating, dirty) into the driver every main-loop iteration. /// The vsync thread re-posts the first Choreographer callback, and /// `frame_callback` re-posts the chain, while `animating || dirty`. @@ -259,17 +363,29 @@ unsafe extern "C" fn frame_callback( data: *mut core::ffi::c_void, ) { let shared = &*(data as *const Shared); - // CRITICAL PATH: wake the main loop FIRST. The VSYNC log line below is - // a file write (50-200us typical, worse under I/O pressure) — doing it - // before the wake would push the main loop's render start (and thus the - // present phase) later in the slot. - shared.main_waker.wake(); - // PRESENT GATE: publish the exact vsync edge (CLOCK_MONOTONIC ns — the - // same epoch as `mon_ns()` above) so the renderer can quantize its - // `Queue::present` to the vsync grid (wgpu_30_surface::vkprobe). + // Publish the exact vsync edge (CLOCK_MONOTONIC ns — the same epoch as + // `mon_ns()` above) FIRST: the trampoline reads VSYNC_EDGE_NS, and the + // slot guard in the event loop keys off it. i_slint_renderer_skia::wgpu_30_surface::vkprobe::note_vsync_edge( frame_time_nanos as u64, ); + // CRITICAL PATH: deliver the edge to the main loop. (1) the dedicated + // eventfd — its trampoline runs inside ALooper_pollOnce and CANNOT be + // swallowed by a co-batched input response (the wake-fd race); (2) + // ALooper_wake as secondary (still the normal PollEvent::Wake path when + // no other fd is ready in the batch). The VSYNC log line below is a + // file write (50-200us typical) — kept after both wakes so it cannot + // push the main loop's render start later in the slot. + let evfd = shared.evfd_raw.load(Ordering::Acquire); + if evfd >= 0 { + let one: u64 = 1; + libc::write( + evfd, + &one as *const u64 as *const core::ffi::c_void, + core::mem::size_of::(), + ); + } + shared.main_waker.wake(); let mon = unsafe { mon_ns() }; if mon > 0 { log::info!("VSYNC ftn={} mon={}", frame_time_nanos, mon); diff --git a/vendor/i-slint-renderer-skia/wgpu_30_surface.rs b/vendor/i-slint-renderer-skia/wgpu_30_surface.rs index 94a8161..c264004 100644 --- a/vendor/i-slint-renderer-skia/wgpu_30_surface.rs +++ b/vendor/i-slint-renderer-skia/wgpu_30_surface.rs @@ -617,10 +617,21 @@ impl crate::Surface for WGPUSurface { } let __cpu0 = vkprobe::thread_cpu_ns(); let __t_acq = vkprobe::now_us(); + // NOPRES log (2026-10-30): an early return here leaves pending_frame + // EMPTY (the held frame was already presented at the top of this + // call), so the NEXT draw has nothing to present — one display slot + // vanishes with no PRES line. The cause is logged HERE, at the + // failing draw. Balance check: #FR == #PRES + #NOPRES + first-draw. + let __nopres = |why: &str| { + let mon = vkprobe::monotonic_ns(); + if mon > 0 { + log::info!("NOPRES f={} mon={} why={}", __f, mon, why); + } + }; let frame = match surface.get_current_texture() { wgpu::CurrentSurfaceTexture::Success(t) => t, - wgpu::CurrentSurfaceTexture::Occluded => return Ok(DrawOutcome::Occluded), - wgpu::CurrentSurfaceTexture::Timeout => return Ok(DrawOutcome::Timeout), + wgpu::CurrentSurfaceTexture::Occluded => { __nopres("occluded"); return Ok(DrawOutcome::Occluded); }, + wgpu::CurrentSurfaceTexture::Timeout => { __nopres("timeout"); return Ok(DrawOutcome::Timeout); }, wgpu::CurrentSurfaceTexture::Validation => { return Err("WGPU surface validation error in get_current_texture".into()); } @@ -636,7 +647,7 @@ impl crate::Surface for WGPUSurface { surface.configure(&self.wgpu.device, surface_config); match surface.get_current_texture() { wgpu::CurrentSurfaceTexture::Success(t) => t, - _ => return Ok(DrawOutcome::Occluded), + _ => { __nopres("reconfig"); return Ok(DrawOutcome::Occluded); }, } } };