From 252a2412086d4becec08fce4296f384847f05a7e Mon Sep 17 00:00:00 2001 From: Dynamitos Date: Tue, 13 Aug 2024 22:44:04 +0200 Subject: [PATCH] Trying to add water --- allcull.png | Bin 64003 -> 0 bytes allnocull.png | Bin 70656 -> 0 bytes benchmark.py | 4 - plots.py | 81 --- res/shaders/WaterCommon.slang | 53 ++ res/shaders/WaterCompute.slang | 329 ++++++++++++ res/shaders/WaterMesh.slang | 69 +++ res/shaders/WaterPass.slang | 103 ++++ res/shaders/WaterTask.slang | 15 + res/shaders/lib/Common.slang | 2 + res/shaders/lib/Scene.slang | 2 - src/Benchmark/PlayView.cpp | 57 ++- src/Editor/main.cpp | 8 +- src/Engine/Asset/FontAsset.cpp | 2 +- src/Engine/Asset/TextureAsset.cpp | 2 +- src/Engine/Component/CMakeLists.txt | 6 +- src/Engine/Component/WaterTile.h | 15 + src/Engine/Graphics/Enums.h | 2 +- src/Engine/Graphics/Graphics.h | 1 + src/Engine/Graphics/Initializer.h | 3 +- src/Engine/Graphics/RenderPass/BasePass.cpp | 97 ++-- src/Engine/Graphics/RenderPass/BasePass.h | 5 +- src/Engine/Graphics/RenderPass/CMakeLists.txt | 4 +- .../Graphics/RenderPass/CachedDepthPass.cpp | 10 +- .../Graphics/RenderPass/DepthCullingPass.cpp | 10 +- .../Graphics/RenderPass/DepthCullingPass.h | 2 +- .../Graphics/RenderPass/LightCullingPass.cpp | 8 +- .../Graphics/RenderPass/LightCullingPass.h | 2 +- .../Graphics/RenderPass/VisibilityPass.cpp | 9 +- .../Graphics/RenderPass/VisibilityPass.h | 2 +- .../Graphics/RenderPass/WaterRenderer.cpp | 474 ++++++++++++++++++ .../Graphics/RenderPass/WaterRenderer.h | 136 +++++ src/Engine/Graphics/StaticMeshVertexData.cpp | 14 +- src/Engine/Graphics/Vulkan/Buffer.cpp | 4 +- src/Engine/Graphics/Vulkan/Command.cpp | 2 +- src/Engine/Graphics/Vulkan/Descriptor.cpp | 34 +- src/Engine/Graphics/Vulkan/Graphics.cpp | 8 + src/Engine/Graphics/Vulkan/Graphics.h | 1 + src/Engine/Graphics/Vulkan/Query.cpp | 1 - src/Engine/Graphics/Vulkan/Texture.cpp | 20 +- src/Engine/Graphics/Vulkan/Window.cpp | 9 +- src/Engine/Graphics/slang-compile.cpp | 3 +- test.py | 18 + 43 files changed, 1408 insertions(+), 219 deletions(-) delete mode 100644 allcull.png delete mode 100644 allnocull.png delete mode 100644 benchmark.py delete mode 100644 plots.py create mode 100644 res/shaders/WaterCommon.slang create mode 100644 res/shaders/WaterCompute.slang create mode 100644 src/Engine/Component/WaterTile.h create mode 100644 src/Engine/Graphics/RenderPass/WaterRenderer.cpp create mode 100644 src/Engine/Graphics/RenderPass/WaterRenderer.h create mode 100644 test.py diff --git a/allcull.png b/allcull.png deleted file mode 100644 index 355b82ad1352169a212c40e59f9f65eabe2ab19c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 64003 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

