From 71f69d1d483efecdc224a70a1c74c5ca56d80c67 Mon Sep 17 00:00:00 2001 From: kd-11 Date: Wed, 17 Jan 2018 19:14:00 +0300 Subject: [PATCH] rsx/overlays: Introduce 'native' HUD UI and implement some common dialogs (#4011) --- bin/Icons/ui/L1.png | Bin 0 -> 2829 bytes bin/Icons/ui/L2.png | Bin 0 -> 2224 bytes bin/Icons/ui/R1.png | Bin 0 -> 2704 bytes bin/Icons/ui/R2.png | Bin 0 -> 2569 bytes bin/Icons/ui/circle.png | Bin 0 -> 4307 bytes bin/Icons/ui/cross.png | Bin 0 -> 3560 bytes bin/Icons/ui/dpad.png | Bin 0 -> 3474 bytes bin/Icons/ui/dpad_down.png | Bin 0 -> 3618 bytes bin/Icons/ui/dpad_left.png | Bin 0 -> 3641 bytes bin/Icons/ui/dpad_right.png | Bin 0 -> 3706 bytes bin/Icons/ui/dpad_up.png | Bin 0 -> 3655 bytes bin/Icons/ui/fade_bottom.png | Bin 0 -> 43214 bytes bin/Icons/ui/fade_top.png | Bin 0 -> 43606 bytes bin/Icons/ui/left_stick.png | Bin 0 -> 3888 bytes bin/Icons/ui/new.png | Bin 0 -> 19960 bytes bin/Icons/ui/right_stick.png | Bin 0 -> 4178 bytes bin/Icons/ui/save.png | Bin 0 -> 26456 bytes bin/Icons/ui/select.png | Bin 0 -> 1813 bytes bin/Icons/ui/square.png | Bin 0 -> 3385 bytes bin/Icons/ui/start.png | Bin 0 -> 2606 bytes bin/Icons/ui/triangle.png | Bin 0 -> 3784 bytes rpcs3/Emu/Cell/Modules/cellMsgDialog.cpp | 101 +- rpcs3/Emu/Cell/Modules/cellSaveData.cpp | 13 +- rpcs3/Emu/Cell/Modules/sceNpTrophy.cpp | 5 +- rpcs3/Emu/RSX/D3D12/D3D12GSRender.cpp | 2 +- rpcs3/Emu/RSX/D3D12/D3D12GSRender.h | 2 +- rpcs3/Emu/RSX/GL/GLGSRender.cpp | 103 +- rpcs3/Emu/RSX/GL/GLGSRender.h | 9 +- rpcs3/Emu/RSX/GL/GLHelpers.cpp | 5 + rpcs3/Emu/RSX/GL/GLHelpers.h | 93 +- rpcs3/Emu/RSX/GL/GLOverlays.h | 378 ++++- rpcs3/Emu/RSX/GL/GLRenderTargets.cpp | 4 +- rpcs3/Emu/RSX/GL/GLTextureCache.h | 1 - rpcs3/Emu/RSX/RSXThread.cpp | 77 +- rpcs3/Emu/RSX/RSXThread.h | 22 +- rpcs3/Emu/RSX/VK/VKGSRender.cpp | 204 ++- rpcs3/Emu/RSX/VK/VKGSRender.h | 7 +- rpcs3/Emu/RSX/VK/VKHelpers.h | 38 +- rpcs3/Emu/RSX/VK/VKOverlays.h | 398 ++++- rpcs3/Emu/RSX/VK/VKTexture.cpp | 8 +- rpcs3/Emu/RSX/VK/VKTextureCache.h | 6 +- rpcs3/Emu/RSX/overlay_controls.h | 1456 ++++++++++++++++++ rpcs3/Emu/RSX/overlays.cpp | 31 + rpcs3/Emu/RSX/overlays.h | 762 +++++++++ rpcs3/Emu/RSX/rsx_cache.h | 78 +- rpcs3/Emu/RSX/rsx_utils.cpp | 1 + rpcs3/Emu/RSX/rsx_utils.h | 2 +- rpcs3/Emu/System.h | 1 + rpcs3/Json/tooltips.json | 3 +- rpcs3/emucore.vcxproj | 3 + rpcs3/emucore.vcxproj.filters | 11 +- rpcs3/rpcs3qt/emu_settings.h | 2 + rpcs3/rpcs3qt/save_data_dialog.cpp | 35 +- rpcs3/rpcs3qt/settings_dialog.cpp | 3 + rpcs3/rpcs3qt/settings_dialog.ui | 9 +- rpcs3/rpcs3qt/trophy_notification_helper.cpp | 22 +- 56 files changed, 3682 insertions(+), 213 deletions(-) create mode 100644 bin/Icons/ui/L1.png create mode 100644 bin/Icons/ui/L2.png create mode 100644 bin/Icons/ui/R1.png create mode 100644 bin/Icons/ui/R2.png create mode 100644 bin/Icons/ui/circle.png create mode 100644 bin/Icons/ui/cross.png create mode 100644 bin/Icons/ui/dpad.png create mode 100644 bin/Icons/ui/dpad_down.png create mode 100644 bin/Icons/ui/dpad_left.png create mode 100644 bin/Icons/ui/dpad_right.png create mode 100644 bin/Icons/ui/dpad_up.png create mode 100644 bin/Icons/ui/fade_bottom.png create mode 100644 bin/Icons/ui/fade_top.png create mode 100644 bin/Icons/ui/left_stick.png create mode 100644 bin/Icons/ui/new.png create mode 100644 bin/Icons/ui/right_stick.png create mode 100644 bin/Icons/ui/save.png create mode 100644 bin/Icons/ui/select.png create mode 100644 bin/Icons/ui/square.png create mode 100644 bin/Icons/ui/start.png create mode 100644 bin/Icons/ui/triangle.png create mode 100644 rpcs3/Emu/RSX/overlay_controls.h create mode 100644 rpcs3/Emu/RSX/overlays.cpp create mode 100644 rpcs3/Emu/RSX/overlays.h diff --git a/bin/Icons/ui/L1.png b/bin/Icons/ui/L1.png new file mode 100644 index 0000000000000000000000000000000000000000..8a1d87c5401994ee31aecba235b7b539e007e414 GIT binary patch literal 2829 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 dr7Cd#m;A&1U4tqKJ)X@4z%O03s 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 yyId~+X17VR7t8*z1pbab%A@+HQ6_NGyh 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 literal 0 HcmV?d00001 diff --git a/bin/Icons/ui/circle.png b/bin/Icons/ui/circle.png new file mode 100644 index 0000000000000000000000000000000000000000..6ed88bb0a6c56106b9417aee086b6fea70677412 GIT binary patch literal 4307 zcmV;^5G?PBP)^{q#f z-LyQI0}v9~kGl7(^S2AnGmu=2)VV$=VVz1AtaCM;CbGn zOWFy7U}sf;k`)2?5h3JG-g^~AfvT!d6a}&@gHpQCwSpjk=Xr2l7mnk=b=@MO|IsQq zlq?y2#U~=*ai|TW7WKX9Du##f$$U(uIT& zRI61~Diz`9R8>W-Rzs)LLA_o-GxvF(2g5Kh9*;2|kKy~i@O`t{4Abcpj4{#6cl!PQ zzn&|Q?RL9asZ{QZoM=T+&}=q^BHZb8mIhuH9UUEEI2^(-457%6$Ky1^wA1hR|LI(S zY`5D@P1E+0`j8NUN~MBowJHR#+wGmgTwEbzR5Pr%&f}sq4BJjYc9cKN<`MzgiiPBAqD9 zGMde1j4LEb!p6o1TCI66$g(V0mX*tOrzJ^}P%f8IE|=$Cd;a`6Uc7iQCk^BASmfX@ zq%)1rdTAVa;^OQk#ktez#6awJySQ-S!kqXRhH+-`J&V}aYPFc! z@jMStpFWN0PT%)291iDhvhVxbMO>p?AiLe}yOJdB<>|zYjSZ}=t<7MIJg1%JS!hX;?kE4A1hL!g&Ve|YOkNofL#KQ)nZzXH^5x4h z6{IMNNUygDA@|RuBiGi}{tLhx{NL*Jddv!UIvrfPbSWk_7sHrU{988lLB2e}6wFL6ga3F7d$_yOoycAQ_R% z0Utky7CO9>nx^5}wQG^z7K-yCZp}5v!8@jjo7tI~T>77K84olK`#CXs(oJeK8qt1c zvl;gH_oMX(L4eU{6myFhV+Vu5;7Vc9?sPgI6GCqCe`_|IF=zMc)vGZP&x ya3n5 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 A7XSbN literal 0 HcmV?d00001 diff --git a/bin/Icons/ui/dpad_down.png b/bin/Icons/ui/dpad_down.png new file mode 100644 index 0000000000000000000000000000000000000000..dddcc0d479f410730f8b7c96cbf69278eb997c54 GIT binary patch literal 3618 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 literal 0 HcmV?d00001 diff --git a/bin/Icons/ui/dpad_left.png b/bin/Icons/ui/dpad_left.png new file mode 100644 index 0000000000000000000000000000000000000000..78beb6350ce0550f0d28dfd1c59ab6b6d6db1e64 GIT binary patch literal 3641 zcmV-94#x3`P)f&q8h%EktS{P9oH}q|SD}nF=`HRpN2TWi%^m{mE$%;X&bf4fUfX(4^=+M=y69mY zpgk;5*zPY__fjOOYfoGo&bqOySTZG9{Mds;?#ytgFH0hW>H!RRh7w8f(Y)_F@B7RM z7-Nhv#&~N$!oC3(g+k$u<2cIz?gPjT`En%y>x7V%y}iAkhJ3BA5g9^SWNcyhM2IL6 zLW)r#B@%>$NQR5ZkP|7|ti|4LaKxHW>gAi^ZU68Wcql58WTyMY}}kBSeHqhNfxIG;JW~ zu2d?hR;#cq%OmB@n>PbF_n;63LMWI~;yx?@n3>? zQ7~CF=)la(%)l>p_3Bmcf7`aZzL!xU3WNkAkw}Dn3nNDqOjg&ZR;w~I(-G-(dI)zj z97KT-$~H{XL_8h`02qeBJ%@5#xDXR@L=2LYl(NQrH*cK+3?S1=5NdvQQL z+=E-5WnmZw4i68d*N76LLdXi+uq?|X!Llsu@9%Tru=~Je{(%sJ$;nABA~Q`B`}_Nz znKBGRX0leqjxf-B*fIo^N~M>%TrLOTuG?o>78FH+q9`y;6ZLu>SFT*~-V=+(KnU^t zjf+esgNca=u8&gcMQj%r7qZc@LMi?GKtEKUhy)=3P%4%Foy+C!1DJLD++tNM7DK&W z2LMb?PI~WARTYloc%vtoO#1xB@$oT!|NVCWO_I;g&&5k`Q%XbCVzCI!*4?{z&lNuF z!l7+2HlGl}wG~C#{^5ro`qI`ALhd_`vyGN;&T$-^pPyrDY6?KppJ`Es^Ye4D1Y9D7^sU8OSXju~w!O%7$?6ifOWPnkw|#od__@k`}XaQ0*HFOj_1#xbAH1eWeo09N}tN!UnmrAIF7Rf zpvZK~9&kShl&$&*888`vHw0jLYisL??4YjeUjkTW`&i$QN~Q4r`|r2Cz_Vx1JYR8f zaUnBRE0oel0oQ+srjsA)Gmw$O5i%k&Sgfh4x-2tsx~~5R%(u~vp4@PEdU_f+Zrt#l z&@@dP9vG7VJbwZ-&6VpMQU6YuBzJ znN0dFP^;B^c_CS!nEv?gyYKM*_utFzc{>)1-QU>Q@Jx#6Px=JsH(DlfU3T7NGU-cV zim4byQGBnF^#RSG!WDP}z#o77!5wD_>1zCGnuh7=Y3Z?EM$qfmukrfzYXHDUAAN*d zw{Br|byYNBi?(fV-@A8jX>)V)Dd3-<^Z{((uN_St8$B?mdjan_5);+KF|$y>U_rI%bM z-ZFvGRa;Y2Qyv+Lq9C8oV}5>qAfoSXi)$m-_GYtLBoYZ69UY-wuOppK?WfkG;kLO!1lMWo#qA*N}1`6D;U$AS>T z^$|ii=a@_;kh-!WRSq=$N%I?{PNz{Q6gcNO=(U9`*in&6rEvT9Z7zchG)Ml~+1bb| zizBZXT9zf9r^Ms&p^^}Ud_M1sqD<4o+1Z(NL=PLGlyDu#L9JHvImLeM?qwiGQngx5 zmj4I~kl6+UF2$c{;QHHvOMmG@sN7zfTLwgz%6BE7< zABNo)M~J3r$Ye61Nl-^l2q6T?WKvpJI668S_3dyX2p7p@5>r!Cp^V;42 zKB#3`*xTFV)b7KK|5>N~=FJ;Cd-lv1@u{kcY&LtTf>b{k86oj_9LZ$TX9^6%z^}jl z%9)OUMB~6vuh+TyOto6Y&p-dnZ7KjXzd+e+wkLu2WpQcxWy4xbH3yYGPft%dG4;vG zi7b!ho@3qD*(C@eYpu=6Jk;xTn5OCFvn2w;Zs`^HeY~py? z6<~c45|76(HHot<3&+RDTq$#Vdpoe+?usB_4m2>;uo6)xIQlU^N?(OY8 zEffliVok(VN1CST&Du{-Pf@K_L#c^$MaZCXI^}X1M@L8AWUzzaVP$J;>k;7TrPk12 z2{-3B&f3DlLUDI@_a%T2WWtGhy$;K=JYhq%TE))J4)XatnmX{yq1&uPSA>+yWuK#pfKq-Bs>-rW16Xqrr-MYFP z{mHf&l$kKu+zM#=MpwAk30)Cl+qN`b?-xBPP98$2r^n;*jx}n_vcxaNlCJApl+q^v zKq(D0VU1JAWdG{zi6U6xP(bocIEJbd`j zYrlB$f;&N{c7T6iAyqB5{KggpeQz%_Xt&5D>Uv|bSaElEms6{CE3g(x?$lXf$Nz zW{pz%iP+hXzIOoNq^+DGrj$O{b)8}~8w#(fs`^*fM7d~vTR&y{lF6j!Fx@pGu8n8s zMag6mxm*q>CnsDPh9A_4zA!UhD`cJk)n$j#5 ziw_~Os{;Zhge_vyY_>&;CG6g!*rGtMy zo!UTiMcIms3?Kr(j=-Xm-^gaOSXfx#-czg9yvQY!$@s)+FJHd&<^a9;lduq(Rk)q} zMkyK2{krfHtJ>cyXgg-8%mC=_m>DS-CA-tPJgpy|X#-By4JLZi{( z){n$uF)#XZ90&1u9HwcC^NZq@D_6cLl}e%S%pbbQ0RUR5anFgn%K}`;l6;j)MLKD- zZQEDZNT<`t=ks1$Qlia?>G`k_2|^$^zH~ax{X&A6+%pWLZNk$uP2U2cTrTHr?(dG8 zcJ~k#qC$vIl@2sb)$Hsn_rid!F?^_2tG-$B?Ch-PMb>}ocK0$WM1hb%A2JB1-Y&|L_^YuG5i#~5V?+v3AS5)q9Yr4! zh2Jp@^dVy1JdsHBEb zJ#6Df7RUdjC`*)NOWw7eHCFLbhBupTo$k7mjO`L2z9PU?iPJgV25D350ZE(nm8s6# zU9in=5#X+obzQo1nqW6^B3f7@jsrWg^=*-yD=acI!_lWHk-_N)81PUMhvc90-prdf zBrs*llqr80B)<=UCX>nBCxko%un8bF=IY7-o=K9lQ!Ez09dlg+6Bt1{ViYibA$ZD? zBxQplN+1OBQ4E{Fh~m2-0T9C`5TYo4Rt^A!kVoLe2!%pOBodHic`S8WmW67y3ez;{ z%_&024uITPDjksVobhmM(uHx00RXZrBauiT6bkVlWyef$JSTGh=22zmX&fs;Z9R zXvRYj1lb4GyD?1@;cyrLP^nZfJ3H%qvE6RNFpQygt|$s42rVkt?FFVj1V9fTG7e}F zraoj!U#5Ztj35ZYm|m{;JdkvMnhOvhp7*lG+wtr zulc_dByjjC>O+3|=_e-KO51h81si(*y~6ms5Al0)3J8E8y`UX1 zFj^dnpYz~53PB^#g7A#eLPq(Gp9Dn^#*f(XD&A!AajAZ&s_JT0_C5tMG2i_M5F9~- zW06Lqfz#7d=8LgdY@qQ-I2=YS7Gt)Zo}M~pFo+c5dz)#R2!%ojg+ee41FcpI^Yio0Z)919BuQwu+swY= z@i=B?W|(cdt~)6qC{qLTh)QAK^5CABb%l}TLQkwv7+1m`mKsX$REX!!M zS^&UISSgAELWt9PqS2_!Z=9T*;MZS&1#l&Dtybf!-q&^gFW#TgXmst^cQOihah1fT zQy=NQ%914M#bWVrl!K8ZX_FAL->DOX5Y%cl%+1XK0FIB3kw_#MQR4A9B9X||$#6ZY zN~Oa1jase7JM*$6NlaeYFl=mW+#!T$cDeJvMtwz8)+I@LxVN__9E1ux=^m$d>fi*F zB05nbeFQ^1MyZ2;wcW>Od zfjf8ZxOQk529A!7aCUYE;L2}QDiz*T?d!V!7XRA^4;~~9!`K0^b-mLX7?&uF(e0M_ zOplKF5pKV7AGR#ZTF+!MTYO-zpFcS}J9932F9!7+xm@nPVHmrhZh}F$d-pC=e&dZd zx|W|leTrAFUeO(=_JDy-0kfyQPE>)a!N8C|PL2`uUT74C*(!nJ86N z@&5boGxhNod>HEWIvziM%!`6Tq2TDyQ;=jb>3J5qWm%}zYTa7WuCD7Ja>wNICy_{` zZ_tnsA`-*pH?rC6KOitsckkZC_V)I`;txu_UdJb&e8Oy_1?d^jro~uXTomToUS3|} z^769pwnn1?(=;K=GG=FI(P%W#Y&QAbZE2deE=kg+-3!xo{bNnjvY*W`Xd3{WpPxHf_WcANXo4Y`Od_355AxNn z7>0r4<6~!#W=CQLHh!HDvae~{mTg*ie=-7$-^k^1?^>3%%kNKBRcvo>GiGUiejW=8 z3q50J{fNb4Xt&z~rNRzmkG#l@RTKs3bQ&uwE6&Mjl7T%priwyshA zmfhmA+3aIN$TM(aq|<49_St8S7_uy5d3hPHz4qF*#IPCE^i4ds_vX!;P*oMBQVESl z1F|e*Zf>rd1AeG!TDD`to@<)6i6MJI01CRU|EHIU+Su5@d+)vHlp~P{mY0`ZW@-|Q zBuKN_bS|FAX7bx)S!V9CSS*IMwKbGVCC0WYiX!TTtrJ4_GnveKu~>W#Ksd7QMMn&m z$+LS=>+8R3lt?77yu9pc6_#bWdWV7qKTc#oQIwIUoJSJGvMi^UBDW z3RYKFnH5blGcyQ&{|knz{0|U>-VN<4&9L$ zHEDbzCW{b)i;D}Ws_L?(%gf71BoZhVi%!UWZf*|MYSk55zXpsL_wV1w_V%_@mSq{~ zbXsWV+wHcO$I+LGPEa)(4P0DYxSIOx>@4oxyT^1Myl|x5pnHswNCcTo#wCV5j+vww zfDbc&7qK6T2iRtrD=_@gU4E=dxi(dbBP8%O2Zp_5Q3QZYig6v3<0kC|2^j0w~EE$x0y`l4k09q!E>W@S(cyW^ZCPE zF1JY~{s919y?TYuKmVMG_@11cpw((&X=#b+aZT!>H4~o?sn_eyTnN3bi;3E|r*lV5 zQL-$noXKRedwYA&H#Rob`8knBqk%8K{F0dyIX^!~v)S}zPGle-;?>}-TrT6_;J^{X zvMf|8m2P6}=(_$^u~aVUFPKjUIwp!v)v+vV z2gA=4(yVv(@gV>d3WbkzxmddQKmW89EBZR|YSAvHq@jOSz3%tCUYO0c~KMvi9`abs`mU#BoZP=ywlky03e^wA1aEH z1t&NFeEs#;j0y9KCr^aw%np1DVe)B#t7+AEJnobY!*H!7AEqoon6dMjp{gn}nT%`wIemfYRl7EcM8Yvw!}t@w2!tS+Oy&-* zdUww4ttg7q-42C9$Ye51R!SEWgm$~ltRInO+39@|LJ$szVHgGh~I8C9Dcvy2S9KHVX*JsP$-03w{9_?b$K*~ zi$2$w+#dkq8P5U!lp5z;F z_`2vP`}l?Lf=pqV3KB2^Er=IBWJss34PAl91VNB2U7+o{7rm@rPF>)yw4p2Tm>>u; zg<>j5z?cfcqWOIQL3od0mM+kCop-=9Gc!Xk4d7FT_{Yq0XhZFM3nzIXUU|vF3S&LlOj&m8A=`k2p9uz~bVfXG@hYFE5!zLP4Ap34$OD zwOWmtPq!ZlIIg;aMi5LE^CFclPoeglTCFB}jE3(obqPWXc?2%OTl7@Z2_B2D_@0+F z;RNqqZ0WlGRsXMz7vBW|fO5I~tAnYMkxh?qzL*=qnxsYElvsF28TXM_R zq8Hnydy_^G0bZcFO69`*ZnzwgsB zFvS#8O!3ZuMBM>5*4NiRu`KH`fDHioF*m0Icuoj;y0^FY)0o=|jo1*f=vT*>gEbBQaGM1N@v9+}YQ5470W~0%-lP6Db ze0=P#&RdqX4dAb1X*49bC>!rWau>!;1^|enh^?(HEH5w9A8m}S(U6FIgwUYL5Jhoh z~{?DDJ}R*ROjXNXTeIZ&Q=S03k%_^;xso^prVn@b24%MhT;5wd3iZ>U&e*V2?>JR z0RzJkLQe$l?(Xi@r*R|JgwPm6-ERBrd?JzXtu0vKdEQ6f_4T!5vZjOtLvMS&$J$ptgPJDfc3jN3a1fVF&p(IIp0DrOTs?{o%mzPl}6kr$zTCEo5=H{Gp2q7SZ zc!&ZzqlY$WzyA8GlRvLiD){loAL%YUEtkvx80nW960slz02D>}k0ePO019^9{{B8b z`|LA_qKH-RIAmY7FZm|ZJ4H6>9z??)3CF%gKxh120+K3(6XSJ znHdOzV4v@~UZc^#+qZ9>^3Ki?mu0!{^tat^qgt(c{Km_d zFFnT)LN@mH_Fk~g@80Zf1mECcp%OyYSfuz&)`-bK_d1g6Zxz61KA%5RRn^xaMNxhZ z@+X#Mq0wkyettf%(W9dy$21M z1a)X5pU+DR3k&6|t1C}Dqn|%93sz6%?Mf%Efo+}_>-7|d^!N~K&bm)ilb z-R#HYNzxOb zlgT55Y!2HtA!O6e26nIM+qK2>Jm%)+95Q&GN3mGM^78V%$gq*kWl>D34P?3qA?3Zjz5fQ8sAMwfS^cNe zX%vgavF_sPg`o&(Hk;1PW6e6~I*#K!qd*iztgNiy=;+9CaH1$e5CqmNY}K-?iY&{k z-PsXyWgUbr89tL|&!XIRKbOm)SSh(Hny51ARPLC&&53wu@wOY+5S^$6t4<2A;WyQHMv@IVIJY>+mi|2W) ztgJA}XfztA)oKHgaUV=HL8WOLYPFhg=+Dm1;^D)GzNzN8;J$V`oyNn54}D=6(=<`5 z)mZxw$BKz(4BPECYPFir)FqQi$g(`??x0vK`m&F@uA^3~4ZUOQK8PfRYZwMXv0W5!pC_u~3~TA{RMC^(5Meo02h7LXyekSef8Bp%T_A_UPzn(%r$y$%(Hnl}cf5 zZf@kE_b`}fi-I5^m&^IW61uMA)vH&YJi#~}tNYs5*VlOU>XmPF_Uh}nF)>2Y=`=Ip zRj=2vzrXKUfb|G8jtqBqcb=$$rfJyU-$%V(_Z-J@9CEqbNTS_gac}y0p7+^XK@dE3 zf*>#xH*ep*^_UF9Fj#Rc001H6xvw%L2qC-O+_i(dyF2K*?!>bU!@$A80hX4Q@c#Sn zd(O85pYBm)S(a~9S(bJ0S>sTI-1i+;x~}8oRj+K|s4?+;NTbnkav|=z9wy33 z=MF1TGEGyJWqGYyt-g?Dd6mwI*b7u$*PVy#^70azrbUty3FSjZwRlri6$b|g4jHCt zqF%4}BIBu|D4$lV)x%P$lw+k_Ln3ck)^4d(%2lh?Lq$h-!4_|!BF z2L}gGRh2$;D1^O-p+?B%pEK^Xi6SpjxeBW@g6sGBuX)f#-SVh`PAAaI^&d z5L$IIN(`bn>1w8S-3Nu)MP;iSDQvp6UxO%5Mu{NRNgyFJGc(9!G9&9qKTQ_LagX1; zdE+cbSbjqg1g3rGu>FSnX-%ioSXx@b?Ch+w$P+~os;c7b?95TLWHK3A59RSE zHWWqqUY6xbck;mWSFE+i2$ERa?t;eY29|$rBW#; z63g?vC*_++B%B<4DwRS$pU1_;1uV;Qo>=NeuIBT3h29RKs_OZ|!om-hWfgFLrDPav zDvI(icZJV0s;c_am`SMs_`9Mg-_qL`i0c=}tW=P9ENo)UOP5c9U(h$N*yS>j-YJl<3)P)-gx9Tn?pD z$#YJ#*>qCDnM}qP);~NvbVf?M-Dc@)eTw@i5u07O{rpBcorWySo*9;|>z*5|>$>lC z8rf{tnf?yqPohFB2$5y^Lv&_$OuxbNJY-q+q*r^GAhg?U&x-?Wf1;`?EXxY=CqJmF z8p?Ey6OoG?0RL7xJH=~*7390QxnaHn(2kF}A&Mf3#iCPYq-gVWcs?q`f)G#>2T>Fq zv$MFk=!>B>8jZmDzpm>(*=jzY$Kv85fImh}P#>Z~TnO>0(t(btDijKy16!?DD0!!* zX}$$*p-^!A2xGM$^-nGVhz}v2siA$->|Iti48sVu3&{q(!BP+eXZ^y+l}}Or$ChQFHn5E@e-GDWNq2SOsV+i~o<4-uUnP^tfnO*<-}F9v;Uo7U zaUjHlARjW!yF+M9dIQd&h=a-Mi756QMvXWS($j9!zl<;nxPKL4>ON5(h#&&@|1HPanr9u@T2NNyNcqDegk6)tbnS>Lq+0d&7MYhY|7^lNERe zVLOUO-H2S5HDQ5w5PZVtC=fXzz(fSz{lFAHM}b%n0(eMS#pI@+3K*M(rkG-iDW(X8 Z{{x4QCvM-0NZ|kg002ovPDHLkV1nP)8UO$Q literal 0 HcmV?d00001 diff --git a/bin/Icons/ui/fade_bottom.png b/bin/Icons/ui/fade_bottom.png new file mode 100644 index 0000000000000000000000000000000000000000..dbb7d04b4cf63a93382439116ec1644446130eaa GIT binary patch literal 43214 zcmXtVP_ zakFq&t@GEy({uyiYUHDpx_LiLL=0#y;4cmkpoUqIfI@h2Ck;9awZ3N%yePRP0=AD!gdd9uN4o;12_~WU1zn|8FmY?req2-FH}67G7I>|Uo|S#it;13 zaGKEIn6%!inbxy@SNWu2Y@g*?AbeX4ZVQ*Tt&kt(J;OeAfK!?;KzUkz+^sX{@j+qg zl2fn6Z_)ynquf#-$Z>GZdggGv4X^jQt3Ih@b~kXJfr@d1#Vpo2 zht8cJ5_kcn)SE0z)3oe$DYbMVMI8F<#ju*8b{ohU_!4Rg1`Ev^<#S;JyG{(Nj49fu z+bb(oXik?&1<&1v#p?vIdCwF96nTtd9JCSK)Tdt5ljc9jsO0CQ@gje|E>1_gN84z< zU;~|Q)D806(WseS;X8FeqyV0aX0BV`iSL}gqp`N)`p!9+-F`4;_sF~NMKZ4&k`HVW zZ9aT*G)KX!ODXsG+2*o+(+0&h7T8NSHgBM85l{|w*Hp)`I)7|j$;GJYgM_V%bfU2= zPC$m@q!Q`HC1*wjdh@85m^OXNFfRehW&YnLs;!eZHc480PmIx=i-hK?l*ioTnvN}@ zM4NM+L@r*;7A4!-Qs~Xivk9bev}kNe3HWmqgJ>nZ#-3)t2#RV;ls4JZbb|5~D$L-_ z&Bu5}4-O8-1CU%GS~vzlyP!>P&ef(5t{b|#2gn{lS}~nHT}x!Cg_|Ts&7#f^M!p?Y zn&X!)TKp@{0>j9Inr9`HR#mxkFaF_pTIzZDW+VdSa?)5FFIDj~Z9;0UXXYdM_$7kW z7E`ygGlqY9a_**C)mUfy*H2XRseh1|ggFP7*I!5!Lm`3YUF}{F7!^~htNXs-O|-5~ z+jo-N5va5}CMN3er>@R>jk9Lc%&4H&A6Vz)>W}uXRA}c@Nu^W8rz|RBzapJoW0px( zg}WX0oTf%xVU&#Rlb`*a8|3RTpzBTG8{c(mansy$0Dm_w^8_ZjlJk%6?hQ@Z^PBHA zS>Ti$a&e6Iz8}C&I#yayF}a1JxxyU3O2OKuO$C;Ib7CsVjDU2amG+ClBfN5*_Qb~0 zmnFTUWJ(P4mFO$18?0#2k%Pzclk=ngz*b89(CFGjPD~{{xc2;(^VlTFt3IXV=hjkO zQ1MVrG@wh39aUFy$CgAIi$@m}Ze$;glw;lT2L@Cxs(bJNxyU&!hY@pg#&oRj#e@di z&vWF`Q27)XbQN1z=p+0KGSzeXv})zc7+0=txB4KJjn1}aNJ=@wJH<5>S8O)%;K&~? z$?>9;YZ9;O=MGfT{@(C>uiu0!-W~vXenJM9b1`d1McuEF>GXs94yg|Q94!Yc$c+>l z4KotYq_s^sE|KM=^plNwj6^p>onpM2>CDICownk;KPE}(h7TkLwqvjIif5mHa~fHv z8Z@j2t2pshx)L(FuO>g+YxF>+YFLPA)v2?%ly$sd6;X+R{o-;)%36+|=IN@kds}yF zht-3-$t;X0!dd)SJ+pg|1SG_9`PPoAP4pI^mL*yG^^jirT%wMo!WNkOx*Cb?JaF>x zin`*PF#NS6jg`g;`Q#VsS*mANpyvT@b^U;zD;bT~E`C|E%xIa3Xj?Afo4U>1VCR(m zq^kRv@{NlLlin@SInA=9lcr{{;7V|xD?Gb+G%9^4#Jw?|Uv&2E>}ASXIdp8EagASb z<3qNe0ON9zikP6Mft7X@^qC2CS>QK*@e92U#w&$BjH~KEdf9sU{Bq6X;vl~=qJN!l z-C37ni!*CzoghMlMWCYFWUfSrnN@5>;P7L^aQu~#1?|kBSSFO}jn|2x})|R+oICzYVM>X7eO(D%8wgLn0?;fEE_k>uO zerpzbBD*1f&aTFaomSCjyY(Mi6NqosOfAtCPP&@7WjxGJeE4?bvdvoVPn3eJi+y+N z#<1|#&(R8Kf+xTz`dIxgH_-$g*l^JKo6xxM<1wqv?)BYM#Y(V1-IxmZ3Kyx~BofG6 zEVjFiwz$L53z3YHE%kCuwbVF0vr+6{<#UC18`r(jv^o%go;E;*`$k-!Z zdAe~PUPaYXymyTy6_zCQy=)D!H3RZ4(>A9@NXSdvsu_brcDhT29+B3QLV06c?gh4Lu)A zJbh^6K4lNC(4}Tm+puj|({ReeJo;E;Vs(|_%ot7^fpve;>RY%4rqMyVVbroJ@GB_v zhkxoC4V9p}P^y;GDF!uxlJ}}*$$gxJ+gO<9sZ4mtL=Ez_W9!8uNXcEm9vbS@p{r+d zma*LJcum;|eg12O;E9Oj=%KbWq7VdfYibd9v11#&RjZCx2=ylt%*UMhUX(9Af<}kcf7yq^FYW7ei`4VV&X;echO&Vki+;M`I<*H=tTTTWjCT$FZ%8qHO~M zI{Wx#$A(s{L+r&LQ^;l$`RUf95I9AUj@W>8a8Q_EVQQ#AdtqBpgu~YtiEWgD0m$Wj zwPvT7MwZzYM+b#JxCJaD$!aImAvOz0=MN$&OEovQ6~<+4QJLuW@gX>3?ay6W&C;5C zwW6xVqI*XQ76mC*OeQ-xNM)E)*ey6u5_u2w7<`U^UmB+rm)%`j4scB-Rp^rb`^Qd1 zV2%lZcwge0KY6BQ$FVpVGOZ1O>0ZOOe+*+MrK zz;!nF&|F@gcYo)WQ%@x{3%e`ETZL+*U7M*G@F(rsr}J4aEW zLAjy-q|OgjyVjlHFFGX)HKyN|A~q`Ic`?x$ar0Sz@7>oRbpyJc>Ybi7S@z|`s4YJ$ z7ZKO@i$!q%gx?9_>oTIPbA+>;R52bg0pp8SDwA=Q2|?9q6eQ{fz9r88bGJysPO1F7 zRdn;C-J@v%)ZC0tOp9ue!fqyMFJjrQqjodms;fGIOFHnWX8N4RINxM1P2g9r)#dMT7pMU5lNtssuUUR} z!%O3H-;JOwpf&QMLf~S9XVl{e_h4xKUT=I;@CC=EY$&`9Y#ZQAE(`e=qi=1JaA2@x zX%ir;diZaLvk=>$Xtnwo^kYpU({-Lr+WoFxqWDFKNm|A$P^q?%N8nYG&h;Or%1PdH zpYNC)@^suxo9S7R7O424x!GV*IYl4&)Mi{=%unImqsuy3I(Ai|L#I#9#f%*`B`%Gq zqd`e(4Zv=heLA|xX(*C0cIy1d9(XJht2MR0BNLQFpzK|(;0;RBYMw;n4q`kbF`II{ z2k)Aunqti9A|r*iPoluxJM)kUUs4nev3GKncRyCkiP53NiY~HZ@7(@T#R+tOzw!$N z+PVSwt6njs8JK5F8NG>rWfX0NBl&dp%+CXQw16H>aXO?w4z5|#R-4gOu_^t6V-bgE zEUom}vkiVALU79-wT+{q;sr2~W3YPW4d`Wt6KP-lUrSUWnt+ZToeYc$58j)jb4w&^%#tcAw%5u}C2};lYd@@6L{G`G4Q1y~r@gLf^itMka5Jzw6YBa`X0${aXpS~pcX(zsW@_=ThJ?Y8O$=;g6^ohWN zCxM$I3m^_NM%sn<{M2S%?`7e|BHN`R=b-)#Vuf4(JVfs=ZBMKOrSuhJxrx@QdLno4 z|Kik^MD{pYue>#Q+OFSOZH3Z#E7$vX2vQ2$#zlT(?3mLrRD!6G{@i|Fv7kYXbv^pk zcPZ*-nPdRN@-vr($jA`NeqM#BygmQx(DbZ7;`4D0kG8ce;A!S>qLB+NPwZ|k9#u_u z5`cd+qS=GZn|!k$)gRHJf*XcUV0&dt%{$#V;w8nLQr7w)xDI^~mAxq=ZOhimy2!^u+ahCw^Xq*P7Z@L(MOMT>xtiRJdN_0}l_AXw(eERW1Lsor zE3eyutMKhs9fdu4gA2GjNov>|fPQ?!YZcx#LFs(~qq(+EFQh|4V0v|S{>fKEQN#~8 zy)gDVp&o%w-s*dutV^&P5`DL$uJ`EVz~?kPyG1mQVvhm`el12BWhFGgd%8u4$$3+4 zRe$pLImPAmDjzIKRwz&_@bL|W>qR^yo5*L{LaMW4j0m&Gs{*7u z5hlu)@LeJAee^3TM#FDcNSCjkf{pMFN03=3i(#YSO1!y{{05UBG!~!0Cl4Ctb_g<3 zOpm@bAb23$Cd>3%Bhuu8gALarxlp168c_ErqU+f@Y}xVPYP(~-vI(Y+f;zoOCR2H_ zc#$@9GWkpd+bTT}p$m1PElNW=TDe=#Gia*R@6_|L{SL&e5f(+HoV8FF0E;QBfs|8n z@{gkv`TZIapm1z6P!d5o!u4@)x0wd%HZ?{%_Prb}wu1f#Y-mR$;?lj&Td0t81@8RBlSMBMuaKlbeM73@nHO-~45TE)ylZyPdF7bjUV z+*u(egDRfW&=*=XKaX*#c;MkuC{+>x$9+}6$NIQwu3yKMYQ4lbW63sw6rI5bhzB9( zXQBNZ0gG=;#g40@EJC8A{t|9npD}WKvPbZ|u`=vr4ggrt%3H`^#E~N2Pa$W*jcs^x*9Ue_KRodpGg;)#s zJCV+K$0B=fk#|LfC;FDX2;o;Oja0#2j~1uDWG}cr>uR98D-g^gP=9(2?-aF#{_^oi zAeEDCd=lzoCnfFDQ~_N$;C%sb9-VC|Y6q}ZE5 zS0S?%XQktP;JdH-T+SvYPELpw;*tRD_i_mUyI&-#eOy8pmZffj(qXrb5jV_Rgg6YYwdzmCJ$mft3bYV29; zeuulSY|byi4`>2o@~ii;j62jq*gTkvMG=TVMTR|gUbpUu3#r!rigPssy$=oJ=3WP; z|K&P;-oXeF4So&Vut#{h3_9@0MYuU>dYpH;JYmNSo*^R*t`CV$m}Aa6(C6d0@HBel zzQSMIq!6Cx>Q+O=WnFWpL-(cf%~u#i8t=%9d?=3N>^V#BVsO55&4h`i`Xnt^dT9fB zaD)*4&{n~!F{#pC}*e@H+OaAAHL9$>XM<6d<81`(p z)wd^mh%0cjT=ve;FN_3Wh@SC2=c$tX;NHVACI?lR|bKX zjr75WXDe-4^3zM6insq`<`j;o7kT~b(>gnXWJwD2DyAZtKk(FY-lYtk&Jix4aJv8f zbh5nhinQ*{Bz7%kRT2EEauq=t88kr#I43#cJ0-MF-L2(*w{kv zID(!CCjKJ=z6iA+?Q7Pr9Oo2}|2vvg-GAVcy$u3l-!UfsLX5G=j_EGW&~mo?MqCt| z6i@_*%&ufKQpu;}GB=Qv2|~>i&mf&h?ka4?>uV$IOTQsBj9RHkxn5~vie^Nfkk4{z zOdz6`H+aDO)=a6H6Wi6L&}%WI5;g$`4A~A@_477)WUrDvXr@IWui3eQ|bJ$;EkrtTvt?`JB$WUmg z?vEI3c#}C{D9j(-ff35Hn?NL^_Pj5w7jHC-NCQeEe5uZH9sJcdyoWwmJJ2bEXBaG7 ztp4`aV7{h*D4niPY3h%@QlPE?+)gQw1bnvc^H{xi;+3ykwS_jX&{L}C|0*&IhouYuB@F+-1bJ5JK65m z%y&ee7)_0064X8qJtO~FCSU@xycC8boHV0>yiJu+SN6W0I~*+qw2y^hUF=S^6#o10^6$QRSGlGBgnG4RFex$7Kk$SUh!sp!yZbS^w*hmMDj=k~ zvn;h8C0WKK#=ZM3cEG>@TsLL)cFp-g-ofXso}g=P5+?u^(VDbC4&PbsUo-~6mGjlg z{-&V@N?={XYCKNe|Ljm{v?UN~-V0XwYi37lneE>g9ttg~&cuOZDg2`mhH|tCcTm zpdIi&_0+qNS~OOwnApe6@ylr^T4^UiogObCnW8aTQ80Bc^jzw%rZsD%kU#8v5~VVB zg0aowiVGS#K2F13oC;Sad(bjztQ8DYm?)Noz&H#sVpMh)b-qOMr%)&&4K4ErFz;3mXh2iDR=^hEK5!N?1EG-i zWCJizj6#4+@TIB!qQ>7-6Go%TyE{0QWeN~O(vr#=+LKF)kv|vJt$ci;z8)8)mXou& znR9;j+kQIXF**?2!W0F$gC<@K4`{f@;ZG-(A;NJ;i4g0R%@vM(OyXX6$kjyM=`{_! z+vX#E(uAW62&~$f8SId1gtGy->h4qq%rq?ILCBtSs=o1T^}`pn$&t!Kdm`Dn{;exu zrP0cQzM4q;-5CGkLkHPkv#)2y%YWIPSM(+9mTr4NAy+4xF4pva)j{nh?jk9BEwh3; zVkby#Xa))R$2l9Va#pLDQ?^hfJ*B(wFLYjPmb_pOTb96mBN9u8ZJ+N@zQM1YZ&Mky^HY$qn%gg~roC>y`(uC>#2>nro4#Dgi^^ z$qqxXzVf*jbAjv`$I9=`m5~Pa?FTt-paJAo!$MGz4tyRrN#<_=w^|7QL^JKkFB%GM zHa%|jSr&0(^?T9u`$($9pVm7Wz@Z#4w2v)eel938TZ8SU8 zTIWs$4)GBOq$cA`UkoW!1<}h^LjSbRT{2iW6!@}Kv6WIa{q@J@tB1$s|Dci4kLHxi zqOuy;d-^>xcpzSOJr*F7a;_|fXjmU5v&R|oj@TEB#&L)Srg`o)iN4eh5yU8vr0t-s zC)Ol0(VM~v5HBRpAgLfNp7MHyp*sxjjpNhJejkQ&WN^)ZCXf8eBfS0HgMJ-}+f&ZA&X} z4$5Nt2vvcY&D0Ie_tgz_HTwPu?l72StI>7Gb%4_{mQ_O(*EgPOM^uK1W!~RAdsri0 zGeL|@o_9+9+S-!G&WL8Dk2}zRUxD=r@Cap(pj*3Ce;IhE_(9>8Tk+X4u>0e@p)0_v zI&}A9qfeI3kG){(W9?~I{tC873;Mp2=dtrfB}9J`ny3p}WL9fqiOuVL@%X~5*y)d{ zIl9w^QDdU*D2MJj?i4i>ue+(GHm|l_LQtbpB*+vg@)ZP$esO(;vEi{4Z4?_u18^-C zJ$Y($TZt8m{x(4=>|>XRuCfeZ#-_f)vm6{BHKDY`;Ec(lLC1TlbbNB=3sguvW4rL6 zKzUN0@yZ4XryHps2b1ZS5_-esdaIs8Ec@T>I;?Jw>T#UV<@8iB(^iWc6M^!LEe#J3 zcL&~&vUMdz#cJvEpS4+Eo0@;J+Yu%NioY<9uB%U8%GdHUamyNN7qvizA3baRSIA{` zedlpu&e^WTohKYE^D?s$oq!aUpOsv6D)H-z)Bw6-))M}3MS~tL5NPh(2wdi=%1Otl z({r&$$XvnQKlrVYJYu5~b24~7wVmW4%%3ZD!vMaa0AqG>XNYQ)-Kot;@(5`ZtmX7m z=emGsUR!%xDiA%P90AMB_-r7;m?O?aEo4c2n)5-$j+M$d;kHa^!`H|ij>K(xnY+d@gxgge%lbanqf6EnNVS8&Q4&zz%S=~ruoDzM#yn4 z=_~x4G(=yi*J#mI-NU2_>!}vkHKY_=AUsfRxb|5;wVWLx=-)<%)2x@@xP?{eonbH`LIB{kgLv zhiXwM@y68AmRA_ZhK7(Ur*ZHtW;NTEvxK}n|Lc^CYSX*3=Ok%X^r~ujXp{~-8DUiw zV~pxUG?hu66>Q`V)|2x0Ulf3}&K*L~;0aY2amDq#jUynSa;pU()fJ7?%nz6a)AeRY z!^rvPM;5M>Z*p9F>m!Mr03ne4w!+xZv=pEi{#P%0Lg(Mcp5~U{V@PC+y>iK~wfiAR zNtUXNQQ=7D#_tgB-R;t7(1Zg(vDI(Rs&6%AMO!e7W_RLG4PCN9qz8ftoExMg+Yt;S zQ`*)Y-A1Q8tL1}x073W6<`^07K)zzZas45 z&A0K`GwJZj{>imDni8Uvh~7Q;0QMw=b0E9awU@vDl*{I?3AAgQmC*7>HzrDSZ-|7n z%b-5M>_P6r{72S2r`^&B-DVogOn;gzgu~z1Ws*s|!4jE1#<9}I1POYZo}OI^K}cl{ZfxCXra8ALd%Q6C&fr97I~n`++Mu zhG)b#(bV&=E`N)a`X5%nM*+j$KqyWAs+Y`{FI{^1?>^Sh_KM4+9x#}wU3>Cek1l)r zi3F3(D~_ZJSl$a;I?|7niQ4#_-<4;>l-S>^`|qY%s?t8(zM1e% zK(J;gA^D#Xc$-7(XNDukYV*5D3Ac)Vcxu#Cx6v5y(@2m|+sFo_?Tg@ilB1RTEv%wT zB~JKSl^xDhGz!uMo7t-b}oG z_u+=bfc@7h>{#7`U*w5|-%#!6?mjO`H$Qmzd-0zcZBv#pl$YE9(2I%VOGj)mlSrBL zyP21seV%W#m-gt)&}oWOArX=WDvXi3ghssHo{>d|X;b9U5EmBcb$KW8nC?jA6)})% zNX?NQQ6a_RLOAb>40#o?Yeg$c`5cb<2?sC^nHlv3U_!lDP)SU_;yzljO@J-nu099O zZHkh}br~Gixh(TNQc}A;X(Du2Kbih zIp>BH+x&?Amv!jd;1329Ezg%~Cc141m5W85?<#?Xw8uiEC{HY*yEC3nXZ@Ahr>)VNn`6@8uVPovWq-Fwv z={yy^pPUvXxSpG!BWiqIz!!g?{DUeY%tA^qQq>i@Gj~v4IRrN`r=Kzp?l4<^ST2(n z%%>%;x>0i4D}RwcF6ixfQBR$a)`MDi)isDyNxXJ(EmNi-dbf43aWGH>tATQ|b+*mw z{#7Y(N7%lSWVoE(=~J)aR-J@+I>cwUWBA+u#!<4&vMgnKY-qMP1s;21iWDzo{W^Eh zr@XEs=0VDI1lJDqV%TJ+Exo;$4t}PgjKcW1iUsh_Pfru{1~bSw z=AJX%zY_Lh6hnDg!-tTcLMo{&pR>O-@?c1)B$t7-f~ZSm>&DLrv+cfYJf@-d3!8o7 z%7Z#F75`=Wj&yyhAZ&8(K)}V$E5Fn3_IbwoB;-8tFa=&I@X}S*YgE45ua9M|p+q|+ zGzxp+?CE|4BVe!AT}Y4ft6IQD*W&i=h7W{f@CKr2WJR)#0VJrgR+OcIqEb=UJa1DJkKM-KE0Rkjd6vT~f z*=<$3B2Msz!%=(%{ZI^ zE#XM>e~&VQ>Nf4+o4dl=&;1dIUu4SL>mw{QB914)ADSuEXl>o=w`|9k#GirDiPTBJ62O1z#LX3D5S%yZ#pZf)<#_NNVv8=d9ygRcN|1 zCj=0Wi^$PTxkb(%F-Ql|i+e!$=J9WL1LSi$NzDSC3A3|UWH#CV!#Y#f>hxiZJy$lt z!vJI{zf6{b7EAmO+DU>FA;?Raok6NIRyCyOj^1g+XrIYOvnww@K(@HlgAGl-kJCF`5y<3usgU5b~o09)58w92{os{aKdf{GF5mxVMqF<7(vDg5^UE8ilOQa!NRUsA5} zVcixuQYHYh5#bGuBoWf|&orxJo|PFJ*MKV;UnQ#Ybu7Q!OLhG{ z^JUhF!AUEIo>x6TFrDo~=x^<_fZzQ*3~%=kL_gq(^hS0yUXT5i|90G{85WgUq`>ew znBB7JYj$P&O;c`d-SlSe(=D45ux0JPtvhkoTd_esZlUGKWr5!O*RG73B+a-}Try2c z{7Q*BLkj(;d;4P%|3Sm>eZj`9+d)k=!Eaxr;y8@ayGo)Z-RNwieD(bxR;@2^*-76OJhAH$3u_&GWO3-whQqJVMw4o`Nq=KsD>>b+MuM zZ^U#3>l|ujK103o>vj7Dn)`&}wE*YBU~X}zu28P``T9w*kNl?9#p#{{&kWb z>ZYZMa7F37Wy`-74)jRyGc;HN(tEn)d0}{n!16Da)Ho>qC8EnGlB8B01dH>N(4C($ zEVVcLubMF>aVeKJ(5y>)<>~$?o1N(=ps#hkX9kOI&~B?BzbG7G?a78;>q7Zo zoR>h&)V6McJf1N8Be|PDLdP6afxocSGcYIy{b7O#yOY`4*F83fB)px_( zTN!L}-R_BBd0b%)w-<$6ZGw7Q0IZuPZ3!j~J>L29+30XP+n~BMCq4J*_Ue>X*UuLL z<`T_Xj<;dT+!{WRq=xsw370k?aia4beK&lm4Q#>EmEwPwr%sV^X%x z0_-Xfkl0uCs7J=v>Y#`|n|*y6t}=IYiK>{tso`-)=bnCb@8{sXyB9y5KD0Z&yz|H{ zgN;-@T;ci1+wFe8*T?AY@!($Ut;@Z9Vx7nb`w9FSCldzp^FRJ(nz}LLEgYhE6|-fw+{)aI4#%U&j1b4b}* zh*Z|24=XFeJ2j7=@bHozE^_kMw3IrUKZCn;(=SA_XuiBt{k!kzk^kA-dNJ|C#*_;p z4&wFUb|EXiE)IV$s@CPHWYH4#HCvHBeUhoN*`j7f=g7+S586?)b$hKBdlU-1)Yk4~ z%tiXv*Hw>41dpLHa=Hb49muA}nHr)+*%HN%Y3Cc(v;rsYq^>LKi77B{W$g74#47Jv zZXR`5Q$Dg)^rzcx=BC8_RPsBlwoh<_v;d+4Hjd9&-fO!5{VCm4w@XsR86+j4{i=U7 zJqebZv0gn=2%NX5a4nkW*Gjt|;ePaFH$ZaA!Ez(nY}YSqRd~<$G<8$P)5LQjSy!Nl zqO-l`ag8_1W+9%aXc-N)a+3Z9&*XUhnIEtuJY0vuQX_JO~#4K4pRicJy z+%TYyKHWNG1AShWfTGQ>6og>?wBF|rvR8iZ)M_70hmPYJ5O3dXTrA+gNxxFoM*BmE zXQ6Me-j=>3?G%x3d^&o)IHFF!E^@So7YqkA)Vuy8vet9Y;zjL%y& zL=)zeDLH;WI!$6ST_0B{cX!MeCKT}?cyzh`yW*Aw8#!hbqSJOE73rKo1k=%)f^yBs zvp4v5`jX8M#`Va=_~9M-ZiF0hk@rpVo}mc4fBcE3>t6I&pSeZA;L*n>j??fDCBnh5 z7uq@p7xMsP7ZYAYoWpvuNIK4&tZ+H}^8QhT`8zGEgRgR2u>~qG%(9pL00eO{c!Y_x zu~s4Qy_!R6AOEW6J6ji9b=ZKS!$UufK}>7zzzgc)^kPYt@^0N&8RWGBs-3YsyN~0d zmDbF~SDN(qdZ*x&nldE$B4;IN1Jjr09%$LEo{bvu*ROUef&~1Wuf@bHlm`LtYmq+=Zo_K}Y7rQ6UH}$bv z6tlg=6&HM`HqN4yrW$2i?D+1EHFJG1NTSS4Fh51PDChLO{YA;q#1Om9>;Y1^oK;hF znc9qzCF+e7Rds?eKZ8(qo!ScUaN8g;O0`<#8UGn;^j;{N?D=EvQ*T>sK4oyi1l+Iw zJpYa|ROlDuoTdhDn6s;!J~7uUlbFTXO5fAEKRvhW9BNTU0Ziemb(6KFGnmdSGk|$q z7=^9xkH4RDn!rQB5TSl*nJSAB%sm3Zyi+kV|F5!`inBw_tUd{i=z2~T$K%iuVtI5U z)6M{lhalyj-`Dd@XF3KCxfB;4pxK|C?nGVv2Z-SDfDM7iQR4^y{9GgQ@5vx!p4X$> zA@#2K=T#~0+uZ85gNpX@T4t6`K_s7}s@t+|@7!J-H-8@n2E<6EGIf`jP0qLUX3m27 z#Zq=cvLrmH64}3=Uc@2Q%=ivr7KY({6!nzy%^s zCtz0G$@(z<9n4G};EsK+kLkayOG zUL0ekfCu*HxdO`gc=n)-RJPEXGsf@;q+Gz=(*VfyW_hyirp2j}YMYDp-9=~%-^+Ud zC#8`p*#)h#Dl^=cJZ9CVq#6;nH)?7QE1FEbNfM$}`}t_4NJX>D>+^oL$?zS2c>AF3 z1tILAgOa|qiY9C!pNlRIiO^MA}u~;c$(`Rzlc$?RR zZ@>a!)$gf-4lmi=KDUY;=VJ!F@0c9y1mJS?l>*u;AO z=;om0+zY$elTp8{Q03e^YafjH16LtevYZ5#6bICWNP68!rUZGz0;jpS%=rg&fFT6T?8qA{@~H(_TA|B zi$*mkJ{5%nd!deA!iiU}DBq3vv*S*2mXbs0WGPgp<*d3WWk+*}_pEf=sP@^DYtH26 zQF}ob0ehf-V55^8A$Ho#v3~aI-@8X2P8tuE_mvNdO-IPcnPMWSxR1NpOD5c>%0?b? z^c#6*bUHcTSFhSX_u;%1yO_J|Wb*i6SrXRwLprky$p1vUU47`0{>AI^4t*|flH^4B zn4UX4QeX_9{}&`IV2zA3Pg?C|N0Ppp&a@AlJdn9vt~2qo-_`KScitB_7*l$ClkIN) zdjEzYSjy2$T*Sc0_X4AAgPcz?q(GkT`VV!B`eKZk!kosY@)OQ4stTdYBW`~meSOFl zFZxpbQBp!5^WUyETb9jp`&(Ao_vkaaKJ3Nw>1yi<+?P|4aa>)hb(V^JoC>1~C2>P- z4IV^zI;beKLmAMMR8OyQdwX;b!6iTZ!9uSpg7es%3s$FmLZc3-# z({(y0ze@u=e5)p_`I0XS25aO6f=<_P!KsqVzdZ~D9bL+7+LFgj;^gE;BGo8}l zckfnT6c2d^Y^-)JXAxiTeW-6@t)iWrCO=Si7J2;Vt&Jk0#Ppt{^**DYln#n>gc&`D zG3?UsYk7>zpGwEc7J=jzdC$EAk&E4V(xG~PUmE7u^w^19xdysCuV$fD`2NkquDNCZG#<|EzNcbQI>d4|r@X9a*m+^9=u$Wmbvm!R(2 zEa{zGsDZHd%|CB_4MN{k2A_bN`V8Nz3+z2IoI>v|jtzLs$b!NS9ho^F{P&bY{{8li z!0Xz43tmKWs2erUiK%~0>NBor(N&1*sp2=Vef5FvYx`!_w-0caAGhqIH2xkhH*i|K zci#J>ECQC#!C0JT&dHk`PJ2S&#*V5*jEgO&#}coCKOf1ENs+Uj)}5PQv_c!>nTiA4 zBLH;6i=TrZkOltxf}@FKWYm27prw#u26-V6@vNUI^6!lszOSQAN4C4DOo7{GEcX9Z z@4ET=E4m7epZxv6mP%fH>M+%Vix&vLkZr|f&zmU`0Os%|^XQTXDJ<)`yBd^YMX+JX zl#r-=?l%ngObx*36GZV#Bc~Ooay9zhZHwdZ3{gKuds8khd%QZH zqjFWEW6ZzQ{t-z}pp{Q&nJ1>YEvmf++24YcBJa!`{F6TvUh4eb2=cjva88S+F5n^^ z|1E19p#MkMPEH@{vhH&eKd^duhQ9;d) z)ET^ue9n@EJtikzi7V`;c1&%(h$9jEoxK-U;JN0|V)8IzX<9*S<`WQ4j1q=C*Xy5J zZE#YBzlpe1?K=3JnY%xG)jBk4J@t6zxq|{6rZQzee-nm5ibvg1-iT48HO4qsQa;@4k>UGj@t_Q#|> zR(KQi1YXaAiGyi$UK_J{t0U29*dlhW+XkX$~{>t@L>-RWb`H$n8mK~>ZXmn(;?j4SCr6;W=Ao<~Y(?1VO4wOHcZX7*w znQX^CHxXD2=-QMUej9iae7j(0%>Xr!`JqAc@aogsqFL9)hO!2L*OVuwLLJ6~6HPLM zcV}>Rsvj2DF7saoOf5ZAA66c5hqa3*CJ6tKw@K5`>)`qj^3bSQPh-IAf1(UmY!iAzp^gM0sf#XC6rsoscA5v?X|e_E>byOX&@a`I#JLfpE8~=2YHpwvm2e$ zLV#Xo9(B_kEu~tD2!Ezy6-yBt;A&BgQTTm?P#mRqlimtTz{0cS#`D}JCFa<~!W~QH ze(Bnp9&@UiO4(_umE82tK0Z~EC4@NU`WcP;bUM{3$T!*VQceC)@QC4ju+c50j_kP&!4Z@RBlu2WQ zOR?a%yMqES=MD>Jdq(1)y-i(_vjw-&?Bnm!TFoW13!YA@%p4t65veijOmk1`GsDAt zm69p*f8SW_v(f3Ei}b&3`RJ8dDlVq{4LhwWe^eWE?VpnGGa;hS*V#zi)rZDx7E*SB za^U&F$PGTJztuf9MBul4n{$S+wb2vY(V#R`^==K3HMiw%xyZ!2Vy%Om@~h|OiVa(# z4yEPbXE8q!-N>htp+Uh6Wi109vRt7;h;K>rHz<0w-+E%qRm4YbbPv7>pAbuh_VOgJ zV3m>@1PM=V;jwO`M*Y*)0bhiN;ZXT;mhw-7zI{(EHePBZJGU}LcRuZKn-%{AaG6YT zo($ReKLAcZvA?;I(wP%}=K?0SR*tx1<<4XLn0h8Ji z`)xJovq&#Wbt|S#pp@PpewgtsDC11N0(DtZ!apHYI(vOPA7^jE-}wy z|L`7+zR=N~?P)eK&)tdLZN@x#o-j{jFNUErZT2kCV=c4kiE**|xR$85ygD2dshGO3 zE$YGs9|g4oQvma zvS77ywxq=fFrBya|HzZJ`shXyn2uKL&X5*{QtdLJ{cuJH%=%-%v2IoSp3K?{ z2YEST)}tcUDrng?H)jf0FW$t7`zoM20UUb*bcL}S6EcUj z!$L_-uuNXvO=ihWJ+*%jq-NM_NJH~W0_lw8hQ=LqDwhF;5(6pzw=oJ4AL%XkC+%Ah z%Qu#udM;WYPdgymf`G4_y~hMC%G7=j+3{v+Sg10YLl26<8Fs4S4o%V_P0hPFx3CN$ zR)C&L#6DB~I1u|;s5l+e0qIxPGXo=iU1=>hxnDvfR<%*;xuxVC)X;sNvg6fDR@u}Yvt6_SC6gZ30iC=9VJ;`1vH&#jPec_l z$>bhGg3b9+aK&19ga0`z7aNrLaNm=p$Cob_X7v*BWeDTK7wtjDJ%556yRsYf4X2@g zAA8U^2F{@kRK#d)kQ(G4`fA`}m37yi=IDrgjN?qmHTmi0!?w%KSG(Rb-3u6iquxB4 z2gVqQ)OP8bq@POYe-(NcP~*3bUwW$y?Oz6I{L2f)Nf`?e+kn|vtuZOW%Sw3RZ4OO_ zxeMSj_q~JycajBc?XntpqZ)k1{FYA6eeFonRmSHVnigxRN6)1pToK(Och)zK5~^wG zq9g=E=TPGy3n0X$^BzjN>9EpAdrZ=U4cj;N||6=X$*D&U< zl=rm{yk^Y54&PU3aL~|a-1PEjvFR6jkEBV%XQjJb#wuJF8b{atRy+C=Z1I=p^^gAV zYfTV;5j1}1vyNiG$eMq#hsUR{`AjFnzZ_5x$NROuYL}k#^}cl1_6;4z>R*{aim!iv ztU8?1)ja)IJ1+e>7@673(EWd{UC%GsLbliR*>v;Q*_mBrFcEs{`p$3D}Ic5#e{;*|GPi8emKd!fe9;6uwq52&#&L*@y2J^^W%8x^WbyZ zpW}0m-yePkU@a?}d`hi3T(M4AtCaQ!$aqIO>E=Q6!&+Rn>pHFQ-}kYsexH3{`?FDi zHU@hB3QWanhSzCb3w_n+vCn(giRg~Uu`38GkFnYnfUT2b8^^j%`(ukdZ(T8=$~BXh z%r}vh-*2}|*Gsjf0-ck|z5ry;BJbkQZ{xCYqHMWt*9oIJG@Vx6bkFq3PR+JftnOz> zo&}Yu*#C-YjsBU~b@UtdarQ^kPqjCbIr&)p`#!%F`#3Qn>ah2S9Sg{A8V0Z&c0G`u z8y@YB2^r4{@I3@e(y=t(SnEW4{5is$d|K-S6s+N%D^UHPY_R%PlLJs~!JeZw)?r@r zspjwOzi|%qnb}&EA7fgxQ?VcN=>{YXqb|tA`$m|4a~z6z#D6zi$i~FFY&s_$C|<1M zThT9v*gojr(#@SQ_@b>;d93WWG>6WnDlP>XaIA=9b6=`L5C6n=SVs{b!!tC#IEFTp zNARhLt8u)UjupQg&pD1*42-rEaIM#u%DLUkFs<27vjdj>+W*|)?Yd6QuQNPunAYqu ze%7#9u^{0S5r0wfMdazaiddWRpNQpM99dDZpJGoI&v#z{;v8DMT?5Bqy@-r=8`}R~ zbt3vUZUO+E?-|EMk98fEuIghsrzz$HU7U$Ha4Ck2@R_+s@DUHn>n;xp)LCrqc{%%d zh}Xm)jhB~#vQ{1gOl4gIA12|Wg zrCc)dG~pcq6X3CJ#URfc%R^)>pvs+mG~+8>95>s}^`81iloQ3Q6M!3!RV`Q)lTiV-wP32|W(HLs zybob5SldEMB`zx`3$)N+$;yh|XjU{5ZZyG(1BmGH41jBP}&pU|mu6 zdrfHLFfu_xUiZRG-t!w2Dj4cMO)A#?=!i(_j0m0HKM!Aux!NwyE|n^`hlt11=i->_4iV} z1N;4KYz1Xoi!TloUc;*MHNxG3YyAvlyQ{5Nt|mmlNV9!bO4)XMY4%2XtMg;>Vd62O z83QF8R8rDJ0osErfRk&F?(GG?_RS zm4;7Z3y9cT7chPNX68Z=_u#Vfec>`GW(U3)1?VDXfseJfGfG67253BOeC@eKl4y7$ z(k;d|jH>wEI}SLqkR>c*hsN5uf! zSXy0=#7l^V?Dj?RY^7n@!f=gIh5V>+_QaD$i|vhDbv_Ip9?P2jqAeh-75gmlUs<)g zjqIk@_?u#b`MVJEny7>r3c^leUJXrzi_5}1R`zF$d(Hmp7`L`LW^0#X+AEwt)Z}iZ zFSgA-0@(susA;^eE|%rA%CnmvND%z zGobqww9Vp*>u{`IX@h2kG26P9sVTci-nf!&QGP5z5o92|7`O6R_k3gp z+x(vMc%E(CW!3%PU1p5orEJyiCJ^5*l>=~Ga1-<#teBh~r{w^{N_Ey!>U+cm1ZVpB(&nv>T%bfQFD!=IZJ7|Sbf&CZQK8xtO(1VR<}7=G*OjR z`51lKcUEyqed3z@omqR08uEgHMc?a@kdEZ6l@NP%yZC>0)K ze`|!KEfJ(AG_uZ%Zmu2aS=|w7#dM^l-T>IpL}&_HC4LtNb8U_wr;_+NVTihD(~WJV zMVaO}w!_ye8k`m=1XcZpOZr`Kw$sjfNZo4BYrl8p3nyi4dkU3B7Dd;#5TlOi0iZ-; z6ipx*|B;Wbz0W|BJM|e9Ig2IK!AAcyo;}ZnD%7~)$ zgW$xCdHFmb*h(Z9MQoZkAqunIK5#Iq@UDLtncIT-jWCGB_SaPsH?xU=pG@M?z10lY z0wO?jH%Ua%hyz5Qk`z0qPVjORZ{V9VVn-KlA(j%%sl*?AS8eJw;`Y?}_8KzMj2ke# z1ve`s2Tp!?qX~?sjI@RM1%htX6NC$~9!l; z$7M~47q9aI!S`jciTI>M_Jimb#vV8c?ck?en)ELlF1%1sLL%V&qr~9a`B%#K54h-> zMZlGC5GC;%h<*iCoVbf)L?4ovPu#?~(yF;^=8-GgH4tn$lfWu?5@faUA_x0qOpTOE zuK|yX;iANyLK}inAiUz1ku4+yq^SGSDJ|U`S&PVz^RrP>!nv2=MHX(mg?+^`Ly^pe zQ;rhivrVPv?v5 zE6T&j=2Foz{?r^yu$dNNN(tezcal{%n`s-b#B`@85N&}*=@k!I4nBxa405)R#T+N< zEBT~tX3@rI0G#H zj}-ptBmZLM@Xs9KA5#(6i2q-*icVP9oBhYay9{X`lW{Fqe#jkM!_qe~)$eef@Ln)x>p%UJuxpE49)U{ygpX(i|j_VcP#E^xkN? zWqnDHpQIkwYR0()$L{Gi=to!E?WmVdS0n1>3Go_!F4uIf@t7{xWR}p^iTe4Z0Kr)`v3FDJ zJqGR@5bElshZbaO=eYMtlRS`e{f_c;83CF~lymk|F?E4-L{8Z=J0`$-lzmar@;S23Fo0;|28$8`+`74#eJBC6w=ko^EEZY771SLm#uGcUR^Q zf)nU_pJ+{a-?KqU5tYwUe;=6(upP#0w&^g25iYq7$OgpT0mY5d_m$#OpR_N^eoyIh zG3wz|{d9NfVM%n#hg=uhcfoonNq<4{UqQ+~P)1vFqWj!gSWUuu7CSn{J`p+PSvkh?EDha-G zc_|;Zkv>~1mEf0*w=|9#@Y~8DNWqA99NHzI?|IaCnf8YA9ejj7YO(iM*TabVUnp;5 zJtZ+Ai+u?WN*__sNvqQLOX7Y*%q!NtgqeDcaJ*F>KDG_*L&7nH^ANGss27g^Qa+pX z^MjY~Q@M2CUyF_n6=_S6^(1nCi+#Ofzd-4Q%Kb~pU$w7~wgJ%oPlZo*Yy9xj(%UGO zWj!d5V8bckcpzF+AfO2*vSXX#SU zy3kW?6n8?enW?2jj1zQ%eH-gkx)VttZx!SkNzpj~#=L2y#s@*GI2@EF1l4(-oQ&QfXcYBsC!@ z;pioiX)Jm)z*g7^j*V6?xW#}DK!=AOBiG^$9>)% zVh#PHB+;UUBF2~kuDM=A?<^=*g?>sa^%f{p*Umj%qTiT(#`E>ygEMna9Cwe-&#eu7H`UG8FK)OV;m*}C%LY-@%p z$ME|E&k`tzE{L#qr6HzZ^yh`wexXEb+`Hr6;hbyXW`=#j1ujXAi!Lo_^MY7tgxE@h zbx~~fa?(&QzB)VhJ|nV$P`Xk=KHj_r4EM$O^d$z9>7lREi;K!I41o4U;dto!=JMM$ z^u^#9q%73?0#V#vlSLYi(Tw$Uk|r*EOFMWjGcJiO85UDw#tXi&oV+XZ=A+vjyF;0C zLAR)@rJ8+#Fu)7-vLXZSeF4S1K^ch5LwX8H@?<@pwC^F8Xpt6OmnfmUfd8KCno@}+ z;e)&&LO0o{@jkq-5ED_0?1~i|FIIDJBIh+-@F(5J5@#up-S|Zd;65&~)+NleDav-Jtwl@+EBD;dJ}%vcY+1!N?PV;qp%XaZ7B&ed z=I^#(Z6&4D4@UauG8gyKe+q-q|LDaju|!Xd#4v6p?h-eJn3}n&wJ4j2YL%)-*_Y(&Os3B=5q1fvP$a`U6(CriJf`b-pN18fga<$fA-qs?~xh7dLZLekR3N+fdWdPR5~I4TWTV{*MG zCfznv-~T4-AgH~(kj7l@@5T$&v6m(%aOEHe=sh~p;0{YezrvLk@%l|=dF##8^|;Gh z&R}fWEBcoOWL*`ryd~on=mxNstjA2x+IxN!7dVbs!kYI4q(;Ni-|3PS2HnIBFxeYR z`-MR_xVj#2!D$n@bxmbc;*(r54y@>oH6Xs=AbWi5IfJejSo*hvo=z!Ju87`@|ceB!kcKD8hXPEGuCwBzT-S0UkRt!$MTRDt0esbkEA*_fZpTQejOn@ zp2_(sO5j*st{h@R)Jf7;lKDA8{vmi88rmmqk2?(49<*7{K44mw?SH@c8saOCvHt2k z{;Fq=UIG6X40!s?GcV2WGslcC%HuD(Pg>UZo1Xuxw(|F|;j71T!FGmYzjWON+5f6c zvil!B^H*)*E8{)7MvfTg$LN3Q`TeoXFW|^0apkxB|6n|Sg(!%>8@Gcl65o-|B<^NHOgf@#!gI+_g{DTxM>8S)MJ&X13 zT7qWB%2qml()A!Hea&56$)vaD=uzo9xc#-!7zcM zE3{*XcEge$x93{cSYunH-kgs0kY1F$K1TMf{wAGKbg?g8SkIBI!8YG z4|+ZLZcpjrpwuLX9!y+&NwueX46pBX;-yRI`$IH;YW=86eKje`f<`*|s|k~%_C;&K zQyW7(o=WxN18}udBWtMf1-}JLUk%-4D*a-(-$YlZA9k#O&<{!KLgL=7^n3xdR^7@M zxxSo_XI6KR*P8nNiDDwnrxdNH6~E*D1$1njU&qb9Cw)i6zehdP{J8s&ID}f^!tZE@ zmQZk?nppoWr6JuhA+FyC9oa2DWnIJy5pPMoQi z7x*o4p&t{;Io*g1F|W-v!)fao1fKUgmC_SZ?tvw?)Aip8Bq3m!m*g4XO)f$|wk!*i zp$AXb0}v(j5#iU-az6z-O`-Ms;n;l%e#G__5(}VE8I+ZxB9w-bt`Yh~#7&r$N|eeN zYY^i~=(*sr%*zZT_b6ozr<~}dO6V(Le%#8mLphJhWnHBgPVA+~{Yzvq0E535mt!_p zWF;hPH+Kl{OnO4Fo$5i!vXw$5I>KWI<%!HsL*Ez25iX^I_gmrnL;LBO4@+;2&{K@L z%NkyFEVpgv2{03607RygbcKdH#4<~NU*7q->-iXpS5PiN9 z@rE&u^v^w9nP14?1*<3*DIHHlSKZBhz|hJ%rBBTi-jusVoMU}-{i=xjbUev=pp_PH zq|J!*hcsQ5iB35W`$lz~5Z;4_n&+^OPM)P|Z<%)W{t9(Th@_9J^1;53DC;BUY>|6( zeOaZ4ruH~$ey0a^ingTyaH+o`_xc^#NIazOe7R^21V5p5)C+SZ%vs3>y=;u+FtDMb zJf2Fm5&N=D5*KUi;Jot$l+!*QI)>y)!B(7PSPdr`h|`N|WW@|76t1uFbZ=X5D0Jkm zbg%c+MIcBsPVHyo^nRrw7>V7514aZtl2%Ofw3x7U&G)1hy`7vY-BDF@t607ejyM-O zGbzOsrAQANkfV05Y$dR2`j8U0YFx3`MO$33+FuZ9S28y;J43mm!2#kvIA5de&OSm3kFfLQw<|rH6Y2;LiS zi)a7tw z)&eePSf$@dos-yDYt-zu->Z?r_I&EwK}`+U!ljf{CMtTQ=9XX_EVMB|Jq#4oJzms> zaL{vBtahDyyOUL3^pU!dA`2_kZS~x}HA-yW8G1%}4n!q5;dR9&lq@C z!!`*hD6I{z%4#KlhAG6{+@>7``Dn5>{N`S8^m^s4kAcgD8|yhQC=zeGI{tRdoy!9G z^{MuX>@v|l(N_4?%SFq4cq3>{UKTvIp&t;Ww@58vqds;!FK`|2$=9lKM%NiEF1G2gt*;dI z6_G`H?M=7FjX>`yLM>?Y2udh5J)))0O&?0poQGr;CyWyjdGC^c+N_k*EV-4Q7&~+WnZjXwGYq=>0|0u zNc+8PfsIE=7_yo-?73&-++5y{mFJ+fL}euozXITwz|?%!d@U=>)7qh}piG{^dg+t% z%I5oujm|-;4|%^IOXv&A6gE}0-zW7DvVKNIdtb7Dmr$~+srSkp#9xF|d3&MWZ4por zH|RR<%T-*|^RC4ODhu6PXOLloU+`2H*e%y8Ugp${ri2^pQM+{k+by$x&DD?N<0CBKmJ4U)l*EL-5mF(_ZUEaa+*cI{*(xwodVfAy-UMA(@WJ3~C zu9Zvl_0ZoZX@k<2<%as?!Sg$-SR)pMI&2~*4AABioLD9Jqcz=Yx+sl-wIVd6FQZl? zK-XKW(?i#DX3%de>9=z9YGrhN3G&WRI;u74%0ky=VdSW|os#U4K=bnc*yD}Hkzj<{ z(YU#%EVz}-xnj>W;41JU1R6zlQxv4G=dhc|-Q2HgD&?86}VMe&w#b5Su-3kMp zQX$)?@oSTq94_f4z=U5y!@em`P&hIADw~MHG!m;fGj9< zn&mOCX{VrJ7)kpm;rdz2MBFbdS2&^k=NL4pBJL??j}PU%jS#jjL8rU z`#6#abk(MX0|qo;X<3gTPf)^(K1L7_;s79Mu zso>(Nv}52(``GDb3ma08h-vgb()KlVMS&Y@yq3kVK|7<2KXsp{95ePaLoWBLt#!R@ z^9i}{v0x3oUwAQ>H}J|;t)GA^a;K~8w>d;e08q0wuCKWR&&W1@#g{(sdrzJMpc!5}BZS2!NL(*45CrtbBipYefAUx|(m z`wV401U^&a{3q9yANps&s;m7Vyk<0e&HQh{*DqkiwK45dUz*Q6Zy2BBt7<>RG^ zeOb$vuH+e?vj5*13!0omxmf*YW&K}%)+pDy+MYuR^H<~jwYlMMvY8PkeG11uK-mP$# z`tjr{cxp9L=eSsT5a{ZP%3SUb((TWVh!`oYOCq>fAae6l{Xcr7gej74Yl_QR0*-m!H(Y) zu|@FV-U-Q$MAtvbj$jrocOwE1(5o={2(Hz|7b}}R1qqm~)t5s_mL)S~ke(o^1CYCGpo+;c@hkNxXo)P9w z{3_?Qmg7YN@gHi1FT7 z>{ni2&b9YaPPAr=vtIVfz0~wo!?9mc>HF-&XXqlM^&n{du?qFGp+i%XV~3&Nmi5um zI(Y!xAENMy9)Dv7m#)Z3v_idgR_ZYmNE`Q!+Wobl@GI^Gk97m5@LM?fR12uLMInFE zlOIW(;(i$`mBgr@6teNid%WaUUl-3k6v|pU{Kx1mo*^#orAVR2#h_=EC&eEBxkQm) zc^gZ>Qa|e!0m?*YPK0wtu1O_f#5aq?mbj=x&Emj))Vxdq;MWd|E)!nlVy?DJB?aBM z5XMd$anj;o&ErJuf7$gjlvmcf{IpLVm6uW}7jqnb?eJbYC&z_1@6Y3>DAGPlt8_xx zOHv!Rah@|t`vCO%^jH<+eD(%ie=W4zZF?gdh;OW7Uwx^3+~C#Mj57%%8}!~LE5~la z;I3Dj=2ODiiuQT&`>t}zpm5JpUL2=arl*Vck{^yZ1Y*xd!%XfKwuVh)Yr7$R$^v%9 z7aY>bE`8NKjGIfZSBcHg)+ce3#J#f20$c9O{g=WqO?_Q_ zSjejF2dKQzxg)5qhWbcZT#LPZsob7Ql@tGj_ZwWbWkb_pJ&9*)2d&kPL!g)a@6}3$ z+rU`d&>p40+HI1p6CYlzC8^uGVw#q1`M=^(RDSiIG%};)XkA0yWl@LGqqXaxc!`NA zzw%m3H_W)!q$%*KwQ@}8jr=1VwWM=ttBiFQG-X;Z8i9C;x{DBrX85kBJ@ghr*MU#Mgc+#`!*CjACTA!72npdhE(O+R2VfLI39j_G^v1@EdR z^I1#!dTVZqrbnUC{nQ;f=Gpd*9jEvjW$Skp*I>izP0e4k3I`Rny&}4Qk#*M#$gmNR zrg+q-kzbpcqPHx5u%8xnI=`YMuU(A?%<0&z98phS=}Ns{B#GqSPSoJIth(@>B#%yZ zSiX7`J~W`$?V6f*!#6Xy5h(K~-2_?1Mlu_Z^5h;uYf=)@^Nlq&NFJ2utf8j}_wev!QWn-N z-WF@Bi_BkX(yMH4NW;j<7m>LsH|%8t)Et|zMCDrg_rY4<3R={RhyAVh#L7JmDM?=S zg$yFRHQYyD5W9xb>=Mf^f^Eg)?b@(&lKv33_&pd+XrwJTQ2Gc^pnYHZ;)wp0&HBn* zX>c1$j`h{otM-7==JM@m|EkT4vG!FH(t|Ga)u06n=44qb0yd)Xdi9NxZvLn9UwI77 zxp?z#mCeGH`cCKzAUvKz7Qg!*Yikxy2^;iP$Aw=m11{lp(@|CaK8ZulB|Yy$FPiu48UgiTz+Ci#NKE8CRfrs{UD z63W$A@XH?zZ_wvpbdm8vSyb`GvDkpvZFkjvFV;7+u%EBGXpEBdoq_8U-29mCywV0e z^?&hr%3O906{u2TE{i7;@hOIjZ$TE=Cn*2ZmI|fM!FYk&`=cdoFEn=2f~nR6#o|DT z6-8QTXdX?yT-T8Q7v^efOoEEUapL&2f#bUq67N9e!L~?48$8w<&U35Cm}SkNZho^* zaG$Ofx_Q)<;WrHBe2ncn#C!<%UgNEU`%8YU!yG)4=-9zWJnFfH5mf2ZaCed02# z8BgFxu8(pm$@qN{CvD`Fwok|#Noc)g+Jj>tz*r%NL>^h+l{Is;i}i!yuWM|mSCX1@ zC3I|Dyfc>ejyTZN8UQKHUIQuY?}ShbtqpEVxhRAn;#f$V8zVRN99!eaz>m%M==+DQ zHlPQ<`VR5pnA527)tb=vl?k!pO{m{bM0lMFdf6TH=omCx8jOE5fKe}_!AN7mlqFIc zt_F43C2au$#5N$>6mM8Locdb-?U)c=G{dl<+Vxc$XtodBpRNbtp4ZyzD-z6d-0r5c zQH;C6Wq%nbLW~2kM}lq^jL|+WE|e=g9v7Nx=-)#-rLKp=s5KY{vgq229r@Eh*4<2p z{*y@Lu!$3f{x5O<5Fjq{8~r6~P@^e!SfRQC0cF$bDtEc+)`PAaN0e?vHOFCU*dOas zY)hKplo8fn87^1k%Fg-%#hzb4+LY*1@#G@sNP0MlNo5!d@Fq<+s%!|sXFCjE^M&;5 zj4qmZ=tz+yo#ldPl7+MZQ@-v_Blg{^Zq*)10|J{`f?S_8bsX6zl2e=4J#n!3Qt}>$ z!*FpO&&-AmjBSuNsNx10R&Zh)>;y^Qp$oP->~~g<0^0EU>)m1Pcu3werwo6(*!o2_ zwv=0@TyBt?%99V}NH5grFfa`yy~^0<%Y_^%#m_8`Uh?xRm3#2F>v~3xcyCf|aw)aA zkQa7a3^T=|5VLz$88MK0ob`lFW5DhwNDRH8ycPs)=7sd`%7NXXJ5R&AT7hUBl}ghZvF6A;akz?{aU{CSM;IG;?cGEc@=&Qa;1y; z(n0UHD_Rrub{SrceJJb0yoz4`TtYe5XZ-pz{c%Fvk1<>4qrDeD)pO0TmP(Hf=5j4( z5?mf&VJr4RKX>whmeCJ$W_tFtuG|0HZZ=`VHcgm}_-&KZzzLYJZCeGkY5!otv~AcX zY!jww|K_$~+JD|AVA`;4(}u|!>`W834VWhE_iNYLwQbX9!iH^|Fx6)On6~;v8zxNCR-ZQ6vw+QpV4u-H8%td~YlGOPNxHr4qpoA+4nxzl z%dri`E}6DA+>P&VW8bgcC)&$M{)-9QR7YRGTyOsG+dF$DNs?VTJIwaJ|HDwBNPz;y zt5BgxfiFOT3T<(P$_ThS5LsD0Kf8Nwk8o_yOm|gg`~V09`~#5njd^{1U_H}o6fN{n1 z?{7$cHeN4@jJSq+#n+uIIXbs^)i1UcMfb{^Iq0Yk$YyR2jAIukDk(9%Qm=u6RCZzw%DV-{Mrh%3$yIP-pPB3)R@1%CIGUb^)>bQK36^` zy!PoQ`ur{Pb^YJH_vvZYYS)i5`y6D|&1bDqsOQ|{bj`o-8`>X1V}M$`eV?<#ec3UP zohkcM<}b+ZylgY}Gl6~Z_DnmrA8Z$Ox-p+>shYvSaZoS z497=@SgBk~rtFU$CnR|fkS|&bc#U-?#Ybf`-S&B`m+VC-{!8-Y$uFL>UEK>lX?#jm`q**;h7T?Z16S6AG<<__WG zgh=SlV}SL>iw%uwrRxqRM{damK| zl9--j9XdmS&ag>#HF^DKU=E+6zi%&~hnk(BOW3 zn}VM2;5Aj>`3HbM)>%`Il^UeK{_nB`xnjepV|6)+9mv!K$Ie&CfhD)^t;2LsItlUm zuxlx=fHY8t-H5)^x6{h9Altm}IG3I&`K%2<96$dHr4iUJ?Tu3_J}%>~V_3)95W7Ts zke#Fdx!n5-m2@?f0a!~#mg6;+Yl7-~D|qK9JU9qWv$av4fer+ce3LkiR_tD~l4M9a zkH&U<93j;v2z}N{&dH%}9}+0gh@dPNInvjHjS4LGgVVX2(_m%kVD_`(tnhd>w&AQe zSaN84kG;P4+Q{NwX{PD<_}CkJjgalG*V*$IlnMk=4_Vu1C$E+A^g##nsQu6Tdhhg= z%uDs)l{$U>Td)P=v?uBrcksOIM$o?IoEbTb8n3b~wc&!y^zz#kz{WlYX`b~JxUKKh zogCOfh=tt|JcoSNLqaTL1TAE7exV1=YgU5SD~)Yzr&>v1FU!eG5~>zt$VQbwln%_7&DRV{3q15FNyk*mKCm^BBxKN%-e=LW*TKoI zP)>RqhoBR3ke8~HECTK^QQ3gh z`z{1U(faRtLV@nz489zzD+|axzq0d_kyP12Qv$Z({hOYlW)YXY6>3=7NreYu)axy-ZKR2NHi_}HUW7jxUDh3t7+KaJk3#!Rh?P(sb(KPDm@f@Y)HgDkn14<_RQd;vs}_dCAVvh zieFB-^rj_|6aXY&pwudpe1yiedZmrJPgpT*?7g|q@3L01F3Tzr(hsqcYTD-|iql*2 zn(G7VJXN(0xqVWd0Vv#E`<)9DAQ!e`uj(v;5F1g6kSn&~G-4$y>jFj&P7;H@){)j8 z=dzPoXDpNtc>VW|Cu(l4I1)R;(R2M?keoFMaWa(4NQ$jfPTng^!M~7-EZ;B zF_L-wuj$6&V7kfU$Sqv^yN3^w9zwU*PB~oMyvY0a=Q^2>evcyv8>!SJ+VN$Fv)uSP zcb{{&XyjZJ4~8mXCs#`p4L_epkN$q0rDm|_XM8t4H%Y(!&wO5u<9>Xy``9+e%`|AT zsVx}Uw}>w-;YWMBzZF>>`tG*pZtTz9oc7;;H>2kzg|{Z(o>l+L#yZJHL1|sG3kYF3*%RK$%>Moo{Wf zd@!WSZ}z1R>_l3kK{w!=wa{1VJYDxI?HpU=amCOv@@&V3cu;DgPG9rh<-U2mhxR4? z6#3zHZ)SUlPjPUZ6oa*-kbT~xZN`_KmS%=sJm3&J_e@$pKa9_mp0tho&u?GvC!UW? zu7|Vdp`Pko$adykNZWQ%f1mLgBxm>8nc}0>`0jx4Nu2Qb-)&W-*DgH^2yf%YA!L#2 z>|wIod%iud4BvG;lhYGuLKSzfeLN>U{##qK@0QWv%(`F9RsnoCSpUE|S}%w0VwFRP zb};9CPLg#!cjxDP64D;duw3J`=K9b4^S^HSl$Tv{CJ!oGaVf`8?)yx~<}xM!Y~=Z~ zNCq~P`FZJ8|IPp(Wx^_Cza7p62LoP1!x(qfGA zvg{QrPi#Pyma2_8?#;uxoo!ePcjh(6Yh|#DIPBR^?d~{l@5b_Go@H&{*uMRK?Yn!K zxx63$9x#>PVwCModw=qwH$IUo?mqIh5a9rwYvOjkPqz59HcoMX`z`y|s1C z^3h>##&z&+pAY^JnAh5DzXm%T7$x!NJv{sOyM1)p3-fno00$XZxP+1mlvO%)URxz+ z7Rk@_KDO5H_9=EHVazhwH+E8_FHh$3;eI;VyeFSI*{LZGIK`mteYG~F$8OLuc&55xoC`Xr`*XQ!qZQKY0pynBpC zF~@>jWg8}QY|nfc+mqbv?kstk^(;7`P#(;qtf|8p-``u28Rs+V@m&ndaPmj&uiY0ujn&caMDEX6 zPi)N5H_~`C`+FFRKY;jyilA6vzZpC*5f5dpDvb!%8z$A^N3gNZC6}xwwp+*%(}Pw!bm>J)z~ef!xJn*KOv`yzs3;R{3N< zEc0Ra^@WjI)+!enTiU8+Z(MS{vVK7+*YK&V4YhSeg|pycVl94Yo8oI8P9~LR zl5S6{-f~L-c7`BCpCLgK)eJ8AS7ntixzny^4@sqMF(7T!KpHwBOd>e3ADAFy+?&gZ z26JW@+Y{iTz}gL-j8VOhC^_pt*H4sesw7QWhZdfvP3@pEiPx8xP9)h@9S+$WwL?X~ z%-+Z_AV!epOXZ*Yb++bQQ~Nu&M*~^Z3x2+>EzqEmC7DzLT6+V80*OiwWxuPuERukz zs@{^!t^2AVbmFZ%9(LAtx zda~}Hq#m^}wV1sQDNW)jNvORN>0el0vzL+~{`S73?3?nsVvi<3in?`ZUgM`Qh1#FO zD++Wp-)kL5m@hA%IA)Sbxh@`_nT4EClmsQmdP%^EU>pfj1^Bs%6We}~gA&o{Qs5Hc z$k$Gg*D9&^P~S%HRY5()zRc+$UdyoGWUdLDL0AvsKrI^=k`SseT1X;UN;RfugiHh`@>+R_SI$PjYV6X3&-;RukacDNGZA~->$q&+U2U@2xqKM!*(n1bHW`KEeN`PsGBqL@ADj(;-J)UNE4rvf(06Og1^7#m@EgI>YZ9m8Whe-_`DQe1kR*z1+;Pg;VttS&`ErUy0 zS{WWgexuc7D&MR{i+n>o$Y-LLA~!BH8f|SZXsi-7Uc8cNuzJ9hp|r|7awYzKajHuP zvqfm55&wt|s&%xcvzOOEx})XP46~PTkf;r%dxrM~l%>N^Sgvrd=k}7jBwanfvwnyP z83TfK)OyUW$=U|Y%Jvlk%ZYRkC>YJo%G88Ky7Ery6ij5iRcPvQwPrOHO6NwF{SP2~ zguc(FTIZfjlDkJM(qy;p6tZ4&D;Rd8S3(n6Y9=@ht`&J?&)^z!W04$H1(JY*?m4je z)kVyQO^5LTHV;jH!dwW}p676ZVZME|8*!GI`JD%5g$3tIq7892#S4rL<#oe?$_dpo z?m{^@65gDQoY^>BQ%*pD6WMgg2^t$f67z-FFrNj5>dHP97<|)Q*n)zUB73#Gy$_vk zmA}PjGcQw?H0V*-*R%-E46y5ZkQYU0fic^F`b^9n? z<&c_qSwq>pLad!Jb@1Ba^G(9Vz5H7$`d@K8NNAQ;A@9E_fm;@Dc&mk5jWQ}hx zxemjuX{)$`89_XFC{lsQ8Xpj(=(efnfWJij~Y-8mz3G0t0oA{Etd)??wOzAH|H-i|N9%o+vgRL zc0+Iu;7;Gew|)0YZ$DUkMjyBlkshS$U251h;CCP7+@hxA18$|Y@3=5$CeM>f)?;k{ zAtU2JWVrcN-CFN-r1M4Z(YPKb@9^*AsZ#Y3LvG~l3Ot=^tk=mKroltrRjLQ!L!ws^ zeU2M=zPP?X1G)Jr4^EyEdcOLk9NfM8*kP2W0QAGh8g;%8G{rq5ja!t(aa+Cp(VO*- z0~X}nmp=woy!n|?IDdZmiB56L1B>VLHvjlT9?bY_IUHHvEf420eF-CSKfy<1e}30b z)cJ3B{0E4dk2hgEfZh+}c|P8;FN~{nn+?11kdJ=vJ(cCo_Z(SHZ~b?C!3N$@-rhdc zgQ0&zuzJgLdjCBinAne&Wnj8IOtgekJPmN4#q1{z#b*%$1U>p>mCVTG<^As_HAY&veI)9Jz+$2e&_4hW*H=Gu3DJ{P*?TwBz@t*1PgaN_~m^VBTX%9!eg6^0?l_|W}zrOn{ zZ|BUiwpPw**X+B};6^8@zn8Q&4w_Ap*!1xP+It>##onfRy4S~e?|(bKH)}?cxG5Dj z!*`rnHp0ljnPmfK4)n37e809Uztef2cm`v;VBWXPRAN>T(6|ZQNw6i0v5KnWE1Rw^WT5*foh9l&Mm!>hIVPeiIeaYR_zt*~&I%)6bsPW?HwjC2=_L zXQnmC@}6;ho8%CDkuvdwL>hA7V!YvSO?&WYXTe2c2gU&R7kl`9<98mOXJT2Mco8EX zN}5`*@2rxI#!z{~YdLA*x!vny4#6}|0Nq?UH%)!?wo_l=q1-WOM&EJ#jARk^uy<~4 zKU8)$@hn*4-aq$_AHuv5yknX9+DsE4?jn_(xqWZ{dMA%*CvL?#zH}al+NR~oxFz|MwQ$_a@8Z!6 zjPdz{z4hehg!^Rif-}EB{^#wwdT=(-jfKu?yYtZ~PR-OhT;vp|=f18J!@xA47O=mJ;-sK~EevXG`pSZ&VW2UITT!j*6ZqLyUTWj-)XDjW;oy&E2 zZ$T)W)9?L!X>qdE=u`RCT3hxVxAyVCK+3s;>c|S^^U82`f5#+M2C2m8oTWMYkvmS& zifyc#N7jZje{j!fd-n_{zT9aIK4K$sOu6PQMGiC1rxg12v)3O0{>|lFh5l3RvViu3 zeq!f?LYFFpry*sc$y(l^GDX+J(*dO_k4f_ztm98g)M`x}S2;v=(4?$Kh>WqC6TUtx zxb3oy2Dd6_F>Y0`X0@OP<3Ls|Ru+@R`fo>S%fem3mBnb0a%_z^LsBs<$N0u=XlBf)_FE)kVZn-NWvWwYj^+J9XZs8)hcs~{tmWzohMd2Z z^6z0a-4K~a`8gJpHW*=Pfns3biN@P5kfJV3hN?$KeYAVeH0_#JA*Rc$QVw40tulm| zvXh1p5uBK2l%DkU?!_?T&#u{s&|nOhyKlKXhl=s*nl1K?2Aidn?Vd5DQ<;=s zleBzO8ZsE%t+L5t2u z?zo-W&lB@ULQ+a_SOPOx-o-+SaPAb#2nd*w#KKrjp_QjXyhY*o6r?QlARv6W&>bOh z^j`laSqpZaoVX2YUtdAbo_4~W5%URBZCf!ya7SYihB7(EWk^Ep<95!qf`v&L?hBK* zurgaVLozqQqV-xzN#39_w;a~6?0B<3OQs9=dCNG=%EgruO12y`$xNhPi^ zpmi0Nc;PsDv*Bu8mM|}w&S(wBl;5}#MiDKXV6jU?SfbHbXz~lfm`l@!g@E)(&P2>f zEl4qbrf}Hyh5jUGi$SwbLP!RGDtC<-hwB{!DxS6jAc)JP&uGXKi|KcaPBuU;*EX!R zj?SEbV-wD9$hmeI#($v;1eNkph?-^g}fiL0r#T9LaKI-Z6F|&A^8Z${mVxvhK~ui_csxX3s_>G1DMe7I$&#p!>To1>DhqQ-dePVf6$R*EI`x_aLQ6WR%1`4e#W-aqv50hnoexa)wwSj zmpdh?ToXp4ls6jpa;WGc1?#?{7&N6Ew7oBXmJS`M!sC|Yp=yOge`%)VkU?QxnY9Z~ zO&Nk=&KYVD6hfno^m3Sw07H;#K{XvZmn95s5htb;=E_VNALSiYLM(9T3kh3Cbt-Q4 z{exI(RMjN93Je;&35%_1wvBJ27haO~A{{DV0#tVFt>Te2-^67`gNmNFH@J6QP{h4c z2aLa!Jn}3@OaW?7g`^fJ9eVtx=-7!T)czhuMuxBmG|3HFkG9yzQV2$ME;-d;O#+!p z%mISSXKrp_;MACoH_1=LNp9MBA&$cG{q#B}K zukuqx%(MemXMV7+3=zWj7RJqj@x_e$W@E=q#qA^W!Vcid85-mQW5TTV6bIhoTKUi! zF^aL-Crh0Wyrpd(;M#AuAAKoHMm&RkJe(?D z)IXw){h@*SqR{n$2KP<;`IDyV*#dZqKG8V*)91kL9{=Pm4#4Ff-hTL%OUwo`yD|NE zG~@X~d%T~Yn$mN^yC01EXZeTz?EU=($jfjb`ud*v%)i|I%)Rvfnf>^9iQh8Ieq^Kc z&&(|K}m=cTus`!xwJDSz!cJ=A%|4A{N)A@gtt@4hw`BQA-gK>H6&ws`cPdtxb z7;(R0#Jvq)a5~FBbgnMXlOoYiTR|Cbbu`PI;ZIw~kRq-TEBj z#Lh@*D{E~NMAj+IJWVs-Ssris26I5UKg+o>qpq6gXW(&+c5LE3tudDZvh19I!8Uxd z-qo-6j@!&X89(vra@r1c&em?9^J%;a~?zoqjKgaVcf6XrGapOpW`2p3KF{Z^n>F6okd%2g zSq*UD&F=RuWdw#Io?%Z?Tlo-*gf;CQxkKX`UU&%L$f!GU+0+Le_vIMZQg`Y8-yB~! z!^ydk+g_&}cvD)>8^hOFYCzn^nG(lhW~;vEr7@fIDlX!)W|LIn87C}NIyy0dgi)C{ z@t(7qDE5=Sjl3rg+#xxd8|y6t_#=Toyh}WCXctVZ7{uIu?b2qq?jGEhmZbs3=cuda z2&wf#(Kx$95^oh%7}%Vm~iIX!?Z zDa&F{EGKLwuiiFse@;7nzy3==*5)$yl7y8|T5~|x41$!S5OsR77OIWirMe8!81H`M z`~lJ1A!;vRD|zE;4DCjoD2X;N>R1cvGEN~Mwf}*xskxZ4%Kfzro;ArV@vOo~&*z0T za*Up^wA}#GTjzP;o@{fX%sIhj?@#cGkmGLGaiA37%u+S`xt8}N1z5k1W zBJS5l#_C9k$FWyi2>fCr$Ye!@>D^fxvF(+}_q5?C)eEmfBr~kGn~oAbFZPF&*&wLg zw)QJSejo(}L6T(`Yaw*3mpzJFeqH>P+)tA4z6QYt*OSuP)s>la+Y|ffv}TwJe7D^X z2v>tt{#~h4D_j-rYmPbh5MRAD4KSla}J}u%KpZj3g}CxP}ya0a}ua z3&r)pX?&DoCv~u^P+B$*uHEoWaufbXHXKONSzJ^K)}EETC2LO>H!oCk)M206W;D>W z_GZ(j`}Pt)Z$?L>r-)w*FbffjCs_8FjEhKpF;D_`UUP%;E|*wb2CzIk>+jZQFdj*` zg1k9{bYybdR6d)61Dfj%4!D+t&)@Ys%^${@}rdRB;nnvVb)loqTYwf%&Nh$qEi z2()G~f(EhDdIqg<$Uw}^D2QFBEu}?PT{t+3L*?ep`ku3N0QMUP#bNRMKyw7GeA$wo zI20FE^1f`dLWNt>Cn@M=IBXOg>RLu~z32)DG?_2CrtJq8H8rJ^O#8Vey~_--<1B{> z)7D=cl=9_Kq%Q9LEeIO9dtLaa*!xGTX810H_jDc3wqU2<#uhRnLR651s=UHrIui+D zLuG1gOTAA#GnFXfAZkaL8Yj2g2x>JgvcDZoaEz*%fzaOJd(db}Rgz>afOH90RjBjf zilt&?5%i2FXB3(`I#8uRA}a>mTuFkBa-OFk@65^JTryz;c+Dkqpfq@2T$dU{C1Gjb zdO9XSPeIt$gXK#Q3Fn%iH~sH@zaT(&A2j9V-Y-CFN8(h5@J z3I&mD6QxN?Y$7HZvBi$f*PH!sQ;9<%L>@_=0X8EQ$1c}hGs;?89SFZrB_0L3w)AOd z+~gcgnC=|}iwjrJ`@ud9EPXCy=k$zG`~)N$rbV@HZ}TGKZ8pQQ8Or+^%9~xcT=o}2 zI=d|{%OrsCP3u)|H6Fc1cRKTkpHt+WL&03kYofJ01?ROm^oBl9@BPsrs>-BMcrkl( z#uYnv$50$fTD8)TNw%&+WS}*v0*>vLWu>J$TRI}KgOy;*WWcmq?J#bKDw!8LoP>$} zA%%nub8p-c9Ux(va0+n(pEgbh>(~pIBK<;Jgw<+ykNsmU()aVfH|(eLgb@eXh_*w{ISL9^czD@txR8{0(ZhkaBI;O3+3 zQd1T*0-ZxRa^fUbp1#4J*A|Ulf8vI9ImDE0iCE_YXKS|k^vm@7_)NI8!#~a-#XbYp zM?fz)IR^11(9_A2s&*u7LJD8iA1ByX#psIqqP3i>ySK#a)x|mIyrhw;i z@-yxZ?2sefKaBebw`+c)`tr^1WVGdGjofP?ipXp9+QXw9N@?gBb_ z0Kw&G@#4gbnF*Az7>wDSzJKFXPZmzF3FEU-El%Gtb9Nw|sWe zzyC4iH$Bl`^5XBm^cFeyTVL}ZWbHm=#Qm+_{x=Tu-*QHO^+Wx_&;J9!AJm_&WD3Bb znS^ToNw??PHrB9CJd5?;u2oab8nGgxw}!M)YI@Hazl=y}YdJAiUdb|l4&j;@KS5X( zz@*>1`r=xenLB8(ev&)L(|F0apBNaF_RVSyiIL?n=&dJ~1?d=FYrYf*%ng9C2vpM_ zIv=EEcHT~D8%=kcRvMVuH1T_$yg9p$F;@^g;IDvm;iT6!E|F-FO^WAZT#GKMEgxtN zNqura#=RYzZS`PYMlNtvu|Pb&Y0G`pydGTa_}(h4k@ixI&ZAs)-1XZ03`*;!1o{*<8v^C53p2>`l%sqSyW5Q24i5{5$Q_GR3S z?a*y1F7Z2I1Ir-as@T?A<7Noj{w{Kr}1MJhLQjVAKffjrbPZwiB0& z$G@tDQ(Ivl8D`p|N+jZtN)Y<~S_5G$u#y}a6Qvhax^MTkrXxpAe4npZXU#ZoZ(?HT z*m!Xkp`#wBxwmCd3AkWNy1#W!Ig z)pJ`d@Zt|tJf6%<*dM(=k4*zJlMvcxGFhVwcqmU(cIAwyl{-7_C~fRY3k9FK!9jx#t)=Oln*SEFfo%$70vMFR&zbYNsZ(ccsBVh6<`N&9k` zn+0&a6R|vyT2r-YM2n@ewB7;b{sc%0=F%sDNbb!YGW@Ue)3#9F=31H&KpAb)#>{u(t z%!mM?-J+?%MM=$Rf0?FO)(2}1I~JQ23BP7g|)iLCo&O@ft}!LSn$fNnp3tmodb02Yq}pu{9G zJkW25-H*X$@(t{_17j12Tu~tIb%6&Mrdr@pb)4RCccJX4)Q+4I5>xA916&E^5NDP`LXB8}`;h5-&nGv`EhIY=q+cbecD6H? z{1v;dUB3CaEa-B%tqpU$_Fba79oJU$PW1l0(U;TQfZ9i9YEC?fx259XO$naAP#8DG zff_^b)!xVcQr065>abz#hqXF!KDeN&A@?jk0B(!j3h7C>>am>6cAK!UN?xG||>*c{E>xJ9O6ch*O0s{aIm zVtXRDbMF6O(~=Fc8uuCtOdPuY}vX?jJzd1LK{EDkXPv!n&2GPFuOcfaa{VkY80+^u=pp< z+Dt!c5sIy$W;fE3@N#y_-dqRKS*|TEjXChu@<+K?)U*2FGg;ij31(x+G-O=d02fYY zCXWpeN05C(r48pMH7+?5p9a$KOeH-$XJZokk4ko8)ZS`amf=kAStbA@zpbGvt&ztV zs3amY|KG3Es6A`RdunU?^Q3aW_VYh&J^Wjq=~v4e$2Ocl|NY*5&sV6Z%_Y~zyEuF{<{7D)8Fme4*ixf`dhBazj*F{XZ(Kp zt|o<7nd(3OjPWnA{bp(3cRcntc-K$mppWg~KVUv0|NQg+zXm~=-Ev{G+F_>Z;-WQ% zU`?AbPr^zB#{twfH=`Ch)4wkCa%q|*V>*nJf0(SDL`?LytHvv;SJ=OqqjpeQ-J)8& zv5qvkaYSknGHF1`sK`aaNfUd`y7CHG%WTpM*Kn?PEm=&6b~d~QDMB369>d`KH6bW; zt$msxI=C&&{!1A{*M#$YVkloeZB*B4?(t@c9cwk?FlK0N@=!&rt)-31P1Y_jCX=wI zn3Ztglqhp6MWhERpWn6%teLGnDeH-^TV!9@^U^a1Nw`WjSp@uStQ6J6k~#shuTy{) z{oRdGC2dT(sXEnAJFkK@%+8V6?x>#MjMhZzu0QZCab1 zK{c~X>U}_E*V$MnjYR`iWDi^dmS^bS#I%ov(KLLXb`2|Y?tDVf{%zbmUjI!R7jug` z_h}k%HMW4SvHHpC%+uI&v%hN$8eI=VOf|4YCZ0zcJ1U1xO(SoD7!bOCud&#jOH=pn z5DO$-o9;D4zQ}kE7AKG|BzE97kTfnlKVtVne!n*UVvqz&EQk5BwIyA@+-KL`9oU9a zh;DvHWlrq7G`_w*U&}>;W4v*VFb^s_*J^tQAX4gBJ-?f;Zq(W6{z4vH( z%W#TaXin24pUs|df9kaL!4fAJ>xu-)6*J=2PVLWhCm-SR4`r!9&kN4_|9SdJVE%f&WucJkP9>l7R&9ReIkcFC2EkWe;eXoI$5-5mOXG{F$psi#!)3H zwc#HduMafC z->lESp4pm>VWTx`^Z(7pc>O+&W>}j~ZHDhLc*fAzH5#iwYFP7kjPd$TYa_q%_1rnA z;_q{)Yd$vT+>F07-?2V3zd2|eGPW_SwKb73+G`%`d9VLi>$hgyEIu3S@cQT&b1&AK zza9|Z_s6{U@ijM?$C%4FziBqd*v4GxnCr0S&zZAoV>I(6Ut8JY@3e-^_iv8RN95y) zy)oLDwq%5VNo&Y57hfOqwGH=&#wq&``?uE7$27l!a_Rf3Yvv+~?-{rgbKbT-!xN9+ z8|?wg$keMbw}t0DVumm6^wHZ~PG8rhqZT`ZI!W>b+iRp3GF}+-nyR%mzZRnbJELn) zzZ;L)uA?MSLc4M^>zC^*apBlxz3@}z&%@QI1DYH z$kpo;q~Bw-^{=6NQS7D5@^m1t&!D_E(&z9vER(1JgY@9F0B&c&rr6ZpK{{(8gJ7T4 zhLODnD>yA$IB*+bH^g4JE`seF;4OSs(}yp6r+QiG=9(YNPWV}WKKjmOh79Zi`2g^* zAdl7hpg&o2to#D%G1|=PdCSL!H*Ovr(7Zu^$&Z2U4SB-&sgX_@=opQi11^uL)YcAB z_t$o?3##k^>@CGzY5l91O5`!?2*e4vZWzI}2)fcfOdatU$mMsg#k7e7R5Hh(>;qOz z6?>#{?zA%$Cx9>XIY3+i>w^13cCXR*L-FVOpnWL!OJvJ+yYktgyRVso4uyg&TZOtj ze?7viyY5kZJ)b{NZtmX4Q#pEU-7#V^S`XV$F9wN?_5~X zcZgkJPb1fvHc9Qth`7s!$VbFHM!61JK$H)&OFQMyR3{HHvWX6oe_k_2pYAyY#%fiZ zk7F^7ferP@bz^A!Zjf&cvN?55$-WThglmU>*G0uuTKy5mx)CG0eYdO=E5wS(#z!A= z5-C`=H84Mn=}?uMQS#(AF>H_M;A?!YCu99{t^Np`;rXTPqmikHIjwl3h<_ooNMj}) z(73R$4QO#!{$Q$CV!u+prm>UA=@JIg4UeZV_T1ZK%E6}A4)M2DJ-~Bglnt0{rvyd-tC2yz8%6`^$HH zLtg%d0`Ti|_*Ynm|3Cix*|&e5q2asd-nN7P1pCp(dH%nz|9YJU7*w~R35fo* zmTf))S8|%5Rqii7vmR!RpGBqK>bryfd!C2HNiduo@EGCv3d+K5$fKhc3KNE!rJtl) z94A0IwHTt&DNCaYFwfiSWit9S9EZUirN3rAh7R6A1{)92rwHf#MpfOk_be7v2uL?2>wUK3De_j`1AKx1 z<>tY+JTj4=IDU{drJg`F2YHy$VP|;If;t~3xw{o=8~dC;8YeMog$kS0HnLtTSds-6 zS6tg6r$A@}*QVu{;t0t_kV&irGxmZlfS;r$^G`z1iuG10p<}hM`K7*@<&)U?%s5*F zKc(X%Q64*cg58hF|CwwHo;8yYWm8gFyYw+=7cpz1^T{IThk6%8vHsMTRt~-&9ln`xxg`~pHkn0YbpMFvvPP+y|C{Re(8lVNhbY=KMy#yh*f^LRRSoE*c)hcwkWRfM#fZ_#Pxf|546++l_U3Kt zum;8@`cd3N>;c$>T&r1_dUw*epf%svkCEuKi;xqB6fScUFOY9A@`n^w4Q|XRT}|E; zZI}iZ$#%95VcGUITmz8RY#c${Ytd(^!8Vkw#kWlWknL$1p@pmmHst}=9Va> z{zi?Z>-#ICcQO)$u5 z!`dqSkgOBoAY#X3eXPM z%vOhVN*1{!F?{O>z=Q(C=KIF(W~|}3xJ7&LG}HzS_v~Psrfwqwx$r~F2(;Ps8hY0V zDl)udp0~sthU1cJP`7JEy5&*plCS2$+(?lOMoVVYwF$EC`yp+V90Z{BkstBx8HB26 zDP$d>BT%YZPxV%3>ipGCbS*CcOlS?Lg}vYs!dxrBu$irQ+8`)Z?CnUwPYp~pK1UQMZbUnqa+Co&4vG$ z7XFvr8BHKKl!{>-9V)~@iw`RWgSj#>8pE@iCS}vsVj(30b=o%)(FAz|+urm*5S~`& znM>~sWC8ZZ5T@G*b_1^iVQ@hXgrCjW03oLVYoZEi97n8mY!OlY zHC*C_y<$iQKwQzbI_#-c70ZC8G;%rtA7}u-Fo8A?h?ScpWNy-6eHh02`cj(#$$)Ao zC(iLaDpR&L#%Qfc4@1}=HtmfLUm_)efY{g7;RkD?tworD@fae^Awz9kjEVnlQdAF6 zp(Mist6COdpmC4~l4hqweNJpk!ubSS-)G2mBYMXW9_0EcoX=>7_F%T`n@JfUn< z1^_&~0o)&_IpapibA}n2ESIh2Q!rdYxU>7R#yf*R9q5FZgPv}uGJ$Zu>s7=wzT11fg` zcZAlKG1_QgT42%I7~}uJpc^r6Q9v=X3cH+e>w={uPLZMw3Nf3=UD2c-IhParXS8)L zbC~465mq+0V*#fPV_%6&1@Xd!!-NE*Y7f(D1@aA)bBFb!br)V9qgZ;BrQ|#9hssBQ zg#kXoB>n)!%p<;%c!Y`;JLHNXOuyqhkxv8gmVA_#ap4<&fJUduigh0i`Gt60fQtib z>ww!;PAjW$ps>aUVb>Uv=uC0;w%Yd5(^#l^Z zQOrFTvmxah6fxAcfSxrV>>;PVMrX|5H5w)-Sf_KH!a&TJgQcn7dOU4V^J|Fcsfs{9~|AJMNfOZ#3#!N%^aCx$3b5`71euWZt~R zC6^klD%)z|%g`=`4p1!ARL&v>l%|}-*Qho*#i$aC$~r_%LikLTQ?Y1*!HiV}*HeyF%9=uL%cw%7TzgsGneR$DiwP%C)g`L6Ds&_c0JH|eh4ePY z`1kMH`Tr@Pe}CH{e=xEQHstTH9vA4Qzh*)|wF19m7{C0?EpF@AU+^23{pTKiw>9+r z_kXM0e@_YiNi5P|VljT3EPe0$zw*|9LMQnT_i+D6fAII-zmHt$A8GxEO{jts{+-6Z zK^A=Yt`EO( z@%dk7_DC&MZD>QBB`o13AC1DUxAt>ObEI$*J=*)g>S!n5A58@3!UCJ~-Qv@3u_ zbtZO(8jQB3LhzM|^FlL4P?wnQmKk@Ou+}$Z{KVv(!?Xu{>rwZkW`}sDN((84YTBZgh{H ze3Us0bh{LMo%j?iZY|i-Wn~>i=wDuqSW|3)A0Qidkkc1aMiA+k!G78PYcW!0gD#Qh|;|7U*%O+p9{ zp^Mw@){X{80L0zIybIL!EZi@e>j6EN0E?h+N`qN&p%Hh}kocj9nZx9?l4o(f zBHi}7i_g$e4u+o+6Pi-vwYuz;DJ9M10!5C!N~+VETgZwNr+|+YL|_Cf$;3{Xjw8p$ ze)|yl5G$A~OZ8EQW;*uFWV2r180=j5nY5-Ve}?3S!fmE@Z94&kIp(-^!_&eLJZj{! zOTHT`dM^k9*sv=(6Lk_dW|1&g{Iis8r})M40OfCF9fdnrdXyk+X~`9sF*76WwAj;l zcjkyKs{^AccUInSkgrw=gpW;nQ8S4x?MuY!xwbJ9w_1?};d`t21oBD)U!(OD$?Ef( zwK}#V|7*epXcci4awERp{WWYnURVD`z}0|kliK1;8m@(;h;H-6U=BnYT22Z4*@~(V zUN0Rm-zmW0l!kDS7zG(@H5Yc0*lfTcBRNwGri9iatE6=rjl`ayVFqD;10Dz9O>rzd zUK1G%4a|+X{_faM%6Qk@u-ua)WuXl6V5yZnG(K~r0uWh1D40p14hTLCaFY#qXGRWm zTH!L*moAn?;va>6ROBgJ)@1;ZW6=y4MWZshTb6_C-xN)qY&7&#^(== zvg4$Ov88@WLiVNs8yfHvG)NeSD;M_0^IkKF(jXqg2FVka8$DbB-ko*-yuGSd4HkGqkkL#?o%)JCZ`3 z`-kAGB%;lR&lOsaFoKx^?D0CDH0 zyo|<33^KDwi=~?=#6`ZV+@S+Fzy!*Vk*}Y9MJxlAPSEK_#cC>x4B@+>#8@jP!*g1I zRn46My%_+Oq9b@Cel?15+!|x!#7H^asCcIv1n6VXLAV^Ijfye3;i@+72ap($1&E6c z1M6l1#X=(~#W#(oT1%i(hiLnRmQH$1>b3;=9}*ur99YrCFb<~+)`_O4olbi zu{%XVrRUmW5GY(=!YH zg$lzyHh687(J-Qb%dfa&itjq&b|5LSaL2sWtsMsj@X#o_cQ6M{%h#+8UHfu>3T~Bn zN0Vb^;m)A-u6U%fj^ROjkgrm9GL*Zc(qEO6$ki)8GUf$>{CkT}PxcMD*^s54^;9povqn&XQ0NAyN{4ifhn?psv4k>kYq{0-T?QRN}Qhl6^f9H_|6 zm}2d%A5^AziC^y8effP_lVXi`IUgRbzT=SE=35ssUUBUyw;XfQHnSbC5FFwmw~X-- zi0WjME)KrW1v=B-^5Rh}$owx?aiQi1hLQ+=!2pYVzT!hvjzgnEg^Sz<$eE+CW?0?L zmr4{mJtX(XdcYXx|9u~K|Ni>r&$uJrVUa(>D*p^5?#3EuTjuY4ST$0^{d=;j8xg~e2w|j7ayCcf+ve$}JeX}R&#s^KAxFcns{cnbEg+=7 z#ee+(flC(Ui;ezCvX=t({z35by+M@bUV3OK7C1J^9_c@@w(3aKNvM7B39b+RpRsNT87q$G80z5 z*|v~YV&9+zaPZZzLOx;2Nx~PuSjK;s35@U%K#yg?SY*-KW(F1aXx68jHZJT8o@+GW zw2|u@>&vHWsul#_tt;&{0FV~McEhp}?0L|Y(%1$H(!-6!)tW;~yD`XGiJTCEf-BWzyfFdy#6%2dfNz44lQ~kT~FNeKVc^$%sK8i>MTOPs_aqkXc>CFC}Z)sd$_eMi&I`(V?HaBvn$csMaUpk z!(8k}CqleGc)p9*ND>@{im4D!Ghb27Z8e0`r!rryj#AC&`telUOV^K%WVaa(Yj#l` z0i3^l$1+zQwe~`Lv+*S-LTI(g?1$Vv16P3(=aq$yt7KQ)K5px=8;2Q8S>y3Dch|-|aor(iXIqZlGu}Ci4zG4p;Kg;1A z2E9h|^n(3uf;XYy$VWakSDNx3v3ZBaNwX{4+Ij z6y+?Bn)zzYh|P*Yl{PmYKz_gl04^^ z0xlZ38hbSI4N8JB^(bgc!f~}G338+UP9y*v3RA((VN7;g8_xbSYGO^uN?)@iUL&P^ z5Gtz0%QS#~+B>z^JBlK8W4zSE>(Yd(foz@Z`5YT)OgXQZvToeJH@Su%Nrdu;@coi#H4~ zqAhqE!nRC(i$HM7eG?7~FEB0st)g%^2KgNzx4{G@xQw)~cKJ!UC!-;tt`WWCAngq@ zR5}nE##LPA84T_&zkCh_n zSicC~n-*7x!y))!yEaHIpzz!z7xI{>aSy|A3aBO|V!%rV*-8|meG#JA%0)`Zj`>3M z*7QF3LOC!m_WusJS{#Qp25+~B5y=J#Y2bjxcgJs;B%W&q2Ap3SBad`%0hXbc{IoH~ z|Aja?=AzO6M;7OdA0L>onDXN&*`qOkfEb-kpJ2Pf#-p*4Ce0e>Zy1Ob_;2D)thsKA zhEO1*zQ!ZJdlqqJ06$9T0~3ByLD4MnJ(OG*hN+!d;<2#I;Zm=sWlZpvPlD%Fts&q2 zbaQ<#WQK~cqsFh}VOcXPxdGl`O=*6{7*tt%lf-J27XH zzxlrMojdN6f`_D@H)FNYJ|@02d^}gYDc61X#D|ggQ+%}B zshvDuxA>Re+4_#{GX@m=D&4uX{e*w7{D*SLBo3;LJaB@*d~;XpRdLL5=gQ!4b>q*k z=3M!!c?o&Dc6-`5pa)pup3;{L)yP!$ip)q`TMIVWZAiBb6ZYEBx{R(yN=`0|>U z$qU)d5*Ocg+HDZ#qMiqmH;xJDdpqu2;Y-}x zgMl}b)cU|Ny0j~7{7A(;)vVyNVZWw%R>{Fs#bR&9u;X{#$>8>LlvC*?#(1&C(AMkC z6t|%JVh;IMQBlJ;52`QGE+9BN zEUXoX(F#xbjqR)OmHBz!V+(kPJ^qXf;_6z%3psc*Uqk zZ{U%K$@QNtUpt#*vYx^xDCN(s$h~K!ySK}`H-E)`)pjTWPud^@gdiQU+f%TJr#17zQ{LB%!PSCg+;gSM9F4QEe=*WTp6yKP?gS0eAl zgTM3E*1VhM4v>GB1LYeOPoHq^+!^XeN|>%YyI#ose!SzJV@GkH`b0eBt?#|(U%T=> zwePs&zuPx#V3w)vmAX5-^9PaKl>L<>_=j$U0h9oXf;pQRe*ahX5aA$ear$@ zU>h%Wz4EQmt|kXV6;W6-yX#7?_BSo;uW@JEkLt(!q~_iH-==CiTYiJCW~)xS?+(4< z`n$wGV-I8d%;4U3G(W+w^AWY!uadg`@__Ruma)V}2=3_M{T%sw)>nC_pdzc6xqRSF zzZ39(sQonu4E^rQY&;NLk_vl+e~(u=duhob9dNtNFmyf%uM%ZUxz8AnVBCS>jfIHo zMh>7xi|dnz zTDqpQ5D1`OGHln>+a=+50+Sn*&?|oe_lY;;aj`(RG=d|5?5Eso&7iUEHr+31j!En! zgLNBa2YdABSR9+H;y5{Rwp>b&DvY;FKG1+o@i$6R~1rk;DMNEX4)&*!yb63u(Fx0W%J0bW6lDiL0q)4h-)G zUr{jk^==a|-COb`ENc!arZE)sR!w9km ziHuUhfP6&CR$6dEl265jXJ@DftuY3%7Oh~I90Jb{R2KEytKC7!JH@}kW60bSNQ@&_ zpqp*!`1l@33Blx%#KMu!^jN)`;BKtAtg$ubA{%^V`Y1VeiVEKluAkN_Qn8A2H83QG zx55EG7V%TC5op0{LTf?#3g$d&8XSLUR}crV1X*W)gg2`B9sxzcVCn=$1)am(^%w$`=SVFodKT z(CTJ^4L541$bdiV<;fFEd zkx>*y#JtKe88C26WE!>4O2LHQ(cr&F$Uh`YjDa>JuJJ@XBxazztGtn|f|A&TKZk>AR3~rKmKex*PPkH&h@rN;jCOO~Q(jutBD;um4bv-2mMhQeDtBIP?iw{^Z zj%!B;b7Tzk?&%75T8wE`A%~{x%v;EemSv;d8!-eH#~Z+EnXq#(GdVEUazj0gy&*eN zBO=UULcUI<;2>$3(T$c4cUEt{ujK$H!KEfp9a`K>ScP1cpc#{2LljI0eE>W1c~LPI z-D49Vi7yr$NLVZi60Hb(ySAz~RS-B7Yed{Ts*bEAbves@nW0&uoPe`Am0v-B!yfCW8+Ia!E~(C+Cjw1&KqaELhg2urfNjJ+)X7qB8;Df zBP)lLD)&m9M)S)$Y9um0N z!Z1e015u9d2i;rI_?i>I%NiIuPg)|bW83+1L&0$~h%b?fsmLOOq@fhuK^2q=IZb#1 zLpG@-jrW^SnKz|uA#_YN)_^=3d4vEcweiKB7Tl%I@d*4|R4l=sj22MOR40j+mmp)K_#B%?mfj}5Yy+hLC@taSC=OU|F0_~hN^ukh>o9VAe-4Ij(> zH8HbybGqWP&_e%>F(AMAmcK@MTt30@Lb6BvC{UYg7yMA$eG*JFaX4-G01(e#?eQJn zjCXj=OMZhLzFxTl%KfV`v&u!o7+S>~3op(Mow(vY+TpX6`)Iu5z97=aduw`y%iYZf zx;@67z0~z9IYp{<7n^w74ph851%5dH`z{AZ-B(lcS5&-S1xLVJd-ujxRKDbLEpK7= zukG)az*8(yPZ96RV|3L9VR@Sf{!>kltDl=3^NXEl#i_(>Aq z(dL580Y}jd4=%1^1(EeDJvAt{TyeGS?4%zk+}+EY`n0oocb-|G) zUUZ?!-B{|~8obF>@Ioif1#fboZG7HUj(^2ZYZp1{JHD<<4lVK5>I?72>X^Nkdh!h$ zv*)B0-ag@htJb28bNug{j?`a z-dEBXNWHe;LXhsvZ}=n2*IIt`J?!+CmD+L!RY2SuY&W0a?{^$K?{1(sZRl<{C`57# zfT7ygl{(r@s{36`k6HF_)Z5B*%B=SRAxQSSk}Lf*fZ zqx}63**b~~+1`CGS*8y$m2Zg0p!mezW^t|e_BTU;<*w{H0o?c4l@w|^@#`%a7sXO{h<$g*t{ z>tD#Djjyl2h*h?fcBMJbdqPmotNeM~328SuEM5aOfAuxY9*s|K1ufV38fCnN2CiXk zLblT+=tuY|CMP~hCW4SC$LRV*!+Ybj4HtPAh(E{ZGASApv}w1NwUJwW6?4w5($PK} zBT@XWQQJ%KBjhXYlZHd7C@zJJhPEa>{9@!PIDaA^5$EQ71C0H_yUxrq6?-$e+ET6p zj1xh*MFKD2ZUobwrv`8}F9t0N>`8yi_`~>!#Ic6|nK}M5+ zA-o>Z7#TOJG9GZ^v=&!k4#9yFl!C7EqIOn8SR3JZjoQDFB+@&;F#hFR+dz%wBBKgw zAeD`lFw4<%*5nzPoBpVAI&)kZH#74T}o8=N(lZUCzPNwa)kDTp`i#Wb9 zqFvpAT&-YsGwLoM00l}R-^d&;h9lJ^lmcVrPGjt8Y!QHo8?j%ZP(=4T`AoQe6_!L) zg2}R!pAUQz!KVjuO$<^A;t7dvR)U^7%nLbe28+kSznK=U2_lLpWTTF}M!XHsRSh=; z8D7=vbE1q$bzI zV7%(4cQCZe%0o8^jJWM|P`Ho_zRHm}%EWpQUMd|dY!1R2^1LxhmTrlTn>;b1H1w*F zd$V(FNP8e-Zziqd$}dD|NyNrt=VTUQ9g1o0W*cbG; z5o)0gM(>cbju@V8iH~QAbWzzsnkc$-8)yoa9tQ;5h?;~hXdlcoiJeYCR~2~`<`PUH zL)V%De7+*36z;##EZ=Eqz$xW9l-B_UyR?WSTV}YUe}e*IRO>Maa{5B-!!&gkJf02X zPR5JjXsY8hf@>Q`xupAUdYFHfc&04<4SZNkVzgmjX7(6>j~l@GVUv$1aFY$N>9KIz z>q}dBqJenog`2Iep$tr-aXZ#%bV`6YLMm3^wPkLI=_)jDF9(Hb0SpFAAqfKqG9X=TqO}uBJCMyh~6K{`vs&iXe5^dXv{)`_-o{pu;7z`YZ@^# z9GR(h+00>%{PrTuXhgHNYlC-;H84~Ye-s%pVDuwMYb3SETQmSGjtO7rtRuLm?F2rF z!2u_+TeL>R4C`4939V)}MX~6>R2hO*NF)ZbL_AMs9S3>b$1EiQVMhV<#L1b_5f#v z^C)2L-5Am`3|qT1xpL+P5xWf4nxUxc260u`J+Nb3P>!RB2K;A6{2&biV?+2S0&W@> zBVuoneRKr`z{xZH17FI}!j_JxT6=UFZeiIzY65{60g8=FjvEqUAU{oG<}Aq1#K!}N z(<(+IZCJjRBPwf9^%RhQ#2*lw52L7Z+C6SPaGHY2mqailPj8%IzcnGmro0@lF*e}r zF-bBZDG>@MPH~WPZUuyT@$Ir)tC+#q3Q8^`$ZEt_Vy=`(`iysYg#8>4uo`gv3su0vr`5I$M*%5)omc+*N&>4Kiw{+cDs2 zG>l;6uxu)7gIYLO@8JG~6{57k{C(3uF}r7T?m*%i#KX6unU(n1F!#m7@@X60bn>+w zzAt=p#Gtd427a5knIiddwj`*P)z@u9-n(|Fp|;Zbh-(O*Zw#UD1_y-^DKZ&0tfv^p z>!ac&ZEd7Zkubqc!r;XTWYS*}hs?u_!&3@ChyWf%k4w;cQ3;;G4Tho9^Njd$+xtLq2f%h`4FaX0$=`t|6kbxMv zN3adHwoN{%*gOUvbX(-S66>Ik@!W1OgWza%$kl|CGB!N&Z7+-t=E0(;(Agl}Si^K3 zV@U#nrH%O-DfwW|Riu=&Ak8*@pV8GcZH)21-D!h5&)1iF{~QN>p4Ptez4_&KcC&7M zY@XLnJL&J=dFT54Y1aCAn)N<@Y98ll&AN>7Jlom3b+cjT+3)>+0`52A!vl^R&E&@80unp1OX&BG{M$D4$i` zxFr+UqiP0o550K*j63Ub>p8wI*3N!fC8NnkSU1%R8qfU9m0aKX_f|Km-(SgFp^JCq zx?Br=&*r@!jlGbGPxf8q=l1uO?VI|GpE*x!-Lh@g=j`?0nu$K6Ox)Fzw{~!2CqC1| zn`3x0mff0FW1F(2In775&OdiU26vxbvBs633t5>y^g`b&JHE38pUnH6FW&m!O83wC z50!0^<`2*tYY8`E6xb)~dyKe`%AtnvI!m$f~6jNldM7EiKx(P?)2=V+W)iy%u3>v>M0DTML$ z9@{<;_q5!d$uQRNkltHGI-C-SYDTsPu(c@+Oml5R{EU()9sBO zz?gb3CHhhlao+t~j^0&&t_f_+YvEtdPI8#-b&}ET4%92gbHH(+9q1yGFP|d6uGbO% zqh7CTIouoC*Y8iuevACgsWK<)sh&VTf2B{GwN5kL>QAvE=Y@XmOsV?$O9QcgG{*oa zU=l3Q*u_WPv4B&l@9NKuB`Km0-kGbf)?Q-craUgOcaHWleq#a5vCS=ad0M^M7~F;K zWHtkgOLkV{jr?E795aoqOv{O}U5DpQ0M!b9I3RQl^=bl=QlU^;&&(7;=WtXUEB+{qnLJ?TD-{%mmTa^0+L)K zEBO0pV-xLF^^PJHzLw|L*zsBIIiCNECeE?8vjB0P(||;@=8I~{k*vy65j>?JYF3x~ zwAC@%4$6G4(?4+l5k#)1lh2sd*G@@CRDM^XWS*qJyShbEvsSqq8_yU&RUxbJ%sUre z@0u05)9joUwx6J6okP`5Vs}blf(nw;+jD-n&v()OV9{j#J&en%GFt5IGDKX|2&u;e z^);*>IWtt6=04;i4msYA0qmfPIXWYrSF-$=1L)Y@XOeZgNf;fnj@|!|%i*Ajy3PrJrle5yzCGa~1YDq~Me2 zSLNAjgD1>ob<3Go)sqoFdnA2!!VdP%^C^?F6N<525@$>Q6DOS>6A9Xo5J7dkDxV!9ckqnMFLBaLyl)x=Juxv~!VA7*!VWx*3#3pnv%)aCa z?QaL)hIS5hM-uugVk1GrOcIz$9#NU!^zUKFL4 zT%DYf`gASyrOUiH=sQ8@JNcHPKEm@qaSRx9K_u_~EM&?!&UQHrZ9XBk>oJG5hm}7C z+t8^{v-ZF^eFwX@_5|Z%`v0kT-ci3gM4*^taV#0qbfyE`5#7Y#LsA_B4&0W_cK_Dn zx%j&>?l(C6maJvkHY?>ri%v%C!1)6Et0k|#5A-aE)jGy0SiC>cWdXfd_uw)S`F#ED(S2G7=G7>L;@A49nopKla< zOZ;nHI3OgDR!}S``mm8! z=PvEG*lNkJABHYnw4gSyf4LoY!H&vPmQIvk*jDJpC26llCjrR+-BGUXEwrSH| zE=OI!yikR5d#&&D(sk1U=5)rYU(3$@j&}S203ZNKL_t)*!!_A3O+--_qxF;58=)gR z7|BWuUpjEQb6}en1v-<&SPETHND0TfX?B|H#qFe|hl4^`0&NKtXCW8{xm$MDgZN74ryV`T-pP8py*FcfJNMkjQtek149Omy1!U9U zK!ysN07D_D1Ge;&3_-X)lhn0oayl6Mi!J>4JZT?SeZh4%2$a>{&EI*|IaC+)5%d%a z7lof%pQA&hLQnc&8x3|s4#SL~MP}?b!v0s4zz#)Qgwt~|A=@t0Rl~!L+&Vr8&anyt zy;DLIdL}T6GIoks4Q0E6EGTqC+wkN27R6VIwbppa9w%A=*27g`fg`d!t|)Qv*k@to zlw`!$MqsvN+cT^hJC@m2pIp4rci=%&$AUU{IBA}ZA`03H_0>onA^DWTm300cI;T!} zzZ~)n&L@(e+~$Wlh6uT)n1S_n#p-*=s^=>lj}b5d=H`273^NZ|0tnYCOrD@a-EwS) zIjo!*D)cAqJLQf*3sU4uY|A)*$LF5t8#mCa(53K=YaFpZCc)0hafL;!)U`m;`S6?% ziY5QFI4Dd9Z*YJ|@m-=lSck1W4*5?h8uutPxjZW@JMs`Zh(!J50^S@m7SL!9qZoaf zw2VLj{MC?w4Jke4=)Ui&Zz#|5`SyH{|LGlv!exUJ3TTxmzQe$Oj6T@#T-+UNmHgk} z;&{vV@?HLhn>-*IFj8^k+uR*LvmYfFkpvoX^6}YtE;$Wua0~p>-rV^9k8$7J;0t;C zoC{gHgI~VGGxM>&eP~a9Kp-0?W~m_cA>Ywy-{(-e%lq>&56)XYhnx0AZQOTwINorM zP{HP|9$3b}f7_OQ$m?}SrtkQ)cZKFp-naG7n4Jyw6QA~-@&3x(w?1#jH@4wc_*Fmf%XaM-_47NN zHs6YCzOiqgaII8HaK(L89a?{9ZXff&-QqNiu8E&|=5O%@Yn`?53`MA4lG|Obbz7dP z^YHur{%&sgLo&1F3;M*4eWHuVarcgkZ^NJYOEwvbDa5^06uWQh&_%5MiLL$ayGomN zgG2WY|Kqpp;hVK@&oTbT4K{gp6WBFGHev4G;9z$^b^4+ z&sg)S;5Kn9WFm92EK?>eiJ4laXm30-q^#s=xgza3`B+Rq(#a~6WF7rl73x3ixNms! zc`nhSTEkZ_r1l*dkdn_RYYX(}ml*eiM;09M=KYyVLTv%#ykjcS+KlZM2wAJ_4?TF=rJF zUI9}uj>1N;9Gzl&PdGjAu;i6;fxMUgpt4hNqEre)c3oxw!Fb0VC*wN{2t$JBcw2wp z+RfyDt-`Jcgvw9ts_|cNgpt`N-*MsO#15YFncnSUy|!c#Acx$#O%O>9QO-*Pewpo| zjWn{Ayn7s-ke&tDmiHY#7o8-GsQ|f3Ak?cs zy;M6nJT5pDi)gUon;~vQhN*5MpiPw4lg*1gJn-{BK^D2lDG=c($C4ikNCg9{yXXte zyJuy|(bXM!SI)Q%qgY-z>V|#K;aV7#ZW&?^uzV}_zOs1s-g;gPE&H_?3iGt6ncC5W zrN5Ukrt(Lc?U~%}NbC7t=8NDy+XApFL?IE+?pH*xg`V9`CobjXq6VPcI6yD33{)4Jo}p#_uM4~d7z&K}%A(DDbZY6y>N5d^NvZ1;6=IEy3&>%Tj2Y`p%x+FkoR znTOy3IhDK>6cz;An5!g0Fqo$hzYw>NAr$qTaT4ITi6ct_y%V5R%1fc)65RAb1~3wv z(H(k+mLdYb2!iYdz32cx3mh%OHJ3hgg(+|V8*murQgZk~4~W(4cOsJ(y3mm$=C!xZ zHGnPNunn%E$i=c+SCPw{2;tV2^$1*?(*fJteg~iSL)&ubA3e6(Zd#Wj0G$lnsBfozlAAxOh?X|1kb^kVw@e} zcSbx@9>VG&1q3Z;H;PZl&rngW3uj8FgcRcgDpq)FcIZ||o+|jfLtGLeLb$u-Uze=` zjs}8s0bd^RX}Hbl&?-+ci16GP6w134)F3tM3zCJLD{ep7eGJ}qs1zBQxG^a2M9ww4PZdTgw`RbC$e_tDm>>n`;=AYU}7F0;s@#y zu=h?H-Sx5tC|{V-`M{XJYA5Lz#Gw#Jg#A3{I`z!);hZr_94y5mjOc}An&6oh{DTO8 z>k+5AAB>x&&Lo4&pLK-7(!5UMZaRtQ0!)=*Y+w-IzT()Ki%fZsyy&z(;kl?&Q2b(U zs38Tr_VTAMUk2qIhX?n9`z2C^6L_YKud+GGZyEEutleL6R+qvjLXkVcdvfdu{BJY< zFyg==m}twFCx~7^cDnS+n5#(+u>of4c}ZvfvpI*pU^)nXgt*Ur$O+KGUK~bK9y0fM zeug;3PAJg&VZITU;CZ55S?h%-06la%x?$(EXkQ%^RQy6vmjyg zw-ZNLt{Xwjg0>TjJ;d7-ISXGv)j8KZne!`5ZbyFwMIz8;q7zUUwRs%yWp+OL+I3VE zxGf4c1eJ*Kq_3o%v5G4IJ|Ure8NqgPu_&G;-h8$1_2tYkxwnO(4pBaHDfcBDD@pdC z9B_zE&KRdPs~9oY0+LO8B^bcF)6o$iW^}&CjUTpVb!g#w3Fut69_^G-nH|jh8tvdS za3<`$lO&>XF&k5|e7|t6?))s0-Pd2Hp?QsCp2B7BfPOAaBc1#R&l7fuFh%3q;|N}t z>{#Hw=WNH~+I(+hx6B!i+;NAgX)Y3rm8mcW>w5&O(_K8=GGGaKjG8xi1kBzsK+ z>2rMD6F=AMZmz>#tdpWh*sPG{PL2hfXJ7W9#>58{#Wld^;f^s9Rat{%-KC?x;@SFQQgsmpjb!fqRV#P1B+4siFl}jPvVwQ8*Hh zK7;NknD!Ad?HWt>!wxFS#GyX$GGH~(1K&A6Bs>&u1H;7MqDXgfE$6bKf#J7NUG=#F zf%Ur%95hz7>7O02`N)0YMB399Pn*^IIL{>$7Jd%x=!qo1ueE#mDdZIDX0SdUOLS-< z?~6z7^aBgGeKVNBfIaNaNgNKuWwd!31lmq7rc-gY1MD<4f7k%_NsEDh5qXI;{Z*GfU#sTI7 zw&v_$`+%#^lafyRvBnm5EQ*2{b5o*^vIB`bov;^n2hWP2kp0@@ej+Kn$E^;V?05*Z z;$3$&{w(*|XY54}do|<69&s?}FRlq(#1Zc&B4Z05K^KHD*IDmj2fC1)Mj^x87aJxv zizly0G}pV$BIfSIt%RTVVvzG_;9=NI&VlDfp%x^9mrbcAQBH<0UAcW0-k|4 zNuH=T2E|*LEBLZkQHZK{^q$xLn#bX$hx{u(?mb^6K64e`|KK}We+#ZIAYOh=%=1j+ zQqC*_fV|<{J03{6V!?qmUW71xd+!nJ&SLe+g^ZPeIpxF|e#w6ypPeypHzOV$EU0wm z9$WVN3i1=*_t>#+8_EJuu5<>pmusZ-0u6M<9`e|)i z3(}2(D*(HM`MiS+09J(&ywlC&ksjit6(vO6O z0^7QD{KPd$&*!T(mO*OmXCRza)#D-fzf4w2xIL}_}HGi)s6es)Kyx&>h!dDo>TS|7CVqGB2?Gj&J zQDK~HhoGH5U@U6$DhYH?>m7uZ&te!0`M=DwrMLBzc7R~)#!%``{A!kP9*HW!k>tpa z1GjwjjyJFhB6J%e>qBILR@?RUZsE%#PL;DzeABD4H9k_FldNeSNV;m~ zAMS+N#e#iH6v!k~!gj%~Qur1Gfjk z3VyKicU8Py#BL{r9Aq!Y>cZ`t?8lU+Fkd>qU5Ac)U>n|4?;bskZdI=M%>)j2f?h;x zw$)sf>h??iIrhU{h?uTeZ<5BH`TJF+#N)Msd(Tp?EWSba1h^#WhCJ)YIhL-uM~ao+ ziwqa?*-X1AHbo3%8_(ze5YP}>%8{--_n_iT#D;hcUbM{y&XcZCMyM2F;P~2e>l{F; zx9vi@iqN04{0uRPLo&#wQ;C`+A?7RLR+0<;bjmniF^t!?Njq;M0R#Ag5c;JAG7>Iy ztVTF-#*{MO3|(&vg6`0FSpkxXy<{kW8Y!(ApW<7`%p>S`<5ylpA7Tz5SW5 zXcS(jwZ@`16zrRliBh4H+qF}9MXu2-cqI%M@=ny5v_3ErhVvM6XLR7E z07J@LTUanRPWs3Zxx8ah2Hj)SLlYDbgn64Xl9uef)Gr3w7lME^5pJKyZ7eLqODqJYmr#!=b zxB=Wdu4eC+Xe402L53}pTAv#tX@^k~dNcTy=)}wNnqtxdw?Z>EpT-A?3d$0|1^LQt zA^)~aR2+npXTF-DXKoUQt1vA}x&oWhvVNxY4>&+9)8W;?XeXd}3SSI_fD}KgqU9HHQvuFqVXy zZVY{vG5kh&zOa}Qcrp{(bAWyke6t5M*o4~bgm;Bpn($AY z8!lvpWq0t&V7sxFR!{k!oYmKmORA>=z1T1c+W zT;0`^`WF69igCzO23J_~RrX~0vY+6q;v<2_E6X|7=eiGU=t;b5l*@^ZBF%u&wbtnZ z5N{BO{EIF>IQa_fIDe{$eFQAFJn4%1 zfE59R9?8D}vS}Ivl8$w8!JIf^}S-cU>qA&JWcAa+5! zbap-F?D%ee5AWMRdIL>0s&vIHYySpH7I6$RO(uASt6V@7{f^giI6k7N?47%UlW-QmRas8DH4v7Y8{DZiLQ5sxq*X0 z*?D(OCUPW@E|x(vB!Fj6qRzY461V(d-K9!1J!I4}II_4>qfW6>P3L=K>m^LYWrQfFrK- z*C^96hMisNK*UYOIOjH!Vp3y*G@+Bb2#-><73&P7L*}bdk*`BOIEM9ooOSy|C|iXo!UsBp1iSAJs38QN@1GIN-FLlu;2|_EE=1 zT!I*~b0kp*@ix_T&Pd`+^jU(gh&KF)3WW(U}ue^_8vG)|8`dY=6X**xdsM zG^A%-qd>XMxdq1{#EEMhAG3W?pUE{1Jp5*E$5g-^8Vlms+Zf}&`{%^G*}H3k|NQ*< z`P+%;A36P(*~LFU|17)n8@BM*ANfbF{?=Oj+qUQY1lRnt1ODcmZh!6KgWq1xKWg&F zLw@sZzf*-b+zkuyl^LHq( zMvi+vn8(C2_~~v}&V` zco57CFHNN@*1MJ0BX^S1Bt7Yx%NnDd@wHd{Br(p;TpuSwb^t1fa9U`=%P5Ot{8u`9 zW}S(acl|7LnqbQz;e7E*`(AK175lbh2BWK)dxml`q(X7-rf9k;VrSy!!ucVdNTE2H zjD_W0E(|t>toD+u2DmOtz7WwzXkCRaN%#WtJjivtoi%rlrOtREVfnxv&GsH;G;uu$ z|JvENIDUuiT7s-&msW|lE@fYMt)in$d4&pyTCFseue9Y_0Lo+N24BURxD*fxXYaVl zz~7iLoPkyM>Io%BSdeIjD}!s5 z^x`zUgD33nDV!n#?F2i<94PQRy;`^o;8FIwn$^V9VF;J%eHWBk=6k~MrPCD53LZtg zr4Ke+QOEraFD8v$ViN3Ls$yw`fcWRmli>l92q2F|zQ&#PtBm(AV$qVdW+P9;Dg*mV zRz=AR&lDDBpO5rN$XjU_IR@z^1!A^eftr76{zSLHg?-PIho{)%?sk#<4#Hxr7Z}!F zCkWM8C@w`6)%XBn^ok>OlayfYrFeKN`~vZT3E!JrkyFN3@Ey_ZdKg-Dj1Sf1pEWOG z;=L%E>H)5Vd4o?mDKsflK>&Js|W7E$!CU)ictg}1qPTKlG^aw__`E(c^ zLA*8wC3Dyo%&(i=K4Aab*ay0>nDB1TDJkrISBd&bbS{AY5MC(_C8Z8QSpy41&`yAU zwK2xOaifYs5ww&CS+Ef-iO8SL(v@r$&Wo__AZ#y#o2QbXArCl;BilComUUU?V~3VC@+mi z?h}GF;Ow|{wIKnSw;=*FhuX zDeSu5pyQz{SU`k*WT#OnfOPQ))0@0N3r#ns;u2*=gn zq0cIJz)b|Om}T7SAoL_MbHaH$F}%ZFG02D(S0N(gbhB$w-Ap*hPFx?nMi`4O%#p=G zEf%ee7#VWwwxPW-BHAI&k;M4~c7c)uyo%i{w-qClIG`|Qa)oomqD2!ITmgSazL!JvIaanD0tr%e)}Yt)q!D@}j{Z3Bo%NqJ4@D3z%9!o|X3w z18jzS^Z2V+vRl3*faIqKNiy(XL`(;VdL?&dC*Ga}7vV$};3hp4j#H5t;QV-il?c~C zmOCG$Ml62z78nok=F?z=xZx2q|hVc5fa_OyUK&7 z(y-b>SHbs={FcODIuduCL&BIt5orz*2k8Zup7Il^J@&Og_?F3)_2C=6BNPtu|<=7>z~8;~)i*sxUJWKJPH;7A38C@x!~$^Z(6 z9^8?1VCeZia%RF|0Bi#J5vuof;u^)+6$_JaW`X=@$N{WG>cNqNaT)lB;GWAIKoJt? z;U_6F!?`GD4cM;hGajrtuPajvAbgT34&<1BU93byhj>lmOr)*gA_{QO&QG^7#(%rz z@Hye0dq_l;8wLvwSadMGHeE@b69*6{V%%K=0Va;OxSD-?-5Zcj7o?Zz``f=65Ek~L z5MqKQymPJgY3cKcTR}M=kl@LHN5OzqXg_V zw^PPl^V%25P0t)X>hEFz03ZNKL_t&{U_cDX#1j7naB?V~lgY_|_om>#l94vs%|W;+ z(mP|aEr`i77L?v)yrvb$#1+>~r8gPFid-ZdT&s@h@8Q%z9NHcOuRJeQH~{C0$pCT# zBn~1eK9DUb#{vYRXKjD3Y|iE=swuBb@Ku-)ou9;aL#?Ga=CGlx*#AHdKTTw)Y(rK0s4+m7?Nl7}4!%trn( z2u*WHGG&&ofW5f>6N9-FRib{Oub@Q2w(qF8Jm}O^bV}`Nbv3f?altQ`@%O-2m;Scn zvg$T;orycD;_$+?zv4t7+4RC)KB(eb_cpwEGkM&|QF@*k6;iAHRa;x~7Kkm+d_-`V z<^L}Hd32v-o)P7O(tV*dmSU@^?J(#r*l=8(naj#5f2sV4WlWhQ+p+k|S*Kt=dAL3+Gn|@lptxS0FV(uwvt+_&E z4=Q`Im3g%Rpu>_7axukntW~vY*$nq{eB&4M#RX`>?89ZkLm7a+50?TRso?SoIJ^<9gzh+r^E=LA2SK zxkGhLPvdTG;p8DX!_UZt70gK!d^IS?snO$|UE}#Oum;Rpg__Ji18s zU)W^KnMxj^d1-`j4ko*yT&1|~prcj1k{ejylAU7TOa0Jw0-x2!82^ob`Dyk%o_5$# zNZ-@Uf4}>0{_OrQpZ><)KINm2-TM0c(7Vsx97_Gw-CAG2;_BbOFr14o<63=Mg{f+TZiQw?gytd;bgi zb@ln5+GmpgOIiB9jrl9r`|bxtfnrEh)puS}q{um5;kbvdQmar*u>~w(9>Kr*A(a3p%Ioq?^CC z&KKVgM+WCf3HxiWo`&z*X;N~Eb0DwBKVz&OzpT_}j`7&r!E8$8|JUn0 zaF5>~hkSdDTqeERz^DtXR-A7<3r=<7aL%w+`SV@>Rd zzZNrn;CGG~ zBirz?J$+mkjvM2=D)#i(z8Lw=KrgmWnoG_NE96{-VrwR@DXRHZcp(w^?}5` z2qV7B|4kp{J$Q|2Jm-2Zo9lk*88#=(d)80Q)5A8*{l!C?XVzWTezdjAG#=p}O!dGd z#6tyujb87Yb1I9EPw_e6pSR)U>;J~ntQ{gJ&-DE)CTm@u#(IB+m@uYn`6h`GXB)YaK1*mQ$9c&$77-K9gM6L!$SsjoYj zjb-z#u#>E3Rout|Uqyb;w9YvLRBK7|b(xZQyg2(Ki;QCg8&{Zt@Le|JCvs{=HuSI~ z7n`RnU@XnC2*r!9IW2t+WfF-#WNp#Mp#66Ba=V{GVu(2mZ1l46PlH?nW&Jod#ry0- z=XiW>C^{ye4qdRBqp&7u4)IJm40#ZaWu72`$FtZ&&~=e@l6#ST{Dk`SnJ7fAYSzfm zu%ZmJXM+_f?DaDl0QRk9`7N1Pw3*K;0fan6oWTVKHTiu_}=E0{HR950134^_Yx+ksdoZ_np5)+_g#-}mAyNWHx+LUKJ%RJzD zjW4}>{u_dn7D{)(IOe}+66i5H6tdje`ZL%OaFOTb(4G>29P96(kQC5>Vqp;t69_L_ z*+GImgN@fZ*fh$|mmydQ_A!Ou4m4)ytgw9!5=@HM4=NI_U}$~z^`9@}OM%uCT(Qgb z+@HC3ukps-EJOZ;1k`2s`D{=C2nPvsc#7dOHR9;ps=Wzg`r1>>q}Cp+EJXpp?k}j87E#C6j5@Lbja5ezq`il0qX$;q9gP!bQ^Ta6H}Y}@>&x&kQiJX zAq!8ErPus?Y<586CZ^DH0S^X9MrL3ZGLPo;$VEalARaV=zRa&S*PR0cCrkvw32LA~ zuUvaTpFiC8MzPjRB7~5;C6C$8HG`md1?N<;vee-@mfld92SFnj#EoSe3d3oSjFW%Y zFmTWj(>+~&FtA*KJ6J$02u?cyiHNJ1+gvZ?nSKwV_x1TyWZwm8eo*q-KO8rey0PFh zWyp((hZNz#zb(CrVk!F~O-!H1Q{tAx3xA?}R=R7IYmig-jZo}5pvca0F>Nr|ER5iu zB(oNW6DjOZ1=YDf#swO+opVnph&&7q7CZ@$gBLn*nKn!hULXO91Q!dh;WN<&L^?NP zqE(DBi9?qBEZJJ?w&eIpK}fR05to49_w~Ry<;ls4c~O&#x?DXESw|gmVjt+ZrK)eFa2YdO7Gtke5yF^{Ubq#r) z?~H@wd}!dYAgw*eYV9dCzyo}jB+vfNXCcFfVV|aR_t|%R7O$R zV-)}8O zHZqEGE9aw}`AKmC`V%`ij)*)`WzZMQZ5HP><^{F`nlskC&=$wxhhc$bcx}MUs(1nm zGEcH$PaIeHL9H!#zLpLE{oMwQ5pG-E4;wTGSnTKWP3!Oi4%^!pl zo+n=^ppSP}YXX5Yj}>JF>%WJL)f2x|pF+dgB*1bvlc%)>5<%#W*ZS0llx}?u| zpdfZa$0f2|=?FM^k`P_hh?p8 z!vht%n~Oi>chTlbA{OO>&G!pCoNR{bpmuwpEb0N8<#@-c;CCYnSRB-12nMmmL5?L^ zQ{SQgO$GdF9)-`k_+4oeHg;ESbn-K#TiGv_bA8+XKM!6ues!lSOI~44Nq%=a2*u+< z*Xy|GC<>qN)2TGCpsj%Shq3{qV^Ao5u~6 zBU?{-brDlNp>s!Lj=Zm&|5WjGwTJm|>?vLDF+R+TOmod@57h5#41kZi(C;d@%6a*9 z9D5lr#B-Zfxr`rfR{0*r42A4$V#cyAauH*f`HsX~s2A(d3T1T_INKQGzxW(6wr5qj zi^qLR*Z3W5+BGDvf`EMoC3k@?y!qUxm7P8}*6A}8pd~D?vq$&$K{gsYdsGhsEK8 znQk{~&tq1inclo=zXg@8{XSp9c5b)q+Xbn@1@!t?Fz(;uo489VaCfxaYX2#F!VhU} zZ4QcQJ_P}~-S*w|t-`()ju^U8t<=J6ezy|hcqy`X;u1^nfJ^iCU6O;X!@u;%0)4pQ zw9>MFLz^=C>t?`aRsw>4z^vXgfHz}-JxcCky?%j9%Mjx6d8|^xFs0@H6XuoAxkWzv z#%1`paqmAjrcd_tzTja8eWDTAz3W#wem+WQ?%9>gjO>>P38mZbxSKt_=i98p!%J2C zaW0l$BL>>WAa-v*QLf&a8xqJG%Dqkd#Esvz9!k2BAaQRs>$|bn_ZI(tG>m}7j*fn9=D*II0Ff8X;`a@y;^`#!~iN|G#mlp75GYrGA!7YF~%4_ zsB{a4`$3#8g0IAa2bAPfK`s;?x;to%;)Dphs0g|sFds5V@f=g+Y3wxLJ?789kz@MYcE8!HwjM~%@E6glZh3QFm4A5d|g!1c8BU2x=3u9+*fRiVtE;s=sYqi7J@8+ z&kCmx!8IY2063j+2^^K-H`aHomJ#2I`b^8*FBOm6mN4^~x@Lq2EAthRM#F+3BrE+tAq#e)O;kwP>4MhQ?FyP62F;VD8LQ z2;Fc+9MAfB0IZE{u@+bzmO~QF@k5=E3x5rA+(is!%0)!6g8>B73c6N|%k0f11|{tQ zERy(-#9PI&a;QD$dQTu&I_Td((|~^7pu>$N4?9U1QjTk&oH}MfZ2{eiW(5ZqQk5Cx zRs_3~SX6vML1*ML4sh^V*LDQp#Ho4+#YJ|oJ6>4gpL-ItYI6|r3}~9Mjbf@I<^bAW z@UuZL%fX9x=`gLqo*X#QI9X=C1KEXcSKDy3?W>y#ze77$nhJ!OJ<~p3tkq44>9?y9fuEWDX-EfdUAg}+e)<6_&uRrpNK()X!!t4B=O#?@5(a|B;7pL z_nxeH2exA!(AF0?+5-z>hmciLmIdw6(587D&f77aMet*giHI?^1C;rR{4U-aJy%H} zILDj=k-0sQ18Ba{2_vOwWZX6eAD|-F6AhS0no5#QN$YD32_^)|`tu)b5V5AS= zfDliR_0((}A)m_YF%$PAIa-7rBC()MX3k~F+<^q9jmSR_Bj|)-F=pz@;Vtz~VLg1# z&r*@sTS$+9Mg&eAu>h5QB*j`f+V$b25&>=Pn%-{4iYKdxN}CHt!1^J#FlHy z%QNhG%$C+;9K=Tit;U+@Kab4SYS`+IZQj8bREd%&NDL$KR(s@?nwvvBaL-8kUV00j z3c2c@KyojU-*kw$i1;_#FO|xR$_WE z4C?uG9ev5yVZd{O!$?7S9D?|uL%)xy-^arIFU^SL^;rI8jtc_8<~HEzHlH98hdmo& z`h!zR3L#P%LhcO5@C*0O)cY2MvgR>^0XG|RV7M*ULjK|on!bXH+)uQxM`MC}6WAIQ z^(}n#v23=G?+OPP@Pz~<>q$1$6PG#9qhUfkOIy&<(HwBBFi${&@ML=PZDk@E*A>jL>DwzY`>VjTd8zRrm{To#Q3j zNS|=TUSNf3{Vx^io+KY(cb5Dg)`Xarc)OkjmzL|+alH06thoEB>q_^!XT1oBC@t@0ej6a?r&KGuAQSQ9MlK6vB)rCFRG8uiak8;V2e>4nuu< zVyRma#)#b*j%35aW&>o+?VHD9bOKKVz+pe8eR#?I^36{uFh+a`7c)#rKr-{!-mf}H zJ0h-_&$cmV?J)^)1QjR3PuG9o^s*q1ro-)ZnB;|~*Sin1hCFmD(t0AF-w}TcSl59$ z6~tAcOV8&4yp_RM89a4RBPH1IDnPA^j{qk0iZKV$G?@B$@HNa`>cQH8oa8l5xTwR6 zK)^AB3_|vVqKm_%o{2{?^RP0XfT#xi3%P24?z!b#cv@%>gYRtRNq21m;%3*Bqv|Y^IGi#`w=LV0_r4 zec9Jr{4jmzVeRW{zVPvAk1sQP2L1i@n`RF)`tSCHGkXc`_1>>}CSKp!K6~!dRG-?;s?NwbmJNC< z%U8B{XXA?XO=Fur0_|Y(nQn8Y{=qy-J7?Bj;#SM<`kcz}YYc^)F8z~nRPz4L?xLMX z`x)ye@xZi?_&IYs_EqbqeZ6F79%DVe9=5YB$=;F8-08<0uh|E_@X;*E@S{CYZr`k< zO`U&;uP9?8lJ7azDn8oy)Fe9~dv9gD*zcVVXE}!b-r22-@qXv$t^DfmG$t+LfKqR+ z2SLv){~Kf|*-Us>*pj?wYztrVL@_GFQ>5o&7pj<*=Q(ZjgsAhq};>f{ROSzmA-aa0vKPd_h3VJ_UK()oc_L^34Z5BT$J*FDVHfGEbVMP_H8__ za=hZ6D$c#g8{himH|x6dTO?a{?U&VGscpH;+rOL3L;eu9!P?j3VLRLX8t0dNJz`%= zJ9rT*R_kU(toHR-vj55xs#tO>)1;p(pIJJzkGb*tZH)1s%Am}R;4Yk>7Ux~oK%c?B zqObM$$1;-em3Pm7Y(|H_f7ts0ek)vfJ-2xL!-Hxa!Tb4;5uru8F_ZG0pRC*NL3us- z2{`VryDL}x*5s+1r#;+J9V&cM8U>my9&W-s^i(0Q-@^ktH5d&Dqlt?cF-?A|D1 zW{}%&Xi&o`y(x7dlgl}W&+ku|%(>s^^btRt|&wMkkyw;faR@>(Gamno?_>Xernutdj#xLlG;DazP>)p zC15`9*5loH>p>Sj@hjVCwTrHUb`e{AoT6U&iVJ%$(TlDM?mNw+l;x|q{mLRgk|#*^ z%4%7+cWQjc=M_H9F7&9(9A<;>B%@@t@2pkq8KEu2_r5 z=JfDOoy7wab*e~nANq2Z7|4*2hBtot-F8WHI7*ASBu}{ zVXsQyI&c6s`-cYV!`CK|a{2ak{&IPL!27>wY_ML~fs%?xyLFAfL&p$$@HpwVIyc65<4LyV zfOxEql)0=UPLjcpC(rwU`NtSt|G)hEbV}&sw;2FSFNp4U~Z>2fhT*a%scMnV&zH<)CC2_~3i{(tPBK}d983t>1i6YTm} zwb|G{w{Lg9S5NmjHn4YntXj1S==*%rwqF!a1g!4{^PD5ij*_^DgW|zNxW%`@7~dwJ zf_=vm0V9iqUZ7O!!}eWz%F9p5@D*1wj^^6@!md|Xe+zyq7e{=Co7jOjAvf{d!7sHp zzfqR+JNMkua2pDVMc6T$_Gn+V0yl z#8=+ml}o?5WV}y@I%6I2#eIic*cbM_C9l;l|9c*#mWnsuStwC2EKF_O-*!(kv}WC& zFUC-XLSOr`n#pc=lTNoj|4pTq#h>3gF6C>dW8=dHa3(cjWvpuSH1x?8*kahj$XQ_>jW-QEmaxTi8D>$oa}AvTuI#KTIsh z@1uXmxr;?c?7`m|zkih^4Kx9yVWme-+0g^A6-|(2d^+GdUqE=)>I& z)BHJ|&m0xsh~xD_joFXyZ0zG3ZP;vnVZ9&Zc<2=s_bb7zUsrMq^s4qH7&5}Isodmmi?1|HLw9|wSxR3|I>A8|5esa`wDRC{8g&8g2hSVSEoJn zd$!p;k>PvAS|wj4<}0w)HPY^GU%F~djzOKXIHtXm-y7DoO2<+zR?QQDb(OXA zXZFuN>P5`)@uyH;|-mhwZmUlT@PF-$opxXK~1LDs5k zY%z~@z@r!$@@W`I`2<$kQ~XFs^&&H0SIxHVg;h?hYsLp--~3c?F@`JK8*Am8!Z9oH zV}G3w#1qOP@jwNw%~gGpU|K$9U8|kd5u)`*^{GAu{{$amo5ebui#6qTt%Dt2FEL)s ziJnWwI2;3tQ##LLC^)qk>gT!^-f|2k8!4Z39@mia9p_4FG+j&esTmJ8uHFM=Pp^5Z zAFnaZrLAN+T|qs?v66D2cu;(VGIDNIUKj2ZZWrI_C*=Ywf@5%gOmFBe$8tj+2Gi#B{QpYHs`>6 z%yb6xf^nFv73zmMLVv5RL0gT0G>z_K(AxD$(kF)1SmUV=Xuou@)X-Qfg}Tp_UGKm2 zopZQg7oZ*|eI)c{>do30QehK6){??C=*gP<%NlsY;^*9hr~{-&Ti#ah3C>y0J>E+Y zIM(_>s3Z9v+y`@9CXHjgMp?njiu6@Y(2Ind@T5JU-eJ)5oc8kGiGVr1Hq<5YqV*~3 z*V5aRT3Hhiro0YPE%S_^{agE){bbE$b?f}jGX`UYv2I)vdP2fV(CC4?>(Gy!R=s~o zOpNaN(*G*!;{)i>%h4^i|tBolMV%^LGwK=9KhQZe^JN-7-V1Q@4}0+pzuH{4M(tLss*F01 zXLl#j+Hp>rq~;d2Mm)GqAq`T>r}L!GC*gap?}#__|#Ecl>Ttj zqvH7V#Wf#fNMIja9}1H8TMI)h+qa?&z|y)Ue|N`e9lm*WbY5Z23Mkmizz%CsLFP?_ zZde4HbFo^-Tz+Kh4|DkvH!m^t;@H7?c@9gtImQGHLZ7Tr+)u1obC*1Uo{vOAhu%fZofjzI`cN|~f-oiu1m%U(Dr=>nm zIeQte!dgG!{omG8hyyy~@EyKO`1*uj@;-c|ON+T+U7QW&uekOVPHm|BKqKY*z0`sq z+ETPV)XH&REccR3U;J?Y!}a-QPMtlxFL7hNe^CCmuHM8q*K~({;Eg_b$&n>I9>fIU zq4Zs$+m4*&RTlvu-?BJ_09Ur6Bv*Znzu zxU})gZ?^2t$R6azr-!Vbx^$I3-OgfD zvI5M+W#j#Ap4`Rwu)K(w6?OKolI*uT$*VsxDuv`AEl2hGo-+0US>rS2jQ;fI)5tJVidA@IOtA(ErxpSGX@7cI&GdEv zzeeM4vmBClecTuL{e$|IV>w7)cA1>;%2JoN zp!4%%V43umND2|9w|XP*y{gF%WNSgMd@c<5&ACrlm|3Efx8sZVt;lYAydYLYZIyGG zYJr_buV)n>o{Kg_2E3Gz~6v7o^@Zbyv+~@YwUv)J^g(8I%;FK3DvgNAn z@%Y~hPB@(aEDLY~768DB{*4V-DlGEpv|zzWj-|7)0LuwDEqc)j_NyFRPV#wKfYWKi zf+}5;_SFRzoM;_DcT1P0>zC6BC;h2roJ5Rd*CuODa-D2Nd)VmmIal^=9J6AM6~s53EJGWB%3?=GE~jM32^z-~ zX*XdBc}V#e%PnslK*5Qey8t+ykn(J>Fej5;a6cGT;M_8}7R`_Uj+3Gd9$fJWZ2n@< z+!mlxY%f?U3k~Be?45Fv@~Ti-1X+Rw!&J)EiGEs42v?G$w7&#vv0zFpbRWYF;a%PU1HeZD{@pYbWE3li_};l%!&>(7l%cK;vw)Mb8rd#Q6{(;bl8h3xau1 zv6wEaI0xotgkb^UfwaK&rQe(aMV{D3#P+B8VlK8W8gd;&w8ZEy9*@xXj4PJVm%&`A zm<;WQ>@uz5HhDfg3RF23_Y>J+b&92O-t)D=_DVm~bG@l}=QOop_$OXpPTp%5s;Hnr z=ohV(4BiWagX@C(0`+w7X^Zs_79`%IPMj<2&!MLUJ|})z90Lpa>6H|7apFEeS>Chm zLPX?yNv`Q3WWa#%E?o<-Y;!BDkH!9ktaotE7uN)_hvXWVe(rQGIM1-&$-Kb2%6WnD zM&MAMwaZQ+~WWNah zPONQ%-b^#h0%UmK2^GTOVg$T|^*pbJ|5KcEVmx9FVtpq2mIc+x3yB*E_rncXs>SfP zu-&37c`iU4aoS?G~LlrAq~^FTxp^dYF$ zPjNOgA2S>vYK2qIJ;G@Sd%Bp>T3m!v5=QfR&|>G@lg+BT=^=LgQ|xLk7v?~xXGh=FIE9}ipefky?L(??*l>+wi-+3TW@3P^T7I!odXB|T z?M-<~UOmnpJzJN2d!R}#dmcRY5_4d~1Jy%0@pEZO6(DdVM{^@yVO{nC=V)l?YJy7XS1q|ax5?Ru<=XbbJRLq~Q?)uOpj;<3g^~2Dim{ zL-O+=pj!RvHCks6x9xF$SnUKGWNoJeeqB@iB`$Cr!E1+el3nY0eN+$2b;5|@pYV2m zJR(nuQ4htPaj9S<<|%PkhApnyvvW4bnsW50@Bq}uP_M$HK0~?f zA?H8N%Hhg|!H;0K?g!&>G^vK&XDJfxw;Aa2<PFrVLs5@7%zh` zBI}SnhK;jwjhZO+3&b9m^?Q%EdaVDByzaeT!%V$j`M`31_WtI3xtM;q&DNVFFZPo- zwxoSxeFeTi;A*LN#o;+L912*9HilTS1~BFKQ0<_$x-LA69xxqK?|vqwq9H|0+iZ4HI=vz&n+# zt;;oD;JKX+HJ$%pRc?Ip&^m~CbM5iSv?lRbwEt0Q{=El2SS)z`JzQ&Lx});4f$5#p zw-i6gZ>xnz%3tIG!b7fy2wy?tiA`oP)sgEr>XkT~HrVw|#U}AN$C5eEZK8ezb{-{{q=Nt{`=?`E(Lx}I56Ve2vkMbVQ z`}4eVCXR@+681BWW8N{Uh5Fd{G|Tnlv73iLyk>bi^P2EnEASQnHvd0xG;@TRQiAF8 ze&kBmT+9co6?vbB@sr2S`x4h<@uowr$9RA>I6SiU7U#YEzKq@%;a+o~4|ad5ZQK6m z%XhM(=zRMpZ<6oU-rqFX19w+|2gBr#|9oO&$-jJXdAHUF`|O&@Z}^sjV3)`9am)hH z5A5E7=?EL8f&Jjz-HnwXq7QBsU)yBkjB^}`TwbZsGqrI?n4;%{MwuP@ZmVLtQt z<3%mM!QoFiDPP|N>I1tWtth7PAx;YU|6OkXq+jGi!XcLyFjNpH#dF{Fx5AC{lb-&r=l*v8X8r;Gp4hzMr+4;# z#*MGB{c?}&;mz-2@{h);WcTmPu&kAjwAR5x;zPV1&}gQ)&qP|_-=2X?+EeNPfXr5$37i>NAXF=n}P{qxCL(B`Mx`) zlnQlEeraU?xoHlsIdmjDVMZfi6x-CT7dh3K71N!iB)7k5cy9z`%^apz0U}yGs&IL>d;CuZDE2us6>&QkV!Q(^=PX+> z1H(J0@7xI&9K}jFo9y24oPcIu$*Ct;JS3!G?s=J`lKp%8?D|4+xUa%-h08-t(i-Lc zVtvg(x##hj1ZwSnWm-Ip!8XsR8s;d>yN&a7ayS%UuiEMnre6XH+&dzt@3|=FxQtkE zq#a<|V$S*C@Y4A(%HkTui60VNwKd&`VUpYwk+tMj$p$#*t zw8cz3-|YLDUDv#7nXfTi?1u%@cjrYGJ**Js!~zW@ADcrPUZY};``bFPf)DE{Bf57Q zW~V)jSoT58G06uTPfQZGk3jkq!e*i*1@vp#|C|oj(C+p8(Kq zXix!7+WiT%z=o%Y-)X|sChHpU3vIF|`TKM{O^x`awoiB>`H2Z=+X3hwrQah zswaSUYsA*)1fZQXDrlQ@y?AA7O@x_N?42~8$@mRGHQBcl+9ugFwLsf6$%|~X}5%+sgh%2 zXIisq+ik*BwMh0q6?~2R;LgCDnWjcvO}J<{mIgtyIrM#U|OyhtyNysPYTFsPVDK%v0O=!iZ#;$55pBlRc zsL-SUNtm|g>Gd*sz&A)8L99}=r~kER<+F2y0x5lJfDL$7PxYyp&`fQLcQXlW+Ef>o zi-sX4XcqQpWF?6`6n|)dmT)wonvnAT1TZxzP7_eIm=gBj7!lG+xdr7zp_Fg|0NZm) zYoyPO`Q1!N`FMIwv`>l41Uv&Ct)jPSYN`~1r|h3N4^vZ3t>&=R@L|$glbxqlqm%eV zpry5*YIkZ=HbA^>wUW4j*`QH^Z6yzl@FUwReAZa~$+g6%65I`#s$g@nQ8)|6D-9G6 zUcfD$=F9X{OgL}qK2y_3{)q$>r%tfD-Z8aQaav`f}aM!B>fX@Vg${>GdZ`R0h&o~pf%1P()Sa#62D7R+$J=MJFGV+%r-5y zw8T^yHl(0a{ke`}ct4fk;JGsm(zYoVi1%1DE^PzrPa13GTA@>7o@BC|Xg3a(>Yvo_ zQ(9WlK#A`m`Yb38iKF^tJ(xS4a$=kC-co+t|Fq82^~$E&CFM+NsBO`Trku1_fVB%F z++|5j!WzofM&IRikCf$%MS$5SE*SWwaBfr&;T9*R`Ypw5&Vvw3te?tI5U*=T~cs z#u2DmV0(&IH9aqQdQ$suA$d_Eo+i^74Qn~oHCkgnu9+T9niY6b&w=;GRKHFp+Y`^q zhLg4l+FUN$fCf+1n?}*%&(ncvn&g=le5So!A7}?9z1}uFg*i7QY(1U#6+19-v;nA| zuzE~9*tE8f&pWgqVY$ilKdi&6Mba}T$7aAE_fX@IR!~dfhvq5S)jUy6t@lNr=Nn8}KS`X}NPqI< zsfUWX5Zj-P&X({xhBXJ%`z^e-8siGFQ|;IJU-aiw>bT|k=&3CPK$CQLvM2S6r}i<` zTWERwaAhQM*6C$xM0;p{h-rlEyxR!JAo1Wf8SZJWcAc_K(tnMYC*i)pWR!23BD9ZA zZR2xmBXdm)RR8&(+yB7jjtef~wV?Z5#Lnf83xNCmf(!0`!sW8vfeMgo@AvACbk6;P zbPa$DF5CT%>Vi9Pkv;U>3%2_m6)wQ#eoq(Svf+;EegSmt`yG{@d$|DVRN3ba+%Evs zzO+Y$`yI9i?$zZE02kaZzNWd7{zK;D)Hih`z@0GATCJ`;|slJ1AkbH&K;(y$VIo-gkWhD*bW*$u(4oa=o-_mMT#5#{Y&z1}OvxyHcOB@A+`yv|bmOB`qX zYK)L4hCLwj*vOuCrS4{^i)`J()j3e_$>pUWvU9#3jU&YWro+MWOZ% zumSBm$gY!Gx=1|kq4#-j%x&J`9{ap1IT&|`^%728OIQ{iS9vch>YcQAs(tNe%Ewa6 zggvk8xR>$SMe8lu(DtAyIz!RBqBG=Sidt~@pCWKmqWd19pD1y3+(WFF+A+* z4y5_KmkHlrz{>rpM(8Gm2 z_8d<-^OA9z>O;~{K7RyVO;`YSKG52FUBF+ap?zPNmSMhj-;-U2RpwP_?MFSL9FWDd z^3Ls=J@ouoPp&{n&l?YD_r0K)Q|`&R{<7h&?)evF+E-@_(`TYd zWkFa^M9_&rTa{+(fRXz?ckubf!7R){^N`LtHe+3aCS@){i9-tfQK=@i{vldo)7616(mGTR}sw=KrW=w_ab^EVqZ^41;*Yy{I4<-9Q z@abw7Hx%?0+O+Rk(EPv+#Ky?%$k{t)N=$yofjo`0dj ze}$g%H}#8uvS%Lc^-uThpYHbw$Nt;e<8RylAJZtr>2LVsU+4Be^JjOn`%mYv)Kfmu zNPl<#`5iz1a%}!Zt^Jp6`?q`0zZtt<3A4Cfw}62In?AwGAH#NL!@Ip-?OVL|FLK>S zHS;RBpX$5686UqrtG{fe*X<4z5#p# z$8P|1&S>Y2!1svpThHNtM!sgZ{w@bFjAAdo$Ln=v{{ejCdR_N%Krxnl-B^g(&ad2V&e4eP z>jI0Xo}YdnLrll8o?rs;-0O8*_x2R>e_dA3kz6}29+zPAm|=4a=P33xzE>>kRbn3f zrd$Ev*F|i-E-S{-{2K*d7)ObX`i3>s1>-ndJ76ZeCbwpD^mMxHsY>)oCse2oJh zUO?P5j`5n2>tl3$0^j3xS;vF8V5Ar@KS{ov+mErN^?ik}BiLKVoa4vQd&79Z*TDC( z5dVx>EK9#%rS3A08W}GmcKHZcusUEHcXe2fA@!nh+1Gh)gYn8Z7V)0%vs}mE-^e|0 zEL=ajKO<{^qD|3;(4PrQ(u>x-e~sMl$84`%e#afQ9rrQD*87N010P#XM&j6Ik2%&X zOQt;-ljFWe?`$5AxO&{xNa)e((^YEX5YJrmW5%c4m-Luxl)&YD?wIS6bB^x9000T6 zNkl)?zN}$FZ0v5*ZJe#_G0fY=OOBf5#MEfOV=%|6|!7g58Crz&RIMD z#tZF=f-WFl4xDwpu4AT^u*+SJ`LW1p;{k8y7!0~Y_SZRMqBhV#9pzvVJvQ#Op&pC6 zo?@cbQ%9a^A=`k<;fAFanJ7D11-S% z%ka4@k&DOcdQ>C%3=GnbVGWH7^;Vv*N5ZYn6JO}C0z(D#TQ{H@0H6nYU>ks{p9gg4 zH*{SE448*L-8$+!400E#9|jD4FMq?(fFUgb3_WH5eGll#b`7urb2VeYF!bpAba!e% zSM`3@1^^v83^S@89WbP0T@MUHGBlk&0DZp=9jYD!2K2UB-($m&UYY8W`5Vvy11j`D z2W%LIYCw-3gD#G)mo<89!+@&C3=9K?9_VH7A%#N!Y#2~={QwNIeGjC+U`fs${3rV4 z&wxI~NH!bfgg#xoLGsdL@I5i0Q|E{D)9jLcyTRAA7+HpZe(ufixl8K|w(soMcGx2wSjy5ywhx}W9v zAW`c(%Q<>%n1?E*R`%|?Dn)Gu=JZ=X^zJjn7l~JIu~FZ9!M?|}n|qIM*IQlmA?gZQ zk21R1){qiHr#SFzVHgBX{hYq*X7v80VXpTIv^dH+8-}4u28cg1&?$z@xi0$L8IUcH z{V-sb3+5RZ1Oz>>p+hwYzI(!F@}Wa-ag`j-!XH{g`k!tM1}vZlX7uQX8C(mUoHrn= zN4a*kI-JEv4N{85-1itd@l~+WC0ta>QJp=JohwjDxqThF9vd)JiSs%;t~Hu!d=7X* zKVX;*4O`cx>r+@e+N#4a6VIXFv~~vN>N(-rK&Isg$>nTlH(njOq0-EDgOg>#i`$)2 zDC{@@LqDVMQhyMB7T?7aS!?Lw_!2J@!qYjkySfdj2MHEn`AApLe#$ws9p8o-mDXzC zK|My_DVF5S*)k!&1N|U>5&zJIoQl62hPeXqI6B3%M00@PIrSdR*X_^`1^i-y%aI0oF|=0iE#iJaj3C*7Jo&1Rw5US0&DIaW`lk zVu0y7<|mlGO4_H#HVj?W_XE%+UQ}Ku86gyvdb;a_w2u3m&Pm$P?Vo$+@*acL;M^tc z(e-_bw}3)9zaM6VyqB7G#$zDcd$C&K;4Hzoz!`8o*%T?^D6!jJWCl%P*fa);r z8QS+r!+K4iQ;rawItbnpoV%pc6KQ+Rp%a}uR9)ZurbZmS^(XX+Gy8$Gk#K`afw}Wb z2VfAt`#F1r?E2MW4|IY-xZ(qZA01)+JWF?@`9N|$09E3Gl<&k_Mr;;p#agdD(RDLA z(WBOrSoaJ%r}QZn7$|+S=UB9k+Bq15rCz1H-?P4{4Py_YZWESjpG_)JkhH$R^jl!%CJ0Hn=)AMtMIX3NC0p0l?60$g3sxq-! z?1_YC0N6Url^3UDumjA`j(TM3$@0DGw!tYHz35qqZ_>H5)kVN>--*Og40j1{*wr%yc zZC|(BR`1TeZS{7$@$oue>$cr$y_VO@*WmMX&FyxR_0#dR#um>ltw(DF8>25ZUt>1j zwq)Zi?PL4vIr5pmXJ6R{^MBidZ#P@p&-1-w{ENQnIvd%xk=?f~#B9@_w7$hXi(~ZR zMlqm$X^-gVEvymp80>K0x9zsuC&ZsI$9dWvi}4S3hnUdy(f_aQ7BR8I(NV1w@XdHF zdu@zz*alCC?h%s{z{Zr@m1RJxsC?Dvt5(YwU zIX(q!+=yF_t}SBKe!m6I3w6$%kk@PQG0R!D?lHE@c6#k(d|&W=$6F=N$oV>p(JkXF z#><|km;-NREYP(v|IvrQlQ*}k)MxXY`0?1AL$A(iYAdZt`3*R@Wj18}#r+}f+3!uC zWB<-;BgaXJOTxal0w!-!yRZ$`S8fsSIWO<(G5Y#ayX0ghp##6cEE8#SKZ3oP1i-f4;W(3VT~5@zSEGqK5;wd^;cZ!He|h#>*fWA1zT=q zJcN46*1H8g<>!)}p*NS;&ipB0rP#~zzL4wrQoc)=KH$ot9yTs1Xu?~}f0#kI_uq=M zNu6sW*8OGuRMs0uI>2gSyTy7f*7|mckK7+#??~$%^{!awS*_k~uWR<8XC2OrJ1uz7 z`(LfI|!B8TNR?0VL1KTn4_l{7)A*76#Qx~Hu5%d`9Kbx&rF y*|y>=P}JcEoV&wyVV3v#c-zlk2bwYOU;iJb9{>kFbCAIR0000}G literal 0 HcmV?d00001 diff --git a/bin/Icons/ui/left_stick.png b/bin/Icons/ui/left_stick.png new file mode 100644 index 0000000000000000000000000000000000000000..101b99cb170ad267ae83d33153fbdfdf647af792 GIT binary patch literal 3888 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 literal 0 HcmV?d00001 diff --git a/bin/Icons/ui/right_stick.png b/bin/Icons/ui/right_stick.png new file mode 100644 index 0000000000000000000000000000000000000000..374f589bf9c23971b215a74b08609ab0d69799c2 GIT binary patch literal 4178 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 BeQE#z literal 0 HcmV?d00001 diff --git a/bin/Icons/ui/square.png b/bin/Icons/ui/square.png new file mode 100644 index 0000000000000000000000000000000000000000..0be20e14c9796487acbec23cfd4b516d70833794 GIT binary patch literal 3385 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 P00000NkvXXu0mjf7I#ga literal 0 HcmV?d00001 diff --git a/bin/Icons/ui/start.png b/bin/Icons/ui/start.png new file mode 100644 index 0000000000000000000000000000000000000000..3822fd85ff33829767a01fca014ea8ac39b11d37 GIT binary patch literal 2606 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 msgString, vm::ptr(Emu.GetCallbacks().get_msg_dialog); - - if (!dlg) - { - return CELL_SYSUTIL_ERROR_BUSY; - } - if (_type.se_mute_on) { // TODO @@ -81,6 +75,33 @@ s32 cellMsgDialogOpen2(u32 type, vm::cptr msgString, vm::ptr()) + { + if (auto dlg = rsxthr->shell_open_message_dialog()) + { + dlg->show(msgString.get_ptr(), _type, [callback, userData](s32 status) + { + if (callback) + { + sysutil_register_cb([=](ppu_thread& ppu) -> s32 + { + callback(ppu, status, userData); + return CELL_OK; + }); + } + }); + + return CELL_OK; + } + } + + const auto dlg = fxm::import(Emu.GetCallbacks().get_msg_dialog); + + if (!dlg) + { + return CELL_SYSUTIL_ERROR_BUSY; + } + dlg->type = _type; dlg->on_close = [callback, userData, wptr = std::weak_ptr(dlg)](s32 status) @@ -213,6 +234,33 @@ s32 cellMsgDialogClose(f32 delay) { cellSysutil.warning("cellMsgDialogClose(delay=%f)", delay); + extern u64 get_system_time(); + const u64 wait_until = get_system_time() + static_cast(std::max(delay, 0.0f) * 1000); + + if (auto rsxthr = fxm::get()) + { + if (auto dlg = rsxthr->shell_get_current_dialog()) + { + thread_ctrl::spawn("cellMsgDialogClose() Thread", [=] + { + while (get_system_time() < wait_until) + { + if (Emu.IsStopped()) + return; + + if (rsxthr->shell_get_current_dialog() != dlg) + return; + + std::this_thread::sleep_for(1ms); + } + + dlg->close(); + }); + + return CELL_OK; + } + } + const auto dlg = fxm::get(); if (!dlg) @@ -220,10 +268,6 @@ s32 cellMsgDialogClose(f32 delay) return CELL_MSGDIALOG_ERROR_DIALOG_NOT_OPENED; } - extern u64 get_system_time(); - - const u64 wait_until = get_system_time() + static_cast(std::max(delay, 0.0f) * 1000); - thread_ctrl::spawn("cellMsgDialogClose() Thread", [=]() { while (dlg->state == MsgDialogState::Open && get_system_time() < wait_until) @@ -243,6 +287,14 @@ s32 cellMsgDialogAbort() { cellSysutil.warning("cellMsgDialogAbort()"); + if (auto rsxthr = fxm::get()) + { + if (rsxthr->shell_close_dialog()) + { + return CELL_OK; + } + } + const auto dlg = fxm::get(); if (!dlg) @@ -263,6 +315,15 @@ s32 cellMsgDialogProgressBarSetMsg(u32 progressBarIndex, vm::cptr msgStrin { cellSysutil.warning("cellMsgDialogProgressBarSetMsg(progressBarIndex=%d, msgString=%s)", progressBarIndex, msgString); + if (auto rsxthr = fxm::get()) + { + if (auto dlg2 = rsxthr->shell_get_current_dialog()) + { + if (auto casted = dynamic_cast(dlg2)) + return casted->progress_bar_set_message(progressBarIndex, msgString.get_ptr()); + } + } + const auto dlg = fxm::get(); if (!dlg) @@ -287,6 +348,15 @@ s32 cellMsgDialogProgressBarReset(u32 progressBarIndex) { cellSysutil.warning("cellMsgDialogProgressBarReset(progressBarIndex=%d)", progressBarIndex); + if (auto rsxthr = fxm::get()) + { + if (auto dlg2 = rsxthr->shell_get_current_dialog()) + { + if (auto casted = dynamic_cast(dlg2)) + return casted->progress_bar_reset(progressBarIndex); + } + } + const auto dlg = fxm::get(); if (!dlg) @@ -311,6 +381,15 @@ s32 cellMsgDialogProgressBarInc(u32 progressBarIndex, u32 delta) { cellSysutil.warning("cellMsgDialogProgressBarInc(progressBarIndex=%d, delta=%d)", progressBarIndex, delta); + if (auto rsxthr = fxm::get()) + { + if (auto dlg2 = rsxthr->shell_get_current_dialog()) + { + if (auto casted = dynamic_cast(dlg2)) + return casted->progress_bar_increment(progressBarIndex, (f32)delta); + } + } + const auto dlg = fxm::get(); if (!dlg) diff --git a/rpcs3/Emu/Cell/Modules/cellSaveData.cpp b/rpcs3/Emu/Cell/Modules/cellSaveData.cpp index 1c3615aac9..2f45e39e1b 100644 --- a/rpcs3/Emu/Cell/Modules/cellSaveData.cpp +++ b/rpcs3/Emu/Cell/Modules/cellSaveData.cpp @@ -355,18 +355,7 @@ static NEVER_INLINE s32 savedata_op(ppu_thread& ppu, u32 operation, u32 version, while (funcList) { // Display Save Data List asynchronously in the GUI thread. - atomic_t dlg_result(false); - - Emu.CallAfter([&]() - { - selected = Emu.GetCallbacks().get_save_dialog()->ShowSaveDataList(save_entries, focused, operation, listSet); - dlg_result = true; - }); - - while (!dlg_result) - { - thread_ctrl::wait_for(1000); - } + selected = Emu.GetCallbacks().get_save_dialog()->ShowSaveDataList(save_entries, focused, operation, listSet); // UI returns -1 for new save games if (selected == -1) diff --git a/rpcs3/Emu/Cell/Modules/sceNpTrophy.cpp b/rpcs3/Emu/Cell/Modules/sceNpTrophy.cpp index f7f1ee6112..46c4738ef3 100644 --- a/rpcs3/Emu/Cell/Modules/sceNpTrophy.cpp +++ b/rpcs3/Emu/Cell/Modules/sceNpTrophy.cpp @@ -534,9 +534,8 @@ error_code sceNpTrophyUnlockTrophy(u32 context, u32 handle, s32 trophyId, vm::pt sceNpTrophy.error("Failed to get info for trophy dialog. Error code %x", ret); *details = SceNpTrophyDetails(); } - Emu.CallAfter([det = *details, trophyIconData]() { - Emu.GetCallbacks().get_trophy_notification_dialog()->ShowTrophyNotification(det, trophyIconData); - }); + + Emu.GetCallbacks().get_trophy_notification_dialog()->ShowTrophyNotification(*details, trophyIconData); } return CELL_OK; diff --git a/rpcs3/Emu/RSX/D3D12/D3D12GSRender.cpp b/rpcs3/Emu/RSX/D3D12/D3D12GSRender.cpp index b1d3d6d729..5b34c42d2e 100644 --- a/rpcs3/Emu/RSX/D3D12/D3D12GSRender.cpp +++ b/rpcs3/Emu/RSX/D3D12/D3D12GSRender.cpp @@ -301,7 +301,7 @@ void D3D12GSRender::on_exit() return GSRender::on_exit(); } -void D3D12GSRender::do_local_task() +void D3D12GSRender::do_local_task(bool) { //TODO m_frame->clear_wm_events(); diff --git a/rpcs3/Emu/RSX/D3D12/D3D12GSRender.h b/rpcs3/Emu/RSX/D3D12/D3D12GSRender.h index 18a94718e3..7674cd13f5 100644 --- a/rpcs3/Emu/RSX/D3D12/D3D12GSRender.h +++ b/rpcs3/Emu/RSX/D3D12/D3D12GSRender.h @@ -173,7 +173,7 @@ private: protected: virtual void on_init_thread() override; virtual void on_exit() override; - virtual void do_local_task() override; + virtual void do_local_task(bool idle) override; virtual bool do_method(u32 cmd, u32 arg) override; virtual void end() override; virtual void flip(int buffer) override; diff --git a/rpcs3/Emu/RSX/GL/GLGSRender.cpp b/rpcs3/Emu/RSX/GL/GLGSRender.cpp index 7417ba8e81..be3fe8b014 100644 --- a/rpcs3/Emu/RSX/GL/GLGSRender.cpp +++ b/rpcs3/Emu/RSX/GL/GLGSRender.cpp @@ -32,6 +32,7 @@ GLGSRender::GLGSRender() : GSRender() m_vertex_cache.reset(new gl::weak_vertex_cache()); supports_multidraw = !g_cfg.video.strict_rendering_mode; + supports_native_ui = (bool)g_cfg.misc.use_native_interface; } extern CellGcmContextData current_context; @@ -614,9 +615,6 @@ void GLGSRender::on_init_thread() { GSRender::on_init_thread(); - m_frame->disable_wm_event_queue(); - m_frame->hide(); - gl::init(); //Enable adaptive vsync if vsync is requested @@ -756,16 +754,71 @@ void GLGSRender::on_init_thread() glEnable(GL_CLIP_DISTANCE0 + 5); m_depth_converter.create(); + m_ui_renderer.create(); m_gl_texture_cache.initialize(); m_thread_id = std::this_thread::get_id(); - m_shaders_cache->load(); + if (!supports_native_ui) + { + m_frame->disable_wm_event_queue(); + m_frame->hide(); - m_frame->enable_wm_event_queue(); - m_frame->show(); + m_shaders_cache->load(nullptr); + + m_frame->enable_wm_event_queue(); + m_frame->show(); + } + else + { + struct native_helper : gl::shader_cache::progress_dialog_helper + { + rsx::thread *owner = nullptr; + rsx::overlays::message_dialog *dlg = nullptr; + + native_helper(GLGSRender *ptr) : + owner(ptr) {} + + void create() override + { + MsgDialogType type = {}; + type.disable_cancel = true; + type.progress_bar_count = 1; + + dlg = owner->shell_open_message_dialog(); + dlg->show("Loading precompiled shaders from disk...", type, [](s32 status) + { + if (status != CELL_OK) + Emu.Stop(); + }); + } + + void update_msg(u32 processed, u32 entry_count) override + { + dlg->progress_bar_set_message(0, fmt::format("Loading pipeline object %u of %u", processed, entry_count)); + owner->flip(0); + } + + void inc_value(u32 value) override + { + dlg->progress_bar_increment(0, (f32)value); + owner->flip(0); + } + + void close() override + { + dlg->return_code = CELL_OK; + dlg->close(); + } + } + helper(this); + + m_frame->enable_wm_event_queue(); + m_shaders_cache->load(&helper); + } } + void GLGSRender::on_exit() { m_prog_buffer.clear(); @@ -826,6 +879,7 @@ void GLGSRender::on_exit() m_text_printer.close(); m_gl_texture_cache.destroy(); m_depth_converter.destroy(); + m_ui_renderer.destroy(); for (u32 i = 0; i < occlusion_query_count; ++i) { @@ -963,8 +1017,22 @@ void GLGSRender::load_program(u32 vertex_base, u32 vertex_count) m_program->use(); if (m_prog_buffer.check_cache_missed()) + { m_shaders_cache->store(pipeline_properties, vertex_program, fragment_program); + //Notify the user with HUD notification + if (!m_custom_ui) + { + //Create notification but do not draw it at this time. No need to spam flip requests + m_custom_ui = std::make_unique(); + } + else if (auto casted = dynamic_cast(m_custom_ui.get())) + { + //Probe the notification + casted->touch(); + } + } + u8 *buf; u32 vertex_state_offset; u32 vertex_constants_offset; @@ -1244,6 +1312,13 @@ void GLGSRender::flip(int buffer) gl::screen.clear(gl::buffers::color); m_flip_fbo.blit(gl::screen, screen_area, areai(aspect_ratio).flipped_vertical(), gl::buffers::color, gl::filter::linear); + if (m_custom_ui) + { + gl::screen.bind(); + glViewport(0, 0, m_frame->client_width(), m_frame->client_height()); + m_ui_renderer.run(m_frame->client_width(), m_frame->client_height(), 0, *m_custom_ui.get()); + } + if (g_cfg.video.overlay) { gl::screen.bind(); @@ -1334,7 +1409,7 @@ void GLGSRender::on_notify_memory_unmapped(u32 address_base, u32 size) } } -void GLGSRender::do_local_task() +void GLGSRender::do_local_task(bool idle) { m_frame->clear_wm_events(); @@ -1354,6 +1429,15 @@ void GLGSRender::do_local_task() lock.unlock(); q.cv.notify_one(); } + + if (m_custom_ui) + { + if (!in_begin_end && idle && native_ui_flip_request.load()) + { + native_ui_flip_request.store(false); + flip((s32)current_display_buffer); + } + } } work_item& GLGSRender::post_flush_request(u32 address, gl::texture_cache::thrashed_set& flush_data) @@ -1415,4 +1499,9 @@ void GLGSRender::get_occlusion_query_result(rsx::occlusion_query_info* query) glGetQueryObjectiv((GLuint)query->driver_handle, GL_QUERY_RESULT, &result); query->result += result; +} + +void GLGSRender::shell_do_cleanup() +{ + m_ui_renderer.remove_temp_resources(); } \ No newline at end of file diff --git a/rpcs3/Emu/RSX/GL/GLGSRender.h b/rpcs3/Emu/RSX/GL/GLGSRender.h index 8c3cac44bc..f99313fae7 100644 --- a/rpcs3/Emu/RSX/GL/GLGSRender.h +++ b/rpcs3/Emu/RSX/GL/GLGSRender.h @@ -294,6 +294,7 @@ private: gl::text_writer m_text_printer; gl::depth_convert_pass m_depth_converter; + gl::ui_overlay_renderer m_ui_renderer; std::mutex queue_guard; std::list work_queue; @@ -361,12 +362,14 @@ protected: void flip(int buffer) override; u64 timestamp() const override; - void do_local_task() override; + void do_local_task(bool idle) override; bool on_access_violation(u32 address, bool is_writing) override; void on_notify_memory_unmapped(u32 address_base, u32 size) override; void notify_tile_unbound(u32 tile) override; - virtual std::array, 4> copy_render_targets_to_memory() override; - virtual std::array, 2> copy_depth_stencil_buffer_to_memory() override; + std::array, 4> copy_render_targets_to_memory() override; + std::array, 2> copy_depth_stencil_buffer_to_memory() override; + + void shell_do_cleanup() override; }; diff --git a/rpcs3/Emu/RSX/GL/GLHelpers.cpp b/rpcs3/Emu/RSX/GL/GLHelpers.cpp index b5e4f08996..e892f72c31 100644 --- a/rpcs3/Emu/RSX/GL/GLHelpers.cpp +++ b/rpcs3/Emu/RSX/GL/GLHelpers.cpp @@ -557,4 +557,9 @@ namespace gl fmt::throw_exception("unknown primitive type" HERE); } } + + attrib_t vao::operator[](u32 index) const noexcept + { + return attrib_t(index); + } } diff --git a/rpcs3/Emu/RSX/GL/GLHelpers.h b/rpcs3/Emu/RSX/GL/GLHelpers.h index d687a1be3d..4884a76df1 100644 --- a/rpcs3/Emu/RSX/GL/GLHelpers.h +++ b/rpcs3/Emu/RSX/GL/GLHelpers.h @@ -58,6 +58,8 @@ namespace gl void enable_debugging(); capabilities& get_driver_caps(); + bool is_primitive_native(rsx::primitive_type in); + GLenum draw_mode(rsx::primitive_type in); class exception : public std::exception { @@ -489,6 +491,7 @@ namespace gl }; class vao; + class attrib_t; class buffer_pointer { @@ -1167,6 +1170,43 @@ namespace gl { return{ (vao*)this }; } + + attrib_t operator [] (u32 index) const noexcept; + }; + + class attrib_t + { + GLint m_location; + + public: + attrib_t(GLint location) + : m_location(location) + { + } + + GLint location() const + { + return m_location; + } + + void operator = (float rhs) const { glDisableVertexAttribArray(location()); glVertexAttrib1f(location(), rhs); } + void operator = (double rhs) const { glDisableVertexAttribArray(location()); glVertexAttrib1d(location(), rhs); } + + void operator = (const color1f& rhs) const { glDisableVertexAttribArray(location()); glVertexAttrib1f(location(), rhs.r); } + void operator = (const color1d& rhs) const { glDisableVertexAttribArray(location()); glVertexAttrib1d(location(), rhs.r); } + void operator = (const color2f& rhs) const { glDisableVertexAttribArray(location()); glVertexAttrib2f(location(), rhs.r, rhs.g); } + void operator = (const color2d& rhs) const { glDisableVertexAttribArray(location()); glVertexAttrib2d(location(), rhs.r, rhs.g); } + void operator = (const color3f& rhs) const { glDisableVertexAttribArray(location()); glVertexAttrib3f(location(), rhs.r, rhs.g, rhs.b); } + void operator = (const color3d& rhs) const { glDisableVertexAttribArray(location()); glVertexAttrib3d(location(), rhs.r, rhs.g, rhs.b); } + void operator = (const color4f& rhs) const { glDisableVertexAttribArray(location()); glVertexAttrib4f(location(), rhs.r, rhs.g, rhs.b, rhs.a); } + void operator = (const color4d& rhs) const { glDisableVertexAttribArray(location()); glVertexAttrib4d(location(), rhs.r, rhs.g, rhs.b, rhs.a); } + + void operator = (buffer_pointer& pointer) const + { + pointer.get_vao().enable_for_attribute(m_location); + glVertexAttribPointer(location(), pointer.size(), (GLenum)pointer.get_type(), pointer.normalize(), + pointer.stride(), (const void*)(size_t)pointer.offset()); + } }; class texture_view; @@ -1221,8 +1261,7 @@ namespace gl enum class format { - red = GL_RED, - r = GL_R, + r = GL_RED, rg = GL_RG, rgb = GL_RGB, rgba = GL_RGBA, @@ -1629,9 +1668,9 @@ namespace gl copy_from(nullptr, format, type, pixel_settings); } - void copy_from(void* dst, texture::format format, texture::type type) + void copy_from(void* src, texture::format format, texture::type type) { - copy_from(dst, format, type, pixel_unpack_settings()); + copy_from(src, format, type, pixel_unpack_settings()); } void copy_from(const buffer& buf, texture::format format, texture::type type) @@ -1872,9 +1911,6 @@ namespace gl settings& border_color(color4f value); }; - GLenum draw_mode(rsx::primitive_type in); - bool is_primitive_native(rsx::primitive_type in); - enum class indices_type { ubyte = GL_UNSIGNED_BYTE, @@ -2270,43 +2306,8 @@ namespace gl void operator = (const color3f& rhs) const { glProgramUniform3f(m_program.id(), location(), rhs.r, rhs.g, rhs.b); } void operator = (const color4i& rhs) const { glProgramUniform4i(m_program.id(), location(), rhs.r, rhs.g, rhs.b, rhs.a); } void operator = (const color4f& rhs) const { glProgramUniform4f(m_program.id(), location(), rhs.r, rhs.g, rhs.b, rhs.a); } - }; - - class attrib_t - { - GLuint m_program; - GLint m_location; - - public: - attrib_t(GLuint program, GLint location) - : m_program(program) - , m_location(location) - { - } - - GLint location() const - { - return m_location; - } - - void operator = (float rhs) const { glDisableVertexAttribArray(location()); glVertexAttrib1f(location(), rhs); } - void operator = (double rhs) const { glDisableVertexAttribArray(location()); glVertexAttrib1d(location(), rhs); } - - void operator = (const color1f& rhs) const { glDisableVertexAttribArray(location()); glVertexAttrib1f(location(), rhs.r); } - void operator = (const color1d& rhs) const { glDisableVertexAttribArray(location()); glVertexAttrib1d(location(), rhs.r); } - void operator = (const color2f& rhs) const { glDisableVertexAttribArray(location()); glVertexAttrib2f(location(), rhs.r, rhs.g); } - void operator = (const color2d& rhs) const { glDisableVertexAttribArray(location()); glVertexAttrib2d(location(), rhs.r, rhs.g); } - void operator = (const color3f& rhs) const { glDisableVertexAttribArray(location()); glVertexAttrib3f(location(), rhs.r, rhs.g, rhs.b); } - void operator = (const color3d& rhs) const { glDisableVertexAttribArray(location()); glVertexAttrib3d(location(), rhs.r, rhs.g, rhs.b); } - void operator = (const color4f& rhs) const { glDisableVertexAttribArray(location()); glVertexAttrib4f(location(), rhs.r, rhs.g, rhs.b, rhs.a); } - void operator = (const color4d& rhs) const { glDisableVertexAttribArray(location()); glVertexAttrib4d(location(), rhs.r, rhs.g, rhs.b, rhs.a); } - - void operator =(buffer_pointer& pointer) const - { - pointer.get_vao().enable_for_attribute(location()); - glVertexAttribPointer(location(), pointer.size(), (GLenum)pointer.get_type(), pointer.normalize(), - pointer.stride(), (const void*)(size_t)pointer.offset()); - } + void operator = (const areaf& rhs) const { glProgramUniform4f(m_program.id(), location(), rhs.x1, rhs.y1, rhs.x2, rhs.y2); } + void operator = (const areai& rhs) const { glProgramUniform4i(m_program.id(), location(), rhs.x1, rhs.y1, rhs.x2, rhs.y2); } }; class uniforms_t @@ -2471,12 +2472,12 @@ namespace gl attrib_t operator[](GLint location) { - return{ m_program.id(), location }; + return{ location }; } attrib_t operator[](const std::string &name) { - return{ m_program.id(), location(name) }; + return{ location(name) }; } void swap(attribs_t& attribs) diff --git a/rpcs3/Emu/RSX/GL/GLOverlays.h b/rpcs3/Emu/RSX/GL/GLOverlays.h index 451a5aaac6..428a15a867 100644 --- a/rpcs3/Emu/RSX/GL/GLOverlays.h +++ b/rpcs3/Emu/RSX/GL/GLOverlays.h @@ -2,6 +2,9 @@ #include "stdafx.h" #include "GLHelpers.h" +#include "../overlays.h" + +extern u64 get_system_time(); namespace gl { @@ -16,8 +19,14 @@ namespace gl gl::fbo fbo; + gl::vao m_vao; + gl::buffer m_vertex_data_buffer; + bool compiled = false; + u32 num_drawable_elements = 4; + GLenum primitives = GL_TRIANGLE_STRIP; + void create() { if (!compiled) @@ -37,6 +46,20 @@ namespace gl fbo.create(); + m_vertex_data_buffer.create(); + + int old_vao; + glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &old_vao); + + m_vao.create(); + m_vao.bind(); + + m_vao.array_buffer = m_vertex_data_buffer; + auto ptr = buffer_pointer(&m_vao); + m_vao[0] = ptr; + + glBindVertexArray(old_vao); + compiled = true; } } @@ -50,6 +73,8 @@ namespace gl fs.remove(); fbo.remove(); + m_vao.remove(); + m_vertex_data_buffer.remove(); compiled = false; } @@ -61,12 +86,24 @@ namespace gl virtual void bind_resources() {} virtual void cleanup_resources() {} - virtual void emit_geometry() + virtual void upload_vertex_data(f32* data, u32 elements_count) { - glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + elements_count <<= 2; + m_vertex_data_buffer.data(elements_count, data); } - virtual void run(u16 w, u16 h, GLuint target_texture, bool depth_target) + virtual void emit_geometry() + { + int old_vao; + glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &old_vao); + + m_vao.bind(); + glDrawArrays(primitives, 0, num_drawable_elements); + + glBindVertexArray(old_vao); + } + + virtual void run(u16 w, u16 h, GLuint target_texture, bool depth_target, bool use_blending = false) { if (!compiled) { @@ -81,22 +118,32 @@ namespace gl GLboolean color_writes[4]; GLboolean depth_write; - glGetIntegerv(GL_FRAMEBUFFER_BINDING, &old_fbo); - glBindFramebuffer(GL_FRAMEBUFFER, fbo.id()); + GLint blend_src_rgb; + GLint blend_src_a; + GLint blend_dst_rgb; + GLint blend_dst_a; + GLint blend_eq_a; + GLint blend_eq_rgb; - if (depth_target) + if (target_texture) { - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, target_texture, 0); - glDrawBuffer(GL_NONE); - } - else - { - GLenum buffer = GL_COLOR_ATTACHMENT0; - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, target_texture, 0); - glDrawBuffers(1, &buffer); + glGetIntegerv(GL_FRAMEBUFFER_BINDING, &old_fbo); + glBindFramebuffer(GL_FRAMEBUFFER, fbo.id()); + + if (depth_target) + { + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, target_texture, 0); + glDrawBuffer(GL_NONE); + } + else + { + GLenum buffer = GL_COLOR_ATTACHMENT0; + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, target_texture, 0); + glDrawBuffers(1, &buffer); + } } - if (glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE) + if (!target_texture || glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE) { // Push rasterizer state glGetIntegerv(GL_VIEWPORT, viewport); @@ -111,6 +158,16 @@ namespace gl GLboolean blend_enabled = glIsEnabled(GL_BLEND); GLboolean stencil_test_enabled = glIsEnabled(GL_STENCIL_TEST); + if (use_blending) + { + glGetIntegerv(GL_BLEND_SRC_RGB, &blend_src_rgb); + glGetIntegerv(GL_BLEND_SRC_ALPHA, &blend_src_a); + glGetIntegerv(GL_BLEND_DST_RGB, &blend_dst_rgb); + glGetIntegerv(GL_BLEND_DST_ALPHA, &blend_dst_a); + glGetIntegerv(GL_BLEND_EQUATION_RGB, &blend_eq_rgb); + glGetIntegerv(GL_BLEND_EQUATION_ALPHA, &blend_eq_a); + } + // Set initial state glViewport(0, 0, w, h); glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); @@ -122,9 +179,21 @@ namespace gl if (scissor_enabled) glDisable(GL_SCISSOR_TEST); if (cull_face_enabled) glDisable(GL_CULL_FACE); - if (blend_enabled) glDisable(GL_BLEND); if (stencil_test_enabled) glDisable(GL_STENCIL_TEST); + if (use_blending) + { + if (!blend_enabled) + glEnable(GL_BLEND); + + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glBlendEquation(GL_FUNC_ADD); + } + else if (blend_enabled) + { + glDisable(GL_BLEND); + } + // Render program_handle.use(); on_load(); @@ -132,12 +201,16 @@ namespace gl emit_geometry(); // Clean up - if (depth_target) - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0); - else - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); + if (target_texture) + { + if (depth_target) + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0); + else + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); + + glBindFramebuffer(GL_FRAMEBUFFER, old_fbo); + } - glBindFramebuffer(GL_FRAMEBUFFER, old_fbo); glUseProgram((GLuint)program); glViewport(viewport[0], viewport[1], viewport[2], viewport[3]); @@ -148,8 +221,20 @@ namespace gl if (!depth_test_enabled) glDisable(GL_DEPTH_TEST); if (scissor_enabled) glEnable(GL_SCISSOR_TEST); if (cull_face_enabled) glEnable(GL_CULL_FACE); - if (blend_enabled) glEnable(GL_BLEND); if (stencil_test_enabled) glEnable(GL_STENCIL_TEST); + + if (use_blending) + { + if (!blend_enabled) + glDisable(GL_BLEND); + + glBlendFuncSeparate(blend_src_rgb, blend_dst_rgb, blend_src_a, blend_dst_a); + glBlendEquationSeparate(blend_eq_rgb, blend_eq_a); + } + else if (blend_enabled) + { + glEnable(GL_BLEND); + } } else { @@ -237,4 +322,251 @@ namespace gl overlay_pass::run(w, h, target, false); } }; -} \ No newline at end of file + + struct ui_overlay_renderer : public overlay_pass + { + u32 num_elements = 0; + std::vector> resources; + std::unordered_map> temp_image_cache; + std::unordered_map> font_cache; + bool is_font_draw = false; + + ui_overlay_renderer() + { + vs_src = + { + "#version 420\n\n" + "layout(location=0) in vec4 in_pos;\n" + "layout(location=0) out vec2 tc0;\n" + "layout(location=1) out vec4 clip_rect;\n" + "uniform vec4 ui_scale;\n" + "uniform vec4 clip_bounds;\n" + "\n" + "void main()\n" + "{\n" + " tc0.xy = in_pos.zw;\n" + " clip_rect = (clip_bounds * ui_scale.zwzw);\n" + " clip_rect.yw = ui_scale.yy - clip_rect.wy; //invert y axis\n" + " vec4 pos = vec4((in_pos.xy * ui_scale.zw) / ui_scale.xy, 0., 1.);\n" + " pos.y = (1. - pos.y); //invert y axis\n" + " gl_Position = (pos + pos) - 1.;\n" + "}\n" + }; + + fs_src = + { + "#version 420\n\n" + "layout(binding=31) uniform sampler2D fs0;\n" + "layout(location=0) in vec2 tc0;\n" + "layout(location=1) in vec4 clip_rect;\n" + "layout(location=0) out vec4 ocol;\n" + "uniform vec4 color;\n" + "uniform float time;\n" + "uniform int read_texture;\n" + "uniform int pulse_glow;\n" + "uniform int clip_region;\n" + "\n" + "void main()\n" + "{\n" + " if (clip_region != 0)\n" + " {" + " if (gl_FragCoord.x < clip_rect.x || gl_FragCoord.x > clip_rect.z ||\n" + " gl_FragCoord.y < clip_rect.y || gl_FragCoord.y > clip_rect.w)\n" + " {\n" + " discard;\n" + " return;\n" + " }\n" + " }\n" + "\n" + " vec4 diff_color = color;\n" + " if (pulse_glow != 0)\n" + " diff_color.a *= (sin(time) + 1.f) * 0.5f;\n" + "\n" + " if (read_texture != 0)\n" + " ocol = texture(fs0, tc0) * diff_color;\n" + " else\n" + " ocol = diff_color;\n" + "}\n" + }; + } + + gl::texture* load_simple_image(rsx::overlays::image_info* desc, bool temp_resource) + { + auto tex = std::make_unique(gl::texture::target::texture2D); + tex->create(); + tex->config() + .size({ desc->w, desc->h }) + .format(gl::texture::format::rgba) + .type(gl::texture::type::uint_8_8_8_8) + .wrap(gl::texture::wrap::clamp_to_border, gl::texture::wrap::clamp_to_border, gl::texture::wrap::clamp_to_border) + .swizzle(gl::texture::channel::a, gl::texture::channel::b, gl::texture::channel::g, gl::texture::channel::r) + .filter(gl::min_filter::linear, gl::filter::linear) + .apply(); + tex->copy_from(desc->data, gl::texture::format::rgba, gl::texture::type::uint_8_8_8_8); + + if (!temp_resource) + { + resources.push_back(std::move(tex)); + } + else + { + u64 key = (u64)desc; + temp_image_cache[key] = std::move(tex); + } + + return resources.back().get(); + } + + void create() + { + overlay_pass::create(); + + rsx::overlays::resource_config configuration; + configuration.load_files(); + + for (const auto &res : configuration.texture_raw_data) + { + load_simple_image(res.get(), false); + } + + configuration.free_resources(); + } + + void destroy() + { + temp_image_cache.clear(); + resources.clear(); + font_cache.clear(); + overlay_pass::destroy(); + } + + void remove_temp_resources() + { + temp_image_cache.clear(); + } + + gl::texture* find_font(rsx::overlays::font *font) + { + u64 key = (u64)font; + auto found = font_cache.find(key); + if (found != font_cache.end()) + return found->second.get(); + + //Create font file + auto tex = std::make_unique(gl::texture::target::texture2D); + tex->create(); + tex->config() + .size({ (int)font->width, (int)font->height }) + .format(gl::texture::format::r) + .type(gl::texture::type::ubyte) + .internal_format(gl::texture::internal_format::r8) + .wrap(gl::texture::wrap::clamp_to_border, gl::texture::wrap::clamp_to_border, gl::texture::wrap::clamp_to_border) + .swizzle(gl::texture::channel::r, gl::texture::channel::r, gl::texture::channel::r, gl::texture::channel::r) + .filter(gl::min_filter::linear, gl::filter::linear) + .apply(); + tex->copy_from(font->glyph_data.data(), gl::texture::format::r, gl::texture::type::ubyte); + + auto result = tex.get(); + font_cache[key] = std::move(tex); + + return result; + } + + gl::texture* find_temp_image(rsx::overlays::image_info *desc) + { + auto key = (u64)desc; + auto cached = temp_image_cache.find(key); + if (cached != temp_image_cache.end()) + { + return cached->second.get(); + } + else + { + return load_simple_image(desc, true); + } + } + + void emit_geometry() override + { + if (!is_font_draw) + { + overlay_pass::emit_geometry(); + } + else + { + int num_quads = num_drawable_elements / 4; + std::vector firsts; + std::vector counts; + + firsts.resize(num_quads); + counts.resize(num_quads); + + for (int n = 0; n < num_quads; ++n) + { + firsts[n] = (n * 4); + counts[n] = 4; + } + + int old_vao; + glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &old_vao); + + m_vao.bind(); + glMultiDrawArrays(GL_TRIANGLE_STRIP, firsts.data(), counts.data(), num_quads); + + glBindVertexArray(old_vao); + } + } + + void run(u16 w, u16 h, GLuint target, rsx::overlays::user_interface& ui) + { + program_handle.uniforms["ui_scale"] = color4f((f32)ui.virtual_width, (f32)ui.virtual_height, 1.f, 1.f); + program_handle.uniforms["time"] = (f32)(get_system_time() / 1000) * 0.005f; + for (auto &cmd : ui.get_compiled().draw_commands) + { + upload_vertex_data((f32*)cmd.second.data(), (u32)cmd.second.size() * 4u); + num_drawable_elements = (u32)cmd.second.size(); + is_font_draw = false; + GLint texture_exists = GL_TRUE; + + glActiveTexture(GL_TEXTURE31); + switch (cmd.first.texture_ref) + { + case rsx::overlays::image_resource_id::game_icon: + case rsx::overlays::image_resource_id::backbuffer: + //TODO + case rsx::overlays::image_resource_id::none: + { + texture_exists = GL_FALSE; + glBindTexture(GL_TEXTURE_2D, GL_NONE); + break; + } + case rsx::overlays::image_resource_id::raw_image: + { + glBindTexture(GL_TEXTURE_2D, find_temp_image((rsx::overlays::image_info*)cmd.first.external_data_ref)->id()); + break; + } + case rsx::overlays::image_resource_id::font_file: + { + is_font_draw = true; + glBindTexture(GL_TEXTURE_2D, find_font(cmd.first.font_ref)->id()); + break; + } + default: + { + glBindTexture(GL_TEXTURE_2D, resources[cmd.first.texture_ref - 1]->id()); + break; + } + } + + program_handle.uniforms["color"] = cmd.first.color; + program_handle.uniforms["read_texture"] = texture_exists; + program_handle.uniforms["pulse_glow"] = (s32)cmd.first.pulse_glow; + program_handle.uniforms["clip_region"] = (s32)cmd.first.clip_region; + program_handle.uniforms["clip_bounds"] = cmd.first.clip_rect; + overlay_pass::run(w, h, target, false, true); + } + + ui.update(); + } + }; +} diff --git a/rpcs3/Emu/RSX/GL/GLRenderTargets.cpp b/rpcs3/Emu/RSX/GL/GLRenderTargets.cpp index 10e00a58d1..700d2c8ee1 100644 --- a/rpcs3/Emu/RSX/GL/GLRenderTargets.cpp +++ b/rpcs3/Emu/RSX/GL/GLRenderTargets.cpp @@ -31,13 +31,13 @@ color_format rsx::internals::surface_color_format_to_gl(rsx::surface_color_forma return{ ::gl::texture::type::f32, ::gl::texture::format::rgba, true, 4, 4 }; case rsx::surface_color_format::b8: - return{ ::gl::texture::type::ubyte, ::gl::texture::format::red, false, 1, 1 }; + return{ ::gl::texture::type::ubyte, ::gl::texture::format::r, false, 1, 1 }; case rsx::surface_color_format::g8b8: return{ ::gl::texture::type::ubyte, ::gl::texture::format::rg, false, 2, 1 }; case rsx::surface_color_format::x32: - return{ ::gl::texture::type::f32, ::gl::texture::format::red, true, 1, 4 }; + return{ ::gl::texture::type::f32, ::gl::texture::format::r, true, 1, 4 }; default: LOG_ERROR(RSX, "Surface color buffer: Unsupported surface color format (0x%x)", (u32)color_format); diff --git a/rpcs3/Emu/RSX/GL/GLTextureCache.h b/rpcs3/Emu/RSX/GL/GLTextureCache.h index ca54ff8077..e028a43239 100644 --- a/rpcs3/Emu/RSX/GL/GLTextureCache.h +++ b/rpcs3/Emu/RSX/GL/GLTextureCache.h @@ -142,7 +142,6 @@ namespace gl switch (fmt_) { - case texture::format::red: case texture::format::r: break; case texture::format::rg: diff --git a/rpcs3/Emu/RSX/RSXThread.cpp b/rpcs3/Emu/RSX/RSXThread.cpp index bb65cb1bb4..ae86c25072 100644 --- a/rpcs3/Emu/RSX/RSXThread.cpp +++ b/rpcs3/Emu/RSX/RSXThread.cpp @@ -481,9 +481,6 @@ namespace rsx // TODO: exit condition while (!Emu.IsStopped()) { - //Execute backend-local tasks first - do_local_task(); - //Wait for external pause events if (external_interrupt_lock.load()) { @@ -491,6 +488,9 @@ namespace rsx while (external_interrupt_lock.load()) _mm_pause(); } + //Execute backend-local tasks first + do_local_task(ctrl->put.load() == internal_get.load()); + //Set up restore state if needed if (sync_point_request) { @@ -2179,7 +2179,13 @@ namespace rsx void thread::pause() { external_interrupt_lock.store(true); - while (!external_interrupt_ack.load()) _mm_pause(); + while (!external_interrupt_ack.load()) + { + if (Emu.IsStopped()) + break; + + _mm_pause(); + } external_interrupt_ack.store(false); } @@ -2187,4 +2193,67 @@ namespace rsx { external_interrupt_lock.store(false); } + + //TODO: Move these helpers into a better class dedicated to shell interface handling (use idm?) + //They are not dependent on rsx at all + rsx::overlays::save_dialog* thread::shell_open_save_dialog() + { + if (supports_native_ui) + { + auto ptr = new rsx::overlays::save_dialog(); + m_custom_ui.reset(ptr); + return ptr; + } + else + { + return nullptr; + } + } + + rsx::overlays::message_dialog* thread::shell_open_message_dialog() + { + if (supports_native_ui) + { + auto ptr = new rsx::overlays::message_dialog(); + m_custom_ui.reset(ptr); + return ptr; + } + else + { + return nullptr; + } + } + + rsx::overlays::trophy_notification* thread::shell_open_trophy_notification() + { + if (supports_native_ui) + { + auto ptr = new rsx::overlays::trophy_notification(); + m_custom_ui.reset(ptr); + return ptr; + } + else + { + return nullptr; + } + } + + rsx::overlays::user_interface* thread::shell_get_current_dialog() + { + //TODO: Only get dialog type interfaces + return m_custom_ui.get(); + } + + bool thread::shell_close_dialog() + { + //TODO: Only get dialog type interfaces + if (m_custom_ui) + { + m_invalidated_ui = std::move(m_custom_ui); + shell_do_cleanup(); + return true; + } + + return false; + } } diff --git a/rpcs3/Emu/RSX/RSXThread.h b/rpcs3/Emu/RSX/RSXThread.h index 36d3845c48..c836c469d8 100644 --- a/rpcs3/Emu/RSX/RSXThread.h +++ b/rpcs3/Emu/RSX/RSXThread.h @@ -12,6 +12,7 @@ #include "RSXFragmentProgram.h" #include "rsx_methods.h" #include "rsx_utils.h" +#include "overlays.h" #include #include "Utilities/Thread.h" @@ -216,6 +217,7 @@ namespace rsx bool skip_frame = false; bool supports_multidraw = false; + bool supports_native_ui = false; //occlusion query bool zcull_surface_active = false; @@ -230,6 +232,9 @@ namespace rsx rsx::gcm_framebuffer_info m_depth_surface_info; bool framebuffer_status_valid = false; + std::unique_ptr m_custom_ui; + std::unique_ptr m_invalidated_ui; + public: RsxDmaControl* ctrl = nullptr; atomic_t internal_get{ 0 }; @@ -237,6 +242,9 @@ namespace rsx atomic_t external_interrupt_lock{ false }; atomic_t external_interrupt_ack{ false }; + //native UI interrupts + atomic_t native_ui_flip_request{ false }; + GcmTileInfo tiles[limits::tiles_count]; GcmZcullInfo zculls[limits::zculls_count]; @@ -328,8 +336,9 @@ namespace rsx /** * Execute a backend local task queue + * Idle argument checks that the FIFO queue is in an idle state */ - virtual void do_local_task() {} + virtual void do_local_task(bool idle) {} public: virtual std::string get_name() const override; @@ -419,6 +428,8 @@ namespace rsx public: //std::future add_internal_task(std::function callback); //void invoke(std::function callback); + void add_user_interface(std::shared_ptr iface); + void remove_user_interface(); /** * Fill buffer with 4x4 scale offset matrix. @@ -484,5 +495,14 @@ namespace rsx void pause(); void unpause(); + + //HLE vsh stuff + //TODO: Move into a separate helper + virtual rsx::overlays::save_dialog* shell_open_save_dialog(); + virtual rsx::overlays::message_dialog* shell_open_message_dialog(); + virtual rsx::overlays::trophy_notification* shell_open_trophy_notification(); + virtual rsx::overlays::user_interface* shell_get_current_dialog(); + virtual bool shell_close_dialog(); + virtual void shell_do_cleanup(){} }; } diff --git a/rpcs3/Emu/RSX/VK/VKGSRender.cpp b/rpcs3/Emu/RSX/VK/VKGSRender.cpp index 2edccd8d43..8293750951 100644 --- a/rpcs3/Emu/RSX/VK/VKGSRender.cpp +++ b/rpcs3/Emu/RSX/VK/VKGSRender.cpp @@ -646,25 +646,30 @@ VKGSRender::VKGSRender() : GSRender() for (u32 i = 0; i < m_swap_chain->get_swap_image_count(); ++i) { + VkClearColorValue clear_color{}; + VkImageSubresourceRange range = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; + vk::change_image_layout(*m_current_command_buffer, m_swap_chain->get_swap_chain_image(i), VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL, - vk::get_image_subresource_range(0, 0, 1, 1, VK_IMAGE_ASPECT_COLOR_BIT)); + range); - VkClearColorValue clear_color{}; - auto range = vk::get_image_subresource_range(0, 0, 1, 1, VK_IMAGE_ASPECT_COLOR_BIT); vkCmdClearColorImage(*m_current_command_buffer, m_swap_chain->get_swap_chain_image(i), VK_IMAGE_LAYOUT_GENERAL, &clear_color, 1, &range); vk::change_image_layout(*m_current_command_buffer, m_swap_chain->get_swap_chain_image(i), VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, - vk::get_image_subresource_range(0, 0, 1, 1, VK_IMAGE_ASPECT_COLOR_BIT)); + range); } m_current_frame = &frame_context_storage[0]; m_texture_cache.initialize((*m_device), m_memory_type_mapping, m_optimal_tiling_supported_formats, m_swap_chain->get_present_queue(), - m_texture_upload_buffer_ring_info, m_texture_upload_buffer_ring_info.heap.get()); + m_texture_upload_buffer_ring_info); + + m_ui_renderer.reset(new vk::ui_overlay_renderer()); + m_ui_renderer->create(*m_current_command_buffer, m_memory_type_mapping, m_texture_upload_buffer_ring_info); supports_multidraw = !g_cfg.video.strict_rendering_mode; + supports_native_ui = (bool)g_cfg.misc.use_native_interface; } VKGSRender::~VKGSRender() @@ -743,6 +748,10 @@ VKGSRender::~VKGSRender() //Overlay text handler m_text_writer.reset(); + //Overlay UI renderer + m_ui_renderer->destroy(); + m_ui_renderer.reset(); + //RGBA->depth cast helper m_depth_converter->destroy(); m_depth_converter.reset(); @@ -1597,13 +1606,63 @@ void VKGSRender::on_init_thread() GSRender::on_init_thread(); rsx_thread = std::this_thread::get_id(); - m_frame->disable_wm_event_queue(); - m_frame->hide(); + if (!supports_native_ui) + { + m_frame->disable_wm_event_queue(); + m_frame->hide(); + m_shaders_cache->load(nullptr, *m_device, pipeline_layout); + m_frame->enable_wm_event_queue(); + m_frame->show(); + } + else + { + struct native_helper : vk::shader_cache::progress_dialog_helper + { + rsx::thread *owner = nullptr; + rsx::overlays::message_dialog *dlg = nullptr; - m_shaders_cache->load(*m_device, pipeline_layout); + native_helper(VKGSRender *ptr) : + owner(ptr) {} - m_frame->enable_wm_event_queue(); - m_frame->show(); + void create() override + { + MsgDialogType type = {}; + type.disable_cancel = true; + type.progress_bar_count = 1; + + dlg = owner->shell_open_message_dialog(); + dlg->show("Loading precompiled shaders from disk...", type, [](s32 status) + { + if (status != CELL_OK) + Emu.Stop(); + }); + } + + void update_msg(u32 processed, u32 entry_count) override + { + dlg->progress_bar_set_message(0, fmt::format("Loading pipeline object %u of %u", processed, entry_count)); + owner->flip(0); + } + + void inc_value(u32 value) override + { + dlg->progress_bar_increment(0, (f32)value); + owner->flip(0); + } + + void close() override + { + dlg->return_code = CELL_OK; + dlg->close(); + } + } + helper(this); + + //TODO: Handle window resize messages during loading on GPUs without OUT_OF_DATE_KHR support + m_frame->disable_wm_event_queue(); + m_shaders_cache->load(&helper, *m_device, pipeline_layout); + m_frame->enable_wm_event_queue(); + } } void VKGSRender::on_exit() @@ -1951,6 +2010,7 @@ void VKGSRender::process_swap_request(frame_context_t *ctx, bool free_resources) } m_depth_converter->free_resources(); + m_ui_renderer->free_resources(); ctx->buffer_views_to_clean.clear(); ctx->samplers_to_clean.clear(); @@ -1975,8 +2035,16 @@ void VKGSRender::process_swap_request(frame_context_t *ctx, bool free_resources) ctx->swap_command_buffer = nullptr; } -void VKGSRender::do_local_task() +void VKGSRender::do_local_task(bool idle) { + //TODO: Guard this + if (m_overlay_cleanup_requests.size()) + { + flush_command_queue(true); + m_ui_renderer->remove_temp_resources(); + m_overlay_cleanup_requests.clear(); + } + if (m_flush_commands) { std::lock_guard lock(m_flush_queue_mutex); @@ -2082,6 +2150,16 @@ void VKGSRender::do_local_task() } #endif + + if (m_custom_ui) + { + if (!in_begin_end && native_ui_flip_request.load()) + { + native_ui_flip_request.store(false); + flush_command_queue(true); + flip((s32)current_display_buffer); + } + } } bool VKGSRender::do_method(u32 cmd, u32 arg) @@ -2255,8 +2333,22 @@ void VKGSRender::load_program(u32 vertex_count, u32 vertex_base) m_program = m_prog_buffer->getGraphicPipelineState(vertex_program, fragment_program, properties, *m_device, pipeline_layout).get(); if (m_prog_buffer->check_cache_missed()) + { m_shaders_cache->store(properties, vertex_program, fragment_program); + //Notify the user with HUD notification + if (!m_custom_ui) + { + //Create notification but do not draw it at this time. No need to spam flip requests + m_custom_ui = std::make_unique(); + } + else if (auto casted = dynamic_cast(m_custom_ui.get())) + { + //Probe the notification + casted->touch(); + } + } + vk::leave_uninterruptible(); const size_t fragment_constants_sz = m_prog_buffer->get_fragment_constants_buffer_size(fragment_program); @@ -2740,14 +2832,14 @@ void VKGSRender::reinitialize_swapchain() { vk::change_image_layout(*m_current_command_buffer, m_swap_chain->get_swap_chain_image(i), VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL, - vk::get_image_subresource_range(0, 0, 1, 1, VK_IMAGE_ASPECT_COLOR_BIT)); + { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }); VkClearColorValue clear_color{}; auto range = vk::get_image_subresource_range(0, 0, 1, 1, VK_IMAGE_ASPECT_COLOR_BIT); vkCmdClearColorImage(*m_current_command_buffer, m_swap_chain->get_swap_chain_image(i), VK_IMAGE_LAYOUT_GENERAL, &clear_color, 1, &range); vk::change_image_layout(*m_current_command_buffer, m_swap_chain->get_swap_chain_image(i), VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, - vk::get_image_subresource_range(0, 0, 1, 1, VK_IMAGE_ASPECT_COLOR_BIT)); + { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }); } //Will have to block until rendering is completed @@ -2827,9 +2919,6 @@ void VKGSRender::flip(int buffer) u32 buffer_width = display_buffers[buffer].width; u32 buffer_height = display_buffers[buffer].height; - u32 buffer_pitch = display_buffers[buffer].pitch; - - areai screen_area = coordi({}, { (int)buffer_width, (int)buffer_height }); coordi aspect_ratio; @@ -2905,22 +2994,49 @@ void VKGSRender::flip(int buffer) //Blit contents to screen.. vk::image* image_to_flip = nullptr; - if (std::get<1>(m_rtts.m_bound_render_targets[0]) != nullptr) - image_to_flip = std::get<1>(m_rtts.m_bound_render_targets[0]); - else if (std::get<1>(m_rtts.m_bound_render_targets[1]) != nullptr) - image_to_flip = std::get<1>(m_rtts.m_bound_render_targets[1]); + rsx::tiled_region buffer_region = get_tiled_address(display_buffers[buffer].offset, CELL_GCM_LOCATION_LOCAL); + u32 absolute_address = buffer_region.address + buffer_region.base; + + if (auto render_target_texture = m_rtts.get_texture_from_render_target_if_applicable(absolute_address)) + { + image_to_flip = render_target_texture; + } + else if (auto surface = m_texture_cache.find_texture_from_dimensions(absolute_address)) + { + //Hack - this should be the first location to check for output + //The render might have been done offscreen or in software and a blit used to display + image_to_flip = surface->get_raw_texture(); + } VkImage target_image = m_swap_chain->get_swap_chain_image(m_current_frame->present_image); if (image_to_flip) { - vk::copy_scaled_image(*m_current_command_buffer, image_to_flip->value, target_image, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, + VkImageLayout target_layout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; + VkImageSubresourceRange range = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; + + if (aspect_ratio.x) + { + VkClearColorValue clear_black {}; + vk::change_image_layout(*m_current_command_buffer, target_image, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, range); + vkCmdClearColorImage(*m_current_command_buffer, target_image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &clear_black, 1, &range); + + target_layout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + } + + vk::copy_scaled_image(*m_current_command_buffer, image_to_flip->value, target_image, image_to_flip->current_layout, target_layout, 0, 0, image_to_flip->width(), image_to_flip->height(), aspect_ratio.x, aspect_ratio.y, aspect_ratio.width, aspect_ratio.height, 1, VK_IMAGE_ASPECT_COLOR_BIT, false); + + if (target_layout != VK_IMAGE_LAYOUT_PRESENT_SRC_KHR) + { + vk::change_image_layout(*m_current_command_buffer, target_image, target_layout, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, range); + } } else { //No draw call was issued! - VkImageSubresourceRange range = vk::get_image_subresource_range(0, 0, 1, 1, VK_IMAGE_ASPECT_COLOR_BIT); - VkClearColorValue clear_black = { 0 }; + //TODO: Upload raw bytes from cpu for rendering + VkImageSubresourceRange range = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; + VkClearColorValue clear_black {}; vk::change_image_layout(*m_current_command_buffer, m_swap_chain->get_swap_chain_image(m_current_frame->present_image), VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_IMAGE_LAYOUT_GENERAL, range); vkCmdClearColorImage(*m_current_command_buffer, m_swap_chain->get_swap_chain_image(m_current_frame->present_image), VK_IMAGE_LAYOUT_GENERAL, &clear_black, 1, &range); vk::change_image_layout(*m_current_command_buffer, m_swap_chain->get_swap_chain_image(m_current_frame->present_image), VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, range); @@ -2928,10 +3044,10 @@ void VKGSRender::flip(int buffer) std::unique_ptr direct_fbo; std::vector> swap_image_view; - if (g_cfg.video.overlay) + if (g_cfg.video.overlay || m_custom_ui) { //Change the image layout whilst setting up a dependency on waiting for the blit op to finish before we start writing - auto subres = vk::get_image_subresource_range(0, 0, 1, 1, VK_IMAGE_ASPECT_COLOR_BIT); + VkImageSubresourceRange subres = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; VkImageMemoryBarrier barrier = {}; barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; barrier.newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; @@ -2966,19 +3082,27 @@ void VKGSRender::flip(int buffer) direct_fbo.reset(new vk::framebuffer_holder(*m_device, single_target_pass, m_client_width, m_client_height, std::move(swap_image_view))); } - m_text_writer->print_text(*m_current_command_buffer, *direct_fbo, 0, 0, direct_fbo->width(), direct_fbo->height(), "draw calls: " + std::to_string(m_draw_calls)); - m_text_writer->print_text(*m_current_command_buffer, *direct_fbo, 0, 18, direct_fbo->width(), direct_fbo->height(), "draw call setup: " + std::to_string(m_setup_time) + "us"); - m_text_writer->print_text(*m_current_command_buffer, *direct_fbo, 0, 36, direct_fbo->width(), direct_fbo->height(), "vertex upload time: " + std::to_string(m_vertex_upload_time) + "us"); - m_text_writer->print_text(*m_current_command_buffer, *direct_fbo, 0, 54, direct_fbo->width(), direct_fbo->height(), "texture upload time: " + std::to_string(m_textures_upload_time) + "us"); - m_text_writer->print_text(*m_current_command_buffer, *direct_fbo, 0, 72, direct_fbo->width(), direct_fbo->height(), "draw call execution: " + std::to_string(m_draw_time) + "us"); - m_text_writer->print_text(*m_current_command_buffer, *direct_fbo, 0, 90, direct_fbo->width(), direct_fbo->height(), "submit and flip: " + std::to_string(m_flip_time) + "us"); + if (m_custom_ui) + { + m_ui_renderer->run(*m_current_command_buffer, direct_fbo->width(), direct_fbo->height(), direct_fbo.get(), single_target_pass, m_memory_type_mapping, m_texture_upload_buffer_ring_info, *m_custom_ui); + } - auto num_dirty_textures = m_texture_cache.get_unreleased_textures_count(); - auto texture_memory_size = m_texture_cache.get_texture_memory_in_use() / (1024 * 1024); - auto tmp_texture_memory_size = m_texture_cache.get_temporary_memory_in_use() / (1024 * 1024); - m_text_writer->print_text(*m_current_command_buffer, *direct_fbo, 0, 126, direct_fbo->width(), direct_fbo->height(), "Unreleased textures: " + std::to_string(num_dirty_textures)); - m_text_writer->print_text(*m_current_command_buffer, *direct_fbo, 0, 144, direct_fbo->width(), direct_fbo->height(), "Texture cache memory: " + std::to_string(texture_memory_size) + "M"); - m_text_writer->print_text(*m_current_command_buffer, *direct_fbo, 0, 162, direct_fbo->width(), direct_fbo->height(), "Temporary texture memory: " + std::to_string(tmp_texture_memory_size) + "M"); + if (g_cfg.video.overlay) + { + m_text_writer->print_text(*m_current_command_buffer, *direct_fbo, 0, 0, direct_fbo->width(), direct_fbo->height(), "draw calls: " + std::to_string(m_draw_calls)); + m_text_writer->print_text(*m_current_command_buffer, *direct_fbo, 0, 18, direct_fbo->width(), direct_fbo->height(), "draw call setup: " + std::to_string(m_setup_time) + "us"); + m_text_writer->print_text(*m_current_command_buffer, *direct_fbo, 0, 36, direct_fbo->width(), direct_fbo->height(), "vertex upload time: " + std::to_string(m_vertex_upload_time) + "us"); + m_text_writer->print_text(*m_current_command_buffer, *direct_fbo, 0, 54, direct_fbo->width(), direct_fbo->height(), "texture upload time: " + std::to_string(m_textures_upload_time) + "us"); + m_text_writer->print_text(*m_current_command_buffer, *direct_fbo, 0, 72, direct_fbo->width(), direct_fbo->height(), "draw call execution: " + std::to_string(m_draw_time) + "us"); + m_text_writer->print_text(*m_current_command_buffer, *direct_fbo, 0, 90, direct_fbo->width(), direct_fbo->height(), "submit and flip: " + std::to_string(m_flip_time) + "us"); + + auto num_dirty_textures = m_texture_cache.get_unreleased_textures_count(); + auto texture_memory_size = m_texture_cache.get_texture_memory_in_use() / (1024 * 1024); + auto tmp_texture_memory_size = m_texture_cache.get_temporary_memory_in_use() / (1024 * 1024); + m_text_writer->print_text(*m_current_command_buffer, *direct_fbo, 0, 126, direct_fbo->width(), direct_fbo->height(), "Unreleased textures: " + std::to_string(num_dirty_textures)); + m_text_writer->print_text(*m_current_command_buffer, *direct_fbo, 0, 144, direct_fbo->width(), direct_fbo->height(), "Texture cache memory: " + std::to_string(texture_memory_size) + "M"); + m_text_writer->print_text(*m_current_command_buffer, *direct_fbo, 0, 162, direct_fbo->width(), direct_fbo->height(), "Temporary texture memory: " + std::to_string(tmp_texture_memory_size) + "M"); + } vk::change_image_layout(*m_current_command_buffer, target_image, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, subres); m_framebuffers_to_clean.push_back(std::move(direct_fbo)); @@ -3088,4 +3212,10 @@ void VKGSRender::get_occlusion_query_result(rsx::occlusion_query_info* query) m_occlusion_query_pool.reset_queries(*m_current_command_buffer, data.indices); m_occlusion_map.erase(query->driver_handle); +} + +void VKGSRender::shell_do_cleanup() +{ + //TODO: Guard this + m_overlay_cleanup_requests.push_back(0); } \ No newline at end of file diff --git a/rpcs3/Emu/RSX/VK/VKGSRender.h b/rpcs3/Emu/RSX/VK/VKGSRender.h index e7e054f85a..976e02306a 100644 --- a/rpcs3/Emu/RSX/VK/VKGSRender.h +++ b/rpcs3/Emu/RSX/VK/VKGSRender.h @@ -136,6 +136,7 @@ private: std::unique_ptr m_text_writer; std::unique_ptr m_depth_converter; + std::unique_ptr m_ui_renderer; std::mutex m_sampler_mutex; u64 surface_store_tag = 0; @@ -287,6 +288,8 @@ private: //Vertex layout rsx::vertex_input_layout m_vertex_layout; + + std::vector m_overlay_cleanup_requests; #if !defined(_WIN32) && defined(HAVE_VULKAN) Display *m_display_handle = nullptr; @@ -341,10 +344,12 @@ protected: bool do_method(u32 id, u32 arg) override; void flip(int buffer) override; - void do_local_task() override; + void do_local_task(bool idle) override; bool scaled_image_from_memory(rsx::blit_src_info& src, rsx::blit_dst_info& dst, bool interpolate) override; void notify_tile_unbound(u32 tile) override; bool on_access_violation(u32 address, bool is_writing) override; void on_notify_memory_unmapped(u32 address_base, u32 size) override; + + void shell_do_cleanup() override; }; diff --git a/rpcs3/Emu/RSX/VK/VKHelpers.h b/rpcs3/Emu/RSX/VK/VKHelpers.h index 7b18ae35b0..f0400d2188 100644 --- a/rpcs3/Emu/RSX/VK/VKHelpers.h +++ b/rpcs3/Emu/RSX/VK/VKHelpers.h @@ -455,7 +455,7 @@ namespace vk struct image_view { - VkImageView value; + VkImageView value = VK_NULL_HANDLE; VkImageViewCreateInfo info = {}; image_view(VkDevice dev, VkImage image, VkImageViewType view_type, VkFormat format, VkComponentMapping mapping, VkImageSubresourceRange range) @@ -477,6 +477,34 @@ namespace vk CHECK_RESULT(vkCreateImageView(m_device, &info, nullptr, &value)); } + image_view(VkDevice dev, vk::image* resource, VkImageSubresourceRange range = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}, VkComponentMapping mapping = { VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_G, VK_COMPONENT_SWIZZLE_B, VK_COMPONENT_SWIZZLE_A }) + : m_device(dev) + { + info.format = resource->info.format; + info.image = resource->value; + info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + info.components = mapping; + info.subresourceRange = range; + + switch (resource->info.imageType) + { + case VK_IMAGE_TYPE_1D: + info.viewType = VK_IMAGE_VIEW_TYPE_1D; + break; + case VK_IMAGE_TYPE_2D: + if (resource->info.flags == VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT) + info.viewType = VK_IMAGE_VIEW_TYPE_CUBE; + else + info.viewType = VK_IMAGE_VIEW_TYPE_2D; + break; + case VK_IMAGE_TYPE_3D: + info.viewType = VK_IMAGE_VIEW_TYPE_3D; + break; + } + + CHECK_RESULT(vkCreateImageView(m_device, &info, nullptr, &value)); + } + ~image_view() { vkDestroyImageView(m_device, value, nullptr); @@ -1181,7 +1209,7 @@ namespace vk m_instance = nullptr; //Check that some critical entry-points have been loaded into memory indicating prescence of a loader - loader_exists = (&vkCreateInstance != nullptr); + loader_exists = (vkCreateInstance != nullptr); } ~context() @@ -1279,8 +1307,8 @@ namespace vk instance_info.pApplicationInfo = &app; instance_info.enabledLayerCount = static_cast(layers.size()); instance_info.ppEnabledLayerNames = layers.data(); - instance_info.enabledExtensionCount = fast? 0: static_cast(extensions.size()); - instance_info.ppEnabledExtensionNames = fast? nullptr: extensions.data(); + instance_info.enabledExtensionCount = fast ? 0 : static_cast(extensions.size()); + instance_info.ppEnabledExtensionNames = fast ? nullptr : extensions.data(); VkInstance instance; if (vkCreateInstance(&instance_info, nullptr, &instance) != VK_SUCCESS) @@ -1704,5 +1732,5 @@ namespace vk */ void copy_mipmaped_image_using_buffer(VkCommandBuffer cmd, VkImage dst_image, const std::vector& subresource_layout, int format, bool is_swizzled, u16 mipmap_count, - VkImageAspectFlags flags, vk::vk_data_heap &upload_heap, vk::buffer* upload_buffer); + VkImageAspectFlags flags, vk::vk_data_heap &upload_heap); } diff --git a/rpcs3/Emu/RSX/VK/VKOverlays.h b/rpcs3/Emu/RSX/VK/VKOverlays.h index 23cee8087e..dec6017f7f 100644 --- a/rpcs3/Emu/RSX/VK/VKOverlays.h +++ b/rpcs3/Emu/RSX/VK/VKOverlays.h @@ -4,6 +4,8 @@ #include "VKFragmentProgram.h" #include "VKRenderTargets.h" +#include "../overlays.h" + namespace vk { //TODO: Refactor text print class to inherit from this base class @@ -21,6 +23,8 @@ namespace vk std::unordered_map> m_program_cache; std::unique_ptr m_sampler; std::unique_ptr m_draw_fbo; + std::unique_ptr m_vao; + std::unique_ptr m_ubo; vk::render_device* m_device = nullptr; std::string vs_src; @@ -32,33 +36,43 @@ namespace vk bool write_color = true; bool write_depth = true; bool no_depth_test = true; + bool enable_blend = false; } renderpass_config; bool initialized = false; bool compiled = false; + u32 num_drawable_elements = 4; + u32 first_vertex = 0; + void init_descriptors() { - VkDescriptorPoolSize descriptor_pools[1] = + VkDescriptorPoolSize descriptor_pool_sizes[2] = { { VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 120 }, + { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 120 }, }; //Reserve descriptor pools - m_descriptor_pool.create(*m_device, descriptor_pools, 1); + m_descriptor_pool.create(*m_device, descriptor_pool_sizes, 2); - VkDescriptorSetLayoutBinding bindings[1] = {}; + VkDescriptorSetLayoutBinding bindings[2] = {}; bindings[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; bindings[0].descriptorCount = 1; bindings[0].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; bindings[0].binding = 0; + bindings[1].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + bindings[1].descriptorCount = 1; + bindings[1].stageFlags = VK_SHADER_STAGE_VERTEX_BIT; + bindings[1].binding = 1; + VkDescriptorSetLayoutCreateInfo infos = {}; infos.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; infos.pBindings = bindings; - infos.bindingCount = 1; + infos.bindingCount = 2; CHECK_RESULT(vkCreateDescriptorSetLayout(*m_device, &infos, nullptr, &m_descriptor_layout)); @@ -70,6 +84,45 @@ namespace vk CHECK_RESULT(vkCreatePipelineLayout(*m_device, &layout_info, nullptr, &m_pipeline_layout)); } + virtual void update_uniforms(vk::glsl::program *program) + { + } + + virtual std::vector get_vertex_inputs() + { + if (!m_vao) + { + auto memory_types = vk::get_memory_mapping(m_device->gpu()); + m_vao = std::make_unique(*m_device, 1 * 0x100000, memory_types.host_visible_coherent, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, 0); + m_ubo = std::make_unique(*m_device, 8 * 0x100000, memory_types.host_visible_coherent, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, 0); + } + + return{}; + } + + virtual std::vector get_fragment_inputs() + { + std::vector fs_inputs; + fs_inputs.push_back({ ::glsl::program_domain::glsl_fragment_program, vk::glsl::program_input_type::input_type_texture,{},{}, 0, "fs0" }); + fs_inputs.push_back({ ::glsl::program_domain::glsl_fragment_program, vk::glsl::program_input_type::input_type_uniform_buffer,{},{}, 1, "static_data" }); + return fs_inputs; + } + + void upload_vertex_data(f32 *data, u32 first, u32 count) + { + verify(HERE), (first + count) <= 65536; + if (!m_vao) + { + auto memory_types = vk::get_memory_mapping(m_device->gpu()); + m_vao = std::make_unique(*m_device, 1 * 0x100000, memory_types.host_visible_coherent, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, 0); + m_ubo = std::make_unique(*m_device, 8 * 0x100000, memory_types.host_visible_coherent, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, 0); + } + + auto dst = m_vao->map((first * 4), VK_WHOLE_SIZE); + std::memcpy(dst, data, count * sizeof(f32)); + m_vao->unmap(); + } + vk::glsl::program* build_pipeline(VkRenderPass render_pass) { if (!compiled) @@ -101,8 +154,14 @@ namespace vk dynamic_state_descriptors[dynamic_state_info.dynamicStateCount++] = VK_DYNAMIC_STATE_SCISSOR; dynamic_state_info.pDynamicStates = dynamic_state_descriptors; + VkVertexInputBindingDescription vb = { 0, 16, VK_VERTEX_INPUT_RATE_VERTEX }; + VkVertexInputAttributeDescription via = { 0, 0, VK_FORMAT_R32G32B32A32_SFLOAT, 0 }; VkPipelineVertexInputStateCreateInfo vi = {}; vi.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO; + vi.vertexBindingDescriptionCount = 1; + vi.pVertexBindingDescriptions = &vb; + vi.vertexAttributeDescriptionCount = 1; + vi.pVertexAttributeDescriptions = &via; VkPipelineViewportStateCreateInfo vp = {}; vp.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; @@ -125,8 +184,19 @@ namespace vk VkPipelineColorBlendAttachmentState att = {}; if (renderpass_config.write_color) + { att.colorWriteMask = 0xf; + if (renderpass_config.enable_blend) + { + att.blendEnable = VK_TRUE; + att.alphaBlendOp = VK_BLEND_OP_ADD; + att.colorBlendOp = VK_BLEND_OP_ADD; + att.dstAlphaBlendFactor = att.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; + att.srcAlphaBlendFactor = att.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; + } + } + VkPipelineColorBlendStateCreateInfo cs = {}; cs.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO; cs.attachmentCount = renderpass_config.color_attachments; @@ -158,17 +228,14 @@ namespace vk CHECK_RESULT(vkCreateGraphicsPipelines(*m_device, nullptr, 1, &info, NULL, &pipeline)); - const std::vector unused; - std::vector fs_inputs; - fs_inputs.push_back({ ::glsl::program_domain::glsl_fragment_program, vk::glsl::program_input_type::input_type_texture, {}, {}, 0, "fs0"}); - auto program = std::make_unique(*m_device, pipeline, unused, fs_inputs); + auto program = std::make_unique(*m_device, pipeline, get_vertex_inputs(), get_fragment_inputs()); auto result = program.get(); m_program_cache[render_pass] = std::move(program); return result; } - void load_program(vk::command_buffer cmd, VkRenderPass pass, vk::image_view *src) + void load_program(vk::command_buffer cmd, VkRenderPass pass, VkImageView src) { vk::glsl::program *program = nullptr; auto found = m_program_cache.find(pass); @@ -195,11 +262,18 @@ namespace vk VK_FALSE, 0.f, 1.f, 0.f, 0.f, VK_FILTER_LINEAR, VK_FILTER_LINEAR, VK_SAMPLER_MIPMAP_MODE_NEAREST, VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK); } - VkDescriptorImageInfo info = { m_sampler->value, src->value, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL }; + VkDescriptorImageInfo info = { m_sampler->value, src, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL }; program->bind_uniform(info, "fs0", m_descriptor_set); + program->bind_uniform({ m_ubo->value, first_vertex * 128, 128 }, 1, m_descriptor_set); + + update_uniforms(program); vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, program->pipeline); vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, m_pipeline_layout, 0, 1, &m_descriptor_set, 0, nullptr); + + VkBuffer buffers = m_vao->value; + VkDeviceSize offsets = 0; + vkCmdBindVertexBuffers(cmd, 0, 1, &buffers, &offsets); } void create(vk::render_device &dev) @@ -274,10 +348,13 @@ namespace vk return result; } - void run(vk::command_buffer &cmd, u16 w, u16 h, vk::image* target, vk::image_view* src, VkRenderPass render_pass, std::list>& framebuffer_resources) + virtual void emit_geometry(vk::command_buffer &cmd) { - vk::framebuffer *fbo = get_framebuffer(target, render_pass, framebuffer_resources); + vkCmdDraw(cmd, num_drawable_elements, 1, first_vertex, 0); + } + void run(vk::command_buffer &cmd, u16 w, u16 h, vk::framebuffer* fbo, VkImageView src, VkRenderPass render_pass) + { load_program(cmd, render_pass, src); VkViewport vp{}; @@ -300,9 +377,20 @@ namespace vk rp_begin.renderArea.extent.height = h; vkCmdBeginRenderPass(cmd, &rp_begin, VK_SUBPASS_CONTENTS_INLINE); - vkCmdDraw(cmd, 4, 1, 0, 0); + emit_geometry(cmd); vkCmdEndRenderPass(cmd); } + + void run(vk::command_buffer &cmd, u16 w, u16 h, vk::image* target, VkImageView src, VkRenderPass render_pass, std::list>& framebuffer_resources) + { + vk::framebuffer *fbo = get_framebuffer(target, render_pass, framebuffer_resources); + run(cmd, w, h, fbo, src, render_pass); + } + + void run(vk::command_buffer &cmd, u16 w, u16 h, vk::image* target, vk::image_view* src, VkRenderPass render_pass, std::list>& framebuffer_resources) + { + run(cmd, w, h, target, src->value, render_pass, framebuffer_resources); + } }; struct depth_convert_pass : public overlay_pass @@ -344,4 +432,288 @@ namespace vk m_fragment_shader.id = 100003; } }; + + struct ui_overlay_renderer : public overlay_pass + { + f32 m_time = 0.f; + color4f m_scale_offset; + color4f m_color; + bool m_pulse_glow = false; + bool m_skip_texture_read = false; + bool m_clip_enabled; + areaf m_clip_region; + + std::vector> resources; + std::unordered_map> font_cache; + std::unordered_map> view_cache; + std::vector> temp_image_cache; + std::unordered_map> temp_view_cache; + + ui_overlay_renderer() + { + vs_src = + { + "#version 450\n" + "#extension GL_ARB_separate_shader_objects : enable\n" + "layout(location=0) in vec4 in_pos;\n" + "layout(std140, set=0, binding=1) uniform static_data{ vec4 regs[8]; };\n" + "layout(location=0) out vec2 tc0;\n" + "layout(location=1) out vec4 color;\n" + "layout(location=2) out vec4 parameters;\n" + "layout(location=3) out vec4 clip_rect;\n" + "\n" + "void main()\n" + "{\n" + " tc0.xy = in_pos.zw;\n" + " color = regs[1];\n" + " parameters = regs[2];\n" + " clip_rect = regs[3] * regs[0].zwzw;\n" + " vec4 pos = vec4((in_pos.xy * regs[0].zw) / regs[0].xy, 0.5, 1.);\n" + " gl_Position = (pos + pos) - 1.;\n" + "}\n" + }; + + fs_src = + { + "#version 420\n" + "#extension GL_ARB_separate_shader_objects : enable\n" + "layout(set=0, binding=0) uniform sampler2D fs0;\n" + "layout(location=0) in vec2 tc0;\n" + "layout(location=1) in vec4 color;\n" + "layout(location=2) in vec4 parameters;\n" + "layout(location=3) in vec4 clip_rect;\n" + "layout(location=0) out vec4 ocol;\n" + "\n" + "void main()\n" + "{\n" + " if (parameters.w != 0)\n" + " {" + " if (gl_FragCoord.x < clip_rect.x || gl_FragCoord.x > clip_rect.z ||\n" + " gl_FragCoord.y < clip_rect.y || gl_FragCoord.y > clip_rect.w)\n" + " {\n" + " discard;\n" + " return;\n" + " }\n" + " }\n" + "\n" + " vec4 diff_color = color;\n" + " if (parameters.y != 0)\n" + " diff_color.a *= (sin(parameters.x) + 1.f) * 0.5f;\n" + "\n" + " if (parameters.z != 0)\n" + " ocol = texture(fs0, tc0) * diff_color;\n" + " else\n" + " ocol = diff_color;\n" + "}\n" + }; + + renderpass_config.color_attachments = 1; + renderpass_config.write_color = true; + renderpass_config.write_depth = false; + renderpass_config.enable_blend = true; + + m_vertex_shader.id = 100004; + m_fragment_shader.id = 100005; + } + + vk::image_view* upload_simple_texture(vk::render_device &dev, vk::command_buffer &cmd, vk::memory_type_mapping &memory_types, + vk::vk_data_heap& upload_heap, u64 key, int w, int h, bool font, bool temp, void *pixel_src) + { + const VkFormat format = (font) ? VK_FORMAT_R8_UNORM : VK_FORMAT_B8G8R8A8_UNORM; + const u32 pitch = (font) ? w : w * 4; + const u32 data_size = pitch * h; + const auto offset = upload_heap.alloc<512>(data_size); + const auto addr = upload_heap.map(offset, data_size); + + const VkComponentMapping bgra = { VK_COMPONENT_SWIZZLE_B, VK_COMPONENT_SWIZZLE_G, VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_A }; + const VkComponentMapping rrrr = { VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_R }; + const VkComponentMapping mapping = (font) ? rrrr : bgra; + + const VkImageSubresourceRange range = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; + + auto tex = std::make_unique(dev, memory_types.device_local, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, + VK_IMAGE_TYPE_2D, format, w, h, 1, 1, 1, VK_SAMPLE_COUNT_1_BIT, VK_IMAGE_LAYOUT_UNDEFINED, + VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, + 0); + + if (pixel_src && data_size) + std::memcpy(addr, pixel_src, data_size); + else if (data_size) + std::memset(addr, 0, data_size); + + upload_heap.unmap(); + + VkBufferImageCopy region; + region.imageSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 }; + region.bufferOffset = offset; + region.bufferRowLength = w; + region.bufferImageHeight = h; + region.imageOffset = {}; + region.imageExtent = { (u32)w, (u32)h, 1u}; + + change_image_layout(cmd, tex.get(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, range); + vkCmdCopyBufferToImage(cmd, upload_heap.heap->value, tex->value, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); + change_image_layout(cmd, tex.get(), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, range); + + auto view = std::make_unique(dev, tex.get(), range, mapping); + + auto result = view.get(); + + if (!temp || font) + view_cache[key] = std::move(view); + else + temp_view_cache[key] = std::move(view); + + if (font) + font_cache[key] = std::move(tex); + else if (!temp) + resources.push_back(std::move(tex)); + else + temp_image_cache.push_back(std::move(tex)); + + return result; + } + + void create(vk::command_buffer &cmd, vk::memory_type_mapping &memory_types, vk::vk_data_heap &upload_heap) + { + auto& dev = cmd.get_command_pool().get_owner(); + overlay_pass::create(dev); + + rsx::overlays::resource_config configuration; + configuration.load_files(); + + u64 storage_key = 1; + for (const auto &res : configuration.texture_raw_data) + { + upload_simple_texture(dev, cmd, memory_types, upload_heap, storage_key++, res->w, res->h, false, false, res->data); + } + + configuration.free_resources(); + } + + void destroy() + { + temp_image_cache.clear(); + temp_view_cache.clear(); + + resources.clear(); + font_cache.clear(); + view_cache.clear(); + + overlay_pass::destroy(); + } + + void remove_temp_resources() + { + temp_image_cache.clear(); + temp_view_cache.clear(); + } + + vk::image_view* find_font(rsx::overlays::font *font, vk::command_buffer &cmd, vk::memory_type_mapping &memory_types, vk::vk_data_heap &upload_heap) + { + u64 key = (u64)font; + auto found = view_cache.find(key); + if (found != view_cache.end()) + return found->second.get(); + + //Create font file + return upload_simple_texture(cmd.get_command_pool().get_owner(), cmd, memory_types, upload_heap, key, font->width, font->height, true, false, font->glyph_data.data()); + } + + vk::image_view* find_temp_image(rsx::overlays::image_info *desc, vk::command_buffer &cmd, vk::memory_type_mapping &memory_types, vk::vk_data_heap &upload_heap) + { + u64 key = (u64)desc; + auto found = temp_view_cache.find(key); + if (found != temp_view_cache.end()) + return found->second.get(); + + return upload_simple_texture(cmd.get_command_pool().get_owner(), cmd, memory_types, upload_heap, key, desc->w, desc->h, false, true, desc->data); + } + + void update_uniforms(vk::glsl::program *program) override + { + auto dst = (f32*)m_ubo->map(first_vertex * 128, 128); + dst[0] = m_scale_offset.r; + dst[1] = m_scale_offset.g; + dst[2] = m_scale_offset.b; + dst[3] = m_scale_offset.a; + dst[4] = m_color.r; + dst[5] = m_color.g; + dst[6] = m_color.b; + dst[7] = m_color.a; + dst[8] = m_time; + dst[9] = m_pulse_glow? 1.f : 0.f; + dst[10] = m_skip_texture_read? 0.f : 1.f; + dst[11] = m_clip_enabled ? 1.f : 0.f; + dst[12] = m_clip_region.x1; + dst[13] = m_clip_region.y1; + dst[14] = m_clip_region.x2; + dst[15] = m_clip_region.y2; + m_ubo->unmap(); + } + + void emit_geometry(vk::command_buffer &cmd) + { + //Split into groups of 4 + auto tmp_first = first_vertex; + auto num_quads = num_drawable_elements / 4; + + for (u32 n = 0; n < num_quads; ++n) + { + vkCmdDraw(cmd, 4, 1, tmp_first, 0); + tmp_first += 4; + } + } + + void run(vk::command_buffer &cmd, u16 w, u16 h, vk::framebuffer* target, VkRenderPass render_pass, vk::memory_type_mapping &memory_types, + vk::vk_data_heap &upload_heap, rsx::overlays::user_interface &ui) + { + m_scale_offset = color4f((f32)ui.virtual_width, (f32)ui.virtual_height, 1.f, 1.f); + m_time = (f32)(get_system_time() / 1000) * 0.005f; + + u32 vertex_data_offset = 0; + first_vertex = 0; + + for (auto &command : ui.get_compiled().draw_commands) + { + num_drawable_elements = (u32)command.second.size(); + const u32 value_count = num_drawable_elements * 4; + + upload_vertex_data((f32*)command.second.data(), vertex_data_offset, value_count); + + m_skip_texture_read = false; + m_color = command.first.color; + m_pulse_glow = command.first.pulse_glow; + m_clip_enabled = command.first.clip_region; + m_clip_region = command.first.clip_rect; + + auto src = vk::null_image_view(cmd); + switch (command.first.texture_ref) + { + case rsx::overlays::image_resource_id::game_icon: + case rsx::overlays::image_resource_id::backbuffer: + //TODO + case rsx::overlays::image_resource_id::none: + m_skip_texture_read = true; + break; + case rsx::overlays::image_resource_id::font_file: + src = find_font(command.first.font_ref, cmd, memory_types, upload_heap)->value; + break; + case rsx::overlays::image_resource_id::raw_image: + src = find_temp_image((rsx::overlays::image_info*)command.first.external_data_ref, cmd, memory_types, upload_heap)->value; + break; + default: + src = view_cache[command.first.texture_ref]->value; + break; + } + + overlay_pass::run(cmd, w, h, target, src, render_pass); + + vertex_data_offset += value_count; + first_vertex += num_drawable_elements; + } + + ui.update(); + } + }; } diff --git a/rpcs3/Emu/RSX/VK/VKTexture.cpp b/rpcs3/Emu/RSX/VK/VKTexture.cpp index bc5e793ee4..93d90fb2cb 100644 --- a/rpcs3/Emu/RSX/VK/VKTexture.cpp +++ b/rpcs3/Emu/RSX/VK/VKTexture.cpp @@ -149,7 +149,7 @@ namespace vk void copy_mipmaped_image_using_buffer(VkCommandBuffer cmd, VkImage dst_image, const std::vector& subresource_layout, int format, bool is_swizzled, u16 mipmap_count, - VkImageAspectFlags flags, vk::vk_data_heap &upload_heap, vk::buffer* upload_buffer) + VkImageAspectFlags flags, vk::vk_data_heap &upload_heap) { u32 mipmap_level = 0; u32 block_in_pixel = get_format_block_size_in_texel(format); @@ -160,10 +160,10 @@ namespace vk u32 image_linear_size = row_pitch * layout.height_in_block * layout.depth; size_t offset_in_buffer = upload_heap.alloc<512>(image_linear_size); - void *mapped_buffer = upload_buffer->map(offset_in_buffer, image_linear_size); + void *mapped_buffer = upload_heap.map(offset_in_buffer, image_linear_size); gsl::span mapped{ (gsl::byte*)mapped_buffer, ::narrow(image_linear_size) }; upload_texture_subresource(mapped, layout, format, is_swizzled, 256); - upload_buffer->unmap(); + upload_heap.unmap(); VkBufferImageCopy copy_info = {}; copy_info.bufferOffset = offset_in_buffer; @@ -176,7 +176,7 @@ namespace vk copy_info.imageSubresource.mipLevel = mipmap_level % mipmap_count; copy_info.bufferRowLength = block_in_pixel * row_pitch / block_size_in_bytes; - vkCmdCopyBufferToImage(cmd, upload_buffer->value, dst_image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©_info); + vkCmdCopyBufferToImage(cmd, upload_heap.heap->value, dst_image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©_info); mipmap_level++; } } diff --git a/rpcs3/Emu/RSX/VK/VKTextureCache.h b/rpcs3/Emu/RSX/VK/VKTextureCache.h index a2cd9b75eb..0cddd96a4a 100644 --- a/rpcs3/Emu/RSX/VK/VKTextureCache.h +++ b/rpcs3/Emu/RSX/VK/VKTextureCache.h @@ -425,7 +425,6 @@ namespace vk vk::gpu_formats_support m_formats_support; VkQueue m_submit_queue; vk_data_heap* m_texture_upload_heap; - vk::buffer* m_texture_upload_buffer; //Stuff that has been dereferenced goes into these std::list m_discardable_storage; @@ -787,7 +786,7 @@ namespace vk } vk::copy_mipmaped_image_using_buffer(cmd, image->value, subresource_layout, gcm_format, input_swizzled, mipmaps, subres_range.aspectMask, - *m_texture_upload_heap, m_texture_upload_buffer); + *m_texture_upload_heap); vk::leave_uninterruptible(); @@ -848,14 +847,13 @@ namespace vk public: void initialize(vk::render_device& device, vk::memory_type_mapping& memory_types, vk::gpu_formats_support& formats_support, - VkQueue submit_queue, vk::vk_data_heap& upload_heap, vk::buffer* upload_buffer) + VkQueue submit_queue, vk::vk_data_heap& upload_heap) { m_memory_types = memory_types; m_formats_support = formats_support; m_device = &device; m_submit_queue = submit_queue; m_texture_upload_heap = &upload_heap; - m_texture_upload_buffer = upload_buffer; } void destroy() override diff --git a/rpcs3/Emu/RSX/overlay_controls.h b/rpcs3/Emu/RSX/overlay_controls.h new file mode 100644 index 0000000000..67482c120a --- /dev/null +++ b/rpcs3/Emu/RSX/overlay_controls.h @@ -0,0 +1,1456 @@ +#pragma once +#include "Utilities/types.h" +#include "Utilities/geometry.h" + +#include +#include +#include + +#ifndef _WIN32 +#include +#include +#include +#endif + +// STB_IMAGE_IMPLEMENTATION and STB_TRUETYPE_IMPLEMENTATION defined externally +#include +#include + +// Definitions for common UI controls and their routines +namespace rsx +{ + namespace overlays + { + enum image_resource_id : u8 + { + //NOTE: 1 - 252 are user defined + none = 0, //No image + raw_image = 252, //Raw image data passed via image_info struct + font_file = 253, //Font file + game_icon = 254, //Use game icon + backbuffer = 255 //Use current backbuffer contents + }; + + struct vertex + { + float values[4]; + + vertex() {} + + vertex(float x, float y) + { + vec2(x, y); + } + + vertex(float x, float y, float z) + { + vec3(x, y, z); + } + + vertex(float x, float y, float z, float w) + { + vec4(x, y, z, w); + } + + float& operator[](int index) + { + return values[index]; + } + + void vec2(float x, float y) + { + values[0] = x; + values[1] = y; + values[2] = 0.f; + values[3] = 1.f; + } + + void vec3(float x, float y, float z) + { + values[0] = x; + values[1] = y; + values[2] = z; + values[3] = 1.f; + } + + void vec4(float x, float y, float z, float w) + { + values[0] = x; + values[1] = y; + values[2] = z; + values[3] = w; + } + + void operator += (const vertex& other) + { + values[0] += other.values[0]; + values[1] += other.values[1]; + values[2] += other.values[2]; + values[3] += other.values[3]; + } + + void operator -= (const vertex& other) + { + values[0] -= other.values[0]; + values[1] -= other.values[1]; + values[2] -= other.values[2]; + values[3] -= other.values[3]; + } + }; + + struct font + { + const u32 width = 1024; + const u32 height = 1024; + const u32 oversample = 2; + const u32 char_count = 256; //16x16 grid at max 48pt + + f32 size_pt = 12.f; + f32 size_px = 16.f; //Default font 12pt size + f32 em_size = 0.f; + std::string font_name; + std::vector pack_info; + std::vector glyph_data; + bool initialized = false; + + font(const char *ttf_name, f32 size) + { + //Init glyph + std::vector bytes; +#ifdef _WIN32 + std::string font_dir = "C:/Windows/Fonts/"; + std::string fallback_font = "C:/Windows/Fonts/Arial.ttf"; +#else + char *home = getenv("HOME"); + if (home == nullptr) + home = getpwuid(getuid())->pw_dir; + + std::string font_dir = home; + if (home[font_dir.length() - 1] == '/') + font_dir += ".fonts/"; + else + font_dir += "/.fonts/"; + + std::string fallback_font = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"; +#endif + std::string requested_file = font_dir + ttf_name + ".ttf"; + std::string file_path = requested_file; + + if (!fs::is_file(requested_file)) + { + if (fs::is_file(fallback_font)) + { + //TODO: Multiple fallbacks + file_path = fallback_font; + } + else + { + LOG_ERROR(RSX, "Failed to initialize font '%s.ttf'", ttf_name); + return; + } + } + + fs::file f(file_path); + f.read(bytes, f.size()); + + glyph_data.resize(width * height); + pack_info.resize(256); + + stbtt_pack_context context; + if (!stbtt_PackBegin(&context, glyph_data.data(), width, height, 0, 1, nullptr)) + { + LOG_ERROR(RSX, "Font packing failed"); + return; + } + + stbtt_PackSetOversampling(&context, oversample, oversample); + + //Convert pt to px + size_px = ceilf((f32)size * 96.f / 72.f); + size_pt = size; + + if (!stbtt_PackFontRange(&context, bytes.data(), 0, size_px, 0, 256, pack_info.data())) + { + LOG_ERROR(RSX, "Font packing failed"); + stbtt_PackEnd(&context); + return; + } + + stbtt_PackEnd(&context); + + font_name = ttf_name; + initialized = true; + + f32 unused; + get_char('m', em_size, unused); + } + + stbtt_aligned_quad get_char(char c, f32 &x_advance, f32 &y_advance) + { + if (!initialized) + return{}; + + stbtt_aligned_quad quad; + stbtt_GetPackedQuad(pack_info.data(), width, height, c, &x_advance, &y_advance, &quad, true); + return quad; + } + + std::vector render_text(const char *text, u16 text_limit = UINT16_MAX, bool wrap = false) + { + if (!initialized) + { + return{}; + } + + std::vector result; + + int i = 0; + f32 x_advance = 0.f, y_advance = 0.f; + bool skip_whitespace = false; + + while (true) + { + if (char c = text[i++]) + { + if ((u32)c >= char_count) + { + //Unsupported glyph, render null for now + c = ' '; + } + + switch (c) + { + case '\n': + { + y_advance += size_px + 2.f; + x_advance = 0.f; + continue; + } + case '\r': + { + x_advance = 0.f; + continue; + } + default: + { + stbtt_aligned_quad quad; + if (skip_whitespace && text[i - 1] == ' ') + { + quad = {}; + } + else + { + quad = get_char(c, x_advance, y_advance); + skip_whitespace = false; + } + + if (quad.x1 > text_limit) + { + bool wrapped = false; + bool non_whitespace_break = false; + + if (wrap) + { + //scan previous chars + for (int j = i - 1, nb_chars = 0; j > 0; j--, nb_chars++) + { + if (text[j] == '\n') + break; + + if (text[j] == ' ') + { + non_whitespace_break = true; + continue; + } + + if (non_whitespace_break) + { + if (nb_chars > 1) + { + nb_chars--; + + auto first_affected = result.size() - (nb_chars * 4); + f32 base_x = result[first_affected].values[0]; + + for (size_t n = first_affected; n < result.size(); ++n) + { + auto char_index = n / 4; + if (text[char_index] == ' ') + { + //Skip character + result[n++].vec2(0.f, 0.f); + result[n++].vec2(0.f, 0.f); + result[n++].vec2(0.f, 0.f); + result[n].vec2(0.f, 0.f); + continue; + } + + result[n].values[0] -= base_x; + result[n].values[1] += size_px + 2.f; + } + + x_advance = result.back().values[0]; + } + else + { + x_advance = 0.f; + } + + wrapped = true; + y_advance += size_px + 2.f; + + if (text[i - 1] == ' ') + { + quad = {}; + skip_whitespace = true; + } + else + { + quad = get_char(c, x_advance, y_advance); + } + + break; + } + } + } + + if (!wrapped) + { + //TODO: Ellipsize + break; + } + } + + result.push_back({ quad.x0, quad.y0, quad.s0, quad.t0 }); + result.push_back({ quad.x1, quad.y0, quad.s1, quad.t0 }); + result.push_back({ quad.x0, quad.y1, quad.s0, quad.t1 }); + result.push_back({ quad.x1, quad.y1, quad.s1, quad.t1 }); + break; + } + } //switch + } + else + { + break; + } + } + + return result; + } + + }; + + //TODO: Singletons are cancer + class fontmgr + { + private: + std::vector> fonts; + static fontmgr *m_instance; + + font* find(const char *name, int size) + { + for (auto &f : fonts) + { + if (f->font_name == name && + f->size_pt == size) + return f.get(); + } + + fonts.push_back(std::make_unique(name, (f32)size)); + return fonts.back().get(); + } + + public: + + fontmgr() {} + ~fontmgr() + { + if (m_instance) + { + delete m_instance; + m_instance = nullptr; + } + } + + static font* get(const char *name, int size) + { + if (m_instance == nullptr) + m_instance = new fontmgr; + + return m_instance->find(name, size); + } + }; + + struct image_info + { + int w = 0, h = 0; + int bpp = 0; + u8* data = nullptr; + + image_info(image_info&) = delete; + + image_info(const char* filename) + { + if (!fs::is_file(filename)) + { + LOG_ERROR(RSX, "Image resource file `%s' not found", filename); + return; + } + + std::vector bytes; + fs::file f(filename); + f.read(bytes, f.size()); + data = stbi_load_from_memory(bytes.data(), (s32)f.size(), &w, &h, &bpp, STBI_rgb_alpha); + } + + image_info(const std::vector& bytes) + { + data = stbi_load_from_memory(bytes.data(), (s32)bytes.size(), &w, &h, &bpp, STBI_rgb_alpha); + } + + ~image_info() + { + stbi_image_free(data); + data = nullptr; + w = h = bpp = 0; + } + }; + + struct resource_config + { + enum standard_image_resource : u8 + { + fade_top = 1, + fade_bottom, + cross, + circle, + triangle, + square, + save, + new_entry + }; + + //Define resources + std::vector texture_resource_files; + std::vector> texture_raw_data; + + resource_config() + { + texture_resource_files.push_back("fade_top.png"); + texture_resource_files.push_back("fade_bottom.png"); + texture_resource_files.push_back("cross.png"); + texture_resource_files.push_back("circle.png"); + texture_resource_files.push_back("triangle.png"); + texture_resource_files.push_back("square.png"); + texture_resource_files.push_back("save.png"); + texture_resource_files.push_back("new.png"); + } + + void load_files() + { + for (const auto &res : texture_resource_files) + { + //First check the global config dir + auto info = std::make_unique((fs::get_config_dir() + "Icons/ui/" + res).c_str()); + + if (info->data == nullptr) + { + //Resource was not found in config dir, try and grab from relative path (linux) + info = std::make_unique(("Icons/ui/" + res).c_str()); + + if (info->data != nullptr) + { + //Install the image to config dir + auto dst_dir = fs::get_config_dir() + "Icons/ui/"; + auto src = "Icons/ui/" + res; + auto dst = dst_dir + res; + + if (!fs::is_dir(dst_dir)) + { + auto root_folder = fs::get_config_dir() + "Icons/"; + if (!fs::is_dir(root_folder)) + fs::create_dir(root_folder); + + fs::create_dir(dst_dir); + } + + fs::copy_file(src, dst, true); + } + } + + texture_raw_data.push_back(std::move(info)); + } + } + + void free_resources() + { + texture_raw_data.clear(); + } + }; + + struct compiled_resource + { + struct command_config + { + color4f color = { 1.f, 1.f, 1.f, 1.f }; + bool pulse_glow = false; + + areaf clip_rect = {}; + bool clip_region = false; + + u8 texture_ref = image_resource_id::none; + font *font_ref = nullptr; + void *external_data_ref = nullptr; + + command_config() {} + + command_config(u8 ref) + { + texture_ref = ref; + font_ref = nullptr; + } + + command_config(font *ref) + { + texture_ref = image_resource_id::font_file; + font_ref = ref; + } + }; + + std::vector>> draw_commands; + + void add(const compiled_resource& other) + { + auto old_size = draw_commands.size(); + draw_commands.resize(old_size + other.draw_commands.size()); + std::copy(other.draw_commands.begin(), other.draw_commands.end(), draw_commands.begin() + old_size); + } + + void add(const compiled_resource& other, f32 x_offset, f32 y_offset) + { + auto old_size = draw_commands.size(); + draw_commands.resize(old_size + other.draw_commands.size()); + std::copy(other.draw_commands.begin(), other.draw_commands.end(), draw_commands.begin() + old_size); + + for (size_t n = old_size; n < draw_commands.size(); ++n) + { + for (auto &v : draw_commands[n].second) + { + v += vertex(x_offset, y_offset, 0.f, 0.f); + } + } + } + + void add(const compiled_resource& other, f32 x_offset, f32 y_offset, const areaf& clip_rect) + { + auto old_size = draw_commands.size(); + draw_commands.resize(old_size + other.draw_commands.size()); + std::copy(other.draw_commands.begin(), other.draw_commands.end(), draw_commands.begin() + old_size); + + for (size_t n = old_size; n < draw_commands.size(); ++n) + { + for (auto &v : draw_commands[n].second) + { + v += vertex(x_offset, y_offset, 0.f, 0.f); + } + + draw_commands[n].first.clip_rect = clip_rect; + draw_commands[n].first.clip_region = true; + } + } + }; + + struct overlay_element + { + enum text_align + { + left = 0, + center + }; + + u16 x = 0; + u16 y = 0; + u16 w = 0; + u16 h = 0; + + std::string text; + font* font_ref = nullptr; + text_align alignment = left; + bool wrap_text = false; + bool clip_text = true; + + color4f back_color = { 0.f, 0.f, 0.f, 1.f }; + color4f fore_color = { 1.f, 1.f, 1.f, 1.f }; + bool pulse_effect_enabled = false; + + compiled_resource compiled_resources; + bool is_compiled = false; + + f32 padding_left = 0.f; + f32 padding_right = 0.f; + f32 padding_top = 0.f; + f32 padding_bottom = 0.f; + + overlay_element() {} + overlay_element(u16 _w, u16 _h) : w(_w), h(_h) {} + + virtual void refresh() + { + //Just invalidate for draw when get_compiled() is called + is_compiled = false; + } + + virtual void translate(s16 _x, s16 _y) + { + x = (u16)(x + _x); + y = (u16)(y + _y); + + is_compiled = false; + } + + virtual void scale(f32 _x, f32 _y, bool origin_scaling) + { + if (origin_scaling) + { + x = (u16)(_x * x); + y = (u16)(_y * y); + } + + w = (u16)(_x * w); + h = (u16)(_y * h); + + is_compiled = false; + } + + virtual void set_pos(u16 _x, u16 _y) + { + x = _x; + y = _y; + + is_compiled = false; + } + + virtual void set_size(u16 _w, u16 _h) + { + w = _w; + h = _h; + + is_compiled = false; + } + + virtual void set_padding(f32 left, f32 right, f32 top, f32 bottom) + { + padding_left = left; + padding_right = right; + padding_top = top; + padding_bottom = bottom; + + is_compiled = false; + } + + virtual void set_padding(f32 padding) + { + padding_left = padding_right = padding_top = padding_bottom = padding; + is_compiled = false; + } + + virtual void set_text(const std::string& text) + { + this->text = text; + is_compiled = false; + } + + virtual void set_text(const char* text) + { + this->text = text; + is_compiled = false; + } + + virtual void set_font(const char* font_name, u16 font_size) + { + font_ref = fontmgr::get(font_name, font_size); + is_compiled = false; + } + + virtual void align_text(text_align align) + { + alignment = align; + is_compiled = false; + } + + virtual void set_wrap_text(bool state) + { + wrap_text = state; + is_compiled = false; + } + + virtual std::vector render_text(const char *string, f32 x, f32 y) + { + auto renderer = font_ref; + if (!renderer) renderer = fontmgr::get("Arial", 12); + + f32 text_extents_w = 0.f; + u16 clip_width = clip_text ? w : UINT16_MAX; + std::vector result = renderer->render_text(string, clip_width, wrap_text); + + if (result.size() > 0) + { + for (auto &v : result) + { + //Check for real text region extent + //TODO: Ellipsis + text_extents_w = std::max(v.values[0], text_extents_w); + + //Apply transform. + //(0, 0) has text sitting 50% off the top left corner (text is outside the rect) hence the offset by text height / 2 + v.values[0] += x + padding_left; + v.values[1] += y + padding_top + (f32)renderer->size_px * 0.5f; + } + + if (alignment == center) + { + //Scan for lines and measure them + //Reposition them to the center + std::vector> lines; + u32 line_begin = 0; + u32 ctr = 0; + + for (auto c : text) + { + switch (c) + { + case '\r': + continue; + case '\n': + lines.push_back({ line_begin, ctr }); + line_begin = ctr; + continue; + default: + ctr += 4; + } + } + + lines.push_back({ line_begin, ctr }); + const auto max_region_w = std::max(text_extents_w, w); + + for (auto p : lines) + { + if (p.first >= p.second) + continue; + + const f32 line_length = result[p.second - 1].values[0] - result[p.first].values[0]; + const bool wrapped = fabs(result[p.second - 1].values[1] - result[p.first + 3].values[1]) >= (renderer->size_px * 0.5f); + + if (wrapped) + continue; + + if (line_length < max_region_w) + { + f32 offset = (max_region_w - line_length) * 0.5f; + for (auto n = p.first; n < p.second; ++n) + { + result[n].values[0] += offset; + } + } + } + } + } + + return result; + } + + virtual compiled_resource& get_compiled() + { + if (!is_compiled) + { + compiled_resources = {}; + compiled_resources.draw_commands.push_back({}); + + auto &config = compiled_resources.draw_commands.front().first; + config.color = back_color; + config.pulse_glow = pulse_effect_enabled; + + auto& verts = compiled_resources.draw_commands.front().second; + verts.resize(4); + verts[0].vec4(x + padding_left, y + padding_bottom, 0.f, 0.f); + verts[1].vec4(x + w - padding_right, y + padding_bottom, 1.f, 0.f); + verts[2].vec4(x + padding_left, y + h - padding_top, 0.f, 1.f); + verts[3].vec4(x + w - padding_right, y + h - padding_top, 1.f, 1.f); + + if (!text.empty()) + { + compiled_resources.draw_commands.push_back({}); + compiled_resources.draw_commands.back().first = font_ref? font_ref : fontmgr::get("Arial", 12); + compiled_resources.draw_commands.back().first.color = fore_color; + compiled_resources.draw_commands.back().second = render_text(text.c_str(), (f32)x, (f32)y); + + if (compiled_resources.draw_commands.back().second.size() == 0) + compiled_resources.draw_commands.pop_back(); + } + + is_compiled = true; + } + + return compiled_resources; + } + + void measure_text(u16& width, u16& height, bool ignore_word_wrap = false) const + { + if (text.empty()) + { + width = height = 0; + return; + } + + auto renderer = font_ref; + if (!renderer) renderer = fontmgr::get("Arial", 12); + + f32 text_width = 0.f; + f32 unused = 0.f; + f32 max_w = 0.f; + f32 last_word = 0.f; + height = (u16)renderer->size_px; + + for (auto c : text) + { + if (c == '\n') + { + height += (u16)renderer->size_px + 2; + max_w = std::max(max_w, text_width); + text_width = 0.f; + last_word = 0.f; + continue; + } + + if (c == ' ') + { + last_word = text_width; + } + + renderer->get_char(c, text_width, unused); + + if (!ignore_word_wrap && wrap_text && text_width >= w) + { + if ((text_width - last_word) < w) + { + max_w = std::max(max_w, last_word); + text_width -= (last_word + renderer->em_size); + height += (u16)renderer->size_px + 2; + } + } + } + + max_w = std::max(max_w, text_width); + width = (u16)ceilf(max_w); + } + + }; + + struct animation_base + { + float duration = 0.f; + float t = 0.f; + overlay_element *ref = nullptr; + + virtual void update(float /*elapsed*/) {} + void reset() { t = 0.f; } + }; + + struct layout_container : public overlay_element + { + std::vector> m_items; + u16 advance_pos = 0; + u16 pack_padding = 0; + u16 scroll_offset_value = 0; + bool auto_resize = true; + + virtual overlay_element* add_element(std::unique_ptr&, int = -1) = 0; + + layout_container() + { + //Transparent by default + back_color.a = 0.f; + } + + void translate(s16 _x, s16 _y) override + { + overlay_element::translate(_x, _y); + + for (auto &itm : m_items) + itm->translate(_x, _y); + } + + void set_pos(u16 _x, u16 _y) override + { + s16 dx = (s16)(_x - x); + s16 dy = (s16)(_y - y); + translate(dx, dy); + } + + compiled_resource& get_compiled() override + { + if (!is_compiled) + { + compiled_resource result = overlay_element::get_compiled(); + + for (auto &itm : m_items) + result.add(itm->get_compiled()); + + compiled_resources = result; + } + + return compiled_resources; + } + + virtual u16 get_scroll_offset_px() = 0; + }; + + struct vertical_layout : public layout_container + { + overlay_element* add_element(std::unique_ptr& item, int offset = -1) + { + if (auto_resize) + { + item->set_pos(item->x + x, h + pack_padding + y); + h += item->h + pack_padding; + w = std::max(w, item->w); + } + else + { + item->set_pos(item->x + x, advance_pos + pack_padding + y); + advance_pos += item->h + pack_padding; + } + + if (offset < 0) + { + m_items.push_back(std::move(item)); + return m_items.back().get(); + } + else + { + auto result = item.get(); + m_items.insert(m_items.begin() + offset, std::move(item)); + return result; + } + } + + compiled_resource& get_compiled() override + { + if (scroll_offset_value == 0 && auto_resize) + return layout_container::get_compiled(); + + if (!is_compiled) + { + compiled_resource result = overlay_element::get_compiled(); + const f32 global_y_offset = (f32)-scroll_offset_value; + + for (auto &item : m_items) + { + const s32 item_y_limit = (s32)(item->y + item->h) - scroll_offset_value - y; + const s32 item_y_base = (s32)item->y - scroll_offset_value - y; + + if (item_y_limit < 0 || item_y_base > h) + { + //Out of bounds + continue; + } + else if (item_y_limit > h || item_y_base < 0) + { + //Partial render + areaf clip_rect = { (f32)x, (f32)y, (f32)(x + w), (f32)(y + h) }; + result.add(item->get_compiled(), 0.f, global_y_offset, clip_rect); + } + else + { + //Normal + result.add(item->get_compiled(), 0.f, global_y_offset); + } + } + + compiled_resources = result; + } + + return compiled_resources; + } + + u16 get_scroll_offset_px() override + { + return scroll_offset_value; + } + }; + + struct horizontal_layout : public layout_container + { + overlay_element* add_element(std::unique_ptr& item, int offset = -1) + { + if (auto_resize) + { + item->set_pos(w + pack_padding + x, item->y + y); + w += item->w + pack_padding; + h = std::max(h, item->h); + } + else + { + item->set_pos(advance_pos + pack_padding + x, item->y + y); + advance_pos += item->w + pack_padding; + } + + if (offset < 0) + { + m_items.push_back(std::move(item)); + return m_items.back().get(); + } + else + { + auto result = item.get(); + m_items.insert(m_items.begin() + offset, std::move(item)); + return result; + } + } + + compiled_resource& get_compiled() override + { + if (scroll_offset_value == 0 && auto_resize) + return layout_container::get_compiled(); + + if (!is_compiled) + { + compiled_resource result = overlay_element::get_compiled(); + const f32 global_x_offset = (f32)-scroll_offset_value; + + for (auto &item : m_items) + { + const s32 item_x_limit = (s32)(item->x + item->w) - scroll_offset_value - w; + const s32 item_x_base = (s32)item->x - scroll_offset_value - w; + + if (item_x_limit < 0 || item_x_base > h) + { + //Out of bounds + continue; + } + else if (item_x_limit > h || item_x_base < 0) + { + //Partial render + areaf clip_rect = { (f32)x, (f32)y, (f32)(x + w), (f32)(y + h) }; + result.add(item->get_compiled(), global_x_offset, 0.f, clip_rect); + } + else + { + //Normal + result.add(item->get_compiled(), global_x_offset, 0.f); + } + } + + compiled_resources = result; + } + + return compiled_resources; + } + + u16 get_scroll_offset_px() override + { + return scroll_offset_value; + } + }; + + //Controls + struct spacer : public overlay_element + { + using overlay_element::overlay_element; + compiled_resource& get_compiled() override + { + //No draw + return compiled_resources; + } + }; + + struct image_view : public overlay_element + { + private: + u8 image_resource_ref = image_resource_id::none; + void *external_ref = nullptr; + + public: + using overlay_element::overlay_element; + + compiled_resource& get_compiled() override + { + if (!is_compiled) + { + auto &result = overlay_element::get_compiled(); + result.draw_commands.front().first = image_resource_ref; + result.draw_commands.front().first.color = fore_color; + result.draw_commands.front().first.external_data_ref = external_ref; + } + + return compiled_resources; + } + + void set_image_resource(u8 resource_id) + { + image_resource_ref = resource_id; + external_ref = nullptr; + } + + void set_raw_image(image_info *raw_image) + { + image_resource_ref = image_resource_id::raw_image; + external_ref = raw_image; + } + }; + + struct image_button : public image_view + { + u16 text_offset = 0; + + image_button() + { + //Do not clip text to region extents + //TODO: Define custom clipping region or use two controls to emulate + clip_text = false; + } + + image_button(u16 _w, u16 _h) + { + clip_text = false; + set_size(_w, _h); + } + + void set_size(u16 w, u16 h) override + { + image_view::set_size(h, h); + text_offset = (h / 2) + 10; //By default text is at the horizontal center + } + + compiled_resource& get_compiled() override + { + if (!is_compiled) + { + auto& compiled = image_view::get_compiled(); + for (auto &cmd : compiled.draw_commands) + { + if (cmd.first.texture_ref == image_resource_id::font_file) + { + //Text, translate geometry to the right + const f32 text_height = font_ref ? font_ref->size_px : 16.f; + const f32 offset_y = (h > text_height) ? (f32)(h - text_height) : ((f32)h - text_height); + + for (auto &v : cmd.second) + { + v.values[0] += text_offset + 15.f; + v.values[1] += offset_y + 5.f; + } + } + } + } + + return compiled_resources; + } + }; + + struct label : public overlay_element + { + label() {} + + label(const char *text) + { + this->text = text; + } + + void auto_resize(bool grow_only = false, u16 limit_w = UINT16_MAX, u16 limit_h = UINT16_MAX) + { + u16 new_width, new_height; + measure_text(new_width, new_height, true); + + if (new_width > limit_w && wrap_text) + measure_text(new_width, new_height, false); + + if (grow_only) + { + new_width = std::max(w, new_width); + new_height = std::max(h, new_height); + } + + w = std::min(new_width, limit_w); + h = std::min(new_height, limit_h); + } + }; + + struct progress_bar : public overlay_element + { + private: + overlay_element indicator; + label text_view; + + f32 m_limit = 100.f; + f32 m_value = 0.f; + + public: + using overlay_element::overlay_element; + + void inc(f32 value) + { + set_value(m_value + value); + } + + void dec(f32 value) + { + set_value(m_value - value); + } + + void set_limit(f32 limit) + { + m_limit = limit; + is_compiled = false; + } + + void set_value(f32 value) + { + if (value < 0.f) + m_value = 0.f; + else if (value > m_limit) + m_value = m_limit; + else + m_value = value; + + f32 indicator_width = (w * m_value) / m_limit; + indicator.set_size((u16)indicator_width, h); + is_compiled = false; + } + + void set_pos(u16 _x, u16 _y) override + { + u16 text_w, text_h; + text_view.measure_text(text_w, text_h); + text_h += 5; + + overlay_element::set_pos(_x, _y + text_h); + indicator.set_pos(_x, _y + text_h); + text_view.set_pos(_x, _y); + } + + void set_size(u16 _w, u16 _h) override + { + overlay_element::set_size(_w, _h); + text_view.set_size(_w, text_view.h); + set_value(m_value); + } + + void translate(s16 dx, s16 dy) override + { + set_pos(x + dx, y + dy); + } + + void set_text(const char* str) override + { + text_view.set_text(str); + text_view.align_text(text_align::center); + + u16 text_w, text_h; + text_view.measure_text(text_w, text_h); + text_view.set_size(w, text_h); + + set_pos(text_view.x, text_view.y); + is_compiled = false; + } + + void set_text(const std::string& str) override + { + text_view.set_text(str); + text_view.align_text(text_align::center); + + u16 text_w, text_h; + text_view.measure_text(text_w, text_h); + text_view.set_size(w, text_h); + + set_pos(text_view.x, text_view.y); + is_compiled = false; + } + + compiled_resource& get_compiled() override + { + if (!is_compiled) + { + auto& compiled = overlay_element::get_compiled(); + compiled.add(text_view.get_compiled()); + + indicator.back_color = fore_color; + indicator.refresh(); + compiled.add(indicator.get_compiled()); + } + + return compiled_resources; + } + }; + + struct list_view : public vertical_layout + { + private: + std::unique_ptr m_scroll_indicator_top; + std::unique_ptr m_scroll_indicator_bottom; + std::unique_ptr m_cancel_btn; + std::unique_ptr m_accept_btn; + std::unique_ptr m_highlight_box; + + u16 m_elements_height = 0; + s16 m_selected_entry = -1; + u16 m_elements_count = 0; + + public: + list_view(u16 width, u16 height) + { + w = width; + h = height; + + m_scroll_indicator_top = std::make_unique(width, 5); + m_scroll_indicator_bottom = std::make_unique(width, 5); + m_accept_btn = std::make_unique(120, 20); + m_cancel_btn = std::make_unique(120, 20); + m_highlight_box = std::make_unique(width, 0); + + m_scroll_indicator_top->set_size(width, 40); + m_scroll_indicator_bottom->set_size(width, 40); + m_accept_btn->set_size(120, 30); + m_cancel_btn->set_size(120, 30); + + m_scroll_indicator_top->set_image_resource(resource_config::standard_image_resource::fade_top); + m_scroll_indicator_bottom->set_image_resource(resource_config::standard_image_resource::fade_bottom); + m_accept_btn->set_image_resource(resource_config::standard_image_resource::cross); + m_cancel_btn->set_image_resource(resource_config::standard_image_resource::circle); + + m_scroll_indicator_bottom->set_pos(0, height - 40); + m_accept_btn->set_pos(30, height + 20); + m_cancel_btn->set_pos(180, height + 20); + + m_accept_btn->text = "Select"; + m_cancel_btn->text = "Cancel"; + + auto fnt = fontmgr::get("Arial", 16); + m_accept_btn->font_ref = fnt; + m_cancel_btn->font_ref = fnt; + + auto_resize = false; + back_color = { 0.15f, 0.15f, 0.15f, 0.8f }; + + m_highlight_box->back_color = { .5f, .5f, .8f, 0.2f }; + m_highlight_box->pulse_effect_enabled = true; + m_scroll_indicator_top->fore_color.a = 0.f; + m_scroll_indicator_bottom->fore_color.a = 0.f; + } + + void update_selection() + { + auto current_element = m_items[m_selected_entry * 2].get(); + + //Calculate bounds + auto min_y = current_element->y - y; + auto max_y = current_element->y + current_element->h + pack_padding + 2 - y; + + if (min_y < scroll_offset_value) + { + scroll_offset_value = min_y; + } + else if (max_y > (h + scroll_offset_value)) + { + scroll_offset_value = max_y - h - 2; + } + + if ((scroll_offset_value + h + 2) >= m_elements_height) + m_scroll_indicator_bottom->fore_color.a = 0.f; + else + m_scroll_indicator_bottom->fore_color.a = 0.5f; + + if (scroll_offset_value == 0) + m_scroll_indicator_top->fore_color.a = 0.f; + else + m_scroll_indicator_top->fore_color.a = 0.5f; + + m_highlight_box->set_pos(current_element->x, current_element->y); + m_highlight_box->h = current_element->h + pack_padding; + m_highlight_box->y -= scroll_offset_value; + + m_highlight_box->refresh(); + m_scroll_indicator_top->refresh(); + m_scroll_indicator_bottom->refresh(); + refresh(); + } + + void select_next() + { + if (m_selected_entry < (m_elements_count - 1)) + { + m_selected_entry++; + update_selection(); + } + } + + void select_previous() + { + if (m_selected_entry > 0) + { + m_selected_entry--; + update_selection(); + } + } + + void add_entry(std::unique_ptr& entry) + { + //Add entry view + add_element(entry); + m_elements_count++; + + //Add separator + auto separator = std::make_unique(); + separator->back_color = fore_color; + separator->w = w; + separator->h = 2; + add_element(separator); + + if (m_selected_entry < 0) + m_selected_entry = 0; + + m_elements_height = advance_pos; + update_selection(); + } + + int get_selected_index() + { + return m_selected_entry; + } + + std::string get_selected_item() + { + if (m_selected_entry < 0) + return{}; + + return m_items[m_selected_entry]->text; + } + + void translate(s16 _x, s16 _y) override + { + layout_container::translate(_x, _y); + m_scroll_indicator_top->translate(_x, _y); + m_scroll_indicator_bottom->translate(_x, _y); + m_accept_btn->translate(_x, _y); + m_cancel_btn->translate(_x, _y); + } + + compiled_resource& get_compiled() + { + if (!is_compiled) + { + auto compiled = vertical_layout::get_compiled(); + compiled.add(m_highlight_box->get_compiled()); + compiled.add(m_scroll_indicator_top->get_compiled()); + compiled.add(m_scroll_indicator_bottom->get_compiled()); + compiled.add(m_accept_btn->get_compiled()); + compiled.add(m_cancel_btn->get_compiled()); + + compiled_resources = compiled; + } + + return compiled_resources; + } + }; + } +} diff --git a/rpcs3/Emu/RSX/overlays.cpp b/rpcs3/Emu/RSX/overlays.cpp new file mode 100644 index 0000000000..d95af226db --- /dev/null +++ b/rpcs3/Emu/RSX/overlays.cpp @@ -0,0 +1,31 @@ +#include "stdafx.h" +#include "overlays.h" +#include "GSRender.h" + +namespace rsx +{ + namespace overlays + { + //Singleton instance declaration + fontmgr* fontmgr::m_instance = nullptr; + + void user_interface::close() + { + //Force unload + exit = true; + if (auto rsxthr = fxm::get()) + rsxthr->shell_close_dialog(); + + if (on_close) + on_close(return_code); + } + + void user_interface::refresh() + { + if (auto rsxthr = fxm::get()) + { + rsxthr->native_ui_flip_request.store(true); + } + } + } +} diff --git a/rpcs3/Emu/RSX/overlays.h b/rpcs3/Emu/RSX/overlays.h new file mode 100644 index 0000000000..234ec613b4 --- /dev/null +++ b/rpcs3/Emu/RSX/overlays.h @@ -0,0 +1,762 @@ +#pragma once +#include "overlay_controls.h" + +#include "../../Utilities/Thread.h" +#include "../Io/PadHandler.h" +#include "Emu/Memory/vm.h" +#include "Emu/IdManager.h" +#include "pad_thread.h" + +#include "Emu/Cell/ErrorCodes.h" +#include "Emu/Cell/Modules/cellSaveData.h" +#include "Emu/Cell/Modules/cellMsgDialog.h" +#include "Emu/Cell/Modules/sceNpTrophy.h" + +#include + +extern u64 get_system_time(); + +// Definition of user interface implementations +namespace rsx +{ + namespace overlays + { + struct user_interface + { + //Move this somewhere to avoid duplication + enum selection_code + { + new_save = -1, + canceled = -2, + error = -3 + }; + + enum pad_button : u8 + { + dpad_up = 0, + dpad_down, + dpad_left, + dpad_right, + triangle, + circle, + square, + cross + }; + + u16 virtual_width = 1280; + u16 virtual_height = 720; + + u64 input_timestamp = 0; + bool exit = false; + + s32 return_code = CELL_OK; + std::function on_close; + + virtual compiled_resource get_compiled() = 0; + + void close(); + void refresh(); + + virtual void update(){} + + virtual void on_button_pressed(pad_button button_press) + { + close(); + }; + + s32 run_input_loop() + { + const auto handler = fxm::get(); + if (!handler) + { + LOG_ERROR(RSX, "Pad handler expected but none initialized!"); + return selection_code::error; + } + + const PadInfo& rinfo = handler->GetInfo(); + if (rinfo.max_connect == 0 || !rinfo.now_connect) + return selection_code::error; + + std::array button_state; + button_state.fill(true); + + while (!exit) + { + if (Emu.IsStopped()) + return selection_code::canceled; + + if (Emu.IsPaused()) + { + std::this_thread::sleep_for(10ms); + continue; + } + + for (const auto &pad : handler->GetPads()) + { + for (auto &button : pad->m_buttons) + { + u8 button_id = 255; + if (button.m_offset == CELL_PAD_BTN_OFFSET_DIGITAL1) + { + switch (button.m_outKeyCode) + { + case CELL_PAD_CTRL_LEFT: + button_id = pad_button::dpad_left; + break; + case CELL_PAD_CTRL_RIGHT: + button_id = pad_button::dpad_right; + break; + case CELL_PAD_CTRL_DOWN: + button_id = pad_button::dpad_down; + break; + case CELL_PAD_CTRL_UP: + button_id = pad_button::dpad_up; + break; + } + } + else if (button.m_offset == CELL_PAD_BTN_OFFSET_DIGITAL2) + { + switch (button.m_outKeyCode) + { + case CELL_PAD_CTRL_TRIANGLE: + button_id = pad_button::triangle; + break; + case CELL_PAD_CTRL_CIRCLE: + button_id = pad_button::circle; + break; + case CELL_PAD_CTRL_SQUARE: + button_id = pad_button::square; + break; + case CELL_PAD_CTRL_CROSS: + button_id = pad_button::cross; + break; + } + } + + if (button_id < 255) + { + if (button.m_pressed != button_state[button_id]) + if (button.m_pressed) on_button_pressed(static_cast(button_id)); + + button_state[button_id] = button.m_pressed; + } + + if (button.m_flush) + { + button.m_pressed = false; + button.m_flush = false; + button.m_value = 0; + } + + if (exit) + return 0; + } + } + + refresh(); + } + + //Unreachable + return 0; + } + }; + + struct fps_display : user_interface + { + label m_display; + + fps_display() + { + m_display.w = 150; + m_display.h = 30; + m_display.set_font("Arial", 16); + m_display.set_pos(1100, 20); + } + + void update(std::string current_fps) + { + m_display.text = current_fps; + m_display.refresh(); + } + + compiled_resource get_compiled() override + { + return m_display.get_compiled(); + } + }; + + struct save_dialog : public user_interface + { + private: + struct save_dialog_entry : horizontal_layout + { + private: + std::unique_ptr icon_data; + + public: + save_dialog_entry(const char* text1, const char* text2, u8 resource_id, const std::vector& icon_buf) + { + std::unique_ptr image = std::make_unique(); + image->set_size(160, 110); + image->set_padding(36.f, 36.f, 11.f, 11.f); //Square image, 88x88 + + if (resource_id != image_resource_id::raw_image) + { + static_cast(image.get())->set_image_resource(resource_id); + } + else if (icon_buf.size()) + { + image->set_padding(0.f, 0.f, 11.f, 11.f); //Half sized icon, 320x176->160x88 + icon_data = std::make_unique(icon_buf); + static_cast(image.get())->set_raw_image(icon_data.get()); + } + else + { + //Fallback + static_cast(image.get())->set_image_resource(resource_config::standard_image_resource::save); + } + + std::unique_ptr text_stack = std::make_unique(); + std::unique_ptr padding = std::make_unique(); + std::unique_ptr header_text = std::make_unique