aDf&oF3kQswKZ1ZNV7^r+`d5!${ZD${^~ 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 diff --git a/allnocull.png b/allnocull.png deleted file mode 100644 index c71b18279554e4646b5059c5ef93598125084c75..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 70656 zcmeEuXE>LC`1WmOXNHg+Ws{w~Qz3*Tn`D#hz4s_W*%FdfS!K(}2uT#ON4D(!Tz9|! z@jP#yH_!X$II5qo-`DrP@6YG@T-SM>=Xr_LR97J;pd~;Mg!qQ4q85T+1|SH=B;Gmr zj_|-qFB|d4CdHQM0^23c68GBosi+Q5b?t3?08vfp6C=+dc$4wZ-ga?0EOcFf2FtO!G z;ZKM+GgiWX-)&<0|6l$8HNa!}zpg=orx#{JjCw)q>S#3?1H<_+Q%R{y|H8>G!1CVg z!a|-wrA>k=<{-85ITM^3-y;!n3JNJH;(u?UpKOn%D>SPASWqxU)``4DrlzKlZO810 z2t1O$uz%5azq7kr!OV>1Oo|~Q0Ye|7rrc3c5&L2Q8l_X_*e-{ekeC>lo6Dl1 z5xT*zmQTL9xtY*CmzhZ*N2(q|Bpbw}xacbJDnDP2GdUN1qVBm_3<6pG=b>u(E#sfu z6Q4g1`}kz}+*=b~xqVg(gdCe$K3_|&V31irfJ`m_+u$Ixq^hbaUQkew)9NqbLLE+Q zTwHH2HEcOCe6Ms?xWn01X2gq;G^9~J%kJOfL?FTFb@3RPu3p8z`J8)mTL>>`gQ63G z=i#FfFvF$^6F92%Ie1r4pv(TreZ5M|ajI`^{L|)y|JhV1wb915D?Yrx5HExYuQA7c z{kML-SEcp+WX`)i?L|7AWr@On`g5=Z1qJta@G01t`1mxoB{=WCnHAo;jMpN=M3POt zHBZ7Z;Qd9WlSj+jixYc_;tD0f=eD*;zgJpU;bFs^XWC7tKSxGHM#|aPa3Us~({0w@ zUg66tC?uw)Mx+?~UBtn`IX?LD_Fiyd;T5}|?{IdOhr_jsK40sT42HFS``{DRjT<*2 z;^Nx(H)oWBe&*-rdz|bHCnh8W^!ICiupg!R_goKa4S8QUs;Wl(T!wYGVm1jg-8fjt z%5$7((Bw?E9w}!&PcIhGox+!QzuEzhm{=+N{3WNkt_uS@ey0ZyKiK^wZ*Fcbyjx85 z+jHTPwGi#X2?eYqekZbRCc@AVxRrJ=;YU)@2P z#%do6*p2Yh%lOd~(+V{`?vo0qHufPKe0{rhsx`d%9XA}8j`)f94@ts^>B3;wCRcvG z-#^~!tFY>&?a7jvDB5DdYdOjxEl+Wo?2_^m50?xG!M&<$n<%*K9&dQGlCE=Qu4>|S zmEF&v@^XH^lK~aRw%4~`BFK~FA8kkLjeN8+&eMv1;t+9L^S!Mj73P~iA7*Wi4%rAl z=QED`RcVWbWO~kD^nqRK?CO%&*Qe3a((14Gc0b-3Hl`7CVk$Lmj2kM}n>pNBF4Qfd zMJD0<9lNWe6<#|wbw_KT+|Dk&6>ynVl{{K`j|7NL_)tLN=zul#Bv}Z-r{IVG<#&92 zy)L*&yTELJV+sj?(`LFhR+}nlN$}h6$iARAnh9?`oJf{$?q%@s{v2uT_uy;HcoR9^ zdu#d)KH`lGw6tA()iy(2cE?%%r;#{>RFlUCTaZSAmGk6o+`x~Dit-?{%h&os=Z}!m z(M7*{#g>?J5x%(YzJ?&`84@@py3t-cOOd5_4Gf~XlDV!ggwk67`cPzj9_fE|gJPiS z=~MFAxjAMQ7EBKH^d<-oMFyD?UEAFkaNhhOsK4-A-6rn*VPR7-_6<@QkHJKOMm4^192_}NC&9p zZEaIVAYrC7pIXWrS`t}RvIqZ^TMw`_jJEAf_#01cRap;U8kA7EuZ_tqe9hqUk$(8? zo#jo#8mHzriML1gpU5-`Z9d4q43$6qc80{0Y~A9`%?`#H_)C2uTqgw9$Fwtn znMTBphKk1^1pZD!>KUnh%<)3RK1$m6P|S2UPQdmD_x9>2wXn_iCOGm~k%~bS9O{$X ze+E-U9Wy)Tl}JO5ayU)+*vb^Szi6s4$Obt{^u7>oooHd1-nP#3caY6dT9W8C`3d?S)W4J+&#hmc|(g)Xlwg*%S z@4bJmvVdO5RR7@;H#Uu@r)NY=jG}`BFAEDxy4&x!b>ts>-8mYiKWYnF?cR3&b%3AC zt)`}y0116|)}o7$b@4=Y|8nW!>F(uDti{mO`QyY$^?wNI8Z%Zu;lSc&Dn`P0{}mo5 z1`EWC*@nmSe_mcfU$Py^|9d+Z`t~neRMmcY68}}mbo`3}MiE78jA{P=K$jSWAW~1N z9UM>pue{0qdrkJkHJKZ&13^sbxc}bYtO6_b@AZE_->kCB_z$yvg+j&mxd&k*OklEO zzjlqHtl^N;X6Ox0sC?k(&x(?#`!ldZ1=eFhID|~eYMaftjAA~S5dYlw-hEyf`T71~ zp6Awl?vpUn9zR+k>!$cWKT7BtALM8$Gdp(Vr~`$ zeN=nQ#cSu_RgKFWf#30_%BxqeO!~9sJfMQne`JR%30q`lBBpV$O0cReB1V1(mi$D) z3gPFOg@lqOeizck(t?-uLCI#50_K@RA9yh4L@DU04bp7q|%v38c2nJ6@ih zo*a!d_+B^ioV$=X&`x&YLK~m|&X69Bqz6CLa(<5u6CZeaKBMCj5u-Hyz`y_{0Xtqx zQ37s1p#UEyGhxC8(-O<%xJ_p5?)_?v+FCNJNGMdnmiO)$WnBLI_wPn4iOghEAQlpk zqY$3^c;T!5N7t(@EiHD^Bpoju8zL`Wvx=yKat-SeQ3VH)z|_<<*B&0fE$RYSmt>TPwSeW-0X#He-I)4b)JvEzr9C6C+hHd zaUgHip?<%e&e7y^1X-?q_j8_BnX_ZoU%HKczQ=#N`Eu>a$jC$CMmWlF&h0(1DWdx!Yagx zmT#bW@%wijpTj=|G%|j5_fCz})z3SO)daSNlbG+VjkB<>!tN|WjKbyP&k@rmc{8}r zW0V4_AYqVf8q|4kZ zGnV=M`7;xg9f*S#WB-%6?O`iED+ZZ&XLv2_aLz+vwR7RrrAm4V(uz97WJ-CPL4rpk zs^MUst~m1pJUs34kCI@665cIIh(mX42iPz2OPp3wps zh>V_|2r1MnV_5o85)D7j#KwjV;J5jOuq{BkWS5u^Hrh7>SOKu z>v5aQ8da)IDp)VCuO4IHp}Ktevg*y73N9{!XZM&fsl8-KPa}#O8h!E0HV|H;IvVr# znC8wz*2JezgD!CC$`s*>WS}U{bm@B$K!vO5&4&_F=N1;?6=-C+ z4d1Me@VrpoVek4cZy)2u(*Pd3T4`g|JN}+oZe(fSXr^N zt~9vAB7}TyZ#PIb?d<9KAeMQveBLib7?8m+AkxK&Mwyn@R^~)i%}SfQD0GGFMOAhm zK+m^tT7FV|e0_0xHrI7QmLr_^>>^9-_r82@@b zCk6&lWX?q#P-1Atb@#t0W^!8_iv-AdU-Gr@(QfW0Qf}UH-p>bOVS0L6(HxSl)5fHn z->o}$$PnL#9MVWd%nbF1n;!{6F(K*YGU}Q9CQSXelr5eflDMP@`|TGYfWPwqBwG)@ zCVtL+FQ}zOK89YL9N`DJ&c?=O(j0_?1OOnTmS-N4!`CB_wDeqjkK!_a1%VWG+1x9v))f!|lz^iNMyL zNmi)lC_BS3ec>EhDV+D#9`w8rCQM69v;JKtUgbFPmD<^`(eJ@*(K0l2Kgx{bp=eB& zRR5NTK$*~-Gw+BK9^LEWDhn{BiJ=h) zV(gy7CXBbFIp;db20q$-yNE6l*4N`#bNU5m8lw)a4TSD+_d=3O< z9ClZJ9@mD*K=q&Oa9#dEOw?-98b+M!$$Re|Hguw25B}wAw1$5EKI4AnMEdD#hq+-^ z`Iw@YE>}+o@#R&4ia?`udfLqIJ~R{PYSRkaBzfAc|E^C23IR<5e`mj>N_mS&?@wYO zUNh^p3z@CZ-)@eW$4|jYX@V@bIizQdmI7$9C^CxgEv+4;j zC~^B%%z+nFPF!{cllgN}a#Gp@&8RSEvJp46iGf$fTQ+NHlyducXo7;6Dfis6BDyGu zji6tAFT8{G<;xdKhGNYt9wq%LMqc=5d;VwP#(oDlXfp{Zy#m_RW9S?`rXxA_&27FH z20}xdcERzBgBA4=TeN_X6GOX#DI4(il}@?pF^F%xIA4;irxM0 z$`8sEnSPv*PWdMOnDGvX9fD%LvU%brND-#9orxp$-qpUtB>2q$_7G&kckj~Da2cQA zX^5C$AJ9CvvZOCPp6_Xfb3#T-YqKW~kxq}UzoM;D#4mCs&f5pUc)6IAwowCazW1Sp zoqUX&L!|Y~cdS20LBRf(kl`mcVx-)0LfUSmyx9sX!Sde9SS>~4+39s7-<`Wi6BN!n zfN2J5A}KhgyZIWi5k)1XsrlZQ#|_#A>df5SJ$6Q1x^GGCy$|M6DqR0)aYJ>u_VDY6 zv# zo4v|KVH--=Ph4bwdr-q(;`RTu!Lu&nP>Ii_^>{?Tym6?gwIA868PLURxy@jt+ z>)o&;L~$gXdNeQH!6^^#Bjz0a`z{YR(L52=d!6=E(VA)QnIqC8qDke zMaWB`f`qq}XTFA72^}k(PAFL-mndKZhpg!U8KX~)2G`B=fY+<2#)dg7DV@)j)u42lrp^q&Sx7PT ztSX7BG0YPKwx?T@x8N3hUT&|*`_<~??d{$^m||dO8w`;*NE_2QsC0zo~%P7si%obpLpUf9`%yFz3xPO=Dz*>rIixg^ZittlFYg^ zE(1PYTLI@8=MR~C9Pj?VT^1t`;gdU;O|rE<u|ZnI4bo1;*i$5Mi>)!sMs9hYrY~z~ljR@$qqDO<>nQJ( z%2Bba6^Rl>>$H0p#d&d%`e6d$(xY%AxoeEC?$L#!LQ8sh^sAh)U%9dAsdLsn{#u*Y z|F$uT+;L0Co+?H^T9u6`u4l2i%=i<>c`U-%hNDnUz*GnK`^*NX=p0_M%GEq0V^xMC z2a%AS%>?Kz7eKgvrOgFb33Fy0EiG~x_5JrwQ}T$cm)A#YZzy&H#d>`7*WG&TvhuTp zZGV_R;{iufTwL60>z0~Y%hSs@6-NP8g<)WwV-gXeLb*`L2+(*3M^o?6y6&dO10aOt zL3O5VnZZFy_WGL>gUnYfePKi66Rw`4(I=;IbPrWXOM#ANEO;tc$$-2*^-%jzmaSul ztTlL0bM3vFie*%P`c7)9dV6i04!U_&4UG=R#*;AMgv_D%E{<073DM3xH2az^{;$%Z zi7xl?37}AT*#tCBTu>0EM`n(o1SLZ*Hzu zPfP<#x*<#-{LDbwvNcu5$l`8#K!TTPfstMQod)xC)s_9%(3_qza%wXJn^@7X$om$rXt? zD(U}mD+~D5K~@D6@5r=Syx~iHd@`w7u`VD7VxTWG1)NZaVa>u?)* z33StZ%vKX=(S^$xTL0`~9WM3X#-qvqc6Gs~KN~|mQ>bdsYpYjOz`UJkAa%m;h!{Y% z$aN784qV_Sl%THt`tZnn_zBc_hpLjo&JoY)Z}~TmkMZ{2xh`Dx6K4^_Yi4I?O4WRf zRi3Sbm8^_g8zWeVHxWi8D>E7U{cwMBkI-s#E2@p5NnTFfq#}xPHL@wl@?N6oW~}J% zrU)`tMRRg;a>eCK60QNjZJ3%eqm3;zjVvM}KT4O^e!j;Akto+5pe;pj^ahcjg2oQ- z&Km@wl+zkGE$nAfrICI3m#>TOPM%?3+}?O&4V{s7fYyuFR!G7v+kgr)5xpc%-nUcFjqSo^LuX9QZ*_vV&@GbPnV z^CaW2zns5fw$Y+OGAW!);-Qn_LP*E=;44zL`9((A6hu4*-kPbcb-l7?=0P& zouB_3#%6l`;PPRis$#jcjD$Yv590GrWjRerSyIw22FQov1Z~^}3JtWx9miNFDg>_8T$E23tqdX z)WMmo_Iq%i7i6DJfQn{K0T?K#U?te1Rxp)Y@P=cWdS(iofd*Z zkG^4xI*c`;Oc&r!U`5X7ZoSUWp8~v`YY+X_T#819#ASlp*NB4Q9Mo=CdeNmfJ+5g+ zD8m9;DG%+LUA7Em$bWDgW1x#l+I*Xc>-?7%jK)zpY>ul%Hdun^rk)_5WGWkte!cV#N&lj|RrUmVG5 zN)nNQ$T|kmz{IsshNcLU6A{aVgJCw{GyMY~JY_v*9FNx2U-7MAAs(8qMEA{FW;=N` zBU@-LQ)03oC!jeUXz{tYn;IH?kNvf@p7yp~eE+bo9mJpBD);3dy-VWU4;o>$_JRHG zuG4jLDj!@7tWVIIKGWVI3d(Yc~S?c%rSoFg(s9vO`Xr5=1 zBgG(EXnV%5%$IcqyT=n}Zho{AOLZR$@Cd#$S@W?=j#~G^*7WRhY6$OxSYRBClx!0!Bs%glpZWZ_qZ@KPXY-O}{Ev za}G8zf~ly2wKG_Li*vH3(?mLJ&QLkin?nVFlv1i21_pO`(<1f*?ObA|CkK~ldq%zD zCK%ZA%d}edvfWDjN=BLR?TP0o_N5hP9Vn2#!qr$(I_dukcg6&A-oRk!&<)MK>r zi+TQKqX}!?oas35r4u;`cwREfBq&G@C*S&eAsNsho$JQXGv(&yDlQYXZpZ+m4II`5 z5Er;VT2(uYD=r8;5DonglUOZOZ*O(7#%JGn#;#fPxxS&=e)L?eVH_}>&yV(4LX?<- zI86wZm|99b$FR0H>BMs%&m|}DGEI2{*iv!&(v$M;?}5Eb+P?8vp#gT|E#h8$e^a2_ zK-t7~_+9s?$`NEG>+z4mz!oTuf+7}qM(lZh+-t9Ge!_hQ7(!H_`@Zy!F$Ug%S_EQZ zhyZI5LBr#*`}3jmR7)s9V_YB>?%##4ot?5ryQ>0LJv0MV(5bM>$S?v167e^L+JK@h z04pf7bsrCcsi2OSAFVwRmY!tBl~cXD*o;>t}o6T7Ch1 z1&Rhdh;+sA5vTX|WI@MN+NI-ME_1GUGJ%%&3U5|29GHe~Rttjf0_Bat62K~91&`2g zQwdT^`A>n0ii#SSO-bGi8A7E5^J;mhIZ*a61MW*Xg1w>xI`Oz455fDwl-?WoCrZ_ z-<)j58rV-UVhEo&JtpkiUWo|#OuIOeBc34 z2wFhC4+JcEeV65u1#pBY(}p&(O3_p~a>3^u>Kn?;iwMM|S!fLHwNYW{tY&wA8|IpePJJ_kFW^ubmb=yFuZxV{Rj`yeB|11eZwGngQwGM zgH`^v$%Jc{Dk)5O%In1XX8t*M*O2C5(1qFpz@<_{j}fcrq<*xvzX!Zb>ld9T-pcQE>o zl^*C)rrkPMq+>In&)oI8N&dWvC$vdSS6dt1wdmS5UHFx3cvw`=hixz~jxVX;QKVu_ z9(=9jjyqZlbvu6dBGVw!1v(=+f+7{a6=Azf-`#tvY~*i^8yP?oX$3j(ZD#NhIW>8T zCKj{WWnkG?KbGonDmgn>Jm?_JY|}cQ&BM>%kVT#=?-6ez5E_Wj7%Um!7s5YLW}eZd z+i_W$kSnkWcd zJL2Ctg}ruH9_@%dw@gecTCaQnDkjKLlc3e3oN!nAC>#JK+7N`4kuiQ`ME|8R;}pHC z1QibF7QO6!(${xyvJoY@T>X1f=nC6ioR*GkM?aaFN`@U$Hp`8Hrg`0tPD}Dm;+!b? zH%6?-F#~xjGtI%csL%xz^xHVw;R*@|qsFj{6dk$#dmvceh;)m8~2qh73p8@e`Bu9yU$ENd02d)-W+{PHAk+!0|+|C`$)C<*rIqk_U#zS$fX!sa+K<&epU2Q#)cz zM(dBPh-`j+-uld5{Co$$Ww=~vV9`vs;ipJxqpgP%=^HxE*$aZBALB_I z+G25)i!uqfyz+FD%L%fc0ZUjW&pe5Qc6)LP%_3`!Ph%H!m*~2+y~SFnD_$+@F}yZ4 zs%6e9*2xRi?TGp)P1>NLTsnZbK2PlXQ}!H%{{rYUgwVPm;;V1;h8eeJUGZ{TRI%N! zWni^4A$Yy@TNgcRcCzu>*pg>O4I2v_H;2WVJ?~~2;?*R<<>TefnFQp{K1Ozo9rg(B z9!nEgk~YL$_Pv=AIephKr1n*$7OzrBPJm+fyBfX1mi+5Dojk9<Nx`qftN78^Zj*pdeU%X9s4^a0$ZF+~}ye*&k^dfWTRoy(DytZCkrQ_73!zUZP zDc#HzI=c*jIIwWfqslDs6gEHJMuO>Rdf3<>XZZ5gkJ#miXd)DD^_Gs7+!wu-E=Km} z&mR+-*F7j5gdnGf%Y2f%Bj(V|3MQ#t2E!IVbYEj1pAaJD;2Q%z{GLw+QR^fi22AAi z#q;^QzdbR@o?9Mx-&c&-3NnT!#OHz_f|VqEG+45B-+l7M5xYF8dees{1y#VsdxST0 zhW<2Wi$6*MztzRnUzG}Sa{rWF=&DTt5qbRm!h!M$(3ZaNq$)wZfqIp|iKQEmtX;^Y9^4SGIo193v%5*B?rA5;iP_eNH2D z#1Be~s+yX-lN0~1D*I??ACtxX03%ofmk|^kJOFkryV0uKAF?d(p^_OSGJrjD@eEQ< z;5>N0WjFru0`M|QV9dm>4FP zx^v6RN#RD7fnY9wvaE~t7KA=0K3ft<2o}N?uIeTGwOVN@%tS`{*ToN=!oac|4 z2fuwQhjNb!9ZE_{gc&pvKu8X#(3!Jb+SD*fv#l^>#?^i7#G6!Sesz zI)ZKzMYAYw@YtUU2hG{(aQl(U8Jl$5QJ+xTym_^x6mbi#$vvtU-?ZWl@4wdE;HmBk zsy^c;;^F_}{y_6hb7Z~$)bDz4l&2bcyi~R<$-D^03%Pu)d-d$0why8Oj_E80y}NWN z1$nV=onxB1KVpyg9j#WQo`v&_(lJo)bc%I}f%vNNKa(ystPPr+G(mMPwD*RC@Mut+ zn$IW(!pj82CL{pFd=n_UQL| zwmiu7jeR4o!Ir$?E0mwFFLHb_-q8E_7bB@fE7oP(DwB-QYHG)Bn=D8+X!pmvXQxLb zP|X*HML|QPy2Lm&eBE^siM)Yv!`WHjg{Y&^sS<|aM^}8%p`qedKa!j>tPx2eYyNtn zaZ)lM9}Ni_x6pw55CG_=fizmjK#({u|KI_P z^Z^`$aCkhxQP;hVJb(V21+@#$bbnUH&NI-7__@n+LFxu(7qgh;e`(kP!J3rM^lG8;Ht86mUHWnQ~}oem6)&@<$&!arjEr*&=SaPK^E zeLX$1dyhxo#@2f6fFl|7Ui~@`0XS-?0TNYwAzBns6H@6XAb0AvzFwbeU^gx(Pckqj zQ??+$X)ilzHXp9S`7wF;?76wgmsf{;%V2fJL{MuBFjN@*6OG{UFy9#gL*4rmX6AVk z%^#>;1?(tr@bA2)<_CXHXlQ8vA6J%mbLjk#k>r=EG-P1`8TdetG(;{twv#4mwS*`O-*!6-H5wL!Tpt8WE zN&NB1lE92)fF&{Y(iwEHgy8CK0hp|}Yg_$iscN(5TAwU$%FM~$L^ZGy8z8xo#44M! zue)q7-a)(+o|ZFy#aiqkFbS|Q!JBBCvWS3`#&YEf-t8Ac7}+U0C|XC$g!isp=iKvF z?$_Wul7)=M#dY(86c-mE7&oZmBcr3a9!u_Bhi`7b%1BObTUd4)4HfqX8wF&0y-)3hFggSgc)%>qUp+{zcD$mSWRhQFktF9P1}Sf&y?oECE}@3aiGsZp1yqU zJbeV%M@^3RH&K;>fPkRf_s9)7>@s@z6beVY^dTPTeF_lcs0A9li(_@36ma4dfSN{~ zs37;}=#?4KfGUc6QI&P*yc8+exY68_|e-rR5R5gm&FWud(fV>X`N(?H4JCBan< z)=7I_qfP3nx?lLn#@aJ4^GwUDS)m&|u9zf~Q&`>tPrAzAmN({%Do_vG9+k?*N{G712bp9LY6-Rq!rX( zf8Y!rKt=G}Y=Si<7u*M6uag9{9}YkFZ6!VU9I^aASU-m4A}1I4vjOeB{*-l7Pe#k8 zU%@0srq0Ei`9*%bCC(DGA)${O84+Q^XWY>9j=LL9Ny6$-F?IJlZm4)hRUn(XW!OS- zd@G-0GW_;lE0~6!`+~P~1O@^`u3dX(dGGnj#Mx;Fct(>M8$rg`Wq($|=(o@8y*4Jk z3u=#5a8S?)DD|7>RJ?|r%1TNV;8VQj{de|8m2sn=9hiFV8yfb+r}3L&zD!7HIt6lI z!C*-d8pmSS7{c{|wd;IwEJ;ctzq4Yu7@cn^5u6EzP^Vs!@R+r!NY@1^Qe=oG_ zz@|P_%DUjlvFd=~J>8Ba@+mt+%6SIk>{;Udp3IuffT1PV^BH?Tf>};HU82$@%_o|v zCrF-qoE**k`>3pykF(#m=BbKDUN|nTzii`-)$Y4pbKd{x)D1hFF;xHg+}7gh@s`Hv zBXQvLffk3Afex|8dyf}&{PpzQ5_0@aKU(d$5G@8SdS11#wf|Q;PqYw$446)USU+C( zKU093fVQFl$0SPGRLH_$vNX& zJ9^N%5~v9$^~vv3N@NJ&^0TQ%nN|MC{hXMmY0|ehHCvM8vOpUkr*)28IV()_%@0iV zuL9ebg$yofS)H6mJgS~N0W^M%q%YzMVfbSt1-x01%gmt2q)Pir@WrG4+HAmu>Mxn5 zjA}xxzG1TY;AhCl~8a-MNs`DC08- zqXl-&Qz|(q6#;67)hN2Y1$upCYpgt&Ndvd>;wY_L+h?86RmT z6-~{_kIg56d)3BG<-0d5s% zj`8oiAO#N6ZFK(FQ&&N7UqU2Cw^XRICp2Gun;1;Lgn4e8K^DPS$FS3tiZVW(v3tG~ z7@{}1k=GBl8xSd>a)kmkTXMb_aO^c*RZt*H>T}Ad>Bl9;;&k=O09}A7!Gbq(L2&6D zCSp^v*_xVof4Vu1v}$sLW9oAxg_z4M9@M1uo&!<_c1iuPV{+o$)z-?f;n}i)<({1v2asxsnu?O*!H1=Y%u-9W|;6 z?6H;F_Eo+Ya83s5F+d~7e`jA^M zK3+h{`bNdrV{+#uQffDjI*M=^e6$R;$6bnsVIp({!Agk{AZy9SEZ$!0-Oda569TW^ zvE4jMyX9JXA17IU?uyVCiVv;*MKqlP{}_swVQJ`}ui9~JE#dUy`D z$l2}0;@k+grJfVsY%?e5_W0F(Rm*h7f4E4Geabb2rJq$f25IF#)Lg63*gXyUNWX*x`14xL3^Ma%h+DD*T)lE<=8fE^r-P@<0LpdC zjEE6%?R0vki==+@F_d^h z2`&eUSh{OgG54yO^R@nL58u#utUH^tfjLUtdN%Kh_9A^xOh{0KmHlP!S-!M}+9F71nV!LKmzI9e|HBTZME{+oEi zK!8E#f3l?Gx;AzZwoECR^Kk43LD1_UKLv-$c zZ<0~GmR)za)LUAn!+Bs!_WR}>zc?~;()-hip-GktGZL&W_43I-cYzK#{@WVuW<;{>GEyvW6Box@aa)Vf3X0TLT$z|J32o{pK*q zFovwNK3i;D%5+Gce7bdXldLnyej!2V374RKg3|%tiL~SKm+sEuIP+d5^4aA}b{o)&G+0YD}+*O>Q;Vy4zf4)~k{o|}+q-nvkfX9P~n=rk%Q(+>0&p@g#jCwT3GS(WZDJf^l zw8<}DqQhxpKmTvvOK8qiAH?|AoVVb;(!xyNYKoO`(^aI`A7i_>qViXhTG$SCkYzjH z=u48QJO1r%b-#-lq>pEEhsj8N7ETDWyOOPoxI#iLmQJ( zx3ci~LyQ_hLrwvs-*|=3feX?E{rv_w^rz5iK9JBg-&@^AXD>jbg3(M21m%9v4i=qQ zL_IACN;??VdtJDHc1-taZ>6ome-E8J@mR=k=YKqZ>-2>}7*R7!X(>I9ioGn(<~S4B ztbJEpXYJ`0myZzk*y8F02F+IO?Hid@y|1^NF3E}|uDVrnnyWV|r|xWB^y_(-ky38A zaQS-)Z5WJKrKe{s?SIBEm@LPz{&keHK|;%+m}<|}y^j^cY!Z8eC=Q)53C5?3OYt6$ zTVIt|cEJA;YZJ`7(>2V*Hik>VCRZtbNz!fg-J}p=tB0>pIMvM|B&3t{@))*}1%yS^ zs)tStf?mV|Lr;Qfo4W-=HWiROek0i1Cq1K27DEhf8?ah+z0}|N@{hkp*@Ib^dS*Be zH$3~L5$ZQFZiyJyL>S%$Gv?i2Mro=~X^?t7$1(Sg{g%|IcYD2SJ{ZqQP4!5bQ`Q!> zqamInu?86`#KN3NW}`1FifQ#;GP|tk-lnz|ZjgZ6`Hnlx&ojZ1PRMSE`BRxj4ON&a zQ{qjjzuG^J?u=liZZ+v=G%5yxi5S=^;)8<&F}D>O=##&mp?MXJXBQV2Zev1OH8lbu zT4FUd)n6^cHH?!CvS0VHwvD80+l}8ZCGNZbVqKi5Ge}#Qkx*Jq`*vw4k4LXQeldYf zqU`F$+RLghK33KhQSg4|C7Fa~iQl@P>1F%x8fUEGGGo#1H0HXj2@W^8rKkRO42~u5 z99BeCHk99Z7B_ty^lyzD{+JC2$wB%``|s|b=lhcJ3|)gT$#U>qA3eGXoi@gzLQ|H{ zUwr7#Huh)Y&0zop1sgD>!7iZ(cWvaoD(nM14mtpaw(dj>k~BAnU&wNhv~JSrZLGbm zMu$cf6=~{cfz&##6a!upD7XikPw{a*Cv8ZwzkiCBHEW@)qsqNp6}KLew2yZhX!|9b zN`K!~TNPa2Y1sZ5jlmx)Os`xS%9cy_OzI@p!f#z5RbwO4e_kp6>Q)WzCrNZB7?mwh z7v~Jv#;lH@5vZ^q6+vAcVDrjPv!a@!`yO4XQTu??l$$m%Wu0=ett?(&^7Vp>7uK_z zU)k}z$*kz{E`hy7CO%@cjPqRDZaa&EA5xO`2&|)PFYV7D@ z@iTY1IPATQK078CW-!_oU7KI7_g~I(j2;%v z2+(3~&3g9H%g(64YH#{}hyC&TIa-d?@Cf{2bN7E$B#{c?C`%n-GxEC94X1mwMk@y0 zHoVMJ67;7+=tW`5R6U(wiqP)T1itU{v}L*c39jjsIju;g<^}ti>oXOW-upWTqdkLo zh{@qu20K>573v)DhP)Lxos!V6S{6h$rc@ucdLs2a*gl84nbuN8E`{M%W?z?!b_A@( zO%d6Gz0;O>zDgB~2>K^)ex(E{ipAM7gyS{N0t;=eQ{tm{(n|cs?C7Gi*)drARvI$Z zJ3g+2-QJg8+eWlr2d!&qzS-+a*86EdRzolAxQQ&ea({jJ!o4Cf(W0z4`~yK)h&$6dR)M|&+)>(*uorVoUXo?%bztUn3Rk@WG~CXW>CDn zL?-I>`f*?#x$t*`OOVY-x2iKM|LC*uGm$Wv5;oE?D90OC*EiPI(-6z{c_U}F9@Le; zf3ZhZd9Z?(5MHrrPy6X~@(FbxJ3ss;k@C*#C$y%$o#Be3xkUKiv4}kVwXwr~=VB<| zi~V)??Wc__-_Ege>;UDV&qJ2?)vdC#b%I4jGMb9+*N%9L(}Bg*v*at)l%E(0lXzt0 zyGgBYjE+kZBuoWZ`qMb_{e=G49jokhCn(cBMo+KA6hF$)!8e!SNV&e&@7lM(bI1xB z1~)l9n^N`R-QFsUrm@plf$S$N217zrPLktqBr-*4!sZ6@Oz$3hs*f?O^{h%poBPW= z3&kjjQ8ouO;F4zhivBq8y65H8_jBA~?y4C1>Al>TdGr$Lf4yWJAgr_x@u0(Vqa_A%QpA2g=V2Qh#&c3?99bEik{c z%#)`+**M71JbtXD7=G?fv?leG5SC`~5ZZFW1T%YN_959N8@={ZE-xvS(is(}1WNm- z-6Fiuz)r?~{2e>3ctk~F7x(%cJ`(q;OU*881c%tlLG*fx=Eh#uP4|H^&!7`NHsp%d zqyG5^z7%^r9M^ibtL&6`QxsSdCxNU&RfSoXT7^qyhPNL@61-K$$xw`bNB~=t*v}X}PAXc4r#yb=>yrIw?Yk#FbM;Yo56jpE$>*mHvi<7M=S5%cS~C zwkh#koR*>=UZ)1~`ycF!Zbm$MvTm&q!NSamUF{O3bo7^4V&%zJs#IG;|tdgKeB(`S#7q}uiO)1u&0k7)~*+gndgLG%V8my zrsox4pC54znJe8wTuLUnzZloj+F~ASdx<)2-NG}5CQi}KO=Nm{J2L~&zbOzjD1QyS zX()_8O4tX5JiO;VN!v(IZ}AD&BrCg-Z#bf!TiNv%CZZFonR+px<<)%7s?7*^f>_J+ zSq&Ckg6P4lg^h#5e5F%(oPtxWl(r_hObMS$$*J5{UMc@AW%b|6PVH99VJ5?yiE3xH z20Sp>76)`8X1}OPirRxc zIo7f6nckS>$GbX{1hEb0HlI5xKJ)%DlfoLbqugGxEN+<@U9R46!+0*8>Fyd_5L*Lp`P+pH~X>K!TeW8Codbkwc=zE3(D=4gO< zN0}`I9T60=mMwkgZ~8NP{`g&(X`z;lhUINkPozUzj=3#>LpzOxPgnshp?Nc$}!+M z?NId(opnbin66MBvf06eB>d)!f7@V6bIw+!vU$6MHrKwSON6o7%=#vBxMXBImB2Yg z;3Hfo3^twC1?+}L3ctH>8;#3783IEdQ3tJ`KdAP2Gk~@Dkxyc+*4 zoF+LKi)(72Y*R{G-|r@5!(+a1s3^JBa}5cA-)Z4D3Z7Ul5!ROFMT0Vek3Wjdh6U>B zI%ZbVzq?SBF+QT$#t^%H9W zcict>3-hFAvR6U5RiolyU=peAM?@sa{yXrn~oX&Xikdk7=*IrMNhJgHjisg>dVsdvcxP9sP{ziRe91i9`OS@?rmPirj{ozX= zL|m0!+}G?&bMV*%+3-}XwwtthC^k-_t9C{%W70twp)MlS@WreU7V*aKrLb+-)f~G&r$*RUM3FkvO+zSU!`}12G^QG4*u(Y`B!Yan&vE>@-6ROLIz%gh>x)9f^olopq&_Uy4!T8>whtJmQhtkUAx|!mIf)2M!J!fZczb| zZUhAB?$~rmNlBLo(%mI34bt5pAl-S^e%~M8cmAAl{x}?tp|YR7p0(Dz=Y3z3N)`Kq z;c?Z_z`qG;TybQ~cDPUlD*aFM&O+o97LSRUR0~q4U5q}0FEyML_bG*%_soeN1@ z@^QlNG$cdb)VhAgrlN!1U`7@`i=YZu)qamk?b=S5Oc3uuk3{wwxFZzF*bzV=@)vjk z;pODm1&|`3Y}(ZYM_p(i@4wZ~I9<_ueD7NGlBE!R+-7bY`5?mI@~jl?g*>yw{fwP% zU;p;J;Wcd!ZuxT)!#jbbamwEuV^qsS)GM4p58eW<16sQM{c?HAg3LkvcqQ?fDM0w~tlbgg(Puqi5SPDPnS!>GNSJ`iED^WDK*xZlJlt#xgK4B?1-LfnJgDMKT z66fZ0ZOyA~&rPG$`d&11X)>us38{4+eL6&OX-k^aCVy!0po9?m;a;qlLuWwt{tl3Q z;KTqRsB!_Y2^=c~w_Yw222!OV1J^6(KO^CXV^MGm&WXVke+j-$_PC?fEHW`Z$QyTR zbB$ow&!?efRIF~yjFVKL;h8qy##I=|qU&={+&Cc#9-7Hk!irCJ_@leBiEgV#fxkko z)KA$EaGsiwsOVhi_RE6H%fH0=Ij=MNHr6=hRsd!{h9X4 zP4FKjx@mrRJ&3(VM1-!)e*hZ)hOQZvb0iu|95nvsXmC3;R+exeoW6U4WUUtv*sa65 z|9AW|`7ky|OpxhRx3n*7Hz$eGyr+b$==n^(Xa&v<#!c=Q>MA*(&l#PH3`G4u!`D#3P(pnS6 z#a%U?G8q}c8aYI&P&rg_nJloY-r#a3`z}TEnnG;(AuQvzH$K^|2=$-~{?tqw2_5@6 z3=RbX)E#(h&TKe@s^0{>t4C)ii2wPA)*{47zHJ5UIkCR*iGNW#S@9f1EcqM9^$dZ&nX-luC#y~_pX?lB;mlfz6-2An!Z;FpL{4@A@d0}v+yDv^ z=t1D9qh*xe|6~jDJVC$(o_O^)A!_IGuP8)ZF*cmTZKXcWz`rksig6+4`PMsg%yb+R z{3jM_PddjXhPaG6?I9kBqvRg$2mXKj;uT1yiTp@>sx}bLJXp^=E|AH*qZe5igQAdM zU@8lK!4&WC0XHNNkd6RS4cw=3#}{nWUdc)l?5DohMvjIyLbH!?H@U>~_Q3S5lz{t7 zR3)z*e(XQQ7xuRc!cV4e6zC^?^MseGQ^p`?Rl`haeYS4pgKQKOh0tRkugQTSfp6w1 z380XYVcLwjdGCR`DhmETIdG$abBUS`TXVsJFJUBMjIPL%+y4q zwjfcO-e|B2+(4@R?*EaLmGc$J|MQ*G(adMkXd1G*kwM_#>5UhnV8+A0vu6nszm)TFDsIzpsNzhkC zS{%rHK+V#Gw1Y(c;Kh49v{j3(3)?TrV(BP`gSUG(zih@x3`)?QkP=NJ)!J1+BWs1c zoc`-xksXS4K}XHMGUl8y*?Qs{anwIacHdE?^Tv)*Y)NWhv}9eYu2G8H1_q3Qwz0`N zR`#Bo%7U5C;}Z3DhMC+6(pToC;t?57#X#CcqtK(;@)eb`b?RYzOYf%2^4C`mFHpRAs`mT4~90|unC5{Fca&<83TsQJt4ej)q z)JEz3$$HC6_}W(WVv%01oFhMmICLvHB)`at$41-!n1pIKTWJcv#Nm>WeF3(d1VIdY$;nUtnB$5hQOT4JdQS!~;-|lAhLa9jpo!5I;Y-Xb?=ruK~780|a3$ z*b??3^sFKWO>Pd0=vm-s0=o}{21vA+?Qq)z5M?ER&rS4Zhqh>)Bp4Lcc1(%!hpBY3 zVGfIPnq2tzc#%{ntY;AP&R@h;C-)tYjW1ro_TC+Bxz>$nm$_Ra;L--!^_~ClS>MV$ zXy>1I)RV&=c0^84tUTVG;s9I{3{XcjhQg@GB=e24iAW^zmv1NvaJAD%5zTpSsu2b@ zL1BDmtvyt7>VTYwf@4_zcGm&w!HjV1bFh&__0NVH{P?_D1ajk;0vrO*VA3+>w{hb@ z=UZs=T_$v-4n+sCYvmliU|576EB^jFJ3$z}>@qY^>MF0&>ypq_X@Jh|(OBaGvUObI zR0-P%i@r2kA%0wwh}FORGc62@a!OqlMFjD=Fm%I9hHm{D2Y)+4P!|)GBw19~GZ);k zp;IA_enp`&)bCEw*|=SsNxPJycDzr@9D5~oDPl|avP`=Az)=tuyc`t-^WLk$UkZ{s zq}aWEFQ z$tg;Qia?6t4TGMRtCY!B9He|50Isfss}slSH;Ahgtmpph1Bh;=5>odaKi-9U+%R!U zOx=bzVTg`QKoMgcAYtJvF;Ef;il`XeY4mb@X?GAN7JWXB-&-oy-Y@c*@%^4OsXZOk z1Y&_D2vd2ARn5G*hRW-U8@P3vxq!Qys~O_z*d}gTJ2cgA=R8H;SHvLTso-LaSukq@ zZ9Jt8Ju)EUh9t4+1Osw_kS9Bm5q~sqm|RsCWAQ>#L3QA|e!wgI74jH!+J(MDv%&5q>mlgG!Z9|Y z@)~R+rUEU{0rI4`;&yR8+8`s)Hn;ZUM0xR&CL?IAUT~O||5K%}O&hd3-CsaVLNwP# z!hePz2=-x_{6;Rw-2mc_;vp)bpBtq%dBpYna9AzVrb>}}IqLv|Kc9U{lj|>w8iDC7 z>S11UG;95gH`|t2a^HjHQDf{-^gkfq0kM__01iP8+I+s24Vzp@b?XZB^5B6Cpp?qS z(Z4GP5^)6=8=cK?=JLo&6oTQ+K$HuhSRf31V%BwNwx>REWgFR0g6Jl3f;Dg<_vJr!MQYb z%RbM6Z&s%2(-2&81`I<@vNqpGAGqKSj1WD*?>Gv4t#C?DzS46RG7ud0X4R?11k8(e zpg#lXMmRA4#m9eJ=K1k9+23N)%YcesnQ4!H=K8+v=r$RoMHGF0zWTA@0t^iaq6KLt z3P0!SL|`nB4gDNVd1ze&RgfAXA`!8h5Ch+}IB$xyNYCFad`Ud$ES-=ELc*{q-q~5h z$sWh}G~vI^zi0huTVOxY0_{*-p2L7KCFt#z@@|$I(f@cjH0dyd1%qf+|gmENAK^wc{hU@>Lex;>+}I(!pm{_ z*6fv`DO3ah^1xoK_Wv@Wx_ZLTe}6PKj!piozlsb26rukzn1W5U;GF@`Y?lGmm37J< za14Wp{qIN6)Sh!9TlKI?@PWWUmzq5-%xqNiFH*-U2Ss~k)Z$vw;gdDVxDhDFZ}-V9L9#9du3hI&x!J8IDL|Xq${&VP_P-QdD?WYJW+xH ziIJ6UG_0M9)pUk&a4SGG0H% zcT_42EnzS6&L97sR>&2Wj?yBaT0nhoy>x_UX`pS*b3gA8v2y;-H4BS!D68k8f7B<> zU)D46WSn}&NmK~Se{jRMURn&>_&m=_0a_5hAupKyJ4Z&X=c?_)O~Q5A<%-lx${n{A zeNq9xc{u&mSgW^ZL~ph2vOW(51k{wjqLmaCWp#Da%-;gPB{qd{U$j=O)icF30Y6~q zT?ZNM<}ZVwkl^xtxI=Ga?`Vlf2L&FJ37T5p(a=t93}^d%biSd=h({=P0wUyq@Fo-5 z=ZsL1g4pK?@t%jUUMTyT*$zQBn2ii}R0giZE{=tjbifG^YkCC9XibQSa{dTUFqvvV zpMPlym80C8p8e=86v{(`fSq;`nR@_rAsM9Z_(UkL z3tDkU7b(OKXp5@W-zD0jK|kQ3y~iW-xx^&pL4&|k_&xwu=(WdIGIfzU`v2gEk>g8r zD8WO9KKCAj-?$KMTh58#96LDQ4ulN0dfz5W z{F)K1W{o-MrVLuZR)+SXPpl9Y6n2s;MAvlZ5#{vRj=00(B6NkS6<`0~x@;?3mkiSS zu3k%isZyL4wM zf?(hpbkxMa#x|6E+78@?oHh#r5w|XfOV+j4K+FbAW!U5?7be_oXb5;Ce0pL`Okr~( z*MbR>=MH$u4G+^;-@T~KAN!yi%{myF76^F@=LSAYb5EqrXdrA!udfu`5_%pFjsIDw zfriT^XZ^bzwX!;=#M6lxBiwg!z{s*`7xoD-+I>!yT+U zutOG5SjRIpowu831%r(ntjJ{vV7U1^=#DELL^4MG#PT9P*Rq9Kz1QyJ?|C+!m`mq= zpwQfgVPY;LsvTrtqum+&8L%q!!U&>}@WJ1zLq4doioS~*#zjb!mEFY>mx|4L(QgRp zKgUBba>aFRcSXQ!?hVXb2U1Zi)NSL&+h>;I{)0bfp5$RhS=$3CNE1rK9ZCQ^4rvDm z<`E#8yfSq_GGh62%hQzu9V*|M{iNQD)`YZ?aFwJgd#%>)5%5zO$I!ziTjMNF?X#0+ z>n-~55)lTuqYTx;o;~rBI+DY(B%LQNF@USkE)%J053gI_YLE~kBuYz*<9}AQi;?|C z0%N}X5qiYU>2KYES_5V2!opKr;+bX#+9{A|bpfjxeu3$w@Wcct3$I}VRaF9j)>|vS z##3i(aeCrPgU)11Mg0Q1BG~di;-dLsJvZ7?L`)!^(m8;aB)&-?98`Y zdEBW2WjuLc-X5%FAwVZ+)RJQw9Tj$K=h%L%0q8P~+cRW{yrdM~Qb{U?*{0{2ekJN( zj9WJkQpN0Mvi3gQ7T0x&*#6?e7%5s#vi;cacE}wY7Yt(aI1qu%WYFbZr1Sj+es$oT z|4saz)9W!ue||y}2_J%DD5!S$gMdP~i(?=s7jL}zS@^>}F@&l_^$DyU5iI~(P+rn& zC&Sg+i$o!g@hDXi)eQhwr+XdoME-V2AtEqfk|zqvssk+_;8amJQIr;3(5enKm?9*I z=Nj(|a5j+A&<=ORB3-j+dtgsVK3wI<7&|G12|h_vi5#{VdH;TARLYkqJqlzQwa-kJ zyp+HAs{o~Qi%guuGt&;AdU01nBGse?`cz(qN;f07gpI07%z#=uNk;B;V0?!WLXVR5 z{1g&NZMe>-So*hYy6JfSU?XA@qm^QDYVrf^9a#ugJ3tVuCTQsY0w*4@z=FCV5E#vk zfqn*0HvtjyTg+`|^j+nPiCWnmiOC{b*V9iB@xMVB%C~lu+u?9wIp@6jI@R>{1?4_=U<$x%<-*>+F3vJi*(23lTT{Q<=SkZ{0W6W zvf=T(;grT__)qz()O$}l4q~Y#Le+P-CwzXfKYU|IQKQ8(&*=bCyC_fr+zwkV;U+o) zLc&!zxA0_h01m~|f01H`g{LGu%&~WJS`zhCw0q0zv868}_H;cu5&PIve^*_n`eFV3 zAqUE=_bT?5tq2SHSW1T{q$CjbELeihbXIbvgXm*w^kStzPr{w!U-mZELFk6L>X2r` z-%>L1T-j6|cC(krUL2aa(+U(I{@!VcSy_YD#mTz+li?hLd;ON7z(Q@Ea~Pspl-+29 zS+}Iqijb#KMsqoBkbZ#0$A*9xd=wDyA~YJ4>($o1nbl#n40stv^_<|8a&8keKz&zM zR)!V&qD3j>;)H`otdt zT2#YIK~RW`hi_6PUs;yDRRpv4qy(3A$PR`$L|h#>GL?udt>_q@p!*Zuglvx&XqeIVbYlq5wk@4f3Ix_w9YWcHqF%I{$ z8`D)&>~?Ocb|4dteLTSaqI;Stx3z^WFiWdb1an6m#l6A8J1g9lY`=L*Dgw(RW7mQQ%0E+xVlDdOvjNeVy=bo0AQ9Z0i}8TtsE+bjn;Q;#22!ValD4s9F{C&?iFN3ODLtbA$MbmX0jnZLjH_BHkk< zqzl33$XeXE%^(W#3KI{fN}MWBNF)R_@n39oopk4s%0jf}m+Y66p{vhH&3=qk`0DxkD;x!5> z>j2H-=(d3Gw1IM79<|GjQ+Xa;{dM@J&+o+1trVyC0`B8(k>l+b5SP;D0U}A2Vg^LA}VX_>LM!BxV4*UC0;_9F^!h^ zJ^68;!7A_$V)Lo*D1mASw1HFu5Cz=k0=GZ_GoH2@#EQegF`}e18$KC8ZaPaa;Lq?~ zIx!6EDlARqQ(E8-C0wX?f*wGv^s4Yd$0;}x+dA6FQu89Z>Zb6Za^wB5*{(=C+_)HEG+i_GU{1=FWa11ap|WZoBu{+tXskQVCmd! zHk!$aE-}9g9fU&Ub8`mBdI|j&GVU6$+$4BpG&UC41v@m_D|>WxgW9u{!lK)?q>NPK z0_F4dGtqR(T3=etRX+g=z{`12ryb=xk}Sd>!)sebWpkpM`G`D;aof)H)@wYMhsOhc z2hcBQW4`uV2_M{6KG!dZVy6!;5Yz47+1=|`%=$#B^F|u@q}3g6+D85)?VyQSp}Hi+ z0{9^a4A@x1Y9INsU)Dw*vd&G>zZUMvbNkFve}}(EZAdLB^Dudu(n&#Wu3oY=i7zlD zW{KA1hc@=f>~x?}c;2DhT4noKlC)nK^qZdfLmQF$$i>?* z-uaO+Dt3T!Yzlw>>O@_Ui=%<&jL$*H218jvyUC!2L0f|-C(H=ZUo4F z4a_TcGPBex3m-3^kTyNd#dW$iFhcgcI2q8L=eI=b&Vpu*gFGO_x|Ay=2ahCRSloOh zOtPK(ry*tl@D_5mD20D0dxyk_J-$$ay^QvhEz09 z)aH2gJr6VF*MgQaSg@g%2Tz1yPzr>tR@O`1{vMEMFCcohme9icF7JJ9#$bD2)bAW? zNm{OZ1w(d>%>r;eTx8w@-`ms^NSL(^@PPg!5orv~TJO0jx=*7Zx8 zf}uG<1}*@2(2m%-Fu2KD|G?kV7iIP+nXr(ObG&l%!&?!UM1}n)$ z86#7O^Lws_wRf(%&HztUh08x}GC>C)m7s}cY-N zZZCn3Ng2HWx{X_ts{nJwMj@`2qQA>jx%_l9ikBB+sy4(jJe!lljePYl2(KB5h&3}K z8!an`qT{9N4*3PJw0{K43Z9TYSZ<33Xu4Hk2X~%YKp?qob1OUBH(e$~SU|i|9oWq| zmr~b`ZU~6NaHR&2ss%5@3|$-29KN9^NJu&D)TXb_2EIl!PMQyy81fLEVCngdo{~_o zb}E7Qd_54Ym@@a!*}~$iaT3CFcR>$2Eo|btNFT3VeC}HvBZwBdapY_aPkubz0bL&5 z1|2GHB==_|&xK)yXO<9HRu}1Jlqta)x{lV{FWvULgM$turYxUisJyVL!bF~y=5<7q zfS5AB?NS#Q6C@)RxGsHm5D5l(6d%~SJ&=)Rh-n6bEhFfCx8R zK**2nHQ&Jt;`s2$O60cm-s`vaHVoTz1hvRjfvO#bo8?nPmkEW=p9p&`_-hE7;>^5A zK9dmpZH2v`Ik7NvDkyn8;l2MF?O+U4wupVPrIndty+H4B63Y+ zmpMo*$uSn-MSAUajpO};Li}fzY1&Sj%rHg~z*`tLfZ-dGPEO zd=~)X@*1FMO(1uLajGzNcnEitP2XNZ+H~C~pEZ2$PCJ7Ek3WgvOgJzt8Th(b1L}H5RqDI6mhc;?y=4Nm@20@g)9w^wGpq=SbxB= zGtXd6ENeaROBkb$ZufRJG{}><^y8lg^+&$Tu^wP1^A{)k1h_|2g9 zqMdiFkxmKA402(qA8C7x3<1s_WnSIK?yhaf+zNCo4P8%cw^obg?0AsCEn zaLb~8p)`@VpXl#7lD;OQzbCTCq4&4nk4+NIo9IW982Tm#RKQ}?KrdMSqf)-3Sx#~P z8LU7dDX#6SY?3WRVFMih_@wBvG=47xNVMZ_>2&sKaHs}IgR+>A-qw?ZEE@rKWuHqW z1-=m_W+#<yYg7QZ9eE%cvFZ+fnzZ@QR1m`?qo8dUp?@dAD0e5tSt8k@i&sozY)v8 zjtv@@a-DAy1&FKlO8VV$w0ObbqhqIm-R6VbG7ktF;jPzIugq9u0daX&K1b2`9_6ko zv#5cR`YZ}g78MdJm%Jqx^;Y)>p#Z4n(dBaecpV2itNGeo_$Xw;69WHGsGtA?LnClL z{CN8dP-X28mqh;SKN-m(yu;}Lv6hn*UYXknr8GbqZpOEbzs9XxjcLtN$TfyLeLr@5 zLB?I5+PmF8$!T|Sg9r-Xglk(0BHi81J#y|uAQHWbQH=-jv7@!=kn8g}e#d?hSti%R zBpL(ceHl|KkTo!NUatJbW@iH$#vZlL<|fCt%dRc34Nc^)adq zD)gIr3$oBKftxdmu3zu2_g}Z$W7$4-P8XMY!y|};APkSs%P6J8U3o$%T?Riw`5RH|vsZgiisFiRf`K>lOLd4;6PqO+=`C3dZFWwrA zp;Wp=Cb!NP6%ygQj_u6)#s=*@ilEnk1DY)>!f(C}hy%60Wgux`U<3#+=~_iV8K24* zpdGk3c1XsmMwCC7*WyIat>_Jc(fy*%v2OH7h}Umr?H5#?D6#x=%|?9&34i4Wgk?K6 z?B#b&F8_9cj7KO=YI8?pIBD@-Y+&WnI~n7ERG!r&>lUV)SE&XEuzXPURGoC;Fe2(Cv3>4Z4PE_<+1v;1+oN}o5uZZiy(>ZYnnKikFndpb1VQ)A zAto2~kJ%c!QYF@j;3Em*>h3UykUkC znI07fdkG%9pV(VpIUycei`m-y{@IPatK>utPhBs0>kpslJhm{y7N}M95h|!js%k z2q)cSOXY_i8r(y1&1CW)jPvWynWqmgyO|A*+SV&^NtH~U`9Hp~e%eYL1d}>P$tp(| zU}VRJWHm6f%BfbrM0?DFCW2P*Jlx{SZ%Y9mzXQ_4H1fC(AiYZq=4?Q_AO9Ayyy4J2 zI(pko`Df$k7t5n_GEz9*v!_9C{rP@-goUd_5&>Q+S+fUDognOc&dx51b2r=Usoys+-_1O&5OVl|u-w{S<9VSmi!RkJiQfj|Ol=QyKOE6c?_UHjScHw612?lYkCqla zvDx)b1g%{r+c@;aLRcq?A)s!u8?;*0dIQ@V7(J;TL@zU=q`5tO2Y3~j>Sk!P6F;r_ z`2L1aUYY7~J&{CgkI?J#&1Z3CAsVj;=B14Q?l6H=)7ryiZe|z&5yTm|bm^E5LU}k4 zR#%YRO;G|mRif1`H;9VhZUWN51snUv^&ziZ^u;&(V!|!D%Lm7aSU8Lep8exeA$9Aa zs7u;t0`>31hcdBbZKzR|;jIL%9d+8t=N5D5o6ud~2&r~J!PSlEJS>O9lmN6HP5}iN zS1={!Kh$;j9LF~tEgOS^_Mj7Zj{h<(2{i4?GwBt0?Pjf$`Zc{ptSTp=Fs@j^@N z4w+n4YhMKb##&3#T|nK#ApwVjK9SUX7|U2m5G*e?mZ(lgk^q6v4HDD>%cEbE&dGc} zNWA(No!r-aVyZ&te$0hAcbGnX7iI+Rhj8wH|UQ@?*X^BleuUb z>gZQY%wFHWmomoxxFF9ps+PR7{CnKB-WNy>0^<-g zeUAX9f3eW$lm5|fkv$`QU;EAa8r<@uB~1!2_(1|qn-TeU5KM5w^+4Q$k?5y9b!hqA z!pxIE@Sa=R&p=E_PylVQ7eKr00elOdcZL%~wuaLqJ~Ht2)9*8w{lRIL|H&iNzeaNK zQ{9Xcq6IcY_}{3pT;!4|Gx;-f!QK|^jFYt)y;g#nDE8)F=f%+gn3l!JPdP~Xs5{|F z=4w#i=?MY!1&PH%+0WVbID4nG4Z?Jug|K&YE++o&QM>UQqiKr~u7rMeYV+pm_?D0koXU(DK0LZi$4d!{BW;@Z@hpp;#RPQ zMUf)gZrNw!q5hc`22rD_&r%Cr0NsA@jK|cT27;8`H~3{V?4(v6LxG<8b2B% z>uAmSJt)>N8l&x;kZ%+F&pb!in(NTY(YY_Yvgnc`L^rs!`995Cq&2Jgx6gS#<~1FS zl+-Q^YRuIyzgv83ljrd(^)L0J})NEGep z1i%7}5h3pX8oY&Ez}*8JNgbel5E~yKcy{K5Nx})?Xg)%K+ZW*@b^yoA4a^zP0> zGf^9KEDYr5q5f(ZfI50K!FuQON7&79HMCuviGOL|lNA^=e?R_Mtq1d_v=)Z(J}B0O zgm*aOtZG;Ne(^=kIQ}K%-xFxjJ03)wAB)>bC3?N7oJ4+ZPNWbRD#~G|E`3D-!{_kP zfQ^4aRJ}GEnhV3vDjc3hXMP?dGQ@;poL=qAXdbhy=T+Z`<5NWSF=X-#FvK$wc-7d+ zXj`J28mfYwaUpPVNQ&QjE)ZbWSHl=G+Ci>stori@FisdwXxNkffQTjfc@{;W0g)Z4 zn1vI)%z{!_Tv=@(yH}jneU5KGZGo_+Xy&1g4da8!D;FpSRxFk#8iKm-lqduMfHX5n=148YCygH~G^Xh$8({cCSLT zzjhR47J)nOQ*FAZJg2uNTCFnGuOL{E3j0Mz-=_S-H3Q7l{k|=$Q0+Z%brj+JQDIPp zIx3M`lwdFP!ugdN#+P*i09HL#n4quL0%#}7j0Et(1I@d#&k8jAwJL(Q4)h=}J_gj3 zeJdYw&Mh2#4+^a_{&C>V?r#W0$M(Rf0(hRGrA|M>IQRPu-Gn@oP%8^E5!Y(=&jeX4YQMjWjz zSFYwot_z1>i|id6qtyLeC0fVfv}K^j^h@n^nIk6yEqw%V;V}Mjv+JMOzf83>7DyP zmOHhhXC@(fV@f|^cFL2+U+fJJkL99$%~8%fx4>D+c;kBf>oCzX(NPw+lQRY$3|Umc zrlLepujjL!B2d)KED_hBTS7E<^LF^zWsTz}z4Gx=&mCBr3bbpO0UQtx5rNkY!18h@ zbSYI&X@n*Kh>8PbI-jpNffds0DErTr2}@FwOLs|F8TDSv<)V-E8@rRIWVdBAhu#%| zHKLii>=Y|YqtTytWwoeR1Oh5FEkcUX490eHuit(lzJ^F)U1ud&#lTk4aHc5ZG}c-Y z?jLXaJ?cnS%_Qns2hCK?OtTG_MUBzw5d>kixFRNKkgSt8#^=*7$5fq>O3?yOAzv?} z?ND8sv;k!1U${H2qWDZDh_suu-{#re%J{X{vA8s$^p8+}s2NQ-kDjt8|o-HDdLj<;j4lJEY+< zX$l*0bY`lb-=6;SLqx^YcbWyBdkIaFyvmP|N_r8P z9y!7%6K-&PAARNgihCd6DkIshi~EfQAsgMWgWQd8Hr!6A=5fIyT==*4mgHWDooF83f$x3fmA1RPVq0Pr=TNk#^}0R0>kf>$B}Wb58}G36 ze$3hP#K*u3&K$soI{qu%KZa;4&0Qil|3)M+>f4S$t5}p0EA>q0zN})6U=mB~8c$EM zV`6C=tT@P0_%`Dfrx{DUF3QoU;`o~EHL|JUevs!k7g-L&Rx~i!tpjVciB&wdyDO`| z>q!B(``6jJ)Rt^a_56U^G(P!-Auh`YuQ3<;ndZsaMI}+>B9$2+en609r=37}v zb#opXNK;UkNh6BySFqufb{kkeU6hGb4~?$?2~QYeOyr!QStKlXg#nz#A;vypW;pQodHswDQZ zmy$1X&L z7w_ZADZ~{Uw~uO2`v!SMXRN_!Nv=I?+@L%+^*) zr|S7ljB+a`rD9C|x{Z5}5)3xz+bq{-exr)%(tE|^4XkwABc}s(Jxb*Q!IS3Fd*FBN z3Hm?#()cNX2e8`h%bX#V=+(ii_vSL5)9(5L8d-lTNB~gci|#$;cx9yi7*N=sx@#T~ z&Y77s;b$9NUdrtGE&~Q^bpakSq+UmsKm7Yoo%gmkwlmJ65lLT;Ckth5pFqnNj9J&p znI-lVgn!HtQyf_An>ZySm=hVYK{7?3=$xHBW!fJn+ufo=?T^p|G^C zLcx&z`_SLOuTc4VM2{rA&{W>1M$y9lyKt1VA#j24ncNWHUs8_WB$e=^LFT*$8f0)< zuTk7LL)DgbjvESodkl|=C3%Xt8nWSV(vDnpH=d0|+?`}`JP<|#2irBxpD27fM&?k~ z{PrvfT>*&w05f$HR)$i7ddPcA>~*gn559*i>0)oWeq^~%A>3W8IEO^N#A)o~BjK5zOM) zRh{%KztA7VDK-9ZXx=^w2Go+Y@0;W9W@X zUK@Bf)rhLr?@QCWR?v4<{qv8)= zbLL%SCSu7)zFso8sEqz6TK6*a$%mEDFg(Vt2@*HgdBKS6aZp!h>v-=iN!b2SC-hbk z-Tui*CpzvBRQPr`J)`=RO=nd%Ah=%88OKI(5pgoo%cp@yg!ro$;df3P2!HxL>af|? z`s@e}73hfG`Sgg~S9SXTi?hM1QIYXHFMU{|iBn)zh)Nq7?CiqomF{MMiS{O}n?KVR zuTeL1DaJimWt3|SBOf`k}c?$DncarU(>2%5S)BJ>%D=xvF4{f2tH-!A< z*D~vMz8tRm0W`cs4=xl{ndUF%9>gTJu?(a>+0AOQkT9xfJ@_&td>H!sL^Be_>OG#M z(7Ua=#p^SHHZRxi*MX2LtW`I;TpVlSpFBjK)2&xESAN@*A$x`nWjDjg`R(^Jxkkl$ zz5T_i7h9y5kyJmC959SMIL1QM|5Rcep9&bqYR0bkeR|xqIx{DUkV|?9I9j2ya?2*S zt)EOUOP0U2Ki-jvRNFIK3rA`1=(#_-AGikV_~gFiitmVJO8xg z_YtY|^_3Pqt?qV|5j~IBI~}-w@GQDj<+qivmcq`>4V<;7)ixgAMaL!9yCVI{joEJc zio>WQc%3NPvD!1drJNN*?=uq*4h5C*g;l(M%@RThj|lndeH)Cmr!t$qgaA_=f$>*X zg`#iK+vM|CQMxQ*XYhU+cv%H0VxC_{%F1kV-Ztre2edf`vrByIc~7m(wd2L?y?Px( z5nb1g7ZaHVADbVgnKrK+M$~i%ncHJ6agJ6Gi3g(||5Z1b@vt?G4jR@@Up*!5=Hh4z zQ~;B!!GfW1XGCHKehgh$cCezbE^j%%wKa((UciNqJy14MGpB+oM0hY`BDs8;&Lvvyu9@_-nwJnSC45 zWKH@C5CoD2e?5)ggz7eTI#i5+ShAiV9Ud{I1)`n=R8qTI{J6rn@813;X`0Yyd#kbc zjwP~Df$rqL`d-Kz_+nSa%^DU@kdvo8pv?^~f3{zSUq6&p=$Z@ym|ptudCaj9I>)1S za)zQ|cYv$b`dHpt4hXjK9%5|(bAv(7c^VDjf;We^ zdvp2!`C$EsPviCZ!Hh+zzj~@hAd0ka_)1_Hu}^J?p|`#*WtL(hIfzj-&o{JQM6S$^ zstnJDuXJNjWZ|}_^4P`m0-9RJ5b0#=*(UA>7H_Dc^6O_b*S%J4b20fF2Np9OQvbQb z4)cyKZ`53#Q(#9X^f#p%8&uZokrIUmZ+G(}zpj7SqE=F<{LHS4=<6!D-)j9)WTg#)nFT9^M49BWo z=VMySdYY3^`rTBlar}MsYzUaGx4i7qFWz!=S@B!Vh^OTw3JCa~L(!QgYhok#K78bn z&%<>4!cw(IelI?Q-{U!ue6iCzlQ|ViDebz!7?QUlWlPe>*SCGz4ii>QyNwMCNxbVL z`pj5%wwb%)@Tt)+wmpsAzH3f*@7C-5xe&Uai@m=^gn;gFm(}vQk$Oh;WTNd=N50dW z7Z^PEBtZ{P%KD`TK92t=R}aUPOyS>7a+Wcz%P(R)&9w5ba#VqTo}u36N2d?O!+qyR z)P{-O{l2AZ10so1#Y6bHN2+fI1k+1lYL@TIH6D?_6KtF%JLFyF){8hII{0!8>m)qj zocMY`k=vAzXRK6HP2;83`c>&56i?koN?UKg$ZEs$zdJSyfQf(qZQJb9#lIMbX z;Wxv-tBM)F0=SFQe)j1&xjp4-3dIhZ2}a)EhWfgm=G*Y9Fpo;?LXQ2xN8D0B$#(rv znpzSW;(Fz1>=?)wRnI53h@aCDe?X>fw#(Vc7w45dr)} zcHVg+-8KqlF){73C0o0ia_#vSw#C0E`U*+oHh7;sLo3{!OQXtLxT8IrjY)L7);VP` zf$aR@S;{-L=ha0M$d`}2o6*d@WNR8NtR4` zh0G6QN!uS#;6ueLqliv?4g9|RM(8y6lUqU&Qd+w#IwYdDj9q&3?eX%R^jYDP zp4Z;O&w@m!L)dtU&mz5cnKo>eFvT-z`|wW3HDjS9@iCC|5}h35PEYMbrJpx9dw^bJ zIUw1U5R3zt|F6$qfyr4%86wnW?ARRWO~D|xj7L3`q9vSJ_1a(tmCoY-@O4&Qadh1l z?#3kqm*50q#`ReP_!)|~TU zl%CN7HWJQDH)b~Pfi{ksM)$u>_q)k7vN9qsZJs#n;AUoUb zM$=aq*<7&XtuFek0!Xm`SSr6)uESB7kpnN!v}`HVqvNhQru6(`fzJezFo!t1*x>R{ zk7#j)w%ha8?e!@3f9AqgNSi+=OWn*iviibS2v=1k$S4Xh5eg|G0)NpvU@2=Lt{|%& z;da-m?*OGfhLrZRNYJPgIMm1gfhx{Oc!V8mUi(nv%3_;SQE0FPy?m-(58JzdDd@O) z1jbWBC{c817sO3{qR+QflO9Jum1yArEUJUnA$dn5V~?kwoFVm`{B^#(!uZELLn>*D zdinvX*+1BHAOeV%DxVguCrDfTi6lQbiLQ8nin!-q)@OkSGNSwZb+aNICbb_5&; zf8}+BR1=D>gfUlMh4pEDte^M`n};ZWD3tXT>;>I9xZ^VKCFgH$wdHOk{4j&dF7Lw= zs#d-A3eaM#_;a_{sGhAZc9l66pQ7P_3jb)JEz(2<;8sTt{V48Ps|gk*R&Oj)eQ#xa<>A?ozA{%V;Gk z1Y|Ar^Y*g52a7Jaa}mh6>HWPTD+hrf`RfCJ&Tswmbob8<<~@(azM4)^HDe^9+J<&A zt#l90LckzmJB4a^7$&IhL0xmuTBbi*ix0MuIJ>w*`vL0!D;YL2IBl!QyzcYPX$x2| zYsB68D3f(S>#4&K?+3>lNDzZTWEOYz8YIUFw+r$jFZ&4-F!wT~qew4Z=jVJxj728#i+X4$S%WxB=EjrBvqEy| z_+KcI8jtxVW(6McZa-?^YMC=&wf_O6B9?ON5#RU&ZtV3?fbp4N7S-j!6bsHfx$jvk zajL|OEtNtvJpAkU%JA;(MtY(LM?}_snuikzRgd3F%4Z#)?(fpd;v_XfYSRaXJlTV+{a1QZi;ptn&Oq?{L+`tDP&bpGOh=ux0&bdnp@!COjzRS9IBWwB<>q*LUAVhK z*KDmXw>Qku_5h;r8XuigZ}i72h+lSls?d_ehTjV_zC$3;SDEDQCSh3AWJ88=9e?=B zK<-2l7t`CLK&x^mw;7(fuCzG<9b{p56q;V@`#{#?eV~kKb^Z{Xj0$zrToj=~2l!a3 z67DU30@lbAI<%I@Y=n%pXJmlbbpf#3bA8*DRtS{fZ?cX%WVfq#O&(KI{WELNhi-u3EHx^d~hktoNy#gLIK=McrM+*8oVMDDo@z^=hd50#MrmdHwK+if0qY)$rvT_Q|b}62FWYLIc3riuDN5pZvuNVPJ6|2Zjl?FP_sB z8tlWgv>QolDyKN(si|_`9Ji~r&J6pjn7+USrcpGV@nI7fCVca<&0C1kxG6Iiwj@Ed z3+$Wt)?1yzN5ns1hOG1zyHAx^P_4Xotk_$WD_@v(k3@7+@fzOnY?HRA#W+p^nEY-u zB4+3syZ`{53=P)V=WvKoaS3-dLAzT zuz=D644wCUlLp)6tx2+LHyL3-1|9;iEJ-;z@c?X0H}C_|X8`#NAPe6?g@Eq$Jx334 z?{D1~78V)GtU&&BHPCuWb>nb>^FIwrD6dsh#>cY8Umms`|X|AlcX3~U-BuE%&h{roy!<{R#+r(1IAvb;&icf_> zY0ozY0-{ty^!2%mxcq#Ip@DIj5MUm%(CqENLeq9g1baZ>tABgGL^L2&FlR!7&zZOO zX835h4R5@Lt&etXG;pn=xuHgC1|P-rvGPaM$&J^9OnhCixH|Rv%0E5F+!r=kP#;}t zNqRJ5VyvBpOsZ8EbyR)Dz;O{4Z4{ONp5^dG&k*zuDF@OjdY0`wBi|KH0LJ<~V>p8+ zsTT=A#0+e72fstf-g&ivd_V&5w>Uo4DSiR?;p+o6rXy7V?3$3*11%>f=N$+6ldSRZ ztzzt;8fd-crmeNWpOj(7`6==X5IRO|yW@@6X~{;5Y>{Z6J8caiL$F2{($APIc` z&O9%;+=_es0Mm#KlwpmBo5kW4Pq`@+^J578Yt5JUn_BPw*J%B|vo*jb_&(ws_Qdo) z{iV|`K>$He#lQVbPh$Xv1p_W(LueunA7FcWgo2wqE{y3oxRG0ol^q z{|DcD|Fu%}O7Tj)c7;+kM&8T6nD4xv>P1rLEyWWAFtU`!Qv^bg*TJr!X&h{Njwvf?`k_TXl>4LElwFT6JS@(vks4>k`!9a*5l)js#)fE3~XQ z4`cg`5g{Cy>%Rt__ason^oicmx=i;MP%5HB&R(1*y!V4q!62nz^>JN}VIZXc=iJ2D zAxh|I>!0AQ&%u}1O>z%L8rEe5vv5EVH&P1LGL=g|Zm(9dKNR%_5-KS@%ULhCib(xo z{|!Ene6Bc%#v}Ctq2=-?5i2|s#@OWMNUqUVT_ewPy{iVCj0)H&UaIDPT;PgdxAoJxsXwb zgzv6)?d-Gmr8M59G;+P|l^t~cEPuDw=Bms( zGQUk>KOM>IJp=$zJu9I1BfE*SbYHo_l6=j;qkRlBH%3MC=e4u$ailRG9PZw2{|hz2 zJN|S<4{d|qL3k2_8b2V_X8ZMXO|&W5b#{rgivGf<;iUeO{pa;ec+9fS^`mfJeb};b zy1&5s?r>b*KDl=Yk-OTFq!|4wZi=x7n7o0)&CpNc?}tK_6v4i2)h-}K8z3zI7l|Fl z4@7=AzP)(<8Za?4(<01DNf|odZ_p$#9tLp2mq6ymHUP@0Mxz(f!3>osokhXiZ|PD{U*LZ$i?sZ z&T8md=Z}=;%+N(Jwo&}a#RnJYD#U#FvGTlN(0{(ij3({ROjdr4YX#SR5x(g2D31e= z{;#m7(CvYXfGw&OzK-r#p)e;#ipVY&ho-3qx@dfIoo9!=r(1Hw4^(ku<9iU!M)Sob zo0hjM3-uTbxaUfQ_QE7?_;$)oUT6r^;mG6vqx1YU@$8R)Dsm120mM0BK)LtDpbG@N z=KV)aKq>?PF^_{teRokdDg$Hz1rBRnbj<(t<~N#?biMyW%=-`zq{|7C?(qDRX!wmz z%n3~om`}dPF{%{TSYg)%5mb%GoYUUe>_VES51lW?HOg^HjvIC%7Xt`RpcK-PW>tVx z(&qdCJQ7tGhXpa7gU|4w7N}S6-;7f-k)nL$ehWoxC;k>5Z|P&6-!X*PpU0B*`uRjU zXw^~VJ>a=rnat713K8#Yhy!F!UA0%VFLt5e(__&w))`88qtOI!aqq6~-;d6T{ecl1&lIyK!)XXBX8y z)X|6OuaJ-90z%w*6v^NX(W40hoSF#%KwHmvhWJE;T;Vh1Zv5+as}kETcUD{PbB6Z& zkN-qr#r_6ipDcJV7HKFbqL(dP)c=AN>rBqoP^gB23Stuy^a_J@!*yHKE8KmKO&K*Y zZ>T}xymv*Rpr#&5sZH4#Qq~0Ume6n?E?Or7z|bzQU?Bgf?ACsmO zlye^iq#}-Mp8M}iAO7c)uRGGiiJA;JJv}`xS=>1|u!5i=(rJ??gU_fBgETyN^K8)Un_G3PMvHI%fn;R*BZqn_{(({vgs=^F6G zoj3Arh0n2hI-;Q`*ZmW_L4sLhYeFUM0v4Xp^2Rl30I7N~UrF-L<^WGro&i3 zxr%qnD$x?!xi^+}4MexbZ<=n=%3o{P&#)l;`Ai}8xyhWcn@@Fr*H~FSFJbD*bnJ3w zHcosgO7;f_99(jEc=e{%arj9)2}kZyo!(}$s{DNr5XKEF)}Azi zj8;M$w-fgo0s=UAw%{s{u+)de72XfZ-)}J*By<+VWxAMQ`xIKNafN*8D{ytS7g$x2 zab;R1jqL3?^>w-rrSi}~0}-D@#Kqwzc&|SIVmhF{4%YX7RYl$ctjklD`vC255FlQ7 z0)SjkK%8R;@IiaiL7rj;j4P)`3+Xk-UTI= zis5`zZ_2Zvr;TqJ>^FQu*?A4upd?}yUx37}(z&DO&63fR?WO|=*y?;8F2O3nb*H&2BnUw>|E{*R<=Y> z90n=nLiz)J&;D5Sx!AVTJr1oDv)8$nqTr{1iWY1LGak_T(!Lqmu%bC1kFY}ohD?NO zX5`8>QOF?d(pJjQMV#2WFb=Mlca>lNy8hMlirHe+jtLHSC216}wA~t2PlWzarPXmJ zA8LOVQjWY)ylsF~)Yh5ZZNUbW-IZ2DwQ-c}Qp#j!sNl#sl-ilLRi3hYH|DYK-+leZ zd|c>bNhgP*RGdZa32HjviD;UMU+QaR$lwTEZB=eF{l%odkrI@b9dV&1ZhgW~77nM- z8BK?#X7jDWzGiGbh zu(Zhm=Qtp1a@1_bdO_!~Fa0wNfxd$qR9I!)KL}Gr&p5sgzVF(v@ zDO1U{vJ9Li?xNep(lO0q3wA(Jt6oo%qF;jPk+bzr;`x-WkGIL+mlHH@DW>f@#moc! z2M(t{nFs25eC@9zKZ%Rod42iS(%yswWuS?YJyQ5DGb56bGBiLgV^|hdS)bF1_wc8Q zI!x6vjL!0#Kb=wv1opP?%+5+Ichr4tc8U9JN$PUWHlgLHW&Q_T)duqfva#tSWVrVq z>vwDrV~)mBL!YL`f3;N#vNF3*M6{HYoYK+`K(HMI?{o>>&2bhplIM!AQ{C8we5%g( z=r*E;J+p)c3W5Nx`V!YM)@WOi-0Zznyn9gku_dg;m_)%bH8Q@oksKD!lGPRzo;#!c z1m@z@!1bN?yQXrxzkuB8Ha~Ue>H-hl#p*1jceL=w4s>Rik_3t#*G1R)dOAEf?f&fQ#<02f&<|<#ZnrdDb)}#B|oB)?m8FIQ_JVJfn9f&L`uoV+pB0p zfEzhqPgWW;Q`oY9nAxkf+soMX#sVDP6HEvj#@G8#e^YQjdQzd`S6V$5>=g+R`?oFC ztf`)aullou+kA<;upsIA@jQ=|cs4{ek6o^2j>l);Rqn4kmZ{jdDt`5RKD`qOfr@Fg zKo3wOKK_-yeH;kAtYG^5$Ayo+fz78L^Kh@OiWdVUA^MJ?>&6M};N=mOf3ZO`ooPmu&)I*j z6m-Cfaa=G%+59i<(_Jb*~v#M+7PC^{KgF8&=* z9;O0jsnY&1#oP+@o209Q_o*B9}JMCGXn;ts-iXs9u$apbsi?4ej)JXv@D>3niS5<#vey@%K%uEN*0oJ_&Sx7A%$- zd9oS_e^wVi|KZ5RHH}WSF^wV>Wd35oTR`1=EPLu5Y_6Y#+5A+McjWS!7 zVu%?PoR=)(5?KE4DbZC7Sk3TOU32wQ$20kT@l6QpBexW2__E>PX!Dx~1BEtHjcK#f zD(uBfONI*q9h;<&HJ2Kx_cFa>R?P0;WGaX^vevxH6vw?|nXI_fSduFZ3hsb_o1b^u-wlt@Zf-A|K>WbaKysqtI%FDO}intoK;uZ*Gnz2vQ& zrlUDWml2UP6PrL_H7GM`u;wX`W;A48%X>+^GtaMJZze^miHf3=1+TBz;Y5m^dZxNi z6|{&G> z^opr27N;9ACdZA{Vi~+TzndFAqEDL2cAfczF9rYI7;aTZ^`sr?)y(Bj-w8l)WuRQd zTPb*nW>$bep4<+c>iq_`c?|9-GjGwSRc%|QKUR44h_c=C8Im`B8&^wLMKiFnf>N{j zNZ4HRV+v23hl%!w12Q^!Y2_jJTYvO|oTx&^K=tb%ZjQ4XW_asw=3Gq_DKw zD1-k}KN};Ib{=8od*5M9zG0DX%EdzGRsM0AXVPMPLYGPV1~`w`UA}_)A7DOLwEQz~ z)s83obh=ST1`mtjXvMvEiKbPEd}$Y*0$tP{rK@V7IBt^TnjeYxg`fbgs%Rl}u49cZRI8&0PG*zxhpxoDn6pU^8 ztNN4oRjh^N2M7kWc(2XvR#7e0Xb11PGnfM5E-cN?Z_VGkZi!&F28YPDPaCWA3#_!r zhVsE5xGFp8e!o2g_!W|n1eEKSGK)-j!am=$W1eZN-^M?CI$B!%(|gs%d;=+E>E?}y zxKk<2TJf=$bJ+3Zz$O3Q3VyL(%aD^-DVZ+zvzh%`tcHEM+Z5i&VIGT;HsU{(8W+tc zOEPxG$O&A5M&lS7Q6$BP7Dz`S7kxSzcY3o$r3Yy5{1Uh+kI%S{ufp-rB-Z*6$dfE= z;~hg^y?_@X{9yeT0etbDj7EAEPOFE{wv(@TtqULhOQRdr<1QJ+_mTz3xL@w?o_foL zsAANhvd31gZ=R~nQrc<0+{gheYo%)BmtP}r7&Dtry4*sH)~;`7Aq zDuZqX)t1?8gifr!swQR?LE4DDk`*b6!PHVc2#Z0#A|(L3^r?z11fR^ewD3A~BH2HC zb0VnPi%P`50y#3YX6dFbd2aKBIWn zw>Z=B$kLciXa09%Ah~VJ(Z6_hcigBRR>Y-We8)*@o19M0FT`_br`q*WUSwLc;hl@H zZje9gR3h<0nlRK!Xlp&{1RfWSoJv_fEj@y)-G`xQpb1gpRg`;+ZZ;vJ`zkDpGyymDWKp}DLnbN-?W zhq|j2syTC~6md)OKKhFCeUaDU%(ak*DgP$Md^;Z!Duh~u&D-Yr6jDM&!dv|WJvka2 zDT1cbrl-*((Lmvs%b&HGL%;`h5jd5%$Ya5B_d%ag$#}qT%VRe~4lEIc4>J0X0aI5? z55`mTO4)Gz-_ob=lv;ar`}0S0M9pXES>6l&mwj3WX@q6|f#3Q?gX^#GH$)pk?2I>S z8WCF^ILC0#KOaJHrX6QdT^>3ITy>p0A(^Y~&lY~&LDK|epv+>?#vkUq^`95t%AfWC z?VDvtPc{M9py&$UD&nW#qJsexP|FgHubEx2ppbQi^d}lJra=D`73>TbV_tl+|1FA8p zF}RIjrrcK%1xLv~cxe+c8Rpr}>%&qeZy zzbi$*EQ}M*jYSL?$h@M^;Fh)W`FWWHbZUG zO+*E2tmC1rhaX$90&a5IcB9yZypod01kb z3kjVxOGQWn`K^#{H%_rqO2WzFcsr8qvbBr8UA9ir`0OSnEnOwKvF&G$9#xVOA!XzJ zqR!+6H?(P-;&V>!la;{ceEt?7{Wol>=Jvv{yhrj3Rls6ISkmU@edQDXcEBFE#!CQp z9+*3JkV_01dUZJt^)FPe5hxTNpnHME|c}kHw5hG&Eg&z6Iiq1fQC+<#zw68h5OE6!p@Ps#_yk9 zyZOUq{q0#AZF01eWswM9)3Q0~$Cx>PQjSuVMP(NAgS(wetc|eHnxZuV3MRLy>e*4%i-dz z^q-tJl9laVo_v`oExAn#d%$k~T+9lUL94%h_tMS+d34O<9S>H;!?Zggxa4sS%}SVi zPukz4C~rn6k_I)~{Wk2tk+>0oizOf~GBdS;lIOa5-*Y%vHQ~AXi{j0Mz#slQ&ai&H zO6zFfrpC7zd}4`h3P*gmy%w8-4|?9V#NkAxD>mrjI+FsTyf*?gNm zdJbjFx)uP1HTdpN5tRG`$CeX)4m`T$$(olm8R zu*cJ!z#AwxzAiTOMp#x1y93#JCR%~HRVNSFRrso*LrdiqT{`>4(t?&7Uk~j@?iFKF zFH)&KzaZ}U6*Xau%;@_`4kJ&=Z>9Vk+SOCU3)V(O%oTW4LWBpscvJm^lHgv`H+E4C zUVObm%sa^D;fz($c~smVGi}iClR~DJdb4iyA2jZNtDiXC;C%;$s8ZVw1bXP&gJBRImY?TWEK}7seZDD^BM`-}l32PWhxEABSpxM*uGeEDjl`{U zTaAxMpPM?lGiwxE#(jJh{>!hIsNJqGMMCp+rXEy+{c5>x?>BBp=D+Eoo5oH_(ziE7 z*9`QNEkR?9S|BEYHm%$n2V$=EGw5s&XSfs+``y3GX$VBG=s9@;J^yy`vwYJjOD^mq z>O6>B-by@X;38>|77npdj>dJ}UuNg)zG;^XtoEktqyJ0HrGtTDgJNCtlhAhCDbi{P zu+s(WgD+Ir0_++Q^V~s_E%PIj?W%*AmWYbydCF~8Z)b1$;@hzwDh8^K?T_AgM!Kmw zxsXJ{*hM}LURoxP$@Ku`n6=D66mjHwBFV!|Ijt$QEbH5|zUy~f{qG|r9q^=26|FA5 zHT&av2U+2aBtbuf5CY8U!G=d;o0lT0>*o8YR*L7An^v-e|Db3rnh<)6j(IFbQnGCK zfx`M$2pwBN8Iq%%ZxiTEO9~pYmaF&(Zh2A0DvuwSH-A z{x3Vn%Q>A)$<>@?fK$jZz~)NUS4GBRRvteP)u5yFN#ufyn6ZYvZ`(q@#;>5k=06)m z@!NBaZ*eWv7_Q5W6<7&i+(SV74-J<@!+P99XyF)-7rMV>dBqY z@{#>{%{G!pknzvAjYbZtG?uKsEIGZrO!S+$-{ymQg`Yk1^O{d|F>ubP---(@uKFYJ zBlGECvUvU$G8UPmLM_$jZC7Lf8v%=~z?(+n>jer&%+roCs6K8|Ux5eMhHR)={#_Sv zGU5_1=HVhSdoM%$?A<~SWjAiWUCfr>vd+}NxLuUf#oYyIN&RK~P|#lYQCobLIKUK| z;Rq@}0(9V)H^)Zk=Btu(2Xcu8mcayKUdg*g@1a*I?2ociWZ5*F86`2Fhmc;1?=2H_ zd(xD&q!%vsGEpkN;16yu!7FAIqFg%Ot7&P29^5I;p|dRZCCiSy8ZW7lMze z)k4O+!f(cp-{qvIR1&o6D&R0b1xq@cpJU~0c#FDl#Cz*s*eENuVnO&kv@F%rZ5ikvLy!|P=X(Q`qM8xoxv<1nPftk+JX=)A=#_j}Wv z$MMSXTND2Ap#ZqcFn2dFIlF$(j66IpCh7xnRtXnVORkjYDkNkeq??fbDD!et5ozB0rThEOZiTqxDbR3N4X(>`Q@y*KHEkRN`j;hTXHM56 zn+&rW9h7XI2_trxt`pBp8f*J&_woCuFyHun__zxX+$-EDbpZiB$tkM|chnPij)J{l zMEr;BIWxBwpZ=}Vvr#==d>O1N_pYdB%c6!88iiF*+`2ceaaroayk_oG;is(`PcB`y z0ND45SK^uCvF7swC^HCTF`@!%q+BKuv)4qQp&&eeL{haco4GrK|C?LZ4w!}j05o63 z(Pzt?BqhpiU0+RkiKwG#>x5qxtSo8YzS_+zY0t7d8IUHaF{E&OaeG*U zMo2C31yvD{&xp?13+57tx&PdKO=};`G)2vApqHM76*O)eU^{-ZdmDa~qJj=BRrAER z>Eo2MnZ89l@ch+aBmFV(yDBGX!xn~Dp(yzmaO-qZfXw!BPfF$6+)j)FMXikUajLmU zQ!2uX3p%J>`BxKStIDj=ToP(_F3rMM74Rh9@xKb(^neqQBPUrH!E@etcFB*7t$Jk? zivhVbsi@gTw+|0y9nVaf&ihu!cQ&dZww-g)IKgmR?N`UF&p%=eQhm99JxIk5pOQ;q z@?~s2+WklHEf*p5uQi|XVb*IG5dH_bWAMsZSL8VkrK}NBCHO~ zo%CzTwuN?Gjs6-aD4R=y<`%>B^@VrXtsn6@=Zn+x%$FO`_(lyJIIPH4P&v&lX=%3y zjKia;RYvkC(K#(X_b)`5-m%BA^(C%Rv!jS%f^>OQW}>we{6M1KANXfz4z2)J_LpVAHy?jLw?G?C?<2{b{9} zcd3F{?1&JciF{`<5~m(#&^Wh*hM$Z0*kEj3?P9TcsJ!wZ+tslBM(;lu__Q82rzb)p zXx@R6KLd`_ioyuUpYY^{d2C5k`MIG%GpZ20U-CvlE{pWsIo3ow@ZnU{@roFhWm($CQ(qX0ob$SbY$^P|AHuw+cS42OQqBYtg@>Zz3Dd4{*@BxXwfM7J@g7J^Y zW=1*f1|rpZQK#&+{YipGv&qa%JyY9aqw92L-=*4wvjx??-^7Lm9<`N^3_uL;m%gLW z;Q#1#p!Da!w~)cT8SF?(guh;s@@HJUP}zhSK_-gOKTif!uUm}NL!ZKjA?!G9%3U7h zDYNThn&ov^3U97j#NoX|myJ4zY1lCl6~=ZYp=|1TYYLF{*RKnuDIUuQx2ssNVI%Mg zm&Q_yv-JeeOnBl$YV~jRHZF((=wm)l-^4$Vb;GzFe!qrz?&-|n*l~uyDJbwr#?c{+ zo}%wp)u?-8UB<5zIb2As&zL%WgTuVpK%vLeh@DM{P6{#W9fjew zWpI5Fx!CILAC!NHC=IC1yK_M%Zl%H15Qie+9|;v(r1F|uBE*$p-m<5d2a z7+J6613F>3G$%(3h%)oXRrzIY5Is!3`RoN z$x_A$n@kecr{P3reU3Nn_`PJ_M z`c%bPX3(2=sk@^kv_h*!uJw+#)Zv_OKOk4K#S^v^hl}9Bq$P-FQ?Ou!>;zfW{V7e~ zQCge*mFdsPDH0_di97yqwKnM!4__=|ZCsBaL8eqV|I%Bb>g1rQ+^;E`s6?N=#|dBG zPepUgmPY84WI7IF1CdUK~r0e)GQi54L?2|Urii`cQLL$C8|Y5 zePA|R)6bY&>M750iR{kQ?YZRmW<}Y&;>9(@fa&TB549u~#PB$_uBIH-xr@Hh!(^4i z=xr^xvtbaORhWGKJAc|DJjr{77@vMT$Ol3&d?}IGOJ3sY<9@2o@x|RWwAcD3qeV}> z-1rC~;)a3#%-52nBG5xpJBm}K$93yGTE;Mw-vyK8mYTgvJv%CIq3MrV| zbBVL5yd6d@3Yac9VT6F7r9O-!Z92CPTsLV>&$1`#+x+YGkCp_=Ghug{4S&)wsn) z^0XcCwL15LvC7p)Hl)nH)-0Z&UyZ;V(%kivSRTr~*w{_uAM?wHcy!n7hJ3h0xnK=Z z(LJ`BV48YeH99|y%GbnWxRn0QPpLdRoPl%?f$rq3Ag1PoBm6c$P*U)pqXwU?oO)|- zl#5BS+Mrl9k7G-20Jb)nQ&UNfP5ptyM;)(EdjR;0SV5QM^D?%es58raWPNT0;=#<% z75fg#^ZhKuqj_8|n*U<1CkD{)wI0WVw1A^PP)p&2?c}xD!?EncwrBlVIZAr!w85~Rt_E!aJ zxM-8MWfk(Hy1IxKL^a+|*=OYIsCM-DFHxk`pr@s2>GiMDLRI&*h_TJFauRN{+6UATgrg4K_+ffTt z%Za2s&Gw1?ipFg)U2%LJyEntVDdxMtf#s)nHnmx0(dv=W1l{Fw1!o3fhC^pd*{s7I z!feaNCuPyd34Q%0@&H*!M{e+B!b$0BqDTpUmT7SJ4rw0U3;4vza;nk;G7~DGaD2R` z(m~a#Cxfjd7U)0ttB;VrH zn}0cqPt_aDApP?kn$LWbAF4$!jBqU>n=JYf!Cz_ubrR9vPWrwXN)1Uan??^RdkALU zck}Qe-73>5IhOuuIS<7V#|w&K=f6G4Kx}JFs1`TqF0TiN(ASlw(D*^^{zDFp+iQaf z74m@9=96KOX;2Hv+Dx_Ehzw}-Ma4gZN{%b+y>w=s^#8~ffN5|4`L?ef%U(e0vVuYS z&J1Xyh5Zw0PAJ$h^&poX4dQ;O%@OJz-Ug(>rIwI~MBd!{c z#d|}G71mqmZ`>1Gh!oJ@0`DjuWbZTd28icRxAY?~aZXV;-x8e5Na$ZubX6XLhTOSXc*o z8nE@hCP7F?$vgZG%gh&xlJd)=LLIMXZ^#%&Ox^aHM%4U+Ljd9_wj=c4s(`T_7uL|p zm#>SpGAyDRRm7ux34|rr`lWQ5`se+$kx$zvO-RA40K z*_0QRk)NsK8%ke*=^6Ug%FU{Zl1;{qZ*0SG_W?o)SMTIF8-a?|@}v8Jg~~yxq7z+UR%%0h&j{2tfb%09JNL*QLupOg(kGF0uKo zAe6lDXnIr9OhL!L;2*RmOkHF?nIe|hK7pnejXFw8%b)xLFdNm8%+n_$IzO_a|AMfe zq%m7`axByMMqE`TT1xO_nQGg+$w5hb=rehk2|Fc+b;EJHPZ|9Wd$4>N2qf%C&eu3Q zK3pU+%MFD7mMwPAnfxU`1h!g^C4J5`{ln1Gp@czLGXi7qJzp17&4!uZLV5;JMGQnU zK1h{1fi>Kj{# zGyVow|MOXFnD4~-H$`d}WoHF99@v3bx{VGdiahF5$({OL0T)WCo^&kXf3?t@m*Zw9 z6(yKIB$1tB5Yv&uMHC_KbQ=mY|Gea2&2zX5Lka%TYLw4={gt124&k=xU_w+<#76@r zY9CYvopt$BYTeM?^#dxxC4H5e?D%K_ajZe5Ufu6fI8xKm7uosX46Cp|tIWhrnamjqiDpp7A7c~@t(CSv`Tc~huysj0_TYxF+xUs}s9StX8a1ur|! z42wqHZV$}g$5aDbYhJ9h`Y;59NW(?Q?1v1I<_j%*BBs|wIKswxRV`F?x>@YV7X5^= zqarUIHMAQ3^rb@{`8s)K5kr|!3@wa8xA{z8V18?)s_Qw)G~l^v^^|N3Hpbm;abuO* z3q~Ya^%o!|$fcRr2(cd2=lqyN>oQj2zg#mjf=r{xj5ws{yQPP^MJR$3PR7Km zc@PiKLdi>EL{tRJ#lPU%E{P78TeV+o`TGFMoEq7XsJH)KNY3(2lBr1yW{4V}`7)Q8 zClxL79LkS4w5zW0fP-#)>jTT9%lTV^0MA~9#o@8>g9b!vI1V>6N`Gf}j3Zs1j7V-Qm8Q0z^6 zD-^Fx8bGdgHTrCKy^mC5Hum}0IU1tlnMeb)oQv)ELXUav3!RD(%{wGGhf`~RT`11_ z%S5N$6v+p=asVjc<9^9IL##0M77Y8G%OA{}T(kP49vj+S@h@DDulGaRbqL36>%f^M z--twTD5G+6uM}JsSNX@61T?;d0W>PZPAWTc7Yl3aeB25i9>VX`CeEuw0!Q4xl=(nf zmjqc4U&AT4pvf%HXj{K7@atrg06O!(Mdgi)TaJgi$5JrC!o5JknXb@DFlx;G^~B#5 zSKKIGdZ%o#$tPM;gg!Is?1`^`+G{ei&oZsK?)QUf)WV`Ka3{I6aJkAId$1Lz5MXGA zGH@r6`wmG9e$lg!JL}yKTAGe_8_FIThTKL|jJQ0wBfsmFxin!vBZyAo9eT!l^kwYXQ$N??zTU3Wk@RXmILALLKl-^kqTWzkwYX8po&F-1 ziS%QGvHScK#lymJZJ){7s1nRYQLE`2letig44SgEHwoUOvHLg$4;7;4z#rEaRNIA# zU^H@4PS2x;(+E1Sg=#QQOwjhKWT+7%0{VHvfC6QEaFpUUR$|sRCudNGx)ik;^2m&h z4GhfaUTEcFBCQZBTK+`9&EYB5Vwk83u@3T6KBcy?X+Z8>61^R@X8oKjXjU!>zK`5a zfuyN0<4!0bVcxE%iHQE7W`UBouutdqITqK5to*CuXIs4swbBkaI7WOvcoI1%=2Ejh zLhXuJp~msP`Zy6kSHIMIf{d@k>R#U9rK1<50QCjXz}Q<*E=6btRtRQqT#$qt?c+%F z!5wqjIIWL~jFH`)vbCBbuP22ukBaw8y_~2rg(mY0?&~A#9$IDd>z*9DLtk3;H9kYI zurF!KnD9fXF>r2m@N(Gw-gIKC`n+#;40Y?g?)9Io{#(Z^#mUvk;Pa-0?P_dw{7dMz zl;627VY6q5E^FM3<=b_)>W|t33a6MOR9s(WiBTyst<;!>R&M1gC+uoeeud315sD?} ze~jw4MtQ@@OxeSycT4x;74Y$RBv3F|=-5b-hGG*H>+oa2cS`DyEbSW#b(frqyN}I@ zm*CO#_KY}P%|t(YN z)23E4ftB`?G0)6+BDFHrROa49x$Y1~%o^7y;yi+E1(TO7#RMxYe&Y=>;bf%S@!$15 zW2w+R^^2!6ATxyZpXI%pP9PLMA<~V79`S6$CR7imr}$*F^mMMA6+!`eata$y@73sO zhY`GQA5Z(3JoCB$W6mZFO~dKSlDe6~&?J_{mU90TSbujUMb&^Jzo*4{d*>Zl7F zq`ONHDQN)-r9`^BK^ml_OS(fqy1TnOq`OP$2I)@enseXx`)1aBGqcvrTEjo+<=)>t zvCrPmjm1JiJw4%)tCP2wI8b8PlRxVAc|KGEt8)Yq#g0cd7I%K@bK;rSWnL=jl*8|oO> zNIn0LLOi@8J)r|Odt@oRtNpq8@imZ1#+lnB2)dpMQT#X}fD zbnMpxpkl((M@{%|@pqR$4930ElJyBHnxLW(p_I=qa8TmSLOZgLv(sLY*3GPa+{rVt zw7ci=c$7nG)JqS=#9WaWHLN$Gp<#7?o<1|1K8BskaA$lX7kddlnj6-$oAjLJ%L6eK zN}^2H@?SPalJ@}F(?uG_s$=t3eDEdt>=&>Ecf{oy)9v_xe1S5H(UwJT--#U&aiiiw zzw%jrWx_u7zJpT<=|Gw{o7kgQ=JgY9$dK~0W;S;dQ&qmMQXO+S|AO_z9riNW*J ziBg-=NL(|Vd1VQ*Qk5y0VHR#$hCYt+rAhC~3igs6F({#tJ47>;HQ3_AW60 z*@wJ$&n<)Zt=jHBrCX^DqI6c(Zg}tg*E8L>zx+;J|F z>azplf}X2XUwT>_$=T;`D=_-&K8P&+?tZp01?U18EK0xBDkZ1;52cjKKzM?}Yr}gz zB51l}fnTo^^TwS+4mBvqsgu*$Ws%03>7`|s0D%&|gI8`aSiPz)yy)RlwO5keC$N)_ zi3k*w%=6HGy)+;?JKPjmabwWP^o}^TMRC8sqJB&%{rToY0?owX6l3y+KWm@6uGF&Y z`!m&Ejm#w!so~$0GNYM9b!tRKuzq=Z zk`SW}puX?$*0~ep#Z<5@070*={jogQGfi$z`$hqmP0rs=F5wt9A&~=kWb&Y})oyER zeQ?6`<;SbL{o-%knViLud=dw_zn|Bb7KK}eEGpe*lFEsss#7qiEaZ{So%Jer;&Ned zt2qCZ|4n<)KXxY^y^o`WK2E`!*U5GX<-@)NNh_RrUV_8FZ(tDRdI<}iD-vPDxGpZ~ zS8MuC=~s-0LpZGe@#pba(=IbTLjP+PeR4s)eYyH%YKa(J^zgG*^?MJTD#iw!H`0K%1Kj4-%1;A6cK+-GE`&pFS?xt-jH@+&@Wh0-3XBvI>csU3SohO%)eP!0TuZJ>c zNpltblddoRArNNJz@UUi2|?KsOVMxtlaCVKmph0{tZ$ZBTpGfC@_y_E)E{MX$On5pick_uzK1>`(+gj>akTKLdd z>u=f5&A+Z`Gh&->)lkqNaC0T?9r8qhN-iAIsksHF)fivOfdbs;qh$A=t!m-ZJT5ac zMvrsefUR_wxhRsBGlQa2av*tg@0S_6T`bMr1TW`>>dR)w&Qyu#gmT|DrN$io?mOy% zk+te2!nC^*{;VSsxVMs2pRcH*F`Fjashs9i-Og#YBX|~8S}-5RJAC{T^#q4WoYg)B zbYdv>M%d&aJJIsr`bchzejnv5 z!~zbx6AA(KS3&3gyPvc~_()F3J~hyb;aJ_7S+m#>r7@tda=9WqqB_<-We>>{KD7Vo zqC{&xu(?>_J-F{ivBz|yAPTS3eROYHf^B)&A-JqaXF!}LPfW|{?25M)D1U8(p-c&9 zB`Wrf7p<=V#ZHv~${1E=qz~^XYo?E=uC*4wy!}D#hJ`~6-bLm|Op5XtvAEakzEWLb z`gh%m3c<$`@Pb4YJ^i9g2$EfRmW9U$j<&0t`0CF4FJ$%RVVv@&0v@I>PZq@LZW!Ls ztXIRSy4=i5k-dspC_V_@%(${{r7xx=0vO2WD;Rj?I;DCqyJW{a??N_i9!T3>pfR*J z&aJN0bFXegneVpb;5xtct{09wOj|YHk?5sYyH=QTkIRQ71$OGs;;o&e3?jrPA}Ej- zQ3fD?u{HVI9-G0h@wAoSO-%B1vK)v|1XmWb zU+I;NeDxk4Z6S4fDYK{va8nZb^1;5887hGyOCP`YerGyXre+*-G$UR3o}mPx?%TeK zkJJ+-v=BlQjx3pu_yB7~e^zh_x?IBFzd8zOs@{H?s7!BLve|uB2NBZl+G+IB8_im& zUTF;7telAcOLw-th}F=q{X01P^^~0b?fi5ea)aRjxHi6J@b#oQ^s(>R;)}a-jUl4= zvqPi2drOaHuqBvKvqmGw1g~ z?r4x^B1x|G!hQ_CGa^xab>N|u;KLp2Kx{{qefEUx$mxs^;biM0>_qxjhl6VX^=!)- zqRRzTLK|EShR{6+xCpcHhK@;reVF#9$HMAwtzvy*%C}T?x5S|7tT&BWS*S2}xa&U= zI6OG&x`hXL??IxN;2BEx7IrY{JWzhx-4UtHmMew|o5LLMd_pGs%BEv-Co&%nlTcyP za5gyikaq<0xMibokvi+y^;|v9wn}q&D zD;4F7i+ytxoHHhMb!6;0-B5bg=;0#$FVUDx~lXVw>GqB{9SB9ImU)8*zvfQ@@Akkxh^jrtw;1) zd)WzK6*+Gcew1=5X`7t-AR*ZpQBQ*+gL`LHPpBUt+0}>ytzdgiL9D;gaZuBPp|=X> z?)B$}K2@n8MIuH7a6|{75#``QEio4Nh857ANjbbMrCb0?Qkk9;aNf>$a;ut={O{skrXZ}|22K#Zt_+92Nbg29y3F(WYIXE<| zH?VV_(0VIE1J%WJC=#fZt@Ap0cV%nL9?$7| zXOZ;N39jc7S31kigG;tgvzw{2me-bk6Z?^rw21C%1mJk<57ky3pg)aoCJLj!epVN>}9zuiIZta-lYp zjCZv*h&X8S4lCsF|4~&(3V^9UPD?&0+xeNt#2-ZQ zH}JuyFawRQv&`G{@B@T{kjT5c=IPY(!~oE`Yf(!1a;85w%WVr zQ_WO_-MFgZyEQ=t=eR415?OE|)kMHwQkLT4hLMm%wA@i!6h*C~d%1>sXG=NQ@^d>0 zA7POV@jvo9V&pLfiMedLl7fHfy6sCFE#1_Wg0Z^yoqq>3xz`| zSgfPpGlfumN@-w7*c_aoJq%?I{&gWWI%fW*;AAQ~s%EkFL>$#)@1B@$H4i~eMH1?J zE?t#D-ll``~XFnJpf0?#pmN>VlFLDeq{jdBWU;NqWjs z;*H^^?pjY~fwW?9w{YaEF)@Go-!-B?JaD^OB0F>h6I66Xe zA91XX?;I>;V*^Qks*o`=_Ks;7+>B4_<-JJV< zEwr)&tF9}$FQ>Zqp|}6H_4e=q37JaxznVn-v6i!5?As$8F5Rb#!0-$Ue)amxLc!jq z8&uKDCI{pLN4F-av`i)oPlS+qSIdb*!`i1k~AmsKXj22yy zVblv3HrenzzSyE45~Kd=VKm=+k+!nPSq3Fy&}zzNM;)0*a3t6?YE2D;fBwm7zVv!X z&r{d!k4qZ#jZWN)Aa?75FZ|4bu5sN~5$opqcB3$@YOS+F$?b7 z)KbmgjqXA%*eqvWE|vUTQl636Zwl3Jk;8+sBZ~zY>`p#P&5SP0cZ$61@~1tX38ze? zYPp*FW+h_RVF&G%_@txmvd{Qfn#jF{Y#-9zUI5}&k+*Q_t4cTnhJEH2wfad{zA2S{*! z? zvGKNBOdC$%aWhi&!NVXij0!N73?+lkrpKVNF^ILq3ABkRklDEP^;~{9*M<=~ASbF7 zApaJJMB+x-``0$k3>zma3mf}2ChQ5%CP?}#x4I}tpQ#hWU7g5o)S1=2nC7mr0u;Gn z)2RH`bBz?QFgDIA)#7R1ucX%W_?^?^owzWjuq^?_N1t#kP{CWo^#jVKSy@x1_Zi({ zpXD1}O(qH{xJ;qcPts#u|MlZs+d2axyejj69{TyS*Tb`}^rdxM63&P}FjsXZ&*c-Osx;iRZ=Ty40O-qE{s=NQGUS*bUS=Ju|ruVbu ziFgDbe@h%7Xqo+NXmyye_ewhd3LCL=WIPM&Y(7F8ng4NJ0?rgy2zDNcz7!fy&c_r3 z@tK}>A|p%jE7SYQ@>jdN*f_3k`Gn`$1AHtzZ21QvBrS_ailiKSN`-pWb+|Jkw-3mEw*OIuti7KXpf=bzHVo$P<7#-c?P33tKI*ll8*+I` zM?W~%;QiD+<#nT+CRaw?$N18rF-qt_zhE{{!amCs>Rqh%^1-*pJG8ECrsxgjf0Epm z0bIw%;A+Q~fqQ|{(bak!SKCD*eTa}*v@YuPz!4_+4r3!y?qz}868D{WhItc? zh&~8v1}>itB_!vpe}2T}rs448n2fFoLaNstw$B7vn|z_FyT1quS$cKu>z7pI_AWj#30W`+?I(n+rh1IMXs$_VpnofAL(N zRJQoSru66+zG&CJt$yP|ReYR+rizH2AfIRxH=dDcbYimL@a)(&>4ZU}>b`r^`SXej zV&8tR^<`V7s+pc>@xQc=F^^Cnu-k*z-WK061Xq54lj>>%eCVy$ixzo>g;Q-9n?@(Fm89DzPmkFzqxZ+o&!wf22ux97Tgl z`MxJ|6Dk(n;QY$W-8r`1n>qTY?4TU4RBHP!%h9#_e1{#cG@mQxf_|24hwqU_EskUY z(dTP|?ysA4OpN3&Ml>V{8PfO9Z4xyy_s_hR&&RYei_UKyZ!n-@c$_HORf#9BPiPz` zvFxGnhAcI23P5~8@LNCUiGuyXY*P};Xm^wALPnj zynLO;iFK0%o!m;~cT>Edv-Y9Z28z&a zD!uG>jkeh{9m6!;)K+1!KWc?PWB@^SC)VA1YEd?UT=NQC>G9??i#~L26aYO?;bTR3|i=8uc6l|-XV>U_gCAJ%eKxcwK5C5&$x>c2CYci(OHKUAtXpad(~ zhKoQwUW_m|fyQo0+zvFLFYWFa=bBNMAN7K72G3fuYUuzTj6Nkc6zHidzt-;Qdb8pTT~A;=RUq30 z4vp)^Pg;-rNTLsc-Q7Y2Oc%FOX%6GukbZ2*A53%TP$VTKKVJ+|e!kz!h1%NL!674? zI9jMB;&Ec+aX!gYE|hx&{doWFPa*n%{%!HnphF4hVaY3492{m;T2LzsHJ)2R0HwCJYM0GgfM2Ibu?of0-Pbe+StDs3erBiM?8#gQ1np8 z*^!5vZD9c$5~3XS72P%+PAu?xfn8;qqLzK$f9W*TnRl%umgnqr3@}%5cD#ZOoMHE34dtU9~6Vo2W zX846^lm)Eq1HCd5VNXN_1sgexBkIGb@S(xLFHvYdL~%G&+*p0d@@6LkgF6^W{;IJp z8;qToKEf^4+{Z)`fwojbN9+W&at{xhj(n7#-jOBgN%n_nmqlT7oWFCuKc`WS7bg^rjFIeG#p z{WxBU9L^_qQB*`hj+h}zewGpHAY|yp#Nxd(?{V=Oz1Tl7uUujqnDL%i!01 zHYArV&^}}O=OfNOzGQQ;v4TxC3Kr!MDY`V}fr-J+gDP{)Y}r|=?XWZt=iWlWGtIbf zOdLJn+SbSGlQbJoJ-B^KdU5{{HhJwluT+Kaar|0bUdn-AsZy99j7L1if94R5SZ)d1 z@(2IkJy=Dq^7d!bgy5j*LtPBSH(XYTifK*c8da~Dpy`nu2ID0H=>)n`Ua0$hdQ_byYkD}P#P^^e6ZiBfu?+bUiBn$k;(?E`iCt2&=Vo@ zhnixdh)G%UAOnncxWZ$Wnqv5n53l;7(PNOov9SF*mbsf;dv;C^pUow=@jZ`%cq1wt znaeV$IFxAfQL%K0nL@2{`{Ud5` zO3ALp>PG`08(M!*WPJUjWrr$QwG_QMlYO8qp5KH)!?GeYuY`zYbA?vnGxeD92X;*& z`!_MvP-w5LMHfEt8XGFhbmh{GrT?VJs{B;g088i}Xvk7Lm(HXIt(ZeCg8NW8wI z)=dwoQ@0Mzzv;vEr0o4G3VK5SeAIp)2irx=%{r1U{r&7QN}mnJE-({I^B+`_6|Q<= ze?Bz02 z@({B)a>mB31TL#EVB7gx>e|Ce#{7C*Xdl(Pr?k_7AzXQUH|yaToYSf_iMeIJDw`g? z$g!Ehe-ao=pnLqt(G?JtsS`r4khATB#JBn}RC+O-O3`c>TkCVIms?EGC;6`rL*;N< zeoY%`*b+qMx@@_3pK&yQ)Sl|2>_1%W%{P%zU=v)_hTd)dwnyn%1bBrZRSIQ&+Qk@ zn~1&?Sa%GPJPi>xZZm4Qz~sG8-{B6Q7l$%d=(XLK6uSMy>@4*-Q1#YC?zxNM#0Yb<%Vk;~PabrZ6icr+^+w)KY?vySAA#R9ox=7dCp94W1GajA#ibKFKjY zcxoSC!m$y%x$v6PcNYE-%8b=_6jE<^$sCu>-yl$83z=L>?@#A37335y5Vpk)ZVr8k zAPTl34xp%_9>55v{3^oQGFzympd#}SrkfOdM8!cgu{Ik|HI%LG^^6t5c8vEbvFh5H zm>~DZv-7}I{yyfLA^z?n8!=YQ~l>qe&PhK|NMdvbps7#QvT zl)mG!XXOl>r#Fx9pLu#uJ&iN`Ok&39ehP)4`~d~WrvDp;*l;XT@b8Cc`VfD#O`M7p z4@&RnI)9RZMtg%$)I0KB)qAq0xFR2SR(gVa-9Km+?LuD*vD)u!nan6!kePq(E~YZC zf9dpEeLkjqzmQM*;39$2vhn=426bXHWG7d{D~|+q?zulU5zS~Srsv=5RX=*KKAb4>D-TWhMB}kzbafKT9Q2SPpPDK z2LBGCd%1_VRqu~0<#c2tZ#f>-t%OW|vo_at#1LrcNUkXPSjxQRB!|tVjMmpe zTxo}nKDe9e!HkL`77{2_l5*;OOKe!rH`K<%%6^M}qU(_E*6!sX$-!A@XhfUBV6{+9 z7aMwV6gK@XvwCfH@b~1%H|bXmG}Z;mN5&o<`=(+ip`DgLs+>5LQ{H3$E+^aULvLKMiR+b@Qi2tnbuSU|bZ`cx--~(xDY(mVU z2|HyhGljXdINpM!3j=OY$4yeP^xG*`F`lR!YMcZQ%6!)nAFJRx!wJ^6^lUMTvR^z; zNM4Me8Z#cuV#oi+?I6Y`vxP(zu<12=#ePvr=DydtK9!(-PoI`^S4(E0h<1 zyvxxOqugzBX%Z3+@4?|g_%c4a`DIsjw02^nH30P+(M+0Y3SWRE2ZbaK8(R&Pe?Y@g z<%-7J^QS|tL9>|dp)!Zy6IutwIarIO;D#4Zr!F>+Uw2-wf!9h`E3dCyAK9O3pqvnDnx3`57!vAG(D| zh1@<3o__!6lxsHSf-ve%Sj+2^Er_9+_m7I}L&DLdrKXNyw(&lans;p^Dw);TeUmsV z;+oG8)rY;1YomOEntu)~B78EIQ&@7>YvUEN;y-x@1YU9z?JeEkKYjZ7SnTLG*$K!b zWT4|_O4Sb+X=PLL{vFOuobu7cN+$C-S5zDWmsELUV*^iAF|OnJUK%=XdRj#%)1tCX zt?8us5c2XXJr6~*xr+3OYC}WA_?tEFwNuwZx%30I_C2GdA|J;n3o5rL$E0tyu1v!+j%EnQQc zy}iBqEQDNM+NXLBkGFO>IFE_MQW6qHRUHp6!}n=ic0!FS){U|8@v0}TwDK7;*47o* z-1b{&H~u(C@5p}>N~{I66B_FY`~9)Z(piIp@tX+iox2LBy1T*VA&D|V_Al-tV}kL` z!g>9uZqAXqztX}fo%nucF-?vZ{5J0Nf%du~V$t`h;P2e%3$1@<(rTchpb(s+1vw|j z!&x&=1T;bcIXMhsPV4FWN8oXDrON?ye0tML5c;F4ie*Kd9QYyIGO z%16@o6@U!GZz^7l0xAMHH(_CD8Lm4-lT%ase{vDQ?v@htM#seX9jOj_tatiCyfnaJ zkwTm45d%UBHMW0+m=MW34I_EQ3=AkX2O?={X@h`IaCunSVa^Q&y)&BJ6M&3M$YGh3 zfOLP{Xps@D`akELrMLzCSqIxS0IY%lIoA``#HrXiEZu)rMS#3W4_rw2@4symC_sku zzXPpB1K0ik^8u_hV~`#B9}i;`M)H9?1$@Ai2$ai1Ru(*S1e6N?=S7${ z%v?dNdLMb{!YN5}^LG$Zl^B)Mf$@s zYP3Fw4m=d>NdRMJ`l_uEtP9Fujf}m$eR>8ZBcseRmtYAl6cnz3q==x3%mFg^P*Bq0 zz38a(6U9#RGuKQLpBB5f^|)NM`v07=*#BwDmU&+o2@2%WL;Aj&?PaH(|1To|4_g$3 z^ZoC-K$Z^u|8`;iAJ?2iXx06cSF`S$pnw1raOX{*?yt~!^QJPutSKa8PNuSj5v|-M zC6R&GO!nu`pMZb>+p8IMa5T>m-kk6EWqLmk87Jz9DJkK^P*nr5S#c6oblFKZm5`C4 zVquAbxR}nTx;mLZ-iZ>)7POO|<*l#~eB&G7^U1$+DYSj{G>j-1>*z2e2> zRd?=NFUK#N8cy3EtO42boT@nRc*B@G9%F!Gn^de^crn5M@*4!&#KH)>e&W);cKcSy zo6h4*!^9-}P^MWgwRHVk81sBHlD|g33kER42m&!s*Kt&Zjkx}o1NyC>tU!2rPw!Tq z4@xU9u9s{+IPAy({ugmuTNX;Ou&lg1kwb^gaLyXf2PZ%XFq$J8dV4v+PsH!#;pzEf zDsXt`^*UsRI)|4#V^dbIz&ydXba~#a3@p}LXtcVpf`CWp`{rN}hbWgfzQG9U=_*pp zt!Rx`wp0H+^5L>A;Ycf&O}e<0Z$80zT0(J zYH&X2nV8Dgm~CGxI#u9{Dh6VZlt47$e?IO8S_e~ho7dOaZHj?VLq+{i&FI#nc%%y?iq@ut+)DGVIgO2 z{T>Q!Wfg}m)>XHKfpUQzhnYwZ57Mu<{qZBkWfgsSu;(B2e z5)x7e9#9%oosCF-7L}a*v*+(`omQyx^Yf#{`chvpN0Z+j-rgn?8L|)8$CA?0n+v9y z#-Ny|&?sNTJk zdPFZ7>U*U(s;aIdI+DP&(BaK5&v!=!*m#V_QrHR65LoBfd-uPy*=>jb&My%_>jhQv z{Zv9;{>J^dr4iZVoO2F}bJ-SlER~a%p8luz%TwY2nRnVsi}U4i;~ET@LMYI(rtsi_ z(+Mq(hhl(?%51fjThH?f-`Cd{or}#k%L&B9${LpIE!3OA8^Drkw0m&(1fpAXMyIbX z1IYrDlHKKDEFvCGwXorzsnBVONlF^5GU%BH!!6fptT;8uxJCjEyx-E$AozE!;|e1wyemhsU_SJ zr@aLvAmA~9;O)?02La6h?w7i6*kElaxw$ngDu>%BDJZ&5>rYo{ek-qPKjeD9JVP~G zXpK6~V1r;`H)!|;SUVEl>y*@Ur^|ug!+`1IHK=eRg@=bfTIns;yz(`geceb~ytyex-c_IE^(ZVqea}kkvBO)k^z5e04ein^ZzVL+cH-JRCxlE z4omF7fu#HU_wVq<_DFnP-N$2P6_vcJ;-giA#-(_0+UQ>Ce2=biy)@aL)ALpZWJ&Sh z@N)h~QF6?Iv!x7Sk}w9+_@4zh?Re1zRo?M{AYs>h0Lwv#7cdzhx$c6>M93PL)N1Sl zJzC>*yckG2X*Q9>Y8VIhyU&t?0l+oI{xG84qoqdqC0%XpvSs(vHYp>cJRCf{JZ+Gx zk1tLB=r!donPfIm5Gqk;BnV}*oM$nJXiu`AP!Vx1P1n zu*0UAo&{IM!)=?tvxSETqbcJ%I|Zm|XvAE%qQ&u2$J8d)3ha~@v?yl&qP zK@b|kDd^~>D;e|Um#sm1X5m|i8Wa%`@lW@jEz{8c{zuf&$)j{eDwKd}zREy2Jzba_ zYnlXt0mqfV8~nTU^{0yzubjpqxh`ND=N8Y0QIf1DCnpKH?TyOQ3mL(FlEzTQKX9M6 zUJ`yVL36)8Dz0T!5dmmMxj@F?V7l1R38QvlG&hoO7}$MX(_H{F9fN?T^Q(;m=EDw9 z?OpC>ddH-sXfp4jVPaOkD*qxR1Va#-KMu&+8`okI62w5V;&84KQGY0gT5|~yBTDP* zliS(ZIdTMs+ydSjWK>kc-&ul=ZW(}4E*db*RRfjui8m4eVNb~Vr2W9d!#e}E)E-DU z1o@MdF9R8mruqq38-H4_G-G37b#m{f^#D{}$7hYZ+x-I-!Uh8k4IM_v>IW2Zocn4G zjegUy5>Kp6su^7|SYIq)BN zPFrJvC<6%iYuyiZzQ$O($pMW85COe!Ib!Q@c$t)D#wH_^7ZHI2Yw^aZC0#O>1{)t= z)ZU&g)AKsHArfRSfF=hz69)@R0I0uYKF{U`Ucl=m=#Kyhg&t7`D6x=Ull@0Vgxc>9 z7y$p3066_k`vs}D|EA>6)d@T%^CTe@L>({BsbDOSZ%$WGQBmm)Wo{n?Yt{v_IHJJ4 z1F)w2f{R8zV|0YMV-%94sy0M)*d3?lM%hRbhG(^ZE zKVKjLSULOA#;`x^oLd~YPXHZ4$j2aG339ttMngC>GD%s7jg|{+*QaX}fXfj=*$DKu zQ+Y}$G~^`!Tf4#P54XBrCb3%(mx9ZUgkS70$V;55yjlmlF{as{t131i7yt!GPyvARcK~Y4HLSTEmJj2rYi9$qvbDWU53Jc8kl@nKCv&>)vcOeu zY|y>FwT9YmIc6P)pYD5R0BnN$=1o~W`s+aWm&T=7 z?w~3LMuvorJ~O+yxw$`Gj9bHaxVXH#Qe%t?;0`$dxHvdDp1?Q{K0iG?W#s<)Md;(> z1C?n71pS&@eXc^M-qF7cq7R$^?`?lIu)`1x4U74?6PVf)hf!da02VI!fDXh5Ti3(0uT}q6f`%&1p^Dqu-pz(lMrN|;d!kAf!Z!ET$j5O zroj9$PQ7wZ@8ot!v;8Oa9fp?TAF*LSJSofA}JH1lo$0dNHj8&Ja)uN{LNc!BHX z&gZLXWsn*U0=nD&_RK(stac%+=Wo&3XA&MKBY;Km2nfur02F#v!#D-f+)@JzQU4BR zLILs*6Qi_Rto!5n_3PKi6GITD0-FL-MF(KVC?;-|Y~mzx)N? zum@~ld73mZlK)kJZwZ(*MZj>Km+cUBn!o??tZ2VCC;M>bFu^-8nJ>N2U_}h9FeLyB zulg+5k|6~o8jaQ@Ag*P!{LAh?NpN7;CadA#;LgAyyqq_TJO@5TmG_I+Y=ur~c(&D2 zLtb7UM1Mn67GSX*gSd}85Lx1KIU`Tyw4wR%Ar>%FW?nA4Y=(p90M^UK8mlD^fN%T3 z@t6Rm17)aOW?^V#d}+|I2rPi@*6ZcT8C1UX9?X=w9uyb8lj-~nQqpSWnvTP}-oU#~ z;Jae5u`IgqV z0jLUbF9A_&Dp+K%$8+&fliCJeI=WzpyF+aK%NJM@Y#f~KL_IH!?Q;Oyeu$I02AF2J z21bzZuz__SdjS{KnRl~;NWTJ81St~$!PF0+Tql!N4WAaW|SlAWBTYx&k1i)b%z&yy`Qpo-STRS*5ww+>8!JU5zIZonAc>GOd5Y;>e z00j5T}V6+-%c F{~HBa6$k(T diff --git a/benchmark.py b/benchmark.py deleted file mode 100644 index 5ec632e..0000000 --- a/benchmark.py +++ /dev/null @@ -1,4 +0,0 @@ -import subprocess - -subprocess.run(['build/Benchmark.exe'], cwd='build') -subprocess.run(['build/Benchmark.exe', 'NOCULL'], cwd='build') \ No newline at end of file diff --git a/plots.py b/plots.py deleted file mode 100644 index 9400eca..0000000 --- a/plots.py +++ /dev/null @@ -1,81 +0,0 @@ -import pandas as pd -import matplotlib.pyplot as plt - -#areas = pd.read_csv('build/s.csv') -# -#plot = areas.hist(column='area', bins=100) -#plt.yscale('log') -#plt.show() - -df = pd.read_csv('build/stats.csv') -noculldf = pd.read_csv('build/statsNOCULL.csv') - -fig, ax = plt.subplots() - -scaled_reltime = df['RelTime'] / 10**9 - -scaled_FrameTime = df['FrameTime'].rolling(window=100).mean() / 10**9 -scaled_CACHED = df['CACHED'].rolling(window=100).mean() / 10**9 -scaled_MIPGEN = df['MIPGEN'].rolling(window=100).mean() / 10**9 -scaled_DEPTHCULL = df['DEPTHCULL'].rolling(window=100).mean() / 10**9 -scaled_VISIBILITY = df['VISIBILITY'].rolling(window=100).mean() / 10**9 -scaled_LIGHTCULL = df['LIGHTCULL'].rolling(window=100).mean() / 10**9 -scaled_BASE = df['BASE'].rolling(window=100).mean() / 10**9 - -ax.plot(scaled_reltime, scaled_FrameTime, label='Frame Time') -ax.plot(scaled_reltime, scaled_CACHED, label='CACHED') -ax.plot(scaled_reltime, scaled_MIPGEN, label='MIPGEN') -ax.plot(scaled_reltime, scaled_DEPTHCULL, label='DEPTHCULL') -ax.plot(scaled_reltime, scaled_VISIBILITY, label='VISIBILITY') -ax.plot(scaled_reltime, scaled_LIGHTCULL, label='LIGHTCULL') -ax.plot(scaled_reltime, scaled_BASE, label='BASE') - - - -ax.set_xlabel('Application Time (ms)') -ax.set_ylabel('Render Times (ms)') - -ax.legend() - -fig.savefig('allcull.png') - -nocullfig, nocullax = plt.subplots() - -nocullscaled_reltime = noculldf['RelTime'] / 10**9 - -nocullscaled_FrameTime = noculldf['FrameTime'].rolling(window=100).mean() / 10**9 -nocullscaled_CACHED = noculldf['CACHED'].rolling(window=100).mean() / 10**9 -nocullscaled_MIPGEN = noculldf['MIPGEN'].rolling(window=100).mean() / 10**9 -nocullscaled_DEPTHCULL = noculldf['DEPTHCULL'].rolling(window=100).mean() / 10**9 -nocullscaled_VISIBILITY = noculldf['VISIBILITY'].rolling(window=100).mean() / 10**9 -nocullscaled_LIGHTCULL = noculldf['LIGHTCULL'].rolling(window=100).mean() / 10**9 -nocullscaled_BASE = noculldf['BASE'].rolling(window=100).mean() / 10**9 - -nocullax.plot(nocullscaled_reltime, nocullscaled_FrameTime, label='Frame Time') -nocullax.plot(nocullscaled_reltime, nocullscaled_CACHED, label='CACHED') -nocullax.plot(nocullscaled_reltime, nocullscaled_MIPGEN, label='MIPGEN') -nocullax.plot(nocullscaled_reltime, nocullscaled_DEPTHCULL, label='DEPTHCULL') -nocullax.plot(nocullscaled_reltime, nocullscaled_VISIBILITY, label='VISIBILITY') -nocullax.plot(nocullscaled_reltime, nocullscaled_LIGHTCULL, label='LIGHTCULL') -nocullax.plot(nocullscaled_reltime, nocullscaled_BASE, label='BASE') - -nocullax.set_xlabel('Application Time (ms)') -nocullax.set_ylabel('Render Times (ms)') - -nocullax.legend() - -nocullfig.savefig('allnocull.png') - -combfig, combax = plt.subplots() - -combax.plot(scaled_reltime, scaled_FrameTime, label='Culling Frametime') -combax.plot(nocullscaled_reltime, nocullscaled_FrameTime, label='No Culling Frametime') - -combax.set_xlabel('Application Time (ms)') -combax.set_ylabel('Render Times (ms)') - -combax.legend() - -combfig.savefig('combined.png') - - diff --git a/res/shaders/WaterCommon.slang b/res/shaders/WaterCommon.slang new file mode 100644 index 0000000..ad8d289 --- /dev/null +++ b/res/shaders/WaterCommon.slang @@ -0,0 +1,53 @@ +struct WaterPayload +{ + float2 offset; + float extent; + float height; +}; + +struct WaterVertex +{ + float4 position_CS : SV_Position; + float3 position_WS : POSITION0; + float2 texCoord : TEXCOORD0; + float depth : DEPTH; +}; + +struct MaterialParams +{ + float3 sunDirection; + float displacementDepthAttenuation; + + float foamSubtract0; + float foamSubtract1; + float foamSubtract2; + float foamSubtract3; + + float normalStrength; + float foamDepthAttenuation; + float normalDepthAttenuation; + float roughness; + + float3 sunIrradiance; + float foamRoughnessModifier; + + float3 scatterColor; + float environmentLightStrength; + + float3 bubbleColor; + float heightModifier; + + float bubbleDensity; + float wavePeakScatterStrength; + float scatterStrength; + float scatterShadowStrength; + + float3 foamColor; + Texture2DArray displacementTextures; + Texture2DArray slopeTextures; + TextureCube environmentMap; + SamplerState sampler; + StructuredBuffer tiles; +}; +layout(set = 1) +ParameterBlock pWaterMaterial; diff --git a/res/shaders/WaterCompute.slang b/res/shaders/WaterCompute.slang new file mode 100644 index 0000000..d51a324 --- /dev/null +++ b/res/shaders/WaterCompute.slang @@ -0,0 +1,329 @@ +const static float PI = 3.1415926535897932f; +// FFT and JONSWAP Implementation largely referenced from https://github.com/gasgiant/FFT-Ocean/ +struct SpectrumParameters { + float scale; + float angle; + float spreadBlend; + float swell; + float alpha; + float peakOmega; + float gamma; + float shortWavesFade; +}; + +struct ComputeParams +{ + float frameTime, deltaTime, gravity, repeatTime, depth, lowCutoff, highCutoff; + int seed; + float2 lambda; + uint N, lengthScale0, lengthScale1, lengthScale2, lengthScale3; + float foamBias, foamDecayRate, foamAdd, foamThreshold; + RWTexture2DArray spectrumTextures, initialSpectrumTextures, displacementTextures; + RWTexture2DArray slopeTexture; + RWTexture2D boyancyData; + StructuredBuffer spectrums; + SamplerState linear_repeat_sampler; +}; +layout(set = 0) +ParameterBlock pParams; + +float2 ComplexMult(float2 a, float2 b) { + return float2(a.x * b.x - a.y * b.y, a.x * b.y + a.y * b.x); +} + +float2 EulerFormula(float x) { + return float2(cos(x), sin(x)); +} + +float hash(uint n) { + // integer hash copied from Hugo Elias + n = (n << 13U) ^ n; + n = n * (n * n * 15731U + 0x789221U) + (0x1376312589U); + return float(n & uint(0x7fffffffU)) / float(0x7fffffff); +} + +float2 UniformToGaussian(float u1, float u2) { + float R = sqrt(-2.0f * log(u1)); + float theta = 2.0f * PI * u2; + + return float2(R * cos(theta), R * sin(theta)); +} + + +float Dispersion(float kMag) { + return sqrt(pParams.gravity * kMag * tanh(min(kMag * pParams.depth, 20))); +} + +float DispersionDerivative(float kMag) { + float th = tanh(min(kMag * pParams.depth, 20)); + float ch = cosh(kMag * pParams.depth); + return pParams.gravity * (pParams.depth * kMag / ch / ch + th) / Dispersion(kMag) / 2.0f; +} + +float NormalizationFactor(float s) { + float s2 = s * s; + float s3 = s2 * s; + float s4 = s3 * s; + if (s < 5) return -0.000564f * s4 + 0.00776f * s3 - 0.044f * s2 + 0.192f * s + 0.163f; + else return -4.80e-08f * s4 + 1.07e-05f * s3 - 9.53e-04f * s2 + 5.90e-02f * s + 3.93e-01f; +} + +float DonelanBannerBeta(float x) { + if (x < 0.95f) return 2.61f * pow(abs(x), 1.3f); + if (x < 1.6f) return 2.28f * pow(abs(x), -1.3f); + + float p = -0.4f + 0.8393f * exp(-0.567f * log(x * x)); + return pow(10.0f, p); +} + +float DonelanBanner(float theta, float omega, float peakOmega) { + float beta = DonelanBannerBeta(omega / peakOmega); + float sech = 1.0f / cosh(beta * theta); + return beta / 2.0f / tanh(beta * 3.1416f) * sech * sech; +} + +float Cosine2s(float theta, float s) { + return NormalizationFactor(s) * pow(abs(cos(0.5f * theta)), 2.0f * s); +} + +float SpreadPower(float omega, float peakOmega) { + if (omega > peakOmega) + return 9.77f * pow(abs(omega / peakOmega), -2.5f); + else + return 6.97f * pow(abs(omega / peakOmega), 5.0f); +} + +float DirectionSpectrum(float theta, float omega, SpectrumParameters spectrum) { + float s = SpreadPower(omega, spectrum.peakOmega) + 16 * tanh(min(omega / spectrum.peakOmega, 20)) * spectrum.swell * spectrum.swell; + return lerp(2.0f / 3.1415f * cos(theta) * cos(theta), Cosine2s(theta - spectrum.angle, s), spectrum.spreadBlend); +} + +float TMACorrection(float omega) { + float omegaH = omega * sqrt(pParams.depth / pParams.gravity); + if (omegaH <= 1.0f) + return 0.5f * omegaH * omegaH; + if (omegaH < 2.0f) + return 1.0f - 0.5f * (2.0f - omegaH) * (2.0f - omegaH); + + return 1.0f; +} + +float JONSWAP(float omega, SpectrumParameters spectrum) { + float sigma = (omega <= spectrum.peakOmega) ? 0.07f : 0.09f; + + float r = exp(-(omega - spectrum.peakOmega) * (omega - spectrum.peakOmega) / 2.0f / sigma / sigma / spectrum.peakOmega / spectrum.peakOmega); + + float oneOverOmega = 1.0f / omega; + float peakOmegaOverOmega = spectrum.peakOmega / omega; + return spectrum.scale * TMACorrection(omega) * spectrum.alpha * pParams.gravity * pParams.gravity + * oneOverOmega * oneOverOmega * oneOverOmega * oneOverOmega * oneOverOmega + * exp(-1.25f * peakOmegaOverOmega * peakOmegaOverOmega * peakOmegaOverOmega * peakOmegaOverOmega) + * pow(abs(spectrum.gamma), r); +} + +float ShortWavesFade(float kLength, SpectrumParameters spectrum) { + return exp(-spectrum.shortWavesFade * spectrum.shortWavesFade * kLength * kLength); +} + +[numthreads(8,8,1)] +void CS_InitializeSpectrum(uint3 id : SV_DISPATCHTHREADID) { + uint seed = id.x + pParams.N * id.y + pParams.N; + seed += pParams.seed; + + float lengthScales[4] = { pParams.lengthScale0, pParams.lengthScale1, pParams.lengthScale2, pParams.lengthScale3 }; + + for (uint i = 0; i < 4; ++i) { + float halfN = pParams.N / 2.0f; + + float deltaK = 2.0f * PI / lengthScales[i]; + float2 K = (id.xy - halfN) * deltaK; + float kLength = length(K); + + seed += i + uint(hash(seed) * 10); + float4 uniformRandSamples = float4(hash(seed), hash(seed * 2), hash(seed * 3), hash(seed * 4)); + float2 gauss1 = float2(1, 1);UniformToGaussian(uniformRandSamples.x, uniformRandSamples.y); + float2 gauss2 = float2(1, 1);UniformToGaussian(uniformRandSamples.z, uniformRandSamples.w); + + if (pParams.lowCutoff <= kLength && kLength <= pParams.highCutoff) { + float kAngle = atan2(K.y, K.x); + float omega = Dispersion(kLength); + + float dOmegadk = DispersionDerivative(kLength); + + float spectrum = JONSWAP(omega, pParams.spectrums[i * 2]) * DirectionSpectrum(kAngle, omega, pParams.spectrums[i * 2]) * ShortWavesFade(kLength, pParams.spectrums[i * 2]); + + if (pParams.spectrums[i * 2 + 1].scale > 0) + spectrum += JONSWAP(omega, pParams.spectrums[i * 2 + 1]) * DirectionSpectrum(kAngle, omega, pParams.spectrums[i * 2 + 1]) * ShortWavesFade(kLength, pParams.spectrums[i * 2 + 1]); + + pParams.initialSpectrumTextures[uint3(id.xy, i)] = float4(float2(gauss2.x, gauss1.y) * sqrt(2 * spectrum * abs(dOmegadk) / kLength * deltaK * deltaK), 0.0f, 0.0f); + } else { + pParams.initialSpectrumTextures[uint3(id.xy, i)] = 0.0f; + } + } +} + +[numthreads(8,8,1)] +void CS_PackSpectrumConjugate(uint3 id : SV_DISPATCHTHREADID) { + for (uint i = 0; i < 4; ++i) { + float2 h0 = pParams.initialSpectrumTextures[uint3(id.xy, i)].rg; + float2 h0conj = pParams.initialSpectrumTextures[uint3((pParams.N - id.x ) % pParams.N, (pParams.N - id.y) % pParams.N, i)].rg; + + pParams.initialSpectrumTextures[uint3(id.xy, i)] = float4(h0, h0conj.x, -h0conj.y); + } +} + +[numthreads(8, 8, 1)] +void CS_UpdateSpectrumForFFT(uint3 id : SV_DISPATCHTHREADID) { + + float lengthScales[4] = { pParams.lengthScale0, pParams.lengthScale1, pParams.lengthScale2, pParams.lengthScale3 }; + + for (int i = 0; i < 4; ++i) { + float4 initialSignal = pParams.initialSpectrumTextures[uint3(id.xy, i)]; + float2 h0 = initialSignal.xy; + float2 h0conj = initialSignal.zw; + + float halfN = pParams.N / 2.0f; + float2 K = (id.xy - halfN) * 2.0f * PI / lengthScales[i]; + float kMag = length(K); + float kMagRcp = rcp(kMag); + + if (kMag < 0.0001f) { + kMagRcp = 1.0f; + } + + float w_0 = 2.0f * PI / pParams.repeatTime; + float dispersion = floor(sqrt(pParams.gravity * kMag) / w_0) * w_0 * pParams.frameTime; + + float2 exponent = EulerFormula(dispersion); + + float2 htilde = ComplexMult(h0, exponent) + ComplexMult(h0conj, float2(exponent.x, -exponent.y)); + float2 ih = float2(-htilde.y, htilde.x); + + float2 displacementX = ih * K.x * kMagRcp; + float2 displacementY = htilde; + float2 displacementZ = ih * K.y * kMagRcp; + + float2 displacementX_dx = -htilde * K.x * K.x * kMagRcp; + float2 displacementY_dx = ih * K.x; + float2 displacementZ_dx = -htilde * K.x * K.y * kMagRcp; + + float2 displacementY_dz = ih * K.y; + float2 displacementZ_dz = -htilde * K.y * K.y * kMagRcp; + + float2 htildeDisplacementX = float2(displacementX.x - displacementZ.y, displacementX.y + displacementZ.x); + float2 htildeDisplacementZ = float2(displacementY.x - displacementZ_dx.y, displacementY.y + displacementZ_dx.x); + + float2 htildeSlopeX = float2(displacementY_dx.x - displacementY_dz.y, displacementY_dx.y + displacementY_dz.x); + float2 htildeSlopeZ = float2(displacementX_dx.x - displacementZ_dz.y, displacementX_dx.y + displacementZ_dz.x); + + pParams.spectrumTextures[uint3(id.xy, i * 2)] = float4(htildeDisplacementX, htildeDisplacementZ); + pParams.spectrumTextures[uint3(id.xy, i * 2 + 1)] = float4(htildeSlopeX, htildeSlopeZ); + } +} + +float2 ComplexExp(float2 a) { + return float2(cos(a.y), sin(a.y) * exp(a.x)); +} + + +float4 ComputeTwiddleFactorAndInputIndices(uint2 id) { + uint b = pParams.N >> (id.x + 1); + float2 mult = 2 * PI * float2(0.0f, 1.0f) / pParams.N; + uint i = (2 * b * (id.y / b) + id.y % b) % pParams.N; + float2 twiddle = ComplexExp(-mult * ((id.y / b) * b)); + + return float4(twiddle, i, i + b); +} + +#define SIZE 1024 +#define LOG_SIZE 10 + +groupshared float4 fftGroupBuffer[2][SIZE]; + +void ButterflyValues(uint step, uint index, out uint2 indices, out float2 twiddle) { + const float twoPi = 6.28318530718; + uint b = SIZE >> (step + 1); + uint w = b * (index / b); + uint i = (w + index) % SIZE; + sincos(-twoPi / SIZE * w, twiddle.y, twiddle.x); + + //This is what makes it the inverse FFT + twiddle.y = -twiddle.y; + indices = uint2(i, i + b); +} + +float4 FFT(uint threadIndex, float4 input) { + fftGroupBuffer[0][threadIndex] = input; + GroupMemoryBarrierWithGroupSync(); + bool flag = false; + + [unroll] + for (uint step = 0; step < LOG_SIZE; ++step) { + uint2 inputsIndices; + float2 twiddle; + ButterflyValues(step, threadIndex, inputsIndices, twiddle); + + float4 v = fftGroupBuffer[uint(flag)][inputsIndices.y]; + fftGroupBuffer[uint(flag)][threadIndex] = fftGroupBuffer[uint(flag)][inputsIndices.x] + float4(ComplexMult(twiddle, v.xy), ComplexMult(twiddle, v.zw)); + + flag = !flag; + GroupMemoryBarrierWithGroupSync(); + } + + return fftGroupBuffer[uint(flag)][threadIndex]; +} + +[numthreads(SIZE, 1, 1)] +void CS_HorizontalFFT(uint3 id : SV_DISPATCHTHREADID) { + for (int i = 0; i < 8; ++i) { + pParams.spectrumTextures[uint3(id.xy, i)] = FFT(id.x, pParams.spectrumTextures[uint3(id.xy, i)]); + } +} + +[numthreads(SIZE, 1, 1)] +void CS_VerticalFFT(uint3 id : SV_DISPATCHTHREADID) { + for (int i = 0; i < 8; ++i) { + pParams.spectrumTextures[uint3(id.yx, i)] = FFT(id.x, pParams.spectrumTextures[uint3(id.yx, i)]); + } +} + +float4 Permute(float4 data, float3 id) { + return data * (1.0f - 2.0f * ((id.x + id.y) % 2)); +} + +[numthreads(8, 8, 1)] +void CS_AssembleMaps(uint3 id : SV_DISPATCHTHREADID) { + for (int i = 0; i < 4; ++i) { + float4 htildeDisplacement = Permute(pParams.spectrumTextures[uint3(id.xy, i * 2)], id); + float4 htildeSlope = Permute(pParams.spectrumTextures[uint3(id.xy, i * 2 + 1)], id); + + float2 dxdz = htildeDisplacement.rg; + float2 dydxz = htildeDisplacement.ba; + float2 dyxdyz = htildeSlope.rg; + float2 dxxdzz = htildeSlope.ba; + + float jacobian = (1.0f + pParams.lambda.x * dxxdzz.x) * (1.0f + pParams.lambda.y * dxxdzz.y) - pParams.lambda.x * pParams.lambda.y * dydxz.y * dydxz.y; + + float3 displacement = float3(pParams.lambda.x * dxdz.x, dydxz.x, pParams.lambda.y * dxdz.y); + + float2 slopes = dyxdyz.xy / (1 + abs(dxxdzz * pParams.lambda)); + float covariance = slopes.x * slopes.y; + + float foam = pParams.displacementTextures[uint3(id.xy, i)].a; + foam *= exp(-pParams.foamDecayRate); + foam = saturate(foam); + + float biasedJacobian = max(0.0f, -(jacobian - pParams.foamBias)); + + if (biasedJacobian > pParams.foamThreshold) + foam += pParams.foamAdd * biasedJacobian; + + + pParams.displacementTextures[uint3(id.xy, i)] = float4(displacement, foam); + pParams.slopeTexture[uint3(id.xy, i)] = float2(slopes); + + if (i == 0) { + pParams.boyancyData[id.xy] = half(displacement.y); + } + } +} diff --git a/res/shaders/WaterMesh.slang b/res/shaders/WaterMesh.slang index e69de29..391ae65 100644 --- a/res/shaders/WaterMesh.slang +++ b/res/shaders/WaterMesh.slang @@ -0,0 +1,69 @@ +import Common; +import Scene; +import WaterCommon; +import MaterialParameter; + +const static uint64_t TILE_VERTS = 144; +const static uint64_t TILE_PRIMS = 242; + +const static float2 VERT[TILE_VERTS] = {float2(0.0f / 11, 0.0f / 11), float2(0.0f / 11, 1.0f / 11), float2(0.0f / 11, 2.0f / 11), float2(0.0f / 11, 3.0f / 11), float2(0.0f / 11, 4.0f / 11), float2(0.0f / 11, 5.0f / 11), float2(0.0f / 11, +6.0f / 11), float2(0.0f / 11, 7.0f / 11), float2(0.0f / 11, 8.0f / 11), float2(0.0f / 11, 9.0f / 11), float2(0.0f / 11, 10.0f / 11), float2(0.0f / 11, 11.0f / 11), float2(1.0f / 11, 0.0f / 11), float2(1.0f / 11, 1.0f / 11), float2(1.0f / 11, 2.0f / 11), float2(1.0f / 11, 3.0f / 11), float2(1.0f / +11, 4.0f / 11), float2(1.0f / 11, 5.0f / 11), float2(1.0f / 11, 6.0f / 11), float2(1.0f / 11, 7.0f / 11), float2(1.0f / 11, 8.0f / 11), float2(1.0f / 11, 9.0f / 11), float2(1.0f / 11, 10.0f / 11), float2(1.0f / 11, 11.0f / 11), float2(2.0f / 11, 0.0f / 11), float2(2.0f / 11, 1.0f / 11), float2(2.0f / 11, 2.0f / 11), float2(2.0f / 11, 3.0f / 11), float2(2.0f / 11, 4.0f / 11), float2(2.0f / 11, 5.0f / 11), float2(2.0f / 11, 6.0f / 11), float2(2.0f / 11, 7.0f / 11), float2(2.0f / 11, 8.0f / 11), float2(2.0f / 11, 9.0f / 11), float2(2.0f / 11, 10.0f / 11), float2(2.0f / 11, 11.0f / 11), float2(3.0f / 11, 0.0f / 11), float2(3.0f / 11, 1.0f / 11), float2(3.0f / 11, 2.0f / 11), float2(3.0f / 11, 3.0f / 11), float2(3.0f / 11, 4.0f / 11), float2(3.0f / 11, 5.0f / 11), float2(3.0f / 11, 6.0f / 11), float2(3.0f / 11, 7.0f / 11), float2(3.0f / 11, 8.0f / 11), float2(3.0f / 11, 9.0f / 11), float2(3.0f / 11, 10.0f / 11), float2(3.0f / 11, 11.0f / 11), float2(4.0f / 11, 0.0f / 11), float2(4.0f +/ 11, 1.0f / 11), float2(4.0f / 11, 2.0f / 11), float2(4.0f / 11, 3.0f / 11), float2(4.0f / 11, 4.0f / 11), float2(4.0f / 11, 5.0f / 11), float2(4.0f / 11, 6.0f / 11), float2(4.0f / 11, 7.0f / 11), float2(4.0f / 11, 8.0f / 11), float2(4.0f / 11, 9.0f / 11), float2(4.0f / 11, 10.0f / 11), float2(4.0f / 11, 11.0f / 11), float2(5.0f / 11, 0.0f / 11), float2(5.0f / 11, 1.0f / 11), float2(5.0f / 11, +2.0f / 11), float2(5.0f / 11, 3.0f / 11), float2(5.0f / 11, 4.0f / 11), float2(5.0f / 11, 5.0f / 11), float2(5.0f / 11, 6.0f / 11), float2(5.0f / 11, 7.0f / 11), float2(5.0f / 11, 8.0f / 11), float2(5.0f / 11, 9.0f / 11), float2(5.0f / 11, 10.0f / 11), float2(5.0f / 11, 11.0f / 11), float2(6.0f / +11, 0.0f / 11), float2(6.0f / 11, 1.0f / 11), float2(6.0f / 11, 2.0f / 11), float2(6.0f / 11, 3.0f / 11), float2(6.0f / 11, 4.0f / 11), float2(6.0f / 11, 5.0f / 11), float2(6.0f / 11, 6.0f / 11), float2(6.0f / 11, 7.0f / 11), float2(6.0f / 11, 8.0f / 11), float2(6.0f / 11, 9.0f / 11), float2(6.0f +/ 11, 10.0f / 11), float2(6.0f / 11, 11.0f / 11), float2(7.0f / 11, 0.0f / 11), float2(7.0f / 11, 1.0f / 11), float2(7.0f / 11, 2.0f / 11), float2(7.0f / 11, 3.0f / 11), float2(7.0f / 11, 4.0f / 11), float2(7.0f / 11, 5.0f / 11), float2(7.0f / 11, 6.0f / 11), float2(7.0f / 11, 7.0f / 11), float2(7.0f / 11, 8.0f / 11), float2(7.0f / 11, 9.0f / 11), float2(7.0f / 11, 10.0f / 11), float2(7.0f / 11, 11.0f / 11), float2(8.0f / 11, 0.0f / 11), float2(8.0f / 11, 1.0f / 11), float2(8.0f / 11, 2.0f / 11), float2(8.0f / 11, 3.0f / 11), float2(8.0f / 11, 4.0f / 11), float2(8.0f / 11, 5.0f / 11), float2(8.0f / 11, 6.0f / 11), float2(8.0f / 11, 7.0f / 11), float2(8.0f / 11, 8.0f / 11), float2(8.0f / +11, 9.0f / 11), float2(8.0f / 11, 10.0f / 11), float2(8.0f / 11, 11.0f / 11), float2(9.0f / 11, 0.0f / 11), float2(9.0f / 11, 1.0f / 11), float2(9.0f / 11, 2.0f / 11), float2(9.0f / 11, 3.0f / 11), float2(9.0f / 11, 4.0f / 11), float2(9.0f / 11, 5.0f / 11), float2(9.0f / 11, 6.0f / 11), float2(9.0f / 11, 7.0f / 11), float2(9.0f / 11, 8.0f / 11), float2(9.0f / 11, 9.0f / 11), float2(9.0f / 11, 10.0f / 11), float2(9.0f / 11, 11.0f / 11), float2(10.0f / 11, 0.0f / 11), float2(10.0f / 11, 1.0f / 11), float2(10.0f / 11, 2.0f / 11), float2(10.0f / 11, 3.0f / 11), float2(10.0f / 11, 4.0f / 11), float2(10.0f / 11, 5.0f / 11), float2(10.0f / 11, 6.0f / 11), float2(10.0f / 11, 7.0f / 11), float2(10.0f / 11, 8.0f / 11), float2(10.0f / 11, 9.0f / 11), float2(10.0f / 11, 10.0f / 11), float2(10.0f / 11, 11.0f / 11), float2(11.0f / 11, 0.0f / 11), float2(11.0f / 11, 1.0f / 11), float2(11.0f / 11, 2.0f / 11), float2(11.0f / 11, 3.0f / 11), float2(11.0f / 11, 4.0f / 11), float2(11.0f / 11, 5.0f / 11), float2(11.0f / 11, 6.0f / 11), float2(11.0f / 11, 7.0f / 11), float2(11.0f / 11, 8.0f / 11), float2(11.0f / 11, 9.0f / 11), float2(11.0f / 11, 10.0f / 11), float2(11.0f / 11, 11.0f / 11)}; + +const static uint3 IND[TILE_PRIMS] = {uint3(0, 1, 12), uint3(12, 1, 13), uint3(1, 2, 13), uint3(13, 2, 14), uint3(2, 3, 14), uint3(14, 3, 15), uint3(3, 4, 15), uint3(15, 4, 16), uint3(4, 5, 16), uint3(16, 5, 17), uint3(5, 6, 17), uint3(17, 6, 18), uint3(6, 7, 18), uint3(18, 7, 19), uint3(7, 8, 19), uint3(19, 8, 20), uint3(8, 9, 20), uint3(20, 9, 21), uint3(9, 10, 21), uint3(21, 10, 22), uint3(10, 11, 22), uint3(22, 11, 23), uint3(12, 13, 24), uint3(24, 13, 25), uint3(13, 14, 25), uint3(25, 14, 26), uint3(14, +15, 26), uint3(26, 15, 27), uint3(15, 16, 27), uint3(27, 16, 28), uint3(16, 17, 28), uint3(28, 17, 29), uint3(17, 18, 29), uint3(29, 18, 30), uint3(18, 19, 30), uint3(30, 19, 31), uint3(19, 20, 31), uint3(31, 20, 32), uint3(20, 21, 32), uint3(32, 21, 33), uint3(21, 22, 33), uint3(33, 22, 34), uint3(22, 23, 34), uint3(34, 23, 35), uint3(24, 25, 36), uint3(36, 25, 37), uint3(25, 26, +37), uint3(37, 26, 38), uint3(26, 27, 38), uint3(38, 27, 39), uint3(27, 28, 39), uint3(39, 28, 40), uint3(28, 29, 40), uint3(40, 29, 41), uint3(29, 30, 41), uint3(41, 30, 42), uint3(30, 31, 42), uint3(42, 31, 43), uint3(31, 32, 43), uint3(43, 32, 44), uint3(32, 33, 44), uint3(44, 33, 45), uint3(33, 34, 45), uint3(45, 34, 46), uint3(34, 35, 46), uint3(46, 35, 47), uint3(36, 37, 48), uint3(48, 37, 49), uint3(37, 38, 49), uint3(49, 38, 50), uint3(38, 39, 50), uint3(50, 39, 51), uint3(39, 40, 51), uint3(51, 40, 52), uint3(40, 41, 52), uint3(52, 41, 53), uint3(41, 42, 53), +uint3(53, 42, 54), uint3(42, 43, 54), uint3(54, 43, 55), uint3(43, 44, 55), uint3(55, 44, 56), uint3(44, 45, 56), uint3(56, 45, 57), uint3(45, 46, 57), uint3(57, 46, 58), uint3(46, 47, 58), uint3(58, 47, 59), uint3(48, 49, 60), uint3(60, 49, 61), uint3(49, 50, 61), uint3(61, 50, 62), uint3(50, 51, 62), uint3(62, 51, 63), uint3(51, 52, 63), uint3(63, 52, 64), uint3(52, 53, 64), uint3(64, 53, 65), uint3(53, 54, 65), uint3(65, 54, 66), uint3(54, 55, 66), uint3(66, 55, 67), uint3(55, 56, 67), uint3(67, 56, 68), uint3(56, 57, 68), uint3(68, 57, 69), uint3(57, 58, 69), uint3(69, 58, 70), uint3(58, 59, 70), uint3(70, 59, 71), uint3(60, 61, 72), uint3(72, 61, 73), uint3(61, 62, 73), uint3(73, 62, 74), uint3(62, 63, 74), uint3(74, 63, 75), uint3(63, 64, 75), uint3(75, 64, 76), uint3(64, 65, 76), uint3(76, 65, 77), uint3(65, 66, 77), uint3(77, 66, 78), uint3(66, 67, 78), uint3(78, 67, 79), uint3(67, 68, 79), uint3(79, 68, 80), uint3(68, 69, 80), uint3(80, 69, 81), uint3(69, 70, 81), uint3(81, 70, 82), uint3(70, 71, 82), uint3(82, 71, 83), uint3(72, +73, 84), uint3(84, 73, 85), uint3(73, 74, 85), uint3(85, 74, 86), uint3(74, 75, 86), uint3(86, 75, 87), uint3(75, 76, 87), uint3(87, 76, 88), uint3(76, 77, 88), uint3(88, 77, 89), uint3(77, 78, 89), uint3(89, 78, 90), uint3(78, 79, 90), uint3(90, 79, 91), uint3(79, 80, 91), uint3(91, 80, 92), uint3(80, 81, 92), uint3(92, 81, 93), uint3(81, 82, 93), uint3(93, 82, 94), uint3(82, 83, +94), uint3(94, 83, 95), uint3(84, 85, 96), uint3(96, 85, 97), uint3(85, 86, 97), uint3(97, 86, 98), uint3(86, 87, 98), uint3(98, 87, 99), uint3(87, 88, 99), uint3(99, 88, 100), uint3(88, 89, 100), uint3(100, 89, 101), uint3(89, 90, 101), uint3(101, 90, 102), uint3(90, 91, 102), uint3(102, 91, 103), uint3(91, 92, 103), uint3(103, 92, 104), uint3(92, 93, 104), uint3(104, 93, 105), uint3(93, 94, 105), uint3(105, 94, 106), uint3(94, 95, 106), uint3(106, 95, 107), uint3(96, 97, 108), uint3(108, 97, 109), uint3(97, 98, 109), uint3(109, 98, 110), uint3(98, 99, 110), uint3(110, 99, 111), uint3(99, 100, 111), uint3(111, 100, 112), uint3(100, 101, 112), uint3(112, 101, 113), uint3(101, 102, 113), uint3(113, 102, 114), uint3(102, 103, 114), uint3(114, 103, 115), uint3(103, 104, 115), uint3(115, 104, 116), uint3(104, 105, 116), uint3(116, 105, 117), uint3(105, 106, 117), uint3(117, 106, 118), uint3(106, 107, 118), uint3(118, 107, 119), uint3(108, 109, 120), uint3(120, 109, 121), uint3(109, 110, 121), uint3(121, 110, 122), uint3(110, 111, 122), uint3(122, 111, +123), uint3(111, 112, 123), uint3(123, 112, 124), uint3(112, 113, 124), uint3(124, 113, 125), uint3(113, 114, 125), uint3(125, 114, 126), uint3(114, 115, 126), uint3(126, 115, 127), uint3(115, 116, 127), uint3(127, 116, 128), uint3(116, 117, 128), uint3(128, 117, 129), uint3(117, 118, 129), uint3(129, 118, 130), uint3(118, 119, 130), uint3(130, 119, 131), uint3(120, 121, 132), uint3(132, 121, 133), uint3(121, 122, 133), uint3(133, 122, 134), uint3(122, 123, 134), uint3(134, 123, 135), uint3(123, 124, 135), uint3(135, 124, 136), uint3(124, 125, 136), uint3(136, 125, 137), uint3(125, 126, 137), uint3(137, 126, 138), uint3(126, 127, 138), uint3(138, 127, 139), uint3(127, 128, 139), uint3(139, 128, 140), uint3(128, 129, 140), uint3(140, 129, 141), uint3(129, 130, 141), uint3(141, 130, 142), uint3(130, 131, 142), uint3(142, 131, 143)}; + + +[numthreads(MESH_GROUP_SIZE, 1, 1)] +[outputtopology("triangle")] +[shader("mesh")] +void meshMain( + in uint threadID: SV_GroupThreadID, + in uint groupID: SV_GroupID, + in payload WaterPayload params, + out vertices WaterVertex vertices[TILE_VERTS], + out indices uint3 indices[TILE_PRIMS], +) { + float4x4 vp = mul(pViewParams.projectionMatrix, pViewParams.viewMatrix); + SetMeshOutputCounts(TILE_VERTS, TILE_PRIMS); + + for(uint i = threadID; i < TILE_PRIMS; i += MESH_GROUP_SIZE) + { + indices[i] = IND[i]; + } + for(uint i = threadID; i < TILE_VERTS; i += MESH_GROUP_SIZE) + { + float2 base = VERT[i]; + float2 worldTransformed = params.offset + (params.extent * base); + float3 worldPos = float3(worldTransformed.x, params.height, worldTransformed.y); + float3 displacement1 = pWaterMaterial.displacementTextures.Sample(pWaterMaterial.sampler, float3(worldPos.xz, 0)).xyz; + float3 displacement2 = pWaterMaterial.displacementTextures.Sample(pWaterMaterial.sampler, float3(worldPos.xz, 1)).xyz; + float3 displacement3 = pWaterMaterial.displacementTextures.Sample(pWaterMaterial.sampler, float3(worldPos.xz, 2)).xyz; + float3 displacement4 = pWaterMaterial.displacementTextures.Sample(pWaterMaterial.sampler, float3(worldPos.xz, 3)).xyz; + float3 displacement = displacement1 + displacement2 + displacement3 + displacement4; + + float4 clipPos = mul(vp, float4(worldPos, 1)); + float depth = 1 - clamp(1 / (clipPos.z / clipPos.w), 0, 1); + + displacement = lerp(0, displacement, pow(saturate(depth), pWaterMaterial.displacementDepthAttenuation)); + + worldPos += displacement; + + WaterVertex v; + v.position_WS = worldPos; + v.position_CS = clipPos;// mul(vp, float4(worldPos, 1)); + v.texCoord = worldPos.xz; + v.depth = clipPos.z / clipPos.w; + vertices[i] = v; + } +} \ No newline at end of file diff --git a/res/shaders/WaterPass.slang b/res/shaders/WaterPass.slang index e69de29..99daf85 100644 --- a/res/shaders/WaterPass.slang +++ b/res/shaders/WaterPass.slang @@ -0,0 +1,103 @@ +import Common; +import WaterCommon; + +float SchlickFresnel(float3 normal, float3 viewDir) { + // 0.02f comes from the reflectivity bias of water kinda idk it's from a paper somewhere i'm not gonna link it tho lmaooo + return 0.02f + (1 - 0.02f) * (pow(1 - clamp(dot(normal, viewDir), 0, 1), 5.0f)); +} + +float SmithMaskingBeckmann(float3 H, float3 S, float roughness) { + float hdots = max(0.001f, clamp(dot(H, S), 0, 1)); + float a = hdots / (roughness * sqrt(1 - hdots * hdots)); + float a2 = a * a; + + return a < 1.6f ? (1.0f - 1.259f * a + 0.396f * a2) / (3.535f * a + 2.181 * a2) : 0.0f; +} + +float Beckmann(float ndoth, float roughness) { + float exp_arg = (ndoth * ndoth - 1) / (roughness * roughness * ndoth * ndoth); + + return exp(exp_arg) / (PI * roughness * roughness * ndoth * ndoth * ndoth * ndoth); +} + +[shader("pixel")] +float4 main(WaterVertex vert) : SV_TARGET { + float3 lightDir = -normalize(pWaterMaterial.sunDirection); + float3 viewDir = normalize(pViewParams.cameraPos_WS.xyz - vert.position_WS); + float3 halfwayDir = normalize(lightDir + viewDir); + float depth = vert.depth; + float LdotH = clamp(dot(lightDir, halfwayDir), 0, 1); + float VdotH = clamp(dot(viewDir, halfwayDir), 0, 1); + + + float4 displacementFoam1 = pWaterMaterial.displacementTextures.Sample(pWaterMaterial.sampler, float3(vert.texCoord, 0)); + displacementFoam1.a += pWaterMaterial.foamSubtract0; + float4 displacementFoam2 = pWaterMaterial.displacementTextures.Sample(pWaterMaterial.sampler, float3(vert.texCoord, 0)); + displacementFoam2.a += pWaterMaterial.foamSubtract1; + float4 displacementFoam3 = pWaterMaterial.displacementTextures.Sample(pWaterMaterial.sampler, float3(vert.texCoord, 0)); + displacementFoam3.a += pWaterMaterial.foamSubtract2; + float4 displacementFoam4 = pWaterMaterial.displacementTextures.Sample(pWaterMaterial.sampler, float3(vert.texCoord, 0)); + displacementFoam4.a += pWaterMaterial.foamSubtract3; + float4 displacementFoam = displacementFoam1 + displacementFoam2 + displacementFoam3 + displacementFoam4; + + float2 slopes1 = pWaterMaterial.slopeTextures.Sample(pWaterMaterial.sampler, float3(vert.texCoord, 0)); + float2 slopes2 = pWaterMaterial.slopeTextures.Sample(pWaterMaterial.sampler, float3(vert.texCoord, 1)); + float2 slopes3 = pWaterMaterial.slopeTextures.Sample(pWaterMaterial.sampler, float3(vert.texCoord, 2)); + float2 slopes4 = pWaterMaterial.slopeTextures.Sample(pWaterMaterial.sampler, float3(vert.texCoord, 3)); + float2 slopes = slopes1 + slopes2 + slopes3 + slopes4; + + slopes *= pWaterMaterial.normalStrength; + float foam = lerp(0.0f, saturate(displacementFoam.a), pow(depth, pWaterMaterial.foamDepthAttenuation)); + + float3 macroNormal = float3(0, 1, 0); + float3 mesoNormal = normalize(float3(-slopes.x, 1.0f, -slopes.y)); + mesoNormal = normalize(lerp(float3(0, 1, 0), mesoNormal, pow(saturate(depth), pWaterMaterial.normalDepthAttenuation))); + mesoNormal = normalize(mesoNormal); + + float NdotL = clamp(dot(mesoNormal, lightDir), 0, 1); + + + float a = pWaterMaterial.roughness + foam * pWaterMaterial.foamRoughnessModifier; + float ndoth = max(0.0001f, dot(mesoNormal, halfwayDir)); + + float viewMask = SmithMaskingBeckmann(halfwayDir, viewDir, a); + float lightMask = SmithMaskingBeckmann(halfwayDir, lightDir, a); + + float G = rcp(1 + viewMask + lightMask); + + float eta = 1.33f; + float R = ((eta - 1) * (eta - 1)) / ((eta + 1) * (eta + 1)); + float thetaV = acos(viewDir.y); + + float numerator = pow(1 - dot(mesoNormal, viewDir), 5 * exp(-2.69 * a)); + float F = R + (1 - R) * numerator / (1.0f + 22.7f * pow(a, 1.5f)); + F = saturate(F); + + float3 specular = pWaterMaterial.sunIrradiance * F * G * Beckmann(ndoth, a); + specular /= 4.0f * max(0.001f, clamp(dot(macroNormal, lightDir), 0, 1)); + specular *= clamp(dot(mesoNormal, lightDir), 0, 1); + + float3 envReflection = pWaterMaterial.environmentMap.Sample(pWaterMaterial.sampler, reflect(-viewDir, mesoNormal)).rgb; + envReflection *= pWaterMaterial.environmentLightStrength; + + float H = max(0.0f, displacementFoam.y) * pWaterMaterial.heightModifier; + float3 scatterColor = pWaterMaterial.scatterColor; + float3 bubbleColor = pWaterMaterial.bubbleColor; + float bubbleDensity = pWaterMaterial.bubbleDensity; + + + float k1 = pWaterMaterial.wavePeakScatterStrength * H * pow(clamp(dot(lightDir, -viewDir), 0, 1), 4.0f) * pow(0.5f - 0.5f * dot(lightDir, mesoNormal), 3.0f); + float k2 = pWaterMaterial.scatterStrength * pow(clamp(dot(viewDir, mesoNormal), 0, 1), 2.0f); + float k3 = pWaterMaterial.scatterShadowStrength * NdotL; + float k4 = bubbleDensity; + + float3 scatter = (k1 + k2) * scatterColor * pWaterMaterial.sunIrradiance * rcp(1 + lightMask); + scatter += k3 * scatterColor * pWaterMaterial.sunIrradiance + k4 * bubbleColor * pWaterMaterial.sunIrradiance; + + + float3 output = (1 - F) * scatter + specular + F * envReflection; + output = max(0.0f, output); + output = lerp(output, pWaterMaterial.foamColor, saturate(foam)); + + return float4(output, 1.0f); +} diff --git a/res/shaders/WaterTask.slang b/res/shaders/WaterTask.slang index e69de29..cc38ad4 100644 --- a/res/shaders/WaterTask.slang +++ b/res/shaders/WaterTask.slang @@ -0,0 +1,15 @@ +import Common; +import Scene; +import WaterCommon; + +groupshared WaterPayload p; + +[numthreads(1, 1, 1)] +[shader("amplification")] +void taskMain( + uint threadID: SV_GroupThreadID, + uint groupID: SV_GroupID, ) +{ + p = pWaterMaterial.tiles[groupID]; + DispatchMesh(1, 1, 1, p); +} diff --git a/res/shaders/lib/Common.slang b/res/shaders/lib/Common.slang index d69664e..9a6d749 100644 --- a/res/shaders/lib/Common.slang +++ b/res/shaders/lib/Common.slang @@ -1,6 +1,8 @@ const static float PI = 3.1415926535897932f; const static uint BLOCK_SIZE = 32; static const uint64_t MAX_TEXCOORDS = 8; +static const uint32_t TASK_GROUP_SIZE = 32; +static const uint32_t MESH_GROUP_SIZE = 32; struct ViewParameter { diff --git a/res/shaders/lib/Scene.slang b/res/shaders/lib/Scene.slang index 4126b3a..4eefe2a 100644 --- a/res/shaders/lib/Scene.slang +++ b/res/shaders/lib/Scene.slang @@ -22,8 +22,6 @@ struct MeshData static const uint32_t MAX_VERTICES = 256; static const uint32_t MAX_PRIMITIVES = 256; -static const uint32_t TASK_GROUP_SIZE = 32; -static const uint32_t MESH_GROUP_SIZE = 32; static const uint32_t MAX_MESHLETS_PER_INSTANCE = 2048; struct InstanceData diff --git a/src/Benchmark/PlayView.cpp b/src/Benchmark/PlayView.cpp index e71b2f0..a47ec06 100644 --- a/src/Benchmark/PlayView.cpp +++ b/src/Benchmark/PlayView.cpp @@ -15,41 +15,52 @@ PlayView::PlayView(Gfx::PGraphics graphics, PWindow window, const ViewportCreate Gfx::PPipelineStatisticsQuery baseQuery = res->requestQuery("BASEPASS_QUERY"); Gfx::PPipelineStatisticsQuery lightCullQuery = res->requestQuery("LIGHTCULL_QUERY"); Gfx::PPipelineStatisticsQuery visibilityQuery = res->requestQuery("VISIBILITY_QUERY"); - Gfx::PTimestampQuery timeQuery = res->requestTimestampQuery("TIMESTAMP"); + Gfx::PTimestampQuery cachedTS = res->requestTimestampQuery("CACHED_TS"); + Gfx::PTimestampQuery depthTS = res->requestTimestampQuery("DEPTH_TS"); + Gfx::PTimestampQuery lightCullTS = res->requestTimestampQuery("LIGHTCULL_TS"); + Gfx::PTimestampQuery visibilityTS = res->requestTimestampQuery("VISIBILITY_TS"); + Gfx::PTimestampQuery baseTS = res->requestTimestampQuery("BASE_TS"); std::ofstream stats(fmt::format("stats{}.csv", useMeshCulling ? "" : "NOCULL")); - stats << std::fixed << std::setprecision(0); stats << "RelTime," - << "CachedIAV,CachedIAP,CachedVS,CachedClipInv,CachedClipPrim,CachedFS,CachedCS,CachedTS,CachedMS," - << "DepthIAV,DepthIAP,DepthVS,DepthClipInv,DepthClipPrim,DepthFS,DepthCS,DepthTS,DepthMS," - << "BaseIAV,BaseIAP,BaseVS,BaseClipInv,BaseClipPrim,BaseFS,BaseCS,BaseTS,BaseMS," - << "LightCullIAV,LightCullIAP,LightCullVS,LightCullClipInv,LightCullClipPrim,LightCullFS,LightCullCS,LightCullTS,LightCullMS," - << "VisibilityIAV,VisibilityIAP,VisibilityVS,VisibilityClipInv,VisibilityClipPrim,VisibilityFS,VisibilityCS,VisibilityTS," - "VisibilityMS," - << "CACHED,MIPGEN,DEPTHCULL,VISIBILITY,LIGHTCULL,BASE,FrameTime" << std::endl; - float start = 0; + << "CACHED,MIPGEN,DEPTHCULL,VISIBILITY,LIGHTCULL,BASE,FrameTime," + << "CachedIAV,CachedIAP,CachedVS,CachedClipInv,CachedClipPrim,CachedFS,CachedCS," + << "DepthIAV,DepthIAP,DepthVS,DepthClipInv,DepthClipPrim,DepthFS,DepthCS," + << "BaseIAV,BaseIAP,BaseVS,BaseClipInv,BaseClipPrim,BaseFS,BaseCS," + << "LightCullIAV,LightCullIAP,LightCullVS,LightCullClipInv,LightCullClipPrim,LightCullFS,LightCullCS," + << "VisibilityIAV,VisibilityIAP,VisibilityVS,VisibilityClipInv,VisibilityClipPrim,VisibilityFS,VisibilityCS" << std::endl; + uint64 start = 0; + uint64 prevBegin = 0; std::this_thread::sleep_for(std::chrono::milliseconds(500)); + stats << std::fixed << std::setprecision(0); while (getGlobals().running) { - auto timestamps = timeQuery->getResults(); + auto cached = cachedTS->getResults(); + auto depth = depthTS->getResults(); + auto lightCull = lightCullTS->getResults(); + auto visibility = visibilityTS->getResults(); + auto base = baseTS->getResults(); auto cachedResults = cachedQuery->getResults(); auto depthResults = depthQuery->getResults(); auto baseResults = baseQuery->getResults(); auto lightCullResults = lightCullQuery->getResults(); auto visiblityResults = visibilityQuery->getResults(); if (start == 0) { - start = timestamps[0].time; + start = cached[0].time; } - uint64 t0 = timestamps[0].time; - uint64 t1 = timestamps[1].time; - uint64 t2 = timestamps[2].time; - uint64 t3 = timestamps[3].time; - uint64 t4 = timestamps[4].time; - uint64 t5 = timestamps[5].time; - uint64 t6 = timestamps[6].time; - if (t1 < t0 || t2 < t1 || t3 < t2 || t4 < t3 || t5 < t4 || t6 < t5) - continue; - stats << t0 - start << "," << cachedResults << depthResults << baseResults << lightCullResults << visiblityResults << t1 - t0 - << "," << t2 - t1 << "," << t3 - t2 << "," << t4 - t3 << "," << t5 - t4 << "," << t6 - t5 << "," << t6 - t0 << std::endl; + int64 relTime = cached[0].time - start; + int64 cachedTime = cached[1].time - cached[0].time; + int64 mipTime = depth[1].time - depth[0].time; + int64 depthTime = depth[2].time - depth[0].time; + int64 baseTime = base[1].time - base[0].time; + int64 lightCullTime = lightCull[1].time - lightCull[0].time; + int64 visibilityTime = visibility[1].time - visibility[0].time; + int64 frameTime = cachedTime + mipTime + depthTime + baseTime + lightCullTime + visibilityTime; + + stats << relTime << "," << cachedTime << "," << mipTime << "," << depthTime << "," + << visibilityTime + << "," << lightCullTime << "," << baseTime << "," + << frameTime << "," << cachedResults << depthResults << baseResults << lightCullResults << visiblityResults << std::endl; stats.flush(); + // std::cout << "Writing " << timestamps[0].time << std::endl; } }); } diff --git a/src/Editor/main.cpp b/src/Editor/main.cpp index e90649b..acf907a 100644 --- a/src/Editor/main.cpp +++ b/src/Editor/main.cpp @@ -64,10 +64,10 @@ int main() { .filePath = sourcePath / "import/textures/skyboxsun5deg_tn.jpg", .type = TextureImportType::TEXTURE_CUBEMAP, }); - AssetImporter::importMesh(MeshImportArgs{ - .filePath = sourcePath / "import/models/after-the-rain-vr-sound/source/Whitechapel.glb", - .importPath = "Whitechapel", - }); + //ssetImporter::importMesh(MeshImportArgs{ + // .filePath = sourcePath / "import/models/after-the-rain-vr-sound/source/Whitechapel.glb", + // .importPath = "Whitechapel", + //); // AssetImporter::importMesh(MeshImportArgs{ // .filePath = sourcePath / "import/models/city-suburbs/source/city-suburbs.obj", // .importPath = "suburbs", diff --git a/src/Engine/Asset/FontAsset.cpp b/src/Engine/Asset/FontAsset.cpp index 3614d76..dcc65f8 100644 --- a/src/Engine/Asset/FontAsset.cpp +++ b/src/Engine/Asset/FontAsset.cpp @@ -86,9 +86,9 @@ void FontAsset::load(ArchiveBuffer& buffer) { .width = kTexture->baseWidth, .height = kTexture->baseHeight, .depth = kTexture->baseDepth, - .mipLevels = kTexture->numLevels, .layers = kTexture->numFaces, .elements = kTexture->numLayers, + .useMip = true, .usage = Gfx::SE_IMAGE_USAGE_SAMPLED_BIT, }; diff --git a/src/Engine/Asset/TextureAsset.cpp b/src/Engine/Asset/TextureAsset.cpp index 492acc0..6517525 100644 --- a/src/Engine/Asset/TextureAsset.cpp +++ b/src/Engine/Asset/TextureAsset.cpp @@ -47,9 +47,9 @@ void TextureAsset::load(ArchiveBuffer& buffer) { .width = ktxHandle->baseWidth, .height = ktxHandle->baseHeight, .depth = ktxHandle->baseDepth, - .mipLevels = ktxHandle->numLevels, .layers = ktxHandle->numFaces, .elements = ktxHandle->numLayers, + .useMip = true, .usage = Gfx::SE_IMAGE_USAGE_SAMPLED_BIT, .name = name, }; diff --git a/src/Engine/Component/CMakeLists.txt b/src/Engine/Component/CMakeLists.txt index 14f6240..6e03346 100644 --- a/src/Engine/Component/CMakeLists.txt +++ b/src/Engine/Component/CMakeLists.txt @@ -17,7 +17,8 @@ target_sources(Engine Skybox.h SphereCollider.h Transform.h - Transform.cpp) + Transform.cpp + WaterTile.h) target_sources(Engine @@ -36,4 +37,5 @@ target_sources(Engine ShapeBase.h Skybox.h SphereCollider.h - Transform.h) \ No newline at end of file + Transform.h + WaterTile.h) \ No newline at end of file diff --git a/src/Engine/Component/WaterTile.h b/src/Engine/Component/WaterTile.h new file mode 100644 index 0000000..bb4582c --- /dev/null +++ b/src/Engine/Component/WaterTile.h @@ -0,0 +1,15 @@ +#pragma once +#include "MinimalEngine.h" +#include "Math/Vector.h" + +namespace Seele { +namespace Component +{ +struct WaterTile +{ + IVector2 location; + float height; + constexpr static float DIMENSIONS = 100; +}; +} +} \ No newline at end of file diff --git a/src/Engine/Graphics/Enums.h b/src/Engine/Graphics/Enums.h index aabceac..b900614 100644 --- a/src/Engine/Graphics/Enums.h +++ b/src/Engine/Graphics/Enums.h @@ -159,10 +159,10 @@ static constexpr bool useAsyncCompute = false; static constexpr bool useMeshShading = true; static constexpr uint32 numFramesBuffered = 3; -// meshlet dimensions curated by NVIDIA static constexpr uint32 numVerticesPerMeshlet = 256; static constexpr uint32 numPrimitivesPerMeshlet = 256; double getCurrentFrameDelta(); +double getCurrentFrameTime(); uint32 getCurrentFrameIndex(); enum class QueueType { diff --git a/src/Engine/Graphics/Graphics.h b/src/Engine/Graphics/Graphics.h index 63b7a0f..efd9b86 100644 --- a/src/Engine/Graphics/Graphics.h +++ b/src/Engine/Graphics/Graphics.h @@ -62,6 +62,7 @@ class Graphics { virtual void waitDeviceIdle() = 0; virtual void executeCommands(Array commands) = 0; + virtual void executeCommands(OComputeCommand commands) = 0; virtual void executeCommands(Array commands) = 0; virtual OTexture2D createTexture2D(const TextureCreateInfo& createInfo) = 0; diff --git a/src/Engine/Graphics/Initializer.h b/src/Engine/Graphics/Initializer.h index 4cffa8e..4277b8e 100644 --- a/src/Engine/Graphics/Initializer.h +++ b/src/Engine/Graphics/Initializer.h @@ -50,10 +50,10 @@ struct TextureCreateInfo { uint32 width = 1; uint32 height = 1; uint32 depth = 1; - uint32 mipLevels = 1; uint32 layers = 1; uint32 elements = 1; uint32 samples = 1; + bool useMip = false; Gfx::SeImageUsageFlags usage = Gfx::SE_IMAGE_USAGE_SAMPLED_BIT; Gfx::SeMemoryPropertyFlags memoryProps = Gfx::SE_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; std::string name; @@ -111,6 +111,7 @@ struct ShaderCompilationInfo { Array> typeParameter; Map defines; Gfx::PPipelineLayout rootSignature; + bool dumpIntermediate = false; }; struct ShaderCreateInfo { uint32 entryPointIndex; diff --git a/src/Engine/Graphics/RenderPass/BasePass.cpp b/src/Engine/Graphics/RenderPass/BasePass.cpp index f61b7f2..ffd831c 100644 --- a/src/Engine/Graphics/RenderPass/BasePass.cpp +++ b/src/Engine/Graphics/RenderPass/BasePass.cpp @@ -3,6 +3,7 @@ #include "Asset/AssetRegistry.h" #include "Component/Camera.h" #include "Component/Mesh.h" +#include "Component/WaterTile.h" #include "Graphics/Command.h" #include "Graphics/Descriptor.h" #include "Graphics/Enums.h" @@ -25,6 +26,7 @@ void Seele::addDebugVertex(DebugVertex vert) { gDebugVertices.add(vert); } void Seele::addDebugVertices(Array verts) { gDebugVertices.addAll(verts); } BasePass::BasePass(Gfx::PGraphics graphics, PScene scene) : RenderPass(graphics, scene) { + waterRenderer = new WaterRenderer(graphics, scene, viewParamsLayout); basePassLayout = graphics->createPipelineLayout("BasePassLayout"); basePassLayout->addDescriptorLayout(viewParamsLayout); @@ -96,37 +98,42 @@ void BasePass::beginFrame(const Component::Camera& cam) { opaqueCulling = lightCullingLayout->allocateDescriptorSet(); transparentCulling = lightCullingLayout->allocateDescriptorSet(); - // Debug vertices - VertexBufferCreateInfo vertexBufferInfo = { - .sourceData = - { - .size = sizeof(DebugVertex) * gDebugVertices.size(), - .data = (uint8*)gDebugVertices.data(), - }, - .vertexSize = sizeof(DebugVertex), - .numVertices = (uint32)gDebugVertices.size(), - .name = "DebugVertices", - }; - debugVertices = graphics->createVertexBuffer(vertexBufferInfo); - debugVertices->pipelineBarrier(Gfx::SE_ACCESS_TRANSFER_WRITE_BIT, Gfx::SE_PIPELINE_STAGE_TRANSFER_BIT, - Gfx::SE_ACCESS_VERTEX_ATTRIBUTE_READ_BIT, Gfx::SE_PIPELINE_STAGE_VERTEX_INPUT_BIT); + waterRenderer->beginFrame(); + // Debug vertices + { + VertexBufferCreateInfo vertexBufferInfo = { + .sourceData = + { + .size = sizeof(DebugVertex) * gDebugVertices.size(), + .data = (uint8*)gDebugVertices.data(), + }, + .vertexSize = sizeof(DebugVertex), + .numVertices = (uint32)gDebugVertices.size(), + .name = "DebugVertices", + }; + debugVertices = graphics->createVertexBuffer(vertexBufferInfo); + debugVertices->pipelineBarrier(Gfx::SE_ACCESS_TRANSFER_WRITE_BIT, Gfx::SE_PIPELINE_STAGE_TRANSFER_BIT, + Gfx::SE_ACCESS_VERTEX_ATTRIBUTE_READ_BIT, Gfx::SE_PIPELINE_STAGE_VERTEX_INPUT_BIT); + } // Skybox - skyboxDataLayout->reset(); - textureLayout->reset(); - skyboxData.transformMatrix = glm::rotate(skyboxData.transformMatrix, (float)(Gfx::getCurrentFrameDelta()), Vector(0, 1, 0)); - skyboxBuffer->rotateBuffer(sizeof(SkyboxData)); - skyboxBuffer->updateContents(0, sizeof(SkyboxData), &skyboxData); - skyboxBuffer->pipelineBarrier(Gfx::SE_ACCESS_TRANSFER_WRITE_BIT, Gfx::SE_PIPELINE_STAGE_TRANSFER_BIT, Gfx::SE_ACCESS_UNIFORM_READ_BIT, - Gfx::SE_PIPELINE_STAGE_FRAGMENT_SHADER_BIT); - skyboxDataSet = skyboxDataLayout->allocateDescriptorSet(); - skyboxDataSet->updateBuffer(0, skyboxBuffer); - skyboxDataSet->writeChanges(); - textureSet = textureLayout->allocateDescriptorSet(); - textureSet->updateTexture(0, skybox.day); - textureSet->updateTexture(1, skybox.night); - textureSet->updateSampler(2, skyboxSampler); - textureSet->writeChanges(); + { + skyboxDataLayout->reset(); + textureLayout->reset(); + skyboxData.transformMatrix = glm::rotate(skyboxData.transformMatrix, (float)(Gfx::getCurrentFrameDelta()), Vector(0, 1, 0)); + skyboxBuffer->rotateBuffer(sizeof(SkyboxData)); + skyboxBuffer->updateContents(0, sizeof(SkyboxData), &skyboxData); + skyboxBuffer->pipelineBarrier(Gfx::SE_ACCESS_TRANSFER_WRITE_BIT, Gfx::SE_PIPELINE_STAGE_TRANSFER_BIT, + Gfx::SE_ACCESS_UNIFORM_READ_BIT, Gfx::SE_PIPELINE_STAGE_FRAGMENT_SHADER_BIT); + skyboxDataSet = skyboxDataLayout->allocateDescriptorSet(); + skyboxDataSet->updateBuffer(0, skyboxBuffer); + skyboxDataSet->writeChanges(); + textureSet = textureLayout->allocateDescriptorSet(); + textureSet->updateTexture(0, skybox.day); + textureSet->updateTexture(1, skybox.night); + textureSet->updateSampler(2, skyboxSampler); + textureSet->writeChanges(); + } } void BasePass::render() { @@ -138,13 +145,15 @@ void BasePass::render() { transparentCulling->writeChanges(); query->beginQuery(); - timestamps->write(Gfx::SE_PIPELINE_STAGE_TOP_OF_PIPE_BIT, "BASEPASS"); + timestamps->begin(); + timestamps->write(Gfx::SE_PIPELINE_STAGE_TOP_OF_PIPE_BIT, "BaseBegin"); graphics->beginRenderPass(renderPass); Array commands; Gfx::ShaderPermutation permutation = graphics->getShaderCompiler()->getTemplate("BasePass"); permutation.setDepthCulling(true); // always use the culling info permutation.setPositionOnly(false); Array transparentData; + // Base Rendering for (VertexData* vertexData : VertexData::getList()) { transparentData.addAll(vertexData->getTransparentData()); vertexData->getInstanceDataSet()->updateBuffer(6, cullingBuffer); @@ -238,14 +247,18 @@ void BasePass::render() { commands.add(std::move(command)); } } - - Gfx::ORenderCommand skyboxCommand = graphics->createRenderCommand("SkyboxRender"); - skyboxCommand->setViewport(viewport); - skyboxCommand->bindPipeline(pipeline); - skyboxCommand->bindDescriptor({viewParamsSet, skyboxDataSet, textureSet}); - skyboxCommand->draw(36, 1, 0, 0); - commands.add(std::move(skyboxCommand)); - + + commands.add(waterRenderer->render(viewParamsSet)); + + // Skybox + { + Gfx::ORenderCommand skyboxCommand = graphics->createRenderCommand("SkyboxRender"); + skyboxCommand->setViewport(viewport); + skyboxCommand->bindPipeline(pipeline); + skyboxCommand->bindDescriptor({viewParamsSet, skyboxDataSet, textureSet}); + skyboxCommand->draw(36, 1, 0, 0); + commands.add(std::move(skyboxCommand)); + } // Transparent rendering { permutation.setDepthCulling(false); // ignore visibility infos for transparency @@ -360,7 +373,7 @@ void BasePass::render() { graphics->executeCommands(std::move(commands)); graphics->endRenderPass(); query->endQuery(); - timestamps->write(Gfx::SE_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, "END"); + timestamps->write(Gfx::SE_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, "BaseEnd"); timestamps->end(); gDebugVertices.clear(); } @@ -412,12 +425,13 @@ void BasePass::publishOutputs() { resources->registerRenderPassOutput("BASEPASS_COLOR", colorAttachment); resources->registerRenderPassOutput("BASEPASS_DEPTH", depthAttachment); + timestamps = graphics->createTimestampQuery(2, "BaseTS"); + resources->registerTimestampQueryOutput("BASE_TS", timestamps); query = graphics->createPipelineStatisticsQuery("BasePassPipelineStatistics"); resources->registerQueryOutput("BASEPASS_QUERY", query); } void BasePass::createRenderPass() { - timestamps = resources->requestTimestampQuery("TIMESTAMP"); cullingBuffer = resources->requestBuffer("CULLINGBUFFER"); Gfx::RenderTargetLayout layout = Gfx::RenderTargetLayout{ @@ -458,6 +472,8 @@ void BasePass::createRenderPass() { oLightGrid = resources->requestTexture("LIGHTCULLING_OLIGHTGRID"); tLightGrid = resources->requestTexture("LIGHTCULLING_TLIGHTGRID"); + waterRenderer->setViewport(viewport, renderPass); + // Debug rendering { debugPipelineLayout = graphics->createPipelineLayout("DebugPassLayout"); @@ -527,7 +543,6 @@ void BasePass::createRenderPass() { }; debugPipeline = graphics->createGraphicsPipeline(std::move(gfxInfo)); } - // Skybox { skyboxDataLayout = graphics->createDescriptorLayout("pSkyboxData"); diff --git a/src/Engine/Graphics/RenderPass/BasePass.h b/src/Engine/Graphics/RenderPass/BasePass.h index 101b3f7..e2123c6 100644 --- a/src/Engine/Graphics/RenderPass/BasePass.h +++ b/src/Engine/Graphics/RenderPass/BasePass.h @@ -1,6 +1,7 @@ #pragma once #include "MinimalEngine.h" #include "RenderPass.h" +#include "WaterRenderer.h" namespace Seele { DECLARE_REF(CameraActor) @@ -44,10 +45,12 @@ class BasePass : public RenderPass { Gfx::ODescriptorLayout lightCullingLayout; Gfx::OPipelineStatisticsQuery query; - Gfx::PTimestampQuery timestamps; + Gfx::OTimestampQuery timestamps; Gfx::PShaderBuffer cullingBuffer; + OWaterRenderer waterRenderer; + // Debug rendering Gfx::OVertexInput debugVertexInput; Gfx::OVertexBuffer debugVertices; diff --git a/src/Engine/Graphics/RenderPass/CMakeLists.txt b/src/Engine/Graphics/RenderPass/CMakeLists.txt index c7dc368..34904c2 100644 --- a/src/Engine/Graphics/RenderPass/CMakeLists.txt +++ b/src/Engine/Graphics/RenderPass/CMakeLists.txt @@ -20,7 +20,9 @@ target_sources(Engine UIPass.h UIPass.cpp VisibilityPass.h - VisibilityPass.cpp) + VisibilityPass.cpp + WaterRenderer.h + WaterRenderer.cpp) target_sources(Engine PUBLIC FILE_SET HEADERS diff --git a/src/Engine/Graphics/RenderPass/CachedDepthPass.cpp b/src/Engine/Graphics/RenderPass/CachedDepthPass.cpp index 75ee601..ac7b6df 100644 --- a/src/Engine/Graphics/RenderPass/CachedDepthPass.cpp +++ b/src/Engine/Graphics/RenderPass/CachedDepthPass.cpp @@ -47,13 +47,13 @@ void CachedDepthPass::beginFrame(const Component::Camera& cam) { void CachedDepthPass::render() { query->beginQuery(); timestamps->begin(); - timestamps->write(Gfx::SE_PIPELINE_STAGE_TOP_OF_PIPE_BIT, "CACHED"); + timestamps->write(Gfx::SE_PIPELINE_STAGE_TOP_OF_PIPE_BIT, "CachedBegin"); graphics->beginRenderPass(renderPass); Array commands; Gfx::ShaderPermutation permutation = graphics->getShaderCompiler()->getTemplate("CachedDepthPass"); permutation.setPositionOnly(getGlobals().usePositionOnly); - permutation.setDepthCulling(getGlobals().useDepthCulling); + permutation.setDepthCulling(true); for (VertexData* vertexData : VertexData::getList()) { permutation.setVertexData(vertexData->getTypeName()); vertexData->getInstanceDataSet()->updateBuffer(6, cullingBuffer); @@ -127,6 +127,8 @@ void CachedDepthPass::render() { graphics->executeCommands(std::move(commands)); graphics->endRenderPass(); + timestamps->write(Gfx::SE_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, "CachedEnd"); + timestamps->end(); query->endQuery(); } @@ -161,8 +163,8 @@ void CachedDepthPass::publishOutputs() { query = graphics->createPipelineStatisticsQuery("CachedPipelineStatistics"); resources->registerQueryOutput("CACHED_QUERY", query); - timestamps = graphics->createTimestampQuery(7, "Timestamps"); - resources->registerTimestampQueryOutput("TIMESTAMP", timestamps); + timestamps = graphics->createTimestampQuery(2, "CachedTS"); + resources->registerTimestampQueryOutput("CACHED_TS", timestamps); } void CachedDepthPass::createRenderPass() { diff --git a/src/Engine/Graphics/RenderPass/DepthCullingPass.cpp b/src/Engine/Graphics/RenderPass/DepthCullingPass.cpp index fc91f06..2ac33e5 100644 --- a/src/Engine/Graphics/RenderPass/DepthCullingPass.cpp +++ b/src/Engine/Graphics/RenderPass/DepthCullingPass.cpp @@ -78,7 +78,8 @@ void DepthCullingPass::render() { set->updateBuffer(1, depthMipBuffer); set->writeChanges(); - timestamps->write(Gfx::SE_PIPELINE_STAGE_TOP_OF_PIPE_BIT, "DEPTHMIP"); + timestamps->begin(); + timestamps->write(Gfx::SE_PIPELINE_STAGE_TOP_OF_PIPE_BIT, "MipBegin"); Gfx::OComputeCommand computeCommand = graphics->createComputeCommand("DepthMipGenCommand"); computeCommand->bindPipeline(depthInitialReduce); computeCommand->bindDescriptor({viewParamsSet, set}); @@ -115,7 +116,7 @@ void DepthCullingPass::render() { Gfx::SE_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | Gfx::SE_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT, Gfx::SE_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT); - timestamps->write(Gfx::SE_PIPELINE_STAGE_TOP_OF_PIPE_BIT, "DEPTHCULL"); + timestamps->write(Gfx::SE_PIPELINE_STAGE_TOP_OF_PIPE_BIT, "CullingBegin"); graphics->beginRenderPass(renderPass); Array commands; @@ -194,6 +195,8 @@ void DepthCullingPass::render() { graphics->executeCommands(std::move(commands)); graphics->endRenderPass(); + timestamps->write(Gfx::SE_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, "CullingEnd"); + timestamps->end(); query->endQuery(); // Sync depth read/write with compute read depthAttachment.getTexture()->pipelineBarrier( @@ -251,12 +254,13 @@ void DepthCullingPass::publishOutputs() { depthMipGen = graphics->createComputePipeline(pipelineCreateInfo); + timestamps = graphics->createTimestampQuery(3, "CullingTS"); + resources->registerTimestampQueryOutput("DEPTH_TS", timestamps); query = graphics->createPipelineStatisticsQuery("DepthPipelineStatistics"); resources->registerQueryOutput("DEPTH_QUERY", query); } void DepthCullingPass::createRenderPass() { - timestamps = resources->requestTimestampQuery("TIMESTAMP"); cullingBuffer = resources->requestBuffer("CULLINGBUFFER"); depthAttachment = resources->requestRenderTarget("DEPTHPREPASS_DEPTH"); diff --git a/src/Engine/Graphics/RenderPass/DepthCullingPass.h b/src/Engine/Graphics/RenderPass/DepthCullingPass.h index 03f61d2..b9407f7 100644 --- a/src/Engine/Graphics/RenderPass/DepthCullingPass.h +++ b/src/Engine/Graphics/RenderPass/DepthCullingPass.h @@ -33,7 +33,7 @@ class DepthCullingPass : public RenderPass { Gfx::ODescriptorLayout depthAttachmentLayout; Gfx::OPipelineLayout depthCullingLayout; Gfx::OPipelineStatisticsQuery query; - Gfx::PTimestampQuery timestamps; + Gfx::OTimestampQuery timestamps; Gfx::OPipelineLayout depthComputeLayout; Gfx::OComputeShader depthInitialReduceShader; diff --git a/src/Engine/Graphics/RenderPass/LightCullingPass.cpp b/src/Engine/Graphics/RenderPass/LightCullingPass.cpp index 8b5f050..1ccbc51 100644 --- a/src/Engine/Graphics/RenderPass/LightCullingPass.cpp +++ b/src/Engine/Graphics/RenderPass/LightCullingPass.cpp @@ -37,7 +37,8 @@ void LightCullingPass::beginFrame(const Component::Camera& cam) { void LightCullingPass::render() { query->beginQuery(); - timestamps->write(Gfx::SE_PIPELINE_STAGE_TOP_OF_PIPE_BIT, "LIGHTCULL"); + timestamps->begin(); + timestamps->write(Gfx::SE_PIPELINE_STAGE_TOP_OF_PIPE_BIT, "LightCullBegin"); oLightGrid->pipelineBarrier(Gfx::SE_ACCESS_SHADER_READ_BIT, Gfx::SE_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, Gfx::SE_ACCESS_SHADER_WRITE_BIT, Gfx::SE_PIPELINE_STAGE_COMPUTE_SHADER_BIT); tLightGrid->pipelineBarrier(Gfx::SE_ACCESS_SHADER_READ_BIT, Gfx::SE_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, Gfx::SE_ACCESS_SHADER_WRITE_BIT, @@ -62,6 +63,8 @@ void LightCullingPass::render() { commands.add(std::move(computeCommand)); // std::cout << "Execute" << std::endl; graphics->executeCommands(std::move(commands)); + timestamps->write(Gfx::SE_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, "LightCullEnd"); + timestamps->end(); query->endQuery(); oLightIndexList->pipelineBarrier(Gfx::SE_ACCESS_SHADER_WRITE_BIT, Gfx::SE_PIPELINE_STAGE_COMPUTE_SHADER_BIT, Gfx::SE_ACCESS_SHADER_READ_BIT, Gfx::SE_PIPELINE_STAGE_FRAGMENT_SHADER_BIT); @@ -224,13 +227,14 @@ void LightCullingPass::publishOutputs() { resources->registerTextureOutput("LIGHTCULLING_OLIGHTGRID", Gfx::PTexture2D(oLightGrid)); resources->registerTextureOutput("LIGHTCULLING_TLIGHTGRID", Gfx::PTexture2D(tLightGrid)); + timestamps = graphics->createTimestampQuery(2, "LightCullTS"); + resources->registerTimestampQueryOutput("LIGHTCULL_TS", timestamps); query = graphics->createPipelineStatisticsQuery("LightCullPipelineStatistics"); resources->registerQueryOutput("LIGHTCULL_QUERY", query); } void LightCullingPass::createRenderPass() { depthAttachment = resources->requestRenderTarget("DEPTHPREPASS_DEPTH").getTexture(); - timestamps = resources->requestTimestampQuery("TIMESTAMP"); } void LightCullingPass::setupFrustums() { diff --git a/src/Engine/Graphics/RenderPass/LightCullingPass.h b/src/Engine/Graphics/RenderPass/LightCullingPass.h index 191c38f..0277e64 100644 --- a/src/Engine/Graphics/RenderPass/LightCullingPass.h +++ b/src/Engine/Graphics/RenderPass/LightCullingPass.h @@ -64,7 +64,7 @@ class LightCullingPass : public RenderPass { Gfx::PComputePipeline cullingPipeline; Gfx::PComputePipeline cullingEnabledPipeline; Gfx::OPipelineStatisticsQuery query; - Gfx::PTimestampQuery timestamps; + Gfx::OTimestampQuery timestamps; }; DEFINE_REF(LightCullingPass) } // namespace Seele diff --git a/src/Engine/Graphics/RenderPass/VisibilityPass.cpp b/src/Engine/Graphics/RenderPass/VisibilityPass.cpp index bcdf919..e73481e 100644 --- a/src/Engine/Graphics/RenderPass/VisibilityPass.cpp +++ b/src/Engine/Graphics/RenderPass/VisibilityPass.cpp @@ -26,7 +26,8 @@ void VisibilityPass::render() { visibilitySet->writeChanges(); query->beginQuery(); - timestamps->write(Gfx::SE_PIPELINE_STAGE_TOP_OF_PIPE_BIT, "VISIBILITY"); + timestamps->begin(); + timestamps->write(Gfx::SE_PIPELINE_STAGE_TOP_OF_PIPE_BIT, "VisibilityBegin"); Gfx::OComputeCommand command = graphics->createComputeCommand("VisibilityCommand"); command->bindPipeline(visibilityPipeline); command->bindDescriptor({viewParamsSet, visibilitySet}); @@ -34,6 +35,8 @@ void VisibilityPass::render() { Array commands; commands.add(std::move(command)); graphics->executeCommands(std::move(commands)); + timestamps->write(Gfx::SE_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, "VisibilityEnd"); + timestamps->end(); query->endQuery(); cullingBuffer->pipelineBarrier(Gfx::SE_ACCESS_SHADER_WRITE_BIT, Gfx::SE_PIPELINE_STAGE_COMPUTE_SHADER_BIT, Gfx::SE_ACCESS_SHADER_READ_BIT, @@ -85,8 +88,10 @@ void VisibilityPass::publishOutputs() { }); resources->registerBufferOutput("CULLINGBUFFER", cullingBuffer); + timestamps = graphics->createTimestampQuery(2, "VisibilityTS"); + resources->registerTimestampQueryOutput("VISIBILITY_TS", timestamps); + query = graphics->createPipelineStatisticsQuery("VisibilityPipelineStatistics"); - timestamps = resources->requestTimestampQuery("TIMESTAMP"); resources->registerQueryOutput("VISIBILITY_QUERY", query); } diff --git a/src/Engine/Graphics/RenderPass/VisibilityPass.h b/src/Engine/Graphics/RenderPass/VisibilityPass.h index 75c681e..43f0eff 100644 --- a/src/Engine/Graphics/RenderPass/VisibilityPass.h +++ b/src/Engine/Graphics/RenderPass/VisibilityPass.h @@ -24,7 +24,7 @@ class VisibilityPass : public RenderPass { Gfx::OComputeShader visibilityShader; Gfx::PComputePipeline visibilityPipeline; Gfx::OPipelineStatisticsQuery query; - Gfx::PTimestampQuery timestamps; + Gfx::OTimestampQuery timestamps; // Holds culling information for every meshlet for each instance Gfx::OShaderBuffer cullingBuffer; diff --git a/src/Engine/Graphics/RenderPass/WaterRenderer.cpp b/src/Engine/Graphics/RenderPass/WaterRenderer.cpp new file mode 100644 index 0000000..8884769 --- /dev/null +++ b/src/Engine/Graphics/RenderPass/WaterRenderer.cpp @@ -0,0 +1,474 @@ +#include "WaterRenderer.h" +#include "Asset/AssetRegistry.h" +#include "Component/WaterTile.h" +#include "Graphics/Graphics.h" +#include "Graphics/Shader.h" + +using namespace Seele; + +WaterRenderer::WaterRenderer(Gfx::PGraphics graphics, PScene scene, Gfx::PDescriptorLayout viewParamsLayout) + : graphics(graphics), scene(scene) { + skyBox = AssetRegistry::findTexture("", "skyboxsun5deg_tn")->getTexture().cast(); + logN = (int)log2(params.N); + threadGroupsX = ceil(params.N / 8.0f); + threadGroupsY = ceil(params.N / 8.0f); + + materialUniforms = graphics->createUniformBuffer(UniformBufferCreateInfo{ + .sourceData = + { + .size = sizeof(MaterialParams), + .data = (uint8*)&materialParams, + }, + .dynamic = true, + .name = "WaterMaterialParams", + }); + materialUniforms->pipelineBarrier(Gfx::SE_ACCESS_TRANSFER_WRITE_BIT, Gfx::SE_PIPELINE_STAGE_TRANSFER_BIT, + Gfx::SE_ACCESS_UNIFORM_READ_BIT, + Gfx::SE_PIPELINE_STAGE_MESH_SHADER_BIT_EXT | Gfx::SE_PIPELINE_STAGE_FRAGMENT_SHADER_BIT); + + paramsBuffer = graphics->createUniformBuffer(UniformBufferCreateInfo{ + .sourceData = + { + .size = sizeof(ComputeParams), + }, + .dynamic = true, + .name = "WaterComputeParams", + }); + paramsBuffer->pipelineBarrier(Gfx::SE_ACCESS_TRANSFER_WRITE_BIT, Gfx::SE_PIPELINE_STAGE_TRANSFER_BIT, Gfx::SE_ACCESS_UNIFORM_READ_BIT, + Gfx::SE_PIPELINE_STAGE_COMPUTE_SHADER_BIT); + + initialSpectrumTextures = graphics->createTexture2D(TextureCreateInfo{ + .format = Gfx::SE_FORMAT_R32G32B32A32_SFLOAT, + .width = params.N, + .height = params.N, + .elements = 4, + .useMip = true, + .usage = Gfx::SE_IMAGE_USAGE_STORAGE_BIT, + .name = "InitialSpectrumTextures", + }); + initialSpectrumTextures->changeLayout(Gfx::SE_IMAGE_LAYOUT_GENERAL, Gfx::SE_ACCESS_NONE, Gfx::SE_PIPELINE_STAGE_TOP_OF_PIPE_BIT, + Gfx::SE_ACCESS_SHADER_READ_BIT | Gfx::SE_ACCESS_SHADER_WRITE_BIT, + Gfx::SE_PIPELINE_STAGE_COMPUTE_SHADER_BIT); + + boyancyData = graphics->createTexture2D(TextureCreateInfo{ + .format = Gfx::SE_FORMAT_R16_SFLOAT, + .width = params.N, + .height = params.N, + .useMip = false, + .usage = Gfx::SE_IMAGE_USAGE_STORAGE_BIT, + .name = "Bouyancy", + }); + boyancyData->changeLayout(Gfx::SE_IMAGE_LAYOUT_GENERAL, Gfx::SE_ACCESS_NONE, Gfx::SE_PIPELINE_STAGE_TOP_OF_PIPE_BIT, + Gfx::SE_ACCESS_SHADER_READ_BIT | Gfx::SE_ACCESS_SHADER_WRITE_BIT, Gfx::SE_PIPELINE_STAGE_COMPUTE_SHADER_BIT); + + displacementTextures = graphics->createTexture2D(TextureCreateInfo{ + .format = Gfx::SE_FORMAT_R32G32B32A32_SFLOAT, + .width = params.N, + .height = params.N, + .elements = 4, + .useMip = true, + .usage = Gfx::SE_IMAGE_USAGE_STORAGE_BIT, + .name = "DisplacementTexture", + }); + displacementTextures->changeLayout(Gfx::SE_IMAGE_LAYOUT_GENERAL, Gfx::SE_ACCESS_NONE, Gfx::SE_PIPELINE_STAGE_TOP_OF_PIPE_BIT, + Gfx::SE_ACCESS_SHADER_READ_BIT | Gfx::SE_ACCESS_SHADER_WRITE_BIT, + Gfx::SE_PIPELINE_STAGE_COMPUTE_SHADER_BIT); + + slopeTextures = graphics->createTexture2D(TextureCreateInfo{ + .format = Gfx::SE_FORMAT_R32G32_SFLOAT, + .width = params.N, + .height = params.N, + .elements = 4, + .useMip = true, + .usage = Gfx::SE_IMAGE_USAGE_STORAGE_BIT, + .name = "SlopeTextures", + }); + slopeTextures->changeLayout(Gfx::SE_IMAGE_LAYOUT_GENERAL, Gfx::SE_ACCESS_NONE, Gfx::SE_PIPELINE_STAGE_TOP_OF_PIPE_BIT, + Gfx::SE_ACCESS_SHADER_READ_BIT | Gfx::SE_ACCESS_SHADER_WRITE_BIT, + Gfx::SE_PIPELINE_STAGE_COMPUTE_SHADER_BIT); + + spectrumTextures = graphics->createTexture2D(TextureCreateInfo{ + .format = Gfx::SE_FORMAT_R32G32B32A32_SFLOAT, + .width = params.N, + .height = params.N, + .elements = 8, + .useMip = true, + .usage = Gfx::SE_IMAGE_USAGE_STORAGE_BIT, + .name = "SpectrumTextures", + }); + spectrumTextures->changeLayout(Gfx::SE_IMAGE_LAYOUT_GENERAL, Gfx::SE_ACCESS_NONE, Gfx::SE_PIPELINE_STAGE_TOP_OF_PIPE_BIT, + Gfx::SE_ACCESS_SHADER_READ_BIT | Gfx::SE_ACCESS_SHADER_WRITE_BIT, + Gfx::SE_PIPELINE_STAGE_COMPUTE_SHADER_BIT); + + spectrumBuffer = graphics->createShaderBuffer(ShaderBufferCreateInfo{ + .sourceData = + { + .size = spectrums.size() * sizeof(SpectrumParameters), + .data = (uint8*)spectrums.data(), + }, + .numElements = 8, + .dynamic = true, + .name = "Spectrums", + }); + spectrumBuffer->pipelineBarrier(Gfx::SE_ACCESS_TRANSFER_WRITE_BIT, Gfx::SE_PIPELINE_STAGE_TRANSFER_BIT, Gfx::SE_ACCESS_SHADER_READ_BIT, + Gfx::SE_PIPELINE_STAGE_COMPUTE_SHADER_BIT); + + linearRepeatSampler = graphics->createSampler(SamplerCreateInfo{ + .magFilter = Gfx::SE_FILTER_LINEAR, + .minFilter = Gfx::SE_FILTER_LINEAR, + .addressModeU = Gfx::SE_SAMPLER_ADDRESS_MODE_REPEAT, + .addressModeV = Gfx::SE_SAMPLER_ADDRESS_MODE_REPEAT, + .addressModeW = Gfx::SE_SAMPLER_ADDRESS_MODE_REPEAT, + .anisotropyEnable = true, + .maxAnisotropy = 16, + }); + + computeLayout = graphics->createDescriptorLayout("pParams"); + computeLayout->addDescriptorBinding(Gfx::DescriptorBinding{ + .binding = 0, + .descriptorType = Gfx::SE_DESCRIPTOR_TYPE_UNIFORM_BUFFER, + }); + computeLayout->addDescriptorBinding(Gfx::DescriptorBinding{ + .binding = 1, + .descriptorType = Gfx::SE_DESCRIPTOR_TYPE_STORAGE_IMAGE, + }); + computeLayout->addDescriptorBinding(Gfx::DescriptorBinding{ + .binding = 2, + .descriptorType = Gfx::SE_DESCRIPTOR_TYPE_STORAGE_IMAGE, + }); + computeLayout->addDescriptorBinding(Gfx::DescriptorBinding{ + .binding = 3, + .descriptorType = Gfx::SE_DESCRIPTOR_TYPE_STORAGE_IMAGE, + }); + computeLayout->addDescriptorBinding(Gfx::DescriptorBinding{ + .binding = 4, + .descriptorType = Gfx::SE_DESCRIPTOR_TYPE_STORAGE_IMAGE, + }); + computeLayout->addDescriptorBinding(Gfx::DescriptorBinding{ + .binding = 5, + .descriptorType = Gfx::SE_DESCRIPTOR_TYPE_STORAGE_IMAGE, + }); + computeLayout->addDescriptorBinding(Gfx::DescriptorBinding{ + .binding = 6, + .descriptorType = Gfx::SE_DESCRIPTOR_TYPE_STORAGE_BUFFER, + }); + computeLayout->addDescriptorBinding(Gfx::DescriptorBinding{ + .binding = 7, + .descriptorType = Gfx::SE_DESCRIPTOR_TYPE_SAMPLER, + }); + computeLayout->create(); + + pipelineLayout = graphics->createPipelineLayout("WaterComputeLayout"); + pipelineLayout->addDescriptorLayout(computeLayout); + + ShaderCompilationInfo info = { + .name = "WaterCompute", + .modules = {"WaterCompute"}, + .entryPoints = + { + {"CS_InitializeSpectrum", "WaterCompute"}, + {"CS_PackSpectrumConjugate", "WaterCompute"}, + {"CS_UpdateSpectrumForFFT", "WaterCompute"}, + {"CS_HorizontalFFT", "WaterCompute"}, + {"CS_VerticalFFT", "WaterCompute"}, + {"CS_AssembleMaps", "WaterCompute"}, + }, + .rootSignature = pipelineLayout, + .dumpIntermediate = true, + }; + graphics->beginShaderCompilation(info); + initSpectrumCS = graphics->createComputeShader({0}); + packSpectrumConjugateCS = graphics->createComputeShader({1}); + updateSpectrumForFFTCS = graphics->createComputeShader({2}); + horizontalFFTCS = graphics->createComputeShader({3}); + verticalFFTCS = graphics->createComputeShader({4}); + assembleMapsCS = graphics->createComputeShader({5}); + + pipelineLayout->create(); + + initSpectrum = graphics->createComputePipeline(Gfx::ComputePipelineCreateInfo{ + .computeShader = initSpectrumCS, + .pipelineLayout = pipelineLayout, + }); + packSpectrumConjugate = graphics->createComputePipeline(Gfx::ComputePipelineCreateInfo{ + .computeShader = packSpectrumConjugateCS, + .pipelineLayout = pipelineLayout, + }); + updateSpectrumForFFT = graphics->createComputePipeline(Gfx::ComputePipelineCreateInfo{ + .computeShader = updateSpectrumForFFTCS, + .pipelineLayout = pipelineLayout, + }); + horizontalFFT = graphics->createComputePipeline(Gfx::ComputePipelineCreateInfo{ + .computeShader = horizontalFFTCS, + .pipelineLayout = pipelineLayout, + }); + verticalFFT = graphics->createComputePipeline(Gfx::ComputePipelineCreateInfo{ + .computeShader = verticalFFTCS, + .pipelineLayout = pipelineLayout, + }); + assembleMaps = graphics->createComputePipeline(Gfx::ComputePipelineCreateInfo{ + .computeShader = assembleMapsCS, + .pipelineLayout = pipelineLayout, + }); + + materialLayout = graphics->createDescriptorLayout("pWaterMaterial"); + materialLayout->addDescriptorBinding(Gfx::DescriptorBinding{ + .binding = 0, + .descriptorType = Gfx::SE_DESCRIPTOR_TYPE_UNIFORM_BUFFER, + }); + materialLayout->addDescriptorBinding(Gfx::DescriptorBinding{ + .binding = 1, + .descriptorType = Gfx::SE_DESCRIPTOR_TYPE_SAMPLED_IMAGE, + }); + materialLayout->addDescriptorBinding(Gfx::DescriptorBinding{ + .binding = 2, + .descriptorType = Gfx::SE_DESCRIPTOR_TYPE_SAMPLED_IMAGE, + }); + materialLayout->addDescriptorBinding(Gfx::DescriptorBinding{ + .binding = 3, + .descriptorType = Gfx::SE_DESCRIPTOR_TYPE_SAMPLED_IMAGE, + }); + materialLayout->addDescriptorBinding(Gfx::DescriptorBinding{ + .binding = 4, + .descriptorType = Gfx::SE_DESCRIPTOR_TYPE_SAMPLER, + }); + materialLayout->addDescriptorBinding(Gfx::DescriptorBinding{ + .binding = 5, + .descriptorType = Gfx::SE_DESCRIPTOR_TYPE_STORAGE_BUFFER, + }); + materialLayout->create(); + waterLayout = graphics->createPipelineLayout("WaterLayout"); + waterLayout->addDescriptorLayout(viewParamsLayout); + waterLayout->addDescriptorLayout(materialLayout); + + ShaderCompilationInfo createInfo = { + .name = "WaterTiles", + .modules = {"WaterTask", "WaterMesh", "WaterPass"}, + .entryPoints = + { + {"taskMain", "WaterTask"}, + {"meshMain", "WaterMesh"}, + {"main", "WaterPass"}, + }, + .rootSignature = waterLayout, + }; + graphics->beginShaderCompilation(createInfo); + waterTask = graphics->createTaskShader({0}); + waterMesh = graphics->createMeshShader({1}); + waterFragment = graphics->createFragmentShader({2}); + + waterLayout->create(); +} + +WaterRenderer::~WaterRenderer() {} + +void WaterRenderer::beginFrame() { + std::cout << "Test" << std::endl; + updateFFTDescriptor(); + struct WaterPayload { + Vector2 offset; + float extent; + float height; + }; + Array payloads; + scene->view([&](Component::WaterTile& tile) { + payloads.add(WaterPayload{ + .offset = Vector2(tile.location) * Component::WaterTile::DIMENSIONS, + .extent = Component::WaterTile::DIMENSIONS, + .height = tile.height, + }); + }); + waterTiles = graphics->createShaderBuffer(ShaderBufferCreateInfo{ + .sourceData = + { + .size = payloads.size() * sizeof(WaterPayload), + .data = (uint8*)payloads.data(), + }, + .numElements = payloads.size(), + .name = "WaterTiles", + }); + waterTiles->pipelineBarrier(Gfx::SE_ACCESS_TRANSFER_WRITE_BIT, Gfx::SE_PIPELINE_STAGE_TRANSFER_BIT, Gfx::SE_ACCESS_SHADER_READ_BIT, + Gfx::SE_PIPELINE_STAGE_TASK_SHADER_BIT_EXT); + + displacementTextures->changeLayout(Gfx::SE_IMAGE_LAYOUT_GENERAL, Gfx::SE_ACCESS_SHADER_READ_BIT, Gfx::SE_SHADER_STAGE_MESH_BIT_EXT, + Gfx::SE_ACCESS_SHADER_WRITE_BIT, Gfx::SE_PIPELINE_STAGE_COMPUTE_SHADER_BIT); + + slopeTextures->changeLayout(Gfx::SE_IMAGE_LAYOUT_GENERAL, Gfx::SE_ACCESS_SHADER_READ_BIT, Gfx::SE_SHADER_STAGE_MESH_BIT_EXT, + Gfx::SE_ACCESS_SHADER_WRITE_BIT, Gfx::SE_PIPELINE_STAGE_COMPUTE_SHADER_BIT); + + boyancyData->changeLayout(Gfx::SE_IMAGE_LAYOUT_GENERAL, Gfx::SE_ACCESS_SHADER_READ_BIT, Gfx::SE_SHADER_STAGE_MESH_BIT_EXT, + Gfx::SE_ACCESS_SHADER_WRITE_BIT, Gfx::SE_PIPELINE_STAGE_COMPUTE_SHADER_BIT); + + Gfx::OComputeCommand updateCommand = graphics->createComputeCommand("WaterUpdate"); + updateCommand->bindPipeline(updateSpectrumForFFT); + updateCommand->bindDescriptor(computeSet); + updateCommand->dispatch(threadGroupsX, threadGroupsY, 1); + graphics->executeCommands(std::move(updateCommand)); + + spectrumTextures->pipelineBarrier(Gfx::SE_ACCESS_SHADER_WRITE_BIT, Gfx::SE_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + Gfx::SE_ACCESS_SHADER_READ_BIT | Gfx::SE_ACCESS_SHADER_WRITE_BIT, + Gfx::SE_PIPELINE_STAGE_COMPUTE_SHADER_BIT); + Gfx::OComputeCommand horizontalCommand = graphics->createComputeCommand("HorizontalFFT"); + horizontalCommand->bindPipeline(horizontalFFT); + horizontalCommand->bindDescriptor(computeSet); + horizontalCommand->dispatch(1, params.N, 1); + graphics->executeCommands(std::move(horizontalCommand)); + + spectrumTextures->pipelineBarrier( + Gfx::SE_ACCESS_SHADER_READ_BIT | Gfx::SE_ACCESS_SHADER_WRITE_BIT, Gfx::SE_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + Gfx::SE_ACCESS_SHADER_READ_BIT | Gfx::SE_ACCESS_SHADER_WRITE_BIT, Gfx::SE_PIPELINE_STAGE_COMPUTE_SHADER_BIT); + + Gfx::OComputeCommand verticalCommand = graphics->createComputeCommand("VerticalFFT"); + verticalCommand->bindPipeline(verticalFFT); + verticalCommand->bindDescriptor(computeSet); + verticalCommand->dispatch(1, params.N, 1); + graphics->executeCommands(std::move(verticalCommand)); + + spectrumTextures->pipelineBarrier( + Gfx::SE_ACCESS_SHADER_READ_BIT | Gfx::SE_ACCESS_SHADER_WRITE_BIT, Gfx::SE_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + Gfx::SE_ACCESS_SHADER_READ_BIT | Gfx::SE_ACCESS_SHADER_WRITE_BIT, Gfx::SE_PIPELINE_STAGE_COMPUTE_SHADER_BIT); + + Gfx::OComputeCommand assembleCommand = graphics->createComputeCommand("AssembleCommand"); + assembleCommand->bindPipeline(assembleMaps); + assembleCommand->bindDescriptor(computeSet); + assembleCommand->dispatch(threadGroupsX, threadGroupsY, 1); + graphics->executeCommands(std::move(assembleCommand)); + + // transition for mipmap gen + displacementTextures->changeLayout( + Gfx::SE_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, Gfx::SE_ACCESS_SHADER_WRITE_BIT, Gfx::SE_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + Gfx::SE_ACCESS_TRANSFER_READ_BIT | Gfx::SE_ACCESS_TRANSFER_WRITE_BIT, Gfx::SE_PIPELINE_STAGE_TRANSFER_BIT); + + slopeTextures->changeLayout(Gfx::SE_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, Gfx::SE_ACCESS_SHADER_WRITE_BIT, + Gfx::SE_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + Gfx::SE_ACCESS_TRANSFER_READ_BIT | Gfx::SE_ACCESS_TRANSFER_WRITE_BIT, Gfx::SE_PIPELINE_STAGE_TRANSFER_BIT); + + displacementTextures->generateMipmaps(); + slopeTextures->generateMipmaps(); + + displacementTextures->changeLayout(Gfx::SE_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, Gfx::SE_ACCESS_TRANSFER_WRITE_BIT, + Gfx::SE_PIPELINE_STAGE_TRANSFER_BIT, Gfx::SE_ACCESS_SHADER_READ_BIT, + Gfx::SE_SHADER_STAGE_MESH_BIT_EXT); + + slopeTextures->changeLayout(Gfx::SE_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, Gfx::SE_ACCESS_TRANSFER_WRITE_BIT, + Gfx::SE_PIPELINE_STAGE_TRANSFER_BIT, Gfx::SE_ACCESS_SHADER_READ_BIT, Gfx::SE_SHADER_STAGE_MESH_BIT_EXT); + + boyancyData->changeLayout(Gfx::SE_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, Gfx::SE_ACCESS_SHADER_WRITE_BIT, + Gfx::SE_PIPELINE_STAGE_COMPUTE_SHADER_BIT, Gfx::SE_ACCESS_SHADER_READ_BIT, Gfx::SE_SHADER_STAGE_MESH_BIT_EXT); + + updateMaterialDescriptor(); + graphics->waitDeviceIdle(); +} + +Gfx::ORenderCommand WaterRenderer::render(Gfx::PDescriptorSet viewParamsSet) { + Gfx::ORenderCommand waterCommand = graphics->createRenderCommand("WaterRender"); + waterCommand->setViewport(viewport); + waterCommand->bindPipeline(waterPipeline); + waterCommand->bindDescriptor({viewParamsSet, materialSet}); + waterCommand->drawMesh(waterTiles->getNumElements(), 1, 1); + return waterCommand; +} + +void WaterRenderer::setViewport(Gfx::PViewport _viewport, Gfx::PRenderPass renderPass) { + viewport = _viewport; + updateFFTDescriptor(); + Gfx::MeshPipelineCreateInfo pipelineInfo = { + .taskShader = waterTask, + .meshShader = waterMesh, + .fragmentShader = waterFragment, + .renderPass = renderPass, + .pipelineLayout = waterLayout, + .multisampleState = + { + .samples = viewport->getSamples(), + }, + .rasterizationState = + { + .cullMode = Gfx::SE_CULL_MODE_BACK_BIT, + }, + .colorBlend = + { + .attachmentCount = 1, + .blendAttachments = + { + Gfx::ColorBlendState::BlendAttachment{ + .blendEnable = true, + }, + }, + }, + }; + waterPipeline = graphics->createGraphicsPipeline(std::move(pipelineInfo)); + Gfx::OComputeCommand initCmd = graphics->createComputeCommand("InitialSpectrumCompute"); + initCmd->bindPipeline(initSpectrum); + initCmd->bindDescriptor(computeSet); + initCmd->dispatch(threadGroupsX, threadGroupsY, 1); + graphics->executeCommands(std::move(initCmd)); + + initialSpectrumTextures->pipelineBarrier(Gfx::SE_ACCESS_SHADER_WRITE_BIT, Gfx::SE_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + Gfx::SE_ACCESS_SHADER_READ_BIT | Gfx::SE_ACCESS_SHADER_WRITE_BIT, + Gfx::SE_PIPELINE_STAGE_COMPUTE_SHADER_BIT); + + Gfx::OComputeCommand packCmd = graphics->createComputeCommand("PackConjugate"); + packCmd->bindPipeline(packSpectrumConjugate); + packCmd->bindDescriptor(computeSet); + packCmd->dispatch(threadGroupsX, threadGroupsY, 1); + graphics->executeCommands(std::move(packCmd)); + + initialSpectrumTextures->pipelineBarrier(Gfx::SE_ACCESS_SHADER_WRITE_BIT, Gfx::SE_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + Gfx::SE_ACCESS_SHADER_READ_BIT | Gfx::SE_ACCESS_SHADER_WRITE_BIT, + Gfx::SE_PIPELINE_STAGE_COMPUTE_SHADER_BIT); +} + +float WaterRenderer::jonswapAlpha(float fetch, float windSpeed) const { + return 0.076f * pow(params.gravity * fetch / windSpeed / windSpeed, -0.22f); +} + +float WaterRenderer::jonswapPeakFrequency(float fetch, float windSpeed) const { + return 22 * pow(windSpeed * fetch / params.gravity / params.gravity, -0.33f); +} + +void WaterRenderer::updateFFTDescriptor() { + for (uint32 i = 0; i < displaySpectrums.size(); ++i) { + spectrums[i] = { + .scale = displaySpectrums[i].scale, + .angle = displaySpectrums[i].windDirection / 180 * 3.1415f, + .spreadBlend = displaySpectrums[i].spreadBlend, + .swell = std::clamp(displaySpectrums[i].swell, 0.01f, 1.0f), + .alpha = jonswapAlpha(displaySpectrums[i].fetch, displaySpectrums[i].windSpeed), + .peakOmega = jonswapPeakFrequency(displaySpectrums[i].fetch, displaySpectrums[i].windSpeed), + .gamma = displaySpectrums[i].peakEnhancement, + .shortWavesFade = displaySpectrums[i].shortWavesFade, + }; + } + spectrumBuffer->updateContents(0, sizeof(SpectrumParameters) * spectrums.size(), spectrums.data()); + spectrumBuffer->pipelineBarrier(Gfx::SE_ACCESS_TRANSFER_WRITE_BIT, Gfx::SE_PIPELINE_STAGE_TRANSFER_BIT, Gfx::SE_ACCESS_SHADER_READ_BIT, + Gfx::SE_PIPELINE_STAGE_COMPUTE_SHADER_BIT); + params.deltaTime = Gfx::getCurrentFrameDelta(); + params.frameTime = Gfx::getCurrentFrameTime(); + paramsBuffer->updateContents(0, sizeof(ComputeParams), ¶ms); + paramsBuffer->pipelineBarrier(Gfx::SE_ACCESS_TRANSFER_WRITE_BIT, Gfx::SE_PIPELINE_STAGE_TRANSFER_BIT, Gfx::SE_ACCESS_UNIFORM_READ_BIT, + Gfx::SE_PIPELINE_STAGE_COMPUTE_SHADER_BIT); + computeLayout->reset(); + computeSet = computeLayout->allocateDescriptorSet(); + computeSet->updateBuffer(0, paramsBuffer); + computeSet->updateTexture(1, 0, Gfx::PTexture2D(spectrumTextures)); + computeSet->updateTexture(2, 0, Gfx::PTexture2D(initialSpectrumTextures)); + computeSet->updateTexture(3, 0, Gfx::PTexture2D(displacementTextures)); + computeSet->updateTexture(4, 0, Gfx::PTexture2D(slopeTextures)); + computeSet->updateTexture(5, 0, Gfx::PTexture2D(boyancyData)); + computeSet->updateBuffer(6, 0, spectrumBuffer); + computeSet->updateSampler(7, 0, linearRepeatSampler); + computeSet->writeChanges(); +} + +void WaterRenderer::updateMaterialDescriptor() { + materialLayout->reset(); + materialSet = materialLayout->allocateDescriptorSet(); + materialSet->updateBuffer(0, materialUniforms); + materialSet->updateTexture(1, Gfx::PTexture2D(displacementTextures)); + materialSet->updateTexture(2, Gfx::PTexture2D(slopeTextures)); + materialSet->updateTexture(3, Gfx::PTextureCube(skyBox)); + materialSet->updateSampler(4, linearRepeatSampler); + materialSet->updateBuffer(5, waterTiles); + materialSet->writeChanges(); +} diff --git a/src/Engine/Graphics/RenderPass/WaterRenderer.h b/src/Engine/Graphics/RenderPass/WaterRenderer.h new file mode 100644 index 0000000..3f59c02 --- /dev/null +++ b/src/Engine/Graphics/RenderPass/WaterRenderer.h @@ -0,0 +1,136 @@ +#pragma once +#include "RenderPass.h" + +namespace Seele { +class WaterRenderer { + public: + WaterRenderer(Gfx::PGraphics graphics, PScene scene, Gfx::PDescriptorLayout viewParamsLayout); + ~WaterRenderer(); + void beginFrame(); + Gfx::ORenderCommand render(Gfx::PDescriptorSet viewParamsSet); + void setViewport(Gfx::PViewport viewport, Gfx::PRenderPass renderPass); + + private: + float jonswapAlpha(float fetch, float windSpeed) const; + float jonswapPeakFrequency(float fetch, float windSpeed) const; + void updateFFTDescriptor(); + void updateMaterialDescriptor(); + Gfx::PGraphics graphics; + PScene scene; + Gfx::ODescriptorLayout computeLayout; + Gfx::PDescriptorSet computeSet; + Gfx::OPipelineLayout pipelineLayout; + + Gfx::OComputeShader initSpectrumCS; + Gfx::PComputePipeline initSpectrum; + Gfx::OComputeShader packSpectrumConjugateCS; + Gfx::PComputePipeline packSpectrumConjugate; + Gfx::OComputeShader updateSpectrumForFFTCS; + Gfx::PComputePipeline updateSpectrumForFFT; + Gfx::OComputeShader horizontalFFTCS; + Gfx::PComputePipeline horizontalFFT; + Gfx::OComputeShader verticalFFTCS; + Gfx::PComputePipeline verticalFFT; + Gfx::OComputeShader assembleMapsCS; + Gfx::PComputePipeline assembleMaps; + + struct SpectrumParameters { + float scale; + float angle; + float spreadBlend; + float swell; + float alpha; + float peakOmega; + float gamma; + float shortWavesFade; + }; + StaticArray spectrums; + struct DisplaySpectrumSettings { + float scale = 1; + float windSpeed = 1; + float windDirection = 0; + float fetch = 1; + float spreadBlend = 1; + float swell = 1; + float peakEnhancement = 1; + float shortWavesFade = 1; + }; + StaticArray displaySpectrums; + struct ComputeParams { + float frameTime; + float deltaTime; + float gravity = 9.81f; + float repeatTime = 200.0f; + float depth = 20.0f; + float lowCutoff = 0.0001f; + float highCutoff = 9000.0f; + int seed = 0; + Vector2 lambda = Vector2(1, 1); + uint32 N = 1024; + uint32 lengthScale0 = 256; + uint32 lengthScale1 = 256; + uint32 lengthScale2 = 256; + uint32 lengthScale3 = 256; + float foamBias = -0.5f; + float foamDecayRate = 0.05f; + float foamAdd = 0.5f; + float foamThreshold = 0.0f; + } params; + float speed = 1.0f; + float displacementDepthFalloff = 1.0f; + uint32 logN; + uint32 threadGroupsX; + uint32 threadGroupsY; + Gfx::OUniformBuffer paramsBuffer; + Gfx::OTexture2D spectrumTextures, initialSpectrumTextures, displacementTextures; + Gfx::OTexture2D slopeTextures; + Gfx::OTexture2D boyancyData; + Gfx::OShaderBuffer spectrumBuffer; + Gfx::OSampler linearRepeatSampler; + + struct MaterialParams { + Vector sunDirection = Vector(0, -1, 0); + float displacementDepthAttenuation = 1; + + float foamSubtract0 = 0; + float foamSubtract1 = 0; + float foamSubtract2 = 0; + float foamSubtract3 = 0; + + float normalStrength = 1; + float foamDepthAttenuation = 1; + float normalDepthAttenuation = 1; + float roughness = 0.1f; + + Vector sunIrradiance = Vector(1, 1, 1); + float foamRoughnessModifier = 1; + + Vector scatterColor = Vector(1, 1, 1); + float environmentLightStrength = 1; + + Vector bubbleColor = Vector(1, 1, 1); + float heightModifier = 1; + + float bubbleDensity = 1; + float wavePeakScatterStrength = 1; + float scatterStrength = 1; + float scatterShadowStrength = 1; + + Vector foamColor = Vector(1, 1, 1); + } materialParams; + // Water + Gfx::OUniformBuffer materialUniforms; + Gfx::PTextureCube skyBox; + Gfx::OShaderBuffer waterTiles; + Gfx::OTaskShader waterTask; + Gfx::OMeshShader waterMesh; + Gfx::OFragmentShader waterFragment; + Gfx::ODescriptorLayout materialLayout; + Gfx::PDescriptorSet materialSet; + Gfx::OPipelineLayout waterLayout; + Gfx::PGraphicsPipeline waterPipeline; + + Gfx::PViewport viewport; +}; +DEFINE_REF(WaterRenderer) +} \ No newline at end of file diff --git a/src/Engine/Graphics/StaticMeshVertexData.cpp b/src/Engine/Graphics/StaticMeshVertexData.cpp index d3169eb..6538e30 100644 --- a/src/Engine/Graphics/StaticMeshVertexData.cpp +++ b/src/Engine/Graphics/StaticMeshVertexData.cpp @@ -66,22 +66,22 @@ void StaticMeshVertexData::serializeMesh(MeshId id, uint64 numVertices, ArchiveB std::unique_lock l(vertexDataLock); offset = meshOffsets[id]; } - Array pos(numVertices); Array tex[MAX_TEXCOORDS]; for (size_t i = 0; i < MAX_TEXCOORDS; ++i) { tex[i].resize(numVertices); - texCoords[i]->readContents(offset * sizeof(Vector2), numVertices * sizeof(Vector2), tex[i].data()); + std::memcpy(tex[i].data(), texData[i].data() + offset, numVertices * sizeof(Vector2)); Serialization::save(buffer, tex[i]); } Array nor(numVertices); Array tan(numVertices); Array bit(numVertices); Array col(numVertices); - positions->readContents(offset * sizeof(Vector4), numVertices * sizeof(Vector4), pos.data()); - normals->readContents(offset * sizeof(Vector4), numVertices * sizeof(Vector4), nor.data()); - tangents->readContents(offset * sizeof(Vector4), numVertices * sizeof(Vector4), tan.data()); - biTangents->readContents(offset * sizeof(Vector4), numVertices * sizeof(Vector4), bit.data()); - colors->readContents(offset * sizeof(Vector4), numVertices * sizeof(Vector4), col.data()); + Array pos(numVertices); + std::memcpy(pos.data(), posData.data() + offset, numVertices * sizeof(Vector4)); + std::memcpy(nor.data(), norData.data() + offset, numVertices * sizeof(Vector4)); + std::memcpy(tan.data(), tanData.data() + offset, numVertices * sizeof(Vector4)); + std::memcpy(bit.data(), bitData.data() + offset, numVertices * sizeof(Vector4)); + std::memcpy(col.data(), colData.data() + offset, numVertices * sizeof(Vector4)); Serialization::save(buffer, pos); Serialization::save(buffer, nor); Serialization::save(buffer, tan); diff --git a/src/Engine/Graphics/Vulkan/Buffer.cpp b/src/Engine/Graphics/Vulkan/Buffer.cpp index 8fa4910..347615f 100644 --- a/src/Engine/Graphics/Vulkan/Buffer.cpp +++ b/src/Engine/Graphics/Vulkan/Buffer.cpp @@ -17,6 +17,7 @@ BufferAllocation::BufferAllocation(PGraphics graphics, const std::string& name, : CommandBoundResource(graphics, name), size(bufferInfo.size), owner(owner) { VK_CHECK(vmaCreateBufferWithAlignment(graphics->getAllocator(), &bufferInfo, &allocInfo, alignment, &buffer, &allocation, &info)); vmaGetAllocationMemoryProperties(graphics->getAllocator(), allocation, &properties); + assert(!name.empty()); VkDebugUtilsObjectNameInfoEXT nameInfo = { .sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_OBJECT_NAME_INFO_EXT, .pNext = nullptr, @@ -24,11 +25,10 @@ BufferAllocation::BufferAllocation(PGraphics graphics, const std::string& name, .objectHandle = (uint64)buffer, .pObjectName = name.c_str(), }; + vkSetDebugUtilsObjectNameEXT(graphics->getDevice(), &nameInfo); if (!(properties & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT)) { bufferSize += size; } - assert(!name.empty()); - vkSetDebugUtilsObjectNameEXT(graphics->getDevice(), &nameInfo); if (bufferInfo.usage & VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT) { VkBufferDeviceAddressInfo addrInfo = { .sType = VK_STRUCTURE_TYPE_BUFFER_DEVICE_ADDRESS_INFO_KHR, diff --git a/src/Engine/Graphics/Vulkan/Command.cpp b/src/Engine/Graphics/Vulkan/Command.cpp index 0b55cf7..1f6bd65 100644 --- a/src/Engine/Graphics/Vulkan/Command.cpp +++ b/src/Engine/Graphics/Vulkan/Command.cpp @@ -401,7 +401,7 @@ void ComputeCommand::bindDescriptor(Gfx::PDescriptorSet descriptorSet, ArraywriteDescriptors.size() == 0); boundResources.add(descriptor.getHandle()); - for (auto binding : descriptor->boundResources) { + for (const auto& binding : descriptor->boundResources) { for (auto res : binding) { res->bind(); boundResources.add(res); diff --git a/src/Engine/Graphics/Vulkan/Descriptor.cpp b/src/Engine/Graphics/Vulkan/Descriptor.cpp index 6e858cc..f552467 100644 --- a/src/Engine/Graphics/Vulkan/Descriptor.cpp +++ b/src/Engine/Graphics/Vulkan/Descriptor.cpp @@ -3,8 +3,8 @@ #include "CRC.h" #include "Command.h" #include "Graphics.h" -#include "Texture.h" #include "RayTracing.h" +#include "Texture.h" using namespace Seele; using namespace Seele::Vulkan; @@ -159,10 +159,10 @@ void DescriptorPool::reset() { } DescriptorSet::DescriptorSet(PGraphics graphics, PDescriptorPool owner) - : Gfx::DescriptorSet(owner->getLayout()), CommandBoundResource(graphics, owner->getLayout()->getName()), setHandle(VK_NULL_HANDLE), graphics(graphics), owner(owner){ + : Gfx::DescriptorSet(owner->getLayout()), CommandBoundResource(graphics, owner->getLayout()->getName()), setHandle(VK_NULL_HANDLE), + graphics(graphics), owner(owner) { boundResources.resize(owner->getLayout()->getBindings().size()); - for (uint32 i = 0; i < boundResources.size(); ++i) - { + for (uint32 i = 0; i < boundResources.size(); ++i) { boundResources[i].resize(owner->getLayout()->getBindings()[i].descriptorCount); } } @@ -245,7 +245,6 @@ void DescriptorSet::updateBuffer(uint32_t binding, Gfx::PIndexBuffer indexBuffer boundResources[binding][0] = vulkanBuffer->getAlloc(); } - void DescriptorSet::updateBuffer(uint32_t binding, uint32 index, Gfx::PShaderBuffer shaderBuffer) { PShaderBuffer vulkanBuffer = shaderBuffer.cast(); if (boundResources[binding][index] == vulkanBuffer->getAlloc()) { @@ -298,8 +297,7 @@ void DescriptorSet::updateSampler(uint32_t binding, Gfx::PSampler samplerState) boundResources[binding][0] = vulkanSampler->getHandle(); } -void DescriptorSet::updateSampler(uint32_t binding, uint32 dstArrayIndex, Gfx::PSampler samplerState) -{ +void DescriptorSet::updateSampler(uint32_t binding, uint32 dstArrayIndex, Gfx::PSampler samplerState) { PSampler vulkanSampler = samplerState.cast(); if (boundResources[binding][dstArrayIndex] == vulkanSampler->getHandle()) { return; @@ -325,7 +323,6 @@ void DescriptorSet::updateSampler(uint32_t binding, uint32 dstArrayIndex, Gfx::P boundResources[binding][dstArrayIndex] = vulkanSampler->getHandle(); } - void DescriptorSet::updateTexture(uint32_t binding, Gfx::PTexture texture, Gfx::PSampler samplerState) { TextureBase* vulkanTexture = texture.cast().getHandle(); if (boundResources[binding][0] == vulkanTexture->getHandle()) { @@ -338,12 +335,6 @@ void DescriptorSet::updateTexture(uint32_t binding, Gfx::PTexture texture, Gfx:: .imageView = vulkanTexture->getView(), .imageLayout = cast(vulkanTexture->getLayout()), }); - VkDescriptorType descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE; - if (samplerState != nullptr) { - descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; - } else if (vulkanTexture->getUsage() & VK_IMAGE_USAGE_STORAGE_BIT) { - descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; - } writeDescriptors.add(VkWriteDescriptorSet{ .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .pNext = nullptr, @@ -351,15 +342,14 @@ void DescriptorSet::updateTexture(uint32_t binding, Gfx::PTexture texture, Gfx:: .dstBinding = binding, .dstArrayElement = 0, .descriptorCount = 1, - .descriptorType = descriptorType, + .descriptorType = cast(layout->getBindings()[binding].descriptorType), .pImageInfo = &imageInfos.back(), }); boundResources[binding][0] = vulkanTexture->getHandle(); } -void DescriptorSet::updateTexture(uint32 binding, uint32 dstArrayIndex, Gfx::PTexture texture) -{ +void DescriptorSet::updateTexture(uint32 binding, uint32 dstArrayIndex, Gfx::PTexture texture) { TextureBase* vulkanTexture = texture.cast().getHandle(); if (boundResources[binding][dstArrayIndex] == vulkanTexture->getHandle()) { return; @@ -378,14 +368,13 @@ void DescriptorSet::updateTexture(uint32 binding, uint32 dstArrayIndex, Gfx::PTe .dstBinding = binding, .dstArrayElement = dstArrayIndex, .descriptorCount = 1, - .descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, + .descriptorType = cast(layout->getBindings()[binding].descriptorType), .pImageInfo = &imageInfos.back(), }); boundResources[binding][dstArrayIndex] = vulkanTexture->getHandle(); } - void DescriptorSet::updateTextureArray(uint32_t binding, Array textures) { // maybe make this a parameter? uint32 arrayElement = 0; @@ -400,10 +389,6 @@ void DescriptorSet::updateTextureArray(uint32_t binding, Array .imageLayout = cast(vulkanTexture->getLayout()), }); - VkDescriptorType descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE; - if (vulkanTexture->getUsage() & VK_IMAGE_USAGE_STORAGE_BIT) { - descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; - } boundResources[binding][arrayElement] = vulkanTexture->getHandle(); writeDescriptors.add(VkWriteDescriptorSet{ .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, @@ -412,13 +397,12 @@ void DescriptorSet::updateTextureArray(uint32_t binding, Array .dstBinding = binding, .dstArrayElement = arrayElement++, .descriptorCount = 1, - .descriptorType = descriptorType, + .descriptorType = cast(layout->getBindings()[binding].descriptorType), .pImageInfo = &imageInfos.back(), }); } } - void DescriptorSet::updateSamplerArray(uint32_t binding, Array samplers) { // maybe make this a parameter? uint32 arrayElement = 0; diff --git a/src/Engine/Graphics/Vulkan/Graphics.cpp b/src/Engine/Graphics/Vulkan/Graphics.cpp index 8972e4e..eb892aa 100644 --- a/src/Engine/Graphics/Vulkan/Graphics.cpp +++ b/src/Engine/Graphics/Vulkan/Graphics.cpp @@ -175,6 +175,12 @@ void Graphics::executeCommands(Array commands) { getGraphicsCommands()->getCommands()->executeCommands(std::move(commands)); } +void Graphics::executeCommands(Gfx::OComputeCommand commands) { + Array commandArray; + commandArray.add(std::move(commands)); + getComputeCommands()->getCommands()->executeCommands(std::move(commandArray)); +} + void Graphics::executeCommands(Array commands) { getComputeCommands()->getCommands()->executeCommands(std::move(commands)); } @@ -747,6 +753,7 @@ void Graphics::pickPhysicalDevice() { .pNext = &meshShaderFeatures, .storageBuffer8BitAccess = true, .uniformAndStorageBuffer8BitAccess = true, + .shaderFloat16 = true, .shaderUniformBufferArrayNonUniformIndexing = true, .shaderSampledImageArrayNonUniformIndexing = true, .shaderStorageBufferArrayNonUniformIndexing = true, @@ -770,6 +777,7 @@ void Graphics::pickPhysicalDevice() { .geometryShader = true, .fillModeNonSolid = true, .wideLines = true, + .samplerAnisotropy = true, .pipelineStatisticsQuery = true, .fragmentStoresAndAtomics = true, .shaderInt64 = true, diff --git a/src/Engine/Graphics/Vulkan/Graphics.h b/src/Engine/Graphics/Vulkan/Graphics.h index 6e9b883..1c82ade 100644 --- a/src/Engine/Graphics/Vulkan/Graphics.h +++ b/src/Engine/Graphics/Vulkan/Graphics.h @@ -41,6 +41,7 @@ class Graphics : public Gfx::Graphics { virtual void waitDeviceIdle() override; virtual void executeCommands(Array commands) override; + virtual void executeCommands(Gfx::OComputeCommand commands) override; virtual void executeCommands(Array commands) override; virtual Gfx::OTexture2D createTexture2D(const TextureCreateInfo& createInfo) override; diff --git a/src/Engine/Graphics/Vulkan/Query.cpp b/src/Engine/Graphics/Vulkan/Query.cpp index 3f31e83..0e2f710 100644 --- a/src/Engine/Graphics/Vulkan/Query.cpp +++ b/src/Engine/Graphics/Vulkan/Query.cpp @@ -46,7 +46,6 @@ void QueryPool::begin() { void QueryPool::end() { PCommand cmd = graphics->getGraphicsCommands()->getCommands(); vkCmdEndQuery(cmd->getHandle(), handle, head); - graphics->getGraphicsCommands()->submitCommands(); std::unique_lock l(queryMutex); head = (head + 1) % numQueries; queryCV.notify_all(); diff --git a/src/Engine/Graphics/Vulkan/Texture.cpp b/src/Engine/Graphics/Vulkan/Texture.cpp index 4847c55..8be183f 100644 --- a/src/Engine/Graphics/Vulkan/Texture.cpp +++ b/src/Engine/Graphics/Vulkan/Texture.cpp @@ -3,9 +3,9 @@ #include "Enums.h" #include "Graphics/Enums.h" #include "Graphics/Initializer.h" +#include #include #include -#include using namespace Seele; using namespace Seele::Vulkan; @@ -29,10 +29,13 @@ VkImageAspectFlags getAspectFromFormat(Gfx::SeFormat format) { } TextureHandle::TextureHandle(PGraphics graphics, VkImageViewType viewType, const TextureCreateInfo& createInfo, VkImage existingImage) - : CommandBoundResource(graphics, createInfo.name), image(existingImage), width(createInfo.width), height(createInfo.height), depth(createInfo.depth), - arrayCount(createInfo.elements), layerCount(createInfo.layers), mipLevels(createInfo.mipLevels), samples(createInfo.samples), + : CommandBoundResource(graphics, createInfo.name), image(existingImage), width(createInfo.width), height(createInfo.height), + depth(createInfo.depth), arrayCount(createInfo.elements), mipLevels(1), layerCount(createInfo.layers), samples(createInfo.samples), format(createInfo.format), usage(createInfo.usage), layout(Gfx::SE_IMAGE_LAYOUT_UNDEFINED), aspect(getAspectFromFormat(createInfo.format)), ownsImage(false), owner(createInfo.sourceData.owner) { + if (createInfo.useMip) { + mipLevels = static_cast(std::floor(std::log2(std::max(width, height)))) + 1; + } if (existingImage == VK_NULL_HANDLE) { VkImageType type = VK_IMAGE_TYPE_MAX_ENUM; VkImageCreateFlags flags = 0; @@ -84,6 +87,14 @@ TextureHandle::TextureHandle(PGraphics graphics, VkImageViewType viewType, const .usage = VMA_MEMORY_USAGE_AUTO, }; VK_CHECK(vmaCreateImage(graphics->getAllocator(), &info, &allocInfo, &image, &allocation, nullptr)); + VkDebugUtilsObjectNameInfoEXT nameInfo = { + .sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_OBJECT_NAME_INFO_EXT, + .pNext = nullptr, + .objectType = VK_OBJECT_TYPE_IMAGE, + .objectHandle = (uint64)image, + .pObjectName = name.c_str(), + }; + vkSetDebugUtilsObjectNameEXT(graphics->getDevice(), &nameInfo); ownsImage = true; } const DataSource& sourceData = createInfo.sourceData; @@ -103,7 +114,8 @@ TextureHandle::TextureHandle(PGraphics graphics, VkImageViewType viewType, const .flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT | VMA_ALLOCATION_CREATE_MAPPED_BIT, .usage = VMA_MEMORY_USAGE_AUTO, }; - OBufferAllocation stagingAlloc = new BufferAllocation(graphics, fmt::format("{}Staging", createInfo.name), stagingInfo, alloc, Gfx::QueueType::TRANSFER); + OBufferAllocation stagingAlloc = + new BufferAllocation(graphics, fmt::format("{}Staging", createInfo.name), stagingInfo, alloc, Gfx::QueueType::TRANSFER); vmaMapMemory(graphics->getAllocator(), stagingAlloc->allocation, &data); std::memcpy(data, sourceData.data, sourceData.size); vmaUnmapMemory(graphics->getAllocator(), stagingAlloc->allocation); diff --git a/src/Engine/Graphics/Vulkan/Window.cpp b/src/Engine/Graphics/Vulkan/Window.cpp index 0cb161b..0802eb7 100644 --- a/src/Engine/Graphics/Vulkan/Window.cpp +++ b/src/Engine/Graphics/Vulkan/Window.cpp @@ -11,6 +11,9 @@ using namespace Seele::Vulkan; double currentFrameDelta = 0; double Gfx::getCurrentFrameDelta() { return currentFrameDelta; } +double currentFrameTime = 0; +double Gfx::getCurrentFrameTime() { return currentFrameTime; } + uint32 currentFrameIndex = 0; uint32 Gfx::getCurrentFrameIndex() { return currentFrameIndex; } @@ -98,6 +101,11 @@ void Window::beginFrame() { imageAvailableFences[currentSemaphoreIndex]->submit(); graphics->getGraphicsCommands()->getCommands()->waitForSemaphore(VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, imageAvailableSemaphores[currentSemaphoreIndex]); + static double start = glfwGetTime(); + double end = glfwGetTime(); + currentFrameDelta = end - start; + currentFrameTime += currentFrameDelta; + start = end; } void Window::endFrame() { @@ -261,7 +269,6 @@ void Window::createSwapChain() { .width = extent.width, .height = extent.height, .depth = 1, - .mipLevels = 1, .layers = 1, .elements = 1, .samples = 1, diff --git a/src/Engine/Graphics/slang-compile.cpp b/src/Engine/Graphics/slang-compile.cpp index 2dbcba7..8e44edb 100644 --- a/src/Engine/Graphics/slang-compile.cpp +++ b/src/Engine/Graphics/slang-compile.cpp @@ -48,7 +48,7 @@ void Seele::beginCompilation(const ShaderCompilationInfo& info, SlangCompileTarg option[3].value.intValue0 = SLANG_DEBUG_INFO_FORMAT_PDB; option[4].name = slang::CompilerOptionName::DumpIntermediates; option[4].value.kind = slang::CompilerOptionValueKind::Int; - option[4].value.intValue0 = 0; + option[4].value.intValue0 = info.dumpIntermediate; sessionDesc.compilerOptionEntries = option.data(); sessionDesc.compilerOptionEntryCount = option.size(); @@ -124,6 +124,7 @@ void Seele::beginCompilation(const ShaderCompilationInfo& info, SlangCompileTarg layout->addMapping("pLightEnv", 3); layout->addMapping("pRayTracingParams", 5); layout->addMapping("pScene", 2); + layout->addMapping("pWaterMaterial", 1); } Pair, std::string> Seele::generateShader(const ShaderCreateInfo& createInfo) { diff --git a/test.py b/test.py new file mode 100644 index 0000000..6f377ec --- /dev/null +++ b/test.py @@ -0,0 +1,18 @@ + +i = 12 +positions = [] +for x in range(i): + for y in range(i): + positions.append(f"float2({x}.0f / 11, {y}.0f / 11)") +indices = [] +for y in range(i - 1): + for x in range(i - 1): + indices.append(f'uint3({(x) + i * (y)}, {(x + 1) + i * (y)}, {(x) + i * (y + 1)})') + indices.append(f'uint3({(x) + i * (y + 1)}, {(x + 1) + i * (y)}, {(x + 1) + i * (y + 1)})') +print(f"Dim {i}") +print(len(positions)) +print(len(indices)) +print(positions) +print(indices) + +