From beb848373b28624bad8a2314b6b0496f5d007937 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Mon, 13 Mar 2017 10:15:06 -0700 Subject: [PATCH 001/118] Update from zfox23/PALv2 branch --- .../resources/icons/profilePicLoading.gif | Bin 0 -> 12294 bytes .../resources/qml/controls-uit/CheckBox.qml | 2 +- .../resources/qml/controls-uit/Table.qml | 7 +- .../qml/controls-uit/TabletComboBox.qml | 2 +- interface/resources/qml/hifi/NameCard.qml | 755 ++++++++-------- interface/resources/qml/hifi/Pal.qml | 832 ++++++++++++++---- .../qml/styles-uit/HifiConstants.qml | 2 +- .../src/scripting/HMDScriptingInterface.h | 3 +- scripts/defaultScripts.js | 2 +- scripts/system/makeUserConnection.js | 372 ++++++++ scripts/system/pal.js | 257 +++++- 11 files changed, 1681 insertions(+), 553 deletions(-) create mode 100644 interface/resources/icons/profilePicLoading.gif create mode 100644 scripts/system/makeUserConnection.js diff --git a/interface/resources/icons/profilePicLoading.gif b/interface/resources/icons/profilePicLoading.gif new file mode 100644 index 0000000000000000000000000000000000000000..7e85f7cb6aae0e060ec01b5d022603e5bf1e9864 GIT binary patch literal 12294 zcmb8VcUMzu+dY~>RthTxM0W(lNDb9crD~|6qSB-ZNK-+Ip?6I}k=~{EE-2EZqoIRT zX(FI>q^T&Ph;BV&|HgUVao+RCbN0Evz`Dnn>zZ@k^P1-`=qM`LQXy2xS2zUKuq)B| z<;vh5d*d$Kvro6i_v<$gUw-;|tYtTI>EO?wKb+0GpAP@_pZOj=|NZU$-|Gi|L;nAq zQ-9d6Xd4@8s_SdZNKxQW2n2HKH#fhg$4y(Wn+_yjCoe~mrlXT9&Bb5J{;n%O}-tbP-=Og@eu+7xm2wQ=Le2dmo& zgM@(dA+AD*U>GO};-DxL6lD2VZal9Y;a*HA6?`FaQpR?vZ{pp4CQ|eYdc*ER9{rnw07^K&H}>Td4aI zNbHZp`OBN*4<-3FcxLivIS{YB(3G-HhewO{&VFnr_>$x>QP**mZM-7?QhcF7p#Pn-jRWf1M8_IsA+XWmN z3r;|h<%SRak5*2XSw7V`sCeRxE;GI=)Ba74FwI`z&#AY z(wG+0gy%k9)B4wNh^+$lDT&)Jn$oX;zZmWgkzH-W?0(zX`N59?kcM3=7iML9MJkNG zpBTumx0tNK6Se)7zHH7T3k!=?1KD{QJmkmGUH`wqwDO`NbufM9v`gFPjk^RW} zAURzA)c|Z#4n3Y! zAqgV&@$I+3d1#n89=o&iFdP`iNr4}}(RNt}lE`o0Ms>`Zf3ET~UikFZ4p986)s@32 zhw^NW8y!$l@vReFrL;?QD2YRk&c|?^poyw1-A_Eqfuj?zhl4VK5ZXJT+u*C?c#Gb-@Q&kk98;B>>KtXq!#8JTxI(jB7f)dkO}FJ%a(3Bh-8cO; zFBI6Uhs+G6oIe%#dAaFP%fp|?3R(}fH=g=_zxDBNmF1D9tTg1gqn6%n<>z-iPP};j z_s@g9PiW}B`2*yB5qk?J+t}rq`T4Ys;|4KInz?Ex39lnoV|mQy%5;IjhH!D|o&7|s z<1HH+a8;S`{)7Iy`ML=v7#lfevHq${gX?6YxZH1eyGEpGfJ76u`3aVKBqT?P+WHH*T8{Q?$1E(i^ zr#cMqb2;o3nakcfnxk@Ghhuin@kq&;J&LQK_i+S1)BG%FK;Djf+2?t;RH-xcy4K@= zs(yfCyL0WADdiXD@9fOUX2_blVjFfcs!Qg+O2VSNwqomEw@H{o;lN^Z<&Dwjf4~3F z)c;pHmmA{kE1GPp6htt_pqs}HqQYsSWUhFH@RuwH<@~Dig|BG5KEgM>MSCjv=DoTg z!ic`k2TCQh@(JVwk5pbB&esVojH6w4y(b7TEKP;*fqH;2;ks3@4m~DB7?}vmj)Fmp zi_sBspbx`e#3J@dR7oAQKsEDDnJlKLz9d~0U+G+a8u`2~S)YKvDTnSaiHnp4UKLMF zO5^yvGHJLUi30_|E(vbg>V}7);~2N~S{!h=Xa0R=ulD z8q_Pc%<7fjZdCYdwte^V(!TddR81RGn+( zM&)DHT8V;}18xo;b9gQ4HTUjjqUopWb-jmGs-5{z7$jo*P5)1!SEk(w99OTocTsT# zY9!t`UL}{;uxusIx0jge8)I&t_!NizJzh^eKGiyMM_FNcQu_IyU%3MMOGTm@V2)O_+KE&h#DvhbDfx z3D2)FzBngYb1bO-{9AxLmUjv|}P+>s$17#BU$1ZVsCgiLPBMbn3*)|0~L4;yvV=iOun(ymNiMfy41 z69PR6!WcYYeo5Pxc~it!-$IChfMsT;s$yK3G!aY;4$u?~&#Ej`#oL9%$zUs=m8%fY zrlqyg;In5rS{Sfh2G6Ptz27&G6)>bQlKDikyU`gvg6I{)qZiBWW9pwFBBYRu+Zrpg zS-2Uoyq%?2QA*GEw_nMFKNk;Q<(uL&sltCUx1k`BOGX zq8{XQ9`5%IJPCRFJV+IT7#seF4cY@aPeJ|->Rzr3ACl$5JQt~x1yl)crY=XQsl@Lj z@t#wO6smssEm`O_QO-Epf%!$p=vcRxBsW>Tb(x@+M~@1tRvFl?Hl3nN#U001$<7;IJ+qEJ5BH<6N2nT1ma0ULBJ1@Wx% z!98h^Rf+GC>FLevACRu-MZ}I$fFaA%#U;p2tNP0D*%sq#lXsA~7g*rEoAJR9=g-F3KRgq~ty_bXdz&;9Bjz;Df!u8AiUxERLmZ<&Pi7s}IRYp|>Fr5} z=_K;Bk3SSV1zGF8ck+5mHtNXAK!%uU6iO+%_{xzpN&&y%P>m}QoJ!WJQ1!&ZqI;Z& zeg}Pzj4IA)t5zS%iAO!LEV$*$Jw2L`iokV3DjmrL072-_nM<4rLE7CVN2oMDrMD5W zUr)r4vP>Z`)ORd>zi7F`?ZmlUp5yk*11Rn;eHN=6i%bum)u(-zvN;OE^sFz!H8xu_ zO?mY%`;B2=KZS}rwiVxHWKw8p(`~JOQCdi`QGJr?UH28DlI+;us^3Pprk`q_8)|R; z{%Ntxl>I+a|6gg}%XLB0UN8>2MCB@0m*8XSCUzRQ$Rss)EfrAJGB3luymkHBuTDU( z>$w=cefnBoGIjvF_oLY4&qdI0Me)qk3%r*D+EoaH$J?skxAqkVE~BFaFaRPAk)-bA z9VU!1OcunYr(+A{9(u>gq?V+6Q1JkOET>?fmgELgT1pz*C0?X=bcz+f!1l@DyMv_i z4UL15Ef%Mh>+6wi7YJxA^q3TKZtn5W>GyN1TT(mopQP7!HuuEy-+$~>MxT|$UEZ4( zVbDX6SC>3(38y+?Y=0#wE>_z~M2helKPGb6Gh%U{Y`H#jtg0sp$ZuzIjT!K3C;xRm z)zOT@?koUWxD{lKrK14D;*C(4?E?n&2NRd_2*xxl&@)mDR^}4WuLx#Y%?jAxDQ7}E zqg?eLhQd%v9Z^=!vRf#W^9sOP_o2d?UAB|DTTMREVL?5I-ULNDorIw^IM~pjbO_c= zjm#^16pl(4=tQuZ_h2n>?%kt{vOKqjoIbN{3~{qyju=SaQMzI8+61BPsN>tiU9C4T zIYe`8=3nr>Hgf^B)TXBN)hFt*rqo@C*Yrbajiz_zEkjeU|4PZdP5&wL0-K-wO?+8u z8-aNvF3r+v`t_4t^WTp76Axl!$0C(WV2OO}WXEcRqulQliJle61?kLTxYxd}UtrV( z)LG8ODB;s%?Lqz1Of{rC^&hi zih+bQ0U)^$5u%Q~b5R57r&U&fSL^RLw|1nLH`jK>U%%NN0P(p>1U!CYQ(2!% z?|`{))lh7z3)CGjrc|1)`}OW4XtMHCNH%17tOjEsJXLli5LYp#) zdd2sIfxruMeTiQfK3jeG2Cgf2V~FEt1WeUJchj&`sF*B|S>qz!)V{y0NFvcqpQ?Bg z#{;@puj`<9_->9J{a~6dGTicaMQTY%^9^koSNiM9cSYz!z${Xev2>KJ6+@PXF>Or4 zuqQYTZ5V5VFo}0L*XfVh5lPsB@}Oofx+CVGP?AIgHpWE2l-%}w^`dwfA;9ODMLuM< zgrolKUO+9Hjde-$iVqR-Wi>S|4}`pxnUBL(u+O-TNPn$%&l_#PUSl(zSj>pJM571( z`0k=BbgNc&-td1$62UxR4x_do^d zm)%J2XV(LJN}P|M6PfxDB;a7?8Z30*nZeZ52@|@bh4!FY`NWA}EQImcoZK`8ytA!5 zCbtZmd=A9p2~iYec`iC!4yen)HOMwKx3)=VH(%|L#x-Y3KE}V)zgu1*g-1>y2@Qod z)nefE^n9<_)1+ z&LMFnGoK1YG7z_Yco)m)uj1l;Kzd8{R&?2QfpXp<`VQ>e(P|y6&#Bp0&et^@^_r-k z65mywvXYW!`1ych*R>p@?-1UFO6)o!fQJ-^x_Aje>FXo8$bg+W+iBq=t=y8ux z>tucj-sH;(d(HlZCfK`^n)iN(zx5Y%5AqVZ1t95yAW{Q}QosZOPI4d)n}NkWR0KV= z)cE{c2_EpJ5xDC}1l#=eZR68QN3{G+-o^pdX9IAd&b1 z2Mj(^#$s}^t6OT~UBS|e)ydcE;%``J@|Msk_~=D zq5L)h5)w+Vm*;hd*mbfUxolidCfN6C^CB+(iWpn&j2D9KYe=NK-+19Dm`~CCar2E# z_{p2Ip-Kj2mor6tMusNhZfE2$SST~Gd?;JK0G~|GAy-AnBO~_3dL&eQI#HI-@~A$G zWpWhRc4gnOtApN1gVS%?QYDQVd!DU{19tm><1UdlI)!+l5t`c9S` z-MRMDMMUUr9f``_ZJ1z^IG$yhdi^C7BA?C&X?e=lzJwwA&{eF>ZMb){#M>L_{p&;& zVWf5`^GEs#xo|O_PV?Uofb(B@buZV2s!j^#V#aisg!wAg{!WiVCr!RjgrVJpTrUqgNMe_ z7y=$o=(-9HC%zmUYA>CfoKBn>ob8_&Y)Vh>LVWqs`biwv`+Yfmh5>WA7)%{#VHgwQAD|CK_ zU+<9zY0yl?BA48Z$%8-<8aaZA+oe~tPawGueoS;+;Xp+E2BQX9=a5J2Df)*KCRjL> z;5cPLcZzysd{-em(WcUz4d#%l%dKz+MBN@0rg9XIMY7qlm#Zz2s@^~fNn9wZA{|l* zAyx}*DTG5_+bG@ltjS9j7+qdj8aEMK3^Rh_R5V;Pt*MmpkHXn z>o9hf<%#RvuJcf@J{gGl=}0yfoY?M7-Jb>$NP_3vDrQPrn_^G?eYtu26~_y=V~`*1 zZT=7b$D*_S@1X9~|4LxJTo;}eF8+GS8%`=dFDS~??W)!d5=AY20&Dazc?GPF+NS>g zmytuPk+PxQamk*6>De*>jYf9O2?E%)wd!T>_3iXcVchoi(k%FS`}s$<{cmdr&el^h z9V>r*q(UGlC~CKH4f4WAm=n}Zr$M{VtWFoG*xK;-M1{WGIaJVtGUHG39IhU*g{<`< z_EMB+KctHr069^a`e_*vI#l|my4hkK>zKtF@BAI<=lq|J!wcSf zM{SO_{>b4EX;YI9Ryuqr(Ddhb^bNL9whN4@1v(p!_-|+5uPXYbI}zFI*8lT(9Dr~~ zs`H<`lnR5{k`3_hKNGG=2c6Xwt6cM`vB}%*9^>x5ZFKsCo_yYGzfJWUMMUF!A{fv_ z3k*7o_m{(?%tw;>1sAF)U}<@s)Qx!H zNpo0HlXz%Yc*p7P?%uxs_Lt&#@vtl%O#5XkPapS#k)J_UiD6y69Djm=?reyx5rpeb;#Qn zzPBU1=OE`9FCsdkB5v6|x?XV_N7AvJ2{nVJANeXnQRtX8Oy;=Wt7k!FSR=n2n{m{1 zb8HdfOp&s(;j-sNTX*pl*nD&8R}tKYb$AZ39+3Ml zH?k&RueI;sW!L>Z{CjWbr6Kc&r9SVh4C0Udd*tJX_NY-gw6BW9*p7lYcU6rP350zG zQ!W-?ei6EesUmyMgtBi zhQ?nnW_DkB%$9VA{z%t~j%Am7^rns``;t7U0F7g#?Ew_U5a=9C!--7DKl9Y; z$Pxxc&d0l(JxRyB-`C}5&so7@uhWOs|myHJspm$iGbX6%f z^jime8%1ING4KChyzf^P53)(Ok|N#7e5B6&PDeIj^QiKuepSUhGQxgPzc_VDe7bur zz#x9hUr);S#b%fNuvGJcn3LM|(uq_6FwwB^Q@}7H0lPp7K8k^il|?2nr0GnThY3li z0Vt#p0?oY;pMXD&g%=gU0kiDHd!o3iCiaS}pp&N*h-zv=BufHa&Chy;8V9OchsYzN zg}US9{wWA*aE1-jIfY#i1)dB*W9%{KB+%?#s8W{*^!I3SWI-Vu@yZ*V=SjCYk--nt zP)Ujj*3Czd#O1(p{=#lEE(3{_jj?Vx5`j@{n${(Cg)VY`V>26&=@1M&(C5@2s4X2# z`)EwB7)W-|;XwF(@!Cxe)z9sTic+*Nq8oC+(yIi&%P}Sm%_4{pl+czzMJOmooXcNi zM_D@`+x5|ucvFL$$<$9^RYKq%&J@3&4J)JYpn-SYP%}CmcFPHZvs5^5)s?{BZ~U3= z4v$d)sqZB6)!#|rh*#Y@GU{#s0IwotG5Ne>Ev{*VdY${rA_rY_>#VEv$)(wZ$Rv7x zs~_~m1xNUc%Or;TTbpgjnVsqm&$mUMypWI0-QGLNa-MG0BZuEdRn8vUi1{AM_%!s8 zDR3|x za<2wd@YNVCm^_oyXtGH%$rd_0;xu{qSKpIYvRg~Lthpi|BoG21?|DlSLPPNpviRr_ zqzo8nYL<|gd>VkU=b;b;GcXY-lER_#9sARv6BT%2(j6UK1WrYo4Jq$z*zZzB6t%V(YL6FK{q3w@ zE7W?^%b1t;#*j{R6mUvSnbD_@BrOOh?f#IZ>KE{v9aD=m4rLTV-KJ5*BG%Q?3pkf~ z;y40cA{mc))$-68e1gE3brq;JjdyO zI67oTmF5Q4g~^W#)$Mzurgn^+%vPq^GMvawL&R+q3O8QrN~_T-J*`MeI3{t7dVQ)% zvo7bvj2s=_C^42!d$QqgtK&xCK(OsR&*Ii~Yj;A| zBe=cf@7jSszwfTZqyCfI{nsU}m+L>kUwpblO@e8J{$f&f5+m58BKw3tI;E_qXop{W za8s*skyb6>u`ZQMv2mA&G}F<4}ljOIolb z7#kmlHNk+^6l_KY-d{YukeO9NCX~m+D#>6~Ty>2|S$RdH5V{^!+$NM()!HdYU}RvE zE_(xaBrpI13dMHV5rFx5WFL9r?c1nD0dVE*n|1z;O<;Rxd2>zVE*kyWW^QFc41hg0 z7tbGkDJws$MI`j=Q-wSU#EUWAI4UfX^CX`$J+Xih#j9fRv;SjZKLl~NRhm- zM6g91&02KAAoQ}t9(Is+W@-d)^Dc6~2J##2MROJ5SCgg+E_gQeh?^|b0}(umV}2jw z%>N2qm_C|Fs#$w>88{O6omIOwVDgc3TBVczz8m+lJ{Fw)>eTH@l4G%Rvly?<-p^km z9I(OXt?j+PS@PK3e5@@lc4MUN$G=;;|AXg<%mpJkWU59%v=AZ0!El!3V5OdiA|Gd#{^o)!|1+2X(G)@*%RFriFkA=YyK(+LfDrA#5PzS@_4KB>U zmxzG~=+H1O6jRuD8Uq`C4UGua)R~#dkBoZvdUi=_G?v)VkZy9MVy)jW>H414n=e&TD8H#(1uDJdC!Ra%-~a zLL~-bk;s_#HN3!EZc&R0fulL+qok*Ao1ah}e;J$wx-#K5jyLw4%17^f#x?*L>jaVx z!ZfDXi+%@uP=?+-@yNZ)j+OO&N-5X<0Wj9mwT+8#6GKHcsiVVvNaEaC#A08Uq2-zO&mO^)n|_Y+7!Y((Ao!th4+!|= zXn8aZ_V9F4a*8tGYlyfgi^<6;R>S~U1dj2bESG}EArNRnFjz*&yo7~zcHMA|NPbS~ z=o$i;GRUFvjM10l<0Dg2u-8L9vl8=fI~Nwka4Vew>*Dy$$sKX@%J%0k%bl?OQB3v= zA&~PcCE;QM6OQ6IR-d-4-WmFb80VKxE9mCrI%;jalwhaD(Z&}P#D}9Yji(4=00>M61Y2zR(g_1wlxCqWo$hz5?TV zrz-^tM>b{B^Y=^5xGOo!FH`jawpef6W2@G?6)3pp>vb2~EvgKU-=6SHrQOt%>$s89 zgsd|>IQRp+$RiHQQz>2tfs;N#%#MW-2A0(VHQ##zko~^WY!*+RJb~6bdA1-x-kDZ% zxk##j)!^Ebe)F$IiNe%ReBM7-Q`^Q4KCcGj>Ra}U_CE6!J-cv6Y**2i26cU38@Sx@ z8p#@{yfK;^^dHFoi-n7kxnLj{|M=4Y?AHsJ{dpI8x-OM!kn&H*X2L;Nn0@%KV)_O@ z3@~D4hB}zX(|tdX)cXgz%}WT6KRtK_jdK|?0(^a80YOyP`y!T1XH7QD*>KxPVV@Z5 zTl|P{lu?RcbQ&s-4MYX`=Ls(O{v63X8A+}mXXebB!A32HUtwKnW#C0O| zcs@%j;kQYYK1r(Max8|Amv|^845G9i{FZCca2$`<%Q_pzEsq#4zEnBxp=?~n0AOwo z-szrUFeGfeVz~Ta)NJGFxp6Uw=?95sx2=l(kna!Q)tep0mGAwPXv4ApHO{pXB4OE( zQgwFBTtgkpGVW4;qxVMZT+~$s&4Kyv^o`*}ksB)5wb2lunuoX%vkY|}m%(+%4aY{n zruV$Q&Ys}$WzQ`N*&e&h`|T|!7{E<`^x4^SM#Qmx?)kxyz4g)a)pPHv&h+=beq3O7N#NW;YLQ&MM&zxS5zdJi%06gF4bO+?AH8@IKEtk6aMO`z4Y%yIQCj0(M zuG-}>FMnCb$O4MFs_t06It!N>x zNnO^jN1CYKEZDiRHlo5L>_({q zR4Ml2;bmoXO0S8+$p9>w81{;=_u&Y&4CR@wEM;XHBE$mlmK$J=1kkw5jq@r|lsQ2LYN{zS6lOiB<76zY2@VO7g& z_)x?L<;%!g;JRt{arlU%>|53jJvHqADfA5(;Xw1Ti{b6o0E;4CZVSV|jk^ z(Bi1r#N(@7QPg#=Q31q>kP5#976T5KEI2XatSN~`1pWBfhCa1x-Du0sqoW$y!Ds=9 zd9w3q=#Unl52>*RAx|4b0hwsNd5sde8-?0;0s4n(OUFmDcmED#(Ou`Bz9mOI@t^)M zS1EsDF}U{I-8uA8n-PiQtj}}Y&LR=nod=&5&m331i@WyI^`qs=;fbry+x$8HXXM%c zSCqTR|Nn?`A8RD=FB}vKYAmWGWGD1&{D@<8(fq|od0^@G5*ft}T;JRcQZAMVvC(tW zx)3X62r-Eq*bO&XH9k2f+GL&2JM{tTCqf9Mp#{-&C_?D}bYO35BEuYR2!K(+`~V7) z4?*EP6+=^D4+H@pYy`ZZrb@FsNcgd}C$hSxrb6j%AfiMd4vR&r3&PslQOSy`0f?6r zaCkUd35m5wB%MLdw7+0VFRv7=X^pvpZJV>AAb>?6uwM)jn-CYRZy(WeBvhw}3;b)2 zE~(m93pY&zw^$2y}%U@0|-Qd_~9}c%Y7-MR)04|=i(#ra+CHb`LKyHgVq4;iuy1n z900+igUpvT_UO*q;nl?tbKahpChLASSiIx({=sK-aHe`O_r59mY=c4#Dvr6Ltnius z6kcV+<@j9vv^o>+kwNyOEka%ni0GtQpHMy>GF2)4Cm8#az957j=Sy&H_~s@Wr^^09 zGQ`Jec`j7^uG&Kv9{9@#qk+^MrQD59p7{QpX^7*au^NE~W9_XznfU74N{og!-^a*8 zPuEY+x1FDPG^dGV&m;!E^Uo>ap_dNLOE>pd*2ojkSOgXr{XZ4!dour2tSPXCEL5); z@*hh%X^Gxqcch{}|3a+m(x0F58|$RLxc4D}8qpxeI>$AG>*aZa+D$Z(yjO(@?c$&+Vvo z@z+n@HMvMmUyC$l3#oH#eA2B9@8qw5oN@_+G*z!Y!IS?|D1gY`>bF)2ya(Y9FN9mh zTlQL-_YT{3U#uKE%!rns>EimSJ8KaRov-%(a_f-iKs62tlHWTUrhksbB#E0!%_X{b z@{{!Pt|Y(To~Jy!d!|~U*m0P*k7wigkMVEt_CYaPjPkvoKM(e{7w+=2K~(-PL>+5Y literal 0 HcmV?d00001 diff --git a/interface/resources/qml/controls-uit/CheckBox.qml b/interface/resources/qml/controls-uit/CheckBox.qml index 09a0e04148..35b81b44b4 100644 --- a/interface/resources/qml/controls-uit/CheckBox.qml +++ b/interface/resources/qml/controls-uit/CheckBox.qml @@ -88,7 +88,7 @@ Original.CheckBox { label: Label { text: control.text colorScheme: checkBox.colorScheme - x: checkBox.boxSize / 2 + x: 2 wrapMode: Text.Wrap enabled: checkBox.enabled } diff --git a/interface/resources/qml/controls-uit/Table.qml b/interface/resources/qml/controls-uit/Table.qml index c7e0809b29..21bd8b60dc 100644 --- a/interface/resources/qml/controls-uit/Table.qml +++ b/interface/resources/qml/controls-uit/Table.qml @@ -48,11 +48,12 @@ TableView { HiFiGlyphs { id: titleSort text: sortIndicatorOrder == Qt.AscendingOrder ? hifi.glyphs.caratUp : hifi.glyphs.caratDn - color: hifi.colors.baseGrayHighlight + color: hifi.colors.darkGray + opacity: 0.6; size: hifi.fontSizes.tableHeadingIcon anchors { left: titleText.right - leftMargin: -hifi.fontSizes.tableHeadingIcon / 3 - (centerHeaderText ? 3 : 0) + leftMargin: -hifi.fontSizes.tableHeadingIcon / 3 - (centerHeaderText ? 5 : 0) right: parent.right rightMargin: hifi.dimensions.tablePadding verticalCenter: titleText.verticalCenter @@ -89,7 +90,7 @@ TableView { Rectangle { color: "#00000000" anchors { fill: parent; margins: -2 } - radius: hifi.dimensions.borderRadius + //radius: hifi.dimensions.borderRadius border.color: isLightColorScheme ? hifi.colors.lightGrayText : hifi.colors.baseGrayHighlight border.width: 2 } diff --git a/interface/resources/qml/controls-uit/TabletComboBox.qml b/interface/resources/qml/controls-uit/TabletComboBox.qml index e5dec315e5..e58a465298 100644 --- a/interface/resources/qml/controls-uit/TabletComboBox.qml +++ b/interface/resources/qml/controls-uit/TabletComboBox.qml @@ -183,7 +183,7 @@ FocusScope { anchors.leftMargin: hifi.dimensions.textPadding anchors.verticalCenter: parent.verticalCenter id: popupText - text: listView.model[index] ? listView.model[index] : "" + text: listView.model[index] ? listView.model[index] : (listView.model.get(index).text ? listView.model.get(index).text : "") size: hifi.fontSizes.textFieldInput color: hifi.colors.baseGray } diff --git a/interface/resources/qml/hifi/NameCard.qml b/interface/resources/qml/hifi/NameCard.qml index 020a85b46d..fcde5817fc 100644 --- a/interface/resources/qml/hifi/NameCard.qml +++ b/interface/resources/qml/hifi/NameCard.qml @@ -14,388 +14,453 @@ import QtQuick.Controls 1.4 import QtQuick.Controls.Styles 1.4 import QtGraphicalEffects 1.0 import "../styles-uit" +import "toolbars" Item { id: thisNameCard - // Anchors - anchors { - verticalCenter: parent.verticalCenter - leftMargin: 10 - rightMargin: 10 - } + // Size + width: isMyCard ? pal.myCardWidth - anchors.leftMargin : pal.nearbyNameCardWidth; + height: isMyCard ? pal.myCardHeight : pal.rowHeight; + anchors.left: parent.left + anchors.leftMargin: 5 + anchors.top: parent.top; // Properties + property string profileUrl: ""; + property string defaultBaseUrl: "http://highfidelity.com"; + property string connectionStatus : "" property string uuid: "" property string displayName: "" property string userName: "" property real displayNameTextPixelSize: 18 - property int usernameTextHeight: 12 + property int usernameTextPixelSize: 14 property real audioLevel: 0.0 property real avgAudioLevel: 0.0 property bool isMyCard: false property bool selected: false property bool isAdmin: false - property bool currentlyEditingDisplayName: false + property string imageMaskColor: pal.color; + property string profilePicBorderColor: (connectionStatus == "connection" ? hifi.colors.indigoAccent : (connectionStatus == "friend" ? hifi.colors.greenHighlight : imageMaskColor)) - /* User image commented out for now - will probably be re-introduced later. - Column { + Item { id: avatarImage + visible: profileUrl !== ""; // Size - height: parent.height - width: height + height: isMyCard ? 70 : 42; + width: visible ? height : 0; + anchors.top: parent.top; + anchors.topMargin: isMyCard ? 0 : 8; + anchors.left: parent.left + clip: true Image { id: userImage - source: "../../icons/defaultNameCardUser.png" + source: profileUrl !== "" ? ((0 === profileUrl.indexOf("http")) ? profileUrl : (defaultBaseUrl + profileUrl)) : ""; + mipmap: true; // Anchors + anchors.fill: parent + } + AnimatedImage { + source: "../../icons/profilePicLoading.gif" + anchors.fill: parent; + visible: userImage.status != Image.Ready; + } + // Circular mask + Rectangle { + id: avatarImageMask; + visible: avatarImage.visible; + anchors.verticalCenter: avatarImage.verticalCenter; + anchors.horizontalCenter: avatarImage.horizontalCenter; + width: avatarImage.width * 2; + height: avatarImage.height * 2; + color: "transparent" + radius: avatarImage.height; + border.color: imageMaskColor; + border.width: avatarImage.height/2; + } + StateImage { + id: infoHoverImage; + visible: avatarImageMouseArea.containsMouse ? true : false; + imageURL: "../../images/info-icon-2-state.svg"; + size: 32; + buttonState: 1; + anchors.centerIn: parent; + } + MouseArea { + id: avatarImageMouseArea; + anchors.fill: parent + enabled: selected || isMyCard; + hoverEnabled: enabled + onClicked: { + /* + THIS WILL OPEN THE BROWSER TO THE USER'S INFO PAGE! + I've no idea how to do this yet.. + */ + } + } + } + + // Colored border around avatarImage + Rectangle { + id: avatarImageBorder; + visible: avatarImage.visible; + anchors.verticalCenter: avatarImage.verticalCenter; + anchors.horizontalCenter: avatarImage.horizontalCenter; + width: avatarImage.width + border.width; + height: avatarImage.height + border.width; + color: "transparent" + radius: avatarImage.height; + border.color: profilePicBorderColor; + border.width: 4; + } + + // DisplayName field for my card + Rectangle { + id: myDisplayName + visible: isMyCard + // Size + width: parent.width - avatarImage.width - anchors.leftMargin*2 - anchors.rightMargin; + height: 40 + // Anchors + anchors.top: avatarImage.top + anchors.left: avatarImage.right + anchors.leftMargin: 5; + anchors.rightMargin: 5; + // Style + color: myDisplayNameMouseArea.containsMouse ? hifi.colors.lightGrayText : hifi.colors.textFieldLightBackground + border.color: hifi.colors.blueHighlight + border.width: 0 + TextInput { + id: myDisplayNameText + // Properties + text: thisNameCard.displayName + maximumLength: 256 + clip: true + // Size width: parent.width height: parent.height - } - } - */ - Item { - id: textContainer - // Size - width: parent.width - /*avatarImage.width - parent.spacing - */parent.anchors.leftMargin - parent.anchors.rightMargin - height: selected || isMyCard ? childrenRect.height : childrenRect.height - 15 - anchors.verticalCenter: parent.verticalCenter - - // DisplayName field for my card - Rectangle { - id: myDisplayName - visible: isMyCard - // Size - width: parent.width + 70 - height: 35 // Anchors - anchors.top: parent.top + anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left - anchors.leftMargin: -10 + anchors.leftMargin: 10 + anchors.right: parent.right + anchors.rightMargin: editGlyph.width + editGlyph.anchors.rightMargin // Style - color: hifi.colors.textFieldLightBackground - border.color: hifi.colors.blueHighlight - border.width: 0 - TextInput { - id: myDisplayNameText - // Properties - text: thisNameCard.displayName - maximumLength: 256 - clip: true - // Size - width: parent.width - height: parent.height - // Anchors - anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.left - anchors.leftMargin: 10 - anchors.right: parent.right - anchors.rightMargin: editGlyph.width + editGlyph.anchors.rightMargin - // Style - color: hifi.colors.darkGray - FontLoader { id: firaSansSemiBold; source: "../../fonts/FiraSans-SemiBold.ttf"; } - font.family: firaSansSemiBold.name - font.pixelSize: displayNameTextPixelSize - selectionColor: hifi.colors.blueHighlight - selectedTextColor: "black" - // Text Positioning - verticalAlignment: TextInput.AlignVCenter - horizontalAlignment: TextInput.AlignLeft - // Signals - onEditingFinished: { - pal.sendToScript({method: 'displayNameUpdate', params: text}) - cursorPosition = 0 - focus = false - myDisplayName.border.width = 0 - color = hifi.colors.darkGray - currentlyEditingDisplayName = false - } - } - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.LeftButton - hoverEnabled: true - onClicked: { - myDisplayName.border.width = 1 - myDisplayNameText.focus ? myDisplayNameText.cursorPosition = myDisplayNameText.positionAt(mouseX, mouseY, TextInput.CursorOnCharacter) : myDisplayNameText.selectAll(); - myDisplayNameText.focus = true - myDisplayNameText.color = "black" - currentlyEditingDisplayName = true - } - onDoubleClicked: { - myDisplayNameText.selectAll(); - myDisplayNameText.focus = true; - currentlyEditingDisplayName = true - } - onEntered: myDisplayName.color = hifi.colors.lightGrayText - onExited: myDisplayName.color = hifi.colors.textFieldLightBackground - } - // Edit pencil glyph - HiFiGlyphs { - id: editGlyph - text: hifi.glyphs.editPencil - // Text Size - size: displayNameTextPixelSize*1.5 - // Anchors - anchors.right: parent.right - anchors.rightMargin: 5 - anchors.verticalCenter: parent.verticalCenter - // Style - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - color: hifi.colors.baseGray - } - } - // Spacer for DisplayName for my card - Item { - id: myDisplayNameSpacer - width: 1 - height: 4 - // Anchors - anchors.top: myDisplayName.bottom - } - // DisplayName container for others' cards - Item { - id: displayNameContainer - visible: !isMyCard - // Size - width: parent.width - height: displayNameTextPixelSize + 4 - // Anchors - anchors.top: parent.top - anchors.left: parent.left - // DisplayName Text for others' cards - FiraSansSemiBold { - id: displayNameText - // Properties - text: thisNameCard.displayName - elide: Text.ElideRight - // Size - width: isAdmin ? Math.min(displayNameTextMetrics.tightBoundingRect.width + 8, parent.width - adminLabelText.width - adminLabelQuestionMark.width + 8) : parent.width - // Anchors - anchors.top: parent.top - anchors.left: parent.left - // Text Size - size: displayNameTextPixelSize - // Text Positioning - verticalAlignment: Text.AlignVCenter - // Style - color: hifi.colors.darkGray - } - TextMetrics { - id: displayNameTextMetrics - font: displayNameText.font - text: displayNameText.text - } - // "ADMIN" label for other users' cards - RalewaySemiBold { - id: adminLabelText - visible: isAdmin - text: "ADMIN" - // Text size - size: displayNameText.size - 4 - // Anchors - anchors.verticalCenter: parent.verticalCenter - anchors.left: displayNameText.right - // Style - font.capitalization: Font.AllUppercase - color: hifi.colors.redHighlight - // Alignment - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignTop - } - // This Rectangle refers to the [?] popup button next to "ADMIN" - Item { - id: adminLabelQuestionMark - visible: isAdmin - // Size - width: 20 - height: displayNameText.height - // Anchors - anchors.verticalCenter: parent.verticalCenter - anchors.left: adminLabelText.right - RalewayRegular { - id: adminLabelQuestionMarkText - text: "[?]" - size: adminLabelText.size - font.capitalization: Font.AllUppercase - color: hifi.colors.redHighlight - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - anchors.fill: parent - } - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.LeftButton - hoverEnabled: true - onClicked: letterbox(hifi.glyphs.question, - "Domain Admin", - "This user is an admin on this domain. Admins can Silence and Ban other users at their discretion - so be extra nice!") - onEntered: adminLabelQuestionMarkText.color = "#94132e" - onExited: adminLabelQuestionMarkText.color = hifi.colors.redHighlight - } - } - } - - // UserName Text - FiraSansRegular { - id: userNameText - // Properties - text: thisNameCard.userName - elide: Text.ElideRight - visible: thisNameCard.displayName - // Size - width: parent.width - // Anchors - anchors.top: isMyCard ? myDisplayNameSpacer.bottom : displayNameContainer.bottom - // Text Size - size: thisNameCard.usernameTextHeight + color: hifi.colors.darkGray + FontLoader { id: firaSansSemiBold; source: "../../fonts/FiraSans-SemiBold.ttf"; } + font.family: firaSansSemiBold.name + font.pixelSize: displayNameTextPixelSize + selectionColor: hifi.colors.blueHighlight + selectedTextColor: "black" // Text Positioning - verticalAlignment: Text.AlignVCenter + verticalAlignment: TextInput.AlignVCenter + horizontalAlignment: TextInput.AlignLeft + autoScroll: false; + // Signals + onEditingFinished: { + pal.sendToScript({method: 'displayNameUpdate', params: text}) + cursorPosition = 0 + focus = false + myDisplayName.border.width = 0 + color = hifi.colors.darkGray + pal.currentlyEditingDisplayName = false + autoScroll = false; + } + } + MouseArea { + id: myDisplayNameMouseArea; + anchors.fill: parent + hoverEnabled: true + onClicked: { + myDisplayName.border.width = 1 + myDisplayNameText.focus ? myDisplayNameText.cursorPosition = myDisplayNameText.positionAt(mouseX, mouseY, TextInput.CursorOnCharacter) : myDisplayNameText.selectAll(); + myDisplayNameText.focus = true + myDisplayNameText.color = "black" + pal.currentlyEditingDisplayName = true + myDisplayNameText.autoScroll = true; + } + onDoubleClicked: { + myDisplayNameText.selectAll(); + myDisplayNameText.focus = true; + pal.currentlyEditingDisplayName = true + myDisplayNameText.autoScroll = true; + } + } + // Edit pencil glyph + HiFiGlyphs { + id: editGlyph + text: hifi.glyphs.editPencil + // Text Size + size: displayNameTextPixelSize*1.5 + // Anchors + anchors.right: parent.right + anchors.rightMargin: 5 + anchors.verticalCenter: parent.verticalCenter // Style + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter color: hifi.colors.baseGray } - - // Spacer - Item { - id: userNameSpacer - height: 4 - width: parent.width - // Anchors - anchors.top: userNameText.bottom - } - - // VU Meter - Rectangle { - id: nameCardVUMeter - // Size - width: isMyCard ? myDisplayName.width - 70 : ((gainSlider.value - gainSlider.minimumValue)/(gainSlider.maximumValue - gainSlider.minimumValue)) * parent.width - height: 8 - // Anchors - anchors.top: userNameSpacer.bottom - // Style - radius: 4 - color: "#c5c5c5" - visible: isMyCard || selected - // Rectangle for the zero-gain point on the VU meter - Rectangle { - id: vuMeterZeroGain - visible: gainSlider.visible - // Size - width: 4 - height: 18 - // Style - color: hifi.colors.darkGray - // Anchors - anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.left - anchors.leftMargin: (-gainSlider.minimumValue)/(gainSlider.maximumValue - gainSlider.minimumValue) * gainSlider.width - 4 - } - // Rectangle for the VU meter line - Rectangle { - id: vuMeterLine - width: gainSlider.width - visible: gainSlider.visible - // Style - color: vuMeterBase.color - radius: nameCardVUMeter.radius - height: nameCardVUMeter.height / 2 - anchors.verticalCenter: nameCardVUMeter.verticalCenter - } - // Rectangle for the VU meter base - Rectangle { - id: vuMeterBase - // Anchors - anchors.fill: parent - visible: isMyCard || selected - // Style - color: parent.color - radius: parent.radius - } - // Rectangle for the VU meter audio level - Rectangle { - id: vuMeterLevel - visible: isMyCard || selected - // Size - width: (thisNameCard.audioLevel) * parent.width - // Style - color: parent.color - radius: parent.radius - // Anchors - anchors.bottom: parent.bottom - anchors.top: parent.top - anchors.left: parent.left - } - // Gradient for the VU meter audio level - LinearGradient { - anchors.fill: vuMeterLevel - source: vuMeterLevel - start: Qt.point(0, 0) - end: Qt.point(parent.width, 0) - gradient: Gradient { - GradientStop { position: 0.0; color: "#2c8e72" } - GradientStop { position: 0.9; color: "#1fc6a6" } - GradientStop { position: 0.91; color: "#ea4c5f" } - GradientStop { position: 1.0; color: "#ea4c5f" } - } - } - } - - // Per-Avatar Gain Slider - Slider { - id: gainSlider - // Size - width: parent.width - height: 14 - // Anchors - anchors.verticalCenter: nameCardVUMeter.verticalCenter + } + // DisplayName container for others' cards + Item { + id: displayNameContainer + visible: !isMyCard + // Size + width: parent.width - anchors.leftMargin - avatarImage.width - anchors.leftMargin; + height: displayNameTextPixelSize + 4 + // Anchors + anchors.top: pal.activeTab == "connectionsTab" ? undefined : avatarImage.top; + anchors.bottom: pal.activeTab == "connectionsTab" ? avatarImage.bottom : undefined; + anchors.left: avatarImage.right + anchors.leftMargin: avatarImage.visible ? 5 : 0; + // DisplayName Text for others' cards + FiraSansSemiBold { + id: displayNameText // Properties - visible: !isMyCard && selected - value: Users.getAvatarGain(uuid) - minimumValue: -60.0 - maximumValue: 20.0 - stepSize: 5 - updateValueWhileDragging: true - onValueChanged: updateGainFromQML(uuid, value, false) - onPressedChanged: { - if (!pressed) { - updateGainFromQML(uuid, value, true) - } + text: thisNameCard.displayName + elide: Text.ElideRight + // Size + width: isAdmin ? Math.min(displayNameTextMetrics.tightBoundingRect.width + 8, parent.width - adminLabelText.width - adminLabelQuestionMark.width + 8) : parent.width + // Anchors + anchors.top: parent.top + anchors.left: parent.left + // Text Size + size: displayNameTextPixelSize + // Text Positioning + verticalAlignment: Text.AlignTop + // Style + color: (pal.activeTab == "nearbyTab" && (displayNameTextMouseArea.containsMouse || userNameTextMouseArea.containsMouse)) + ? hifi.colors.blueHighlight : (pal.activeTab == "nearbyTab" ? hifi.colors.darkGray : hifi.colors.greenShadow); + MouseArea { + id: displayNameTextMouseArea; + anchors.fill: parent + enabled: selected && pal.activeTab == "nearbyTab" && thisNameCard.userName !== ""; + hoverEnabled: enabled + onClicked: pal.sendToScript({method: 'goToUser', params: thisNameCard.userName}); + } + } + TextMetrics { + id: displayNameTextMetrics + font: displayNameText.font + text: displayNameText.text + } + // "ADMIN" label for other users' cards + RalewaySemiBold { + id: adminLabelText + visible: isAdmin + text: "ADMIN" + // Text size + size: displayNameText.size - 4 + // Anchors + anchors.verticalCenter: parent.verticalCenter + anchors.left: displayNameText.right + // Style + font.capitalization: Font.AllUppercase + color: hifi.colors.redHighlight + // Alignment + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignTop + } + // This Rectangle refers to the [?] popup button next to "ADMIN" + Item { + id: adminLabelQuestionMark + visible: isAdmin + // Size + width: 20 + height: displayNameText.height + // Anchors + anchors.verticalCenter: parent.verticalCenter + anchors.left: adminLabelText.right + RalewayRegular { + id: adminLabelQuestionMarkText + text: "[?]" + size: adminLabelText.size + font.capitalization: Font.AllUppercase + color: hifi.colors.redHighlight + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + anchors.fill: parent } MouseArea { anchors.fill: parent - onWheel: { - // Do nothing. - } - onDoubleClicked: { - gainSlider.value = 0.0 - } - onPressed: { - // Pass through to Slider - mouse.accepted = false - } - onReleased: { - // the above mouse.accepted seems to make this - // never get called, nonetheless... - mouse.accepted = false - } - } - style: SliderStyle { - groove: Rectangle { - color: "#c5c5c5" - implicitWidth: gainSlider.width - implicitHeight: 4 - radius: 2 - opacity: 0 - } - handle: Rectangle { - anchors.centerIn: parent - color: (control.pressed || control.hovered) ? "#00b4ef" : "#8F8F8F" - implicitWidth: 10 - implicitHeight: 16 - } + hoverEnabled: true + onClicked: letterbox(hifi.glyphs.question, + "Domain Admin", + "This user is an admin on this domain. Admins can Silence and Ban other users at their discretion - so be extra nice!") + onEntered: adminLabelQuestionMarkText.color = "#94132e" + onExited: adminLabelQuestionMarkText.color = hifi.colors.redHighlight } } } + // UserName Text + FiraSansRegular { + id: userNameText + // Properties + text: thisNameCard.userName + elide: Text.ElideRight + visible: thisNameCard.displayName + // Size + width: parent.width + height: usernameTextPixelSize + 4 + // Anchors + anchors.top: isMyCard ? myDisplayName.bottom : undefined; + anchors.bottom: isMyCard ? undefined : avatarImage.bottom + anchors.left: avatarImage.right; + anchors.leftMargin: avatarImage.visible ? 5 : 0; + // Text Size + size: usernameTextPixelSize; + // Text Positioning + verticalAlignment: Text.AlignBottom + // Style + color: (pal.activeTab == "nearbyTab" && (displayNameTextMouseArea.containsMouse || userNameTextMouseArea.containsMouse)) ? hifi.colors.blueHighlight : hifi.colors.greenShadow; + MouseArea { + id: userNameTextMouseArea; + anchors.fill: parent + enabled: selected && pal.activeTab == "nearbyTab" && thisNameCard.userName !== ""; + hoverEnabled: enabled + onClicked: pal.sendToScript({method: 'goToUser', params: thisNameCard.userName}); + } + } + // VU Meter + Rectangle { + id: nameCardVUMeter + // Size + width: isMyCard ? myDisplayName.width - 20 : ((gainSlider.value - gainSlider.minimumValue)/(gainSlider.maximumValue - gainSlider.minimumValue)) * (gainSlider.width); + height: 8 + // Anchors + anchors.bottom: isMyCard ? avatarImage.bottom : parent.bottom; + anchors.bottomMargin: isMyCard ? 0 : height; + anchors.left: isMyCard ? userNameText.left : parent.left; + // Style + radius: 4 + color: "#c5c5c5" + visible: isMyCard || (selected && pal.activeTab == "nearbyTab") + // Rectangle for the zero-gain point on the VU meter + Rectangle { + id: vuMeterZeroGain + visible: gainSlider.visible + // Size + width: 4 + height: 18 + // Style + color: hifi.colors.darkGray + // Anchors + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.leftMargin: (-gainSlider.minimumValue)/(gainSlider.maximumValue - gainSlider.minimumValue) * gainSlider.width - 4 + } + // Rectangle for the VU meter line + Rectangle { + id: vuMeterLine + width: gainSlider.width + visible: gainSlider.visible + // Style + color: vuMeterBase.color + radius: nameCardVUMeter.radius + height: nameCardVUMeter.height / 2 + anchors.verticalCenter: nameCardVUMeter.verticalCenter + } + // Rectangle for the VU meter base + Rectangle { + id: vuMeterBase + // Anchors + anchors.fill: parent + visible: isMyCard || selected + // Style + color: parent.color + radius: parent.radius + } + // Rectangle for the VU meter audio level + Rectangle { + id: vuMeterLevel + visible: isMyCard || selected + // Size + width: (thisNameCard.audioLevel) * parent.width + // Style + color: parent.color + radius: parent.radius + // Anchors + anchors.bottom: parent.bottom + anchors.top: parent.top + anchors.left: parent.left + } + // Gradient for the VU meter audio level + LinearGradient { + anchors.fill: vuMeterLevel + source: vuMeterLevel + start: Qt.point(0, 0) + end: Qt.point(parent.width, 0) + gradient: Gradient { + GradientStop { position: 0.0; color: "#2c8e72" } + GradientStop { position: 0.9; color: "#1fc6a6" } + GradientStop { position: 0.91; color: "#ea4c5f" } + GradientStop { position: 1.0; color: "#ea4c5f" } + } + } + } + + // Per-Avatar Gain Slider + Slider { + id: gainSlider + // Size + width: thisNameCard.width; + height: 14 + // Anchors + anchors.verticalCenter: nameCardVUMeter.verticalCenter; + anchors.left: nameCardVUMeter.left; + // Properties + visible: !isMyCard && selected && pal.activeTab == "nearbyTab"; + value: Users.getAvatarGain(uuid) + minimumValue: -60.0 + maximumValue: 20.0 + stepSize: 5 + updateValueWhileDragging: true + onValueChanged: { + if (uuid !== "") { + updateGainFromQML(uuid, value, false); + } + } + onPressedChanged: { + if (!pressed) { + updateGainFromQML(uuid, value, true) + } + } + MouseArea { + anchors.fill: parent + onWheel: { + // Do nothing. + } + onDoubleClicked: { + gainSlider.value = 0.0 + } + onPressed: { + // Pass through to Slider + mouse.accepted = false + } + onReleased: { + // the above mouse.accepted seems to make this + // never get called, nonetheless... + mouse.accepted = false + } + } + style: SliderStyle { + groove: Rectangle { + color: "#c5c5c5" + implicitWidth: gainSlider.width + implicitHeight: 4 + radius: 2 + opacity: 0 + } + handle: Rectangle { + anchors.centerIn: parent + color: (control.pressed || control.hovered) ? "#00b4ef" : "#8F8F8F" + implicitWidth: 10 + implicitHeight: 16 + } + } + } + function updateGainFromQML(avatarUuid, sliderValue, isReleased) { Users.setAvatarGain(avatarUuid, sliderValue); if (isReleased) { diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index 25362d98f1..5fb11b4e2f 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -28,16 +28,21 @@ Rectangle { // Style color: "#E3E3E3"; // Properties - property int myCardHeight: 90; - property int rowHeight: 70; + property int myCardWidth: palContainer.width - upperRightInfoContainer.width; + property int myCardHeight: 82; + property int rowHeight: 60; property int actionButtonWidth: 55; - property int actionButtonAllowance: actionButtonWidth * 2; - property int minNameCardWidth: palContainer.width - (actionButtonAllowance * 2) - 4 - hifi.dimensions.scrollbarBackgroundWidth; - property int nameCardWidth: minNameCardWidth + (iAmAdmin ? 0 : actionButtonAllowance); - property var myData: ({displayName: "", userName: "", audioLevel: 0.0, avgAudioLevel: 0.0, admin: true}); // valid dummy until set + property int locationColumnWidth: 170; + property int nearbyNameCardWidth: nearbyTable.width - (iAmAdmin ? (actionButtonWidth * 4) : (actionButtonWidth * 2)) - 4 - hifi.dimensions.scrollbarBackgroundWidth; + property int connectionsNameCardWidth: connectionsTable.width - locationColumnWidth - actionButtonWidth - 4 - hifi.dimensions.scrollbarBackgroundWidth; + property var myData: ({profileUrl: "", displayName: "", userName: "", audioLevel: 0.0, avgAudioLevel: 0.0, admin: true, placeName: "", connection: ""}); // valid dummy until set property var ignored: ({}); // Keep a local list of ignored avatars & their data. Necessary because HashMap is slow to respond after ignoring. - property var userModelData: []; // This simple list is essentially a mirror of the userModel listModel without all the extra complexities. + property var nearbyUserModelData: []; // This simple list is essentially a mirror of the nearbyUserModel listModel without all the extra complexities. + property var connectionsUserModelData: []; // This simple list is essentially a mirror of the connectionsUserModel listModel without all the extra complexities. property bool iAmAdmin: false; + property var activeTab: "nearbyTab"; + property int usernameAvailability; + property bool currentlyEditingDisplayName: false HifiConstants { id: hifi; } @@ -59,24 +64,39 @@ Rectangle { category: "pal"; property bool filtered: false; property int nearDistance: 30; - property int sortIndicatorColumn: 1; - property int sortIndicatorOrder: Qt.AscendingOrder; + property int nearbySortIndicatorColumn: 1; + property int nearbySortIndicatorOrder: Qt.AscendingOrder; + property int connectionsSortIndicatorColumn: 0; + property int connectionsSortIndicatorOrder: Qt.AscendingOrder; } - function getSelectedSessionIDs() { + function getSelectedNearbySessionIDs() { var sessionIDs = []; - table.selection.forEach(function (userIndex) { - sessionIDs.push(userModelData[userIndex].sessionId); + nearbyTable.selection.forEach(function (userIndex) { + var datum = nearbyUserModelData[userIndex]; + if (datum) { // Might have been filtered out + sessionIDs.push(datum.sessionId); + } }); return sessionIDs; } - function refreshWithFilter() { - // We should just be able to set settings.filtered to filter.checked, but see #3249, so send to .js for saving. - var userIds = getSelectedSessionIDs(); - var params = {filter: filter.checked && {distance: settings.nearDistance}}; + function getSelectedConnectionsUserNames() { + var userNames = []; + connectionsTable.selection.forEach(function (userIndex) { + var datum = connectionsUserModelData[userIndex]; + if (datum) { + userNames.push(datum.userName); + } + }); + return userNames; + } + function refreshNearbyWithFilter() { + // We should just be able to set settings.filtered to inViewCheckbox.checked, but see #3249, so send to .js for saving. + var userIds = getSelectedNearbySessionIDs(); + var params = {filter: inViewCheckbox.checked && {distance: settings.nearDistance}}; if (userIds.length > 0) { params.selected = [[userIds[0]], true, true]; } - pal.sendToScript({method: 'refresh', params: params}); + pal.sendToScript({method: 'refreshNearby', params: params}); } // This is the container for the PAL @@ -90,8 +110,6 @@ Rectangle { color: pal.color; // Anchors anchors.centerIn: pal; - // Properties - radius: hifi.dimensions.borderRadius; // This contains the current user's NameCard and will contain other information in the future Rectangle { @@ -103,75 +121,351 @@ Rectangle { color: pal.color; // Anchors anchors.top: palContainer.top; - // Properties - radius: hifi.dimensions.borderRadius; - // This NameCard refers to the current user's NameCard (the one above the table) + // This NameCard refers to the current user's NameCard (the one above the nearbyTable) NameCard { id: myCard; // Properties + profileUrl: myData.profileUrl; + imageMaskColor: pal.color; displayName: myData.displayName; userName: myData.userName; audioLevel: myData.audioLevel; avgAudioLevel: myData.avgAudioLevel; isMyCard: true; // Size - width: minNameCardWidth; + width: myCardWidth; height: parent.height; // Anchors + anchors.top: parent.top anchors.left: parent.left; } - Row { - HifiControls.CheckBox { - id: filter; - checked: settings.filtered; - text: "in view"; - boxSize: reload.height * 0.70; - onCheckedChanged: refreshWithFilter(); + Item { + id: upperRightInfoContainer; + width: 160; + height: parent.height; + anchors.top: parent.top; + anchors.right: parent.right; + + RalewayRegular { + id: availabilityText; + text: "set availability"; + // Text size + size: hifi.fontSizes.tabularData; + // Anchors + anchors.top: availabilityComboBox.bottom; + anchors.horizontalCenter: parent.horizontalCenter; + // Style + color: hifi.colors.baseGrayHighlight; + // Alignment + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignTop; } - HifiControls.GlyphButton { - id: reload; - glyph: hifi.glyphs.reload; - width: reload.height; - onClicked: refreshWithFilter(); - } - spacing: 50; - anchors { - right: parent.right; - top: parent.top; - topMargin: 10; + HifiControls.TabletComboBox { + id: availabilityComboBox; + // Anchors + anchors.top: parent.top; + anchors.horizontalCenter: parent.horizontalCenter; + // Size + width: parent.width; + height: 40; + currentIndex: usernameAvailability; + model: ListModel { + id: availabilityComboBoxListItems + ListElement { text: "Everyone"; value: "all"; } + ListElement { text: "Friends Only"; value: "friends"; } + ListElement { text: "Appear Offline"; value: "none" } + } + onCurrentIndexChanged: { pal.sendToScript({method: 'setAvailability', params: availabilityComboBoxListItems.get(currentIndex).value})} } } } - // Rectangles used to cover up rounded edges on bottom of MyInfo Rectangle - Rectangle { - color: pal.color; - width: palContainer.width; - height: 10; - anchors.top: myInfo.bottom; - anchors.left: parent.left; + Item { + id: palTabContainer; + // Anchors + anchors { + top: myInfo.bottom; + bottom: parent.bottom; + left: parent.left; + right: parent.right; } Rectangle { - color: pal.color; - width: palContainer.width; - height: 10; - anchors.bottom: table.top; - anchors.left: parent.left; + id: tabSelectorContainer; + // Anchors + anchors { + top: parent.top; + topMargin: 2; + horizontalCenter: parent.horizontalCenter; + } + width: parent.width; + height: 35 - anchors.topMargin; + Rectangle { + id: nearbyTabSelector; + // Anchors + anchors { + top: parent.top; + left: parent.left; + } + width: parent.width/2; + height: parent.height; + color: activeTab == "nearbyTab" ? pal.color : "#CCCCCC"; + MouseArea { + anchors.fill: parent; + onClicked: { + if (activeTab != "nearbyTab") { + refreshNearbyWithFilter(); + } + activeTab = "nearbyTab"; + } + } + + // "NEARBY" Text Container + Item { + id: nearbyTabSelectorTextContainer; + anchors.fill: parent; + anchors.leftMargin: 15; + // "NEARBY" text + RalewaySemiBold { + id: nearbyTabSelectorText; + text: "NEARBY"; + // Text size + size: hifi.fontSizes.tabularData; + // Anchors + anchors.fill: parent; + // Style + font.capitalization: Font.AllUppercase; + color: hifi.colors.redHighlight; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + } + // "In View" Checkbox + HifiControls.CheckBox { + id: inViewCheckbox; + visible: activeTab == "nearbyTab"; + anchors.right: reloadNearbyContainer.left; + anchors.rightMargin: 25; + anchors.verticalCenter: parent.verticalCenter; + checked: settings.filtered; + text: "in view"; + boxSize: 24; + onCheckedChanged: refreshNearbyWithFilter(); + } + // Refresh button + Rectangle { + id: reloadNearbyContainer + visible: activeTab == "nearbyTab"; + anchors.verticalCenter: parent.verticalCenter; + anchors.right: parent.right; + anchors.rightMargin: 6; + height: reloadNearby.height; + width: height; + HifiControls.GlyphButton { + id: reloadNearby; + width: reloadNearby.height; + glyph: hifi.glyphs.reload; + onClicked: refreshNearbyWithFilter(); + } + } + } + } + Rectangle { + id: connectionsTabSelector; + // Anchors + anchors { + top: parent.top; + left: nearbyTabSelector.right; + } + width: parent.width/2; + height: parent.height; + color: activeTab == "connectionsTab" ? pal.color : "#CCCCCC"; + MouseArea { + anchors.fill: parent; + onClicked: { + if (activeTab != "connectionsTab") { + pal.sendToScript({method: 'refreshConnections'}); + } + activeTab = "connectionsTab"; + connectionsLoading.visible = true; + } + } + + // "CONNECTIONS" Text Container + Item { + id: connectionsTabSelectorTextContainer; + anchors.fill: parent; + anchors.leftMargin: 15; + // Refresh button + Rectangle { + visible: activeTab == "connectionsTab"; + anchors.verticalCenter: parent.verticalCenter; + anchors.right: parent.right; + anchors.rightMargin: 6; + height: reloadConnections.height; + width: height; + HifiControls.GlyphButton { + id: reloadConnections; + width: reloadConnections.height; + glyph: hifi.glyphs.reload; + onClicked: { + connectionsLoading.visible = true; + pal.sendToScript({method: 'refreshConnections'}); + } + } + } + // "CONNECTIONS" text + RalewaySemiBold { + id: connectionsTabSelectorText; + text: "CONNECTIONS"; + // Text size + size: hifi.fontSizes.tabularData; + // Anchors + anchors.fill: parent; + // Style + font.capitalization: Font.AllUppercase; + color: hifi.colors.redHighlight; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + } + TextMetrics { + id: connectionsTabSelectorTextMetrics; + text: connectionsTabSelectorText.text; + } + + // This Rectangle refers to the [?] popup button next to "CONNECTIONS" + Rectangle { + color: connectionsTabSelector.color; + width: 20; + height: connectionsTabSelectorText.height - 2; + anchors.left: connectionsTabSelectorTextContainer.left; + anchors.top: connectionsTabSelectorTextContainer.top; + anchors.topMargin: 1; + anchors.leftMargin: connectionsTabSelectorTextMetrics.width + 42; + RalewayRegular { + id: connectionsHelpText; + text: "[?]"; + size: connectionsTabSelectorText.size + 6; + font.capitalization: Font.AllUppercase; + color: connectionsTabSelectorMouseArea.containsMouse ? hifi.colors.redAccent : hifi.colors.redHighlight; + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + anchors.fill: parent; + } + MouseArea { + id: connectionsTabSelectorMouseArea; + anchors.fill: parent; + hoverEnabled: true; + onClicked: letterbox(hifi.glyphs.question, + "Connections and Friends", + "Purple borders around profile pictures are Connections.
" + + "When your availability is set to Everyone, Connections can see your username and location.

" + + "Green borders around profile pictures are Friends.
" + + "When your availability is set to Friends, only Friends can see your username and location."); + } + } + } + } } + Item { + id: tabBorders; + anchors.fill: parent; + property var color: hifi.colors.lightGray; + property int borderWeight: 3; + // Left border + Rectangle { + color: parent.color; + anchors { + left: parent.left; + bottom: parent.bottom; + } + width: parent.borderWeight; + height: parent.height - (activeTab == "nearbyTab" ? 0 : tabSelectorContainer.height); + } + // Right border + Rectangle { + color: parent.color; + anchors { + right: parent.right; + bottom: parent.bottom; + } + width: parent.borderWeight; + height: parent.height - (activeTab == "nearbyTab" ? tabSelectorContainer.height : 0); + } + // Bottom border + Rectangle { + color: parent.color; + anchors { + bottom: parent.bottom; + left: parent.left; + right: parent.right; + } + height: parent.borderWeight; + } + // Border between buttons + Rectangle { + color: parent.color; + anchors { + horizontalCenter: parent.horizontalCenter; + top: parent.top; + } + width: parent.borderWeight; + height: tabSelectorContainer.height + width; + } + // Border above selected tab + Rectangle { + color: parent.color; + anchors { + top: parent.top; + left: parent.left; + leftMargin: activeTab == "nearbyTab" ? 0 : parent.width/2; + } + width: parent.width/2; + height: parent.borderWeight; + } + // Border below unselected tab + Rectangle { + color: parent.color; + anchors { + top: parent.top; + topMargin: tabSelectorContainer.height; + left: parent.left; + leftMargin: activeTab == "nearbyTab" ? parent.width/2 : 0; + } + width: parent.width/2; + height: parent.borderWeight; + } + } + + /***************************************** + NEARBY TAB + *****************************************/ + Rectangle { + id: nearbyTab; + // Anchors + anchors { + top: tabSelectorContainer.bottom; + topMargin: 12 + (iAmAdmin ? -adminTab.anchors.topMargin : 0); + bottom: parent.bottom; + bottomMargin: 12; + horizontalCenter: parent.horizontalCenter; + } + width: parent.width - 12; + visible: activeTab == "nearbyTab"; + // Rectangle that houses "ADMIN" string Rectangle { id: adminTab; // Size - width: 2*actionButtonWidth + hifi.dimensions.scrollbarBackgroundWidth + 2; + width: 2*actionButtonWidth + hifi.dimensions.scrollbarBackgroundWidth + 6; height: 40; // Anchors - anchors.bottom: myInfo.bottom; - anchors.bottomMargin: -10; - anchors.right: myInfo.right; + anchors.top: parent.top; + anchors.topMargin: -30; + anchors.right: parent.right; // Properties visible: iAmAdmin; // Style color: hifi.colors.tableRowLightEven; - radius: hifi.dimensions.borderRadius; border.color: hifi.colors.lightGrayText; border.width: 2; // "ADMIN" text @@ -194,27 +488,23 @@ Rectangle { verticalAlignment: Text.AlignTop; } } - // This TableView refers to the table (below the current user's NameCard) + // This TableView refers to the Nearby Table (on the "Nearby" tab below the current user's NameCard) HifiControls.Table { - id: table; - // Size - height: palContainer.height - myInfo.height - 4; - width: palContainer.width - 4; + id: nearbyTable; // Anchors - anchors.left: parent.left; - anchors.top: myInfo.bottom; + anchors.fill: parent; // Properties centerHeaderText: true; sortIndicatorVisible: true; headerVisible: true; - sortIndicatorColumn: settings.sortIndicatorColumn; - sortIndicatorOrder: settings.sortIndicatorOrder; + sortIndicatorColumn: settings.nearbySortIndicatorColumn; + sortIndicatorOrder: settings.nearbySortIndicatorOrder; onSortIndicatorColumnChanged: { - settings.sortIndicatorColumn = sortIndicatorColumn; + settings.nearbySortIndicatorColumn = sortIndicatorColumn; sortModel(); } onSortIndicatorOrderChanged: { - settings.sortIndicatorOrder = sortIndicatorOrder; + settings.nearbySortIndicatorOrder = sortIndicatorOrder; sortModel(); } @@ -229,8 +519,8 @@ Rectangle { TableViewColumn { id: displayNameHeader; role: "displayName"; - title: table.rowCount + (table.rowCount === 1 ? " NAME" : " NAMES"); - width: nameCardWidth; + title: nearbyTable.rowCount + (nearbyTable.rowCount === 1 ? " NAME" : " NAMES"); + width: nearbyNameCardWidth; movable: false; resizable: false; } @@ -258,16 +548,14 @@ Rectangle { resizable: false; } model: ListModel { - id: userModel; + id: nearbyUserModel; } - // This Rectangle refers to each Row in the table. + // This Rectangle refers to each Row in the nearbyTable. rowDelegate: Rectangle { // The only way I know to specify a row height. // Size - height: styleData.selected ? rowHeight : rowHeight - 15; - color: styleData.selected - ? hifi.colors.orangeHighlight - : styleData.alternate ? hifi.colors.tableRowLightEven : hifi.colors.tableRowLightOdd; + height: styleData.selected ? rowHeight + 15 : rowHeight; + color: rowColor(styleData.selected, styleData.alternate); } // This Item refers to the contents of each Cell @@ -282,8 +570,11 @@ Rectangle { NameCard { id: nameCard; // Properties + profileUrl: (model && model.profileUrl) || ""; + imageMaskColor: rowColor(styleData.selected, styleData.row % 2); displayName: styleData.value; userName: model ? model.userName : ""; + connectionStatus: model ? model.connection : ""; audioLevel: model ? model.audioLevel : 0.0; avgAudioLevel: model ? model.avgAudioLevel : 0.0; visible: !isCheckBox && !isButton && !isAvgAudio; @@ -291,7 +582,7 @@ Rectangle { selected: styleData.selected; isAdmin: model && model.admin; // Size - width: nameCardWidth; + width: nearbyNameCardWidth; height: parent.height; // Anchors anchors.left: parent.left; @@ -316,8 +607,8 @@ Rectangle { // cannot change mute status when ignoring if (!model["ignore"]) { var newValue = !model["personalMute"]; - userModel.setProperty(model.userIndex, "personalMute", newValue); - userModelData[model.userIndex]["personalMute"] = newValue; // Defensive programming + nearbyUserModel.setProperty(model.userIndex, "personalMute", newValue); + nearbyUserModelData[model.userIndex]["personalMute"] = newValue; // Defensive programming Users["personalMute"](model.sessionId, newValue); UserActivityLogger["palAction"](newValue ? "personalMute" : "un-personalMute", model.sessionId); } @@ -340,15 +631,15 @@ Rectangle { boxSize: 24; onClicked: { var newValue = !model[styleData.role]; - userModel.setProperty(model.userIndex, styleData.role, newValue); - userModelData[model.userIndex][styleData.role] = newValue; // Defensive programming + nearbyUserModel.setProperty(model.userIndex, styleData.role, newValue); + nearbyUserModelData[model.userIndex][styleData.role] = newValue; // Defensive programming Users[styleData.role](model.sessionId, newValue); UserActivityLogger["palAction"](newValue ? styleData.role : "un-" + styleData.role, model.sessionId); if (styleData.role === "ignore") { - userModel.setProperty(model.userIndex, "personalMute", newValue); - userModelData[model.userIndex]["personalMute"] = newValue; // Defensive programming + nearbyUserModel.setProperty(model.userIndex, "personalMute", newValue); + nearbyUserModelData[model.userIndex]["personalMute"] = newValue; // Defensive programming if (newValue) { - ignored[model.sessionId] = userModelData[model.userIndex]; + ignored[model.sessionId] = nearbyUserModelData[model.userIndex]; } else { delete ignored[model.sessionId]; } @@ -373,8 +664,8 @@ Rectangle { Users[styleData.role](model.sessionId); UserActivityLogger["palAction"](styleData.role, model.sessionId); if (styleData.role === "kick") { - userModelData.splice(model.userIndex, 1); - userModel.remove(model.userIndex); // after changing userModelData, b/c ListModel can frob the data + nearbyUserModelData.splice(model.userIndex, 1); + nearbyUserModel.remove(model.userIndex); // after changing nearbyUserModelData, b/c ListModel can frob the data } } // muted/error glyphs @@ -397,10 +688,10 @@ Rectangle { Rectangle { // Size width: 2; - height: table.height; + height: nearbyTable.height; // Anchors anchors.left: adminTab.left; - anchors.top: table.top; + anchors.top: nearbyTable.top; // Properties visible: iAmAdmin; color: hifi.colors.lightGrayText; @@ -412,34 +703,36 @@ Rectangle { } // This Rectangle refers to the [?] popup button next to "NAMES" Rectangle { + id: helpText; color: hifi.colors.tableBackgroundLight; width: 20; height: hifi.dimensions.tableHeaderHeight - 2; - anchors.left: table.left; - anchors.top: table.top; + anchors.left: nearbyTable.left; + anchors.top: nearbyTable.top; anchors.topMargin: 1; - anchors.leftMargin: actionButtonWidth + nameCardWidth/2 + displayNameHeaderMetrics.width/2 + 6; + anchors.leftMargin: actionButtonWidth + nearbyNameCardWidth/2 + displayNameHeaderMetrics.width/2 + 6; RalewayRegular { - id: helpText; text: "[?]"; size: hifi.fontSizes.tableHeading + 2; font.capitalization: Font.AllUppercase; - color: hifi.colors.darkGray; + color: helpTextMouseArea.containsMouse ? hifi.colors.baseGrayHighlight : hifi.colors.darkGray; horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignVCenter; anchors.fill: parent; } MouseArea { + id: helpTextMouseArea; anchors.fill: parent; - acceptedButtons: Qt.LeftButton; hoverEnabled: true; onClicked: letterbox(hifi.glyphs.question, "Display Names", "Bold names in the list are avatar display names.
" + - "If a display name isn't set, a unique session display name is assigned." + - "

Administrators of this domain can also see the username or machine ID associated with each avatar present."); - onEntered: helpText.color = hifi.colors.baseGrayHighlight; - onExited: helpText.color = hifi.colors.darkGray; + "Purple borders around profile pictures are connections.
" + + "Green borders around profile pictures are friends.
" + + "(TEMPORARY LANGUAGE) In some situations, you can also see others' usernames.
" + + "If you can see someone's username, you can GoTo them by selecting them in the PAL, then clicking their name.
" + + "
If someone's display name isn't set, a unique session display name is assigned to them.
" + + "
Administrators of this domain can also see the username or machine ID associated with each avatar present."); } } // This Rectangle refers to the [?] popup button next to "ADMIN" @@ -449,7 +742,7 @@ Rectangle { width: 20; height: 28; anchors.right: adminTab.right; - anchors.rightMargin: 10 + hifi.dimensions.scrollbarBackgroundWidth; + anchors.rightMargin: 12 + hifi.dimensions.scrollbarBackgroundWidth; anchors.top: adminTab.top; anchors.topMargin: 2; RalewayRegular { @@ -457,37 +750,204 @@ Rectangle { text: "[?]"; size: hifi.fontSizes.tableHeading + 2; font.capitalization: Font.AllUppercase; - color: hifi.colors.redHighlight; + color: adminHelpTextMouseArea.containsMouse ? "#94132e" : hifi.colors.redHighlight; horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignVCenter; anchors.fill: parent; } MouseArea { + id: adminHelpTextMouseArea; anchors.fill: parent; - acceptedButtons: Qt.LeftButton; hoverEnabled: true; onClicked: letterbox(hifi.glyphs.question, "Admin Actions", "Silence mutes a user's microphone. Silenced users can unmute themselves by clicking "UNMUTE" on their toolbar.

" + "Ban removes a user from this domain and prevents them from returning. Admins can un-ban users from the Sandbox Domain Settings page."); - onEntered: adminHelpText.color = "#94132e"; - onExited: adminHelpText.color = hifi.colors.redHighlight; } } + } // "Nearby" Tab + + + /***************************************** + CONNECTIONS TAB + *****************************************/ + Rectangle { + id: connectionsTab; + color: "#E3E3E3"; + // Anchors + anchors { + top: tabSelectorContainer.bottom; + topMargin: 12; + bottom: parent.bottom; + bottomMargin: 12; + horizontalCenter: parent.horizontalCenter; + } + width: parent.width - 12; + visible: activeTab == "connectionsTab"; + + AnimatedImage { + id: connectionsLoading; + source: "../../icons/profilePicLoading.gif" + width: 120; + height: width; + anchors.centerIn: parent; + visible: true; + } + + // This TableView refers to the Connections Table (on the "Connections" tab below the current user's NameCard) + HifiControls.Table { + id: connectionsTable; + visible: !connectionsLoading.visible; + // Anchors + anchors.fill: parent; + // Properties + centerHeaderText: true; + sortIndicatorVisible: true; + headerVisible: true; + sortIndicatorColumn: settings.connectionsSortIndicatorColumn; + sortIndicatorOrder: settings.connectionsSortIndicatorOrder; + onSortIndicatorColumnChanged: { + settings.connectionsSortIndicatorColumn = sortIndicatorColumn; + sortConnectionsModel(); + } + onSortIndicatorOrderChanged: { + settings.connectionsSortIndicatorOrder = sortIndicatorOrder; + sortConnectionsModel(); + } + + TableViewColumn { + id: connectionsUserNameHeader; + role: "userName"; + title: connectionsTable.rowCount + (connectionsTable.rowCount === 1 ? " NAME" : " NAMES"); + width: connectionsNameCardWidth; + movable: false; + resizable: false; + } + TableViewColumn { + role: "placeName"; + title: "LOCATION"; + width: locationColumnWidth; + movable: false; + resizable: false; + } + TableViewColumn { + role: "friends"; + title: "FRIEND"; + width: actionButtonWidth; + movable: false; + resizable: false; + } + + model: ListModel { + id: connectionsUserModel; + } + + // This Rectangle refers to each Row in the connectionsTable. + rowDelegate: Rectangle { + // Size + height: rowHeight; + color: rowColor(styleData.selected, styleData.alternate); + } + + // This Item refers to the contents of each Cell + itemDelegate: Item { + id: connectionsItemCell; + + // This NameCard refers to the cell that contains a connection's UserName + NameCard { + id: connectionsNameCard; + // Properties + visible: styleData.role === "userName"; + profileUrl: (model && model.profileUrl) || ""; + imageMaskColor: rowColor(styleData.selected, styleData.row % 2); + displayName: model ? model.userName : ""; + userName: ""; + connectionStatus : model ? model.connection : ""; + selected: styleData.selected; + // Size + width: connectionsNameCardWidth; + height: parent.height; + // Anchors + anchors.left: parent.left; + } + + // LOCATION data + FiraSansSemiBold { + id: connectionsLocationData + // Properties + visible: styleData.role === "placeName"; + text: (model && model.placeName) || ""; + elide: Text.ElideRight; + // Size + width: parent.width; + // Anchors + anchors.fill: parent; + // Text Size + size: 16; + // Text Positioning + verticalAlignment: Text.AlignVCenter + // Style + color: connectionsLocationDataMouseArea.containsMouse ? hifi.colors.blueHighlight : hifi.colors.darkGray; + MouseArea { + id: connectionsLocationDataMouseArea; + anchors.fill: parent + hoverEnabled: enabled + enabled: connectionsNameCard.selected && pal.activeTab == "connectionsTab" + onClicked: pal.sendToScript({method: 'goToUser', params: model.userName}); + } + } + + // "Friends" checkbox + HifiControls.CheckBox { + id: friendsCheckBox; + visible: styleData.role === "friends"; + anchors.centerIn: parent; + checked: model ? (model["connection"] === "friend" ? true : false) : false; + boxSize: 24; + onClicked: { + var newValue = !model[styleData.role]; + connectionsUserModel.setProperty(model.userIndex, styleData.role, newValue); + connectionsUserModelData[model.userIndex][styleData.role] = newValue; // Defensive programming + // Insert line here about actually taking the friend/unfriend action + // Also insert line here about logging the activity, similar to the commented line below + //UserActivityLogger["palAction"](newValue ? styleData.role : "un-" + styleData.role, model.sessionId); + + // http://doc.qt.io/qt-5/qtqml-syntax-propertybinding.html#creating-property-bindings-from-javascript + // I'm using an explicit binding here because clicking a checkbox breaks the implicit binding as set by + // "checked:" statement above. + checked = Qt.binding(function() { return (model["connection"] === "friend" ? true : false)}); + } + } + } + } + } // "Connections" Tab + } // palTabContainer HifiControls.Keyboard { id: keyboard; - raised: myCard.currentlyEditingDisplayName && HMD.active; + raised: currentlyEditingDisplayName && HMD.mounted; numeric: parent.punctuationMode; anchors { bottom: parent.bottom; left: parent.left; right: parent.right; } - } - } + } // Keyboard - // Timer used when selecting table rows that aren't yet present in the model + /* + THIS WILL BE THE BROWSER THAT OPENS THE USER'S INFO PAGE! + I've no idea how to do this yet.. + + HifiTablet.TabletAddressDialog { + id: userInfoViewer; + visible: false; + } + */ + + + } // PAL container + + // Timer used when selecting nearbyTable rows that aren't yet present in the model // (i.e. when selecting avatars using edit.js or sphere overlays) Timer { property bool selected; // Selected or deselected? @@ -495,17 +955,20 @@ Rectangle { id: selectionTimer; onTriggered: { if (selected) { - table.selection.clear(); // for now, no multi-select - table.selection.select(userIndex); - table.positionViewAtRow(userIndex, ListView.Beginning); + nearbyTable.selection.clear(); // for now, no multi-select + nearbyTable.selection.select(userIndex); + nearbyTable.positionViewAtRow(userIndex, ListView.Beginning); } else { - table.selection.deselect(userIndex); + nearbyTable.selection.deselect(userIndex); } } } - function findSessionIndex(sessionId, optionalData) { // no findIndex in .qml - var data = optionalData || userModelData, length = data.length; + function rowColor(selected, alternate) { + return selected ? hifi.colors.orangeHighlight : alternate ? hifi.colors.tableRowLightEven : hifi.colors.tableRowLightOdd; + } + function findNearbySessionIndex(sessionId, optionalData) { // no findIndex in .qml + var data = optionalData || nearbyUserModelData, length = data.length; for (var i = 0; i < length; i++) { if (data[i].sessionId === sessionId) { return i; @@ -515,10 +978,10 @@ Rectangle { } function fromScript(message) { switch (message.method) { - case 'users': + case 'nearbyUsers': var data = message.params; var index = -1; - index = findSessionIndex('', data); + index = findNearbySessionIndex('', data); if (index !== -1) { iAmAdmin = Users.canKick; myData = data[index]; @@ -526,22 +989,28 @@ Rectangle { } else { console.log("This user's data was not found in the user list. PAL will not function properly."); } - userModelData = data; + nearbyUserModelData = data; for (var ignoredID in ignored) { - index = findSessionIndex(ignoredID); + index = findNearbySessionIndex(ignoredID); if (index === -1) { // Add back any missing ignored to the PAL, because they sometimes take a moment to show up. - userModelData.push(ignored[ignoredID]); + nearbyUserModelData.push(ignored[ignoredID]); } else { // Already appears in PAL; update properties of existing element in model data - userModelData[index] = ignored[ignoredID]; + nearbyUserModelData[index] = ignored[ignoredID]; } } sortModel(); break; + case 'connections': + var data = message.params; + connectionsUserModelData = data; + sortConnectionsModel(); + connectionsLoading.visible = false; + break; case 'select': var sessionIds = message.params[0]; var selected = message.params[1]; var alreadyRefreshed = message.params[2]; - var userIndex = findSessionIndex(sessionIds[0]); + var userIndex = findNearbySessionIndex(sessionIds[0]); if (sessionIds.length > 1) { letterbox("", "", 'Only one user can be selected at a time.'); } else if (userIndex < 0) { @@ -554,11 +1023,11 @@ Rectangle { } else { // If we've already refreshed the PAL and found the avatar in the model if (alreadyRefreshed === true) { - // Wait a little bit before trying to actually select the avatar in the table + // Wait a little bit before trying to actually select the avatar in the nearbyTable selectionTimer.interval = 250; } else { // If we've found the avatar in the model and didn't need to refresh, - // select the avatar in the table immediately + // select the avatar in the nearbyTable immediately selectionTimer.interval = 0; } selectionTimer.selected = selected; @@ -569,25 +1038,25 @@ Rectangle { // Received an "updateUsername()" request from the JS case 'updateUsername': // The User ID (UUID) is the first parameter in the message. - var userId = message.params[0]; - // The text that goes in the userName field is the second parameter in the message. - var userName = message.params[1]; - var admin = message.params[2]; - // If the userId is empty, we're updating "myData". - if (!userId) { - myData.userName = userName; - myCard.userName = userName; // Defensive programming - } else { - // Get the index in userModel and userModelData associated with the passed UUID - var userIndex = findSessionIndex(userId); - if (userIndex != -1) { - // Set the userName appropriately - userModel.setProperty(userIndex, "userName", userName); - userModelData[userIndex].userName = userName; // Defensive programming - // Set the admin status appropriately - userModel.setProperty(userIndex, "admin", admin); - userModelData[userIndex].admin = admin; // Defensive programming + var userId = message.params.sessionId; + // If the userId is empty, we're probably updating "myData". + if (userId) { + // Get the index in nearbyUserModel and nearbyUserModelData associated with the passed UUID + var userIndex = findNearbySessionIndex(userId); + if (userIndex !== -1) { + ['userName', 'admin', 'connection', 'profileUrl', 'placeName'].forEach(function (name) { + var value = message.params[name]; + if (value === undefined) { + return; + } + nearbyUserModel.setProperty(userIndex, name, value); + nearbyUserModelData[userIndex][name] = value; // for refill after sort + }); } + // In this "else if" case, the only param of the message is the profile pic URL. + } else if (message.params.profileUrl) { + myData.profileUrl = message.params.profileUrl; + myCard.profileUrl = message.params.profileUrl; } break; case 'updateAudioLevel': @@ -601,12 +1070,12 @@ Rectangle { myData.avgAudioLevel = avgAudioLevel; myCard.avgAudioLevel = avgAudioLevel; } else { - var userIndex = findSessionIndex(userId); + var userIndex = findNearbySessionIndex(userId); if (userIndex != -1) { - userModel.setProperty(userIndex, "audioLevel", audioLevel); - userModelData[userIndex].audioLevel = audioLevel; // Defensive programming - userModel.setProperty(userIndex, "avgAudioLevel", avgAudioLevel); - userModelData[userIndex].avgAudioLevel = avgAudioLevel; + nearbyUserModel.setProperty(userIndex, "audioLevel", audioLevel); + nearbyUserModelData[userIndex].audioLevel = audioLevel; // Defensive programming + nearbyUserModel.setProperty(userIndex, "avgAudioLevel", avgAudioLevel); + nearbyUserModelData[userIndex].avgAudioLevel = avgAudioLevel; } } } @@ -618,18 +1087,21 @@ Rectangle { var sessionID = message.params[0]; delete ignored[sessionID]; break; + case 'updateAvailability': + usernameAvailability = message.params; + break; default: console.log('Unrecognized message:', JSON.stringify(message)); } } function sortModel() { - var column = table.getColumn(table.sortIndicatorColumn); + var column = nearbyTable.getColumn(nearbyTable.sortIndicatorColumn); var sortProperty = column ? column.role : "displayName"; - var before = (table.sortIndicatorOrder === Qt.AscendingOrder) ? -1 : 1; + var before = (nearbyTable.sortIndicatorOrder === Qt.AscendingOrder) ? -1 : 1; var after = -1 * before; // get selection(s) before sorting - var selectedIDs = getSelectedSessionIDs(); - userModelData.sort(function (a, b) { + var selectedIDs = getSelectedNearbySessionIDs(); + nearbyUserModelData.sort(function (a, b) { var aValue = a[sortProperty].toString().toLowerCase(), bValue = b[sortProperty].toString().toLowerCase(); switch (true) { case (aValue < bValue): return before; @@ -637,39 +1109,77 @@ Rectangle { default: return 0; } }); - table.selection.clear(); + nearbyTable.selection.clear(); - userModel.clear(); + nearbyUserModel.clear(); var userIndex = 0; var newSelectedIndexes = []; - userModelData.forEach(function (datum) { + nearbyUserModelData.forEach(function (datum) { function init(property) { if (datum[property] === undefined) { - datum[property] = false; + // These properties must have values of type 'string'. + if (property === 'userName' || property === 'profileUrl' || property === 'placeName' || property === 'connection') { + datum[property] = ""; + // All other properties must have values of type 'bool'. + } else { + datum[property] = false; + } } } - ['personalMute', 'ignore', 'mute', 'kick'].forEach(init); + ['personalMute', 'ignore', 'mute', 'kick', 'admin', 'userName', 'profileUrl', 'placeName', 'connection'].forEach(init); datum.userIndex = userIndex++; - userModel.append(datum); + nearbyUserModel.append(datum); if (selectedIDs.indexOf(datum.sessionId) != -1) { newSelectedIndexes.push(datum.userIndex); } }); if (newSelectedIndexes.length > 0) { - table.selection.select(newSelectedIndexes); - table.positionViewAtRow(newSelectedIndexes[0], ListView.Beginning); + nearbyTable.selection.select(newSelectedIndexes); + nearbyTable.positionViewAtRow(newSelectedIndexes[0], ListView.Beginning); + } + } + function sortConnectionsModel() { + var column = connectionsTable.getColumn(connectionsTable.sortIndicatorColumn); + var sortProperty = column ? column.role : "userName"; + var before = (connectionsTable.sortIndicatorOrder === Qt.AscendingOrder) ? -1 : 1; + var after = -1 * before; + // get selection(s) before sorting + var selectedIDs = getSelectedConnectionsUserNames(); + connectionsUserModelData.sort(function (a, b) { + var aValue = a[sortProperty].toString().toLowerCase(), bValue = b[sortProperty].toString().toLowerCase(); + switch (true) { + case (aValue < bValue): return before; + case (aValue > bValue): return after; + default: return 0; + } + }); + connectionsTable.selection.clear(); + + connectionsUserModel.clear(); + var userIndex = 0; + var newSelectedIndexes = []; + connectionsUserModelData.forEach(function (datum) { + datum.userIndex = userIndex++; + connectionsUserModel.append(datum); + if (selectedIDs.indexOf(datum.sessionId) != -1) { + newSelectedIndexes.push(datum.userIndex); + } + }); + if (newSelectedIndexes.length > 0) { + connectionsTable.selection.select(newSelectedIndexes); + connectionsTable.positionViewAtRow(newSelectedIndexes[0], ListView.Beginning); } } signal sendToScript(var message); function noticeSelection() { var userIds = []; - table.selection.forEach(function (userIndex) { - userIds.push(userModelData[userIndex].sessionId); + nearbyTable.selection.forEach(function (userIndex) { + userIds.push(nearbyUserModelData[userIndex].sessionId); }); pal.sendToScript({method: 'selected', params: userIds}); } Connections { - target: table.selection; + target: nearbyTable.selection; onSelectionChanged: pal.noticeSelection(); } } diff --git a/interface/resources/qml/styles-uit/HifiConstants.qml b/interface/resources/qml/styles-uit/HifiConstants.qml index 031e80283e..e00175d4e7 100644 --- a/interface/resources/qml/styles-uit/HifiConstants.qml +++ b/interface/resources/qml/styles-uit/HifiConstants.qml @@ -171,7 +171,7 @@ Item { readonly property real textFieldInputLabel: dimensions.largeScreen ? 13 : 9 readonly property real textFieldSearchIcon: dimensions.largeScreen ? 30 : 24 readonly property real tableHeading: dimensions.largeScreen ? 12 : 10 - readonly property real tableHeadingIcon: dimensions.largeScreen ? 40 : 33 + readonly property real tableHeadingIcon: dimensions.largeScreen ? 60 : 33 readonly property real tableText: dimensions.largeScreen ? 15 : 12 readonly property real buttonLabel: dimensions.largeScreen ? 13 : 9 readonly property real iconButton: dimensions.largeScreen ? 13 : 9 diff --git a/interface/src/scripting/HMDScriptingInterface.h b/interface/src/scripting/HMDScriptingInterface.h index 276e23d2d5..7ecafdcbcb 100644 --- a/interface/src/scripting/HMDScriptingInterface.h +++ b/interface/src/scripting/HMDScriptingInterface.h @@ -27,7 +27,7 @@ class HMDScriptingInterface : public AbstractHMDScriptingInterface, public Depen Q_OBJECT Q_PROPERTY(glm::vec3 position READ getPosition) Q_PROPERTY(glm::quat orientation READ getOrientation) - Q_PROPERTY(bool mounted READ isMounted) + Q_PROPERTY(bool mounted READ isMounted NOTIFY mountedChanged) Q_PROPERTY(bool showTablet READ getShouldShowTablet) Q_PROPERTY(QUuid tabletID READ getCurrentTabletFrameID WRITE setCurrentTabletFrameID) Q_PROPERTY(QUuid homeButtonID READ getCurrentHomeButtonID WRITE setCurrentHomeButtonID) @@ -80,6 +80,7 @@ public: signals: bool shouldShowHandControllersChanged(); + void mountedChanged(); public: HMDScriptingInterface(); diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index 5d8813e988..240c1de4c3 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -21,10 +21,10 @@ var DEFAULT_SCRIPTS = [ "system/snapshot.js", "system/help.js", "system/pal.js", // "system/mod.js", // older UX, if you prefer + "system/makeUserConnection.js", "system/goto.js", "system/marketplaces/marketplaces.js", "system/edit.js", - "system/tablet-users.js", "system/selectAudioDevice.js", "system/notifications.js", "system/controllers/controllerDisplayManager.js", diff --git a/scripts/system/makeUserConnection.js b/scripts/system/makeUserConnection.js new file mode 100644 index 0000000000..a93687eda7 --- /dev/null +++ b/scripts/system/makeUserConnection.js @@ -0,0 +1,372 @@ +"use strict"; +// +// friends.js +// scripts/developer/tests/performance/ +// +// Created by David Kelly on 3/7/2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +const version = 0.1; +const label = "Friends"; +const MAX_AVATAR_DISTANCE = 1.0; +const GRIP_MIN = 0.05; +const MESSAGE_CHANNEL = "io.highfidelity.friends"; +const STATES = { + inactive : 0, + waiting: 1, + friending: 2, +}; +const STATE_STRINGS = ["inactive", "waiting", "friending"]; +const WAITING_INTERVAL = 100; // ms +const FRIENDING_INTERVAL = 100; // ms +const FRIENDING_TIME = 3000; // ms +const OVERLAY_COLORS = [{red: 0x00, green: 0xFF, blue: 0x00}, {red: 0x00, green: 0x00, blue: 0xFF}]; +const FRIENDING_HAPTIC_STRENGTH = 0.5; +const FRIENDING_SUCCESS_HAPTIC_STRENGTH = 1.0; +const HAPTIC_DURATION = 20; + +var currentHand; +var isWaiting = false; +var nearbyAvatars = []; +var state = STATES.inactive; +var waitingInterval; +var friendingInterval; +var entity; +var makingFriends = false; // really just for visualizations for now +var animHandlerId; +var entityDimensionMultiplier = 1.0; + +function debug() { + var stateString = "<" + STATE_STRINGS[state] + ">"; + var versionString = "v" + version; + print.apply(null, [].concat.apply([label, versionString, stateString], [].map.call(arguments, JSON.stringify))); +} + +function handToString(hand) { + if (hand === Controller.Standard.RightHand) { + return "RightHand"; + } else if (hand === Controller.Standard.LeftHand) { + return "LeftHand"; + } + return ""; +} + +function handToHaptic(hand) { + if (hand === Controller.Standard.RightHand) { + return 1; + } else if (hand === Controller.Standard.LeftHand) { + return 0; + } + return -1; +} + + +function getHandPosition(avatar, hand) { + if (!hand) { + debug("calling getHandPosition with no hand!"); + return; + } + var jointName = handToString(hand) + "Middle1"; + return avatar.getJointPosition(avatar.getJointIndex(jointName)); +} + +function shakeHandsAnimation(animationProperties) { + // all we are doing here is moving the right hand to a spot + // that is in front of and a bit above the hips. Basing how + // far in front as scaling with the avatar's height (say hips + // to head distance) + var headIndex = MyAvatar.getJointIndex("Head"); + var offset = 0.5; // default distance of hand in front of you + var result = {}; + if (headIndex) { + offset = 0.8 * MyAvatar.getAbsoluteJointTranslationInObjectFrame(headIndex).y; + } + var handPos = Vec3.multiply(offset, {x: -0.25, y: 0.8, z: 1.3}); + result.rightHandPosition = handPos; + result.rightHandRotation = Quat.fromPitchYawRollDegrees(90, 0, 90); + return result; +} + +// this is called frequently, but usually does nothing +function updateVisualization() { + if (state == STATES.inactive) { + if (entity) { + entity = Entities.deleteEntity(entity); + } + return; + } + + var color = state == STATES.waiting ? OVERLAY_COLORS[0] : OVERLAY_COLORS[1]; + var position = getHandPosition(MyAvatar, currentHand); + + // temp code, though all of this stuff really is temp... + if (makingFriends) { + color = { red: 0xFF, green: 0x00, blue: 0x00 }; + } + + // TODO: make the size scale with avatar, up to + // the actual size of MAX_AVATAR_DISTANCE + var wrist = MyAvatar.getJointPosition(MyAvatar.getJointIndex(handToString(currentHand))); + var d = entityDimensionMultiplier * Vec3.distance(wrist, position); + var dimension = {x: d, y: d, z: d}; + if (!entity) { + var props = { + type: "Sphere", + color: color, + position: position, + dimensions: dimension + } + entity = Entities.addEntity(props); + } else { + Entities.editEntity(entity, {dimensions: dimension, position: position, color: color}); + } + +} + +// this should find the nearest avatars, returning an array of avatar, hand pairs. Currently +// looking at distance between hands. +function findNearbyAvatars() { + var nearbyAvatars = []; + var handPos = getHandPosition(MyAvatar, currentHand); + AvatarList.getAvatarIdentifiers().forEach(function (identifier) { + if (!identifier) { return; } + var avatar = AvatarList.getAvatar(identifier); + var distanceR = Vec3.distance(getHandPosition(avatar, Controller.Standard.RightHand), handPos); + var distanceL = Vec3.distance(getHandPosition(avatar, Controller.Standard.LeftHand), handPos); + var distance = Math.min(distanceL, distanceR); + if (distance < MAX_AVATAR_DISTANCE) { + if (distance == distanceR) { + nearbyAvatars.push({avatar: identifier, hand: Controller.Standard.RightHand}); + } else { + nearbyAvatars.push({avatar: identifier, hand: Controller.Standard.LeftHand}); + } + } + }); + return nearbyAvatars; +} + +function startHandshake(fromKeyboard) { + if (fromKeyboard) { + debug("adding animation"); + animHandlerId = MyAvatar.addAnimationStateHandler(shakeHandsAnimation, []); + } + debug("starting handshake for", currentHand); + state = STATES.waiting; + waitingInterval = Script.setInterval( + function () { + debug("currentHand", handToString(currentHand)); + messageSend({ + key: "waiting", + hand: handToString(currentHand) + }); + }, WAITING_INTERVAL); +} + +function endHandshake() { + debug("ending handshake for", currentHand); + currentHand = undefined; + state = STATES.inactive; + if (waitingInterval) { + waitingInterval = Script.clearInterval(waitingInterval); + } + if (friendingInterval) { + friendingInterval = Script.clearInterval(friendingInterval); + } + if (animHandlerId) { + debug("removing animation"); + MyAvatar.removeAnimationStateHandler(animHandlerId); + } +} + +function updateTriggers(value, fromKeyboard, hand) { + if (currentHand && hand !== currentHand) { + debug("currentHand", currentHand, "ignoring messages from", hand); + return; + } + if (!currentHand) { + currentHand = hand; + } + // ok now, we are either initiating or quitting... + var isGripping = value > GRIP_MIN; + if (isGripping) { + if (state != STATES.inactive) { + return; + } else { + startHandshake(fromKeyboard); + } + } else { + if (state != STATES.inactive) { + endHandshake(); + } else { + return; + } + } +} + +function messageSend(message) { + Messages.sendMessage(MESSAGE_CHANNEL, JSON.stringify(message)); +} + +function isNearby(id, hand) { + for(var i = 0; i < nearbyAvatars.length; i++) { + if (nearbyAvatars[i].avatar == id && handToString(nearbyAvatars[i].hand) == hand) { + return true; + } + } + return false; +} + +// this should be where we make the appropriate friend call. For now just make the +// visualization change. +function makeFriends(id) { + // temp code to just flash the visualization really (for now!) + makingFriends = true; + Controller.triggerHapticPulse(FRIENDING_SUCCESS_HAPTIC_STRENGTH, HAPTIC_DURATION, handToHaptic(currentHand)); + Script.setTimeout(function () { makingFriends = false; entityDimensionMultiplier = 1.0; }, 1000); +} +// we change states, start the friendingInterval where we check +// to be sure the hand is still close enough. If not, we terminate +// the interval, go back to the waiting state. If we make it +// the entire FRIENDING_TIME, we make friends. +function startFriending(id, hand) { + var count = 0; + debug("friending", id, "hand", hand); + state = STATES.friending; + Controller.triggerHapticPulse(FRIENDING_HAPTIC_STRENGTH, HAPTIC_DURATION, handToHaptic(currentHand)); + if (waitingInterval) { + waitingInterval = Script.clearInterval(waitingInterval); + } + friendingInterval = Script.setInterval(function () { + nearbyAvatars = findNearbyAvatars(); + entityDimensionMultiplier = 1.0 + 2.0 * ++count * FRIENDING_INTERVAL / FRIENDING_TIME; + // insure senderID is still nearby + if (state != STATES.friending) { + debug("stopping friending interval, state changed"); + friendingInterval = Script.clearInterval(friendingInterval); + } + if (!isNearby(id, hand)) { + // gotta go back to waiting + debug(id, "moved, back to waiting"); + friendingInterval = Script.clearInterval(friendingInterval); + startHandshake(); + } else if (count > FRIENDING_TIME/FRIENDING_INTERVAL) { + debug("made friends with " + id); + makeFriends(id); + friendingInterval = Script.clearInterval(friendingInterval); + } + }, FRIENDING_INTERVAL); +} +/* +A simple sequence diagram: + + Avatar A Avatar B + | | + | <---------(waiting) --- startHandshake + startHandshake -- (waiting) -----> | + | | + | <-------(friending) -- startFriending + startFriending -- (friending) ---> | + | | + | friends + friends | + | ` | +*/ +function messageHandler(channel, messageString, senderID) { + if (channel !== MESSAGE_CHANNEL) { + return; + } + if (state == STATES.inactive) { + return; + } + if (MyAvatar.sessionUUID === senderID) { // ignore my own + return; + } + var message = {}; + try { + message = JSON.parse(messageString); + } catch (e) { + debug(e); + } + switch (message.key) { + case "waiting": + case "friending": + if (state == STATES.waiting) { + if (message.key == "friending" && message.id != MyAvatar.sessionUUID) { + // for now, just ignore these. Hmm + debug("ignoring friending message", message, "from", senderID); + break; + } + nearbyAvatars = findNearbyAvatars(); + if (isNearby(senderID, message.hand)) { + // if we are responding to a friending message (they didn't send a + // waiting before noticing us and friending), don't bother with sending + // a friending message? + messageSend({ + key: "friending", + id: senderID, + hand: handToString(currentHand) + }); + startFriending(senderID, message.hand); + } else { + // for now, ignore this. Hmm. + if (message.key == "friending") { + debug(senderID, "is friending us, but not close enough??"); + } + } + } + break; + default: + debug("unknown message", message); + } +} + +Messages.subscribe(MESSAGE_CHANNEL); +Messages.messageReceived.connect(messageHandler); + + +function makeGripHandler(hand) { + // determine if we are gripping or un-gripping + return function (value) { + updateTriggers(value, false, hand); + }; +} + +function keyPressEvent(event) { + if ((event.text === "x") && !event.isAutoRepeat) { + updateTriggers(1.0, true, Controller.Standard.RightHand); + } +} +function keyReleaseEvent(event) { + if ((event.text === "x") && !event.isAutoRepeat) { + updateTriggers(0.0, true, Controller.Standard.RightHand); + } +} +// map controller actions +var friendsMapping = Controller.newMapping(Script.resolvePath('') + '-grip'); +friendsMapping.from(Controller.Standard.LeftGrip).peek().to(makeGripHandler(Controller.Standard.LeftHand)); +friendsMapping.from(Controller.Standard.RightGrip).peek().to(makeGripHandler(Controller.Standard.RightHand)); + +// setup keyboard initiation + +Controller.keyPressEvent.connect(keyPressEvent); +Controller.keyReleaseEvent.connect(keyReleaseEvent); + +// it is easy to forget this and waste a lot of time for nothing +friendsMapping.enable(); + +// connect updateVisualization to update frequently +Script.update.connect(updateVisualization); + +Script.scriptEnding.connect(function () { + debug("removing controller mappings"); + friendsMapping.disable(); + debug("removing key mappings"); + Controller.keyPressEvent.disconnect(keyPressEvent); + Controller.keyReleaseEvent.disconnect(keyReleaseEvent); + debug("disconnecting updateVisualization"); + Script.update.disconnect(updateVisualization); +}); + diff --git a/scripts/system/pal.js b/scripts/system/pal.js index fdb6cbcaf5..ff0fb80c4d 100644 --- a/scripts/system/pal.js +++ b/scripts/system/pal.js @@ -1,6 +1,6 @@ "use strict"; -/* jslint vars: true, plusplus: true, forin: true*/ -/* globals Tablet, Script, AvatarList, Users, Entities, MyAvatar, Camera, Overlays, Vec3, Quat, Controller, print, getControllerWorldLocation */ +/*jslint vars:true, plusplus:true, forin:true*/ +/*global Tablet, Settings, Script, AvatarList, Users, Entities, MyAvatar, Camera, Overlays, Vec3, Quat, HMD, Controller, Account, UserActivityLogger, Messages, Window, XMLHttpRequest, print, location, getControllerWorldLocation, GlobalServices*/ /* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ // // pal.js @@ -14,6 +14,8 @@ (function() { // BEGIN LOCAL_SCOPE +var populateNearbyUserList, color, textures, removeOverlays, controllerComputePickRay, onTabletButtonClicked, onTabletScreenChanged, receiveMessage, avatarDisconnected, clearLocalQMLDataAndClosePAL, createAudioInterval, tablet, CHANNEL, getConnectionData, findableByChanged; // forward references; + // hardcoding these as it appears we cannot traverse the originalTextures in overlays??? Maybe I've missed // something, will revisit as this is sorta horrible. var UNSELECTED_TEXTURES = { @@ -97,9 +99,8 @@ ExtendedOverlay.prototype.hover = function (hovering) { if (this.key === lastHoveringId) { if (hovering) { return; - } else { - lastHoveringId = 0; } + lastHoveringId = 0; } this.editOverlay({color: color(this.selected, hovering, this.audioLevel)}); if (this.model) { @@ -214,9 +215,8 @@ function convertDbToLinear(decibels) { // but, your perception is that something 2x as loud is +10db // so we go from -60 to +20 or 1/64x to 4x. For now, we can // maybe scale the signal this way?? - return Math.pow(2, decibels/10.0); + return Math.pow(2, decibels / 10.0); } - function fromQml(message) { // messages are {method, params}, like json-rpc. See also sendToQml. var data; switch (message.method) { @@ -247,7 +247,7 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See }); } break; - case 'refresh': + case 'refreshNearby': data = {}; ExtendedOverlay.some(function (overlay) { // capture the audio data data[overlay.key] = overlay; @@ -257,8 +257,12 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See if (message.params.filter !== undefined) { Settings.setValue('pal/filtered', !!message.params.filter); } - populateUserList(message.params.selected, data); - UserActivityLogger.palAction("refresh", ""); + populateNearbyUserList(message.params.selected, data); + UserActivityLogger.palAction("refresh_nearby", ""); + break; + case 'refreshConnections': + getConnectionData(); + UserActivityLogger.palAction("refresh_connections", ""); break; case 'displayNameUpdate': if (MyAvatar.displayName !== message.params) { @@ -266,6 +270,18 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See UserActivityLogger.palAction("display_name_change", ""); } break; + case 'goToUser': + location.goToUser(message.params); + UserActivityLogger.palAction("go_to_user", ""); + break; + case 'setAvailability': + GlobalServices.findableBy = message.params; + UserActivityLogger.palAction("set_availability", ""); + print('Setting availability:', JSON.stringify(message)); + break; + case 'getAvailability': + findableByChanged(GlobalServices.findableBy); + break; default: print('Unrecognized message from Pal.qml:', JSON.stringify(message)); } @@ -274,6 +290,147 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See function sendToQml(message) { tablet.sendToQml(message); } +function updateUser(data) { + print('PAL update:', JSON.stringify(data)); + sendToQml({ method: 'updateUsername', params: data }); +} +// +// User management services +// +// These are prototype versions that will be changed when the back end changes. +var METAVERSE_BASE = 'https://metaverse.highfidelity.com'; + +function request(url, callback) { // cb(error, responseOfCorrectContentType) of url. General for 'get' text/html/json, but without redirects. + var httpRequest = new XMLHttpRequest(); + // QT bug: apparently doesn't handle onload. Workaround using readyState. + httpRequest.onreadystatechange = function () { + var READY_STATE_DONE = 4; + var HTTP_OK = 200; + if (httpRequest.readyState >= READY_STATE_DONE) { + var error = (httpRequest.status !== HTTP_OK) && httpRequest.status.toString() + ':' + httpRequest.statusText, + response = !error && httpRequest.responseText, + contentType = !error && httpRequest.getResponseHeader('content-type'); + if (!error && contentType.indexOf('application/json') === 0) { + try { + response = JSON.parse(response); + } catch (e) { + error = e; + } + } + callback(error, response); + } + }; + httpRequest.open("GET", url, true); + httpRequest.send(); +} +function requestJSON(url, callback) { // callback(data) if successfull. Logs otherwise. + request(url, function (error, response) { + if (error || (response.status !== 'success')) { + print("Error: unable to get", url, error || response.status); + return; + } + callback(response.data); + }); +} +function getProfilePicture(username, callback) { // callback(url) if successfull. (Logs otherwise) + // FIXME Prototype scrapes profile picture. We should include in user status, and also make available somewhere for myself + request(METAVERSE_BASE + '/users/' + username, function (error, html) { + var matched = !error && html.match(/img class="users-img" src="([^"]*)"/); + if (!matched) { + print('Error: Unable to get profile picture for', username, error); + return; + } + callback(matched[1]); + }); +} +function getAvailableConnections(domain, callback) { // callback([{usename, location}...]) if successfull. (Logs otherwise) + // The back end doesn't do user connections yet. Fake it by getting all users that have made themselves accessible to us, + // and pretending that they are all connections. + function getData(cb) { + requestJSON(METAVERSE_BASE + '/api/v1/users?status=online', function (connectionsData) { + + // The above does not include friend status. Fetch that separately. + requestJSON(METAVERSE_BASE + '/api/v1/user/friends', function (friendsData) { + var users = connectionsData.users || [], friends = friendsData.friends || []; + users.forEach(function (user) { + user.connection = (friends.indexOf(user.username) < 0) ? 'connection' : 'friend'; + }); + + // The back end doesn't include the profile picture data, but we can add that here. + // For our current purposes, there's no need to be fancy and try to reduce latency by doing some number of requests in parallel, + // so these requests are all sequential. + function addPicture(index) { + if (index >= users.length) { + return cb(users); + } + var user = users[index]; + getProfilePicture(user.username, function (url) { + user.profileUrl = url; + addPicture(index + 1); + }); + } + addPicture(0); + }); + }); + } + + if (domain) { + // The back end doesn't keep sessionUUID in the location data yet. Fake it by finding the avatar closest to the path. + var positions = {}; + AvatarList.getAvatarIdentifiers().forEach(function (id) { + positions[id || ''] = AvatarList.getAvatar(id).position; // Don't use null id as a key. Properties must be a string, and we don't want 'null'. + }); + getData(function (users) { + // The endpoint in getData doesn't take a domain filter. So filter out the unwanted stuff now. + domain = domain.slice(1, -1); // without curly braces + users = users.filter(function (user) { return (user.location.domain || (user.location.root && user.location.root.domain) || {}).id === domain; }); + + // Now fill in the sessionUUID as if it were in the data all along. + users.forEach(function (user) { + var coordinates = user.location.path.match(/\/([^,]+)\,([^,]+),([^\/]+)\//); + if (coordinates) { + var position = {x: Number(coordinates[1]), y: Number(coordinates[2]), z: Number(coordinates[3])}; + var none = 'not found', closestId = none, bestDistance = Infinity, distance, id; + for (id in positions) { + distance = Vec3.distance(position, positions[id]); + if (distance < bestDistance) { + closestId = id; + bestDistance = distance; + } + } + if (closestId !== none) { + user.location.sessionUUID = closestId; + } + } + }); + + callback(users); + }); + } else { // We don't need to filter, nor add any sessionUUID data + getData(callback); + } +} + +function getConnectionData(domain) { // Update all the usernames that I am entitled to see, using my login but not dependent on canKick. + function frob(user) { // get into the right format + return { + sessionId: user.location.sessionUUID || '', + userName: user.username, + connection: user.connection, + profileUrl: user.profileUrl, + placeName: (user.location.root || user.location.domain || {}).name || '' + }; + } + getAvailableConnections(domain, function (users) { + if (domain) { + users.forEach(function (user) { + updateUser(frob(user)); + }); + } else { + sendToQml({ method: 'connections', params: users.map(frob) }); + } + }); +} // // Main operations. @@ -285,15 +442,16 @@ function addAvatarNode(id) { solid: true, alpha: 0.8, color: color(selected, false, 0.0), - ignoreRayIntersection: false}, selected, !conserveResources); + ignoreRayIntersection: false + }, selected, !conserveResources); } // Each open/refresh will capture a stable set of avatarsOfInterest, within the specified filter. var avatarsOfInterest = {}; -function populateUserList(selectData, oldAudioData) { - var filter = Settings.getValue('pal/filtered') && {distance: Settings.getValue('pal/nearDistance')}; - var data = [], avatars = AvatarList.getAvatarIdentifiers(); - avatarsOfInterest = {}; - var myPosition = filter && Camera.position, +function populateNearbyUserList(selectData, oldAudioData) { + var filter = Settings.getValue('pal/filtered') && {distance: Settings.getValue('pal/nearDistance')}, + data = [], + avatars = AvatarList.getAvatarIdentifiers(), + myPosition = filter && Camera.position, frustum = filter && Camera.frustum, verticalHalfAngle = filter && (frustum.fieldOfView / 2), horizontalHalfAngle = filter && (verticalHalfAngle * frustum.aspectRatio), @@ -301,7 +459,8 @@ function populateUserList(selectData, oldAudioData) { front = filter && Quat.getFront(orientation), verticalAngleNormal = filter && Quat.getRight(orientation), horizontalAngleNormal = filter && Quat.getUp(orientation); - avatars.forEach(function (id) { // sorting the identifiers is just an aid for debugging + avatarsOfInterest = {}; + avatars.forEach(function (id) { var avatar = AvatarList.getAvatar(id); var name = avatar.sessionDisplayName; if (!name) { @@ -323,8 +482,10 @@ function populateUserList(selectData, oldAudioData) { } var oldAudio = oldAudioData && oldAudioData[id]; var avatarPalDatum = { + profileUrl: '', displayName: name, userName: '', + connection: '', sessionId: id || '', audioLevel: (oldAudio && oldAudio.audioLevel) || 0.0, avgAudioLevel: (oldAudio && oldAudio.avgAudioLevel) || 0.0, @@ -337,12 +498,19 @@ function populateUserList(selectData, oldAudioData) { // Everyone needs to see admin status. Username and fingerprint returns default constructor output if the requesting user isn't an admin. Users.requestUsernameFromID(id); avatarsOfInterest[id] = true; + } else { + // Return our username from the Account API + avatarPalDatum.userName = Account.username; + getProfilePicture(avatarPalDatum.userName, function (url) { + sendToQml({ method: 'updateUsername', params: { profileUrl: url } }); + }); } data.push(avatarPalDatum); print('PAL data:', JSON.stringify(avatarPalDatum)); }); + getConnectionData(location.domainId); // Even admins don't get relationship data in requestUsernameFromID (which is still needed for admin status, which comes from domain). conserveResources = Object.keys(avatarsOfInterest).length > 20; - sendToQml({ method: 'users', params: data }); + sendToQml({ method: 'nearbyUsers', params: data }); if (selectData) { selectData[2] = true; sendToQml({ method: 'select', params: selectData }); @@ -351,15 +519,15 @@ function populateUserList(selectData, oldAudioData) { // The function that handles the reply from the server function usernameFromIDReply(id, username, machineFingerprint, isAdmin) { - var data = [ - (MyAvatar.sessionUUID === id) ? '' : id, // Pal.qml recognizes empty id specially. + var data = { + sessionId: (MyAvatar.sessionUUID === id) ? '' : id, // Pal.qml recognizes empty id specially. // If we get username (e.g., if in future we receive it when we're friends), use it. // Otherwise, use valid machineFingerprint (which is not valid when not an admin). - username || (Users.canKick && machineFingerprint) || '', - isAdmin - ]; + userName: username || (Users.canKick && machineFingerprint) || '', + admin: isAdmin + }; // Ship the data off to QML - sendToQml({ method: 'updateUsername', params: data }); + updateUser(data); } var pingPong = true; @@ -381,16 +549,12 @@ function updateOverlays() { var target = avatar.position; var distance = Vec3.distance(target, eye); var offset = 0.2; - - // base offset on 1/2 distance from hips to head if we can - var headIndex = avatar.getJointIndex("Head"); + var diff = Vec3.subtract(target, eye); // get diff between target and eye (a vector pointing to the eye from avatar position) + var headIndex = avatar.getJointIndex("Head"); // base offset on 1/2 distance from hips to head if we can if (headIndex > 0) { offset = avatar.getAbsoluteJointTranslationInObjectFrame(headIndex).y / 2; } - // get diff between target and eye (a vector pointing to the eye from avatar position) - var diff = Vec3.subtract(target, eye); - // move a bit in front, towards the camera target = Vec3.subtract(target, Vec3.multiply(Vec3.normalize(diff), offset)); @@ -418,7 +582,7 @@ function updateOverlays() { overlay.deleteOverlay(); } }); - // We could re-populateUserList if anything added or removed, but not for now. + // We could re-populateNearbyUserList if anything added or removed, but not for now. HighlightedEntity.updateOverlays(); } function removeOverlays() { @@ -543,6 +707,7 @@ function startup() { Messages.subscribe(CHANNEL); Messages.messageReceived.connect(receiveMessage); Users.avatarDisconnected.connect(avatarDisconnected); + GlobalServices.findableByChanged.connect(findableByChanged); } startup(); @@ -579,7 +744,8 @@ function onTabletButtonClicked() { tablet.loadQMLSource("../Pal.qml"); onPalScreen = true; Users.requestsDomainListData = true; - populateUserList(); + populateNearbyUserList(); + findableByChanged(GlobalServices.findableBy); isWired = true; Script.update.connect(updateOverlays); Controller.mousePressEvent.connect(handleMouseEvent); @@ -607,8 +773,7 @@ function onTabletScreenChanged(type, url) { // var CHANNEL = 'com.highfidelity.pal'; function receiveMessage(channel, messageString, senderID) { - if ((channel !== CHANNEL) || - (senderID !== MyAvatar.sessionUUID)) { + if ((channel !== CHANNEL) || (senderID !== MyAvatar.sessionUUID)) { return; } var message = JSON.parse(messageString); @@ -633,7 +798,7 @@ function scaleAudio(val) { if (val <= LOUDNESS_FLOOR) { audioLevel = val / LOUDNESS_FLOOR * LOUDNESS_SCALE; } else { - audioLevel = (val -(LOUDNESS_FLOOR -1 )) * LOUDNESS_SCALE; + audioLevel = (val - (LOUDNESS_FLOOR - 1)) * LOUDNESS_SCALE; } if (audioLevel > 1.0) { audioLevel = 1; @@ -659,14 +824,14 @@ function getAudioLevel(id) { audioLevel = scaleAudio(Math.log(data.accumulatedLevel + 1) / LOG2); // decay avgAudioLevel - avgAudioLevel = Math.max((1-AUDIO_PEAK_DECAY) * (data.avgAudioLevel || 0), audioLevel); + avgAudioLevel = Math.max((1 - AUDIO_PEAK_DECAY) * (data.avgAudioLevel || 0), audioLevel); data.avgAudioLevel = avgAudioLevel; data.audioLevel = audioLevel; // now scale for the gain. Also, asked to boost the low end, so one simple way is // to take sqrt of the value. Lets try that, see how it feels. - avgAudioLevel = Math.min(1.0, Math.sqrt(avgAudioLevel *(sessionGains[id] || 0.75))); + avgAudioLevel = Math.min(1.0, Math.sqrt(avgAudioLevel * (sessionGains[id] || 0.75))); } return [audioLevel, avgAudioLevel]; } @@ -677,9 +842,8 @@ function createAudioInterval(interval) { return Script.setInterval(function () { var param = {}; AvatarList.getAvatarIdentifiers().forEach(function (id) { - var level = getAudioLevel(id); - // qml didn't like an object with null/empty string for a key, so... - var userId = id || 0; + var level = getAudioLevel(id), + userId = id || 0; // qml didn't like an object with null/empty string for a key, so... param[userId] = level; }); sendToQml({method: 'updateAudioLevel', params: param}); @@ -691,6 +855,20 @@ function avatarDisconnected(nodeID) { sendToQml({method: 'avatarDisconnected', params: [nodeID]}); } +function findableByChanged(usernameAvailability) { + // Update PAL availability dropdown + // Default to "friends" if undeterminable + var availability = 1; + if (usernameAvailability === "all") { + availability = 0; + } else if (usernameAvailability === "friends") { + availability = 1; + } else if (usernameAvailability === "none") { + availability = 2; + } + sendToQml({ method: 'updateAvailability', params: availability }); +} + function clearLocalQMLDataAndClosePAL() { sendToQml({ method: 'clearLocalQMLData' }); } @@ -708,6 +886,7 @@ function shutdown() { Messages.subscribe(CHANNEL); Messages.messageReceived.disconnect(receiveMessage); Users.avatarDisconnected.disconnect(avatarDisconnected); + GlobalServices.findableByChanged.disconnect(findableByChanged); off(); } From e2e7573e9337d4e17f69aae6ae42a1bcea28713d Mon Sep 17 00:00:00 2001 From: David Kelly Date: Mon, 13 Mar 2017 11:04:51 -0700 Subject: [PATCH 002/118] handle stopping handshake part-way through --- scripts/system/makeUserConnection.js | 29 +++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/scripts/system/makeUserConnection.js b/scripts/system/makeUserConnection.js index a93687eda7..50243aa7e4 100644 --- a/scripts/system/makeUserConnection.js +++ b/scripts/system/makeUserConnection.js @@ -38,6 +38,7 @@ var entity; var makingFriends = false; // really just for visualizations for now var animHandlerId; var entityDimensionMultiplier = 1.0; +var friendingId; function debug() { var stateString = "<" + STATE_STRINGS[state] + ">"; @@ -155,9 +156,10 @@ function startHandshake(fromKeyboard) { } debug("starting handshake for", currentHand); state = STATES.waiting; + friendingId = undefined; + entityDimensionMultiplier = 1.0; waitingInterval = Script.setInterval( function () { - debug("currentHand", handToString(currentHand)); messageSend({ key: "waiting", hand: handToString(currentHand) @@ -174,6 +176,10 @@ function endHandshake() { } if (friendingInterval) { friendingInterval = Script.clearInterval(friendingInterval); + // send done to let friend know you are not making friends now + messageSend({ + key: "done" + }); } if (animHandlerId) { debug("removing animation"); @@ -224,6 +230,11 @@ function isNearby(id, hand) { function makeFriends(id) { // temp code to just flash the visualization really (for now!) makingFriends = true; + // send done to let the friend know you have made friends. + messageSend({ + key: "done", + friendId: id + }); Controller.triggerHapticPulse(FRIENDING_SUCCESS_HAPTIC_STRENGTH, HAPTIC_DURATION, handToHaptic(currentHand)); Script.setTimeout(function () { makingFriends = false; entityDimensionMultiplier = 1.0; }, 1000); } @@ -234,6 +245,7 @@ function makeFriends(id) { function startFriending(id, hand) { var count = 0; debug("friending", id, "hand", hand); + friendingId = id; state = STATES.friending; Controller.triggerHapticPulse(FRIENDING_HAPTIC_STRENGTH, HAPTIC_DURATION, handToHaptic(currentHand)); if (waitingInterval) { @@ -318,6 +330,21 @@ function messageHandler(channel, messageString, senderID) { } } break; + case "done": + if (state == STATES.friending && friendingId == senderID) { + // if they are done, and didn't friend us, terminate our + // friending + if (message.friendId !== friendingId) { + if (friendingInterval) { + friendingInterval = Script.clearInterval(friendingInterval); + } + // now just call startHandshake. Should be ok to do so without a + // value for isKeyboard, as we should not change the animation + // state anyways (if any) + startHandshake(); + } + } + break; default: debug("unknown message", message); } From d04b1da1c2fe8a6aaff42512e0a8c424d0526786 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Mon, 13 Mar 2017 12:54:01 -0700 Subject: [PATCH 003/118] Merging from PAL_v2_Zach --- interface/resources/qml/hifi/NameCard.qml | 6 +- interface/resources/qml/hifi/Pal.qml | 137 ++++++++++++++++++++-- 2 files changed, 132 insertions(+), 11 deletions(-) diff --git a/interface/resources/qml/hifi/NameCard.qml b/interface/resources/qml/hifi/NameCard.qml index fcde5817fc..aa908c0039 100644 --- a/interface/resources/qml/hifi/NameCard.qml +++ b/interface/resources/qml/hifi/NameCard.qml @@ -91,10 +91,8 @@ Item { enabled: selected || isMyCard; hoverEnabled: enabled onClicked: { - /* - THIS WILL OPEN THE BROWSER TO THE USER'S INFO PAGE! - I've no idea how to do this yet.. - */ + userInfoViewer.url = "http://highfidelity.com/users/" + (pal.activeTab == "nearbyTab" ? userName : displayName); // Connections tab puts username in "displayname" field + userInfoViewer.visible = true; } } } diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index 5fb11b4e2f..8bd6824246 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -934,15 +934,138 @@ Rectangle { } } // Keyboard - /* - THIS WILL BE THE BROWSER THAT OPENS THE USER'S INFO PAGE! - I've no idea how to do this yet.. + Item { + id: webViewContainer; + anchors.fill: parent; - HifiTablet.TabletAddressDialog { - id: userInfoViewer; - visible: false; + Rectangle { + id: navigationContainer; + visible: userInfoViewer.visible; + height: 75; + anchors { + top: parent.top; + left: parent.left; + right: parent.right; + } + color: hifi.colors.faintGray; + + Item { + id: backButton + anchors { + top: parent.top; + left: parent.left; + } + height: parent.height - urlBar.height; + width: parent.width/2; + + FiraSansSemiBold { + // Properties + text: "BACK"; + elide: Text.ElideRight; + // Anchors + anchors.fill: parent; + // Text Size + size: 16; + // Text Positioning + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter; + // Style + color: backButtonMouseArea.containsMouse || !userInfoViewer.canGoBack ? hifi.colors.lightGray : hifi.colors.darkGray; + MouseArea { + id: backButtonMouseArea; + anchors.fill: parent + hoverEnabled: enabled + onClicked: userInfoViewer.goBack(); + } + } + } + + Item { + id: closeButton + anchors { + top: parent.top; + right: parent.right; + } + height: parent.height - urlBar.height; + width: parent.width/2; + + FiraSansSemiBold { + // Properties + text: "CLOSE"; + elide: Text.ElideRight; + // Anchors + anchors.fill: parent; + // Text Size + size: 16; + // Text Positioning + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter; + // Style + color: closeButtonMouseArea.containsMouse ? hifi.colors.lightGray : hifi.colors.darkGray; + MouseArea { + id: closeButtonMouseArea; + anchors.fill: parent + hoverEnabled: enabled + onClicked: userInfoViewer.visible = false; + } + } + } + + Item { + id: urlBar + anchors { + top: closeButton.bottom; + left: parent.left; + right: parent.right; + } + height: 25; + width: parent.width; + + FiraSansRegular { + // Properties + text: userInfoViewer.url; + elide: Text.ElideRight; + // Anchors + anchors.fill: parent; + // Text Size + size: 14; + // Text Positioning + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter; + // Style + color: hifi.colors.lightGray; + MouseArea { + anchors.fill: parent + onClicked: userInfoViewer.visible = false; + } + } + } + } + + Rectangle { + id: webViewBackground; + color: "white"; + visible: userInfoViewer.visible; + anchors { + top: navigationContainer.bottom; + bottom: parent.bottom; + left: parent.left; + right: parent.right; + } + } + + HifiControls.WebView { + id: userInfoViewer; + anchors { + top: navigationContainer.bottom; + topMargin: 5; + bottom: parent.bottom; + left: parent.left; + right: parent.right; + } + visible: false; + } } - */ } // PAL container From 515e6fd34eb1e1487d44de1954cf0fcc9e0f279f Mon Sep 17 00:00:00 2001 From: David Kelly Date: Mon, 13 Mar 2017 13:10:05 -0700 Subject: [PATCH 004/118] map Controller.Standard.RB so xbox controller works. whee. --- scripts/system/makeUserConnection.js | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/scripts/system/makeUserConnection.js b/scripts/system/makeUserConnection.js index 50243aa7e4..1b49ed92cb 100644 --- a/scripts/system/makeUserConnection.js +++ b/scripts/system/makeUserConnection.js @@ -354,11 +354,18 @@ Messages.subscribe(MESSAGE_CHANNEL); Messages.messageReceived.connect(messageHandler); -function makeGripHandler(hand) { +function makeGripHandler(hand, animate) { // determine if we are gripping or un-gripping - return function (value) { - updateTriggers(value, false, hand); - }; + if (animate) { + return function(value) { + updateTriggers(value, true, hand); + }; + + } else { + return function (value) { + updateTriggers(value, false, hand); + }; + } } function keyPressEvent(event) { @@ -377,10 +384,12 @@ friendsMapping.from(Controller.Standard.LeftGrip).peek().to(makeGripHandler(Cont friendsMapping.from(Controller.Standard.RightGrip).peek().to(makeGripHandler(Controller.Standard.RightHand)); // setup keyboard initiation - Controller.keyPressEvent.connect(keyPressEvent); Controller.keyReleaseEvent.connect(keyReleaseEvent); +// xbox controller cuz that's important +friendsMapping.from(Controller.Standard.RB).peek().to(makeGripHandler(Controller.Standard.RightHand, true)); + // it is easy to forget this and waste a lot of time for nothing friendsMapping.enable(); From fa7283a6e2ec82c71b05bd9828430708f29fd150 Mon Sep 17 00:00:00 2001 From: David Kelly Date: Mon, 13 Mar 2017 18:33:36 -0700 Subject: [PATCH 005/118] initial new messaging - working but needs cleanup --- scripts/system/makeUserConnection.js | 142 ++++++++++++++++----------- 1 file changed, 87 insertions(+), 55 deletions(-) diff --git a/scripts/system/makeUserConnection.js b/scripts/system/makeUserConnection.js index 1b49ed92cb..7001c2f861 100644 --- a/scripts/system/makeUserConnection.js +++ b/scripts/system/makeUserConnection.js @@ -29,16 +29,15 @@ const FRIENDING_SUCCESS_HAPTIC_STRENGTH = 1.0; const HAPTIC_DURATION = 20; var currentHand; -var isWaiting = false; -var nearbyAvatars = []; var state = STATES.inactive; -var waitingInterval; var friendingInterval; var entity; var makingFriends = false; // really just for visualizations for now var animHandlerId; var entityDimensionMultiplier = 1.0; var friendingId; +var pendingFriendAckFrom; +var latestFriendRequestFrom; function debug() { var stateString = "<" + STATE_STRINGS[state] + ">"; @@ -126,24 +125,23 @@ function updateVisualization() { } } - -// this should find the nearest avatars, returning an array of avatar, hand pairs. Currently -// looking at distance between hands. -function findNearbyAvatars() { - var nearbyAvatars = []; +function findNearbyAvatars(nearestOnly) { var handPos = getHandPosition(MyAvatar, currentHand); + var minDistance = MAX_AVATAR_DISTANCE; + var nearbyAvatars = []; AvatarList.getAvatarIdentifiers().forEach(function (identifier) { if (!identifier) { return; } var avatar = AvatarList.getAvatar(identifier); var distanceR = Vec3.distance(getHandPosition(avatar, Controller.Standard.RightHand), handPos); var distanceL = Vec3.distance(getHandPosition(avatar, Controller.Standard.LeftHand), handPos); var distance = Math.min(distanceL, distanceR); - if (distance < MAX_AVATAR_DISTANCE) { - if (distance == distanceR) { - nearbyAvatars.push({avatar: identifier, hand: Controller.Standard.RightHand}); - } else { - nearbyAvatars.push({avatar: identifier, hand: Controller.Standard.LeftHand}); + if (distance < minDistance) { + if (nearestOnly) { + minDistance = distance; + nearbyAvatars = []; } + var hand = (distance == distanceR ? Controller.Standard.RightHand : Controller.Standard.LeftHand); + nearbyAvatars.push({avatar: identifier, hand: hand}); } }); return nearbyAvatars; @@ -156,24 +154,35 @@ function startHandshake(fromKeyboard) { } debug("starting handshake for", currentHand); state = STATES.waiting; - friendingId = undefined; entityDimensionMultiplier = 1.0; - waitingInterval = Script.setInterval( - function () { + pendingFriendAckFrom = undefined; + // if we have a recent friendRequest, send an ack back + if (latestFriendRequestFrom) { + debug("sending friendAck to", latestFriendRequestFrom); + messageSend({ + key: "friendAck", + id: latestFriendRequestFrom, + hand: handToString(currentHand) + }); + } else { + var nearestAvatar = findNearbyAvatars(true)[0]; + debug("nearest avatar", nearestAvatar); + if (nearestAvatar) { + pendingFriendAckFrom = nearestAvatar.avatar; + debug("sending friendRequest to", pendingFriendAckFrom); messageSend({ - key: "waiting", - hand: handToString(currentHand) + key: "friendRequest", + id: nearestAvatar.avatar, + hand: handToString(nearestAvatar.hand) }); - }, WAITING_INTERVAL); + } + } } function endHandshake() { debug("ending handshake for", currentHand); currentHand = undefined; state = STATES.inactive; - if (waitingInterval) { - waitingInterval = Script.clearInterval(waitingInterval); - } if (friendingInterval) { friendingInterval = Script.clearInterval(friendingInterval); // send done to let friend know you are not making friends now @@ -217,6 +226,7 @@ function messageSend(message) { } function isNearby(id, hand) { + var nearbyAvatars = findNearbyAvatars(); for(var i = 0; i < nearbyAvatars.length; i++) { if (nearbyAvatars[i].avatar == id && handToString(nearbyAvatars[i].hand) == hand) { return true; @@ -246,20 +256,24 @@ function startFriending(id, hand) { var count = 0; debug("friending", id, "hand", hand); friendingId = id; + pendingFriendAckFrom = undefined; + latestFriendRequestFrom = undefined; state = STATES.friending; Controller.triggerHapticPulse(FRIENDING_HAPTIC_STRENGTH, HAPTIC_DURATION, handToHaptic(currentHand)); - if (waitingInterval) { - waitingInterval = Script.clearInterval(waitingInterval); - } + + // send message that we are friending them + messageSend({ + key: "friending", + id: id, + hand: handToString(currentHand) + }); + friendingInterval = Script.setInterval(function () { - nearbyAvatars = findNearbyAvatars(); entityDimensionMultiplier = 1.0 + 2.0 * ++count * FRIENDING_INTERVAL / FRIENDING_TIME; - // insure senderID is still nearby if (state != STATES.friending) { debug("stopping friending interval, state changed"); friendingInterval = Script.clearInterval(friendingInterval); - } - if (!isNearby(id, hand)) { + } else if (!isNearby(id, hand)) { // gotta go back to waiting debug(id, "moved, back to waiting"); friendingInterval = Script.clearInterval(friendingInterval); @@ -276,23 +290,21 @@ A simple sequence diagram: Avatar A Avatar B | | - | <---------(waiting) --- startHandshake - startHandshake -- (waiting) -----> | + | <-----(FriendRequest) -- startHandshake + startHandshake -- (FriendAck) ---> | | | | <-------(friending) -- startFriending startFriending -- (friending) ---> | | | | friends friends | - | ` | + | <--------- (done) ---------- | + | ---------- (done) ---------> | */ function messageHandler(channel, messageString, senderID) { if (channel !== MESSAGE_CHANNEL) { return; } - if (state == STATES.inactive) { - return; - } if (MyAvatar.sessionUUID === senderID) { // ignore my own return; } @@ -302,32 +314,42 @@ function messageHandler(channel, messageString, senderID) { } catch (e) { debug(e); } + debug("message", message); switch (message.key) { - case "waiting": + case "friendRequest": + if (state == STATES.inactive && message.id == MyAvatar.sessionUUID) { + debug("setting latestFriendRequestFrom", senderID); + latestFriendRequestFrom = senderID; + } else if (state == STATES.waiting && !pendingFriendAckFrom) { + // you are waiting for a friend request, so send the ack + pendingFriendAckFrom = senderID; + messageSend({ + key: "friendAck", + id: senderID, + hand: handToString(currentHand) + }); + } + // TODO: ponder keeping this up-to-date during + // other states? + break; + case "friendAck": + if (state == STATES.waiting && message.id == MyAvatar.sessionUUID) { + if (pendingFriendAckFrom && senderID != pendingFriendAckFrom) { + debug("ignoring friendAck from", senderID, ", waiting on", pendingFriendAckFrom); + break; + } + // start friending... + startFriending(senderID, message.hand); + } + break; case "friending": - if (state == STATES.waiting) { - if (message.key == "friending" && message.id != MyAvatar.sessionUUID) { + if (state == STATES.waiting && senderID == latestFriendRequestFrom) { + if (message.id != MyAvatar.sessionUUID) { // for now, just ignore these. Hmm debug("ignoring friending message", message, "from", senderID); break; } - nearbyAvatars = findNearbyAvatars(); - if (isNearby(senderID, message.hand)) { - // if we are responding to a friending message (they didn't send a - // waiting before noticing us and friending), don't bother with sending - // a friending message? - messageSend({ - key: "friending", - id: senderID, - hand: handToString(currentHand) - }); - startFriending(senderID, message.hand); - } else { - // for now, ignore this. Hmm. - if (message.key == "friending") { - debug(senderID, "is friending us, but not close enough??"); - } - } + startFriending(senderID, message.hand); } break; case "done": @@ -343,6 +365,16 @@ function messageHandler(channel, messageString, senderID) { // state anyways (if any) startHandshake(); } + } else { + // if waiting or inactive, lets clear the pending stuff + if (pendingFriendAckFrom == senderID || lastestFriendRequestFrom == senderID) { + if (state == STATES.inactive) { + pendingFriendAckFrom = undefined; + latestFriendRequestFrom = undefined; + } else { + startHandshake(); + } + } } break; default: From e3d0842ddcc8a9b4dc0d740016909ea2c5fe3f2e Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Mon, 13 Mar 2017 18:39:47 -0700 Subject: [PATCH 006/118] Implement browser in PAL --- interface/resources/qml/hifi/NameCard.qml | 21 ++++++++++----------- interface/resources/qml/hifi/Pal.qml | 6 +++--- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/interface/resources/qml/hifi/NameCard.qml b/interface/resources/qml/hifi/NameCard.qml index aa908c0039..88ecededd1 100644 --- a/interface/resources/qml/hifi/NameCard.qml +++ b/interface/resources/qml/hifi/NameCard.qml @@ -91,7 +91,7 @@ Item { enabled: selected || isMyCard; hoverEnabled: enabled onClicked: { - userInfoViewer.url = "http://highfidelity.com/users/" + (pal.activeTab == "nearbyTab" ? userName : displayName); // Connections tab puts username in "displayname" field + userInfoViewer.url = "http://highfidelity.com/users/" + userName; userInfoViewer.visible = true; } } @@ -202,13 +202,12 @@ Item { // DisplayName container for others' cards Item { id: displayNameContainer - visible: !isMyCard + visible: !isMyCard && pal.activeTab !== "connectionsTab" // Size width: parent.width - anchors.leftMargin - avatarImage.width - anchors.leftMargin; height: displayNameTextPixelSize + 4 // Anchors - anchors.top: pal.activeTab == "connectionsTab" ? undefined : avatarImage.top; - anchors.bottom: pal.activeTab == "connectionsTab" ? avatarImage.bottom : undefined; + anchors.top: avatarImage.top; anchors.left: avatarImage.right anchors.leftMargin: avatarImage.visible ? 5 : 0; // DisplayName Text for others' cards @@ -227,8 +226,7 @@ Item { // Text Positioning verticalAlignment: Text.AlignTop // Style - color: (pal.activeTab == "nearbyTab" && (displayNameTextMouseArea.containsMouse || userNameTextMouseArea.containsMouse)) - ? hifi.colors.blueHighlight : (pal.activeTab == "nearbyTab" ? hifi.colors.darkGray : hifi.colors.greenShadow); + color: (displayNameTextMouseArea.containsMouse || userNameTextMouseArea.containsMouse) ? hifi.colors.blueHighlight : hifi.colors.darkGray; MouseArea { id: displayNameTextMouseArea; anchors.fill: parent @@ -297,21 +295,22 @@ Item { // Properties text: thisNameCard.userName elide: Text.ElideRight - visible: thisNameCard.displayName + visible: thisNameCard.userName !== ""; // Size width: parent.width - height: usernameTextPixelSize + 4 + height: pal.activeTab == "nearbyTab" || isMyCard ? usernameTextPixelSize + 4 : displayNameTextPixelSize + 4 // Anchors anchors.top: isMyCard ? myDisplayName.bottom : undefined; - anchors.bottom: isMyCard ? undefined : avatarImage.bottom + anchors.bottom: !isMyCard ? avatarImage.bottom : undefined; anchors.left: avatarImage.right; anchors.leftMargin: avatarImage.visible ? 5 : 0; // Text Size - size: usernameTextPixelSize; + size: pal.activeTab == "nearbyTab" || isMyCard ? usernameTextPixelSize : displayNameTextPixelSize; // Text Positioning verticalAlignment: Text.AlignBottom // Style - color: (pal.activeTab == "nearbyTab" && (displayNameTextMouseArea.containsMouse || userNameTextMouseArea.containsMouse)) ? hifi.colors.blueHighlight : hifi.colors.greenShadow; + color: (pal.activeTab == "nearbyTab" && (displayNameTextMouseArea.containsMouse || userNameTextMouseArea.containsMouse)) + ? hifi.colors.blueHighlight : hifi.colors.greenShadow; MouseArea { id: userNameTextMouseArea; anchors.fill: parent diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index 8bd6824246..43e874dc57 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -860,8 +860,8 @@ Rectangle { visible: styleData.role === "userName"; profileUrl: (model && model.profileUrl) || ""; imageMaskColor: rowColor(styleData.selected, styleData.row % 2); - displayName: model ? model.userName : ""; - userName: ""; + displayName: ""; + userName: model ? model.userName : ""; connectionStatus : model ? model.connection : ""; selected: styleData.selected; // Size @@ -1001,7 +1001,7 @@ Rectangle { verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter; // Style - color: closeButtonMouseArea.containsMouse ? hifi.colors.lightGray : hifi.colors.darkGray; + color: closeButtonMouseArea.containsMouse ? hifi.colors.redAccent : hifi.colors.redHighlight; MouseArea { id: closeButtonMouseArea; anchors.fill: parent From 7765daf38223a42e3c4e6843db1817a6a413018b Mon Sep 17 00:00:00 2001 From: David Kelly Date: Mon, 13 Mar 2017 18:57:01 -0700 Subject: [PATCH 007/118] handle cleanup, race, etc..., more to come --- scripts/system/makeUserConnection.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/scripts/system/makeUserConnection.js b/scripts/system/makeUserConnection.js index 7001c2f861..5bf8ba2047 100644 --- a/scripts/system/makeUserConnection.js +++ b/scripts/system/makeUserConnection.js @@ -158,7 +158,6 @@ function startHandshake(fromKeyboard) { pendingFriendAckFrom = undefined; // if we have a recent friendRequest, send an ack back if (latestFriendRequestFrom) { - debug("sending friendAck to", latestFriendRequestFrom); messageSend({ key: "friendAck", id: latestFriendRequestFrom, @@ -166,10 +165,8 @@ function startHandshake(fromKeyboard) { }); } else { var nearestAvatar = findNearbyAvatars(true)[0]; - debug("nearest avatar", nearestAvatar); if (nearestAvatar) { pendingFriendAckFrom = nearestAvatar.avatar; - debug("sending friendRequest to", pendingFriendAckFrom); messageSend({ key: "friendRequest", id: nearestAvatar.avatar, @@ -314,14 +311,13 @@ function messageHandler(channel, messageString, senderID) { } catch (e) { debug(e); } - debug("message", message); switch (message.key) { case "friendRequest": if (state == STATES.inactive && message.id == MyAvatar.sessionUUID) { - debug("setting latestFriendRequestFrom", senderID); latestFriendRequestFrom = senderID; - } else if (state == STATES.waiting && !pendingFriendAckFrom) { - // you are waiting for a friend request, so send the ack + } else if (state == STATES.waiting && (pendingFriendAckFrom == senderID || !pendingFriendAckFrom)) { + // you are waiting for a friend request, so send the ack. Or, you and the other + // guy raced and both send friendRequests. Handle that too pendingFriendAckFrom = senderID; messageSend({ key: "friendAck", @@ -436,5 +432,8 @@ Script.scriptEnding.connect(function () { Controller.keyReleaseEvent.disconnect(keyReleaseEvent); debug("disconnecting updateVisualization"); Script.update.disconnect(updateVisualization); + if (entity) { + entity = Entities.deleteEntity(entity); + } }); From 246d2f4017132e775a91373ee1b9f75ea8af6acd Mon Sep 17 00:00:00 2001 From: David Kelly Date: Tue, 14 Mar 2017 08:58:14 -0700 Subject: [PATCH 008/118] fix comments at top --- scripts/system/makeUserConnection.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/system/makeUserConnection.js b/scripts/system/makeUserConnection.js index 5bf8ba2047..8dc3b156c7 100644 --- a/scripts/system/makeUserConnection.js +++ b/scripts/system/makeUserConnection.js @@ -1,7 +1,7 @@ "use strict"; // -// friends.js -// scripts/developer/tests/performance/ +// makeUserConnetion.js +// scripts/system // // Created by David Kelly on 3/7/2017. // Copyright 2017 High Fidelity, Inc. From f821ccc8c347dad96187e1ec454de72dc342a191 Mon Sep 17 00:00:00 2001 From: David Kelly Date: Tue, 14 Mar 2017 10:00:19 -0700 Subject: [PATCH 009/118] fix typo - doh! --- scripts/system/makeUserConnection.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/makeUserConnection.js b/scripts/system/makeUserConnection.js index 8dc3b156c7..526d01706f 100644 --- a/scripts/system/makeUserConnection.js +++ b/scripts/system/makeUserConnection.js @@ -363,7 +363,7 @@ function messageHandler(channel, messageString, senderID) { } } else { // if waiting or inactive, lets clear the pending stuff - if (pendingFriendAckFrom == senderID || lastestFriendRequestFrom == senderID) { + if (pendingFriendAckFrom == senderID || latestFriendRequestFrom == senderID) { if (state == STATES.inactive) { pendingFriendAckFrom = undefined; latestFriendRequestFrom = undefined; From 5da0dfc4acc3b387250a7b5be41ad6d3d29d5428 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Tue, 14 Mar 2017 14:05:50 -0700 Subject: [PATCH 010/118] Merging from Pal_v2_Zach --- interface/resources/qml/hifi/NameCard.qml | 8 ++++---- interface/resources/qml/hifi/Pal.qml | 5 +++-- scripts/system/pal.js | 1 + 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/interface/resources/qml/hifi/NameCard.qml b/interface/resources/qml/hifi/NameCard.qml index 88ecededd1..f547975533 100644 --- a/interface/resources/qml/hifi/NameCard.qml +++ b/interface/resources/qml/hifi/NameCard.qml @@ -91,7 +91,7 @@ Item { enabled: selected || isMyCard; hoverEnabled: enabled onClicked: { - userInfoViewer.url = "http://highfidelity.com/users/" + userName; + userInfoViewer.url = defaultBaseUrl + "/users/" + userName; userInfoViewer.visible = true; } } @@ -116,12 +116,12 @@ Item { id: myDisplayName visible: isMyCard // Size - width: parent.width - avatarImage.width - anchors.leftMargin*2 - anchors.rightMargin; + width: parent.width - avatarImage.width - anchors.leftMargin - anchors.rightMargin*2; height: 40 // Anchors anchors.top: avatarImage.top anchors.left: avatarImage.right - anchors.leftMargin: 5; + anchors.leftMargin: avatarImage.visible ? 5 : 0; anchors.rightMargin: 5; // Style color: myDisplayNameMouseArea.containsMouse ? hifi.colors.lightGrayText : hifi.colors.textFieldLightBackground @@ -293,7 +293,7 @@ Item { FiraSansRegular { id: userNameText // Properties - text: thisNameCard.userName + text: thisNameCard.userName === "Unknown user" ? "not logged in" : thisNameCard.userName; elide: Text.ElideRight visible: thisNameCard.userName !== ""; // Size diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index 43e874dc57..762a0a14c7 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -941,7 +941,7 @@ Rectangle { Rectangle { id: navigationContainer; visible: userInfoViewer.visible; - height: 75; + height: 70; anchors { top: parent.top; left: parent.left; @@ -1018,7 +1018,7 @@ Rectangle { left: parent.left; right: parent.right; } - height: 25; + height: 30; width: parent.width; FiraSansRegular { @@ -1125,6 +1125,7 @@ Rectangle { break; case 'connections': var data = message.params; + console.log('Got connection data: ', JSON.stringify(data)); connectionsUserModelData = data; sortConnectionsModel(); connectionsLoading.visible = false; diff --git a/scripts/system/pal.js b/scripts/system/pal.js index ff0fb80c4d..ec0d19962c 100644 --- a/scripts/system/pal.js +++ b/scripts/system/pal.js @@ -261,6 +261,7 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See UserActivityLogger.palAction("refresh_nearby", ""); break; case 'refreshConnections': + print('Refreshing Connections...'); getConnectionData(); UserActivityLogger.palAction("refresh_connections", ""); break; From 0a35fa34f9036be84d98b68aa8f9301920be8e73 Mon Sep 17 00:00:00 2001 From: David Kelly Date: Tue, 14 Mar 2017 14:22:44 -0700 Subject: [PATCH 011/118] some reworking, cleaning, bug fixing... --- scripts/system/makeUserConnection.js | 107 +++++++++++++++------------ 1 file changed, 60 insertions(+), 47 deletions(-) diff --git a/scripts/system/makeUserConnection.js b/scripts/system/makeUserConnection.js index 526d01706f..881a828a17 100644 --- a/scripts/system/makeUserConnection.js +++ b/scripts/system/makeUserConnection.js @@ -9,21 +9,23 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +(function() { // BEGIN LOCAL_SCOPE const version = 0.1; -const label = "Friends"; -const MAX_AVATAR_DISTANCE = 1.0; +const label = "makeUserConnection"; +const MAX_AVATAR_DISTANCE = 1.25; const GRIP_MIN = 0.05; -const MESSAGE_CHANNEL = "io.highfidelity.friends"; +const MESSAGE_CHANNEL = "io.highfidelity.makeUserConnection"; const STATES = { inactive : 0, waiting: 1, friending: 2, + makingFriends: 3 }; -const STATE_STRINGS = ["inactive", "waiting", "friending"]; +const STATE_STRINGS = ["inactive", "waiting", "friending", "makingFriends"]; const WAITING_INTERVAL = 100; // ms const FRIENDING_INTERVAL = 100; // ms const FRIENDING_TIME = 3000; // ms -const OVERLAY_COLORS = [{red: 0x00, green: 0xFF, blue: 0x00}, {red: 0x00, green: 0x00, blue: 0xFF}]; +const ENTITY_COLORS = [{red: 0x00, green: 0xFF, blue: 0x00}, {red: 0x00, green: 0x00, blue: 0xFF}, {red: 0xFF, green: 0x00, blue: 0x00}]; const FRIENDING_HAPTIC_STRENGTH = 0.5; const FRIENDING_SUCCESS_HAPTIC_STRENGTH = 1.0; const HAPTIC_DURATION = 20; @@ -32,17 +34,14 @@ var currentHand; var state = STATES.inactive; var friendingInterval; var entity; -var makingFriends = false; // really just for visualizations for now var animHandlerId; var entityDimensionMultiplier = 1.0; var friendingId; -var pendingFriendAckFrom; -var latestFriendRequestFrom; function debug() { var stateString = "<" + STATE_STRINGS[state] + ">"; var versionString = "v" + version; - print.apply(null, [].concat.apply([label, versionString, stateString], [].map.call(arguments, JSON.stringify))); + print.apply(null, [].concat.apply([label, versionString, stateString, friendingId], [].map.call(arguments, JSON.stringify))); } function handToString(hand) { @@ -99,14 +98,9 @@ function updateVisualization() { return; } - var color = state == STATES.waiting ? OVERLAY_COLORS[0] : OVERLAY_COLORS[1]; + var color = ENTITY_COLORS[state-1]; var position = getHandPosition(MyAvatar, currentHand); - // temp code, though all of this stuff really is temp... - if (makingFriends) { - color = { red: 0xFF, green: 0x00, blue: 0x00 }; - } - // TODO: make the size scale with avatar, up to // the actual size of MAX_AVATAR_DISTANCE var wrist = MyAvatar.getJointPosition(MyAvatar.getJointIndex(handToString(currentHand))); @@ -150,26 +144,32 @@ function findNearbyAvatars(nearestOnly) { function startHandshake(fromKeyboard) { if (fromKeyboard) { debug("adding animation"); + // just in case order of press/unpress is broken + if (animHandlerId) { + animHandlerId = MyAvatar.removeAnimationStateHandler(animHandlerId); + } animHandlerId = MyAvatar.addAnimationStateHandler(shakeHandsAnimation, []); } debug("starting handshake for", currentHand); state = STATES.waiting; entityDimensionMultiplier = 1.0; - pendingFriendAckFrom = undefined; - // if we have a recent friendRequest, send an ack back - if (latestFriendRequestFrom) { + // if we have a recent friendRequest, send an ack back. + // TODO: be sure the friendingId resets when we get the done message + if (friendingId) { + debug("sending friendAck to", friendingId); messageSend({ key: "friendAck", - id: latestFriendRequestFrom, + id: friendingId, hand: handToString(currentHand) }); } else { var nearestAvatar = findNearbyAvatars(true)[0]; if (nearestAvatar) { - pendingFriendAckFrom = nearestAvatar.avatar; + friendingId = nearestAvatar.avatar; + debug("sending friendRequest to", friendingId); messageSend({ key: "friendRequest", - id: nearestAvatar.avatar, + id: friendingId, hand: handToString(nearestAvatar.hand) }); } @@ -181,6 +181,7 @@ function endHandshake() { currentHand = undefined; state = STATES.inactive; if (friendingInterval) { + friendingId = undefined; friendingInterval = Script.clearInterval(friendingInterval); // send done to let friend know you are not making friends now messageSend({ @@ -235,15 +236,19 @@ function isNearby(id, hand) { // this should be where we make the appropriate friend call. For now just make the // visualization change. function makeFriends(id) { - // temp code to just flash the visualization really (for now!) - makingFriends = true; // send done to let the friend know you have made friends. messageSend({ key: "done", friendId: id }); Controller.triggerHapticPulse(FRIENDING_SUCCESS_HAPTIC_STRENGTH, HAPTIC_DURATION, handToHaptic(currentHand)); - Script.setTimeout(function () { makingFriends = false; entityDimensionMultiplier = 1.0; }, 1000); + state = STATES.makingFriends; + // now that we made friends, reset everything + Script.setTimeout(function () { + state = STATES.waiting; + friendingId = undefined; + entityDimensionMultiplier = 1.0; + }, 1000); } // we change states, start the friendingInterval where we check // to be sure the hand is still close enough. If not, we terminate @@ -252,9 +257,8 @@ function makeFriends(id) { function startFriending(id, hand) { var count = 0; debug("friending", id, "hand", hand); + // do we need to do this? friendingId = id; - pendingFriendAckFrom = undefined; - latestFriendRequestFrom = undefined; state = STATES.friending; Controller.triggerHapticPulse(FRIENDING_HAPTIC_STRENGTH, HAPTIC_DURATION, handToHaptic(currentHand)); @@ -274,6 +278,10 @@ function startFriending(id, hand) { // gotta go back to waiting debug(id, "moved, back to waiting"); friendingInterval = Script.clearInterval(friendingInterval); + messageSend({ + key: "done" + }); + friendingId = undefined; startHandshake(); } else if (count > FRIENDING_TIME/FRIENDING_INTERVAL) { debug("made friends with " + id); @@ -311,38 +319,41 @@ function messageHandler(channel, messageString, senderID) { } catch (e) { debug(e); } + debug("recv'd message:", message); switch (message.key) { case "friendRequest": if (state == STATES.inactive && message.id == MyAvatar.sessionUUID) { - latestFriendRequestFrom = senderID; - } else if (state == STATES.waiting && (pendingFriendAckFrom == senderID || !pendingFriendAckFrom)) { - // you are waiting for a friend request, so send the ack. Or, you and the other + friendingId = senderID; + } else if (state == STATES.waiting && message.id == MyAvatar.sessionUUID && (!friendingId || friendingId == senderID)) { + // you were waiting for a friend request, so send the ack. Or, you and the other // guy raced and both send friendRequests. Handle that too - pendingFriendAckFrom = senderID; + if (!friendingId) { + friendingId = senderID; + } messageSend({ key: "friendAck", id: senderID, hand: handToString(currentHand) }); } - // TODO: ponder keeping this up-to-date during - // other states? + // TODO: check to see if the person we are trying to friend sent this to someone else, + // and try again break; case "friendAck": - if (state == STATES.waiting && message.id == MyAvatar.sessionUUID) { - if (pendingFriendAckFrom && senderID != pendingFriendAckFrom) { - debug("ignoring friendAck from", senderID, ", waiting on", pendingFriendAckFrom); - break; - } + if (state == STATES.waiting && message.id == MyAvatar.sessionUUID && (!friendingId || friendingId == senderID)) { // start friending... + friendingId = senderID; startFriending(senderID, message.hand); } + // TODO: check to see if we are waiting for this but the person we are friending sent it to + // someone else, and try again break; case "friending": - if (state == STATES.waiting && senderID == latestFriendRequestFrom) { + if (state == STATES.waiting && senderID == friendingId) { if (message.id != MyAvatar.sessionUUID) { - // for now, just ignore these. Hmm - debug("ignoring friending message", message, "from", senderID); + // the person we were trying to friend is friending someone else + // so try again + startHandshake(); break; } startFriending(senderID, message.hand); @@ -352,22 +363,22 @@ function messageHandler(channel, messageString, senderID) { if (state == STATES.friending && friendingId == senderID) { // if they are done, and didn't friend us, terminate our // friending - if (message.friendId !== friendingId) { + if (message.friendId !== MyAvatar.sessionUUID) { if (friendingInterval) { friendingInterval = Script.clearInterval(friendingInterval); } // now just call startHandshake. Should be ok to do so without a // value for isKeyboard, as we should not change the animation // state anyways (if any) + friendingId = undefined; startHandshake(); } } else { - // if waiting or inactive, lets clear the pending stuff - if (pendingFriendAckFrom == senderID || latestFriendRequestFrom == senderID) { - if (state == STATES.inactive) { - pendingFriendAckFrom = undefined; - latestFriendRequestFrom = undefined; - } else { + // if waiting or inactive, lets clear the friending id. If in makingFriends, + // do nothing (so you see the red for a bit) + if (state != STATES.makingFriends && friendingId == senderID) { + friendingId = undefined; + if (state != STATES.inactive) { startHandshake(); } } @@ -437,3 +448,5 @@ Script.scriptEnding.connect(function () { } }); +}()); // END LOCAL_SCOPE + From 0a6bcd7fca5ecba8fd1256250a6fcd74606f6332 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Tue, 14 Mar 2017 16:22:16 -0700 Subject: [PATCH 012/118] Massive performance boost --- interface/resources/qml/hifi/NameCard.qml | 33 ++++++++++++++++------- interface/resources/qml/hifi/Pal.qml | 32 +++++++++++++--------- 2 files changed, 43 insertions(+), 22 deletions(-) diff --git a/interface/resources/qml/hifi/NameCard.qml b/interface/resources/qml/hifi/NameCard.qml index f547975533..2b44a72fe3 100644 --- a/interface/resources/qml/hifi/NameCard.qml +++ b/interface/resources/qml/hifi/NameCard.qml @@ -79,14 +79,13 @@ Item { } StateImage { id: infoHoverImage; - visible: avatarImageMouseArea.containsMouse ? true : false; + visible: false; imageURL: "../../images/info-icon-2-state.svg"; size: 32; buttonState: 1; anchors.centerIn: parent; } MouseArea { - id: avatarImageMouseArea; anchors.fill: parent enabled: selected || isMyCard; hoverEnabled: enabled @@ -94,6 +93,8 @@ Item { userInfoViewer.url = defaultBaseUrl + "/users/" + userName; userInfoViewer.visible = true; } + onEntered: infoHoverImage.visible = true; + onExited: infoHoverImage.visible = false; } } @@ -124,7 +125,7 @@ Item { anchors.leftMargin: avatarImage.visible ? 5 : 0; anchors.rightMargin: 5; // Style - color: myDisplayNameMouseArea.containsMouse ? hifi.colors.lightGrayText : hifi.colors.textFieldLightBackground + color: hifi.colors.textFieldLightBackground border.color: hifi.colors.blueHighlight border.width: 0 TextInput { @@ -165,7 +166,6 @@ Item { } } MouseArea { - id: myDisplayNameMouseArea; anchors.fill: parent hoverEnabled: true onClicked: { @@ -182,6 +182,8 @@ Item { pal.currentlyEditingDisplayName = true myDisplayNameText.autoScroll = true; } + onEntered: myDisplayName.color = hifi.colors.lightGrayText; + onExited: myDisplayName.color = hifi.colors.textFieldLightBackground; } // Edit pencil glyph HiFiGlyphs { @@ -226,13 +228,20 @@ Item { // Text Positioning verticalAlignment: Text.AlignTop // Style - color: (displayNameTextMouseArea.containsMouse || userNameTextMouseArea.containsMouse) ? hifi.colors.blueHighlight : hifi.colors.darkGray; + color: hifi.colors.darkGray; MouseArea { - id: displayNameTextMouseArea; anchors.fill: parent enabled: selected && pal.activeTab == "nearbyTab" && thisNameCard.userName !== ""; hoverEnabled: enabled onClicked: pal.sendToScript({method: 'goToUser', params: thisNameCard.userName}); + onEntered: { + displayNameText.color = hifi.colors.blueHighlight; + userNameText.color = hifi.colors.blueHighlight; + } + onExited: { + displayNameText.color = hifi.colors.darkGray + userNameText.color = hifi.colors.greenShadow; + } } } TextMetrics { @@ -309,14 +318,20 @@ Item { // Text Positioning verticalAlignment: Text.AlignBottom // Style - color: (pal.activeTab == "nearbyTab" && (displayNameTextMouseArea.containsMouse || userNameTextMouseArea.containsMouse)) - ? hifi.colors.blueHighlight : hifi.colors.greenShadow; + color: hifi.colors.greenShadow; MouseArea { - id: userNameTextMouseArea; anchors.fill: parent enabled: selected && pal.activeTab == "nearbyTab" && thisNameCard.userName !== ""; hoverEnabled: enabled onClicked: pal.sendToScript({method: 'goToUser', params: thisNameCard.userName}); + onEntered: { + displayNameText.color = hifi.colors.blueHighlight; + userNameText.color = hifi.colors.blueHighlight; + } + onExited: { + displayNameText.color = hifi.colors.darkGray; + userNameText.color = hifi.colors.greenShadow; + } } } // VU Meter diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index 762a0a14c7..3099087bd4 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -346,13 +346,12 @@ Rectangle { text: "[?]"; size: connectionsTabSelectorText.size + 6; font.capitalization: Font.AllUppercase; - color: connectionsTabSelectorMouseArea.containsMouse ? hifi.colors.redAccent : hifi.colors.redHighlight; + color: hifi.colors.redHighlight; horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignVCenter; anchors.fill: parent; } MouseArea { - id: connectionsTabSelectorMouseArea; anchors.fill: parent; hoverEnabled: true; onClicked: letterbox(hifi.glyphs.question, @@ -361,6 +360,8 @@ Rectangle { "When your availability is set to Everyone, Connections can see your username and location.

" + "Green borders around profile pictures are Friends.
" + "When your availability is set to Friends, only Friends can see your username and location."); + onEntered: connectionsHelpText.color = hifi.colors.redAccent; + onExited: connectionsHelpText.color = hifi.colors.redHighlight; } } } @@ -703,7 +704,6 @@ Rectangle { } // This Rectangle refers to the [?] popup button next to "NAMES" Rectangle { - id: helpText; color: hifi.colors.tableBackgroundLight; width: 20; height: hifi.dimensions.tableHeaderHeight - 2; @@ -712,16 +712,16 @@ Rectangle { anchors.topMargin: 1; anchors.leftMargin: actionButtonWidth + nearbyNameCardWidth/2 + displayNameHeaderMetrics.width/2 + 6; RalewayRegular { + id: helpText; text: "[?]"; size: hifi.fontSizes.tableHeading + 2; font.capitalization: Font.AllUppercase; - color: helpTextMouseArea.containsMouse ? hifi.colors.baseGrayHighlight : hifi.colors.darkGray; + color: hifi.colors.darkGray; horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignVCenter; anchors.fill: parent; } MouseArea { - id: helpTextMouseArea; anchors.fill: parent; hoverEnabled: true; onClicked: letterbox(hifi.glyphs.question, @@ -733,6 +733,8 @@ Rectangle { "If you can see someone's username, you can GoTo them by selecting them in the PAL, then clicking their name.
" + "
If someone's display name isn't set, a unique session display name is assigned to them.
" + "
Administrators of this domain can also see the username or machine ID associated with each avatar present."); + onEntered: helpText.color = hifi.colors.baseGrayHighlight; + onExited: helpText.color = hifi.colors.darkGray; } } // This Rectangle refers to the [?] popup button next to "ADMIN" @@ -750,19 +752,20 @@ Rectangle { text: "[?]"; size: hifi.fontSizes.tableHeading + 2; font.capitalization: Font.AllUppercase; - color: adminHelpTextMouseArea.containsMouse ? "#94132e" : hifi.colors.redHighlight; + color: hifi.colors.redHighlight; horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignVCenter; anchors.fill: parent; } MouseArea { - id: adminHelpTextMouseArea; anchors.fill: parent; hoverEnabled: true; onClicked: letterbox(hifi.glyphs.question, "Admin Actions", "Silence mutes a user's microphone. Silenced users can unmute themselves by clicking "UNMUTE" on their toolbar.

" + "Ban removes a user from this domain and prevents them from returning. Admins can un-ban users from the Sandbox Domain Settings page."); + onEntered: adminHelpText.color = "#94132e"; + onExited: adminHelpText.color = hifi.colors.redHighlight; } } } // "Nearby" Tab @@ -887,13 +890,14 @@ Rectangle { // Text Positioning verticalAlignment: Text.AlignVCenter // Style - color: connectionsLocationDataMouseArea.containsMouse ? hifi.colors.blueHighlight : hifi.colors.darkGray; + color: hifi.colors.darkGray; MouseArea { - id: connectionsLocationDataMouseArea; anchors.fill: parent hoverEnabled: enabled enabled: connectionsNameCard.selected && pal.activeTab == "connectionsTab" onClicked: pal.sendToScript({method: 'goToUser', params: model.userName}); + onEntered: connectionsLocationData.color = hifi.colors.blueHighlight; + onExited: connectionsLocationData.color = hifi.colors.darkGray; } } @@ -981,7 +985,7 @@ Rectangle { } Item { - id: closeButton + id: closeButtonContainer anchors { top: parent.top; right: parent.right; @@ -990,6 +994,7 @@ Rectangle { width: parent.width/2; FiraSansSemiBold { + id: closeButton; // Properties text: "CLOSE"; elide: Text.ElideRight; @@ -1001,12 +1006,13 @@ Rectangle { verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter; // Style - color: closeButtonMouseArea.containsMouse ? hifi.colors.redAccent : hifi.colors.redHighlight; + color: hifi.colors.redHighlight; MouseArea { - id: closeButtonMouseArea; anchors.fill: parent hoverEnabled: enabled onClicked: userInfoViewer.visible = false; + onEntered: closeButton.color = hifi.colors.redAccent; + onExited: closeButton.color = hifi.colors.redHighlight; } } } @@ -1014,7 +1020,7 @@ Rectangle { Item { id: urlBar anchors { - top: closeButton.bottom; + top: closeButtonContainer.bottom; left: parent.left; right: parent.right; } From d8719ac3a9b4a7db1b9ae695c6b579da61818c93 Mon Sep 17 00:00:00 2001 From: David Kelly Date: Wed, 15 Mar 2017 11:15:58 -0700 Subject: [PATCH 013/118] cleanup, switch to overlays with texture swapping, move overlay to between hands --- scripts/system/makeUserConnection.js | 93 ++++++++++++++++++++++------ 1 file changed, 73 insertions(+), 20 deletions(-) diff --git a/scripts/system/makeUserConnection.js b/scripts/system/makeUserConnection.js index 881a828a17..e97d1074e3 100644 --- a/scripts/system/makeUserConnection.js +++ b/scripts/system/makeUserConnection.js @@ -25,23 +25,30 @@ const STATE_STRINGS = ["inactive", "waiting", "friending", "makingFriends"]; const WAITING_INTERVAL = 100; // ms const FRIENDING_INTERVAL = 100; // ms const FRIENDING_TIME = 3000; // ms -const ENTITY_COLORS = [{red: 0x00, green: 0xFF, blue: 0x00}, {red: 0x00, green: 0x00, blue: 0xFF}, {red: 0xFF, green: 0x00, blue: 0x00}]; const FRIENDING_HAPTIC_STRENGTH = 0.5; const FRIENDING_SUCCESS_HAPTIC_STRENGTH = 1.0; const HAPTIC_DURATION = 20; +const MODEL_URL = "http://hifi-content.s3.amazonaws.com/alan/dev/Test/sphere-3-color.fbx"; +const TEXTURES = [ + {"Texture": "http://hifi-content.s3.amazonaws.com/alan/dev/Test/sphere-3-color.fbx/sphere-3-color.fbm/green-50pct-opaque-64.png"}, + {"Texture": "http://hifi-content.s3.amazonaws.com/alan/dev/Test/sphere-3-color.fbx/sphere-3-color.fbm/blue-50pct-opaque-64.png"}, + {"Texture": "http://hifi-content.s3.amazonaws.com/alan/dev/Test/sphere-3-color.fbx/sphere-3-color.fbm/red-50pct-opaque-64.png"} +]; var currentHand; var state = STATES.inactive; var friendingInterval; -var entity; +var overlay; var animHandlerId; var entityDimensionMultiplier = 1.0; var friendingId; +var friendingHand; function debug() { var stateString = "<" + STATE_STRINGS[state] + ">"; var versionString = "v" + version; - print.apply(null, [].concat.apply([label, versionString, stateString, friendingId], [].map.call(arguments, JSON.stringify))); + var friending = "[" + friendingId + "/" + friendingHand + "]"; + print.apply(null, [].concat.apply([label, versionString, stateString, friending], [].map.call(arguments, JSON.stringify))); } function handToString(hand) { @@ -53,6 +60,16 @@ function handToString(hand) { return ""; } +function stringToHand(hand) { + if (hand == "RightHand") { + return Controller.Standard.RightHand; + } else if (hand == "LeftHand") { + return Controller.Standard.LeftHand; + } + debug("stringToHand called with bad hand string:", hand); + return 0; +} + function handToHaptic(hand) { if (hand === Controller.Standard.RightHand) { return 1; @@ -62,11 +79,13 @@ function handToHaptic(hand) { return -1; } - +// This returns the position of the palm, really. Which relies on the avatar +// having the expected middle1 joint. TODO: fallback for when this isn't part +// of the avatar? function getHandPosition(avatar, hand) { if (!hand) { - debug("calling getHandPosition with no hand!"); - return; + debug("calling getHandPosition with no hand! (returning avatar position but this is a BUG)"); + return avatar.position; } var jointName = handToString(hand) + "Middle1"; return avatar.getJointPosition(avatar.getJointIndex(jointName)); @@ -92,33 +111,43 @@ function shakeHandsAnimation(animationProperties) { // this is called frequently, but usually does nothing function updateVisualization() { if (state == STATES.inactive) { - if (entity) { - entity = Entities.deleteEntity(entity); + if (overlay) { + overlay = Overlays.deleteOverlay(overlay); } return; } - var color = ENTITY_COLORS[state-1]; + var textures = TEXTURES[state-1]; var position = getHandPosition(MyAvatar, currentHand); // TODO: make the size scale with avatar, up to // the actual size of MAX_AVATAR_DISTANCE var wrist = MyAvatar.getJointPosition(MyAvatar.getJointIndex(handToString(currentHand))); var d = entityDimensionMultiplier * Vec3.distance(wrist, position); - var dimension = {x: d, y: d, z: d}; - if (!entity) { - var props = { - type: "Sphere", - color: color, - position: position, - dimensions: dimension + if (friendingId) { + // put the position between the 2 hands, if we have a friendingId + var other = AvatarList.getAvatar(friendingId); + if (other) { + var otherHand = getHandPosition(other, stringToHand(friendingHand)); + position = Vec3.sum(position, Vec3.multiply(0.5, Vec3.subtract(otherHand, position))); } - entity = Entities.addEntity(props); + } + var dimension = {x: d, y: d, z: d}; + if (!overlay) { + var props = { + url: MODEL_URL, + position: position, + dimensions: dimension, + textures: textures + }; + overlay = Overlays.addOverlay("model", props); } else { - Entities.editEntity(entity, {dimensions: dimension, position: position, color: color}); + Overlays.editOverlay(overlay, {textures: textures}); + Overlays.editOverlay(overlay, {dimensions: dimension, position: position}); } } + function findNearbyAvatars(nearestOnly) { var handPos = getHandPosition(MyAvatar, currentHand); var minDistance = MAX_AVATAR_DISTANCE; @@ -141,6 +170,15 @@ function findNearbyAvatars(nearestOnly) { return nearbyAvatars; } +// As currently implemented, we select the closest avatar (if close enough) and send +// them a friendRequest, or if someone already has sent us one, we just send the friendAck +// back to them. If nobody is close enough or has sent us a friendRequest, we just wait +// transition to waiting and wait for a friendRequest. If the 2 people who want to connect +// are both somewhat out of range when they initiate the shake, then neither gets a message +// and they both just stand there with their hands out. +// Ideally we'd either show that (so they ungrip/regrip and adjust position), or do what I +// initially did and start an interval to look for nearby avatars. The issue with the latter +// is this introduces some race condition we may need to handle (hence I didn't do it yet). function startHandshake(fromKeyboard) { if (fromKeyboard) { debug("adding animation"); @@ -170,7 +208,7 @@ function startHandshake(fromKeyboard) { messageSend({ key: "friendRequest", id: friendingId, - hand: handToString(nearestAvatar.hand) + hand: handToString(currentHand) }); } } @@ -179,9 +217,14 @@ function startHandshake(fromKeyboard) { function endHandshake() { debug("ending handshake for", currentHand); currentHand = undefined; + // note that setting the state to inactive should really + // only be done here, unless we change how the triggering works, + // as we ignore the key release event when inactive. See updateTriggers + // below. state = STATES.inactive; if (friendingInterval) { friendingId = undefined; + friendingHand = undefined; friendingInterval = Script.clearInterval(friendingInterval); // send done to let friend know you are not making friends now messageSend({ @@ -211,6 +254,7 @@ function updateTriggers(value, fromKeyboard, hand) { startHandshake(fromKeyboard); } } else { + // TODO: should we end handshake even when inactive? Ponder if (state != STATES.inactive) { endHandshake(); } else { @@ -247,6 +291,7 @@ function makeFriends(id) { Script.setTimeout(function () { state = STATES.waiting; friendingId = undefined; + friendingHand = undefined; entityDimensionMultiplier = 1.0; }, 1000); } @@ -270,7 +315,8 @@ function startFriending(id, hand) { }); friendingInterval = Script.setInterval(function () { - entityDimensionMultiplier = 1.0 + 2.0 * ++count * FRIENDING_INTERVAL / FRIENDING_TIME; + count += 1; + entityDimensionMultiplier = 1.0 + 2.0 * count * FRIENDING_INTERVAL / FRIENDING_TIME; if (state != STATES.friending) { debug("stopping friending interval, state changed"); friendingInterval = Script.clearInterval(friendingInterval); @@ -282,6 +328,7 @@ function startFriending(id, hand) { key: "done" }); friendingId = undefined; + friendingHand = undefined; startHandshake(); } else if (count > FRIENDING_TIME/FRIENDING_INTERVAL) { debug("made friends with " + id); @@ -324,11 +371,13 @@ function messageHandler(channel, messageString, senderID) { case "friendRequest": if (state == STATES.inactive && message.id == MyAvatar.sessionUUID) { friendingId = senderID; + friendingHand = message.hand; } else if (state == STATES.waiting && message.id == MyAvatar.sessionUUID && (!friendingId || friendingId == senderID)) { // you were waiting for a friend request, so send the ack. Or, you and the other // guy raced and both send friendRequests. Handle that too if (!friendingId) { friendingId = senderID; + friendingHand = message.hand; } messageSend({ key: "friendAck", @@ -343,6 +392,7 @@ function messageHandler(channel, messageString, senderID) { if (state == STATES.waiting && message.id == MyAvatar.sessionUUID && (!friendingId || friendingId == senderID)) { // start friending... friendingId = senderID; + friendingHand = message.hand; startFriending(senderID, message.hand); } // TODO: check to see if we are waiting for this but the person we are friending sent it to @@ -371,6 +421,7 @@ function messageHandler(channel, messageString, senderID) { // value for isKeyboard, as we should not change the animation // state anyways (if any) friendingId = undefined; + friendingHand = undefined; startHandshake(); } } else { @@ -378,6 +429,7 @@ function messageHandler(channel, messageString, senderID) { // do nothing (so you see the red for a bit) if (state != STATES.makingFriends && friendingId == senderID) { friendingId = undefined; + friendingHand = undefined; if (state != STATES.inactive) { startHandshake(); } @@ -386,6 +438,7 @@ function messageHandler(channel, messageString, senderID) { break; default: debug("unknown message", message); + break; } } From 786180291bb0d018eb59f2b1e3e74741fd81e764 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Wed, 15 Mar 2017 11:45:23 -0700 Subject: [PATCH 014/118] Bugfixes and improvements --- interface/resources/qml/hifi/Pal.qml | 68 +++++++++++++++++++++++----- 1 file changed, 57 insertions(+), 11 deletions(-) diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index 3099087bd4..85a0402b9a 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -17,6 +17,7 @@ import QtGraphicalEffects 1.0 import Qt.labs.settings 1.0 import "../styles-uit" import "../controls-uit" as HifiControls +import HFWebEngineProfile 1.0 // references HMD, Users, UserActivityLogger from root context @@ -285,7 +286,9 @@ Rectangle { pal.sendToScript({method: 'refreshConnections'}); } activeTab = "connectionsTab"; + connectionsLoading.visible = false; connectionsLoading.visible = true; + connectionsRefreshProblemText.visible = false; } } @@ -307,6 +310,7 @@ Rectangle { width: reloadConnections.height; glyph: hifi.glyphs.reload; onClicked: { + connectionsLoading.visible = false; connectionsLoading.visible = true; pal.sendToScript({method: 'refreshConnections'}); } @@ -795,6 +799,35 @@ Rectangle { height: width; anchors.centerIn: parent; visible: true; + onVisibleChanged: { + if (visible) { + connectionsTimeoutTimer.start(); + } else { + connectionsTimeoutTimer.stop(); + connectionsRefreshProblemText.visible = false; + } + } + } + + // "This is taking too long..." text + FiraSansSemiBold { + id: connectionsRefreshProblemText + // Properties + text: "This is taking longer than normal.\nIf you get stuck, try refreshing the Connections tab."; + // Anchors + anchors.top: connectionsLoading.bottom; + anchors.topMargin: 10; + anchors.left: parent.left; + anchors.bottom: parent.bottom; + width: parent.width; + // Text Size + size: 16; + // Text Positioning + verticalAlignment: Text.AlignTop; + horizontalAlignment: Text.AlignHCenter; + wrapMode: Text.WordWrap; + // Style + color: hifi.colors.darkGray; } // This TableView refers to the Connections Table (on the "Connections" tab below the current user's NameCard) @@ -945,7 +978,7 @@ Rectangle { Rectangle { id: navigationContainer; visible: userInfoViewer.visible; - height: 70; + height: 60; anchors { top: parent.top; left: parent.left; @@ -959,7 +992,7 @@ Rectangle { top: parent.top; left: parent.left; } - height: parent.height - urlBar.height; + height: parent.height - addressBar.height; width: parent.width/2; FiraSansSemiBold { @@ -979,7 +1012,11 @@ Rectangle { id: backButtonMouseArea; anchors.fill: parent hoverEnabled: enabled - onClicked: userInfoViewer.goBack(); + onClicked: { + if (userInfoViewer.canGoBack) { + userInfoViewer.goBack(); + } + } } } } @@ -990,7 +1027,7 @@ Rectangle { top: parent.top; right: parent.right; } - height: parent.height - urlBar.height; + height: parent.height - addressBar.height; width: parent.width/2; FiraSansSemiBold { @@ -1018,7 +1055,7 @@ Rectangle { } Item { - id: urlBar + id: addressBar anchors { top: closeButtonContainer.bottom; left: parent.left; @@ -1033,17 +1070,14 @@ Rectangle { elide: Text.ElideRight; // Anchors anchors.fill: parent; + anchors.leftMargin: 5; // Text Size size: 14; // Text Positioning verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter; + horizontalAlignment: Text.AlignLeft; // Style color: hifi.colors.lightGray; - MouseArea { - anchors.fill: parent - onClicked: userInfoViewer.visible = false; - } } } } @@ -1062,9 +1096,11 @@ Rectangle { HifiControls.WebView { id: userInfoViewer; + profile: HFWebEngineProfile { + storageName: "qmlWebEngine" + } anchors { top: navigationContainer.bottom; - topMargin: 5; bottom: parent.bottom; left: parent.left; right: parent.right; @@ -1093,6 +1129,15 @@ Rectangle { } } + // Timer used when refreshing the Connections tab + Timer { + id: connectionsTimeoutTimer; + interval: 3000; // 3 seconds + onTriggered: { + connectionsRefreshProblemText.visible = true; + } + } + function rowColor(selected, alternate) { return selected ? hifi.colors.orangeHighlight : alternate ? hifi.colors.tableRowLightEven : hifi.colors.tableRowLightOdd; } @@ -1135,6 +1180,7 @@ Rectangle { connectionsUserModelData = data; sortConnectionsModel(); connectionsLoading.visible = false; + connectionsRefreshProblemText.visible = false; break; case 'select': var sessionIds = message.params[0]; From f662571cabdf4e66d8b84bf31a44892c3e00bf93 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Wed, 15 Mar 2017 15:46:25 -0700 Subject: [PATCH 015/118] send credentials to private data-webs as well --- libraries/script-engine/src/XMLHttpRequestClass.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/script-engine/src/XMLHttpRequestClass.cpp b/libraries/script-engine/src/XMLHttpRequestClass.cpp index 4e528ec52c..1d3c8fda32 100644 --- a/libraries/script-engine/src/XMLHttpRequestClass.cpp +++ b/libraries/script-engine/src/XMLHttpRequestClass.cpp @@ -143,7 +143,7 @@ void XMLHttpRequestClass::open(const QString& method, const QString& url, bool a if (url.toLower().left(METAVERSE_API_URL.length()) == METAVERSE_API_URL) { auto accountManager = DependencyManager::get(); - if (_url.scheme() == "https" && accountManager->hasValidAccessToken()) { + if (accountManager->hasValidAccessToken()) { static const QString HTTP_AUTHORIZATION_HEADER = "Authorization"; QString bearerString = "Bearer " + accountManager->getAccountInfo().getAccessToken().token; _request.setRawHeader(HTTP_AUTHORIZATION_HEADER.toLocal8Bit(), bearerString.toLocal8Bit()); From 84fd17bd884253a558f241f0751f4627ec6be840 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Wed, 15 Mar 2017 16:33:23 -0700 Subject: [PATCH 016/118] use location.metaverseServerUrl instead of hardcoded value. --- interface/resources/qml/hifi/NameCard.qml | 2 +- interface/src/Application.cpp | 1 + scripts/system/pal.js | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/interface/resources/qml/hifi/NameCard.qml b/interface/resources/qml/hifi/NameCard.qml index 2b44a72fe3..9f73d1090e 100644 --- a/interface/resources/qml/hifi/NameCard.qml +++ b/interface/resources/qml/hifi/NameCard.qml @@ -27,7 +27,7 @@ Item { // Properties property string profileUrl: ""; - property string defaultBaseUrl: "http://highfidelity.com"; + property string defaultBaseUrl: location.metaverseServerUrl; property string connectionStatus : "" property string uuid: "" property string displayName: "" diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 1bb4c64884..9c86c4b8c6 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2011,6 +2011,7 @@ void Application::initializeUi() { rootContext->setContextProperty("Scene", DependencyManager::get().data()); rootContext->setContextProperty("Render", _renderEngine->getConfiguration().get()); rootContext->setContextProperty("Reticle", getApplicationCompositor().getReticleInterface()); + rootContext->setContextProperty("location", DependencyManager::get().data()); rootContext->setContextProperty("ApplicationCompositor", &getApplicationCompositor()); diff --git a/scripts/system/pal.js b/scripts/system/pal.js index ec0d19962c..b87f25294b 100644 --- a/scripts/system/pal.js +++ b/scripts/system/pal.js @@ -299,7 +299,8 @@ function updateUser(data) { // User management services // // These are prototype versions that will be changed when the back end changes. -var METAVERSE_BASE = 'https://metaverse.highfidelity.com'; +var METAVERSE_BASE = location.metaverseServerUrl; + function request(url, callback) { // cb(error, responseOfCorrectContentType) of url. General for 'get' text/html/json, but without redirects. var httpRequest = new XMLHttpRequest(); From 4f6123ee294a561d1ddaf07f9d83f79a92cedd20 Mon Sep 17 00:00:00 2001 From: David Kelly Date: Wed, 15 Mar 2017 16:41:07 -0700 Subject: [PATCH 017/118] more messaging rework - dealing with edges --- scripts/system/makeUserConnection.js | 189 +++++++++++++++++---------- 1 file changed, 120 insertions(+), 69 deletions(-) diff --git a/scripts/system/makeUserConnection.js b/scripts/system/makeUserConnection.js index e97d1074e3..d73eaedddb 100644 --- a/scripts/system/makeUserConnection.js +++ b/scripts/system/makeUserConnection.js @@ -38,17 +38,19 @@ const TEXTURES = [ var currentHand; var state = STATES.inactive; var friendingInterval; +var waitingInterval; var overlay; var animHandlerId; var entityDimensionMultiplier = 1.0; var friendingId; var friendingHand; +var waitingList = {}; function debug() { var stateString = "<" + STATE_STRINGS[state] + ">"; var versionString = "v" + version; var friending = "[" + friendingId + "/" + friendingHand + "]"; - print.apply(null, [].concat.apply([label, versionString, stateString, friending], [].map.call(arguments, JSON.stringify))); + print.apply(null, [].concat.apply([label, versionString, stateString, JSON.stringify(waitingList), friending], [].map.call(arguments, JSON.stringify))); } function handToString(hand) { @@ -85,6 +87,7 @@ function handToHaptic(hand) { function getHandPosition(avatar, hand) { if (!hand) { debug("calling getHandPosition with no hand! (returning avatar position but this is a BUG)"); + debug(new Error().stack); return avatar.position; } var jointName = handToString(hand) + "Middle1"; @@ -148,33 +151,42 @@ function updateVisualization() { } -function findNearbyAvatars(nearestOnly) { - var handPos = getHandPosition(MyAvatar, currentHand); - var minDistance = MAX_AVATAR_DISTANCE; - var nearbyAvatars = []; - AvatarList.getAvatarIdentifiers().forEach(function (identifier) { - if (!identifier) { return; } - var avatar = AvatarList.getAvatar(identifier); - var distanceR = Vec3.distance(getHandPosition(avatar, Controller.Standard.RightHand), handPos); - var distanceL = Vec3.distance(getHandPosition(avatar, Controller.Standard.LeftHand), handPos); - var distance = Math.min(distanceL, distanceR); - if (distance < minDistance) { - if (nearestOnly) { - minDistance = distance; - nearbyAvatars = []; - } - var hand = (distance == distanceR ? Controller.Standard.RightHand : Controller.Standard.LeftHand); - nearbyAvatars.push({avatar: identifier, hand: hand}); +function isNearby(id, hand) { + if (currentHand) { + var handPos = getHandPosition(MyAvatar, currentHand); + var avatar = AvatarList.getAvatar(id); + if (avatar) { + var otherHand = stringToHand(hand); + var distance = Vec3.distance(getHandPosition(avatar, otherHand), handPos); + return (distance < MAX_AVATAR_DISTANCE); } - }); - return nearbyAvatars; + } + return false; } -// As currently implemented, we select the closest avatar (if close enough) and send -// them a friendRequest, or if someone already has sent us one, we just send the friendAck -// back to them. If nobody is close enough or has sent us a friendRequest, we just wait -// transition to waiting and wait for a friendRequest. If the 2 people who want to connect -// are both somewhat out of range when they initiate the shake, then neither gets a message +function findNearestWaitingAvatar() { + var handPos = getHandPosition(MyAvatar, currentHand); + var minDistance = MAX_AVATAR_DISTANCE; + var nearestAvatar = {}; + Object.keys(waitingList).forEach(function (identifier) { + var avatar = AvatarList.getAvatar(identifier); + if (avatar) { + var hand = stringToHand(waitingList[identifier]); + var distance = Vec3.distance(getHandPosition(avatar, hand), handPos); + if (distance < minDistance) { + minDistance = distance; + nearestAvatar = {avatar: identifier, hand: hand}; + } + } + }); + return nearestAvatar; +} + + +// As currently implemented, we select the closest waiting avatar (if close enough) and send +// them a friendRequest. If nobody is close enough we just wait for a friendRequest. If the +// 2 people who want to connect are both somewhat out of range when they initiate the shake, +// then neither gets a message // and they both just stand there with their hands out. // Ideally we'd either show that (so they ungrip/regrip and adjust position), or do what I // initially did and start an interval to look for nearby avatars. The issue with the latter @@ -191,26 +203,26 @@ function startHandshake(fromKeyboard) { debug("starting handshake for", currentHand); state = STATES.waiting; entityDimensionMultiplier = 1.0; - // if we have a recent friendRequest, send an ack back. - // TODO: be sure the friendingId resets when we get the done message - if (friendingId) { - debug("sending friendAck to", friendingId); + friendingId = undefined; + friendingHand = undefined; + var nearestAvatar = findNearestWaitingAvatar(); + if (nearestAvatar.avatar) { + friendingId = nearestAvatar.avatar; + friendingHand = handToString(nearestAvatar.hand); + debug("sending friendRequest to", friendingId); messageSend({ - key: "friendAck", + key: "friendRequest", id: friendingId, hand: handToString(currentHand) }); } else { - var nearestAvatar = findNearbyAvatars(true)[0]; - if (nearestAvatar) { - friendingId = nearestAvatar.avatar; - debug("sending friendRequest to", friendingId); - messageSend({ - key: "friendRequest", - id: friendingId, - hand: handToString(currentHand) - }); - } + // send waiting message + debug("sending waiting message"); + messageSend({ + key: "waiting", + hand: handToString(currentHand) + }); + lookForWaitingAvatar(); } } @@ -222,9 +234,9 @@ function endHandshake() { // as we ignore the key release event when inactive. See updateTriggers // below. state = STATES.inactive; + friendingId = undefined; + friendingHand = undefined; if (friendingInterval) { - friendingId = undefined; - friendingHand = undefined; friendingInterval = Script.clearInterval(friendingInterval); // send done to let friend know you are not making friends now messageSend({ @@ -267,14 +279,39 @@ function messageSend(message) { Messages.sendMessage(MESSAGE_CHANNEL, JSON.stringify(message)); } -function isNearby(id, hand) { - var nearbyAvatars = findNearbyAvatars(); - for(var i = 0; i < nearbyAvatars.length; i++) { - if (nearbyAvatars[i].avatar == id && handToString(nearbyAvatars[i].hand) == hand) { - return true; - } +function lookForWaitingAvatar() { + // we started with nobody close enough, but maybe I've moved + // or they did. Note that 2 people doing this race, so stop + // as soon as you have a friendingId (which means you got their + // message before noticing they were in range in this loop) + + // just in case we reenter before stopping + if (waitingInterval) { + waitingInterval = Script.clearInterval(waitingInterval); } - return false; + debug("started looking for waiting avatars"); + waitingInterval = Script.setInterval(function () { + if (state == STATES.waiting && !friendingId) { + // find the closest in-range avatar, and send friend request + // TODO: this is same code as in startHandshake - get this + // cleaned up. + var nearestAvatar = findNearestWaitingAvatar(); + if (nearestAvatar.avatar) { + friendingId = nearestAvatar.avatar; + friendingHand = handToString(nearestAvatar.hand); + debug("sending friendRequest to", friendingId); + messageSend({ + key: "friendRequest", + id: friendingId, + hand: handToString(currentHand) + }); + } + } else { + // something happened, stop looking for avatars to friend + waitingInterval = Script.clearInterval(waitingInterval); + debug("stopped looking for waiting avatars"); + } + }, WAITING_INTERVAL); } // this should be where we make the appropriate friend call. For now just make the @@ -289,12 +326,12 @@ function makeFriends(id) { state = STATES.makingFriends; // now that we made friends, reset everything Script.setTimeout(function () { - state = STATES.waiting; friendingId = undefined; friendingHand = undefined; entityDimensionMultiplier = 1.0; }, 1000); } + // we change states, start the friendingInterval where we check // to be sure the hand is still close enough. If not, we terminate // the interval, go back to the waiting state. If we make it @@ -304,6 +341,7 @@ function startFriending(id, hand) { debug("friending", id, "hand", hand); // do we need to do this? friendingId = id; + friendingHand = hand; state = STATES.friending; Controller.triggerHapticPulse(FRIENDING_HAPTIC_STRENGTH, HAPTIC_DURATION, handToHaptic(currentHand)); @@ -327,8 +365,6 @@ function startFriending(id, hand) { messageSend({ key: "done" }); - friendingId = undefined; - friendingHand = undefined; startHandshake(); } else if (count > FRIENDING_TIME/FRIENDING_INTERVAL) { debug("made friends with " + id); @@ -368,17 +404,18 @@ function messageHandler(channel, messageString, senderID) { } debug("recv'd message:", message); switch (message.key) { + case "waiting": + // add this guy to waiting object. Any other message from this person will + // remove it from the list + waitingList[senderID] = message.hand; + break; case "friendRequest": - if (state == STATES.inactive && message.id == MyAvatar.sessionUUID) { - friendingId = senderID; - friendingHand = message.hand; - } else if (state == STATES.waiting && message.id == MyAvatar.sessionUUID && (!friendingId || friendingId == senderID)) { + delete waitingList[senderID]; + if (state == STATES.waiting && message.id == MyAvatar.sessionUUID && (!friendingId || friendingId == senderID)) { // you were waiting for a friend request, so send the ack. Or, you and the other // guy raced and both send friendRequests. Handle that too - if (!friendingId) { - friendingId = senderID; - friendingHand = message.hand; - } + friendingId = senderID; + friendingHand = message.hand; messageSend({ key: "friendAck", id: senderID, @@ -389,17 +426,32 @@ function messageHandler(channel, messageString, senderID) { // and try again break; case "friendAck": - if (state == STATES.waiting && message.id == MyAvatar.sessionUUID && (!friendingId || friendingId == senderID)) { - // start friending... - friendingId = senderID; - friendingHand = message.hand; - startFriending(senderID, message.hand); + delete waitingList[senderID]; + if (state == STATES.waiting && (!friendingId || friendingId == senderID)) { + if (message.id == MyAvatar.sessionUUID) { + // start friending... + friendingId = senderID; + friendingHand = message.hand; + startFriending(senderID, message.hand); + } else { + if (friendingId) { + // this is for someone else (we lost race in friendRequest), + // so lets start over + startHandshake(); + } + } } // TODO: check to see if we are waiting for this but the person we are friending sent it to // someone else, and try again break; case "friending": + delete waitingList[senderID]; if (state == STATES.waiting && senderID == friendingId) { + // temporary logging + if (friendingHand != message.hand) { + debug("friending hand", friendingHand, "not same as friending hand in message", message.hand); + } + friendingHand = message.hand; if (message.id != MyAvatar.sessionUUID) { // the person we were trying to friend is friending someone else // so try again @@ -410,6 +462,7 @@ function messageHandler(channel, messageString, senderID) { } break; case "done": + delete waitingList[senderID]; if (state == STATES.friending && friendingId == senderID) { // if they are done, and didn't friend us, terminate our // friending @@ -420,8 +473,6 @@ function messageHandler(channel, messageString, senderID) { // now just call startHandshake. Should be ok to do so without a // value for isKeyboard, as we should not change the animation // state anyways (if any) - friendingId = undefined; - friendingHand = undefined; startHandshake(); } } else { @@ -496,8 +547,8 @@ Script.scriptEnding.connect(function () { Controller.keyReleaseEvent.disconnect(keyReleaseEvent); debug("disconnecting updateVisualization"); Script.update.disconnect(updateVisualization); - if (entity) { - entity = Entities.deleteEntity(entity); + if (overlay) { + overlay = Overlays.deleteOverlay(overlay); } }); From 05fec328c02f7ac422c10caf3290857d68346b92 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Thu, 16 Mar 2017 10:32:49 -0700 Subject: [PATCH 018/118] Make tablet popups work like they should --- interface/resources/qml/controls/WebView.qml | 10 +++---- interface/resources/qml/desktop/Desktop.qml | 7 +++++ interface/resources/qml/hifi/Pal.qml | 29 +++++++++----------- interface/src/ui/overlays/Web3DOverlay.cpp | 3 ++ 4 files changed, 28 insertions(+), 21 deletions(-) diff --git a/interface/resources/qml/controls/WebView.qml b/interface/resources/qml/controls/WebView.qml index ae96590e97..b67948d651 100644 --- a/interface/resources/qml/controls/WebView.qml +++ b/interface/resources/qml/controls/WebView.qml @@ -101,11 +101,11 @@ Item { } onNewViewRequested:{ - // desktop is not defined for web-entities - if (desktop) { - var component = Qt.createComponent("../Browser.qml"); - var newWindow = component.createObject(desktop); - request.openIn(newWindow.webView); + // desktop is not defined for web-entities or tablet + if (typeof desktop !== "undefined") { + desktop.openBrowserWindow(request, profile); + } else { + console.log("onNewViewRequested: desktop not defined"); } } } diff --git a/interface/resources/qml/desktop/Desktop.qml b/interface/resources/qml/desktop/Desktop.qml index cc64d0f2b4..d8aedf6666 100644 --- a/interface/resources/qml/desktop/Desktop.qml +++ b/interface/resources/qml/desktop/Desktop.qml @@ -490,6 +490,13 @@ FocusScope { desktop.forceActiveFocus(); } + function openBrowserWindow(request, profile) { + var component = Qt.createComponent("../Browser.qml"); + var newWindow = component.createObject(desktop); + newWindow.webView.profile = profile; + request.openIn(newWindow.webView); + } + FocusHack { id: focusHack; } Rectangle { diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index 85a0402b9a..ae79c11d1a 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -16,8 +16,8 @@ import QtQuick.Controls 1.4 import QtGraphicalEffects 1.0 import Qt.labs.settings 1.0 import "../styles-uit" -import "../controls-uit" as HifiControls -import HFWebEngineProfile 1.0 +import "../controls-uit" as HifiControlsUit +import "../controls" as HifiControls // references HMD, Users, UserActivityLogger from root context @@ -161,7 +161,7 @@ Rectangle { horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignTop; } - HifiControls.TabletComboBox { + HifiControlsUit.TabletComboBox { id: availabilityComboBox; // Anchors anchors.top: parent.top; @@ -240,7 +240,7 @@ Rectangle { verticalAlignment: Text.AlignVCenter; } // "In View" Checkbox - HifiControls.CheckBox { + HifiControlsUit.CheckBox { id: inViewCheckbox; visible: activeTab == "nearbyTab"; anchors.right: reloadNearbyContainer.left; @@ -260,7 +260,7 @@ Rectangle { anchors.rightMargin: 6; height: reloadNearby.height; width: height; - HifiControls.GlyphButton { + HifiControlsUit.GlyphButton { id: reloadNearby; width: reloadNearby.height; glyph: hifi.glyphs.reload; @@ -305,7 +305,7 @@ Rectangle { anchors.rightMargin: 6; height: reloadConnections.height; width: height; - HifiControls.GlyphButton { + HifiControlsUit.GlyphButton { id: reloadConnections; width: reloadConnections.height; glyph: hifi.glyphs.reload; @@ -494,7 +494,7 @@ Rectangle { } } // This TableView refers to the Nearby Table (on the "Nearby" tab below the current user's NameCard) - HifiControls.Table { + HifiControlsUit.Table { id: nearbyTable; // Anchors anchors.fill: parent; @@ -592,7 +592,7 @@ Rectangle { // Anchors anchors.left: parent.left; } - HifiControls.GlyphButton { + HifiControlsUit.GlyphButton { function getGlyph() { var fileName = "vol_"; if (model && model.personalMute) { @@ -626,7 +626,7 @@ Rectangle { // Clicking on the sides of the sorting header doesn't cause this problem. // I'm guessing this is a QT bug and not anything I can fix. I spent too long trying to work around it... // I'm just going to leave the minor visual bug in. - HifiControls.CheckBox { + HifiControlsUit.CheckBox { id: actionCheckBox; visible: isCheckBox; anchors.centerIn: parent; @@ -658,7 +658,7 @@ Rectangle { } // This Button belongs in the columns that contain the stateless action buttons ("Silence" & "Ban" for now) - HifiControls.Button { + HifiControlsUit.Button { id: actionButton; color: 2; // Red visible: isButton; @@ -831,7 +831,7 @@ Rectangle { } // This TableView refers to the Connections Table (on the "Connections" tab below the current user's NameCard) - HifiControls.Table { + HifiControlsUit.Table { id: connectionsTable; visible: !connectionsLoading.visible; // Anchors @@ -935,7 +935,7 @@ Rectangle { } // "Friends" checkbox - HifiControls.CheckBox { + HifiControlsUit.CheckBox { id: friendsCheckBox; visible: styleData.role === "friends"; anchors.centerIn: parent; @@ -960,7 +960,7 @@ Rectangle { } // "Connections" Tab } // palTabContainer - HifiControls.Keyboard { + HifiControlsUit.Keyboard { id: keyboard; raised: currentlyEditingDisplayName && HMD.mounted; numeric: parent.punctuationMode; @@ -1096,9 +1096,6 @@ Rectangle { HifiControls.WebView { id: userInfoViewer; - profile: HFWebEngineProfile { - storageName: "qmlWebEngine" - } anchors { top: navigationContainer.bottom; bottom: parent.bottom; diff --git a/interface/src/ui/overlays/Web3DOverlay.cpp b/interface/src/ui/overlays/Web3DOverlay.cpp index 7b9e075d64..90dcaafc21 100644 --- a/interface/src/ui/overlays/Web3DOverlay.cpp +++ b/interface/src/ui/overlays/Web3DOverlay.cpp @@ -39,6 +39,7 @@ #include "scripting/HMDScriptingInterface.h" #include #include "FileDialogHelper.h" +#include static const float DPI = 30.47f; static const float INCHES_TO_METERS = 1.0f / 39.3701f; @@ -177,6 +178,8 @@ void Web3DOverlay::loadSourceURL() { } } _webSurface->getRootContext()->setContextProperty("globalPosition", vec3toVariant(getPosition())); + auto offscreenUi = DependencyManager::get(); + _webSurface->getRootContext()->setContextProperty("desktop", offscreenUi->getDesktop()); } void Web3DOverlay::render(RenderArgs* args) { From b3821bb02d5d8919004e482e0ab90b3a4a00bad1 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Thu, 16 Mar 2017 12:56:54 -0700 Subject: [PATCH 019/118] fix location vs AddressManager woes --- interface/resources/qml/hifi/NameCard.qml | 2 +- interface/src/Application.cpp | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/interface/resources/qml/hifi/NameCard.qml b/interface/resources/qml/hifi/NameCard.qml index 9f73d1090e..d43cf90386 100644 --- a/interface/resources/qml/hifi/NameCard.qml +++ b/interface/resources/qml/hifi/NameCard.qml @@ -27,7 +27,7 @@ Item { // Properties property string profileUrl: ""; - property string defaultBaseUrl: location.metaverseServerUrl; + property string defaultBaseUrl: AddressManager.metaverseServerUrl; property string connectionStatus : "" property string uuid: "" property string displayName: "" diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 9c86c4b8c6..1bb4c64884 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2011,7 +2011,6 @@ void Application::initializeUi() { rootContext->setContextProperty("Scene", DependencyManager::get().data()); rootContext->setContextProperty("Render", _renderEngine->getConfiguration().get()); rootContext->setContextProperty("Reticle", getApplicationCompositor().getReticleInterface()); - rootContext->setContextProperty("location", DependencyManager::get().data()); rootContext->setContextProperty("ApplicationCompositor", &getApplicationCompositor()); From 60000becce729d4ae2755a4cdb819c65fbdf655d Mon Sep 17 00:00:00 2001 From: David Kelly Date: Thu, 16 Mar 2017 13:19:17 -0700 Subject: [PATCH 020/118] more careful stopping of intervals, edge case stuff --- scripts/system/makeUserConnection.js | 94 ++++++++++++++++++---------- 1 file changed, 60 insertions(+), 34 deletions(-) diff --git a/scripts/system/makeUserConnection.js b/scripts/system/makeUserConnection.js index d73eaedddb..d68c328071 100644 --- a/scripts/system/makeUserConnection.js +++ b/scripts/system/makeUserConnection.js @@ -24,7 +24,8 @@ const STATES = { const STATE_STRINGS = ["inactive", "waiting", "friending", "makingFriends"]; const WAITING_INTERVAL = 100; // ms const FRIENDING_INTERVAL = 100; // ms -const FRIENDING_TIME = 3000; // ms +const MAKING_FRIENDS_TIMEOUT = 1000; // ms +const FRIENDING_TIME = 2000; // ms const FRIENDING_HAPTIC_STRENGTH = 0.5; const FRIENDING_SUCCESS_HAPTIC_STRENGTH = 1.0; const HAPTIC_DURATION = 20; @@ -39,6 +40,7 @@ var currentHand; var state = STATES.inactive; var friendingInterval; var waitingInterval; +var makingFriendsTimeout; var overlay; var animHandlerId; var entityDimensionMultiplier = 1.0; @@ -81,6 +83,24 @@ function handToHaptic(hand) { return -1; } +function stopWaiting() { + if (waitingInterval) { + waitingInterval = Script.clearInterval(waitingInterval); + } +} + +function stopFriending() { + if (friendingInterval) { + friendingInterval = Script.clearInterval(friendingInterval); + } +} + +function stopMakingFriends() { + if (makingFriendsTimeout) { + makingFriendsTimeout = Script.clearTimeout(makingFriendsTimeout); + } +} + // This returns the position of the palm, really. Which relies on the avatar // having the expected middle1 joint. TODO: fallback for when this isn't part // of the avatar? @@ -184,13 +204,10 @@ function findNearestWaitingAvatar() { // As currently implemented, we select the closest waiting avatar (if close enough) and send -// them a friendRequest. If nobody is close enough we just wait for a friendRequest. If the -// 2 people who want to connect are both somewhat out of range when they initiate the shake, -// then neither gets a message -// and they both just stand there with their hands out. -// Ideally we'd either show that (so they ungrip/regrip and adjust position), or do what I -// initially did and start an interval to look for nearby avatars. The issue with the latter -// is this introduces some race condition we may need to handle (hence I didn't do it yet). +// them a friendRequest. If nobody is close enough we send a waiting message, and wait for a +// friendRequest. If the 2 people who want to connect are both somewhat out of range when they +// initiate the shake, they will race to see who sends the friendRequest after noticing the +// waiting message. Either way, they will start friending eachother at that point. function startHandshake(fromKeyboard) { if (fromKeyboard) { debug("adding animation"); @@ -205,6 +222,11 @@ function startHandshake(fromKeyboard) { entityDimensionMultiplier = 1.0; friendingId = undefined; friendingHand = undefined; + // just in case + stopWaiting(); + stopFriending(); + stopMakingFriends(); + var nearestAvatar = findNearestWaitingAvatar(); if (nearestAvatar.avatar) { friendingId = nearestAvatar.avatar; @@ -236,13 +258,14 @@ function endHandshake() { state = STATES.inactive; friendingId = undefined; friendingHand = undefined; - if (friendingInterval) { - friendingInterval = Script.clearInterval(friendingInterval); - // send done to let friend know you are not making friends now - messageSend({ - key: "done" - }); - } + stopWaiting(); + stopFriending(); + stopMakingFriends(); + // send done to let friend know you are not making friends now + messageSend({ + key: "done" + }); + if (animHandlerId) { debug("removing animation"); MyAvatar.removeAnimationStateHandler(animHandlerId); @@ -286,9 +309,7 @@ function lookForWaitingAvatar() { // message before noticing they were in range in this loop) // just in case we reenter before stopping - if (waitingInterval) { - waitingInterval = Script.clearInterval(waitingInterval); - } + stopWaiting(); debug("started looking for waiting avatars"); waitingInterval = Script.setInterval(function () { if (state == STATES.waiting && !friendingId) { @@ -308,7 +329,7 @@ function lookForWaitingAvatar() { } } else { // something happened, stop looking for avatars to friend - waitingInterval = Script.clearInterval(waitingInterval); + stopWaiting(); debug("stopped looking for waiting avatars"); } }, WAITING_INTERVAL); @@ -325,11 +346,12 @@ function makeFriends(id) { Controller.triggerHapticPulse(FRIENDING_SUCCESS_HAPTIC_STRENGTH, HAPTIC_DURATION, handToHaptic(currentHand)); state = STATES.makingFriends; // now that we made friends, reset everything - Script.setTimeout(function () { + makingFriendsTimeout = Script.setTimeout(function () { friendingId = undefined; friendingHand = undefined; entityDimensionMultiplier = 1.0; - }, 1000); + makingFriendsTimeout = undefined; + }, MAKING_FRIENDS_TIMEOUT); } // we change states, start the friendingInterval where we check @@ -357,11 +379,11 @@ function startFriending(id, hand) { entityDimensionMultiplier = 1.0 + 2.0 * count * FRIENDING_INTERVAL / FRIENDING_TIME; if (state != STATES.friending) { debug("stopping friending interval, state changed"); - friendingInterval = Script.clearInterval(friendingInterval); + stopFriending(); } else if (!isNearby(id, hand)) { // gotta go back to waiting debug(id, "moved, back to waiting"); - friendingInterval = Script.clearInterval(friendingInterval); + stopFriending(); messageSend({ key: "done" }); @@ -369,19 +391,21 @@ function startFriending(id, hand) { } else if (count > FRIENDING_TIME/FRIENDING_INTERVAL) { debug("made friends with " + id); makeFriends(id); - friendingInterval = Script.clearInterval(friendingInterval); + stopFriending(); } }, FRIENDING_INTERVAL); } /* -A simple sequence diagram: +A simple sequence diagram: NOTE that the FriendAck is somewhat +vestigial, and probably should be removed shortly. Avatar A Avatar B | | - | <-----(FriendRequest) -- startHandshake - startHandshake -- (FriendAck) ---> | + | <-----(waiting) ----- startHandshake + startHandshake -- (FriendRequest) -> | | | - | <-------(friending) -- startFriending + | <-------(FriendAck) --------- | + | <--------(friending) -- startFriending startFriending -- (friending) ---> | | | | friends @@ -402,7 +426,6 @@ function messageHandler(channel, messageString, senderID) { } catch (e) { debug(e); } - debug("recv'd message:", message); switch (message.key) { case "waiting": // add this guy to waiting object. Any other message from this person will @@ -421,9 +444,13 @@ function messageHandler(channel, messageString, senderID) { id: senderID, hand: handToString(currentHand) }); + } else { + if (state == STATES.waiting && friendingId == senderID) { + // the person you are trying to friend sent a request to someone else. See the + // if statement above. So, don't cry, just start the handshake over again + startHandshake(); + } } - // TODO: check to see if the person we are trying to friend sent this to someone else, - // and try again break; case "friendAck": delete waitingList[senderID]; @@ -432,6 +459,7 @@ function messageHandler(channel, messageString, senderID) { // start friending... friendingId = senderID; friendingHand = message.hand; + stopWaiting(); startFriending(senderID, message.hand); } else { if (friendingId) { @@ -467,9 +495,7 @@ function messageHandler(channel, messageString, senderID) { // if they are done, and didn't friend us, terminate our // friending if (message.friendId !== MyAvatar.sessionUUID) { - if (friendingInterval) { - friendingInterval = Script.clearInterval(friendingInterval); - } + stopFriending(); // now just call startHandshake. Should be ok to do so without a // value for isKeyboard, as we should not change the animation // state anyways (if any) From fc2a501474b6af75fade47964d266cc0713111f2 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Thu, 16 Mar 2017 15:37:59 -0700 Subject: [PATCH 021/118] Cleanup JS<->QML messaging --- interface/resources/qml/hifi/NameCard.qml | 15 ++++++-- interface/resources/qml/hifi/Pal.qml | 29 +++++++++++---- .../GlobalServicesScriptingInterface.h | 1 + interface/src/ui/overlays/Web3DOverlay.cpp | 4 ++ libraries/avatars/src/AvatarData.h | 5 ++- libraries/networking/src/AddressManager.h | 4 +- scripts/system/pal.js | 37 +------------------ 7 files changed, 47 insertions(+), 48 deletions(-) diff --git a/interface/resources/qml/hifi/NameCard.qml b/interface/resources/qml/hifi/NameCard.qml index d43cf90386..ea490d248d 100644 --- a/interface/resources/qml/hifi/NameCard.qml +++ b/interface/resources/qml/hifi/NameCard.qml @@ -156,7 +156,10 @@ Item { autoScroll: false; // Signals onEditingFinished: { - pal.sendToScript({method: 'displayNameUpdate', params: text}) + if (MyAvatar.displayName !== text) { + MyAvatar.displayName = text; + UserActivityLogger.palAction("display_name_change", text); + } cursorPosition = 0 focus = false myDisplayName.border.width = 0 @@ -233,7 +236,10 @@ Item { anchors.fill: parent enabled: selected && pal.activeTab == "nearbyTab" && thisNameCard.userName !== ""; hoverEnabled: enabled - onClicked: pal.sendToScript({method: 'goToUser', params: thisNameCard.userName}); + onClicked: { + AddressManager.goToUser(thisNameCard.userName); + UserActivityLogger.palAction("go_to_user", thisNameCard.userName); + } onEntered: { displayNameText.color = hifi.colors.blueHighlight; userNameText.color = hifi.colors.blueHighlight; @@ -323,7 +329,10 @@ Item { anchors.fill: parent enabled: selected && pal.activeTab == "nearbyTab" && thisNameCard.userName !== ""; hoverEnabled: enabled - onClicked: pal.sendToScript({method: 'goToUser', params: thisNameCard.userName}); + onClicked: { + AddressManager.goToUser(thisNameCard.userName); + UserActivityLogger.palAction("go_to_user", thisNameCard.userName); + } onEntered: { displayNameText.color = hifi.colors.blueHighlight; userNameText.color = hifi.colors.blueHighlight; diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index ae79c11d1a..8dfab8c62d 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -42,7 +42,6 @@ Rectangle { property var connectionsUserModelData: []; // This simple list is essentially a mirror of the connectionsUserModel listModel without all the extra complexities. property bool iAmAdmin: false; property var activeTab: "nearbyTab"; - property int usernameAvailability; property bool currentlyEditingDisplayName: false HifiConstants { id: hifi; } @@ -162,6 +161,18 @@ Rectangle { verticalAlignment: Text.AlignTop; } HifiControlsUit.TabletComboBox { + function determineAvailabilityIndex() { + var globalServicesAvailability = GlobalServices.findableBy; + if (globalServicesAvailability === "all") { + return 0; + } else if (globalServicesAvailability === "friends") { + return 1; + } else if (globalServicesAvailability === "none") { + return 2; + } else { + return 1; + } + } id: availabilityComboBox; // Anchors anchors.top: parent.top; @@ -169,14 +180,18 @@ Rectangle { // Size width: parent.width; height: 40; - currentIndex: usernameAvailability; + currentIndex: determineAvailabilityIndex(); model: ListModel { id: availabilityComboBoxListItems ListElement { text: "Everyone"; value: "all"; } ListElement { text: "Friends Only"; value: "friends"; } ListElement { text: "Appear Offline"; value: "none" } } - onCurrentIndexChanged: { pal.sendToScript({method: 'setAvailability', params: availabilityComboBoxListItems.get(currentIndex).value})} + onCurrentIndexChanged: { + GlobalServices.findableBy = availabilityComboBoxListItems.get(currentIndex).value; + UserActivityLogger.palAction("set_availability", availabilityComboBoxListItems.get(currentIndex).value); + print('Setting availability:', JSON.stringify(GlobalServices.findableBy)); + } } } } @@ -928,7 +943,10 @@ Rectangle { anchors.fill: parent hoverEnabled: enabled enabled: connectionsNameCard.selected && pal.activeTab == "connectionsTab" - onClicked: pal.sendToScript({method: 'goToUser', params: model.userName}); + onClicked: { + AddressManager.goToUser(model.userName); + UserActivityLogger.palAction("go_to_user", model.userName); + } onEntered: connectionsLocationData.color = hifi.colors.blueHighlight; onExited: connectionsLocationData.color = hifi.colors.darkGray; } @@ -1260,9 +1278,6 @@ Rectangle { var sessionID = message.params[0]; delete ignored[sessionID]; break; - case 'updateAvailability': - usernameAvailability = message.params; - break; default: console.log('Unrecognized message:', JSON.stringify(message)); } diff --git a/interface/src/scripting/GlobalServicesScriptingInterface.h b/interface/src/scripting/GlobalServicesScriptingInterface.h index 11d8735187..299815b3fd 100644 --- a/interface/src/scripting/GlobalServicesScriptingInterface.h +++ b/interface/src/scripting/GlobalServicesScriptingInterface.h @@ -18,6 +18,7 @@ #include #include #include +#include class DownloadInfoResult { public: diff --git a/interface/src/ui/overlays/Web3DOverlay.cpp b/interface/src/ui/overlays/Web3DOverlay.cpp index 90dcaafc21..3db1aa65bb 100644 --- a/interface/src/ui/overlays/Web3DOverlay.cpp +++ b/interface/src/ui/overlays/Web3DOverlay.cpp @@ -40,6 +40,8 @@ #include #include "FileDialogHelper.h" #include +#include "avatar/AvatarManager.h" +#include "scripting/GlobalServicesScriptingInterface.h" static const float DPI = 30.47f; static const float INCHES_TO_METERS = 1.0f / 39.3701f; @@ -171,6 +173,8 @@ void Web3DOverlay::loadSourceURL() { _webSurface->getRootContext()->setContextProperty("Account", AccountScriptingInterface::getInstance()); _webSurface->getRootContext()->setContextProperty("HMD", DependencyManager::get().data()); _webSurface->getRootContext()->setContextProperty("fileDialogHelper", new FileDialogHelper()); + _webSurface->getRootContext()->setContextProperty("MyAvatar", DependencyManager::get()->getMyAvatar().get()); + _webSurface->getRootContext()->setContextProperty("GlobalServices", GlobalServicesScriptingInterface::getInstance()); tabletScriptingInterface->setQmlTabletRoot("com.highfidelity.interface.tablet.system", _webSurface->getRootItem(), _webSurface.data()); // Override min fps for tablet UI, for silky smooth scrolling diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index f0759aedbd..1327798a0a 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -340,7 +340,7 @@ class AvatarData : public QObject, public SpatiallyNestable { Q_PROPERTY(float audioLoudness READ getAudioLoudness WRITE setAudioLoudness) Q_PROPERTY(float audioAverageLoudness READ getAudioAverageLoudness WRITE setAudioAverageLoudness) - Q_PROPERTY(QString displayName READ getDisplayName WRITE setDisplayName) + Q_PROPERTY(QString displayName READ getDisplayName WRITE setDisplayName NOTIFY displayNameChanged) // sessionDisplayName is sanitized, defaulted version displayName that is defined by the AvatarMixer rather than by Interface clients. // The result is unique among all avatars present at the time. Q_PROPERTY(QString sessionDisplayName READ getSessionDisplayName WRITE setSessionDisplayName) @@ -614,6 +614,9 @@ public: +signals: + void displayNameChanged(); + public slots: void sendAvatarDataPacket(); void sendIdentityPacket(); diff --git a/libraries/networking/src/AddressManager.h b/libraries/networking/src/AddressManager.h index c7d283ad02..83eedfc82f 100644 --- a/libraries/networking/src/AddressManager.h +++ b/libraries/networking/src/AddressManager.h @@ -41,7 +41,7 @@ class AddressManager : public QObject, public Dependency { Q_PROPERTY(QString pathname READ currentPath) Q_PROPERTY(QString placename READ getPlaceName) Q_PROPERTY(QString domainId READ getDomainId) - Q_PROPERTY(QUrl metaverseServerUrl READ getMetaverseServerUrl) + Q_PROPERTY(QUrl metaverseServerUrl READ getMetaverseServerUrl NOTIFY metaverseServerUrlChanged) public: Q_INVOKABLE QString protocolVersion(); using PositionGetter = std::function; @@ -123,6 +123,8 @@ signals: void goBackPossible(bool isPossible); void goForwardPossible(bool isPossible); + void metaverseServerUrlChanged(); + protected: AddressManager(); private slots: diff --git a/scripts/system/pal.js b/scripts/system/pal.js index b87f25294b..014d89dde7 100644 --- a/scripts/system/pal.js +++ b/scripts/system/pal.js @@ -1,6 +1,6 @@ "use strict"; /*jslint vars:true, plusplus:true, forin:true*/ -/*global Tablet, Settings, Script, AvatarList, Users, Entities, MyAvatar, Camera, Overlays, Vec3, Quat, HMD, Controller, Account, UserActivityLogger, Messages, Window, XMLHttpRequest, print, location, getControllerWorldLocation, GlobalServices*/ +/*global Tablet, Settings, Script, AvatarList, Users, Entities, MyAvatar, Camera, Overlays, Vec3, Quat, HMD, Controller, Account, UserActivityLogger, Messages, Window, XMLHttpRequest, print, location, getControllerWorldLocation*/ /* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ // // pal.js @@ -265,24 +265,6 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See getConnectionData(); UserActivityLogger.palAction("refresh_connections", ""); break; - case 'displayNameUpdate': - if (MyAvatar.displayName !== message.params) { - MyAvatar.displayName = message.params; - UserActivityLogger.palAction("display_name_change", ""); - } - break; - case 'goToUser': - location.goToUser(message.params); - UserActivityLogger.palAction("go_to_user", ""); - break; - case 'setAvailability': - GlobalServices.findableBy = message.params; - UserActivityLogger.palAction("set_availability", ""); - print('Setting availability:', JSON.stringify(message)); - break; - case 'getAvailability': - findableByChanged(GlobalServices.findableBy); - break; default: print('Unrecognized message from Pal.qml:', JSON.stringify(message)); } @@ -709,7 +691,6 @@ function startup() { Messages.subscribe(CHANNEL); Messages.messageReceived.connect(receiveMessage); Users.avatarDisconnected.connect(avatarDisconnected); - GlobalServices.findableByChanged.connect(findableByChanged); } startup(); @@ -747,7 +728,6 @@ function onTabletButtonClicked() { onPalScreen = true; Users.requestsDomainListData = true; populateNearbyUserList(); - findableByChanged(GlobalServices.findableBy); isWired = true; Script.update.connect(updateOverlays); Controller.mousePressEvent.connect(handleMouseEvent); @@ -857,20 +837,6 @@ function avatarDisconnected(nodeID) { sendToQml({method: 'avatarDisconnected', params: [nodeID]}); } -function findableByChanged(usernameAvailability) { - // Update PAL availability dropdown - // Default to "friends" if undeterminable - var availability = 1; - if (usernameAvailability === "all") { - availability = 0; - } else if (usernameAvailability === "friends") { - availability = 1; - } else if (usernameAvailability === "none") { - availability = 2; - } - sendToQml({ method: 'updateAvailability', params: availability }); -} - function clearLocalQMLDataAndClosePAL() { sendToQml({ method: 'clearLocalQMLData' }); } @@ -888,7 +854,6 @@ function shutdown() { Messages.subscribe(CHANNEL); Messages.messageReceived.disconnect(receiveMessage); Users.avatarDisconnected.disconnect(avatarDisconnected); - GlobalServices.findableByChanged.disconnect(findableByChanged); off(); } From 2758e740bf98c6a8f9bf42fa995cb9d2071cdc22 Mon Sep 17 00:00:00 2001 From: David Kelly Date: Fri, 17 Mar 2017 08:45:24 -0700 Subject: [PATCH 022/118] hacked in a particle effect (as entity) plus popped sphere into existence --- scripts/system/makeUserConnection.js | 101 ++++++++++++++++++++++----- 1 file changed, 85 insertions(+), 16 deletions(-) diff --git a/scripts/system/makeUserConnection.js b/scripts/system/makeUserConnection.js index d68c328071..a05b43f047 100644 --- a/scripts/system/makeUserConnection.js +++ b/scripts/system/makeUserConnection.js @@ -10,6 +10,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // (function() { // BEGIN LOCAL_SCOPE + const version = 0.1; const label = "makeUserConnection"; const MAX_AVATAR_DISTANCE = 1.25; @@ -35,6 +36,45 @@ const TEXTURES = [ {"Texture": "http://hifi-content.s3.amazonaws.com/alan/dev/Test/sphere-3-color.fbx/sphere-3-color.fbm/blue-50pct-opaque-64.png"}, {"Texture": "http://hifi-content.s3.amazonaws.com/alan/dev/Test/sphere-3-color.fbx/sphere-3-color.fbm/red-50pct-opaque-64.png"} ]; +const PARTICLE_EFFECT_PROPS = { + "color": { + "blue": 0, + "green": 104, + "red": 0 + }, + "colorFinish": { + "blue": 172, + "green": 75, + "red": 255 + }, + "colorStart": { + "blue": 0, + "green": 104, + "red": 255 + }, + "dimensions": { + "x": 0.03, + "y": 0.03, + "z": 0.03 + }, + "emitOrientation": { + "w": 0.707, + "x": -0.707, + "y": 0.0, + "z": 0.0 + }, + "emitRate": 360, + "emitSpeed": 0.0003, + "emitterShouldTrail": 1, + "maxParticles": 1370, + "polarFinish": 1, + "radiusSpread": 9, + "radiusStart": 0.04, + "radiusFinish": 0.02, + "speedSpread": 0.09, + "textures": "http://hifi-content.s3.amazonaws.com/alan/dev/Particles/Bokeh-Particle.png", + "type": "ParticleEffect" +}; var currentHand; var state = STATES.inactive; @@ -47,6 +87,8 @@ var entityDimensionMultiplier = 1.0; var friendingId; var friendingHand; var waitingList = {}; +var particleEffect; +var waitingBallScale; function debug() { var stateString = "<" + STATE_STRINGS[state] + ">"; @@ -131,12 +173,23 @@ function shakeHandsAnimation(animationProperties) { return result; } +function positionFractionallyTowards(posA, posB, frac) { + return Vec3.sum(posA, Vec3.multiply(frac, Vec3.subtract(posB, posA))); +} + // this is called frequently, but usually does nothing function updateVisualization() { - if (state == STATES.inactive) { + if (state != STATES.waiting) { if (overlay) { overlay = Overlays.deleteOverlay(overlay); } + } + if (state == STATES.inactive || state == STATES.waiting) { + if (particleEffect) { + particleEffect = Entities.deleteEntity(particleEffect); + } + } + if (state == STATES.inactive) { return; } @@ -146,29 +199,45 @@ function updateVisualization() { // TODO: make the size scale with avatar, up to // the actual size of MAX_AVATAR_DISTANCE var wrist = MyAvatar.getJointPosition(MyAvatar.getJointIndex(handToString(currentHand))); - var d = entityDimensionMultiplier * Vec3.distance(wrist, position); + var d = Math.abs(entityDimensionMultiplier) * Vec3.distance(wrist, position); if (friendingId) { // put the position between the 2 hands, if we have a friendingId var other = AvatarList.getAvatar(friendingId); if (other) { var otherHand = getHandPosition(other, stringToHand(friendingHand)); - position = Vec3.sum(position, Vec3.multiply(0.5, Vec3.subtract(otherHand, position))); + position = positionFractionallyTowards(position, otherHand, entityDimensionMultiplier); } } - var dimension = {x: d, y: d, z: d}; - if (!overlay) { - var props = { - url: MODEL_URL, - position: position, - dimensions: dimension, - textures: textures - }; - overlay = Overlays.addOverlay("model", props); + if (state == STATES.waiting) { + var dimension = {x: d, y: d, z: d}; + if (!overlay) { + waitingBallScale = 1.0/32.0; + var props = { + url: MODEL_URL, + position: position, + dimensions: Vec3.multiply(waitingBallScale, dimension), + textures: textures + }; + overlay = Overlays.addOverlay("model", props); + } else { + waitingBallScale = Math.min(1.0, waitingBallScale * 1.1); + Overlays.editOverlay(overlay, {textures: textures}); + Overlays.editOverlay(overlay, {dimensions: Vec3.multiply(waitingBallScale, dimension), position: position}); + } } else { - Overlays.editOverlay(overlay, {textures: textures}); - Overlays.editOverlay(overlay, {dimensions: dimension, position: position}); + var particleProps = {}; + particleProps.position = position; + if (!particleEffect) { + particleProps = PARTICLE_EFFECT_PROPS; + particleEffect = Entities.addEntity(particleProps); + } else { + if (state == STATES.makingFriends) { + particleProps.colorFinish = {red: 0xFF, green: 0x00, blue: 0x00}; + particleProps.color = particleProps.colorFinish; + } + Entities.editEntity(particleEffect, particleProps); + } } - } function isNearby(id, hand) { @@ -376,7 +445,7 @@ function startFriending(id, hand) { friendingInterval = Script.setInterval(function () { count += 1; - entityDimensionMultiplier = 1.0 + 2.0 * count * FRIENDING_INTERVAL / FRIENDING_TIME; + entityDimensionMultiplier = Math.abs(Math.sin(Math.PI * 2 * 2 * count * FRIENDING_INTERVAL / FRIENDING_TIME)); if (state != STATES.friending) { debug("stopping friending interval, state changed"); stopFriending(); From c425c32b8f10e8ee8e398594760c632015416b12 Mon Sep 17 00:00:00 2001 From: David Kelly Date: Fri, 17 Mar 2017 14:56:47 -0700 Subject: [PATCH 023/118] hack somewhat different visualization in. If this is ok, will cleanup --- scripts/system/makeUserConnection.js | 50 ++++++++++++++++------------ 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/scripts/system/makeUserConnection.js b/scripts/system/makeUserConnection.js index a05b43f047..e23e2c2447 100644 --- a/scripts/system/makeUserConnection.js +++ b/scripts/system/makeUserConnection.js @@ -63,15 +63,17 @@ const PARTICLE_EFFECT_PROPS = { "y": 0.0, "z": 0.0 }, - "emitRate": 360, - "emitSpeed": 0.0003, + "emitRate": 100, + "emitSpeed": 0.001,//0.0003, "emitterShouldTrail": 1, - "maxParticles": 1370, - "polarFinish": 1, + "maxParticles": 25, + "polarStart" : -Math.PI/2, + "polarFinish": Math.PI/2, "radiusSpread": 9, "radiusStart": 0.04, "radiusFinish": 0.02, "speedSpread": 0.09, + "lifespan": 3.0, "textures": "http://hifi-content.s3.amazonaws.com/alan/dev/Particles/Bokeh-Particle.png", "type": "ParticleEffect" }; @@ -83,12 +85,12 @@ var waitingInterval; var makingFriendsTimeout; var overlay; var animHandlerId; -var entityDimensionMultiplier = 1.0; var friendingId; var friendingHand; var waitingList = {}; var particleEffect; var waitingBallScale; +var particleRotationAngle = 0.0; function debug() { var stateString = "<" + STATE_STRINGS[state] + ">"; @@ -179,42 +181,50 @@ function positionFractionallyTowards(posA, posB, frac) { // this is called frequently, but usually does nothing function updateVisualization() { - if (state != STATES.waiting) { + // after making friends, if you are still holding the grip lets transition + // back to waiting + if (state == STATES.makingFriends && !friendingId) { + startHandshake(); + } + if (state == STATES.friending) { if (overlay) { overlay = Overlays.deleteOverlay(overlay); } - } - if (state == STATES.inactive || state == STATES.waiting) { + } else { if (particleEffect) { particleEffect = Entities.deleteEntity(particleEffect); } } if (state == STATES.inactive) { + if (overlay) { + overlay = Overlays.deleteOverlay(overlay); + } return; } var textures = TEXTURES[state-1]; - var position = getHandPosition(MyAvatar, currentHand); + var myHandPosition = getHandPosition(MyAvatar, currentHand); + var position = myHandPosition; // TODO: make the size scale with avatar, up to // the actual size of MAX_AVATAR_DISTANCE var wrist = MyAvatar.getJointPosition(MyAvatar.getJointIndex(handToString(currentHand))); - var d = Math.abs(entityDimensionMultiplier) * Vec3.distance(wrist, position); + var d = Vec3.distance(wrist, position); if (friendingId) { // put the position between the 2 hands, if we have a friendingId var other = AvatarList.getAvatar(friendingId); if (other) { var otherHand = getHandPosition(other, stringToHand(friendingHand)); - position = positionFractionallyTowards(position, otherHand, entityDimensionMultiplier); + position = positionFractionallyTowards(position, otherHand, 0.5); } } - if (state == STATES.waiting) { + if (state != STATES.friending) { var dimension = {x: d, y: d, z: d}; if (!overlay) { - waitingBallScale = 1.0/32.0; + waitingBallScale = (state == STATES.waiting ? 1.0/32.0 : 1.0); var props = { url: MODEL_URL, - position: position, + position: myHandPosition, dimensions: Vec3.multiply(waitingBallScale, dimension), textures: textures }; @@ -222,19 +232,18 @@ function updateVisualization() { } else { waitingBallScale = Math.min(1.0, waitingBallScale * 1.1); Overlays.editOverlay(overlay, {textures: textures}); - Overlays.editOverlay(overlay, {dimensions: Vec3.multiply(waitingBallScale, dimension), position: position}); + Overlays.editOverlay(overlay, {dimensions: Vec3.multiply(waitingBallScale, dimension), position: myHandPosition}); } } else { var particleProps = {}; particleProps.position = position; if (!particleEffect) { + particleRotationAngle = 0.0; particleProps = PARTICLE_EFFECT_PROPS; particleEffect = Entities.addEntity(particleProps); } else { - if (state == STATES.makingFriends) { - particleProps.colorFinish = {red: 0xFF, green: 0x00, blue: 0x00}; - particleProps.color = particleProps.colorFinish; - } + particleRotationAngle += 360/45; // about 1 hz + particleProps.position = Vec3.sum(position, Vec3.multiplyQbyV(Quat.angleAxis(particleRotationAngle, Quat.getFront(MyAvatar.orientation)), {x:0, y:0.2, z:0})); Entities.editEntity(particleEffect, particleProps); } } @@ -288,7 +297,6 @@ function startHandshake(fromKeyboard) { } debug("starting handshake for", currentHand); state = STATES.waiting; - entityDimensionMultiplier = 1.0; friendingId = undefined; friendingHand = undefined; // just in case @@ -418,7 +426,6 @@ function makeFriends(id) { makingFriendsTimeout = Script.setTimeout(function () { friendingId = undefined; friendingHand = undefined; - entityDimensionMultiplier = 1.0; makingFriendsTimeout = undefined; }, MAKING_FRIENDS_TIMEOUT); } @@ -445,7 +452,6 @@ function startFriending(id, hand) { friendingInterval = Script.setInterval(function () { count += 1; - entityDimensionMultiplier = Math.abs(Math.sin(Math.PI * 2 * 2 * count * FRIENDING_INTERVAL / FRIENDING_TIME)); if (state != STATES.friending) { debug("stopping friending interval, state changed"); stopFriending(); From 81cb63df6ba32f26603f7b942fa9b949f314412f Mon Sep 17 00:00:00 2001 From: David Kelly Date: Mon, 20 Mar 2017 11:42:58 -0700 Subject: [PATCH 024/118] new particle effect, first pass --- scripts/system/makeUserConnection.js | 121 +++++++++++++-------------- 1 file changed, 57 insertions(+), 64 deletions(-) diff --git a/scripts/system/makeUserConnection.js b/scripts/system/makeUserConnection.js index e23e2c2447..b1bfe49529 100644 --- a/scripts/system/makeUserConnection.js +++ b/scripts/system/makeUserConnection.js @@ -13,7 +13,7 @@ const version = 0.1; const label = "makeUserConnection"; -const MAX_AVATAR_DISTANCE = 1.25; +const MAX_AVATAR_DISTANCE = 0.5; const GRIP_MIN = 0.05; const MESSAGE_CHANNEL = "io.highfidelity.makeUserConnection"; const STATES = { @@ -30,6 +30,8 @@ const FRIENDING_TIME = 2000; // ms const FRIENDING_HAPTIC_STRENGTH = 0.5; const FRIENDING_SUCCESS_HAPTIC_STRENGTH = 1.0; const HAPTIC_DURATION = 20; +const PARTICLE_RADIUS = 0.2; +const PARTICLE_ANGLE_INCREMENT = 360/45; const MODEL_URL = "http://hifi-content.s3.amazonaws.com/alan/dev/Test/sphere-3-color.fbx"; const TEXTURES = [ {"Texture": "http://hifi-content.s3.amazonaws.com/alan/dev/Test/sphere-3-color.fbx/sphere-3-color.fbm/green-50pct-opaque-64.png"}, @@ -37,44 +39,29 @@ const TEXTURES = [ {"Texture": "http://hifi-content.s3.amazonaws.com/alan/dev/Test/sphere-3-color.fbx/sphere-3-color.fbm/red-50pct-opaque-64.png"} ]; const PARTICLE_EFFECT_PROPS = { - "color": { - "blue": 0, - "green": 104, - "red": 0 - }, - "colorFinish": { - "blue": 172, - "green": 75, - "red": 255 - }, - "colorStart": { - "blue": 0, - "green": 104, - "red": 255 - }, - "dimensions": { - "x": 0.03, - "y": 0.03, - "z": 0.03 - }, - "emitOrientation": { - "w": 0.707, - "x": -0.707, - "y": 0.0, - "z": 0.0 - }, - "emitRate": 100, - "emitSpeed": 0.001,//0.0003, + "alpha": 0.8, + "azimuthFinish": Math.PI, + "azimuthStart": -1*Math.PI, + "emitRate": 220, + "emitSpeed": 0.0, "emitterShouldTrail": 1, - "maxParticles": 25, - "polarStart" : -Math.PI/2, - "polarFinish": Math.PI/2, - "radiusSpread": 9, - "radiusStart": 0.04, - "radiusFinish": 0.02, - "speedSpread": 0.09, - "lifespan": 3.0, + "isEmitting": 1, + "lifespan": 3, + "maxParticles": 1000, + "particleRadius": 0.003, + "polarStart": 1, + "polarFinish": 1, + "radiusFinish": 0.006, + "radiusStart": 0.001, + "speedSpread": 0.025, "textures": "http://hifi-content.s3.amazonaws.com/alan/dev/Particles/Bokeh-Particle.png", + "color": {"red": 255, "green": 255, "blue": 255}, + "colorFinish": {"red": 0, "green": 164, "blue": 255}, + "colorStart": {"red": 255, "green": 255, "blue": 255}, + "emitOrientation": {"w": -0.71, "x":0.0, "y":0.0, "z": 0.71}, + "emitAcceleration": {"x": 0.0, "y": 0.0, "z": 0.0}, + "accelerationSpread": {"x": 0.0, "y": 0.0, "z": 0.0}, + "dimensions": {"x":0.05, "y": 0.05, "z": 0.05}, "type": "ParticleEffect" }; @@ -179,47 +166,49 @@ function positionFractionallyTowards(posA, posB, frac) { return Vec3.sum(posA, Vec3.multiply(frac, Vec3.subtract(posB, posA))); } +function deleteOverlay() { + if (overlay) { + overlay = Overlays.deleteOverlay(overlay); + } +} + +function deleteParticleEffect() { + if (particleEffect) { + particleEffect = Entities.deleteEntity(particleEffect); + } +} + // this is called frequently, but usually does nothing function updateVisualization() { // after making friends, if you are still holding the grip lets transition // back to waiting if (state == STATES.makingFriends && !friendingId) { startHandshake(); - } - if (state == STATES.friending) { - if (overlay) { - overlay = Overlays.deleteOverlay(overlay); - } - } else { - if (particleEffect) { - particleEffect = Entities.deleteEntity(particleEffect); - } + return; } if (state == STATES.inactive) { - if (overlay) { - overlay = Overlays.deleteOverlay(overlay); - } + deleteOverlay(); + deleteParticleEffect(); return; } var textures = TEXTURES[state-1]; var myHandPosition = getHandPosition(MyAvatar, currentHand); var position = myHandPosition; - - // TODO: make the size scale with avatar, up to - // the actual size of MAX_AVATAR_DISTANCE - var wrist = MyAvatar.getJointPosition(MyAvatar.getJointIndex(handToString(currentHand))); - var d = Vec3.distance(wrist, position); + var otherHand; if (friendingId) { - // put the position between the 2 hands, if we have a friendingId var other = AvatarList.getAvatar(friendingId); if (other) { - var otherHand = getHandPosition(other, stringToHand(friendingHand)); - position = positionFractionallyTowards(position, otherHand, 0.5); + otherHand = getHandPosition(other, stringToHand(friendingHand)); } } + // scale the dimensions of the waiting/makingFriends ball to hand, capping + // at MAX_AVATAR_DISTANCE if someone happens to be huge + var wrist = MyAvatar.getJointPosition(MyAvatar.getJointIndex(handToString(currentHand))); + var d = Math.min(MAX_AVATAR_DISTANCE, Vec3.distance(wrist, position)); if (state != STATES.friending) { - var dimension = {x: d, y: d, z: d}; + deleteParticleEffect(); + var dimension = {x: d, y: d, z: d}; if (!overlay) { waitingBallScale = (state == STATES.waiting ? 1.0/32.0 : 1.0); var props = { @@ -232,18 +221,23 @@ function updateVisualization() { } else { waitingBallScale = Math.min(1.0, waitingBallScale * 1.1); Overlays.editOverlay(overlay, {textures: textures}); - Overlays.editOverlay(overlay, {dimensions: Vec3.multiply(waitingBallScale, dimension), position: myHandPosition}); + Overlays.editOverlay(overlay, {dimensions: Vec3.multiply(waitingBallScale, dimension), position: state == STATES.waiting ? myHandPosition : otherHand}); } } else { + deleteOverlay(); var particleProps = {}; + // put the position between the 2 hands, if we have a friendingId. This + // helps define the plane in which the particles move. + position = positionFractionallyTowards(position, otherHand, 0.5); particleProps.position = position; + // now manage the rest of the entity if (!particleEffect) { particleRotationAngle = 0.0; particleProps = PARTICLE_EFFECT_PROPS; particleEffect = Entities.addEntity(particleProps); } else { - particleRotationAngle += 360/45; // about 1 hz - particleProps.position = Vec3.sum(position, Vec3.multiplyQbyV(Quat.angleAxis(particleRotationAngle, Quat.getFront(MyAvatar.orientation)), {x:0, y:0.2, z:0})); + particleRotationAngle += PARTICLE_ANGLE_INCREMENT; // about 1 hz + particleProps.position = Vec3.sum(position, Vec3.multiplyQbyV(Quat.angleAxis(particleRotationAngle, Quat.getFront(MyAvatar.orientation)), {x:0, y:PARTICLE_RADIUS, z:0})); Entities.editEntity(particleEffect, particleProps); } } @@ -648,9 +642,8 @@ Script.scriptEnding.connect(function () { Controller.keyReleaseEvent.disconnect(keyReleaseEvent); debug("disconnecting updateVisualization"); Script.update.disconnect(updateVisualization); - if (overlay) { - overlay = Overlays.deleteOverlay(overlay); - } + deleteOverlay(); + deleteParticleEffect(); }); }()); // END LOCAL_SCOPE From 0506fc00333260cf3ce88e313b3f5f8a0a589917 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Mon, 20 Mar 2017 12:55:38 -0700 Subject: [PATCH 025/118] New goTo() behavior; Fix goBack --- interface/resources/qml/controls/WebView.qml | 2 ++ interface/resources/qml/hifi/NameCard.qml | 30 ++++++++++++++++---- interface/src/ui/overlays/Web3DOverlay.cpp | 3 ++ 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/interface/resources/qml/controls/WebView.qml b/interface/resources/qml/controls/WebView.qml index b67948d651..a3badd7e1f 100644 --- a/interface/resources/qml/controls/WebView.qml +++ b/interface/resources/qml/controls/WebView.qml @@ -8,6 +8,8 @@ Item { property alias url: root.url property alias scriptURL: root.userScriptUrl property alias eventBridge: eventBridgeWrapper.eventBridge + property alias canGoBack: root.canGoBack; + property var goBack: root.goBack; property bool keyboardEnabled: true // FIXME - Keyboard HMD only: Default to false property bool keyboardRaised: false property bool punctuationMode: false diff --git a/interface/resources/qml/hifi/NameCard.qml b/interface/resources/qml/hifi/NameCard.qml index ea490d248d..be4bafe9a6 100644 --- a/interface/resources/qml/hifi/NameCard.qml +++ b/interface/resources/qml/hifi/NameCard.qml @@ -16,6 +16,8 @@ import QtGraphicalEffects 1.0 import "../styles-uit" import "toolbars" +// references Users, UserActivityLogger, MyAvatar, Vec3, Quat, AddressManager from root context + Item { id: thisNameCard // Size @@ -44,7 +46,7 @@ Item { Item { id: avatarImage - visible: profileUrl !== ""; + visible: profileUrl !== "" && userName !== ""; // Size height: isMyCard ? 70 : 42; width: visible ? height : 0; @@ -237,8 +239,8 @@ Item { enabled: selected && pal.activeTab == "nearbyTab" && thisNameCard.userName !== ""; hoverEnabled: enabled onClicked: { - AddressManager.goToUser(thisNameCard.userName); - UserActivityLogger.palAction("go_to_user", thisNameCard.userName); + goToUserInDomain(thisNameCard.uuid); + UserActivityLogger.palAction("go_to_user_in_domain", thisNameCard.uuid); } onEntered: { displayNameText.color = hifi.colors.blueHighlight; @@ -330,8 +332,8 @@ Item { enabled: selected && pal.activeTab == "nearbyTab" && thisNameCard.userName !== ""; hoverEnabled: enabled onClicked: { - AddressManager.goToUser(thisNameCard.userName); - UserActivityLogger.palAction("go_to_user", thisNameCard.userName); + goToUserInDomain(thisNameCard.uuid); + UserActivityLogger.palAction("go_to_user_in_domain", thisNameCard.uuid); } onEntered: { displayNameText.color = hifi.colors.blueHighlight; @@ -488,4 +490,22 @@ Item { UserActivityLogger.palAction("avatar_gain_changed", avatarUuid); } } + + // Function body by Howard Stearns 2017-01-08 + function goToUserInDomain(avatarUuid) { + var avatar = AvatarList.getAvatar(avatarUuid); + if (!avatar) { + console.log("This avatar is no longer present. goToUserInDomain() failed."); + return; + } + var vector = Vec3.subtract(avatar.position, MyAvatar.position); + var distance = Vec3.length(vector); + var target = Vec3.multiply(Vec3.normalize(vector), distance - 2.0); + // FIXME: We would like the avatar to recompute the avatar's "maybe fly" test at the new position, so that if high enough up, + // the avatar goes into fly mode rather than falling. However, that is not exposed to Javascript right now. + // FIXME: it would be nice if this used the same teleport steps and smoothing as in the teleport.js script. + // Note, however, that this script allows teleporting to a person in the air, while teleport.js is going to a grounded target. + MyAvatar.orientation = Quat.lookAtSimple(MyAvatar.position, avatar.position); + MyAvatar.position = Vec3.sum(MyAvatar.position, target); + } } diff --git a/interface/src/ui/overlays/Web3DOverlay.cpp b/interface/src/ui/overlays/Web3DOverlay.cpp index 3db1aa65bb..ba864d2c5c 100644 --- a/interface/src/ui/overlays/Web3DOverlay.cpp +++ b/interface/src/ui/overlays/Web3DOverlay.cpp @@ -175,6 +175,9 @@ void Web3DOverlay::loadSourceURL() { _webSurface->getRootContext()->setContextProperty("fileDialogHelper", new FileDialogHelper()); _webSurface->getRootContext()->setContextProperty("MyAvatar", DependencyManager::get()->getMyAvatar().get()); _webSurface->getRootContext()->setContextProperty("GlobalServices", GlobalServicesScriptingInterface::getInstance()); + _webSurface->getRootContext()->setContextProperty("AvatarList", DependencyManager::get().data()); + _webSurface->getRootContext()->setContextProperty("Vec3", new Vec3()); + _webSurface->getRootContext()->setContextProperty("Quat", new Quat()); tabletScriptingInterface->setQmlTabletRoot("com.highfidelity.interface.tablet.system", _webSurface->getRootItem(), _webSurface.data()); // Override min fps for tablet UI, for silky smooth scrolling From 6c4b23195b1c2a59f2b8166819dd195af1966ecc Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Mon, 20 Mar 2017 13:26:31 -0700 Subject: [PATCH 026/118] One-line warning fix...I swear I've done this before --- interface/src/scripting/GlobalServicesScriptingInterface.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/scripting/GlobalServicesScriptingInterface.h b/interface/src/scripting/GlobalServicesScriptingInterface.h index 299815b3fd..8d8b78e149 100644 --- a/interface/src/scripting/GlobalServicesScriptingInterface.h +++ b/interface/src/scripting/GlobalServicesScriptingInterface.h @@ -36,7 +36,7 @@ class GlobalServicesScriptingInterface : public QObject { Q_OBJECT Q_PROPERTY(QString username READ getUsername) - Q_PROPERTY(QString findableBy READ getFindableBy WRITE setFindableBy) + Q_PROPERTY(QString findableBy READ getFindableBy WRITE setFindableBy NOTIFY findableByChanged) public: static GlobalServicesScriptingInterface* getInstance(); From d16acc5a7fe1fbe649cc468b7e9f340acf8c68f0 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Mon, 20 Mar 2017 15:19:28 -0700 Subject: [PATCH 027/118] Stale PAL Notification --- interface/resources/qml/hifi/Pal.qml | 10 +++++++++- scripts/system/pal.js | 24 +++++++++++++++++++++++- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index 8dfab8c62d..f941ff12b3 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -279,7 +279,9 @@ Rectangle { id: reloadNearby; width: reloadNearby.height; glyph: hifi.glyphs.reload; - onClicked: refreshNearbyWithFilter(); + onClicked: { + refreshNearbyWithFilter(); + } } } } @@ -1188,6 +1190,8 @@ Rectangle { } } sortModel(); + reloadNearby.glyph = hifi.glyphs.reload; + reloadNearby.color = 0; break; case 'connections': var data = message.params; @@ -1278,6 +1282,10 @@ Rectangle { var sessionID = message.params[0]; delete ignored[sessionID]; break; + case 'palIsStale': + reloadNearby.glyph = hifi.glyphs.alert; + reloadNearby.color = 2; + break; default: console.log('Unrecognized message:', JSON.stringify(message)); } diff --git a/scripts/system/pal.js b/scripts/system/pal.js index 014d89dde7..f0067e6843 100644 --- a/scripts/system/pal.js +++ b/scripts/system/pal.js @@ -14,7 +14,11 @@ (function() { // BEGIN LOCAL_SCOPE -var populateNearbyUserList, color, textures, removeOverlays, controllerComputePickRay, onTabletButtonClicked, onTabletScreenChanged, receiveMessage, avatarDisconnected, clearLocalQMLDataAndClosePAL, createAudioInterval, tablet, CHANNEL, getConnectionData, findableByChanged; // forward references; +var populateNearbyUserList, color, textures, removeOverlays, + controllerComputePickRay, onTabletButtonClicked, onTabletScreenChanged, + receiveMessage, avatarDisconnected, clearLocalQMLDataAndClosePAL, + createAudioInterval, tablet, CHANNEL, getConnectionData, findableByChanged, + avatarAdded, avatarRemoved, avatarSessionChanged; // forward references; // hardcoding these as it appears we cannot traverse the originalTextures in overlays??? Maybe I've missed // something, will revisit as this is sorta horrible. @@ -691,6 +695,9 @@ function startup() { Messages.subscribe(CHANNEL); Messages.messageReceived.connect(receiveMessage); Users.avatarDisconnected.connect(avatarDisconnected); + AvatarList.avatarAddedEvent.connect(avatarAdded); + AvatarList.avatarRemovedEvent.connect(avatarRemoved); + AvatarList.avatarSessionChangedEvent.connect(avatarSessionChanged); } startup(); @@ -841,6 +848,18 @@ function clearLocalQMLDataAndClosePAL() { sendToQml({ method: 'clearLocalQMLData' }); } +function avatarAdded() { + sendToQml({ method: 'palIsStale' }); +} + +function avatarRemoved() { + sendToQml({ method: 'palIsStale' }); +} + +function avatarSessionChanged() { + sendToQml({ method: 'palIsStale' }); +} + function shutdown() { if (onPalScreen) { tablet.gotoHomeScreen(); @@ -854,6 +873,9 @@ function shutdown() { Messages.subscribe(CHANNEL); Messages.messageReceived.disconnect(receiveMessage); Users.avatarDisconnected.disconnect(avatarDisconnected); + AvatarList.avatarAddedEvent.disconnect(avatarAdded); + AvatarList.avatarRemovedEvent.disconnect(avatarRemoved); + AvatarList.avatarSessionChangedEvent.disconnect(avatarSessionChanged); off(); } From 29e97381eba5d2ba3f3a6d0ed3a38d65e22577c1 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Mon, 20 Mar 2017 17:20:03 -0700 Subject: [PATCH 028/118] PAL open, user leaves, gray out row --- interface/resources/qml/hifi/NameCard.qml | 12 +++++---- interface/resources/qml/hifi/Pal.qml | 30 ++++++++++++++++------- scripts/system/pal.js | 7 +++--- 3 files changed, 32 insertions(+), 17 deletions(-) diff --git a/interface/resources/qml/hifi/NameCard.qml b/interface/resources/qml/hifi/NameCard.qml index be4bafe9a6..0ef86dd342 100644 --- a/interface/resources/qml/hifi/NameCard.qml +++ b/interface/resources/qml/hifi/NameCard.qml @@ -41,6 +41,7 @@ Item { property bool isMyCard: false property bool selected: false property bool isAdmin: false + property bool isPresent: true property string imageMaskColor: pal.color; property string profilePicBorderColor: (connectionStatus == "connection" ? hifi.colors.indigoAccent : (connectionStatus == "friend" ? hifi.colors.greenHighlight : imageMaskColor)) @@ -236,7 +237,7 @@ Item { color: hifi.colors.darkGray; MouseArea { anchors.fill: parent - enabled: selected && pal.activeTab == "nearbyTab" && thisNameCard.userName !== ""; + enabled: selected && pal.activeTab == "nearbyTab" && thisNameCard.userName !== "" && isPresent; hoverEnabled: enabled onClicked: { goToUserInDomain(thisNameCard.uuid); @@ -296,7 +297,8 @@ Item { } MouseArea { anchors.fill: parent - hoverEnabled: true + enabled: isPresent + hoverEnabled: enabled onClicked: letterbox(hifi.glyphs.question, "Domain Admin", "This user is an admin on this domain. Admins can Silence and Ban other users at their discretion - so be extra nice!") @@ -329,7 +331,7 @@ Item { color: hifi.colors.greenShadow; MouseArea { anchors.fill: parent - enabled: selected && pal.activeTab == "nearbyTab" && thisNameCard.userName !== ""; + enabled: selected && pal.activeTab == "nearbyTab" && thisNameCard.userName !== "" && isPresent; hoverEnabled: enabled onClicked: { goToUserInDomain(thisNameCard.uuid); @@ -358,7 +360,7 @@ Item { // Style radius: 4 color: "#c5c5c5" - visible: isMyCard || (selected && pal.activeTab == "nearbyTab") + visible: (isMyCard || (selected && pal.activeTab == "nearbyTab")) && isPresent // Rectangle for the zero-gain point on the VU meter Rectangle { id: vuMeterZeroGain @@ -433,7 +435,7 @@ Item { anchors.verticalCenter: nameCardVUMeter.verticalCenter; anchors.left: nameCardVUMeter.left; // Properties - visible: !isMyCard && selected && pal.activeTab == "nearbyTab"; + visible: !isMyCard && selected && pal.activeTab == "nearbyTab" && isPresent; value: Users.getAvatarGain(uuid) minimumValue: -60.0 maximumValue: 20.0 diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index f941ff12b3..b4b041587b 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -36,7 +36,7 @@ Rectangle { property int locationColumnWidth: 170; property int nearbyNameCardWidth: nearbyTable.width - (iAmAdmin ? (actionButtonWidth * 4) : (actionButtonWidth * 2)) - 4 - hifi.dimensions.scrollbarBackgroundWidth; property int connectionsNameCardWidth: connectionsTable.width - locationColumnWidth - actionButtonWidth - 4 - hifi.dimensions.scrollbarBackgroundWidth; - property var myData: ({profileUrl: "", displayName: "", userName: "", audioLevel: 0.0, avgAudioLevel: 0.0, admin: true, placeName: "", connection: ""}); // valid dummy until set + property var myData: ({profileUrl: "", displayName: "", userName: "", audioLevel: 0.0, avgAudioLevel: 0.0, admin: true, placeName: "", connection: "", isPresent: true}); // valid dummy until set property var ignored: ({}); // Keep a local list of ignored avatars & their data. Necessary because HashMap is slow to respond after ignoring. property var nearbyUserModelData: []; // This simple list is essentially a mirror of the nearbyUserModel listModel without all the extra complexities. property var connectionsUserModelData: []; // This simple list is essentially a mirror of the connectionsUserModel listModel without all the extra complexities. @@ -132,6 +132,7 @@ Rectangle { audioLevel: myData.audioLevel; avgAudioLevel: myData.avgAudioLevel; isMyCard: true; + isPresent: true; // Size width: myCardWidth; height: parent.height; @@ -576,8 +577,9 @@ Rectangle { // This Rectangle refers to each Row in the nearbyTable. rowDelegate: Rectangle { // The only way I know to specify a row height. // Size - height: styleData.selected ? rowHeight + 15 : rowHeight; - color: rowColor(styleData.selected, styleData.alternate); + height: rowHeight + (styleData.selected && model.isPresent ? 15 : 0); + color: rowColor(styleData.selected, styleData.alternate, model ? model.isPresent : true); + opacity: model.isPresent ? 1.0 : 0.6; } // This Item refers to the contents of each Cell @@ -586,6 +588,7 @@ Rectangle { property bool isCheckBox: styleData.role === "personalMute" || styleData.role === "ignore"; property bool isButton: styleData.role === "mute" || styleData.role === "kick"; property bool isAvgAudio: styleData.role === "avgAudioLevel"; + opacity: model.isPresent ? 1.0 : 0.6; // This NameCard refers to the cell that contains an avatar's // DisplayName and UserName @@ -593,7 +596,7 @@ Rectangle { id: nameCard; // Properties profileUrl: (model && model.profileUrl) || ""; - imageMaskColor: rowColor(styleData.selected, styleData.row % 2); + imageMaskColor: rowColor(styleData.selected, styleData.row % 2, model.isPresent); displayName: styleData.value; userName: model ? model.userName : ""; connectionStatus: model ? model.connection : ""; @@ -603,6 +606,7 @@ Rectangle { uuid: model ? model.sessionId : ""; selected: styleData.selected; isAdmin: model && model.admin; + isPresent: model && model.isPresent; // Size width: nearbyNameCardWidth; height: parent.height; @@ -899,7 +903,7 @@ Rectangle { rowDelegate: Rectangle { // Size height: rowHeight; - color: rowColor(styleData.selected, styleData.alternate); + color: rowColor(styleData.selected, styleData.alternate, model ? model.isPresent : true); } // This Item refers to the contents of each Cell @@ -912,7 +916,7 @@ Rectangle { // Properties visible: styleData.role === "userName"; profileUrl: (model && model.profileUrl) || ""; - imageMaskColor: rowColor(styleData.selected, styleData.row % 2); + imageMaskColor: rowColor(styleData.selected, styleData.row % 2, model.isPresent); displayName: ""; userName: model ? model.userName : ""; connectionStatus : model ? model.connection : ""; @@ -1155,8 +1159,8 @@ Rectangle { } } - function rowColor(selected, alternate) { - return selected ? hifi.colors.orangeHighlight : alternate ? hifi.colors.tableRowLightEven : hifi.colors.tableRowLightOdd; + function rowColor(selected, alternate, isPresent) { + return isPresent ? (selected ? hifi.colors.orangeHighlight : alternate ? hifi.colors.tableRowLightEven : hifi.colors.tableRowLightOdd) : hifi.colors.gray; } function findNearbySessionIndex(sessionId, optionalData) { // no findIndex in .qml var data = optionalData || nearbyUserModelData, length = data.length; @@ -1211,7 +1215,7 @@ Rectangle { } else if (userIndex < 0) { // If we've already refreshed the PAL and the avatar still isn't present in the model... if (alreadyRefreshed === true) { - letterbox('', '', 'The last editor of this object is either you or not among this list of users.'); + letterbox('', '', 'The user you attempted to select is no longer available.'); } else { pal.sendToScript({method: 'refresh', params: {selected: message.params}}); } @@ -1283,6 +1287,14 @@ Rectangle { delete ignored[sessionID]; break; case 'palIsStale': + if (message.params) { + var sessionID = message.params[0]; + var userIndex = findNearbySessionIndex(sessionID); + if (userIndex != -1) { + nearbyUserModel.setProperty(userIndex, "isPresent", false); + nearbyUserModelData[userIndex].isPresent = false; + } + } reloadNearby.glyph = hifi.glyphs.alert; reloadNearby.color = 2; break; diff --git a/scripts/system/pal.js b/scripts/system/pal.js index f0067e6843..95e208d126 100644 --- a/scripts/system/pal.js +++ b/scripts/system/pal.js @@ -479,7 +479,8 @@ function populateNearbyUserList(selectData, oldAudioData) { avgAudioLevel: (oldAudio && oldAudio.avgAudioLevel) || 0.0, admin: false, personalMute: !!id && Users.getPersonalMuteStatus(id), // expects proper boolean, not null - ignore: !!id && Users.getIgnoreStatus(id) // ditto + ignore: !!id && Users.getIgnoreStatus(id), // ditto + isPresent: true }; if (id) { addAvatarNode(id); // No overlay for ourselves @@ -852,8 +853,8 @@ function avatarAdded() { sendToQml({ method: 'palIsStale' }); } -function avatarRemoved() { - sendToQml({ method: 'palIsStale' }); +function avatarRemoved(avatarID) { + sendToQml({ method: 'palIsStale', params: [avatarID] }); } function avatarSessionChanged() { From 058faa8c0f54474995d621cc69f2285cf46f77b8 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Tue, 21 Mar 2017 10:03:22 -0700 Subject: [PATCH 029/118] Row color bugfix --- interface/resources/qml/hifi/Pal.qml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index b4b041587b..b52dce5841 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -596,7 +596,7 @@ Rectangle { id: nameCard; // Properties profileUrl: (model && model.profileUrl) || ""; - imageMaskColor: rowColor(styleData.selected, styleData.row % 2, model.isPresent); + imageMaskColor: rowColor(styleData.selected, styleData.row % 2, model ? model.isPresent : false); displayName: styleData.value; userName: model ? model.userName : ""; connectionStatus: model ? model.connection : ""; @@ -903,7 +903,7 @@ Rectangle { rowDelegate: Rectangle { // Size height: rowHeight; - color: rowColor(styleData.selected, styleData.alternate, model ? model.isPresent : true); + color: rowColor(styleData.selected, styleData.alternate, true); } // This Item refers to the contents of each Cell @@ -916,7 +916,7 @@ Rectangle { // Properties visible: styleData.role === "userName"; profileUrl: (model && model.profileUrl) || ""; - imageMaskColor: rowColor(styleData.selected, styleData.row % 2, model.isPresent); + imageMaskColor: rowColor(styleData.selected, styleData.row % 2, true); displayName: ""; userName: model ? model.userName : ""; connectionStatus : model ? model.connection : ""; From 78f1acfd622cd0f2044cf2c830618b549e4321ce Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Tue, 21 Mar 2017 10:45:51 -0700 Subject: [PATCH 030/118] Don't crash when ignoring not-present node --- assignment-client/src/avatars/AvatarMixer.cpp | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index d2bfdde7ea..9ea2ea3f10 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -437,17 +437,20 @@ void AvatarMixer::handleNodeIgnoreRequestPacket(QSharedPointer while (message->getBytesLeftToRead()) { // parse out the UUID being ignored from the packet QUuid ignoredUUID = QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID)); - // Reset the lastBroadcastTime for the ignored avatar to 0 - // so the AvatarMixer knows it'll have to send identity data about the ignored avatar - // to the ignorer if the ignorer unignores. - nodeData->setLastBroadcastTime(ignoredUUID, 0); - // Reset the lastBroadcastTime for the ignorer (FROM THE PERSPECTIVE OF THE IGNORED) to 0 - // so the AvatarMixer knows it'll have to send identity data about the ignorer - // to the ignored if the ignorer unignores. - auto ignoredNode = nodeList->nodeWithUUID(ignoredUUID); - AvatarMixerClientData* ignoredNodeData = reinterpret_cast(ignoredNode->getLinkedData()); - ignoredNodeData->setLastBroadcastTime(senderNode->getUUID(), 0); + if (nodeList->nodeWithUUID(ignoredUUID)) { + // Reset the lastBroadcastTime for the ignored avatar to 0 + // so the AvatarMixer knows it'll have to send identity data about the ignored avatar + // to the ignorer if the ignorer unignores. + nodeData->setLastBroadcastTime(ignoredUUID, 0); + + // Reset the lastBroadcastTime for the ignorer (FROM THE PERSPECTIVE OF THE IGNORED) to 0 + // so the AvatarMixer knows it'll have to send identity data about the ignorer + // to the ignored if the ignorer unignores. + auto ignoredNode = nodeList->nodeWithUUID(ignoredUUID); + AvatarMixerClientData* ignoredNodeData = reinterpret_cast(ignoredNode->getLinkedData()); + ignoredNodeData->setLastBroadcastTime(senderNode->getUUID(), 0); + } if (addToIgnore) { senderNode->addIgnoredNode(ignoredUUID); From 5bcd7ca97f4ab2fa69aacc41766c89b0b1537877 Mon Sep 17 00:00:00 2001 From: David Kelly Date: Tue, 21 Mar 2017 13:27:00 -0700 Subject: [PATCH 031/118] visuals update. Will do some cleaning shortly --- scripts/system/makeUserConnection.js | 167 ++++++++++++++++++++------- 1 file changed, 124 insertions(+), 43 deletions(-) diff --git a/scripts/system/makeUserConnection.js b/scripts/system/makeUserConnection.js index b1bfe49529..fa3b0935c9 100644 --- a/scripts/system/makeUserConnection.js +++ b/scripts/system/makeUserConnection.js @@ -13,7 +13,7 @@ const version = 0.1; const label = "makeUserConnection"; -const MAX_AVATAR_DISTANCE = 0.5; +const MAX_AVATAR_DISTANCE = 0.2; const GRIP_MIN = 0.05; const MESSAGE_CHANNEL = "io.highfidelity.makeUserConnection"; const STATES = { @@ -25,13 +25,13 @@ const STATES = { const STATE_STRINGS = ["inactive", "waiting", "friending", "makingFriends"]; const WAITING_INTERVAL = 100; // ms const FRIENDING_INTERVAL = 100; // ms -const MAKING_FRIENDS_TIMEOUT = 1000; // ms +const MAKING_FRIENDS_TIMEOUT = 3000; // ms const FRIENDING_TIME = 2000; // ms const FRIENDING_HAPTIC_STRENGTH = 0.5; const FRIENDING_SUCCESS_HAPTIC_STRENGTH = 1.0; const HAPTIC_DURATION = 20; -const PARTICLE_RADIUS = 0.2; -const PARTICLE_ANGLE_INCREMENT = 360/45; +const PARTICLE_RADIUS = 0.1; +const PARTICLE_ANGLE_INCREMENT = 360/45; // 1hz const MODEL_URL = "http://hifi-content.s3.amazonaws.com/alan/dev/Test/sphere-3-color.fbx"; const TEXTURES = [ {"Texture": "http://hifi-content.s3.amazonaws.com/alan/dev/Test/sphere-3-color.fbx/sphere-3-color.fbm/green-50pct-opaque-64.png"}, @@ -42,7 +42,7 @@ const PARTICLE_EFFECT_PROPS = { "alpha": 0.8, "azimuthFinish": Math.PI, "azimuthStart": -1*Math.PI, - "emitRate": 220, + "emitRate": 500, "emitSpeed": 0.0, "emitterShouldTrail": 1, "isEmitting": 1, @@ -51,8 +51,8 @@ const PARTICLE_EFFECT_PROPS = { "particleRadius": 0.003, "polarStart": 1, "polarFinish": 1, - "radiusFinish": 0.006, - "radiusStart": 0.001, + "radiusFinish": 0.008, + "radiusStart": 0.0025, "speedSpread": 0.025, "textures": "http://hifi-content.s3.amazonaws.com/alan/dev/Particles/Bokeh-Particle.png", "color": {"red": 255, "green": 255, "blue": 255}, @@ -64,6 +64,36 @@ const PARTICLE_EFFECT_PROPS = { "dimensions": {"x":0.05, "y": 0.05, "z": 0.05}, "type": "ParticleEffect" }; +const MAKING_FRIENDS_PARTICLE_PROPS = { + "alpha": 0.07, + "alphaStart":0.011, + "alphaSpread": 0, + "alphaFinish": 0, + "azimuthFinish": Math.PI, + "azimuthStart": -1*Math.PI, + "emitRate": 2000, + "emitSpeed": 0.0, + "emitterShouldTrail": 1, + "isEmitting": 1, + "lifespan": 3.6, + "maxParticles": 4000, + "particleRadius": 0.048, + "polarStart": 0, + "polarFinish": 1, + "radiusFinish": 0.2, + "radiusStart": 0.04, + "speedSpread": 0.01, + "radiusSpread": 0.9, + "textures": "http://hifi-content.s3.amazonaws.com/alan/dev/Particles/Bokeh-Particle.png", + "color": {"red": 200, "green": 170, "blue": 255}, + "colorFinish": {"red": 0, "green": 134, "blue": 255}, + "colorStart": {"red": 185, "green": 222, "blue": 255}, + "emitOrientation": {"w": -0.71, "x":0.0, "y":0.0, "z": 0.71}, + "emitAcceleration": {"x": 0.0, "y": 0.0, "z": 0.0}, + "accelerationSpread": {"x": 0.0, "y": 0.0, "z": 0.0}, + "dimensions": {"x":0.05, "y": 0.05, "z": 0.05}, + "type": "ParticleEffect" +}; var currentHand; var state = STATES.inactive; @@ -78,6 +108,9 @@ var waitingList = {}; var particleEffect; var waitingBallScale; var particleRotationAngle = 0.0; +var makingFriendsParticleEffect; +var makingFriendsEmitRate = 2000; +var particleEmitRate = 500; function debug() { var stateString = "<" + STATE_STRINGS[state] + ">"; @@ -178,6 +211,17 @@ function deleteParticleEffect() { } } +function calcParticlePos(myHand, otherHand, otherOrientation, reset) { + if (reset) { + particleRotationAngle = 0.0; + } + var position = positionFractionallyTowards(myHand, otherHand, 0.5); + particleRotationAngle += PARTICLE_ANGLE_INCREMENT; // about 0.5 hz + var radius = Math.min(PARTICLE_RADIUS, PARTICLE_RADIUS * particleRotationAngle / 720); + var axis = Vec3.mix(Quat.getFront(MyAvatar.orientation), Quat.inverse(Quat.getFront(otherOrientation)), 0.5); + return Vec3.sum(position, Vec3.multiplyQbyV(Quat.angleAxis(particleRotationAngle, axis), {x: 0, y: radius, z: 0})); +} + // this is called frequently, but usually does nothing function updateVisualization() { // after making friends, if you are still holding the grip lets transition @@ -189,57 +233,94 @@ function updateVisualization() { if (state == STATES.inactive) { deleteOverlay(); deleteParticleEffect(); + if (makingFriendsParticleEffect) { + makingFriendsParticleEffect = Entities.deleteEntity(makingFriendsParticleEffect); + } return; } var textures = TEXTURES[state-1]; var myHandPosition = getHandPosition(MyAvatar, currentHand); - var position = myHandPosition; var otherHand; + var otherOrientation; if (friendingId) { var other = AvatarList.getAvatar(friendingId); if (other) { + otherOrientation = other.orientation; otherHand = getHandPosition(other, stringToHand(friendingHand)); } } // scale the dimensions of the waiting/makingFriends ball to hand, capping // at MAX_AVATAR_DISTANCE if someone happens to be huge var wrist = MyAvatar.getJointPosition(MyAvatar.getJointIndex(handToString(currentHand))); - var d = Math.min(MAX_AVATAR_DISTANCE, Vec3.distance(wrist, position)); - if (state != STATES.friending) { - deleteParticleEffect(); - var dimension = {x: d, y: d, z: d}; - if (!overlay) { - waitingBallScale = (state == STATES.waiting ? 1.0/32.0 : 1.0); - var props = { - url: MODEL_URL, - position: myHandPosition, - dimensions: Vec3.multiply(waitingBallScale, dimension), - textures: textures - }; - overlay = Overlays.addOverlay("model", props); - } else { - waitingBallScale = Math.min(1.0, waitingBallScale * 1.1); - Overlays.editOverlay(overlay, {textures: textures}); - Overlays.editOverlay(overlay, {dimensions: Vec3.multiply(waitingBallScale, dimension), position: state == STATES.waiting ? myHandPosition : otherHand}); - } - } else { - deleteOverlay(); - var particleProps = {}; - // put the position between the 2 hands, if we have a friendingId. This - // helps define the plane in which the particles move. - position = positionFractionallyTowards(position, otherHand, 0.5); - particleProps.position = position; - // now manage the rest of the entity - if (!particleEffect) { - particleRotationAngle = 0.0; - particleProps = PARTICLE_EFFECT_PROPS; - particleEffect = Entities.addEntity(particleProps); - } else { - particleRotationAngle += PARTICLE_ANGLE_INCREMENT; // about 1 hz - particleProps.position = Vec3.sum(position, Vec3.multiplyQbyV(Quat.angleAxis(particleRotationAngle, Quat.getFront(MyAvatar.orientation)), {x:0, y:PARTICLE_RADIUS, z:0})); - Entities.editEntity(particleEffect, particleProps); - } + var d = Math.min(MAX_AVATAR_DISTANCE, Vec3.distance(wrist, myHandPosition)); + switch (state) { + case STATES.waiting: + deleteParticleEffect(); + if (makingFriendsParticleEffect) { + makingFriendsParticleEffect = Entities.deleteEntity(makingFriendsParticleEffect); + } + var dimension = {x: d, y: d, z: d}; + if (!overlay) { + waitingBallScale = (state == STATES.waiting ? 1.0/32.0 : 1.0); + var props = { + url: MODEL_URL, + position: myHandPosition, + dimensions: Vec3.multiply(waitingBallScale, dimension), + textures: textures + }; + overlay = Overlays.addOverlay("model", props); + } else { + waitingBallScale = Math.min(1.0, waitingBallScale * 1.1); + Overlays.editOverlay(overlay, {textures: textures}); + Overlays.editOverlay(overlay, {dimensions: Vec3.multiply(waitingBallScale, dimension), position: myHandPosition}); + } + break; + case STATES.friending: + deleteOverlay(); + var particleProps = {}; + // put the position between the 2 hands, if we have a friendingId. This + // helps define the plane in which the particles move. + positionFractionallyTowards(myHandPosition, otherHand, 0.5); + // now manage the rest of the entity + if (!particleEffect) { + particleRotationAngle = 0.0; + var position = calcParticlePos(myHandPosition, otherHand, otherOrientation); + particleProps = PARTICLE_EFFECT_PROPS; + particleProps.isEmitting = 0; + particleProps.position = position; + particleEffect = Entities.addEntity(particleProps); + } else { + var position = calcParticlePos(myHandPosition, otherHand, otherOrientation); + particleProps.position = position; //Vec3.sum(position, Vec3.multiplyQbyV(Quat.angleAxis(particleRotationAngle, axis), {x: 0, y: radius, z: 0})); + //particleProps.position = Vec3.sum(position, Vec3.multiplyQbyV(Quat.angleAxis(particleRotationAngle, Quat.getFront(MyAvatar.orientation)),{x: 0, y: radius, z: 0})); + particleProps.isEmitting = 1; + /*if (particleRotationAngle > 0.9 * 720) { + particleProps.lifespan = 6; + particleProps.isEmitting = 0; + }*/ + Entities.editEntity(particleEffect, particleProps); + } + if (!makingFriendsParticleEffect) { + props = MAKING_FRIENDS_PARTICLE_PROPS; + makingFriendsEmitRate = 2000; + props.emitRate = makingFriendsEmitRate; + props.position = myHandPosition; + makingFriendsParticleEffect = Entities.addEntity(props); + } else { + makingFriendsEmitRate *= 0.5; + Entities.editEntity(makingFriendsParticleEffect, {emitRate: makingFriendsEmitRate, position: myHandPosition, isEmitting: 1}); + } + break; + case STATES.makingFriends: + particleEmitRate *= 0.5; + Entities.editEntity(makingFriendsParticleEffect, {emitRate: 0, isEmitting: 0, position: myHandPosition}); + var pos = calcParticlePos(myHandPosition, otherHand, otherOrientation); + Entities.editEntity(particleEffect, {position: position, emitRate: particleEmitRate}); + break; + default: + debug("unexpected state", state); + break; } } From 59b02261e57a7f8f128ed7a105fb401cb7004057 Mon Sep 17 00:00:00 2001 From: David Kelly Date: Tue, 21 Mar 2017 16:08:25 -0700 Subject: [PATCH 032/118] minor cleanup, remove waiting visualization completely --- scripts/system/makeUserConnection.js | 48 ++++++---------------------- 1 file changed, 10 insertions(+), 38 deletions(-) diff --git a/scripts/system/makeUserConnection.js b/scripts/system/makeUserConnection.js index fa3b0935c9..f8c5912886 100644 --- a/scripts/system/makeUserConnection.js +++ b/scripts/system/makeUserConnection.js @@ -32,12 +32,6 @@ const FRIENDING_SUCCESS_HAPTIC_STRENGTH = 1.0; const HAPTIC_DURATION = 20; const PARTICLE_RADIUS = 0.1; const PARTICLE_ANGLE_INCREMENT = 360/45; // 1hz -const MODEL_URL = "http://hifi-content.s3.amazonaws.com/alan/dev/Test/sphere-3-color.fbx"; -const TEXTURES = [ - {"Texture": "http://hifi-content.s3.amazonaws.com/alan/dev/Test/sphere-3-color.fbx/sphere-3-color.fbm/green-50pct-opaque-64.png"}, - {"Texture": "http://hifi-content.s3.amazonaws.com/alan/dev/Test/sphere-3-color.fbx/sphere-3-color.fbm/blue-50pct-opaque-64.png"}, - {"Texture": "http://hifi-content.s3.amazonaws.com/alan/dev/Test/sphere-3-color.fbx/sphere-3-color.fbm/red-50pct-opaque-64.png"} -]; const PARTICLE_EFFECT_PROPS = { "alpha": 0.8, "azimuthFinish": Math.PI, @@ -100,7 +94,6 @@ var state = STATES.inactive; var friendingInterval; var waitingInterval; var makingFriendsTimeout; -var overlay; var animHandlerId; var friendingId; var friendingHand; @@ -199,18 +192,18 @@ function positionFractionallyTowards(posA, posB, frac) { return Vec3.sum(posA, Vec3.multiply(frac, Vec3.subtract(posB, posA))); } -function deleteOverlay() { - if (overlay) { - overlay = Overlays.deleteOverlay(overlay); - } -} - function deleteParticleEffect() { if (particleEffect) { particleEffect = Entities.deleteEntity(particleEffect); } } +function deleteMakeFriendsParticleEffect() { + if (makingFriendsParticleEffect) { + makingFriendsParticleEffect = Entities.deleteEntity(makingFriendsParticleEffect); + } +} + function calcParticlePos(myHand, otherHand, otherOrientation, reset) { if (reset) { particleRotationAngle = 0.0; @@ -231,15 +224,11 @@ function updateVisualization() { return; } if (state == STATES.inactive) { - deleteOverlay(); deleteParticleEffect(); - if (makingFriendsParticleEffect) { - makingFriendsParticleEffect = Entities.deleteEntity(makingFriendsParticleEffect); - } + deleteMakeFriendsParticleEffect(); return; } - var textures = TEXTURES[state-1]; var myHandPosition = getHandPosition(MyAvatar, currentHand); var otherHand; var otherOrientation; @@ -256,28 +245,11 @@ function updateVisualization() { var d = Math.min(MAX_AVATAR_DISTANCE, Vec3.distance(wrist, myHandPosition)); switch (state) { case STATES.waiting: + // no visualization while waiting deleteParticleEffect(); - if (makingFriendsParticleEffect) { - makingFriendsParticleEffect = Entities.deleteEntity(makingFriendsParticleEffect); - } - var dimension = {x: d, y: d, z: d}; - if (!overlay) { - waitingBallScale = (state == STATES.waiting ? 1.0/32.0 : 1.0); - var props = { - url: MODEL_URL, - position: myHandPosition, - dimensions: Vec3.multiply(waitingBallScale, dimension), - textures: textures - }; - overlay = Overlays.addOverlay("model", props); - } else { - waitingBallScale = Math.min(1.0, waitingBallScale * 1.1); - Overlays.editOverlay(overlay, {textures: textures}); - Overlays.editOverlay(overlay, {dimensions: Vec3.multiply(waitingBallScale, dimension), position: myHandPosition}); - } + deleteMakeFriendsParticleEffect(); break; case STATES.friending: - deleteOverlay(); var particleProps = {}; // put the position between the 2 hands, if we have a friendingId. This // helps define the plane in which the particles move. @@ -723,8 +695,8 @@ Script.scriptEnding.connect(function () { Controller.keyReleaseEvent.disconnect(keyReleaseEvent); debug("disconnecting updateVisualization"); Script.update.disconnect(updateVisualization); - deleteOverlay(); deleteParticleEffect(); + deleteMakeFriendsParticleEffect(); }); }()); // END LOCAL_SCOPE From a594419688471c09224ba6adfb88dd73b81fb80e Mon Sep 17 00:00:00 2001 From: David Kelly Date: Wed, 22 Mar 2017 10:09:48 -0700 Subject: [PATCH 033/118] friend->connection which I should have done long ago --- scripts/system/makeUserConnection.js | 312 +++++++++++++-------------- 1 file changed, 153 insertions(+), 159 deletions(-) diff --git a/scripts/system/makeUserConnection.js b/scripts/system/makeUserConnection.js index f8c5912886..621fce93af 100644 --- a/scripts/system/makeUserConnection.js +++ b/scripts/system/makeUserConnection.js @@ -19,16 +19,16 @@ const MESSAGE_CHANNEL = "io.highfidelity.makeUserConnection"; const STATES = { inactive : 0, waiting: 1, - friending: 2, - makingFriends: 3 + connecting: 2, + makingConnection: 3 }; -const STATE_STRINGS = ["inactive", "waiting", "friending", "makingFriends"]; +const STATE_STRINGS = ["inactive", "waiting", "connecting", "makingConnection"]; const WAITING_INTERVAL = 100; // ms -const FRIENDING_INTERVAL = 100; // ms -const MAKING_FRIENDS_TIMEOUT = 3000; // ms -const FRIENDING_TIME = 2000; // ms -const FRIENDING_HAPTIC_STRENGTH = 0.5; -const FRIENDING_SUCCESS_HAPTIC_STRENGTH = 1.0; +const CONNECTING_INTERVAL = 100; // ms +const MAKING_CONNECTION_TIMEOUT = 3000; // ms +const CONNECTING_TIME = 2000; // ms +const CONNECTING_HAPTIC_STRENGTH = 0.5; +const CONNECTING_SUCCESS_HAPTIC_STRENGTH = 1.0; const HAPTIC_DURATION = 20; const PARTICLE_RADIUS = 0.1; const PARTICLE_ANGLE_INCREMENT = 360/45; // 1hz @@ -58,7 +58,7 @@ const PARTICLE_EFFECT_PROPS = { "dimensions": {"x":0.05, "y": 0.05, "z": 0.05}, "type": "ParticleEffect" }; -const MAKING_FRIENDS_PARTICLE_PROPS = { +const MAKING_CONNECTION_PARTICLE_PROPS = { "alpha": 0.07, "alphaStart":0.011, "alphaSpread": 0, @@ -91,25 +91,25 @@ const MAKING_FRIENDS_PARTICLE_PROPS = { var currentHand; var state = STATES.inactive; -var friendingInterval; +var connectingInterval; var waitingInterval; -var makingFriendsTimeout; +var makingConnectionTimeout; var animHandlerId; -var friendingId; -var friendingHand; +var connectingId; +var connectingHand; var waitingList = {}; var particleEffect; var waitingBallScale; var particleRotationAngle = 0.0; -var makingFriendsParticleEffect; -var makingFriendsEmitRate = 2000; +var makingConnectionParticleEffect; +var makingConnectionEmitRate = 2000; var particleEmitRate = 500; function debug() { var stateString = "<" + STATE_STRINGS[state] + ">"; var versionString = "v" + version; - var friending = "[" + friendingId + "/" + friendingHand + "]"; - print.apply(null, [].concat.apply([label, versionString, stateString, JSON.stringify(waitingList), friending], [].map.call(arguments, JSON.stringify))); + var connecting = "[" + connectingId + "/" + connectingHand + "]"; + print.apply(null, [].concat.apply([label, versionString, stateString, JSON.stringify(waitingList), connecting], [].map.call(arguments, JSON.stringify))); } function handToString(hand) { @@ -146,15 +146,15 @@ function stopWaiting() { } } -function stopFriending() { - if (friendingInterval) { - friendingInterval = Script.clearInterval(friendingInterval); +function stopConnecting() { + if (connectingInterval) { + connectingInterval = Script.clearInterval(connectingInterval); } } -function stopMakingFriends() { - if (makingFriendsTimeout) { - makingFriendsTimeout = Script.clearTimeout(makingFriendsTimeout); +function stopMakingConnection() { + if (makingConnectionTimeout) { + makingConnectionTimeout = Script.clearTimeout(makingConnectionTimeout); } } @@ -198,9 +198,9 @@ function deleteParticleEffect() { } } -function deleteMakeFriendsParticleEffect() { - if (makingFriendsParticleEffect) { - makingFriendsParticleEffect = Entities.deleteEntity(makingFriendsParticleEffect); +function deleteMakeConnectionParticleEffect() { + if (makingConnectionParticleEffect) { + makingConnectionParticleEffect = Entities.deleteEntity(makingConnectionParticleEffect); } } @@ -217,41 +217,40 @@ function calcParticlePos(myHand, otherHand, otherOrientation, reset) { // this is called frequently, but usually does nothing function updateVisualization() { - // after making friends, if you are still holding the grip lets transition + // after making connection, if you are still holding the grip lets transition // back to waiting - if (state == STATES.makingFriends && !friendingId) { + if (state == STATES.makingConnection && !connectingId) { startHandshake(); return; } if (state == STATES.inactive) { deleteParticleEffect(); - deleteMakeFriendsParticleEffect(); + deleteMakeConnectionParticleEffect(); return; } var myHandPosition = getHandPosition(MyAvatar, currentHand); var otherHand; var otherOrientation; - if (friendingId) { - var other = AvatarList.getAvatar(friendingId); + if (connectingId) { + var other = AvatarList.getAvatar(connectingId); if (other) { otherOrientation = other.orientation; - otherHand = getHandPosition(other, stringToHand(friendingHand)); + otherHand = getHandPosition(other, stringToHand(connectingHand)); } } - // scale the dimensions of the waiting/makingFriends ball to hand, capping - // at MAX_AVATAR_DISTANCE if someone happens to be huge + var wrist = MyAvatar.getJointPosition(MyAvatar.getJointIndex(handToString(currentHand))); var d = Math.min(MAX_AVATAR_DISTANCE, Vec3.distance(wrist, myHandPosition)); switch (state) { case STATES.waiting: // no visualization while waiting deleteParticleEffect(); - deleteMakeFriendsParticleEffect(); + deleteMakeConnectionParticleEffect(); break; - case STATES.friending: + case STATES.connecting: var particleProps = {}; - // put the position between the 2 hands, if we have a friendingId. This + // put the position between the 2 hands, if we have a ingId. This // helps define the plane in which the particles move. positionFractionallyTowards(myHandPosition, otherHand, 0.5); // now manage the rest of the entity @@ -264,29 +263,24 @@ function updateVisualization() { particleEffect = Entities.addEntity(particleProps); } else { var position = calcParticlePos(myHandPosition, otherHand, otherOrientation); - particleProps.position = position; //Vec3.sum(position, Vec3.multiplyQbyV(Quat.angleAxis(particleRotationAngle, axis), {x: 0, y: radius, z: 0})); - //particleProps.position = Vec3.sum(position, Vec3.multiplyQbyV(Quat.angleAxis(particleRotationAngle, Quat.getFront(MyAvatar.orientation)),{x: 0, y: radius, z: 0})); + particleProps.position = position; particleProps.isEmitting = 1; - /*if (particleRotationAngle > 0.9 * 720) { - particleProps.lifespan = 6; - particleProps.isEmitting = 0; - }*/ Entities.editEntity(particleEffect, particleProps); } - if (!makingFriendsParticleEffect) { - props = MAKING_FRIENDS_PARTICLE_PROPS; - makingFriendsEmitRate = 2000; - props.emitRate = makingFriendsEmitRate; + if (!makingConnectionParticleEffect) { + props = MAKING_CONNECTION_PARTICLE_PROPS; + makingConnectionEmitRate = 2000; + props.emitRate = makingConnectionEmitRate; props.position = myHandPosition; - makingFriendsParticleEffect = Entities.addEntity(props); + makingConnectionParticleEffect = Entities.addEntity(props); } else { - makingFriendsEmitRate *= 0.5; - Entities.editEntity(makingFriendsParticleEffect, {emitRate: makingFriendsEmitRate, position: myHandPosition, isEmitting: 1}); + makingConnectionEmitRate *= 0.5; + Entities.editEntity(makingConnectionParticleEffect, {emitRate: makingConnectionEmitRate, position: myHandPosition, isEmitting: 1}); } break; - case STATES.makingFriends: + case STATES.makingConnection: particleEmitRate *= 0.5; - Entities.editEntity(makingFriendsParticleEffect, {emitRate: 0, isEmitting: 0, position: myHandPosition}); + Entities.editEntity(makingConnectionParticleEffect, {emitRate: 0, isEmitting: 0, position: myHandPosition}); var pos = calcParticlePos(myHandPosition, otherHand, otherOrientation); Entities.editEntity(particleEffect, {position: position, emitRate: particleEmitRate}); break; @@ -329,10 +323,10 @@ function findNearestWaitingAvatar() { // As currently implemented, we select the closest waiting avatar (if close enough) and send -// them a friendRequest. If nobody is close enough we send a waiting message, and wait for a -// friendRequest. If the 2 people who want to connect are both somewhat out of range when they -// initiate the shake, they will race to see who sends the friendRequest after noticing the -// waiting message. Either way, they will start friending eachother at that point. +// them a connectionRequest. If nobody is close enough we send a waiting message, and wait for a +// connectionRequest. If the 2 people who want to connect are both somewhat out of range when they +// initiate the shake, they will race to see who sends the connectionRequest after noticing the +// waiting message. Either way, they will start connecting eachother at that point. function startHandshake(fromKeyboard) { if (fromKeyboard) { debug("adding animation"); @@ -344,21 +338,21 @@ function startHandshake(fromKeyboard) { } debug("starting handshake for", currentHand); state = STATES.waiting; - friendingId = undefined; - friendingHand = undefined; + connectingId = undefined; + connectingHand = undefined; // just in case stopWaiting(); - stopFriending(); - stopMakingFriends(); + stopConnecting(); + stopMakingConnection(); var nearestAvatar = findNearestWaitingAvatar(); if (nearestAvatar.avatar) { - friendingId = nearestAvatar.avatar; - friendingHand = handToString(nearestAvatar.hand); - debug("sending friendRequest to", friendingId); + connectingId = nearestAvatar.avatar; + connectingHand = handToString(nearestAvatar.hand); + debug("sending connectionRequest to", connectingId); messageSend({ - key: "friendRequest", - id: friendingId, + key: "connectionRequest", + id: connectingId, hand: handToString(currentHand) }); } else { @@ -380,12 +374,12 @@ function endHandshake() { // as we ignore the key release event when inactive. See updateTriggers // below. state = STATES.inactive; - friendingId = undefined; - friendingHand = undefined; + connectingId = undefined; + connectingHand = undefined; stopWaiting(); - stopFriending(); - stopMakingFriends(); - // send done to let friend know you are not making friends now + stopConnecting(); + stopMakingConnection(); + // send done to let connection know you are not making connections now messageSend({ key: "done" }); @@ -429,109 +423,109 @@ function messageSend(message) { function lookForWaitingAvatar() { // we started with nobody close enough, but maybe I've moved // or they did. Note that 2 people doing this race, so stop - // as soon as you have a friendingId (which means you got their + // as soon as you have a connectingId (which means you got their // message before noticing they were in range in this loop) // just in case we reenter before stopping stopWaiting(); debug("started looking for waiting avatars"); waitingInterval = Script.setInterval(function () { - if (state == STATES.waiting && !friendingId) { - // find the closest in-range avatar, and send friend request + if (state == STATES.waiting && !connectingId) { + // find the closest in-range avatar, and send connection request // TODO: this is same code as in startHandshake - get this // cleaned up. var nearestAvatar = findNearestWaitingAvatar(); if (nearestAvatar.avatar) { - friendingId = nearestAvatar.avatar; - friendingHand = handToString(nearestAvatar.hand); - debug("sending friendRequest to", friendingId); + connectingId = nearestAvatar.avatar; + connectingHand = handToString(nearestAvatar.hand); + debug("sending connectionRequest to", connectingId); messageSend({ - key: "friendRequest", - id: friendingId, + key: "connectionRequest", + id: connectingId, hand: handToString(currentHand) }); } } else { - // something happened, stop looking for avatars to friend + // something happened, stop looking for avatars to connect stopWaiting(); debug("stopped looking for waiting avatars"); } }, WAITING_INTERVAL); } -// this should be where we make the appropriate friend call. For now just make the +// this should be where we make the appropriate connection call. For now just make the // visualization change. -function makeFriends(id) { - // send done to let the friend know you have made friends. +function makeConnection(id) { + // send done to let the connection know you have made connection. messageSend({ key: "done", - friendId: id + connectionId: id }); - Controller.triggerHapticPulse(FRIENDING_SUCCESS_HAPTIC_STRENGTH, HAPTIC_DURATION, handToHaptic(currentHand)); - state = STATES.makingFriends; - // now that we made friends, reset everything + Controller.triggerHapticPulse(CONNECTING_SUCCESS_HAPTIC_STRENGTH, HAPTIC_DURATION, handToHaptic(currentHand)); + state = STATES.makingConnection; + // now that we made connection, reset everything makingFriendsTimeout = Script.setTimeout(function () { - friendingId = undefined; - friendingHand = undefined; - makingFriendsTimeout = undefined; - }, MAKING_FRIENDS_TIMEOUT); + connectingId = undefined; + connectingHand = undefined; + makingConnectionTimeout = undefined; + }, MAKING_CONNECTION_TIMEOUT); } -// we change states, start the friendingInterval where we check +// we change states, start the connectionInterval where we check // to be sure the hand is still close enough. If not, we terminate // the interval, go back to the waiting state. If we make it -// the entire FRIENDING_TIME, we make friends. -function startFriending(id, hand) { +// the entire CONNECTING_TIME, we make the connection. +function startConnecting(id, hand) { var count = 0; - debug("friending", id, "hand", hand); + debug("connecting", id, "hand", hand); // do we need to do this? - friendingId = id; - friendingHand = hand; - state = STATES.friending; - Controller.triggerHapticPulse(FRIENDING_HAPTIC_STRENGTH, HAPTIC_DURATION, handToHaptic(currentHand)); + connectingId = id; + connectingHand = hand; + state = STATES.connecting; + Controller.triggerHapticPulse(CONNECTING_HAPTIC_STRENGTH, HAPTIC_DURATION, handToHaptic(currentHand)); - // send message that we are friending them + // send message that we are ing them messageSend({ - key: "friending", + key: "connecting", id: id, hand: handToString(currentHand) }); - friendingInterval = Script.setInterval(function () { + connectingInterval = Script.setInterval(function () { count += 1; - if (state != STATES.friending) { - debug("stopping friending interval, state changed"); - stopFriending(); + if (state != STATES.connecting) { + debug("stopping connecting interval, state changed"); + stopConnecting(); } else if (!isNearby(id, hand)) { // gotta go back to waiting debug(id, "moved, back to waiting"); - stopFriending(); + stopConnecting(); messageSend({ key: "done" }); startHandshake(); - } else if (count > FRIENDING_TIME/FRIENDING_INTERVAL) { - debug("made friends with " + id); - makeFriends(id); - stopFriending(); + } else if (count > CONNECTING_TIME/CONNECTING_INTERVAL) { + debug("made connection with " + id); + makeConnection(id); + stopConnecting(); } - }, FRIENDING_INTERVAL); + }, CONNECTING_INTERVAL); } /* -A simple sequence diagram: NOTE that the FriendAck is somewhat +A simple sequence diagram: NOTE that the ConnectionAck is somewhat vestigial, and probably should be removed shortly. Avatar A Avatar B | | | <-----(waiting) ----- startHandshake - startHandshake -- (FriendRequest) -> | +startHandshake - (connectionRequest) -> | | | - | <-------(FriendAck) --------- | - | <--------(friending) -- startFriending - startFriending -- (friending) ---> | + | <----(connectionAck) -------- | + | <-----(connecting) -- startConnecting + startConnecting ---(connecting) ----> | | | - | friends - friends | + | connected + connected | | <--------- (done) ---------- | | ---------- (done) ---------> | */ @@ -554,81 +548,81 @@ function messageHandler(channel, messageString, senderID) { // remove it from the list waitingList[senderID] = message.hand; break; - case "friendRequest": + case "connectionRequest": delete waitingList[senderID]; - if (state == STATES.waiting && message.id == MyAvatar.sessionUUID && (!friendingId || friendingId == senderID)) { - // you were waiting for a friend request, so send the ack. Or, you and the other - // guy raced and both send friendRequests. Handle that too - friendingId = senderID; - friendingHand = message.hand; + if (state == STATES.waiting && message.id == MyAvatar.sessionUUID && (!connectingId || connectingId == senderID)) { + // you were waiting for a connection request, so send the ack. Or, you and the other + // guy raced and both send connectionRequests. Handle that too + connectingId = senderID; + connectingHand = message.hand; messageSend({ - key: "friendAck", + key: "connectionAck", id: senderID, hand: handToString(currentHand) }); } else { - if (state == STATES.waiting && friendingId == senderID) { - // the person you are trying to friend sent a request to someone else. See the + if (state == STATES.waiting && connectingId == senderID) { + // the person you are trying to connect sent a request to someone else. See the // if statement above. So, don't cry, just start the handshake over again startHandshake(); } } break; - case "friendAck": + case "connectionAck": delete waitingList[senderID]; - if (state == STATES.waiting && (!friendingId || friendingId == senderID)) { + if (state == STATES.waiting && (!connectingId || connectingId == senderID)) { if (message.id == MyAvatar.sessionUUID) { - // start friending... - friendingId = senderID; - friendingHand = message.hand; + // start connecting... + connectingId = senderID; + connectingHand = message.hand; stopWaiting(); - startFriending(senderID, message.hand); + startConnecting(senderID, message.hand); } else { - if (friendingId) { - // this is for someone else (we lost race in friendRequest), + if (connectingId) { + // this is for someone else (we lost race in connectionRequest), // so lets start over startHandshake(); } } } - // TODO: check to see if we are waiting for this but the person we are friending sent it to + // TODO: check to see if we are waiting for this but the person we are connecting sent it to // someone else, and try again break; - case "friending": + case "connecting": delete waitingList[senderID]; - if (state == STATES.waiting && senderID == friendingId) { + if (state == STATES.waiting && senderID == connectingId) { // temporary logging - if (friendingHand != message.hand) { - debug("friending hand", friendingHand, "not same as friending hand in message", message.hand); + if (connectingHand != message.hand) { + debug("connecting hand", connectingHand, "not same as connecting hand in message", message.hand); } - friendingHand = message.hand; + connectingHand = message.hand; if (message.id != MyAvatar.sessionUUID) { - // the person we were trying to friend is friending someone else + // the person we were trying to connect is connecting to someone else // so try again startHandshake(); break; } - startFriending(senderID, message.hand); + startConnecting(senderID, message.hand); } break; case "done": delete waitingList[senderID]; - if (state == STATES.friending && friendingId == senderID) { - // if they are done, and didn't friend us, terminate our - // friending - if (message.friendId !== MyAvatar.sessionUUID) { - stopFriending(); + if (state == STATES.connecting && connectingId == senderID) { + // if they are done, and didn't connect us, terminate our + // connecting + if (message.connectionId !== MyAvatar.sessionUUID) { + stopConnecting(); // now just call startHandshake. Should be ok to do so without a // value for isKeyboard, as we should not change the animation // state anyways (if any) startHandshake(); } } else { - // if waiting or inactive, lets clear the friending id. If in makingFriends, - // do nothing (so you see the red for a bit) - if (state != STATES.makingFriends && friendingId == senderID) { - friendingId = undefined; - friendingHand = undefined; + // if waiting or inactive, lets clear the connecting id. If in makingConnection, + // do nothing + if (state != STATES.makingConnection && connectingId == senderID) { + connectingId = undefined; + connectingHand = undefined; if (state != STATES.inactive) { startHandshake(); } @@ -670,33 +664,33 @@ function keyReleaseEvent(event) { } } // map controller actions -var friendsMapping = Controller.newMapping(Script.resolvePath('') + '-grip'); -friendsMapping.from(Controller.Standard.LeftGrip).peek().to(makeGripHandler(Controller.Standard.LeftHand)); -friendsMapping.from(Controller.Standard.RightGrip).peek().to(makeGripHandler(Controller.Standard.RightHand)); +var connectionMapping = Controller.newMapping(Script.resolvePath('') + '-grip'); +connectionMapping.from(Controller.Standard.LeftGrip).peek().to(makeGripHandler(Controller.Standard.LeftHand)); +connectionMapping.from(Controller.Standard.RightGrip).peek().to(makeGripHandler(Controller.Standard.RightHand)); // setup keyboard initiation Controller.keyPressEvent.connect(keyPressEvent); Controller.keyReleaseEvent.connect(keyReleaseEvent); // xbox controller cuz that's important -friendsMapping.from(Controller.Standard.RB).peek().to(makeGripHandler(Controller.Standard.RightHand, true)); +connectionMapping.from(Controller.Standard.RB).peek().to(makeGripHandler(Controller.Standard.RightHand, true)); // it is easy to forget this and waste a lot of time for nothing -friendsMapping.enable(); +connectionMapping.enable(); // connect updateVisualization to update frequently Script.update.connect(updateVisualization); Script.scriptEnding.connect(function () { debug("removing controller mappings"); - friendsMapping.disable(); + connectionMapping.disable(); debug("removing key mappings"); Controller.keyPressEvent.disconnect(keyPressEvent); Controller.keyReleaseEvent.disconnect(keyReleaseEvent); debug("disconnecting updateVisualization"); Script.update.disconnect(updateVisualization); deleteParticleEffect(); - deleteMakeFriendsParticleEffect(); + deleteMakeConnectionParticleEffect(); }); }()); // END LOCAL_SCOPE From 34a89e4b9a885247c6ed59060d314d49241669d0 Mon Sep 17 00:00:00 2001 From: David Kelly Date: Wed, 22 Mar 2017 13:26:51 -0700 Subject: [PATCH 034/118] added sounds, new haptics, and some cleanup --- scripts/system/makeUserConnection.js | 59 +++++++++++++++++++++------- 1 file changed, 45 insertions(+), 14 deletions(-) diff --git a/scripts/system/makeUserConnection.js b/scripts/system/makeUserConnection.js index 621fce93af..b03324e099 100644 --- a/scripts/system/makeUserConnection.js +++ b/scripts/system/makeUserConnection.js @@ -25,13 +25,17 @@ const STATES = { const STATE_STRINGS = ["inactive", "waiting", "connecting", "makingConnection"]; const WAITING_INTERVAL = 100; // ms const CONNECTING_INTERVAL = 100; // ms -const MAKING_CONNECTION_TIMEOUT = 3000; // ms -const CONNECTING_TIME = 2000; // ms -const CONNECTING_HAPTIC_STRENGTH = 0.5; -const CONNECTING_SUCCESS_HAPTIC_STRENGTH = 1.0; -const HAPTIC_DURATION = 20; +const MAKING_CONNECTION_TIMEOUT = 800; // ms +const CONNECTING_TIME = 1600; // ms const PARTICLE_RADIUS = 0.1; const PARTICLE_ANGLE_INCREMENT = 360/45; // 1hz +const HANDSHAKE_SOUND_URL = "https://s3-us-west-1.amazonaws.com/hifi-content/davidkelly/production/audio/4beat_sweep.wav"; +const SUCCESSFUL_HANDSHAKE_SOUND_URL = "https://s3-us-west-1.amazonaws.com/hifi-content/davidkelly/production/audio/3rdbeat_success_bell.wav"; +const HAPTIC_DATA = { + initial: { duration: 20, strength: 0.6}, + background: { duration: 100, strength: 0.3 }, + success: { duration: 60, strength: 1.0} +}; const PARTICLE_EFFECT_PROPS = { "alpha": 0.8, "azimuthFinish": Math.PI, @@ -104,6 +108,10 @@ var particleRotationAngle = 0.0; var makingConnectionParticleEffect; var makingConnectionEmitRate = 2000; var particleEmitRate = 500; +var handshakeInjector; +var successfulHandshakeInjector; +var handshakeSound; +var successfulHandshakeSound; function debug() { var stateString = "<" + STATE_STRINGS[state] + ">"; @@ -256,19 +264,17 @@ function updateVisualization() { // now manage the rest of the entity if (!particleEffect) { particleRotationAngle = 0.0; - var position = calcParticlePos(myHandPosition, otherHand, otherOrientation); particleProps = PARTICLE_EFFECT_PROPS; particleProps.isEmitting = 0; - particleProps.position = position; + particleProps.position = calcParticlePos(myHandPosition, otherHand, otherOrientation); particleEffect = Entities.addEntity(particleProps); } else { - var position = calcParticlePos(myHandPosition, otherHand, otherOrientation); - particleProps.position = position; + particleProps.position = calcParticlePos(myHandPosition, otherHand, otherOrientation); particleProps.isEmitting = 1; Entities.editEntity(particleEffect, particleProps); } if (!makingConnectionParticleEffect) { - props = MAKING_CONNECTION_PARTICLE_PROPS; + var props = MAKING_CONNECTION_PARTICLE_PROPS; makingConnectionEmitRate = 2000; props.emitRate = makingConnectionEmitRate; props.position = myHandPosition; @@ -281,8 +287,7 @@ function updateVisualization() { case STATES.makingConnection: particleEmitRate *= 0.5; Entities.editEntity(makingConnectionParticleEffect, {emitRate: 0, isEmitting: 0, position: myHandPosition}); - var pos = calcParticlePos(myHandPosition, otherHand, otherOrientation); - Entities.editEntity(particleEffect, {position: position, emitRate: particleEmitRate}); + Entities.editEntity(particleEffect, {position: calcParticlePos(myHandPosition, otherHand, otherOrientation), emitRate: particleEmitRate}); break; default: debug("unexpected state", state); @@ -379,6 +384,9 @@ function endHandshake() { stopWaiting(); stopConnecting(); stopMakingConnection(); + if (handshakeInjector) { + handshakeInjector.stop(); + } // send done to let connection know you are not making connections now messageSend({ key: "done" @@ -461,13 +469,24 @@ function makeConnection(id) { key: "done", connectionId: id }); - Controller.triggerHapticPulse(CONNECTING_SUCCESS_HAPTIC_STRENGTH, HAPTIC_DURATION, handToHaptic(currentHand)); + state = STATES.makingConnection; + + // continue the haptic background until the timeout fires. When we make calls, we will have an interval + // probably, in which we do this. + Controller.triggerHapticPulse(HAPTIC_DATA.background.strength, MAKING_CONNECTION_TIMEOUT, handToHaptic(currentHand)); + // now that we made connection, reset everything makingFriendsTimeout = Script.setTimeout(function () { connectingId = undefined; connectingHand = undefined; makingConnectionTimeout = undefined; + if (!successfulHandshakeInjector) { + successfulHandshakeInjector = Audio.playSound(successfulHandshakeSound, {position: getHandPosition(MyAvatar, currentHand), volume: 0.5, localOnly: true}); + } else { + successfulHandshakeInjector.restart(); + } + Controller.triggerHapticPulse(HAPTIC_DATA.success.strength, HAPTIC_DATA.success.duration, handToHaptic(currentHand)); }, MAKING_CONNECTION_TIMEOUT); } @@ -482,7 +501,13 @@ function startConnecting(id, hand) { connectingId = id; connectingHand = hand; state = STATES.connecting; - Controller.triggerHapticPulse(CONNECTING_HAPTIC_STRENGTH, HAPTIC_DURATION, handToHaptic(currentHand)); + + // play sound + if (!handshakeInjector) { + handshakeInjector = Audio.playSound(handshakeSound, {position: getHandPosition(MyAvatar, currentHand), volume: 0.5, localOnly: true}); + } else { + handshakeInjector.restart(); + } // send message that we are ing them messageSend({ @@ -490,9 +515,11 @@ function startConnecting(id, hand) { id: id, hand: handToString(currentHand) }); + Controller.triggerHapticPulse(HAPTIC_DATA.initial.strength, HAPTIC_DATA.initial.duration, handToHaptic(currentHand)); connectingInterval = Script.setInterval(function () { count += 1; + Controller.triggerHapticPulse(HAPTIC_DATA.background.strength, HAPTIC_DATA.background.duration, handToHaptic(currentHand)); if (state != STATES.connecting) { debug("stopping connecting interval, state changed"); stopConnecting(); @@ -681,6 +708,10 @@ connectionMapping.enable(); // connect updateVisualization to update frequently Script.update.connect(updateVisualization); +// load the sounds when the script loads +handshakeSound = SoundCache.getSound(HANDSHAKE_SOUND_URL); +successfulHandshakeSound = SoundCache.getSound(SUCCESSFUL_HANDSHAKE_SOUND_URL); + Script.scriptEnding.connect(function () { debug("removing controller mappings"); connectionMapping.disable(); From bf191eabb7b3f3481f67fb780d555c427fbb83d3 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Wed, 22 Mar 2017 13:35:48 -0700 Subject: [PATCH 035/118] Some new stale PAL behavior --- interface/resources/qml/hifi/Pal.qml | 30 +++++++++++++++------------- scripts/system/pal.js | 6 +++--- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index b52dce5841..f9f8c42d56 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -577,9 +577,8 @@ Rectangle { // This Rectangle refers to each Row in the nearbyTable. rowDelegate: Rectangle { // The only way I know to specify a row height. // Size - height: rowHeight + (styleData.selected && model.isPresent ? 15 : 0); - color: rowColor(styleData.selected, styleData.alternate, model ? model.isPresent : true); - opacity: model.isPresent ? 1.0 : 0.6; + height: rowHeight + (styleData.selected ? 15 : 0); + color: rowColor(styleData.selected, styleData.alternate, true); } // This Item refers to the contents of each Cell @@ -588,7 +587,7 @@ Rectangle { property bool isCheckBox: styleData.role === "personalMute" || styleData.role === "ignore"; property bool isButton: styleData.role === "mute" || styleData.role === "kick"; property bool isAvgAudio: styleData.role === "avgAudioLevel"; - opacity: model.isPresent ? 1.0 : 0.6; + opacity: model && model.isPresent ? 1.0 : 0.4; // This NameCard refers to the cell that contains an avatar's // DisplayName and UserName @@ -629,6 +628,7 @@ Rectangle { size: height; anchors.verticalCenter: parent.verticalCenter; anchors.horizontalCenter: parent.horizontalCenter; + enabled: (model ? !model["ignore"] && model["isPresent"] : true); onClicked: { // cannot change mute status when ignoring if (!model["ignore"]) { @@ -641,7 +641,7 @@ Rectangle { } } - // This CheckBox belongs in the columns that contain the stateful action buttons ("Mute" & "Ignore" for now) + // This CheckBox belongs in the columns that contain the stateful action buttons ("Ignore" for now) // KNOWN BUG with the Checkboxes: When clicking in the center of the sorting header, the checkbox // will appear in the "hovered" state. Hovering over the checkbox will fix it. // Clicking on the sides of the sorting header doesn't cause this problem. @@ -652,8 +652,8 @@ Rectangle { visible: isCheckBox; anchors.centerIn: parent; checked: model ? model[styleData.role] : false; - // If this is a "Personal Mute" checkbox, disable the checkbox if the "Ignore" checkbox is checked. - enabled: !(styleData.role === "personalMute" && (model ? model["ignore"] : true)); + // If this is an "Ignore" checkbox, disable the checkbox if user isn't present. + enabled: styleData.role === "ignore" ? (model ? model["isPresent"] : true) : true; boxSize: 24; onClicked: { var newValue = !model[styleData.role]; @@ -1194,7 +1194,6 @@ Rectangle { } } sortModel(); - reloadNearby.glyph = hifi.glyphs.reload; reloadNearby.color = 0; break; case 'connections': @@ -1287,16 +1286,19 @@ Rectangle { delete ignored[sessionID]; break; case 'palIsStale': - if (message.params) { - var sessionID = message.params[0]; - var userIndex = findNearbySessionIndex(sessionID); - if (userIndex != -1) { + var sessionID = message.params[0]; + var reason = message.params[1]; + var userIndex = findNearbySessionIndex(sessionID); + if (userIndex != -1) { + if (!nearbyUserModelData[userIndex].ignore) { nearbyUserModel.setProperty(userIndex, "isPresent", false); nearbyUserModelData[userIndex].isPresent = false; + nearbyTable.selection.deselect(userIndex); + reloadNearby.color = 2; } + } else { + reloadNearby.color = 2; } - reloadNearby.glyph = hifi.glyphs.alert; - reloadNearby.color = 2; break; default: console.log('Unrecognized message:', JSON.stringify(message)); diff --git a/scripts/system/pal.js b/scripts/system/pal.js index 95e208d126..ab983d6ff1 100644 --- a/scripts/system/pal.js +++ b/scripts/system/pal.js @@ -850,15 +850,15 @@ function clearLocalQMLDataAndClosePAL() { } function avatarAdded() { - sendToQml({ method: 'palIsStale' }); + sendToQml({ method: 'palIsStale', params: [avatarID, 'avatarAdded'] }); } function avatarRemoved(avatarID) { - sendToQml({ method: 'palIsStale', params: [avatarID] }); + sendToQml({ method: 'palIsStale', params: [avatarID, 'avatarRemoved'] }); } function avatarSessionChanged() { - sendToQml({ method: 'palIsStale' }); + sendToQml({ method: 'palIsStale', params: [avatarID, 'avatarSessionChanged'] }); } function shutdown() { From 020dcca78dcd69a4c8e3cada5ff3e49e9f45a22f Mon Sep 17 00:00:00 2001 From: David Kelly Date: Wed, 22 Mar 2017 14:55:57 -0700 Subject: [PATCH 036/118] end handshake when successful, stop handshake sound if other guy pulls out --- scripts/system/makeUserConnection.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/scripts/system/makeUserConnection.js b/scripts/system/makeUserConnection.js index b03324e099..74835f7e5a 100644 --- a/scripts/system/makeUserConnection.js +++ b/scripts/system/makeUserConnection.js @@ -212,6 +212,12 @@ function deleteMakeConnectionParticleEffect() { } } +function stopHandshakeSound() { + if (handshakeInjector) { + handshakeInjector.stop(); + } +} + function calcParticlePos(myHand, otherHand, otherOrientation, reset) { if (reset) { particleRotationAngle = 0.0; @@ -234,6 +240,7 @@ function updateVisualization() { if (state == STATES.inactive) { deleteParticleEffect(); deleteMakeConnectionParticleEffect(); + stopHandshakeSound(); return; } @@ -255,6 +262,7 @@ function updateVisualization() { // no visualization while waiting deleteParticleEffect(); deleteMakeConnectionParticleEffect(); + stopHandshakeSound(); break; case STATES.connecting: var particleProps = {}; @@ -478,15 +486,13 @@ function makeConnection(id) { // now that we made connection, reset everything makingFriendsTimeout = Script.setTimeout(function () { - connectingId = undefined; - connectingHand = undefined; - makingConnectionTimeout = undefined; if (!successfulHandshakeInjector) { successfulHandshakeInjector = Audio.playSound(successfulHandshakeSound, {position: getHandPosition(MyAvatar, currentHand), volume: 0.5, localOnly: true}); } else { successfulHandshakeInjector.restart(); } Controller.triggerHapticPulse(HAPTIC_DATA.success.strength, HAPTIC_DATA.success.duration, handToHaptic(currentHand)); + endHandshake(); }, MAKING_CONNECTION_TIMEOUT); } From 692ef369c78d403fbca84a2a54c72e71696d619e Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Wed, 22 Mar 2017 15:04:44 -0700 Subject: [PATCH 037/118] Attempted Stale PAL fix - hard to test! --- interface/resources/qml/hifi/Pal.qml | 20 +++++++++++--------- scripts/system/pal.js | 4 ++-- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index f9f8c42d56..837b804c7b 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -578,7 +578,7 @@ Rectangle { rowDelegate: Rectangle { // The only way I know to specify a row height. // Size height: rowHeight + (styleData.selected ? 15 : 0); - color: rowColor(styleData.selected, styleData.alternate, true); + color: rowColor(styleData.selected, styleData.alternate); } // This Item refers to the contents of each Cell @@ -595,7 +595,7 @@ Rectangle { id: nameCard; // Properties profileUrl: (model && model.profileUrl) || ""; - imageMaskColor: rowColor(styleData.selected, styleData.row % 2, model ? model.isPresent : false); + imageMaskColor: rowColor(styleData.selected, styleData.row % 2); displayName: styleData.value; userName: model ? model.userName : ""; connectionStatus: model ? model.connection : ""; @@ -903,7 +903,7 @@ Rectangle { rowDelegate: Rectangle { // Size height: rowHeight; - color: rowColor(styleData.selected, styleData.alternate, true); + color: rowColor(styleData.selected, styleData.alternate); } // This Item refers to the contents of each Cell @@ -916,7 +916,7 @@ Rectangle { // Properties visible: styleData.role === "userName"; profileUrl: (model && model.profileUrl) || ""; - imageMaskColor: rowColor(styleData.selected, styleData.row % 2, true); + imageMaskColor: rowColor(styleData.selected, styleData.row % 2); displayName: ""; userName: model ? model.userName : ""; connectionStatus : model ? model.connection : ""; @@ -1159,8 +1159,8 @@ Rectangle { } } - function rowColor(selected, alternate, isPresent) { - return isPresent ? (selected ? hifi.colors.orangeHighlight : alternate ? hifi.colors.tableRowLightEven : hifi.colors.tableRowLightOdd) : hifi.colors.gray; + function rowColor(selected, alternate) { + return selected ? hifi.colors.orangeHighlight : alternate ? hifi.colors.tableRowLightEven : hifi.colors.tableRowLightOdd; } function findNearbySessionIndex(sessionId, optionalData) { // no findIndex in .qml var data = optionalData || nearbyUserModelData, length = data.length; @@ -1291,9 +1291,11 @@ Rectangle { var userIndex = findNearbySessionIndex(sessionID); if (userIndex != -1) { if (!nearbyUserModelData[userIndex].ignore) { - nearbyUserModel.setProperty(userIndex, "isPresent", false); - nearbyUserModelData[userIndex].isPresent = false; - nearbyTable.selection.deselect(userIndex); + if (reason !== 'avatarAdded') { + nearbyUserModel.setProperty(userIndex, "isPresent", false); + nearbyUserModelData[userIndex].isPresent = false; + nearbyTable.selection.deselect(userIndex); + } reloadNearby.color = 2; } } else { diff --git a/scripts/system/pal.js b/scripts/system/pal.js index ab983d6ff1..d9734850e5 100644 --- a/scripts/system/pal.js +++ b/scripts/system/pal.js @@ -849,7 +849,7 @@ function clearLocalQMLDataAndClosePAL() { sendToQml({ method: 'clearLocalQMLData' }); } -function avatarAdded() { +function avatarAdded(avatarID) { sendToQml({ method: 'palIsStale', params: [avatarID, 'avatarAdded'] }); } @@ -857,7 +857,7 @@ function avatarRemoved(avatarID) { sendToQml({ method: 'palIsStale', params: [avatarID, 'avatarRemoved'] }); } -function avatarSessionChanged() { +function avatarSessionChanged(avatarID) { sendToQml({ method: 'palIsStale', params: [avatarID, 'avatarSessionChanged'] }); } From 7b52afb58b496eddbef5d99c1ede0acce52c5ae5 Mon Sep 17 00:00:00 2001 From: David Kelly Date: Wed, 22 Mar 2017 17:01:42 -0700 Subject: [PATCH 038/118] vis changes to fade. Still needs more work --- scripts/system/makeUserConnection.js | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/scripts/system/makeUserConnection.js b/scripts/system/makeUserConnection.js index 74835f7e5a..b7dbef0520 100644 --- a/scripts/system/makeUserConnection.js +++ b/scripts/system/makeUserConnection.js @@ -27,7 +27,7 @@ const WAITING_INTERVAL = 100; // ms const CONNECTING_INTERVAL = 100; // ms const MAKING_CONNECTION_TIMEOUT = 800; // ms const CONNECTING_TIME = 1600; // ms -const PARTICLE_RADIUS = 0.1; +const PARTICLE_RADIUS = 0.15; const PARTICLE_ANGLE_INCREMENT = 360/45; // 1hz const HANDSHAKE_SOUND_URL = "https://s3-us-west-1.amazonaws.com/hifi-content/davidkelly/production/audio/4beat_sweep.wav"; const SUCCESSFUL_HANDSHAKE_SOUND_URL = "https://s3-us-west-1.amazonaws.com/hifi-content/davidkelly/production/audio/3rdbeat_success_bell.wav"; @@ -78,7 +78,7 @@ const MAKING_CONNECTION_PARTICLE_PROPS = { "particleRadius": 0.048, "polarStart": 0, "polarFinish": 1, - "radiusFinish": 0.2, + "radiusFinish": 0.3, "radiusStart": 0.04, "speedSpread": 0.01, "radiusSpread": 0.9, @@ -200,6 +200,15 @@ function positionFractionallyTowards(posA, posB, frac) { return Vec3.sum(posA, Vec3.multiply(frac, Vec3.subtract(posB, posA))); } +function fadeEffects() { + if (particleEffect) { + Entities.editEntity(particleEffect, {isEmitting: 0, lifespan: 1.0}); + } + if(makingConnectionParticleEffect) { + Entities.editEntity(makingConnectionParticleEffect, {isEmitting: 0, lifespan: 1.5}); + } +} + function deleteParticleEffect() { if (particleEffect) { particleEffect = Entities.deleteEntity(particleEffect); @@ -224,22 +233,15 @@ function calcParticlePos(myHand, otherHand, otherOrientation, reset) { } var position = positionFractionallyTowards(myHand, otherHand, 0.5); particleRotationAngle += PARTICLE_ANGLE_INCREMENT; // about 0.5 hz - var radius = Math.min(PARTICLE_RADIUS, PARTICLE_RADIUS * particleRotationAngle / 720); + var radius = Math.min(PARTICLE_RADIUS, PARTICLE_RADIUS * particleRotationAngle / 360); var axis = Vec3.mix(Quat.getFront(MyAvatar.orientation), Quat.inverse(Quat.getFront(otherOrientation)), 0.5); return Vec3.sum(position, Vec3.multiplyQbyV(Quat.angleAxis(particleRotationAngle, axis), {x: 0, y: radius, z: 0})); } // this is called frequently, but usually does nothing function updateVisualization() { - // after making connection, if you are still holding the grip lets transition - // back to waiting - if (state == STATES.makingConnection && !connectingId) { - startHandshake(); - return; - } if (state == STATES.inactive) { - deleteParticleEffect(); - deleteMakeConnectionParticleEffect(); + fadeEffects(); stopHandshakeSound(); return; } @@ -272,6 +274,7 @@ function updateVisualization() { // now manage the rest of the entity if (!particleEffect) { particleRotationAngle = 0.0; + particleEmitRate = 500; particleProps = PARTICLE_EFFECT_PROPS; particleProps.isEmitting = 0; particleProps.position = calcParticlePos(myHandPosition, otherHand, otherOrientation); @@ -293,7 +296,7 @@ function updateVisualization() { } break; case STATES.makingConnection: - particleEmitRate *= 0.5; + particleEmitRate = Math.max(50, particleEmitRate * 0.5); Entities.editEntity(makingConnectionParticleEffect, {emitRate: 0, isEmitting: 0, position: myHandPosition}); Entities.editEntity(particleEffect, {position: calcParticlePos(myHandPosition, otherHand, otherOrientation), emitRate: particleEmitRate}); break; From aa085e577d3850f3b46c00647d6aabcfe00e205e Mon Sep 17 00:00:00 2001 From: David Kelly Date: Thu, 23 Mar 2017 09:19:04 -0700 Subject: [PATCH 039/118] more vis work --- scripts/system/makeUserConnection.js | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/scripts/system/makeUserConnection.js b/scripts/system/makeUserConnection.js index b7dbef0520..a9d2653146 100644 --- a/scripts/system/makeUserConnection.js +++ b/scripts/system/makeUserConnection.js @@ -200,15 +200,6 @@ function positionFractionallyTowards(posA, posB, frac) { return Vec3.sum(posA, Vec3.multiply(frac, Vec3.subtract(posB, posA))); } -function fadeEffects() { - if (particleEffect) { - Entities.editEntity(particleEffect, {isEmitting: 0, lifespan: 1.0}); - } - if(makingConnectionParticleEffect) { - Entities.editEntity(makingConnectionParticleEffect, {isEmitting: 0, lifespan: 1.5}); - } -} - function deleteParticleEffect() { if (particleEffect) { particleEffect = Entities.deleteEntity(particleEffect); @@ -241,8 +232,8 @@ function calcParticlePos(myHand, otherHand, otherOrientation, reset) { // this is called frequently, but usually does nothing function updateVisualization() { if (state == STATES.inactive) { - fadeEffects(); - stopHandshakeSound(); + deleteParticleEffect(); + deleteMakeConnectionParticleEffect(); return; } @@ -384,6 +375,9 @@ function startHandshake(fromKeyboard) { function endHandshake() { debug("ending handshake for", currentHand); + + deleteParticleEffect(); + deleteMakeConnectionParticleEffect(); currentHand = undefined; // note that setting the state to inactive should really // only be done here, unless we change how the triggering works, @@ -495,7 +489,7 @@ function makeConnection(id) { successfulHandshakeInjector.restart(); } Controller.triggerHapticPulse(HAPTIC_DATA.success.strength, HAPTIC_DATA.success.duration, handToHaptic(currentHand)); - endHandshake(); + // don't change state (so animation continues while gripped) }, MAKING_CONNECTION_TIMEOUT); } From e48123b5bbc171e7df32d93b652f4077367df6d8 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Thu, 23 Mar 2017 09:53:43 -0700 Subject: [PATCH 040/118] Merge from Master --- BUILD_WIN.md | 145 +-- assignment-client/src/Agent.cpp | 32 +- assignment-client/src/Agent.h | 8 + assignment-client/src/assets/AssetServer.cpp | 4 +- assignment-client/src/octree/OctreeServer.cpp | 9 +- .../src/scripts/EntityScriptServer.cpp | 30 +- domain-server/src/DomainServer.cpp | 4 +- .../src/DomainServerSettingsManager.cpp | 37 +- interface/CMakeLists.txt | 2 +- interface/resources/controllers/standard.json | 44 +- interface/resources/html/img/devices.png | Bin 7492 -> 0 bytes interface/resources/html/img/models.png | Bin 8664 -> 0 bytes interface/resources/html/img/move.png | Bin 6121 -> 0 bytes interface/resources/html/img/run-script.png | Bin 4873 -> 0 bytes interface/resources/html/img/talk.png | Bin 2611 -> 0 bytes interface/resources/html/img/write-script.png | Bin 2006 -> 0 bytes .../resources/html/interface-welcome.html | 187 --- interface/resources/icons/load-script.svg | 125 -- interface/resources/icons/new-script.svg | 129 -- interface/resources/icons/save-script.svg | 674 ----------- interface/resources/icons/start-script.svg | 550 --------- interface/resources/icons/stop-script.svg | 163 --- interface/resources/qml/AvatarInputs.qml | 57 +- interface/resources/qml/Stats.qml | 12 +- interface/resources/styles/log_dialog.qss | 4 +- interface/src/Application.cpp | 220 ++-- interface/src/Application.h | 7 +- interface/src/Menu.cpp | 21 +- interface/src/Menu.h | 6 +- interface/src/avatar/AvatarManager.cpp | 2 +- interface/src/avatar/AvatarManager.h | 2 +- .../src/avatar/CauterizedMeshPartPayload.cpp | 53 +- .../src/avatar/CauterizedMeshPartPayload.h | 7 +- interface/src/avatar/CauterizedModel.cpp | 38 +- interface/src/avatar/MyAvatar.cpp | 109 +- interface/src/avatar/MyAvatar.h | 56 +- interface/src/ui/ApplicationOverlay.cpp | 49 +- interface/src/ui/ApplicationOverlay.h | 3 - interface/src/ui/AvatarInputs.cpp | 20 - interface/src/ui/AvatarInputs.h | 6 - interface/src/ui/BaseLogDialog.cpp | 48 +- interface/src/ui/BaseLogDialog.h | 4 +- interface/src/ui/CachesSizeDialog.cpp | 84 -- interface/src/ui/CachesSizeDialog.h | 45 - interface/src/ui/DialogsManager.cpp | 24 - interface/src/ui/DialogsManager.h | 6 - interface/src/ui/DiskCacheEditor.cpp | 146 --- interface/src/ui/DiskCacheEditor.h | 49 - interface/src/ui/ScriptEditBox.cpp | 111 -- interface/src/ui/ScriptEditBox.h | 38 - interface/src/ui/ScriptEditorWidget.cpp | 256 ---- interface/src/ui/ScriptEditorWidget.h | 64 - interface/src/ui/ScriptEditorWindow.cpp | 259 ----- interface/src/ui/ScriptEditorWindow.h | 64 - interface/src/ui/ScriptLineNumberArea.cpp | 28 - interface/src/ui/ScriptLineNumberArea.h | 32 - interface/src/ui/ScriptsTableWidget.cpp | 49 - interface/src/ui/ScriptsTableWidget.h | 28 - interface/src/ui/Stats.cpp | 10 +- interface/src/ui/Stats.h | 8 +- interface/src/ui/overlays/Overlays.cpp | 4 +- interface/src/ui/overlays/Web3DOverlay.cpp | 2 +- interface/ui/scriptEditorWidget.ui | 142 --- interface/ui/scriptEditorWindow.ui | 324 ------ libraries/audio-client/src/AudioClient.cpp | 244 ++-- libraries/audio-client/src/AudioClient.h | 16 +- .../display-plugins/OpenGLDisplayPlugin.cpp | 6 +- .../display-plugins/hmd/HmdDisplayPlugin.cpp | 35 +- .../src/EntityTreeRenderer.cpp | 3 +- .../src/RenderablePolyVoxEntityItem.cpp | 78 +- .../src/RenderablePolyVoxEntityItem.h | 9 +- .../src/RenderableShapeEntityItem.cpp | 13 +- .../src/RenderableWebEntityItem.cpp | 2 +- .../src/EntitiesScriptEngineProvider.h | 4 +- libraries/entities/src/EntityItem.cpp | 8 +- .../entities/src/EntityItemProperties.cpp | 21 - libraries/entities/src/EntityItemProperties.h | 4 - .../entities/src/EntityScriptingInterface.cpp | 176 ++- .../entities/src/EntityScriptingInterface.h | 54 +- libraries/entities/src/PolyVoxEntityItem.cpp | 4 + libraries/entities/src/PolyVoxEntityItem.h | 6 +- libraries/entities/src/PropertyGroup.h | 29 +- libraries/fbx/src/FBXReader.cpp | 19 +- libraries/fbx/src/FBXReader.h | 20 - libraries/fbx/src/FBXReader_Node.cpp | 3 +- libraries/fbx/src/OBJReader.cpp | 10 +- libraries/fbx/src/OBJReader.h | 2 +- libraries/fbx/src/OBJWriter.cpp | 148 +++ libraries/fbx/src/OBJWriter.h | 26 + libraries/gpu-gl/src/gpu/gl/GLBackend.cpp | 11 +- libraries/gpu-gl/src/gpu/gl/GLBackend.h | 13 +- .../gpu-gl/src/gpu/gl/GLBackendTexture.cpp | 54 +- libraries/gpu-gl/src/gpu/gl/GLFramebuffer.cpp | 9 +- libraries/gpu-gl/src/gpu/gl/GLFramebuffer.h | 2 +- libraries/gpu-gl/src/gpu/gl/GLTexelFormat.cpp | 5 + libraries/gpu-gl/src/gpu/gl/GLTexture.cpp | 233 +--- libraries/gpu-gl/src/gpu/gl/GLTexture.h | 207 +--- .../gpu-gl/src/gpu/gl/GLTextureTransfer.cpp | 208 ---- .../gpu-gl/src/gpu/gl/GLTextureTransfer.h | 78 -- libraries/gpu-gl/src/gpu/gl41/GL41Backend.h | 33 +- .../gpu-gl/src/gpu/gl41/GL41BackendOutput.cpp | 10 +- .../src/gpu/gl41/GL41BackendTexture.cpp | 192 +-- libraries/gpu-gl/src/gpu/gl45/GL45Backend.cpp | 11 +- libraries/gpu-gl/src/gpu/gl45/GL45Backend.h | 254 +++- .../gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp | 10 +- .../src/gpu/gl45/GL45BackendTexture.cpp | 536 ++------- .../gpu/gl45/GL45BackendVariableTexture.cpp | 1033 +++++++++++++++++ libraries/gpu/CMakeLists.txt | 2 +- libraries/gpu/src/gpu/Batch.cpp | 7 - libraries/gpu/src/gpu/Buffer.h | 2 +- libraries/gpu/src/gpu/Context.cpp | 17 + libraries/gpu/src/gpu/Context.h | 4 + libraries/gpu/src/gpu/Format.cpp | 7 + libraries/gpu/src/gpu/Format.h | 6 + libraries/gpu/src/gpu/Framebuffer.cpp | 12 +- libraries/gpu/src/gpu/Texture.cpp | 225 ++-- libraries/gpu/src/gpu/Texture.h | 178 ++- libraries/gpu/src/gpu/Texture_ktx.cpp | 289 +++++ libraries/ktx/CMakeLists.txt | 3 + libraries/ktx/src/ktx/KTX.cpp | 165 +++ libraries/ktx/src/ktx/KTX.h | 494 ++++++++ libraries/ktx/src/ktx/Reader.cpp | 195 ++++ libraries/ktx/src/ktx/Writer.cpp | 171 +++ libraries/model-networking/CMakeLists.txt | 2 +- .../src/model-networking/KTXCache.cpp | 47 + .../src/model-networking/KTXCache.h | 51 + .../src/model-networking/TextureCache.cpp | 492 +++++--- .../src/model-networking/TextureCache.h | 26 +- libraries/model/CMakeLists.txt | 2 +- libraries/model/src/model/Geometry.cpp | 112 +- libraries/model/src/model/Geometry.h | 14 +- libraries/model/src/model/TextureMap.cpp | 149 ++- libraries/model/src/model/TextureMap.h | 3 +- libraries/networking/src/Assignment.cpp | 1 - libraries/networking/src/FileCache.cpp | 243 ++++ libraries/networking/src/FileCache.h | 158 +++ libraries/networking/src/NodePermissions.h | 48 +- libraries/networking/src/udt/PacketQueue.cpp | 23 +- libraries/networking/src/udt/PacketQueue.h | 5 +- libraries/physics/src/EntityMotionState.cpp | 24 +- libraries/physics/src/EntityMotionState.h | 1 + .../physics/src/PhysicalEntitySimulation.cpp | 18 +- .../physics/src/PhysicalEntitySimulation.h | 5 +- libraries/physics/src/PhysicsEngine.cpp | 2 +- libraries/physics/src/PhysicsEngine.h | 3 +- .../physics/src/ThreadSafeDynamicsWorld.cpp | 27 +- .../physics/src/ThreadSafeDynamicsWorld.h | 4 + libraries/recording/src/recording/Deck.cpp | 5 +- libraries/render-utils/CMakeLists.txt | 2 +- .../render-utils/src/AntialiasingEffect.cpp | 2 +- .../render-utils/src/DeferredFramebuffer.cpp | 10 +- .../src/DeferredLightingEffect.cpp | 4 +- .../render-utils/src/FramebufferCache.cpp | 16 - libraries/render-utils/src/FramebufferCache.h | 5 - libraries/render-utils/src/LightAmbient.slh | 13 +- libraries/render-utils/src/LightingModel.cpp | 10 + libraries/render-utils/src/LightingModel.h | 12 +- libraries/render-utils/src/LightingModel.slh | 9 +- .../render-utils/src/MaterialTextures.slh | 2 +- .../render-utils/src/MeshPartPayload.cpp | 26 +- libraries/render-utils/src/MeshPartPayload.h | 6 +- libraries/render-utils/src/Model.cpp | 18 +- .../render-utils/src/RenderDeferredTask.cpp | 27 +- .../render-utils/src/RenderPipelines.cpp | 2 +- .../render-utils/src/SubsurfaceScattering.cpp | 6 +- .../render-utils/src/SurfaceGeometryPass.cpp | 12 +- libraries/render-utils/src/text/Font.cpp | 3 +- libraries/render/CMakeLists.txt | 2 +- libraries/render/src/render/DrawTask.cpp | 12 +- libraries/render/src/render/DrawTask.h | 4 +- libraries/render/src/render/ShapePipeline.h | 8 +- .../render/src/render/drawItemStatus.slv | 4 +- .../src/AudioScriptingInterface.cpp | 5 - .../src/AudioScriptingInterface.h | 9 +- .../script-engine/src/BaseScriptEngine.h | 67 -- libraries/script-engine/src/MeshProxy.h | 41 + .../src/ModelScriptingInterface.cpp | 159 +++ .../src/ModelScriptingInterface.h | 45 + libraries/script-engine/src/ScriptEngine.cpp | 561 ++++++++- libraries/script-engine/src/ScriptEngine.h | 37 +- .../script-engine/src/ScriptEngineLogging.cpp | 1 + .../script-engine/src/ScriptEngineLogging.h | 1 + libraries/script-engine/src/ScriptEngines.cpp | 20 +- libraries/script-engine/src/ScriptEngines.h | 10 +- .../src/BaseScriptEngine.cpp | 130 ++- libraries/shared/src/BaseScriptEngine.h | 90 ++ libraries/shared/src/HifiConfigVariantMap.cpp | 6 +- libraries/shared/src/PathUtils.cpp | 22 +- libraries/shared/src/PathUtils.h | 7 +- libraries/shared/src/RenderArgs.h | 1 + libraries/shared/src/ServerPathUtils.cpp | 31 - libraries/shared/src/ServerPathUtils.h | 22 - libraries/shared/src/shared/Storage.cpp | 92 ++ libraries/shared/src/shared/Storage.h | 82 ++ libraries/ui/src/ui/Menu.cpp | 2 +- .../src/OculusLegacyDisplayPlugin.cpp | 2 +- plugins/openvr/src/OpenVrDisplayPlugin.cpp | 8 +- .../developer/libraries/jasmine/hifi-boot.js | 13 +- scripts/developer/tests/.gitignore | 1 + scripts/developer/tests/scaling.png | Bin 0 -> 3172 bytes .../tests/unit_tests/moduleTests/cycles/a.js | 10 + .../tests/unit_tests/moduleTests/cycles/b.js | 10 + .../unit_tests/moduleTests/cycles/main.js | 17 + .../entity/entityConstructorAPIException.js | 13 + .../entity/entityConstructorModule.js | 23 + .../entity/entityConstructorNested.js | 14 + .../entity/entityConstructorNested2.js | 25 + .../entityConstructorRequireException.js | 10 + .../entity/entityPreloadAPIError.js | 13 + .../entity/entityPreloadRequire.js | 11 + .../tests/unit_tests/moduleTests/example.json | 9 + .../moduleTests/exceptions/exception.js | 4 + .../exceptions/exceptionInFunction.js | 38 + .../tests/unit_tests/moduleUnitTests.js | 378 ++++++ .../developer/tests/unit_tests/package.json | 6 + .../tests/unit_tests/scriptUnitTests.js | 18 +- .../developer/utilities/record/recorder.js | 97 +- .../utilities/render/deferredLighting.qml | 3 +- scripts/modules/vec3.js | 69 ++ .../system/assets/images/icon-particles.svg | 29 + .../system/assets/images/icon-point-light.svg | 57 + .../system/assets/images/icon-spot-light.svg | 37 + scripts/system/controllers/teleport.js | 19 +- ...oggleAdvancedMovementForHandControllers.js | 13 +- scripts/system/edit.js | 134 ++- ...Manager.js => entityIconOverlayManager.js} | 51 +- .../system/libraries/entitySelectionTool.js | 8 +- scripts/system/libraries/toolBars.js | 4 + scripts/tutorials/entity_scripts/sit.js | 230 ++-- tests/ktx/CMakeLists.txt | 15 + tests/ktx/src/main.cpp | 150 +++ tests/render-perf/CMakeLists.txt | 2 +- tests/render-perf/src/main.cpp | 1 - tests/render-texture-load/src/main.cpp | 1 + tests/shared/src/StorageTests.cpp | 75 ++ tests/shared/src/StorageTests.h | 32 + tools/CMakeLists.txt | 2 + tools/atp-get/CMakeLists.txt | 3 + tools/atp-get/src/ATPGetApp.cpp | 269 +++++ tools/atp-get/src/ATPGetApp.h | 52 + tools/atp-get/src/main.cpp | 31 + .../marketplace/boppo/boppoClownEntity.js | 80 ++ .../marketplace/boppo/boppoServer.js | 303 +++++ .../marketplace/boppo/clownGloveDispenser.js | 154 +++ .../marketplace/boppo/createElBoppo.js | 430 +++++++ .../marketplace/boppo/lookAtEntity.js | 98 ++ 246 files changed, 9680 insertions(+), 6769 deletions(-) delete mode 100644 interface/resources/html/img/devices.png delete mode 100644 interface/resources/html/img/models.png delete mode 100644 interface/resources/html/img/move.png delete mode 100644 interface/resources/html/img/run-script.png delete mode 100644 interface/resources/html/img/talk.png delete mode 100644 interface/resources/html/img/write-script.png delete mode 100644 interface/resources/html/interface-welcome.html delete mode 100644 interface/resources/icons/load-script.svg delete mode 100644 interface/resources/icons/new-script.svg delete mode 100644 interface/resources/icons/save-script.svg delete mode 100644 interface/resources/icons/start-script.svg delete mode 100644 interface/resources/icons/stop-script.svg delete mode 100644 interface/src/ui/CachesSizeDialog.cpp delete mode 100644 interface/src/ui/CachesSizeDialog.h delete mode 100644 interface/src/ui/DiskCacheEditor.cpp delete mode 100644 interface/src/ui/DiskCacheEditor.h delete mode 100644 interface/src/ui/ScriptEditBox.cpp delete mode 100644 interface/src/ui/ScriptEditBox.h delete mode 100644 interface/src/ui/ScriptEditorWidget.cpp delete mode 100644 interface/src/ui/ScriptEditorWidget.h delete mode 100644 interface/src/ui/ScriptEditorWindow.cpp delete mode 100644 interface/src/ui/ScriptEditorWindow.h delete mode 100644 interface/src/ui/ScriptLineNumberArea.cpp delete mode 100644 interface/src/ui/ScriptLineNumberArea.h delete mode 100644 interface/src/ui/ScriptsTableWidget.cpp delete mode 100644 interface/src/ui/ScriptsTableWidget.h delete mode 100644 interface/ui/scriptEditorWidget.ui delete mode 100644 interface/ui/scriptEditorWindow.ui create mode 100644 libraries/fbx/src/OBJWriter.cpp create mode 100644 libraries/fbx/src/OBJWriter.h delete mode 100644 libraries/gpu-gl/src/gpu/gl/GLTextureTransfer.cpp delete mode 100644 libraries/gpu-gl/src/gpu/gl/GLTextureTransfer.h create mode 100644 libraries/gpu-gl/src/gpu/gl45/GL45BackendVariableTexture.cpp create mode 100644 libraries/gpu/src/gpu/Texture_ktx.cpp create mode 100644 libraries/ktx/CMakeLists.txt create mode 100644 libraries/ktx/src/ktx/KTX.cpp create mode 100644 libraries/ktx/src/ktx/KTX.h create mode 100644 libraries/ktx/src/ktx/Reader.cpp create mode 100644 libraries/ktx/src/ktx/Writer.cpp create mode 100644 libraries/model-networking/src/model-networking/KTXCache.cpp create mode 100644 libraries/model-networking/src/model-networking/KTXCache.h create mode 100644 libraries/networking/src/FileCache.cpp create mode 100644 libraries/networking/src/FileCache.h delete mode 100644 libraries/script-engine/src/BaseScriptEngine.h create mode 100644 libraries/script-engine/src/MeshProxy.h create mode 100644 libraries/script-engine/src/ModelScriptingInterface.cpp create mode 100644 libraries/script-engine/src/ModelScriptingInterface.h rename libraries/{script-engine => shared}/src/BaseScriptEngine.cpp (68%) create mode 100644 libraries/shared/src/BaseScriptEngine.h delete mode 100644 libraries/shared/src/ServerPathUtils.cpp delete mode 100644 libraries/shared/src/ServerPathUtils.h create mode 100644 libraries/shared/src/shared/Storage.cpp create mode 100644 libraries/shared/src/shared/Storage.h create mode 100644 scripts/developer/tests/.gitignore create mode 100644 scripts/developer/tests/scaling.png create mode 100644 scripts/developer/tests/unit_tests/moduleTests/cycles/a.js create mode 100644 scripts/developer/tests/unit_tests/moduleTests/cycles/b.js create mode 100644 scripts/developer/tests/unit_tests/moduleTests/cycles/main.js create mode 100644 scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorAPIException.js create mode 100644 scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorModule.js create mode 100644 scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested.js create mode 100644 scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested2.js create mode 100644 scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorRequireException.js create mode 100644 scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadAPIError.js create mode 100644 scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadRequire.js create mode 100644 scripts/developer/tests/unit_tests/moduleTests/example.json create mode 100644 scripts/developer/tests/unit_tests/moduleTests/exceptions/exception.js create mode 100644 scripts/developer/tests/unit_tests/moduleTests/exceptions/exceptionInFunction.js create mode 100644 scripts/developer/tests/unit_tests/moduleUnitTests.js create mode 100644 scripts/developer/tests/unit_tests/package.json create mode 100644 scripts/modules/vec3.js create mode 100644 scripts/system/assets/images/icon-particles.svg create mode 100644 scripts/system/assets/images/icon-point-light.svg create mode 100644 scripts/system/assets/images/icon-spot-light.svg rename scripts/system/libraries/{lightOverlayManager.js => entityIconOverlayManager.js} (67%) create mode 100644 tests/ktx/CMakeLists.txt create mode 100644 tests/ktx/src/main.cpp create mode 100644 tests/shared/src/StorageTests.cpp create mode 100644 tests/shared/src/StorageTests.h create mode 100644 tools/atp-get/CMakeLists.txt create mode 100644 tools/atp-get/src/ATPGetApp.cpp create mode 100644 tools/atp-get/src/ATPGetApp.h create mode 100644 tools/atp-get/src/main.cpp create mode 100644 unpublishedScripts/marketplace/boppo/boppoClownEntity.js create mode 100644 unpublishedScripts/marketplace/boppo/boppoServer.js create mode 100644 unpublishedScripts/marketplace/boppo/clownGloveDispenser.js create mode 100644 unpublishedScripts/marketplace/boppo/createElBoppo.js create mode 100644 unpublishedScripts/marketplace/boppo/lookAtEntity.js diff --git a/BUILD_WIN.md b/BUILD_WIN.md index 45373d3093..e37bf27503 100644 --- a/BUILD_WIN.md +++ b/BUILD_WIN.md @@ -1,104 +1,81 @@ -Please read the [general build guide](BUILD.md) for information on dependencies required for all platforms. Only Windows specific instructions are found in this file. +This is a stand-alone guide for creating your first High Fidelity build for Windows 64-bit. -Interface can be built as 32 or 64 bit. +###Step 1. Installing Visual Studio 2013 -###Visual Studio 2013 +If you don't already have the Community or Professional edition of Visual Studio 2013, download and install [Visual Studio Community 2013](https://www.visualstudio.com/en-us/news/releasenotes/vs2013-community-vs). You do not need to install any of the optional components when going through the installer. -You can use the Community or Professional editions of Visual Studio 2013. +Note: Newer versions of Visual Studio are not yet compatible. -You can start a Visual Studio 2013 command prompt using the shortcut provided in the Visual Studio Tools folder installed as part of Visual Studio 2013. +###Step 2. Installing CMake -Or you can start a regular command prompt and then run: +Download and install the CMake 3.8.0-rc2 "win64-x64 Installer" from the [CMake Website](https://cmake.org/download/). Make sure "Add CMake to system PATH for all users" is checked when going through the installer. - "%VS120COMNTOOLS%\vsvars32.bat" +###Step 3. Installing Qt -####Windows SDK 8.1 +Download and install the [Qt 5.6.1 Installer](https://download.qt.io/official_releases/qt/5.6/5.6.1-1/qt-opensource-windows-x86-msvc2013_64-5.6.1-1.exe). Please note that the download file is large (850MB) and may take some time. -If using Visual Studio 2013 and building as a Visual Studio 2013 project you need the Windows 8 SDK which you should already have as part of installing Visual Studio 2013. You should be able to see it at `C:\Program Files (x86)\Windows Kits\8.1\Lib\winv6.3\um\x86`. +Make sure to select all components when going through the installer. -####nmake +###Step 4. Setting Qt Environment Variable -Some of the external projects may require nmake to compile and install. If it is not installed at the location listed below, please ensure that it is in your PATH so CMake can find it when required. +Go to "Control Panel > System > Advanced System Settings > Environment Variables > New..." (or search “Environment Variables” in Start Search). +* Set "Variable name": QT_CMAKE_PREFIX_PATH +* Set "Variable value": `C:\Qt\Qt5.6.1\5.6\msvc2013_64\lib\cmake` -We expect nmake.exe to be located at the following path. +###Step 5. Installing OpenSSL - C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\bin +Download and install the "Win64 OpenSSL v1.0.2k" Installer from [this website](https://slproweb.com/products/Win32OpenSSL.html). -###Qt -You can use the online installer or the offline installer. If you use the offline installer, be sure to select the "OpenGL" version. - -* [Download the online installer](http://www.qt.io/download-open-source/#section-2) - * When it asks you to select components, ONLY select one of the following, 32- or 64-bit to match your build preference: - * Qt > Qt 5.6.1 > **msvc2013 32-bit** - * Qt > Qt 5.6.1 > **msvc2013 64-bit** - -* Download the offline installer, 32- or 64-bit to match your build preference: - * [32-bit](https://download.qt.io/official_releases/qt/5.6/5.6.1-1/qt-opensource-windows-x86-msvc2013-5.6.1-1.exe) - * [64-bit](https://download.qt.io/official_releases/qt/5.6/5.6.1-1/qt-opensource-windows-x86-msvc2013_64-5.6.1-1.exe) - -Once Qt is installed, you need to manually configure the following: -* Set the QT_CMAKE_PREFIX_PATH environment variable to your `Qt\5.6.1\msvc2013\lib\cmake` or `Qt\5.6.1\msvc2013_64\lib\cmake` directory. - * You can set an environment variable from Control Panel > System > Advanced System Settings > Environment Variables > New - -###External Libraries - -All libraries should be 32- or 64-bit to match your build preference. - -CMake will need to know where the headers and libraries for required external dependencies are. - -We use CMake's `fixup_bundle` to find the DLLs all of our executable targets require, and then copy them beside the executable in a post-build step. If `fixup_bundle` is having problems finding a DLL, you can fix it manually on your end by adding the folder containing that DLL to your path. Let us know which DLL CMake had trouble finding, as it is possible a tweak to our CMake files is required. - -The recommended route for CMake to find the external dependencies is to place all of the dependencies in one folder and set one ENV variable - HIFI_LIB_DIR. That ENV variable should point to a directory with the following structure: - - root_lib_dir - -> openssl - -> bin - -> include - -> lib - -For many of the external libraries where precompiled binaries are readily available you should be able to simply copy the extracted folder that you get from the download links provided at the top of the guide. Otherwise you may need to build from source and install the built product to this directory. The `root_lib_dir` in the above example can be wherever you choose on your system - as long as the environment variable HIFI_LIB_DIR is set to it. From here on, whenever you see %HIFI_LIB_DIR% you should substitute the directory that you chose. - -####OpenSSL - -Qt will use OpenSSL if it's available, but it doesn't install it, so you must install it separately. - -Your system may already have several versions of the OpenSSL DLL's (ssleay32.dll, libeay32.dll) lying around, but they may be the wrong version. If these DLL's are in the PATH then QT will try to use them, and if they're the wrong version then you will see the following errors in the console: - - QSslSocket: cannot resolve TLSv1_1_client_method - QSslSocket: cannot resolve TLSv1_2_client_method - QSslSocket: cannot resolve TLSv1_1_server_method - QSslSocket: cannot resolve TLSv1_2_server_method - QSslSocket: cannot resolve SSL_select_next_proto - QSslSocket: cannot resolve SSL_CTX_set_next_proto_select_cb - QSslSocket: cannot resolve SSL_get0_next_proto_negotiated - -To prevent these problems, install OpenSSL yourself. Download one of the following binary packages [from this website](https://slproweb.com/products/Win32OpenSSL.html): -* Win32 OpenSSL v1.0.1q -* Win64 OpenSSL v1.0.1q - -Install OpenSSL into the Windows system directory, to make sure that Qt uses the version that you've just installed, and not some other version. - -###Build High Fidelity using Visual Studio -Follow the same build steps from the CMake section of [BUILD.md](BUILD.md), but pass a different generator to CMake. - -For 32-bit builds: - - cmake .. -G "Visual Studio 12" - -For 64-bit builds: +###Step 6. Running CMake to Generate Build Files +Run Command Prompt from Start and run the following commands: + cd "%HIFI_DIR%" + mkdir build + cd build cmake .. -G "Visual Studio 12 Win64" + +Where %HIFI_DIR% is the directory for the highfidelity repository. -Open %HIFI_DIR%\build\hifi.sln and compile. +###Step 7. Making a Build -###Running Interface -If you need to debug Interface, you can run interface from within Visual Studio (see the section below). You can also run Interface by launching it from command line or File Explorer from %HIFI_DIR%\build\interface\Debug\interface.exe +Open '%HIFI_DIR%\build\hifi.sln' using Visual Studio. -###Debugging Interface -* In the Solution Explorer, right click interface and click Set as StartUp Project -* Set the "Working Directory" for the Interface debugging sessions to the Debug output directory so that your application can load resources. Do this: right click interface and click Properties, choose Debugging from Configuration Properties, set Working Directory to .\Debug -* Now you can run and debug interface through Visual Studio +Change the Solution Configuration (next to the green play button) from "Debug" to "Release" for best performance. -For better performance when running debug builds, set the environment variable ```_NO_DEBUG_HEAP``` to ```1``` +Run Build > Build Solution. + +###Step 8. Testing Interface + +Create another environment variable (see Step #4) +* Set "Variable name": _NO_DEBUG_HEAP +* Set "Variable value": 1 + +In Visual Studio, right+click "interface" under the Apps folder in Solution Explorer and select "Set as Startup Project". Run Debug > Start Debugging. + +Now, you should have a full build of High Fidelity and be able to run the Interface using Visual Studio. Please check our [Docs](https://wiki.highfidelity.com/wiki/Main_Page) for more information regarding the programming workflow. + +Note: You can also run Interface by launching it from command line or File Explorer from %HIFI_DIR%\build\interface\Release\interface.exe + +###Troubleshooting + +For any problems after Step #6, first try this: +* Delete your locally cloned copy of the highfidelity repository +* Restart your computer +* Redownload the [repository](https://github.com/highfidelity/hifi) +* Restart directions from Step #6 + +####CMake gives you the same error message repeatedly after the build fails + +Remove `CMakeCache.txt` found in the '%HIFI_DIR%\build' directory + +####nmake cannot be found + +Make sure nmake.exe is located at the following path: + C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\bin + +If not, add the directory where nmake is located to the PATH environment variable. + +####Qt is throwing an error + +Make sure you have the correct version (5.6.1-1) installed and 'QT_CMAKE_PREFIX_PATH' environment variable is set correctly. -http://preshing.com/20110717/the-windows-heap-is-slow-when-launched-from-the-debugger/ diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index be23dcfa25..a0c80453e0 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -371,25 +371,39 @@ void Agent::executeScript() { using namespace recording; static const FrameType AUDIO_FRAME_TYPE = Frame::registerFrameType(AudioConstants::getAudioFrameName()); Frame::registerFrameHandler(AUDIO_FRAME_TYPE, [this, &scriptedAvatar](Frame::ConstPointer frame) { - const QByteArray& audio = frame->data; static quint16 audioSequenceNumber{ 0 }; - Transform audioTransform; + QByteArray audio(frame->data); + + if (_isNoiseGateEnabled) { + static int numSamples = AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL; + _noiseGate.gateSamples(reinterpret_cast(audio.data()), numSamples); + } + + computeLoudness(&audio, scriptedAvatar); + + // the codec needs a flush frame before sending silent packets, so + // do not send one if the gate closed in this block (eventually this can be crossfaded). + auto packetType = PacketType::MicrophoneAudioNoEcho; + if (scriptedAvatar->getAudioLoudness() == 0.0f && !_noiseGate.closedInLastBlock()) { + packetType = PacketType::SilentAudioFrame; + } + + Transform audioTransform; auto headOrientation = scriptedAvatar->getHeadOrientation(); audioTransform.setTranslation(scriptedAvatar->getPosition()); audioTransform.setRotation(headOrientation); - computeLoudness(&audio, scriptedAvatar); - QByteArray encodedBuffer; if (_encoder) { _encoder->encode(audio, encodedBuffer); } else { encodedBuffer = audio; } + AbstractAudioInterface::emitAudioPacket(encodedBuffer.data(), encodedBuffer.size(), audioSequenceNumber, audioTransform, scriptedAvatar->getPosition(), glm::vec3(0), - PacketType::MicrophoneAudioNoEcho, _selectedCodecName); + packetType, _selectedCodecName); }); auto avatarHashMap = DependencyManager::set(); @@ -483,6 +497,14 @@ void Agent::setIsListeningToAudioStream(bool isListeningToAudioStream) { _isListeningToAudioStream = isListeningToAudioStream; } +void Agent::setIsNoiseGateEnabled(bool isNoiseGateEnabled) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setIsNoiseGateEnabled", Q_ARG(bool, isNoiseGateEnabled)); + return; + } + _isNoiseGateEnabled = isNoiseGateEnabled; +} + void Agent::setIsAvatar(bool isAvatar) { // this must happen on Agent's main thread if (QThread::currentThread() != thread()) { diff --git a/assignment-client/src/Agent.h b/assignment-client/src/Agent.h index 0ce7b71d5d..620ac8e047 100644 --- a/assignment-client/src/Agent.h +++ b/assignment-client/src/Agent.h @@ -29,6 +29,7 @@ #include +#include "AudioNoiseGate.h" #include "MixedAudioStream.h" #include "avatars/ScriptableAvatar.h" @@ -38,6 +39,7 @@ class Agent : public ThreadedAssignment { Q_PROPERTY(bool isAvatar READ isAvatar WRITE setIsAvatar) Q_PROPERTY(bool isPlayingAvatarSound READ isPlayingAvatarSound) Q_PROPERTY(bool isListeningToAudioStream READ isListeningToAudioStream WRITE setIsListeningToAudioStream) + Q_PROPERTY(bool isNoiseGateEnabled READ isNoiseGateEnabled WRITE setIsNoiseGateEnabled) Q_PROPERTY(float lastReceivedAudioLoudness READ getLastReceivedAudioLoudness) Q_PROPERTY(QUuid sessionUUID READ getSessionUUID) @@ -52,6 +54,9 @@ public: bool isListeningToAudioStream() const { return _isListeningToAudioStream; } void setIsListeningToAudioStream(bool isListeningToAudioStream); + bool isNoiseGateEnabled() const { return _isNoiseGateEnabled; } + void setIsNoiseGateEnabled(bool isNoiseGateEnabled); + float getLastReceivedAudioLoudness() const { return _lastReceivedAudioLoudness; } QUuid getSessionUUID() const; @@ -106,6 +111,9 @@ private: QTimer* _avatarIdentityTimer = nullptr; QHash _outgoingScriptAudioSequenceNumbers; + AudioNoiseGate _noiseGate; + bool _isNoiseGateEnabled { false }; + CodecPluginPointer _codec; QString _selectedCodecName; Encoder* _encoder { nullptr }; diff --git a/assignment-client/src/assets/AssetServer.cpp b/assignment-client/src/assets/AssetServer.cpp index 82dd23a9de..3886ff8d92 100644 --- a/assignment-client/src/assets/AssetServer.cpp +++ b/assignment-client/src/assets/AssetServer.cpp @@ -24,7 +24,7 @@ #include #include -#include +#include #include "NetworkLogging.h" #include "NodeType.h" @@ -162,7 +162,7 @@ void AssetServer::completeSetup() { if (assetsPath.isRelative()) { // if the domain settings passed us a relative path, make an absolute path that is relative to the // default data directory - absoluteFilePath = ServerPathUtils::getDataFilePath("assets/" + assetsPathString); + absoluteFilePath = PathUtils::getAppDataFilePath("assets/" + assetsPathString); } _resourcesDirectory = QDir(absoluteFilePath); diff --git a/assignment-client/src/octree/OctreeServer.cpp b/assignment-client/src/octree/OctreeServer.cpp index 2eee2ee229..f2dbe5d1d2 100644 --- a/assignment-client/src/octree/OctreeServer.cpp +++ b/assignment-client/src/octree/OctreeServer.cpp @@ -29,7 +29,7 @@ #include "OctreeQueryNode.h" #include "OctreeServerConsts.h" #include -#include +#include #include int OctreeServer::_clientCount = 0; @@ -279,8 +279,7 @@ OctreeServer::~OctreeServer() { void OctreeServer::initHTTPManager(int port) { // setup the embedded web server - - QString documentRoot = QString("%1/web").arg(ServerPathUtils::getDataDirectory()); + QString documentRoot = QString("%1/web").arg(PathUtils::getAppDataPath()); // setup an httpManager with us as the request handler and the parent _httpManager = new HTTPManager(QHostAddress::AnyIPv4, port, documentRoot, this, this); @@ -1179,7 +1178,7 @@ void OctreeServer::domainSettingsRequestComplete() { if (persistPath.isRelative()) { // if the domain settings passed us a relative path, make an absolute path that is relative to the // default data directory - persistAbsoluteFilePath = QDir(ServerPathUtils::getDataFilePath("entities/")).absoluteFilePath(_persistFilePath); + persistAbsoluteFilePath = QDir(PathUtils::getAppDataFilePath("entities/")).absoluteFilePath(_persistFilePath); } static const QString ENTITY_PERSIST_EXTENSION = ".json.gz"; @@ -1245,7 +1244,7 @@ void OctreeServer::domainSettingsRequestComplete() { QDir backupDirectory { _backupDirectoryPath }; QString absoluteBackupDirectory; if (backupDirectory.isRelative()) { - absoluteBackupDirectory = QDir(ServerPathUtils::getDataFilePath("entities/")).absoluteFilePath(_backupDirectoryPath); + absoluteBackupDirectory = QDir(PathUtils::getAppDataFilePath("entities/")).absoluteFilePath(_backupDirectoryPath); absoluteBackupDirectory = QDir(absoluteBackupDirectory).absolutePath(); } else { absoluteBackupDirectory = backupDirectory.absolutePath(); diff --git a/assignment-client/src/scripts/EntityScriptServer.cpp b/assignment-client/src/scripts/EntityScriptServer.cpp index 47071b10b7..954c25a342 100644 --- a/assignment-client/src/scripts/EntityScriptServer.cpp +++ b/assignment-client/src/scripts/EntityScriptServer.cpp @@ -58,6 +58,8 @@ EntityScriptServer::EntityScriptServer(ReceivedMessage& message) : ThreadedAssig DependencyManager::registerInheritance(); + DependencyManager::set(); + DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); @@ -324,7 +326,26 @@ void EntityScriptServer::nodeActivated(SharedNodePointer activatedNode) { void EntityScriptServer::nodeKilled(SharedNodePointer killedNode) { switch (killedNode->getType()) { case NodeType::EntityServer: { - clear(); + // Before we clear, make sure this was our only entity server. + // Otherwise we're assuming that we have "trading" entity servers + // (an old one going away and a new one coming onboard) + // and that we shouldn't clear here because we're still doing work. + bool hasAnotherEntityServer = false; + auto nodeList = DependencyManager::get(); + + nodeList->eachNodeBreakable([&hasAnotherEntityServer, &killedNode](const SharedNodePointer& node){ + if (node->getType() == NodeType::EntityServer && node->getUUID() != killedNode->getUUID()) { + // we're talking to > 1 entity servers, we know we won't clear + hasAnotherEntityServer = true; + return false; + } + + return true; + }); + + if (!hasAnotherEntityServer) { + clear(); + } break; } @@ -395,7 +416,8 @@ void EntityScriptServer::selectAudioFormat(const QString& selectedCodecName) { void EntityScriptServer::resetEntitiesScriptEngine() { auto engineName = QString("about:Entities %1").arg(++_entitiesScriptEngineCount); - auto newEngine = QSharedPointer(new ScriptEngine(ScriptEngine::ENTITY_SERVER_SCRIPT, NO_SCRIPT, engineName)); + auto newEngine = QSharedPointer(new ScriptEngine(ScriptEngine::ENTITY_SERVER_SCRIPT, NO_SCRIPT, engineName), + &ScriptEngine::deleteLater); auto webSocketServerConstructorValue = newEngine->newFunction(WebSocketServerClass::constructor); newEngine->globalObject().setProperty("WebSocketServer", webSocketServerConstructorValue); @@ -455,13 +477,13 @@ void EntityScriptServer::addingEntity(const EntityItemID& entityID) { void EntityScriptServer::deletingEntity(const EntityItemID& entityID) { if (_entityViewer.getTree() && !_shuttingDown && _entitiesScriptEngine) { - _entitiesScriptEngine->unloadEntityScript(entityID); + _entitiesScriptEngine->unloadEntityScript(entityID, true); } } void EntityScriptServer::entityServerScriptChanging(const EntityItemID& entityID, const bool reload) { if (_entityViewer.getTree() && !_shuttingDown) { - _entitiesScriptEngine->unloadEntityScript(entityID); + _entitiesScriptEngine->unloadEntityScript(entityID, true); checkAndCallPreload(entityID, reload); } } diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index c741c22b83..620b11d8ad 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -38,7 +38,7 @@ #include #include #include -#include +#include #include #include "DomainServerNodeData.h" @@ -1618,7 +1618,7 @@ QJsonObject DomainServer::jsonObjectForNode(const SharedNodePointer& node) { QDir pathForAssignmentScriptsDirectory() { static const QString SCRIPTS_DIRECTORY_NAME = "/scripts/"; - QDir directory(ServerPathUtils::getDataDirectory() + SCRIPTS_DIRECTORY_NAME); + QDir directory(PathUtils::getAppDataPath() + SCRIPTS_DIRECTORY_NAME); if (!directory.exists()) { directory.mkpath("."); qInfo() << "Created path to " << directory.path(); diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index 661a6213b8..d6b57b450a 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -246,10 +246,13 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList _agentPermissions[editorKey]->set(NodePermissions::Permission::canAdjustLocks); } - QList> permissionsSets; - permissionsSets << _standardAgentPermissions.get() << _agentPermissions.get(); + std::list> permissionsSets{ + _standardAgentPermissions.get(), + _agentPermissions.get() + }; foreach (auto permissionsSet, permissionsSets) { - foreach (NodePermissionsKey userKey, permissionsSet.keys()) { + for (auto entry : permissionsSet) { + const auto& userKey = entry.first; if (onlyEditorsAreRezzers) { if (permissionsSet[userKey]->can(NodePermissions::Permission::canAdjustLocks)) { permissionsSet[userKey]->set(NodePermissions::Permission::canRezPermanentEntities); @@ -300,7 +303,6 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList } QVariantMap& DomainServerSettingsManager::getDescriptorsMap() { - static const QString DESCRIPTORS{ "descriptors" }; auto& settingsMap = getSettingsMap(); @@ -1355,18 +1357,12 @@ QStringList DomainServerSettingsManager::getAllKnownGroupNames() { // extract all the group names from the group-permissions and group-forbiddens settings QSet result; - QHashIterator i(_groupPermissions.get()); - while (i.hasNext()) { - i.next(); - NodePermissionsKey key = i.key(); - result += key.first; + for (const auto& entry : _groupPermissions.get()) { + result += entry.first.first; } - QHashIterator j(_groupForbiddens.get()); - while (j.hasNext()) { - j.next(); - NodePermissionsKey key = j.key(); - result += key.first; + for (const auto& entry : _groupForbiddens.get()) { + result += entry.first.first; } return result.toList(); @@ -1377,20 +1373,17 @@ bool DomainServerSettingsManager::setGroupID(const QString& groupName, const QUu _groupIDs[groupName.toLower()] = groupID; _groupNames[groupID] = groupName; - QHashIterator i(_groupPermissions.get()); - while (i.hasNext()) { - i.next(); - NodePermissionsPointer perms = i.value(); + + for (const auto& entry : _groupPermissions.get()) { + auto& perms = entry.second; if (perms->getID().toLower() == groupName.toLower() && !perms->isGroup()) { changed = true; perms->setGroupID(groupID); } } - QHashIterator j(_groupForbiddens.get()); - while (j.hasNext()) { - j.next(); - NodePermissionsPointer perms = j.value(); + for (const auto& entry : _groupForbiddens.get()) { + auto& perms = entry.second; if (perms->getID().toLower() == groupName.toLower() && !perms->isGroup()) { changed = true; perms->setGroupID(groupID); diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index dbc484d0b9..87048d752c 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -189,7 +189,7 @@ endif() # link required hifi libraries link_hifi_libraries( - shared octree gpu gl gpu-gl procedural model render + shared octree ktx gpu gl gpu-gl procedural model render recording fbx networking model-networking entities avatars audio audio-client animation script-engine physics render-utils entities-renderer ui auto-updater diff --git a/interface/resources/controllers/standard.json b/interface/resources/controllers/standard.json index 04a3f560b6..9e3b2f4d13 100644 --- a/interface/resources/controllers/standard.json +++ b/interface/resources/controllers/standard.json @@ -2,7 +2,27 @@ "name": "Standard to Action", "channels": [ { "from": "Standard.LY", "to": "Actions.TranslateZ" }, - { "from": "Standard.LX", "to": "Actions.TranslateX" }, + + { "from": "Standard.LX", + "when": [ + "Application.InHMD", "!Application.AdvancedMovement", + "Application.SnapTurn", "!Standard.RX" + ], + "to": "Actions.StepYaw", + "filters": + [ + { "type": "deadZone", "min": 0.15 }, + "constrainToInteger", + { "type": "pulse", "interval": 0.25 }, + { "type": "scale", "scale": 22.5 } + ] + }, + { "from": "Standard.LX", "to": "Actions.TranslateX", + "when": [ "Application.AdvancedMovement" ] + }, + { "from": "Standard.LX", "to": "Actions.Yaw", + "when": [ "!Application.AdvancedMovement", "!Application.SnapTurn" ] + }, { "from": "Standard.RX", "when": [ "Application.InHMD", "Application.SnapTurn" ], @@ -15,29 +35,29 @@ { "type": "scale", "scale": 22.5 } ] }, + { "from": "Standard.RX", "to": "Actions.Yaw", + "when": [ "!Application.SnapTurn" ] + }, - { "from": "Standard.RX", "to": "Actions.Yaw" }, - { "from": "Standard.RY", - "when": "Application.Grounded", - "to": "Actions.Up", - "filters": + { "from": "Standard.RY", + "when": "Application.Grounded", + "to": "Actions.Up", + "filters": [ { "type": "deadZone", "min": 0.6 }, "invert" ] - }, + }, - { "from": "Standard.RY", "to": "Actions.Up", "filters": "invert"}, + { "from": "Standard.RY", "to": "Actions.Up", "filters": "invert"}, { "from": "Standard.Back", "to": "Actions.CycleCamera" }, { "from": "Standard.Start", "to": "Actions.ContextMenu" }, - { "from": "Standard.LT", "to": "Actions.LeftHandClick" }, + { "from": "Standard.LT", "to": "Actions.LeftHandClick" }, { "from": "Standard.RT", "to": "Actions.RightHandClick" }, - { "from": "Standard.LeftHand", "to": "Actions.LeftHand" }, + { "from": "Standard.LeftHand", "to": "Actions.LeftHand" }, { "from": "Standard.RightHand", "to": "Actions.RightHand" } ] } - - diff --git a/interface/resources/html/img/devices.png b/interface/resources/html/img/devices.png deleted file mode 100644 index fc4231e96e25732a0659c911e7c15ded5b54911b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7492 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 diff --git a/interface/resources/html/img/models.png b/interface/resources/html/img/models.png deleted file mode 100644 index b09c36011d540759da5326ed70bda97592e9636b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8664 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 diff --git a/interface/resources/html/img/run-script.png b/interface/resources/html/img/run-script.png deleted file mode 100644 index 941b8ee9f13664fc2b2ec51a75273ba3e553d3a5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4873 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 diff --git a/interface/resources/html/img/write-script.png b/interface/resources/html/img/write-script.png deleted file mode 100644 index dae97e59b14bae50013b583cd9cb3b5bdf6ad464..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2006 zcmb`HX*Ao38pi)h(Ww^WC~;|$Yo=Oy6}h!H%2>+{#aNnJBh9EKC6-vKrnL4lqlR8f z+CxLd)*4blsO1QWsHU;S(vqMgmZY&S_c%TCW#-;9=gheuo-gltK0NP--!H=jVRz~) zwXXmGIOSk(?Fs;Z5CDKBS!n`!Um!t4q*Wpv@1Fw zC@!KOZ4Ll3cn51scl-o@sspQp0s1~_21*qu6P+5Qx8)(@64A${yN;nQG-&a!vK46|YxiCC+UCW# z?78c1pDn_!(s`qA{2QK}Osh^O6w)&nq|Y#8K%!6GZ2QSKTb`%$ z3fZ>{+WbzhbKE8?t5!s}?(f#PZ^l`Cy89ua>7VGH@y;Zlu%JU%-FpS2``0zP)S?M~ zYMcE+3MG7Uj=OMBeYTuBMD$bP8*mLO*t0C(S41!!jdI!=H4CS}wHew%@uOnH0Z!6q zos_8@nng~FU`KAV#e8;03_6sDa8IZg`}c(Z%?KG9_pr}Qda$=!#5pMW2uHg4cCb2Rtjhd*rqmR@Ph0qaA_#**$5H#z#5Q?MJ=)Ym$FZY9?% zS|;<*8MQ}Nr2o58>dbfJE24Op%Qi~X%CMlTv`)|WljBXHaM;DiJZ4^&B916Q8tH`B z-RQPdyR4W#97+6;nfFACb8g?Ma&$S41a8QYlLaXlK!T2Pm6n`mNlVU?fJ$0PA0<^z z7IYK^1HETh;ov5s#Q3_pMbh3JFLrfW89PH2b8Kmi@!LWkWApCXhc|bgSl5s0FiXum zOH6P?>2-|Y#)3HGd^xH%B$d%Dc>UAX!Tv6x z))ph)9&g0hbj*MckS!%+&ZVZ86Ki7DK>eB|4uq?38_etv!)W7hRZ-DI{NAdSd0U7JdT(r6szh|#Qp-YgZ1M|+?autgHT9)N;V z*=+WZE+#tuUAJQfygZURO})$U1FYaQ#g_IR^PIiC=Z=LB^;7cX?RSkMaK*8%=h7iu z$&u;eD}s+&3PSZCR;TJP(Ld&?*l!q;$yn^<>-AoQo`O)_u}9EtJ04EOEg&L9^MlNm z5eV1hasGwKu@{1_2Pb>;wVWSXVm4G{EX(7_nALToi@caH&RYn`95W@l$;-i zzxg}V%;`a%N@yzc?8*kM#E35mz2wCZcn2UTS7|4!+U+>gA*tq?>zzglk-I){VpB1! zv@IRN)o`N7;Ek}O`C$`Ii$&tzr`v{YKBS2?pR^Js(v*Jz`me?trK~fp%{Z8;)?)BvYwS>KaG-Vg!;)==52-4{=z4A@{ucqO zr$jL4JyU49Nq10&i%vjed#)3d+T8q2FnBT)9piD=py#!GLdYRlBx)gCVJAACE-2@9 z_|1WtF~PR8^M)a_FACc|?wxgSmv$j-rr)dWLl4yybP%YO0g=3!ZBa!thC}mtHPA*# zi(Yc}ks^)!S2T~f{lcyUH02Assd8V~RWSGh_g^XeM*g3w;SU8j - - - - - Welcome to Interface - - - - - -
-
-

Move around

- Move around -

- Move around with WASD & fly
- up or down with E & C.
- Cmnd/Ctrl+G will send you
- home. Hitting Enter will let you
- teleport to a user or location. -

-
-
-

Listen & talk

- Talk -

- Use your best headphones
- and microphone for high
- fidelity audio. -

-
-
-

Connect devices

- Connect devices -

- Have an Oculus Rift, a Razer
- Hydra, or a PrimeSense 3D
- camera? We support them all. -

-
-
-

Run a script

- Run a script -

- Cmnd/Cntrl+J will launch a
- Running Scripts dialog to help
- manage your scripts and search
- for new ones to run. -

-
-
-

Script something

- Write a script -

- Write a script; we're always
- adding new features.
- Cmnd/Cntrl+J will launch a
- Running Scripts dialog to help
- manage your scripts. -

-
-
-

Import models

- Import models -

- Use the edit.js script to
- add FBX models in-world. You
- can use grids and fine tune
- placement-related parameters
- with ease. -

-
-
-
-

Read the docs

-

- We are writing documentation on
- just about everything. Please,
- devour all we've written and make
- suggestions where necessary.
- Documentation is always at
- docs.highfidelity.com -

-
-
-
- - - - - diff --git a/interface/resources/icons/load-script.svg b/interface/resources/icons/load-script.svg deleted file mode 100644 index 21be61c321..0000000000 --- a/interface/resources/icons/load-script.svg +++ /dev/null @@ -1,125 +0,0 @@ - - - - - - - - - - image/svg+xml - - - - - T.Hofmeister - - - - - - - - - - - - - - - - - - - - - - diff --git a/interface/resources/icons/new-script.svg b/interface/resources/icons/new-script.svg deleted file mode 100644 index f68fcfa967..0000000000 --- a/interface/resources/icons/new-script.svg +++ /dev/null @@ -1,129 +0,0 @@ - - - - - - - - - - image/svg+xml - - - - - T.Hofmeister - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/interface/resources/icons/save-script.svg b/interface/resources/icons/save-script.svg deleted file mode 100644 index 04d41b8302..0000000000 --- a/interface/resources/icons/save-script.svg +++ /dev/null @@ -1,674 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - T.Hofmeister - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/interface/resources/icons/start-script.svg b/interface/resources/icons/start-script.svg deleted file mode 100644 index 994eb61efe..0000000000 --- a/interface/resources/icons/start-script.svg +++ /dev/null @@ -1,550 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - Maximillian Merlin - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/interface/resources/icons/stop-script.svg b/interface/resources/icons/stop-script.svg deleted file mode 100644 index 31cdcee749..0000000000 --- a/interface/resources/icons/stop-script.svg +++ /dev/null @@ -1,163 +0,0 @@ - - - - - - - - - - image/svg+xml - - - - - Maximillian Merlin - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/interface/resources/qml/AvatarInputs.qml b/interface/resources/qml/AvatarInputs.qml index 384504aaa0..28f3c0c7b9 100644 --- a/interface/resources/qml/AvatarInputs.qml +++ b/interface/resources/qml/AvatarInputs.qml @@ -15,12 +15,11 @@ import Qt.labs.settings 1.0 Hifi.AvatarInputs { id: root objectName: "AvatarInputs" - width: mirrorWidth - height: controls.height + mirror.height + width: rootWidth + height: controls.height x: 10; y: 5 - readonly property int mirrorHeight: 215 - readonly property int mirrorWidth: 265 + readonly property int rootWidth: 265 readonly property int iconSize: 24 readonly property int iconPadding: 5 @@ -39,61 +38,15 @@ Hifi.AvatarInputs { anchors.fill: parent } - Item { - id: mirror - width: root.mirrorWidth - height: root.mirrorVisible ? root.mirrorHeight : 0 - visible: root.mirrorVisible - anchors.left: parent.left - clip: true - - Image { - id: closeMirror - visible: hover.containsMouse - width: root.iconSize - height: root.iconSize - anchors.top: parent.top - anchors.topMargin: root.iconPadding - anchors.left: parent.left - anchors.leftMargin: root.iconPadding - source: "../images/close.svg" - MouseArea { - anchors.fill: parent - onClicked: { - root.closeMirror(); - } - } - } - - Image { - id: zoomIn - visible: hover.containsMouse - width: root.iconSize - height: root.iconSize - anchors.bottom: parent.bottom - anchors.bottomMargin: root.iconPadding - anchors.left: parent.left - anchors.leftMargin: root.iconPadding - source: root.mirrorZoomed ? "../images/minus.svg" : "../images/plus.svg" - MouseArea { - anchors.fill: parent - onClicked: { - root.toggleZoom(); - } - } - } - } - Item { id: controls - width: root.mirrorWidth + width: root.rootWidth height: 44 visible: root.showAudioTools - anchors.top: mirror.bottom Rectangle { anchors.fill: parent - color: root.mirrorVisible ? (root.audioClipping ? "red" : "#696969") : "#00000000" + color: "#00000000" Item { id: audioMeter diff --git a/interface/resources/qml/Stats.qml b/interface/resources/qml/Stats.qml index 564c74b526..17e6578e4d 100644 --- a/interface/resources/qml/Stats.qml +++ b/interface/resources/qml/Stats.qml @@ -198,7 +198,7 @@ Item { } StatText { visible: root.expanded; - text: "Audio Out Mic: " + root.audioMicOutboundPPS + " pps, " + + text: "Audio Out Mic: " + root.audioOutboundPPS + " pps, " + "Silent: " + root.audioSilentOutboundPPS + " pps"; } StatText { @@ -266,7 +266,7 @@ Item { text: "GPU Textures: "; } StatText { - text: " Sparse Enabled: " + (0 == root.gpuSparseTextureEnabled ? "false" : "true"); + text: " Pressure State: " + root.gpuTextureMemoryPressureState; } StatText { text: " Count: " + root.gpuTextures; @@ -278,14 +278,10 @@ Item { text: " Decimated: " + root.decimatedTextureCount; } StatText { - text: " Sparse Count: " + root.gpuTexturesSparse; - visible: 0 != root.gpuSparseTextureEnabled; + text: " Pending Transfer: " + root.texturePendingTransfers + " MB"; } StatText { - text: " Virtual Memory: " + root.gpuTextureVirtualMemory + " MB"; - } - StatText { - text: " Commited Memory: " + root.gpuTextureMemory + " MB"; + text: " Resource Memory: " + root.gpuTextureMemory + " MB"; } StatText { text: " Framebuffer Memory: " + root.gpuTextureFramebufferMemory + " MB"; diff --git a/interface/resources/styles/log_dialog.qss b/interface/resources/styles/log_dialog.qss index 1fc4df0717..d3ae4e0a00 100644 --- a/interface/resources/styles/log_dialog.qss +++ b/interface/resources/styles/log_dialog.qss @@ -1,6 +1,6 @@ QPlainTextEdit { - font-family: Inconsolata, Lucida Console, Andale Mono, Monaco; + font-family: Inconsolata, Consolas, Courier New, monospace; font-size: 16px; padding-left: 28px; padding-top: 7px; @@ -11,7 +11,7 @@ QPlainTextEdit { } QLineEdit { - font-family: Inconsolata, Lucida Console, Andale Mono, Monaco; + font-family: Inconsolata, Consolas, Courier New, monospace; padding-left: 7px; background-color: #CCCCCC; border-width: 0; diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 1bb4c64884..f1e771866f 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -177,6 +177,8 @@ #include "FrameTimingsScriptingInterface.h" #include #include +#include +#include // On Windows PC, NVidia Optimus laptop, we want to enable NVIDIA GPU // FIXME seems to be broken. @@ -213,18 +215,10 @@ static const QString FBX_EXTENSION = ".fbx"; static const QString OBJ_EXTENSION = ".obj"; static const QString AVA_JSON_EXTENSION = ".ava.json"; -static const int MIRROR_VIEW_TOP_PADDING = 5; -static const int MIRROR_VIEW_LEFT_PADDING = 10; -static const int MIRROR_VIEW_WIDTH = 265; -static const int MIRROR_VIEW_HEIGHT = 215; static const float MIRROR_FULLSCREEN_DISTANCE = 0.389f; -static const float MIRROR_REARVIEW_DISTANCE = 0.722f; -static const float MIRROR_REARVIEW_BODY_DISTANCE = 2.56f; -static const float MIRROR_FIELD_OF_VIEW = 30.0f; static const quint64 TOO_LONG_SINCE_LAST_SEND_DOWNSTREAM_AUDIO_STATS = 1 * USECS_PER_SECOND; -static const QString INFO_WELCOME_PATH = "html/interface-welcome.html"; static const QString INFO_EDIT_ENTITIES_PATH = "html/edit-commands.html"; static const QString INFO_HELP_PATH = "html/help.html"; @@ -423,6 +417,7 @@ static const QString STATE_CAMERA_THIRD_PERSON = "CameraThirdPerson"; static const QString STATE_CAMERA_ENTITY = "CameraEntity"; static const QString STATE_CAMERA_INDEPENDENT = "CameraIndependent"; static const QString STATE_SNAP_TURN = "SnapTurn"; +static const QString STATE_ADVANCED_MOVEMENT_CONTROLS = "AdvancedMovement"; static const QString STATE_GROUNDED = "Grounded"; static const QString STATE_NAV_FOCUSED = "NavigationFocused"; @@ -513,7 +508,7 @@ bool setupEssentials(int& argc, char** argv) { DependencyManager::set(); controller::StateController::setStateVariables({ { STATE_IN_HMD, STATE_CAMERA_FULL_SCREEN_MIRROR, STATE_CAMERA_FIRST_PERSON, STATE_CAMERA_THIRD_PERSON, STATE_CAMERA_ENTITY, STATE_CAMERA_INDEPENDENT, - STATE_SNAP_TURN, STATE_GROUNDED, STATE_NAV_FOCUSED } }); + STATE_SNAP_TURN, STATE_ADVANCED_MOVEMENT_CONTROLS, STATE_GROUNDED, STATE_NAV_FOCUSED } }); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); @@ -565,7 +560,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo _entityClipboardRenderer(false, this, this), _entityClipboard(new EntityTree()), _lastQueriedTime(usecTimestampNow()), - _mirrorViewRect(QRect(MIRROR_VIEW_LEFT_PADDING, MIRROR_VIEW_TOP_PADDING, MIRROR_VIEW_WIDTH, MIRROR_VIEW_HEIGHT)), _previousScriptLocation("LastScriptLocation", DESKTOP_LOCATION), _fieldOfView("fieldOfView", DEFAULT_FIELD_OF_VIEW_DEGREES), _hmdTabletScale("hmdTabletScale", DEFAULT_HMD_TABLET_SCALE_PERCENT), @@ -746,23 +740,24 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo } }); - auto& audioScriptingInterface = AudioScriptingInterface::getInstance(); + auto audioScriptingInterface = DependencyManager::set(); connect(audioThread, &QThread::started, audioIO.data(), &AudioClient::start); connect(audioIO.data(), &AudioClient::destroyed, audioThread, &QThread::quit); connect(audioThread, &QThread::finished, audioThread, &QThread::deleteLater); connect(audioIO.data(), &AudioClient::muteToggled, this, &Application::audioMuteToggled); - connect(audioIO.data(), &AudioClient::mutedByMixer, &audioScriptingInterface, &AudioScriptingInterface::mutedByMixer); - connect(audioIO.data(), &AudioClient::receivedFirstPacket, &audioScriptingInterface, &AudioScriptingInterface::receivedFirstPacket); - connect(audioIO.data(), &AudioClient::disconnected, &audioScriptingInterface, &AudioScriptingInterface::disconnected); + connect(audioIO.data(), &AudioClient::mutedByMixer, audioScriptingInterface.data(), &AudioScriptingInterface::mutedByMixer); + connect(audioIO.data(), &AudioClient::receivedFirstPacket, audioScriptingInterface.data(), &AudioScriptingInterface::receivedFirstPacket); + connect(audioIO.data(), &AudioClient::disconnected, audioScriptingInterface.data(), &AudioScriptingInterface::disconnected); connect(audioIO.data(), &AudioClient::muteEnvironmentRequested, [](glm::vec3 position, float radius) { auto audioClient = DependencyManager::get(); + auto audioScriptingInterface = DependencyManager::get(); auto myAvatarPosition = DependencyManager::get()->getMyAvatar()->getPosition(); float distance = glm::distance(myAvatarPosition, position); bool shouldMute = !audioClient->isMuted() && (distance < radius); if (shouldMute) { audioClient->toggleMute(); - AudioScriptingInterface::getInstance().environmentMuted(); + audioScriptingInterface->environmentMuted(); } }); @@ -1129,6 +1124,10 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo _applicationStateDevice->setInputVariant(STATE_SNAP_TURN, []() -> float { return qApp->getMyAvatar()->getSnapTurn() ? 1 : 0; }); + _applicationStateDevice->setInputVariant(STATE_ADVANCED_MOVEMENT_CONTROLS, []() -> float { + return qApp->getMyAvatar()->useAdvancedMovementControls() ? 1 : 0; + }); + _applicationStateDevice->setInputVariant(STATE_GROUNDED, []() -> float { return qApp->getMyAvatar()->getCharacterController()->onGround() ? 1 : 0; }); @@ -1183,10 +1182,10 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo // set the local loopback interface for local sounds AudioInjector::setLocalAudioInterface(audioIO.data()); - AudioScriptingInterface::getInstance().setLocalAudioInterface(audioIO.data()); - connect(audioIO.data(), &AudioClient::noiseGateOpened, &AudioScriptingInterface::getInstance(), &AudioScriptingInterface::noiseGateOpened); - connect(audioIO.data(), &AudioClient::noiseGateClosed, &AudioScriptingInterface::getInstance(), &AudioScriptingInterface::noiseGateClosed); - connect(audioIO.data(), &AudioClient::inputReceived, &AudioScriptingInterface::getInstance(), &AudioScriptingInterface::inputReceived); + audioScriptingInterface->setLocalAudioInterface(audioIO.data()); + connect(audioIO.data(), &AudioClient::noiseGateOpened, audioScriptingInterface.data(), &AudioScriptingInterface::noiseGateOpened); + connect(audioIO.data(), &AudioClient::noiseGateClosed, audioScriptingInterface.data(), &AudioScriptingInterface::noiseGateClosed); + connect(audioIO.data(), &AudioClient::inputReceived, audioScriptingInterface.data(), &AudioScriptingInterface::inputReceived); this->installEventFilter(this); @@ -1445,7 +1444,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo scriptEngines->loadScript(testScript, false); } else { // Get sandbox content set version, if available - auto acDirPath = PathUtils::getRootDataDirectory() + BuildInfo::MODIFIED_ORGANIZATION + "/assignment-client/"; + auto acDirPath = PathUtils::getAppDataPath() + "../../" + BuildInfo::MODIFIED_ORGANIZATION + "/assignment-client/"; auto contentVersionPath = acDirPath + "content-version.txt"; qCDebug(interfaceapp) << "Checking " << contentVersionPath << " for content version"; auto contentVersion = 0; @@ -1951,7 +1950,7 @@ void Application::initializeUi() { // For some reason there is already an "Application" object in the QML context, // though I can't find it. Hence, "ApplicationInterface" rootContext->setContextProperty("ApplicationInterface", this); - rootContext->setContextProperty("Audio", &AudioScriptingInterface::getInstance()); + rootContext->setContextProperty("Audio", DependencyManager::get().data()); rootContext->setContextProperty("AudioStats", DependencyManager::get()->getStats().data()); rootContext->setContextProperty("AudioScope", DependencyManager::get().data()); @@ -2119,21 +2118,6 @@ void Application::paintGL() { batch.resetStages(); }); - auto inputs = AvatarInputs::getInstance(); - if (inputs->mirrorVisible()) { - PerformanceTimer perfTimer("Mirror"); - - renderArgs._renderMode = RenderArgs::MIRROR_RENDER_MODE; - renderArgs._blitFramebuffer = DependencyManager::get()->getSelfieFramebuffer(); - - _mirrorViewRect.moveTo(inputs->x(), inputs->y()); - - renderRearViewMirror(&renderArgs, _mirrorViewRect, inputs->mirrorZoomed()); - - renderArgs._blitFramebuffer.reset(); - renderArgs._renderMode = RenderArgs::DEFAULT_RENDER_MODE; - } - { PerformanceTimer perfTimer("renderOverlay"); // NOTE: There is no batch associated with this renderArgs @@ -2381,10 +2365,6 @@ void Application::setSettingConstrainToolbarPosition(bool setting) { DependencyManager::get()->setConstrainToolbarToCenterX(setting); } -void Application::aboutApp() { - InfoView::show(INFO_WELCOME_PATH); -} - void Application::showHelp() { static const QString HAND_CONTROLLER_NAME_VIVE = "vive"; static const QString HAND_CONTROLLER_NAME_OCULUS_TOUCH = "oculus"; @@ -2766,8 +2746,6 @@ void Application::keyPressEvent(QKeyEvent* event) { case Qt::Key_S: if (isShifted && isMeta && !isOption) { Menu::getInstance()->triggerOption(MenuOption::SuppressShortTimings); - } else if (isOption && !isShifted && !isMeta) { - Menu::getInstance()->triggerOption(MenuOption::ScriptEditor); } else if (!isOption && !isShifted && isMeta) { takeSnapshot(true); } @@ -2886,51 +2864,49 @@ void Application::keyPressEvent(QKeyEvent* event) { break; #endif - case Qt::Key_H: - if (isShifted) { - Menu::getInstance()->triggerOption(MenuOption::MiniMirror); - } else { - // whenever switching to/from full screen mirror from the keyboard, remember - // the state you were in before full screen mirror, and return to that. - auto previousMode = _myCamera.getMode(); - if (previousMode != CAMERA_MODE_MIRROR) { - switch (previousMode) { - case CAMERA_MODE_FIRST_PERSON: - _returnFromFullScreenMirrorTo = MenuOption::FirstPerson; - break; - case CAMERA_MODE_THIRD_PERSON: - _returnFromFullScreenMirrorTo = MenuOption::ThirdPerson; - break; + case Qt::Key_H: { + // whenever switching to/from full screen mirror from the keyboard, remember + // the state you were in before full screen mirror, and return to that. + auto previousMode = _myCamera.getMode(); + if (previousMode != CAMERA_MODE_MIRROR) { + switch (previousMode) { + case CAMERA_MODE_FIRST_PERSON: + _returnFromFullScreenMirrorTo = MenuOption::FirstPerson; + break; + case CAMERA_MODE_THIRD_PERSON: + _returnFromFullScreenMirrorTo = MenuOption::ThirdPerson; + break; - // FIXME - it's not clear that these modes make sense to return to... - case CAMERA_MODE_INDEPENDENT: - _returnFromFullScreenMirrorTo = MenuOption::IndependentMode; - break; - case CAMERA_MODE_ENTITY: - _returnFromFullScreenMirrorTo = MenuOption::CameraEntityMode; - break; + // FIXME - it's not clear that these modes make sense to return to... + case CAMERA_MODE_INDEPENDENT: + _returnFromFullScreenMirrorTo = MenuOption::IndependentMode; + break; + case CAMERA_MODE_ENTITY: + _returnFromFullScreenMirrorTo = MenuOption::CameraEntityMode; + break; - default: - _returnFromFullScreenMirrorTo = MenuOption::ThirdPerson; - break; - } + default: + _returnFromFullScreenMirrorTo = MenuOption::ThirdPerson; + break; } - - bool isMirrorChecked = Menu::getInstance()->isOptionChecked(MenuOption::FullscreenMirror); - Menu::getInstance()->setIsOptionChecked(MenuOption::FullscreenMirror, !isMirrorChecked); - if (isMirrorChecked) { - - // if we got here without coming in from a non-Full Screen mirror case, then our - // _returnFromFullScreenMirrorTo is unknown. In that case we'll go to the old - // behavior of returning to ThirdPerson - if (_returnFromFullScreenMirrorTo.isEmpty()) { - _returnFromFullScreenMirrorTo = MenuOption::ThirdPerson; - } - Menu::getInstance()->setIsOptionChecked(_returnFromFullScreenMirrorTo, true); - } - cameraMenuChanged(); } + + bool isMirrorChecked = Menu::getInstance()->isOptionChecked(MenuOption::FullscreenMirror); + Menu::getInstance()->setIsOptionChecked(MenuOption::FullscreenMirror, !isMirrorChecked); + if (isMirrorChecked) { + + // if we got here without coming in from a non-Full Screen mirror case, then our + // _returnFromFullScreenMirrorTo is unknown. In that case we'll go to the old + // behavior of returning to ThirdPerson + if (_returnFromFullScreenMirrorTo.isEmpty()) { + _returnFromFullScreenMirrorTo = MenuOption::ThirdPerson; + } + Menu::getInstance()->setIsOptionChecked(_returnFromFullScreenMirrorTo, true); + } + cameraMenuChanged(); break; + } + case Qt::Key_P: { if (!(isShifted || isMeta || isOption)) { bool isFirstPersonChecked = Menu::getInstance()->isOptionChecked(MenuOption::FirstPerson); @@ -3845,8 +3821,6 @@ void Application::init() { DependencyManager::get()->init(); _myCamera.setMode(CAMERA_MODE_FIRST_PERSON); - _mirrorCamera.setMode(CAMERA_MODE_MIRROR); - _timerStart.start(); _lastTimeUpdated.start(); @@ -4383,16 +4357,16 @@ void Application::update(float deltaTime) { myAvatar->clearDriveKeys(); if (_myCamera.getMode() != CAMERA_MODE_INDEPENDENT) { if (!_controllerScriptingInterface->areActionsCaptured()) { - myAvatar->setDriveKeys(TRANSLATE_Z, -1.0f * userInputMapper->getActionState(controller::Action::TRANSLATE_Z)); - myAvatar->setDriveKeys(TRANSLATE_Y, userInputMapper->getActionState(controller::Action::TRANSLATE_Y)); - myAvatar->setDriveKeys(TRANSLATE_X, userInputMapper->getActionState(controller::Action::TRANSLATE_X)); + myAvatar->setDriveKey(MyAvatar::TRANSLATE_Z, -1.0f * userInputMapper->getActionState(controller::Action::TRANSLATE_Z)); + myAvatar->setDriveKey(MyAvatar::TRANSLATE_Y, userInputMapper->getActionState(controller::Action::TRANSLATE_Y)); + myAvatar->setDriveKey(MyAvatar::TRANSLATE_X, userInputMapper->getActionState(controller::Action::TRANSLATE_X)); if (deltaTime > FLT_EPSILON) { - myAvatar->setDriveKeys(PITCH, -1.0f * userInputMapper->getActionState(controller::Action::PITCH)); - myAvatar->setDriveKeys(YAW, -1.0f * userInputMapper->getActionState(controller::Action::YAW)); - myAvatar->setDriveKeys(STEP_YAW, -1.0f * userInputMapper->getActionState(controller::Action::STEP_YAW)); + myAvatar->setDriveKey(MyAvatar::PITCH, -1.0f * userInputMapper->getActionState(controller::Action::PITCH)); + myAvatar->setDriveKey(MyAvatar::YAW, -1.0f * userInputMapper->getActionState(controller::Action::YAW)); + myAvatar->setDriveKey(MyAvatar::STEP_YAW, -1.0f * userInputMapper->getActionState(controller::Action::STEP_YAW)); } } - myAvatar->setDriveKeys(ZOOM, userInputMapper->getActionState(controller::Action::TRANSLATE_CAMERA_Z)); + myAvatar->setDriveKey(MyAvatar::ZOOM, userInputMapper->getActionState(controller::Action::TRANSLATE_CAMERA_Z)); } controller::Pose leftHandPose = userInputMapper->getPoseState(controller::Action::LEFT_HAND); @@ -4463,9 +4437,12 @@ void Application::update(float deltaTime) { getEntities()->getTree()->withWriteLock([&] { PerformanceTimer perfTimer("handleOutgoingChanges"); - const VectorOfMotionStates& outgoingChanges = _physicsEngine->getOutgoingChanges(); - _entitySimulation->handleOutgoingChanges(outgoingChanges); - avatarManager->handleOutgoingChanges(outgoingChanges); + const VectorOfMotionStates& deactivations = _physicsEngine->getDeactivatedMotionStates(); + _entitySimulation->handleDeactivatedMotionStates(deactivations); + + const VectorOfMotionStates& outgoingChanges = _physicsEngine->getChangedMotionStates(); + _entitySimulation->handleChangedMotionStates(outgoingChanges); + avatarManager->handleChangedMotionStates(outgoingChanges); }); if (!_aboutToQuit) { @@ -5122,58 +5099,6 @@ void Application::displaySide(RenderArgs* renderArgs, Camera& theCamera, bool se activeRenderingThread = nullptr; } -void Application::renderRearViewMirror(RenderArgs* renderArgs, const QRect& region, bool isZoomed) { - auto originalViewport = renderArgs->_viewport; - // Grab current viewport to reset it at the end - - float aspect = (float)region.width() / region.height(); - float fov = MIRROR_FIELD_OF_VIEW; - - auto myAvatar = getMyAvatar(); - - // bool eyeRelativeCamera = false; - if (!isZoomed) { - _mirrorCamera.setPosition(myAvatar->getChestPosition() + - myAvatar->getOrientation() * glm::vec3(0.0f, 0.0f, -1.0f) * MIRROR_REARVIEW_BODY_DISTANCE * myAvatar->getScale()); - - } else { // HEAD zoom level - // FIXME note that the positioning of the camera relative to the avatar can suffer limited - // precision as the user's position moves further away from the origin. Thus at - // /1e7,1e7,1e7 (well outside the buildable volume) the mirror camera veers and sways - // wildly as you rotate your avatar because the floating point values are becoming - // larger, squeezing out the available digits of precision you have available at the - // human scale for camera positioning. - - // Previously there was a hack to correct this using the mechanism of repositioning - // the avatar at the origin of the world for the purposes of rendering the mirror, - // but it resulted in failing to render the avatar's head model in the mirror view - // when in first person mode. Presumably this was because of some missed culling logic - // that was not accounted for in the hack. - - // This was removed in commit 71e59cfa88c6563749594e25494102fe01db38e9 but could be further - // investigated in order to adapt the technique while fixing the head rendering issue, - // but the complexity of the hack suggests that a better approach - _mirrorCamera.setPosition(myAvatar->getDefaultEyePosition() + - myAvatar->getOrientation() * glm::vec3(0.0f, 0.0f, -1.0f) * MIRROR_REARVIEW_DISTANCE * myAvatar->getScale()); - } - _mirrorCamera.setProjection(glm::perspective(glm::radians(fov), aspect, DEFAULT_NEAR_CLIP, DEFAULT_FAR_CLIP)); - _mirrorCamera.setOrientation(myAvatar->getWorldAlignedOrientation() * glm::quat(glm::vec3(0.0f, PI, 0.0f))); - - - // set the bounds of rear mirror view - // the region is in device independent coordinates; must convert to device - float ratio = (float)QApplication::desktop()->windowHandle()->devicePixelRatio() * getRenderResolutionScale(); - int width = region.width() * ratio; - int height = region.height() * ratio; - gpu::Vec4i viewport = gpu::Vec4i(0, 0, width, height); - renderArgs->_viewport = viewport; - - // render rear mirror view - displaySide(renderArgs, _mirrorCamera, true); - - renderArgs->_viewport = originalViewport; -} - void Application::resetSensors(bool andReload) { DependencyManager::get()->reset(); DependencyManager::get()->reset(); @@ -5503,8 +5428,7 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri scriptEngine->registerGlobalObject("Rates", new RatesScriptingInterface(this)); // hook our avatar and avatar hash map object into this script engine - scriptEngine->registerGlobalObject("MyAvatar", getMyAvatar().get()); - qScriptRegisterMetaType(scriptEngine, audioListenModeToScriptValue, audioListenModeFromScriptValue); + getMyAvatar()->registerMetaTypes(scriptEngine); scriptEngine->registerGlobalObject("AvatarList", DependencyManager::get().data()); diff --git a/interface/src/Application.h b/interface/src/Application.h index 98080783a6..7ae4160f8b 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -72,6 +72,8 @@ #include #include +#include + class OffscreenGLCanvas; class GLCanvas; @@ -276,8 +278,6 @@ public: virtual void pushPostUpdateLambda(void* key, std::function func) override; - const QRect& getMirrorViewRect() const { return _mirrorViewRect; } - void updateMyAvatarLookAtPosition(); float getAvatarSimrate() const { return _avatarSimCounter.rate(); } @@ -368,7 +368,6 @@ public slots: void calibrateEyeTracker5Points(); #endif - void aboutApp(); static void showHelp(); void cycleCamera(); @@ -557,8 +556,6 @@ private: int _avatarSimsPerSecondReport {0}; quint64 _lastAvatarSimsPerSecondUpdate {0}; Camera _myCamera; // My view onto the world - Camera _mirrorCamera; // Camera for mirror view - QRect _mirrorViewRect; Setting::Handle _previousScriptLocation; Setting::Handle _fieldOfView; diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index beacbaccab..a48ee4e7db 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -74,9 +74,6 @@ Menu::Menu() { // File > Help addActionToQMenuAndActionHash(fileMenu, MenuOption::Help, 0, qApp, SLOT(showHelp())); - // File > About - addActionToQMenuAndActionHash(fileMenu, MenuOption::AboutApp, 0, qApp, SLOT(aboutApp()), QAction::AboutRole); - // File > Quit addActionToQMenuAndActionHash(fileMenu, MenuOption::Quit, Qt::CTRL | Qt::Key_Q, qApp, SLOT(quit()), QAction::QuitRole); @@ -120,11 +117,6 @@ Menu::Menu() { scriptEngines.data(), SLOT(reloadAllScripts()), QAction::NoRole, UNSPECIFIED_POSITION, "Advanced"); - // Edit > Scripts Editor... [advanced] - addActionToQMenuAndActionHash(editMenu, MenuOption::ScriptEditor, Qt::ALT | Qt::Key_S, - dialogsManager.data(), SLOT(showScriptEditor()), - QAction::NoRole, UNSPECIFIED_POSITION, "Advanced"); - // Edit > Console... [advanced] addActionToQMenuAndActionHash(editMenu, MenuOption::Console, Qt::CTRL | Qt::ALT | Qt::Key_J, DependencyManager::get().data(), @@ -249,9 +241,6 @@ Menu::Menu() { viewMenu->addSeparator(); - // View > Mini Mirror - addCheckableActionToQMenuAndActionHash(viewMenu, MenuOption::MiniMirror, 0, false); - // View > Center Player In View addCheckableActionToQMenuAndActionHash(viewMenu, MenuOption::CenterPlayerInView, 0, true, qApp, SLOT(rotationModeChanged()), @@ -417,6 +406,9 @@ Menu::Menu() { } // Developer > Assets >>> + // Menu item is not currently needed but code should be kept in case it proves useful again at some stage. +//#define WANT_ASSET_MIGRATION +#ifdef WANT_ASSET_MIGRATION MenuWrapper* assetDeveloperMenu = developerMenu->addMenu("Assets"); auto& atpMigrator = ATPAssetMigrator::getInstance(); atpMigrator.setDialogParent(this); @@ -424,6 +416,7 @@ Menu::Menu() { addActionToQMenuAndActionHash(assetDeveloperMenu, MenuOption::AssetMigration, 0, &atpMigrator, SLOT(loadEntityServerFile())); +#endif // Developer > Avatar >>> MenuWrapper* avatarDebugMenu = developerMenu->addMenu("Avatar"); @@ -554,16 +547,14 @@ Menu::Menu() { "NetworkingPreferencesDialog"); }); addActionToQMenuAndActionHash(networkMenu, MenuOption::ReloadContent, 0, qApp, SLOT(reloadResourceCaches())); + addActionToQMenuAndActionHash(networkMenu, MenuOption::ClearDiskCache, 0, + DependencyManager::get().data(), SLOT(clearCache())); addCheckableActionToQMenuAndActionHash(networkMenu, MenuOption::DisableActivityLogger, 0, false, &UserActivityLogger::getInstance(), SLOT(disable(bool))); - addActionToQMenuAndActionHash(networkMenu, MenuOption::CachesSize, 0, - dialogsManager.data(), SLOT(cachesSizeDialog())); - addActionToQMenuAndActionHash(networkMenu, MenuOption::DiskCacheEditor, 0, - dialogsManager.data(), SLOT(toggleDiskCacheEditor())); addActionToQMenuAndActionHash(networkMenu, MenuOption::ShowDSConnectTable, 0, dialogsManager.data(), SLOT(showDomainConnectionDialog())); diff --git a/interface/src/Menu.h b/interface/src/Menu.h index c806ffa9ee..b4eaf56758 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -26,7 +26,6 @@ public: }; namespace MenuOption { - const QString AboutApp = "About Interface"; const QString AddRemoveFriends = "Add/Remove Friends..."; const QString AddressBar = "Show Address Bar"; const QString Animations = "Animations..."; @@ -52,11 +51,11 @@ namespace MenuOption { const QString BinaryEyelidControl = "Binary Eyelid Control"; const QString BookmarkLocation = "Bookmark Location"; const QString Bookmarks = "Bookmarks"; - const QString CachesSize = "RAM Caches Size"; const QString CalibrateCamera = "Calibrate Camera"; const QString CameraEntityMode = "Entity Mode"; const QString CenterPlayerInView = "Center Player In View"; const QString Chat = "Chat..."; + const QString ClearDiskCache = "Clear Disk Cache"; const QString Collisions = "Collisions"; const QString Connexion = "Activate 3D Connexion Devices"; const QString Console = "Console..."; @@ -83,7 +82,6 @@ namespace MenuOption { const QString DisableActivityLogger = "Disable Activity Logger"; const QString DisableEyelidAdjustment = "Disable Eyelid Adjustment"; const QString DisableLightEntities = "Disable Light Entities"; - const QString DiskCacheEditor = "Disk Cache Editor"; const QString DisplayCrashOptions = "Display Crash Options"; const QString DisplayHandTargets = "Show Hand Targets"; const QString DisplayModelBounds = "Display Model Bounds"; @@ -124,7 +122,6 @@ namespace MenuOption { const QString LogExtraTimings = "Log Extra Timing Details"; const QString LowVelocityFilter = "Low Velocity Filter"; const QString MeshVisible = "Draw Mesh"; - const QString MiniMirror = "Mini Mirror"; const QString MuteAudio = "Mute Microphone"; const QString MuteEnvironment = "Mute Environment"; const QString MuteFaceTracking = "Mute Face Tracking"; @@ -169,7 +166,6 @@ namespace MenuOption { const QString RunningScripts = "Running Scripts..."; const QString RunClientScriptTests = "Run Client Script Tests"; const QString RunTimingTests = "Run Timing Tests"; - const QString ScriptEditor = "Script Editor..."; const QString ScriptedMotorControl = "Enable Scripted Motor Control"; const QString SendWrongDSConnectVersion = "Send wrong DS connect version"; const QString SendWrongProtocolVersion = "Send wrong protocol version"; diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 94ce444416..6152148887 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -424,7 +424,7 @@ void AvatarManager::getObjectsToChange(VectorOfMotionStates& result) { } } -void AvatarManager::handleOutgoingChanges(const VectorOfMotionStates& motionStates) { +void AvatarManager::handleChangedMotionStates(const VectorOfMotionStates& motionStates) { // TODO: extract the MyAvatar results once we use a MotionState for it. } diff --git a/interface/src/avatar/AvatarManager.h b/interface/src/avatar/AvatarManager.h index e1f5a3b411..b94f9e6a96 100644 --- a/interface/src/avatar/AvatarManager.h +++ b/interface/src/avatar/AvatarManager.h @@ -70,7 +70,7 @@ public: void getObjectsToRemoveFromPhysics(VectorOfMotionStates& motionStates); void getObjectsToAddToPhysics(VectorOfMotionStates& motionStates); void getObjectsToChange(VectorOfMotionStates& motionStates); - void handleOutgoingChanges(const VectorOfMotionStates& motionStates); + void handleChangedMotionStates(const VectorOfMotionStates& motionStates); void handleCollisionEvents(const CollisionEvents& collisionEvents); Q_INVOKABLE float getAvatarDataRate(const QUuid& sessionID, const QString& rateName = QString("")) const; diff --git a/interface/src/avatar/CauterizedMeshPartPayload.cpp b/interface/src/avatar/CauterizedMeshPartPayload.cpp index c8ec90dcee..c11f92083b 100644 --- a/interface/src/avatar/CauterizedMeshPartPayload.cpp +++ b/interface/src/avatar/CauterizedMeshPartPayload.cpp @@ -20,55 +20,28 @@ using namespace render; CauterizedMeshPartPayload::CauterizedMeshPartPayload(Model* model, int meshIndex, int partIndex, int shapeIndex, const Transform& transform, const Transform& offsetTransform) : ModelMeshPartPayload(model, meshIndex, partIndex, shapeIndex, transform, offsetTransform) {} -void CauterizedMeshPartPayload::updateTransformForSkinnedCauterizedMesh(const Transform& transform, - const QVector& clusterMatrices, - const QVector& cauterizedClusterMatrices) { - _transform = transform; - _cauterizedTransform = transform; - - if (clusterMatrices.size() > 0) { - _worldBound = AABox(); - for (auto& clusterMatrix : clusterMatrices) { - AABox clusterBound = _localBound; - clusterBound.transform(clusterMatrix); - _worldBound += clusterBound; - } - - _worldBound.transform(transform); - if (clusterMatrices.size() == 1) { - _transform = _transform.worldTransform(Transform(clusterMatrices[0])); - if (cauterizedClusterMatrices.size() != 0) { - _cauterizedTransform = _cauterizedTransform.worldTransform(Transform(cauterizedClusterMatrices[0])); - } else { - _cauterizedTransform = _transform; - } - } - } else { - _worldBound = _localBound; - _worldBound.transform(_drawTransform); - } +void CauterizedMeshPartPayload::updateTransformForCauterizedMesh( + const Transform& renderTransform, + const gpu::BufferPointer& buffer) { + _cauterizedTransform = renderTransform; + _cauterizedClusterBuffer = buffer; } void CauterizedMeshPartPayload::bindTransform(gpu::Batch& batch, const render::ShapePipeline::LocationsPointer locations, RenderArgs::RenderMode renderMode) const { // Still relying on the raw data from the model - const Model::MeshState& state = _model->getMeshState(_meshIndex); SkeletonModel* skeleton = static_cast(_model); bool useCauterizedMesh = (renderMode != RenderArgs::RenderMode::SHADOW_RENDER_MODE) && skeleton->getEnableCauterization(); - if (state.clusterBuffer) { - if (useCauterizedMesh) { - const Model::MeshState& cState = skeleton->getCauterizeMeshState(_meshIndex); - batch.setUniformBuffer(ShapePipeline::Slot::BUFFER::SKINNING, cState.clusterBuffer); - } else { - batch.setUniformBuffer(ShapePipeline::Slot::BUFFER::SKINNING, state.clusterBuffer); + if (useCauterizedMesh) { + if (_cauterizedClusterBuffer) { + batch.setUniformBuffer(ShapePipeline::Slot::BUFFER::SKINNING, _cauterizedClusterBuffer); + } + batch.setModelTransform(_cauterizedTransform); + } else { + if (_clusterBuffer) { + batch.setUniformBuffer(ShapePipeline::Slot::BUFFER::SKINNING, _clusterBuffer); } batch.setModelTransform(_transform); - } else { - if (useCauterizedMesh) { - batch.setModelTransform(_cauterizedTransform); - } else { - batch.setModelTransform(_transform); - } } } diff --git a/interface/src/avatar/CauterizedMeshPartPayload.h b/interface/src/avatar/CauterizedMeshPartPayload.h index f4319ead6f..dc88e950c1 100644 --- a/interface/src/avatar/CauterizedMeshPartPayload.h +++ b/interface/src/avatar/CauterizedMeshPartPayload.h @@ -17,12 +17,13 @@ class CauterizedMeshPartPayload : public ModelMeshPartPayload { public: CauterizedMeshPartPayload(Model* model, int meshIndex, int partIndex, int shapeIndex, const Transform& transform, const Transform& offsetTransform); - void updateTransformForSkinnedCauterizedMesh(const Transform& transform, - const QVector& clusterMatrices, - const QVector& cauterizedClusterMatrices); + + void updateTransformForCauterizedMesh(const Transform& renderTransform, const gpu::BufferPointer& buffer); void bindTransform(gpu::Batch& batch, const render::ShapePipeline::LocationsPointer locations, RenderArgs::RenderMode renderMode) const override; + private: + gpu::BufferPointer _cauterizedClusterBuffer; Transform _cauterizedTransform; }; diff --git a/interface/src/avatar/CauterizedModel.cpp b/interface/src/avatar/CauterizedModel.cpp index 1ca87a498a..d8db83fbb7 100644 --- a/interface/src/avatar/CauterizedModel.cpp +++ b/interface/src/avatar/CauterizedModel.cpp @@ -26,8 +26,8 @@ CauterizedModel::~CauterizedModel() { } void CauterizedModel::deleteGeometry() { - Model::deleteGeometry(); - _cauterizeMeshStates.clear(); + Model::deleteGeometry(); + _cauterizeMeshStates.clear(); } bool CauterizedModel::updateGeometry() { @@ -41,7 +41,7 @@ bool CauterizedModel::updateGeometry() { _cauterizeMeshStates.append(state); } } - return needsFullUpdate; + return needsFullUpdate; } void CauterizedModel::createVisibleRenderItemSet() { @@ -86,13 +86,13 @@ void CauterizedModel::createVisibleRenderItemSet() { } } } else { - Model::createVisibleRenderItemSet(); + Model::createVisibleRenderItemSet(); } } void CauterizedModel::createCollisionRenderItemSet() { // Temporary HACK: use base class method for now - Model::createCollisionRenderItemSet(); + Model::createCollisionRenderItemSet(); } void CauterizedModel::updateClusterMatrices() { @@ -122,8 +122,8 @@ void CauterizedModel::updateClusterMatrices() { state.clusterBuffer->setSubData(0, state.clusterMatrices.size() * sizeof(glm::mat4), (const gpu::Byte*) state.clusterMatrices.constData()); } - } - } + } + } // as an optimization, don't build cautrizedClusterMatrices if the boneSet is empty. if (!_cauterizeBoneSet.empty()) { @@ -191,6 +191,9 @@ void CauterizedModel::updateRenderItems() { return; } + // lazy update of cluster matrices used for rendering. We need to update them here, so we can correctly update the bounding box. + self->updateClusterMatrices(); + render::ScenePointer scene = AbstractViewStateInterface::instance()->getMain3DScene(); Transform modelTransform; @@ -209,15 +212,22 @@ void CauterizedModel::updateRenderItems() { if (data._model && data._model->isLoaded()) { // Ensure the model geometry was not reset between frames if (deleteGeometryCounter == data._model->getGeometryCounter()) { - // lazy update of cluster matrices used for rendering. We need to update them here, so we can correctly update the bounding box. - data._model->updateClusterMatrices(); - - // update the model transform and bounding box for this render item. + // this stuff identical to what happens in regular Model const Model::MeshState& state = data._model->getMeshState(data._meshIndex); + Transform renderTransform = modelTransform; + if (state.clusterMatrices.size() == 1) { + renderTransform = modelTransform.worldTransform(Transform(state.clusterMatrices[0])); + } + data.updateTransformForSkinnedMesh(renderTransform, modelTransform, state.clusterBuffer); + + // this stuff for cauterized mesh CauterizedModel* cModel = static_cast(data._model); - assert(data._meshIndex < cModel->_cauterizeMeshStates.size()); - const Model::MeshState& cState = cModel->_cauterizeMeshStates.at(data._meshIndex); - data.updateTransformForSkinnedCauterizedMesh(modelTransform, state.clusterMatrices, cState.clusterMatrices); + const Model::MeshState& cState = cModel->getCauterizeMeshState(data._meshIndex); + renderTransform = modelTransform; + if (cState.clusterMatrices.size() == 1) { + renderTransform = modelTransform.worldTransform(Transform(cState.clusterMatrices[0])); + } + data.updateTransformForCauterizedMesh(renderTransform, cState.clusterBuffer); } } }); diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 969268c549..b40ef601ea 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -104,6 +104,7 @@ MyAvatar::MyAvatar(RigPointer rig) : _eyeContactTarget(LEFT_EYE), _realWorldFieldOfView("realWorldFieldOfView", DEFAULT_REAL_WORLD_FIELD_OF_VIEW_DEGREES), + _useAdvancedMovementControls("advancedMovementForHandControllersIsChecked", false), _hmdSensorMatrix(), _hmdSensorOrientation(), _hmdSensorPosition(), @@ -119,9 +120,7 @@ MyAvatar::MyAvatar(RigPointer rig) : using namespace recording; _skeletonModel->flagAsCauterized(); - for (int i = 0; i < MAX_DRIVE_KEYS; i++) { - _driveKeys[i] = 0.0f; - } + clearDriveKeys(); // Necessary to select the correct slot using SlotType = void(MyAvatar::*)(const glm::vec3&, bool, const glm::quat&, bool); @@ -154,9 +153,12 @@ MyAvatar::MyAvatar(RigPointer rig) : if (recordingInterface->getPlayFromCurrentLocation()) { setRecordingBasis(); } + _wasCharacterControllerEnabled = _characterController.isEnabled(); + _characterController.setEnabled(false); } else { clearRecordingBasis(); useFullAvatarURL(_fullAvatarURLFromPreferences, _fullAvatarModelName); + _characterController.setEnabled(_wasCharacterControllerEnabled); } auto audioIO = DependencyManager::get(); @@ -227,6 +229,21 @@ MyAvatar::~MyAvatar() { _lookAtTargetAvatar.reset(); } +void MyAvatar::registerMetaTypes(QScriptEngine* engine) { + QScriptValue value = engine->newQObject(this, QScriptEngine::QtOwnership, QScriptEngine::ExcludeDeleteLater | QScriptEngine::ExcludeChildObjects); + engine->globalObject().setProperty("MyAvatar", value); + + QScriptValue driveKeys = engine->newObject(); + auto metaEnum = QMetaEnum::fromType(); + for (int i = 0; i < MAX_DRIVE_KEYS; ++i) { + driveKeys.setProperty(metaEnum.key(i), metaEnum.value(i)); + } + engine->globalObject().setProperty("DriveKeys", driveKeys); + + qScriptRegisterMetaType(engine, audioListenModeToScriptValue, audioListenModeFromScriptValue); + qScriptRegisterMetaType(engine, driveKeysToScriptValue, driveKeysFromScriptValue); +} + void MyAvatar::setOrientationVar(const QVariant& newOrientationVar) { Avatar::setOrientation(quatFromVariant(newOrientationVar)); } @@ -459,7 +476,7 @@ void MyAvatar::simulate(float deltaTime) { // When there are no step values, we zero out the last step pulse. // This allows a user to do faster snapping by tapping a control for (int i = STEP_TRANSLATE_X; !stepAction && i <= STEP_YAW; ++i) { - if (_driveKeys[i] != 0.0f) { + if (getDriveKey((DriveKeys)i) != 0.0f) { stepAction = true; } } @@ -1652,7 +1669,7 @@ bool MyAvatar::shouldRenderHead(const RenderArgs* renderArgs) const { void MyAvatar::updateOrientation(float deltaTime) { // Smoothly rotate body with arrow keys - float targetSpeed = _driveKeys[YAW] * _yawSpeed; + float targetSpeed = getDriveKey(YAW) * _yawSpeed; if (targetSpeed != 0.0f) { const float ROTATION_RAMP_TIMESCALE = 0.1f; float blend = deltaTime / ROTATION_RAMP_TIMESCALE; @@ -1681,8 +1698,8 @@ void MyAvatar::updateOrientation(float deltaTime) { // Comfort Mode: If you press any of the left/right rotation drive keys or input, you'll // get an instantaneous 15 degree turn. If you keep holding the key down you'll get another // snap turn every half second. - if (_driveKeys[STEP_YAW] != 0.0f) { - totalBodyYaw += _driveKeys[STEP_YAW]; + if (getDriveKey(STEP_YAW) != 0.0f) { + totalBodyYaw += getDriveKey(STEP_YAW); } // use head/HMD orientation to turn while flying @@ -1719,7 +1736,7 @@ void MyAvatar::updateOrientation(float deltaTime) { // update body orientation by movement inputs setOrientation(getOrientation() * glm::quat(glm::radians(glm::vec3(0.0f, totalBodyYaw, 0.0f)))); - getHead()->setBasePitch(getHead()->getBasePitch() + _driveKeys[PITCH] * _pitchSpeed * deltaTime); + getHead()->setBasePitch(getHead()->getBasePitch() + getDriveKey(PITCH) * _pitchSpeed * deltaTime); if (qApp->isHMDMode()) { glm::quat orientation = glm::quat_cast(getSensorToWorldMatrix()) * getHMDSensorOrientation(); @@ -1753,14 +1770,14 @@ void MyAvatar::updateActionMotor(float deltaTime) { } // compute action input - glm::vec3 front = (_driveKeys[TRANSLATE_Z]) * IDENTITY_FRONT; - glm::vec3 right = (_driveKeys[TRANSLATE_X]) * IDENTITY_RIGHT; + glm::vec3 front = (getDriveKey(TRANSLATE_Z)) * IDENTITY_FRONT; + glm::vec3 right = (getDriveKey(TRANSLATE_X)) * IDENTITY_RIGHT; glm::vec3 direction = front + right; CharacterController::State state = _characterController.getState(); if (state == CharacterController::State::Hover) { // we're flying --> support vertical motion - glm::vec3 up = (_driveKeys[TRANSLATE_Y]) * IDENTITY_UP; + glm::vec3 up = (getDriveKey(TRANSLATE_Y)) * IDENTITY_UP; direction += up; } @@ -1799,7 +1816,7 @@ void MyAvatar::updateActionMotor(float deltaTime) { _actionMotorVelocity = MAX_WALKING_SPEED * direction; } - float boomChange = _driveKeys[ZOOM]; + float boomChange = getDriveKey(ZOOM); _boomLength += 2.0f * _boomLength * boomChange + boomChange * boomChange; _boomLength = glm::clamp(_boomLength, ZOOM_MIN, ZOOM_MAX); } @@ -1830,11 +1847,11 @@ void MyAvatar::updatePosition(float deltaTime) { } // capture the head rotation, in sensor space, when the user first indicates they would like to move/fly. - if (!_hoverReferenceCameraFacingIsCaptured && (fabs(_driveKeys[TRANSLATE_Z]) > 0.1f || fabs(_driveKeys[TRANSLATE_X]) > 0.1f)) { + if (!_hoverReferenceCameraFacingIsCaptured && (fabs(getDriveKey(TRANSLATE_Z)) > 0.1f || fabs(getDriveKey(TRANSLATE_X)) > 0.1f)) { _hoverReferenceCameraFacingIsCaptured = true; // transform the camera facing vector into sensor space. _hoverReferenceCameraFacing = transformVectorFast(glm::inverse(_sensorToWorldMatrix), getHead()->getCameraOrientation() * Vectors::UNIT_Z); - } else if (_hoverReferenceCameraFacingIsCaptured && (fabs(_driveKeys[TRANSLATE_Z]) <= 0.1f && fabs(_driveKeys[TRANSLATE_X]) <= 0.1f)) { + } else if (_hoverReferenceCameraFacingIsCaptured && (fabs(getDriveKey(TRANSLATE_Z)) <= 0.1f && fabs(getDriveKey(TRANSLATE_X)) <= 0.1f)) { _hoverReferenceCameraFacingIsCaptured = false; } } @@ -2090,17 +2107,61 @@ bool MyAvatar::getCharacterControllerEnabled() { } void MyAvatar::clearDriveKeys() { - for (int i = 0; i < MAX_DRIVE_KEYS; ++i) { - _driveKeys[i] = 0.0f; + _driveKeys.fill(0.0f); +} + +void MyAvatar::setDriveKey(DriveKeys key, float val) { + try { + _driveKeys.at(key) = val; + } catch (const std::exception&) { + qCCritical(interfaceapp) << Q_FUNC_INFO << ": Index out of bounds"; + } +} + +float MyAvatar::getDriveKey(DriveKeys key) const { + return isDriveKeyDisabled(key) ? 0.0f : getRawDriveKey(key); +} + +float MyAvatar::getRawDriveKey(DriveKeys key) const { + try { + return _driveKeys.at(key); + } catch (const std::exception&) { + qCCritical(interfaceapp) << Q_FUNC_INFO << ": Index out of bounds"; + return 0.0f; } } void MyAvatar::relayDriveKeysToCharacterController() { - if (_driveKeys[TRANSLATE_Y] > 0.0f) { + if (getDriveKey(TRANSLATE_Y) > 0.0f) { _characterController.jump(); } } +void MyAvatar::disableDriveKey(DriveKeys key) { + try { + _disabledDriveKeys.set(key); + } catch (const std::exception&) { + qCCritical(interfaceapp) << Q_FUNC_INFO << ": Index out of bounds"; + } +} + +void MyAvatar::enableDriveKey(DriveKeys key) { + try { + _disabledDriveKeys.reset(key); + } catch (const std::exception&) { + qCCritical(interfaceapp) << Q_FUNC_INFO << ": Index out of bounds"; + } +} + +bool MyAvatar::isDriveKeyDisabled(DriveKeys key) const { + try { + return _disabledDriveKeys.test(key); + } catch (const std::exception&) { + qCCritical(interfaceapp) << Q_FUNC_INFO << ": Index out of bounds"; + return true; + } +} + glm::vec3 MyAvatar::getWorldBodyPosition() const { return transformPoint(_sensorToWorldMatrix, extractTranslation(_bodySensorMatrix)); } @@ -2186,7 +2247,15 @@ QScriptValue audioListenModeToScriptValue(QScriptEngine* engine, const AudioList } void audioListenModeFromScriptValue(const QScriptValue& object, AudioListenerMode& audioListenerMode) { - audioListenerMode = (AudioListenerMode)object.toUInt16(); + audioListenerMode = static_cast(object.toUInt16()); +} + +QScriptValue driveKeysToScriptValue(QScriptEngine* engine, const MyAvatar::DriveKeys& driveKeys) { + return driveKeys; +} + +void driveKeysFromScriptValue(const QScriptValue& object, MyAvatar::DriveKeys& driveKeys) { + driveKeys = static_cast(object.toUInt16()); } @@ -2379,7 +2448,7 @@ bool MyAvatar::didTeleport() { } bool MyAvatar::hasDriveInput() const { - return fabsf(_driveKeys[TRANSLATE_X]) > 0.0f || fabsf(_driveKeys[TRANSLATE_Y]) > 0.0f || fabsf(_driveKeys[TRANSLATE_Z]) > 0.0f; + return fabsf(getDriveKey(TRANSLATE_X)) > 0.0f || fabsf(getDriveKey(TRANSLATE_Y)) > 0.0f || fabsf(getDriveKey(TRANSLATE_Z)) > 0.0f; } void MyAvatar::setAway(bool value) { @@ -2495,7 +2564,7 @@ bool MyAvatar::pinJoint(int index, const glm::vec3& position, const glm::quat& o return false; } - setPosition(position); + slamPosition(position); setOrientation(orientation); _rig->setMaxHipsOffsetLength(0.05f); diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 3cc665b533..5f812f1f99 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -12,6 +12,8 @@ #ifndef hifi_MyAvatar_h #define hifi_MyAvatar_h +#include + #include #include @@ -29,20 +31,6 @@ class AvatarActionHold; class ModelItemID; -enum DriveKeys { - TRANSLATE_X = 0, - TRANSLATE_Y, - TRANSLATE_Z, - YAW, - STEP_TRANSLATE_X, - STEP_TRANSLATE_Y, - STEP_TRANSLATE_Z, - STEP_YAW, - PITCH, - ZOOM, - MAX_DRIVE_KEYS -}; - enum eyeContactTarget { LEFT_EYE, RIGHT_EYE, @@ -86,11 +74,29 @@ class MyAvatar : public Avatar { Q_PROPERTY(bool hmdLeanRecenterEnabled READ getHMDLeanRecenterEnabled WRITE setHMDLeanRecenterEnabled) Q_PROPERTY(bool characterControllerEnabled READ getCharacterControllerEnabled WRITE setCharacterControllerEnabled) + Q_PROPERTY(bool useAdvancedMovementControls READ useAdvancedMovementControls WRITE setUseAdvancedMovementControls) public: + enum DriveKeys { + TRANSLATE_X = 0, + TRANSLATE_Y, + TRANSLATE_Z, + YAW, + STEP_TRANSLATE_X, + STEP_TRANSLATE_Y, + STEP_TRANSLATE_Z, + STEP_YAW, + PITCH, + ZOOM, + MAX_DRIVE_KEYS + }; + Q_ENUM(DriveKeys) + explicit MyAvatar(RigPointer rig); ~MyAvatar(); + void registerMetaTypes(QScriptEngine* engine); + virtual void simulateAttachments(float deltaTime) override; AudioListenerMode getAudioListenerModeHead() const { return FROM_HEAD; } @@ -171,6 +177,10 @@ public: Q_INVOKABLE void setHMDLeanRecenterEnabled(bool value) { _hmdLeanRecenterEnabled = value; } Q_INVOKABLE bool getHMDLeanRecenterEnabled() const { return _hmdLeanRecenterEnabled; } + bool useAdvancedMovementControls() const { return _useAdvancedMovementControls.get(); } + void setUseAdvancedMovementControls(bool useAdvancedMovementControls) + { _useAdvancedMovementControls.set(useAdvancedMovementControls); } + // get/set avatar data void saveData(); void loadData(); @@ -180,9 +190,15 @@ public: // Set what driving keys are being pressed to control thrust levels void clearDriveKeys(); - void setDriveKeys(int key, float val) { _driveKeys[key] = val; }; + void setDriveKey(DriveKeys key, float val); + float getDriveKey(DriveKeys key) const; + Q_INVOKABLE float getRawDriveKey(DriveKeys key) const; void relayDriveKeysToCharacterController(); + Q_INVOKABLE void disableDriveKey(DriveKeys key); + Q_INVOKABLE void enableDriveKey(DriveKeys key); + Q_INVOKABLE bool isDriveKeyDisabled(DriveKeys key) const; + eyeContactTarget getEyeContactTarget(); Q_INVOKABLE glm::vec3 getTrackedHeadPosition() const { return _trackedHeadPosition; } @@ -352,7 +368,6 @@ private: virtual bool shouldRenderHead(const RenderArgs* renderArgs) const override; void setShouldRenderLocally(bool shouldRender) { _shouldRender = shouldRender; setEnableMeshVisible(shouldRender); } bool getShouldRenderLocally() const { return _shouldRender; } - bool getDriveKeys(int key) { return _driveKeys[key] != 0.0f; }; bool isMyAvatar() const override { return true; } virtual int parseDataFromBuffer(const QByteArray& buffer) override; virtual glm::vec3 getSkeletonPosition() const override; @@ -388,7 +403,9 @@ private: void clampScaleChangeToDomainLimits(float desiredScale); glm::mat4 computeCameraRelativeHandControllerMatrix(const glm::mat4& controllerSensorMatrix) const; - float _driveKeys[MAX_DRIVE_KEYS]; + std::array _driveKeys; + std::bitset _disabledDriveKeys; + bool _wasPushing; bool _isPushing; bool _isBeingPushed; @@ -411,6 +428,7 @@ private: SharedSoundPointer _collisionSound; MyCharacterController _characterController; + bool _wasCharacterControllerEnabled { true }; AvatarWeakPointer _lookAtTargetAvatar; glm::vec3 _targetAvatarPosition; @@ -423,6 +441,7 @@ private: glm::vec3 _trackedHeadPosition; Setting::Handle _realWorldFieldOfView; + Setting::Handle _useAdvancedMovementControls; // private methods void updateOrientation(float deltaTime); @@ -540,4 +559,7 @@ private: QScriptValue audioListenModeToScriptValue(QScriptEngine* engine, const AudioListenerMode& audioListenerMode); void audioListenModeFromScriptValue(const QScriptValue& object, AudioListenerMode& audioListenerMode); +QScriptValue driveKeysToScriptValue(QScriptEngine* engine, const MyAvatar::DriveKeys& driveKeys); +void driveKeysFromScriptValue(const QScriptValue& object, MyAvatar::DriveKeys& driveKeys); + #endif // hifi_MyAvatar_h diff --git a/interface/src/ui/ApplicationOverlay.cpp b/interface/src/ui/ApplicationOverlay.cpp index 364dff52a3..f2d97a0137 100644 --- a/interface/src/ui/ApplicationOverlay.cpp +++ b/interface/src/ui/ApplicationOverlay.cpp @@ -13,7 +13,6 @@ #include #include -#include #include #include #include @@ -42,7 +41,6 @@ ApplicationOverlay::ApplicationOverlay() _domainStatusBorder = geometryCache->allocateID(); _magnifierBorder = geometryCache->allocateID(); _qmlGeometryId = geometryCache->allocateID(); - _rearViewGeometryId = geometryCache->allocateID(); } ApplicationOverlay::~ApplicationOverlay() { @@ -51,7 +49,6 @@ ApplicationOverlay::~ApplicationOverlay() { geometryCache->releaseID(_domainStatusBorder); geometryCache->releaseID(_magnifierBorder); geometryCache->releaseID(_qmlGeometryId); - geometryCache->releaseID(_rearViewGeometryId); } } @@ -86,7 +83,6 @@ void ApplicationOverlay::renderOverlay(RenderArgs* renderArgs) { // Now render the overlay components together into a single texture renderDomainConnectionStatusBorder(renderArgs); // renders the connected domain line renderAudioScope(renderArgs); // audio scope in the very back - NOTE: this is the debug audio scope, not the VU meter - renderRearView(renderArgs); // renders the mirror view selfie renderOverlays(renderArgs); // renders Scripts Overlay and AudioScope renderQmlUi(renderArgs); // renders a unit quad with the QML UI texture, and the text overlays from scripts renderStatsAndLogs(renderArgs); // currently renders nothing @@ -99,7 +95,7 @@ void ApplicationOverlay::renderQmlUi(RenderArgs* renderArgs) { PROFILE_RANGE(app, __FUNCTION__); if (!_uiTexture) { - _uiTexture = gpu::TexturePointer(gpu::Texture::createExternal2D(OffscreenQmlSurface::getDiscardLambda())); + _uiTexture = gpu::TexturePointer(gpu::Texture::createExternal(OffscreenQmlSurface::getDiscardLambda())); _uiTexture->setSource(__FUNCTION__); } // Once we move UI rendering and screen rendering to different @@ -163,45 +159,6 @@ void ApplicationOverlay::renderOverlays(RenderArgs* renderArgs) { qApp->getOverlays().renderHUD(renderArgs); } -void ApplicationOverlay::renderRearViewToFbo(RenderArgs* renderArgs) { -} - -void ApplicationOverlay::renderRearView(RenderArgs* renderArgs) { - if (!qApp->isHMDMode() && Menu::getInstance()->isOptionChecked(MenuOption::MiniMirror) && - !Menu::getInstance()->isOptionChecked(MenuOption::FullscreenMirror)) { - gpu::Batch& batch = *renderArgs->_batch; - - auto geometryCache = DependencyManager::get(); - - auto framebuffer = DependencyManager::get(); - auto selfieTexture = framebuffer->getSelfieFramebuffer()->getRenderBuffer(0); - - int width = renderArgs->_viewport.z; - int height = renderArgs->_viewport.w; - mat4 legacyProjection = glm::ortho(0, width, height, 0, ORTHO_NEAR_CLIP, ORTHO_FAR_CLIP); - batch.setProjectionTransform(legacyProjection); - batch.setModelTransform(Transform()); - batch.resetViewTransform(); - - float screenRatio = ((float)qApp->getDevicePixelRatio()); - float renderRatio = ((float)qApp->getRenderResolutionScale()); - - auto viewport = qApp->getMirrorViewRect(); - glm::vec2 bottomLeft(viewport.left(), viewport.top() + viewport.height()); - glm::vec2 topRight(viewport.left() + viewport.width(), viewport.top()); - bottomLeft *= screenRatio; - topRight *= screenRatio; - glm::vec2 texCoordMinCorner(0.0f, 0.0f); - glm::vec2 texCoordMaxCorner(viewport.width() * renderRatio / float(selfieTexture->getWidth()), viewport.height() * renderRatio / float(selfieTexture->getHeight())); - - batch.setResourceTexture(0, selfieTexture); - float alpha = DependencyManager::get()->getDesktop()->property("unpinnedAlpha").toFloat(); - geometryCache->renderQuad(batch, bottomLeft, topRight, texCoordMinCorner, texCoordMaxCorner, glm::vec4(1.0f, 1.0f, 1.0f, alpha), _rearViewGeometryId); - - batch.setResourceTexture(0, renderArgs->_whiteTexture); - } -} - void ApplicationOverlay::renderStatsAndLogs(RenderArgs* renderArgs) { // Display stats and log text onscreen @@ -272,13 +229,13 @@ void ApplicationOverlay::buildFramebufferObject() { auto width = uiSize.x; auto height = uiSize.y; if (!_overlayFramebuffer->getDepthStencilBuffer()) { - auto overlayDepthTexture = gpu::TexturePointer(gpu::Texture::create2D(DEPTH_FORMAT, width, height, DEFAULT_SAMPLER)); + auto overlayDepthTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(DEPTH_FORMAT, width, height, DEFAULT_SAMPLER)); _overlayFramebuffer->setDepthStencilBuffer(overlayDepthTexture, DEPTH_FORMAT); } if (!_overlayFramebuffer->getRenderBuffer(0)) { const gpu::Sampler OVERLAY_SAMPLER(gpu::Sampler::FILTER_MIN_MAG_LINEAR, gpu::Sampler::WRAP_CLAMP); - auto colorBuffer = gpu::TexturePointer(gpu::Texture::create2D(COLOR_FORMAT, width, height, OVERLAY_SAMPLER)); + auto colorBuffer = gpu::TexturePointer(gpu::Texture::createRenderBuffer(COLOR_FORMAT, width, height, OVERLAY_SAMPLER)); _overlayFramebuffer->setRenderBuffer(0, colorBuffer); } } diff --git a/interface/src/ui/ApplicationOverlay.h b/interface/src/ui/ApplicationOverlay.h index 7ace5ee885..af4d8779d4 100644 --- a/interface/src/ui/ApplicationOverlay.h +++ b/interface/src/ui/ApplicationOverlay.h @@ -31,8 +31,6 @@ public: private: void renderStatsAndLogs(RenderArgs* renderArgs); void renderDomainConnectionStatusBorder(RenderArgs* renderArgs); - void renderRearViewToFbo(RenderArgs* renderArgs); - void renderRearView(RenderArgs* renderArgs); void renderQmlUi(RenderArgs* renderArgs); void renderAudioScope(RenderArgs* renderArgs); void renderOverlays(RenderArgs* renderArgs); @@ -51,7 +49,6 @@ private: gpu::TexturePointer _overlayColorTexture; gpu::FramebufferPointer _overlayFramebuffer; int _qmlGeometryId { 0 }; - int _rearViewGeometryId { 0 }; }; #endif // hifi_ApplicationOverlay_h diff --git a/interface/src/ui/AvatarInputs.cpp b/interface/src/ui/AvatarInputs.cpp index b09289c78a..944be4bf9e 100644 --- a/interface/src/ui/AvatarInputs.cpp +++ b/interface/src/ui/AvatarInputs.cpp @@ -20,10 +20,6 @@ HIFI_QML_DEF(AvatarInputs) static AvatarInputs* INSTANCE{ nullptr }; -static const char SETTINGS_GROUP_NAME[] = "Rear View Tools"; -static const char ZOOM_LEVEL_SETTINGS[] = "ZoomLevel"; - -static Setting::Handle rearViewZoomLevel(QStringList() << SETTINGS_GROUP_NAME << ZOOM_LEVEL_SETTINGS, 0); AvatarInputs* AvatarInputs::getInstance() { if (!INSTANCE) { @@ -36,8 +32,6 @@ AvatarInputs* AvatarInputs::getInstance() { AvatarInputs::AvatarInputs(QQuickItem* parent) : QQuickItem(parent) { INSTANCE = this; - int zoomSetting = rearViewZoomLevel.get(); - _mirrorZoomed = zoomSetting == 0; } #define AI_UPDATE(name, src) \ @@ -62,8 +56,6 @@ void AvatarInputs::update() { if (!Menu::getInstance()) { return; } - AI_UPDATE(mirrorVisible, Menu::getInstance()->isOptionChecked(MenuOption::MiniMirror) && !qApp->isHMDMode() - && !Menu::getInstance()->isOptionChecked(MenuOption::FullscreenMirror)); AI_UPDATE(cameraEnabled, !Menu::getInstance()->isOptionChecked(MenuOption::NoFaceTracking)); AI_UPDATE(cameraMuted, Menu::getInstance()->isOptionChecked(MenuOption::MuteFaceTracking)); AI_UPDATE(isHMD, qApp->isHMDMode()); @@ -122,15 +114,3 @@ void AvatarInputs::toggleAudioMute() { void AvatarInputs::resetSensors() { qApp->resetSensors(); } - -void AvatarInputs::toggleZoom() { - _mirrorZoomed = !_mirrorZoomed; - rearViewZoomLevel.set(_mirrorZoomed ? 0 : 1); - emit mirrorZoomedChanged(); -} - -void AvatarInputs::closeMirror() { - if (Menu::getInstance()->isOptionChecked(MenuOption::MiniMirror)) { - Menu::getInstance()->triggerOption(MenuOption::MiniMirror); - } -} diff --git a/interface/src/ui/AvatarInputs.h b/interface/src/ui/AvatarInputs.h index 85570ecd3c..5535469445 100644 --- a/interface/src/ui/AvatarInputs.h +++ b/interface/src/ui/AvatarInputs.h @@ -28,8 +28,6 @@ class AvatarInputs : public QQuickItem { AI_PROPERTY(bool, audioMuted, false) AI_PROPERTY(bool, audioClipping, false) AI_PROPERTY(float, audioLevel, 0) - AI_PROPERTY(bool, mirrorVisible, false) - AI_PROPERTY(bool, mirrorZoomed, true) AI_PROPERTY(bool, isHMD, false) AI_PROPERTY(bool, showAudioTools, true) @@ -44,8 +42,6 @@ signals: void audioMutedChanged(); void audioClippingChanged(); void audioLevelChanged(); - void mirrorVisibleChanged(); - void mirrorZoomedChanged(); void isHMDChanged(); void showAudioToolsChanged(); @@ -53,8 +49,6 @@ protected: Q_INVOKABLE void resetSensors(); Q_INVOKABLE void toggleCameraMute(); Q_INVOKABLE void toggleAudioMute(); - Q_INVOKABLE void toggleZoom(); - Q_INVOKABLE void closeMirror(); private: float _trailingAudioLoudness{ 0 }; diff --git a/interface/src/ui/BaseLogDialog.cpp b/interface/src/ui/BaseLogDialog.cpp index 7e0027e0a8..571d3ac403 100644 --- a/interface/src/ui/BaseLogDialog.cpp +++ b/interface/src/ui/BaseLogDialog.cpp @@ -28,17 +28,23 @@ const int SEARCH_BUTTON_LEFT = 25; const int SEARCH_BUTTON_WIDTH = 20; const int SEARCH_TOGGLE_BUTTON_WIDTH = 50; const int SEARCH_TEXT_WIDTH = 240; +const int TIME_STAMP_LENGTH = 16; +const int FONT_WEIGHT = 75; const QColor HIGHLIGHT_COLOR = QColor("#3366CC"); +const QColor BOLD_COLOR = QColor("#445c8c"); +const QString BOLD_PATTERN = "\\[\\d*\\/.*:\\d*:\\d*\\]"; -class KeywordHighlighter : public QSyntaxHighlighter { +class Highlighter : public QSyntaxHighlighter { public: - KeywordHighlighter(QTextDocument* parent = nullptr); + Highlighter(QTextDocument* parent = nullptr); + void setBold(int indexToBold); QString keyword; protected: void highlightBlock(const QString& text) override; private: + QTextCharFormat boldFormat; QTextCharFormat keywordFormat; }; @@ -89,7 +95,7 @@ void BaseLogDialog::initControls() { _leftPad += SEARCH_TOGGLE_BUTTON_WIDTH + BUTTON_MARGIN; _searchPrevButton->show(); connect(_searchPrevButton, SIGNAL(clicked()), SLOT(toggleSearchPrev())); - + _searchNextButton = new QPushButton(this); _searchNextButton->setObjectName("searchNextButton"); _searchNextButton->setGeometry(_leftPad, ELEMENT_MARGIN, SEARCH_TOGGLE_BUTTON_WIDTH, ELEMENT_HEIGHT); @@ -101,9 +107,8 @@ void BaseLogDialog::initControls() { _logTextBox = new QPlainTextEdit(this); _logTextBox->setReadOnly(true); _logTextBox->show(); - _highlighter = new KeywordHighlighter(_logTextBox->document()); + _highlighter = new Highlighter(_logTextBox->document()); connect(_logTextBox, SIGNAL(selectionChanged()), SLOT(updateSelection())); - } void BaseLogDialog::showEvent(QShowEvent* event) { @@ -116,7 +121,9 @@ void BaseLogDialog::resizeEvent(QResizeEvent* event) { void BaseLogDialog::appendLogLine(QString logLine) { if (logLine.contains(_searchTerm, Qt::CaseInsensitive)) { + int indexToBold = _logTextBox->document()->characterCount(); _logTextBox->appendPlainText(logLine.trimmed()); + _highlighter->setBold(indexToBold); } } @@ -128,7 +135,7 @@ void BaseLogDialog::handleSearchTextChanged(QString searchText) { if (searchText.isEmpty()) { return; } - + QTextCursor cursor = _logTextBox->textCursor(); if (cursor.hasSelection()) { QString selectedTerm = cursor.selectedText(); @@ -136,16 +143,16 @@ void BaseLogDialog::handleSearchTextChanged(QString searchText) { return; } } - + cursor.setPosition(0, QTextCursor::MoveAnchor); _logTextBox->setTextCursor(cursor); bool foundTerm = _logTextBox->find(searchText); - + if (!foundTerm) { cursor.movePosition(QTextCursor::End, QTextCursor::MoveAnchor); _logTextBox->setTextCursor(cursor); } - + _searchTerm = searchText; _highlighter->keyword = searchText; _highlighter->rehighlight(); @@ -175,6 +182,7 @@ void BaseLogDialog::showLogData() { _logTextBox->clear(); _logTextBox->appendPlainText(getCurrentLog()); _logTextBox->ensureCursorVisible(); + _highlighter->rehighlight(); } void BaseLogDialog::updateSelection() { @@ -187,16 +195,28 @@ void BaseLogDialog::updateSelection() { } } -KeywordHighlighter::KeywordHighlighter(QTextDocument* parent) : QSyntaxHighlighter(parent) { +Highlighter::Highlighter(QTextDocument* parent) : QSyntaxHighlighter(parent) { + boldFormat.setFontWeight(FONT_WEIGHT); + boldFormat.setForeground(BOLD_COLOR); keywordFormat.setForeground(HIGHLIGHT_COLOR); } -void KeywordHighlighter::highlightBlock(const QString& text) { +void Highlighter::highlightBlock(const QString& text) { + QRegExp expression(BOLD_PATTERN); + + int index = text.indexOf(expression, 0); + + while (index >= 0) { + int length = expression.matchedLength(); + setFormat(index, length, boldFormat); + index = text.indexOf(expression, index + length); + } + if (keyword.isNull() || keyword.isEmpty()) { return; } - int index = text.indexOf(keyword, 0, Qt::CaseInsensitive); + index = text.indexOf(keyword, 0, Qt::CaseInsensitive); int length = keyword.length(); while (index >= 0) { @@ -204,3 +224,7 @@ void KeywordHighlighter::highlightBlock(const QString& text) { index = text.indexOf(keyword, index + length, Qt::CaseInsensitive); } } + +void Highlighter::setBold(int indexToBold) { + setFormat(indexToBold, TIME_STAMP_LENGTH, boldFormat); +} diff --git a/interface/src/ui/BaseLogDialog.h b/interface/src/ui/BaseLogDialog.h index d097010bae..e18d23937f 100644 --- a/interface/src/ui/BaseLogDialog.h +++ b/interface/src/ui/BaseLogDialog.h @@ -23,7 +23,7 @@ const int BUTTON_MARGIN = 8; class QPushButton; class QLineEdit; class QPlainTextEdit; -class KeywordHighlighter; +class Highlighter; class BaseLogDialog : public QDialog { Q_OBJECT @@ -56,7 +56,7 @@ private: QPushButton* _searchPrevButton { nullptr }; QPushButton* _searchNextButton { nullptr }; QString _searchTerm; - KeywordHighlighter* _highlighter { nullptr }; + Highlighter* _highlighter { nullptr }; void initControls(); void showLogData(); diff --git a/interface/src/ui/CachesSizeDialog.cpp b/interface/src/ui/CachesSizeDialog.cpp deleted file mode 100644 index 935a6d126e..0000000000 --- a/interface/src/ui/CachesSizeDialog.cpp +++ /dev/null @@ -1,84 +0,0 @@ -// -// CachesSizeDialog.cpp -// -// -// Created by Clement on 1/12/15. -// Copyright 2015 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include -#include -#include - -#include -#include -#include -#include -#include - -#include "CachesSizeDialog.h" - - -QDoubleSpinBox* createDoubleSpinBox(QWidget* parent) { - QDoubleSpinBox* box = new QDoubleSpinBox(parent); - box->setDecimals(0); - box->setRange(MIN_UNUSED_MAX_SIZE / BYTES_PER_MEGABYTES, MAX_UNUSED_MAX_SIZE / BYTES_PER_MEGABYTES); - - return box; -} - -CachesSizeDialog::CachesSizeDialog(QWidget* parent) : - QDialog(parent, Qt::Window | Qt::WindowCloseButtonHint) -{ - setWindowTitle("Caches Size"); - - // Create layouter - QFormLayout* form = new QFormLayout(this); - setLayout(form); - - form->addRow("Animations cache size (MB):", _animations = createDoubleSpinBox(this)); - form->addRow("Geometries cache size (MB):", _geometries = createDoubleSpinBox(this)); - form->addRow("Sounds cache size (MB):", _sounds = createDoubleSpinBox(this)); - form->addRow("Textures cache size (MB):", _textures = createDoubleSpinBox(this)); - - resetClicked(true); - - // Add a button to reset - QPushButton* confirmButton = new QPushButton("Confirm", this); - QPushButton* resetButton = new QPushButton("Reset", this); - form->addRow(confirmButton, resetButton); - connect(confirmButton, SIGNAL(clicked(bool)), this, SLOT(confirmClicked(bool))); - connect(resetButton, SIGNAL(clicked(bool)), this, SLOT(resetClicked(bool))); -} - -void CachesSizeDialog::confirmClicked(bool checked) { - DependencyManager::get()->setUnusedResourceCacheSize(_animations->value() * BYTES_PER_MEGABYTES); - DependencyManager::get()->setUnusedResourceCacheSize(_geometries->value() * BYTES_PER_MEGABYTES); - DependencyManager::get()->setUnusedResourceCacheSize(_sounds->value() * BYTES_PER_MEGABYTES); - // Disabling the texture cache because it's a liability in cases where we're overcommiting GPU memory -#if 0 - DependencyManager::get()->setUnusedResourceCacheSize(_textures->value() * BYTES_PER_MEGABYTES); -#endif - - QDialog::close(); -} - -void CachesSizeDialog::resetClicked(bool checked) { - _animations->setValue(DependencyManager::get()->getUnusedResourceCacheSize() / BYTES_PER_MEGABYTES); - _geometries->setValue(DependencyManager::get()->getUnusedResourceCacheSize() / BYTES_PER_MEGABYTES); - _sounds->setValue(DependencyManager::get()->getUnusedResourceCacheSize() / BYTES_PER_MEGABYTES); - _textures->setValue(DependencyManager::get()->getUnusedResourceCacheSize() / BYTES_PER_MEGABYTES); -} - -void CachesSizeDialog::reject() { - // Just regularly close upon ESC - QDialog::close(); -} - -void CachesSizeDialog::closeEvent(QCloseEvent* event) { - QDialog::closeEvent(event); - emit closed(); -} diff --git a/interface/src/ui/CachesSizeDialog.h b/interface/src/ui/CachesSizeDialog.h deleted file mode 100644 index 025d0f2bac..0000000000 --- a/interface/src/ui/CachesSizeDialog.h +++ /dev/null @@ -1,45 +0,0 @@ -// -// CachesSizeDialog.h -// -// -// Created by Clement on 1/12/15. -// Copyright 2015 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#ifndef hifi_CachesSizeDialog_h -#define hifi_CachesSizeDialog_h - -#include - -class QDoubleSpinBox; - -class CachesSizeDialog : public QDialog { - Q_OBJECT -public: - // Sets up the UI - CachesSizeDialog(QWidget* parent); - -signals: - void closed(); - -public slots: - void reject() override; - void confirmClicked(bool checked); - void resetClicked(bool checked); - -protected: - // Emits a 'closed' signal when this dialog is closed. - void closeEvent(QCloseEvent* event) override; - -private: - QDoubleSpinBox* _animations = nullptr; - QDoubleSpinBox* _geometries = nullptr; - QDoubleSpinBox* _scripts = nullptr; - QDoubleSpinBox* _sounds = nullptr; - QDoubleSpinBox* _textures = nullptr; -}; - -#endif // hifi_CachesSizeDialog_h diff --git a/interface/src/ui/DialogsManager.cpp b/interface/src/ui/DialogsManager.cpp index 3252fef4f0..f1d6f585d7 100644 --- a/interface/src/ui/DialogsManager.cpp +++ b/interface/src/ui/DialogsManager.cpp @@ -19,16 +19,13 @@ #include #include "AddressBarDialog.h" -#include "CachesSizeDialog.h" #include "ConnectionFailureDialog.h" -#include "DiskCacheEditor.h" #include "DomainConnectionDialog.h" #include "HMDToolsDialog.h" #include "LodToolsDialog.h" #include "LoginDialog.h" #include "OctreeStatsDialog.h" #include "PreferencesDialog.h" -#include "ScriptEditorWindow.h" #include "UpdateDialog.h" template @@ -67,11 +64,6 @@ void DialogsManager::setDomainConnectionFailureVisibility(bool visible) { } } -void DialogsManager::toggleDiskCacheEditor() { - maybeCreateDialog(_diskCacheEditor); - _diskCacheEditor->toggle(); -} - void DialogsManager::toggleLoginDialog() { LoginDialog::toggleAction(); } @@ -97,16 +89,6 @@ void DialogsManager::octreeStatsDetails() { _octreeStatsDialog->raise(); } -void DialogsManager::cachesSizeDialog() { - if (!_cachesSizeDialog) { - maybeCreateDialog(_cachesSizeDialog); - - connect(_cachesSizeDialog, SIGNAL(closed()), _cachesSizeDialog, SLOT(deleteLater())); - _cachesSizeDialog->show(); - } - _cachesSizeDialog->raise(); -} - void DialogsManager::lodTools() { if (!_lodToolsDialog) { maybeCreateDialog(_lodToolsDialog); @@ -137,12 +119,6 @@ void DialogsManager::hmdToolsClosed() { } } -void DialogsManager::showScriptEditor() { - maybeCreateDialog(_scriptEditor); - _scriptEditor->show(); - _scriptEditor->raise(); -} - void DialogsManager::showTestingResults() { if (!_testingDialog) { _testingDialog = new TestingDialog(qApp->getWindow()); diff --git a/interface/src/ui/DialogsManager.h b/interface/src/ui/DialogsManager.h index 54aef38984..608195aca7 100644 --- a/interface/src/ui/DialogsManager.h +++ b/interface/src/ui/DialogsManager.h @@ -22,7 +22,6 @@ class AnimationsDialog; class AttachmentsDialog; class CachesSizeDialog; -class DiskCacheEditor; class LodToolsDialog; class OctreeStatsDialog; class ScriptEditorWindow; @@ -46,14 +45,11 @@ public slots: void showAddressBar(); void showFeed(); void setDomainConnectionFailureVisibility(bool visible); - void toggleDiskCacheEditor(); void toggleLoginDialog(); void showLoginDialog(); void octreeStatsDetails(); - void cachesSizeDialog(); void lodTools(); void hmdTools(bool showTools); - void showScriptEditor(); void showDomainConnectionDialog(); void showTestingResults(); @@ -77,12 +73,10 @@ private: QPointer _animationsDialog; QPointer _attachmentsDialog; QPointer _cachesSizeDialog; - QPointer _diskCacheEditor; QPointer _ircInfoBox; QPointer _hmdToolsDialog; QPointer _lodToolsDialog; QPointer _octreeStatsDialog; - QPointer _scriptEditor; QPointer _testingDialog; QPointer _domainConnectionDialog; }; diff --git a/interface/src/ui/DiskCacheEditor.cpp b/interface/src/ui/DiskCacheEditor.cpp deleted file mode 100644 index 1a7be8642b..0000000000 --- a/interface/src/ui/DiskCacheEditor.cpp +++ /dev/null @@ -1,146 +0,0 @@ -// -// DiskCacheEditor.cpp -// -// -// Created by Clement on 3/4/15. -// Copyright 2015 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include "DiskCacheEditor.h" - -#include -#include -#include -#include -#include -#include -#include - -#include - -#include "OffscreenUi.h" - -DiskCacheEditor::DiskCacheEditor(QWidget* parent) : QObject(parent) { -} - -QWindow* DiskCacheEditor::windowHandle() { - return (_dialog) ? _dialog->windowHandle() : nullptr; -} - -void DiskCacheEditor::toggle() { - if (!_dialog) { - makeDialog(); - } - - if (!_dialog->isActiveWindow()) { - _dialog->show(); - _dialog->raise(); - _dialog->activateWindow(); - } else { - _dialog->close(); - } -} - -void DiskCacheEditor::makeDialog() { - _dialog = new QDialog(static_cast(parent())); - Q_CHECK_PTR(_dialog); - _dialog->setAttribute(Qt::WA_DeleteOnClose); - _dialog->setWindowTitle("Disk Cache Editor"); - - QGridLayout* layout = new QGridLayout(_dialog); - Q_CHECK_PTR(layout); - _dialog->setLayout(layout); - - - QLabel* path = new QLabel("Path : ", _dialog); - Q_CHECK_PTR(path); - path->setAlignment(Qt::AlignRight); - layout->addWidget(path, 0, 0); - - QLabel* size = new QLabel("Current Size : ", _dialog); - Q_CHECK_PTR(size); - size->setAlignment(Qt::AlignRight); - layout->addWidget(size, 1, 0); - - QLabel* maxSize = new QLabel("Max Size : ", _dialog); - Q_CHECK_PTR(maxSize); - maxSize->setAlignment(Qt::AlignRight); - layout->addWidget(maxSize, 2, 0); - - - _path = new QLabel(_dialog); - Q_CHECK_PTR(_path); - _path->setAlignment(Qt::AlignLeft); - layout->addWidget(_path, 0, 1, 1, 3); - - _size = new QLabel(_dialog); - Q_CHECK_PTR(_size); - _size->setAlignment(Qt::AlignLeft); - layout->addWidget(_size, 1, 1, 1, 3); - - _maxSize = new QLabel(_dialog); - Q_CHECK_PTR(_maxSize); - _maxSize->setAlignment(Qt::AlignLeft); - layout->addWidget(_maxSize, 2, 1, 1, 3); - - refresh(); - - - static const int REFRESH_INTERVAL = 100; // msec - _refreshTimer = new QTimer(_dialog); - _refreshTimer->setInterval(REFRESH_INTERVAL); // Qt::CoarseTimer acceptable, no need for real time accuracy - _refreshTimer->setSingleShot(false); - QObject::connect(_refreshTimer.data(), &QTimer::timeout, this, &DiskCacheEditor::refresh); - _refreshTimer->start(); - - QPushButton* clearCacheButton = new QPushButton(_dialog); - Q_CHECK_PTR(clearCacheButton); - clearCacheButton->setText("Clear"); - clearCacheButton->setToolTip("Erases the entire content of the disk cache."); - connect(clearCacheButton, SIGNAL(clicked()), SLOT(clear())); - layout->addWidget(clearCacheButton, 3, 3); -} - -void DiskCacheEditor::refresh() { - DependencyManager::get()->cacheInfoRequest(this, "cacheInfoCallback"); -} - -void DiskCacheEditor::cacheInfoCallback(QString cacheDirectory, qint64 cacheSize, qint64 maximumCacheSize) { - static const auto stringify = [](qint64 number) { - static const QStringList UNITS = QStringList() << "B" << "KB" << "MB" << "GB"; - static const qint64 CHUNK = 1024; - QString unit; - int i = 0; - for (i = 0; i < 4; ++i) { - if (number / CHUNK > 0) { - number /= CHUNK; - } else { - break; - } - } - return QString("%0 %1").arg(number).arg(UNITS[i]); - }; - - if (_path) { - _path->setText(cacheDirectory); - } - if (_size) { - _size->setText(stringify(cacheSize)); - } - if (_maxSize) { - _maxSize->setText(stringify(maximumCacheSize)); - } -} - -void DiskCacheEditor::clear() { - auto buttonClicked = OffscreenUi::question(_dialog, "Clearing disk cache", - "You are about to erase all the content of the disk cache, " - "are you sure you want to do that?", - QMessageBox::Ok | QMessageBox::Cancel); - if (buttonClicked == QMessageBox::Ok) { - DependencyManager::get()->clearCache(); - } -} diff --git a/interface/src/ui/DiskCacheEditor.h b/interface/src/ui/DiskCacheEditor.h deleted file mode 100644 index 3f8fa1a883..0000000000 --- a/interface/src/ui/DiskCacheEditor.h +++ /dev/null @@ -1,49 +0,0 @@ -// -// DiskCacheEditor.h -// -// -// Created by Clement on 3/4/15. -// Copyright 2015 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#ifndef hifi_DiskCacheEditor_h -#define hifi_DiskCacheEditor_h - -#include -#include - -class QDialog; -class QLabel; -class QWindow; -class QTimer; - -class DiskCacheEditor : public QObject { - Q_OBJECT - -public: - DiskCacheEditor(QWidget* parent = nullptr); - - QWindow* windowHandle(); - -public slots: - void toggle(); - -private slots: - void refresh(); - void cacheInfoCallback(QString cacheDirectory, qint64 cacheSize, qint64 maximumCacheSize); - void clear(); - -private: - void makeDialog(); - - QPointer _dialog; - QPointer _path; - QPointer _size; - QPointer _maxSize; - QPointer _refreshTimer; -}; - -#endif // hifi_DiskCacheEditor_h \ No newline at end of file diff --git a/interface/src/ui/ScriptEditBox.cpp b/interface/src/ui/ScriptEditBox.cpp deleted file mode 100644 index 2aea225b17..0000000000 --- a/interface/src/ui/ScriptEditBox.cpp +++ /dev/null @@ -1,111 +0,0 @@ -// -// ScriptEditBox.cpp -// interface/src/ui -// -// Created by Thijs Wenker on 4/30/14. -// Copyright 2014 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include "ScriptEditBox.h" - -#include -#include - -#include "ScriptLineNumberArea.h" - -ScriptEditBox::ScriptEditBox(QWidget* parent) : - QPlainTextEdit(parent) -{ - _scriptLineNumberArea = new ScriptLineNumberArea(this); - - connect(this, &ScriptEditBox::blockCountChanged, this, &ScriptEditBox::updateLineNumberAreaWidth); - connect(this, &ScriptEditBox::updateRequest, this, &ScriptEditBox::updateLineNumberArea); - connect(this, &ScriptEditBox::cursorPositionChanged, this, &ScriptEditBox::highlightCurrentLine); - - updateLineNumberAreaWidth(0); - highlightCurrentLine(); -} - -int ScriptEditBox::lineNumberAreaWidth() { - int digits = 1; - const int SPACER_PIXELS = 3; - const int BASE_TEN = 10; - int max = qMax(1, blockCount()); - while (max >= BASE_TEN) { - max /= BASE_TEN; - digits++; - } - return SPACER_PIXELS + fontMetrics().width(QLatin1Char('H')) * digits; -} - -void ScriptEditBox::updateLineNumberAreaWidth(int blockCount) { - setViewportMargins(lineNumberAreaWidth(), 0, 0, 0); -} - -void ScriptEditBox::updateLineNumberArea(const QRect& rect, int deltaY) { - if (deltaY) { - _scriptLineNumberArea->scroll(0, deltaY); - } else { - _scriptLineNumberArea->update(0, rect.y(), _scriptLineNumberArea->width(), rect.height()); - } - - if (rect.contains(viewport()->rect())) { - updateLineNumberAreaWidth(0); - } -} - -void ScriptEditBox::resizeEvent(QResizeEvent* event) { - QPlainTextEdit::resizeEvent(event); - - QRect localContentsRect = contentsRect(); - _scriptLineNumberArea->setGeometry(QRect(localContentsRect.left(), localContentsRect.top(), lineNumberAreaWidth(), - localContentsRect.height())); -} - -void ScriptEditBox::highlightCurrentLine() { - QList extraSelections; - - if (!isReadOnly()) { - QTextEdit::ExtraSelection selection; - - QColor lineColor = QColor(Qt::gray).lighter(); - - selection.format.setBackground(lineColor); - selection.format.setProperty(QTextFormat::FullWidthSelection, true); - selection.cursor = textCursor(); - selection.cursor.clearSelection(); - extraSelections.append(selection); - } - - setExtraSelections(extraSelections); -} - -void ScriptEditBox::lineNumberAreaPaintEvent(QPaintEvent* event) -{ - QPainter painter(_scriptLineNumberArea); - painter.fillRect(event->rect(), Qt::lightGray); - QTextBlock block = firstVisibleBlock(); - int blockNumber = block.blockNumber(); - int top = (int) blockBoundingGeometry(block).translated(contentOffset()).top(); - int bottom = top + (int) blockBoundingRect(block).height(); - - while (block.isValid() && top <= event->rect().bottom()) { - if (block.isVisible() && bottom >= event->rect().top()) { - QFont font = painter.font(); - font.setBold(this->textCursor().blockNumber() == block.blockNumber()); - painter.setFont(font); - QString number = QString::number(blockNumber + 1); - painter.setPen(Qt::black); - painter.drawText(0, top, _scriptLineNumberArea->width(), fontMetrics().height(), - Qt::AlignRight, number); - } - - block = block.next(); - top = bottom; - bottom = top + (int) blockBoundingRect(block).height(); - blockNumber++; - } -} diff --git a/interface/src/ui/ScriptEditBox.h b/interface/src/ui/ScriptEditBox.h deleted file mode 100644 index 0b037db16a..0000000000 --- a/interface/src/ui/ScriptEditBox.h +++ /dev/null @@ -1,38 +0,0 @@ -// -// ScriptEditBox.h -// interface/src/ui -// -// Created by Thijs Wenker on 4/30/14. -// Copyright 2014 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#ifndef hifi_ScriptEditBox_h -#define hifi_ScriptEditBox_h - -#include - -class ScriptEditBox : public QPlainTextEdit { - Q_OBJECT - -public: - ScriptEditBox(QWidget* parent = NULL); - - void lineNumberAreaPaintEvent(QPaintEvent* event); - int lineNumberAreaWidth(); - -protected: - void resizeEvent(QResizeEvent* event) override; - -private slots: - void updateLineNumberAreaWidth(int blockCount); - void highlightCurrentLine(); - void updateLineNumberArea(const QRect& rect, int deltaY); - -private: - QWidget* _scriptLineNumberArea; -}; - -#endif // hifi_ScriptEditBox_h diff --git a/interface/src/ui/ScriptEditorWidget.cpp b/interface/src/ui/ScriptEditorWidget.cpp deleted file mode 100644 index ada6b11355..0000000000 --- a/interface/src/ui/ScriptEditorWidget.cpp +++ /dev/null @@ -1,256 +0,0 @@ -// -// ScriptEditorWidget.cpp -// interface/src/ui -// -// Created by Thijs Wenker on 4/14/14. -// Copyright 2014 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include "ui_scriptEditorWidget.h" -#include "ScriptEditorWidget.h" -#include "ScriptEditorWindow.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -#include "Application.h" -#include "ScriptHighlighting.h" - -ScriptEditorWidget::ScriptEditorWidget() : - _scriptEditorWidgetUI(new Ui::ScriptEditorWidget), - _scriptEngine(NULL), - _isRestarting(false), - _isReloading(false) -{ - setAttribute(Qt::WA_DeleteOnClose); - - _scriptEditorWidgetUI->setupUi(this); - - connect(_scriptEditorWidgetUI->scriptEdit->document(), &QTextDocument::modificationChanged, this, - &ScriptEditorWidget::scriptModified); - connect(_scriptEditorWidgetUI->scriptEdit->document(), &QTextDocument::contentsChanged, this, - &ScriptEditorWidget::onScriptModified); - - // remove the title bar (see the Qt docs on setTitleBarWidget) - setTitleBarWidget(new QWidget()); - QFontMetrics fm(_scriptEditorWidgetUI->scriptEdit->font()); - _scriptEditorWidgetUI->scriptEdit->setTabStopWidth(fm.width('0') * 4); - // We create a new ScriptHighligting QObject and provide it with a parent so this is NOT a memory leak. - new ScriptHighlighting(_scriptEditorWidgetUI->scriptEdit->document()); - QTimer::singleShot(0, _scriptEditorWidgetUI->scriptEdit, SLOT(setFocus())); - - _console = new JSConsole(this); - _console->setFixedHeight(CONSOLE_HEIGHT); - _scriptEditorWidgetUI->verticalLayout->addWidget(_console); - connect(_scriptEditorWidgetUI->clearButton, &QPushButton::clicked, _console, &JSConsole::clear); -} - -ScriptEditorWidget::~ScriptEditorWidget() { - delete _scriptEditorWidgetUI; - delete _console; -} - -void ScriptEditorWidget::onScriptModified() { - if(_scriptEditorWidgetUI->onTheFlyCheckBox->isChecked() && isModified() && isRunning() && !_isReloading) { - _isRestarting = true; - setRunning(false); - // Script is restarted once current script instance finishes. - } -} - -void ScriptEditorWidget::onScriptFinished(const QString& scriptPath) { - _scriptEngine = NULL; - _console->setScriptEngine(NULL); - if (_isRestarting) { - _isRestarting = false; - setRunning(true); - } -} - -bool ScriptEditorWidget::isModified() { - return _scriptEditorWidgetUI->scriptEdit->document()->isModified(); -} - -bool ScriptEditorWidget::isRunning() { - return (_scriptEngine != NULL) ? _scriptEngine->isRunning() : false; -} - -bool ScriptEditorWidget::setRunning(bool run) { - if (run && isModified() && !save()) { - return false; - } - - if (_scriptEngine != NULL) { - disconnect(_scriptEngine, &ScriptEngine::runningStateChanged, this, &ScriptEditorWidget::runningStateChanged); - disconnect(_scriptEngine, &ScriptEngine::update, this, &ScriptEditorWidget::onScriptModified); - disconnect(_scriptEngine, &ScriptEngine::finished, this, &ScriptEditorWidget::onScriptFinished); - } - - auto scriptEngines = DependencyManager::get(); - if (run) { - const QString& scriptURLString = QUrl(_currentScript).toString(); - // Reload script so that an out of date copy is not retrieved from the cache - _scriptEngine = scriptEngines->loadScript(scriptURLString, true, true, false, true); - connect(_scriptEngine, &ScriptEngine::runningStateChanged, this, &ScriptEditorWidget::runningStateChanged); - connect(_scriptEngine, &ScriptEngine::update, this, &ScriptEditorWidget::onScriptModified); - connect(_scriptEngine, &ScriptEngine::finished, this, &ScriptEditorWidget::onScriptFinished); - } else { - connect(_scriptEngine, &ScriptEngine::finished, this, &ScriptEditorWidget::onScriptFinished); - const QString& scriptURLString = QUrl(_currentScript).toString(); - scriptEngines->stopScript(scriptURLString); - _scriptEngine = NULL; - } - _console->setScriptEngine(_scriptEngine); - return true; -} - -bool ScriptEditorWidget::saveFile(const QString &scriptPath) { - QFile file(scriptPath); - if (!file.open(QFile::WriteOnly | QFile::Text)) { - OffscreenUi::warning(this, tr("Interface"), tr("Cannot write script %1:\n%2.").arg(scriptPath) - .arg(file.errorString())); - return false; - } - - QTextStream out(&file); - out << _scriptEditorWidgetUI->scriptEdit->toPlainText(); - file.close(); - - setScriptFile(scriptPath); - return true; -} - -void ScriptEditorWidget::loadFile(const QString& scriptPath) { - QUrl url(scriptPath); - - // if the scheme length is one or lower, maybe they typed in a file, let's try - const int WINDOWS_DRIVE_LETTER_SIZE = 1; - if (url.scheme().size() <= WINDOWS_DRIVE_LETTER_SIZE) { - QFile file(scriptPath); - if (!file.open(QFile::ReadOnly | QFile::Text)) { - OffscreenUi::warning(this, tr("Interface"), tr("Cannot read script %1:\n%2.").arg(scriptPath) - .arg(file.errorString())); - return; - } - QTextStream in(&file); - _scriptEditorWidgetUI->scriptEdit->setPlainText(in.readAll()); - file.close(); - setScriptFile(scriptPath); - - if (_scriptEngine != NULL) { - disconnect(_scriptEngine, &ScriptEngine::runningStateChanged, this, &ScriptEditorWidget::runningStateChanged); - disconnect(_scriptEngine, &ScriptEngine::update, this, &ScriptEditorWidget::onScriptModified); - disconnect(_scriptEngine, &ScriptEngine::finished, this, &ScriptEditorWidget::onScriptFinished); - } - } else { - QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); - QNetworkRequest networkRequest = QNetworkRequest(url); - networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); - networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); - QNetworkReply* reply = networkAccessManager.get(networkRequest); - qDebug() << "Downloading included script at" << scriptPath; - QEventLoop loop; - QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); - loop.exec(); - _scriptEditorWidgetUI->scriptEdit->setPlainText(reply->readAll()); - delete reply; - - if (!saveAs()) { - static_cast(this->parent()->parent()->parent())->terminateCurrentTab(); - } - } - const QString& scriptURLString = QUrl(_currentScript).toString(); - _scriptEngine = DependencyManager::get()->getScriptEngine(scriptURLString); - if (_scriptEngine != NULL) { - connect(_scriptEngine, &ScriptEngine::runningStateChanged, this, &ScriptEditorWidget::runningStateChanged); - connect(_scriptEngine, &ScriptEngine::update, this, &ScriptEditorWidget::onScriptModified); - connect(_scriptEngine, &ScriptEngine::finished, this, &ScriptEditorWidget::onScriptFinished); - } - _console->setScriptEngine(_scriptEngine); -} - -bool ScriptEditorWidget::save() { - return _currentScript.isEmpty() ? saveAs() : saveFile(_currentScript); -} - -bool ScriptEditorWidget::saveAs() { - auto scriptEngines = DependencyManager::get(); - QString fileName = QFileDialog::getSaveFileName(this, tr("Save script"), - qApp->getPreviousScriptLocation(), - tr("JavaScript Files (*.js)")); - if (!fileName.isEmpty()) { - qApp->setPreviousScriptLocation(fileName); - return saveFile(fileName); - } else { - return false; - } -} - -void ScriptEditorWidget::setScriptFile(const QString& scriptPath) { - _currentScript = scriptPath; - _currentScriptModified = QFileInfo(_currentScript).lastModified(); - _scriptEditorWidgetUI->scriptEdit->document()->setModified(false); - setWindowModified(false); - - emit scriptnameChanged(); -} - -bool ScriptEditorWidget::questionSave() { - if (_scriptEditorWidgetUI->scriptEdit->document()->isModified()) { - QMessageBox::StandardButton button = OffscreenUi::warning(this, tr("Interface"), - tr("The script has been modified.\nDo you want to save your changes?"), QMessageBox::Save | QMessageBox::Discard | - QMessageBox::Cancel, QMessageBox::Save); - return button == QMessageBox::Save ? save() : (button == QMessageBox::Discard); - } - return true; -} - -void ScriptEditorWidget::onWindowActivated() { - if (!_isReloading) { - _isReloading = true; - - QDateTime fileStamp = QFileInfo(_currentScript).lastModified(); - if (fileStamp > _currentScriptModified) { - bool doReload = false; - auto window = static_cast(this->parent()->parent()->parent()); - window->inModalDialog = true; - if (window->autoReloadScripts() - || OffscreenUi::question(this, tr("Reload Script"), - tr("The following file has been modified outside of the Interface editor:") + "\n" + _currentScript + "\n" - + (isModified() - ? tr("Do you want to reload it and lose the changes you've made in the Interface editor?") - : tr("Do you want to reload it?")), - QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { - doReload = true; - } - window->inModalDialog = false; - if (doReload) { - loadFile(_currentScript); - if (_scriptEditorWidgetUI->onTheFlyCheckBox->isChecked() && isRunning()) { - _isRestarting = true; - setRunning(false); - // Script is restarted once current script instance finishes. - } - } else { - _currentScriptModified = fileStamp; // Asked and answered. Don't ask again until the external file is changed again. - } - } - _isReloading = false; - } -} diff --git a/interface/src/ui/ScriptEditorWidget.h b/interface/src/ui/ScriptEditorWidget.h deleted file mode 100644 index f53fd7b718..0000000000 --- a/interface/src/ui/ScriptEditorWidget.h +++ /dev/null @@ -1,64 +0,0 @@ -// -// ScriptEditorWidget.h -// interface/src/ui -// -// Created by Thijs Wenker on 4/14/14. -// Copyright 2014 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#ifndef hifi_ScriptEditorWidget_h -#define hifi_ScriptEditorWidget_h - -#include - -#include "JSConsole.h" -#include "ScriptEngine.h" - -namespace Ui { - class ScriptEditorWidget; -} - -class ScriptEditorWidget : public QDockWidget { - Q_OBJECT - -public: - ScriptEditorWidget(); - ~ScriptEditorWidget(); - - bool isModified(); - bool isRunning(); - bool setRunning(bool run); - bool saveFile(const QString& scriptPath); - void loadFile(const QString& scriptPath); - void setScriptFile(const QString& scriptPath); - bool save(); - bool saveAs(); - bool questionSave(); - const QString getScriptName() const { return _currentScript; }; - -signals: - void runningStateChanged(); - void scriptnameChanged(); - void scriptModified(); - -public slots: - void onWindowActivated(); - -private slots: - void onScriptModified(); - void onScriptFinished(const QString& scriptName); - -private: - JSConsole* _console; - Ui::ScriptEditorWidget* _scriptEditorWidgetUI; - ScriptEngine* _scriptEngine; - QString _currentScript; - QDateTime _currentScriptModified; - bool _isRestarting; - bool _isReloading; -}; - -#endif // hifi_ScriptEditorWidget_h diff --git a/interface/src/ui/ScriptEditorWindow.cpp b/interface/src/ui/ScriptEditorWindow.cpp deleted file mode 100644 index 58abd23979..0000000000 --- a/interface/src/ui/ScriptEditorWindow.cpp +++ /dev/null @@ -1,259 +0,0 @@ -// -// ScriptEditorWindow.cpp -// interface/src/ui -// -// Created by Thijs Wenker on 4/14/14. -// Copyright 2014 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include - -#include "ui_scriptEditorWindow.h" -#include "ScriptEditorWindow.h" -#include "ScriptEditorWidget.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include "Application.h" -#include "PathUtils.h" - -ScriptEditorWindow::ScriptEditorWindow(QWidget* parent) : - QWidget(parent), - _ScriptEditorWindowUI(new Ui::ScriptEditorWindow), - _loadMenu(new QMenu), - _saveMenu(new QMenu) -{ - setAttribute(Qt::WA_DeleteOnClose); - - _ScriptEditorWindowUI->setupUi(this); - - this->setWindowFlags(Qt::Tool); - addScriptEditorWidget("New script"); - connect(_loadMenu, &QMenu::aboutToShow, this, &ScriptEditorWindow::loadMenuAboutToShow); - _ScriptEditorWindowUI->loadButton->setMenu(_loadMenu); - - _saveMenu->addAction("Save as..", this, SLOT(saveScriptAsClicked()), Qt::CTRL | Qt::SHIFT | Qt::Key_S); - - _ScriptEditorWindowUI->saveButton->setMenu(_saveMenu); - - connect(new QShortcut(QKeySequence("Ctrl+N"), this), &QShortcut::activated, this, &ScriptEditorWindow::newScriptClicked); - connect(new QShortcut(QKeySequence("Ctrl+S"), this), &QShortcut::activated, this,&ScriptEditorWindow::saveScriptClicked); - connect(new QShortcut(QKeySequence("Ctrl+O"), this), &QShortcut::activated, this, &ScriptEditorWindow::loadScriptClicked); - connect(new QShortcut(QKeySequence("F5"), this), &QShortcut::activated, this, &ScriptEditorWindow::toggleRunScriptClicked); - - _ScriptEditorWindowUI->loadButton->setIcon(QIcon(QPixmap(PathUtils::resourcesPath() + "icons/load-script.svg"))); - _ScriptEditorWindowUI->newButton->setIcon(QIcon(QPixmap(PathUtils::resourcesPath() + "icons/new-script.svg"))); - _ScriptEditorWindowUI->saveButton->setIcon(QIcon(QPixmap(PathUtils::resourcesPath() + "icons/save-script.svg"))); - _ScriptEditorWindowUI->toggleRunButton->setIcon(QIcon(QPixmap(PathUtils::resourcesPath() + "icons/start-script.svg"))); -} - -ScriptEditorWindow::~ScriptEditorWindow() { - delete _ScriptEditorWindowUI; -} - -void ScriptEditorWindow::setRunningState(bool run) { - if (_ScriptEditorWindowUI->tabWidget->currentIndex() != -1) { - static_cast(_ScriptEditorWindowUI->tabWidget->currentWidget())->setRunning(run); - } - this->updateButtons(); -} - -void ScriptEditorWindow::updateButtons() { - bool isRunning = _ScriptEditorWindowUI->tabWidget->currentIndex() != -1 && - static_cast(_ScriptEditorWindowUI->tabWidget->currentWidget())->isRunning(); - _ScriptEditorWindowUI->toggleRunButton->setEnabled(_ScriptEditorWindowUI->tabWidget->currentIndex() != -1); - _ScriptEditorWindowUI->toggleRunButton->setIcon(QIcon(QPixmap(PathUtils::resourcesPath() + ((isRunning ? - "icons/stop-script.svg" : "icons/start-script.svg"))))); -} - -void ScriptEditorWindow::loadScriptMenu(const QString& scriptName) { - addScriptEditorWidget("loading...")->loadFile(scriptName); - updateButtons(); -} - -void ScriptEditorWindow::loadScriptClicked() { - QString scriptName = QFileDialog::getOpenFileName(this, tr("Interface"), - qApp->getPreviousScriptLocation(), - tr("JavaScript Files (*.js)")); - if (!scriptName.isEmpty()) { - qApp->setPreviousScriptLocation(scriptName); - addScriptEditorWidget("loading...")->loadFile(scriptName); - updateButtons(); - } -} - -void ScriptEditorWindow::loadMenuAboutToShow() { - _loadMenu->clear(); - QStringList runningScripts = DependencyManager::get()->getRunningScripts(); - if (runningScripts.count() > 0) { - QSignalMapper* signalMapper = new QSignalMapper(this); - foreach (const QString& runningScript, runningScripts) { - QAction* runningScriptAction = new QAction(runningScript, _loadMenu); - connect(runningScriptAction, SIGNAL(triggered()), signalMapper, SLOT(map())); - signalMapper->setMapping(runningScriptAction, runningScript); - _loadMenu->addAction(runningScriptAction); - } - connect(signalMapper, SIGNAL(mapped(const QString &)), this, SLOT(loadScriptMenu(const QString&))); - } else { - QAction* naAction = new QAction("(no running scripts)", _loadMenu); - naAction->setDisabled(true); - _loadMenu->addAction(naAction); - } -} - -void ScriptEditorWindow::newScriptClicked() { - addScriptEditorWidget(QString("New script")); -} - -void ScriptEditorWindow::toggleRunScriptClicked() { - this->setRunningState(!(_ScriptEditorWindowUI->tabWidget->currentIndex() !=-1 - && static_cast(_ScriptEditorWindowUI->tabWidget->currentWidget())->isRunning())); -} - -void ScriptEditorWindow::saveScriptClicked() { - if (_ScriptEditorWindowUI->tabWidget->currentIndex() != -1) { - ScriptEditorWidget* currentScriptWidget = static_cast(_ScriptEditorWindowUI->tabWidget - ->currentWidget()); - currentScriptWidget->save(); - } -} - -void ScriptEditorWindow::saveScriptAsClicked() { - if (_ScriptEditorWindowUI->tabWidget->currentIndex() != -1) { - ScriptEditorWidget* currentScriptWidget = static_cast(_ScriptEditorWindowUI->tabWidget - ->currentWidget()); - currentScriptWidget->saveAs(); - } -} - -ScriptEditorWidget* ScriptEditorWindow::addScriptEditorWidget(QString title) { - ScriptEditorWidget* newScriptEditorWidget = new ScriptEditorWidget(); - connect(newScriptEditorWidget, &ScriptEditorWidget::scriptnameChanged, this, &ScriptEditorWindow::updateScriptNameOrStatus); - connect(newScriptEditorWidget, &ScriptEditorWidget::scriptModified, this, &ScriptEditorWindow::updateScriptNameOrStatus); - connect(newScriptEditorWidget, &ScriptEditorWidget::runningStateChanged, this, &ScriptEditorWindow::updateButtons); - connect(this, &ScriptEditorWindow::windowActivated, newScriptEditorWidget, &ScriptEditorWidget::onWindowActivated); - _ScriptEditorWindowUI->tabWidget->addTab(newScriptEditorWidget, title); - _ScriptEditorWindowUI->tabWidget->setCurrentWidget(newScriptEditorWidget); - newScriptEditorWidget->setUpdatesEnabled(true); - newScriptEditorWidget->adjustSize(); - return newScriptEditorWidget; -} - -void ScriptEditorWindow::tabSwitched(int tabIndex) { - this->updateButtons(); - if (_ScriptEditorWindowUI->tabWidget->currentIndex() != -1) { - ScriptEditorWidget* currentScriptWidget = static_cast(_ScriptEditorWindowUI->tabWidget - ->currentWidget()); - QString modifiedStar = (currentScriptWidget->isModified() ? "*" : ""); - if (currentScriptWidget->getScriptName().length() > 0) { - this->setWindowTitle("Script Editor [" + currentScriptWidget->getScriptName() + modifiedStar + "]"); - } else { - this->setWindowTitle("Script Editor [New script" + modifiedStar + "]"); - } - } else { - this->setWindowTitle("Script Editor"); - } -} - -void ScriptEditorWindow::tabCloseRequested(int tabIndex) { - if (ignoreCloseForModal(nullptr)) { - return; - } - ScriptEditorWidget* closingScriptWidget = static_cast(_ScriptEditorWindowUI->tabWidget - ->widget(tabIndex)); - if(closingScriptWidget->questionSave()) { - _ScriptEditorWindowUI->tabWidget->removeTab(tabIndex); - } -} - -// If this operating system window causes a qml overlay modal dialog (which might not even be seen by the user), closing this window -// will crash the code that was waiting on the dialog result. So that code whousl set inModalDialog to true while the question is up. -// This code will not be necessary when switch out all operating system windows for qml overlays. -bool ScriptEditorWindow::ignoreCloseForModal(QCloseEvent* event) { - if (!inModalDialog) { - return false; - } - // Deliberately not using OffscreenUi, so that the dialog is seen. - QMessageBox::information(this, tr("Interface"), tr("There is a modal dialog that must be answered before closing."), - QMessageBox::Discard, QMessageBox::Discard); - if (event) { - event->ignore(); // don't close - } - return true; -} - -void ScriptEditorWindow::closeEvent(QCloseEvent *event) { - if (ignoreCloseForModal(event)) { - return; - } - bool unsaved_docs_warning = false; - for (int i = 0; i < _ScriptEditorWindowUI->tabWidget->count(); i++){ - if(static_cast(_ScriptEditorWindowUI->tabWidget->widget(i))->isModified()){ - unsaved_docs_warning = true; - break; - } - } - - if (!unsaved_docs_warning || QMessageBox::warning(this, tr("Interface"), - tr("There are some unsaved scripts, are you sure you want to close the editor? Changes will be lost!"), - QMessageBox::Discard | QMessageBox::Cancel, QMessageBox::Cancel) == QMessageBox::Discard) { - event->accept(); - } else { - event->ignore(); - } -} - -void ScriptEditorWindow::updateScriptNameOrStatus() { - ScriptEditorWidget* source = static_cast(QObject::sender()); - QString modifiedStar = (source->isModified()? "*" : ""); - if (source->getScriptName().length() > 0) { - for (int i = 0; i < _ScriptEditorWindowUI->tabWidget->count(); i++){ - if (_ScriptEditorWindowUI->tabWidget->widget(i) == source) { - _ScriptEditorWindowUI->tabWidget->setTabText(i, modifiedStar + QFileInfo(source->getScriptName()).fileName()); - _ScriptEditorWindowUI->tabWidget->setTabToolTip(i, source->getScriptName()); - } - } - } - - if (_ScriptEditorWindowUI->tabWidget->currentWidget() == source) { - if (source->getScriptName().length() > 0) { - this->setWindowTitle("Script Editor [" + source->getScriptName() + modifiedStar + "]"); - } else { - this->setWindowTitle("Script Editor [New script" + modifiedStar + "]"); - } - } -} - -void ScriptEditorWindow::terminateCurrentTab() { - if (_ScriptEditorWindowUI->tabWidget->currentIndex() != -1) { - _ScriptEditorWindowUI->tabWidget->removeTab(_ScriptEditorWindowUI->tabWidget->currentIndex()); - this->raise(); - } -} - -bool ScriptEditorWindow::autoReloadScripts() { - return _ScriptEditorWindowUI->autoReloadCheckBox->isChecked(); -} - -bool ScriptEditorWindow::event(QEvent* event) { - if (event->type() == QEvent::WindowActivate) { - emit windowActivated(); - } - return QWidget::event(event); -} - diff --git a/interface/src/ui/ScriptEditorWindow.h b/interface/src/ui/ScriptEditorWindow.h deleted file mode 100644 index af9863d136..0000000000 --- a/interface/src/ui/ScriptEditorWindow.h +++ /dev/null @@ -1,64 +0,0 @@ -// -// ScriptEditorWindow.h -// interface/src/ui -// -// Created by Thijs Wenker on 4/14/14. -// Copyright 2014 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#ifndef hifi_ScriptEditorWindow_h -#define hifi_ScriptEditorWindow_h - -#include "ScriptEditorWidget.h" - -namespace Ui { - class ScriptEditorWindow; -} - -class ScriptEditorWindow : public QWidget { - Q_OBJECT - -public: - ScriptEditorWindow(QWidget* parent = nullptr); - ~ScriptEditorWindow(); - - void terminateCurrentTab(); - bool autoReloadScripts(); - - bool inModalDialog { false }; - bool ignoreCloseForModal(QCloseEvent* event); - -signals: - void windowActivated(); - -protected: - void closeEvent(QCloseEvent* event) override; - virtual bool event(QEvent* event) override; - -private: - Ui::ScriptEditorWindow* _ScriptEditorWindowUI; - QMenu* _loadMenu; - QMenu* _saveMenu; - - ScriptEditorWidget* addScriptEditorWidget(QString title); - void setRunningState(bool run); - void setScriptName(const QString& scriptName); - -private slots: - void loadScriptMenu(const QString& scriptName); - void loadScriptClicked(); - void newScriptClicked(); - void toggleRunScriptClicked(); - void saveScriptClicked(); - void saveScriptAsClicked(); - void loadMenuAboutToShow(); - void tabSwitched(int tabIndex); - void tabCloseRequested(int tabIndex); - void updateScriptNameOrStatus(); - void updateButtons(); -}; - -#endif // hifi_ScriptEditorWindow_h diff --git a/interface/src/ui/ScriptLineNumberArea.cpp b/interface/src/ui/ScriptLineNumberArea.cpp deleted file mode 100644 index 6d7e9185ea..0000000000 --- a/interface/src/ui/ScriptLineNumberArea.cpp +++ /dev/null @@ -1,28 +0,0 @@ -// -// ScriptLineNumberArea.cpp -// interface/src/ui -// -// Created by Thijs Wenker on 4/30/14. -// Copyright 2014 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include "ScriptLineNumberArea.h" - -#include "ScriptEditBox.h" - -ScriptLineNumberArea::ScriptLineNumberArea(ScriptEditBox* scriptEditBox) : - QWidget(scriptEditBox) -{ - _scriptEditBox = scriptEditBox; -} - -QSize ScriptLineNumberArea::sizeHint() const { - return QSize(_scriptEditBox->lineNumberAreaWidth(), 0); -} - -void ScriptLineNumberArea::paintEvent(QPaintEvent* event) { - _scriptEditBox->lineNumberAreaPaintEvent(event); -} diff --git a/interface/src/ui/ScriptLineNumberArea.h b/interface/src/ui/ScriptLineNumberArea.h deleted file mode 100644 index 77de8244ce..0000000000 --- a/interface/src/ui/ScriptLineNumberArea.h +++ /dev/null @@ -1,32 +0,0 @@ -// -// ScriptLineNumberArea.h -// interface/src/ui -// -// Created by Thijs Wenker on 4/30/14. -// Copyright 2014 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#ifndef hifi_ScriptLineNumberArea_h -#define hifi_ScriptLineNumberArea_h - -#include - -class ScriptEditBox; - -class ScriptLineNumberArea : public QWidget { - -public: - ScriptLineNumberArea(ScriptEditBox* scriptEditBox); - QSize sizeHint() const override; - -protected: - void paintEvent(QPaintEvent* event) override; - -private: - ScriptEditBox* _scriptEditBox; -}; - -#endif // hifi_ScriptLineNumberArea_h diff --git a/interface/src/ui/ScriptsTableWidget.cpp b/interface/src/ui/ScriptsTableWidget.cpp deleted file mode 100644 index 7b4f9e6b1f..0000000000 --- a/interface/src/ui/ScriptsTableWidget.cpp +++ /dev/null @@ -1,49 +0,0 @@ -// -// ScriptsTableWidget.cpp -// interface -// -// Created by Mohammed Nafees on 04/03/2014. -// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include -#include -#include -#include - -#include "ScriptsTableWidget.h" - -ScriptsTableWidget::ScriptsTableWidget(QWidget* parent) : - QTableWidget(parent) { - verticalHeader()->setVisible(false); - horizontalHeader()->setVisible(false); - setShowGrid(false); - setSelectionMode(QAbstractItemView::NoSelection); - setEditTriggers(QAbstractItemView::NoEditTriggers); - setStyleSheet("QTableWidget { border: none; background: transparent; color: #333333; } QToolTip { color: #000000; background: #f9f6e4; padding: 2px; }"); - setToolTipDuration(200); - setWordWrap(true); - setGeometry(0, 0, parent->width(), parent->height()); -} - -void ScriptsTableWidget::paintEvent(QPaintEvent* event) { - QPainter painter(viewport()); - painter.setPen(QColor::fromRgb(225, 225, 225)); // #e1e1e1 - - int y = 0; - for (int i = 0; i < rowCount(); i++) { - painter.drawLine(5, rowHeight(i) + y, width(), rowHeight(i) + y); - y += rowHeight(i); - } - painter.end(); - - QTableWidget::paintEvent(event); -} - -void ScriptsTableWidget::keyPressEvent(QKeyEvent* event) { - // Ignore keys so they will propagate correctly - event->ignore(); -} diff --git a/interface/src/ui/ScriptsTableWidget.h b/interface/src/ui/ScriptsTableWidget.h deleted file mode 100644 index f5e3407e97..0000000000 --- a/interface/src/ui/ScriptsTableWidget.h +++ /dev/null @@ -1,28 +0,0 @@ -// -// ScriptsTableWidget.h -// interface -// -// Created by Mohammed Nafees on 04/03/2014. -// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#ifndef hifi__ScriptsTableWidget_h -#define hifi__ScriptsTableWidget_h - -#include -#include - -class ScriptsTableWidget : public QTableWidget { - Q_OBJECT -public: - explicit ScriptsTableWidget(QWidget* parent); - -protected: - virtual void paintEvent(QPaintEvent* event) override; - virtual void keyPressEvent(QKeyEvent* event) override; -}; - -#endif // hifi__ScriptsTableWidget_h diff --git a/interface/src/ui/Stats.cpp b/interface/src/ui/Stats.cpp index 923d9f642d..cedcb923d9 100644 --- a/interface/src/ui/Stats.cpp +++ b/interface/src/ui/Stats.cpp @@ -38,6 +38,8 @@ using namespace std; static Stats* INSTANCE{ nullptr }; +QString getTextureMemoryPressureModeString(); + Stats* Stats::getInstance() { if (!INSTANCE) { Stats::registerType(); @@ -220,10 +222,10 @@ void Stats::updateStats(bool force) { STAT_UPDATE(audioMixerInPps, roundf(bandwidthRecorder->getAverageInputPacketsPerSecond(NodeType::AudioMixer))); STAT_UPDATE(audioMixerOutKbps, roundf(bandwidthRecorder->getAverageOutputKilobitsPerSecond(NodeType::AudioMixer))); STAT_UPDATE(audioMixerOutPps, roundf(bandwidthRecorder->getAverageOutputPacketsPerSecond(NodeType::AudioMixer))); - STAT_UPDATE(audioMicOutboundPPS, audioClient->getMicAudioOutboundPPS()); - STAT_UPDATE(audioSilentOutboundPPS, audioClient->getSilentOutboundPPS()); STAT_UPDATE(audioAudioInboundPPS, audioClient->getAudioInboundPPS()); STAT_UPDATE(audioSilentInboundPPS, audioClient->getSilentInboundPPS()); + STAT_UPDATE(audioOutboundPPS, audioClient->getAudioOutboundPPS()); + STAT_UPDATE(audioSilentOutboundPPS, audioClient->getSilentOutboundPPS()); } else { STAT_UPDATE(audioMixerKbps, -1); STAT_UPDATE(audioMixerPps, -1); @@ -231,7 +233,7 @@ void Stats::updateStats(bool force) { STAT_UPDATE(audioMixerInPps, -1); STAT_UPDATE(audioMixerOutKbps, -1); STAT_UPDATE(audioMixerOutPps, -1); - STAT_UPDATE(audioMicOutboundPPS, -1); + STAT_UPDATE(audioOutboundPPS, -1); STAT_UPDATE(audioSilentOutboundPPS, -1); STAT_UPDATE(audioAudioInboundPPS, -1); STAT_UPDATE(audioSilentInboundPPS, -1); @@ -340,10 +342,12 @@ void Stats::updateStats(bool force) { STAT_UPDATE(glContextSwapchainMemory, (int)BYTES_TO_MB(gl::Context::getSwapchainMemoryUsage())); STAT_UPDATE(qmlTextureMemory, (int)BYTES_TO_MB(OffscreenQmlSurface::getUsedTextureMemory())); + STAT_UPDATE(texturePendingTransfers, (int)BYTES_TO_MB(gpu::Texture::getTextureTransferPendingSize())); STAT_UPDATE(gpuTextureMemory, (int)BYTES_TO_MB(gpu::Texture::getTextureGPUMemoryUsage())); STAT_UPDATE(gpuTextureVirtualMemory, (int)BYTES_TO_MB(gpu::Texture::getTextureGPUVirtualMemoryUsage())); STAT_UPDATE(gpuTextureFramebufferMemory, (int)BYTES_TO_MB(gpu::Texture::getTextureGPUFramebufferMemoryUsage())); STAT_UPDATE(gpuTextureSparseMemory, (int)BYTES_TO_MB(gpu::Texture::getTextureGPUSparseMemoryUsage())); + STAT_UPDATE(gpuTextureMemoryPressureState, getTextureMemoryPressureModeString()); STAT_UPDATE(gpuSparseTextureEnabled, gpuContext->getBackend()->isTextureManagementSparseEnabled() ? 1 : 0); STAT_UPDATE(gpuFreeMemory, (int)BYTES_TO_MB(gpu::Context::getFreeGPUMemory())); STAT_UPDATE(rectifiedTextureCount, (int)RECTIFIED_TEXTURE_COUNT.load()); diff --git a/interface/src/ui/Stats.h b/interface/src/ui/Stats.h index 0ce113e0a0..a93a255a06 100644 --- a/interface/src/ui/Stats.h +++ b/interface/src/ui/Stats.h @@ -77,7 +77,7 @@ class Stats : public QQuickItem { STATS_PROPERTY(int, audioMixerOutPps, 0) STATS_PROPERTY(int, audioMixerKbps, 0) STATS_PROPERTY(int, audioMixerPps, 0) - STATS_PROPERTY(int, audioMicOutboundPPS, 0) + STATS_PROPERTY(int, audioOutboundPPS, 0) STATS_PROPERTY(int, audioSilentOutboundPPS, 0) STATS_PROPERTY(int, audioAudioInboundPPS, 0) STATS_PROPERTY(int, audioSilentInboundPPS, 0) @@ -117,11 +117,13 @@ class Stats : public QQuickItem { STATS_PROPERTY(int, gpuTexturesSparse, 0) STATS_PROPERTY(int, glContextSwapchainMemory, 0) STATS_PROPERTY(int, qmlTextureMemory, 0) + STATS_PROPERTY(int, texturePendingTransfers, 0) STATS_PROPERTY(int, gpuTextureMemory, 0) STATS_PROPERTY(int, gpuTextureVirtualMemory, 0) STATS_PROPERTY(int, gpuTextureFramebufferMemory, 0) STATS_PROPERTY(int, gpuTextureSparseMemory, 0) STATS_PROPERTY(int, gpuSparseTextureEnabled, 0) + STATS_PROPERTY(QString, gpuTextureMemoryPressureState, QString()) STATS_PROPERTY(int, gpuFreeMemory, 0) STATS_PROPERTY(float, gpuFrameTime, 0) STATS_PROPERTY(float, batchFrameTime, 0) @@ -198,7 +200,7 @@ signals: void audioMixerOutPpsChanged(); void audioMixerKbpsChanged(); void audioMixerPpsChanged(); - void audioMicOutboundPPSChanged(); + void audioOutboundPPSChanged(); void audioSilentOutboundPPSChanged(); void audioAudioInboundPPSChanged(); void audioSilentInboundPPSChanged(); @@ -232,6 +234,7 @@ signals: void timingStatsChanged(); void glContextSwapchainMemoryChanged(); void qmlTextureMemoryChanged(); + void texturePendingTransfersChanged(); void gpuBuffersChanged(); void gpuBufferMemoryChanged(); void gpuTexturesChanged(); @@ -240,6 +243,7 @@ signals: void gpuTextureVirtualMemoryChanged(); void gpuTextureFramebufferMemoryChanged(); void gpuTextureSparseMemoryChanged(); + void gpuTextureMemoryPressureStateChanged(); void gpuSparseTextureEnabledChanged(); void gpuFreeMemoryChanged(); void gpuFrameTimeChanged(); diff --git a/interface/src/ui/overlays/Overlays.cpp b/interface/src/ui/overlays/Overlays.cpp index f40dd522c4..6514052d26 100644 --- a/interface/src/ui/overlays/Overlays.cpp +++ b/interface/src/ui/overlays/Overlays.cpp @@ -431,7 +431,9 @@ RayToOverlayIntersectionResult Overlays::findRayIntersectionInternal(const PickR if (thisOverlay->findRayIntersectionExtraInfo(ray.origin, ray.direction, thisDistance, thisFace, thisSurfaceNormal, thisExtraInfo)) { bool isDrawInFront = thisOverlay->getDrawInFront(); - if (thisDistance < bestDistance && (!bestIsFront || isDrawInFront)) { + if ((bestIsFront && isDrawInFront && thisDistance < bestDistance) + || (!bestIsFront && (isDrawInFront || thisDistance < bestDistance))) { + bestIsFront = isDrawInFront; bestDistance = thisDistance; result.intersects = true; diff --git a/interface/src/ui/overlays/Web3DOverlay.cpp b/interface/src/ui/overlays/Web3DOverlay.cpp index ba864d2c5c..97e5344062 100644 --- a/interface/src/ui/overlays/Web3DOverlay.cpp +++ b/interface/src/ui/overlays/Web3DOverlay.cpp @@ -270,7 +270,7 @@ void Web3DOverlay::render(RenderArgs* args) { if (!_texture) { auto webSurface = _webSurface; - _texture = gpu::TexturePointer(gpu::Texture::createExternal2D(OffscreenQmlSurface::getDiscardLambda())); + _texture = gpu::TexturePointer(gpu::Texture::createExternal(OffscreenQmlSurface::getDiscardLambda())); _texture->setSource(__FUNCTION__); } OffscreenQmlSurface::TextureAndFence newTextureAndFence; diff --git a/interface/ui/scriptEditorWidget.ui b/interface/ui/scriptEditorWidget.ui deleted file mode 100644 index e2e538a595..0000000000 --- a/interface/ui/scriptEditorWidget.ui +++ /dev/null @@ -1,142 +0,0 @@ - - - ScriptEditorWidget - - - - 0 - 0 - 691 - 549 - - - - - 0 - 0 - - - - - 690 - 328 - - - - font-family: Helvetica, Arial, sans-serif; - - - QDockWidget::DockWidgetFloatable|QDockWidget::DockWidgetMovable - - - Qt::NoDockWidgetArea - - - Edit Script - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - Courier - -1 - 50 - false - false - - - - font: 16px "Courier"; - - - - - - - 0 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - font: 13px "Helvetica","Arial","sans-serif"; - - - Debug Log: - - - - - - - - Helvetica,Arial,sans-serif - -1 - 50 - false - false - - - - font: 13px "Helvetica","Arial","sans-serif"; - - - Run on the fly (Careful: Any valid change made to the code will run immediately) - - - - - - - Clear - - - - 16 - 16 - - - - - - - - - - - - ScriptEditBox - QTextEdit -
ui/ScriptEditBox.h
-
-
- -
diff --git a/interface/ui/scriptEditorWindow.ui b/interface/ui/scriptEditorWindow.ui deleted file mode 100644 index 1e50aaef0b..0000000000 --- a/interface/ui/scriptEditorWindow.ui +++ /dev/null @@ -1,324 +0,0 @@ - - - ScriptEditorWindow - - - Qt::NonModal - - - - 0 - 0 - 780 - 717 - - - - - 400 - 250 - - - - Script Editor - - - font-family: Helvetica, Arial, sans-serif; - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - 3 - - - QLayout::SetNoConstraint - - - 0 - - - 0 - - - - - New Script (Ctrl+N) - - - New - - - - 32 - 32 - - - - - - - - - 30 - 0 - - - - - 25 - 0 - - - - Load Script (Ctrl+O) - - - Load - - - - 32 - 32 - - - - false - - - QToolButton::MenuButtonPopup - - - Qt::ToolButtonIconOnly - - - - - - - - 30 - 0 - - - - - 32 - 0 - - - - Qt::NoFocus - - - Qt::NoContextMenu - - - Save Script (Ctrl+S) - - - Save - - - - 32 - 32 - - - - 316 - - - QToolButton::MenuButtonPopup - - - - - - - Toggle Run Script (F5) - - - Run/Stop - - - - 32 - 32 - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - font: 13px "Helvetica","Arial","sans-serif"; - - - Automatically reload externally changed files - - - - - - - - - true - - - - 250 - 80 - - - - QTabWidget::West - - - QTabWidget::Triangular - - - -1 - - - Qt::ElideNone - - - true - - - true - - - - - - - - - saveButton - clicked() - ScriptEditorWindow - saveScriptClicked() - - - 236 - 10 - - - 199 - 264 - - - - - toggleRunButton - clicked() - ScriptEditorWindow - toggleRunScriptClicked() - - - 330 - 10 - - - 199 - 264 - - - - - newButton - clicked() - ScriptEditorWindow - newScriptClicked() - - - 58 - 10 - - - 199 - 264 - - - - - loadButton - clicked() - ScriptEditorWindow - loadScriptClicked() - - - 85 - 10 - - - 199 - 264 - - - - - tabWidget - currentChanged(int) - ScriptEditorWindow - tabSwitched(int) - - - 352 - 360 - - - 352 - 340 - - - - - tabWidget - tabCloseRequested(int) - ScriptEditorWindow - tabCloseRequested(int) - - - 352 - 360 - - - 352 - 340 - - - - - diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index c32b5600d9..4a2de0a64b 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -160,7 +160,7 @@ AudioClient::AudioClient() : _loopbackAudioOutput(NULL), _loopbackOutputDevice(NULL), _inputRingBuffer(0), - _localInjectorsStream(0), + _localInjectorsStream(0, 1), _receivedAudioStream(RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES), _isStereoInput(false), _outputStarveDetectionStartTimeMsec(0), @@ -184,7 +184,6 @@ AudioClient::AudioClient() : _outgoingAvatarAudioSequenceNumber(0), _audioOutputIODevice(_localInjectorsStream, _receivedAudioStream, this), _stats(&_receivedAudioStream), - _inputGate(), _positionGetter(DEFAULT_POSITION_GETTER), _orientationGetter(DEFAULT_ORIENTATION_GETTER) { // avoid putting a lock in the device callback @@ -971,14 +970,87 @@ void AudioClient::handleLocalEchoAndReverb(QByteArray& inputByteArray) { } } -void AudioClient::handleAudioInput() { +void AudioClient::handleAudioInput(QByteArray& audioBuffer) { + if (_muted) { + _lastInputLoudness = 0.0f; + _timeSinceLastClip = 0.0f; + } else { + int16_t* samples = reinterpret_cast(audioBuffer.data()); + int numSamples = audioBuffer.size() / sizeof(AudioConstants::SAMPLE_SIZE); + bool didClip = false; + + bool shouldRemoveDCOffset = !_isPlayingBackRecording && !_isStereoInput; + if (shouldRemoveDCOffset) { + _noiseGate.removeDCOffset(samples, numSamples); + } + + bool shouldNoiseGate = (_isPlayingBackRecording || !_isStereoInput) && _isNoiseGateEnabled; + if (shouldNoiseGate) { + _noiseGate.gateSamples(samples, numSamples); + _lastInputLoudness = _noiseGate.getLastLoudness(); + didClip = _noiseGate.clippedInLastBlock(); + } else { + float loudness = 0.0f; + for (int i = 0; i < numSamples; ++i) { + int16_t sample = std::abs(samples[i]); + loudness += (float)sample; + didClip = didClip || + (sample > (AudioConstants::MAX_SAMPLE_VALUE * AudioNoiseGate::CLIPPING_THRESHOLD)); + } + _lastInputLoudness = fabs(loudness / numSamples); + } + + if (didClip) { + _timeSinceLastClip = 0.0f; + } else if (_timeSinceLastClip >= 0.0f) { + _timeSinceLastClip += (float)numSamples / (float)AudioConstants::SAMPLE_RATE; + } + + emit inputReceived({ audioBuffer.data(), numSamples }); + + if (_noiseGate.openedInLastBlock()) { + emit noiseGateOpened(); + } else if (_noiseGate.closedInLastBlock()) { + emit noiseGateClosed(); + } + } + + // the codec needs a flush frame before sending silent packets, so + // do not send one if the gate closed in this block (eventually this can be crossfaded). + auto packetType = _shouldEchoToServer ? + PacketType::MicrophoneAudioWithEcho : PacketType::MicrophoneAudioNoEcho; + if (_lastInputLoudness == 0.0f && !_noiseGate.closedInLastBlock()) { + packetType = PacketType::SilentAudioFrame; + _silentOutbound.increment(); + } else { + _audioOutbound.increment(); + } + + Transform audioTransform; + audioTransform.setTranslation(_positionGetter()); + audioTransform.setRotation(_orientationGetter()); + + QByteArray encodedBuffer; + if (_encoder) { + _encoder->encode(audioBuffer, encodedBuffer); + } else { + encodedBuffer = audioBuffer; + } + + emitAudioPacket(encodedBuffer.data(), encodedBuffer.size(), _outgoingAvatarAudioSequenceNumber, + audioTransform, avatarBoundingBoxCorner, avatarBoundingBoxScale, + packetType, _selectedCodecName); + _stats.sentPacket(); +} + +void AudioClient::handleMicAudioInput() { if (!_inputDevice || _isPlayingBackRecording) { return; } // input samples required to produce exactly NETWORK_FRAME_SAMPLES of output - const int inputSamplesRequired = (_inputToNetworkResampler ? - _inputToNetworkResampler->getMinInput(AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL) : + const int inputSamplesRequired = (_inputToNetworkResampler ? + _inputToNetworkResampler->getMinInput(AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL) : AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL) * _inputFormat.channelCount(); const auto inputAudioSamples = std::unique_ptr(new int16_t[inputSamplesRequired]); @@ -1001,126 +1073,27 @@ void AudioClient::handleAudioInput() { static int16_t networkAudioSamples[AudioConstants::NETWORK_FRAME_SAMPLES_STEREO]; while (_inputRingBuffer.samplesAvailable() >= inputSamplesRequired) { - - if (!_muted) { - - - // Increment the time since the last clip - if (_timeSinceLastClip >= 0.0f) { - _timeSinceLastClip += (float)numNetworkSamples / (float)AudioConstants::SAMPLE_RATE; - } - + if (_muted) { + _inputRingBuffer.shiftReadPosition(inputSamplesRequired); + } else { _inputRingBuffer.readSamples(inputAudioSamples.get(), inputSamplesRequired); possibleResampling(_inputToNetworkResampler, inputAudioSamples.get(), networkAudioSamples, inputSamplesRequired, numNetworkSamples, _inputFormat.channelCount(), _desiredInputFormat.channelCount()); - - // Remove DC offset - if (!_isStereoInput) { - _inputGate.removeDCOffset(networkAudioSamples, numNetworkSamples); - } - - // only impose the noise gate and perform tone injection if we are sending mono audio - if (!_isStereoInput && _isNoiseGateEnabled) { - _inputGate.gateSamples(networkAudioSamples, numNetworkSamples); - - // if we performed the noise gate we can get values from it instead of enumerating the samples again - _lastInputLoudness = _inputGate.getLastLoudness(); - - if (_inputGate.clippedInLastBlock()) { - _timeSinceLastClip = 0.0f; - } - - } else { - float loudness = 0.0f; - - for (int i = 0; i < numNetworkSamples; i++) { - int thisSample = std::abs(networkAudioSamples[i]); - loudness += (float)thisSample; - - if (thisSample > (AudioConstants::MAX_SAMPLE_VALUE * AudioNoiseGate::CLIPPING_THRESHOLD)) { - _timeSinceLastClip = 0.0f; - } - } - - _lastInputLoudness = fabs(loudness / numNetworkSamples); - } - - emit inputReceived({ reinterpret_cast(networkAudioSamples), numNetworkBytes }); - - if (_inputGate.openedInLastBlock()) { - emit noiseGateOpened(); - } else if (_inputGate.closedInLastBlock()) { - emit noiseGateClosed(); - } - - } else { - // our input loudness is 0, since we're muted - _lastInputLoudness = 0; - _timeSinceLastClip = 0.0f; - - _inputRingBuffer.shiftReadPosition(inputSamplesRequired); } - - auto packetType = _shouldEchoToServer ? - PacketType::MicrophoneAudioWithEcho : PacketType::MicrophoneAudioNoEcho; - - // if the _inputGate closed in this last frame, then we don't actually want - // to send a silent packet, instead, we want to go ahead and encode and send - // the output from the input gate (eventually, this could be crossfaded) - // and allow the codec to properly encode down to silent/zero. If we still - // have _lastInputLoudness of 0 in our NEXT frame, we will send a silent packet - if (_lastInputLoudness == 0 && !_inputGate.closedInLastBlock()) { - packetType = PacketType::SilentAudioFrame; - _silentOutbound.increment(); - } else { - _micAudioOutbound.increment(); - } - - Transform audioTransform; - audioTransform.setTranslation(_positionGetter()); - audioTransform.setRotation(_orientationGetter()); - // FIXME find a way to properly handle both playback audio and user audio concurrently - - QByteArray decodedBuffer(reinterpret_cast(networkAudioSamples), numNetworkBytes); - QByteArray encodedBuffer; - if (_encoder) { - _encoder->encode(decodedBuffer, encodedBuffer); - } else { - encodedBuffer = decodedBuffer; - } - - emitAudioPacket(encodedBuffer.constData(), encodedBuffer.size(), _outgoingAvatarAudioSequenceNumber, - audioTransform, avatarBoundingBoxCorner, avatarBoundingBoxScale, - packetType, _selectedCodecName); - _stats.sentPacket(); - int bytesInInputRingBuffer = _inputRingBuffer.samplesAvailable() * AudioConstants::SAMPLE_SIZE; float msecsInInputRingBuffer = bytesInInputRingBuffer / (float)(_inputFormat.bytesForDuration(USECS_PER_MSEC)); _stats.updateInputMsUnplayed(msecsInInputRingBuffer); + + QByteArray audioBuffer(reinterpret_cast(networkAudioSamples), numNetworkBytes); + handleAudioInput(audioBuffer); } } -// FIXME - should this go through the noise gate and honor mute and echo? void AudioClient::handleRecordedAudioInput(const QByteArray& audio) { - Transform audioTransform; - audioTransform.setTranslation(_positionGetter()); - audioTransform.setRotation(_orientationGetter()); - - QByteArray encodedBuffer; - if (_encoder) { - _encoder->encode(audio, encodedBuffer); - } else { - encodedBuffer = audio; - } - - _micAudioOutbound.increment(); - - // FIXME check a flag to see if we should echo audio? - emitAudioPacket(encodedBuffer.data(), encodedBuffer.size(), _outgoingAvatarAudioSequenceNumber, - audioTransform, avatarBoundingBoxCorner, avatarBoundingBoxScale, - PacketType::MicrophoneAudioWithEcho, _selectedCodecName); + QByteArray audioBuffer(audio); + handleAudioInput(audioBuffer); } void AudioClient::prepareLocalAudioInjectors() { @@ -1434,7 +1407,7 @@ bool AudioClient::switchInputToAudioDevice(const QAudioDeviceInfo& inputDeviceIn lock.unlock(); if (_inputDevice) { - connect(_inputDevice, SIGNAL(readyRead()), this, SLOT(handleAudioInput())); + connect(_inputDevice, SIGNAL(readyRead()), this, SLOT(handleMicAudioInput())); supportedFormat = true; } else { qCDebug(audioclient) << "Error starting audio input -" << _audioInput->error(); @@ -1540,12 +1513,39 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDevice // setup our general output device for audio-mixer audio _audioOutput = new QAudioOutput(outputDeviceInfo, _outputFormat, this); - int osDefaultBufferSize = _audioOutput->bufferSize(); int deviceChannelCount = _outputFormat.channelCount(); - int deviceFrameSize = (AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * deviceChannelCount * _outputFormat.sampleRate()) / _desiredOutputFormat.sampleRate(); - int requestedSize = _sessionOutputBufferSizeFrames * deviceFrameSize * AudioConstants::SAMPLE_SIZE; + int frameSize = (AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * deviceChannelCount * _outputFormat.sampleRate()) / _desiredOutputFormat.sampleRate(); + int requestedSize = _sessionOutputBufferSizeFrames * frameSize * AudioConstants::SAMPLE_SIZE; _audioOutput->setBufferSize(requestedSize); + // initialize mix buffers on the _audioOutput thread to avoid races + connect(_audioOutput, &QAudioOutput::stateChanged, [&, frameSize, requestedSize](QAudio::State state) { + if (state == QAudio::ActiveState) { + // restrict device callback to _outputPeriod samples + _outputPeriod = (_audioOutput->periodSize() / AudioConstants::SAMPLE_SIZE) * 2; + _outputMixBuffer = new float[_outputPeriod]; + _outputScratchBuffer = new int16_t[_outputPeriod]; + + // size local output mix buffer based on resampled network frame size + _networkPeriod = _localToOutputResampler->getMaxOutput(AudioConstants::NETWORK_FRAME_SAMPLES_STEREO); + _localOutputMixBuffer = new float[_networkPeriod]; + int localPeriod = _outputPeriod * 2; + _localInjectorsStream.resizeForFrameSize(localPeriod); + + int bufferSize = _audioOutput->bufferSize(); + int bufferSamples = bufferSize / AudioConstants::SAMPLE_SIZE; + int bufferFrames = bufferSamples / (float)frameSize; + qCDebug(audioclient) << "frame (samples):" << frameSize; + qCDebug(audioclient) << "buffer (frames):" << bufferFrames; + qCDebug(audioclient) << "buffer (samples):" << bufferSamples; + qCDebug(audioclient) << "buffer (bytes):" << bufferSize; + qCDebug(audioclient) << "requested (bytes):" << requestedSize; + qCDebug(audioclient) << "period (samples):" << _outputPeriod; + qCDebug(audioclient) << "local buffer (samples):" << localPeriod; + + disconnect(_audioOutput, &QAudioOutput::stateChanged, 0, 0); + } + }); connect(_audioOutput, &QAudioOutput::notify, this, &AudioClient::outputNotify); _audioOutputIODevice.start(); @@ -1555,18 +1555,6 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDevice _audioOutput->start(&_audioOutputIODevice); lock.unlock(); - int periodSampleSize = _audioOutput->periodSize() / AudioConstants::SAMPLE_SIZE; - // device callback is not restricted to periodSampleSize, so double the mix/scratch buffer sizes - _outputPeriod = periodSampleSize * 2; - _outputMixBuffer = new float[_outputPeriod]; - _outputScratchBuffer = new int16_t[_outputPeriod]; - _localOutputMixBuffer = new float[_outputPeriod]; - _localInjectorsStream.resizeForFrameSize(_outputPeriod * 2); - - qCDebug(audioclient) << "Output Buffer capacity in frames: " << _audioOutput->bufferSize() / AudioConstants::SAMPLE_SIZE / (float)deviceFrameSize << - "requested bytes:" << requestedSize << "actual bytes:" << _audioOutput->bufferSize() << - "os default:" << osDefaultBufferSize << "period size:" << _audioOutput->periodSize(); - // setup a loopback audio output device _loopbackAudioOutput = new QAudioOutput(outputDeviceInfo, _outputFormat, this); diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index 7e9acc0586..139749e8e8 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -124,16 +124,16 @@ public: void selectAudioFormat(const QString& selectedCodecName); Q_INVOKABLE QString getSelectedAudioFormat() const { return _selectedCodecName; } - Q_INVOKABLE bool getNoiseGateOpen() const { return _inputGate.isOpen(); } - Q_INVOKABLE float getSilentOutboundPPS() const { return _silentOutbound.rate(); } - Q_INVOKABLE float getMicAudioOutboundPPS() const { return _micAudioOutbound.rate(); } + Q_INVOKABLE bool getNoiseGateOpen() const { return _noiseGate.isOpen(); } Q_INVOKABLE float getSilentInboundPPS() const { return _silentInbound.rate(); } Q_INVOKABLE float getAudioInboundPPS() const { return _audioInbound.rate(); } + Q_INVOKABLE float getSilentOutboundPPS() const { return _silentOutbound.rate(); } + Q_INVOKABLE float getAudioOutboundPPS() const { return _audioOutbound.rate(); } const MixedProcessedAudioStream& getReceivedAudioStream() const { return _receivedAudioStream; } MixedProcessedAudioStream& getReceivedAudioStream() { return _receivedAudioStream; } - float getLastInputLoudness() const { return glm::max(_lastInputLoudness - _inputGate.getMeasuredFloor(), 0.0f); } + float getLastInputLoudness() const { return glm::max(_lastInputLoudness - _noiseGate.getMeasuredFloor(), 0.0f); } float getTimeSinceLastClip() const { return _timeSinceLastClip; } float getAudioAverageInputLoudness() const { return _lastInputLoudness; } @@ -180,7 +180,7 @@ public slots: void handleMismatchAudioFormat(SharedNodePointer node, const QString& currentCodec, const QString& recievedCodec); void sendDownstreamAudioStatsPacket() { _stats.publish(); } - void handleAudioInput(); + void handleMicAudioInput(); void handleRecordedAudioInput(const QByteArray& audio); void reset(); void audioMixerKilled(); @@ -250,6 +250,7 @@ protected: private: void outputFormatChanged(); + void handleAudioInput(QByteArray& audioBuffer); bool mixLocalAudioInjectors(float* mixBuffer); float azimuthForSource(const glm::vec3& relativePosition); float gainForSource(float distance, float volume); @@ -339,6 +340,7 @@ private: int16_t _networkScratchBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_AMBISONIC]; // for local audio (used by audio injectors thread) + int _networkPeriod { 0 }; float _localMixBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_STEREO]; int16_t _localScratchBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_AMBISONIC]; float* _localOutputMixBuffer { NULL }; @@ -371,7 +373,7 @@ private: AudioIOStats _stats; - AudioNoiseGate _inputGate; + AudioNoiseGate _noiseGate; AudioPositionGetter _positionGetter; AudioOrientationGetter _orientationGetter; @@ -395,7 +397,7 @@ private: QThread* _checkDevicesThread { nullptr }; RateCounter<> _silentOutbound; - RateCounter<> _micAudioOutbound; + RateCounter<> _audioOutbound; RateCounter<> _silentInbound; RateCounter<> _audioInbound; }; diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index b23b59d3f0..5a317f64bc 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -355,14 +355,16 @@ void OpenGLDisplayPlugin::customizeContext() { if ((image.width() > 0) && (image.height() > 0)) { cursorData.texture.reset( - gpu::Texture::create2D( + gpu::Texture::createStrict( gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA), image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); cursorData.texture->setSource("cursor texture"); auto usage = gpu::Texture::Usage::Builder().withColor().withAlpha(); cursorData.texture->setUsage(usage.build()); - cursorData.texture->assignStoredMip(0, gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA), image.byteCount(), image.constBits()); + cursorData.texture->setStoredMipFormat(gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA)); + cursorData.texture->assignStoredMip(0, image.byteCount(), image.constBits()); + cursorData.texture->autoGenerateMips(-1); } } } diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index a8b8ba3618..c55d985a62 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -296,33 +296,32 @@ void HmdDisplayPlugin::internalPresent() { image = image.convertToFormat(QImage::Format_RGBA8888); if (!_previewTexture) { _previewTexture.reset( - gpu::Texture::create2D( + gpu::Texture::createStrict( gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA), image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); _previewTexture->setSource("HMD Preview Texture"); _previewTexture->setUsage(gpu::Texture::Usage::Builder().withColor().build()); - _previewTexture->assignStoredMip(0, gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA), image.byteCount(), image.constBits()); + _previewTexture->setStoredMipFormat(gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA)); + _previewTexture->assignStoredMip(0, image.byteCount(), image.constBits()); _previewTexture->autoGenerateMips(-1); } - if (getGLBackend()->isTextureReady(_previewTexture)) { - auto viewport = getViewportForSourceSize(uvec2(_previewTexture->getDimensions())); + auto viewport = getViewportForSourceSize(uvec2(_previewTexture->getDimensions())); - render([&](gpu::Batch& batch) { - batch.enableStereo(false); - batch.resetViewTransform(); - batch.setFramebuffer(gpu::FramebufferPointer()); - batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, vec4(0)); - batch.setStateScissorRect(viewport); - batch.setViewportTransform(viewport); - batch.setResourceTexture(0, _previewTexture); - batch.setPipeline(_presentPipeline); - batch.draw(gpu::TRIANGLE_STRIP, 4); - }); - _clearPreviewFlag = false; - swapBuffers(); - } + render([&](gpu::Batch& batch) { + batch.enableStereo(false); + batch.resetViewTransform(); + batch.setFramebuffer(gpu::FramebufferPointer()); + batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, vec4(0)); + batch.setStateScissorRect(viewport); + batch.setViewportTransform(viewport); + batch.setResourceTexture(0, _previewTexture); + batch.setPipeline(_presentPipeline); + batch.draw(gpu::TRIANGLE_STRIP, 4); + }); + _clearPreviewFlag = false; + swapBuffers(); } postPreview(); diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index 27e00b47c6..8c4498edc6 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -146,6 +146,7 @@ void EntityTreeRenderer::clear() { void EntityTreeRenderer::reloadEntityScripts() { _entitiesScriptEngine->unloadAllEntityScripts(); + _entitiesScriptEngine->resetModuleCache(); foreach(auto entity, _entitiesInScene) { if (!entity->getScript().isEmpty()) { _entitiesScriptEngine->loadEntityScript(entity->getEntityItemID(), entity->getScript(), true); @@ -940,7 +941,7 @@ void EntityTreeRenderer::mouseMoveEvent(QMouseEvent* event) { void EntityTreeRenderer::deletingEntity(const EntityItemID& entityID) { if (_tree && !_shuttingDown && _entitiesScriptEngine) { - _entitiesScriptEngine->unloadEntityScript(entityID); + _entitiesScriptEngine->unloadEntityScript(entityID, true); } forceRecheckEntities(); // reset our state to force checking our inside/outsideness of entities diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp index 7359a548fc..1d58527427 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp @@ -14,6 +14,7 @@ #include #include #include +#include "ModelScriptingInterface.h" #if defined(__GNUC__) && !defined(__clang__) #pragma GCC diagnostic push @@ -53,6 +54,8 @@ #include "PhysicalEntitySimulation.h" gpu::PipelinePointer RenderablePolyVoxEntityItem::_pipeline = nullptr; +gpu::PipelinePointer RenderablePolyVoxEntityItem::_wireframePipeline = nullptr; + const float MARCHING_CUBE_COLLISION_HULL_OFFSET = 0.5; @@ -73,7 +76,7 @@ const float MARCHING_CUBE_COLLISION_HULL_OFFSET = 0.5; _meshDirty In RenderablePolyVoxEntityItem::render, these flags are checked and changes are propagated along the chain. - decompressVolumeData() is called to decompress _voxelData into _volData. getMesh() is called to invoke the + decompressVolumeData() is called to decompress _voxelData into _volData. recomputeMesh() is called to invoke the polyVox surface extractor to create _mesh (as well as set Simulation _dirtyFlags). Because Simulation::DIRTY_SHAPE is set, isReadyToComputeShape() gets called and _shape is created either from _volData or _shape, depending on the surface style. @@ -81,7 +84,7 @@ const float MARCHING_CUBE_COLLISION_HULL_OFFSET = 0.5; When a script changes _volData, compressVolumeDataAndSendEditPacket is called to update _voxelData and to send a packet to the entity-server. - decompressVolumeData, getMesh, computeShapeInfoWorker, and compressVolumeDataAndSendEditPacket are too expensive + decompressVolumeData, recomputeMesh, computeShapeInfoWorker, and compressVolumeDataAndSendEditPacket are too expensive to run on a thread that has other things to do. These use QtConcurrent::run to spawn a thread. As each thread finishes, it adjusts the dirty flags so that the next call to render() will kick off the next step. @@ -663,11 +666,8 @@ void RenderablePolyVoxEntityItem::setZTextureURL(QString zTextureURL) { } } -void RenderablePolyVoxEntityItem::render(RenderArgs* args) { - PerformanceTimer perfTimer("RenderablePolyVoxEntityItem::render"); - assert(getType() == EntityTypes::PolyVox); - Q_ASSERT(args->_batch); +bool RenderablePolyVoxEntityItem::updateDependents() { bool voxelDataDirty; bool volDataDirty; withWriteLock([&] { @@ -682,9 +682,20 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) { if (voxelDataDirty) { decompressVolumeData(); } else if (volDataDirty) { - getMesh(); + recomputeMesh(); } + return !volDataDirty; +} + + +void RenderablePolyVoxEntityItem::render(RenderArgs* args) { + PerformanceTimer perfTimer("RenderablePolyVoxEntityItem::render"); + assert(getType() == EntityTypes::PolyVox); + Q_ASSERT(args->_batch); + + updateDependents(); + model::MeshPointer mesh; glm::vec3 voxelVolumeSize; withReadLock([&] { @@ -696,7 +707,7 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) { !mesh->getIndexBuffer()._buffer) { return; } - + if (!_pipeline) { gpu::ShaderPointer vertexShader = gpu::Shader::createVertex(std::string(polyvox_vert)); gpu::ShaderPointer pixelShader = gpu::Shader::createPixel(std::string(polyvox_frag)); @@ -715,6 +726,13 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) { state->setDepthTest(true, true, gpu::LESS_EQUAL); _pipeline = gpu::Pipeline::create(program, state); + + auto wireframeState = std::make_shared(); + wireframeState->setCullMode(gpu::State::CULL_BACK); + wireframeState->setDepthTest(true, true, gpu::LESS_EQUAL); + wireframeState->setFillMode(gpu::State::FILL_LINE); + + _wireframePipeline = gpu::Pipeline::create(program, wireframeState); } if (!_vertexFormat) { @@ -725,7 +743,11 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) { } gpu::Batch& batch = *args->_batch; - batch.setPipeline(_pipeline); + + // Pick correct Pipeline + bool wireframe = (render::ShapeKey(args->_globalShapeKey).isWireframe()); + auto pipeline = (wireframe ? _wireframePipeline : _pipeline); + batch.setPipeline(pipeline); Transform transform(voxelToWorldMatrix()); batch.setModelTransform(transform); @@ -762,7 +784,7 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) { batch.setResourceTexture(2, DependencyManager::get()->getWhiteTexture()); } - int voxelVolumeSizeLocation = _pipeline->getProgram()->getUniforms().findLocation("voxelVolumeSize"); + int voxelVolumeSizeLocation = pipeline->getProgram()->getUniforms().findLocation("voxelVolumeSize"); batch._glUniform3f(voxelVolumeSizeLocation, voxelVolumeSize.x, voxelVolumeSize.y, voxelVolumeSize.z); batch.drawIndexed(gpu::TRIANGLES, (gpu::uint32)mesh->getNumIndices(), 0); @@ -1199,7 +1221,7 @@ void RenderablePolyVoxEntityItem::copyUpperEdgesFromNeighbors() { } } -void RenderablePolyVoxEntityItem::getMesh() { +void RenderablePolyVoxEntityItem::recomputeMesh() { // use _volData to make a renderable mesh PolyVoxSurfaceStyle voxelSurfaceStyle; withReadLock([&] { @@ -1269,12 +1291,20 @@ void RenderablePolyVoxEntityItem::getMesh() { vertexBufferPtr->getSize() , sizeof(PolyVox::PositionMaterialNormal), gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::RAW))); + + std::vector parts; + parts.emplace_back(model::Mesh::Part((model::Index)0, // startIndex + (model::Index)vecIndices.size(), // numIndices + (model::Index)0, // baseVertex + model::Mesh::TRIANGLES)); // topology + mesh->setPartBuffer(gpu::BufferView(new gpu::Buffer(parts.size() * sizeof(model::Mesh::Part), + (gpu::Byte*) parts.data()), gpu::Element::PART_DRAWCALL)); entity->setMesh(mesh); }); } void RenderablePolyVoxEntityItem::setMesh(model::MeshPointer mesh) { - // this catches the payload from getMesh + // this catches the payload from recomputeMesh bool neighborsNeedUpdate; withWriteLock([&] { if (!_collisionless) { @@ -1531,7 +1561,6 @@ std::shared_ptr RenderablePolyVoxEntityItem::getZPN return std::dynamic_pointer_cast(_zPNeighbor.lock()); } - void RenderablePolyVoxEntityItem::bonkNeighbors() { // flag neighbors to the negative of this entity as needing to rebake their meshes. cacheNeighbors(); @@ -1551,7 +1580,6 @@ void RenderablePolyVoxEntityItem::bonkNeighbors() { } } - void RenderablePolyVoxEntityItem::locationChanged(bool tellPhysics) { EntityItem::locationChanged(tellPhysics); if (!_pipeline || !render::Item::isValidID(_myItem)) { @@ -1563,3 +1591,25 @@ void RenderablePolyVoxEntityItem::locationChanged(bool tellPhysics) { scene->enqueuePendingChanges(pendingChanges); } + +bool RenderablePolyVoxEntityItem::getMeshAsScriptValue(QScriptEngine *engine, QScriptValue& result) { + if (!updateDependents()) { + return false; + } + + bool success = false; + MeshProxy* meshProxy = nullptr; + glm::mat4 transform = voxelToLocalMatrix(); + withReadLock([&] { + if (_meshInitialized) { + success = true; + // the mesh will be in voxel-space. transform it into object-space + meshProxy = new MeshProxy( + _mesh->map([=](glm::vec3 position){ return glm::vec3(transform * glm::vec4(position, 1.0f)); }, + [=](glm::vec3 normal){ return glm::vec3(transform * glm::vec4(normal, 0.0f)); }, + [](uint32_t index){ return index; })); + } + }); + result = meshToScriptValue(engine, meshProxy); + return success; +} diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h index 45842c2fb9..cf4672f068 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h @@ -61,6 +61,8 @@ public: virtual uint8_t getVoxel(int x, int y, int z) override; virtual bool setVoxel(int x, int y, int z, uint8_t toValue) override; + int getOnCount() const override { return _onCount; } + void render(RenderArgs* args) override; virtual bool supportsDetailedRayIntersection() const override { return true; } virtual bool findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, @@ -133,6 +135,7 @@ public: QByteArray volDataToArray(quint16 voxelXSize, quint16 voxelYSize, quint16 voxelZSize) const; void setMesh(model::MeshPointer mesh); + bool getMeshAsScriptValue(QScriptEngine *engine, QScriptValue& result) override; void setCollisionPoints(ShapeInfo::PointCollection points, AABox box); PolyVox::SimpleVolume* getVolData() { return _volData; } @@ -163,11 +166,12 @@ private: const int MATERIAL_GPU_SLOT = 3; render::ItemID _myItem{ render::Item::INVALID_ITEM_ID }; static gpu::PipelinePointer _pipeline; + static gpu::PipelinePointer _wireframePipeline; ShapeInfo _shapeInfo; PolyVox::SimpleVolume* _volData = nullptr; - bool _volDataDirty = false; // does getMesh need to be called? + bool _volDataDirty = false; // does recomputeMesh need to be called? int _onCount; // how many non-zero voxels are in _volData bool _neighborsNeedUpdate { false }; @@ -178,7 +182,7 @@ private: // these are run off the main thread void decompressVolumeData(); void compressVolumeDataAndSendEditPacket(); - virtual void getMesh() override; // recompute mesh + virtual void recomputeMesh() override; // recompute mesh void computeShapeInfoWorker(); // these are cached lookups of _xNNeighborID, _yNNeighborID, _zNNeighborID, _xPNeighborID, _yPNeighborID, _zPNeighborID @@ -191,6 +195,7 @@ private: void cacheNeighbors(); void copyUpperEdgesFromNeighbors(); void bonkNeighbors(); + bool updateDependents(); }; bool inUserBounds(const PolyVox::SimpleVolume* vol, PolyVoxEntityItem::PolyVoxSurfaceStyle surfaceStyle, diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp index c3e097382c..1ad60bf7c6 100644 --- a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp @@ -114,13 +114,22 @@ void RenderableShapeEntityItem::render(RenderArgs* args) { auto outColor = _procedural->getColor(color); outColor.a *= _procedural->isFading() ? Interpolate::calculateFadeRatio(_procedural->getFadeStartTime()) : 1.0f; batch._glColor4f(outColor.r, outColor.g, outColor.b, outColor.a); - DependencyManager::get()->renderShape(batch, MAPPING[_shape]); + if (render::ShapeKey(args->_globalShapeKey).isWireframe()) { + DependencyManager::get()->renderWireShape(batch, MAPPING[_shape]); + } else { + DependencyManager::get()->renderShape(batch, MAPPING[_shape]); + } } else { // FIXME, support instanced multi-shape rendering using multidraw indirect color.a *= _isFading ? Interpolate::calculateFadeRatio(_fadeStartTime) : 1.0f; auto geometryCache = DependencyManager::get(); auto pipeline = color.a < 1.0f ? geometryCache->getTransparentShapePipeline() : geometryCache->getOpaqueShapePipeline(); - geometryCache->renderSolidShapeInstance(batch, MAPPING[_shape], color, pipeline); + + if (render::ShapeKey(args->_globalShapeKey).isWireframe()) { + geometryCache->renderWireShapeInstance(batch, MAPPING[_shape], color, pipeline); + } else { + geometryCache->renderSolidShapeInstance(batch, MAPPING[_shape], color, pipeline); + } } static const auto triCount = DependencyManager::get()->getShapeTriangleCount(MAPPING[_shape]); diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp index d7d7013f59..c4ae0db1aa 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp @@ -216,7 +216,7 @@ void RenderableWebEntityItem::render(RenderArgs* args) { if (!_texture) { auto webSurface = _webSurface; - _texture = gpu::TexturePointer(gpu::Texture::createExternal2D(OffscreenQmlSurface::getDiscardLambda())); + _texture = gpu::TexturePointer(gpu::Texture::createExternal(OffscreenQmlSurface::getDiscardLambda())); _texture->setSource(__FUNCTION__); } OffscreenQmlSurface::TextureAndFence newTextureAndFence; diff --git a/libraries/entities/src/EntitiesScriptEngineProvider.h b/libraries/entities/src/EntitiesScriptEngineProvider.h index 69bf73e688..d87dd105c2 100644 --- a/libraries/entities/src/EntitiesScriptEngineProvider.h +++ b/libraries/entities/src/EntitiesScriptEngineProvider.h @@ -15,11 +15,13 @@ #define hifi_EntitiesScriptEngineProvider_h #include +#include #include "EntityItemID.h" class EntitiesScriptEngineProvider { public: virtual void callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const QStringList& params = QStringList()) = 0; + virtual QFuture getLocalEntityScriptDetails(const EntityItemID& entityID) = 0; }; -#endif // hifi_EntitiesScriptEngineProvider_h \ No newline at end of file +#endif // hifi_EntitiesScriptEngineProvider_h diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index 3ef1648fae..0bb085459e 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -655,13 +655,11 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef // pack SimulationOwner and terse update properties near each other - // NOTE: the server is authoritative for changes to simOwnerID so we always unpack ownership data // even when we would otherwise ignore the rest of the packet. bool filterRejection = false; if (propertyFlags.getHasProperty(PROP_SIMULATION_OWNER)) { - QByteArray simOwnerData; int bytes = OctreePacketData::unpackDataFromBytes(dataAt, simOwnerData); SimulationOwner newSimOwner; @@ -1879,6 +1877,7 @@ void EntityItem::setSimulationOwner(const SimulationOwner& owner) { } void EntityItem::updateSimulationOwner(const SimulationOwner& owner) { + // NOTE: this method only used by EntityServer. The Interface uses special code in readEntityDataFromBuffer(). if (wantTerseEditLogging() && _simulationOwner != owner) { qCDebug(entities) << "sim ownership for" << getDebugName() << "is now" << owner; } @@ -1894,8 +1893,9 @@ void EntityItem::clearSimulationOwnership() { } _simulationOwner.clear(); - // don't bother setting the DIRTY_SIMULATOR_ID flag because clearSimulationOwnership() - // is only ever called on the entity-server and the flags are only used client-side + // don't bother setting the DIRTY_SIMULATOR_ID flag because: + // (a) when entity-server calls clearSimulationOwnership() the dirty-flags are meaningless (only used by interface) + // (b) the interface only calls clearSimulationOwnership() in a context that already knows best about dirty flags //_dirtyFlags |= Simulation::DIRTY_SIMULATOR_ID; } diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index ea81df3801..1ed020e592 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -49,13 +49,6 @@ EntityItemProperties::EntityItemProperties(EntityPropertyFlags desiredProperties } -void EntityItemProperties::setSittingPoints(const QVector& sittingPoints) { - _sittingPoints.clear(); - foreach (SittingPoint sitPoint, sittingPoints) { - _sittingPoints.append(sitPoint); - } -} - void EntityItemProperties::calculateNaturalPosition(const glm::vec3& min, const glm::vec3& max) { glm::vec3 halfDimension = (max - min) / 2.0f; _naturalPosition = max - halfDimension; @@ -546,20 +539,6 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_TEXTURES, textures); } - // Sitting properties support - if (!skipDefaults && !strictSemantics) { - QScriptValue sittingPoints = engine->newObject(); - for (int i = 0; i < _sittingPoints.size(); ++i) { - QScriptValue sittingPoint = engine->newObject(); - sittingPoint.setProperty("name", _sittingPoints.at(i).name); - sittingPoint.setProperty("position", vec3toScriptValue(engine, _sittingPoints.at(i).position)); - sittingPoint.setProperty("rotation", quatToScriptValue(engine, _sittingPoints.at(i).rotation)); - sittingPoints.setProperty(i, sittingPoint); - } - sittingPoints.setProperty("length", _sittingPoints.size()); - COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER_ALWAYS(sittingPoints, sittingPoints); // gettable, but not settable - } - if (!skipDefaults && !strictSemantics) { AABox aaBox = getAABox(); QScriptValue boundingBox = engine->newObject(); diff --git a/libraries/entities/src/EntityItemProperties.h b/libraries/entities/src/EntityItemProperties.h index 419740e4ea..590298e102 100644 --- a/libraries/entities/src/EntityItemProperties.h +++ b/libraries/entities/src/EntityItemProperties.h @@ -22,7 +22,6 @@ #include #include -#include // for SittingPoint #include #include #include @@ -255,8 +254,6 @@ public: void clearID() { _id = UNKNOWN_ENTITY_ID; _idSet = false; } void markAllChanged(); - void setSittingPoints(const QVector& sittingPoints); - const glm::vec3& getNaturalDimensions() const { return _naturalDimensions; } void setNaturalDimensions(const glm::vec3& value) { _naturalDimensions = value; } @@ -325,7 +322,6 @@ private: // NOTE: The following are pseudo client only properties. They are only used in clients which can access // properties of model geometry. But these properties are not serialized like other properties. - QVector _sittingPoints; QVariantMap _textureNames; glm::vec3 _naturalDimensions; glm::vec3 _naturalPosition; diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index 540eba4511..1ab5438e53 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -8,8 +8,15 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // + +#include +#include + #include "EntityScriptingInterface.h" +#include +#include + #include "EntityItemID.h" #include #include @@ -289,13 +296,11 @@ EntityItemProperties EntityScriptingInterface::getEntityProperties(QUuid identit results = entity->getProperties(desiredProperties); - // TODO: improve sitting points and naturalDimensions in the future, - // for now we've included the old sitting points model behavior for entity types that are models - // we've also added this hack for setting natural dimensions of models + // TODO: improve naturalDimensions in the future, + // for now we've added this hack for setting natural dimensions of models if (entity->getType() == EntityTypes::Model) { const FBXGeometry* geometry = _entityTree->getGeometryForEntity(entity); if (geometry) { - results.setSittingPoints(geometry->sittingPoints); Extents meshExtents = geometry->getUnscaledMeshExtents(); results.setNaturalDimensions(meshExtents.maximum - meshExtents.minimum); results.calculateNaturalPosition(meshExtents.minimum, meshExtents.maximum); @@ -680,6 +685,118 @@ bool EntityScriptingInterface::reloadServerScripts(QUuid entityID) { return client->reloadServerScript(entityID); } +bool EntityPropertyMetadataRequest::script(EntityItemID entityID, QScriptValue handler) { + using LocalScriptStatusRequest = QFutureWatcher; + + LocalScriptStatusRequest* request = new LocalScriptStatusRequest; + QObject::connect(request, &LocalScriptStatusRequest::finished, _engine, [=]() mutable { + auto details = request->result().toMap(); + QScriptValue err, result; + if (details.contains("isError")) { + if (!details.contains("message")) { + details["message"] = details["errorInfo"]; + } + err = _engine->makeError(_engine->toScriptValue(details)); + } else { + details["success"] = true; + result = _engine->toScriptValue(details); + } + callScopedHandlerObject(handler, err, result); + request->deleteLater(); + }); + auto entityScriptingInterface = DependencyManager::get(); + entityScriptingInterface->withEntitiesScriptEngine([&](EntitiesScriptEngineProvider* entitiesScriptEngine) { + if (entitiesScriptEngine) { + request->setFuture(entitiesScriptEngine->getLocalEntityScriptDetails(entityID)); + } + }); + if (!request->isStarted()) { + request->deleteLater(); + callScopedHandlerObject(handler, _engine->makeError("Entities Scripting Provider unavailable", "InternalError"), QScriptValue()); + return false; + } + return true; +} + +bool EntityPropertyMetadataRequest::serverScripts(EntityItemID entityID, QScriptValue handler) { + auto client = DependencyManager::get(); + auto request = client->createScriptStatusRequest(entityID); + QPointer engine = _engine; + QObject::connect(request, &GetScriptStatusRequest::finished, _engine, [=](GetScriptStatusRequest* request) mutable { + auto engine = _engine; + if (!engine) { + qCDebug(entities) << __FUNCTION__ << " -- engine destroyed while inflight" << entityID; + return; + } + QVariantMap details; + details["success"] = request->getResponseReceived(); + details["isRunning"] = request->getIsRunning(); + details["status"] = EntityScriptStatus_::valueToKey(request->getStatus()).toLower(); + details["errorInfo"] = request->getErrorInfo(); + + QScriptValue err, result; + if (!details["success"].toBool()) { + if (!details.contains("message") && details.contains("errorInfo")) { + details["message"] = details["errorInfo"]; + } + if (details["message"].toString().isEmpty()) { + details["message"] = "entity server script details not found"; + } + err = engine->makeError(engine->toScriptValue(details)); + } else { + result = engine->toScriptValue(details); + } + callScopedHandlerObject(handler, err, result); + request->deleteLater(); + }); + request->start(); + return true; +} + +bool EntityScriptingInterface::queryPropertyMetadata(QUuid entityID, QScriptValue property, QScriptValue scopeOrCallback, QScriptValue methodOrName) { + auto name = property.toString(); + auto handler = makeScopedHandlerObject(scopeOrCallback, methodOrName); + QPointer engine = dynamic_cast(handler.engine()); + if (!engine) { + qCDebug(entities) << "queryPropertyMetadata without detectable engine" << entityID << name; + return false; + } +#ifdef DEBUG_ENGINE_STATE + connect(engine, &QObject::destroyed, this, [=]() { + qDebug() << "queryPropertyMetadata -- engine destroyed!" << (!engine ? "nullptr" : "engine"); + }); +#endif + if (!handler.property("callback").isFunction()) { + qDebug() << "!handler.callback.isFunction" << engine; + engine->raiseException(engine->makeError("callback is not a function", "TypeError")); + return false; + } + + // NOTE: this approach is a work-in-progress and for now just meant to work 100% correctly and provide + // some initial structure for organizing metadata adapters around. + + // The extra layer of indirection is *essential* because in real world conditions errors are often introduced + // by accident and sometimes without exact memory of "what just changed." + + // Here the scripter only needs to know an entityID and a property name -- which means all scripters can + // level this method when stuck in dead-end scenarios or to learn more about "magic" Entity properties + // like .script that work in terms of side-effects. + + // This is an async callback pattern -- so if needed C++ can easily throttle or restrict queries later. + + EntityPropertyMetadataRequest request(engine); + + if (name == "script") { + return request.script(entityID, handler); + } else if (name == "serverScripts") { + return request.serverScripts(entityID, handler); + } else { + engine->raiseException(engine->makeError("metadata for property " + name + " is not yet queryable")); + engine->maybeEmitUncaughtException(__FUNCTION__); + return false; + } +} + bool EntityScriptingInterface::getServerScriptStatus(QUuid entityID, QScriptValue callback) { auto client = DependencyManager::get(); auto request = client->createScriptStatusRequest(entityID); @@ -815,8 +932,7 @@ void RayToEntityIntersectionResultFromScriptValue(const QScriptValue& object, Ra } } -bool EntityScriptingInterface::setVoxels(QUuid entityID, - std::function actor) { +bool EntityScriptingInterface::polyVoxWorker(QUuid entityID, std::function actor) { PROFILE_RANGE(script_entities, __FUNCTION__); if (!_entityTree) { @@ -882,11 +998,9 @@ bool EntityScriptingInterface::setPoints(QUuid entityID, std::function& points) { PROFILE_RANGE(script_entities, __FUNCTION__); @@ -1541,3 +1674,20 @@ bool EntityScriptingInterface::AABoxIntersectsCapsule(const glm::vec3& low, cons AABox aaBox(low, dimensions); return aaBox.findCapsulePenetration(start, end, radius, penetration); } + +glm::mat4 EntityScriptingInterface::getEntityTransform(const QUuid& entityID) { + glm::mat4 result; + if (_entityTree) { + _entityTree->withReadLock([&] { + EntityItemPointer entity = _entityTree->findEntityByEntityItemID(EntityItemID(entityID)); + if (entity) { + glm::mat4 translation = glm::translate(entity->getPosition()); + glm::mat4 rotation = glm::mat4_cast(entity->getRotation()); + glm::mat4 registration = glm::translate(ENTITY_ITEM_DEFAULT_REGISTRATION_POINT - + entity->getRegistrationPoint()); + result = translation * rotation * registration; + } + }); + } + return result; +} diff --git a/libraries/entities/src/EntityScriptingInterface.h b/libraries/entities/src/EntityScriptingInterface.h index e9f0637830..63b5771e60 100644 --- a/libraries/entities/src/EntityScriptingInterface.h +++ b/libraries/entities/src/EntityScriptingInterface.h @@ -34,7 +34,23 @@ #include "EntitiesScriptEngineProvider.h" #include "EntityItemProperties.h" +#include "BaseScriptEngine.h" + class EntityTree; +class MeshProxy; + +// helper factory to compose standardized, async metadata queries for "magic" Entity properties +// like .script and .serverScripts. This is used for automated testing of core scripting features +// as well as to provide early adopters a self-discoverable, consistent way to diagnose common +// problems with their own Entity scripts. +class EntityPropertyMetadataRequest { +public: + EntityPropertyMetadataRequest(BaseScriptEngine* engine) : _engine(engine) {}; + bool script(EntityItemID entityID, QScriptValue handler); + bool serverScripts(EntityItemID entityID, QScriptValue handler); +private: + QPointer _engine; +}; class RayToEntityIntersectionResult { public: @@ -67,6 +83,7 @@ class EntityScriptingInterface : public OctreeScriptingInterface, public Depende Q_PROPERTY(float costMultiplier READ getCostMultiplier WRITE setCostMultiplier) Q_PROPERTY(QUuid keyboardFocusEntity READ getKeyboardFocusEntity WRITE setKeyboardFocusEntity) + friend EntityPropertyMetadataRequest; public: EntityScriptingInterface(bool bidOnSimulationOwnership); @@ -211,6 +228,26 @@ public slots: Q_INVOKABLE RayToEntityIntersectionResult findRayIntersectionBlocking(const PickRay& ray, bool precisionPicking = false, const QScriptValue& entityIdsToInclude = QScriptValue(), const QScriptValue& entityIdsToDiscard = QScriptValue()); Q_INVOKABLE bool reloadServerScripts(QUuid entityID); + + /**jsdoc + * Query additional metadata for "magic" Entity properties like `script` and `serverScripts`. + * + * @function Entities.queryPropertyMetadata + * @param {EntityID} entityID The ID of the entity. + * @param {string} property The name of the property extended metadata is wanted for. + * @param {ResultCallback} callback Executes callback(err, result) with the query results. + */ + /**jsdoc + * Query additional metadata for "magic" Entity properties like `script` and `serverScripts`. + * + * @function Entities.queryPropertyMetadata + * @param {EntityID} entityID The ID of the entity. + * @param {string} property The name of the property extended metadata is wanted for. + * @param {Object} thisObject The scoping "this" context that callback will be executed within. + * @param {ResultCallback} callbackOrMethodName Executes thisObject[callbackOrMethodName](err, result) with the query results. + */ + Q_INVOKABLE bool queryPropertyMetadata(QUuid entityID, QScriptValue property, QScriptValue scopeOrCallback, QScriptValue methodOrName = QScriptValue()); + Q_INVOKABLE bool getServerScriptStatus(QUuid entityID, QScriptValue callback); Q_INVOKABLE void setLightsArePickable(bool value); @@ -229,6 +266,7 @@ public slots: Q_INVOKABLE bool setAllVoxels(QUuid entityID, int value); Q_INVOKABLE bool setVoxelsInCuboid(QUuid entityID, const glm::vec3& lowPosition, const glm::vec3& cuboidSize, int value); + Q_INVOKABLE void voxelsToMesh(QUuid entityID, QScriptValue callback); Q_INVOKABLE bool setAllPoints(QUuid entityID, const QVector& points); Q_INVOKABLE bool appendPoint(QUuid entityID, const glm::vec3& point); @@ -293,6 +331,15 @@ public slots: const glm::vec3& start, const glm::vec3& end, float radius); + /**jsdoc + * Returns object to world transform, excluding scale + * + * @function Entities.getEntityTransform + * @param {EntityID} entityID The ID of the entity whose transform is to be returned + * @return {Mat4} Entity's object to world transform, excluding scale + */ + Q_INVOKABLE glm::mat4 getEntityTransform(const QUuid& entityID); + signals: void collisionWithEntity(const EntityItemID& idA, const EntityItemID& idB, const Collision& collision); @@ -323,9 +370,14 @@ signals: void webEventReceived(const EntityItemID& entityItemID, const QVariant& message); +protected: + void withEntitiesScriptEngine(std::function function) { + std::lock_guard lock(_entitiesScriptEngineLock); + function(_entitiesScriptEngine); + }; private: bool actionWorker(const QUuid& entityID, std::function actor); - bool setVoxels(QUuid entityID, std::function actor); + bool polyVoxWorker(QUuid entityID, std::function actor); bool setPoints(QUuid entityID, std::function actor); void queueEntityMessage(PacketType packetType, EntityItemID entityID, const EntityItemProperties& properties); diff --git a/libraries/entities/src/PolyVoxEntityItem.cpp b/libraries/entities/src/PolyVoxEntityItem.cpp index 2a374c1d17..90344d6c4b 100644 --- a/libraries/entities/src/PolyVoxEntityItem.cpp +++ b/libraries/entities/src/PolyVoxEntityItem.cpp @@ -242,3 +242,7 @@ const QByteArray PolyVoxEntityItem::getVoxelData() const { }); return voxelDataCopy; } + +bool PolyVoxEntityItem::getMeshAsScriptValue(QScriptEngine *engine, QScriptValue& result) { + return false; +} diff --git a/libraries/entities/src/PolyVoxEntityItem.h b/libraries/entities/src/PolyVoxEntityItem.h index 910d8eff88..311a002a4a 100644 --- a/libraries/entities/src/PolyVoxEntityItem.h +++ b/libraries/entities/src/PolyVoxEntityItem.h @@ -57,6 +57,8 @@ class PolyVoxEntityItem : public EntityItem { virtual void setVoxelData(QByteArray voxelData); virtual const QByteArray getVoxelData() const; + virtual int getOnCount() const { return 0; } + enum PolyVoxSurfaceStyle { SURFACE_MARCHING_CUBES, SURFACE_CUBIC, @@ -131,7 +133,9 @@ class PolyVoxEntityItem : public EntityItem { virtual void rebakeMesh() {}; void setVoxelDataDirty(bool value) { withWriteLock([&] { _voxelDataDirty = value; }); } - virtual void getMesh() {}; // recompute mesh + virtual void recomputeMesh() {}; + + virtual bool getMeshAsScriptValue(QScriptEngine *engine, QScriptValue& result); protected: glm::vec3 _voxelVolumeSize; // this is always 3 bytes diff --git a/libraries/entities/src/PropertyGroup.h b/libraries/entities/src/PropertyGroup.h index 38b1e5f599..f45d19f5eb 100644 --- a/libraries/entities/src/PropertyGroup.h +++ b/libraries/entities/src/PropertyGroup.h @@ -14,9 +14,11 @@ #include -//#include "EntityItemProperties.h" +#include + #include "EntityPropertyFlags.h" + class EntityItemProperties; class EncodeBitstreamParams; class OctreePacketData; @@ -24,31 +26,6 @@ class EntityTreeElementExtraEncodeData; class ReadBitstreamToTreeParams; using EntityTreeElementExtraEncodeDataPointer = std::shared_ptr; -#include - -/* -#include - -#include -#include - -#include -#include -#include - -#include -#include // for SittingPoint -#include -#include -#include - -#include "EntityItemID.h" -#include "PropertyGroupMacros.h" -#include "EntityTypes.h" -*/ - -//typedef PropertyFlags EntityPropertyFlags; - class PropertyGroup { public: diff --git a/libraries/fbx/src/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp index fcaef90527..718793fefa 100644 --- a/libraries/fbx/src/FBXReader.cpp +++ b/libraries/fbx/src/FBXReader.cpp @@ -1468,6 +1468,9 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS // Create the Material Library consolidateFBXMaterials(mapping); + // We can't allow the scaling of a given image to different sizes, because the hash used for the KTX cache is based on the original image + // Allowing scaling of the same image to different sizes would cause different KTX files to target the same cache key +#if 0 // HACK: until we get proper LOD management we're going to cap model textures // according to how many unique textures the model uses: // 1 - 8 textures --> 2048 @@ -1481,6 +1484,7 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS int numTextures = uniqueTextures.size(); const int MAX_NUM_TEXTURES_AT_MAX_RESOLUTION = 8; int maxWidth = sqrt(MAX_NUM_PIXELS_FOR_FBX_TEXTURE); + if (numTextures > MAX_NUM_TEXTURES_AT_MAX_RESOLUTION) { int numTextureThreshold = MAX_NUM_TEXTURES_AT_MAX_RESOLUTION; const int MIN_MIP_TEXTURE_WIDTH = 64; @@ -1494,7 +1498,7 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS material.setMaxNumPixelsPerTexture(maxWidth * maxWidth); } } - +#endif geometry.materials = _fbxMaterials; // see if any materials have texture children @@ -1795,19 +1799,6 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS } geometry.palmDirection = parseVec3(mapping.value("palmDirection", "0, -1, 0").toString()); - // Add sitting points - QVariantHash sittingPoints = mapping.value("sit").toHash(); - for (QVariantHash::const_iterator it = sittingPoints.constBegin(); it != sittingPoints.constEnd(); it++) { - SittingPoint sittingPoint; - sittingPoint.name = it.key(); - - QVariantList properties = it->toList(); - sittingPoint.position = parseVec3(properties.at(0).toString()); - sittingPoint.rotation = glm::quat(glm::radians(parseVec3(properties.at(1).toString()))); - - geometry.sittingPoints.append(sittingPoint); - } - // attempt to map any meshes to a named model for (QHash::const_iterator m = meshIDsToMeshIndices.constBegin(); m != meshIDsToMeshIndices.constEnd(); m++) { diff --git a/libraries/fbx/src/FBXReader.h b/libraries/fbx/src/FBXReader.h index 6e51c413dc..fa047e512f 100644 --- a/libraries/fbx/src/FBXReader.h +++ b/libraries/fbx/src/FBXReader.h @@ -265,24 +265,6 @@ public: Q_DECLARE_METATYPE(FBXAnimationFrame) Q_DECLARE_METATYPE(QVector) -/// A point where an avatar can sit -class SittingPoint { -public: - QString name; - glm::vec3 position; // relative postion - glm::quat rotation; // relative orientation -}; - -inline bool operator==(const SittingPoint& lhs, const SittingPoint& rhs) -{ - return (lhs.name == rhs.name) && (lhs.position == rhs.position) && (lhs.rotation == rhs.rotation); -} - -inline bool operator!=(const SittingPoint& lhs, const SittingPoint& rhs) -{ - return (lhs.name != rhs.name) || (lhs.position != rhs.position) || (lhs.rotation != rhs.rotation); -} - /// A set of meshes extracted from an FBX document. class FBXGeometry { public: @@ -320,8 +302,6 @@ public: glm::vec3 palmDirection; - QVector sittingPoints; - glm::vec3 neckPivot; Extents bindExtents; diff --git a/libraries/fbx/src/FBXReader_Node.cpp b/libraries/fbx/src/FBXReader_Node.cpp index d814f58dab..d987f885eb 100644 --- a/libraries/fbx/src/FBXReader_Node.cpp +++ b/libraries/fbx/src/FBXReader_Node.cpp @@ -54,7 +54,8 @@ template QVariant readBinaryArray(QDataStream& in, int& position) { in.readRawData(compressed.data() + sizeof(quint32), compressedLength); position += compressedLength; arrayData = qUncompress(compressed); - if (arrayData.isEmpty() || arrayData.size() != (sizeof(T) * arrayLength)) { // answers empty byte array if corrupt + if (arrayData.isEmpty() || + (unsigned int)arrayData.size() != (sizeof(T) * arrayLength)) { // answers empty byte array if corrupt throw QString("corrupt fbx file"); } } else { diff --git a/libraries/fbx/src/OBJReader.cpp b/libraries/fbx/src/OBJReader.cpp index 73cf7a520e..c1bb72dff8 100644 --- a/libraries/fbx/src/OBJReader.cpp +++ b/libraries/fbx/src/OBJReader.cpp @@ -267,7 +267,7 @@ void OBJReader::parseMaterialLibrary(QIODevice* device) { } if (token == "map_Kd") { currentMaterial.diffuseTextureFilename = filename; - } else { + } else if( token == "map_Ks" ) { currentMaterial.specularTextureFilename = filename; } } @@ -546,6 +546,7 @@ FBXGeometry* OBJReader::readOBJ(QByteArray& model, const QVariantHash& mapping, QString queryPart = _url.query(); bool suppressMaterialsHack = queryPart.contains("hifiusemat"); // If this appears in query string, don't fetch mtl even if used. OBJMaterial& preDefinedMaterial = materials[SMART_DEFAULT_MATERIAL_NAME]; + preDefinedMaterial.used = true; if (suppressMaterialsHack) { needsMaterialLibrary = preDefinedMaterial.userSpecifiesUV = false; // I said it was a hack... } @@ -594,8 +595,8 @@ FBXGeometry* OBJReader::readOBJ(QByteArray& model, const QVariantHash& mapping, } foreach (QString materialID, materials.keys()) { - OBJMaterial& objMaterial = materials[materialID]; - if (!objMaterial.used) { + OBJMaterial& objMaterial = materials[materialID]; + if (!objMaterial.used) { continue; } geometry.materials[materialID] = FBXMaterial(objMaterial.diffuseColor, @@ -611,6 +612,9 @@ FBXGeometry* OBJReader::readOBJ(QByteArray& model, const QVariantHash& mapping, if (!objMaterial.diffuseTextureFilename.isEmpty()) { fbxMaterial.albedoTexture.filename = objMaterial.diffuseTextureFilename; } + if (!objMaterial.specularTextureFilename.isEmpty()) { + fbxMaterial.specularTexture.filename = objMaterial.specularTextureFilename; + } modelMaterial->setEmissive(fbxMaterial.emissiveColor); modelMaterial->setAlbedo(fbxMaterial.diffuseColor); diff --git a/libraries/fbx/src/OBJReader.h b/libraries/fbx/src/OBJReader.h index 200f11548d..b4a48c570e 100644 --- a/libraries/fbx/src/OBJReader.h +++ b/libraries/fbx/src/OBJReader.h @@ -58,7 +58,7 @@ public: QByteArray specularTextureFilename; bool used { false }; bool userSpecifiesUV { false }; - OBJMaterial() : shininess(96.0f), opacity(1.0f), diffuseColor(1.0f), specularColor(1.0f) {} + OBJMaterial() : shininess(0.0f), opacity(1.0f), diffuseColor(0.9f), specularColor(0.9f) {} }; class OBJReader: public QObject { // QObject so we can make network requests. diff --git a/libraries/fbx/src/OBJWriter.cpp b/libraries/fbx/src/OBJWriter.cpp new file mode 100644 index 0000000000..5ee04c5718 --- /dev/null +++ b/libraries/fbx/src/OBJWriter.cpp @@ -0,0 +1,148 @@ +// +// OBJWriter.cpp +// libraries/fbx/src/ +// +// Created by Seth Alves on 2017-1-27. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include +#include +#include "model/Geometry.h" +#include "OBJWriter.h" +#include "ModelFormatLogging.h" + +static QString formatFloat(double n) { + // limit precision to 6, but don't output trailing zeros. + QString s = QString::number(n, 'f', 6); + while (s.endsWith("0")) { + s.remove(s.size() - 1, 1); + } + if (s.endsWith(".")) { + s.remove(s.size() - 1, 1); + } + + // check for non-numbers. if we get NaN or inf or scientific notation, just return 0 + for (int i = 0; i < s.length(); i++) { + auto c = s.at(i).toLatin1(); + if (c != '-' && + c != '.' && + (c < '0' || c > '9')) { + qCDebug(modelformat) << "OBJWriter zeroing bad vertex coordinate:" << s << "because of" << c; + return QString("0"); + } + } + + return s; +} + +bool writeOBJToTextStream(QTextStream& out, QList meshes) { + // each mesh's vertices are numbered from zero. We're combining all their vertices into one list here, + // so keep track of the start index for each mesh. + QList meshVertexStartOffset; + int currentVertexStartOffset = 0; + + // write out all vertices + foreach (const MeshPointer& mesh, meshes) { + meshVertexStartOffset.append(currentVertexStartOffset); + const gpu::BufferView& vertexBuffer = mesh->getVertexBuffer(); + int vertexCount = 0; + gpu::BufferView::Iterator vertexItr = vertexBuffer.cbegin(); + while (vertexItr != vertexBuffer.cend()) { + glm::vec3 v = *vertexItr; + out << "v "; + out << formatFloat(v[0]) << " "; + out << formatFloat(v[1]) << " "; + out << formatFloat(v[2]) << "\n"; + vertexItr++; + vertexCount++; + } + currentVertexStartOffset += vertexCount; + } + out << "\n"; + + // write out faces + int nth = 0; + foreach (const MeshPointer& mesh, meshes) { + currentVertexStartOffset = meshVertexStartOffset.takeFirst(); + + const gpu::BufferView& partBuffer = mesh->getPartBuffer(); + const gpu::BufferView& indexBuffer = mesh->getIndexBuffer(); + + model::Index partCount = (model::Index)mesh->getNumParts(); + for (int partIndex = 0; partIndex < partCount; partIndex++) { + const model::Mesh::Part& part = partBuffer.get(partIndex); + + out << "g part-" << nth++ << "\n"; + + // model::Mesh::TRIANGLES + // TODO -- handle other formats + gpu::BufferView::Iterator indexItr = indexBuffer.cbegin(); + indexItr += part._startIndex; + + int indexCount = 0; + while (indexItr != indexBuffer.cend() && indexCount < part._numIndices) { + uint32_t index0 = *indexItr; + indexItr++; + indexCount++; + if (indexItr == indexBuffer.cend() || indexCount >= part._numIndices) { + qCDebug(modelformat) << "OBJWriter -- index buffer length isn't multiple of 3"; + break; + } + uint32_t index1 = *indexItr; + indexItr++; + indexCount++; + if (indexItr == indexBuffer.cend() || indexCount >= part._numIndices) { + qCDebug(modelformat) << "OBJWriter -- index buffer length isn't multiple of 3"; + break; + } + uint32_t index2 = *indexItr; + indexItr++; + indexCount++; + + out << "f "; + out << currentVertexStartOffset + index0 + 1 << " "; + out << currentVertexStartOffset + index1 + 1 << " "; + out << currentVertexStartOffset + index2 + 1 << "\n"; + } + out << "\n"; + } + } + + return true; +} + +bool writeOBJToFile(QString path, QList meshes) { + if (QFileInfo(path).exists() && !QFile::remove(path)) { + qCDebug(modelformat) << "OBJ writer failed, file exists:" << path; + return false; + } + + QFile file(path); + if (!file.open(QIODevice::WriteOnly)) { + qCDebug(modelformat) << "OBJ writer failed to open output file:" << path; + return false; + } + + QTextStream outStream(&file); + + bool success; + success = writeOBJToTextStream(outStream, meshes); + + file.close(); + return success; +} + +QString writeOBJToString(QList meshes) { + QString result; + QTextStream outStream(&result, QIODevice::ReadWrite); + bool success; + success = writeOBJToTextStream(outStream, meshes); + if (success) { + return result; + } + return QString(""); +} diff --git a/libraries/fbx/src/OBJWriter.h b/libraries/fbx/src/OBJWriter.h new file mode 100644 index 0000000000..b6e20e1ae6 --- /dev/null +++ b/libraries/fbx/src/OBJWriter.h @@ -0,0 +1,26 @@ +// +// OBJWriter.h +// libraries/fbx/src/ +// +// Created by Seth Alves on 2017-1-27. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_objwriter_h +#define hifi_objwriter_h + + +#include +#include +#include + +using MeshPointer = std::shared_ptr; + +bool writeOBJToTextStream(QTextStream& out, QList meshes); +bool writeOBJToFile(QString path, QList meshes); +QString writeOBJToString(QList meshes); + +#endif // hifi_objwriter_h diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp index c51f468908..0800c27839 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp @@ -62,8 +62,6 @@ BackendPointer GLBackend::createBackend() { INSTANCE = result.get(); void* voidInstance = &(*result); qApp->setProperty(hifi::properties::gl::BACKEND, QVariant::fromValue(voidInstance)); - - gl::GLTexture::initTextureTransferHelper(); return result; } @@ -209,7 +207,7 @@ void GLBackend::renderPassTransfer(const Batch& batch) { } } - { // Sync all the buffers + { // Sync all the transform states PROFILE_RANGE(render_gpu_gl_detail, "syncCPUTransform"); _transform._cameras.clear(); _transform._cameraOffsets.clear(); @@ -277,7 +275,7 @@ void GLBackend::renderPassDraw(const Batch& batch) { updateInput(); updateTransform(batch); updatePipeline(); - + CommandCall call = _commandCalls[(*command)]; (this->*(call))(batch, *offset); break; @@ -623,6 +621,7 @@ void GLBackend::queueLambda(const std::function lambda) const { } void GLBackend::recycle() const { + PROFILE_RANGE(render_gpu_gl, __FUNCTION__) { std::list> lamdbasTrash; { @@ -745,10 +744,6 @@ void GLBackend::recycle() const { glDeleteQueries((GLsizei)ids.size(), ids.data()); } } - -#ifndef THREADED_TEXTURE_TRANSFER - gl::GLTexture::_textureTransferHelper->process(); -#endif } void GLBackend::setCameraCorrection(const Mat4& correction) { diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackend.h b/libraries/gpu-gl/src/gpu/gl/GLBackend.h index 950ac65a3f..76c950ec2b 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackend.h +++ b/libraries/gpu-gl/src/gpu/gl/GLBackend.h @@ -187,10 +187,15 @@ public: virtual void do_setStateScissorRect(const Batch& batch, size_t paramOffset) final; virtual GLuint getFramebufferID(const FramebufferPointer& framebuffer) = 0; - virtual GLuint getTextureID(const TexturePointer& texture, bool needTransfer = true) = 0; + virtual GLuint getTextureID(const TexturePointer& texture) final; virtual GLuint getBufferID(const Buffer& buffer) = 0; virtual GLuint getQueryID(const QueryPointer& query) = 0; - virtual bool isTextureReady(const TexturePointer& texture); + + virtual GLFramebuffer* syncGPUObject(const Framebuffer& framebuffer) = 0; + virtual GLBuffer* syncGPUObject(const Buffer& buffer) = 0; + virtual GLTexture* syncGPUObject(const TexturePointer& texture); + virtual GLQuery* syncGPUObject(const Query& query) = 0; + //virtual bool isTextureReady(const TexturePointer& texture); virtual void releaseBuffer(GLuint id, Size size) const; virtual void releaseExternalTexture(GLuint id, const Texture::ExternalRecycler& recycler) const; @@ -206,10 +211,6 @@ public: protected: void recycle() const override; - virtual GLFramebuffer* syncGPUObject(const Framebuffer& framebuffer) = 0; - virtual GLBuffer* syncGPUObject(const Buffer& buffer) = 0; - virtual GLTexture* syncGPUObject(const TexturePointer& texture, bool sync = true) = 0; - virtual GLQuery* syncGPUObject(const Query& query) = 0; static const size_t INVALID_OFFSET = (size_t)-1; bool _inRenderTransferPass { false }; diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackendTexture.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackendTexture.cpp index f51eac0e33..ca4e328612 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackendTexture.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackendTexture.cpp @@ -14,12 +14,56 @@ using namespace gpu; using namespace gpu::gl; -bool GLBackend::isTextureReady(const TexturePointer& texture) { - // DO not transfer the texture, this call is expected for rendering texture - GLTexture* object = syncGPUObject(texture, true); - return object && object->isReady(); + +GLuint GLBackend::getTextureID(const TexturePointer& texture) { + GLTexture* object = syncGPUObject(texture); + + if (!object) { + return 0; + } + + return object->_id; } +GLTexture* GLBackend::syncGPUObject(const TexturePointer& texturePointer) { + const Texture& texture = *texturePointer; + // Special case external textures + if (TextureUsageType::EXTERNAL == texture.getUsageType()) { + Texture::ExternalUpdates updates = texture.getUpdates(); + if (!updates.empty()) { + Texture::ExternalRecycler recycler = texture.getExternalRecycler(); + Q_ASSERT(recycler); + // Discard any superfluous updates + while (updates.size() > 1) { + const auto& update = updates.front(); + // Superfluous updates will never have been read, but we want to ensure the previous + // writes to them are complete before they're written again, so return them with the + // same fences they arrived with. This can happen on any thread because no GL context + // work is involved + recycler(update.first, update.second); + updates.pop_front(); + } + + // The last texture remaining is the one we'll use to create the GLTexture + const auto& update = updates.front(); + // Check for a fence, and if it exists, inject a wait into the command stream, then destroy the fence + if (update.second) { + GLsync fence = static_cast(update.second); + glWaitSync(fence, 0, GL_TIMEOUT_IGNORED); + glDeleteSync(fence); + } + + // Create the new texture object (replaces any previous texture object) + new GLExternalTexture(shared_from_this(), texture, update.first); + } + + // Return the texture object (if any) associated with the texture, without extensive logic + // (external textures are + return Backend::getGPUObject(texture); + } + + return nullptr; +} void GLBackend::do_generateTextureMips(const Batch& batch, size_t paramOffset) { TexturePointer resourceTexture = batch._textures.get(batch._params[paramOffset + 0]._uint); @@ -28,7 +72,7 @@ void GLBackend::do_generateTextureMips(const Batch& batch, size_t paramOffset) { } // DO not transfer the texture, this call is expected for rendering texture - GLTexture* object = syncGPUObject(resourceTexture, false); + GLTexture* object = syncGPUObject(resourceTexture); if (!object) { return; } diff --git a/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.cpp b/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.cpp index 85cf069062..2ac7e9d060 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.cpp @@ -21,13 +21,12 @@ GLFramebuffer::~GLFramebuffer() { } } -bool GLFramebuffer::checkStatus(GLenum target) const { - bool result = false; +bool GLFramebuffer::checkStatus() const { switch (_status) { case GL_FRAMEBUFFER_COMPLETE: // Success ! - result = true; - break; + return true; + case GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT: qCWarning(gpugllogging) << "GLFramebuffer::syncGPUObject : Framebuffer not valid, GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT."; break; @@ -44,5 +43,5 @@ bool GLFramebuffer::checkStatus(GLenum target) const { qCWarning(gpugllogging) << "GLFramebuffer::syncGPUObject : Framebuffer not valid, GL_FRAMEBUFFER_UNSUPPORTED."; break; } - return result; + return false; } diff --git a/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.h b/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.h index 9b4f9703fc..c0633cfdef 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.h +++ b/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.h @@ -64,7 +64,7 @@ public: protected: GLenum _status { GL_FRAMEBUFFER_COMPLETE }; virtual void update() = 0; - bool checkStatus(GLenum target) const; + bool checkStatus() const; GLFramebuffer(const std::weak_ptr& backend, const Framebuffer& framebuffer, GLuint id) : GLObject(backend, framebuffer, id) {} ~GLFramebuffer(); diff --git a/libraries/gpu-gl/src/gpu/gl/GLTexelFormat.cpp b/libraries/gpu-gl/src/gpu/gl/GLTexelFormat.cpp index bd945cbaaa..7e26e65e02 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLTexelFormat.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLTexelFormat.cpp @@ -17,6 +17,7 @@ GLenum GLTexelFormat::evalGLTexelFormatInternal(const gpu::Element& dstFormat) { switch (dstFormat.getDimension()) { case gpu::SCALAR: { switch (dstFormat.getSemantic()) { + case gpu::RED: case gpu::RGB: case gpu::RGBA: case gpu::SRGB: @@ -262,6 +263,7 @@ GLTexelFormat GLTexelFormat::evalGLTexelFormat(const Element& dstFormat, const E texel.type = ELEMENT_TYPE_TO_GL[dstFormat.getType()]; switch (dstFormat.getSemantic()) { + case gpu::RED: case gpu::RGB: case gpu::RGBA: texel.internalFormat = GL_R8; @@ -272,8 +274,10 @@ GLTexelFormat GLTexelFormat::evalGLTexelFormat(const Element& dstFormat, const E break; case gpu::DEPTH: + texel.format = GL_DEPTH_COMPONENT; texel.internalFormat = GL_DEPTH_COMPONENT32; break; + case gpu::DEPTH_STENCIL: texel.type = GL_UNSIGNED_INT_24_8; texel.format = GL_DEPTH_STENCIL; @@ -403,6 +407,7 @@ GLTexelFormat GLTexelFormat::evalGLTexelFormat(const Element& dstFormat, const E texel.internalFormat = GL_COMPRESSED_RED_RGTC1; break; } + case gpu::RED: case gpu::RGB: case gpu::RGBA: case gpu::SRGB: diff --git a/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp b/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp index 1e0dd08ae1..1de820e1df 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp @@ -10,15 +10,13 @@ #include -#include "GLTextureTransfer.h" #include "GLBackend.h" using namespace gpu; using namespace gpu::gl; -std::shared_ptr GLTexture::_textureTransferHelper; -const GLenum GLTexture::CUBE_FACE_LAYOUT[6] = { +const GLenum GLTexture::CUBE_FACE_LAYOUT[GLTexture::TEXTURE_CUBE_NUM_FACES] = { GL_TEXTURE_CUBE_MAP_POSITIVE_X, GL_TEXTURE_CUBE_MAP_NEGATIVE_X, GL_TEXTURE_CUBE_MAP_POSITIVE_Y, GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, GL_TEXTURE_CUBE_MAP_POSITIVE_Z, GL_TEXTURE_CUBE_MAP_NEGATIVE_Z @@ -67,6 +65,17 @@ GLenum GLTexture::getGLTextureType(const Texture& texture) { } +uint8_t GLTexture::getFaceCount(GLenum target) { + switch (target) { + case GL_TEXTURE_2D: + return TEXTURE_2D_NUM_FACES; + case GL_TEXTURE_CUBE_MAP: + return TEXTURE_CUBE_NUM_FACES; + default: + Q_UNREACHABLE(); + break; + } +} const std::vector& GLTexture::getFaceTargets(GLenum target) { static std::vector cubeFaceTargets { GL_TEXTURE_CUBE_MAP_POSITIVE_X, GL_TEXTURE_CUBE_MAP_NEGATIVE_X, @@ -89,216 +98,34 @@ const std::vector& GLTexture::getFaceTargets(GLenum target) { return faceTargets; } -// Default texture memory = GPU total memory - 2GB -#define GPU_MEMORY_RESERVE_BYTES MB_TO_BYTES(2048) -// Minimum texture memory = 1GB -#define TEXTURE_MEMORY_MIN_BYTES MB_TO_BYTES(1024) - - -float GLTexture::getMemoryPressure() { - // Check for an explicit memory limit - auto availableTextureMemory = Texture::getAllowedGPUMemoryUsage(); - - - // If no memory limit has been set, use a percentage of the total dedicated memory - if (!availableTextureMemory) { -#if 0 - auto totalMemory = getDedicatedMemory(); - if ((GPU_MEMORY_RESERVE_BYTES + TEXTURE_MEMORY_MIN_BYTES) > totalMemory) { - availableTextureMemory = TEXTURE_MEMORY_MIN_BYTES; - } else { - availableTextureMemory = totalMemory - GPU_MEMORY_RESERVE_BYTES; - } -#else - // Hardcode texture limit for sparse textures at 1 GB for now - availableTextureMemory = TEXTURE_MEMORY_MIN_BYTES; -#endif - } - - // Return the consumed texture memory divided by the available texture memory. - auto consumedGpuMemory = Context::getTextureGPUMemoryUsage() - Context::getTextureGPUFramebufferMemoryUsage(); - float memoryPressure = (float)consumedGpuMemory / (float)availableTextureMemory; - static Context::Size lastConsumedGpuMemory = 0; - if (memoryPressure > 1.0f && lastConsumedGpuMemory != consumedGpuMemory) { - lastConsumedGpuMemory = consumedGpuMemory; - qCDebug(gpugllogging) << "Exceeded max allowed texture memory: " << consumedGpuMemory << " / " << availableTextureMemory; - } - return memoryPressure; -} - - -// Create the texture and allocate storage -GLTexture::GLTexture(const std::weak_ptr& backend, const Texture& texture, GLuint id, bool transferrable) : - GLObject(backend, texture, id), - _external(false), - _source(texture.source()), - _storageStamp(texture.getStamp()), - _target(getGLTextureType(texture)), - _internalFormat(gl::GLTexelFormat::evalGLTexelFormatInternal(texture.getTexelFormat())), - _maxMip(texture.maxMip()), - _minMip(texture.minMip()), - _virtualSize(texture.evalTotalSize()), - _transferrable(transferrable) -{ - auto strongBackend = _backend.lock(); - strongBackend->recycle(); - Backend::incrementTextureGPUCount(); - Backend::updateTextureGPUVirtualMemoryUsage(0, _virtualSize); - Backend::setGPUObject(texture, this); -} - GLTexture::GLTexture(const std::weak_ptr& backend, const Texture& texture, GLuint id) : GLObject(backend, texture, id), - _external(true), _source(texture.source()), - _storageStamp(0), - _target(getGLTextureType(texture)), - _internalFormat(GL_RGBA8), - // FIXME force mips to 0? - _maxMip(texture.maxMip()), - _minMip(texture.minMip()), - _virtualSize(0), - _transferrable(false) + _target(getGLTextureType(texture)) { Backend::setGPUObject(texture, this); - - // FIXME Is this necessary? - //withPreservedTexture([this] { - // syncSampler(); - // if (_gpuObject.isAutogenerateMips()) { - // generateMips(); - // } - //}); } GLTexture::~GLTexture() { + auto backend = _backend.lock(); + if (backend && _id) { + backend->releaseTexture(_id, 0); + } +} + + +GLExternalTexture::GLExternalTexture(const std::weak_ptr& backend, const Texture& texture, GLuint id) + : Parent(backend, texture, id) { } + +GLExternalTexture::~GLExternalTexture() { auto backend = _backend.lock(); if (backend) { - if (_external) { - auto recycler = _gpuObject.getExternalRecycler(); - if (recycler) { - backend->releaseExternalTexture(_id, recycler); - } else { - qWarning() << "No recycler available for texture " << _id << " possible leak"; - } - } else if (_id) { - // WARNING! Sparse textures do not use this code path. See GL45BackendTexture for - // the GL45Texture destructor for doing any required work tracking GPU stats - backend->releaseTexture(_id, _size); + auto recycler = _gpuObject.getExternalRecycler(); + if (recycler) { + backend->releaseExternalTexture(_id, recycler); + } else { + qWarning() << "No recycler available for texture " << _id << " possible leak"; } - - if (!_external && !_transferrable) { - Backend::updateTextureGPUFramebufferMemoryUsage(_size, 0); - } - } - Backend::updateTextureGPUVirtualMemoryUsage(_virtualSize, 0); -} - -void GLTexture::createTexture() { - withPreservedTexture([&] { - allocateStorage(); - (void)CHECK_GL_ERROR(); - syncSampler(); - (void)CHECK_GL_ERROR(); - }); -} - -void GLTexture::withPreservedTexture(std::function f) const { - GLint boundTex = -1; - switch (_target) { - case GL_TEXTURE_2D: - glGetIntegerv(GL_TEXTURE_BINDING_2D, &boundTex); - break; - - case GL_TEXTURE_CUBE_MAP: - glGetIntegerv(GL_TEXTURE_BINDING_CUBE_MAP, &boundTex); - break; - - default: - qFatal("Unsupported texture type"); - } - (void)CHECK_GL_ERROR(); - - glBindTexture(_target, _texture); - f(); - glBindTexture(_target, boundTex); - (void)CHECK_GL_ERROR(); -} - -void GLTexture::setSize(GLuint size) const { - if (!_external && !_transferrable) { - Backend::updateTextureGPUFramebufferMemoryUsage(_size, size); - } - Backend::updateTextureGPUMemoryUsage(_size, size); - const_cast(_size) = size; -} - -bool GLTexture::isInvalid() const { - return _storageStamp < _gpuObject.getStamp(); -} - -bool GLTexture::isOutdated() const { - return GLSyncState::Idle == _syncState && _contentStamp < _gpuObject.getDataStamp(); -} - -bool GLTexture::isReady() const { - // If we have an invalid texture, we're never ready - if (isInvalid()) { - return false; - } - - auto syncState = _syncState.load(); - if (isOutdated() || Idle != syncState) { - return false; - } - - return true; -} - - -// Do any post-transfer operations that might be required on the main context / rendering thread -void GLTexture::postTransfer() { - setSyncState(GLSyncState::Idle); - ++_transferCount; - - // At this point the mip pixels have been loaded, we can notify the gpu texture to abandon it's memory - switch (_gpuObject.getType()) { - case Texture::TEX_2D: - for (uint16_t i = 0; i < Sampler::MAX_MIP_LEVEL; ++i) { - if (_gpuObject.isStoredMipFaceAvailable(i)) { - _gpuObject.notifyMipFaceGPULoaded(i); - } - } - break; - - case Texture::TEX_CUBE: - // transfer pixels from each faces - for (uint8_t f = 0; f < CUBE_NUM_FACES; f++) { - for (uint16_t i = 0; i < Sampler::MAX_MIP_LEVEL; ++i) { - if (_gpuObject.isStoredMipFaceAvailable(i, f)) { - _gpuObject.notifyMipFaceGPULoaded(i, f); - } - } - } - break; - - default: - qCWarning(gpugllogging) << __FUNCTION__ << " case for Texture Type " << _gpuObject.getType() << " not supported"; - break; + const_cast(_id) = 0; } } - -void GLTexture::initTextureTransferHelper() { - _textureTransferHelper = std::make_shared(); -} - -void GLTexture::startTransfer() { - createTexture(); -} - -void GLTexture::finishTransfer() { - if (_gpuObject.isAutogenerateMips()) { - generateMips(); - } -} - diff --git a/libraries/gpu-gl/src/gpu/gl/GLTexture.h b/libraries/gpu-gl/src/gpu/gl/GLTexture.h index 0f75a6fe51..1f91e17157 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLTexture.h +++ b/libraries/gpu-gl/src/gpu/gl/GLTexture.h @@ -9,7 +9,6 @@ #define hifi_gpu_gl_GLTexture_h #include "GLShared.h" -#include "GLTextureTransfer.h" #include "GLBackend.h" #include "GLTexelFormat.h" @@ -20,210 +19,48 @@ struct GLFilterMode { GLint magFilter; }; - class GLTexture : public GLObject { + using Parent = GLObject; + friend class GLBackend; public: static const uint16_t INVALID_MIP { (uint16_t)-1 }; static const uint8_t INVALID_FACE { (uint8_t)-1 }; - static void initTextureTransferHelper(); - static std::shared_ptr _textureTransferHelper; - - template - static GLTexture* sync(GLBackend& backend, const TexturePointer& texturePointer, bool needTransfer) { - const Texture& texture = *texturePointer; - - // Special case external textures - if (texture.getUsage().isExternal()) { - Texture::ExternalUpdates updates = texture.getUpdates(); - if (!updates.empty()) { - Texture::ExternalRecycler recycler = texture.getExternalRecycler(); - Q_ASSERT(recycler); - // Discard any superfluous updates - while (updates.size() > 1) { - const auto& update = updates.front(); - // Superfluous updates will never have been read, but we want to ensure the previous - // writes to them are complete before they're written again, so return them with the - // same fences they arrived with. This can happen on any thread because no GL context - // work is involved - recycler(update.first, update.second); - updates.pop_front(); - } - - // The last texture remaining is the one we'll use to create the GLTexture - const auto& update = updates.front(); - // Check for a fence, and if it exists, inject a wait into the command stream, then destroy the fence - if (update.second) { - GLsync fence = static_cast(update.second); - glWaitSync(fence, 0, GL_TIMEOUT_IGNORED); - glDeleteSync(fence); - } - - // Create the new texture object (replaces any previous texture object) - new GLTextureType(backend.shared_from_this(), texture, update.first); - } - - // Return the texture object (if any) associated with the texture, without extensive logic - // (external textures are - return Backend::getGPUObject(texture); - } - - if (!texture.isDefined()) { - // NO texture definition yet so let's avoid thinking - return nullptr; - } - - // If the object hasn't been created, or the object definition is out of date, drop and re-create - GLTexture* object = Backend::getGPUObject(texture); - - // Create the texture if need be (force re-creation if the storage stamp changes - // for easier use of immutable storage) - if (!object || object->isInvalid()) { - // This automatically any previous texture - object = new GLTextureType(backend.shared_from_this(), texture, needTransfer); - if (!object->_transferrable) { - object->createTexture(); - object->_contentStamp = texture.getDataStamp(); - object->updateSize(); - object->postTransfer(); - } - } - - // Object maybe doens't neet to be tranasferred after creation - if (!object->_transferrable) { - return object; - } - - // If we just did a transfer, return the object after doing post-transfer work - if (GLSyncState::Transferred == object->getSyncState()) { - object->postTransfer(); - } - - if (object->isOutdated()) { - // Object might be outdated, if so, start the transfer - // (outdated objects that are already in transfer will have reported 'true' for ready() - _textureTransferHelper->transferTexture(texturePointer); - return nullptr; - } - - if (!object->isReady()) { - return nullptr; - } - - ((GLTexture*)object)->updateMips(); - - return object; - } - - template - static GLuint getId(GLBackend& backend, const TexturePointer& texture, bool shouldSync) { - if (!texture) { - return 0; - } - GLTexture* object { nullptr }; - if (shouldSync) { - object = sync(backend, texture, shouldSync); - } else { - object = Backend::getGPUObject(*texture); - } - - if (!object) { - return 0; - } - - if (!shouldSync) { - return object->_id; - } - - // Don't return textures that are in transfer state - if ((object->getSyncState() != GLSyncState::Idle) || - // Don't return transferrable textures that have never completed transfer - (!object->_transferrable || 0 != object->_transferCount)) { - return 0; - } - - return object->_id; - } - ~GLTexture(); - // Is this texture generated outside the GPU library? - const bool _external; const GLuint& _texture { _id }; const std::string _source; - const Stamp _storageStamp; const GLenum _target; - const GLenum _internalFormat; - const uint16 _maxMip; - uint16 _minMip; - const GLuint _virtualSize; // theoretical size as expected - Stamp _contentStamp { 0 }; - const bool _transferrable; - Size _transferCount { 0 }; - GLuint size() const { return _size; } - GLSyncState getSyncState() const { return _syncState; } - // Is the storage out of date relative to the gpu texture? - bool isInvalid() const; + static const std::vector& getFaceTargets(GLenum textureType); + static uint8_t getFaceCount(GLenum textureType); + static GLenum getGLTextureType(const Texture& texture); - // Is the content out of date relative to the gpu texture? - bool isOutdated() const; - - // Is the texture in a state where it can be rendered with no work? - bool isReady() const; - - // Execute any post-move operations that must occur only on the main thread - virtual void postTransfer(); - - uint16 usedMipLevels() const { return (_maxMip - _minMip) + 1; } - - static const size_t CUBE_NUM_FACES = 6; - static const GLenum CUBE_FACE_LAYOUT[6]; + static const uint8_t TEXTURE_2D_NUM_FACES = 1; + static const uint8_t TEXTURE_CUBE_NUM_FACES = 6; + static const GLenum CUBE_FACE_LAYOUT[TEXTURE_CUBE_NUM_FACES]; static const GLFilterMode FILTER_MODES[Sampler::NUM_FILTERS]; static const GLenum WRAP_MODES[Sampler::NUM_WRAP_MODES]; - // Return a floating point value indicating how much of the allowed - // texture memory we are currently consuming. A value of 0 indicates - // no texture memory usage, while a value of 1 indicates all available / allowed memory - // is consumed. A value above 1 indicates that there is a problem. - static float getMemoryPressure(); protected: - - static const std::vector& getFaceTargets(GLenum textureType); - - static GLenum getGLTextureType(const Texture& texture); - - - const GLuint _size { 0 }; // true size as reported by the gl api - std::atomic _syncState { GLSyncState::Idle }; - - GLTexture(const std::weak_ptr& backend, const Texture& texture, GLuint id, bool transferrable); - GLTexture(const std::weak_ptr& backend, const Texture& texture, GLuint id); - - void setSyncState(GLSyncState syncState) { _syncState = syncState; } - - void createTexture(); - - virtual void updateMips() {} - virtual void allocateStorage() const = 0; - virtual void updateSize() const = 0; - virtual void syncSampler() const = 0; + virtual uint32 size() const = 0; virtual void generateMips() const = 0; - virtual void withPreservedTexture(std::function f) const; -protected: - void setSize(GLuint size) const; - - virtual void startTransfer(); - // Returns true if this is the last block required to complete transfer - virtual bool continueTransfer() { return false; } - virtual void finishTransfer(); - -private: - friend class GLTextureTransferHelper; - friend class GLBackend; + GLTexture(const std::weak_ptr& backend, const Texture& texture, GLuint id); }; +class GLExternalTexture : public GLTexture { + using Parent = GLTexture; + friend class GLBackend; +public: + ~GLExternalTexture(); +protected: + GLExternalTexture(const std::weak_ptr& backend, const Texture& texture, GLuint id); + void generateMips() const override {} + uint32 size() const override { return 0; } +}; + + } } #endif diff --git a/libraries/gpu-gl/src/gpu/gl/GLTextureTransfer.cpp b/libraries/gpu-gl/src/gpu/gl/GLTextureTransfer.cpp deleted file mode 100644 index 9dac2986e3..0000000000 --- a/libraries/gpu-gl/src/gpu/gl/GLTextureTransfer.cpp +++ /dev/null @@ -1,208 +0,0 @@ -// -// Created by Bradley Austin Davis on 2016/04/03 -// Copyright 2013-2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// -#include "GLTextureTransfer.h" - -#include -#include - -#include - -#include "GLShared.h" -#include "GLTexture.h" - -#ifdef HAVE_NSIGHT -#include "nvToolsExt.h" -std::unordered_map _map; -#endif - - -#ifdef TEXTURE_TRANSFER_PBOS -#define TEXTURE_TRANSFER_BLOCK_SIZE (64 * 1024) -#define TEXTURE_TRANSFER_PBO_COUNT 128 -#endif - -using namespace gpu; -using namespace gpu::gl; - -GLTextureTransferHelper::GLTextureTransferHelper() { -#ifdef THREADED_TEXTURE_TRANSFER - setObjectName("TextureTransferThread"); - _context.create(); - initialize(true, QThread::LowPriority); - // Clean shutdown on UNIX, otherwise _canvas is freed early - connect(qApp, &QCoreApplication::aboutToQuit, [&] { terminate(); }); -#else - initialize(false, QThread::LowPriority); -#endif -} - -GLTextureTransferHelper::~GLTextureTransferHelper() { -#ifdef THREADED_TEXTURE_TRANSFER - if (isStillRunning()) { - terminate(); - } -#else - terminate(); -#endif -} - -void GLTextureTransferHelper::transferTexture(const gpu::TexturePointer& texturePointer) { - GLTexture* object = Backend::getGPUObject(*texturePointer); - - Backend::incrementTextureGPUTransferCount(); - object->setSyncState(GLSyncState::Pending); - Lock lock(_mutex); - _pendingTextures.push_back(texturePointer); -} - -void GLTextureTransferHelper::setup() { -#ifdef THREADED_TEXTURE_TRANSFER - _context.makeCurrent(); - -#ifdef TEXTURE_TRANSFER_FORCE_DRAW - // FIXME don't use opengl 4.5 DSA functionality without verifying it's present - glCreateRenderbuffers(1, &_drawRenderbuffer); - glNamedRenderbufferStorage(_drawRenderbuffer, GL_RGBA8, 128, 128); - glCreateFramebuffers(1, &_drawFramebuffer); - glNamedFramebufferRenderbuffer(_drawFramebuffer, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, _drawRenderbuffer); - glCreateFramebuffers(1, &_readFramebuffer); -#endif - -#ifdef TEXTURE_TRANSFER_PBOS - std::array pbos; - glCreateBuffers(TEXTURE_TRANSFER_PBO_COUNT, &pbos[0]); - for (uint32_t i = 0; i < TEXTURE_TRANSFER_PBO_COUNT; ++i) { - TextureTransferBlock newBlock; - newBlock._pbo = pbos[i]; - glNamedBufferStorage(newBlock._pbo, TEXTURE_TRANSFER_BLOCK_SIZE, 0, GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT); - newBlock._mapped = glMapNamedBufferRange(newBlock._pbo, 0, TEXTURE_TRANSFER_BLOCK_SIZE, GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT); - _readyQueue.push(newBlock); - } -#endif -#endif -} - -void GLTextureTransferHelper::shutdown() { -#ifdef THREADED_TEXTURE_TRANSFER - _context.makeCurrent(); -#endif - -#ifdef TEXTURE_TRANSFER_FORCE_DRAW - glNamedFramebufferRenderbuffer(_drawFramebuffer, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, 0); - glDeleteFramebuffers(1, &_drawFramebuffer); - _drawFramebuffer = 0; - glDeleteFramebuffers(1, &_readFramebuffer); - _readFramebuffer = 0; - - glNamedFramebufferTexture(_readFramebuffer, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0); - glDeleteRenderbuffers(1, &_drawRenderbuffer); - _drawRenderbuffer = 0; -#endif -} - -void GLTextureTransferHelper::queueExecution(VoidLambda lambda) { - Lock lock(_mutex); - _pendingCommands.push_back(lambda); -} - -#define MAX_TRANSFERS_PER_PASS 2 - -bool GLTextureTransferHelper::process() { - // Take any new textures or commands off the queue - VoidLambdaList pendingCommands; - TextureList newTransferTextures; - { - Lock lock(_mutex); - newTransferTextures.swap(_pendingTextures); - pendingCommands.swap(_pendingCommands); - } - - if (!pendingCommands.empty()) { - for (auto command : pendingCommands) { - command(); - } - glFlush(); - } - - if (!newTransferTextures.empty()) { - for (auto& texturePointer : newTransferTextures) { -#ifdef HAVE_NSIGHT - _map[texturePointer] = nvtxRangeStart("TextureTansfer"); -#endif - GLTexture* object = Backend::getGPUObject(*texturePointer); - object->startTransfer(); - _transferringTextures.push_back(texturePointer); - _textureIterator = _transferringTextures.begin(); - } - _transferringTextures.sort([](const gpu::TexturePointer& a, const gpu::TexturePointer& b)->bool { - return a->getSize() < b->getSize(); - }); - } - - // No transfers in progress, sleep - if (_transferringTextures.empty()) { -#ifdef THREADED_TEXTURE_TRANSFER - QThread::usleep(1); -#endif - return true; - } - PROFILE_COUNTER_IF_CHANGED(render_gpu_gl, "transferringTextures", int, (int) _transferringTextures.size()) - - static auto lastReport = usecTimestampNow(); - auto now = usecTimestampNow(); - auto lastReportInterval = now - lastReport; - if (lastReportInterval > USECS_PER_SECOND * 4) { - lastReport = now; - qCDebug(gpulogging) << "Texture list " << _transferringTextures.size(); - } - - size_t transferCount = 0; - for (_textureIterator = _transferringTextures.begin(); _textureIterator != _transferringTextures.end();) { - if (++transferCount > MAX_TRANSFERS_PER_PASS) { - break; - } - auto texture = *_textureIterator; - GLTexture* gltexture = Backend::getGPUObject(*texture); - if (gltexture->continueTransfer()) { - ++_textureIterator; - continue; - } - - gltexture->finishTransfer(); - -#ifdef TEXTURE_TRANSFER_FORCE_DRAW - // FIXME force a draw on the texture transfer thread before passing the texture to the main thread for use -#endif - -#ifdef THREADED_TEXTURE_TRANSFER - clientWait(); -#endif - gltexture->_contentStamp = gltexture->_gpuObject.getDataStamp(); - gltexture->updateSize(); - gltexture->setSyncState(gpu::gl::GLSyncState::Transferred); - Backend::decrementTextureGPUTransferCount(); -#ifdef HAVE_NSIGHT - // Mark the texture as transferred - nvtxRangeEnd(_map[texture]); - _map.erase(texture); -#endif - _textureIterator = _transferringTextures.erase(_textureIterator); - } - -#ifdef THREADED_TEXTURE_TRANSFER - if (!_transferringTextures.empty()) { - // Don't saturate the GPU - clientWait(); - } else { - // Don't saturate the CPU - QThread::msleep(1); - } -#endif - - return true; -} diff --git a/libraries/gpu-gl/src/gpu/gl/GLTextureTransfer.h b/libraries/gpu-gl/src/gpu/gl/GLTextureTransfer.h deleted file mode 100644 index a23c282fd4..0000000000 --- a/libraries/gpu-gl/src/gpu/gl/GLTextureTransfer.h +++ /dev/null @@ -1,78 +0,0 @@ -// -// Created by Bradley Austin Davis on 2016/04/03 -// Copyright 2013-2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// -#ifndef hifi_gpu_gl_GLTextureTransfer_h -#define hifi_gpu_gl_GLTextureTransfer_h - -#include -#include - -#include - -#include - -#include "GLShared.h" - -#ifdef Q_OS_WIN -#define THREADED_TEXTURE_TRANSFER -#endif - -#ifdef THREADED_TEXTURE_TRANSFER -// FIXME when sparse textures are enabled, it's harder to force a draw on the transfer thread -// also, the current draw code is implicitly using OpenGL 4.5 functionality -//#define TEXTURE_TRANSFER_FORCE_DRAW -// FIXME PBO's increase the complexity and don't seem to work reliably -//#define TEXTURE_TRANSFER_PBOS -#endif - -namespace gpu { namespace gl { - -using TextureList = std::list; -using TextureListIterator = TextureList::iterator; - -class GLTextureTransferHelper : public GenericThread { -public: - using VoidLambda = std::function; - using VoidLambdaList = std::list; - using Pointer = std::shared_ptr; - GLTextureTransferHelper(); - ~GLTextureTransferHelper(); - void transferTexture(const gpu::TexturePointer& texturePointer); - void queueExecution(VoidLambda lambda); - - void setup() override; - void shutdown() override; - bool process() override; - -private: -#ifdef THREADED_TEXTURE_TRANSFER - ::gl::OffscreenContext _context; -#endif - -#ifdef TEXTURE_TRANSFER_FORCE_DRAW - // Framebuffers / renderbuffers for forcing access to the texture on the transfer thread - GLuint _drawRenderbuffer { 0 }; - GLuint _drawFramebuffer { 0 }; - GLuint _readFramebuffer { 0 }; -#endif - - // A mutex for protecting items access on the render and transfer threads - Mutex _mutex; - // Commands that have been submitted for execution on the texture transfer thread - VoidLambdaList _pendingCommands; - // Textures that have been submitted for transfer - TextureList _pendingTextures; - // Textures currently in the transfer process - // Only used on the transfer thread - TextureList _transferringTextures; - TextureListIterator _textureIterator; - -}; - -} } - -#endif \ No newline at end of file diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h b/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h index 72e2f5a804..6d2f91c436 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h +++ b/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h @@ -40,18 +40,28 @@ public: class GL41Texture : public GLTexture { using Parent = GLTexture; - GLuint allocate(); - public: - GL41Texture(const std::weak_ptr& backend, const Texture& buffer, GLuint externalId); - GL41Texture(const std::weak_ptr& backend, const Texture& buffer, bool transferrable); + static GLuint allocate(); + + public: + ~GL41Texture(); + + private: + GL41Texture(const std::weak_ptr& backend, const Texture& buffer); - protected: - void transferMip(uint16_t mipLevel, uint8_t face) const; - void startTransfer() override; - void allocateStorage() const override; - void updateSize() const override; - void syncSampler() const override; void generateMips() const override; + uint32 size() const override; + + friend class GL41Backend; + const Stamp _storageStamp; + mutable Stamp _contentStamp { 0 }; + mutable Stamp _samplerStamp { 0 }; + const uint32 _size; + + + bool isOutdated() const; + void withPreservedTexture(std::function f) const; + void syncContent() const; + void syncSampler() const; }; @@ -62,8 +72,7 @@ protected: GLuint getBufferID(const Buffer& buffer) override; GLBuffer* syncGPUObject(const Buffer& buffer) override; - GLuint getTextureID(const TexturePointer& texture, bool needTransfer = true) override; - GLTexture* syncGPUObject(const TexturePointer& texture, bool sync = true) override; + GLTexture* syncGPUObject(const TexturePointer& texture) override; GLuint getQueryID(const QueryPointer& query) override; GLQuery* syncGPUObject(const Query& query) override; diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41BackendOutput.cpp b/libraries/gpu-gl/src/gpu/gl41/GL41BackendOutput.cpp index 6d11a52035..195b155bf3 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41BackendOutput.cpp +++ b/libraries/gpu-gl/src/gpu/gl41/GL41BackendOutput.cpp @@ -53,10 +53,12 @@ public: GL_COLOR_ATTACHMENT15 }; int unit = 0; + auto backend = _backend.lock(); for (auto& b : _gpuObject.getRenderBuffers()) { surface = b._texture; if (surface) { - gltexture = gl::GLTexture::sync(*_backend.lock().get(), surface, false); // Grab the gltexture and don't transfer + Q_ASSERT(TextureUsageType::RENDERBUFFER == surface->getUsageType()); + gltexture = backend->syncGPUObject(surface); } else { gltexture = nullptr; } @@ -81,9 +83,11 @@ public: } if (_gpuObject.getDepthStamp() != _depthStamp) { + auto backend = _backend.lock(); auto surface = _gpuObject.getDepthStencilBuffer(); if (_gpuObject.hasDepthStencil() && surface) { - gltexture = gl::GLTexture::sync(*_backend.lock().get(), surface, false); // Grab the gltexture and don't transfer + Q_ASSERT(TextureUsageType::RENDERBUFFER == surface->getUsageType()); + gltexture = backend->syncGPUObject(surface); } if (gltexture) { @@ -110,7 +114,7 @@ public: glBindFramebuffer(GL_DRAW_FRAMEBUFFER, currentFBO); } - checkStatus(GL_DRAW_FRAMEBUFFER); + checkStatus(); } diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp b/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp index 65c45111db..8dbef09f06 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp +++ b/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp @@ -29,20 +29,102 @@ GLuint GL41Texture::allocate() { return result; } -GLuint GL41Backend::getTextureID(const TexturePointer& texture, bool transfer) { - return GL41Texture::getId(*this, texture, transfer); +GLTexture* GL41Backend::syncGPUObject(const TexturePointer& texturePointer) { + if (!texturePointer) { + return nullptr; + } + const Texture& texture = *texturePointer; + if (TextureUsageType::EXTERNAL == texture.getUsageType()) { + return Parent::syncGPUObject(texturePointer); + } + + if (!texture.isDefined()) { + // NO texture definition yet so let's avoid thinking + return nullptr; + } + + // If the object hasn't been created, or the object definition is out of date, drop and re-create + GL41Texture* object = Backend::getGPUObject(texture); + if (!object || object->_storageStamp < texture.getStamp()) { + // This automatically any previous texture + object = new GL41Texture(shared_from_this(), texture); + } + + // FIXME internalize to GL41Texture 'sync' function + if (object->isOutdated()) { + object->withPreservedTexture([&] { + if (object->_contentStamp <= texture.getDataStamp()) { + // FIXME implement synchronous texture transfer here + object->syncContent(); + } + + if (object->_samplerStamp <= texture.getSamplerStamp()) { + object->syncSampler(); + } + }); + } + + return object; } -GLTexture* GL41Backend::syncGPUObject(const TexturePointer& texture, bool transfer) { - return GL41Texture::sync(*this, texture, transfer); +GL41Texture::GL41Texture(const std::weak_ptr& backend, const Texture& texture) + : GLTexture(backend, texture, allocate()), _storageStamp { texture.getStamp() }, _size(texture.evalTotalSize()) { + incrementTextureGPUCount(); + withPreservedTexture([&] { + GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat(), _gpuObject.getStoredMipFormat()); + auto numMips = _gpuObject.evalNumMips(); + for (uint16_t mipLevel = 0; mipLevel < numMips; ++mipLevel) { + // Get the mip level dimensions, accounting for the downgrade level + Vec3u dimensions = _gpuObject.evalMipDimensions(mipLevel); + uint8_t face = 0; + for (GLenum target : getFaceTargets(_target)) { + const Byte* mipData = nullptr; + if (_gpuObject.isStoredMipFaceAvailable(mipLevel, face)) { + auto mip = _gpuObject.accessStoredMipFace(mipLevel, face); + mipData = mip->readData(); + } + glTexImage2D(target, mipLevel, texelFormat.internalFormat, dimensions.x, dimensions.y, 0, texelFormat.format, texelFormat.type, mipData); + (void)CHECK_GL_ERROR(); + ++face; + } + } + }); } -GL41Texture::GL41Texture(const std::weak_ptr& backend, const Texture& texture, GLuint externalId) - : GLTexture(backend, texture, externalId) { +GL41Texture::~GL41Texture() { + } -GL41Texture::GL41Texture(const std::weak_ptr& backend, const Texture& texture, bool transferrable) - : GLTexture(backend, texture, allocate(), transferrable) { +bool GL41Texture::isOutdated() const { + if (_samplerStamp <= _gpuObject.getSamplerStamp()) { + return true; + } + if (TextureUsageType::RESOURCE == _gpuObject.getUsageType() && _contentStamp <= _gpuObject.getDataStamp()) { + return true; + } + return false; +} + +void GL41Texture::withPreservedTexture(std::function f) const { + GLint boundTex = -1; + switch (_target) { + case GL_TEXTURE_2D: + glGetIntegerv(GL_TEXTURE_BINDING_2D, &boundTex); + break; + + case GL_TEXTURE_CUBE_MAP: + glGetIntegerv(GL_TEXTURE_BINDING_CUBE_MAP, &boundTex); + break; + + default: + qFatal("Unsupported texture type"); + } + (void)CHECK_GL_ERROR(); + + glBindTexture(_target, _texture); + f(); + glBindTexture(_target, boundTex); + (void)CHECK_GL_ERROR(); } void GL41Texture::generateMips() const { @@ -52,94 +134,12 @@ void GL41Texture::generateMips() const { (void)CHECK_GL_ERROR(); } -void GL41Texture::allocateStorage() const { - GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat()); - glTexParameteri(_target, GL_TEXTURE_BASE_LEVEL, 0); - (void)CHECK_GL_ERROR(); - glTexParameteri(_target, GL_TEXTURE_MAX_LEVEL, _maxMip - _minMip); - (void)CHECK_GL_ERROR(); - if (GLEW_VERSION_4_2 && !_gpuObject.getTexelFormat().isCompressed()) { - // Get the dimensions, accounting for the downgrade level - Vec3u dimensions = _gpuObject.evalMipDimensions(_minMip); - glTexStorage2D(_target, usedMipLevels(), texelFormat.internalFormat, dimensions.x, dimensions.y); - (void)CHECK_GL_ERROR(); - } else { - for (uint16_t l = _minMip; l <= _maxMip; l++) { - // Get the mip level dimensions, accounting for the downgrade level - Vec3u dimensions = _gpuObject.evalMipDimensions(l); - for (GLenum target : getFaceTargets(_target)) { - glTexImage2D(target, l - _minMip, texelFormat.internalFormat, dimensions.x, dimensions.y, 0, texelFormat.format, texelFormat.type, NULL); - (void)CHECK_GL_ERROR(); - } - } - } +void GL41Texture::syncContent() const { + // FIXME actually copy the texture data + _contentStamp = _gpuObject.getDataStamp() + 1; } -void GL41Texture::updateSize() const { - setSize(_virtualSize); - if (!_id) { - return; - } - - if (_gpuObject.getTexelFormat().isCompressed()) { - GLenum proxyType = GL_TEXTURE_2D; - GLuint numFaces = 1; - if (_gpuObject.getType() == gpu::Texture::TEX_CUBE) { - proxyType = CUBE_FACE_LAYOUT[0]; - numFaces = (GLuint)CUBE_NUM_FACES; - } - GLint gpuSize{ 0 }; - glGetTexLevelParameteriv(proxyType, 0, GL_TEXTURE_COMPRESSED, &gpuSize); - (void)CHECK_GL_ERROR(); - - if (gpuSize) { - for (GLuint level = _minMip; level < _maxMip; level++) { - GLint levelSize{ 0 }; - glGetTexLevelParameteriv(proxyType, level, GL_TEXTURE_COMPRESSED_IMAGE_SIZE, &levelSize); - levelSize *= numFaces; - - if (levelSize <= 0) { - break; - } - gpuSize += levelSize; - } - (void)CHECK_GL_ERROR(); - setSize(gpuSize); - return; - } - } -} - -// Move content bits from the CPU to the GPU for a given mip / face -void GL41Texture::transferMip(uint16_t mipLevel, uint8_t face) const { - auto mip = _gpuObject.accessStoredMipFace(mipLevel, face); - GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat(), mip->getFormat()); - //GLenum target = getFaceTargets()[face]; - GLenum target = _target == GL_TEXTURE_2D ? GL_TEXTURE_2D : CUBE_FACE_LAYOUT[face]; - auto size = _gpuObject.evalMipDimensions(mipLevel); - glTexSubImage2D(target, mipLevel, 0, 0, size.x, size.y, texelFormat.format, texelFormat.type, mip->readData()); - (void)CHECK_GL_ERROR(); -} - -void GL41Texture::startTransfer() { - PROFILE_RANGE(render_gpu_gl, __FUNCTION__); - Parent::startTransfer(); - - glBindTexture(_target, _id); - (void)CHECK_GL_ERROR(); - - // transfer pixels from each faces - uint8_t numFaces = (Texture::TEX_CUBE == _gpuObject.getType()) ? CUBE_NUM_FACES : 1; - for (uint8_t f = 0; f < numFaces; f++) { - for (uint16_t i = 0; i < Sampler::MAX_MIP_LEVEL; ++i) { - if (_gpuObject.isStoredMipFaceAvailable(i, f)) { - transferMip(i, f); - } - } - } -} - -void GL41Backend::GL41Texture::syncSampler() const { +void GL41Texture::syncSampler() const { const Sampler& sampler = _gpuObject.getSampler(); const auto& fm = FILTER_MODES[sampler.getFilter()]; glTexParameteri(_target, GL_TEXTURE_MIN_FILTER, fm.minFilter); @@ -161,5 +161,9 @@ void GL41Backend::GL41Texture::syncSampler() const { glTexParameterf(_target, GL_TEXTURE_MIN_LOD, (float)sampler.getMinMip()); glTexParameterf(_target, GL_TEXTURE_MAX_LOD, (sampler.getMaxMip() == Sampler::MAX_MIP_LEVEL ? 1000.f : sampler.getMaxMip())); glTexParameterf(_target, GL_TEXTURE_MAX_ANISOTROPY_EXT, sampler.getMaxAnisotropy()); + _samplerStamp = _gpuObject.getSamplerStamp() + 1; } +uint32 GL41Texture::size() const { + return _size; +} diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45Backend.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45Backend.cpp index d7dde8b7d6..12c4b818f7 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45Backend.cpp +++ b/libraries/gpu-gl/src/gpu/gl45/GL45Backend.cpp @@ -18,6 +18,12 @@ Q_LOGGING_CATEGORY(gpugl45logging, "hifi.gpu.gl45") using namespace gpu; using namespace gpu::gl45; +void GL45Backend::recycle() const { + Parent::recycle(); + GL45VariableAllocationTexture::manageMemory(); + GL45VariableAllocationTexture::_frameTexturesCreated = 0; +} + void GL45Backend::do_draw(const Batch& batch, size_t paramOffset) { Primitive primitiveType = (Primitive)batch._params[paramOffset + 2]._uint; GLenum mode = gl::PRIMITIVE_TO_GL[primitiveType]; @@ -163,8 +169,3 @@ void GL45Backend::do_multiDrawIndexedIndirect(const Batch& batch, size_t paramOf _stats._DSNumAPIDrawcalls++; (void)CHECK_GL_ERROR(); } - -void GL45Backend::recycle() const { - Parent::recycle(); - derezTextures(); -} diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h b/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h index 2242bba5d9..6a9811b055 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h +++ b/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h @@ -8,17 +8,21 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#pragma once #ifndef hifi_gpu_45_GL45Backend_h #define hifi_gpu_45_GL45Backend_h #include "../gl/GLBackend.h" #include "../gl/GLTexture.h" +#include #define INCREMENTAL_TRANSFER 0 +#define THREADED_TEXTURE_BUFFERING 1 namespace gpu { namespace gl45 { using namespace gpu::gl; +using TextureWeakPointer = std::weak_ptr; class GL45Backend : public GLBackend { using Parent = GLBackend; @@ -31,60 +35,219 @@ public: class GL45Texture : public GLTexture { using Parent = GLTexture; + friend class GL45Backend; static GLuint allocate(const Texture& texture); + protected: + GL45Texture(const std::weak_ptr& backend, const Texture& texture); + void generateMips() const override; + void copyMipFaceFromTexture(uint16_t sourceMip, uint16_t targetMip, uint8_t face) const; + void copyMipFaceLinesFromTexture(uint16_t mip, uint8_t face, const uvec3& size, uint32_t yOffset, GLenum format, GLenum type, const void* sourcePointer) const; + virtual void syncSampler() const; + }; + + // + // Textures that have fixed allocation sizes and cannot be managed at runtime + // + + class GL45FixedAllocationTexture : public GL45Texture { + using Parent = GL45Texture; + friend class GL45Backend; + + public: + GL45FixedAllocationTexture(const std::weak_ptr& backend, const Texture& texture); + ~GL45FixedAllocationTexture(); + + protected: + uint32 size() const override { return _size; } + void allocateStorage() const; + void syncSampler() const override; + const uint32 _size { 0 }; + }; + + class GL45AttachmentTexture : public GL45FixedAllocationTexture { + using Parent = GL45FixedAllocationTexture; + friend class GL45Backend; + protected: + GL45AttachmentTexture(const std::weak_ptr& backend, const Texture& texture); + ~GL45AttachmentTexture(); + }; + + class GL45StrictResourceTexture : public GL45FixedAllocationTexture { + using Parent = GL45FixedAllocationTexture; + friend class GL45Backend; + protected: + GL45StrictResourceTexture(const std::weak_ptr& backend, const Texture& texture); + }; + + // + // Textures that can be managed at runtime to increase or decrease their memory load + // + + class GL45VariableAllocationTexture : public GL45Texture { + using Parent = GL45Texture; + friend class GL45Backend; + using PromoteLambda = std::function; + + public: + enum class MemoryPressureState { + Idle, + Transfer, + Oversubscribed, + Undersubscribed, + }; + + using QueuePair = std::pair; + struct QueuePairLess { + bool operator()(const QueuePair& a, const QueuePair& b) { + return a.second < b.second; + } + }; + using WorkQueue = std::priority_queue, QueuePairLess>; + + class TransferJob { + using VoidLambda = std::function; + using VoidLambdaQueue = std::queue; + using ThreadPointer = std::shared_ptr; + const GL45VariableAllocationTexture& _parent; + // Holds the contents to transfer to the GPU in CPU memory + std::vector _buffer; + // Indicates if a transfer from backing storage to interal storage has started + bool _bufferingStarted { false }; + bool _bufferingCompleted { false }; + VoidLambda _transferLambda; + VoidLambda _bufferingLambda; +#if THREADED_TEXTURE_BUFFERING + static Mutex _mutex; + static VoidLambdaQueue _bufferLambdaQueue; + static ThreadPointer _bufferThread; + static std::atomic _shutdownBufferingThread; + static void bufferLoop(); +#endif + + public: + TransferJob(const TransferJob& other) = delete; + TransferJob(const GL45VariableAllocationTexture& parent, std::function transferLambda); + TransferJob(const GL45VariableAllocationTexture& parent, uint16_t sourceMip, uint16_t targetMip, uint8_t face, uint32_t lines = 0, uint32_t lineOffset = 0); + ~TransferJob(); + bool tryTransfer(); + +#if THREADED_TEXTURE_BUFFERING + static void startTransferLoop(); + static void stopTransferLoop(); +#endif + + private: + size_t _transferSize { 0 }; +#if THREADED_TEXTURE_BUFFERING + void startBuffering(); +#endif + void transfer(); + }; + + using TransferQueue = std::queue>; + static MemoryPressureState _memoryPressureState; + protected: + static size_t _frameTexturesCreated; + static std::atomic _memoryPressureStateStale; + static std::list _memoryManagedTextures; + static WorkQueue _transferQueue; + static WorkQueue _promoteQueue; + static WorkQueue _demoteQueue; + static TexturePointer _currentTransferTexture; + static const uvec3 INITIAL_MIP_TRANSFER_DIMENSIONS; + + + static void updateMemoryPressure(); + static void processWorkQueues(); + static void addMemoryManagedTexture(const TexturePointer& texturePointer); + static void addToWorkQueue(const TexturePointer& texture); + static WorkQueue& getActiveWorkQueue(); + + static void manageMemory(); + + protected: + GL45VariableAllocationTexture(const std::weak_ptr& backend, const Texture& texture); + ~GL45VariableAllocationTexture(); + //bool canPromoteNoAllocate() const { return _allocatedMip < _populatedMip; } + bool canPromote() const { return _allocatedMip > 0; } + bool canDemote() const { return _allocatedMip < _maxAllocatedMip; } + bool hasPendingTransfers() const { return _populatedMip > _allocatedMip; } + void executeNextTransfer(const TexturePointer& currentTexture); + uint32 size() const override { return _size; } + virtual void populateTransferQueue() = 0; + virtual void promote() = 0; + virtual void demote() = 0; + + // The allocated mip level, relative to the number of mips in the gpu::Texture object + // The relationship between a given glMip to the original gpu::Texture mip is always + // glMip + _allocatedMip + uint16 _allocatedMip { 0 }; + // The populated mip level, relative to the number of mips in the gpu::Texture object + // This must always be >= the allocated mip + uint16 _populatedMip { 0 }; + // The highest (lowest resolution) mip that we will support, relative to the number + // of mips in the gpu::Texture object + uint16 _maxAllocatedMip { 0 }; + uint32 _size { 0 }; + // Contains a series of lambdas that when executed will transfer data to the GPU, modify + // the _populatedMip and update the sampler in order to fully populate the allocated texture + // until _populatedMip == _allocatedMip + TransferQueue _pendingTransfers; + }; + + class GL45ResourceTexture : public GL45VariableAllocationTexture { + using Parent = GL45VariableAllocationTexture; + friend class GL45Backend; + protected: + GL45ResourceTexture(const std::weak_ptr& backend, const Texture& texture); + + void syncSampler() const override; + void promote() override; + void demote() override; + void populateTransferQueue() override; + + void allocateStorage(uint16 mip); + void copyMipsFromTexture(); + }; + +#if 0 + class GL45SparseResourceTexture : public GL45VariableAllocationTexture { + using Parent = GL45VariableAllocationTexture; + friend class GL45Backend; + using TextureTypeFormat = std::pair; + using PageDimensions = std::vector; + using PageDimensionsMap = std::map; + static PageDimensionsMap pageDimensionsByFormat; + static Mutex pageDimensionsMutex; + + static bool isSparseEligible(const Texture& texture); + static PageDimensions getPageDimensionsForFormat(const TextureTypeFormat& typeFormat); + static PageDimensions getPageDimensionsForFormat(GLenum type, GLenum format); static const uint32_t DEFAULT_PAGE_DIMENSION = 128; static const uint32_t DEFAULT_MAX_SPARSE_LEVEL = 0xFFFF; - public: - GL45Texture(const std::weak_ptr& backend, const Texture& texture, GLuint externalId); - GL45Texture(const std::weak_ptr& backend, const Texture& texture, bool transferrable); - ~GL45Texture(); - - void postTransfer() override; - - struct SparseInfo { - SparseInfo(GL45Texture& texture); - void maybeMakeSparse(); - void update(); - uvec3 getPageCounts(const uvec3& dimensions) const; - uint32_t getPageCount(const uvec3& dimensions) const; - uint32_t getSize() const; - - GL45Texture& texture; - bool sparse { false }; - uvec3 pageDimensions { DEFAULT_PAGE_DIMENSION }; - GLuint maxSparseLevel { DEFAULT_MAX_SPARSE_LEVEL }; - uint32_t allocatedPages { 0 }; - uint32_t maxPages { 0 }; - uint32_t pageBytes { 0 }; - GLint pageDimensionsIndex { 0 }; - }; - protected: - void updateMips() override; - void stripToMip(uint16_t newMinMip); - void startTransfer() override; - bool continueTransfer() override; - void finishTransfer() override; - void incrementalTransfer(const uvec3& size, const gpu::Texture::PixelsPointer& mip, std::function f) const; - void transferMip(uint16_t mipLevel, uint8_t face = 0) const; - void allocateMip(uint16_t mipLevel, uint8_t face = 0) const; - void allocateStorage() const override; - void updateSize() const override; - void syncSampler() const override; - void generateMips() const override; - void withPreservedTexture(std::function f) const override; - void derez(); + GL45SparseResourceTexture(const std::weak_ptr& backend, const Texture& texture); + ~GL45SparseResourceTexture(); + uint32 size() const override { return _allocatedPages * _pageBytes; } + void promote() override; + void demote() override; - SparseInfo _sparseInfo; - uint16_t _mipOffset { 0 }; - friend class GL45Backend; + private: + uvec3 getPageCounts(const uvec3& dimensions) const; + uint32_t getPageCount(const uvec3& dimensions) const; + + uint32_t _allocatedPages { 0 }; + uint32_t _pageBytes { 0 }; + uvec3 _pageDimensions { DEFAULT_PAGE_DIMENSION }; + GLuint _maxSparseLevel { DEFAULT_MAX_SPARSE_LEVEL }; }; +#endif protected: + void recycle() const override; - void derezTextures() const; GLuint getFramebufferID(const FramebufferPointer& framebuffer) override; GLFramebuffer* syncGPUObject(const Framebuffer& framebuffer) override; @@ -92,8 +255,7 @@ protected: GLuint getBufferID(const Buffer& buffer) override; GLBuffer* syncGPUObject(const Buffer& buffer) override; - GLuint getTextureID(const TexturePointer& texture, bool needTransfer = true) override; - GLTexture* syncGPUObject(const TexturePointer& texture, bool sync = true) override; + GLTexture* syncGPUObject(const TexturePointer& texture) override; GLuint getQueryID(const QueryPointer& query) override; GLQuery* syncGPUObject(const Query& query) override; @@ -126,5 +288,5 @@ protected: Q_DECLARE_LOGGING_CATEGORY(gpugl45logging) - #endif + diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp index c5b84b7deb..9648af9b21 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp @@ -49,10 +49,12 @@ public: GL_COLOR_ATTACHMENT15 }; int unit = 0; + auto backend = _backend.lock(); for (auto& b : _gpuObject.getRenderBuffers()) { surface = b._texture; if (surface) { - gltexture = gl::GLTexture::sync(*_backend.lock().get(), surface, false); // Grab the gltexture and don't transfer + Q_ASSERT(TextureUsageType::RENDERBUFFER == surface->getUsageType()); + gltexture = backend->syncGPUObject(surface); } else { gltexture = nullptr; } @@ -78,8 +80,10 @@ public: if (_gpuObject.getDepthStamp() != _depthStamp) { auto surface = _gpuObject.getDepthStencilBuffer(); + auto backend = _backend.lock(); if (_gpuObject.hasDepthStencil() && surface) { - gltexture = gl::GLTexture::sync(*_backend.lock().get(), surface, false); // Grab the gltexture and don't transfer + Q_ASSERT(TextureUsageType::RENDERBUFFER == surface->getUsageType()); + gltexture = backend->syncGPUObject(surface); } if (gltexture) { @@ -102,7 +106,7 @@ public: _status = glCheckNamedFramebufferStatus(_id, GL_DRAW_FRAMEBUFFER); // restore the current framebuffer - checkStatus(GL_DRAW_FRAMEBUFFER); + checkStatus(); } diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp index 6948a045a2..36aaf75e81 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp @@ -8,9 +8,10 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#include "GL45Backend.h" +#include "GL45Backend.h" #include +#include #include #include #include @@ -19,142 +20,70 @@ #include #include +#include #include "../gl/GLTexelFormat.h" using namespace gpu; using namespace gpu::gl; using namespace gpu::gl45; -// Allocate 1 MB of buffer space for paged transfers -#define DEFAULT_PAGE_BUFFER_SIZE (1024*1024) -#define DEFAULT_GL_PIXEL_ALIGNMENT 4 - -using GL45Texture = GL45Backend::GL45Texture; - -static std::map> texturesByMipCounts; -static Mutex texturesByMipCountsMutex; -using TextureTypeFormat = std::pair; -std::map> sparsePageDimensionsByFormat; -Mutex sparsePageDimensionsByFormatMutex; - -static std::vector getPageDimensionsForFormat(const TextureTypeFormat& typeFormat) { - { - Lock lock(sparsePageDimensionsByFormatMutex); - if (sparsePageDimensionsByFormat.count(typeFormat)) { - return sparsePageDimensionsByFormat[typeFormat]; - } - } - GLint count = 0; - glGetInternalformativ(typeFormat.first, typeFormat.second, GL_NUM_VIRTUAL_PAGE_SIZES_ARB, 1, &count); - - std::vector result; - if (count > 0) { - std::vector x, y, z; - x.resize(count); - glGetInternalformativ(typeFormat.first, typeFormat.second, GL_VIRTUAL_PAGE_SIZE_X_ARB, 1, &x[0]); - y.resize(count); - glGetInternalformativ(typeFormat.first, typeFormat.second, GL_VIRTUAL_PAGE_SIZE_Y_ARB, 1, &y[0]); - z.resize(count); - glGetInternalformativ(typeFormat.first, typeFormat.second, GL_VIRTUAL_PAGE_SIZE_Z_ARB, 1, &z[0]); - - result.resize(count); - for (GLint i = 0; i < count; ++i) { - result[i] = uvec3(x[i], y[i], z[i]); - } - } - - { - Lock lock(sparsePageDimensionsByFormatMutex); - if (0 == sparsePageDimensionsByFormat.count(typeFormat)) { - sparsePageDimensionsByFormat[typeFormat] = result; - } - } - - return result; -} - -static std::vector getPageDimensionsForFormat(GLenum target, GLenum format) { - return getPageDimensionsForFormat({ target, format }); -} - -GLTexture* GL45Backend::syncGPUObject(const TexturePointer& texture, bool transfer) { - return GL45Texture::sync(*this, texture, transfer); -} - -using SparseInfo = GL45Backend::GL45Texture::SparseInfo; - -SparseInfo::SparseInfo(GL45Texture& texture) - : texture(texture) { -} - -void SparseInfo::maybeMakeSparse() { - // Don't enable sparse for objects with explicitly managed mip levels - if (!texture._gpuObject.isAutogenerateMips()) { - return; - } - return; - - const uvec3 dimensions = texture._gpuObject.getDimensions(); - auto allowedPageDimensions = getPageDimensionsForFormat(texture._target, texture._internalFormat); - // In order to enable sparse the texture size must be an integer multiple of the page size - for (size_t i = 0; i < allowedPageDimensions.size(); ++i) { - pageDimensionsIndex = (uint32_t) i; - pageDimensions = allowedPageDimensions[i]; - // Is this texture an integer multiple of page dimensions? - if (uvec3(0) == (dimensions % pageDimensions)) { - qCDebug(gpugl45logging) << "Enabling sparse for texture " << texture._source.c_str(); - sparse = true; - break; - } - } - - if (sparse) { - glTextureParameteri(texture._id, GL_TEXTURE_SPARSE_ARB, GL_TRUE); - glTextureParameteri(texture._id, GL_VIRTUAL_PAGE_SIZE_INDEX_ARB, pageDimensionsIndex); - } else { - qCDebug(gpugl45logging) << "Size " << dimensions.x << " x " << dimensions.y << - " is not supported by any sparse page size for texture" << texture._source.c_str(); - } -} - #define SPARSE_PAGE_SIZE_OVERHEAD_ESTIMATE 1.3f +#define MAX_RESOURCE_TEXTURES_PER_FRAME 2 -// This can only be called after we've established our storage size -void SparseInfo::update() { - if (!sparse) { - return; +GLTexture* GL45Backend::syncGPUObject(const TexturePointer& texturePointer) { + if (!texturePointer) { + return nullptr; } - glGetTextureParameterIuiv(texture._id, GL_NUM_SPARSE_LEVELS_ARB, &maxSparseLevel); - pageBytes = texture._gpuObject.getTexelFormat().getSize(); - pageBytes *= pageDimensions.x * pageDimensions.y * pageDimensions.z; - // Testing with a simple texture allocating app shows an estimated 20% GPU memory overhead for - // sparse textures as compared to non-sparse, so we acount for that here. - pageBytes = (uint32_t)(pageBytes * SPARSE_PAGE_SIZE_OVERHEAD_ESTIMATE); - for (uint16_t mipLevel = 0; mipLevel <= maxSparseLevel; ++mipLevel) { - auto mipDimensions = texture._gpuObject.evalMipDimensions(mipLevel); - auto mipPageCount = getPageCount(mipDimensions); - maxPages += mipPageCount; + const Texture& texture = *texturePointer; + if (TextureUsageType::EXTERNAL == texture.getUsageType()) { + return Parent::syncGPUObject(texturePointer); } - if (texture._target == GL_TEXTURE_CUBE_MAP) { - maxPages *= GLTexture::CUBE_NUM_FACES; + + if (!texture.isDefined()) { + // NO texture definition yet so let's avoid thinking + return nullptr; } -} -uvec3 SparseInfo::getPageCounts(const uvec3& dimensions) const { - auto result = (dimensions / pageDimensions) + - glm::clamp(dimensions % pageDimensions, glm::uvec3(0), glm::uvec3(1)); - return result; -} + GL45Texture* object = Backend::getGPUObject(texture); + if (!object) { + switch (texture.getUsageType()) { + case TextureUsageType::RENDERBUFFER: + object = new GL45AttachmentTexture(shared_from_this(), texture); + break; -uint32_t SparseInfo::getPageCount(const uvec3& dimensions) const { - auto pageCounts = getPageCounts(dimensions); - return pageCounts.x * pageCounts.y * pageCounts.z; -} + case TextureUsageType::STRICT_RESOURCE: + qCDebug(gpugllogging) << "Strict texture " << texture.source().c_str(); + object = new GL45StrictResourceTexture(shared_from_this(), texture); + break; + case TextureUsageType::RESOURCE: { + if (GL45VariableAllocationTexture::_frameTexturesCreated < MAX_RESOURCE_TEXTURES_PER_FRAME) { +#if 0 + if (isTextureManagementSparseEnabled() && GL45Texture::isSparseEligible(texture)) { + object = new GL45SparseResourceTexture(shared_from_this(), texture); + } else { + object = new GL45ResourceTexture(shared_from_this(), texture); + } +#else + object = new GL45ResourceTexture(shared_from_this(), texture); +#endif + GL45VariableAllocationTexture::addMemoryManagedTexture(texturePointer); + } else { + auto fallback = texturePointer->getFallbackTexture(); + if (fallback) { + object = static_cast(syncGPUObject(fallback)); + } + } + break; + } -uint32_t SparseInfo::getSize() const { - return allocatedPages * pageBytes; + default: + Q_UNREACHABLE(); + } + } + + return object; } void GL45Backend::initTextureManagementStage() { @@ -171,6 +100,12 @@ void GL45Backend::initTextureManagementStage() { } } +using GL45Texture = GL45Backend::GL45Texture; + +GL45Texture::GL45Texture(const std::weak_ptr& backend, const Texture& texture) + : GLTexture(backend, texture, allocate(texture)) { + incrementTextureGPUCount(); +} GLuint GL45Texture::allocate(const Texture& texture) { GLuint result; @@ -178,164 +113,43 @@ GLuint GL45Texture::allocate(const Texture& texture) { return result; } -GLuint GL45Backend::getTextureID(const TexturePointer& texture, bool transfer) { - return GL45Texture::getId(*this, texture, transfer); -} - -GL45Texture::GL45Texture(const std::weak_ptr& backend, const Texture& texture, GLuint externalId) - : GLTexture(backend, texture, externalId), _sparseInfo(*this) -{ -} - -GL45Texture::GL45Texture(const std::weak_ptr& backend, const Texture& texture, bool transferrable) - : GLTexture(backend, texture, allocate(texture), transferrable), _sparseInfo(*this) - { - - auto theBackend = _backend.lock(); - if (_transferrable && theBackend && theBackend->isTextureManagementSparseEnabled()) { - _sparseInfo.maybeMakeSparse(); - if (_sparseInfo.sparse) { - Backend::incrementTextureGPUSparseCount(); - } - } -} - -GL45Texture::~GL45Texture() { - // Remove this texture from the candidate list of derezzable textures - if (_transferrable) { - auto mipLevels = usedMipLevels(); - Lock lock(texturesByMipCountsMutex); - if (texturesByMipCounts.count(mipLevels)) { - auto& textures = texturesByMipCounts[mipLevels]; - textures.erase(this); - if (textures.empty()) { - texturesByMipCounts.erase(mipLevels); - } - } - } - - if (_sparseInfo.sparse) { - Backend::decrementTextureGPUSparseCount(); - - // Experimenation suggests that allocating sparse textures on one context/thread and deallocating - // them on another is buggy. So for sparse textures we need to queue a lambda with the deallocation - // callls to the transfer thread - auto id = _id; - // Set the class _id to 0 so we don't try to double delete - const_cast(_id) = 0; - std::list> destructionFunctions; - - uint8_t maxFace = (uint8_t)((_target == GL_TEXTURE_CUBE_MAP) ? GLTexture::CUBE_NUM_FACES : 1); - auto maxSparseMip = std::min(_maxMip, _sparseInfo.maxSparseLevel); - for (uint16_t mipLevel = _minMip; mipLevel <= maxSparseMip; ++mipLevel) { - auto mipDimensions = _gpuObject.evalMipDimensions(mipLevel); - destructionFunctions.push_back([id, maxFace, mipLevel, mipDimensions] { - glTexturePageCommitmentEXT(id, mipLevel, 0, 0, 0, mipDimensions.x, mipDimensions.y, maxFace, GL_FALSE); - }); - - auto deallocatedPages = _sparseInfo.getPageCount(mipDimensions) * maxFace; - assert(deallocatedPages <= _sparseInfo.allocatedPages); - _sparseInfo.allocatedPages -= deallocatedPages; - } - - if (0 != _sparseInfo.allocatedPages) { - qCWarning(gpugl45logging) << "Allocated pages remaining " << _id << " " << _sparseInfo.allocatedPages; - } - - auto size = _size; - const_cast(_size) = 0; - _textureTransferHelper->queueExecution([id, size, destructionFunctions] { - for (auto function : destructionFunctions) { - function(); - } - glDeleteTextures(1, &id); - Backend::decrementTextureGPUCount(); - Backend::updateTextureGPUMemoryUsage(size, 0); - Backend::updateTextureGPUSparseMemoryUsage(size, 0); - }); - } -} - -void GL45Texture::withPreservedTexture(std::function f) const { - f(); -} - void GL45Texture::generateMips() const { glGenerateTextureMipmap(_id); (void)CHECK_GL_ERROR(); } -void GL45Texture::allocateStorage() const { - if (_gpuObject.getTexelFormat().isCompressed()) { - qFatal("Compressed textures not yet supported"); +void GL45Texture::copyMipFaceLinesFromTexture(uint16_t mip, uint8_t face, const uvec3& size, uint32_t yOffset, GLenum format, GLenum type, const void* sourcePointer) const { + if (GL_TEXTURE_2D == _target) { + glTextureSubImage2D(_id, mip, 0, yOffset, size.x, size.y, format, type, sourcePointer); + } else if (GL_TEXTURE_CUBE_MAP == _target) { + // DSA ARB does not work on AMD, so use EXT + // unless EXT is not available on the driver + if (glTextureSubImage2DEXT) { + auto target = GLTexture::CUBE_FACE_LAYOUT[face]; + glTextureSubImage2DEXT(_id, target, mip, 0, yOffset, size.x, size.y, format, type, sourcePointer); + } else { + glTextureSubImage3D(_id, mip, 0, yOffset, face, size.x, size.y, 1, format, type, sourcePointer); + } + } else { + Q_ASSERT(false); } - glTextureParameteri(_id, GL_TEXTURE_BASE_LEVEL, 0); - glTextureParameteri(_id, GL_TEXTURE_MAX_LEVEL, _maxMip - _minMip); - // Get the dimensions, accounting for the downgrade level - Vec3u dimensions = _gpuObject.evalMipDimensions(_minMip + _mipOffset); - glTextureStorage2D(_id, usedMipLevels(), _internalFormat, dimensions.x, dimensions.y); (void)CHECK_GL_ERROR(); } -void GL45Texture::updateSize() const { - if (_gpuObject.getTexelFormat().isCompressed()) { - qFatal("Compressed textures not yet supported"); +void GL45Texture::copyMipFaceFromTexture(uint16_t sourceMip, uint16_t targetMip, uint8_t face) const { + if (!_gpuObject.isStoredMipFaceAvailable(sourceMip)) { + return; } - - if (_transferrable && _sparseInfo.sparse) { - auto size = _sparseInfo.getSize(); - Backend::updateTextureGPUSparseMemoryUsage(_size, size); - setSize(size); + auto size = _gpuObject.evalMipDimensions(sourceMip); + auto mipData = _gpuObject.accessStoredMipFace(sourceMip, face); + if (mipData) { + GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat(), _gpuObject.getStoredMipFormat()); + copyMipFaceLinesFromTexture(targetMip, face, size, 0, texelFormat.format, texelFormat.type, mipData->readData()); } else { - setSize(_gpuObject.evalTotalSize(_mipOffset)); + qCDebug(gpugllogging) << "Missing mipData level=" << sourceMip << " face=" << (int)face << " for texture " << _gpuObject.source().c_str(); } } -void GL45Texture::startTransfer() { - Parent::startTransfer(); - _sparseInfo.update(); -} - -bool GL45Texture::continueTransfer() { - PROFILE_RANGE(render_gpu_gl, "continueTransfer") - size_t maxFace = GL_TEXTURE_CUBE_MAP == _target ? CUBE_NUM_FACES : 1; - for (uint8_t face = 0; face < maxFace; ++face) { - for (uint16_t mipLevel = _minMip; mipLevel <= _maxMip; ++mipLevel) { - auto size = _gpuObject.evalMipDimensions(mipLevel); - if (_sparseInfo.sparse && mipLevel <= _sparseInfo.maxSparseLevel) { - glTexturePageCommitmentEXT(_id, mipLevel, 0, 0, face, size.x, size.y, 1, GL_TRUE); - _sparseInfo.allocatedPages += _sparseInfo.getPageCount(size); - } - if (_gpuObject.isStoredMipFaceAvailable(mipLevel, face)) { - PROFILE_RANGE_EX(render_gpu_gl, "texSubImage", 0x0000ffff, (size.x * size.y * maxFace / 1024)); - - auto mip = _gpuObject.accessStoredMipFace(mipLevel, face); - GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat(), mip->getFormat()); - if (GL_TEXTURE_2D == _target) { - glTextureSubImage2D(_id, mipLevel, 0, 0, size.x, size.y, texelFormat.format, texelFormat.type, mip->readData()); - } else if (GL_TEXTURE_CUBE_MAP == _target) { - // DSA ARB does not work on AMD, so use EXT - // unless EXT is not available on the driver - if (glTextureSubImage2DEXT) { - auto target = CUBE_FACE_LAYOUT[face]; - glTextureSubImage2DEXT(_id, target, mipLevel, 0, 0, size.x, size.y, texelFormat.format, texelFormat.type, mip->readData()); - } else { - glTextureSubImage3D(_id, mipLevel, 0, 0, face, size.x, size.y, 1, texelFormat.format, texelFormat.type, mip->readData()); - } - } else { - Q_ASSERT(false); - } - (void)CHECK_GL_ERROR(); - } - } - } - return false; -} - -void GL45Texture::finishTransfer() { - Parent::finishTransfer(); -} - void GL45Texture::syncSampler() const { const Sampler& sampler = _gpuObject.getSampler(); @@ -353,163 +167,63 @@ void GL45Texture::syncSampler() const { glTextureParameteri(_id, GL_TEXTURE_WRAP_S, WRAP_MODES[sampler.getWrapModeU()]); glTextureParameteri(_id, GL_TEXTURE_WRAP_T, WRAP_MODES[sampler.getWrapModeV()]); glTextureParameteri(_id, GL_TEXTURE_WRAP_R, WRAP_MODES[sampler.getWrapModeW()]); + glTextureParameterf(_id, GL_TEXTURE_MAX_ANISOTROPY_EXT, sampler.getMaxAnisotropy()); glTextureParameterfv(_id, GL_TEXTURE_BORDER_COLOR, (const float*)&sampler.getBorderColor()); - // FIXME account for mip offsets here - auto baseMip = std::max(sampler.getMipOffset(), _minMip); + glTextureParameterf(_id, GL_TEXTURE_MIN_LOD, sampler.getMinMip()); + glTextureParameterf(_id, GL_TEXTURE_MAX_LOD, (sampler.getMaxMip() == Sampler::MAX_MIP_LEVEL ? 1000.f : sampler.getMaxMip())); +} + +using GL45FixedAllocationTexture = GL45Backend::GL45FixedAllocationTexture; + +GL45FixedAllocationTexture::GL45FixedAllocationTexture(const std::weak_ptr& backend, const Texture& texture) : GL45Texture(backend, texture), _size(texture.evalTotalSize()) { + allocateStorage(); + syncSampler(); +} + +GL45FixedAllocationTexture::~GL45FixedAllocationTexture() { +} + +void GL45FixedAllocationTexture::allocateStorage() const { + const GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat()); + const auto dimensions = _gpuObject.getDimensions(); + const auto mips = _gpuObject.evalNumMips(); + glTextureStorage2D(_id, mips, texelFormat.internalFormat, dimensions.x, dimensions.y); +} + +void GL45FixedAllocationTexture::syncSampler() const { + Parent::syncSampler(); + const Sampler& sampler = _gpuObject.getSampler(); + auto baseMip = std::max(sampler.getMipOffset(), sampler.getMinMip()); glTextureParameteri(_id, GL_TEXTURE_BASE_LEVEL, baseMip); glTextureParameterf(_id, GL_TEXTURE_MIN_LOD, (float)sampler.getMinMip()); - glTextureParameterf(_id, GL_TEXTURE_MAX_LOD, (sampler.getMaxMip() == Sampler::MAX_MIP_LEVEL ? 1000.f : sampler.getMaxMip() - _mipOffset)); - glTextureParameterf(_id, GL_TEXTURE_MAX_ANISOTROPY_EXT, sampler.getMaxAnisotropy()); + glTextureParameterf(_id, GL_TEXTURE_MAX_LOD, (sampler.getMaxMip() == Sampler::MAX_MIP_LEVEL ? 1000.f : sampler.getMaxMip())); } -void GL45Texture::postTransfer() { - Parent::postTransfer(); - auto mipLevels = usedMipLevels(); - if (_transferrable && mipLevels > 1 && _minMip < _sparseInfo.maxSparseLevel) { - Lock lock(texturesByMipCountsMutex); - texturesByMipCounts[mipLevels].insert(this); - } +// Renderbuffer attachment textures +using GL45AttachmentTexture = GL45Backend::GL45AttachmentTexture; + +GL45AttachmentTexture::GL45AttachmentTexture(const std::weak_ptr& backend, const Texture& texture) : GL45FixedAllocationTexture(backend, texture) { + Backend::updateTextureGPUFramebufferMemoryUsage(0, size()); } -void GL45Texture::stripToMip(uint16_t newMinMip) { - if (newMinMip < _minMip) { - qCWarning(gpugl45logging) << "Cannot decrease the min mip"; - return; - } +GL45AttachmentTexture::~GL45AttachmentTexture() { + Backend::updateTextureGPUFramebufferMemoryUsage(size(), 0); +} - if (_sparseInfo.sparse && newMinMip > _sparseInfo.maxSparseLevel) { - qCWarning(gpugl45logging) << "Cannot increase the min mip into the mip tail"; - return; - } +// Strict resource textures +using GL45StrictResourceTexture = GL45Backend::GL45StrictResourceTexture; - PROFILE_RANGE(render_gpu_gl, "GL45Texture::stripToMip"); - - auto mipLevels = usedMipLevels(); - { - Lock lock(texturesByMipCountsMutex); - assert(0 != texturesByMipCounts.count(mipLevels)); - assert(0 != texturesByMipCounts[mipLevels].count(this)); - texturesByMipCounts[mipLevels].erase(this); - if (texturesByMipCounts[mipLevels].empty()) { - texturesByMipCounts.erase(mipLevels); +GL45StrictResourceTexture::GL45StrictResourceTexture(const std::weak_ptr& backend, const Texture& texture) : GL45FixedAllocationTexture(backend, texture) { + auto mipLevels = _gpuObject.evalNumMips(); + for (uint16_t sourceMip = 0; sourceMip < mipLevels; ++sourceMip) { + uint16_t targetMip = sourceMip; + size_t maxFace = GLTexture::getFaceCount(_target); + for (uint8_t face = 0; face < maxFace; ++face) { + copyMipFaceFromTexture(sourceMip, targetMip, face); } } - - // If we weren't generating mips before, we need to now that we're stripping down mip levels. - if (!_gpuObject.isAutogenerateMips()) { - qCDebug(gpugl45logging) << "Force mip generation for texture"; - glGenerateTextureMipmap(_id); - } - - - uint8_t maxFace = (uint8_t)((_target == GL_TEXTURE_CUBE_MAP) ? GLTexture::CUBE_NUM_FACES : 1); - if (_sparseInfo.sparse) { - for (uint16_t mip = _minMip; mip < newMinMip; ++mip) { - auto id = _id; - auto mipDimensions = _gpuObject.evalMipDimensions(mip); - _textureTransferHelper->queueExecution([id, mip, mipDimensions, maxFace] { - glTexturePageCommitmentEXT(id, mip, 0, 0, 0, mipDimensions.x, mipDimensions.y, maxFace, GL_FALSE); - }); - - auto deallocatedPages = _sparseInfo.getPageCount(mipDimensions) * maxFace; - assert(deallocatedPages < _sparseInfo.allocatedPages); - _sparseInfo.allocatedPages -= deallocatedPages; - } - _minMip = newMinMip; - } else { - GLuint oldId = _id; - // Find the distance between the old min mip and the new one - uint16 mipDelta = newMinMip - _minMip; - _mipOffset += mipDelta; - const_cast(_maxMip) -= mipDelta; - auto newLevels = usedMipLevels(); - - // Create and setup the new texture (allocate) - { - Vec3u newDimensions = _gpuObject.evalMipDimensions(_mipOffset); - PROFILE_RANGE_EX(render_gpu_gl, "Re-Allocate", 0xff0000ff, (newDimensions.x * newDimensions.y)); - - glCreateTextures(_target, 1, &const_cast(_id)); - glTextureParameteri(_id, GL_TEXTURE_BASE_LEVEL, 0); - glTextureParameteri(_id, GL_TEXTURE_MAX_LEVEL, _maxMip - _minMip); - glTextureStorage2D(_id, newLevels, _internalFormat, newDimensions.x, newDimensions.y); - } - - // Copy the contents of the old texture to the new - { - PROFILE_RANGE(render_gpu_gl, "Blit"); - // Preferred path only available in 4.3 - for (uint16 targetMip = _minMip; targetMip <= _maxMip; ++targetMip) { - uint16 sourceMip = targetMip + mipDelta; - Vec3u mipDimensions = _gpuObject.evalMipDimensions(targetMip + _mipOffset); - for (GLenum target : getFaceTargets(_target)) { - glCopyImageSubData( - oldId, target, sourceMip, 0, 0, 0, - _id, target, targetMip, 0, 0, 0, - mipDimensions.x, mipDimensions.y, 1 - ); - (void)CHECK_GL_ERROR(); - } - } - - glDeleteTextures(1, &oldId); - } - } - - // Re-sync the sampler to force access to the new mip level - syncSampler(); - updateSize(); - - // Re-insert into the texture-by-mips map if appropriate - mipLevels = usedMipLevels(); - if (mipLevels > 1 && (!_sparseInfo.sparse || _minMip < _sparseInfo.maxSparseLevel)) { - Lock lock(texturesByMipCountsMutex); - texturesByMipCounts[mipLevels].insert(this); + if (texture.isAutogenerateMips()) { + generateMips(); } } -void GL45Texture::updateMips() { - if (!_sparseInfo.sparse) { - return; - } - auto newMinMip = std::min(_gpuObject.minMip(), _sparseInfo.maxSparseLevel); - if (_minMip < newMinMip) { - stripToMip(newMinMip); - } -} - -void GL45Texture::derez() { - if (_sparseInfo.sparse) { - assert(_minMip < _sparseInfo.maxSparseLevel); - } - assert(_minMip < _maxMip); - assert(_transferrable); - stripToMip(_minMip + 1); -} - -void GL45Backend::derezTextures() const { - if (GLTexture::getMemoryPressure() < 1.0f) { - return; - } - - Lock lock(texturesByMipCountsMutex); - if (texturesByMipCounts.empty()) { - // No available textures to derez - return; - } - - auto mipLevel = texturesByMipCounts.rbegin()->first; - if (mipLevel <= 1) { - // No mips available to remove - return; - } - - GL45Texture* targetTexture = nullptr; - { - auto& textures = texturesByMipCounts[mipLevel]; - assert(!textures.empty()); - targetTexture = *textures.begin(); - } - lock.unlock(); - targetTexture->derez(); -} diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendVariableTexture.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendVariableTexture.cpp new file mode 100644 index 0000000000..d54ad1ea4b --- /dev/null +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendVariableTexture.cpp @@ -0,0 +1,1033 @@ +// +// GL45BackendTexture.cpp +// libraries/gpu/src/gpu +// +// Created by Sam Gateau on 1/19/2015. +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "GL45Backend.h" +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include "../gl/GLTexelFormat.h" + +using namespace gpu; +using namespace gpu::gl; +using namespace gpu::gl45; + +// Variable sized textures +using GL45VariableAllocationTexture = GL45Backend::GL45VariableAllocationTexture; +using MemoryPressureState = GL45VariableAllocationTexture::MemoryPressureState; +using WorkQueue = GL45VariableAllocationTexture::WorkQueue; + +std::list GL45VariableAllocationTexture::_memoryManagedTextures; +MemoryPressureState GL45VariableAllocationTexture::_memoryPressureState = MemoryPressureState::Idle; +std::atomic GL45VariableAllocationTexture::_memoryPressureStateStale { false }; +const uvec3 GL45VariableAllocationTexture::INITIAL_MIP_TRANSFER_DIMENSIONS { 64, 64, 1 }; +WorkQueue GL45VariableAllocationTexture::_transferQueue; +WorkQueue GL45VariableAllocationTexture::_promoteQueue; +WorkQueue GL45VariableAllocationTexture::_demoteQueue; +TexturePointer GL45VariableAllocationTexture::_currentTransferTexture; + +#define OVERSUBSCRIBED_PRESSURE_VALUE 0.95f +#define UNDERSUBSCRIBED_PRESSURE_VALUE 0.85f +#define DEFAULT_ALLOWED_TEXTURE_MEMORY_MB ((size_t)1024) + +static const size_t DEFAULT_ALLOWED_TEXTURE_MEMORY = MB_TO_BYTES(DEFAULT_ALLOWED_TEXTURE_MEMORY_MB); + +using TransferJob = GL45VariableAllocationTexture::TransferJob; + +static const uvec3 MAX_TRANSFER_DIMENSIONS { 1024, 1024, 1 }; +static const size_t MAX_TRANSFER_SIZE = MAX_TRANSFER_DIMENSIONS.x * MAX_TRANSFER_DIMENSIONS.y * 4; + +#if THREADED_TEXTURE_BUFFERING +std::shared_ptr TransferJob::_bufferThread { nullptr }; +std::atomic TransferJob::_shutdownBufferingThread { false }; +Mutex TransferJob::_mutex; +TransferJob::VoidLambdaQueue TransferJob::_bufferLambdaQueue; + +void TransferJob::startTransferLoop() { + if (_bufferThread) { + return; + } + _shutdownBufferingThread = false; + _bufferThread = std::make_shared([] { + TransferJob::bufferLoop(); + }); +} + +void TransferJob::stopTransferLoop() { + if (!_bufferThread) { + return; + } + _shutdownBufferingThread = true; + _bufferThread->join(); + _bufferThread.reset(); + _shutdownBufferingThread = false; +} +#endif + +TransferJob::TransferJob(const GL45VariableAllocationTexture& parent, uint16_t sourceMip, uint16_t targetMip, uint8_t face, uint32_t lines, uint32_t lineOffset) + : _parent(parent) { + + auto transferDimensions = _parent._gpuObject.evalMipDimensions(sourceMip); + GLenum format; + GLenum type; + auto mipData = _parent._gpuObject.accessStoredMipFace(sourceMip, face); + GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_parent._gpuObject.getTexelFormat(), _parent._gpuObject.getStoredMipFormat()); + format = texelFormat.format; + type = texelFormat.type; + + if (0 == lines) { + _transferSize = mipData->getSize(); + _bufferingLambda = [=] { + _buffer.resize(_transferSize); + memcpy(&_buffer[0], mipData->readData(), _transferSize); + _bufferingCompleted = true; + }; + + } else { + transferDimensions.y = lines; + auto dimensions = _parent._gpuObject.evalMipDimensions(sourceMip); + auto mipSize = mipData->getSize(); + auto bytesPerLine = (uint32_t)mipSize / dimensions.y; + _transferSize = bytesPerLine * lines; + auto sourceOffset = bytesPerLine * lineOffset; + _bufferingLambda = [=] { + _buffer.resize(_transferSize); + memcpy(&_buffer[0], mipData->readData() + sourceOffset, _transferSize); + _bufferingCompleted = true; + }; + } + + Backend::updateTextureTransferPendingSize(0, _transferSize); + + _transferLambda = [=] { + _parent.copyMipFaceLinesFromTexture(targetMip, face, transferDimensions, lineOffset, format, type, _buffer.data()); + std::vector emptyVector; + _buffer.swap(emptyVector); + }; +} + +TransferJob::TransferJob(const GL45VariableAllocationTexture& parent, std::function transferLambda) + : _parent(parent), _bufferingCompleted(true), _transferLambda(transferLambda) { +} + +TransferJob::~TransferJob() { + Backend::updateTextureTransferPendingSize(_transferSize, 0); +} + + +bool TransferJob::tryTransfer() { + // Disable threaded texture transfer for now +#if THREADED_TEXTURE_BUFFERING + // Are we ready to transfer + if (_bufferingCompleted) { + _transferLambda(); + return true; + } + + startBuffering(); + return false; +#else + if (!_bufferingCompleted) { + _bufferingLambda(); + _bufferingCompleted = true; + } + _transferLambda(); + return true; +#endif +} + +#if THREADED_TEXTURE_BUFFERING + +void TransferJob::startBuffering() { + if (_bufferingStarted) { + return; + } + _bufferingStarted = true; + { + Lock lock(_mutex); + _bufferLambdaQueue.push(_bufferingLambda); + } +} + +void TransferJob::bufferLoop() { + while (!_shutdownBufferingThread) { + VoidLambdaQueue workingQueue; + { + Lock lock(_mutex); + _bufferLambdaQueue.swap(workingQueue); + } + + if (workingQueue.empty()) { + QThread::msleep(5); + continue; + } + + while (!workingQueue.empty()) { + workingQueue.front()(); + workingQueue.pop(); + } + } +} +#endif + + +void GL45VariableAllocationTexture::addMemoryManagedTexture(const TexturePointer& texturePointer) { + _memoryManagedTextures.push_back(texturePointer); + addToWorkQueue(texturePointer); +} + +void GL45VariableAllocationTexture::addToWorkQueue(const TexturePointer& texturePointer) { + GL45VariableAllocationTexture* object = Backend::getGPUObject(*texturePointer); + switch (_memoryPressureState) { + case MemoryPressureState::Oversubscribed: + if (object->canDemote()) { + // Demote largest first + _demoteQueue.push({ texturePointer, (float)object->size() }); + } + break; + + case MemoryPressureState::Undersubscribed: + if (object->canPromote()) { + // Promote smallest first + _promoteQueue.push({ texturePointer, 1.0f / (float)object->size() }); + } + break; + + case MemoryPressureState::Transfer: + if (object->hasPendingTransfers()) { + // Transfer priority given to smaller mips first + _transferQueue.push({ texturePointer, 1.0f / (float)object->_gpuObject.evalMipSize(object->_populatedMip) }); + } + break; + + case MemoryPressureState::Idle: + break; + + default: + Q_UNREACHABLE(); + } +} + +WorkQueue& GL45VariableAllocationTexture::getActiveWorkQueue() { + static WorkQueue empty; + switch (_memoryPressureState) { + case MemoryPressureState::Oversubscribed: + return _demoteQueue; + + case MemoryPressureState::Undersubscribed: + return _promoteQueue; + + case MemoryPressureState::Transfer: + return _transferQueue; + + default: + break; + } + Q_UNREACHABLE(); + return empty; +} + +// FIXME hack for stats display +QString getTextureMemoryPressureModeString() { + switch (GL45VariableAllocationTexture::_memoryPressureState) { + case MemoryPressureState::Oversubscribed: + return "Oversubscribed"; + + case MemoryPressureState::Undersubscribed: + return "Undersubscribed"; + + case MemoryPressureState::Transfer: + return "Transfer"; + + case MemoryPressureState::Idle: + return "Idle"; + } + Q_UNREACHABLE(); + return "Unknown"; +} + +void GL45VariableAllocationTexture::updateMemoryPressure() { + static size_t lastAllowedMemoryAllocation = gpu::Texture::getAllowedGPUMemoryUsage(); + + size_t allowedMemoryAllocation = gpu::Texture::getAllowedGPUMemoryUsage(); + if (0 == allowedMemoryAllocation) { + allowedMemoryAllocation = DEFAULT_ALLOWED_TEXTURE_MEMORY; + } + + // If the user explicitly changed the allowed memory usage, we need to mark ourselves stale + // so that we react + if (allowedMemoryAllocation != lastAllowedMemoryAllocation) { + _memoryPressureStateStale = true; + lastAllowedMemoryAllocation = allowedMemoryAllocation; + } + + if (!_memoryPressureStateStale.exchange(false)) { + return; + } + + PROFILE_RANGE(render_gpu_gl, __FUNCTION__); + + // Clear any defunct textures (weak pointers that no longer have a valid texture) + _memoryManagedTextures.remove_if([&](const TextureWeakPointer& weakPointer) { + return weakPointer.expired(); + }); + + // Convert weak pointers to strong. This new list may still contain nulls if a texture was + // deleted on another thread between the previous line and this one + std::vector strongTextures; { + strongTextures.reserve(_memoryManagedTextures.size()); + std::transform( + _memoryManagedTextures.begin(), _memoryManagedTextures.end(), + std::back_inserter(strongTextures), + [](const TextureWeakPointer& p) { return p.lock(); }); + } + + size_t totalVariableMemoryAllocation = 0; + size_t idealMemoryAllocation = 0; + bool canDemote = false; + bool canPromote = false; + bool hasTransfers = false; + for (const auto& texture : strongTextures) { + // Race conditions can still leave nulls in the list, so we need to check + if (!texture) { + continue; + } + GL45VariableAllocationTexture* object = Backend::getGPUObject(*texture); + // Track how much the texture thinks it should be using + idealMemoryAllocation += texture->evalTotalSize(); + // Track how much we're actually using + totalVariableMemoryAllocation += object->size(); + canDemote |= object->canDemote(); + canPromote |= object->canPromote(); + hasTransfers |= object->hasPendingTransfers(); + } + + size_t unallocated = idealMemoryAllocation - totalVariableMemoryAllocation; + float pressure = (float)totalVariableMemoryAllocation / (float)allowedMemoryAllocation; + + auto newState = MemoryPressureState::Idle; + if (pressure > OVERSUBSCRIBED_PRESSURE_VALUE && canDemote) { + newState = MemoryPressureState::Oversubscribed; + } else if (pressure < UNDERSUBSCRIBED_PRESSURE_VALUE && unallocated != 0 && canPromote) { + newState = MemoryPressureState::Undersubscribed; + } else if (hasTransfers) { + newState = MemoryPressureState::Transfer; + } + + if (newState != _memoryPressureState) { +#if THREADED_TEXTURE_BUFFERING + if (MemoryPressureState::Transfer == _memoryPressureState) { + TransferJob::stopTransferLoop(); + } + _memoryPressureState = newState; + if (MemoryPressureState::Transfer == _memoryPressureState) { + TransferJob::startTransferLoop(); + } +#else + _memoryPressureState = newState; +#endif + // Clear the existing queue + _transferQueue = WorkQueue(); + _promoteQueue = WorkQueue(); + _demoteQueue = WorkQueue(); + + // Populate the existing textures into the queue + for (const auto& texture : strongTextures) { + addToWorkQueue(texture); + } + } +} + +void GL45VariableAllocationTexture::processWorkQueues() { + if (MemoryPressureState::Idle == _memoryPressureState) { + return; + } + + auto& workQueue = getActiveWorkQueue(); + PROFILE_RANGE(render_gpu_gl, __FUNCTION__); + while (!workQueue.empty()) { + auto workTarget = workQueue.top(); + workQueue.pop(); + auto texture = workTarget.first.lock(); + if (!texture) { + continue; + } + + // Grab the first item off the demote queue + GL45VariableAllocationTexture* object = Backend::getGPUObject(*texture); + if (MemoryPressureState::Oversubscribed == _memoryPressureState) { + if (!object->canDemote()) { + continue; + } + object->demote(); + } else if (MemoryPressureState::Undersubscribed == _memoryPressureState) { + if (!object->canPromote()) { + continue; + } + object->promote(); + } else if (MemoryPressureState::Transfer == _memoryPressureState) { + if (!object->hasPendingTransfers()) { + continue; + } + object->executeNextTransfer(texture); + } else { + Q_UNREACHABLE(); + } + + // Reinject into the queue if more work to be done + addToWorkQueue(texture); + break; + } + + if (workQueue.empty()) { + _memoryPressureStateStale = true; + } +} + +void GL45VariableAllocationTexture::manageMemory() { + PROFILE_RANGE(render_gpu_gl, __FUNCTION__); + updateMemoryPressure(); + processWorkQueues(); +} + +size_t GL45VariableAllocationTexture::_frameTexturesCreated { 0 }; + +GL45VariableAllocationTexture::GL45VariableAllocationTexture(const std::weak_ptr& backend, const Texture& texture) : GL45Texture(backend, texture) { + ++_frameTexturesCreated; +} + +GL45VariableAllocationTexture::~GL45VariableAllocationTexture() { + _memoryPressureStateStale = true; + Backend::updateTextureGPUMemoryUsage(_size, 0); +} + +void GL45VariableAllocationTexture::executeNextTransfer(const TexturePointer& currentTexture) { + if (_populatedMip <= _allocatedMip) { + return; + } + + if (_pendingTransfers.empty()) { + populateTransferQueue(); + } + + if (!_pendingTransfers.empty()) { + // Keeping hold of a strong pointer during the transfer ensures that the transfer thread cannot try to access a destroyed texture + _currentTransferTexture = currentTexture; + if (_pendingTransfers.front()->tryTransfer()) { + _pendingTransfers.pop(); + _currentTransferTexture.reset(); + } + } +} + +// Managed size resource textures +using GL45ResourceTexture = GL45Backend::GL45ResourceTexture; + +GL45ResourceTexture::GL45ResourceTexture(const std::weak_ptr& backend, const Texture& texture) : GL45VariableAllocationTexture(backend, texture) { + auto mipLevels = texture.evalNumMips(); + _allocatedMip = mipLevels; + uvec3 mipDimensions; + for (uint16_t mip = 0; mip < mipLevels; ++mip) { + if (glm::all(glm::lessThanEqual(texture.evalMipDimensions(mip), INITIAL_MIP_TRANSFER_DIMENSIONS))) { + _maxAllocatedMip = _populatedMip = mip; + break; + } + } + + uint16_t allocatedMip = _populatedMip - std::min(_populatedMip, 2); + allocateStorage(allocatedMip); + _memoryPressureStateStale = true; + copyMipsFromTexture(); + syncSampler(); + +} + +void GL45ResourceTexture::allocateStorage(uint16 allocatedMip) { + _allocatedMip = allocatedMip; + const GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat()); + const auto dimensions = _gpuObject.evalMipDimensions(_allocatedMip); + const auto totalMips = _gpuObject.evalNumMips(); + const auto mips = totalMips - _allocatedMip; + glTextureStorage2D(_id, mips, texelFormat.internalFormat, dimensions.x, dimensions.y); + auto mipLevels = _gpuObject.evalNumMips(); + _size = 0; + for (uint16_t mip = _allocatedMip; mip < mipLevels; ++mip) { + _size += _gpuObject.evalMipSize(mip); + } + Backend::updateTextureGPUMemoryUsage(0, _size); + +} + +void GL45ResourceTexture::copyMipsFromTexture() { + auto mipLevels = _gpuObject.evalNumMips(); + size_t maxFace = GLTexture::getFaceCount(_target); + for (uint16_t sourceMip = _populatedMip; sourceMip < mipLevels; ++sourceMip) { + uint16_t targetMip = sourceMip - _allocatedMip; + for (uint8_t face = 0; face < maxFace; ++face) { + copyMipFaceFromTexture(sourceMip, targetMip, face); + } + } +} + +void GL45ResourceTexture::syncSampler() const { + Parent::syncSampler(); + glTextureParameteri(_id, GL_TEXTURE_BASE_LEVEL, _populatedMip - _allocatedMip); +} + +void GL45ResourceTexture::promote() { + PROFILE_RANGE(render_gpu_gl, __FUNCTION__); + Q_ASSERT(_allocatedMip > 0); + GLuint oldId = _id; + uint32_t oldSize = _size; + // create new texture + const_cast(_id) = allocate(_gpuObject); + uint16_t oldAllocatedMip = _allocatedMip; + // allocate storage for new level + allocateStorage(_allocatedMip - std::min(_allocatedMip, 2)); + uint16_t mips = _gpuObject.evalNumMips(); + // copy pre-existing mips + for (uint16_t mip = _populatedMip; mip < mips; ++mip) { + auto mipDimensions = _gpuObject.evalMipDimensions(mip); + uint16_t targetMip = mip - _allocatedMip; + uint16_t sourceMip = mip - oldAllocatedMip; + auto faces = getFaceCount(_target); + for (uint8_t face = 0; face < faces; ++face) { + glCopyImageSubData( + oldId, _target, sourceMip, 0, 0, face, + _id, _target, targetMip, 0, 0, face, + mipDimensions.x, mipDimensions.y, 1 + ); + (void)CHECK_GL_ERROR(); + } + } + // destroy the old texture + glDeleteTextures(1, &oldId); + // update the memory usage + Backend::updateTextureGPUMemoryUsage(oldSize, 0); + _memoryPressureStateStale = true; + syncSampler(); + populateTransferQueue(); +} + +void GL45ResourceTexture::demote() { + PROFILE_RANGE(render_gpu_gl, __FUNCTION__); + Q_ASSERT(_allocatedMip < _maxAllocatedMip); + auto oldId = _id; + auto oldSize = _size; + const_cast(_id) = allocate(_gpuObject); + allocateStorage(_allocatedMip + 1); + _populatedMip = std::max(_populatedMip, _allocatedMip); + uint16_t mips = _gpuObject.evalNumMips(); + // copy pre-existing mips + for (uint16_t mip = _populatedMip; mip < mips; ++mip) { + auto mipDimensions = _gpuObject.evalMipDimensions(mip); + uint16_t targetMip = mip - _allocatedMip; + uint16_t sourceMip = targetMip + 1; + auto faces = getFaceCount(_target); + for (uint8_t face = 0; face < faces; ++face) { + glCopyImageSubData( + oldId, _target, sourceMip, 0, 0, face, + _id, _target, targetMip, 0, 0, face, + mipDimensions.x, mipDimensions.y, 1 + ); + (void)CHECK_GL_ERROR(); + } + } + // destroy the old texture + glDeleteTextures(1, &oldId); + // update the memory usage + Backend::updateTextureGPUMemoryUsage(oldSize, 0); + _memoryPressureStateStale = true; + syncSampler(); + populateTransferQueue(); +} + + +void GL45ResourceTexture::populateTransferQueue() { + PROFILE_RANGE(render_gpu_gl, __FUNCTION__); + if (_populatedMip <= _allocatedMip) { + return; + } + _pendingTransfers = TransferQueue(); + + const uint8_t maxFace = GLTexture::getFaceCount(_target); + uint16_t sourceMip = _populatedMip; + do { + --sourceMip; + auto targetMip = sourceMip - _allocatedMip; + auto mipDimensions = _gpuObject.evalMipDimensions(sourceMip); + for (uint8_t face = 0; face < maxFace; ++face) { + if (!_gpuObject.isStoredMipFaceAvailable(sourceMip, face)) { + continue; + } + + // If the mip is less than the max transfer size, then just do it in one transfer + if (glm::all(glm::lessThanEqual(mipDimensions, MAX_TRANSFER_DIMENSIONS))) { + // Can the mip be transferred in one go + _pendingTransfers.emplace(new TransferJob(*this, sourceMip, targetMip, face)); + continue; + } + + // break down the transfers into chunks so that no single transfer is + // consuming more than X bandwidth + auto mipData = _gpuObject.accessStoredMipFace(sourceMip, face); + const auto lines = mipDimensions.y; + auto bytesPerLine = (uint32_t)mipData->getSize() / lines; + Q_ASSERT(0 == (mipData->getSize() % lines)); + uint32_t linesPerTransfer = (uint32_t)(MAX_TRANSFER_SIZE / bytesPerLine); + uint32_t lineOffset = 0; + while (lineOffset < lines) { + uint32_t linesToCopy = std::min(lines - lineOffset, linesPerTransfer); + _pendingTransfers.emplace(new TransferJob(*this, sourceMip, targetMip, face, linesToCopy, lineOffset)); + lineOffset += linesToCopy; + } + } + + // queue up the sampler and populated mip change for after the transfer has completed + _pendingTransfers.emplace(new TransferJob(*this, [=] { + _populatedMip = sourceMip; + syncSampler(); + })); + } while (sourceMip != _allocatedMip); +} + +// Sparsely allocated, managed size resource textures +#if 0 +#define SPARSE_PAGE_SIZE_OVERHEAD_ESTIMATE 1.3f + +using GL45SparseResourceTexture = GL45Backend::GL45SparseResourceTexture; + +GL45Texture::PageDimensionsMap GL45Texture::pageDimensionsByFormat; +Mutex GL45Texture::pageDimensionsMutex; + +GL45Texture::PageDimensions GL45Texture::getPageDimensionsForFormat(const TextureTypeFormat& typeFormat) { + { + Lock lock(pageDimensionsMutex); + if (pageDimensionsByFormat.count(typeFormat)) { + return pageDimensionsByFormat[typeFormat]; + } + } + + GLint count = 0; + glGetInternalformativ(typeFormat.first, typeFormat.second, GL_NUM_VIRTUAL_PAGE_SIZES_ARB, 1, &count); + + std::vector result; + if (count > 0) { + std::vector x, y, z; + x.resize(count); + glGetInternalformativ(typeFormat.first, typeFormat.second, GL_VIRTUAL_PAGE_SIZE_X_ARB, 1, &x[0]); + y.resize(count); + glGetInternalformativ(typeFormat.first, typeFormat.second, GL_VIRTUAL_PAGE_SIZE_Y_ARB, 1, &y[0]); + z.resize(count); + glGetInternalformativ(typeFormat.first, typeFormat.second, GL_VIRTUAL_PAGE_SIZE_Z_ARB, 1, &z[0]); + + result.resize(count); + for (GLint i = 0; i < count; ++i) { + result[i] = uvec3(x[i], y[i], z[i]); + } + } + + { + Lock lock(pageDimensionsMutex); + if (0 == pageDimensionsByFormat.count(typeFormat)) { + pageDimensionsByFormat[typeFormat] = result; + } + } + + return result; +} + +GL45Texture::PageDimensions GL45Texture::getPageDimensionsForFormat(GLenum target, GLenum format) { + return getPageDimensionsForFormat({ target, format }); +} +bool GL45Texture::isSparseEligible(const Texture& texture) { + Q_ASSERT(TextureUsageType::RESOURCE == texture.getUsageType()); + + // Disabling sparse for the momemnt + return false; + + const auto allowedPageDimensions = getPageDimensionsForFormat(getGLTextureType(texture), + gl::GLTexelFormat::evalGLTexelFormatInternal(texture.getTexelFormat())); + const auto textureDimensions = texture.getDimensions(); + for (const auto& pageDimensions : allowedPageDimensions) { + if (uvec3(0) == (textureDimensions % pageDimensions)) { + return true; + } + } + + return false; +} + + +GL45SparseResourceTexture::GL45SparseResourceTexture(const std::weak_ptr& backend, const Texture& texture) : GL45VariableAllocationTexture(backend, texture) { + const GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat()); + const uvec3 dimensions = _gpuObject.getDimensions(); + auto allowedPageDimensions = getPageDimensionsForFormat(_target, texelFormat.internalFormat); + uint32_t pageDimensionsIndex = 0; + // In order to enable sparse the texture size must be an integer multiple of the page size + for (size_t i = 0; i < allowedPageDimensions.size(); ++i) { + pageDimensionsIndex = (uint32_t)i; + _pageDimensions = allowedPageDimensions[i]; + // Is this texture an integer multiple of page dimensions? + if (uvec3(0) == (dimensions % _pageDimensions)) { + qCDebug(gpugl45logging) << "Enabling sparse for texture " << _gpuObject.source().c_str(); + break; + } + } + glTextureParameteri(_id, GL_TEXTURE_SPARSE_ARB, GL_TRUE); + glTextureParameteri(_id, GL_VIRTUAL_PAGE_SIZE_INDEX_ARB, pageDimensionsIndex); + glGetTextureParameterIuiv(_id, GL_NUM_SPARSE_LEVELS_ARB, &_maxSparseLevel); + + _pageBytes = _gpuObject.getTexelFormat().getSize(); + _pageBytes *= _pageDimensions.x * _pageDimensions.y * _pageDimensions.z; + // Testing with a simple texture allocating app shows an estimated 20% GPU memory overhead for + // sparse textures as compared to non-sparse, so we acount for that here. + _pageBytes = (uint32_t)(_pageBytes * SPARSE_PAGE_SIZE_OVERHEAD_ESTIMATE); + + //allocateStorage(); + syncSampler(); +} + +GL45SparseResourceTexture::~GL45SparseResourceTexture() { + Backend::updateTextureGPUVirtualMemoryUsage(size(), 0); +} + +uvec3 GL45SparseResourceTexture::getPageCounts(const uvec3& dimensions) const { + auto result = (dimensions / _pageDimensions) + + glm::clamp(dimensions % _pageDimensions, glm::uvec3(0), glm::uvec3(1)); + return result; +} + +uint32_t GL45SparseResourceTexture::getPageCount(const uvec3& dimensions) const { + auto pageCounts = getPageCounts(dimensions); + return pageCounts.x * pageCounts.y * pageCounts.z; +} + +void GL45SparseResourceTexture::promote() { +} + +void GL45SparseResourceTexture::demote() { +} + +SparseInfo::SparseInfo(GL45Texture& texture) + : texture(texture) { +} + +void SparseInfo::maybeMakeSparse() { + // Don't enable sparse for objects with explicitly managed mip levels + if (!texture._gpuObject.isAutogenerateMips()) { + return; + } + + const uvec3 dimensions = texture._gpuObject.getDimensions(); + auto allowedPageDimensions = getPageDimensionsForFormat(texture._target, texture._internalFormat); + // In order to enable sparse the texture size must be an integer multiple of the page size + for (size_t i = 0; i < allowedPageDimensions.size(); ++i) { + pageDimensionsIndex = (uint32_t)i; + pageDimensions = allowedPageDimensions[i]; + // Is this texture an integer multiple of page dimensions? + if (uvec3(0) == (dimensions % pageDimensions)) { + qCDebug(gpugl45logging) << "Enabling sparse for texture " << texture._source.c_str(); + sparse = true; + break; + } + } + + if (sparse) { + glTextureParameteri(texture._id, GL_TEXTURE_SPARSE_ARB, GL_TRUE); + glTextureParameteri(texture._id, GL_VIRTUAL_PAGE_SIZE_INDEX_ARB, pageDimensionsIndex); + } else { + qCDebug(gpugl45logging) << "Size " << dimensions.x << " x " << dimensions.y << + " is not supported by any sparse page size for texture" << texture._source.c_str(); + } +} + + +// This can only be called after we've established our storage size +void SparseInfo::update() { + if (!sparse) { + return; + } + glGetTextureParameterIuiv(texture._id, GL_NUM_SPARSE_LEVELS_ARB, &maxSparseLevel); + + for (uint16_t mipLevel = 0; mipLevel <= maxSparseLevel; ++mipLevel) { + auto mipDimensions = texture._gpuObject.evalMipDimensions(mipLevel); + auto mipPageCount = getPageCount(mipDimensions); + maxPages += mipPageCount; + } + if (texture._target == GL_TEXTURE_CUBE_MAP) { + maxPages *= GLTexture::CUBE_NUM_FACES; + } +} + + +void SparseInfo::allocateToMip(uint16_t targetMip) { + // Not sparse, do nothing + if (!sparse) { + return; + } + + if (allocatedMip == INVALID_MIP) { + allocatedMip = maxSparseLevel + 1; + } + + // Don't try to allocate below the maximum sparse level + if (targetMip > maxSparseLevel) { + targetMip = maxSparseLevel; + } + + // Already allocated this level + if (allocatedMip <= targetMip) { + return; + } + + uint32_t maxFace = (uint32_t)(GL_TEXTURE_CUBE_MAP == texture._target ? CUBE_NUM_FACES : 1); + for (uint16_t mip = targetMip; mip < allocatedMip; ++mip) { + auto size = texture._gpuObject.evalMipDimensions(mip); + glTexturePageCommitmentEXT(texture._id, mip, 0, 0, 0, size.x, size.y, maxFace, GL_TRUE); + allocatedPages += getPageCount(size); + } + allocatedMip = targetMip; +} + +uint32_t SparseInfo::getSize() const { + return allocatedPages * pageBytes; +} +using SparseInfo = GL45Backend::GL45Texture::SparseInfo; + +void GL45Texture::updateSize() const { + if (_gpuObject.getTexelFormat().isCompressed()) { + qFatal("Compressed textures not yet supported"); + } + + if (_transferrable && _sparseInfo.sparse) { + auto size = _sparseInfo.getSize(); + Backend::updateTextureGPUSparseMemoryUsage(_size, size); + setSize(size); + } else { + setSize(_gpuObject.evalTotalSize(_mipOffset)); + } +} + +void GL45Texture::startTransfer() { + Parent::startTransfer(); + _sparseInfo.update(); + _populatedMip = _maxMip + 1; +} + +bool GL45Texture::continueTransfer() { + size_t maxFace = GL_TEXTURE_CUBE_MAP == _target ? CUBE_NUM_FACES : 1; + if (_populatedMip == _minMip) { + return false; + } + + uint16_t targetMip = _populatedMip - 1; + while (targetMip > 0 && !_gpuObject.isStoredMipFaceAvailable(targetMip)) { + --targetMip; + } + + _sparseInfo.allocateToMip(targetMip); + for (uint8_t face = 0; face < maxFace; ++face) { + auto size = _gpuObject.evalMipDimensions(targetMip); + if (_gpuObject.isStoredMipFaceAvailable(targetMip, face)) { + auto mip = _gpuObject.accessStoredMipFace(targetMip, face); + GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat(), mip->getFormat()); + if (GL_TEXTURE_2D == _target) { + glTextureSubImage2D(_id, targetMip, 0, 0, size.x, size.y, texelFormat.format, texelFormat.type, mip->readData()); + } else if (GL_TEXTURE_CUBE_MAP == _target) { + // DSA ARB does not work on AMD, so use EXT + // unless EXT is not available on the driver + if (glTextureSubImage2DEXT) { + auto target = CUBE_FACE_LAYOUT[face]; + glTextureSubImage2DEXT(_id, target, targetMip, 0, 0, size.x, size.y, texelFormat.format, texelFormat.type, mip->readData()); + } else { + glTextureSubImage3D(_id, targetMip, 0, 0, face, size.x, size.y, 1, texelFormat.format, texelFormat.type, mip->readData()); + } + } else { + Q_ASSERT(false); + } + (void)CHECK_GL_ERROR(); + break; + } + } + _populatedMip = targetMip; + return _populatedMip != _minMip; +} + +void GL45Texture::finishTransfer() { + Parent::finishTransfer(); +} + +void GL45Texture::postTransfer() { + Parent::postTransfer(); +} + +void GL45Texture::stripToMip(uint16_t newMinMip) { + if (newMinMip < _minMip) { + qCWarning(gpugl45logging) << "Cannot decrease the min mip"; + return; + } + + if (_sparseInfo.sparse && newMinMip > _sparseInfo.maxSparseLevel) { + qCWarning(gpugl45logging) << "Cannot increase the min mip into the mip tail"; + return; + } + + // If we weren't generating mips before, we need to now that we're stripping down mip levels. + if (!_gpuObject.isAutogenerateMips()) { + qCDebug(gpugl45logging) << "Force mip generation for texture"; + glGenerateTextureMipmap(_id); + } + + + uint8_t maxFace = (uint8_t)((_target == GL_TEXTURE_CUBE_MAP) ? GLTexture::CUBE_NUM_FACES : 1); + if (_sparseInfo.sparse) { + for (uint16_t mip = _minMip; mip < newMinMip; ++mip) { + auto id = _id; + auto mipDimensions = _gpuObject.evalMipDimensions(mip); + glTexturePageCommitmentEXT(id, mip, 0, 0, 0, mipDimensions.x, mipDimensions.y, maxFace, GL_FALSE); + auto deallocatedPages = _sparseInfo.getPageCount(mipDimensions) * maxFace; + assert(deallocatedPages < _sparseInfo.allocatedPages); + _sparseInfo.allocatedPages -= deallocatedPages; + } + _minMip = newMinMip; + } else { + GLuint oldId = _id; + // Find the distance between the old min mip and the new one + uint16 mipDelta = newMinMip - _minMip; + _mipOffset += mipDelta; + const_cast(_maxMip) -= mipDelta; + auto newLevels = usedMipLevels(); + + // Create and setup the new texture (allocate) + glCreateTextures(_target, 1, &const_cast(_id)); + glTextureParameteri(_id, GL_TEXTURE_BASE_LEVEL, 0); + glTextureParameteri(_id, GL_TEXTURE_MAX_LEVEL, _maxMip - _minMip); + Vec3u newDimensions = _gpuObject.evalMipDimensions(_mipOffset); + glTextureStorage2D(_id, newLevels, _internalFormat, newDimensions.x, newDimensions.y); + + // Copy the contents of the old texture to the new + GLuint fbo { 0 }; + glCreateFramebuffers(1, &fbo); + glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo); + for (uint16 targetMip = _minMip; targetMip <= _maxMip; ++targetMip) { + uint16 sourceMip = targetMip + mipDelta; + Vec3u mipDimensions = _gpuObject.evalMipDimensions(targetMip + _mipOffset); + for (GLenum target : getFaceTargets(_target)) { + glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, target, oldId, sourceMip); + (void)CHECK_GL_ERROR(); + glCopyTextureSubImage2D(_id, targetMip, 0, 0, 0, 0, mipDimensions.x, mipDimensions.y); + (void)CHECK_GL_ERROR(); + } + } + glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); + glDeleteFramebuffers(1, &fbo); + glDeleteTextures(1, &oldId); + } + + // Re-sync the sampler to force access to the new mip level + syncSampler(); + updateSize(); +} + +bool GL45Texture::derezable() const { + if (_external) { + return false; + } + auto maxMinMip = _sparseInfo.sparse ? _sparseInfo.maxSparseLevel : _maxMip; + return _transferrable && (_targetMinMip < maxMinMip); +} + +size_t GL45Texture::getMipByteCount(uint16_t mip) const { + if (!_sparseInfo.sparse) { + return Parent::getMipByteCount(mip); + } + + auto dimensions = _gpuObject.evalMipDimensions(_targetMinMip); + return _sparseInfo.getPageCount(dimensions) * _sparseInfo.pageBytes; +} + +std::pair GL45Texture::preDerez() { + assert(!_sparseInfo.sparse || _targetMinMip < _sparseInfo.maxSparseLevel); + size_t freedMemory = getMipByteCount(_targetMinMip); + bool liveMip = _populatedMip != INVALID_MIP && _populatedMip <= _targetMinMip; + ++_targetMinMip; + return { freedMemory, liveMip }; +} + +void GL45Texture::derez() { + if (_sparseInfo.sparse) { + assert(_minMip < _sparseInfo.maxSparseLevel); + } + assert(_minMip < _maxMip); + assert(_transferrable); + stripToMip(_minMip + 1); +} + +size_t GL45Texture::getCurrentGpuSize() const { + if (!_sparseInfo.sparse) { + return Parent::getCurrentGpuSize(); + } + + return _sparseInfo.getSize(); +} + +size_t GL45Texture::getTargetGpuSize() const { + if (!_sparseInfo.sparse) { + return Parent::getTargetGpuSize(); + } + + size_t result = 0; + for (auto mip = _targetMinMip; mip <= _sparseInfo.maxSparseLevel; ++mip) { + result += (_sparseInfo.pageBytes * _sparseInfo.getPageCount(_gpuObject.evalMipDimensions(mip))); + } + + return result; +} + +GL45Texture::~GL45Texture() { + if (_sparseInfo.sparse) { + uint8_t maxFace = (uint8_t)((_target == GL_TEXTURE_CUBE_MAP) ? GLTexture::CUBE_NUM_FACES : 1); + auto maxSparseMip = std::min(_maxMip, _sparseInfo.maxSparseLevel); + for (uint16_t mipLevel = _minMip; mipLevel <= maxSparseMip; ++mipLevel) { + auto mipDimensions = _gpuObject.evalMipDimensions(mipLevel); + glTexturePageCommitmentEXT(_texture, mipLevel, 0, 0, 0, mipDimensions.x, mipDimensions.y, maxFace, GL_FALSE); + auto deallocatedPages = _sparseInfo.getPageCount(mipDimensions) * maxFace; + assert(deallocatedPages <= _sparseInfo.allocatedPages); + _sparseInfo.allocatedPages -= deallocatedPages; + } + + if (0 != _sparseInfo.allocatedPages) { + qCWarning(gpugl45logging) << "Allocated pages remaining " << _id << " " << _sparseInfo.allocatedPages; + } + Backend::decrementTextureGPUSparseCount(); + } +} +GL45Texture::GL45Texture(const std::weak_ptr& backend, const Texture& texture) + : GLTexture(backend, texture, allocate(texture)), _sparseInfo(*this), _targetMinMip(_minMip) +{ + + auto theBackend = _backend.lock(); + if (_transferrable && theBackend && theBackend->isTextureManagementSparseEnabled()) { + _sparseInfo.maybeMakeSparse(); + if (_sparseInfo.sparse) { + Backend::incrementTextureGPUSparseCount(); + } + } +} +#endif diff --git a/libraries/gpu/CMakeLists.txt b/libraries/gpu/CMakeLists.txt index 384c5709ee..207431d8c7 100644 --- a/libraries/gpu/CMakeLists.txt +++ b/libraries/gpu/CMakeLists.txt @@ -1,6 +1,6 @@ set(TARGET_NAME gpu) autoscribe_shader_lib(gpu) setup_hifi_library() -link_hifi_libraries(shared) +link_hifi_libraries(shared ktx) target_nsight() diff --git a/libraries/gpu/src/gpu/Batch.cpp b/libraries/gpu/src/gpu/Batch.cpp index c15da61800..f822da129b 100644 --- a/libraries/gpu/src/gpu/Batch.cpp +++ b/libraries/gpu/src/gpu/Batch.cpp @@ -292,15 +292,8 @@ void Batch::setUniformBuffer(uint32 slot, const BufferView& view) { setUniformBuffer(slot, view._buffer, view._offset, view._size); } - void Batch::setResourceTexture(uint32 slot, const TexturePointer& texture) { - if (texture && texture->getUsage().isExternal()) { - auto recycler = texture->getExternalRecycler(); - Q_ASSERT(recycler); - } - ADD_COMMAND(setResourceTexture); - _params.emplace_back(_textures.cache(texture)); _params.emplace_back(slot); } diff --git a/libraries/gpu/src/gpu/Buffer.h b/libraries/gpu/src/gpu/Buffer.h index 2507e8e0a6..290b84bef0 100644 --- a/libraries/gpu/src/gpu/Buffer.h +++ b/libraries/gpu/src/gpu/Buffer.h @@ -198,7 +198,7 @@ public: BufferView(const BufferPointer& buffer, Size offset, Size size, const Element& element = DEFAULT_ELEMENT); BufferView(const BufferPointer& buffer, Size offset, Size size, uint16 stride, const Element& element = DEFAULT_ELEMENT); - Size getNumElements() const { return _size / _element.getSize(); } + Size getNumElements() const { return (_size - _offset) / _stride; } //Template iterator with random access on the buffer sysmem template diff --git a/libraries/gpu/src/gpu/Context.cpp b/libraries/gpu/src/gpu/Context.cpp index 78b472bdae..cc570f696f 100644 --- a/libraries/gpu/src/gpu/Context.cpp +++ b/libraries/gpu/src/gpu/Context.cpp @@ -241,6 +241,7 @@ std::atomic Context::_bufferGPUMemoryUsage { 0 }; std::atomic Context::_textureGPUCount{ 0 }; std::atomic Context::_textureGPUSparseCount { 0 }; +std::atomic Context::_textureTransferPendingSize { 0 }; std::atomic Context::_textureGPUMemoryUsage { 0 }; std::atomic Context::_textureGPUVirtualMemoryUsage { 0 }; std::atomic Context::_textureGPUFramebufferMemoryUsage { 0 }; @@ -317,6 +318,17 @@ void Context::decrementTextureGPUSparseCount() { --_textureGPUSparseCount; } +void Context::updateTextureTransferPendingSize(Size prevObjectSize, Size newObjectSize) { + if (prevObjectSize == newObjectSize) { + return; + } + if (newObjectSize > prevObjectSize) { + _textureTransferPendingSize.fetch_add(newObjectSize - prevObjectSize); + } else { + _textureTransferPendingSize.fetch_sub(prevObjectSize - newObjectSize); + } +} + void Context::updateTextureGPUMemoryUsage(Size prevObjectSize, Size newObjectSize) { if (prevObjectSize == newObjectSize) { return; @@ -390,6 +402,10 @@ uint32_t Context::getTextureGPUSparseCount() { return _textureGPUSparseCount.load(); } +Context::Size Context::getTextureTransferPendingSize() { + return _textureTransferPendingSize.load(); +} + Context::Size Context::getTextureGPUMemoryUsage() { return _textureGPUMemoryUsage.load(); } @@ -419,6 +435,7 @@ void Backend::incrementTextureGPUCount() { Context::incrementTextureGPUCount(); void Backend::decrementTextureGPUCount() { Context::decrementTextureGPUCount(); } void Backend::incrementTextureGPUSparseCount() { Context::incrementTextureGPUSparseCount(); } void Backend::decrementTextureGPUSparseCount() { Context::decrementTextureGPUSparseCount(); } +void Backend::updateTextureTransferPendingSize(Resource::Size prevObjectSize, Resource::Size newObjectSize) { Context::updateTextureTransferPendingSize(prevObjectSize, newObjectSize); } void Backend::updateTextureGPUMemoryUsage(Resource::Size prevObjectSize, Resource::Size newObjectSize) { Context::updateTextureGPUMemoryUsage(prevObjectSize, newObjectSize); } void Backend::updateTextureGPUVirtualMemoryUsage(Resource::Size prevObjectSize, Resource::Size newObjectSize) { Context::updateTextureGPUVirtualMemoryUsage(prevObjectSize, newObjectSize); } void Backend::updateTextureGPUFramebufferMemoryUsage(Resource::Size prevObjectSize, Resource::Size newObjectSize) { Context::updateTextureGPUFramebufferMemoryUsage(prevObjectSize, newObjectSize); } diff --git a/libraries/gpu/src/gpu/Context.h b/libraries/gpu/src/gpu/Context.h index 01c841992d..102c754cd7 100644 --- a/libraries/gpu/src/gpu/Context.h +++ b/libraries/gpu/src/gpu/Context.h @@ -101,6 +101,7 @@ public: static void decrementTextureGPUCount(); static void incrementTextureGPUSparseCount(); static void decrementTextureGPUSparseCount(); + static void updateTextureTransferPendingSize(Resource::Size prevObjectSize, Resource::Size newObjectSize); static void updateTextureGPUMemoryUsage(Resource::Size prevObjectSize, Resource::Size newObjectSize); static void updateTextureGPUSparseMemoryUsage(Resource::Size prevObjectSize, Resource::Size newObjectSize); static void updateTextureGPUVirtualMemoryUsage(Resource::Size prevObjectSize, Resource::Size newObjectSize); @@ -220,6 +221,7 @@ public: static uint32_t getTextureGPUSparseCount(); static Size getFreeGPUMemory(); static Size getUsedGPUMemory(); + static Size getTextureTransferPendingSize(); static Size getTextureGPUMemoryUsage(); static Size getTextureGPUVirtualMemoryUsage(); static Size getTextureGPUFramebufferMemoryUsage(); @@ -263,6 +265,7 @@ protected: static void decrementTextureGPUCount(); static void incrementTextureGPUSparseCount(); static void decrementTextureGPUSparseCount(); + static void updateTextureTransferPendingSize(Size prevObjectSize, Size newObjectSize); static void updateTextureGPUMemoryUsage(Size prevObjectSize, Size newObjectSize); static void updateTextureGPUSparseMemoryUsage(Size prevObjectSize, Size newObjectSize); static void updateTextureGPUVirtualMemoryUsage(Size prevObjectSize, Size newObjectSize); @@ -279,6 +282,7 @@ protected: static std::atomic _textureGPUCount; static std::atomic _textureGPUSparseCount; + static std::atomic _textureTransferPendingSize; static std::atomic _textureGPUMemoryUsage; static std::atomic _textureGPUSparseMemoryUsage; static std::atomic _textureGPUVirtualMemoryUsage; diff --git a/libraries/gpu/src/gpu/Format.cpp b/libraries/gpu/src/gpu/Format.cpp index 2a8185bf94..de202911e3 100644 --- a/libraries/gpu/src/gpu/Format.cpp +++ b/libraries/gpu/src/gpu/Format.cpp @@ -10,8 +10,15 @@ using namespace gpu; +const Element Element::COLOR_R_8 { SCALAR, NUINT8, RED }; +const Element Element::COLOR_SR_8 { SCALAR, NUINT8, SRED }; + const Element Element::COLOR_RGBA_32{ VEC4, NUINT8, RGBA }; const Element Element::COLOR_SRGBA_32{ VEC4, NUINT8, SRGBA }; + +const Element Element::COLOR_BGRA_32{ VEC4, NUINT8, BGRA }; +const Element Element::COLOR_SBGRA_32{ VEC4, NUINT8, SBGRA }; + const Element Element::COLOR_R11G11B10{ SCALAR, FLOAT, R11G11B10 }; const Element Element::VEC4F_COLOR_RGBA{ VEC4, FLOAT, RGBA }; const Element Element::VEC2F_UV{ VEC2, FLOAT, UV }; diff --git a/libraries/gpu/src/gpu/Format.h b/libraries/gpu/src/gpu/Format.h index 13809a41e6..493a2de3c2 100644 --- a/libraries/gpu/src/gpu/Format.h +++ b/libraries/gpu/src/gpu/Format.h @@ -133,6 +133,7 @@ static const int SCALAR_COUNT[NUM_DIMENSIONS] = { enum Semantic { RAW = 0, // used as RAW memory + RED, RGB, RGBA, BGRA, @@ -149,6 +150,7 @@ enum Semantic { STENCIL, // Stencil only buffer DEPTH_STENCIL, // Depth Stencil buffer + SRED, SRGB, SRGBA, SBGRA, @@ -227,8 +229,12 @@ public: return getRaw() != right.getRaw(); } + static const Element COLOR_R_8; + static const Element COLOR_SR_8; static const Element COLOR_RGBA_32; static const Element COLOR_SRGBA_32; + static const Element COLOR_BGRA_32; + static const Element COLOR_SBGRA_32; static const Element COLOR_R11G11B10; static const Element VEC4F_COLOR_RGBA; static const Element VEC2F_UV; diff --git a/libraries/gpu/src/gpu/Framebuffer.cpp b/libraries/gpu/src/gpu/Framebuffer.cpp index e8ccfce3b2..0d3291a74d 100755 --- a/libraries/gpu/src/gpu/Framebuffer.cpp +++ b/libraries/gpu/src/gpu/Framebuffer.cpp @@ -32,7 +32,7 @@ Framebuffer* Framebuffer::create(const std::string& name) { Framebuffer* Framebuffer::create(const std::string& name, const Format& colorBufferFormat, uint16 width, uint16 height) { auto framebuffer = Framebuffer::create(name); - auto colorTexture = TexturePointer(Texture::create2D(colorBufferFormat, width, height, Sampler(Sampler::FILTER_MIN_MAG_POINT))); + auto colorTexture = TexturePointer(Texture::createRenderBuffer(colorBufferFormat, width, height, Sampler(Sampler::FILTER_MIN_MAG_POINT))); colorTexture->setSource("Framebuffer::colorTexture"); framebuffer->setRenderBuffer(0, colorTexture); @@ -43,8 +43,8 @@ Framebuffer* Framebuffer::create(const std::string& name, const Format& colorBuf Framebuffer* Framebuffer::create(const std::string& name, const Format& colorBufferFormat, const Format& depthStencilBufferFormat, uint16 width, uint16 height) { auto framebuffer = Framebuffer::create(name); - auto colorTexture = TexturePointer(Texture::create2D(colorBufferFormat, width, height, Sampler(Sampler::FILTER_MIN_MAG_POINT))); - auto depthTexture = TexturePointer(Texture::create2D(depthStencilBufferFormat, width, height, Sampler(Sampler::FILTER_MIN_MAG_POINT))); + auto colorTexture = TexturePointer(Texture::createRenderBuffer(colorBufferFormat, width, height, Sampler(Sampler::FILTER_MIN_MAG_POINT))); + auto depthTexture = TexturePointer(Texture::createRenderBuffer(depthStencilBufferFormat, width, height, Sampler(Sampler::FILTER_MIN_MAG_POINT))); framebuffer->setRenderBuffer(0, colorTexture); framebuffer->setDepthStencilBuffer(depthTexture, depthStencilBufferFormat); @@ -55,7 +55,7 @@ Framebuffer* Framebuffer::createShadowmap(uint16 width) { auto framebuffer = Framebuffer::create("Shadowmap"); auto depthFormat = Element(gpu::SCALAR, gpu::FLOAT, gpu::DEPTH); // Depth32 texel format - auto depthTexture = TexturePointer(Texture::create2D(depthFormat, width, width)); + auto depthTexture = TexturePointer(Texture::createRenderBuffer(depthFormat, width, width)); Sampler::Desc samplerDesc; samplerDesc._borderColor = glm::vec4(1.0f); samplerDesc._wrapModeU = Sampler::WRAP_BORDER; @@ -143,6 +143,8 @@ int Framebuffer::setRenderBuffer(uint32 slot, const TexturePointer& texture, uin return -1; } + Q_ASSERT(!texture || TextureUsageType::RENDERBUFFER == texture->getUsageType()); + // Check for the slot if (slot >= getMaxNumRenderBuffers()) { return -1; @@ -222,6 +224,8 @@ bool Framebuffer::setDepthStencilBuffer(const TexturePointer& texture, const For return false; } + Q_ASSERT(!texture || TextureUsageType::RENDERBUFFER == texture->getUsageType()); + // Check for the compatibility of size if (texture) { if (!validateTargetCompatibility(*texture)) { diff --git a/libraries/gpu/src/gpu/Texture.cpp b/libraries/gpu/src/gpu/Texture.cpp index 5b0c4c876a..1f66b2900e 100755 --- a/libraries/gpu/src/gpu/Texture.cpp +++ b/libraries/gpu/src/gpu/Texture.cpp @@ -19,6 +19,7 @@ #include #include +#include #include #include "GPULogging.h" @@ -88,6 +89,10 @@ uint32_t Texture::getTextureGPUSparseCount() { return Context::getTextureGPUSparseCount(); } +Texture::Size Texture::getTextureTransferPendingSize() { + return Context::getTextureTransferPendingSize(); +} + Texture::Size Texture::getTextureGPUMemoryUsage() { return Context::getTextureGPUMemoryUsage(); } @@ -120,62 +125,23 @@ void Texture::setAllowedGPUMemoryUsage(Size size) { uint8 Texture::NUM_FACES_PER_TYPE[NUM_TYPES] = { 1, 1, 1, 6 }; -Texture::Pixels::Pixels(const Element& format, Size size, const Byte* bytes) : - _format(format), - _sysmem(size, bytes), - _isGPULoaded(false) { - Texture::updateTextureCPUMemoryUsage(0, _sysmem.getSize()); -} +using Storage = Texture::Storage; +using PixelsPointer = Texture::PixelsPointer; +using MemoryStorage = Texture::MemoryStorage; -Texture::Pixels::~Pixels() { - Texture::updateTextureCPUMemoryUsage(_sysmem.getSize(), 0); -} - -Texture::Size Texture::Pixels::resize(Size pSize) { - auto prevSize = _sysmem.getSize(); - auto newSize = _sysmem.resize(pSize); - Texture::updateTextureCPUMemoryUsage(prevSize, newSize); - return newSize; -} - -Texture::Size Texture::Pixels::setData(const Element& format, Size size, const Byte* bytes ) { - _format = format; - auto prevSize = _sysmem.getSize(); - auto newSize = _sysmem.setData(size, bytes); - Texture::updateTextureCPUMemoryUsage(prevSize, newSize); - _isGPULoaded = false; - return newSize; -} - -void Texture::Pixels::notifyGPULoaded() { - _isGPULoaded = true; - auto prevSize = _sysmem.getSize(); - auto newSize = _sysmem.resize(0); - Texture::updateTextureCPUMemoryUsage(prevSize, newSize); -} - -void Texture::Storage::assignTexture(Texture* texture) { +void Storage::assignTexture(Texture* texture) { _texture = texture; if (_texture) { _type = _texture->getType(); } } -void Texture::Storage::reset() { +void MemoryStorage::reset() { _mips.clear(); bumpStamp(); } -Texture::PixelsPointer Texture::Storage::editMipFace(uint16 level, uint8 face) { - if (level < _mips.size()) { - assert(face < _mips[level].size()); - bumpStamp(); - return _mips[level][face]; - } - return PixelsPointer(); -} - -const Texture::PixelsPointer Texture::Storage::getMipFace(uint16 level, uint8 face) const { +PixelsPointer MemoryStorage::getMipFace(uint16 level, uint8 face) const { if (level < _mips.size()) { assert(face < _mips[level].size()); return _mips[level][face]; @@ -183,20 +149,12 @@ const Texture::PixelsPointer Texture::Storage::getMipFace(uint16 level, uint8 fa return PixelsPointer(); } -void Texture::Storage::notifyMipFaceGPULoaded(uint16 level, uint8 face) const { - PixelsPointer mipFace = getMipFace(level, face); - // Free the mips - if (mipFace) { - mipFace->notifyGPULoaded(); - } -} - -bool Texture::Storage::isMipAvailable(uint16 level, uint8 face) const { +bool MemoryStorage::isMipAvailable(uint16 level, uint8 face) const { PixelsPointer mipFace = getMipFace(level, face); return (mipFace && mipFace->getSize()); } -bool Texture::Storage::allocateMip(uint16 level) { +bool MemoryStorage::allocateMip(uint16 level) { bool changed = false; if (level >= _mips.size()) { _mips.resize(level+1, std::vector(Texture::NUM_FACES_PER_TYPE[getType()])); @@ -206,7 +164,6 @@ bool Texture::Storage::allocateMip(uint16 level) { auto& mip = _mips[level]; for (auto& face : mip) { if (!face) { - face = std::make_shared(); changed = true; } } @@ -216,7 +173,7 @@ bool Texture::Storage::allocateMip(uint16 level) { return changed; } -bool Texture::Storage::assignMipData(uint16 level, const Element& format, Size size, const Byte* bytes) { +void MemoryStorage::assignMipData(uint16 level, const storage::StoragePointer& storagePointer) { allocateMip(level); auto& mip = _mips[level]; @@ -225,64 +182,63 @@ bool Texture::Storage::assignMipData(uint16 level, const Element& format, Size s // The bytes assigned here are supposed to contain all the faces bytes of the mip. // For tex1D, 2D, 3D there is only one face // For Cube, we expect the 6 faces in the order X+, X-, Y+, Y-, Z+, Z- - auto sizePerFace = size / mip.size(); - auto faceBytes = bytes; - Size allocated = 0; + auto sizePerFace = storagePointer->size() / mip.size(); + size_t offset = 0; for (auto& face : mip) { - allocated += face->setData(format, sizePerFace, faceBytes); - faceBytes += sizePerFace; + face = storagePointer->createView(sizePerFace, offset); + offset += sizePerFace; } bumpStamp(); - - return allocated == size; } -bool Texture::Storage::assignMipFaceData(uint16 level, const Element& format, Size size, const Byte* bytes, uint8 face) { - +void Texture::MemoryStorage::assignMipFaceData(uint16 level, uint8 face, const storage::StoragePointer& storagePointer) { allocateMip(level); - auto mip = _mips[level]; - Size allocated = 0; + auto& mip = _mips[level]; if (face < mip.size()) { - auto mipFace = mip[face]; - allocated += mipFace->setData(format, size, bytes); + mip[face] = storagePointer; bumpStamp(); } - - return allocated == size; } -Texture* Texture::createExternal2D(const ExternalRecycler& recycler, const Sampler& sampler) { - Texture* tex = new Texture(); +Texture* Texture::createExternal(const ExternalRecycler& recycler, const Sampler& sampler) { + Texture* tex = new Texture(TextureUsageType::EXTERNAL); tex->_type = TEX_2D; tex->_maxMip = 0; tex->_sampler = sampler; - tex->setUsage(Usage::Builder().withExternal().withColor()); tex->setExternalRecycler(recycler); return tex; } +Texture* Texture::createRenderBuffer(const Element& texelFormat, uint16 width, uint16 height, const Sampler& sampler) { + return create(TextureUsageType::RENDERBUFFER, TEX_2D, texelFormat, width, height, 1, 1, 0, sampler); +} + Texture* Texture::create1D(const Element& texelFormat, uint16 width, const Sampler& sampler) { - return create(TEX_1D, texelFormat, width, 1, 1, 1, 1, sampler); + return create(TextureUsageType::RESOURCE, TEX_1D, texelFormat, width, 1, 1, 1, 0, sampler); } Texture* Texture::create2D(const Element& texelFormat, uint16 width, uint16 height, const Sampler& sampler) { - return create(TEX_2D, texelFormat, width, height, 1, 1, 1, sampler); + return create(TextureUsageType::RESOURCE, TEX_2D, texelFormat, width, height, 1, 1, 0, sampler); +} + +Texture* Texture::createStrict(const Element& texelFormat, uint16 width, uint16 height, const Sampler& sampler) { + return create(TextureUsageType::STRICT_RESOURCE, TEX_2D, texelFormat, width, height, 1, 1, 0, sampler); } Texture* Texture::create3D(const Element& texelFormat, uint16 width, uint16 height, uint16 depth, const Sampler& sampler) { - return create(TEX_3D, texelFormat, width, height, depth, 1, 1, sampler); + return create(TextureUsageType::RESOURCE, TEX_3D, texelFormat, width, height, depth, 1, 0, sampler); } Texture* Texture::createCube(const Element& texelFormat, uint16 width, const Sampler& sampler) { - return create(TEX_CUBE, texelFormat, width, width, 1, 1, 1, sampler); + return create(TextureUsageType::RESOURCE, TEX_CUBE, texelFormat, width, width, 1, 1, 0, sampler); } -Texture* Texture::create(Type type, const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numSamples, uint16 numSlices, const Sampler& sampler) +Texture* Texture::create(TextureUsageType usageType, Type type, const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numSamples, uint16 numSlices, const Sampler& sampler) { - Texture* tex = new Texture(); - tex->_storage.reset(new Storage()); + Texture* tex = new Texture(usageType); + tex->_storage.reset(new MemoryStorage()); tex->_type = type; tex->_storage->assignTexture(tex); tex->_maxMip = 0; @@ -293,16 +249,14 @@ Texture* Texture::create(Type type, const Element& texelFormat, uint16 width, ui return tex; } -Texture::Texture(): - Resource() -{ +Texture::Texture(TextureUsageType usageType) : + Resource(), _usageType(usageType) { _textureCPUCount++; } -Texture::~Texture() -{ +Texture::~Texture() { _textureCPUCount--; - if (getUsage().isExternal()) { + if (_usageType == TextureUsageType::EXTERNAL) { Texture::ExternalUpdates externalUpdates; { Lock lock(_externalMutex); @@ -321,7 +275,7 @@ Texture::~Texture() } Texture::Size Texture::resize(Type type, const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numSamples, uint16 numSlices) { - if (width && height && depth && numSamples && numSlices) { + if (width && height && depth && numSamples) { bool changed = false; if ( _type != type) { @@ -382,20 +336,20 @@ Texture::Size Texture::resize(Type type, const Element& texelFormat, uint16 widt } Texture::Size Texture::resize1D(uint16 width, uint16 numSamples) { - return resize(TEX_1D, getTexelFormat(), width, 1, 1, numSamples, 1); + return resize(TEX_1D, getTexelFormat(), width, 1, 1, numSamples, 0); } Texture::Size Texture::resize2D(uint16 width, uint16 height, uint16 numSamples) { - return resize(TEX_2D, getTexelFormat(), width, height, 1, numSamples, 1); + return resize(TEX_2D, getTexelFormat(), width, height, 1, numSamples, 0); } Texture::Size Texture::resize3D(uint16 width, uint16 height, uint16 depth, uint16 numSamples) { - return resize(TEX_3D, getTexelFormat(), width, height, depth, numSamples, 1); + return resize(TEX_3D, getTexelFormat(), width, height, depth, numSamples, 0); } Texture::Size Texture::resizeCube(uint16 width, uint16 numSamples) { - return resize(TEX_CUBE, getTexelFormat(), width, 1, 1, numSamples, 1); + return resize(TEX_CUBE, getTexelFormat(), width, 1, 1, numSamples, 0); } Texture::Size Texture::reformat(const Element& texelFormat) { - return resize(_type, texelFormat, getWidth(), getHeight(), getDepth(), getNumSamples(), getNumSlices()); + return resize(_type, texelFormat, getWidth(), getHeight(), getDepth(), getNumSamples(), _numSlices); } bool Texture::isColorRenderTarget() const { @@ -426,69 +380,83 @@ uint16 Texture::evalNumMips() const { return evalNumMips({ _width, _height, _depth }); } -bool Texture::assignStoredMip(uint16 level, const Element& format, Size size, const Byte* bytes) { +void Texture::setStoredMipFormat(const Element& format) { + _storage->setFormat(format); +} + +const Element& Texture::getStoredMipFormat() const { + return _storage->getFormat(); +} + +void Texture::assignStoredMip(uint16 level, Size size, const Byte* bytes) { + storage::StoragePointer storage = std::make_shared(size, bytes); + assignStoredMip(level, storage); +} + +void Texture::assignStoredMipFace(uint16 level, uint8 face, Size size, const Byte* bytes) { + storage::StoragePointer storage = std::make_shared(size, bytes); + assignStoredMipFace(level, face, storage); +} + +void Texture::assignStoredMip(uint16 level, storage::StoragePointer& storage) { // Check that level accessed make sense if (level != 0) { if (_autoGenerateMips) { - return false; + return; } if (level >= evalNumMips()) { - return false; + return; } } // THen check that the mem texture passed make sense with its format - Size expectedSize = evalStoredMipSize(level, format); - if (size == expectedSize) { - _storage->assignMipData(level, format, size, bytes); + Size expectedSize = evalStoredMipSize(level, getStoredMipFormat()); + auto size = storage->size(); + if (storage->size() == expectedSize) { + _storage->assignMipData(level, storage); _maxMip = std::max(_maxMip, level); _stamp++; - return true; } else if (size > expectedSize) { // NOTE: We are facing this case sometime because apparently QImage (from where we get the bits) is generating images // and alligning the line of pixels to 32 bits. // We should probably consider something a bit more smart to get the correct result but for now (UI elements) // it seems to work... - _storage->assignMipData(level, format, size, bytes); + _storage->assignMipData(level, storage); _maxMip = std::max(_maxMip, level); _stamp++; - return true; } - - return false; } - -bool Texture::assignStoredMipFace(uint16 level, const Element& format, Size size, const Byte* bytes, uint8 face) { +void Texture::assignStoredMipFace(uint16 level, uint8 face, storage::StoragePointer& storage) { // Check that level accessed make sense if (level != 0) { if (_autoGenerateMips) { - return false; + return; } if (level >= evalNumMips()) { - return false; + return; } } // THen check that the mem texture passed make sense with its format - Size expectedSize = evalStoredMipFaceSize(level, format); + Size expectedSize = evalStoredMipFaceSize(level, getStoredMipFormat()); + auto size = storage->size(); if (size == expectedSize) { - _storage->assignMipFaceData(level, format, size, bytes, face); + _storage->assignMipFaceData(level, face, storage); + _maxMip = std::max(_maxMip, level); _stamp++; - return true; } else if (size > expectedSize) { // NOTE: We are facing this case sometime because apparently QImage (from where we get the bits) is generating images // and alligning the line of pixels to 32 bits. // We should probably consider something a bit more smart to get the correct result but for now (UI elements) // it seems to work... - _storage->assignMipFaceData(level, format, size, bytes, face); + _storage->assignMipFaceData(level, face, storage); + _maxMip = std::max(_maxMip, level); _stamp++; - return true; } - - return false; } + uint16 Texture::autoGenerateMips(uint16 maxMip) { bool changed = false; if (!_autoGenerateMips) { @@ -522,7 +490,7 @@ uint16 Texture::getStoredMipHeight(uint16 level) const { if (mip && mip->getSize()) { return evalMipHeight(level); } - return 0; + return 0; } uint16 Texture::getStoredMipDepth(uint16 level) const { @@ -794,7 +762,16 @@ bool sphericalHarmonicsFromTexture(const gpu::Texture& cubeTexture, std::vector< for(int face=0; face < gpu::Texture::NUM_CUBE_FACES; face++) { PROFILE_RANGE(render_gpu, "ProcessFace"); - auto numComponents = cubeTexture.accessStoredMipFace(0,face)->getFormat().getScalarCount(); + auto mipFormat = cubeTexture.getStoredMipFormat(); + auto numComponents = mipFormat.getScalarCount(); + int roffset { 0 }; + int goffset { 1 }; + int boffset { 2 }; + if ((mipFormat.getSemantic() == gpu::BGRA) || (mipFormat.getSemantic() == gpu::SBGRA)) { + roffset = 2; + boffset = 0; + } + auto data = cubeTexture.accessStoredMipFace(0,face)->readData(); if (data == nullptr) { continue; @@ -882,9 +859,9 @@ bool sphericalHarmonicsFromTexture(const gpu::Texture& cubeTexture, std::vector< for (int i = 0; i < stride; ++i) { for (int j = 0; j < stride; ++j) { int k = (int)(x + i - halfStride + (y + j - halfStride) * width) * numComponents; - red += ColorUtils::sRGB8ToLinearFloat(data[k]); - green += ColorUtils::sRGB8ToLinearFloat(data[k + 1]); - blue += ColorUtils::sRGB8ToLinearFloat(data[k + 2]); + red += ColorUtils::sRGB8ToLinearFloat(data[k + roffset]); + green += ColorUtils::sRGB8ToLinearFloat(data[k + goffset]); + blue += ColorUtils::sRGB8ToLinearFloat(data[k + boffset]); } } glm::vec3 clr(red, green, blue); @@ -911,8 +888,6 @@ bool sphericalHarmonicsFromTexture(const gpu::Texture& cubeTexture, std::vector< // save result for(uint i=0; i < sqOrder; i++) { - // gamma Correct - // output[i] = linearTosRGB(glm::vec3(resultR[i], resultG[i], resultB[i])); output[i] = glm::vec3(resultR[i], resultG[i], resultB[i]); } @@ -1001,3 +976,7 @@ Texture::ExternalUpdates Texture::getUpdates() const { } return result; } + +void Texture::setStorage(std::unique_ptr& newStorage) { + _storage.swap(newStorage); +} diff --git a/libraries/gpu/src/gpu/Texture.h b/libraries/gpu/src/gpu/Texture.h index 856bd4983d..f7297b3280 100755 --- a/libraries/gpu/src/gpu/Texture.h +++ b/libraries/gpu/src/gpu/Texture.h @@ -17,9 +17,17 @@ #include #include +#include + #include "Forward.h" #include "Resource.h" +namespace ktx { + class KTX; + using KTXUniquePointer = std::unique_ptr; + struct Header; +} + namespace gpu { // THe spherical harmonics is a nice tool for cubemap, so if required, the irradiance SH can be automatically generated @@ -135,10 +143,18 @@ public: uint8 getMinMip() const { return _desc._minMip; } uint8 getMaxMip() const { return _desc._maxMip; } + const Desc& getDesc() const { return _desc; } protected: Desc _desc; }; +enum class TextureUsageType { + RENDERBUFFER, // Used as attachments to a framebuffer + RESOURCE, // Resource textures, like materials... subject to memory manipulation + STRICT_RESOURCE, // Resource textures not subject to manipulation, like the normal fitting texture + EXTERNAL, +}; + class Texture : public Resource { static std::atomic _textureCPUCount; static std::atomic _textureCPUMemoryUsage; @@ -147,10 +163,12 @@ class Texture : public Resource { static void updateTextureCPUMemoryUsage(Size prevObjectSize, Size newObjectSize); public: + static const uint32_t CUBE_FACE_COUNT { 6 }; static uint32_t getTextureCPUCount(); static Size getTextureCPUMemoryUsage(); static uint32_t getTextureGPUCount(); static uint32_t getTextureGPUSparseCount(); + static Size getTextureTransferPendingSize(); static Size getTextureGPUMemoryUsage(); static Size getTextureGPUVirtualMemoryUsage(); static Size getTextureGPUFramebufferMemoryUsage(); @@ -173,9 +191,9 @@ public: NORMAL, // Texture is a normal map ALPHA, // Texture has an alpha channel ALPHA_MASK, // Texture alpha channel is a Mask 0/1 - EXTERNAL, NUM_FLAGS, }; + typedef std::bitset Flags; // The key is the Flags @@ -199,7 +217,6 @@ public: Builder& withNormal() { _flags.set(NORMAL); return (*this); } Builder& withAlpha() { _flags.set(ALPHA); return (*this); } Builder& withAlphaMask() { _flags.set(ALPHA_MASK); return (*this); } - Builder& withExternal() { _flags.set(EXTERNAL); return (*this); } }; Usage(const Builder& builder) : Usage(builder._flags) {} @@ -208,37 +225,12 @@ public: bool isAlpha() const { return _flags[ALPHA]; } bool isAlphaMask() const { return _flags[ALPHA_MASK]; } - bool isExternal() const { return _flags[EXTERNAL]; } - bool operator==(const Usage& usage) { return (_flags == usage._flags); } bool operator!=(const Usage& usage) { return (_flags != usage._flags); } }; - class Pixels { - public: - Pixels() {} - Pixels(const Pixels& pixels) = default; - Pixels(const Element& format, Size size, const Byte* bytes); - ~Pixels(); - - const Byte* readData() const { return _sysmem.readData(); } - Size getSize() const { return _sysmem.getSize(); } - Size resize(Size pSize); - Size setData(const Element& format, Size size, const Byte* bytes ); - - const Element& getFormat() const { return _format; } - - void notifyGPULoaded(); - - protected: - Element _format; - Sysmem _sysmem; - bool _isGPULoaded; - - friend class Texture; - }; - typedef std::shared_ptr< Pixels > PixelsPointer; + using PixelsPointer = storage::StoragePointer; enum Type { TEX_1D = 0, @@ -261,46 +253,78 @@ public: NUM_CUBE_FACES, // Not a valid vace index }; + class Storage { public: Storage() {} virtual ~Storage() {} - virtual void reset(); - virtual PixelsPointer editMipFace(uint16 level, uint8 face = 0); - virtual const PixelsPointer getMipFace(uint16 level, uint8 face = 0) const; - virtual bool allocateMip(uint16 level); - virtual bool assignMipData(uint16 level, const Element& format, Size size, const Byte* bytes); - virtual bool assignMipFaceData(uint16 level, const Element& format, Size size, const Byte* bytes, uint8 face); - virtual bool isMipAvailable(uint16 level, uint8 face = 0) const; + virtual void reset() = 0; + virtual PixelsPointer getMipFace(uint16 level, uint8 face = 0) const = 0; + virtual void assignMipData(uint16 level, const storage::StoragePointer& storage) = 0; + virtual void assignMipFaceData(uint16 level, uint8 face, const storage::StoragePointer& storage) = 0; + virtual bool isMipAvailable(uint16 level, uint8 face = 0) const = 0; Texture::Type getType() const { return _type; } - + Stamp getStamp() const { return _stamp; } Stamp bumpStamp() { return ++_stamp; } - protected: - Stamp _stamp = 0; - Texture* _texture = nullptr; // Points to the parent texture (not owned) - Texture::Type _type = Texture::TEX_2D; // The type of texture is needed to know the number of faces to expect - std::vector> _mips; // an array of mips, each mip is an array of faces + void setFormat(const Element& format) { _format = format; } + const Element& getFormat() const { return _format; } + + private: + Stamp _stamp { 0 }; + Element _format; + Texture::Type _type { Texture::TEX_2D }; // The type of texture is needed to know the number of faces to expect + Texture* _texture { nullptr }; // Points to the parent texture (not owned) virtual void assignTexture(Texture* tex); // Texture storage is pointing to ONE corrresponding Texture. const Texture* getTexture() const { return _texture; } - friend class Texture; - - // THis should be only called by the Texture from the Backend to notify the storage that the specified mip face pixels - // have been uploaded to the GPU memory. IT is possible for the storage to free the system memory then - virtual void notifyMipFaceGPULoaded(uint16 level, uint8 face) const; }; - + class MemoryStorage : public Storage { + public: + void reset() override; + PixelsPointer getMipFace(uint16 level, uint8 face = 0) const override; + void assignMipData(uint16 level, const storage::StoragePointer& storage) override; + void assignMipFaceData(uint16 level, uint8 face, const storage::StoragePointer& storage) override; + bool isMipAvailable(uint16 level, uint8 face = 0) const override; + + protected: + bool allocateMip(uint16 level); + std::vector> _mips; // an array of mips, each mip is an array of faces + }; + + class KtxStorage : public Storage { + public: + KtxStorage(ktx::KTXUniquePointer& ktxData); + PixelsPointer getMipFace(uint16 level, uint8 face = 0) const override; + // By convention, all mip levels and faces MUST be populated when using KTX backing + bool isMipAvailable(uint16 level, uint8 face = 0) const override { return true; } + + void assignMipData(uint16 level, const storage::StoragePointer& storage) override { + throw std::runtime_error("Invalid call"); + } + + void assignMipFaceData(uint16 level, uint8 face, const storage::StoragePointer& storage) override { + throw std::runtime_error("Invalid call"); + } + void reset() override { } + + protected: + ktx::KTXUniquePointer _ktxData; + friend class Texture; + }; + static Texture* create1D(const Element& texelFormat, uint16 width, const Sampler& sampler = Sampler()); static Texture* create2D(const Element& texelFormat, uint16 width, uint16 height, const Sampler& sampler = Sampler()); static Texture* create3D(const Element& texelFormat, uint16 width, uint16 height, uint16 depth, const Sampler& sampler = Sampler()); static Texture* createCube(const Element& texelFormat, uint16 width, const Sampler& sampler = Sampler()); - static Texture* createExternal2D(const ExternalRecycler& recycler, const Sampler& sampler = Sampler()); + static Texture* createRenderBuffer(const Element& texelFormat, uint16 width, uint16 height, const Sampler& sampler = Sampler()); + static Texture* createStrict(const Element& texelFormat, uint16 width, uint16 height, const Sampler& sampler = Sampler()); + static Texture* createExternal(const ExternalRecycler& recycler, const Sampler& sampler = Sampler()); - Texture(); + Texture(TextureUsageType usageType); Texture(const Texture& buf); // deep copy of the sysmem texture Texture& operator=(const Texture& buf); // deep copy of the sysmem texture ~Texture(); @@ -325,6 +349,7 @@ public: // Size and format Type getType() const { return _type; } + TextureUsageType getUsageType() const { return _usageType; } bool isColorRenderTarget() const; bool isDepthStencilRenderTarget() const; @@ -347,7 +372,12 @@ public: uint32 getNumTexels() const { return _width * _height * _depth * getNumFaces(); } - uint16 getNumSlices() const { return _numSlices; } + // The texture is an array if the _numSlices is not 0. + // otherwise, if _numSLices is 0, then the texture is NOT an array + // The number of slices returned is 1 at the minimum (if not an array) or the actual _numSlices. + bool isArray() const { return _numSlices > 0; } + uint16 getNumSlices() const { return (isArray() ? _numSlices : 1); } + uint16 getNumSamples() const { return _numSamples; } @@ -429,18 +459,29 @@ public: // Managing Storage and mips + // Mip storage format is constant across all mips + void setStoredMipFormat(const Element& format); + const Element& getStoredMipFormat() const; + // Manually allocate the mips down until the specified maxMip // this is just allocating the sysmem version of it // in case autoGen is on, this doesn't allocate // Explicitely assign mip data for a certain level // If Bytes is NULL then simply allocate the space so mip sysmem can be accessed - bool assignStoredMip(uint16 level, const Element& format, Size size, const Byte* bytes); - bool assignStoredMipFace(uint16 level, const Element& format, Size size, const Byte* bytes, uint8 face); + + void assignStoredMip(uint16 level, Size size, const Byte* bytes); + void assignStoredMipFace(uint16 level, uint8 face, Size size, const Byte* bytes); + + void assignStoredMip(uint16 level, storage::StoragePointer& storage); + void assignStoredMipFace(uint16 level, uint8 face, storage::StoragePointer& storage); // Access the the sub mips bool isStoredMipFaceAvailable(uint16 level, uint8 face = 0) const { return _storage->isMipAvailable(level, face); } const PixelsPointer accessStoredMipFace(uint16 level, uint8 face = 0) const { return _storage->getMipFace(level, face); } + void setStorage(std::unique_ptr& newStorage); + void setKtxBacking(ktx::KTXUniquePointer& newBacking); + // access sizes for the stored mips uint16 getStoredMipWidth(uint16 level) const; uint16 getStoredMipHeight(uint16 level) const; @@ -464,8 +505,8 @@ public: const Sampler& getSampler() const { return _sampler; } Stamp getSamplerStamp() const { return _samplerStamp; } - // Only callable by the Backend - void notifyMipFaceGPULoaded(uint16 level, uint8 face = 0) const { return _storage->notifyMipFaceGPULoaded(level, face); } + void setFallbackTexture(const TexturePointer& fallback) { _fallback = fallback; } + TexturePointer getFallbackTexture() const { return _fallback.lock(); } void setExternalTexture(uint32 externalId, void* externalFence); void setExternalRecycler(const ExternalRecycler& recycler); @@ -475,36 +516,45 @@ public: ExternalUpdates getUpdates() const; + // Textures can be serialized directly to ktx data file, here is how + static ktx::KTXUniquePointer serialize(const Texture& texture); + static Texture* unserialize(const ktx::KTXUniquePointer& srcData, TextureUsageType usageType = TextureUsageType::RESOURCE, Usage usage = Usage(), const Sampler::Desc& sampler = Sampler::Desc()); + static bool evalKTXFormat(const Element& mipFormat, const Element& texelFormat, ktx::Header& header); + static bool evalTextureFormat(const ktx::Header& header, Element& mipFormat, Element& texelFormat); + protected: + const TextureUsageType _usageType; + // Should only be accessed internally or by the backend sync function mutable Mutex _externalMutex; mutable std::list _externalUpdates; ExternalRecycler _externalRecycler; + std::weak_ptr _fallback; // Not strictly necessary, but incredibly useful for debugging std::string _source; std::unique_ptr< Storage > _storage; - Stamp _stamp = 0; + Stamp _stamp { 0 }; Sampler _sampler; - Stamp _samplerStamp; + Stamp _samplerStamp { 0 }; - uint32 _size = 0; + uint32 _size { 0 }; Element _texelFormat; - uint16 _width = 1; - uint16 _height = 1; - uint16 _depth = 1; + uint16 _width { 1 }; + uint16 _height { 1 }; + uint16 _depth { 1 }; - uint16 _numSamples = 1; - uint16 _numSlices = 1; + uint16 _numSamples { 1 }; + uint16 _numSlices { 0 }; // if _numSlices is 0, the texture is not an "Array", the getNumSlices reported is 1 uint16 _maxMip { 0 }; uint16 _minMip { 0 }; - Type _type = TEX_1D; + Type _type { TEX_1D }; Usage _usage; @@ -513,7 +563,7 @@ protected: bool _isIrradianceValid = false; bool _defined = false; - static Texture* create(Type type, const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numSamples, uint16 numSlices, const Sampler& sampler); + static Texture* create(TextureUsageType usageType, Type type, const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numSamples, uint16 numSlices, const Sampler& sampler); Size resize(Type type, const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numSamples, uint16 numSlices); }; diff --git a/libraries/gpu/src/gpu/Texture_ktx.cpp b/libraries/gpu/src/gpu/Texture_ktx.cpp new file mode 100644 index 0000000000..5f0ededee7 --- /dev/null +++ b/libraries/gpu/src/gpu/Texture_ktx.cpp @@ -0,0 +1,289 @@ +// +// Texture_ktx.cpp +// libraries/gpu/src/gpu +// +// Created by Sam Gateau on 2/16/2017. +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + + +#include "Texture.h" + +#include +using namespace gpu; + +using PixelsPointer = Texture::PixelsPointer; +using KtxStorage = Texture::KtxStorage; + +struct GPUKTXPayload { + Sampler::Desc _samplerDesc; + Texture::Usage _usage; + TextureUsageType _usageType; + + static std::string KEY; + static bool isGPUKTX(const ktx::KeyValue& val) { + return (val._key.compare(KEY) == 0); + } + + static bool findInKeyValues(const ktx::KeyValues& keyValues, GPUKTXPayload& payload) { + auto found = std::find_if(keyValues.begin(), keyValues.end(), isGPUKTX); + if (found != keyValues.end()) { + if ((*found)._value.size() == sizeof(GPUKTXPayload)) { + memcpy(&payload, (*found)._value.data(), sizeof(GPUKTXPayload)); + return true; + } + } + return false; + } +}; + +std::string GPUKTXPayload::KEY { "hifi.gpu" }; + +KtxStorage::KtxStorage(ktx::KTXUniquePointer& ktxData) { + + // if the source ktx is valid let's config this KtxStorage correctly + if (ktxData && ktxData->getHeader()) { + + // now that we know the ktx, let's get the header info to configure this Texture::Storage: + Format mipFormat = Format::COLOR_BGRA_32; + Format texelFormat = Format::COLOR_SRGBA_32; + if (Texture::evalTextureFormat(*ktxData->getHeader(), mipFormat, texelFormat)) { + _format = mipFormat; + } + + + } + + _ktxData.reset(ktxData.release()); +} + +PixelsPointer KtxStorage::getMipFace(uint16 level, uint8 face) const { + return _ktxData->getMipFaceTexelsData(level, face); +} + +void Texture::setKtxBacking(ktx::KTXUniquePointer& ktxBacking) { + auto newBacking = std::unique_ptr(new KtxStorage(ktxBacking)); + setStorage(newBacking); +} + +ktx::KTXUniquePointer Texture::serialize(const Texture& texture) { + ktx::Header header; + + // From texture format to ktx format description + auto texelFormat = texture.getTexelFormat(); + auto mipFormat = texture.getStoredMipFormat(); + + if (!Texture::evalKTXFormat(mipFormat, texelFormat, header)) { + return nullptr; + } + + // Set Dimensions + uint32_t numFaces = 1; + switch (texture.getType()) { + case TEX_1D: { + if (texture.isArray()) { + header.set1DArray(texture.getWidth(), texture.getNumSlices()); + } else { + header.set1D(texture.getWidth()); + } + break; + } + case TEX_2D: { + if (texture.isArray()) { + header.set2DArray(texture.getWidth(), texture.getHeight(), texture.getNumSlices()); + } else { + header.set2D(texture.getWidth(), texture.getHeight()); + } + break; + } + case TEX_3D: { + if (texture.isArray()) { + header.set3DArray(texture.getWidth(), texture.getHeight(), texture.getDepth(), texture.getNumSlices()); + } else { + header.set3D(texture.getWidth(), texture.getHeight(), texture.getDepth()); + } + break; + } + case TEX_CUBE: { + if (texture.isArray()) { + header.setCubeArray(texture.getWidth(), texture.getHeight(), texture.getNumSlices()); + } else { + header.setCube(texture.getWidth(), texture.getHeight()); + } + numFaces = Texture::CUBE_FACE_COUNT; + break; + } + default: + return nullptr; + } + + // Number level of mips coming + header.numberOfMipmapLevels = texture.maxMip() + 1; + + ktx::Images images; + for (uint32_t level = 0; level < header.numberOfMipmapLevels; level++) { + auto mip = texture.accessStoredMipFace(level); + if (mip) { + if (numFaces == 1) { + images.emplace_back(ktx::Image((uint32_t)mip->getSize(), 0, mip->readData())); + } else { + ktx::Image::FaceBytes cubeFaces(Texture::CUBE_FACE_COUNT); + cubeFaces[0] = mip->readData(); + for (uint32_t face = 1; face < Texture::CUBE_FACE_COUNT; face++) { + cubeFaces[face] = texture.accessStoredMipFace(level, face)->readData(); + } + images.emplace_back(ktx::Image((uint32_t)mip->getSize(), 0, cubeFaces)); + } + } + } + + GPUKTXPayload keyval; + keyval._samplerDesc = texture.getSampler().getDesc(); + keyval._usage = texture.getUsage(); + keyval._usageType = texture.getUsageType(); + ktx::KeyValues keyValues; + keyValues.emplace_back(ktx::KeyValue(GPUKTXPayload::KEY, sizeof(GPUKTXPayload), (ktx::Byte*) &keyval)); + + auto ktxBuffer = ktx::KTX::create(header, images, keyValues); +#if 0 + auto expectedMipCount = texture.evalNumMips(); + assert(expectedMipCount == ktxBuffer->_images.size()); + assert(expectedMipCount == header.numberOfMipmapLevels); + + assert(0 == memcmp(&header, ktxBuffer->getHeader(), sizeof(ktx::Header))); + assert(ktxBuffer->_images.size() == images.size()); + auto start = ktxBuffer->_storage->data(); + for (size_t i = 0; i < images.size(); ++i) { + auto expected = images[i]; + auto actual = ktxBuffer->_images[i]; + assert(expected._padding == actual._padding); + assert(expected._numFaces == actual._numFaces); + assert(expected._imageSize == actual._imageSize); + assert(expected._faceSize == actual._faceSize); + assert(actual._faceBytes.size() == actual._numFaces); + for (uint32_t face = 0; face < expected._numFaces; ++face) { + auto expectedFace = expected._faceBytes[face]; + auto actualFace = actual._faceBytes[face]; + auto offset = actualFace - start; + assert(offset % 4 == 0); + assert(expectedFace != actualFace); + assert(0 == memcmp(expectedFace, actualFace, expected._faceSize)); + } + } +#endif + return ktxBuffer; +} + +Texture* Texture::unserialize(const ktx::KTXUniquePointer& srcData, TextureUsageType usageType, Usage usage, const Sampler::Desc& sampler) { + if (!srcData) { + return nullptr; + } + const auto& header = *srcData->getHeader(); + + Format mipFormat = Format::COLOR_BGRA_32; + Format texelFormat = Format::COLOR_SRGBA_32; + + if (!Texture::evalTextureFormat(header, mipFormat, texelFormat)) { + return nullptr; + } + + // Find Texture Type based on dimensions + Type type = TEX_1D; + if (header.pixelWidth == 0) { + return nullptr; + } else if (header.pixelHeight == 0) { + type = TEX_1D; + } else if (header.pixelDepth == 0) { + if (header.numberOfFaces == ktx::NUM_CUBEMAPFACES) { + type = TEX_CUBE; + } else { + type = TEX_2D; + } + } else { + type = TEX_3D; + } + + + // If found, use the + GPUKTXPayload gpuktxKeyValue; + bool isGPUKTXPayload = GPUKTXPayload::findInKeyValues(srcData->_keyValues, gpuktxKeyValue); + + auto tex = Texture::create( (isGPUKTXPayload ? gpuktxKeyValue._usageType : usageType), + type, + texelFormat, + header.getPixelWidth(), + header.getPixelHeight(), + header.getPixelDepth(), + 1, // num Samples + header.getNumberOfSlices(), + (isGPUKTXPayload ? gpuktxKeyValue._samplerDesc : sampler)); + + tex->setUsage((isGPUKTXPayload ? gpuktxKeyValue._usage : usage)); + + // Assing the mips availables + tex->setStoredMipFormat(mipFormat); + uint16_t level = 0; + for (auto& image : srcData->_images) { + for (uint32_t face = 0; face < image._numFaces; face++) { + tex->assignStoredMipFace(level, face, image._faceSize, image._faceBytes[face]); + } + level++; + } + + return tex; +} + +bool Texture::evalKTXFormat(const Element& mipFormat, const Element& texelFormat, ktx::Header& header) { + if (texelFormat == Format::COLOR_RGBA_32 && mipFormat == Format::COLOR_BGRA_32) { + header.setUncompressed(ktx::GLType::UNSIGNED_BYTE, 1, ktx::GLFormat::BGRA, ktx::GLInternalFormat_Uncompressed::RGBA8, ktx::GLBaseInternalFormat::RGBA); + } else if (texelFormat == Format::COLOR_RGBA_32 && mipFormat == Format::COLOR_RGBA_32) { + header.setUncompressed(ktx::GLType::UNSIGNED_BYTE, 1, ktx::GLFormat::RGBA, ktx::GLInternalFormat_Uncompressed::RGBA8, ktx::GLBaseInternalFormat::RGBA); + } else if (texelFormat == Format::COLOR_SRGBA_32 && mipFormat == Format::COLOR_SBGRA_32) { + header.setUncompressed(ktx::GLType::UNSIGNED_BYTE, 1, ktx::GLFormat::BGRA, ktx::GLInternalFormat_Uncompressed::SRGB8_ALPHA8, ktx::GLBaseInternalFormat::RGBA); + } else if (texelFormat == Format::COLOR_SRGBA_32 && mipFormat == Format::COLOR_SRGBA_32) { + header.setUncompressed(ktx::GLType::UNSIGNED_BYTE, 1, ktx::GLFormat::RGBA, ktx::GLInternalFormat_Uncompressed::SRGB8_ALPHA8, ktx::GLBaseInternalFormat::RGBA); + } else if (texelFormat == Format::COLOR_R_8 && mipFormat == Format::COLOR_R_8) { + header.setUncompressed(ktx::GLType::UNSIGNED_BYTE, 1, ktx::GLFormat::RED, ktx::GLInternalFormat_Uncompressed::R8, ktx::GLBaseInternalFormat::RED); + } else { + return false; + } + + return true; +} + +bool Texture::evalTextureFormat(const ktx::Header& header, Element& mipFormat, Element& texelFormat) { + if (header.getGLFormat() == ktx::GLFormat::BGRA && header.getGLType() == ktx::GLType::UNSIGNED_BYTE && header.getTypeSize() == 1) { + if (header.getGLInternaFormat_Uncompressed() == ktx::GLInternalFormat_Uncompressed::RGBA8) { + mipFormat = Format::COLOR_BGRA_32; + texelFormat = Format::COLOR_RGBA_32; + } else if (header.getGLInternaFormat_Uncompressed() == ktx::GLInternalFormat_Uncompressed::SRGB8_ALPHA8) { + mipFormat = Format::COLOR_SBGRA_32; + texelFormat = Format::COLOR_SRGBA_32; + } else { + return false; + } + } else if (header.getGLFormat() == ktx::GLFormat::RGBA && header.getGLType() == ktx::GLType::UNSIGNED_BYTE && header.getTypeSize() == 1) { + if (header.getGLInternaFormat_Uncompressed() == ktx::GLInternalFormat_Uncompressed::RGBA8) { + mipFormat = Format::COLOR_RGBA_32; + texelFormat = Format::COLOR_RGBA_32; + } else if (header.getGLInternaFormat_Uncompressed() == ktx::GLInternalFormat_Uncompressed::SRGB8_ALPHA8) { + mipFormat = Format::COLOR_SRGBA_32; + texelFormat = Format::COLOR_SRGBA_32; + } else { + return false; + } + } else if (header.getGLFormat() == ktx::GLFormat::RED && header.getGLType() == ktx::GLType::UNSIGNED_BYTE && header.getTypeSize() == 1) { + mipFormat = Format::COLOR_R_8; + if (header.getGLInternaFormat_Uncompressed() == ktx::GLInternalFormat_Uncompressed::R8) { + texelFormat = Format::COLOR_R_8; + } else { + return false; + } + } else { + return false; + } + return true; +} diff --git a/libraries/ktx/CMakeLists.txt b/libraries/ktx/CMakeLists.txt new file mode 100644 index 0000000000..404660b247 --- /dev/null +++ b/libraries/ktx/CMakeLists.txt @@ -0,0 +1,3 @@ +set(TARGET_NAME ktx) +setup_hifi_library() +link_hifi_libraries() \ No newline at end of file diff --git a/libraries/ktx/src/ktx/KTX.cpp b/libraries/ktx/src/ktx/KTX.cpp new file mode 100644 index 0000000000..bbd4e1bc86 --- /dev/null +++ b/libraries/ktx/src/ktx/KTX.cpp @@ -0,0 +1,165 @@ +// +// KTX.cpp +// ktx/src/ktx +// +// Created by Zach Pomerantz on 2/08/2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "KTX.h" + +#include //min max and more + +using namespace ktx; + +uint32_t Header::evalPadding(size_t byteSize) { + //auto padding = byteSize % PACKING_SIZE; + // return (uint32_t) (padding ? PACKING_SIZE - padding : 0); + return (uint32_t) (3 - (byteSize + 3) % PACKING_SIZE);// padding ? PACKING_SIZE - padding : 0); +} + + +const Header::Identifier ktx::Header::IDENTIFIER {{ + 0xAB, 0x4B, 0x54, 0x58, 0x20, 0x31, 0x31, 0xBB, 0x0D, 0x0A, 0x1A, 0x0A +}}; + +Header::Header() { + memcpy(identifier, IDENTIFIER.data(), IDENTIFIER_LENGTH); +} + +uint32_t Header::evalMaxDimension() const { + return std::max(getPixelWidth(), std::max(getPixelHeight(), getPixelDepth())); +} + +uint32_t Header::evalPixelWidth(uint32_t level) const { + return std::max(getPixelWidth() >> level, 1U); +} +uint32_t Header::evalPixelHeight(uint32_t level) const { + return std::max(getPixelHeight() >> level, 1U); +} +uint32_t Header::evalPixelDepth(uint32_t level) const { + return std::max(getPixelDepth() >> level, 1U); +} + +size_t Header::evalPixelSize() const { + return glTypeSize; // Really we should generate the size from the FOrmat etc +} + +size_t Header::evalRowSize(uint32_t level) const { + auto pixWidth = evalPixelWidth(level); + auto pixSize = evalPixelSize(); + auto netSize = pixWidth * pixSize; + auto padding = evalPadding(netSize); + return netSize + padding; +} +size_t Header::evalFaceSize(uint32_t level) const { + auto pixHeight = evalPixelHeight(level); + auto pixDepth = evalPixelDepth(level); + auto rowSize = evalRowSize(level); + return pixDepth * pixHeight * rowSize; +} +size_t Header::evalImageSize(uint32_t level) const { + auto faceSize = evalFaceSize(level); + if (numberOfFaces == NUM_CUBEMAPFACES && numberOfArrayElements == 0) { + return faceSize; + } else { + return (getNumberOfSlices() * numberOfFaces * faceSize); + } +} + + +KeyValue::KeyValue(const std::string& key, uint32_t valueByteSize, const Byte* value) : + _byteSize((uint32_t) key.size() + 1 + valueByteSize), // keyString size + '\0' ending char + the value size + _key(key), + _value(valueByteSize) +{ + if (_value.size() && value) { + memcpy(_value.data(), value, valueByteSize); + } +} + +KeyValue::KeyValue(const std::string& key, const std::string& value) : + KeyValue(key, (uint32_t) value.size(), (const Byte*) value.data()) +{ + +} + +uint32_t KeyValue::serializedByteSize() const { + return (uint32_t) (sizeof(uint32_t) + _byteSize + Header::evalPadding(_byteSize)); +} + +uint32_t KeyValue::serializedKeyValuesByteSize(const KeyValues& keyValues) { + uint32_t keyValuesSize = 0; + for (auto& keyval : keyValues) { + keyValuesSize += keyval.serializedByteSize(); + } + return (keyValuesSize + Header::evalPadding(keyValuesSize)); +} + + +KTX::KTX() { +} + +KTX::~KTX() { +} + +void KTX::resetStorage(const StoragePointer& storage) { + _storage = storage; +} + +const Header* KTX::getHeader() const { + if (!_storage) { + return nullptr; + } + return reinterpret_cast(_storage->data()); +} + + +size_t KTX::getKeyValueDataSize() const { + if (_storage) { + return getHeader()->bytesOfKeyValueData; + } else { + return 0; + } +} + +size_t KTX::getTexelsDataSize() const { + if (_storage) { + //return _storage->size() - (sizeof(Header) + getKeyValueDataSize()); + return (_storage->data() + _storage->size()) - getTexelsData(); + } else { + return 0; + } +} + +const Byte* KTX::getKeyValueData() const { + if (_storage) { + return (_storage->data() + sizeof(Header)); + } else { + return nullptr; + } +} + +const Byte* KTX::getTexelsData() const { + if (_storage) { + return (_storage->data() + sizeof(Header) + getKeyValueDataSize()); + } else { + return nullptr; + } +} + +storage::StoragePointer KTX::getMipFaceTexelsData(uint16_t mip, uint8_t face) const { + storage::StoragePointer result; + if (mip < _images.size()) { + const auto& faces = _images[mip]; + if (face < faces._numFaces) { + auto faceOffset = faces._faceBytes[face] - _storage->data(); + auto faceSize = faces._faceSize; + result = _storage->createView(faceSize, faceOffset); + } + } + return result; +} diff --git a/libraries/ktx/src/ktx/KTX.h b/libraries/ktx/src/ktx/KTX.h new file mode 100644 index 0000000000..8e901b1105 --- /dev/null +++ b/libraries/ktx/src/ktx/KTX.h @@ -0,0 +1,494 @@ +// +// KTX.h +// ktx/src/ktx +// +// Created by Zach Pomerantz on 2/08/2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#pragma once +#ifndef hifi_ktx_KTX_h +#define hifi_ktx_KTX_h + +#include +#include +#include +#include +#include +#include +#include + +#include + +/* KTX Spec: + +Byte[12] identifier +UInt32 endianness +UInt32 glType +UInt32 glTypeSize +UInt32 glFormat +Uint32 glInternalFormat +Uint32 glBaseInternalFormat +UInt32 pixelWidth +UInt32 pixelHeight +UInt32 pixelDepth +UInt32 numberOfArrayElements +UInt32 numberOfFaces +UInt32 numberOfMipmapLevels +UInt32 bytesOfKeyValueData + +for each keyValuePair that fits in bytesOfKeyValueData + UInt32 keyAndValueByteSize + Byte keyAndValue[keyAndValueByteSize] + Byte valuePadding[3 - ((keyAndValueByteSize + 3) % 4)] +end + +for each mipmap_level in numberOfMipmapLevels* + UInt32 imageSize; + for each array_element in numberOfArrayElements* + for each face in numberOfFaces + for each z_slice in pixelDepth* + for each row or row_of_blocks in pixelHeight* + for each pixel or block_of_pixels in pixelWidth + Byte data[format-specific-number-of-bytes]** + end + end + end + Byte cubePadding[0-3] + end + end + Byte mipPadding[3 - ((imageSize + 3) % 4)] +end + +* Replace with 1 if this field is 0. + +** Uncompressed texture data matches a GL_UNPACK_ALIGNMENT of 4. +*/ + + + +namespace ktx { + const uint32_t PACKING_SIZE { sizeof(uint32_t) }; + using Byte = uint8_t; + + enum class GLType : uint32_t { + COMPRESSED_TYPE = 0, + + // GL 4.4 Table 8.2 + UNSIGNED_BYTE = 0x1401, + BYTE = 0x1400, + UNSIGNED_SHORT = 0x1403, + SHORT = 0x1402, + UNSIGNED_INT = 0x1405, + INT = 0x1404, + HALF_FLOAT = 0x140B, + FLOAT = 0x1406, + UNSIGNED_BYTE_3_3_2 = 0x8032, + UNSIGNED_BYTE_2_3_3_REV = 0x8362, + UNSIGNED_SHORT_5_6_5 = 0x8363, + UNSIGNED_SHORT_5_6_5_REV = 0x8364, + UNSIGNED_SHORT_4_4_4_4 = 0x8033, + UNSIGNED_SHORT_4_4_4_4_REV = 0x8365, + UNSIGNED_SHORT_5_5_5_1 = 0x8034, + UNSIGNED_SHORT_1_5_5_5_REV = 0x8366, + UNSIGNED_INT_8_8_8_8 = 0x8035, + UNSIGNED_INT_8_8_8_8_REV = 0x8367, + UNSIGNED_INT_10_10_10_2 = 0x8036, + UNSIGNED_INT_2_10_10_10_REV = 0x8368, + UNSIGNED_INT_24_8 = 0x84FA, + UNSIGNED_INT_10F_11F_11F_REV = 0x8C3B, + UNSIGNED_INT_5_9_9_9_REV = 0x8C3E, + FLOAT_32_UNSIGNED_INT_24_8_REV = 0x8DAD, + + NUM_GLTYPES = 25, + }; + + enum class GLFormat : uint32_t { + COMPRESSED_FORMAT = 0, + + // GL 4.4 Table 8.3 + STENCIL_INDEX = 0x1901, + DEPTH_COMPONENT = 0x1902, + DEPTH_STENCIL = 0x84F9, + + RED = 0x1903, + GREEN = 0x1904, + BLUE = 0x1905, + RG = 0x8227, + RGB = 0x1907, + RGBA = 0x1908, + BGR = 0x80E0, + BGRA = 0x80E1, + + RG_INTEGER = 0x8228, + RED_INTEGER = 0x8D94, + GREEN_INTEGER = 0x8D95, + BLUE_INTEGER = 0x8D96, + RGB_INTEGER = 0x8D98, + RGBA_INTEGER = 0x8D99, + BGR_INTEGER = 0x8D9A, + BGRA_INTEGER = 0x8D9B, + + NUM_GLFORMATS = 20, + }; + + enum class GLInternalFormat_Uncompressed : uint32_t { + // GL 4.4 Table 8.12 + R8 = 0x8229, + R8_SNORM = 0x8F94, + + R16 = 0x822A, + R16_SNORM = 0x8F98, + + RG8 = 0x822B, + RG8_SNORM = 0x8F95, + + RG16 = 0x822C, + RG16_SNORM = 0x8F99, + + R3_G3_B2 = 0x2A10, + RGB4 = 0x804F, + RGB5 = 0x8050, + RGB565 = 0x8D62, + + RGB8 = 0x8051, + RGB8_SNORM = 0x8F96, + RGB10 = 0x8052, + RGB12 = 0x8053, + + RGB16 = 0x8054, + RGB16_SNORM = 0x8F9A, + + RGBA2 = 0x8055, + RGBA4 = 0x8056, + RGB5_A1 = 0x8057, + RGBA8 = 0x8058, + RGBA8_SNORM = 0x8F97, + + RGB10_A2 = 0x8059, + RGB10_A2UI = 0x906F, + + RGBA12 = 0x805A, + RGBA16 = 0x805B, + RGBA16_SNORM = 0x8F9B, + + SRGB8 = 0x8C41, + SRGB8_ALPHA8 = 0x8C43, + + R16F = 0x822D, + RG16F = 0x822F, + RGB16F = 0x881B, + RGBA16F = 0x881A, + + R32F = 0x822E, + RG32F = 0x8230, + RGB32F = 0x8815, + RGBA32F = 0x8814, + + R11F_G11F_B10F = 0x8C3A, + RGB9_E5 = 0x8C3D, + + + R8I = 0x8231, + R8UI = 0x8232, + R16I = 0x8233, + R16UI = 0x8234, + R32I = 0x8235, + R32UI = 0x8236, + RG8I = 0x8237, + RG8UI = 0x8238, + RG16I = 0x8239, + RG16UI = 0x823A, + RG32I = 0x823B, + RG32UI = 0x823C, + + RGB8I = 0x8D8F, + RGB8UI = 0x8D7D, + RGB16I = 0x8D89, + RGB16UI = 0x8D77, + + RGB32I = 0x8D83, + RGB32UI = 0x8D71, + RGBA8I = 0x8D8E, + RGBA8UI = 0x8D7C, + RGBA16I = 0x8D88, + RGBA16UI = 0x8D76, + RGBA32I = 0x8D82, + + RGBA32UI = 0x8D70, + + // GL 4.4 Table 8.13 + DEPTH_COMPONENT16 = 0x81A5, + DEPTH_COMPONENT24 = 0x81A6, + DEPTH_COMPONENT32 = 0x81A7, + + DEPTH_COMPONENT32F = 0x8CAC, + DEPTH24_STENCIL8 = 0x88F0, + DEPTH32F_STENCIL8 = 0x8CAD, + + STENCIL_INDEX1 = 0x8D46, + STENCIL_INDEX4 = 0x8D47, + STENCIL_INDEX8 = 0x8D48, + STENCIL_INDEX16 = 0x8D49, + + NUM_UNCOMPRESSED_GLINTERNALFORMATS = 74, + }; + + enum class GLInternalFormat_Compressed : uint32_t { + // GL 4.4 Table 8.14 + COMPRESSED_RED = 0x8225, + COMPRESSED_RG = 0x8226, + COMPRESSED_RGB = 0x84ED, + COMPRESSED_RGBA = 0x84EE, + + COMPRESSED_SRGB = 0x8C48, + COMPRESSED_SRGB_ALPHA = 0x8C49, + + COMPRESSED_RED_RGTC1 = 0x8DBB, + COMPRESSED_SIGNED_RED_RGTC1 = 0x8DBC, + COMPRESSED_RG_RGTC2 = 0x8DBD, + COMPRESSED_SIGNED_RG_RGTC2 = 0x8DBE, + + COMPRESSED_RGBA_BPTC_UNORM = 0x8E8C, + COMPRESSED_SRGB_ALPHA_BPTC_UNORM = 0x8E8D, + COMPRESSED_RGB_BPTC_SIGNED_FLOAT = 0x8E8E, + COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT = 0x8E8F, + + COMPRESSED_RGB8_ETC2 = 0x9274, + COMPRESSED_SRGB8_ETC2 = 0x9275, + COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2 = 0x9276, + COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2 = 0x9277, + COMPRESSED_RGBA8_ETC2_EAC = 0x9278, + COMPRESSED_SRGB8_ALPHA8_ETC2_EAC = 0x9279, + + COMPRESSED_R11_EAC = 0x9270, + COMPRESSED_SIGNED_R11_EAC = 0x9271, + COMPRESSED_RG11_EAC = 0x9272, + COMPRESSED_SIGNED_RG11_EAC = 0x9273, + + NUM_COMPRESSED_GLINTERNALFORMATS = 24, + }; + + enum class GLBaseInternalFormat : uint32_t { + // GL 4.4 Table 8.11 + DEPTH_COMPONENT = 0x1902, + DEPTH_STENCIL = 0x84F9, + RED = 0x1903, + RG = 0x8227, + RGB = 0x1907, + RGBA = 0x1908, + STENCIL_INDEX = 0x1901, + + NUM_GLBASEINTERNALFORMATS = 7, + }; + + enum CubeMapFace { + POS_X = 0, + NEG_X = 1, + POS_Y = 2, + NEG_Y = 3, + POS_Z = 4, + NEG_Z = 5, + NUM_CUBEMAPFACES = 6, + }; + + using Storage = storage::Storage; + using StoragePointer = std::shared_ptr; + + // Header + struct Header { + static const size_t IDENTIFIER_LENGTH = 12; + using Identifier = std::array; + static const Identifier IDENTIFIER; + + static const uint32_t ENDIAN_TEST = 0x04030201; + static const uint32_t REVERSE_ENDIAN_TEST = 0x01020304; + + static uint32_t evalPadding(size_t byteSize); + + Header(); + + Byte identifier[IDENTIFIER_LENGTH]; + uint32_t endianness { ENDIAN_TEST }; + + uint32_t glType; + uint32_t glTypeSize { 0 }; + uint32_t glFormat; + uint32_t glInternalFormat; + uint32_t glBaseInternalFormat; + + uint32_t pixelWidth { 1 }; + uint32_t pixelHeight { 0 }; + uint32_t pixelDepth { 0 }; + uint32_t numberOfArrayElements { 0 }; + uint32_t numberOfFaces { 1 }; + uint32_t numberOfMipmapLevels { 1 }; + + uint32_t bytesOfKeyValueData { 0 }; + + uint32_t getPixelWidth() const { return (pixelWidth ? pixelWidth : 1); } + uint32_t getPixelHeight() const { return (pixelHeight ? pixelHeight : 1); } + uint32_t getPixelDepth() const { return (pixelDepth ? pixelDepth : 1); } + uint32_t getNumberOfSlices() const { return (numberOfArrayElements ? numberOfArrayElements : 1); } + uint32_t getNumberOfLevels() const { return (numberOfMipmapLevels ? numberOfMipmapLevels : 1); } + + uint32_t evalMaxDimension() const; + uint32_t evalPixelWidth(uint32_t level) const; + uint32_t evalPixelHeight(uint32_t level) const; + uint32_t evalPixelDepth(uint32_t level) const; + + size_t evalPixelSize() const; + size_t evalRowSize(uint32_t level) const; + size_t evalFaceSize(uint32_t level) const; + size_t evalImageSize(uint32_t level) const; + + void setUncompressed(GLType type, uint32_t typeSize, GLFormat format, GLInternalFormat_Uncompressed internalFormat, GLBaseInternalFormat baseInternalFormat) { + glType = (uint32_t) type; + glTypeSize = typeSize; + glFormat = (uint32_t) format; + glInternalFormat = (uint32_t) internalFormat; + glBaseInternalFormat = (uint32_t) baseInternalFormat; + } + void setCompressed(GLInternalFormat_Compressed internalFormat, GLBaseInternalFormat baseInternalFormat) { + glType = (uint32_t) GLType::COMPRESSED_TYPE; + glTypeSize = 1; + glFormat = (uint32_t) GLFormat::COMPRESSED_FORMAT; + glInternalFormat = (uint32_t) internalFormat; + glBaseInternalFormat = (uint32_t) baseInternalFormat; + } + + GLType getGLType() const { return (GLType)glType; } + uint32_t getTypeSize() const { return glTypeSize; } + GLFormat getGLFormat() const { return (GLFormat)glFormat; } + GLInternalFormat_Uncompressed getGLInternaFormat_Uncompressed() const { return (GLInternalFormat_Uncompressed)glInternalFormat; } + GLInternalFormat_Compressed getGLInternaFormat_Compressed() const { return (GLInternalFormat_Compressed)glInternalFormat; } + GLBaseInternalFormat getGLBaseInternalFormat() const { return (GLBaseInternalFormat)glBaseInternalFormat; } + + + void setDimensions(uint32_t width, uint32_t height = 0, uint32_t depth = 0, uint32_t numSlices = 0, uint32_t numFaces = 1) { + pixelWidth = (width > 0 ? width : 1); + pixelHeight = height; + pixelDepth = depth; + numberOfArrayElements = numSlices; + numberOfFaces = ((numFaces == 1) || (numFaces == NUM_CUBEMAPFACES) ? numFaces : 1); + } + void set1D(uint32_t width) { setDimensions(width); } + void set1DArray(uint32_t width, uint32_t numSlices) { setDimensions(width, 0, 0, (numSlices > 0 ? numSlices : 1)); } + void set2D(uint32_t width, uint32_t height) { setDimensions(width, height); } + void set2DArray(uint32_t width, uint32_t height, uint32_t numSlices) { setDimensions(width, height, 0, (numSlices > 0 ? numSlices : 1)); } + void set3D(uint32_t width, uint32_t height, uint32_t depth) { setDimensions(width, height, depth); } + void set3DArray(uint32_t width, uint32_t height, uint32_t depth, uint32_t numSlices) { setDimensions(width, height, depth, (numSlices > 0 ? numSlices : 1)); } + void setCube(uint32_t width, uint32_t height) { setDimensions(width, height, 0, 0, NUM_CUBEMAPFACES); } + void setCubeArray(uint32_t width, uint32_t height, uint32_t numSlices) { setDimensions(width, height, 0, (numSlices > 0 ? numSlices : 1), NUM_CUBEMAPFACES); } + + }; + + // Key Values + struct KeyValue { + uint32_t _byteSize { 0 }; + std::string _key; + std::vector _value; + + + KeyValue(const std::string& key, uint32_t valueByteSize, const Byte* value); + + KeyValue(const std::string& key, const std::string& value); + + uint32_t serializedByteSize() const; + + static KeyValue parseSerializedKeyAndValue(uint32_t srcSize, const Byte* srcBytes); + static uint32_t writeSerializedKeyAndValue(Byte* destBytes, uint32_t destByteSize, const KeyValue& keyval); + + using KeyValues = std::list; + static uint32_t serializedKeyValuesByteSize(const KeyValues& keyValues); + + }; + using KeyValues = KeyValue::KeyValues; + + + struct Image { + using FaceBytes = std::vector; + + uint32_t _numFaces{ 1 }; + uint32_t _imageSize; + uint32_t _faceSize; + uint32_t _padding; + FaceBytes _faceBytes; + + + Image(uint32_t imageSize, uint32_t padding, const Byte* bytes) : + _numFaces(1), + _imageSize(imageSize), + _faceSize(imageSize), + _padding(padding), + _faceBytes(1, bytes) {} + + Image(uint32_t pageSize, uint32_t padding, const FaceBytes& cubeFaceBytes) : + _numFaces(NUM_CUBEMAPFACES), + _imageSize(pageSize * NUM_CUBEMAPFACES), + _faceSize(pageSize), + _padding(padding) + { + if (cubeFaceBytes.size() == NUM_CUBEMAPFACES) { + _faceBytes = cubeFaceBytes; + } + } + }; + using Images = std::vector; + + class KTX { + void resetStorage(const StoragePointer& src); + + KTX(); + public: + + ~KTX(); + + // Define a KTX object manually to write it somewhere (in a file on disk?) + // This path allocate the Storage where to store header, keyvalues and copy mips + // Then COPY all the data + static std::unique_ptr create(const Header& header, const Images& images, const KeyValues& keyValues = KeyValues()); + + // Instead of creating a full Copy of the src data in a KTX object, the write serialization can be performed with the + // following two functions + // size_t sizeNeeded = KTX::evalStorageSize(header, images); + // + // //allocate a buffer of size "sizeNeeded" or map a file with enough capacity + // Byte* destBytes = new Byte[sizeNeeded]; + // + // // THen perform the writing of the src data to the destinnation buffer + // write(destBytes, sizeNeeded, header, images); + // + // This is exactly what is done in the create function + static size_t evalStorageSize(const Header& header, const Images& images, const KeyValues& keyValues = KeyValues()); + static size_t write(Byte* destBytes, size_t destByteSize, const Header& header, const Images& images, const KeyValues& keyValues = KeyValues()); + static size_t writeKeyValues(Byte* destBytes, size_t destByteSize, const KeyValues& keyValues); + static Images writeImages(Byte* destBytes, size_t destByteSize, const Images& images); + + // Parse a block of memory and create a KTX object from it + static std::unique_ptr create(const StoragePointer& src); + + static bool checkHeaderFromStorage(size_t srcSize, const Byte* srcBytes); + static KeyValues parseKeyValues(size_t srcSize, const Byte* srcBytes); + static Images parseImages(const Header& header, size_t srcSize, const Byte* srcBytes); + + // Access raw pointers to the main sections of the KTX + const Header* getHeader() const; + const Byte* getKeyValueData() const; + const Byte* getTexelsData() const; + storage::StoragePointer getMipFaceTexelsData(uint16_t mip = 0, uint8_t face = 0) const; + const StoragePointer& getStorage() const { return _storage; } + + size_t getKeyValueDataSize() const; + size_t getTexelsDataSize() const; + + StoragePointer _storage; + KeyValues _keyValues; + Images _images; + }; + +} + +#endif // hifi_ktx_KTX_h diff --git a/libraries/ktx/src/ktx/Reader.cpp b/libraries/ktx/src/ktx/Reader.cpp new file mode 100644 index 0000000000..277ce42e69 --- /dev/null +++ b/libraries/ktx/src/ktx/Reader.cpp @@ -0,0 +1,195 @@ +// +// Reader.cpp +// ktx/src/ktx +// +// Created by Zach Pomerantz on 2/08/2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#include "KTX.h" + +#include +#include +#include + +#ifndef _MSC_VER +#define NOEXCEPT noexcept +#else +#define NOEXCEPT +#endif + +namespace ktx { + class ReaderException: public std::exception { + public: + ReaderException(const std::string& explanation) : _explanation("KTX deserialization error: " + explanation) {} + const char* what() const NOEXCEPT override { return _explanation.c_str(); } + private: + const std::string _explanation; + }; + + bool checkEndianness(uint32_t endianness, bool& matching) { + switch (endianness) { + case Header::ENDIAN_TEST: { + matching = true; + return true; + } + break; + case Header::REVERSE_ENDIAN_TEST: + { + matching = false; + return true; + } + break; + default: + throw ReaderException("endianness field has invalid value"); + return false; + } + } + + bool checkIdentifier(const Byte* identifier) { + if (!(0 == memcmp(identifier, Header::IDENTIFIER.data(), Header::IDENTIFIER_LENGTH))) { + throw ReaderException("identifier field invalid"); + return false; + } + return true; + } + + bool KTX::checkHeaderFromStorage(size_t srcSize, const Byte* srcBytes) { + try { + // validation + if (srcSize < sizeof(Header)) { + throw ReaderException("length is too short for header"); + } + const Header* header = reinterpret_cast(srcBytes); + + checkIdentifier(header->identifier); + + bool endianMatch { true }; + checkEndianness(header->endianness, endianMatch); + + // TODO: endian conversion if !endianMatch - for now, this is for local use and is unnecessary + + + // TODO: calculated bytesOfTexData + if (srcSize < (sizeof(Header) + header->bytesOfKeyValueData)) { + throw ReaderException("length is too short for metadata"); + } + + size_t bytesOfTexData = 0; + if (srcSize < (sizeof(Header) + header->bytesOfKeyValueData + bytesOfTexData)) { + + throw ReaderException("length is too short for data"); + } + + return true; + } + catch (const ReaderException& e) { + qWarning() << e.what(); + return false; + } + } + + KeyValue KeyValue::parseSerializedKeyAndValue(uint32_t srcSize, const Byte* srcBytes) { + uint32_t keyAndValueByteSize; + memcpy(&keyAndValueByteSize, srcBytes, sizeof(uint32_t)); + if (keyAndValueByteSize + sizeof(uint32_t) > srcSize) { + throw ReaderException("invalid key-value size"); + } + auto keyValueBytes = srcBytes + sizeof(uint32_t); + + // find the first null character \0 and extract the key + uint32_t keyLength = 0; + while (reinterpret_cast(keyValueBytes)[++keyLength] != '\0') { + if (keyLength == keyAndValueByteSize) { + // key must be null-terminated, and there must be space for the value + throw ReaderException("invalid key-value " + std::string(reinterpret_cast(keyValueBytes), keyLength)); + } + } + uint32_t valueStartOffset = keyLength + 1; + + // parse the key-value + return KeyValue(std::string(reinterpret_cast(keyValueBytes), keyLength), + keyAndValueByteSize - valueStartOffset, keyValueBytes + valueStartOffset); + } + + KeyValues KTX::parseKeyValues(size_t srcSize, const Byte* srcBytes) { + KeyValues keyValues; + try { + auto src = srcBytes; + uint32_t length = (uint32_t) srcSize; + uint32_t offset = 0; + while (offset < length) { + auto keyValue = KeyValue::parseSerializedKeyAndValue(length - offset, src); + keyValues.emplace_back(keyValue); + + // advance offset/src + offset += keyValue.serializedByteSize(); + src += keyValue.serializedByteSize(); + } + } + catch (const ReaderException& e) { + qWarning() << e.what(); + } + return keyValues; + } + + Images KTX::parseImages(const Header& header, size_t srcSize, const Byte* srcBytes) { + Images images; + auto currentPtr = srcBytes; + auto numFaces = header.numberOfFaces; + + // Keep identifying new mip as long as we can at list query the next imageSize + while ((currentPtr - srcBytes) + sizeof(uint32_t) <= (srcSize)) { + + // Grab the imageSize coming up + size_t imageSize = *reinterpret_cast(currentPtr); + currentPtr += sizeof(uint32_t); + + // If enough data ahead then capture the pointer + if ((currentPtr - srcBytes) + imageSize <= (srcSize)) { + auto padding = Header::evalPadding(imageSize); + + if (numFaces == NUM_CUBEMAPFACES) { + size_t faceSize = imageSize / NUM_CUBEMAPFACES; + Image::FaceBytes faces(NUM_CUBEMAPFACES); + for (uint32_t face = 0; face < NUM_CUBEMAPFACES; face++) { + faces[face] = currentPtr; + currentPtr += faceSize; + } + images.emplace_back(Image((uint32_t) faceSize, padding, faces)); + currentPtr += padding; + } else { + images.emplace_back(Image((uint32_t) imageSize, padding, currentPtr)); + currentPtr += imageSize + padding; + } + } else { + break; + } + } + + return images; + } + + std::unique_ptr KTX::create(const StoragePointer& src) { + if (!src) { + return nullptr; + } + + if (!checkHeaderFromStorage(src->size(), src->data())) { + return nullptr; + } + + std::unique_ptr result(new KTX()); + result->resetStorage(src); + + // read metadata + result->_keyValues = parseKeyValues(result->getHeader()->bytesOfKeyValueData, result->getKeyValueData()); + + // populate image table + result->_images = parseImages(*result->getHeader(), result->getTexelsDataSize(), result->getTexelsData()); + + return result; + } +} diff --git a/libraries/ktx/src/ktx/Writer.cpp b/libraries/ktx/src/ktx/Writer.cpp new file mode 100644 index 0000000000..25b363d31b --- /dev/null +++ b/libraries/ktx/src/ktx/Writer.cpp @@ -0,0 +1,171 @@ +// +// Writer.cpp +// ktx/src/ktx +// +// Created by Zach Pomerantz on 2/08/2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#include "KTX.h" + + +#include +#include +#ifndef _MSC_VER +#define NOEXCEPT noexcept +#else +#define NOEXCEPT +#endif + +namespace ktx { + + class WriterException : public std::exception { + public: + WriterException(const std::string& explanation) : _explanation("KTX serialization error: " + explanation) {} + const char* what() const NOEXCEPT override { return _explanation.c_str(); } + private: + const std::string _explanation; + }; + + std::unique_ptr KTX::create(const Header& header, const Images& images, const KeyValues& keyValues) { + StoragePointer storagePointer; + { + auto storageSize = ktx::KTX::evalStorageSize(header, images, keyValues); + auto memoryStorage = new storage::MemoryStorage(storageSize); + ktx::KTX::write(memoryStorage->data(), memoryStorage->size(), header, images, keyValues); + storagePointer.reset(memoryStorage); + } + return create(storagePointer); + } + + size_t KTX::evalStorageSize(const Header& header, const Images& images, const KeyValues& keyValues) { + size_t storageSize = sizeof(Header); + + if (!keyValues.empty()) { + size_t keyValuesSize = KeyValue::serializedKeyValuesByteSize(keyValues); + storageSize += keyValuesSize; + } + + auto numMips = header.getNumberOfLevels(); + for (uint32_t l = 0; l < numMips; l++) { + if (images.size() > l) { + storageSize += sizeof(uint32_t); + storageSize += images[l]._imageSize; + storageSize += Header::evalPadding(images[l]._imageSize); + } + } + return storageSize; + } + + size_t KTX::write(Byte* destBytes, size_t destByteSize, const Header& header, const Images& srcImages, const KeyValues& keyValues) { + // Check again that we have enough destination capacity + if (!destBytes || (destByteSize < evalStorageSize(header, srcImages, keyValues))) { + return 0; + } + + auto currentDestPtr = destBytes; + // Header + auto destHeader = reinterpret_cast(currentDestPtr); + memcpy(currentDestPtr, &header, sizeof(Header)); + currentDestPtr += sizeof(Header); + + // KeyValues + if (!keyValues.empty()) { + destHeader->bytesOfKeyValueData = (uint32_t) writeKeyValues(currentDestPtr, destByteSize - sizeof(Header), keyValues); + } else { + // Make sure the header contains the right bytesOfKeyValueData size + destHeader->bytesOfKeyValueData = 0; + } + currentDestPtr += destHeader->bytesOfKeyValueData; + + // Images + auto destImages = writeImages(currentDestPtr, destByteSize - sizeof(Header) - destHeader->bytesOfKeyValueData, srcImages); + // We chould check here that the amoutn of dest IMages generated is the same as the source + + return destByteSize; + } + + uint32_t KeyValue::writeSerializedKeyAndValue(Byte* destBytes, uint32_t destByteSize, const KeyValue& keyval) { + uint32_t keyvalSize = keyval.serializedByteSize(); + if (keyvalSize > destByteSize) { + throw WriterException("invalid key-value size"); + } + + *((uint32_t*) destBytes) = keyval._byteSize; + + auto dest = destBytes + sizeof(uint32_t); + + auto keySize = keyval._key.size() + 1; // Add 1 for the '\0' character at the end of the string + memcpy(dest, keyval._key.data(), keySize); + dest += keySize; + + memcpy(dest, keyval._value.data(), keyval._value.size()); + + return keyvalSize; + } + + size_t KTX::writeKeyValues(Byte* destBytes, size_t destByteSize, const KeyValues& keyValues) { + size_t writtenByteSize = 0; + try { + auto dest = destBytes; + for (auto& keyval : keyValues) { + size_t keyvalSize = KeyValue::writeSerializedKeyAndValue(dest, (uint32_t) (destByteSize - writtenByteSize), keyval); + writtenByteSize += keyvalSize; + dest += keyvalSize; + } + } + catch (const WriterException& e) { + qWarning() << e.what(); + } + return writtenByteSize; + } + + Images KTX::writeImages(Byte* destBytes, size_t destByteSize, const Images& srcImages) { + Images destImages; + auto imagesDataPtr = destBytes; + if (!imagesDataPtr) { + return destImages; + } + auto allocatedImagesDataSize = destByteSize; + size_t currentDataSize = 0; + auto currentPtr = imagesDataPtr; + + for (uint32_t l = 0; l < srcImages.size(); l++) { + if (currentDataSize + sizeof(uint32_t) < allocatedImagesDataSize) { + size_t imageSize = srcImages[l]._imageSize; + *(reinterpret_cast (currentPtr)) = (uint32_t) imageSize; + currentPtr += sizeof(uint32_t); + currentDataSize += sizeof(uint32_t); + + // If enough data ahead then capture the copy source pointer + if (currentDataSize + imageSize <= (allocatedImagesDataSize)) { + auto padding = Header::evalPadding(imageSize); + + // Single face vs cubes + if (srcImages[l]._numFaces == 1) { + memcpy(currentPtr, srcImages[l]._faceBytes[0], imageSize); + destImages.emplace_back(Image((uint32_t) imageSize, padding, currentPtr)); + currentPtr += imageSize; + } else { + Image::FaceBytes faceBytes(NUM_CUBEMAPFACES); + auto faceSize = srcImages[l]._faceSize; + for (int face = 0; face < NUM_CUBEMAPFACES; face++) { + memcpy(currentPtr, srcImages[l]._faceBytes[face], faceSize); + faceBytes[face] = currentPtr; + currentPtr += faceSize; + } + destImages.emplace_back(Image(faceSize, padding, faceBytes)); + } + + currentPtr += padding; + currentDataSize += imageSize + padding; + } + } + } + + return destImages; + } + +} diff --git a/libraries/model-networking/CMakeLists.txt b/libraries/model-networking/CMakeLists.txt index ed8cd7b5f9..00aa17ff57 100644 --- a/libraries/model-networking/CMakeLists.txt +++ b/libraries/model-networking/CMakeLists.txt @@ -1,4 +1,4 @@ set(TARGET_NAME model-networking) setup_hifi_library() -link_hifi_libraries(shared networking model fbx) +link_hifi_libraries(shared networking model fbx ktx) diff --git a/libraries/model-networking/src/model-networking/KTXCache.cpp b/libraries/model-networking/src/model-networking/KTXCache.cpp new file mode 100644 index 0000000000..63d35fe4a4 --- /dev/null +++ b/libraries/model-networking/src/model-networking/KTXCache.cpp @@ -0,0 +1,47 @@ +// +// KTXCache.cpp +// libraries/model-networking/src +// +// Created by Zach Pomerantz on 2/22/2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "KTXCache.h" + +#include + +using File = cache::File; +using FilePointer = cache::FilePointer; + +KTXCache::KTXCache(const std::string& dir, const std::string& ext) : + FileCache(dir, ext) { + initialize(); +} + +KTXFilePointer KTXCache::writeFile(const char* data, Metadata&& metadata) { + FilePointer file = FileCache::writeFile(data, std::move(metadata)); + return std::static_pointer_cast(file); +} + +KTXFilePointer KTXCache::getFile(const Key& key) { + return std::static_pointer_cast(FileCache::getFile(key)); +} + +std::unique_ptr KTXCache::createFile(Metadata&& metadata, const std::string& filepath) { + qCInfo(file_cache) << "Wrote KTX" << metadata.key.c_str(); + return std::unique_ptr(new KTXFile(std::move(metadata), filepath)); +} + +KTXFile::KTXFile(Metadata&& metadata, const std::string& filepath) : + cache::File(std::move(metadata), filepath) {} + +std::unique_ptr KTXFile::getKTX() const { + ktx::StoragePointer storage = std::make_shared(getFilepath().c_str()); + if (*storage) { + return ktx::KTX::create(storage); + } + return {}; +} diff --git a/libraries/model-networking/src/model-networking/KTXCache.h b/libraries/model-networking/src/model-networking/KTXCache.h new file mode 100644 index 0000000000..4ef5e52721 --- /dev/null +++ b/libraries/model-networking/src/model-networking/KTXCache.h @@ -0,0 +1,51 @@ +// +// KTXCache.h +// libraries/model-networking/src +// +// Created by Zach Pomerantz 2/22/2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_KTXCache_h +#define hifi_KTXCache_h + +#include + +#include + +namespace ktx { + class KTX; +} + +class KTXFile; +using KTXFilePointer = std::shared_ptr; + +class KTXCache : public cache::FileCache { + Q_OBJECT + +public: + KTXCache(const std::string& dir, const std::string& ext); + + KTXFilePointer writeFile(const char* data, Metadata&& metadata); + KTXFilePointer getFile(const Key& key); + +protected: + std::unique_ptr createFile(Metadata&& metadata, const std::string& filepath) override final; +}; + +class KTXFile : public cache::File { + Q_OBJECT + +public: + std::unique_ptr getKTX() const; + +protected: + friend class KTXCache; + + KTXFile(Metadata&& metadata, const std::string& filepath); +}; + +#endif // hifi_KTXCache_h diff --git a/libraries/model-networking/src/model-networking/TextureCache.cpp b/libraries/model-networking/src/model-networking/TextureCache.cpp index 8a4e85cfe6..5dfaddd471 100644 --- a/libraries/model-networking/src/model-networking/TextureCache.cpp +++ b/libraries/model-networking/src/model-networking/TextureCache.cpp @@ -18,27 +18,37 @@ #include #include #include + +#if DEBUG_DUMP_TEXTURE_LOADS #include #include +#endif #include #include #include +#include + #include #include #include -#include #include "ModelNetworkingLogging.h" #include #include Q_LOGGING_CATEGORY(trace_resource_parse_image, "trace.resource.parse.image") +Q_LOGGING_CATEGORY(trace_resource_parse_image_raw, "trace.resource.parse.image.raw") +Q_LOGGING_CATEGORY(trace_resource_parse_image_ktx, "trace.resource.parse.image.ktx") -TextureCache::TextureCache() { +const std::string TextureCache::KTX_DIRNAME { "ktx_cache" }; +const std::string TextureCache::KTX_EXT { "ktx" }; + +TextureCache::TextureCache() : + _ktxCache(KTX_DIRNAME, KTX_EXT) { setUnusedResourceCacheSize(0); setObjectName("TextureCache"); @@ -61,7 +71,7 @@ TextureCache::~TextureCache() { // this list taken from Ken Perlin's Improved Noise reference implementation (orig. in Java) at // http://mrl.nyu.edu/~perlin/noise/ -const int permutation[256] = +const int permutation[256] = { 151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23, 190, 6, 148, @@ -108,7 +118,8 @@ const gpu::TexturePointer& TextureCache::getPermutationNormalTexture() { } _permutationNormalTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element(gpu::VEC3, gpu::NUINT8, gpu::RGB), 256, 2)); - _permutationNormalTexture->assignStoredMip(0, _blueTexture->getTexelFormat(), sizeof(data), data); + _permutationNormalTexture->setStoredMipFormat(_permutationNormalTexture->getTexelFormat()); + _permutationNormalTexture->assignStoredMip(0, sizeof(data), data); } return _permutationNormalTexture; } @@ -120,36 +131,40 @@ const unsigned char OPAQUE_BLACK[] = { 0x00, 0x00, 0x00, 0xFF }; const gpu::TexturePointer& TextureCache::getWhiteTexture() { if (!_whiteTexture) { - _whiteTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, 1, 1)); + _whiteTexture = gpu::TexturePointer(gpu::Texture::createStrict(gpu::Element::COLOR_RGBA_32, 1, 1)); _whiteTexture->setSource("TextureCache::_whiteTexture"); - _whiteTexture->assignStoredMip(0, _whiteTexture->getTexelFormat(), sizeof(OPAQUE_WHITE), OPAQUE_WHITE); + _whiteTexture->setStoredMipFormat(_whiteTexture->getTexelFormat()); + _whiteTexture->assignStoredMip(0, sizeof(OPAQUE_WHITE), OPAQUE_WHITE); } return _whiteTexture; } const gpu::TexturePointer& TextureCache::getGrayTexture() { if (!_grayTexture) { - _grayTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, 1, 1)); + _grayTexture = gpu::TexturePointer(gpu::Texture::createStrict(gpu::Element::COLOR_RGBA_32, 1, 1)); _grayTexture->setSource("TextureCache::_grayTexture"); - _grayTexture->assignStoredMip(0, _grayTexture->getTexelFormat(), sizeof(OPAQUE_GRAY), OPAQUE_GRAY); + _grayTexture->setStoredMipFormat(_grayTexture->getTexelFormat()); + _grayTexture->assignStoredMip(0, sizeof(OPAQUE_GRAY), OPAQUE_GRAY); } return _grayTexture; } const gpu::TexturePointer& TextureCache::getBlueTexture() { if (!_blueTexture) { - _blueTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, 1, 1)); + _blueTexture = gpu::TexturePointer(gpu::Texture::createStrict(gpu::Element::COLOR_RGBA_32, 1, 1)); _blueTexture->setSource("TextureCache::_blueTexture"); - _blueTexture->assignStoredMip(0, _blueTexture->getTexelFormat(), sizeof(OPAQUE_BLUE), OPAQUE_BLUE); + _blueTexture->setStoredMipFormat(_blueTexture->getTexelFormat()); + _blueTexture->assignStoredMip(0, sizeof(OPAQUE_BLUE), OPAQUE_BLUE); } return _blueTexture; } const gpu::TexturePointer& TextureCache::getBlackTexture() { if (!_blackTexture) { - _blackTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, 1, 1)); + _blackTexture = gpu::TexturePointer(gpu::Texture::createStrict(gpu::Element::COLOR_RGBA_32, 1, 1)); _blackTexture->setSource("TextureCache::_blackTexture"); - _blackTexture->assignStoredMip(0, _blackTexture->getTexelFormat(), sizeof(OPAQUE_BLACK), OPAQUE_BLACK); + _blackTexture->setStoredMipFormat(_blackTexture->getTexelFormat()); + _blackTexture->assignStoredMip(0, sizeof(OPAQUE_BLACK), OPAQUE_BLACK); } return _blackTexture; } @@ -173,6 +188,72 @@ NetworkTexturePointer TextureCache::getTexture(const QUrl& url, Type type, const return ResourceCache::getResource(url, QUrl(), &extra).staticCast(); } +gpu::TexturePointer TextureCache::getTextureByHash(const std::string& hash) { + std::weak_ptr weakPointer; + { + std::unique_lock lock(_texturesByHashesMutex); + weakPointer = _texturesByHashes[hash]; + } + auto result = weakPointer.lock(); + if (result) { + qCWarning(modelnetworking) << "QQQ Returning live texture for hash " << hash.c_str(); + } + return result; +} + +gpu::TexturePointer TextureCache::cacheTextureByHash(const std::string& hash, const gpu::TexturePointer& texture) { + gpu::TexturePointer result; + { + std::unique_lock lock(_texturesByHashesMutex); + result = _texturesByHashes[hash].lock(); + if (!result) { + _texturesByHashes[hash] = texture; + result = texture; + } else { + qCWarning(modelnetworking) << "QQQ Swapping out texture with previous live texture in hash " << hash.c_str(); + } + } + return result; +} + + +gpu::TexturePointer getFallbackTextureForType(NetworkTexture::Type type) { + gpu::TexturePointer result; + auto textureCache = DependencyManager::get(); + // Since this can be called on a background thread, there's a chance that the cache + // will be destroyed by the time we request it + if (!textureCache) { + return result; + } + switch (type) { + case NetworkTexture::DEFAULT_TEXTURE: + case NetworkTexture::ALBEDO_TEXTURE: + case NetworkTexture::ROUGHNESS_TEXTURE: + case NetworkTexture::OCCLUSION_TEXTURE: + result = textureCache->getWhiteTexture(); + break; + + case NetworkTexture::NORMAL_TEXTURE: + result = textureCache->getBlueTexture(); + break; + + case NetworkTexture::EMISSIVE_TEXTURE: + case NetworkTexture::LIGHTMAP_TEXTURE: + result = textureCache->getBlackTexture(); + break; + + case NetworkTexture::BUMP_TEXTURE: + case NetworkTexture::SPECULAR_TEXTURE: + case NetworkTexture::GLOSS_TEXTURE: + case NetworkTexture::CUBE_TEXTURE: + case NetworkTexture::CUSTOM_TEXTURE: + case NetworkTexture::STRICT_TEXTURE: + default: + break; + } + return result; +} + NetworkTexture::TextureLoaderFunc getTextureLoaderForType(NetworkTexture::Type type, const QVariantMap& options = QVariantMap()) { @@ -219,11 +300,16 @@ NetworkTexture::TextureLoaderFunc getTextureLoaderForType(NetworkTexture::Type t return model::TextureUsage::createMetallicTextureFromImage; break; } + case Type::STRICT_TEXTURE: { + return model::TextureUsage::createStrict2DTextureFromImage; + break; + } case Type::CUSTOM_TEXTURE: { Q_ASSERT(false); return NetworkTexture::TextureLoaderFunc(); break; } + case Type::DEFAULT_TEXTURE: default: { return model::TextureUsage::create2DTextureFromImage; @@ -245,8 +331,8 @@ QSharedPointer TextureCache::createResource(const QUrl& url, const QSh auto type = textureExtra ? textureExtra->type : Type::DEFAULT_TEXTURE; auto content = textureExtra ? textureExtra->content : QByteArray(); auto maxNumPixels = textureExtra ? textureExtra->maxNumPixels : ABSOLUTE_MAX_TEXTURE_NUM_PIXELS; - return QSharedPointer(new NetworkTexture(url, type, content, maxNumPixels), - &Resource::deleter); + NetworkTexture* texture = new NetworkTexture(url, type, content, maxNumPixels); + return QSharedPointer(texture, &Resource::deleter); } NetworkTexture::NetworkTexture(const QUrl& url, Type type, const QByteArray& content, int maxNumPixels) : @@ -260,7 +346,6 @@ NetworkTexture::NetworkTexture(const QUrl& url, Type type, const QByteArray& con _loaded = true; } - std::string theName = url.toString().toStdString(); // if we have content, load it after we have our self pointer if (!content.isEmpty()) { _startedLoading = true; @@ -268,12 +353,6 @@ NetworkTexture::NetworkTexture(const QUrl& url, Type type, const QByteArray& con } } -NetworkTexture::NetworkTexture(const QUrl& url, const TextureLoaderFunc& textureLoader, const QByteArray& content) : - NetworkTexture(url, CUSTOM_TEXTURE, content, ABSOLUTE_MAX_TEXTURE_NUM_PIXELS) -{ - _textureLoader = textureLoader; -} - NetworkTexture::TextureLoaderFunc NetworkTexture::getTextureLoader() const { if (_type == CUSTOM_TEXTURE) { return _textureLoader; @@ -281,149 +360,6 @@ NetworkTexture::TextureLoaderFunc NetworkTexture::getTextureLoader() const { return getTextureLoaderForType(_type); } - -class ImageReader : public QRunnable { -public: - - ImageReader(const QWeakPointer& resource, const QByteArray& data, - const QUrl& url = QUrl(), int maxNumPixels = ABSOLUTE_MAX_TEXTURE_NUM_PIXELS); - - virtual void run() override; - -private: - static void listSupportedImageFormats(); - - QWeakPointer _resource; - QUrl _url; - QByteArray _content; - int _maxNumPixels; -}; - -void NetworkTexture::downloadFinished(const QByteArray& data) { - // send the reader off to the thread pool - QThreadPool::globalInstance()->start(new ImageReader(_self, data, _url)); -} - -void NetworkTexture::loadContent(const QByteArray& content) { - QThreadPool::globalInstance()->start(new ImageReader(_self, content, _url, _maxNumPixels)); -} - -ImageReader::ImageReader(const QWeakPointer& resource, const QByteArray& data, - const QUrl& url, int maxNumPixels) : - _resource(resource), - _url(url), - _content(data), - _maxNumPixels(maxNumPixels) -{ -#if DEBUG_DUMP_TEXTURE_LOADS - static auto start = usecTimestampNow() / USECS_PER_MSEC; - auto now = usecTimestampNow() / USECS_PER_MSEC - start; - QString urlStr = _url.toString(); - auto dot = urlStr.lastIndexOf("."); - QString outFileName = QString(QCryptographicHash::hash(urlStr.toLocal8Bit(), QCryptographicHash::Md5).toHex()) + urlStr.right(urlStr.length() - dot); - QFile loadRecord("h:/textures/loads.txt"); - loadRecord.open(QFile::Text | QFile::Append | QFile::ReadWrite); - loadRecord.write(QString("%1 %2\n").arg(now).arg(outFileName).toLocal8Bit()); - outFileName = "h:/textures/" + outFileName; - QFileInfo outInfo(outFileName); - if (!outInfo.exists()) { - QFile outFile(outFileName); - outFile.open(QFile::WriteOnly | QFile::Truncate); - outFile.write(data); - outFile.close(); - } -#endif - DependencyManager::get()->incrementStat("PendingProcessing"); -} - -void ImageReader::listSupportedImageFormats() { - static std::once_flag once; - std::call_once(once, []{ - auto supportedFormats = QImageReader::supportedImageFormats(); - qCDebug(modelnetworking) << "List of supported Image formats:" << supportedFormats.join(", "); - }); -} - -void ImageReader::run() { - DependencyManager::get()->decrementStat("PendingProcessing"); - - CounterStat counter("Processing"); - - PROFILE_RANGE_EX(resource_parse_image, __FUNCTION__, 0xffff0000, 0, { { "url", _url.toString() } }); - auto originalPriority = QThread::currentThread()->priority(); - if (originalPriority == QThread::InheritPriority) { - originalPriority = QThread::NormalPriority; - } - QThread::currentThread()->setPriority(QThread::LowPriority); - Finally restorePriority([originalPriority]{ - QThread::currentThread()->setPriority(originalPriority); - }); - - if (!_resource.data()) { - qCWarning(modelnetworking) << "Abandoning load of" << _url << "; could not get strong ref"; - return; - } - listSupportedImageFormats(); - - // Help the QImage loader by extracting the image file format from the url filename ext. - // Some tga are not created properly without it. - auto filename = _url.fileName().toStdString(); - auto filenameExtension = filename.substr(filename.find_last_of('.') + 1); - QImage image = QImage::fromData(_content, filenameExtension.c_str()); - - // Note that QImage.format is the pixel format which is different from the "format" of the image file... - auto imageFormat = image.format(); - int imageWidth = image.width(); - int imageHeight = image.height(); - - if (imageWidth == 0 || imageHeight == 0 || imageFormat == QImage::Format_Invalid) { - if (filenameExtension.empty()) { - qCDebug(modelnetworking) << "QImage failed to create from content, no file extension:" << _url; - } else { - qCDebug(modelnetworking) << "QImage failed to create from content" << _url; - } - return; - } - - if (imageWidth * imageHeight > _maxNumPixels) { - float scaleFactor = sqrtf(_maxNumPixels / (float)(imageWidth * imageHeight)); - int originalWidth = imageWidth; - int originalHeight = imageHeight; - imageWidth = (int)(scaleFactor * (float)imageWidth + 0.5f); - imageHeight = (int)(scaleFactor * (float)imageHeight + 0.5f); - QImage newImage = image.scaled(QSize(imageWidth, imageHeight), Qt::IgnoreAspectRatio, Qt::SmoothTransformation); - image.swap(newImage); - qCDebug(modelnetworking) << "Downscale image" << _url - << "from" << originalWidth << "x" << originalHeight - << "to" << imageWidth << "x" << imageHeight; - } - - gpu::TexturePointer texture = nullptr; - { - // Double-check the resource still exists between long operations. - auto resource = _resource.toStrongRef(); - if (!resource) { - qCWarning(modelnetworking) << "Abandoning load of" << _url << "; could not get strong ref"; - return; - } - - auto url = _url.toString().toStdString(); - - PROFILE_RANGE_EX(resource_parse_image, __FUNCTION__, 0xffffff00, 0); - texture.reset(resource.dynamicCast()->getTextureLoader()(image, url)); - } - - // Ensure the resource has not been deleted - auto resource = _resource.toStrongRef(); - if (!resource) { - qCWarning(modelnetworking) << "Abandoning load of" << _url << "; could not get strong ref"; - } else { - QMetaObject::invokeMethod(resource.data(), "setImage", - Q_ARG(gpu::TexturePointer, texture), - Q_ARG(int, imageWidth), Q_ARG(int, imageHeight)); - } -} - void NetworkTexture::setImage(gpu::TexturePointer texture, int originalWidth, int originalHeight) { _originalWidth = originalWidth; @@ -446,3 +382,231 @@ void NetworkTexture::setImage(gpu::TexturePointer texture, int originalWidth, emit networkTextureCreated(qWeakPointerCast (_self)); } + +gpu::TexturePointer NetworkTexture::getFallbackTexture() const { + if (_type == CUSTOM_TEXTURE) { + return gpu::TexturePointer(); + } + return getFallbackTextureForType(_type); +} + +class Reader : public QRunnable { +public: + Reader(const QWeakPointer& resource, const QUrl& url); + void run() override final; + virtual void read() = 0; + +protected: + QWeakPointer _resource; + QUrl _url; +}; + +class ImageReader : public Reader { +public: + ImageReader(const QWeakPointer& resource, const QUrl& url, + const QByteArray& data, const std::string& hash, int maxNumPixels); + void read() override final; + +private: + static void listSupportedImageFormats(); + + QByteArray _content; + std::string _hash; + int _maxNumPixels; +}; + +void NetworkTexture::downloadFinished(const QByteArray& data) { + loadContent(data); +} + +void NetworkTexture::loadContent(const QByteArray& content) { + // Hash the source image to for KTX caching + std::string hash; + { + QCryptographicHash hasher(QCryptographicHash::Md5); + hasher.addData(content); + hash = hasher.result().toHex().toStdString(); + } + + auto textureCache = static_cast(_cache.data()); + + if (textureCache != nullptr) { + // If we already have a live texture with the same hash, use it + auto texture = textureCache->getTextureByHash(hash); + + // If there is no live texture, check if there's an existing KTX file + if (!texture) { + KTXFilePointer ktxFile = textureCache->_ktxCache.getFile(hash); + if (ktxFile) { + // Ensure that the KTX deserialization worked + auto ktx = ktxFile->getKTX(); + if (ktx) { + texture.reset(gpu::Texture::unserialize(ktx)); + // Ensure that the texture population worked + if (texture) { + texture->setKtxBacking(ktx); + texture = textureCache->cacheTextureByHash(hash, texture); + } + } + } + } + + // If we found the texture either because it's in use or via KTX deserialization, + // set the image and return immediately. + if (texture) { + setImage(texture, texture->getWidth(), texture->getHeight()); + return; + } + } + + // We failed to find an existing live or KTX texture, so trigger an image reader + QThreadPool::globalInstance()->start(new ImageReader(_self, _url, content, hash, _maxNumPixels)); +} + +Reader::Reader(const QWeakPointer& resource, const QUrl& url) : + _resource(resource), _url(url) { + DependencyManager::get()->incrementStat("PendingProcessing"); +} + +void Reader::run() { + PROFILE_RANGE_EX(resource_parse_image, __FUNCTION__, 0xffff0000, 0, { { "url", _url.toString() } }); + DependencyManager::get()->decrementStat("PendingProcessing"); + CounterStat counter("Processing"); + + auto originalPriority = QThread::currentThread()->priority(); + if (originalPriority == QThread::InheritPriority) { + originalPriority = QThread::NormalPriority; + } + QThread::currentThread()->setPriority(QThread::LowPriority); + Finally restorePriority([originalPriority]{ QThread::currentThread()->setPriority(originalPriority); }); + + if (!_resource.data()) { + qCWarning(modelnetworking) << "Abandoning load of" << _url << "; could not get strong ref"; + return; + } + + read(); +} + +ImageReader::ImageReader(const QWeakPointer& resource, const QUrl& url, + const QByteArray& data, const std::string& hash, int maxNumPixels) : + Reader(resource, url), _content(data), _hash(hash), _maxNumPixels(maxNumPixels) { + listSupportedImageFormats(); + +#if DEBUG_DUMP_TEXTURE_LOADS + static auto start = usecTimestampNow() / USECS_PER_MSEC; + auto now = usecTimestampNow() / USECS_PER_MSEC - start; + QString urlStr = _url.toString(); + auto dot = urlStr.lastIndexOf("."); + QString outFileName = QString(QCryptographicHash::hash(urlStr.toLocal8Bit(), QCryptographicHash::Md5).toHex()) + urlStr.right(urlStr.length() - dot); + QFile loadRecord("h:/textures/loads.txt"); + loadRecord.open(QFile::Text | QFile::Append | QFile::ReadWrite); + loadRecord.write(QString("%1 %2\n").arg(now).arg(outFileName).toLocal8Bit()); + outFileName = "h:/textures/" + outFileName; + QFileInfo outInfo(outFileName); + if (!outInfo.exists()) { + QFile outFile(outFileName); + outFile.open(QFile::WriteOnly | QFile::Truncate); + outFile.write(data); + outFile.close(); + } +#endif +} + +void ImageReader::listSupportedImageFormats() { + static std::once_flag once; + std::call_once(once, []{ + auto supportedFormats = QImageReader::supportedImageFormats(); + qCDebug(modelnetworking) << "List of supported Image formats:" << supportedFormats.join(", "); + }); +} + +void ImageReader::read() { + // Help the QImage loader by extracting the image file format from the url filename ext. + // Some tga are not created properly without it. + auto filename = _url.fileName().toStdString(); + auto filenameExtension = filename.substr(filename.find_last_of('.') + 1); + QImage image = QImage::fromData(_content, filenameExtension.c_str()); + int imageWidth = image.width(); + int imageHeight = image.height(); + + // Validate that the image loaded + if (imageWidth == 0 || imageHeight == 0 || image.format() == QImage::Format_Invalid) { + QString reason(filenameExtension.empty() ? "" : "(no file extension)"); + qCWarning(modelnetworking) << "Failed to load" << _url << reason; + return; + } + + // Validate the image is less than _maxNumPixels, and downscale if necessary + if (imageWidth * imageHeight > _maxNumPixels) { + float scaleFactor = sqrtf(_maxNumPixels / (float)(imageWidth * imageHeight)); + int originalWidth = imageWidth; + int originalHeight = imageHeight; + imageWidth = (int)(scaleFactor * (float)imageWidth + 0.5f); + imageHeight = (int)(scaleFactor * (float)imageHeight + 0.5f); + QImage newImage = image.scaled(QSize(imageWidth, imageHeight), Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + image.swap(newImage); + qCDebug(modelnetworking).nospace() << "Downscaled " << _url << " (" << + QSize(originalWidth, originalHeight) << " to " << + QSize(imageWidth, imageHeight) << ")"; + } + + gpu::TexturePointer texture = nullptr; + { + auto resource = _resource.lock(); // to ensure the resource is still needed + if (!resource) { + qCDebug(modelnetworking) << _url << "loading stopped; resource out of scope"; + return; + } + + auto url = _url.toString().toStdString(); + + PROFILE_RANGE_EX(resource_parse_image_raw, __FUNCTION__, 0xffff0000, 0); + // Load the image into a gpu::Texture + auto networkTexture = resource.staticCast(); + texture.reset(networkTexture->getTextureLoader()(image, url)); + texture->setSource(url); + if (texture) { + texture->setFallbackTexture(networkTexture->getFallbackTexture()); + } + + auto textureCache = DependencyManager::get(); + // Save the image into a KTXFile + auto memKtx = gpu::Texture::serialize(*texture); + if (!memKtx) { + qCWarning(modelnetworking) << "Unable to serialize texture to KTX " << _url; + } + + if (memKtx && textureCache) { + const char* data = reinterpret_cast(memKtx->_storage->data()); + size_t length = memKtx->_storage->size(); + KTXFilePointer file; + auto& ktxCache = textureCache->_ktxCache; + if (!memKtx || !(file = ktxCache.writeFile(data, KTXCache::Metadata(_hash, length)))) { + qCWarning(modelnetworking) << _url << "file cache failed"; + } else { + resource.staticCast()->_file = file; + auto fileKtx = file->getKTX(); + if (fileKtx) { + texture->setKtxBacking(fileKtx); + } + } + } + + // We replace the texture with the one stored in the cache. This deals with the possible race condition of two different + // images with the same hash being loaded concurrently. Only one of them will make it into the cache by hash first and will + // be the winner + if (textureCache) { + texture = textureCache->cacheTextureByHash(_hash, texture); + } + } + + auto resource = _resource.lock(); // to ensure the resource is still needed + if (resource) { + QMetaObject::invokeMethod(resource.data(), "setImage", + Q_ARG(gpu::TexturePointer, texture), + Q_ARG(int, imageWidth), Q_ARG(int, imageHeight)); + } else { + qCDebug(modelnetworking) << _url << "loading stopped; resource out of scope"; + } +} diff --git a/libraries/model-networking/src/model-networking/TextureCache.h b/libraries/model-networking/src/model-networking/TextureCache.h index 77311afae6..6005cc1226 100644 --- a/libraries/model-networking/src/model-networking/TextureCache.h +++ b/libraries/model-networking/src/model-networking/TextureCache.h @@ -23,6 +23,8 @@ #include #include +#include "KTXCache.h" + const int ABSOLUTE_MAX_TEXTURE_NUM_PIXELS = 8192 * 8192; namespace gpu { @@ -43,6 +45,7 @@ class NetworkTexture : public Resource, public Texture { public: enum Type { DEFAULT_TEXTURE, + STRICT_TEXTURE, ALBEDO_TEXTURE, NORMAL_TEXTURE, BUMP_TEXTURE, @@ -63,7 +66,6 @@ public: using TextureLoaderFunc = std::function; NetworkTexture(const QUrl& url, Type type, const QByteArray& content, int maxNumPixels); - NetworkTexture(const QUrl& url, const TextureLoaderFunc& textureLoader, const QByteArray& content); QString getType() const override { return "NetworkTexture"; } @@ -74,12 +76,12 @@ public: Type getTextureType() const { return _type; } TextureLoaderFunc getTextureLoader() const; + gpu::TexturePointer getFallbackTexture() const; signals: void networkTextureCreated(const QWeakPointer& self); protected: - virtual bool isCacheable() const override { return _loaded; } virtual void downloadFinished(const QByteArray& data) override; @@ -88,8 +90,12 @@ protected: Q_INVOKABLE void setImage(gpu::TexturePointer texture, int originalWidth, int originalHeight); private: + friend class KTXReader; + friend class ImageReader; + Type _type; TextureLoaderFunc _textureLoader { [](const QImage&, const std::string&){ return nullptr; } }; + KTXFilePointer _file; int _originalWidth { 0 }; int _originalHeight { 0 }; int _width { 0 }; @@ -131,6 +137,10 @@ public: NetworkTexturePointer getTexture(const QUrl& url, Type type = Type::DEFAULT_TEXTURE, const QByteArray& content = QByteArray(), int maxNumPixels = ABSOLUTE_MAX_TEXTURE_NUM_PIXELS); + + gpu::TexturePointer getTextureByHash(const std::string& hash); + gpu::TexturePointer cacheTextureByHash(const std::string& hash, const gpu::TexturePointer& texture); + protected: // Overload ResourceCache::prefetch to allow specifying texture type for loads Q_INVOKABLE ScriptableResource* prefetch(const QUrl& url, int type, int maxNumPixels = ABSOLUTE_MAX_TEXTURE_NUM_PIXELS); @@ -139,9 +149,19 @@ protected: const void* extra) override; private: + friend class ImageReader; + friend class NetworkTexture; + friend class DilatableNetworkTexture; + TextureCache(); virtual ~TextureCache(); - friend class DilatableNetworkTexture; + + static const std::string KTX_DIRNAME; + static const std::string KTX_EXT; + KTXCache _ktxCache; + // Map from image hashes to texture weak pointers + std::unordered_map> _texturesByHashes; + std::mutex _texturesByHashesMutex; gpu::TexturePointer _permutationNormalTexture; gpu::TexturePointer _whiteTexture; diff --git a/libraries/model/CMakeLists.txt b/libraries/model/CMakeLists.txt index 63f632e484..021aa3d027 100755 --- a/libraries/model/CMakeLists.txt +++ b/libraries/model/CMakeLists.txt @@ -1,5 +1,5 @@ set(TARGET_NAME model) AUTOSCRIBE_SHADER_LIB(gpu model) setup_hifi_library() -link_hifi_libraries(shared gpu) +link_hifi_libraries(shared ktx gpu) diff --git a/libraries/model/src/model/Geometry.cpp b/libraries/model/src/model/Geometry.cpp index 2bb6cfa436..04b0db92d3 100755 --- a/libraries/model/src/model/Geometry.cpp +++ b/libraries/model/src/model/Geometry.cpp @@ -117,7 +117,7 @@ Box Mesh::evalPartsBound(int partStart, int partEnd) const { auto partItEnd = _partBuffer.cbegin() + partEnd; for (;part != partItEnd; part++) { - + Box partBound; auto index = _indexBuffer.cbegin() + (*part)._startIndex; auto endIndex = index + (*part)._numIndices; @@ -134,6 +134,115 @@ Box Mesh::evalPartsBound(int partStart, int partEnd) const { return totalBound; } + +model::MeshPointer Mesh::map(std::function vertexFunc, + std::function normalFunc, + std::function indexFunc) { + // vertex data + const gpu::BufferView& vertexBufferView = getVertexBuffer(); + gpu::BufferView::Index numVertices = (gpu::BufferView::Index)getNumVertices(); + gpu::Resource::Size vertexSize = numVertices * sizeof(glm::vec3); + unsigned char* resultVertexData = new unsigned char[vertexSize]; + unsigned char* vertexDataCursor = resultVertexData; + + for (gpu::BufferView::Index i = 0; i < numVertices; i ++) { + glm::vec3 pos = vertexFunc(vertexBufferView.get(i)); + memcpy(vertexDataCursor, &pos, sizeof(pos)); + vertexDataCursor += sizeof(pos); + } + + // normal data + int attributeTypeNormal = gpu::Stream::InputSlot::NORMAL; // libraries/gpu/src/gpu/Stream.h + const gpu::BufferView& normalsBufferView = getAttributeBuffer(attributeTypeNormal); + gpu::BufferView::Index numNormals = (gpu::BufferView::Index)normalsBufferView.getNumElements(); + gpu::Resource::Size normalSize = numNormals * sizeof(glm::vec3); + unsigned char* resultNormalData = new unsigned char[normalSize]; + unsigned char* normalDataCursor = resultNormalData; + + for (gpu::BufferView::Index i = 0; i < numNormals; i ++) { + glm::vec3 normal = normalFunc(normalsBufferView.get(i)); + memcpy(normalDataCursor, &normal, sizeof(normal)); + normalDataCursor += sizeof(normal); + } + // TODO -- other attributes + + // face data + const gpu::BufferView& indexBufferView = getIndexBuffer(); + gpu::BufferView::Index numIndexes = (gpu::BufferView::Index)getNumIndices(); + gpu::Resource::Size indexSize = numIndexes * sizeof(uint32_t); + unsigned char* resultIndexData = new unsigned char[indexSize]; + unsigned char* indexDataCursor = resultIndexData; + + for (gpu::BufferView::Index i = 0; i < numIndexes; i ++) { + uint32_t index = indexFunc(indexBufferView.get(i)); + memcpy(indexDataCursor, &index, sizeof(index)); + indexDataCursor += sizeof(index); + } + + model::MeshPointer result(new model::Mesh()); + + gpu::Element vertexElement = gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ); + gpu::Buffer* resultVertexBuffer = new gpu::Buffer(vertexSize, resultVertexData); + gpu::BufferPointer resultVertexBufferPointer(resultVertexBuffer); + gpu::BufferView resultVertexBufferView(resultVertexBufferPointer, vertexElement); + result->setVertexBuffer(resultVertexBufferView); + + gpu::Element normalElement = gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ); + gpu::Buffer* resultNormalsBuffer = new gpu::Buffer(normalSize, resultNormalData); + gpu::BufferPointer resultNormalsBufferPointer(resultNormalsBuffer); + gpu::BufferView resultNormalsBufferView(resultNormalsBufferPointer, normalElement); + result->addAttribute(attributeTypeNormal, resultNormalsBufferView); + + gpu::Element indexElement = gpu::Element(gpu::SCALAR, gpu::UINT32, gpu::RAW); + gpu::Buffer* resultIndexesBuffer = new gpu::Buffer(indexSize, resultIndexData); + gpu::BufferPointer resultIndexesBufferPointer(resultIndexesBuffer); + gpu::BufferView resultIndexesBufferView(resultIndexesBufferPointer, indexElement); + result->setIndexBuffer(resultIndexesBufferView); + + + // TODO -- shouldn't assume just one part + + std::vector parts; + parts.emplace_back(model::Mesh::Part((model::Index)0, // startIndex + (model::Index)result->getNumIndices(), // numIndices + (model::Index)0, // baseVertex + model::Mesh::TRIANGLES)); // topology + result->setPartBuffer(gpu::BufferView(new gpu::Buffer(parts.size() * sizeof(model::Mesh::Part), + (gpu::Byte*) parts.data()), gpu::Element::PART_DRAWCALL)); + + return result; +} + + +void Mesh::forEach(std::function vertexFunc, + std::function normalFunc, + std::function indexFunc) { + int attributeTypeNormal = gpu::Stream::InputSlot::NORMAL; // libraries/gpu/src/gpu/Stream.h + + // vertex data + const gpu::BufferView& vertexBufferView = getVertexBuffer(); + gpu::BufferView::Index numVertices = (gpu::BufferView::Index)getNumVertices(); + for (gpu::BufferView::Index i = 0; i < numVertices; i ++) { + vertexFunc(vertexBufferView.get(i)); + } + + // normal data + const gpu::BufferView& normalsBufferView = getAttributeBuffer(attributeTypeNormal); + gpu::BufferView::Index numNormals = (gpu::BufferView::Index) normalsBufferView.getNumElements(); + for (gpu::BufferView::Index i = 0; i < numNormals; i ++) { + normalFunc(normalsBufferView.get(i)); + } + // TODO -- other attributes + + // face data + const gpu::BufferView& indexBufferView = getIndexBuffer(); + gpu::BufferView::Index numIndexes = (gpu::BufferView::Index)getNumIndices(); + for (gpu::BufferView::Index i = 0; i < numIndexes; i ++) { + indexFunc(indexBufferView.get(i)); + } +} + + Geometry::Geometry() { } @@ -148,4 +257,3 @@ Geometry::~Geometry() { void Geometry::setMesh(const MeshPointer& mesh) { _mesh = mesh; } - diff --git a/libraries/model/src/model/Geometry.h b/libraries/model/src/model/Geometry.h index 4256f0be03..7ba3e83407 100755 --- a/libraries/model/src/model/Geometry.h +++ b/libraries/model/src/model/Geometry.h @@ -25,6 +25,10 @@ typedef AABox Box; typedef std::vector< Box > Boxes; typedef glm::vec3 Vec3; +class Mesh; +using MeshPointer = std::shared_ptr< Mesh >; + + class Mesh { public: const static Index PRIMITIVE_RESTART_INDEX = -1; @@ -114,6 +118,15 @@ public: static gpu::Primitive topologyToPrimitive(Topology topo) { return static_cast(topo); } + // create a copy of this mesh after passing its vertices, normals, and indexes though the provided functions + MeshPointer map(std::function vertexFunc, + std::function normalFunc, + std::function indexFunc); + + void forEach(std::function vertexFunc, + std::function normalFunc, + std::function indexFunc); + protected: gpu::Stream::FormatPointer _vertexFormat; @@ -130,7 +143,6 @@ protected: void evalVertexStream(); }; -using MeshPointer = std::shared_ptr< Mesh >; class Geometry { diff --git a/libraries/model/src/model/TextureMap.cpp b/libraries/model/src/model/TextureMap.cpp index 7ac8083d9c..d07eae2166 100755 --- a/libraries/model/src/model/TextureMap.cpp +++ b/libraries/model/src/model/TextureMap.cpp @@ -10,10 +10,15 @@ // #include "TextureMap.h" +#include + #include #include #include - +#include +#include +#include +#include #include #include "ModelLogging.h" @@ -149,7 +154,7 @@ const QImage TextureUsage::process2DImageColor(const QImage& srcImage, bool& val return image; } -void TextureUsage::defineColorTexelFormats(gpu::Element& formatGPU, gpu::Element& formatMip, +void TextureUsage::defineColorTexelFormats(gpu::Element& formatGPU, gpu::Element& formatMip, const QImage& image, bool isLinear, bool doCompress) { #ifdef COMPRESS_TEXTURES @@ -202,7 +207,7 @@ const QImage& image, bool isLinear, bool doCompress) { #define CPU_MIPMAPS 1 -void generateMips(gpu::Texture* texture, QImage& image, gpu::Element formatMip, bool fastResize) { +void generateMips(gpu::Texture* texture, QImage& image, bool fastResize) { #if CPU_MIPMAPS PROFILE_RANGE(resource_parse, "generateMips"); auto numMips = texture->evalNumMips(); @@ -210,32 +215,33 @@ void generateMips(gpu::Texture* texture, QImage& image, gpu::Element formatMip, QSize mipSize(texture->evalMipWidth(level), texture->evalMipHeight(level)); if (fastResize) { image = image.scaled(mipSize); - texture->assignStoredMip(level, formatMip, image.byteCount(), image.constBits()); + texture->assignStoredMip(level, image.byteCount(), image.constBits()); } else { QImage mipImage = image.scaled(mipSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); - texture->assignStoredMip(level, formatMip, mipImage.byteCount(), mipImage.constBits()); + texture->assignStoredMip(level, mipImage.byteCount(), mipImage.constBits()); } } + #else texture->autoGenerateMips(-1); #endif } -void generateFaceMips(gpu::Texture* texture, QImage& image, gpu::Element formatMip, uint8 face) { +void generateFaceMips(gpu::Texture* texture, QImage& image, uint8 face) { #if CPU_MIPMAPS PROFILE_RANGE(resource_parse, "generateFaceMips"); auto numMips = texture->evalNumMips(); for (uint16 level = 1; level < numMips; ++level) { QSize mipSize(texture->evalMipWidth(level), texture->evalMipHeight(level)); QImage mipImage = image.scaled(mipSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); - texture->assignStoredMipFace(level, formatMip, mipImage.byteCount(), mipImage.constBits(), face); + texture->assignStoredMipFace(level, face, mipImage.byteCount(), mipImage.constBits()); } #else texture->autoGenerateMips(-1); #endif } -gpu::Texture* TextureUsage::process2DTextureColorFromImage(const QImage& srcImage, const std::string& srcImageName, bool isLinear, bool doCompress, bool generateMips) { +gpu::Texture* TextureUsage::process2DTextureColorFromImage(const QImage& srcImage, const std::string& srcImageName, bool isLinear, bool doCompress, bool generateMips, bool isStrict) { PROFILE_RANGE(resource_parse, "process2DTextureColorFromImage"); bool validAlpha = false; bool alphaAsMask = true; @@ -248,7 +254,11 @@ gpu::Texture* TextureUsage::process2DTextureColorFromImage(const QImage& srcImag gpu::Element formatMip; defineColorTexelFormats(formatGPU, formatMip, image, isLinear, doCompress); - theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); + if (isStrict) { + theTexture = (gpu::Texture::createStrict(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); + } else { + theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); + } theTexture->setSource(srcImageName); auto usage = gpu::Texture::Usage::Builder().withColor(); if (validAlpha) { @@ -258,22 +268,26 @@ gpu::Texture* TextureUsage::process2DTextureColorFromImage(const QImage& srcImag } } theTexture->setUsage(usage.build()); - - theTexture->assignStoredMip(0, formatMip, image.byteCount(), image.constBits()); + theTexture->setStoredMipFormat(formatMip); + theTexture->assignStoredMip(0, image.byteCount(), image.constBits()); if (generateMips) { - ::generateMips(theTexture, image, formatMip, false); + ::generateMips(theTexture, image, false); } + theTexture->setSource(srcImageName); } return theTexture; } +gpu::Texture* TextureUsage::createStrict2DTextureFromImage(const QImage& srcImage, const std::string& srcImageName) { + return process2DTextureColorFromImage(srcImage, srcImageName, false, false, true, true); +} + gpu::Texture* TextureUsage::create2DTextureFromImage(const QImage& srcImage, const std::string& srcImageName) { return process2DTextureColorFromImage(srcImage, srcImageName, false, false, true); } - gpu::Texture* TextureUsage::createAlbedoTextureFromImage(const QImage& srcImage, const std::string& srcImageName) { return process2DTextureColorFromImage(srcImage, srcImageName, false, true, true); } @@ -291,21 +305,25 @@ gpu::Texture* TextureUsage::createNormalTextureFromNormalImage(const QImage& src PROFILE_RANGE(resource_parse, "createNormalTextureFromNormalImage"); QImage image = processSourceImage(srcImage, false); - // Make sure the normal map source image is RGBA32 - if (image.format() != QImage::Format_RGBA8888) { - image = image.convertToFormat(QImage::Format_RGBA8888); + // Make sure the normal map source image is ARGB32 + if (image.format() != QImage::Format_ARGB32) { + image = image.convertToFormat(QImage::Format_ARGB32); } + gpu::Texture* theTexture = nullptr; if ((image.width() > 0) && (image.height() > 0)) { - gpu::Element formatGPU = gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA); - gpu::Element formatMip = gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA); + gpu::Element formatMip = gpu::Element::COLOR_BGRA_32; + gpu::Element formatGPU = gpu::Element::COLOR_RGBA_32; theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); theTexture->setSource(srcImageName); - theTexture->assignStoredMip(0, formatMip, image.byteCount(), image.constBits()); - generateMips(theTexture, image, formatMip, true); + theTexture->setStoredMipFormat(formatMip); + theTexture->assignStoredMip(0, image.byteCount(), image.constBits()); + generateMips(theTexture, image, true); + + theTexture->setSource(srcImageName); } return theTexture; @@ -336,16 +354,17 @@ gpu::Texture* TextureUsage::createNormalTextureFromBumpImage(const QImage& srcIm const double pStrength = 2.0; int width = image.width(); int height = image.height(); - QImage result(width, height, QImage::Format_RGB888); - + + QImage result(width, height, QImage::Format_ARGB32); + for (int i = 0; i < width; i++) { const int iNextClamped = clampPixelCoordinate(i + 1, width - 1); const int iPrevClamped = clampPixelCoordinate(i - 1, width - 1); - + for (int j = 0; j < height; j++) { const int jNextClamped = clampPixelCoordinate(j + 1, height - 1); const int jPrevClamped = clampPixelCoordinate(j - 1, height - 1); - + // surrounding pixels const QRgb topLeft = image.pixel(iPrevClamped, jPrevClamped); const QRgb top = image.pixel(iPrevClamped, j); @@ -355,7 +374,7 @@ gpu::Texture* TextureUsage::createNormalTextureFromBumpImage(const QImage& srcIm const QRgb bottom = image.pixel(iNextClamped, j); const QRgb bottomLeft = image.pixel(iNextClamped, jPrevClamped); const QRgb left = image.pixel(i, jPrevClamped); - + // take their gray intensities // since it's a grayscale image, the value of each component RGB is the same const double tl = qRed(topLeft); @@ -366,15 +385,15 @@ gpu::Texture* TextureUsage::createNormalTextureFromBumpImage(const QImage& srcIm const double b = qRed(bottom); const double bl = qRed(bottomLeft); const double l = qRed(left); - + // apply the sobel filter const double dX = (tr + pStrength * r + br) - (tl + pStrength * l + bl); const double dY = (bl + pStrength * b + br) - (tl + pStrength * t + tr); const double dZ = RGBA_MAX / pStrength; - + glm::vec3 v(dX, dY, dZ); glm::normalize(v); - + // convert to rgb from the value obtained computing the filter QRgb qRgbValue = qRgba(mapComponent(v.x), mapComponent(v.y), mapComponent(v.z), 1.0); result.setPixel(i, j, qRgbValue); @@ -382,13 +401,19 @@ gpu::Texture* TextureUsage::createNormalTextureFromBumpImage(const QImage& srcIm } gpu::Texture* theTexture = nullptr; - if ((image.width() > 0) && (image.height() > 0)) { - gpu::Element formatGPU = gpu::Element(gpu::VEC3, gpu::NUINT8, gpu::RGB); - gpu::Element formatMip = gpu::Element(gpu::VEC3, gpu::NUINT8, gpu::RGB); + if ((result.width() > 0) && (result.height() > 0)) { + + gpu::Element formatMip = gpu::Element::COLOR_BGRA_32; + gpu::Element formatGPU = gpu::Element::COLOR_RGBA_32; + + + theTexture = (gpu::Texture::create2D(formatGPU, result.width(), result.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); + theTexture->setSource(srcImageName); + theTexture->setStoredMipFormat(formatMip); + theTexture->assignStoredMip(0, result.byteCount(), result.constBits()); + generateMips(theTexture, result, true); - theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); theTexture->setSource(srcImageName); - theTexture->assignStoredMip(0, formatMip, image.byteCount(), image.constBits()); } return theTexture; @@ -414,16 +439,17 @@ gpu::Texture* TextureUsage::createRoughnessTextureFromImage(const QImage& srcIma #ifdef COMPRESS_TEXTURES gpu::Element formatGPU = gpu::Element(gpu::SCALAR, gpu::NUINT8, gpu::COMPRESSED_R); #else - gpu::Element formatGPU = gpu::Element(gpu::SCALAR, gpu::NUINT8, gpu::RGB); + gpu::Element formatGPU = gpu::Element::COLOR_R_8; #endif - gpu::Element formatMip = gpu::Element(gpu::SCALAR, gpu::NUINT8, gpu::RGB); + gpu::Element formatMip = gpu::Element::COLOR_R_8; theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); theTexture->setSource(srcImageName); - theTexture->assignStoredMip(0, formatMip, image.byteCount(), image.constBits()); - generateMips(theTexture, image, formatMip, true); + theTexture->setStoredMipFormat(formatMip); + theTexture->assignStoredMip(0, image.byteCount(), image.constBits()); + generateMips(theTexture, image, true); - // FIXME queue for transfer to GPU and block on completion + theTexture->setSource(srcImageName); } return theTexture; @@ -444,27 +470,28 @@ gpu::Texture* TextureUsage::createRoughnessTextureFromGlossImage(const QImage& s // Gloss turned into Rough image.invertPixels(QImage::InvertRgba); - + image = image.convertToFormat(QImage::Format_Grayscale8); - + gpu::Texture* theTexture = nullptr; if ((image.width() > 0) && (image.height() > 0)) { - + #ifdef COMPRESS_TEXTURES gpu::Element formatGPU = gpu::Element(gpu::SCALAR, gpu::NUINT8, gpu::COMPRESSED_R); #else - gpu::Element formatGPU = gpu::Element(gpu::SCALAR, gpu::NUINT8, gpu::RGB); + gpu::Element formatGPU = gpu::Element::COLOR_R_8; #endif - gpu::Element formatMip = gpu::Element(gpu::SCALAR, gpu::NUINT8, gpu::RGB); + gpu::Element formatMip = gpu::Element::COLOR_R_8; theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); theTexture->setSource(srcImageName); - theTexture->assignStoredMip(0, formatMip, image.byteCount(), image.constBits()); - generateMips(theTexture, image, formatMip, true); - - // FIXME queue for transfer to GPU and block on completion + theTexture->setStoredMipFormat(formatMip); + theTexture->assignStoredMip(0, image.byteCount(), image.constBits()); + generateMips(theTexture, image, true); + + theTexture->setSource(srcImageName); } - + return theTexture; } @@ -489,16 +516,17 @@ gpu::Texture* TextureUsage::createMetallicTextureFromImage(const QImage& srcImag #ifdef COMPRESS_TEXTURES gpu::Element formatGPU = gpu::Element(gpu::SCALAR, gpu::NUINT8, gpu::COMPRESSED_R); #else - gpu::Element formatGPU = gpu::Element(gpu::SCALAR, gpu::NUINT8, gpu::RGB); + gpu::Element formatGPU = gpu::Element::COLOR_R_8; #endif - gpu::Element formatMip = gpu::Element(gpu::SCALAR, gpu::NUINT8, gpu::RGB); + gpu::Element formatMip = gpu::Element::COLOR_R_8; theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); theTexture->setSource(srcImageName); - theTexture->assignStoredMip(0, formatMip, image.byteCount(), image.constBits()); - generateMips(theTexture, image, formatMip, true); + theTexture->setStoredMipFormat(formatMip); + theTexture->assignStoredMip(0, image.byteCount(), image.constBits()); + generateMips(theTexture, image, true); - // FIXME queue for transfer to GPU and block on completion + theTexture->setSource(srcImageName); } return theTexture; @@ -521,18 +549,18 @@ public: int _y = 0; bool _horizontalMirror = false; bool _verticalMirror = false; - + Face() {} Face(int x, int y, bool horizontalMirror, bool verticalMirror) : _x(x), _y(y), _horizontalMirror(horizontalMirror), _verticalMirror(verticalMirror) {} }; - + Face _faceXPos; Face _faceXNeg; Face _faceYPos; Face _faceYNeg; Face _faceZPos; Face _faceZNeg; - + CubeLayout(int wr, int hr, Face fXP, Face fXN, Face fYP, Face fYN, Face fZP, Face fZN) : _type(FLAT), _widthRatio(wr), @@ -775,7 +803,7 @@ gpu::Texture* TextureUsage::processCubeTextureColorFromImage(const QImage& srcIm defineColorTexelFormats(formatGPU, formatMip, image, isLinear, doCompress); // Find the layout of the cubemap in the 2D image - // Use the original image size since processSourceImage may have altered the size / aspect ratio + // Use the original image size since processSourceImage may have altered the size / aspect ratio int foundLayout = CubeLayout::findLayout(srcImage.width(), srcImage.height()); std::vector faces; @@ -810,11 +838,12 @@ gpu::Texture* TextureUsage::processCubeTextureColorFromImage(const QImage& srcIm if (faces.size() == gpu::Texture::NUM_FACES_PER_TYPE[gpu::Texture::TEX_CUBE]) { theTexture = gpu::Texture::createCube(formatGPU, faces[0].width(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP)); theTexture->setSource(srcImageName); + theTexture->setStoredMipFormat(formatMip); int f = 0; for (auto& face : faces) { - theTexture->assignStoredMipFace(0, formatMip, face.byteCount(), face.constBits(), f); + theTexture->assignStoredMipFace(0, f, face.byteCount(), face.constBits()); if (generateMips) { - generateFaceMips(theTexture, face, formatMip, f); + generateFaceMips(theTexture, face, f); } f++; } @@ -829,6 +858,8 @@ gpu::Texture* TextureUsage::processCubeTextureColorFromImage(const QImage& srcIm PROFILE_RANGE(resource_parse, "generateIrradiance"); theTexture->generateIrradiance(); } + + theTexture->setSource(srcImageName); } } diff --git a/libraries/model/src/model/TextureMap.h b/libraries/model/src/model/TextureMap.h index 220ee57a97..a4bb861502 100755 --- a/libraries/model/src/model/TextureMap.h +++ b/libraries/model/src/model/TextureMap.h @@ -32,6 +32,7 @@ public: int _environmentUsage = 0; static gpu::Texture* create2DTextureFromImage(const QImage& image, const std::string& srcImageName); + static gpu::Texture* createStrict2DTextureFromImage(const QImage& image, const std::string& srcImageName); static gpu::Texture* createAlbedoTextureFromImage(const QImage& image, const std::string& srcImageName); static gpu::Texture* createEmissiveTextureFromImage(const QImage& image, const std::string& srcImageName); static gpu::Texture* createNormalTextureFromNormalImage(const QImage& image, const std::string& srcImageName); @@ -47,7 +48,7 @@ public: static const QImage process2DImageColor(const QImage& srcImage, bool& validAlpha, bool& alphaAsMask); static void defineColorTexelFormats(gpu::Element& formatGPU, gpu::Element& formatMip, const QImage& srcImage, bool isLinear, bool doCompress); - static gpu::Texture* process2DTextureColorFromImage(const QImage& srcImage, const std::string& srcImageName, bool isLinear, bool doCompress, bool generateMips); + static gpu::Texture* process2DTextureColorFromImage(const QImage& srcImage, const std::string& srcImageName, bool isLinear, bool doCompress, bool generateMips, bool isStrict = false); static gpu::Texture* processCubeTextureColorFromImage(const QImage& srcImage, const std::string& srcImageName, bool isLinear, bool doCompress, bool generateMips, bool generateIrradiance); }; diff --git a/libraries/networking/src/Assignment.cpp b/libraries/networking/src/Assignment.cpp index 9efad15398..27d4a31ccf 100644 --- a/libraries/networking/src/Assignment.cpp +++ b/libraries/networking/src/Assignment.cpp @@ -12,7 +12,6 @@ #include "udt/PacketHeaders.h" #include "SharedUtil.h" #include "UUID.h" -#include "ServerPathUtils.h" #include diff --git a/libraries/networking/src/FileCache.cpp b/libraries/networking/src/FileCache.cpp new file mode 100644 index 0000000000..f8a86903cb --- /dev/null +++ b/libraries/networking/src/FileCache.cpp @@ -0,0 +1,243 @@ +// +// FileCache.cpp +// libraries/model-networking/src +// +// Created by Zach Pomerantz on 2/21/2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "FileCache.h" + +#include +#include +#include +#include + +#include + +#include + +Q_LOGGING_CATEGORY(file_cache, "hifi.file_cache", QtWarningMsg) + +using namespace cache; + +static const std::string MANIFEST_NAME = "manifest"; + +static const size_t BYTES_PER_MEGABYTES = 1024 * 1024; +static const size_t BYTES_PER_GIGABYTES = 1024 * BYTES_PER_MEGABYTES; +const size_t FileCache::DEFAULT_UNUSED_MAX_SIZE = 5 * BYTES_PER_GIGABYTES; // 5GB +const size_t FileCache::MAX_UNUSED_MAX_SIZE = 100 * BYTES_PER_GIGABYTES; // 100GB +const size_t FileCache::DEFAULT_OFFLINE_MAX_SIZE = 2 * BYTES_PER_GIGABYTES; // 2GB + +void FileCache::setUnusedFileCacheSize(size_t unusedFilesMaxSize) { + _unusedFilesMaxSize = std::min(unusedFilesMaxSize, MAX_UNUSED_MAX_SIZE); + reserve(0); + emit dirty(); +} + +void FileCache::setOfflineFileCacheSize(size_t offlineFilesMaxSize) { + _offlineFilesMaxSize = std::min(offlineFilesMaxSize, MAX_UNUSED_MAX_SIZE); +} + +FileCache::FileCache(const std::string& dirname, const std::string& ext, QObject* parent) : + QObject(parent), + _ext(ext), + _dirname(dirname), + _dirpath(PathUtils::getAppLocalDataFilePath(dirname.c_str()).toStdString()) {} + +FileCache::~FileCache() { + clear(); +} + +void fileDeleter(File* file) { + file->deleter(); +} + +void FileCache::initialize() { + QDir dir(_dirpath.c_str()); + + if (dir.exists()) { + auto nameFilters = QStringList(("*." + _ext).c_str()); + auto filters = QDir::Filters(QDir::NoDotAndDotDot | QDir::Files); + auto sort = QDir::SortFlags(QDir::Time); + auto files = dir.entryList(nameFilters, filters, sort); + + // load persisted files + foreach(QString filename, files) { + const Key key = filename.section('.', 0, 1).toStdString(); + const std::string filepath = dir.filePath(filename).toStdString(); + const size_t length = std::ifstream(filepath, std::ios::binary | std::ios::ate).tellg(); + addFile(Metadata(key, length), filepath); + } + + qCDebug(file_cache, "[%s] Initialized %s", _dirname.c_str(), _dirpath.c_str()); + } else { + dir.mkpath(_dirpath.c_str()); + qCDebug(file_cache, "[%s] Created %s", _dirname.c_str(), _dirpath.c_str()); + } + + _initialized = true; +} + +FilePointer FileCache::addFile(Metadata&& metadata, const std::string& filepath) { + FilePointer file(createFile(std::move(metadata), filepath).release(), &fileDeleter); + if (file) { + _numTotalFiles += 1; + _totalFilesSize += file->getLength(); + file->_cache = this; + emit dirty(); + + Lock lock(_filesMutex); + _files[file->getKey()] = file; + } + return file; +} + +FilePointer FileCache::writeFile(const char* data, File::Metadata&& metadata) { + assert(_initialized); + + std::string filepath = getFilepath(metadata.key); + + Lock lock(_filesMutex); + + // if file already exists, return it + FilePointer file = getFile(metadata.key); + if (file) { + qCWarning(file_cache, "[%s] Attempted to overwrite %s", _dirname.c_str(), metadata.key.c_str()); + return file; + } + + // write the new file + FILE* saveFile = fopen(filepath.c_str(), "wb"); + if (saveFile != nullptr && fwrite(data, metadata.length, 1, saveFile) && fclose(saveFile) == 0) { + file = addFile(std::move(metadata), filepath); + } else { + qCWarning(file_cache, "[%s] Failed to write %s (%s)", _dirname.c_str(), metadata.key.c_str(), strerror(errno)); + errno = 0; + } + + return file; +} + +FilePointer FileCache::getFile(const Key& key) { + assert(_initialized); + + FilePointer file; + + Lock lock(_filesMutex); + + // check if file exists + const auto it = _files.find(key); + if (it != _files.cend()) { + file = it->second.lock(); + if (file) { + // if it exists, it is active - remove it from the cache + removeUnusedFile(file); + qCDebug(file_cache, "[%s] Found %s", _dirname.c_str(), key.c_str()); + emit dirty(); + } else { + // if not, remove the weak_ptr + _files.erase(it); + } + } + + return file; +} + +std::string FileCache::getFilepath(const Key& key) { + return _dirpath + '/' + key + '.' + _ext; +} + +void FileCache::addUnusedFile(const FilePointer file) { + { + Lock lock(_filesMutex); + _files[file->getKey()] = file; + } + + reserve(file->getLength()); + file->_LRUKey = ++_lastLRUKey; + + { + Lock lock(_unusedFilesMutex); + _unusedFiles.insert({ file->_LRUKey, file }); + _numUnusedFiles += 1; + _unusedFilesSize += file->getLength(); + } + + emit dirty(); +} + +void FileCache::removeUnusedFile(const FilePointer file) { + Lock lock(_unusedFilesMutex); + const auto it = _unusedFiles.find(file->_LRUKey); + if (it != _unusedFiles.cend()) { + _unusedFiles.erase(it); + _numUnusedFiles -= 1; + _unusedFilesSize -= file->getLength(); + } +} + +void FileCache::reserve(size_t length) { + Lock unusedLock(_unusedFilesMutex); + while (!_unusedFiles.empty() && + _unusedFilesSize + length > _unusedFilesMaxSize) { + auto it = _unusedFiles.begin(); + auto file = it->second; + auto length = file->getLength(); + + unusedLock.unlock(); + { + file->_cache = nullptr; + Lock lock(_filesMutex); + _files.erase(file->getKey()); + } + unusedLock.lock(); + + _unusedFiles.erase(it); + _numTotalFiles -= 1; + _numUnusedFiles -= 1; + _totalFilesSize -= length; + _unusedFilesSize -= length; + } +} + +void FileCache::clear() { + Lock unusedFilesLock(_unusedFilesMutex); + for (const auto& pair : _unusedFiles) { + auto& file = pair.second; + file->_cache = nullptr; + + if (_totalFilesSize > _offlineFilesMaxSize) { + _totalFilesSize -= file->getLength(); + } else { + file->_shouldPersist = true; + qCDebug(file_cache, "[%s] Persisting %s", _dirname.c_str(), file->getKey().c_str()); + } + } + _unusedFiles.clear(); +} + +void File::deleter() { + if (_cache) { + FilePointer self(this, &fileDeleter); + _cache->addUnusedFile(self); + } else { + deleteLater(); + } +} + +File::File(Metadata&& metadata, const std::string& filepath) : + _key(std::move(metadata.key)), + _length(metadata.length), + _filepath(filepath) {} + +File::~File() { + QFile file(getFilepath().c_str()); + if (file.exists() && !_shouldPersist) { + qCInfo(file_cache, "Unlinked %s", getFilepath().c_str()); + file.remove(); + } +} diff --git a/libraries/networking/src/FileCache.h b/libraries/networking/src/FileCache.h new file mode 100644 index 0000000000..f77db555bc --- /dev/null +++ b/libraries/networking/src/FileCache.h @@ -0,0 +1,158 @@ +// +// FileCache.h +// libraries/networking/src +// +// Created by Zach Pomerantz on 2/21/2017. +// Copyright 2017 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_FileCache_h +#define hifi_FileCache_h + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +Q_DECLARE_LOGGING_CATEGORY(file_cache) + +namespace cache { + +class File; +using FilePointer = std::shared_ptr; + +class FileCache : public QObject { + Q_OBJECT + Q_PROPERTY(size_t numTotal READ getNumTotalFiles NOTIFY dirty) + Q_PROPERTY(size_t numCached READ getNumCachedFiles NOTIFY dirty) + Q_PROPERTY(size_t sizeTotal READ getSizeTotalFiles NOTIFY dirty) + Q_PROPERTY(size_t sizeCached READ getSizeCachedFiles NOTIFY dirty) + + static const size_t DEFAULT_UNUSED_MAX_SIZE; + static const size_t MAX_UNUSED_MAX_SIZE; + static const size_t DEFAULT_OFFLINE_MAX_SIZE; + +public: + size_t getNumTotalFiles() const { return _numTotalFiles; } + size_t getNumCachedFiles() const { return _numUnusedFiles; } + size_t getSizeTotalFiles() const { return _totalFilesSize; } + size_t getSizeCachedFiles() const { return _unusedFilesSize; } + + void setUnusedFileCacheSize(size_t unusedFilesMaxSize); + size_t getUnusedFileCacheSize() const { return _unusedFilesSize; } + + void setOfflineFileCacheSize(size_t offlineFilesMaxSize); + + // initialize FileCache with a directory name (not a path, ex.: "temp_jpgs") and an ext (ex.: "jpg") + FileCache(const std::string& dirname, const std::string& ext, QObject* parent = nullptr); + virtual ~FileCache(); + + using Key = std::string; + struct Metadata { + Metadata(const Key& key, size_t length) : + key(key), length(length) {} + Key key; + size_t length; + }; + + // derived classes should implement a setter/getter, for example, for a FileCache backing a network cache: + // + // DerivedFilePointer writeFile(const char* data, DerivedMetadata&& metadata) { + // return writeFile(data, std::forward(metadata)); + // } + // + // DerivedFilePointer getFile(const QUrl& url) { + // auto key = lookup_hash_for(url); // assuming hashing url in create/evictedFile overrides + // return getFile(key); + // } + +signals: + void dirty(); + +protected: + /// must be called after construction to create the cache on the fs and restore persisted files + void initialize(); + + FilePointer writeFile(const char* data, Metadata&& metadata); + FilePointer getFile(const Key& key); + + /// create a file + virtual std::unique_ptr createFile(Metadata&& metadata, const std::string& filepath) = 0; + +private: + using Mutex = std::recursive_mutex; + using Lock = std::unique_lock; + + friend class File; + + std::string getFilepath(const Key& key); + + FilePointer addFile(Metadata&& metadata, const std::string& filepath); + void addUnusedFile(const FilePointer file); + void removeUnusedFile(const FilePointer file); + void reserve(size_t length); + void clear(); + + std::atomic _numTotalFiles { 0 }; + std::atomic _numUnusedFiles { 0 }; + std::atomic _totalFilesSize { 0 }; + std::atomic _unusedFilesSize { 0 }; + + std::string _ext; + std::string _dirname; + std::string _dirpath; + bool _initialized { false }; + + std::unordered_map> _files; + Mutex _filesMutex; + + std::map _unusedFiles; + Mutex _unusedFilesMutex; + size_t _unusedFilesMaxSize { DEFAULT_UNUSED_MAX_SIZE }; + int _lastLRUKey { 0 }; + + size_t _offlineFilesMaxSize { DEFAULT_OFFLINE_MAX_SIZE }; +}; + +class File : public QObject { + Q_OBJECT + +public: + using Key = FileCache::Key; + using Metadata = FileCache::Metadata; + + Key getKey() const { return _key; } + size_t getLength() const { return _length; } + std::string getFilepath() const { return _filepath; } + + virtual ~File(); + /// overrides should call File::deleter to maintain caching behavior + virtual void deleter(); + +protected: + /// when constructed, the file has already been created/written + File(Metadata&& metadata, const std::string& filepath); + +private: + friend class FileCache; + + const Key _key; + const size_t _length; + const std::string _filepath; + + FileCache* _cache; + int _LRUKey { 0 }; + + bool _shouldPersist { false }; +}; + +} + +#endif // hifi_FileCache_h diff --git a/libraries/networking/src/NodePermissions.h b/libraries/networking/src/NodePermissions.h index 5d2755f9b5..6fa005e360 100644 --- a/libraries/networking/src/NodePermissions.h +++ b/libraries/networking/src/NodePermissions.h @@ -13,18 +13,31 @@ #define hifi_NodePermissions_h #include +#include #include #include #include #include - +#include +#include #include "GroupRank.h" class NodePermissions; using NodePermissionsPointer = std::shared_ptr; -using NodePermissionsKey = QPair; // name, rankID +using NodePermissionsKey = std::pair; // name, rankID using NodePermissionsKeyList = QList>; +namespace std { + template<> + struct hash { + size_t operator()(const NodePermissionsKey& key) const { + size_t result = qHash(key.first); + result <<= 32; + result |= qHash(key.second); + return result; + } + }; +} class NodePermissions { public: @@ -100,27 +113,40 @@ public: NodePermissionsMap() { } NodePermissionsPointer& operator[](const NodePermissionsKey& key) { NodePermissionsKey dataKey(key.first.toLower(), key.second); - if (!_data.contains(dataKey)) { + if (0 == _data.count(dataKey)) { _data[dataKey] = NodePermissionsPointer(new NodePermissions(key)); } return _data[dataKey]; } NodePermissionsPointer operator[](const NodePermissionsKey& key) const { - return _data.value(NodePermissionsKey(key.first.toLower(), key.second)); + NodePermissionsPointer result; + auto itr = _data.find(NodePermissionsKey(key.first.toLower(), key.second)); + if (_data.end() != itr) { + result = itr->second; + } + return result; } bool contains(const NodePermissionsKey& key) const { - return _data.contains(NodePermissionsKey(key.first.toLower(), key.second)); + return 0 != _data.count(NodePermissionsKey(key.first.toLower(), key.second)); } - bool contains(const QString& keyFirst, QUuid keySecond) const { - return _data.contains(NodePermissionsKey(keyFirst.toLower(), keySecond)); + bool contains(const QString& keyFirst, const QUuid& keySecond) const { + return 0 != _data.count(NodePermissionsKey(keyFirst.toLower(), keySecond)); } - QList keys() const { return _data.keys(); } - QHash get() { return _data; } + + QList keys() const { + QList result; + for (const auto& entry : _data) { + result.push_back(entry.first); + } + return result; + } + + const std::unordered_map& get() { return _data; } void clear() { _data.clear(); } - void remove(const NodePermissionsKey& key) { _data.remove(key); } + void remove(const NodePermissionsKey& key) { _data.erase(key); } private: - QHash _data; + std::unordered_map _data; }; diff --git a/libraries/networking/src/udt/PacketQueue.cpp b/libraries/networking/src/udt/PacketQueue.cpp index bb20982ca4..9560f2f187 100644 --- a/libraries/networking/src/udt/PacketQueue.cpp +++ b/libraries/networking/src/udt/PacketQueue.cpp @@ -15,6 +15,10 @@ using namespace udt; +PacketQueue::PacketQueue() { + _channels.emplace_back(new std::list()); +} + MessageNumber PacketQueue::getNextMessageNumber() { static const MessageNumber MAX_MESSAGE_NUMBER = MessageNumber(1) << MESSAGE_NUMBER_SIZE; _currentMessageNumber = (_currentMessageNumber + 1) % MAX_MESSAGE_NUMBER; @@ -24,7 +28,7 @@ MessageNumber PacketQueue::getNextMessageNumber() { bool PacketQueue::isEmpty() const { LockGuard locker(_packetsLock); // Only the main channel and it is empty - return (_channels.size() == 1) && _channels.front().empty(); + return (_channels.size() == 1) && _channels.front()->empty(); } PacketQueue::PacketPointer PacketQueue::takePacket() { @@ -34,19 +38,19 @@ PacketQueue::PacketPointer PacketQueue::takePacket() { } // Find next non empty channel - if (_channels[nextIndex()].empty()) { + if (_channels[nextIndex()]->empty()) { nextIndex(); } auto& channel = _channels[_currentIndex]; - Q_ASSERT(!channel.empty()); + Q_ASSERT(!channel->empty()); // Take front packet - auto packet = std::move(channel.front()); - channel.pop_front(); + auto packet = std::move(channel->front()); + channel->pop_front(); // Remove now empty channel (Don't remove the main channel) - if (channel.empty() && _currentIndex != 0) { - channel.swap(_channels.back()); + if (channel->empty() && _currentIndex != 0) { + channel->swap(*_channels.back()); _channels.pop_back(); --_currentIndex; } @@ -61,7 +65,7 @@ unsigned int PacketQueue::nextIndex() { void PacketQueue::queuePacket(PacketPointer packet) { LockGuard locker(_packetsLock); - _channels.front().push_back(std::move(packet)); + _channels.front()->push_back(std::move(packet)); } void PacketQueue::queuePacketList(PacketListPointer packetList) { @@ -70,5 +74,6 @@ void PacketQueue::queuePacketList(PacketListPointer packetList) { } LockGuard locker(_packetsLock); - _channels.push_back(std::move(packetList->_packets)); + _channels.emplace_back(new std::list()); + _channels.back()->swap(packetList->_packets); } diff --git a/libraries/networking/src/udt/PacketQueue.h b/libraries/networking/src/udt/PacketQueue.h index 69784fd8db..2b3d3a4b5b 100644 --- a/libraries/networking/src/udt/PacketQueue.h +++ b/libraries/networking/src/udt/PacketQueue.h @@ -30,10 +30,11 @@ class PacketQueue { using LockGuard = std::lock_guard; using PacketPointer = std::unique_ptr; using PacketListPointer = std::unique_ptr; - using Channel = std::list; + using Channel = std::unique_ptr>; using Channels = std::vector; public: + PacketQueue(); void queuePacket(PacketPointer packet); void queuePacketList(PacketListPointer packetList); @@ -49,7 +50,7 @@ private: MessageNumber _currentMessageNumber { 0 }; mutable Mutex _packetsLock; // Protects the packets to be sent. - Channels _channels = Channels(1); // One channel per packet list + Main channel + Channels _channels; // One channel per packet list + Main channel unsigned int _currentIndex { 0 }; }; diff --git a/libraries/physics/src/EntityMotionState.cpp b/libraries/physics/src/EntityMotionState.cpp index c175a836cc..d383f4c199 100644 --- a/libraries/physics/src/EntityMotionState.cpp +++ b/libraries/physics/src/EntityMotionState.cpp @@ -97,6 +97,21 @@ void EntityMotionState::updateServerPhysicsVariables() { _serverActionData = _entity->getActionData(); } +void EntityMotionState::handleDeactivation() { + // copy _server data to entity + bool success; + _entity->setPosition(_serverPosition, success, false); + _entity->setOrientation(_serverRotation, success, false); + _entity->setVelocity(ENTITY_ITEM_ZERO_VEC3); + _entity->setAngularVelocity(ENTITY_ITEM_ZERO_VEC3); + // and also to RigidBody + btTransform worldTrans; + worldTrans.setOrigin(glmToBullet(_serverPosition)); + worldTrans.setRotation(glmToBullet(_serverRotation)); + _body->setWorldTransform(worldTrans); + // no need to update velocities... should already be zero +} + // virtual void EntityMotionState::handleEasyChanges(uint32_t& flags) { assert(entityTreeIsLocked()); @@ -111,6 +126,8 @@ void EntityMotionState::handleEasyChanges(uint32_t& flags) { flags &= ~Simulation::DIRTY_PHYSICS_ACTIVATION; _body->setActivationState(WANTS_DEACTIVATION); _outgoingPriority = 0; + const float ACTIVATION_EXPIRY = 3.0f; // something larger than the 2.0 hard coded in Bullet + _body->setDeactivationTime(ACTIVATION_EXPIRY); } else { // disowned object is still moving --> start timer for ownership bid // TODO? put a delay in here proportional to distance from object? @@ -221,12 +238,9 @@ void EntityMotionState::getWorldTransform(btTransform& worldTrans) const { } // This callback is invoked by the physics simulation at the end of each simulation step... -// iff the corresponding RigidBody is DYNAMIC and has moved. +// iff the corresponding RigidBody is DYNAMIC and ACTIVE. void EntityMotionState::setWorldTransform(const btTransform& worldTrans) { - if (!_entity) { - return; - } - + assert(_entity); assert(entityTreeIsLocked()); measureBodyAcceleration(); bool positionSuccess; diff --git a/libraries/physics/src/EntityMotionState.h b/libraries/physics/src/EntityMotionState.h index feac47d8ec..380edf3927 100644 --- a/libraries/physics/src/EntityMotionState.h +++ b/libraries/physics/src/EntityMotionState.h @@ -29,6 +29,7 @@ public: virtual ~EntityMotionState(); void updateServerPhysicsVariables(); + void handleDeactivation(); virtual void handleEasyChanges(uint32_t& flags) override; virtual bool handleHardAndEasyChanges(uint32_t& flags, PhysicsEngine* engine) override; diff --git a/libraries/physics/src/PhysicalEntitySimulation.cpp b/libraries/physics/src/PhysicalEntitySimulation.cpp index 903b160a5e..bd76b2d70f 100644 --- a/libraries/physics/src/PhysicalEntitySimulation.cpp +++ b/libraries/physics/src/PhysicalEntitySimulation.cpp @@ -259,13 +259,27 @@ void PhysicalEntitySimulation::getObjectsToChange(VectorOfMotionStates& result) _pendingChanges.clear(); } -void PhysicalEntitySimulation::handleOutgoingChanges(const VectorOfMotionStates& motionStates) { +void PhysicalEntitySimulation::handleDeactivatedMotionStates(const VectorOfMotionStates& motionStates) { + for (auto stateItr : motionStates) { + ObjectMotionState* state = &(*stateItr); + assert(state); + if (state->getType() == MOTIONSTATE_TYPE_ENTITY) { + EntityMotionState* entityState = static_cast(state); + entityState->handleDeactivation(); + EntityItemPointer entity = entityState->getEntity(); + _entitiesToSort.insert(entity); + } + } +} + +void PhysicalEntitySimulation::handleChangedMotionStates(const VectorOfMotionStates& motionStates) { QMutexLocker lock(&_mutex); // walk the motionStates looking for those that correspond to entities for (auto stateItr : motionStates) { ObjectMotionState* state = &(*stateItr); - if (state && state->getType() == MOTIONSTATE_TYPE_ENTITY) { + assert(state); + if (state->getType() == MOTIONSTATE_TYPE_ENTITY) { EntityMotionState* entityState = static_cast(state); EntityItemPointer entity = entityState->getEntity(); assert(entity.get()); diff --git a/libraries/physics/src/PhysicalEntitySimulation.h b/libraries/physics/src/PhysicalEntitySimulation.h index af5def9775..5f6185add3 100644 --- a/libraries/physics/src/PhysicalEntitySimulation.h +++ b/libraries/physics/src/PhysicalEntitySimulation.h @@ -56,7 +56,8 @@ public: void setObjectsToChange(const VectorOfMotionStates& objectsToChange); void getObjectsToChange(VectorOfMotionStates& result); - void handleOutgoingChanges(const VectorOfMotionStates& motionStates); + void handleDeactivatedMotionStates(const VectorOfMotionStates& motionStates); + void handleChangedMotionStates(const VectorOfMotionStates& motionStates); void handleCollisionEvents(const CollisionEvents& collisionEvents); EntityEditPacketSender* getPacketSender() { return _entityPacketSender; } @@ -67,7 +68,7 @@ private: SetOfEntities _entitiesToAddToPhysics; SetOfEntityMotionStates _pendingChanges; // EntityMotionStates already in PhysicsEngine that need their physics changed - SetOfEntityMotionStates _outgoingChanges; // EntityMotionStates for which we need to send updates to entity-server + SetOfEntityMotionStates _outgoingChanges; // EntityMotionStates for which we may need to send updates to entity-server SetOfMotionStates _physicalObjects; // MotionStates of entities in PhysicsEngine diff --git a/libraries/physics/src/PhysicsEngine.cpp b/libraries/physics/src/PhysicsEngine.cpp index 363887de25..a8a8e6acfd 100644 --- a/libraries/physics/src/PhysicsEngine.cpp +++ b/libraries/physics/src/PhysicsEngine.cpp @@ -472,7 +472,7 @@ const CollisionEvents& PhysicsEngine::getCollisionEvents() { return _collisionEvents; } -const VectorOfMotionStates& PhysicsEngine::getOutgoingChanges() { +const VectorOfMotionStates& PhysicsEngine::getChangedMotionStates() { BT_PROFILE("copyOutgoingChanges"); // Bullet will not deactivate static objects (it doesn't expect them to be active) // so we must deactivate them ourselves diff --git a/libraries/physics/src/PhysicsEngine.h b/libraries/physics/src/PhysicsEngine.h index bbafbb06b6..b2ebe58f08 100644 --- a/libraries/physics/src/PhysicsEngine.h +++ b/libraries/physics/src/PhysicsEngine.h @@ -65,7 +65,8 @@ public: bool hasOutgoingChanges() const { return _hasOutgoingChanges; } /// \return reference to list of changed MotionStates. The list is only valid until beginning of next simulation loop. - const VectorOfMotionStates& getOutgoingChanges(); + const VectorOfMotionStates& getChangedMotionStates(); + const VectorOfMotionStates& getDeactivatedMotionStates() const { return _dynamicsWorld->getDeactivatedMotionStates(); } /// \return reference to list of Collision events. The list is only valid until beginning of next simulation loop. const CollisionEvents& getCollisionEvents(); diff --git a/libraries/physics/src/ThreadSafeDynamicsWorld.cpp b/libraries/physics/src/ThreadSafeDynamicsWorld.cpp index 5fe99f137c..24cfbc2609 100644 --- a/libraries/physics/src/ThreadSafeDynamicsWorld.cpp +++ b/libraries/physics/src/ThreadSafeDynamicsWorld.cpp @@ -120,30 +120,41 @@ void ThreadSafeDynamicsWorld::synchronizeMotionState(btRigidBody* body) { void ThreadSafeDynamicsWorld::synchronizeMotionStates() { BT_PROFILE("synchronizeMotionStates"); _changedMotionStates.clear(); + + // NOTE: m_synchronizeAllMotionStates is 'false' by default for optimization. + // See PhysicsEngine::init() where we call _dynamicsWorld->setForceUpdateAllAabbs(false) if (m_synchronizeAllMotionStates) { //iterate over all collision objects for (int i=0;igetMotionState()) { - synchronizeMotionState(body); - _changedMotionStates.push_back(static_cast(body->getMotionState())); - } + if (body && body->getMotionState()) { + synchronizeMotionState(body); + _changedMotionStates.push_back(static_cast(body->getMotionState())); } } } else { //iterate over all active rigid bodies + // TODO? if this becomes a performance bottleneck we could derive our own SimulationIslandManager + // that remembers a list of objects deactivated last step + _activeStates.clear(); + _deactivatedStates.clear(); for (int i=0;iisActive()) { - if (body->getMotionState()) { + ObjectMotionState* motionState = static_cast(body->getMotionState()); + if (motionState) { + if (body->isActive()) { synchronizeMotionState(body); - _changedMotionStates.push_back(static_cast(body->getMotionState())); + _changedMotionStates.push_back(motionState); + _activeStates.insert(motionState); + } else if (_lastActiveStates.find(motionState) != _lastActiveStates.end()) { + // this object was active last frame but is no longer + _deactivatedStates.push_back(motionState); } } } } + _activeStates.swap(_lastActiveStates); } void ThreadSafeDynamicsWorld::saveKinematicState(btScalar timeStep) { diff --git a/libraries/physics/src/ThreadSafeDynamicsWorld.h b/libraries/physics/src/ThreadSafeDynamicsWorld.h index 68062d8d29..b4fcca8cdb 100644 --- a/libraries/physics/src/ThreadSafeDynamicsWorld.h +++ b/libraries/physics/src/ThreadSafeDynamicsWorld.h @@ -49,12 +49,16 @@ public: float getLocalTimeAccumulation() const { return m_localTime; } const VectorOfMotionStates& getChangedMotionStates() const { return _changedMotionStates; } + const VectorOfMotionStates& getDeactivatedMotionStates() const { return _deactivatedStates; } private: // call this instead of non-virtual btDiscreteDynamicsWorld::synchronizeSingleMotionState() void synchronizeMotionState(btRigidBody* body); VectorOfMotionStates _changedMotionStates; + VectorOfMotionStates _deactivatedStates; + SetOfMotionStates _activeStates; + SetOfMotionStates _lastActiveStates; }; #endif // hifi_ThreadSafeDynamicsWorld_h diff --git a/libraries/recording/src/recording/Deck.cpp b/libraries/recording/src/recording/Deck.cpp index 61eb86c91f..186516e01c 100644 --- a/libraries/recording/src/recording/Deck.cpp +++ b/libraries/recording/src/recording/Deck.cpp @@ -33,6 +33,7 @@ void Deck::queueClip(ClipPointer clip, float timeOffset) { // FIXME disabling multiple clips for now _clips.clear(); + _length = 0.0f; // if the time offset is not zero, wrap in an OffsetClip if (timeOffset != 0.0f) { @@ -153,8 +154,8 @@ void Deck::processFrames() { // if doing relative movement emit looped(); } else { - // otherwise pause playback - pause(); + // otherwise stop playback + stop(); } return; } diff --git a/libraries/render-utils/CMakeLists.txt b/libraries/render-utils/CMakeLists.txt index ecafb8f565..3bf389973a 100644 --- a/libraries/render-utils/CMakeLists.txt +++ b/libraries/render-utils/CMakeLists.txt @@ -3,7 +3,7 @@ AUTOSCRIBE_SHADER_LIB(gpu model render) # pull in the resources.qrc file qt5_add_resources(QT_RESOURCES_FILE "${CMAKE_CURRENT_SOURCE_DIR}/res/fonts/fonts.qrc") setup_hifi_library(Widgets OpenGL Network Qml Quick Script) -link_hifi_libraries(shared gpu model model-networking render animation fbx entities) +link_hifi_libraries(shared ktx gpu model model-networking render animation fbx entities) if (NOT ANDROID) target_nsight() diff --git a/libraries/render-utils/src/AntialiasingEffect.cpp b/libraries/render-utils/src/AntialiasingEffect.cpp index 2941197e6d..f95d45de04 100644 --- a/libraries/render-utils/src/AntialiasingEffect.cpp +++ b/libraries/render-utils/src/AntialiasingEffect.cpp @@ -52,7 +52,7 @@ const gpu::PipelinePointer& Antialiasing::getAntialiasingPipeline() { _antialiasingBuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("antialiasing")); auto format = gpu::Element::COLOR_SRGBA_32; // DependencyManager::get()->getLightingTexture()->getTexelFormat(); auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT); - _antialiasingTexture = gpu::TexturePointer(gpu::Texture::create2D(format, width, height, defaultSampler)); + _antialiasingTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(format, width, height, defaultSampler)); _antialiasingBuffer->setRenderBuffer(0, _antialiasingTexture); } diff --git a/libraries/render-utils/src/DeferredFramebuffer.cpp b/libraries/render-utils/src/DeferredFramebuffer.cpp index e8783e0e0d..40c22beba4 100644 --- a/libraries/render-utils/src/DeferredFramebuffer.cpp +++ b/libraries/render-utils/src/DeferredFramebuffer.cpp @@ -53,9 +53,9 @@ void DeferredFramebuffer::allocate() { auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT); - _deferredColorTexture = gpu::TexturePointer(gpu::Texture::create2D(colorFormat, width, height, defaultSampler)); - _deferredNormalTexture = gpu::TexturePointer(gpu::Texture::create2D(linearFormat, width, height, defaultSampler)); - _deferredSpecularTexture = gpu::TexturePointer(gpu::Texture::create2D(colorFormat, width, height, defaultSampler)); + _deferredColorTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(colorFormat, width, height, defaultSampler)); + _deferredNormalTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(linearFormat, width, height, defaultSampler)); + _deferredSpecularTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(colorFormat, width, height, defaultSampler)); _deferredFramebuffer->setRenderBuffer(0, _deferredColorTexture); _deferredFramebuffer->setRenderBuffer(1, _deferredNormalTexture); @@ -65,7 +65,7 @@ void DeferredFramebuffer::allocate() { auto depthFormat = gpu::Element(gpu::SCALAR, gpu::UINT32, gpu::DEPTH_STENCIL); // Depth24_Stencil8 texel format if (!_primaryDepthTexture) { - _primaryDepthTexture = gpu::TexturePointer(gpu::Texture::create2D(depthFormat, width, height, defaultSampler)); + _primaryDepthTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(depthFormat, width, height, defaultSampler)); } _deferredFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, depthFormat); @@ -75,7 +75,7 @@ void DeferredFramebuffer::allocate() { auto smoothSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR); - _lightingTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::R11G11B10), width, height, defaultSampler)); + _lightingTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::R11G11B10), width, height, defaultSampler)); _lightingFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("lighting")); _lightingFramebuffer->setRenderBuffer(0, _lightingTexture); _lightingFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, depthFormat); diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp index 6f1152ac16..ce340583ee 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.cpp +++ b/libraries/render-utils/src/DeferredLightingEffect.cpp @@ -496,14 +496,14 @@ void PreparePrimaryFramebuffer::run(const SceneContextPointer& sceneContext, con auto colorFormat = gpu::Element::COLOR_SRGBA_32; auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT); - auto primaryColorTexture = gpu::TexturePointer(gpu::Texture::create2D(colorFormat, frameSize.x, frameSize.y, defaultSampler)); + auto primaryColorTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(colorFormat, frameSize.x, frameSize.y, defaultSampler)); _primaryFramebuffer->setRenderBuffer(0, primaryColorTexture); auto depthFormat = gpu::Element(gpu::SCALAR, gpu::UINT32, gpu::DEPTH_STENCIL); // Depth24_Stencil8 texel format - auto primaryDepthTexture = gpu::TexturePointer(gpu::Texture::create2D(depthFormat, frameSize.x, frameSize.y, defaultSampler)); + auto primaryDepthTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(depthFormat, frameSize.x, frameSize.y, defaultSampler)); _primaryFramebuffer->setDepthStencilBuffer(primaryDepthTexture, depthFormat); } diff --git a/libraries/render-utils/src/FramebufferCache.cpp b/libraries/render-utils/src/FramebufferCache.cpp index 27429595b4..72b3c2ceb4 100644 --- a/libraries/render-utils/src/FramebufferCache.cpp +++ b/libraries/render-utils/src/FramebufferCache.cpp @@ -21,7 +21,6 @@ void FramebufferCache::setFrameBufferSize(QSize frameBufferSize) { //If the size changed, we need to delete our FBOs if (_frameBufferSize != frameBufferSize) { _frameBufferSize = frameBufferSize; - _selfieFramebuffer.reset(); { std::unique_lock lock(_mutex); _cachedFramebuffers.clear(); @@ -30,16 +29,8 @@ void FramebufferCache::setFrameBufferSize(QSize frameBufferSize) { } void FramebufferCache::createPrimaryFramebuffer() { - auto colorFormat = gpu::Element::COLOR_SRGBA_32; - auto width = _frameBufferSize.width(); - auto height = _frameBufferSize.height(); - auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT); - _selfieFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("selfie")); - auto tex = gpu::TexturePointer(gpu::Texture::create2D(colorFormat, width * 0.5, height * 0.5, defaultSampler)); - _selfieFramebuffer->setRenderBuffer(0, tex); - auto smoothSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR); } @@ -60,10 +51,3 @@ void FramebufferCache::releaseFramebuffer(const gpu::FramebufferPointer& framebu _cachedFramebuffers.push_back(framebuffer); } } - -gpu::FramebufferPointer FramebufferCache::getSelfieFramebuffer() { - if (!_selfieFramebuffer) { - createPrimaryFramebuffer(); - } - return _selfieFramebuffer; -} diff --git a/libraries/render-utils/src/FramebufferCache.h b/libraries/render-utils/src/FramebufferCache.h index f74d224a61..8065357615 100644 --- a/libraries/render-utils/src/FramebufferCache.h +++ b/libraries/render-utils/src/FramebufferCache.h @@ -27,9 +27,6 @@ public: void setFrameBufferSize(QSize frameBufferSize); const QSize& getFrameBufferSize() const { return _frameBufferSize; } - /// Returns the framebuffer object used to render selfie maps; - gpu::FramebufferPointer getSelfieFramebuffer(); - /// Returns a free framebuffer with a single color attachment for temp or intra-frame operations gpu::FramebufferPointer getFramebuffer(); @@ -42,8 +39,6 @@ private: gpu::FramebufferPointer _shadowFramebuffer; - gpu::FramebufferPointer _selfieFramebuffer; - QSize _frameBufferSize{ 100, 100 }; std::mutex _mutex; diff --git a/libraries/render-utils/src/LightAmbient.slh b/libraries/render-utils/src/LightAmbient.slh index 15e23015cb..e343d8c239 100644 --- a/libraries/render-utils/src/LightAmbient.slh +++ b/libraries/render-utils/src/LightAmbient.slh @@ -30,9 +30,8 @@ vec3 fresnelSchlickAmbient(vec3 fresnelColor, vec3 lightDir, vec3 halfDir, float <$declareSkyboxMap()$> <@endif@> -vec3 evalAmbientSpecularIrradiance(LightAmbient ambient, vec3 fragEyeDir, vec3 fragNormal, float roughness, vec3 fresnel) { +vec3 evalAmbientSpecularIrradiance(LightAmbient ambient, vec3 fragEyeDir, vec3 fragNormal, float roughness) { vec3 direction = -reflect(fragEyeDir, fragNormal); - vec3 ambientFresnel = fresnelSchlickAmbient(fresnel, fragEyeDir, fragNormal, 1.0 - roughness); vec3 specularLight; <@if supportIfAmbientMapElseAmbientSphere@> if (getLightHasAmbientMap(ambient)) @@ -53,7 +52,7 @@ vec3 evalAmbientSpecularIrradiance(LightAmbient ambient, vec3 fragEyeDir, vec3 f } <@endif@> - return specularLight * ambientFresnel; + return specularLight; } <@endfunc@> @@ -74,12 +73,14 @@ void evalLightingAmbient(out vec3 diffuse, out vec3 specular, LightAmbient ambie <@endif@> ) { + // Fresnel + vec3 ambientFresnel = fresnelSchlickAmbient(fresnel, eyeDir, normal, 1.0 - roughness); + // Diffuse from ambient - diffuse = (1.0 - metallic) * sphericalHarmonics_evalSphericalLight(getLightAmbientSphere(ambient), normal).xyz; + diffuse = (1.0 - metallic) * (vec3(1.0) - ambientFresnel) * sphericalHarmonics_evalSphericalLight(getLightAmbientSphere(ambient), normal).xyz; // Specular highlight from ambient - specular = evalAmbientSpecularIrradiance(ambient, eyeDir, normal, roughness, fresnel) * obscurance * getLightAmbientIntensity(ambient); - + specular = evalAmbientSpecularIrradiance(ambient, eyeDir, normal, roughness) * ambientFresnel; <@if supportScattering@> float ambientOcclusion = curvatureAO(lowNormalCurvature.w * 20.0f) * 0.5f; diff --git a/libraries/render-utils/src/LightingModel.cpp b/libraries/render-utils/src/LightingModel.cpp index 47af83da36..bd321bad95 100644 --- a/libraries/render-utils/src/LightingModel.cpp +++ b/libraries/render-utils/src/LightingModel.cpp @@ -133,6 +133,7 @@ void LightingModel::setSpotLight(bool enable) { bool LightingModel::isSpotLightEnabled() const { return (bool)_parametersBuffer.get().enableSpotLight; } + void LightingModel::setShowLightContour(bool enable) { if (enable != isShowLightContourEnabled()) { _parametersBuffer.edit().showLightContour = (float)enable; @@ -142,6 +143,14 @@ bool LightingModel::isShowLightContourEnabled() const { return (bool)_parametersBuffer.get().showLightContour; } +void LightingModel::setWireframe(bool enable) { + if (enable != isWireframeEnabled()) { + _parametersBuffer.edit().enableWireframe = (float)enable; + } +} +bool LightingModel::isWireframeEnabled() const { + return (bool)_parametersBuffer.get().enableWireframe; +} MakeLightingModel::MakeLightingModel() { _lightingModel = std::make_shared(); } @@ -167,6 +176,7 @@ void MakeLightingModel::configure(const Config& config) { _lightingModel->setSpotLight(config.enableSpotLight); _lightingModel->setShowLightContour(config.showLightContour); + _lightingModel->setWireframe(config.enableWireframe); } void MakeLightingModel::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, LightingModelPointer& lightingModel) { diff --git a/libraries/render-utils/src/LightingModel.h b/libraries/render-utils/src/LightingModel.h index 45514654f2..c1189d5160 100644 --- a/libraries/render-utils/src/LightingModel.h +++ b/libraries/render-utils/src/LightingModel.h @@ -64,6 +64,9 @@ public: void setShowLightContour(bool enable); bool isShowLightContourEnabled() const; + void setWireframe(bool enable); + bool isWireframeEnabled() const; + UniformBufferView getParametersBuffer() const { return _parametersBuffer; } protected: @@ -89,13 +92,12 @@ protected: float enablePointLight{ 1.0f }; float enableSpotLight{ 1.0f }; - float showLightContour{ 0.0f }; // false by default + float showLightContour { 0.0f }; // false by default float enableObscurance{ 1.0f }; float enableMaterialTexturing { 1.0f }; - - float spares{ 0.0f }; + float enableWireframe { 0.0f }; // false by default Parameters() {} }; @@ -129,6 +131,7 @@ class MakeLightingModelConfig : public render::Job::Config { Q_PROPERTY(bool enablePointLight MEMBER enablePointLight NOTIFY dirty) Q_PROPERTY(bool enableSpotLight MEMBER enableSpotLight NOTIFY dirty) + Q_PROPERTY(bool enableWireframe MEMBER enableWireframe NOTIFY dirty) Q_PROPERTY(bool showLightContour MEMBER showLightContour NOTIFY dirty) public: @@ -152,9 +155,10 @@ public: bool enablePointLight{ true }; bool enableSpotLight{ true }; - bool showLightContour { false }; // false by default + bool enableWireframe { false }; // false by default + signals: void dirty(); }; diff --git a/libraries/render-utils/src/LightingModel.slh b/libraries/render-utils/src/LightingModel.slh index 74285aa6a9..209a1f38d6 100644 --- a/libraries/render-utils/src/LightingModel.slh +++ b/libraries/render-utils/src/LightingModel.slh @@ -17,7 +17,7 @@ struct LightingModel { vec4 _UnlitEmissiveLightmapBackground; vec4 _ScatteringDiffuseSpecularAlbedo; vec4 _AmbientDirectionalPointSpot; - vec4 _ShowContourObscuranceSpare2; + vec4 _ShowContourObscuranceWireframe; }; uniform lightingModelBuffer{ @@ -37,7 +37,7 @@ float isBackgroundEnabled() { return lightingModel._UnlitEmissiveLightmapBackground.w; } float isObscuranceEnabled() { - return lightingModel._ShowContourObscuranceSpare2.y; + return lightingModel._ShowContourObscuranceWireframe.y; } float isScatteringEnabled() { @@ -67,9 +67,12 @@ float isSpotEnabled() { } float isShowLightContour() { - return lightingModel._ShowContourObscuranceSpare2.x; + return lightingModel._ShowContourObscuranceWireframe.x; } +float isWireframeEnabled() { + return lightingModel._ShowContourObscuranceWireframe.z; +} <@endfunc@> <$declareLightingModel()$> diff --git a/libraries/render-utils/src/MaterialTextures.slh b/libraries/render-utils/src/MaterialTextures.slh index 6d2ad23c21..7b73896cc5 100644 --- a/libraries/render-utils/src/MaterialTextures.slh +++ b/libraries/render-utils/src/MaterialTextures.slh @@ -64,7 +64,7 @@ float fetchRoughnessMap(vec2 uv) { uniform sampler2D normalMap; vec3 fetchNormalMap(vec2 uv) { // unpack normal, swizzle to get into hifi tangent space with Y axis pointing out - return normalize(texture(normalMap, uv).xzy -vec3(0.5, 0.5, 0.5)); + return normalize(texture(normalMap, uv).rbg -vec3(0.5, 0.5, 0.5)); } <@endif@> diff --git a/libraries/render-utils/src/MeshPartPayload.cpp b/libraries/render-utils/src/MeshPartPayload.cpp index 5b3d285b47..41a1bb4c74 100644 --- a/libraries/render-utils/src/MeshPartPayload.cpp +++ b/libraries/render-utils/src/MeshPartPayload.cpp @@ -372,19 +372,12 @@ void ModelMeshPartPayload::notifyLocationChanged() { } -void ModelMeshPartPayload::updateTransformForSkinnedMesh(const Transform& transform, const QVector& clusterMatrices) { - _transform = transform; - - if (clusterMatrices.size() > 0) { - _worldBound = _adjustedLocalBound; - _worldBound.transform(_transform); - if (clusterMatrices.size() == 1) { - _transform = _transform.worldTransform(Transform(clusterMatrices[0])); - } - } else { - _worldBound = _localBound; - _worldBound.transform(_transform); - } +void ModelMeshPartPayload::updateTransformForSkinnedMesh(const Transform& renderTransform, const Transform& boundTransform, + const gpu::BufferPointer& buffer) { + _transform = renderTransform; + _worldBound = _adjustedLocalBound; + _worldBound.transform(boundTransform); + _clusterBuffer = buffer; } ItemKey ModelMeshPartPayload::getKey() const { @@ -532,9 +525,8 @@ void ModelMeshPartPayload::bindMesh(gpu::Batch& batch) const { void ModelMeshPartPayload::bindTransform(gpu::Batch& batch, const ShapePipeline::LocationsPointer locations, RenderArgs::RenderMode renderMode) const { // Still relying on the raw data from the model - const Model::MeshState& state = _model->getMeshState(_meshIndex); - if (state.clusterBuffer) { - batch.setUniformBuffer(ShapePipeline::Slot::BUFFER::SKINNING, state.clusterBuffer); + if (_clusterBuffer) { + batch.setUniformBuffer(ShapePipeline::Slot::BUFFER::SKINNING, _clusterBuffer); } batch.setModelTransform(_transform); } @@ -590,8 +582,6 @@ void ModelMeshPartPayload::render(RenderArgs* args) const { auto locations = args->_pipeline->locations; assert(locations); - // Bind the model transform and the skinCLusterMatrices if needed - _model->updateClusterMatrices(); bindTransform(batch, locations, args->_renderMode); //Bind the index buffer and vertex buffer and Blend shapes if needed diff --git a/libraries/render-utils/src/MeshPartPayload.h b/libraries/render-utils/src/MeshPartPayload.h index c585c95025..ef74011c40 100644 --- a/libraries/render-utils/src/MeshPartPayload.h +++ b/libraries/render-utils/src/MeshPartPayload.h @@ -89,8 +89,9 @@ public: typedef Payload::DataPointer Pointer; void notifyLocationChanged() override; - void updateTransformForSkinnedMesh(const Transform& transform, - const QVector& clusterMatrices); + void updateTransformForSkinnedMesh(const Transform& renderTransform, + const Transform& boundTransform, + const gpu::BufferPointer& buffer); float computeFadeAlpha() const; @@ -108,6 +109,7 @@ public: void computeAdjustedLocalBound(const QVector& clusterMatrices); + gpu::BufferPointer _clusterBuffer; Model* _model; int _meshIndex; diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 48c1d29b68..c584b0bc21 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -227,6 +227,10 @@ void Model::updateRenderItems() { return; } + // lazy update of cluster matrices used for rendering. + // We need to update them here so we can correctly update the bounding box. + self->updateClusterMatrices(); + render::ScenePointer scene = AbstractViewStateInterface::instance()->getMain3DScene(); uint32_t deleteGeometryCounter = self->_deleteGeometryCounter; @@ -240,12 +244,12 @@ void Model::updateRenderItems() { Transform modelTransform = data._model->getTransform(); modelTransform.setScale(glm::vec3(1.0f)); - // lazy update of cluster matrices used for rendering. We need to update them here, so we can correctly update the bounding box. - data._model->updateClusterMatrices(); - - // update the model transform and bounding box for this render item. - const Model::MeshState& state = data._model->_meshStates.at(data._meshIndex); - data.updateTransformForSkinnedMesh(modelTransform, state.clusterMatrices); + const Model::MeshState& state = data._model->getMeshState(data._meshIndex); + Transform renderTransform = modelTransform; + if (state.clusterMatrices.size() == 1) { + renderTransform = modelTransform.worldTransform(Transform(state.clusterMatrices[0])); + } + data.updateTransformForSkinnedMesh(renderTransform, modelTransform, state.clusterBuffer); } } }); @@ -1048,7 +1052,7 @@ void Model::updateRig(float deltaTime, glm::mat4 parentTransform) { } void Model::computeMeshPartLocalBounds() { - for (auto& part : _modelMeshRenderItemsSet) { + for (auto& part : _modelMeshRenderItemsSet) { assert(part->_meshIndex < _modelMeshRenderItemsSet.size()); const Model::MeshState& state = _meshStates.at(part->_meshIndex); part->computeAdjustedLocalBound(state.clusterMatrices); diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index 676d176cca..22aa95090c 100644 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -194,7 +194,7 @@ RenderDeferredTask::RenderDeferredTask(RenderFetchCullSortTask::Output items) { { // Grab a texture map representing the different status icons and assign that to the drawStatsuJob auto iconMapPath = PathUtils::resourcesPath() + "icons/statusIconAtlas.svg"; - auto statusIconMap = DependencyManager::get()->getImageTexture(iconMapPath); + auto statusIconMap = DependencyManager::get()->getImageTexture(iconMapPath, NetworkTexture::STRICT_TEXTURE); addJob("DrawStatus", opaques, DrawStatus(statusIconMap)); } } @@ -259,8 +259,18 @@ void DrawDeferred::run(const SceneContextPointer& sceneContext, const RenderCont // Setup lighting model for all items; batch.setUniformBuffer(render::ShapePipeline::Slot::LIGHTING_MODEL, lightingModel->getParametersBuffer()); - renderShapes(sceneContext, renderContext, _shapePlumber, inItems, _maxDrawn); + // From the lighting model define a global shapKey ORED with individiual keys + ShapeKey::Builder keyBuilder; + if (lightingModel->isWireframeEnabled()) { + keyBuilder.withWireframe(); + } + ShapeKey globalKey = keyBuilder.build(); + args->_globalShapeKey = globalKey._flags.to_ulong(); + + renderShapes(sceneContext, renderContext, _shapePlumber, inItems, _maxDrawn, globalKey); + args->_batch = nullptr; + args->_globalShapeKey = 0; }); config->setNumDrawn((int)inItems.size()); @@ -295,12 +305,21 @@ void DrawStateSortDeferred::run(const SceneContextPointer& sceneContext, const R // Setup lighting model for all items; batch.setUniformBuffer(render::ShapePipeline::Slot::LIGHTING_MODEL, lightingModel->getParametersBuffer()); + // From the lighting model define a global shapKey ORED with individiual keys + ShapeKey::Builder keyBuilder; + if (lightingModel->isWireframeEnabled()) { + keyBuilder.withWireframe(); + } + ShapeKey globalKey = keyBuilder.build(); + args->_globalShapeKey = globalKey._flags.to_ulong(); + if (_stateSort) { - renderStateSortShapes(sceneContext, renderContext, _shapePlumber, inItems, _maxDrawn); + renderStateSortShapes(sceneContext, renderContext, _shapePlumber, inItems, _maxDrawn, globalKey); } else { - renderShapes(sceneContext, renderContext, _shapePlumber, inItems, _maxDrawn); + renderShapes(sceneContext, renderContext, _shapePlumber, inItems, _maxDrawn, globalKey); } args->_batch = nullptr; + args->_globalShapeKey = 0; }); config->setNumDrawn((int)inItems.size()); diff --git a/libraries/render-utils/src/RenderPipelines.cpp b/libraries/render-utils/src/RenderPipelines.cpp index 4fbac4170e..414bcf0d63 100644 --- a/libraries/render-utils/src/RenderPipelines.cpp +++ b/libraries/render-utils/src/RenderPipelines.cpp @@ -307,7 +307,7 @@ void initForwardPipelines(render::ShapePlumber& plumber) { void addPlumberPipeline(ShapePlumber& plumber, const ShapeKey& key, const gpu::ShaderPointer& vertex, const gpu::ShaderPointer& pixel) { // These key-values' pipelines are added by this functor in addition to the key passed - assert(!key.isWireFrame()); + assert(!key.isWireframe()); assert(!key.isDepthBiased()); assert(key.isCullFace()); diff --git a/libraries/render-utils/src/SubsurfaceScattering.cpp b/libraries/render-utils/src/SubsurfaceScattering.cpp index 188381b822..25a01bff1b 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.cpp +++ b/libraries/render-utils/src/SubsurfaceScattering.cpp @@ -414,7 +414,7 @@ gpu::TexturePointer SubsurfaceScatteringResource::generateScatteringProfile(Rend const int PROFILE_RESOLUTION = 512; // const auto pixelFormat = gpu::Element::COLOR_SRGBA_32; const auto pixelFormat = gpu::Element::COLOR_R11G11B10; - auto profileMap = gpu::TexturePointer(gpu::Texture::create2D(pixelFormat, PROFILE_RESOLUTION, 1, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP))); + auto profileMap = gpu::TexturePointer(gpu::Texture::createRenderBuffer(pixelFormat, PROFILE_RESOLUTION, 1, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP))); profileMap->setSource("Generated Scattering Profile"); diffuseProfileGPU(profileMap, args); return profileMap; @@ -425,7 +425,7 @@ gpu::TexturePointer SubsurfaceScatteringResource::generatePreIntegratedScatterin const int TABLE_RESOLUTION = 512; // const auto pixelFormat = gpu::Element::COLOR_SRGBA_32; const auto pixelFormat = gpu::Element::COLOR_R11G11B10; - auto scatteringLUT = gpu::TexturePointer(gpu::Texture::create2D(pixelFormat, TABLE_RESOLUTION, TABLE_RESOLUTION, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP))); + auto scatteringLUT = gpu::TexturePointer(gpu::Texture::createRenderBuffer(pixelFormat, TABLE_RESOLUTION, TABLE_RESOLUTION, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP))); //diffuseScatter(scatteringLUT); scatteringLUT->setSource("Generated pre-integrated scattering"); diffuseScatterGPU(profile, scatteringLUT, args); @@ -434,7 +434,7 @@ gpu::TexturePointer SubsurfaceScatteringResource::generatePreIntegratedScatterin gpu::TexturePointer SubsurfaceScatteringResource::generateScatteringSpecularBeckmann(RenderArgs* args) { const int SPECULAR_RESOLUTION = 256; - auto beckmannMap = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32 /*gpu::Element(gpu::SCALAR, gpu::HALF, gpu::RGB)*/, SPECULAR_RESOLUTION, SPECULAR_RESOLUTION, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP))); + auto beckmannMap = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32 /*gpu::Element(gpu::SCALAR, gpu::HALF, gpu::RGB)*/, SPECULAR_RESOLUTION, SPECULAR_RESOLUTION, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP))); beckmannMap->setSource("Generated beckmannMap"); computeSpecularBeckmannGPU(beckmannMap, args); return beckmannMap; diff --git a/libraries/render-utils/src/SurfaceGeometryPass.cpp b/libraries/render-utils/src/SurfaceGeometryPass.cpp index f0ac56ac26..3a23e70664 100644 --- a/libraries/render-utils/src/SurfaceGeometryPass.cpp +++ b/libraries/render-utils/src/SurfaceGeometryPass.cpp @@ -72,18 +72,18 @@ void LinearDepthFramebuffer::allocate() { auto height = _frameSize.y; // For Linear Depth: - _linearDepthTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::RGB), width, height, + _linearDepthTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::RGB), width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); _linearDepthFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("linearDepth")); _linearDepthFramebuffer->setRenderBuffer(0, _linearDepthTexture); _linearDepthFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, _primaryDepthTexture->getTexelFormat()); // For Downsampling: - _halfLinearDepthTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::RGB), _halfFrameSize.x, _halfFrameSize.y, + _halfLinearDepthTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::RGB), _halfFrameSize.x, _halfFrameSize.y, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); _halfLinearDepthTexture->autoGenerateMips(5); - _halfNormalTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element(gpu::VEC3, gpu::NUINT8, gpu::RGB), _halfFrameSize.x, _halfFrameSize.y, + _halfNormalTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element(gpu::VEC3, gpu::NUINT8, gpu::RGB), _halfFrameSize.x, _halfFrameSize.y, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); _downsampleFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("halfLinearDepth")); @@ -304,15 +304,15 @@ void SurfaceGeometryFramebuffer::allocate() { auto width = _frameSize.x; auto height = _frameSize.y; - _curvatureTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); + _curvatureTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32, width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); _curvatureFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("surfaceGeometry::curvature")); _curvatureFramebuffer->setRenderBuffer(0, _curvatureTexture); - _lowCurvatureTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); + _lowCurvatureTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32, width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); _lowCurvatureFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("surfaceGeometry::lowCurvature")); _lowCurvatureFramebuffer->setRenderBuffer(0, _lowCurvatureTexture); - _blurringTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); + _blurringTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32, width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); _blurringFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("surfaceGeometry::blurring")); _blurringFramebuffer->setRenderBuffer(0, _blurringTexture); } diff --git a/libraries/render-utils/src/text/Font.cpp b/libraries/render-utils/src/text/Font.cpp index 4f4ee12622..c405f6d6ae 100644 --- a/libraries/render-utils/src/text/Font.cpp +++ b/libraries/render-utils/src/text/Font.cpp @@ -209,7 +209,8 @@ void Font::read(QIODevice& in) { } _texture = gpu::TexturePointer(gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_POINT_MAG_LINEAR))); - _texture->assignStoredMip(0, formatMip, image.byteCount(), image.constBits()); + _texture->setStoredMipFormat(formatMip); + _texture->assignStoredMip(0, image.byteCount(), image.constBits()); } void Font::setupGPU() { diff --git a/libraries/render/CMakeLists.txt b/libraries/render/CMakeLists.txt index 735bb7f086..8fd05bd320 100644 --- a/libraries/render/CMakeLists.txt +++ b/libraries/render/CMakeLists.txt @@ -3,6 +3,6 @@ AUTOSCRIBE_SHADER_LIB(gpu model) setup_hifi_library() # render needs octree only for getAccuracyAngle(float, int) -link_hifi_libraries(shared gpu model octree) +link_hifi_libraries(shared ktx gpu model octree) target_nsight() diff --git a/libraries/render/src/render/DrawTask.cpp b/libraries/render/src/render/DrawTask.cpp index 2829c6f8e7..e8537e3452 100755 --- a/libraries/render/src/render/DrawTask.cpp +++ b/libraries/render/src/render/DrawTask.cpp @@ -39,9 +39,9 @@ void render::renderItems(const SceneContextPointer& sceneContext, const RenderCo } } -void renderShape(RenderArgs* args, const ShapePlumberPointer& shapeContext, const Item& item) { +void renderShape(RenderArgs* args, const ShapePlumberPointer& shapeContext, const Item& item, const ShapeKey& globalKey) { assert(item.getKey().isShape()); - const auto& key = item.getShapeKey(); + auto key = item.getShapeKey() | globalKey; if (key.isValid() && !key.hasOwnPipeline()) { args->_pipeline = shapeContext->pickPipeline(args, key); if (args->_pipeline) { @@ -56,7 +56,7 @@ void renderShape(RenderArgs* args, const ShapePlumberPointer& shapeContext, cons } void render::renderShapes(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, - const ShapePlumberPointer& shapeContext, const ItemBounds& inItems, int maxDrawnItems) { + const ShapePlumberPointer& shapeContext, const ItemBounds& inItems, int maxDrawnItems, const ShapeKey& globalKey) { auto& scene = sceneContext->_scene; RenderArgs* args = renderContext->args; @@ -66,12 +66,12 @@ void render::renderShapes(const SceneContextPointer& sceneContext, const RenderC } for (auto i = 0; i < numItemsToDraw; ++i) { auto& item = scene->getItem(inItems[i].id); - renderShape(args, shapeContext, item); + renderShape(args, shapeContext, item, globalKey); } } void render::renderStateSortShapes(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, - const ShapePlumberPointer& shapeContext, const ItemBounds& inItems, int maxDrawnItems) { + const ShapePlumberPointer& shapeContext, const ItemBounds& inItems, int maxDrawnItems, const ShapeKey& globalKey) { auto& scene = sceneContext->_scene; RenderArgs* args = renderContext->args; @@ -91,7 +91,7 @@ void render::renderStateSortShapes(const SceneContextPointer& sceneContext, cons { assert(item.getKey().isShape()); - const auto key = item.getShapeKey(); + auto key = item.getShapeKey() | globalKey; if (key.isValid() && !key.hasOwnPipeline()) { auto& bucket = sortedShapes[key]; if (bucket.empty()) { diff --git a/libraries/render/src/render/DrawTask.h b/libraries/render/src/render/DrawTask.h index 6e0e5ba10b..a9c5f3a4d8 100755 --- a/libraries/render/src/render/DrawTask.h +++ b/libraries/render/src/render/DrawTask.h @@ -17,8 +17,8 @@ namespace render { void renderItems(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const ItemBounds& inItems, int maxDrawnItems = -1); -void renderShapes(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const ShapePlumberPointer& shapeContext, const ItemBounds& inItems, int maxDrawnItems = -1); -void renderStateSortShapes(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const ShapePlumberPointer& shapeContext, const ItemBounds& inItems, int maxDrawnItems = -1); +void renderShapes(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const ShapePlumberPointer& shapeContext, const ItemBounds& inItems, int maxDrawnItems = -1, const ShapeKey& globalKey = ShapeKey()); +void renderStateSortShapes(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const ShapePlumberPointer& shapeContext, const ItemBounds& inItems, int maxDrawnItems = -1, const ShapeKey& globalKey = ShapeKey()); class DrawLightConfig : public Job::Config { Q_OBJECT diff --git a/libraries/render/src/render/ShapePipeline.h b/libraries/render/src/render/ShapePipeline.h index 0c77a15184..73e8f82f24 100644 --- a/libraries/render/src/render/ShapePipeline.h +++ b/libraries/render/src/render/ShapePipeline.h @@ -46,6 +46,10 @@ public: ShapeKey() : _flags{ 0 } {} ShapeKey(const Flags& flags) : _flags{flags} {} + friend ShapeKey operator&(const ShapeKey& _Left, const ShapeKey& _Right) { return ShapeKey(_Left._flags & _Right._flags); } + friend ShapeKey operator|(const ShapeKey& _Left, const ShapeKey& _Right) { return ShapeKey(_Left._flags | _Right._flags); } + friend ShapeKey operator^(const ShapeKey& _Left, const ShapeKey& _Right) { return ShapeKey(_Left._flags ^ _Right._flags); } + class Builder { public: Builder() {} @@ -144,7 +148,7 @@ public: bool isSkinned() const { return _flags[SKINNED]; } bool isDepthOnly() const { return _flags[DEPTH_ONLY]; } bool isDepthBiased() const { return _flags[DEPTH_BIAS]; } - bool isWireFrame() const { return _flags[WIREFRAME]; } + bool isWireframe() const { return _flags[WIREFRAME]; } bool isCullFace() const { return !_flags[NO_CULL_FACE]; } bool hasOwnPipeline() const { return _flags[OWN_PIPELINE]; } @@ -180,7 +184,7 @@ inline QDebug operator<<(QDebug debug, const ShapeKey& key) { << "isSkinned:" << key.isSkinned() << "isDepthOnly:" << key.isDepthOnly() << "isDepthBiased:" << key.isDepthBiased() - << "isWireFrame:" << key.isWireFrame() + << "isWireframe:" << key.isWireframe() << "isCullFace:" << key.isCullFace() << "]"; } diff --git a/libraries/render/src/render/drawItemStatus.slv b/libraries/render/src/render/drawItemStatus.slv index cb4ae7ebd2..792f2733c5 100644 --- a/libraries/render/src/render/drawItemStatus.slv +++ b/libraries/render/src/render/drawItemStatus.slv @@ -75,7 +75,7 @@ void main(void) { vec4(1.0, 1.0, 0.0, 1.0) ); - const vec2 ICON_PIXEL_SIZE = vec2(20, 20); + const vec2 ICON_PIXEL_SIZE = vec2(36, 36); const vec2 MARGIN_PIXEL_SIZE = vec2(2, 2); const vec2 ICON_GRID_SLOTS[MAX_NUM_ICONS] = vec2[MAX_NUM_ICONS](vec2(-1.5, 0.5), vec2(-0.5, 0.5), @@ -114,7 +114,7 @@ void main(void) { varColor = vec4(paintRainbow(abs(iconStatus.y)), 1.0); // Pass the texcoord and the z texcoord is representing the texture icon - varTexcoord = vec3((quadPos.xy + 1.0) * 0.5, iconStatus.z); + varTexcoord = vec3( (quadPos.x + 1.0) * 0.5, (quadPos.y + 1.0) * -0.5, iconStatus.z); // Also changes the size of the notification vec2 iconScale = ICON_PIXEL_SIZE; diff --git a/libraries/script-engine/src/AudioScriptingInterface.cpp b/libraries/script-engine/src/AudioScriptingInterface.cpp index fcc1f201f9..8452494d95 100644 --- a/libraries/script-engine/src/AudioScriptingInterface.cpp +++ b/libraries/script-engine/src/AudioScriptingInterface.cpp @@ -19,11 +19,6 @@ void registerAudioMetaTypes(QScriptEngine* engine) { qScriptRegisterMetaType(engine, soundSharedPointerToScriptValue, soundSharedPointerFromScriptValue); } -AudioScriptingInterface& AudioScriptingInterface::getInstance() { - static AudioScriptingInterface staticInstance; - return staticInstance; -} - AudioScriptingInterface::AudioScriptingInterface() : _localAudioInterface(NULL) { diff --git a/libraries/script-engine/src/AudioScriptingInterface.h b/libraries/script-engine/src/AudioScriptingInterface.h index 6cce78d48f..e97bc329c6 100644 --- a/libraries/script-engine/src/AudioScriptingInterface.h +++ b/libraries/script-engine/src/AudioScriptingInterface.h @@ -14,18 +14,20 @@ #include #include +#include #include class ScriptAudioInjector; -class AudioScriptingInterface : public QObject { +class AudioScriptingInterface : public QObject, public Dependency { Q_OBJECT -public: - static AudioScriptingInterface& getInstance(); + SINGLETON_DEPENDENCY +public: void setLocalAudioInterface(AbstractAudioInterface* audioInterface) { _localAudioInterface = audioInterface; } protected: + // this method is protected to stop C++ callers from calling, but invokable from script Q_INVOKABLE ScriptAudioInjector* playSound(SharedSoundPointer sound, const AudioInjectorOptions& injectorOptions = AudioInjectorOptions()); @@ -42,6 +44,7 @@ signals: private: AudioScriptingInterface(); + AbstractAudioInterface* _localAudioInterface; }; diff --git a/libraries/script-engine/src/BaseScriptEngine.h b/libraries/script-engine/src/BaseScriptEngine.h deleted file mode 100644 index 27a6eff33d..0000000000 --- a/libraries/script-engine/src/BaseScriptEngine.h +++ /dev/null @@ -1,67 +0,0 @@ -// -// BaseScriptEngine.h -// libraries/script-engine/src -// -// Created by Timothy Dedischew on 02/01/17. -// Copyright 2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#ifndef hifi_BaseScriptEngine_h -#define hifi_BaseScriptEngine_h - -#include -#include -#include - -#include "SettingHandle.h" - -// common base class for extending QScriptEngine itself -class BaseScriptEngine : public QScriptEngine { - Q_OBJECT -public: - static const QString SCRIPT_EXCEPTION_FORMAT; - static const QString SCRIPT_BACKTRACE_SEP; - - BaseScriptEngine() {} - - Q_INVOKABLE QScriptValue evaluateInClosure(const QScriptValue& locals, const QScriptProgram& program); - - Q_INVOKABLE QScriptValue lintScript(const QString& sourceCode, const QString& fileName, const int lineNumber = 1); - Q_INVOKABLE QScriptValue makeError(const QScriptValue& other = QScriptValue(), const QString& type = "Error"); - Q_INVOKABLE QString formatException(const QScriptValue& exception); - QScriptValue cloneUncaughtException(const QString& detail = QString()); - -signals: - void unhandledException(const QScriptValue& exception); - -protected: - void _emitUnhandledException(const QScriptValue& exception); - QScriptValue newLambdaFunction(std::function operation, const QScriptValue& data = QScriptValue(), const QScriptEngine::ValueOwnership& ownership = QScriptEngine::AutoOwnership); - - static const QString _SETTINGS_ENABLE_EXTENDED_EXCEPTIONS; - Setting::Handle _enableExtendedJSExceptions { _SETTINGS_ENABLE_EXTENDED_EXCEPTIONS, true }; -#ifdef DEBUG_JS - static void _debugDump(const QString& header, const QScriptValue& object, const QString& footer = QString()); -#endif -}; - -// Lambda helps create callable QScriptValues out of std::functions: -// (just meant for use from within the script engine itself) -class Lambda : public QObject { - Q_OBJECT -public: - Lambda(QScriptEngine *engine, std::function operation, QScriptValue data); - ~Lambda(); - public slots: - QScriptValue call(); - QString toString() const; -private: - QScriptEngine* engine; - std::function operation; - QScriptValue data; -}; - -#endif // hifi_BaseScriptEngine_h diff --git a/libraries/script-engine/src/MeshProxy.h b/libraries/script-engine/src/MeshProxy.h new file mode 100644 index 0000000000..82f5038348 --- /dev/null +++ b/libraries/script-engine/src/MeshProxy.h @@ -0,0 +1,41 @@ +// +// MeshProxy.h +// libraries/script-engine/src +// +// Created by Seth Alves on 2017-1-27. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_MeshProxy_h +#define hifi_MeshProxy_h + +#include + +using MeshPointer = std::shared_ptr; + +class MeshProxy : public QObject { + Q_OBJECT + +public: + MeshProxy(MeshPointer mesh) : _mesh(mesh) {} + ~MeshProxy() {} + + MeshPointer getMeshPointer() const { return _mesh; } + + Q_INVOKABLE int getNumVertices() const { return (int)_mesh->getNumVertices(); } + Q_INVOKABLE glm::vec3 getPos3(int index) const { return _mesh->getPos3(index); } + + +protected: + MeshPointer _mesh; +}; + +Q_DECLARE_METATYPE(MeshProxy*); + +class MeshProxyList : public QList {}; // typedef and using fight with the Qt macros/templates, do this instead +Q_DECLARE_METATYPE(MeshProxyList); + +#endif // hifi_MeshProxy_h diff --git a/libraries/script-engine/src/ModelScriptingInterface.cpp b/libraries/script-engine/src/ModelScriptingInterface.cpp new file mode 100644 index 0000000000..833ac5b64d --- /dev/null +++ b/libraries/script-engine/src/ModelScriptingInterface.cpp @@ -0,0 +1,159 @@ +// +// ModelScriptingInterface.cpp +// libraries/script-engine/src +// +// Created by Seth Alves on 2017-1-27. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include +#include +#include +#include "ScriptEngine.h" +#include "ModelScriptingInterface.h" +#include "OBJWriter.h" + +ModelScriptingInterface::ModelScriptingInterface(QObject* parent) : QObject(parent) { + _modelScriptEngine = qobject_cast(parent); +} + +QScriptValue meshToScriptValue(QScriptEngine* engine, MeshProxy* const &in) { + return engine->newQObject(in, QScriptEngine::QtOwnership, + QScriptEngine::ExcludeDeleteLater | QScriptEngine::ExcludeChildObjects); +} + +void meshFromScriptValue(const QScriptValue& value, MeshProxy* &out) { + out = qobject_cast(value.toQObject()); +} + +QScriptValue meshesToScriptValue(QScriptEngine* engine, const MeshProxyList &in) { + return engine->toScriptValue(in); +} + +void meshesFromScriptValue(const QScriptValue& value, MeshProxyList &out) { + QScriptValueIterator itr(value); + while(itr.hasNext()) { + itr.next(); + MeshProxy* meshProxy = qscriptvalue_cast(itr.value()); + if (meshProxy) { + out.append(meshProxy); + } + } +} + +QString ModelScriptingInterface::meshToOBJ(MeshProxyList in) { + QList meshes; + foreach (const MeshProxy* meshProxy, in) { + meshes.append(meshProxy->getMeshPointer()); + } + + return writeOBJToString(meshes); +} + +QScriptValue ModelScriptingInterface::appendMeshes(MeshProxyList in) { + // figure out the size of the resulting mesh + size_t totalVertexCount { 0 }; + size_t totalAttributeCount { 0 }; + size_t totalIndexCount { 0 }; + foreach (const MeshProxy* meshProxy, in) { + MeshPointer mesh = meshProxy->getMeshPointer(); + totalVertexCount += mesh->getNumVertices(); + + int attributeTypeNormal = gpu::Stream::InputSlot::NORMAL; // libraries/gpu/src/gpu/Stream.h + const gpu::BufferView& normalsBufferView = mesh->getAttributeBuffer(attributeTypeNormal); + gpu::BufferView::Index numNormals = (gpu::BufferView::Index)normalsBufferView.getNumElements(); + totalAttributeCount += numNormals; + + totalIndexCount += mesh->getNumIndices(); + } + + // alloc the resulting mesh + gpu::Resource::Size combinedVertexSize = totalVertexCount * sizeof(glm::vec3); + unsigned char* combinedVertexData = new unsigned char[combinedVertexSize]; + unsigned char* combinedVertexDataCursor = combinedVertexData; + + gpu::Resource::Size combinedNormalSize = totalAttributeCount * sizeof(glm::vec3); + unsigned char* combinedNormalData = new unsigned char[combinedNormalSize]; + unsigned char* combinedNormalDataCursor = combinedNormalData; + + gpu::Resource::Size combinedIndexSize = totalIndexCount * sizeof(uint32_t); + unsigned char* combinedIndexData = new unsigned char[combinedIndexSize]; + unsigned char* combinedIndexDataCursor = combinedIndexData; + + uint32_t indexStartOffset { 0 }; + + foreach (const MeshProxy* meshProxy, in) { + MeshPointer mesh = meshProxy->getMeshPointer(); + mesh->forEach( + [&](glm::vec3 position){ + memcpy(combinedVertexDataCursor, &position, sizeof(position)); + combinedVertexDataCursor += sizeof(position); + }, + [&](glm::vec3 normal){ + memcpy(combinedNormalDataCursor, &normal, sizeof(normal)); + combinedNormalDataCursor += sizeof(normal); + }, + [&](uint32_t index){ + index += indexStartOffset; + memcpy(combinedIndexDataCursor, &index, sizeof(index)); + combinedIndexDataCursor += sizeof(index); + }); + + gpu::BufferView::Index numVertices = (gpu::BufferView::Index)mesh->getNumVertices(); + indexStartOffset += numVertices; + } + + model::MeshPointer result(new model::Mesh()); + + gpu::Element vertexElement = gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ); + gpu::Buffer* combinedVertexBuffer = new gpu::Buffer(combinedVertexSize, combinedVertexData); + gpu::BufferPointer combinedVertexBufferPointer(combinedVertexBuffer); + gpu::BufferView combinedVertexBufferView(combinedVertexBufferPointer, vertexElement); + result->setVertexBuffer(combinedVertexBufferView); + + int attributeTypeNormal = gpu::Stream::InputSlot::NORMAL; // libraries/gpu/src/gpu/Stream.h + gpu::Element normalElement = gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ); + gpu::Buffer* combinedNormalsBuffer = new gpu::Buffer(combinedNormalSize, combinedNormalData); + gpu::BufferPointer combinedNormalsBufferPointer(combinedNormalsBuffer); + gpu::BufferView combinedNormalsBufferView(combinedNormalsBufferPointer, normalElement); + result->addAttribute(attributeTypeNormal, combinedNormalsBufferView); + + gpu::Element indexElement = gpu::Element(gpu::SCALAR, gpu::UINT32, gpu::RAW); + gpu::Buffer* combinedIndexesBuffer = new gpu::Buffer(combinedIndexSize, combinedIndexData); + gpu::BufferPointer combinedIndexesBufferPointer(combinedIndexesBuffer); + gpu::BufferView combinedIndexesBufferView(combinedIndexesBufferPointer, indexElement); + result->setIndexBuffer(combinedIndexesBufferView); + + std::vector parts; + parts.emplace_back(model::Mesh::Part((model::Index)0, // startIndex + (model::Index)result->getNumIndices(), // numIndices + (model::Index)0, // baseVertex + model::Mesh::TRIANGLES)); // topology + result->setPartBuffer(gpu::BufferView(new gpu::Buffer(parts.size() * sizeof(model::Mesh::Part), + (gpu::Byte*) parts.data()), gpu::Element::PART_DRAWCALL)); + + + MeshProxy* resultProxy = new MeshProxy(result); + return meshToScriptValue(_modelScriptEngine, resultProxy); +} + + + +QScriptValue ModelScriptingInterface::transformMesh(glm::mat4 transform, MeshProxy* meshProxy) { + if (!meshProxy) { + return QScriptValue(false); + } + MeshPointer mesh = meshProxy->getMeshPointer(); + if (!mesh) { + return QScriptValue(false); + } + + model::MeshPointer result = mesh->map([&](glm::vec3 position){ return glm::vec3(transform * glm::vec4(position, 1.0f)); }, + [&](glm::vec3 normal){ return glm::vec3(transform * glm::vec4(normal, 0.0f)); }, + [&](uint32_t index){ return index; }); + MeshProxy* resultProxy = new MeshProxy(result); + return meshToScriptValue(_modelScriptEngine, resultProxy); +} diff --git a/libraries/script-engine/src/ModelScriptingInterface.h b/libraries/script-engine/src/ModelScriptingInterface.h new file mode 100644 index 0000000000..14789943e3 --- /dev/null +++ b/libraries/script-engine/src/ModelScriptingInterface.h @@ -0,0 +1,45 @@ +// +// ModelScriptingInterface.h +// libraries/script-engine/src +// +// Created by Seth Alves on 2017-1-27. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + + +#ifndef hifi_ModelScriptingInterface_h +#define hifi_ModelScriptingInterface_h + +#include +#include +#include +#include +#include "MeshProxy.h" + +using MeshPointer = std::shared_ptr; +class ScriptEngine; + +class ModelScriptingInterface : public QObject { + Q_OBJECT + +public: + ModelScriptingInterface(QObject* parent); + + Q_INVOKABLE QString meshToOBJ(MeshProxyList in); + Q_INVOKABLE QScriptValue appendMeshes(MeshProxyList in); + Q_INVOKABLE QScriptValue transformMesh(glm::mat4 transform, MeshProxy* meshProxy); + +private: + ScriptEngine* _modelScriptEngine { nullptr }; +}; + +QScriptValue meshToScriptValue(QScriptEngine* engine, MeshProxy* const &in); +void meshFromScriptValue(const QScriptValue& value, MeshProxy* &out); + +QScriptValue meshesToScriptValue(QScriptEngine* engine, const MeshProxyList &in); +void meshesFromScriptValue(const QScriptValue& value, MeshProxyList &out); + +#endif // hifi_ModelScriptingInterface_h diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index d721d1c86f..a5c94c1bb4 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -19,6 +19,9 @@ #include #include +#include +#include + #include #include @@ -65,18 +68,25 @@ #include "RecordingScriptingInterface.h" #include "ScriptEngines.h" #include "TabletScriptingInterface.h" +#include "ModelScriptingInterface.h" + #include #include "MIDIEvent.h" +const QString ScriptEngine::_SETTINGS_ENABLE_EXTENDED_EXCEPTIONS { + "com.highfidelity.experimental.enableExtendedJSExceptions" +}; + +static const int MAX_MODULE_ID_LENGTH { 4096 }; +static const int MAX_DEBUG_VALUE_LENGTH { 80 }; + static const QScriptEngine::QObjectWrapOptions DEFAULT_QOBJECT_WRAP_OPTIONS = QScriptEngine::ExcludeDeleteLater | QScriptEngine::ExcludeChildObjects; static const QScriptValue::PropertyFlags READONLY_PROP_FLAGS { QScriptValue::ReadOnly | QScriptValue::Undeletable }; static const QScriptValue::PropertyFlags READONLY_HIDDEN_PROP_FLAGS { READONLY_PROP_FLAGS | QScriptValue::SkipInEnumeration }; - - static const bool HIFI_AUTOREFRESH_FILE_SCRIPTS { true }; Q_DECLARE_METATYPE(QScriptEngine::FunctionSignature) @@ -84,7 +94,7 @@ int functionSignatureMetaID = qRegisterMetaTypeargumentCount(); i++) { if (i > 0) { @@ -141,7 +151,7 @@ QString encodeEntityIdIntoEntityUrl(const QString& url, const QString& entityID) } QString ScriptEngine::logException(const QScriptValue& exception) { - auto message = formatException(exception); + auto message = formatException(exception, _enableExtendedJSExceptions.get()); scriptErrorMessage(message); return message; } @@ -333,7 +343,7 @@ void ScriptEngine::runInThread() { // The thread interface cannot live on itself, and we want to move this into the thread, so // the thread cannot have this as a parent. QThread* workerThread = new QThread(); - workerThread->setObjectName(QString("Script Thread:") + getFilename()); + workerThread->setObjectName(QString("js:") + getFilename().replace("about:","")); moveToThread(workerThread); // NOTE: If you connect any essential signals for proper shutdown or cleanup of @@ -454,17 +464,17 @@ void ScriptEngine::loadURL(const QUrl& scriptURL, bool reload) { void ScriptEngine::scriptErrorMessage(const QString& message) { qCCritical(scriptengine) << qPrintable(message); - emit errorMessage(message); + emit errorMessage(message, getFilename()); } void ScriptEngine::scriptWarningMessage(const QString& message) { qCWarning(scriptengine) << message; - emit warningMessage(message); + emit warningMessage(message, getFilename()); } void ScriptEngine::scriptInfoMessage(const QString& message) { qCInfo(scriptengine) << message; - emit infoMessage(message); + emit infoMessage(message, getFilename()); } // Even though we never pass AnimVariantMap directly to and from javascript, the queued invokeMethod of @@ -532,6 +542,40 @@ static QScriptValue createScriptableResourcePrototype(QScriptEngine* engine) { return prototype; } +void ScriptEngine::resetModuleCache(bool deleteScriptCache) { + if (QThread::currentThread() != thread()) { + executeOnScriptThread([=]() { resetModuleCache(deleteScriptCache); }); + return; + } + auto jsRequire = globalObject().property("Script").property("require"); + auto cache = jsRequire.property("cache"); + auto cacheMeta = jsRequire.data(); + + if (deleteScriptCache) { + QScriptValueIterator it(cache); + while (it.hasNext()) { + it.next(); + if (it.flags() & QScriptValue::SkipInEnumeration) { + continue; + } + qCDebug(scriptengine) << "resetModuleCache(true) -- staging " << it.name() << " for cache reset at next require"; + cacheMeta.setProperty(it.name(), true); + } + } + cache = newObject(); + if (!cacheMeta.isObject()) { + cacheMeta = newObject(); + cacheMeta.setProperty("id", "Script.require.cacheMeta"); + cacheMeta.setProperty("type", "cacheMeta"); + jsRequire.setData(cacheMeta); + } + cache.setProperty("__created__", (double)QDateTime::currentMSecsSinceEpoch(), QScriptValue::SkipInEnumeration); +#if DEBUG_JS_MODULES + cache.setProperty("__meta__", cacheMeta, READONLY_HIDDEN_PROP_FLAGS); +#endif + jsRequire.setProperty("cache", cache, READONLY_PROP_FLAGS); +} + void ScriptEngine::init() { if (_isInitialized) { return; // only initialize once @@ -541,16 +585,6 @@ void ScriptEngine::init() { auto entityScriptingInterface = DependencyManager::get(); entityScriptingInterface->init(); - connect(entityScriptingInterface.data(), &EntityScriptingInterface::deletingEntity, this, [this](const EntityItemID& entityID) { - if (_entityScripts.contains(entityID)) { - if (isEntityScriptRunning(entityID)) { - qCWarning(scriptengine) << "deletingEntity while entity script is still running!" << entityID; - } - _entityScripts.remove(entityID); - emit entityScriptDetailsUpdated(); - } - }); - // register various meta-types registerMetaTypes(this); @@ -593,9 +627,22 @@ void ScriptEngine::init() { qScriptRegisterMetaType(this, qWSCloseCodeToScriptValue, qWSCloseCodeFromScriptValue); qScriptRegisterMetaType(this, wscReadyStateToScriptValue, wscReadyStateFromScriptValue); + // NOTE: You do not want to end up creating new instances of singletons here. They will be on the ScriptEngine thread + // and are likely to be unusable if we "reset" the ScriptEngine by creating a new one (on a whole new thread). + registerGlobalObject("Script", this); - registerGlobalObject("Audio", &AudioScriptingInterface::getInstance()); + { + // set up Script.require.resolve and Script.require.cache + auto Script = globalObject().property("Script"); + auto require = Script.property("require"); + auto resolve = Script.property("_requireResolve"); + require.setProperty("resolve", resolve, READONLY_PROP_FLAGS); + resetModuleCache(); + } + + registerGlobalObject("Audio", DependencyManager::get().data()); + registerGlobalObject("Entities", entityScriptingInterface.data()); registerGlobalObject("Quat", &_quatLibrary); registerGlobalObject("Vec3", &_vec3Library); @@ -604,7 +651,7 @@ void ScriptEngine::init() { registerGlobalObject("Messages", DependencyManager::get().data()); registerGlobalObject("File", new FileScriptingInterface(this)); - + qScriptRegisterMetaType(this, animVarMapToScriptValue, animVarMapFromScriptValue); qScriptRegisterMetaType(this, resultHandlerToScriptValue, resultHandlerFromScriptValue); @@ -622,6 +669,10 @@ void ScriptEngine::init() { registerGlobalObject("Resources", DependencyManager::get().data()); registerGlobalObject("DebugDraw", &DebugDraw::getInstance()); + + registerGlobalObject("Model", new ModelScriptingInterface(this)); + qScriptRegisterMetaType(this, meshToScriptValue, meshFromScriptValue); + qScriptRegisterMetaType(this, meshesToScriptValue, meshesFromScriptValue); } void ScriptEngine::registerValue(const QString& valueName, QScriptValue value) { @@ -863,6 +914,11 @@ void ScriptEngine::addEventHandler(const EntityItemID& entityID, const QString& handlersForEvent << handlerData; // Note that the same handler can be added many times. See removeEntityEventHandler(). } +// this is not redundant -- the version in BaseScriptEngine is specifically not Q_INVOKABLE +QScriptValue ScriptEngine::evaluateInClosure(const QScriptValue& closure, const QScriptProgram& program) { + return BaseScriptEngine::evaluateInClosure(closure, program); +} + QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fileName, int lineNumber) { if (DependencyManager::get()->isStopped()) { return QScriptValue(); // bail early @@ -885,29 +941,26 @@ QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fi // Check syntax auto syntaxError = lintScript(sourceCode, fileName); if (syntaxError.isError()) { - if (isEvaluating()) { - currentContext()->throwValue(syntaxError); - } else { + if (!isEvaluating()) { syntaxError.setProperty("detail", "evaluate"); - emit unhandledException(syntaxError); } + raiseException(syntaxError); + maybeEmitUncaughtException("lint"); return syntaxError; } QScriptProgram program { sourceCode, fileName, lineNumber }; if (program.isNull()) { // can this happen? auto err = makeError("could not create QScriptProgram for " + fileName); - emit unhandledException(err); + raiseException(err); + maybeEmitUncaughtException("compile"); return err; } QScriptValue result; { result = BaseScriptEngine::evaluate(program); - if (!isEvaluating() && hasUncaughtException()) { - emit unhandledException(cloneUncaughtException(__FUNCTION__)); - clearExceptions(); - } + maybeEmitUncaughtException("evaluate"); } return result; } @@ -930,10 +983,7 @@ void ScriptEngine::run() { { evaluate(_scriptContents, _fileNameString); - if (!isEvaluating() && hasUncaughtException()) { - emit unhandledException(cloneUncaughtException(__FUNCTION__)); - clearExceptions(); - } + maybeEmitUncaughtException(__FUNCTION__); } #ifdef _WIN32 // VS13 does not sleep_until unless it uses the system_clock, see: @@ -1301,7 +1351,354 @@ QUrl ScriptEngine::resourcesPath() const { } void ScriptEngine::print(const QString& message) { - emit printedMessage(message); + emit printedMessage(message, getFilename()); +} + +// Script.require.resolve -- like resolvePath, but performs more validation and throws exceptions on invalid module identifiers (for consistency with Node.js) +QString ScriptEngine::_requireResolve(const QString& moduleId, const QString& relativeTo) { + if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { + return QString(); + } + QUrl defaultScriptsLoc = defaultScriptsLocation(); + QUrl url(moduleId); + + auto displayId = moduleId; + if (displayId.length() > MAX_DEBUG_VALUE_LENGTH) { + displayId = displayId.mid(0, MAX_DEBUG_VALUE_LENGTH) + "..."; + } + auto message = QString("Cannot find module '%1' (%2)").arg(displayId); + + auto throwResolveError = [&](const QScriptValue& error) -> QString { + raiseException(error); + maybeEmitUncaughtException("require.resolve"); + return QString(); + }; + + // de-fuzz the input a little by restricting to rational sizes + auto idLength = url.toString().length(); + if (idLength < 1 || idLength > MAX_MODULE_ID_LENGTH) { + auto details = QString("rejecting invalid module id size (%1 chars [1,%2])") + .arg(idLength).arg(MAX_MODULE_ID_LENGTH); + return throwResolveError(makeError(message.arg(details), "RangeError")); + } + + // this regex matches: absolute, dotted or path-like URLs + // (ie: the kind of stuff ScriptEngine::resolvePath already handles) + QRegularExpression qualified ("^\\w+:|^/|^[.]{1,2}(/|$)"); + + // this is for module.require (which is a bound version of require that's always relative to the module path) + if (!relativeTo.isEmpty()) { + url = QUrl(relativeTo).resolved(moduleId); + url = resolvePath(url.toString()); + } else if (qualified.match(moduleId).hasMatch()) { + url = resolvePath(moduleId); + } else { + // check if the moduleId refers to a "system" module + QString systemPath = defaultScriptsLoc.path(); + QString systemModulePath = QString("%1/modules/%2.js").arg(systemPath).arg(moduleId); + url = defaultScriptsLoc; + url.setPath(systemModulePath); + if (!QFileInfo(url.toLocalFile()).isFile()) { + if (!moduleId.contains("./")) { + // the user might be trying to refer to a relative file without anchoring it + // let's do them a favor and test for that case -- offering specific advice if detected + auto unanchoredUrl = resolvePath("./" + moduleId); + if (QFileInfo(unanchoredUrl.toLocalFile()).isFile()) { + auto msg = QString("relative module ids must be anchored; use './%1' instead") + .arg(moduleId); + return throwResolveError(makeError(message.arg(msg))); + } + } + return throwResolveError(makeError(message.arg("system module not found"))); + } + } + + if (url.isRelative()) { + return throwResolveError(makeError(message.arg("could not resolve module id"))); + } + + // if it looks like a local file, verify that it's an allowed path and really a file + if (url.isLocalFile()) { + QFileInfo file(url.toLocalFile()); + QUrl canonical = url; + if (file.exists()) { + canonical.setPath(file.canonicalFilePath()); + } + + bool disallowOutsideFiles = !defaultScriptsLocation().isParentOf(canonical) && !currentSandboxURL.isLocalFile(); + if (disallowOutsideFiles && !PathUtils::isDescendantOf(canonical, currentSandboxURL)) { + return throwResolveError(makeError(message.arg( + QString("path '%1' outside of origin script '%2' '%3'") + .arg(PathUtils::stripFilename(url)) + .arg(PathUtils::stripFilename(currentSandboxURL)) + .arg(canonical.toString()) + ))); + } + if (!file.exists()) { + return throwResolveError(makeError(message.arg("path does not exist: " + url.toLocalFile()))); + } + if (!file.isFile()) { + return throwResolveError(makeError(message.arg("path is not a file: " + url.toLocalFile()))); + } + } + + maybeEmitUncaughtException(__FUNCTION__); + return url.toString(); +} + +// retrieves the current parent module from the JS scope chain +QScriptValue ScriptEngine::currentModule() { + if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { + return unboundNullValue(); + } + auto jsRequire = globalObject().property("Script").property("require"); + auto cache = jsRequire.property("cache"); + auto candidate = QScriptValue(); + for (auto c = currentContext(); c && !candidate.isObject(); c = c->parentContext()) { + QScriptContextInfo contextInfo { c }; + candidate = cache.property(contextInfo.fileName()); + } + if (!candidate.isObject()) { + return QScriptValue(); + } + return candidate; +} + +// replaces or adds "module" to "parent.children[]" array +// (for consistency with Node.js and userscript cache invalidation without "cache busters") +bool ScriptEngine::registerModuleWithParent(const QScriptValue& module, const QScriptValue& parent) { + auto children = parent.property("children"); + if (children.isArray()) { + auto key = module.property("id"); + auto length = children.property("length").toInt32(); + for (int i = 0; i < length; i++) { + if (children.property(i).property("id").strictlyEquals(key)) { + qCDebug(scriptengine_module) << key.toString() << " updating parent.children[" << i << "] = module"; + children.setProperty(i, module); + return true; + } + } + qCDebug(scriptengine_module) << key.toString() << " appending parent.children[" << length << "] = module"; + children.setProperty(length, module); + return true; + } else if (parent.isValid()) { + qCDebug(scriptengine_module) << "registerModuleWithParent -- unrecognized parent" << parent.toVariant().toString(); + } + return false; +} + +// creates a new JS "module" Object with default metadata properties +QScriptValue ScriptEngine::newModule(const QString& modulePath, const QScriptValue& parent) { + auto closure = newObject(); + auto exports = newObject(); + auto module = newObject(); + qCDebug(scriptengine_module) << "newModule" << modulePath << parent.property("filename").toString(); + + closure.setProperty("module", module, READONLY_PROP_FLAGS); + + // note: this becomes the "exports" free variable, so should not be set read only + closure.setProperty("exports", exports); + + // make the closure available to module instantiation + module.setProperty("__closure__", closure, READONLY_HIDDEN_PROP_FLAGS); + + // for consistency with Node.js Module + module.setProperty("id", modulePath, READONLY_PROP_FLAGS); + module.setProperty("filename", modulePath, READONLY_PROP_FLAGS); + module.setProperty("exports", exports); // not readonly + module.setProperty("loaded", false, READONLY_PROP_FLAGS); + module.setProperty("parent", parent, READONLY_PROP_FLAGS); + module.setProperty("children", newArray(), READONLY_PROP_FLAGS); + + // module.require is a bound version of require that always resolves relative to that module's path + auto boundRequire = QScriptEngine::evaluate("(function(id) { return Script.require(Script.require.resolve(id, this.filename)); })", "(boundRequire)"); + module.setProperty("require", boundRequire, READONLY_PROP_FLAGS); + + return module; +} + +// synchronously fetch a module's source code using BatchLoader +QVariantMap ScriptEngine::fetchModuleSource(const QString& modulePath, const bool forceDownload) { + using UrlMap = QMap; + auto scriptCache = DependencyManager::get(); + QVariantMap req; + qCDebug(scriptengine_module) << "require.fetchModuleSource: " << QUrl(modulePath).fileName() << QThread::currentThread(); + + auto onload = [=, &req](const UrlMap& data, const UrlMap& _status) { + auto url = modulePath; + auto status = _status[url]; + auto contents = data[url]; + qCDebug(scriptengine_module) << "require.fetchModuleSource.onload: " << QUrl(url).fileName() << status << QThread::currentThread(); + if (isStopping()) { + req["status"] = "Stopped"; + req["success"] = false; + } else { + req["url"] = url; + req["status"] = status; + req["success"] = ScriptCache::isSuccessStatus(status); + req["contents"] = contents; + } + }; + + if (forceDownload) { + qCDebug(scriptengine_module) << "require.requestScript -- clearing cache for" << modulePath; + scriptCache->deleteScript(modulePath); + } + BatchLoader* loader = new BatchLoader(QList({ modulePath })); + connect(loader, &BatchLoader::finished, this, onload); + connect(this, &QObject::destroyed, loader, &QObject::deleteLater); + // fail faster? (since require() blocks the engine thread while resolving dependencies) + const int MAX_RETRIES = 1; + + loader->start(MAX_RETRIES); + + if (!loader->isFinished()) { + QTimer monitor; + QEventLoop loop; + QObject::connect(loader, &BatchLoader::finished, this, [this, &monitor, &loop]{ + monitor.stop(); + loop.quit(); + }); + + // this helps detect the case where stop() is invoked during the download + // but not seen in time to abort processing in onload()... + connect(&monitor, &QTimer::timeout, this, [this, &loop, &loader]{ + if (isStopping()) { + loop.exit(-1); + } + }); + monitor.start(500); + loop.exec(); + } + loader->deleteLater(); + return req; +} + +// evaluate a pending module object using the fetched source code +QScriptValue ScriptEngine::instantiateModule(const QScriptValue& module, const QString& sourceCode) { + QScriptValue result; + auto modulePath = module.property("filename").toString(); + auto closure = module.property("__closure__"); + + qCDebug(scriptengine_module) << QString("require.instantiateModule: %1 / %2 bytes") + .arg(QUrl(modulePath).fileName()).arg(sourceCode.length()); + + if (module.property("content-type").toString() == "application/json") { + qCDebug(scriptengine_module) << "... parsing as JSON"; + closure.setProperty("__json", sourceCode); + result = evaluateInClosure(closure, { "module.exports = JSON.parse(__json)", modulePath }); + } else { + // scoped vars for consistency with Node.js + closure.setProperty("require", module.property("require")); + closure.setProperty("__filename", modulePath, READONLY_HIDDEN_PROP_FLAGS); + closure.setProperty("__dirname", QString(modulePath).replace(QRegExp("/[^/]*$"), ""), READONLY_HIDDEN_PROP_FLAGS); + result = evaluateInClosure(closure, { sourceCode, modulePath }); + } + maybeEmitUncaughtException(__FUNCTION__); + return result; +} + +// CommonJS/Node.js like require/module support +QScriptValue ScriptEngine::require(const QString& moduleId) { + qCDebug(scriptengine_module) << "ScriptEngine::require(" << moduleId.left(MAX_DEBUG_VALUE_LENGTH) << ")"; + if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { + return unboundNullValue(); + } + + auto jsRequire = globalObject().property("Script").property("require"); + auto cacheMeta = jsRequire.data(); + auto cache = jsRequire.property("cache"); + auto parent = currentModule(); + + auto throwModuleError = [&](const QString& modulePath, const QScriptValue& error) { + cache.setProperty(modulePath, nullValue()); + if (!error.isNull()) { +#ifdef DEBUG_JS_MODULES + qCWarning(scriptengine_module) << "throwing module error:" << error.toString() << modulePath << error.property("stack").toString(); +#endif + raiseException(error); + } + maybeEmitUncaughtException("module"); + return unboundNullValue(); + }; + + // start by resolving the moduleId into a fully-qualified path/URL + QString modulePath = _requireResolve(moduleId); + if (modulePath.isNull() || hasUncaughtException()) { + // the resolver already threw an exception -- bail early + maybeEmitUncaughtException(__FUNCTION__); + return unboundNullValue(); + } + + // check the resolved path against the cache + auto module = cache.property(modulePath); + + // modules get cached in `Script.require.cache` and (similar to Node.js) users can access it + // to inspect particular entries and invalidate them by deleting the key: + // `delete Script.require.cache[Script.require.resolve(moduleId)];` + + // cacheMeta is just used right now to tell deleted keys apart from undefined ones + bool invalidateCache = module.isUndefined() && cacheMeta.property(moduleId).isValid(); + + // reset the cacheMeta record so invalidation won't apply next time, even if the module fails to load + cacheMeta.setProperty(modulePath, QScriptValue()); + + auto exports = module.property("exports"); + if (!invalidateCache && exports.isObject()) { + // we have found a cached module -- just need to possibly register it with current parent + qCDebug(scriptengine_module) << QString("require - using cached module '%1' for '%2' (loaded: %3)") + .arg(modulePath).arg(moduleId).arg(module.property("loaded").toString()); + registerModuleWithParent(module, parent); + maybeEmitUncaughtException("cached module"); + return exports; + } + + // bootstrap / register new empty module + module = newModule(modulePath, parent); + registerModuleWithParent(module, parent); + + // add it to the cache (this is done early so any cyclic dependencies pick up) + cache.setProperty(modulePath, module); + + // download the module source + auto req = fetchModuleSource(modulePath, invalidateCache); + + if (!req.contains("success") || !req["success"].toBool()) { + auto error = QString("error retrieving script (%1)").arg(req["status"].toString()); + return throwModuleError(modulePath, error); + } + +#if DEBUG_JS_MODULES + qCDebug(scriptengine_module) << "require.loaded: " << + QUrl(req["url"].toString()).fileName() << req["status"].toString(); +#endif + + auto sourceCode = req["contents"].toString(); + + if (QUrl(modulePath).fileName().endsWith(".json", Qt::CaseInsensitive)) { + module.setProperty("content-type", "application/json"); + } else { + module.setProperty("content-type", "application/javascript"); + } + + // evaluate the module + auto result = instantiateModule(module, sourceCode); + + if (result.isError() && !result.strictlyEquals(module.property("exports"))) { + qCWarning(scriptengine_module) << "-- result.isError --" << result.toString(); + return throwModuleError(modulePath, result); + } + + // mark as fully-loaded + module.setProperty("loaded", true, READONLY_PROP_FLAGS); + + // set up a new reference point for detecting cache key deletion + cacheMeta.setProperty(modulePath, module); + + qCDebug(scriptengine_module) << "//ScriptEngine::require(" << moduleId << ")"; + + maybeEmitUncaughtException(__FUNCTION__); + return module.property("exports"); } // If a callback is specified, the included files will be loaded asynchronously and the callback will be called @@ -1309,6 +1706,9 @@ void ScriptEngine::print(const QString& message) { // If no callback is specified, the included files will be loaded synchronously and will block execution until // all of the files have finished loading. void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callback) { + if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { + return; + } if (DependencyManager::get()->isStopped()) { scriptWarningMessage("Script.include() while shutting down is ignored... includeFiles:" + includeFiles.join(",") + "parent script:" + getFilename()); @@ -1371,7 +1771,7 @@ void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callbac doWithEnvironment(capturedEntityIdentifier, capturedSandboxURL, operation); if (hasUncaughtException()) { - emit unhandledException(cloneUncaughtException(__FUNCTION__)); + emit unhandledException(cloneUncaughtException("evaluateInclude")); clearExceptions(); } } else { @@ -1418,6 +1818,9 @@ void ScriptEngine::include(const QString& includeFile, QScriptValue callback) { // as a stand-alone script. To accomplish this, the ScriptEngine class just emits a signal which // the Application or other context will connect to in order to know to actually load the script void ScriptEngine::load(const QString& loadFile) { + if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { + return; + } if (DependencyManager::get()->isStopped()) { scriptWarningMessage("Script.load() while shutting down is ignored... loadFile:" + loadFile + "parent script:" + getFilename()); @@ -1487,6 +1890,52 @@ void ScriptEngine::updateEntityScriptStatus(const EntityItemID& entityID, const emit entityScriptDetailsUpdated(); } +QVariant ScriptEngine::cloneEntityScriptDetails(const EntityItemID& entityID) { + static const QVariant NULL_VARIANT { qVariantFromValue((QObject*)nullptr) }; + QVariantMap map; + if (entityID.isNull()) { + // TODO: find better way to report JS Error across thread/process boundaries + map["isError"] = true; + map["errorInfo"] = "Error: getEntityScriptDetails -- invalid entityID"; + } else { +#ifdef DEBUG_ENTITY_STATES + qDebug() << "cloneEntityScriptDetails" << entityID << QThread::currentThread(); +#endif + EntityScriptDetails scriptDetails; + if (getEntityScriptDetails(entityID, scriptDetails)) { +#ifdef DEBUG_ENTITY_STATES + qDebug() << "gotEntityScriptDetails" << scriptDetails.status << QThread::currentThread(); +#endif + map["isRunning"] = isEntityScriptRunning(entityID); + map["status"] = EntityScriptStatus_::valueToKey(scriptDetails.status).toLower(); + map["errorInfo"] = scriptDetails.errorInfo; + map["entityID"] = entityID.toString(); +#ifdef DEBUG_ENTITY_STATES + { + auto debug = QVariantMap(); + debug["script"] = scriptDetails.scriptText; + debug["scriptObject"] = scriptDetails.scriptObject.toVariant(); + debug["lastModified"] = (qlonglong)scriptDetails.lastModified; + debug["sandboxURL"] = scriptDetails.definingSandboxURL; + map["debug"] = debug; + } +#endif + } else { +#ifdef DEBUG_ENTITY_STATES + qDebug() << "!gotEntityScriptDetails" << QThread::currentThread(); +#endif + map["isError"] = true; + map["errorInfo"] = "Entity script details unavailable"; + map["entityID"] = entityID.toString(); + } + } + return map; +} + +QFuture ScriptEngine::getLocalEntityScriptDetails(const EntityItemID& entityID) { + return QtConcurrent::run(this, &ScriptEngine::cloneEntityScriptDetails, entityID); +} + bool ScriptEngine::getEntityScriptDetails(const EntityItemID& entityID, EntityScriptDetails &details) const { auto it = _entityScripts.constFind(entityID); if (it == _entityScripts.constEnd()) { @@ -1625,10 +2074,10 @@ void ScriptEngine::loadEntityScript(const EntityItemID& entityID, const QString& auto scriptCache = DependencyManager::get(); // note: see EntityTreeRenderer.cpp for shared pointer lifecycle management - QWeakPointer weakRef(sharedFromThis()); + QWeakPointer weakRef(sharedFromThis()); scriptCache->getScriptContents(entityScript, [this, weakRef, entityScript, entityID](const QString& url, const QString& contents, bool isURL, bool success, const QString& status) { - QSharedPointer strongRef(weakRef); + QSharedPointer strongRef(weakRef); if (!strongRef) { qCWarning(scriptengine) << "loadEntityScript.contentAvailable -- ScriptEngine was deleted during getScriptContents!!"; return; @@ -1747,13 +2196,12 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co timeout.setSingleShot(true); timeout.start(SANDBOX_TIMEOUT); connect(&timeout, &QTimer::timeout, [&sandbox, SANDBOX_TIMEOUT, scriptOrURL]{ - auto context = sandbox.currentContext(); - if (context) { qCDebug(scriptengine) << "ScriptEngine::entityScriptContentAvailable timeout(" << scriptOrURL << ")"; // Guard against infinite loops and non-performant code - context->throwError(QString("Timed out (entity constructors are limited to %1ms)").arg(SANDBOX_TIMEOUT)); - } + sandbox.raiseException( + sandbox.makeError(QString("Timed out (entity constructors are limited to %1ms)").arg(SANDBOX_TIMEOUT)) + ); }); testConstructor = sandbox.evaluate(program); @@ -1769,7 +2217,7 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co if (exception.isError()) { // create a local copy using makeError to decouple from the sandbox engine exception = makeError(exception); - setError(formatException(exception), EntityScriptStatus::ERROR_RUNNING_SCRIPT); + setError(formatException(exception, _enableExtendedJSExceptions.get()), EntityScriptStatus::ERROR_RUNNING_SCRIPT); emit unhandledException(exception); return; } @@ -1781,9 +2229,8 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co testConstructorType = "empty"; } QString testConstructorValue = testConstructor.toString(); - const int maxTestConstructorValueSize = 80; - if (testConstructorValue.size() > maxTestConstructorValueSize) { - testConstructorValue = testConstructorValue.mid(0, maxTestConstructorValueSize) + "..."; + if (testConstructorValue.size() > MAX_DEBUG_VALUE_LENGTH) { + testConstructorValue = testConstructorValue.mid(0, MAX_DEBUG_VALUE_LENGTH) + "..."; } auto message = QString("failed to load entity script -- expected a function, got %1, %2") .arg(testConstructorType).arg(testConstructorValue); @@ -1821,7 +2268,7 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co if (entityScriptObject.isError()) { auto exception = entityScriptObject; - setError(formatException(exception), EntityScriptStatus::ERROR_RUNNING_SCRIPT); + setError(formatException(exception, _enableExtendedJSExceptions.get()), EntityScriptStatus::ERROR_RUNNING_SCRIPT); emit unhandledException(exception); return; } @@ -1844,7 +2291,7 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co processDeferredEntityLoads(entityScript, entityID); } -void ScriptEngine::unloadEntityScript(const EntityItemID& entityID) { +void ScriptEngine::unloadEntityScript(const EntityItemID& entityID, bool shouldRemoveFromMap) { if (QThread::currentThread() != thread()) { #ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "*** WARNING *** ScriptEngine::unloadEntityScript() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] " @@ -1852,7 +2299,8 @@ void ScriptEngine::unloadEntityScript(const EntityItemID& entityID) { #endif QMetaObject::invokeMethod(this, "unloadEntityScript", - Q_ARG(const EntityItemID&, entityID)); + Q_ARG(const EntityItemID&, entityID), + Q_ARG(bool, shouldRemoveFromMap)); return; } #ifdef THREAD_DEBUGGING @@ -1864,10 +2312,17 @@ void ScriptEngine::unloadEntityScript(const EntityItemID& entityID) { const EntityScriptDetails &oldDetails = _entityScripts[entityID]; if (isEntityScriptRunning(entityID)) { callEntityScriptMethod(entityID, "unload"); - } else { + } +#ifdef DEBUG_ENTITY_STATES + else { qCDebug(scriptengine) << "unload called while !running" << entityID << oldDetails.status; } - if (oldDetails.status != EntityScriptStatus::UNLOADED) { +#endif + if (shouldRemoveFromMap) { + // this was a deleted entity, we've been asked to remove it from the map + _entityScripts.remove(entityID); + emit entityScriptDetailsUpdated(); + } else if (oldDetails.status != EntityScriptStatus::UNLOADED) { EntityScriptDetails newDetails; newDetails.status = EntityScriptStatus::UNLOADED; newDetails.lastModified = QDateTime::currentMSecsSinceEpoch(); @@ -1875,6 +2330,7 @@ void ScriptEngine::unloadEntityScript(const EntityItemID& entityID) { newDetails.scriptText = oldDetails.scriptText; setEntityScriptDetails(entityID, newDetails); } + stopAllTimersForEntityScript(entityID); { // FIXME: shouldn't have to do this here, but currently something seems to be firing unloads moments after firing initial load requests @@ -1953,10 +2409,7 @@ void ScriptEngine::doWithEnvironment(const EntityItemID& entityID, const QUrl& s #else operation(); #endif - if (!isEvaluating() && hasUncaughtException()) { - emit unhandledException(cloneUncaughtException(!entityID.isNull() ? entityID.toString() : __FUNCTION__)); - clearExceptions(); - } + maybeEmitUncaughtException(!entityID.isNull() ? entityID.toString() : __FUNCTION__); currentEntityIdentifier = oldIdentifier; currentSandboxURL = oldSandboxURL; } diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index b988ccfe90..5ea8d052e9 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -41,6 +41,7 @@ #include "ScriptCache.h" #include "ScriptUUID.h" #include "Vec3.h" +#include "SettingHandle.h" class QScriptEngineDebugger; @@ -78,7 +79,7 @@ public: QUrl definingSandboxURL { QUrl("about:EntityScript") }; }; -class ScriptEngine : public BaseScriptEngine, public EntitiesScriptEngineProvider, public QEnableSharedFromThis { +class ScriptEngine : public BaseScriptEngine, public EntitiesScriptEngineProvider { Q_OBJECT Q_PROPERTY(QString context READ getContext) public: @@ -137,6 +138,8 @@ public: /// evaluate some code in the context of the ScriptEngine and return the result Q_INVOKABLE QScriptValue evaluate(const QString& program, const QString& fileName, int lineNumber = 1); // this is also used by the script tool widget + Q_INVOKABLE QScriptValue evaluateInClosure(const QScriptValue& locals, const QScriptProgram& program); + /// if the script engine is not already running, this will download the URL and start the process of seting it up /// to run... NOTE - this is used by Application currently to load the url. We don't really want it to be exposed /// to scripts. we may not need this to be invokable @@ -157,6 +160,16 @@ public: Q_INVOKABLE void include(const QStringList& includeFiles, QScriptValue callback = QScriptValue()); Q_INVOKABLE void include(const QString& includeFile, QScriptValue callback = QScriptValue()); + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // MODULE related methods + Q_INVOKABLE QScriptValue require(const QString& moduleId); + Q_INVOKABLE void resetModuleCache(bool deleteScriptCache = false); + QScriptValue currentModule(); + bool registerModuleWithParent(const QScriptValue& module, const QScriptValue& parent); + QScriptValue newModule(const QString& modulePath, const QScriptValue& parent = QScriptValue()); + QVariantMap fetchModuleSource(const QString& modulePath, const bool forceDownload = false); + QScriptValue instantiateModule(const QScriptValue& module, const QString& sourceCode); + Q_INVOKABLE QObject* setInterval(const QScriptValue& function, int intervalMS); Q_INVOKABLE QObject* setTimeout(const QScriptValue& function, int timeoutMS); Q_INVOKABLE void clearInterval(QObject* timer) { stopTimer(reinterpret_cast(timer)); } @@ -170,8 +183,10 @@ public: Q_INVOKABLE bool isEntityScriptRunning(const EntityItemID& entityID) { return _entityScripts.contains(entityID) && _entityScripts[entityID].status == EntityScriptStatus::RUNNING; } + QVariant cloneEntityScriptDetails(const EntityItemID& entityID); + QFuture getLocalEntityScriptDetails(const EntityItemID& entityID) override; Q_INVOKABLE void loadEntityScript(const EntityItemID& entityID, const QString& entityScript, bool forceRedownload); - Q_INVOKABLE void unloadEntityScript(const EntityItemID& entityID); // will call unload method + Q_INVOKABLE void unloadEntityScript(const EntityItemID& entityID, bool shouldRemoveFromMap = false); // will call unload method Q_INVOKABLE void unloadAllEntityScripts(); Q_INVOKABLE void callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const QStringList& params = QStringList()) override; @@ -221,10 +236,10 @@ signals: void scriptEnding(); void finished(const QString& fileNameString, ScriptEngine* engine); void cleanupMenuItem(const QString& menuItemString); - void printedMessage(const QString& message); - void errorMessage(const QString& message); - void warningMessage(const QString& message); - void infoMessage(const QString& message); + void printedMessage(const QString& message, const QString& scriptName); + void errorMessage(const QString& message, const QString& scriptName); + void warningMessage(const QString& message, const QString& scriptName); + void infoMessage(const QString& message, const QString& scriptName); void runningStateChanged(); void loadScript(const QString& scriptName, bool isUserLoaded); void reloadScript(const QString& scriptName, bool isUserLoaded); @@ -237,6 +252,9 @@ signals: protected: void init(); Q_INVOKABLE void executeOnScriptThread(std::function function, const Qt::ConnectionType& type = Qt::QueuedConnection ); + // note: this is not meant to be called directly, but just to have QMetaObject take care of wiring it up in general; + // then inside of init() we just have to do "Script.require.resolve = Script._requireResolve;" + Q_INVOKABLE QString _requireResolve(const QString& moduleId, const QString& relativeTo = QString()); QString logException(const QScriptValue& exception); void timerFired(); @@ -290,11 +308,16 @@ protected: AssetScriptingInterface _assetScriptingInterface{ this }; - std::function _emitScriptUpdates{ [](){ return true; } }; + std::function _emitScriptUpdates{ []() { return true; } }; std::recursive_mutex _lock; std::chrono::microseconds _totalTimerExecution { 0 }; + + static const QString _SETTINGS_ENABLE_EXTENDED_MODULE_COMPAT; + static const QString _SETTINGS_ENABLE_EXTENDED_EXCEPTIONS; + + Setting::Handle _enableExtendedJSExceptions { _SETTINGS_ENABLE_EXTENDED_EXCEPTIONS, true }; }; #endif // hifi_ScriptEngine_h diff --git a/libraries/script-engine/src/ScriptEngineLogging.cpp b/libraries/script-engine/src/ScriptEngineLogging.cpp index 2e5d293728..392bc05129 100644 --- a/libraries/script-engine/src/ScriptEngineLogging.cpp +++ b/libraries/script-engine/src/ScriptEngineLogging.cpp @@ -12,3 +12,4 @@ #include "ScriptEngineLogging.h" Q_LOGGING_CATEGORY(scriptengine, "hifi.scriptengine") +Q_LOGGING_CATEGORY(scriptengine_module, "hifi.scriptengine.module") diff --git a/libraries/script-engine/src/ScriptEngineLogging.h b/libraries/script-engine/src/ScriptEngineLogging.h index 0e614dd5bf..62e46632a6 100644 --- a/libraries/script-engine/src/ScriptEngineLogging.h +++ b/libraries/script-engine/src/ScriptEngineLogging.h @@ -15,6 +15,7 @@ #include Q_DECLARE_LOGGING_CATEGORY(scriptengine) +Q_DECLARE_LOGGING_CATEGORY(scriptengine_module) #endif // hifi_ScriptEngineLogging_h diff --git a/libraries/script-engine/src/ScriptEngines.cpp b/libraries/script-engine/src/ScriptEngines.cpp index 57887d2d96..88b0e0b7b5 100644 --- a/libraries/script-engine/src/ScriptEngines.cpp +++ b/libraries/script-engine/src/ScriptEngines.cpp @@ -34,34 +34,24 @@ ScriptsModel& getScriptsModel() { return scriptsModel; } -void ScriptEngines::onPrintedMessage(const QString& message) { - auto scriptEngine = qobject_cast(sender()); - auto scriptName = scriptEngine ? scriptEngine->getFilename() : ""; +void ScriptEngines::onPrintedMessage(const QString& message, const QString& scriptName) { emit printedMessage(message, scriptName); } -void ScriptEngines::onErrorMessage(const QString& message) { - auto scriptEngine = qobject_cast(sender()); - auto scriptName = scriptEngine ? scriptEngine->getFilename() : ""; +void ScriptEngines::onErrorMessage(const QString& message, const QString& scriptName) { emit errorMessage(message, scriptName); } -void ScriptEngines::onWarningMessage(const QString& message) { - auto scriptEngine = qobject_cast(sender()); - auto scriptName = scriptEngine ? scriptEngine->getFilename() : ""; +void ScriptEngines::onWarningMessage(const QString& message, const QString& scriptName) { emit warningMessage(message, scriptName); } -void ScriptEngines::onInfoMessage(const QString& message) { - auto scriptEngine = qobject_cast(sender()); - auto scriptName = scriptEngine ? scriptEngine->getFilename() : ""; +void ScriptEngines::onInfoMessage(const QString& message, const QString& scriptName) { emit infoMessage(message, scriptName); } void ScriptEngines::onErrorLoadingScript(const QString& url) { - auto scriptEngine = qobject_cast(sender()); - auto scriptName = scriptEngine ? scriptEngine->getFilename() : ""; - emit errorLoadingScript(url, scriptName); + emit errorLoadingScript(url); } ScriptEngines::ScriptEngines(ScriptEngine::Context context) diff --git a/libraries/script-engine/src/ScriptEngines.h b/libraries/script-engine/src/ScriptEngines.h index 2fadfc81f8..63b7e8f11c 100644 --- a/libraries/script-engine/src/ScriptEngines.h +++ b/libraries/script-engine/src/ScriptEngines.h @@ -79,13 +79,13 @@ signals: void errorMessage(const QString& message, const QString& engineName); void warningMessage(const QString& message, const QString& engineName); void infoMessage(const QString& message, const QString& engineName); - void errorLoadingScript(const QString& url, const QString& engineName); + void errorLoadingScript(const QString& url); public slots: - void onPrintedMessage(const QString& message); - void onErrorMessage(const QString& message); - void onWarningMessage(const QString& message); - void onInfoMessage(const QString& message); + void onPrintedMessage(const QString& message, const QString& scriptName); + void onErrorMessage(const QString& message, const QString& scriptName); + void onWarningMessage(const QString& message, const QString& scriptName); + void onInfoMessage(const QString& message, const QString& scriptName); void onErrorLoadingScript(const QString& url); protected slots: diff --git a/libraries/script-engine/src/BaseScriptEngine.cpp b/libraries/shared/src/BaseScriptEngine.cpp similarity index 68% rename from libraries/script-engine/src/BaseScriptEngine.cpp rename to libraries/shared/src/BaseScriptEngine.cpp index 16308c0650..c92d629b75 100644 --- a/libraries/script-engine/src/BaseScriptEngine.cpp +++ b/libraries/shared/src/BaseScriptEngine.cpp @@ -10,6 +10,7 @@ // #include "BaseScriptEngine.h" +#include "SharedLogging.h" #include #include @@ -18,18 +19,27 @@ #include #include -#include "ScriptEngineLogging.h" #include "Profile.h" -const QString BaseScriptEngine::_SETTINGS_ENABLE_EXTENDED_EXCEPTIONS { - "com.highfidelity.experimental.enableExtendedJSExceptions" -}; - const QString BaseScriptEngine::SCRIPT_EXCEPTION_FORMAT { "[%0] %1 in %2:%3" }; const QString BaseScriptEngine::SCRIPT_BACKTRACE_SEP { "\n " }; +bool BaseScriptEngine::IS_THREADSAFE_INVOCATION(const QThread *thread, const QString& method) { + if (QThread::currentThread() == thread) { + return true; + } + qCCritical(shared) << QString("Scripting::%1 @ %2 -- ignoring thread-unsafe call from %3") + .arg(method).arg(thread ? thread->objectName() : "(!thread)").arg(QThread::currentThread()->objectName()); + qCDebug(shared) << "(please resolve on the calling side by using invokeMethod, executeOnScriptThread, etc.)"; + Q_ASSERT(false); + return false; +} + // engine-aware JS Error copier and factory QScriptValue BaseScriptEngine::makeError(const QScriptValue& _other, const QString& type) { + if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { + return unboundNullValue(); + } auto other = _other; if (other.isString()) { other = newObject(); @@ -41,7 +51,7 @@ QScriptValue BaseScriptEngine::makeError(const QScriptValue& _other, const QStri } if (!proto.isFunction()) { #ifdef DEBUG_JS_EXCEPTIONS - qCDebug(scriptengine) << "BaseScriptEngine::makeError -- couldn't find constructor for" << type << " -- using Error instead"; + qCDebug(shared) << "BaseScriptEngine::makeError -- couldn't find constructor for" << type << " -- using Error instead"; #endif proto = globalObject().property("Error"); } @@ -64,6 +74,9 @@ QScriptValue BaseScriptEngine::makeError(const QScriptValue& _other, const QStri // check syntax and when there are issues returns an actual "SyntaxError" with the details QScriptValue BaseScriptEngine::lintScript(const QString& sourceCode, const QString& fileName, const int lineNumber) { + if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { + return unboundNullValue(); + } const auto syntaxCheck = checkSyntax(sourceCode); if (syntaxCheck.state() != QScriptSyntaxCheckResult::Valid) { auto err = globalObject().property("SyntaxError") @@ -82,13 +95,16 @@ QScriptValue BaseScriptEngine::lintScript(const QString& sourceCode, const QStri } return err; } - return undefinedValue(); + return QScriptValue(); } // this pulls from the best available information to create a detailed snapshot of the current exception QScriptValue BaseScriptEngine::cloneUncaughtException(const QString& extraDetail) { + if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { + return unboundNullValue(); + } if (!hasUncaughtException()) { - return QScriptValue(); + return unboundNullValue(); } auto exception = uncaughtException(); // ensure the error object is engine-local @@ -144,7 +160,10 @@ QScriptValue BaseScriptEngine::cloneUncaughtException(const QString& extraDetail return err; } -QString BaseScriptEngine::formatException(const QScriptValue& exception) { +QString BaseScriptEngine::formatException(const QScriptValue& exception, bool includeExtendedDetails) { + if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { + return QString(); + } QString note { "UncaughtException" }; QString result; @@ -156,8 +175,8 @@ QString BaseScriptEngine::formatException(const QScriptValue& exception) { const auto lineNumber = exception.property("lineNumber").toString(); const auto stacktrace = exception.property("stack").toString(); - if (_enableExtendedJSExceptions.get()) { - // This setting toggles display of the hints now being added during the loading process. + if (includeExtendedDetails) { + // Display additional exception / troubleshooting hints that can be added via the custom Error .detail property // Example difference: // [UncaughtExceptions] Error: Can't find variable: foobar in atp:/myentity.js\n... // [UncaughtException (construct {1eb5d3fa-23b1-411c-af83-163af7220e3f})] Error: Can't find variable: foobar in atp:/myentity.js\n... @@ -173,14 +192,39 @@ QString BaseScriptEngine::formatException(const QScriptValue& exception) { return result; } +bool BaseScriptEngine::raiseException(const QScriptValue& exception) { + if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { + return false; + } + if (currentContext()) { + // we have an active context / JS stack frame so throw the exception per usual + currentContext()->throwValue(makeError(exception)); + return true; + } else { + // we are within a pure C++ stack frame (ie: being called directly by other C++ code) + // in this case no context information is available so just emit the exception for reporting + emit unhandledException(makeError(exception)); + } + return false; +} + +bool BaseScriptEngine::maybeEmitUncaughtException(const QString& debugHint) { + if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { + return false; + } + if (!isEvaluating() && hasUncaughtException()) { + emit unhandledException(cloneUncaughtException(debugHint)); + clearExceptions(); + return true; + } + return false; +} + QScriptValue BaseScriptEngine::evaluateInClosure(const QScriptValue& closure, const QScriptProgram& program) { PROFILE_RANGE(script, "evaluateInClosure"); - if (QThread::currentThread() != thread()) { - qCCritical(scriptengine) << "*** CRITICAL *** ScriptEngine::evaluateInClosure() is meant to be called from engine thread only."; - // note: a recursive mutex might be needed around below code if this method ever becomes Q_INVOKABLE - return QScriptValue(); + if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { + return unboundNullValue(); } - const auto fileName = program.fileName(); const auto shortName = QUrl(fileName).fileName(); @@ -189,7 +233,7 @@ QScriptValue BaseScriptEngine::evaluateInClosure(const QScriptValue& closure, co auto global = closure.property("global"); if (global.isObject()) { #ifdef DEBUG_JS - qCDebug(scriptengine) << " setting global = closure.global" << shortName; + qCDebug(shared) << " setting global = closure.global" << shortName; #endif oldGlobal = globalObject(); setGlobalObject(global); @@ -200,34 +244,34 @@ QScriptValue BaseScriptEngine::evaluateInClosure(const QScriptValue& closure, co auto thiz = closure.property("this"); if (thiz.isObject()) { #ifdef DEBUG_JS - qCDebug(scriptengine) << " setting this = closure.this" << shortName; + qCDebug(shared) << " setting this = closure.this" << shortName; #endif context->setThisObject(thiz); } context->pushScope(closure); #ifdef DEBUG_JS - qCDebug(scriptengine) << QString("[%1] evaluateInClosure %2").arg(isEvaluating()).arg(shortName); + qCDebug(shared) << QString("[%1] evaluateInClosure %2").arg(isEvaluating()).arg(shortName); #endif { result = BaseScriptEngine::evaluate(program); if (hasUncaughtException()) { auto err = cloneUncaughtException(__FUNCTION__); #ifdef DEBUG_JS_EXCEPTIONS - qCWarning(scriptengine) << __FUNCTION__ << "---------- hasCaught:" << err.toString() << result.toString(); + qCWarning(shared) << __FUNCTION__ << "---------- hasCaught:" << err.toString() << result.toString(); err.setProperty("_result", result); #endif result = err; } } #ifdef DEBUG_JS - qCDebug(scriptengine) << QString("[%1] //evaluateInClosure %2").arg(isEvaluating()).arg(shortName); + qCDebug(shared) << QString("[%1] //evaluateInClosure %2").arg(isEvaluating()).arg(shortName); #endif popContext(); if (oldGlobal.isValid()) { #ifdef DEBUG_JS - qCDebug(scriptengine) << " restoring global" << shortName; + qCDebug(shared) << " restoring global" << shortName; #endif setGlobalObject(oldGlobal); } @@ -236,7 +280,6 @@ QScriptValue BaseScriptEngine::evaluateInClosure(const QScriptValue& closure, co } // Lambda - QScriptValue BaseScriptEngine::newLambdaFunction(std::function operation, const QScriptValue& data, const QScriptEngine::ValueOwnership& ownership) { auto lambda = new Lambda(this, operation, data); auto object = newQObject(lambda, ownership); @@ -262,26 +305,57 @@ Lambda::Lambda(QScriptEngine *engine, std::functionthread(), __FUNCTION__)) { + return BaseScriptEngine::unboundNullValue(); + } return operation(engine->currentContext(), engine); } +QScriptValue makeScopedHandlerObject(QScriptValue scopeOrCallback, QScriptValue methodOrName) { + auto engine = scopeOrCallback.engine(); + if (!engine) { + return scopeOrCallback; + } + auto scope = QScriptValue(); + auto callback = scopeOrCallback; + if (scopeOrCallback.isObject()) { + if (methodOrName.isString()) { + scope = scopeOrCallback; + callback = scope.property(methodOrName.toString()); + } else if (methodOrName.isFunction()) { + scope = scopeOrCallback; + callback = methodOrName; + } + } + auto handler = engine->newObject(); + handler.setProperty("scope", scope); + handler.setProperty("callback", callback); + return handler; +} + +QScriptValue callScopedHandlerObject(QScriptValue handler, QScriptValue err, QScriptValue result) { + return handler.property("callback").call(handler.property("scope"), QScriptValueList({ err, result })); +} + #ifdef DEBUG_JS void BaseScriptEngine::_debugDump(const QString& header, const QScriptValue& object, const QString& footer) { + if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { + return; + } if (!header.isEmpty()) { - qCDebug(scriptengine) << header; + qCDebug(shared) << header; } if (!object.isObject()) { - qCDebug(scriptengine) << "(!isObject)" << object.toVariant().toString() << object.toString(); + qCDebug(shared) << "(!isObject)" << object.toVariant().toString() << object.toString(); return; } QScriptValueIterator it(object); while (it.hasNext()) { it.next(); - qCDebug(scriptengine) << it.name() << ":" << it.value().toString(); + qCDebug(shared) << it.name() << ":" << it.value().toString(); } if (!footer.isEmpty()) { - qCDebug(scriptengine) << footer; + qCDebug(shared) << footer; } } #endif - diff --git a/libraries/shared/src/BaseScriptEngine.h b/libraries/shared/src/BaseScriptEngine.h new file mode 100644 index 0000000000..138e46fafa --- /dev/null +++ b/libraries/shared/src/BaseScriptEngine.h @@ -0,0 +1,90 @@ +// +// BaseScriptEngine.h +// libraries/script-engine/src +// +// Created by Timothy Dedischew on 02/01/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_BaseScriptEngine_h +#define hifi_BaseScriptEngine_h + +#include +#include +#include + +// common base class for extending QScriptEngine itself +class BaseScriptEngine : public QScriptEngine, public QEnableSharedFromThis { + Q_OBJECT +public: + static const QString SCRIPT_EXCEPTION_FORMAT; + static const QString SCRIPT_BACKTRACE_SEP; + + // threadsafe "unbound" version of QScriptEngine::nullValue() + static const QScriptValue unboundNullValue() { return QScriptValue(0, QScriptValue::NullValue); } + + BaseScriptEngine() {} + + Q_INVOKABLE QScriptValue lintScript(const QString& sourceCode, const QString& fileName, const int lineNumber = 1); + Q_INVOKABLE QScriptValue makeError(const QScriptValue& other = QScriptValue(), const QString& type = "Error"); + Q_INVOKABLE QString formatException(const QScriptValue& exception, bool includeExtendedDetails); + + QScriptValue cloneUncaughtException(const QString& detail = QString()); + QScriptValue evaluateInClosure(const QScriptValue& locals, const QScriptProgram& program); + + // if there is a pending exception and we are at the top level (non-recursive) stack frame, this emits and resets it + bool maybeEmitUncaughtException(const QString& debugHint = QString()); + + // if the currentContext() is valid then throw the passed exception; otherwise, immediately emit it. + // note: this is used in cases where C++ code might call into JS API methods directly + bool raiseException(const QScriptValue& exception); + + // helper to detect and log warnings when other code invokes QScriptEngine/BaseScriptEngine in thread-unsafe ways + static bool IS_THREADSAFE_INVOCATION(const QThread *thread, const QString& method); +signals: + void unhandledException(const QScriptValue& exception); + +protected: + // like `newFunction`, but allows mapping inline C++ lambdas with captures as callable QScriptValues + // even though the context/engine parameters are redundant in most cases, the function signature matches `newFunction` + // anyway so that newLambdaFunction can be used to rapidly prototype / test utility APIs and then if becoming + // permanent more easily promoted into regular static newFunction scenarios. + QScriptValue newLambdaFunction(std::function operation, const QScriptValue& data = QScriptValue(), const QScriptEngine::ValueOwnership& ownership = QScriptEngine::AutoOwnership); + +#ifdef DEBUG_JS + static void _debugDump(const QString& header, const QScriptValue& object, const QString& footer = QString()); +#endif +}; + +// Standardized CPS callback helpers (see: http://fredkschott.com/post/2014/03/understanding-error-first-callbacks-in-node-js/) +// These two helpers allow async JS APIs that use a callback parameter to be more friendly to scripters by accepting thisObject +// context and adopting a consistent and intuitable callback signature: +// function callback(err, result) { if (err) { ... } else { /* do stuff with result */ } } +// +// To use, first pass the user-specified callback args in the same order used with optionally-scoped Qt signal connections: +// auto handler = makeScopedHandlerObject(scopeOrCallback, optionalMethodOrName); +// And then invoke the scoped handler later per CPS conventions: +// auto result = callScopedHandlerObject(handler, err, result); +QScriptValue makeScopedHandlerObject(QScriptValue scopeOrCallback, QScriptValue methodOrName); +QScriptValue callScopedHandlerObject(QScriptValue handler, QScriptValue err, QScriptValue result); + +// Lambda helps create callable QScriptValues out of std::functions: +// (just meant for use from within the script engine itself) +class Lambda : public QObject { + Q_OBJECT +public: + Lambda(QScriptEngine *engine, std::function operation, QScriptValue data); + ~Lambda(); + public slots: + QScriptValue call(); + QString toString() const; +private: + QScriptEngine* engine; + std::function operation; + QScriptValue data; +}; + +#endif // hifi_BaseScriptEngine_h diff --git a/libraries/shared/src/HifiConfigVariantMap.cpp b/libraries/shared/src/HifiConfigVariantMap.cpp index 5be6b2cd74..d0fb14e104 100644 --- a/libraries/shared/src/HifiConfigVariantMap.cpp +++ b/libraries/shared/src/HifiConfigVariantMap.cpp @@ -21,7 +21,7 @@ #include #include -#include "ServerPathUtils.h" +#include "PathUtils.h" #include "SharedLogging.h" QVariantMap HifiConfigVariantMap::mergeCLParametersWithJSONConfig(const QStringList& argumentList) { @@ -127,7 +127,7 @@ void HifiConfigVariantMap::loadConfig(const QStringList& argumentList) { _userConfigFilename = argumentList[userConfigIndex + 1]; } else { // we weren't passed a user config path - _userConfigFilename = ServerPathUtils::getDataFilePath(USER_CONFIG_FILE_NAME); + _userConfigFilename = PathUtils::getAppDataFilePath(USER_CONFIG_FILE_NAME); // as of 1/19/2016 this path was moved so we attempt a migration for first run post migration here @@ -153,7 +153,7 @@ void HifiConfigVariantMap::loadConfig(const QStringList& argumentList) { // we have the old file and not the new file - time to copy the file // make the destination directory if it doesn't exist - auto dataDirectory = ServerPathUtils::getDataDirectory(); + auto dataDirectory = PathUtils::getAppDataPath(); if (QDir().mkpath(dataDirectory)) { if (oldConfigFile.copy(_userConfigFilename)) { qCDebug(shared) << "Migrated config file from" << oldConfigFilename << "to" << _userConfigFilename; diff --git a/libraries/shared/src/PathUtils.cpp b/libraries/shared/src/PathUtils.cpp index 265eaaa5b6..6e3acc5e99 100644 --- a/libraries/shared/src/PathUtils.cpp +++ b/libraries/shared/src/PathUtils.cpp @@ -30,18 +30,20 @@ const QString& PathUtils::resourcesPath() { return staticResourcePath; } -QString PathUtils::getRootDataDirectory() { - auto dataPath = QStandardPaths::writableLocation(QStandardPaths::HomeLocation); +QString PathUtils::getAppDataPath() { + return QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/"; +} -#ifdef Q_OS_WIN - dataPath += "/AppData/Roaming/"; -#elif defined(Q_OS_OSX) - dataPath += "/Library/Application Support/"; -#else - dataPath += "/.local/share/"; -#endif +QString PathUtils::getAppLocalDataPath() { + return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/"; +} - return dataPath; +QString PathUtils::getAppDataFilePath(const QString& filename) { + return QDir(getAppDataPath()).absoluteFilePath(filename); +} + +QString PathUtils::getAppLocalDataFilePath(const QString& filename) { + return QDir(getAppLocalDataPath()).absoluteFilePath(filename); } QString fileNameWithoutExtension(const QString& fileName, const QVector possibleExtensions) { diff --git a/libraries/shared/src/PathUtils.h b/libraries/shared/src/PathUtils.h index 1f7dcbe466..a7af44221c 100644 --- a/libraries/shared/src/PathUtils.h +++ b/libraries/shared/src/PathUtils.h @@ -27,7 +27,12 @@ class PathUtils : public QObject, public Dependency { Q_PROPERTY(QString resources READ resourcesPath) public: static const QString& resourcesPath(); - static QString getRootDataDirectory(); + + static QString getAppDataPath(); + static QString getAppLocalDataPath(); + + static QString getAppDataFilePath(const QString& filename); + static QString getAppLocalDataFilePath(const QString& filename); static Qt::CaseSensitivity getFSCaseSensitivity(); static QString stripFilename(const QUrl& url); diff --git a/libraries/shared/src/RenderArgs.h b/libraries/shared/src/RenderArgs.h index b2c05b0548..50722c0deb 100644 --- a/libraries/shared/src/RenderArgs.h +++ b/libraries/shared/src/RenderArgs.h @@ -122,6 +122,7 @@ public: gpu::Batch* _batch = nullptr; std::shared_ptr _whiteTexture; + uint32_t _globalShapeKey { 0 }; bool _enableTexturing { true }; RenderDetails _details; diff --git a/libraries/shared/src/ServerPathUtils.cpp b/libraries/shared/src/ServerPathUtils.cpp deleted file mode 100644 index cf52875c5f..0000000000 --- a/libraries/shared/src/ServerPathUtils.cpp +++ /dev/null @@ -1,31 +0,0 @@ -// -// ServerPathUtils.cpp -// libraries/shared/src -// -// Created by Ryan Huffman on 01/12/16. -// Copyright 2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// -#include "ServerPathUtils.h" - -#include -#include -#include -#include - -#include "PathUtils.h" - -QString ServerPathUtils::getDataDirectory() { - auto dataPath = PathUtils::getRootDataDirectory(); - - dataPath += qApp->organizationName() + "/" + qApp->applicationName(); - - return QDir::cleanPath(dataPath); -} - -QString ServerPathUtils::getDataFilePath(QString filename) { - return QDir(getDataDirectory()).absoluteFilePath(filename); -} - diff --git a/libraries/shared/src/ServerPathUtils.h b/libraries/shared/src/ServerPathUtils.h deleted file mode 100644 index 28a9a71f0d..0000000000 --- a/libraries/shared/src/ServerPathUtils.h +++ /dev/null @@ -1,22 +0,0 @@ -// -// ServerPathUtils.h -// libraries/shared/src -// -// Created by Ryan Huffman on 01/12/16. -// Copyright 2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#ifndef hifi_ServerPathUtils_h -#define hifi_ServerPathUtils_h - -#include - -namespace ServerPathUtils { - QString getDataDirectory(); - QString getDataFilePath(QString filename); -} - -#endif // hifi_ServerPathUtils_h \ No newline at end of file diff --git a/libraries/shared/src/shared/Storage.cpp b/libraries/shared/src/shared/Storage.cpp new file mode 100644 index 0000000000..3c46347a49 --- /dev/null +++ b/libraries/shared/src/shared/Storage.cpp @@ -0,0 +1,92 @@ +// +// Created by Bradley Austin Davis on 2016/02/17 +// Copyright 2013-2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "Storage.h" + +#include +#include +#include + +Q_LOGGING_CATEGORY(storagelogging, "hifi.core.storage") + +using namespace storage; + +ViewStorage::ViewStorage(const storage::StoragePointer& owner, size_t size, const uint8_t* data) + : _owner(owner), _size(size), _data(data) {} + +StoragePointer Storage::createView(size_t viewSize, size_t offset) const { + auto selfSize = size(); + if (0 == viewSize) { + viewSize = selfSize; + } + if ((viewSize + offset) > selfSize) { + throw std::runtime_error("Invalid mapping range"); + } + return std::make_shared(shared_from_this(), viewSize, data() + offset); +} + +StoragePointer Storage::toMemoryStorage() const { + return std::make_shared(size(), data()); +} + +StoragePointer Storage::toFileStorage(const QString& filename) const { + return FileStorage::create(filename, size(), data()); +} + +MemoryStorage::MemoryStorage(size_t size, const uint8_t* data) { + _data.resize(size); + if (data) { + memcpy(_data.data(), data, size); + } +} + +StoragePointer FileStorage::create(const QString& filename, size_t size, const uint8_t* data) { + QFile file(filename); + if (!file.open(QFile::ReadWrite | QIODevice::Truncate)) { + throw std::runtime_error("Unable to open file for writing"); + } + if (!file.resize(size)) { + throw std::runtime_error("Unable to resize file"); + } + { + auto mapped = file.map(0, size); + if (!mapped) { + throw std::runtime_error("Unable to map file"); + } + memcpy(mapped, data, size); + if (!file.unmap(mapped)) { + throw std::runtime_error("Unable to unmap file"); + } + } + file.close(); + return std::make_shared(filename); +} + +FileStorage::FileStorage(const QString& filename) : _file(filename) { + if (_file.open(QFile::ReadOnly)) { + _mapped = _file.map(0, _file.size()); + if (_mapped) { + _valid = true; + } else { + qCWarning(storagelogging) << "Failed to map file " << filename; + } + } else { + qCWarning(storagelogging) << "Failed to open file " << filename; + } +} + +FileStorage::~FileStorage() { + if (_mapped) { + if (!_file.unmap(_mapped)) { + throw std::runtime_error("Unable to unmap file"); + } + } + if (_file.isOpen()) { + _file.close(); + } +} diff --git a/libraries/shared/src/shared/Storage.h b/libraries/shared/src/shared/Storage.h new file mode 100644 index 0000000000..306984040f --- /dev/null +++ b/libraries/shared/src/shared/Storage.h @@ -0,0 +1,82 @@ +// +// Created by Bradley Austin Davis on 2016/02/17 +// Copyright 2013-2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#pragma once +#ifndef hifi_Storage_h +#define hifi_Storage_h + +#include +#include +#include +#include +#include + +namespace storage { + class Storage; + using StoragePointer = std::shared_ptr; + + class Storage : public std::enable_shared_from_this { + public: + virtual ~Storage() {} + virtual const uint8_t* data() const = 0; + virtual size_t size() const = 0; + virtual operator bool() const { return true; } + + StoragePointer createView(size_t size = 0, size_t offset = 0) const; + StoragePointer toFileStorage(const QString& filename) const; + StoragePointer toMemoryStorage() const; + + // Aliases to prevent having to re-write a ton of code + inline size_t getSize() const { return size(); } + inline const uint8_t* readData() const { return data(); } + }; + + class MemoryStorage : public Storage { + public: + MemoryStorage(size_t size, const uint8_t* data = nullptr); + const uint8_t* data() const override { return _data.data(); } + uint8_t* data() { return _data.data(); } + size_t size() const override { return _data.size(); } + operator bool() const override { return true; } + private: + std::vector _data; + }; + + class FileStorage : public Storage { + public: + static StoragePointer create(const QString& filename, size_t size, const uint8_t* data); + FileStorage(const QString& filename); + ~FileStorage(); + // Prevent copying + FileStorage(const FileStorage& other) = delete; + FileStorage& operator=(const FileStorage& other) = delete; + + const uint8_t* data() const override { return _mapped; } + size_t size() const override { return _file.size(); } + operator bool() const override { return _valid; } + private: + bool _valid { false }; + QFile _file; + uint8_t* _mapped { nullptr }; + }; + + class ViewStorage : public Storage { + public: + ViewStorage(const storage::StoragePointer& owner, size_t size, const uint8_t* data); + const uint8_t* data() const override { return _data; } + size_t size() const override { return _size; } + operator bool() const override { return *_owner; } + private: + const storage::StoragePointer _owner; + const size_t _size; + const uint8_t* _data; + }; + +} + +#endif // hifi_Storage_h diff --git a/libraries/ui/src/ui/Menu.cpp b/libraries/ui/src/ui/Menu.cpp index f68fff0204..a793942056 100644 --- a/libraries/ui/src/ui/Menu.cpp +++ b/libraries/ui/src/ui/Menu.cpp @@ -470,8 +470,8 @@ void Menu::removeSeparator(const QString& menuName, const QString& separatorName if (menu) { int textAt = findPositionOfMenuItem(menu, separatorName); QList menuActions = menu->actions(); - QAction* separatorText = menuActions[textAt]; if (textAt > 0 && textAt < menuActions.size()) { + QAction* separatorText = menuActions[textAt]; QAction* separatorLine = menuActions[textAt - 1]; if (separatorLine) { if (separatorLine->isSeparator()) { diff --git a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp index 09f3e6dc8c..b759a06aee 100644 --- a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp +++ b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp @@ -255,7 +255,7 @@ void OculusLegacyDisplayPlugin::hmdPresent() { memset(eyePoses, 0, sizeof(ovrPosef) * 2); eyePoses[0].Orientation = eyePoses[1].Orientation = ovrRotation; - GLint texture = getGLBackend()->getTextureID(_compositeFramebuffer->getRenderBuffer(0), false); + GLint texture = getGLBackend()->getTextureID(_compositeFramebuffer->getRenderBuffer(0)); auto sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); glFlush(); if (_hmdWindow->makeCurrent()) { diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.cpp b/plugins/openvr/src/OpenVrDisplayPlugin.cpp index 6d503a208a..46c2cf3ff2 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.cpp +++ b/plugins/openvr/src/OpenVrDisplayPlugin.cpp @@ -494,9 +494,9 @@ void OpenVrDisplayPlugin::customizeContext() { _compositeInfos[0].texture = _compositeFramebuffer->getRenderBuffer(0); for (size_t i = 0; i < COMPOSITING_BUFFER_SIZE; ++i) { if (0 != i) { - _compositeInfos[i].texture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, _renderTargetSize.x, _renderTargetSize.y, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT))); + _compositeInfos[i].texture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32, _renderTargetSize.x, _renderTargetSize.y, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT))); } - _compositeInfos[i].textureID = getGLBackend()->getTextureID(_compositeInfos[i].texture, false); + _compositeInfos[i].textureID = getGLBackend()->getTextureID(_compositeInfos[i].texture); } _submitThread->_canvas = _submitCanvas; _submitThread->start(QThread::HighPriority); @@ -624,7 +624,7 @@ void OpenVrDisplayPlugin::compositeLayers() { glFlush(); if (!newComposite.textureID) { - newComposite.textureID = getGLBackend()->getTextureID(newComposite.texture, false); + newComposite.textureID = getGLBackend()->getTextureID(newComposite.texture); } withPresentThreadLock([&] { _submitThread->update(newComposite); @@ -638,7 +638,7 @@ void OpenVrDisplayPlugin::hmdPresent() { if (_threadedSubmit) { _submitThread->waitForPresent(); } else { - GLuint glTexId = getGLBackend()->getTextureID(_compositeFramebuffer->getRenderBuffer(0), false); + GLuint glTexId = getGLBackend()->getTextureID(_compositeFramebuffer->getRenderBuffer(0)); vr::Texture_t vrTexture { (void*)glTexId, vr::API_OpenGL, vr::ColorSpace_Auto }; vr::VRCompositor()->Submit(vr::Eye_Left, &vrTexture, &OPENVR_TEXTURE_BOUNDS_LEFT); vr::VRCompositor()->Submit(vr::Eye_Right, &vrTexture, &OPENVR_TEXTURE_BOUNDS_RIGHT); diff --git a/scripts/developer/libraries/jasmine/hifi-boot.js b/scripts/developer/libraries/jasmine/hifi-boot.js index f490a3618f..772dd8c17e 100644 --- a/scripts/developer/libraries/jasmine/hifi-boot.js +++ b/scripts/developer/libraries/jasmine/hifi-boot.js @@ -6,7 +6,7 @@ var lastSpecStartTime; function ConsoleReporter(options) { var startTime = new Date().getTime(); - var errorCount = 0; + var errorCount = 0, pending = []; this.jasmineStarted = function (obj) { print('Jasmine started with ' + obj.totalSpecsDefined + ' tests.'); }; @@ -15,11 +15,14 @@ var endTime = new Date().getTime(); print('
'); if (errorCount === 0) { - print ('All tests passed!'); + print ('All enabled tests passed!'); } else { print('Tests completed with ' + errorCount + ' ' + ERROR + '.'); } + if (pending.length) + print ('disabled:
   '+ + pending.join('
   ')+'
'); print('Tests completed in ' + (endTime - startTime) + 'ms.'); }; this.suiteStarted = function(obj) { @@ -32,6 +35,10 @@ lastSpecStartTime = new Date().getTime(); }; this.specDone = function(obj) { + if (obj.status === 'pending') { + pending.push(obj.fullName); + return print('...(pending ' + obj.fullName +')'); + } var specEndTime = new Date().getTime(); var symbol = obj.status === PASSED ? '' + CHECKMARK + '' : @@ -55,7 +62,7 @@ clearTimeout = Script.clearTimeout; clearInterval = Script.clearInterval; - var jasmine = jasmineRequire.core(jasmineRequire); + var jasmine = this.jasmine = jasmineRequire.core(jasmineRequire); var env = jasmine.getEnv(); diff --git a/scripts/developer/tests/.gitignore b/scripts/developer/tests/.gitignore new file mode 100644 index 0000000000..7cacbf042c --- /dev/null +++ b/scripts/developer/tests/.gitignore @@ -0,0 +1 @@ +cube_texture.ktx \ No newline at end of file diff --git a/scripts/developer/tests/scaling.png b/scripts/developer/tests/scaling.png new file mode 100644 index 0000000000000000000000000000000000000000..1e6a7df45d8440cc52d3b45501b909419fed2f1b GIT binary patch literal 3172 zcmd5;c~p~E7XQ63A%rEG0zw6aOe>MPK~IBJisaL=qAd_A)CCaGwo@pwR8gZC_|%SD z7o4J$st8ugBC@Cz1PuvBYq45TM5xH3-~b|o9YRPlCwSUFX3m*2?O*fPeed4;yT5ne zyYHTRFu>o3XKrr}fVXnRvQ+>DfPl*ZaO3bV9|M+iS1wx;Bqh%uyiNeFQgFJcjPsAZ zQ+zK$=}JukxPSm)@JBWj{=Z@I&oh>M-7es>42Jw255u%;9b7_Ar+anY4|u#h;LQ2T zXWDU%Msp|eqeX1oR5;Ed z%2}=L#||eS>yaPipfV=$G*|spnb+iuN)}3+#M!v_fuM66=g`gr46y9i{f9JUb}sR=GCq% zR(P3YRJ{JO!mW$0%9Jz@eYk?q7U{t?rcyCMhi z6{Z`TtYfCa`{>n@WsR%lQ;+N=tEm?r;P=t9QTaV#{6N zh1Pmom+C}u<3gMv&9tVn=P0E-_pG{G3+9@Qtto#hLS%otuAK(%ys9nwp}|Zmc#fx{ z@4e;VgALMJ*0VC@D40W;6Y8yVt(VamH@OrP?uF{bEryGZ>yr$b7q$3XH8k5~RT|1?GJ0JWv*9}?nbf>SGfY8XT`;`MnhRsYzrz*~= z*A&XdI*PFZ)w71p)L&UCq1xqkU@kKXfq;M)EJ!w0C{PvwOE%?t%W$GO* zQv5VVq%*X-k;SH=>(8gm)6xC*3s9bQ+sx>5RT7 zd4@9yS6et(OIvWcdDMMG9_j>>&9K$pDm%-Ru&KU$#B5$#Qtn?aEa-mH*Htd;G0_(7mw3bheDNh9_>{ z^Qy-bHmNZfzG!*8rbotKzSn=mCMS0TmwUqEoo9E0*ZsPG0VuUUW9$J8a9P8m5Z~r{ znNSEYOmn9J|Bl24<@*GEGdIz|qp>9b*Z}=!1OfPyUG2Y#)@lD4`*&c4Ogob4Bu+Xq z_pKif4jc5(+ssD9x+{L8cDah|fwsBUq1F8w&M`yF?qNW+?YVekiy}GmSVryVJ^eS| zh;`M+4z^hYRd^Q_9@K?wTb$A{7Xri|9nTYI6X~FIpsJ+#E>F2LwJt$rXE|;LH{VE| z;Xo7yWI3rZm-dT5(ROTHRc9U&zROcKs$;mhfnn zo9B75R?*7_8w>h>xgX>%Gl`By(!f8X>z@^CVZb)o~%L;_f%|NAB(nv;`jxp6AniR}RWt zOD#%0&=$Nt`J&_#-1=(EQqWKi8Lkb8!WuZfDcvVI0&)GgVqLa^bHM_2#0)@K3 zdjq1dUpco+wDwFy&qUnnvkH^scz73k? z*0^wSGR<<^RCVlhggmC)f!R|PKFR46xws_6p1H3d{JG8pRmZZE`8aUG%TL(o%%i$p34%Ee%*y;?&!bVsIIHVCU`&x67@V=oz80bPkRU#~ z>gCI6fJ#&z?)_yH7DU1RsqTCaah zxRduvIgME=l>hJbq$|g`^ed--g)kOKgQTUv^iBC-opi?Po#V+ zm|^$~;%#`!C&53@XE@4QRL7AQRrS7~vVf;t!Q#)z7t-jQjVzM8iRz8TkG3?+?X(#m z<7SA&gQxS2ubgGVr}+3Khj9P?+aBJ>0aLF{GTn+ZpK29FWaXm}S4>*R@TAF$j? zP`PuvJC1>5odjZPH}|b$E>yXWsgLIvw%Rj3?!mY;P72a{OGk~S zzi8qDb@f4?vTADBHjg5jCSF2k=JgQ|XpW2w2Br6wKArRmOB~8C&>lM*hdIYVKp2Y| z6JeT!ai>eA(2mL#^M^8v9P}5>P@G3x7M0vnt4+sq z^pA=!`D%4M<-@eFMq`Z_C+h!Q7-w(pixK<}i+|%%4w63`O{v~IOI3Pm{_;5hu<~vH KWra&4_WTP0c9{JD literal 0 HcmV?d00001 diff --git a/scripts/developer/tests/unit_tests/moduleTests/cycles/a.js b/scripts/developer/tests/unit_tests/moduleTests/cycles/a.js new file mode 100644 index 0000000000..265cfaa2df --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/cycles/a.js @@ -0,0 +1,10 @@ +/* eslint-env node */ +var a = exports; +a.done = false; +var b = require('./b.js'); +a.done = true; +a.name = 'a'; +a['a.done?'] = a.done; +a['b.done?'] = b.done; + +print('from a.js a.done =', a.done, '/ b.done =', b.done); diff --git a/scripts/developer/tests/unit_tests/moduleTests/cycles/b.js b/scripts/developer/tests/unit_tests/moduleTests/cycles/b.js new file mode 100644 index 0000000000..c46c872828 --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/cycles/b.js @@ -0,0 +1,10 @@ +/* eslint-env node */ +var b = exports; +b.done = false; +var a = require('./a.js'); +b.done = true; +b.name = 'b'; +b['a.done?'] = a.done; +b['b.done?'] = b.done; + +print('from b.js a.done =', a.done, '/ b.done =', b.done); diff --git a/scripts/developer/tests/unit_tests/moduleTests/cycles/main.js b/scripts/developer/tests/unit_tests/moduleTests/cycles/main.js new file mode 100644 index 0000000000..0ec39cd656 --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/cycles/main.js @@ -0,0 +1,17 @@ +/* eslint-env node */ +/* global print */ +/* eslint-disable comma-dangle */ + +print('main.js'); +var a = require('./a.js'), + b = require('./b.js'); + +print('from main.js a.done =', a.done, 'and b.done =', b.done); + +module.exports = { + name: 'main', + a: a, + b: b, + 'a.done?': a.done, + 'b.done?': b.done, +}; diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorAPIException.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorAPIException.js new file mode 100644 index 0000000000..bbe694b578 --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorAPIException.js @@ -0,0 +1,13 @@ +/* eslint-disable comma-dangle */ +// test module method exception being thrown within main constructor +(function() { + var apiMethod = Script.require('../exceptions/exceptionInFunction.js'); + print(Script.resolvePath(''), "apiMethod", apiMethod); + // this next line throws from within apiMethod + print(apiMethod()); + return { + preload: function(uuid) { + print("entityConstructorAPIException::preload -- never seen --", uuid, Script.resolvePath('')); + }, + }; +}); diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorModule.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorModule.js new file mode 100644 index 0000000000..a4e8c17ab6 --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorModule.js @@ -0,0 +1,23 @@ +/* global module */ +/* eslint-disable comma-dangle */ +// test dual-purpose module and standalone Entity script +function MyEntity(filename) { + return { + preload: function(uuid) { + print("entityConstructorModule.js::preload"); + if (typeof module === 'object') { + print("module.filename", module.filename); + print("module.parent.filename", module.parent && module.parent.filename); + } + }, + clickDownOnEntity: function(uuid, evt) { + print("entityConstructorModule.js::clickDownOnEntity"); + }, + }; +} + +try { + module.exports = MyEntity; +} catch (e) {} // eslint-disable-line no-empty +print('entityConstructorModule::MyEntity', typeof MyEntity); +(MyEntity); diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested.js new file mode 100644 index 0000000000..a90d979877 --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested.js @@ -0,0 +1,14 @@ +/* global module */ +// test Entity constructor based on inherited constructor from a module +function constructor() { + print("entityConstructorNested::constructor"); + var MyEntity = Script.require('./entityConstructorModule.js'); + return new MyEntity("-- created from entityConstructorNested --"); +} + +try { + module.exports = constructor; +} catch (e) { + constructor; +} + diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested2.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested2.js new file mode 100644 index 0000000000..29e0ed65b1 --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested2.js @@ -0,0 +1,25 @@ +/* global module */ +// test Entity constructor based on nested, inherited module constructors +function constructor() { + print("entityConstructorNested2::constructor"); + + // inherit from entityConstructorNested + var MyEntity = Script.require('./entityConstructorNested.js'); + function SubEntity() {} + SubEntity.prototype = new MyEntity('-- created from entityConstructorNested2 --'); + + // create new instance + var entity = new SubEntity(); + // "override" clickDownOnEntity for just this new instance + entity.clickDownOnEntity = function(uuid, evt) { + print("entityConstructorNested2::clickDownOnEntity"); + SubEntity.prototype.clickDownOnEntity.apply(this, arguments); + }; + return entity; +} + +try { + module.exports = constructor; +} catch (e) { + constructor; +} diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorRequireException.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorRequireException.js new file mode 100644 index 0000000000..5872bce529 --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorRequireException.js @@ -0,0 +1,10 @@ +/* eslint-disable comma-dangle */ +// test module-related exception from within "require" evaluation itself +(function() { + var mod = Script.require('../exceptions/exception.js'); + return { + preload: function(uuid) { + print("entityConstructorRequireException::preload (never happens)", uuid, Script.resolvePath(''), mod); + }, + }; +}); diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadAPIError.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadAPIError.js new file mode 100644 index 0000000000..eaee178b0a --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadAPIError.js @@ -0,0 +1,13 @@ +/* eslint-disable comma-dangle */ +// test module method exception being thrown within preload +(function() { + var apiMethod = Script.require('../exceptions/exceptionInFunction.js'); + print(Script.resolvePath(''), "apiMethod", apiMethod); + return { + preload: function(uuid) { + // this next line throws from within apiMethod + print(apiMethod()); + print("entityPreloadAPIException::preload -- never seen --", uuid, Script.resolvePath('')); + }, + }; +}); diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadRequire.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadRequire.js new file mode 100644 index 0000000000..50dab9fa7c --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadRequire.js @@ -0,0 +1,11 @@ +/* eslint-disable comma-dangle */ +// test requiring a module from within preload +(function constructor() { + return { + preload: function(uuid) { + print("entityPreloadRequire::preload"); + var example = Script.require('../example.json'); + print("entityPreloadRequire::example::name", example.name); + }, + }; +}); diff --git a/scripts/developer/tests/unit_tests/moduleTests/example.json b/scripts/developer/tests/unit_tests/moduleTests/example.json new file mode 100644 index 0000000000..42d7fe07da --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/example.json @@ -0,0 +1,9 @@ +{ + "name": "Example JSON Module", + "last-modified": 1485789862, + "config": { + "title": "My Title", + "width": 800, + "height": 600 + } +} diff --git a/scripts/developer/tests/unit_tests/moduleTests/exceptions/exception.js b/scripts/developer/tests/unit_tests/moduleTests/exceptions/exception.js new file mode 100644 index 0000000000..8d25d6b7a4 --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/exceptions/exception.js @@ -0,0 +1,4 @@ +/* eslint-env node */ +module.exports = "n/a"; +throw new Error('exception on line 2'); + diff --git a/scripts/developer/tests/unit_tests/moduleTests/exceptions/exceptionInFunction.js b/scripts/developer/tests/unit_tests/moduleTests/exceptions/exceptionInFunction.js new file mode 100644 index 0000000000..69415a0741 --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/exceptions/exceptionInFunction.js @@ -0,0 +1,38 @@ +/* eslint-env node */ +// dummy lines to make sure exception line number is well below parent test script +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// + + +function myfunc() { + throw new Error('exception on line 32 in myfunc'); +} +module.exports = myfunc; +if (Script[module.filename] === 'throw') { + myfunc(); +} diff --git a/scripts/developer/tests/unit_tests/moduleUnitTests.js b/scripts/developer/tests/unit_tests/moduleUnitTests.js new file mode 100644 index 0000000000..6810dd8b6d --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleUnitTests.js @@ -0,0 +1,378 @@ +/* eslint-env jasmine, node */ +/* global print:true, Script:true, global:true, require:true */ +/* eslint-disable comma-dangle */ +var isNode = instrumentTestrunner(), + runInterfaceTests = !isNode, + runNetworkTests = true; + +// describe wrappers (note: `xdescribe` indicates a disabled or "pending" jasmine test) +var INTERFACE = { describe: runInterfaceTests ? describe : xdescribe }, + NETWORK = { describe: runNetworkTests ? describe : xdescribe }; + +describe('require', function() { + describe('resolve', function() { + it('should resolve relative filenames', function() { + var expected = Script.resolvePath('./moduleTests/example.json'); + expect(require.resolve('./moduleTests/example.json')).toEqual(expected); + }); + describe('exceptions', function() { + it('should reject blank "" module identifiers', function() { + expect(function() { + require.resolve(''); + }).toThrowError(/Cannot find/); + }); + it('should reject excessive identifier sizes', function() { + expect(function() { + require.resolve(new Array(8193).toString()); + }).toThrowError(/Cannot find/); + }); + it('should reject implicitly-relative filenames', function() { + expect(function() { + var mod = require.resolve('example.js'); + mod.exists; + }).toThrowError(/Cannot find/); + }); + it('should reject unanchored, existing filenames with advice', function() { + expect(function() { + var mod = require.resolve('moduleTests/example.json'); + mod.exists; + }).toThrowError(/use '.\/moduleTests\/example\.json'/); + }); + it('should reject unanchored, non-existing filenames', function() { + expect(function() { + var mod = require.resolve('asdfssdf/example.json'); + mod.exists; + }).toThrowError(/Cannot find.*system module not found/); + }); + it('should reject non-existent filenames', function() { + expect(function() { + require.resolve('./404error.js'); + }).toThrowError(/Cannot find/); + }); + it('should reject identifiers resolving to a directory', function() { + expect(function() { + var mod = require.resolve('.'); + mod.exists; + // console.warn('resolved(.)', mod); + }).toThrowError(/Cannot find/); + expect(function() { + var mod = require.resolve('..'); + mod.exists; + // console.warn('resolved(..)', mod); + }).toThrowError(/Cannot find/); + expect(function() { + var mod = require.resolve('../'); + mod.exists; + // console.warn('resolved(../)', mod); + }).toThrowError(/Cannot find/); + }); + (isNode ? xit : it)('should reject non-system, extensionless identifiers', function() { + expect(function() { + require.resolve('./example'); + }).toThrowError(/Cannot find/); + }); + }); + }); + + describe('JSON', function() { + it('should import .json modules', function() { + var example = require('./moduleTests/example.json'); + expect(example.name).toEqual('Example JSON Module'); + }); + // noet: support for loading JSON via content type workarounds reverted + // (leaving these tests intact in case ever revisited later) + // INTERFACE.describe('interface', function() { + // NETWORK.describe('network', function() { + // xit('should import #content-type=application/json modules', function() { + // var results = require('https://jsonip.com#content-type=application/json'); + // expect(results.ip).toMatch(/^[.0-9]+$/); + // }); + // xit('should import content-type: application/json modules', function() { + // var scope = { 'content-type': 'application/json' }; + // var results = require.call(scope, 'https://jsonip.com'); + // expect(results.ip).toMatch(/^[.0-9]+$/); + // }); + // }); + // }); + + }); + + INTERFACE.describe('system', function() { + it('require("vec3")', function() { + expect(require('vec3')).toEqual(jasmine.any(Function)); + }); + it('require("vec3").method', function() { + expect(require('vec3')().isValid).toEqual(jasmine.any(Function)); + }); + it('require("vec3") as constructor', function() { + var vec3 = require('vec3'); + var v = vec3(1.1, 2.2, 3.3); + expect(v).toEqual(jasmine.any(Object)); + expect(v.isValid).toEqual(jasmine.any(Function)); + expect(v.isValid()).toBe(true); + expect(v.toString()).toEqual('[Vec3 (1.100,2.200,3.300)]'); + }); + }); + + describe('cache', function() { + it('should cache modules by resolved module id', function() { + var value = new Date; + var example = require('./moduleTests/example.json'); + // earmark the module object with a unique value + example['.test'] = value; + var example2 = require('../../tests/unit_tests/moduleTests/example.json'); + expect(example2).toBe(example); + // verify earmark is still the same after a second require() + expect(example2['.test']).toBe(example['.test']); + }); + it('should reload cached modules set to null', function() { + var value = new Date; + var example = require('./moduleTests/example.json'); + example['.test'] = value; + require.cache[require.resolve('../../tests/unit_tests/moduleTests/example.json')] = null; + var example2 = require('../../tests/unit_tests/moduleTests/example.json'); + // verify the earmark is *not* the same as before + expect(example2).not.toBe(example); + expect(example2['.test']).not.toBe(example['.test']); + }); + it('should reload when module property is deleted', function() { + var value = new Date; + var example = require('./moduleTests/example.json'); + example['.test'] = value; + delete require.cache[require.resolve('../../tests/unit_tests/moduleTests/example.json')]; + var example2 = require('../../tests/unit_tests/moduleTests/example.json'); + // verify the earmark is *not* the same as before + expect(example2).not.toBe(example); + expect(example2['.test']).not.toBe(example['.test']); + }); + }); + + describe('cyclic dependencies', function() { + describe('should allow lazy-ref cyclic module resolution', function() { + var main; + beforeEach(function() { + // eslint-disable-next-line + try { this._print = print; } catch (e) {} + // during these tests print() is no-op'd so that it doesn't disrupt the reporter output + print = function() {}; + Script.resetModuleCache(); + }); + afterEach(function() { + print = this._print; + }); + it('main is requirable', function() { + main = require('./moduleTests/cycles/main.js'); + expect(main).toEqual(jasmine.any(Object)); + }); + it('transient a and b done values', function() { + expect(main.a['b.done?']).toBe(true); + expect(main.b['a.done?']).toBe(false); + }); + it('ultimate a.done?', function() { + expect(main['a.done?']).toBe(true); + }); + it('ultimate b.done?', function() { + expect(main['b.done?']).toBe(true); + }); + }); + }); + + describe('JS', function() { + it('should throw catchable local file errors', function() { + expect(function() { + require('file:///dev/null/non-existent-file.js'); + }).toThrowError(/path not found|Cannot find.*non-existent-file/); + }); + it('should throw catchable invalid id errors', function() { + expect(function() { + require(new Array(4096 * 2).toString()); + }).toThrowError(/invalid.*size|Cannot find.*,{30}/); + }); + it('should throw catchable unresolved id errors', function() { + expect(function() { + require('foobar:/baz.js'); + }).toThrowError(/could not resolve|Cannot find.*foobar:/); + }); + + NETWORK.describe('network', function() { + // note: depending on retries these tests can take up to 60 seconds each to timeout + var timeout = 75 * 1000; + it('should throw catchable host errors', function() { + expect(function() { + var mod = require('http://non.existent.highfidelity.io/moduleUnitTest.js'); + print("mod", Object.keys(mod)); + }).toThrowError(/error retrieving script .ServerUnavailable.|Cannot find.*non.existent/); + }, timeout); + it('should throw catchable network timeouts', function() { + expect(function() { + require('http://ping.highfidelity.io:1024'); + }).toThrowError(/error retrieving script .Timeout.|Cannot find.*ping.highfidelity/); + }, timeout); + }); + }); + + INTERFACE.describe('entity', function() { + var sampleScripts = [ + 'entityConstructorAPIException.js', + 'entityConstructorModule.js', + 'entityConstructorNested2.js', + 'entityConstructorNested.js', + 'entityConstructorRequireException.js', + 'entityPreloadAPIError.js', + 'entityPreloadRequire.js', + ].filter(Boolean).map(function(id) { + return Script.require.resolve('./moduleTests/entity/'+id); + }); + + var uuids = []; + function cleanup() { + uuids.splice(0,uuids.length).forEach(function(uuid) { + Entities.deleteEntity(uuid); + }); + } + afterAll(cleanup); + // extra sanity check to avoid lingering entities + Script.scriptEnding.connect(cleanup); + + for (var i=0; i < sampleScripts.length; i++) { + maketest(i); + } + + function maketest(i) { + var script = sampleScripts[ i % sampleScripts.length ]; + var shortname = '['+i+'] ' + script.split('/').pop(); + var position = MyAvatar.position; + position.y -= i/2; + // define a unique jasmine test for the current entity script + it(shortname, function(done) { + var uuid = Entities.addEntity({ + text: shortname, + description: Script.resolvePath('').split('/').pop(), + type: 'Text', + position: position, + rotation: MyAvatar.orientation, + script: script, + scriptTimestamp: +new Date, + lifetime: 20, + lineHeight: 1/8, + dimensions: { x: 2, y: 0.5, z: 0.01 }, + backgroundColor: { red: 0, green: 0, blue: 0 }, + color: { red: 0xff, green: 0xff, blue: 0xff }, + }, !Entities.serversExist() || !Entities.canRezTmp()); + uuids.push(uuid); + function stopChecking() { + if (ii) { + Script.clearInterval(ii); + ii = 0; + } + } + var ii = Script.setInterval(function() { + Entities.queryPropertyMetadata(uuid, "script", function(err, result) { + if (err) { + stopChecking(); + throw new Error(err); + } + if (result.success) { + stopChecking(); + if (/Exception/.test(script)) { + expect(result.status).toMatch(/^error_(loading|running)_script$/); + } else { + expect(result.status).toEqual("running"); + } + Entities.deleteEntity(uuid); + done(); + } else { + print('!result.success', JSON.stringify(result)); + } + }); + }, 100); + Script.setTimeout(stopChecking, 4900); + }, 5000 /* jasmine async timeout */); + } + }); +}); + +// support for isomorphic Node.js / Interface unit testing +// note: run `npm install` from unit_tests/ and then `node moduleUnitTests.js` +function run() {} +function instrumentTestrunner() { + var isNode = typeof process === 'object' && process.title === 'node'; + if (typeof describe === 'function') { + // already running within a test runner; assume jasmine is ready-to-go + return isNode; + } + if (isNode) { + /* eslint-disable no-console */ + // Node.js test mode + // to keep things consistent Node.js uses the local jasmine.js library (instead of an npm version) + var jasmineRequire = require('../../libraries/jasmine/jasmine.js'); + var jasmine = jasmineRequire.core(jasmineRequire); + var env = jasmine.getEnv(); + var jasmineInterface = jasmineRequire.interface(jasmine, env); + for (var p in jasmineInterface) { + global[p] = jasmineInterface[p]; + } + env.addReporter(new (require('jasmine-console-reporter'))); + // testing mocks + Script = { + resetModuleCache: function() { + module.require.cache = {}; + }, + setTimeout: setTimeout, + clearTimeout: clearTimeout, + resolvePath: function(id) { + // this attempts to accurately emulate how Script.resolvePath works + var trace = {}; Error.captureStackTrace(trace); + var base = trace.stack.split('\n')[2].replace(/^.*[(]|[)].*$/g,'').replace(/:[0-9]+:[0-9]+.*$/,''); + if (!id) { + return base; + } + var rel = base.replace(/[^\/]+$/, id); + console.info('rel', rel); + return require.resolve(rel); + }, + require: function(mod) { + return require(Script.require.resolve(mod)); + }, + }; + Script.require.cache = require.cache; + Script.require.resolve = function(mod) { + if (mod === '.' || /^\.\.($|\/)/.test(mod)) { + throw new Error("Cannot find module '"+mod+"' (is dir)"); + } + var path = require.resolve(mod); + // console.info('node-require-reoslved', mod, path); + try { + if (require('fs').lstatSync(path).isDirectory()) { + throw new Error("Cannot find module '"+path+"' (is directory)"); + } + // console.info('!path', path); + } catch (e) { + console.error(e); + } + return path; + }; + print = console.info.bind(console, '[print]'); + /* eslint-enable no-console */ + } else { + // Interface test mode + global = this; + Script.require('../../../system/libraries/utils.js'); + this.jasmineRequire = Script.require('../../libraries/jasmine/jasmine.js'); + Script.require('../../libraries/jasmine/hifi-boot.js'); + require = Script.require; + // polyfill console + /* global console:true */ + console = { + log: print, + info: print.bind(this, '[info]'), + warn: print.bind(this, '[warn]'), + error: print.bind(this, '[error]'), + debug: print.bind(this, '[debug]'), + }; + } + // eslint-disable-next-line + run = function() { global.jasmine.getEnv().execute(); }; + return isNode; +} +run(); diff --git a/scripts/developer/tests/unit_tests/package.json b/scripts/developer/tests/unit_tests/package.json new file mode 100644 index 0000000000..91d719b687 --- /dev/null +++ b/scripts/developer/tests/unit_tests/package.json @@ -0,0 +1,6 @@ +{ + "name": "unit_tests", + "devDependencies": { + "jasmine-console-reporter": "^1.2.7" + } +} diff --git a/scripts/developer/tests/unit_tests/scriptUnitTests.js b/scripts/developer/tests/unit_tests/scriptUnitTests.js index 63b451e97f..fa8cb44608 100644 --- a/scripts/developer/tests/unit_tests/scriptUnitTests.js +++ b/scripts/developer/tests/unit_tests/scriptUnitTests.js @@ -15,10 +15,20 @@ describe('Script', function () { // characterization tests // initially these are just to capture how the app works currently var testCases = { + // special relative resolves '': filename, '.': dirname, '..': parentdir, + + // local file "magic" tilde path expansion + '/~/defaultScripts.js': ScriptDiscoveryService.defaultScriptsPath + '/defaultScripts.js', + + // these schemes appear to always get resolved to empty URLs + 'qrc://test': '', 'about:Entities 1': '', + 'ftp://host:port/path': '', + 'data:text/html;text,foo': '', + 'Entities 1': dirname + 'Entities 1', './file.js': dirname + 'file.js', 'c:/temp/': 'file:///c:/temp/', @@ -31,6 +41,12 @@ describe('Script', function () { '/~/libraries/utils.js': 'file:///~/libraries/utils.js', '/temp/file.js': 'file:///temp/file.js', '/~/': 'file:///~/', + + // these schemes appear to always get resolved to the same URL again + 'http://highfidelity.com': 'http://highfidelity.com', + 'atp:/highfidelity': 'atp:/highfidelity', + 'atp:c2d7e3a48cadf9ba75e4f8d9f4d80e75276774880405a093fdee36543aa04f': + 'atp:c2d7e3a48cadf9ba75e4f8d9f4d80e75276774880405a093fdee36543aa04f', }; describe('resolvePath', function () { Object.keys(testCases).forEach(function(input) { @@ -42,7 +58,7 @@ describe('Script', function () { describe('include', function () { var old_cache_buster; - var cache_buster = '#' + +new Date; + var cache_buster = '#' + new Date().getTime().toString(36); beforeAll(function() { old_cache_buster = Settings.getValue('cache_buster'); Settings.setValue('cache_buster', cache_buster); diff --git a/scripts/developer/utilities/record/recorder.js b/scripts/developer/utilities/record/recorder.js index 0e335116d5..ba1c8b0393 100644 --- a/scripts/developer/utilities/record/recorder.js +++ b/scripts/developer/utilities/record/recorder.js @@ -9,12 +9,14 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +/* globals HIFI_PUBLIC_BUCKET:true, Tool, ToolBar */ + HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/"; Script.include("/~/system/libraries/toolBars.js"); var recordingFile = "recording.hfr"; -function setPlayerOptions() { +function setDefaultPlayerOptions() { Recording.setPlayFromCurrentLocation(true); Recording.setPlayerUseDisplayName(false); Recording.setPlayerUseAttachments(false); @@ -38,16 +40,16 @@ var saveIcon; var loadIcon; var spacing; var timerOffset; -setupToolBar(); - var timer = null; var slider = null; + +setupToolBar(); setupTimer(); var watchStop = false; function setupToolBar() { - if (toolBar != null) { + if (toolBar !== null) { print("Multiple calls to Recorder.js:setupToolBar()"); return; } @@ -56,6 +58,8 @@ function setupToolBar() { toolBar = new ToolBar(0, 0, ToolBar.HORIZONTAL); + toolBar.onMove = onToolbarMove; + toolBar.setBack(COLOR_TOOL_BAR, ALPHA_OFF); recordIcon = toolBar.addTool({ @@ -86,7 +90,7 @@ function setupToolBar() { visible: true }, false); - timerOffset = toolBar.width; + timerOffset = toolBar.width + ToolBar.SPACING; spacing = toolBar.addSpacing(0); saveIcon = toolBar.addTool({ @@ -112,15 +116,15 @@ function setupTimer() { text: (0.00).toFixed(3), backgroundColor: COLOR_OFF, x: 0, y: 0, - width: 0, height: 0, - leftMargin: 10, topMargin: 10, + width: 200, height: 25, + leftMargin: 5, topMargin: 3, alpha: 1.0, backgroundAlpha: 1.0, visible: true }); slider = { x: 0, y: 0, w: 200, h: 20, - pos: 0.0, // 0.0 <= pos <= 1.0 + pos: 0.0 // 0.0 <= pos <= 1.0 }; slider.background = Overlays.addOverlay("text", { text: "", @@ -144,20 +148,40 @@ function setupTimer() { }); } +function onToolbarMove(newX, newY, deltaX, deltaY) { + Overlays.editOverlay(timer, { + x: newX + timerOffset - ToolBar.SPACING, + y: newY + }); + + slider.x = newX - ToolBar.SPACING; + slider.y = newY - slider.h - ToolBar.SPACING; + + Overlays.editOverlay(slider.background, { + x: slider.x, + y: slider.y + }); + Overlays.editOverlay(slider.foreground, { + x: slider.x, + y: slider.y + }); +} + function updateTimer() { var text = ""; if (Recording.isRecording()) { text = formatTime(Recording.recorderElapsed()); - } else { - text = formatTime(Recording.playerElapsed()) + " / " + - formatTime(Recording.playerLength()); + text = formatTime(Recording.playerElapsed()) + " / " + formatTime(Recording.playerLength()); } + var timerWidth = text.length * 8 + ((Recording.isRecording()) ? 15 : 0); + Overlays.editOverlay(timer, { - text: text - }) - toolBar.changeSpacing(text.length * 8 + ((Recording.isRecording()) ? 15 : 0), spacing); + text: text, + width: timerWidth + }); + toolBar.changeSpacing(timerWidth + ToolBar.SPACING, spacing); if (Recording.isRecording()) { slider.pos = 1.0; @@ -173,7 +197,7 @@ function updateTimer() { function formatTime(time) { var MIN_PER_HOUR = 60; var SEC_PER_MIN = 60; - var MSEC_PER_SEC = 1000; + var MSEC_DIGITS = 3; var hours = Math.floor(time / (SEC_PER_MIN * MIN_PER_HOUR)); time -= hours * (SEC_PER_MIN * MIN_PER_HOUR); @@ -184,37 +208,19 @@ function formatTime(time) { var seconds = time; var text = ""; - text += (hours > 0) ? hours + ":" : - ""; - text += (minutes > 0) ? ((minutes < 10 && text != "") ? "0" : "") + minutes + ":" : - ""; - text += ((seconds < 10 && text != "") ? "0" : "") + seconds.toFixed(3); + text += (hours > 0) ? hours + ":" : ""; + text += (minutes > 0) ? ((minutes < 10 && text !== "") ? "0" : "") + minutes + ":" : ""; + text += ((seconds < 10 && text !== "") ? "0" : "") + seconds.toFixed(MSEC_DIGITS); return text; } function moveUI() { var relative = { x: 70, y: 40 }; toolBar.move(relative.x, windowDimensions.y - relative.y); - Overlays.editOverlay(timer, { - x: relative.x + timerOffset - ToolBar.SPACING, - y: windowDimensions.y - relative.y - ToolBar.SPACING - }); - - slider.x = relative.x - ToolBar.SPACING; - slider.y = windowDimensions.y - relative.y - slider.h - ToolBar.SPACING; - - Overlays.editOverlay(slider.background, { - x: slider.x, - y: slider.y, - }); - Overlays.editOverlay(slider.foreground, { - x: slider.x, - y: slider.y, - }); } function mousePressEvent(event) { - clickedOverlay = Overlays.getOverlayAtPoint({ x: event.x, y: event.y }); + var clickedOverlay = Overlays.getOverlayAtPoint({ x: event.x, y: event.y }); if (recordIcon === toolBar.clicked(clickedOverlay, false) && !Recording.isPlaying()) { if (!Recording.isRecording()) { @@ -226,7 +232,11 @@ function mousePressEvent(event) { toolBar.setAlpha(ALPHA_OFF, loadIcon); } else { Recording.stopRecording(); - toolBar.selectTool(recordIcon, true ); + toolBar.selectTool(recordIcon, true); + setDefaultPlayerOptions(); + // Plays the recording at the same spot as you recorded it + Recording.setPlayFromCurrentLocation(false); + Recording.setPlayerTime(0); Recording.loadLastRecording(); toolBar.setAlpha(ALPHA_ON, playIcon); toolBar.setAlpha(ALPHA_ON, playLoopIcon); @@ -240,7 +250,6 @@ function mousePressEvent(event) { toolBar.setAlpha(ALPHA_ON, saveIcon); toolBar.setAlpha(ALPHA_ON, loadIcon); } else if (Recording.playerLength() > 0) { - setPlayerOptions(); Recording.setPlayerLoop(false); Recording.startPlaying(); toolBar.setAlpha(ALPHA_OFF, recordIcon); @@ -255,7 +264,6 @@ function mousePressEvent(event) { toolBar.setAlpha(ALPHA_ON, saveIcon); toolBar.setAlpha(ALPHA_ON, loadIcon); } else if (Recording.playerLength() > 0) { - setPlayerOptions(); Recording.setPlayerLoop(true); Recording.startPlaying(); toolBar.setAlpha(ALPHA_OFF, recordIcon); @@ -263,7 +271,7 @@ function mousePressEvent(event) { toolBar.setAlpha(ALPHA_OFF, loadIcon); } } else if (saveIcon === toolBar.clicked(clickedOverlay)) { - if (!Recording.isRecording() && !Recording.isPlaying() && Recording.playerLength() != 0) { + if (!Recording.isRecording() && !Recording.isPlaying() && Recording.playerLength() !== 0) { recordingFile = Window.save("Save recording to file", ".", "Recordings (*.hfr)"); if (!(recordingFile === "null" || recordingFile === null || recordingFile === "")) { Recording.saveRecording(recordingFile); @@ -274,6 +282,7 @@ function mousePressEvent(event) { recordingFile = Window.browse("Load recording from file", ".", "Recordings (*.hfr *.rec *.HFR *.REC)"); if (!(recordingFile === "null" || recordingFile === null || recordingFile === "")) { Recording.loadRecording(recordingFile); + setDefaultPlayerOptions(); } if (Recording.playerLength() > 0) { toolBar.setAlpha(ALPHA_ON, playIcon); @@ -282,8 +291,8 @@ function mousePressEvent(event) { } } } else if (Recording.playerLength() > 0 && - slider.x < event.x && event.x < slider.x + slider.w && - slider.y < event.y && event.y < slider.y + slider.h) { + slider.x < event.x && event.x < slider.x + slider.w && + slider.y < event.y && event.y < slider.y + slider.h) { isSliding = true; slider.pos = (event.x - slider.x) / slider.w; Recording.setPlayerTime(slider.pos * Recording.playerLength()); @@ -308,7 +317,7 @@ function mouseReleaseEvent(event) { function update() { var newDimensions = Controller.getViewportDimensions(); - if (windowDimensions.x != newDimensions.x || windowDimensions.y != newDimensions.y) { + if (windowDimensions.x !== newDimensions.x || windowDimensions.y !== newDimensions.y) { windowDimensions = newDimensions; moveUI(); } diff --git a/scripts/developer/utilities/render/deferredLighting.qml b/scripts/developer/utilities/render/deferredLighting.qml index 99a9f258e3..c7ec8e1153 100644 --- a/scripts/developer/utilities/render/deferredLighting.qml +++ b/scripts/developer/utilities/render/deferredLighting.qml @@ -25,7 +25,7 @@ Column { "Lightmap:LightingModel:enableLightmap", "Background:LightingModel:enableBackground", "ssao:AmbientOcclusion:enabled", - "Textures:LightingModel:enableMaterialTexturing", + "Textures:LightingModel:enableMaterialTexturing" ] CheckBox { text: modelData.split(":")[0] @@ -45,6 +45,7 @@ Column { "Diffuse:LightingModel:enableDiffuse", "Specular:LightingModel:enableSpecular", "Albedo:LightingModel:enableAlbedo", + "Wireframe:LightingModel:enableWireframe" ] CheckBox { text: modelData.split(":")[0] diff --git a/scripts/modules/vec3.js b/scripts/modules/vec3.js new file mode 100644 index 0000000000..f164f01374 --- /dev/null +++ b/scripts/modules/vec3.js @@ -0,0 +1,69 @@ +// Example of using a "system module" to decouple Vec3's implementation details. +// +// Users would bring Vec3 support in as a module: +// var vec3 = Script.require('vec3'); +// + +// (this example is compatible with using as a Script.include and as a Script.require module) +try { + // Script.require + module.exports = vec3; +} catch(e) { + // Script.include + Script.registerValue("vec3", vec3); +} + +vec3.fromObject = function(v) { + //return new vec3(v.x, v.y, v.z); + //... this is even faster and achieves the same effect + v.__proto__ = vec3.prototype; + return v; +}; + +vec3.prototype = { + multiply: function(v2) { + // later on could support overrides like so: + // if (v2 instanceof quat) { [...] } + // which of the below is faster (C++ or JS)? + // (dunno -- but could systematically find out and go with that version) + + // pure JS option + // return new vec3(this.x * v2.x, this.y * v2.y, this.z * v2.z); + + // hybrid C++ option + return vec3.fromObject(Vec3.multiply(this, v2)); + }, + // detects any NaN and Infinity values + isValid: function() { + return isFinite(this.x) && isFinite(this.y) && isFinite(this.z); + }, + // format Vec3's, eg: + // var v = vec3(); + // print(v); // outputs [Vec3 (0.000, 0.000, 0.000)] + toString: function() { + if (this === vec3.prototype) { + return "{Vec3 prototype}"; + } + function fixed(n) { return n.toFixed(3); } + return "[Vec3 (" + [this.x, this.y, this.z].map(fixed) + ")]"; + }, +}; + +vec3.DEBUG = true; + +function vec3(x, y, z) { + if (!(this instanceof vec3)) { + // if vec3 is called as a function then re-invoke as a constructor + // (so that `value instanceof vec3` holds true for created values) + return new vec3(x, y, z); + } + + // unfold default arguments (vec3(), vec3(.5), vec3(0,1), etc.) + this.x = x !== undefined ? x : 0; + this.y = y !== undefined ? y : this.x; + this.z = z !== undefined ? z : this.y; + + if (vec3.DEBUG && !this.isValid()) + throw new Error('vec3() -- invalid initial values ['+[].slice.call(arguments)+']'); +}; + diff --git a/scripts/system/assets/images/icon-particles.svg b/scripts/system/assets/images/icon-particles.svg new file mode 100644 index 0000000000..5e0105d7cd --- /dev/null +++ b/scripts/system/assets/images/icon-particles.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + diff --git a/scripts/system/assets/images/icon-point-light.svg b/scripts/system/assets/images/icon-point-light.svg new file mode 100644 index 0000000000..896c35b63b --- /dev/null +++ b/scripts/system/assets/images/icon-point-light.svg @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/scripts/system/assets/images/icon-spot-light.svg b/scripts/system/assets/images/icon-spot-light.svg new file mode 100644 index 0000000000..ac2f87bb27 --- /dev/null +++ b/scripts/system/assets/images/icon-spot-light.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index c058f046db..1c6c9af272 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -85,6 +85,7 @@ function Trigger(hand) { } var coolInTimeout = null; +var ignoredEntities = []; var TELEPORTER_STATES = { IDLE: 'idle', @@ -239,11 +240,11 @@ function Teleporter() { // We might hit an invisible entity that is not a seat, so we need to do a second pass. // * In the second pass we pick against visible entities only. // - var intersection = Entities.findRayIntersection(pickRay, true, [], [this.targetEntity], false, true); + var intersection = Entities.findRayIntersection(pickRay, true, [], [this.targetEntity].concat(ignoredEntities), false, true); var teleportLocationType = getTeleportTargetType(intersection); if (teleportLocationType === TARGET.INVISIBLE) { - intersection = Entities.findRayIntersection(pickRay, true, [], [this.targetEntity], true, true); + intersection = Entities.findRayIntersection(pickRay, true, [], [this.targetEntity].concat(ignoredEntities), true, true); teleportLocationType = getTeleportTargetType(intersection); } @@ -513,7 +514,7 @@ function cleanup() { Script.scriptEnding.connect(cleanup); var isDisabled = false; -var handleHandMessages = function(channel, message, sender) { +var handleTeleportMessages = function(channel, message, sender) { var data; if (sender === MyAvatar.sessionUUID) { if (channel === 'Hifi-Teleport-Disabler') { @@ -529,12 +530,20 @@ var handleHandMessages = function(channel, message, sender) { if (message === 'none') { isDisabled = false; } - + } else if (channel === 'Hifi-Teleport-Ignore-Add' && !Uuid.isNull(message) && ignoredEntities.indexOf(message) === -1) { + ignoredEntities.push(message); + } else if (channel === 'Hifi-Teleport-Ignore-Remove' && !Uuid.isNull(message)) { + var removeIndex = ignoredEntities.indexOf(message); + if (removeIndex > -1) { + ignoredEntities.splice(removeIndex, 1); + } } } } Messages.subscribe('Hifi-Teleport-Disabler'); -Messages.messageReceived.connect(handleHandMessages); +Messages.subscribe('Hifi-Teleport-Ignore-Add'); +Messages.subscribe('Hifi-Teleport-Ignore-Remove'); +Messages.messageReceived.connect(handleTeleportMessages); }()); // END LOCAL_SCOPE diff --git a/scripts/system/controllers/toggleAdvancedMovementForHandControllers.js b/scripts/system/controllers/toggleAdvancedMovementForHandControllers.js index 46464dc2e1..e6c9b0aee0 100644 --- a/scripts/system/controllers/toggleAdvancedMovementForHandControllers.js +++ b/scripts/system/controllers/toggleAdvancedMovementForHandControllers.js @@ -17,15 +17,14 @@ var mappingName, basicMapping, isChecked; var TURN_RATE = 1000; var MENU_ITEM_NAME = "Advanced Movement For Hand Controllers"; -var SETTINGS_KEY = 'advancedMovementForHandControllersIsChecked'; var isDisabled = false; -var previousSetting = Settings.getValue(SETTINGS_KEY); -if (previousSetting === '' || previousSetting === false || previousSetting === 'false') { +var previousSetting = MyAvatar.useAdvancedMovementControls; +if (previousSetting === false) { previousSetting = false; isChecked = false; } -if (previousSetting === true || previousSetting === 'true') { +if (previousSetting === true) { previousSetting = true; isChecked = true; } @@ -37,7 +36,6 @@ function addAdvancedMovementItemToSettingsMenu() { isCheckable: true, isChecked: previousSetting }); - } function rotate180() { @@ -72,7 +70,6 @@ function registerBasicMapping() { } return; }); - basicMapping.from(Controller.Standard.LX).to(Controller.Standard.RX); basicMapping.from(Controller.Standard.RY).to(function(value) { if (isDisabled) { return; @@ -112,10 +109,10 @@ function menuItemEvent(menuItem) { if (menuItem == MENU_ITEM_NAME) { isChecked = Menu.isOptionChecked(MENU_ITEM_NAME); if (isChecked === true) { - Settings.setValue(SETTINGS_KEY, true); + MyAvatar.useAdvancedMovementControls = true; disableMappings(); } else if (isChecked === false) { - Settings.setValue(SETTINGS_KEY, false); + MyAvatar.useAdvancedMovementControls = false; enableMappings(); } } diff --git a/scripts/system/edit.js b/scripts/system/edit.js index a440fec1ac..fb5a3c2f73 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -33,13 +33,27 @@ Script.include([ "libraries/gridTool.js", "libraries/entityList.js", "particle_explorer/particleExplorerTool.js", - "libraries/lightOverlayManager.js" + "libraries/entityIconOverlayManager.js" ]); var selectionDisplay = SelectionDisplay; var selectionManager = SelectionManager; -var lightOverlayManager = new LightOverlayManager(); +const PARTICLE_SYSTEM_URL = Script.resolvePath("assets/images/icon-particles.svg"); +const POINT_LIGHT_URL = Script.resolvePath("assets/images/icon-point-light.svg"); +const SPOT_LIGHT_URL = Script.resolvePath("assets/images/icon-spot-light.svg"); +entityIconOverlayManager = new EntityIconOverlayManager(['Light', 'ParticleEffect'], function(entityID) { + var properties = Entities.getEntityProperties(entityID, ['type', 'isSpotlight']); + if (properties.type === 'Light') { + return { + url: properties.isSpotlight ? SPOT_LIGHT_URL : POINT_LIGHT_URL, + } + } else { + return { + url: PARTICLE_SYSTEM_URL, + } + } +}); var cameraManager = new CameraManager(); @@ -53,7 +67,45 @@ var entityListTool = new EntityListTool(); selectionManager.addEventListener(function () { selectionDisplay.updateHandles(); - lightOverlayManager.updatePositions(); + entityIconOverlayManager.updatePositions(); + + // Update particle explorer + var needToDestroyParticleExplorer = false; + if (selectionManager.selections.length === 1) { + var selectedEntityID = selectionManager.selections[0]; + if (selectedEntityID === selectedParticleEntityID) { + return; + } + var type = Entities.getEntityProperties(selectedEntityID, "type").type; + if (type === "ParticleEffect") { + // Destroy the old particles web view first + particleExplorerTool.destroyWebView(); + particleExplorerTool.createWebView(); + var properties = Entities.getEntityProperties(selectedEntityID); + var particleData = { + messageType: "particle_settings", + currentProperties: properties + }; + selectedParticleEntityID = selectedEntityID; + particleExplorerTool.setActiveParticleEntity(selectedParticleEntityID); + + particleExplorerTool.webView.webEventReceived.connect(function (data) { + data = JSON.parse(data); + if (data.messageType === "page_loaded") { + particleExplorerTool.webView.emitScriptEvent(JSON.stringify(particleData)); + } + }); + } else { + needToDestroyParticleExplorer = true; + } + } else { + needToDestroyParticleExplorer = true; + } + + if (needToDestroyParticleExplorer && selectedParticleEntityID !== null) { + selectedParticleEntityID = null; + particleExplorerTool.destroyWebView(); + } }); const KEY_P = 80; //Key code for letter p used for Parenting hotkey. @@ -82,13 +134,13 @@ var DEFAULT_LIGHT_DIMENSIONS = Vec3.multiply(20, DEFAULT_DIMENSIONS); var MENU_AUTO_FOCUS_ON_SELECT = "Auto Focus on Select"; var MENU_EASE_ON_FOCUS = "Ease Orientation on Focus"; -var MENU_SHOW_LIGHTS_IN_EDIT_MODE = "Show Lights in Edit Mode"; +var MENU_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE = "Show Lights and Particle Systems in Edit Mode"; var MENU_SHOW_ZONES_IN_EDIT_MODE = "Show Zones in Edit Mode"; var SETTING_INSPECT_TOOL_ENABLED = "inspectToolEnabled"; var SETTING_AUTO_FOCUS_ON_SELECT = "autoFocusOnSelect"; var SETTING_EASE_ON_FOCUS = "cameraEaseOnFocus"; -var SETTING_SHOW_LIGHTS_IN_EDIT_MODE = "showLightsInEditMode"; +var SETTING_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE = "showLightsAndParticlesInEditMode"; var SETTING_SHOW_ZONES_IN_EDIT_MODE = "showZonesInEditMode"; @@ -506,7 +558,7 @@ var toolBar = (function () { toolBar.writeProperty("shown", false); toolBar.writeProperty("shown", true); } - lightOverlayManager.setVisible(isActive && Menu.isOptionChecked(MENU_SHOW_LIGHTS_IN_EDIT_MODE)); + entityIconOverlayManager.setVisible(isActive && Menu.isOptionChecked(MENU_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE)); Entities.setDrawZoneBoundaries(isActive && Menu.isOptionChecked(MENU_SHOW_ZONES_IN_EDIT_MODE)); }; @@ -571,8 +623,8 @@ function findClickedEntity(event) { } var entityResult = Entities.findRayIntersection(pickRay, true); // want precision picking - var lightResult = lightOverlayManager.findRayIntersection(pickRay); - lightResult.accurate = true; + var iconResult = entityIconOverlayManager.findRayIntersection(pickRay); + iconResult.accurate = true; if (pickZones) { Entities.setZonesArePickable(false); @@ -580,18 +632,12 @@ function findClickedEntity(event) { var result; - if (!entityResult.intersects && !lightResult.intersects) { - return null; - } else if (entityResult.intersects && !lightResult.intersects) { + if (iconResult.intersects) { + result = iconResult; + } else if (entityResult.intersects) { result = entityResult; - } else if (!entityResult.intersects && lightResult.intersects) { - result = lightResult; } else { - if (entityResult.distance < lightResult.distance) { - result = entityResult; - } else { - result = lightResult; - } + return null; } if (!result.accurate) { @@ -945,18 +991,18 @@ function setupModelMenus() { }); Menu.addMenuItem({ menuName: "Edit", - menuItemName: MENU_SHOW_LIGHTS_IN_EDIT_MODE, + menuItemName: MENU_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE, afterItem: MENU_EASE_ON_FOCUS, isCheckable: true, - isChecked: Settings.getValue(SETTING_SHOW_LIGHTS_IN_EDIT_MODE) === "true", + isChecked: Settings.getValue(SETTING_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE) !== "false", grouping: "Advanced" }); Menu.addMenuItem({ menuName: "Edit", menuItemName: MENU_SHOW_ZONES_IN_EDIT_MODE, - afterItem: MENU_SHOW_LIGHTS_IN_EDIT_MODE, + afterItem: MENU_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE, isCheckable: true, - isChecked: Settings.getValue(SETTING_SHOW_ZONES_IN_EDIT_MODE) === "true", + isChecked: Settings.getValue(SETTING_SHOW_ZONES_IN_EDIT_MODE) !== "false", grouping: "Advanced" }); @@ -987,7 +1033,7 @@ function cleanupModelMenus() { Menu.removeMenuItem("Edit", MENU_AUTO_FOCUS_ON_SELECT); Menu.removeMenuItem("Edit", MENU_EASE_ON_FOCUS); - Menu.removeMenuItem("Edit", MENU_SHOW_LIGHTS_IN_EDIT_MODE); + Menu.removeMenuItem("Edit", MENU_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE); Menu.removeMenuItem("Edit", MENU_SHOW_ZONES_IN_EDIT_MODE); } @@ -995,7 +1041,7 @@ Script.scriptEnding.connect(function () { toolBar.setActive(false); Settings.setValue(SETTING_AUTO_FOCUS_ON_SELECT, Menu.isOptionChecked(MENU_AUTO_FOCUS_ON_SELECT)); Settings.setValue(SETTING_EASE_ON_FOCUS, Menu.isOptionChecked(MENU_EASE_ON_FOCUS)); - Settings.setValue(SETTING_SHOW_LIGHTS_IN_EDIT_MODE, Menu.isOptionChecked(MENU_SHOW_LIGHTS_IN_EDIT_MODE)); + Settings.setValue(SETTING_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE, Menu.isOptionChecked(MENU_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE)); Settings.setValue(SETTING_SHOW_ZONES_IN_EDIT_MODE, Menu.isOptionChecked(MENU_SHOW_ZONES_IN_EDIT_MODE)); progressDialog.cleanup(); @@ -1184,7 +1230,7 @@ function parentSelectedEntities() { } function deleteSelectedEntities() { if (SelectionManager.hasSelection()) { - selectedParticleEntity = 0; + selectedParticleEntityID = null; particleExplorerTool.destroyWebView(); SelectionManager.saveProperties(); var savedProperties = []; @@ -1283,8 +1329,8 @@ function handeMenuEvent(menuItem) { selectAllEtitiesInCurrentSelectionBox(false); } else if (menuItem === "Select All Entities Touching Box") { selectAllEtitiesInCurrentSelectionBox(true); - } else if (menuItem === MENU_SHOW_LIGHTS_IN_EDIT_MODE) { - lightOverlayManager.setVisible(isActive && Menu.isOptionChecked(MENU_SHOW_LIGHTS_IN_EDIT_MODE)); + } else if (menuItem === MENU_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE) { + entityIconOverlayManager.setVisible(isActive && Menu.isOptionChecked(MENU_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE)); } else if (menuItem === MENU_SHOW_ZONES_IN_EDIT_MODE) { Entities.setDrawZoneBoundaries(isActive && Menu.isOptionChecked(MENU_SHOW_ZONES_IN_EDIT_MODE)); } @@ -1959,43 +2005,13 @@ var showMenuItem = propertyMenu.addMenuItem("Show in Marketplace"); var propertiesTool = new PropertiesTool(); var particleExplorerTool = new ParticleExplorerTool(); -var selectedParticleEntity = 0; +var selectedParticleEntityID = null; entityListTool.webView.webEventReceived.connect(function (data) { data = JSON.parse(data); - if(data.type === 'parent') { + if (data.type === 'parent') { parentSelectedEntities(); } else if(data.type === 'unparent') { unparentSelectedEntities(); - } else if (data.type === "selectionUpdate") { - var ids = data.entityIds; - if (ids.length === 1) { - if (Entities.getEntityProperties(ids[0], "type").type === "ParticleEffect") { - if (JSON.stringify(selectedParticleEntity) === JSON.stringify(ids[0])) { - // This particle entity is already selected, so return - return; - } - // Destroy the old particles web view first - particleExplorerTool.destroyWebView(); - particleExplorerTool.createWebView(); - var properties = Entities.getEntityProperties(ids[0]); - var particleData = { - messageType: "particle_settings", - currentProperties: properties - }; - selectedParticleEntity = ids[0]; - particleExplorerTool.setActiveParticleEntity(ids[0]); - - particleExplorerTool.webView.webEventReceived.connect(function (data) { - data = JSON.parse(data); - if (data.messageType === "page_loaded") { - particleExplorerTool.webView.emitScriptEvent(JSON.stringify(particleData)); - } - }); - } else { - selectedParticleEntity = 0; - particleExplorerTool.destroyWebView(); - } - } } }); diff --git a/scripts/system/libraries/lightOverlayManager.js b/scripts/system/libraries/entityIconOverlayManager.js similarity index 67% rename from scripts/system/libraries/lightOverlayManager.js rename to scripts/system/libraries/entityIconOverlayManager.js index 2d3618096b..7f7a293bc3 100644 --- a/scripts/system/libraries/lightOverlayManager.js +++ b/scripts/system/libraries/entityIconOverlayManager.js @@ -1,9 +1,6 @@ -var POINT_LIGHT_URL = "http://s3.amazonaws.com/hifi-public/images/tools/point-light.svg"; -var SPOT_LIGHT_URL = "http://s3.amazonaws.com/hifi-public/images/tools/spot-light.svg"; - -LightOverlayManager = function() { - var self = this; +/* globals EntityIconOverlayManager:true */ +EntityIconOverlayManager = function(entityTypes, getOverlayPropertiesFunc) { var visible = false; // List of all created overlays @@ -22,9 +19,16 @@ LightOverlayManager = function() { for (var id in entityIDs) { var entityID = entityIDs[id]; var properties = Entities.getEntityProperties(entityID); - Overlays.editOverlay(entityOverlays[entityID], { + var overlayProperties = { position: properties.position - }); + }; + if (getOverlayPropertiesFunc) { + var customProperties = getOverlayPropertiesFunc(entityID, properties); + for (var key in customProperties) { + overlayProperties[key] = customProperties[key]; + } + } + Overlays.editOverlay(entityOverlays[entityID], overlayProperties); } }; @@ -34,7 +38,7 @@ LightOverlayManager = function() { if (result.intersects) { for (var id in entityOverlays) { - if (result.overlayID == entityOverlays[id]) { + if (result.overlayID === entityOverlays[id]) { result.entityID = entityIDs[id]; found = true; break; @@ -50,7 +54,7 @@ LightOverlayManager = function() { }; this.setVisible = function(isVisible) { - if (visible != isVisible) { + if (visible !== isVisible) { visible = isVisible; for (var id in entityOverlays) { Overlays.editOverlay(entityOverlays[id], { @@ -62,12 +66,13 @@ LightOverlayManager = function() { // Allocate or get an unused overlay function getOverlay() { - if (unusedOverlays.length == 0) { - var overlay = Overlays.addOverlay("image3d", {}); + var overlay; + if (unusedOverlays.length === 0) { + overlay = Overlays.addOverlay("image3d", {}); allOverlays.push(overlay); } else { - var overlay = unusedOverlays.pop(); - }; + overlay = unusedOverlays.pop(); + } return overlay; } @@ -79,24 +84,32 @@ LightOverlayManager = function() { } function addEntity(entityID) { - var properties = Entities.getEntityProperties(entityID); - if (properties.type == "Light" && !(entityID in entityOverlays)) { + var properties = Entities.getEntityProperties(entityID, ['position', 'type']); + if (entityTypes.indexOf(properties.type) > -1 && !(entityID in entityOverlays)) { var overlay = getOverlay(); entityOverlays[entityID] = overlay; entityIDs[entityID] = entityID; - Overlays.editOverlay(overlay, { + var overlayProperties = { position: properties.position, - url: properties.isSpotlight ? SPOT_LIGHT_URL : POINT_LIGHT_URL, rotation: Quat.fromPitchYawRollDegrees(0, 0, 270), visible: visible, alpha: 0.9, scale: 0.5, + drawInFront: true, + isFacingAvatar: true, color: { red: 255, green: 255, blue: 255 } - }); + }; + if (getOverlayPropertiesFunc) { + var customProperties = getOverlayPropertiesFunc(entityID, properties); + for (var key in customProperties) { + overlayProperties[key] = customProperties[key]; + } + } + Overlays.editOverlay(overlay, overlayProperties); } } @@ -130,4 +143,4 @@ LightOverlayManager = function() { Overlays.deleteOverlay(allOverlays[i]); } }); -}; \ No newline at end of file +}; diff --git a/scripts/system/libraries/entitySelectionTool.js b/scripts/system/libraries/entitySelectionTool.js index d68a525458..a8c4300fbe 100644 --- a/scripts/system/libraries/entitySelectionTool.js +++ b/scripts/system/libraries/entitySelectionTool.js @@ -1032,10 +1032,12 @@ SelectionDisplay = (function() { var pickRay = controllerComputePickRay(); if (pickRay) { var entityIntersection = Entities.findRayIntersection(pickRay, true); - - + var iconIntersection = entityIconOverlayManager.findRayIntersection(pickRay); var overlayIntersection = Overlays.findRayIntersection(pickRay); - if (entityIntersection.intersects && + + if (iconIntersection.intersects) { + selectionManager.setSelections([iconIntersection.entityID]); + } else if (entityIntersection.intersects && (!overlayIntersection.intersects || (entityIntersection.distance < overlayIntersection.distance))) { if (HMD.tabletID === entityIntersection.entityID) { diff --git a/scripts/system/libraries/toolBars.js b/scripts/system/libraries/toolBars.js index e49f8c4004..351f10e7bd 100644 --- a/scripts/system/libraries/toolBars.js +++ b/scripts/system/libraries/toolBars.js @@ -160,6 +160,7 @@ ToolBar = function(x, y, direction, optionalPersistenceKey, optionalInitialPosit visible: false }); this.spacing = []; + this.onMove = null; this.addTool = function(properties, selectable, selected) { if (direction == ToolBar.HORIZONTAL) { @@ -254,6 +255,9 @@ ToolBar = function(x, y, direction, optionalPersistenceKey, optionalInitialPosit y: y - ToolBar.SPACING }); } + if (this.onMove !== null) { + this.onMove(x, y, dx, dy); + }; } this.setAlpha = function(alpha, tool) { diff --git a/scripts/tutorials/entity_scripts/sit.js b/scripts/tutorials/entity_scripts/sit.js index 2ba19231e0..82afdc8974 100644 --- a/scripts/tutorials/entity_scripts/sit.js +++ b/scripts/tutorials/entity_scripts/sit.js @@ -2,31 +2,41 @@ Script.include("/~/system/libraries/utils.js"); var SETTING_KEY = "com.highfidelity.avatar.isSitting"; - var ROLE = "fly"; var ANIMATION_URL = "https://s3-us-west-1.amazonaws.com/hifi-content/clement/production/animations/sitting_idle.fbx"; var ANIMATION_FPS = 30; var ANIMATION_FIRST_FRAME = 1; var ANIMATION_LAST_FRAME = 10; - var RELEASE_KEYS = ['w', 'a', 's', 'd', 'UP', 'LEFT', 'DOWN', 'RIGHT']; var RELEASE_TIME = 500; // ms var RELEASE_DISTANCE = 0.2; // meters - var MAX_IK_ERROR = 20; - var DESKTOP_UI_CHECK_INTERVAL = 250; + var MAX_IK_ERROR = 30; + var IK_SETTLE_TIME = 250; // ms + var DESKTOP_UI_CHECK_INTERVAL = 100; var DESKTOP_MAX_DISTANCE = 5; - var SIT_DELAY = 25 + var SIT_DELAY = 25; + var MAX_RESET_DISTANCE = 0.5; // meters + var OVERRIDEN_DRIVE_KEYS = [ + DriveKeys.TRANSLATE_X, + DriveKeys.TRANSLATE_Y, + DriveKeys.TRANSLATE_Z, + DriveKeys.STEP_TRANSLATE_X, + DriveKeys.STEP_TRANSLATE_Y, + DriveKeys.STEP_TRANSLATE_Z, + ]; this.entityID = null; - this.timers = {}; this.animStateHandlerID = null; + this.interval = null; + this.sitDownSettlePeriod = null; + this.lastTimeNoDriveKeys = null; this.preload = function(entityID) { this.entityID = entityID; } this.unload = function() { - if (MyAvatar.sessionUUID === this.getSeatUser()) { - this.sitUp(this.entityID); + if (Settings.getValue(SETTING_KEY) === this.entityID) { + this.standUp(); } - if (this.interval) { + if (this.interval !== null) { Script.clearInterval(this.interval); this.interval = null; } @@ -34,42 +44,60 @@ } this.setSeatUser = function(user) { - var userData = Entities.getEntityProperties(this.entityID, ["userData"]).userData; - userData = JSON.parse(userData); + try { + var userData = Entities.getEntityProperties(this.entityID, ["userData"]).userData; + userData = JSON.parse(userData); - if (user) { - userData.seat.user = user; - } else { - delete userData.seat.user; + if (user !== null) { + userData.seat.user = user; + } else { + delete userData.seat.user; + } + + Entities.editEntity(this.entityID, { + userData: JSON.stringify(userData) + }); + } catch (e) { + // Do Nothing } - - Entities.editEntity(this.entityID, { - userData: JSON.stringify(userData) - }); } this.getSeatUser = function() { - var properties = Entities.getEntityProperties(this.entityID, ["userData", "position"]); - var userData = JSON.parse(properties.userData); + try { + var properties = Entities.getEntityProperties(this.entityID, ["userData", "position"]); + var userData = JSON.parse(properties.userData); - if (userData.seat.user && userData.seat.user !== MyAvatar.sessionUUID) { - var avatar = AvatarList.getAvatar(userData.seat.user); - if (avatar && Vec3.distance(avatar.position, properties.position) > RELEASE_DISTANCE) { - return null; + // If MyAvatar return my uuid + if (userData.seat.user === MyAvatar.sessionUUID) { + return userData.seat.user; } + + + // If Avatar appears to be sitting + if (userData.seat.user) { + var avatar = AvatarList.getAvatar(userData.seat.user); + if (avatar && (Vec3.distance(avatar.position, properties.position) < RELEASE_DISTANCE)) { + return userData.seat.user; + } + } + } catch (e) { + // Do nothing } - return userData.seat.user; + + // Nobody on the seat + return null; } + // Is the seat used this.checkSeatForAvatar = function() { var seatUser = this.getSeatUser(); - var avatarIdentifiers = AvatarList.getAvatarIdentifiers(); - for (var i in avatarIdentifiers) { - var avatar = AvatarList.getAvatar(avatarIdentifiers[i]); - if (avatar && avatar.sessionUUID === seatUser) { - return true; - } + + // If MyAvatar appears to be sitting + if (seatUser === MyAvatar.sessionUUID) { + var properties = Entities.getEntityProperties(this.entityID, ["position"]); + return Vec3.distance(MyAvatar.position, properties.position) < RELEASE_DISTANCE; } - return false; + + return seatUser !== null; } this.sitDown = function() { @@ -77,41 +105,53 @@ print("Someone is already sitting in that chair."); return; } + print("Sitting down (" + this.entityID + ")"); - this.setSeatUser(MyAvatar.sessionUUID); + var now = Date.now(); + this.sitDownSettlePeriod = now + IK_SETTLE_TIME; + this.lastTimeNoDriveKeys = now; var previousValue = Settings.getValue(SETTING_KEY); Settings.setValue(SETTING_KEY, this.entityID); + this.setSeatUser(MyAvatar.sessionUUID); if (previousValue === "") { MyAvatar.characterControllerEnabled = false; MyAvatar.hmdLeanRecenterEnabled = false; - MyAvatar.overrideRoleAnimation(ROLE, ANIMATION_URL, ANIMATION_FPS, true, ANIMATION_FIRST_FRAME, ANIMATION_LAST_FRAME); + var ROLES = MyAvatar.getAnimationRoles(); + for (i in ROLES) { + MyAvatar.overrideRoleAnimation(ROLES[i], ANIMATION_URL, ANIMATION_FPS, true, ANIMATION_FIRST_FRAME, ANIMATION_LAST_FRAME); + } MyAvatar.resetSensorsAndBody(); } - var that = this; - Script.setTimeout(function() { - var properties = Entities.getEntityProperties(that.entityID, ["position", "rotation"]); - var index = MyAvatar.getJointIndex("Hips"); - MyAvatar.pinJoint(index, properties.position, properties.rotation); + var properties = Entities.getEntityProperties(this.entityID, ["position", "rotation"]); + var index = MyAvatar.getJointIndex("Hips"); + MyAvatar.pinJoint(index, properties.position, properties.rotation); - that.animStateHandlerID = MyAvatar.addAnimationStateHandler(function(properties) { - return { headType: 0 }; - }, ["headType"]); - Script.update.connect(that, that.update); - Controller.keyPressEvent.connect(that, that.keyPressed); - Controller.keyReleaseEvent.connect(that, that.keyReleased); - for (var i in RELEASE_KEYS) { - Controller.captureKeyEvents({ text: RELEASE_KEYS[i] }); - } - }, SIT_DELAY); + this.animStateHandlerID = MyAvatar.addAnimationStateHandler(function(properties) { + return { headType: 0 }; + }, ["headType"]); + Script.update.connect(this, this.update); + for (var i in OVERRIDEN_DRIVE_KEYS) { + MyAvatar.disableDriveKey(OVERRIDEN_DRIVE_KEYS[i]); + } } - this.sitUp = function() { - this.setSeatUser(null); + this.standUp = function() { + print("Standing up (" + this.entityID + ")"); + MyAvatar.removeAnimationStateHandler(this.animStateHandlerID); + Script.update.disconnect(this, this.update); + for (var i in OVERRIDEN_DRIVE_KEYS) { + MyAvatar.enableDriveKey(OVERRIDEN_DRIVE_KEYS[i]); + } + this.setSeatUser(null); if (Settings.getValue(SETTING_KEY) === this.entityID) { - MyAvatar.restoreRoleAnimation(ROLE); + Settings.setValue(SETTING_KEY, ""); + var ROLES = MyAvatar.getAnimationRoles(); + for (i in ROLES) { + MyAvatar.restoreRoleAnimation(ROLES[i]); + } MyAvatar.characterControllerEnabled = true; MyAvatar.hmdLeanRecenterEnabled = true; @@ -124,19 +164,10 @@ MyAvatar.bodyPitch = 0.0; MyAvatar.bodyRoll = 0.0; }, SIT_DELAY); - - Settings.setValue(SETTING_KEY, ""); - } - - MyAvatar.removeAnimationStateHandler(this.animStateHandlerID); - Script.update.disconnect(this, this.update); - Controller.keyPressEvent.disconnect(this, this.keyPressed); - Controller.keyReleaseEvent.disconnect(this, this.keyReleased); - for (var i in RELEASE_KEYS) { - Controller.releaseKeyEvents({ text: RELEASE_KEYS[i] }); } } + // function called by teleport.js if it detects the appropriate userData this.sit = function () { this.sitDown(); } @@ -183,39 +214,52 @@ } } - this.update = function(dt) { if (MyAvatar.sessionUUID === this.getSeatUser()) { - var properties = Entities.getEntityProperties(this.entityID, ["position"]); + var properties = Entities.getEntityProperties(this.entityID); var avatarDistance = Vec3.distance(MyAvatar.position, properties.position); var ikError = MyAvatar.getIKErrorOnLastSolve(); - if (avatarDistance > RELEASE_DISTANCE || ikError > MAX_IK_ERROR) { + var now = Date.now(); + var shouldStandUp = false; + + // Check if a drive key is pressed + var hasActiveDriveKey = false; + for (var i in OVERRIDEN_DRIVE_KEYS) { + if (MyAvatar.getRawDriveKey(OVERRIDEN_DRIVE_KEYS[i]) != 0.0) { + hasActiveDriveKey = true; + break; + } + } + + // Only standup if user has been pushing a drive key for RELEASE_TIME + if (hasActiveDriveKey) { + var elapsed = now - this.lastTimeNoDriveKeys; + shouldStandUp = elapsed > RELEASE_TIME; + } else { + this.lastTimeNoDriveKeys = Date.now(); + } + + // Allow some time for the IK to settle + if (ikError > MAX_IK_ERROR && now > this.sitDownSettlePeriod) { + shouldStandUp = true; + } + + + if (shouldStandUp || avatarDistance > RELEASE_DISTANCE) { print("IK error: " + ikError + ", distance from chair: " + avatarDistance); - this.sitUp(this.entityID); + + // Move avatar in front of the chair to avoid getting stuck in collision hulls + if (avatarDistance < MAX_RESET_DISTANCE) { + var offset = { x: 0, y: 1.0, z: -0.5 - properties.dimensions.z * properties.registrationPoint.z }; + var position = Vec3.sum(properties.position, Vec3.multiplyQbyV(properties.rotation, offset)); + MyAvatar.position = position; + print("Moving Avatar in front of the chair.") + } + + this.standUp(); } } } - this.keyPressed = function(event) { - if (isInEditMode()) { - return; - } - - if (RELEASE_KEYS.indexOf(event.text) !== -1) { - var that = this; - this.timers[event.text] = Script.setTimeout(function() { - that.sitUp(); - }, RELEASE_TIME); - } - } - this.keyReleased = function(event) { - if (RELEASE_KEYS.indexOf(event.text) !== -1) { - if (this.timers[event.text]) { - Script.clearTimeout(this.timers[event.text]); - delete this.timers[event.text]; - } - } - } - this.canSitDesktop = function() { var properties = Entities.getEntityProperties(this.entityID, ["position"]); var distanceFromSeat = Vec3.distance(MyAvatar.position, properties.position); @@ -223,7 +267,7 @@ } this.hoverEnterEntity = function(event) { - if (isInEditMode() || (MyAvatar.sessionUUID === this.getSeatUser())) { + if (isInEditMode() || this.interval !== null) { return; } @@ -239,18 +283,18 @@ }, DESKTOP_UI_CHECK_INTERVAL); } this.hoverLeaveEntity = function(event) { - if (this.interval) { + if (this.interval !== null) { Script.clearInterval(this.interval); this.interval = null; } this.cleanupOverlay(); } - this.clickDownOnEntity = function () { - if (isInEditMode() || (MyAvatar.sessionUUID === this.getSeatUser())) { + this.clickDownOnEntity = function (id, event) { + if (isInEditMode()) { return; } - if (this.canSitDesktop()) { + if (event.isPrimaryButton && this.canSitDesktop()) { this.sitDown(); } } diff --git a/tests/ktx/CMakeLists.txt b/tests/ktx/CMakeLists.txt new file mode 100644 index 0000000000..d72379efd6 --- /dev/null +++ b/tests/ktx/CMakeLists.txt @@ -0,0 +1,15 @@ + +set(TARGET_NAME ktx-test) + +if (WIN32) + SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /ignore:4049 /ignore:4217") +endif() + +# This is not a testcase -- just set it up as a regular hifi project +setup_hifi_project(Quick Gui OpenGL) +set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/") + +# link in the shared libraries +link_hifi_libraries(shared octree ktx gl gpu gpu-gl render model model-networking networking render-utils fbx entities entities-renderer animation audio avatars script-engine physics) + +package_libraries_for_deployment() diff --git a/tests/ktx/src/main.cpp b/tests/ktx/src/main.cpp new file mode 100644 index 0000000000..aa6795e17b --- /dev/null +++ b/tests/ktx/src/main.cpp @@ -0,0 +1,150 @@ +// +// Created by Bradley Austin Davis on 2016/07/01 +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + + +#include +#include +#include +#include + + +QSharedPointer logger; + +gpu::Texture* cacheTexture(const std::string& name, gpu::Texture* srcTexture, bool write = true, bool read = true); + + +void messageHandler(QtMsgType type, const QMessageLogContext& context, const QString& message) { + QString logMessage = LogHandler::getInstance().printMessage((LogMsgType)type, context, message); + + if (!logMessage.isEmpty()) { +#ifdef Q_OS_WIN + OutputDebugStringA(logMessage.toLocal8Bit().constData()); + OutputDebugStringA("\n"); +#endif + logger->addMessage(qPrintable(logMessage + "\n")); + } +} + +const char * LOG_FILTER_RULES = R"V0G0N( +hifi.gpu=true +)V0G0N"; + +QString getRootPath() { + static std::once_flag once; + static QString result; + std::call_once(once, [&] { + QFileInfo file(__FILE__); + QDir parent = file.absolutePath(); + result = QDir::cleanPath(parent.currentPath() + "/../../.."); + }); + return result; +} + +const QString TEST_IMAGE = getRootPath() + "/scripts/developer/tests/cube_texture.png"; +const QString TEST_IMAGE_KTX = getRootPath() + "/scripts/developer/tests/cube_texture.ktx"; + +int main(int argc, char** argv) { + QApplication app(argc, argv); + QCoreApplication::setApplicationName("KTX"); + QCoreApplication::setOrganizationName("High Fidelity"); + QCoreApplication::setOrganizationDomain("highfidelity.com"); + logger.reset(new FileLogger()); + + Q_ASSERT(sizeof(ktx::Header) == 12 + (sizeof(uint32_t) * 13)); + + DependencyManager::set(); + qInstallMessageHandler(messageHandler); + QLoggingCategory::setFilterRules(LOG_FILTER_RULES); + + QImage image(TEST_IMAGE); + gpu::Texture* testTexture = model::TextureUsage::process2DTextureColorFromImage(image, TEST_IMAGE.toStdString(), true, false, true); + + auto ktxMemory = gpu::Texture::serialize(*testTexture); + { + const auto& ktxStorage = ktxMemory->getStorage(); + QFile outFile(TEST_IMAGE_KTX); + if (!outFile.open(QFile::Truncate | QFile::ReadWrite)) { + throw std::runtime_error("Unable to open file"); + } + auto ktxSize = ktxStorage->size(); + outFile.resize(ktxSize); + auto dest = outFile.map(0, ktxSize); + memcpy(dest, ktxStorage->data(), ktxSize); + outFile.unmap(dest); + outFile.close(); + } + + auto ktxFile = ktx::KTX::create(std::shared_ptr(new storage::FileStorage(TEST_IMAGE_KTX))); + { + const auto& memStorage = ktxMemory->getStorage(); + const auto& fileStorage = ktxFile->getStorage(); + Q_ASSERT(memStorage->size() == fileStorage->size()); + Q_ASSERT(memStorage->data() != fileStorage->data()); + Q_ASSERT(0 == memcmp(memStorage->data(), fileStorage->data(), memStorage->size())); + Q_ASSERT(ktxFile->_images.size() == ktxMemory->_images.size()); + auto imageCount = ktxFile->_images.size(); + auto startMemory = ktxMemory->_storage->data(); + auto startFile = ktxFile->_storage->data(); + for (size_t i = 0; i < imageCount; ++i) { + auto memImages = ktxMemory->_images[i]; + auto fileImages = ktxFile->_images[i]; + Q_ASSERT(memImages._padding == fileImages._padding); + Q_ASSERT(memImages._numFaces == fileImages._numFaces); + Q_ASSERT(memImages._imageSize == fileImages._imageSize); + Q_ASSERT(memImages._faceSize == fileImages._faceSize); + Q_ASSERT(memImages._faceBytes.size() == memImages._numFaces); + Q_ASSERT(fileImages._faceBytes.size() == fileImages._numFaces); + auto faceCount = fileImages._numFaces; + for (uint32_t face = 0; face < faceCount; ++face) { + auto memFace = memImages._faceBytes[face]; + auto memOffset = memFace - startMemory; + auto fileFace = fileImages._faceBytes[face]; + auto fileOffset = fileFace - startFile; + Q_ASSERT(memOffset % 4 == 0); + Q_ASSERT(memOffset == fileOffset); + } + } + } + testTexture->setKtxBacking(ktxFile); + return 0; +} + +#include "main.moc" + diff --git a/tests/render-perf/CMakeLists.txt b/tests/render-perf/CMakeLists.txt index d4f90fdace..96cede9c43 100644 --- a/tests/render-perf/CMakeLists.txt +++ b/tests/render-perf/CMakeLists.txt @@ -10,7 +10,7 @@ setup_hifi_project(Quick Gui OpenGL) set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/") # link in the shared libraries -link_hifi_libraries(shared octree gl gpu gpu-gl render model model-networking networking render-utils fbx entities entities-renderer animation audio avatars script-engine physics) +link_hifi_libraries(shared octree ktx gl gpu gpu-gl render model model-networking networking render-utils fbx entities entities-renderer animation audio avatars script-engine physics) package_libraries_for_deployment() diff --git a/tests/render-perf/src/main.cpp b/tests/render-perf/src/main.cpp index 7e9d2c426f..522fe79b10 100644 --- a/tests/render-perf/src/main.cpp +++ b/tests/render-perf/src/main.cpp @@ -642,7 +642,6 @@ protected: gpu::Texture::setAllowedGPUMemoryUsage(MB_TO_BYTES(64)); return; - default: break; } diff --git a/tests/render-texture-load/src/main.cpp b/tests/render-texture-load/src/main.cpp index 09a420f018..d924f76232 100644 --- a/tests/render-texture-load/src/main.cpp +++ b/tests/render-texture-load/src/main.cpp @@ -48,6 +48,7 @@ #include #include +#include #include #include #include diff --git a/tests/shared/src/StorageTests.cpp b/tests/shared/src/StorageTests.cpp new file mode 100644 index 0000000000..fa538f6911 --- /dev/null +++ b/tests/shared/src/StorageTests.cpp @@ -0,0 +1,75 @@ +// +// Created by Bradley Austin Davis on 2016/02/17 +// Copyright 2013-2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "StorageTests.h" + +QTEST_MAIN(StorageTests) + +using namespace storage; + +StorageTests::StorageTests() { + for (size_t i = 0; i < _testData.size(); ++i) { + _testData[i] = (uint8_t)rand(); + } + _testFile = QDir::tempPath() + "/" + QUuid::createUuid().toString(); +} + +StorageTests::~StorageTests() { + QFileInfo fileInfo(_testFile); + if (fileInfo.exists()) { + QFile(_testFile).remove(); + } +} + + +void StorageTests::testConversion() { + { + QFileInfo fileInfo(_testFile); + QCOMPARE(fileInfo.exists(), false); + } + StoragePointer storagePointer = std::make_unique(_testData.size(), _testData.data()); + QCOMPARE(storagePointer->size(), (quint64)_testData.size()); + QCOMPARE(memcmp(_testData.data(), storagePointer->data(), _testData.size()), 0); + // Convert to a file + storagePointer = storagePointer->toFileStorage(_testFile); + { + QFileInfo fileInfo(_testFile); + QCOMPARE(fileInfo.exists(), true); + QCOMPARE(fileInfo.size(), (qint64)_testData.size()); + } + QCOMPARE(storagePointer->size(), (quint64)_testData.size()); + QCOMPARE(memcmp(_testData.data(), storagePointer->data(), _testData.size()), 0); + + // Convert to memory + storagePointer = storagePointer->toMemoryStorage(); + QCOMPARE(storagePointer->size(), (quint64)_testData.size()); + QCOMPARE(memcmp(_testData.data(), storagePointer->data(), _testData.size()), 0); + { + // ensure the file is unaffected + QFileInfo fileInfo(_testFile); + QCOMPARE(fileInfo.exists(), true); + QCOMPARE(fileInfo.size(), (qint64)_testData.size()); + } + + // truncate the data as a new memory object + auto newSize = _testData.size() / 2; + storagePointer = std::make_unique(newSize, storagePointer->data()); + QCOMPARE(storagePointer->size(), (quint64)newSize); + QCOMPARE(memcmp(_testData.data(), storagePointer->data(), newSize), 0); + + // Convert back to file + storagePointer = storagePointer->toFileStorage(_testFile); + QCOMPARE(storagePointer->size(), (quint64)newSize); + QCOMPARE(memcmp(_testData.data(), storagePointer->data(), newSize), 0); + { + // ensure the file is truncated + QFileInfo fileInfo(_testFile); + QCOMPARE(fileInfo.exists(), true); + QCOMPARE(fileInfo.size(), (qint64)newSize); + } +} diff --git a/tests/shared/src/StorageTests.h b/tests/shared/src/StorageTests.h new file mode 100644 index 0000000000..6a2c153223 --- /dev/null +++ b/tests/shared/src/StorageTests.h @@ -0,0 +1,32 @@ +// +// Created by Bradley Austin Davis on 2016/02/17 +// Copyright 2013-2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_StorageTests_h +#define hifi_StorageTests_h + +#include + +#include +#include + +class StorageTests : public QObject { + Q_OBJECT + +public: + StorageTests(); + ~StorageTests(); + +private slots: + void testConversion(); + +private: + std::array _testData; + QString _testFile; +}; + +#endif // hifi_StorageTests_h diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index a85a112bf5..8dc993e6fe 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -17,3 +17,5 @@ set_target_properties(ac-client PROPERTIES FOLDER "Tools") add_subdirectory(skeleton-dump) set_target_properties(skeleton-dump PROPERTIES FOLDER "Tools") +add_subdirectory(atp-get) +set_target_properties(atp-get PROPERTIES FOLDER "Tools") diff --git a/tools/atp-get/CMakeLists.txt b/tools/atp-get/CMakeLists.txt new file mode 100644 index 0000000000..b1646dc023 --- /dev/null +++ b/tools/atp-get/CMakeLists.txt @@ -0,0 +1,3 @@ +set(TARGET_NAME atp-get) +setup_hifi_project(Core Widgets) +link_hifi_libraries(shared networking) diff --git a/tools/atp-get/src/ATPGetApp.cpp b/tools/atp-get/src/ATPGetApp.cpp new file mode 100644 index 0000000000..30054fffea --- /dev/null +++ b/tools/atp-get/src/ATPGetApp.cpp @@ -0,0 +1,269 @@ +// +// ATPGetApp.cpp +// tools/atp-get/src +// +// Created by Seth Alves on 2017-3-15 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ATPGetApp.h" + +ATPGetApp::ATPGetApp(int argc, char* argv[]) : + QCoreApplication(argc, argv) +{ + // parse command-line + QCommandLineParser parser; + parser.setApplicationDescription("High Fidelity ATP-Get"); + + const QCommandLineOption helpOption = parser.addHelpOption(); + + const QCommandLineOption verboseOutput("v", "verbose output"); + parser.addOption(verboseOutput); + + const QCommandLineOption domainAddressOption("d", "domain-server address", "127.0.0.1"); + parser.addOption(domainAddressOption); + + const QCommandLineOption cacheSTUNOption("s", "cache stun-server response"); + parser.addOption(cacheSTUNOption); + + const QCommandLineOption listenPortOption("listenPort", "listen port", QString::number(INVALID_PORT)); + parser.addOption(listenPortOption); + + + if (!parser.parse(QCoreApplication::arguments())) { + qCritical() << parser.errorText() << endl; + parser.showHelp(); + Q_UNREACHABLE(); + } + + if (parser.isSet(helpOption)) { + parser.showHelp(); + Q_UNREACHABLE(); + } + + _verbose = parser.isSet(verboseOutput); + if (!_verbose) { + QLoggingCategory::setFilterRules("qt.network.ssl.warning=false"); + + const_cast(&networking())->setEnabled(QtDebugMsg, false); + const_cast(&networking())->setEnabled(QtInfoMsg, false); + const_cast(&networking())->setEnabled(QtWarningMsg, false); + + const_cast(&shared())->setEnabled(QtDebugMsg, false); + const_cast(&shared())->setEnabled(QtInfoMsg, false); + const_cast(&shared())->setEnabled(QtWarningMsg, false); + } + + + QStringList filenames = parser.positionalArguments(); + if (filenames.empty() || filenames.size() > 2) { + qDebug() << "give remote url and optional local filename as arguments"; + parser.showHelp(); + Q_UNREACHABLE(); + } + + _url = QUrl(filenames[0]); + if (_url.scheme() != "atp") { + qDebug() << "url should start with atp:"; + parser.showHelp(); + Q_UNREACHABLE(); + } + + if (filenames.size() == 2) { + _localOutputFile = filenames[1]; + } + + QString domainServerAddress = "127.0.0.1:40103"; + if (parser.isSet(domainAddressOption)) { + domainServerAddress = parser.value(domainAddressOption); + } + + if (_verbose) { + qDebug() << "domain-server address is" << domainServerAddress; + } + + int listenPort = INVALID_PORT; + if (parser.isSet(listenPortOption)) { + listenPort = parser.value(listenPortOption).toInt(); + } + + Setting::init(); + DependencyManager::registerInheritance(); + + DependencyManager::set([&]{ return QString("Mozilla/5.0 (HighFidelityATPGet)"); }); + DependencyManager::set(); + DependencyManager::set(NodeType::Agent, listenPort); + + + auto nodeList = DependencyManager::get(); + + // start the nodeThread so its event loop is running + QThread* nodeThread = new QThread(this); + nodeThread->setObjectName("NodeList Thread"); + nodeThread->start(); + + // make sure the node thread is given highest priority + nodeThread->setPriority(QThread::TimeCriticalPriority); + + // setup a timer for domain-server check ins + QTimer* domainCheckInTimer = new QTimer(nodeList.data()); + connect(domainCheckInTimer, &QTimer::timeout, nodeList.data(), &NodeList::sendDomainServerCheckIn); + domainCheckInTimer->start(DOMAIN_SERVER_CHECK_IN_MSECS); + + // put the NodeList and datagram processing on the node thread + nodeList->moveToThread(nodeThread); + + const DomainHandler& domainHandler = nodeList->getDomainHandler(); + + connect(&domainHandler, SIGNAL(hostnameChanged(const QString&)), SLOT(domainChanged(const QString&))); + // connect(&domainHandler, SIGNAL(resetting()), SLOT(resettingDomain())); + // connect(&domainHandler, SIGNAL(disconnectedFromDomain()), SLOT(clearDomainOctreeDetails())); + connect(&domainHandler, &DomainHandler::domainConnectionRefused, this, &ATPGetApp::domainConnectionRefused); + + connect(nodeList.data(), &NodeList::nodeAdded, this, &ATPGetApp::nodeAdded); + connect(nodeList.data(), &NodeList::nodeKilled, this, &ATPGetApp::nodeKilled); + connect(nodeList.data(), &NodeList::nodeActivated, this, &ATPGetApp::nodeActivated); + // connect(nodeList.data(), &NodeList::uuidChanged, getMyAvatar(), &MyAvatar::setSessionUUID); + // connect(nodeList.data(), &NodeList::uuidChanged, this, &ATPGetApp::setSessionUUID); + connect(nodeList.data(), &NodeList::packetVersionMismatch, this, &ATPGetApp::notifyPacketVersionMismatch); + + nodeList->addSetOfNodeTypesToNodeInterestSet(NodeSet() << NodeType::AudioMixer << NodeType::AvatarMixer + << NodeType::EntityServer << NodeType::AssetServer << NodeType::MessagesMixer); + + DependencyManager::get()->handleLookupString(domainServerAddress, false); + + auto assetClient = DependencyManager::set(); + assetClient->init(); + + QTimer* doTimer = new QTimer(this); + doTimer->setSingleShot(true); + connect(doTimer, &QTimer::timeout, this, &ATPGetApp::timedOut); + doTimer->start(4000); +} + +ATPGetApp::~ATPGetApp() { +} + + +void ATPGetApp::domainConnectionRefused(const QString& reasonMessage, int reasonCodeInt, const QString& extraInfo) { + qDebug() << "domainConnectionRefused"; +} + +void ATPGetApp::domainChanged(const QString& domainHostname) { + if (_verbose) { + qDebug() << "domainChanged"; + } +} + +void ATPGetApp::nodeAdded(SharedNodePointer node) { + if (_verbose) { + qDebug() << "node added: " << node->getType(); + } +} + +void ATPGetApp::nodeActivated(SharedNodePointer node) { + if (node->getType() == NodeType::AssetServer) { + lookup(); + } +} + +void ATPGetApp::nodeKilled(SharedNodePointer node) { + qDebug() << "nodeKilled"; +} + +void ATPGetApp::timedOut() { + finish(1); +} + +void ATPGetApp::notifyPacketVersionMismatch() { + if (_verbose) { + qDebug() << "packet version mismatch"; + } + finish(1); +} + +void ATPGetApp::lookup() { + + auto path = _url.path(); + qDebug() << "path is " << path; + + auto request = DependencyManager::get()->createGetMappingRequest(path); + QObject::connect(request, &GetMappingRequest::finished, this, [=](GetMappingRequest* request) mutable { + auto result = request->getError(); + if (result == GetMappingRequest::NotFound) { + qDebug() << "not found"; + } else if (result == GetMappingRequest::NoError) { + qDebug() << "found, hash is " << request->getHash(); + download(request->getHash()); + } else { + qDebug() << "error -- " << request->getError() << " -- " << request->getErrorString(); + } + request->deleteLater(); + }); + request->start(); +} + +void ATPGetApp::download(AssetHash hash) { + auto assetClient = DependencyManager::get(); + auto assetRequest = new AssetRequest(hash); + + connect(assetRequest, &AssetRequest::finished, this, [this](AssetRequest* request) mutable { + Q_ASSERT(request->getState() == AssetRequest::Finished); + + if (request->getError() == AssetRequest::Error::NoError) { + QString data = QString::fromUtf8(request->getData()); + if (_localOutputFile == "") { + QTextStream cout(stdout); + cout << data; + } else { + QFile outputHandle(_localOutputFile); + if (outputHandle.open(QIODevice::ReadWrite)) { + QTextStream stream( &outputHandle ); + stream << data; + } else { + qDebug() << "couldn't open output file:" << _localOutputFile; + } + } + } + + request->deleteLater(); + finish(0); + }); + + assetRequest->start(); +} + +void ATPGetApp::finish(int exitCode) { + auto nodeList = DependencyManager::get(); + + // send the domain a disconnect packet, force stoppage of domain-server check-ins + nodeList->getDomainHandler().disconnect(); + nodeList->setIsShuttingDown(true); + + // tell the packet receiver we're shutting down, so it can drop packets + nodeList->getPacketReceiver().setShouldDropPackets(true); + + QThread* nodeThread = DependencyManager::get()->thread(); + // remove the NodeList from the DependencyManager + DependencyManager::destroy(); + // ask the node thread to quit and wait until it is done + nodeThread->quit(); + nodeThread->wait(); + + QCoreApplication::exit(exitCode); +} diff --git a/tools/atp-get/src/ATPGetApp.h b/tools/atp-get/src/ATPGetApp.h new file mode 100644 index 0000000000..5507d2aa62 --- /dev/null +++ b/tools/atp-get/src/ATPGetApp.h @@ -0,0 +1,52 @@ +// +// ATPGetApp.h +// tools/atp-get/src +// +// Created by Seth Alves on 2017-3-15 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + + +#ifndef hifi_ATPGetApp_h +#define hifi_ATPGetApp_h + +#include +#include +#include +#include +#include +#include +#include +#include + + +class ATPGetApp : public QCoreApplication { + Q_OBJECT +public: + ATPGetApp(int argc, char* argv[]); + ~ATPGetApp(); + +private slots: + void domainConnectionRefused(const QString& reasonMessage, int reasonCodeInt, const QString& extraInfo); + void domainChanged(const QString& domainHostname); + void nodeAdded(SharedNodePointer node); + void nodeActivated(SharedNodePointer node); + void nodeKilled(SharedNodePointer node); + void notifyPacketVersionMismatch(); + +private: + NodeList* _nodeList; + void timedOut(); + void lookup(); + void download(AssetHash hash); + void finish(int exitCode); + bool _verbose; + + QUrl _url; + QString _localOutputFile; +}; + +#endif // hifi_ATPGetApp_h diff --git a/tools/atp-get/src/main.cpp b/tools/atp-get/src/main.cpp new file mode 100644 index 0000000000..bddf30c666 --- /dev/null +++ b/tools/atp-get/src/main.cpp @@ -0,0 +1,31 @@ +// +// main.cpp +// tools/atp-get/src +// +// Created by Seth Alves on 2017-3-15 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +#include +#include +#include +#include + +#include + +#include "ATPGetApp.h" + +using namespace std; + +int main(int argc, char * argv[]) { + QCoreApplication::setApplicationName(BuildInfo::AC_CLIENT_SERVER_NAME); + QCoreApplication::setOrganizationName(BuildInfo::MODIFIED_ORGANIZATION); + QCoreApplication::setOrganizationDomain(BuildInfo::ORGANIZATION_DOMAIN); + QCoreApplication::setApplicationVersion(BuildInfo::VERSION); + + ATPGetApp app(argc, argv); + + return app.exec(); +} diff --git a/unpublishedScripts/marketplace/boppo/boppoClownEntity.js b/unpublishedScripts/marketplace/boppo/boppoClownEntity.js new file mode 100644 index 0000000000..36f2bf5ab0 --- /dev/null +++ b/unpublishedScripts/marketplace/boppo/boppoClownEntity.js @@ -0,0 +1,80 @@ +// +// boppoClownEntity.js +// +// Created by Thijs Wenker on 3/15/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +/* globals LookAtTarget */ + +(function() { + var SFX_PREFIX = 'https://hifi-content.s3-us-west-1.amazonaws.com/caitlyn/production/elBoppo/sfx/'; + var CHANNEL_PREFIX = 'io.highfidelity.boppo_server_'; + var PUNCH_SOUNDS = [ + 'punch_1.wav', + 'punch_2.wav' + ]; + var PUNCH_COOLDOWN = 300; + + Script.include('lookAtEntity.js'); + + var createBoppoClownEntity = function() { + var _this, + _entityID, + _boppoUserData, + _lookAtTarget, + _punchSounds = [], + _lastPlayedPunch = {}; + + var getOwnBoppoUserData = function() { + try { + return JSON.parse(Entities.getEntityProperties(_entityID, ['userData']).userData).Boppo; + } catch (e) { + // e + } + return {}; + }; + + var BoppoClownEntity = function () { + _this = this; + PUNCH_SOUNDS.forEach(function(punch) { + _punchSounds.push(SoundCache.getSound(SFX_PREFIX + punch)); + }); + }; + + BoppoClownEntity.prototype = { + preload: function(entityID) { + _entityID = entityID; + _boppoUserData = getOwnBoppoUserData(); + _lookAtTarget = new LookAtTarget(_entityID); + }, + collisionWithEntity: function(boppoEntity, collidingEntity, collisionInfo) { + if (collisionInfo.type === 0 && + Entities.getEntityProperties(collidingEntity, ['name']).name.indexOf('Boxing Glove ') === 0) { + + if (_lastPlayedPunch[collidingEntity] === undefined || + Date.now() - _lastPlayedPunch[collidingEntity] > PUNCH_COOLDOWN) { + + // If boxing glove detected here: + Messages.sendMessage(CHANNEL_PREFIX + _boppoUserData.gameParentID, 'hit'); + + _lookAtTarget.lookAtByAction(); + var randomPunchIndex = Math.floor(Math.random() * _punchSounds.length); + Audio.playSound(_punchSounds[randomPunchIndex], { + position: collisionInfo.contactPoint + }); + _lastPlayedPunch[collidingEntity] = Date.now(); + } + } + } + + }; + + return new BoppoClownEntity(); + }; + + return createBoppoClownEntity(); +}); diff --git a/unpublishedScripts/marketplace/boppo/boppoServer.js b/unpublishedScripts/marketplace/boppo/boppoServer.js new file mode 100644 index 0000000000..f03154573c --- /dev/null +++ b/unpublishedScripts/marketplace/boppo/boppoServer.js @@ -0,0 +1,303 @@ +// +// boppoServer.js +// +// Created by Thijs Wenker on 3/15/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +(function() { + var SFX_PREFIX = 'https://hifi-content.s3-us-west-1.amazonaws.com/caitlyn/production/elBoppo/sfx/'; + var CLOWN_LAUGHS = [ + 'clown_laugh_1.wav', + 'clown_laugh_2.wav', + 'clown_laugh_3.wav', + 'clown_laugh_4.wav' + ]; + var TICK_TOCK_SOUND = 'ticktock%20-%20tock.wav'; + var BOXING_RING_BELL_START = 'boxingRingBell.wav'; + var BOXING_RING_BELL_END = 'boxingRingBell-end.wav'; + var BOPPO_MUSIC = 'boppoMusic.wav'; + var CHANNEL_PREFIX = 'io.highfidelity.boppo_server_'; + var MESSAGE_HIT = 'hit'; + var MESSAGE_ENTER_ZONE = 'enter-zone'; + var MESSAGE_UNLOAD_FIX = 'unload-fix'; + + var DEFAULT_SOUND_VOLUME = 0.6; + + // don't set the search radius too high, it might remove boppo's from other nearby instances + var BOPPO_SEARCH_RADIUS = 4.0; + + var MILLISECONDS_PER_SECOND = 1000; + // Make sure the entities are loaded at startup (TODO: more solid fix) + var LOAD_TIMEOUT = 5000; + var SECONDS_PER_MINUTE = 60; + var DEFAULT_PLAYTIME = 30; // seconds + var BASE_TEN = 10; + var TICK_TOCK_FROM = 3; // seconds + var COOLDOWN_TIME_MS = MILLISECONDS_PER_SECOND * 3; + + var createBoppoServer = function() { + var _this, + _isInitialized = false, + _clownLaughs = [], + _musicInjector, + _music, + _laughingInjector, + _tickTockSound, + _boxingBellRingStart, + _boxingBellRingEnd, + _entityID, + _boppoClownID, + _channel, + _boppoEntities, + _isGameRunning, + _updateInterval, + _timeLeft, + _hits, + _coolDown; + + var getOwnBoppoUserData = function() { + try { + return JSON.parse(Entities.getEntityProperties(_entityID, ['userData']).userData).Boppo; + } catch (e) { + // e + } + return {}; + }; + + var updateBoppoEntities = function() { + Entities.getChildrenIDs(_entityID).forEach(function(entityID) { + try { + var userData = JSON.parse(Entities.getEntityProperties(entityID, ['userData']).userData); + if (userData.Boppo.type !== undefined) { + _boppoEntities[userData.Boppo.type] = entityID; + } + } catch (e) { + // e + } + }); + }; + + var clearUntrackedBoppos = function() { + var position = Entities.getEntityProperties(_entityID, ['position']).position; + Entities.findEntities(position, BOPPO_SEARCH_RADIUS).forEach(function(entityID) { + try { + if (JSON.parse(Entities.getEntityProperties(entityID, ['userData']).userData).Boppo.type === 'boppo') { + Entities.deleteEntity(entityID); + } + } catch (e) { + // e + } + }); + }; + + var updateTimerDisplay = function() { + if (_boppoEntities['timer']) { + var secondsString = _timeLeft % SECONDS_PER_MINUTE; + if (secondsString < BASE_TEN) { + secondsString = '0' + secondsString; + } + var minutesString = Math.floor(_timeLeft / SECONDS_PER_MINUTE); + Entities.editEntity(_boppoEntities['timer'], { + text: minutesString + ':' + secondsString + }); + } + }; + + var updateScoreDisplay = function() { + if (_boppoEntities['score']) { + Entities.editEntity(_boppoEntities['score'], { + text: 'SCORE: ' + _hits + }); + } + }; + + var playSoundAtBoxingRing = function(sound, properties) { + var _properties = properties ? properties : {}; + if (_properties['volume'] === undefined) { + _properties['volume'] = DEFAULT_SOUND_VOLUME; + } + _properties['position'] = Entities.getEntityProperties(_entityID, ['position']).position; + // play beep + return Audio.playSound(sound, _properties); + }; + + var onUpdate = function() { + _timeLeft--; + + if (_timeLeft > 0 && _timeLeft <= TICK_TOCK_FROM) { + // play beep + playSoundAtBoxingRing(_tickTockSound); + } + if (_timeLeft === 0) { + if (_musicInjector !== undefined && _musicInjector.isPlaying()) { + _musicInjector.stop(); + _musicInjector = undefined; + } + playSoundAtBoxingRing(_boxingBellRingEnd); + _isGameRunning = false; + Script.clearInterval(_updateInterval); + _updateInterval = null; + _coolDown = true; + Script.setTimeout(function() { + _coolDown = false; + _this.resetBoppo(); + }, COOLDOWN_TIME_MS); + } + updateTimerDisplay(); + }; + + var onMessage = function(channel, message, sender) { + if (channel === _channel) { + if (message === MESSAGE_HIT) { + _this.hit(); + } else if (message === MESSAGE_ENTER_ZONE && !_isGameRunning) { + _this.resetBoppo(); + } else if (message === MESSAGE_UNLOAD_FIX && _isInitialized) { + _this.unload(); + } + } + }; + + var BoppoServer = function () { + _this = this; + _hits = 0; + _boppoClownID = null; + _coolDown = false; + CLOWN_LAUGHS.forEach(function(clownLaugh) { + _clownLaughs.push(SoundCache.getSound(SFX_PREFIX + clownLaugh)); + }); + _tickTockSound = SoundCache.getSound(SFX_PREFIX + TICK_TOCK_SOUND); + _boxingBellRingStart = SoundCache.getSound(SFX_PREFIX + BOXING_RING_BELL_START); + _boxingBellRingEnd = SoundCache.getSound(SFX_PREFIX + BOXING_RING_BELL_END); + _music = SoundCache.getSound(SFX_PREFIX + BOPPO_MUSIC); + _boppoEntities = {}; + }; + + BoppoServer.prototype = { + preload: function(entityID) { + _entityID = entityID; + _channel = CHANNEL_PREFIX + entityID; + + Messages.sendLocalMessage(_channel, MESSAGE_UNLOAD_FIX); + Script.setTimeout(function() { + clearUntrackedBoppos(); + updateBoppoEntities(); + Messages.subscribe(_channel); + Messages.messageReceived.connect(onMessage); + _this.resetBoppo(); + _isInitialized = true; + }, LOAD_TIMEOUT); + }, + resetBoppo: function() { + if (_boppoClownID !== null) { + print('deleting boppo: ' + _boppoClownID); + Entities.deleteEntity(_boppoClownID); + } + var boppoBaseProperties = Entities.getEntityProperties(_entityID, ['position', 'rotation']); + _boppoClownID = Entities.addEntity({ + angularDamping: 0.0, + collisionSoundURL: 'https://hifi-content.s3.amazonaws.com/caitlyn/production/elBoppo/51460__andre-rocha-nascimento__basket-ball-01-bounce.wav', + collisionsWillMove: true, + compoundShapeURL: 'https://hifi-content.s3.amazonaws.com/caitlyn/production/elBoppo/bopo_phys.obj', + damping: 1.0, + density: 10000, + dimensions: { + x: 1.2668079137802124, + y: 2.0568051338195801, + z: 0.88563752174377441 + }, + dynamic: 1.0, + friction: 1.0, + gravity: { + x: 0, + y: -25, + z: 0 + }, + modelURL: 'https://hifi-content.s3.amazonaws.com/caitlyn/production/elBoppo/elBoppo3_VR.fbx', + name: 'El Boppo the Punching Bag Clown', + registrationPoint: { + x: 0.5, + y: 0, + z: 0.3 + }, + restitution: 0.99, + rotation: boppoBaseProperties.rotation, + position: Vec3.sum(boppoBaseProperties.position, + Vec3.multiplyQbyV(boppoBaseProperties.rotation, { + x: 0.08666179329156876, + y: -1.5698202848434448, + z: 0.1847127377986908 + })), + script: Script.resolvePath('boppoClownEntity.js'), + shapeType: 'compound', + type: 'Model', + userData: JSON.stringify({ + lookAt: { + targetID: _boppoEntities['lookAtThis'], + disablePitch: true, + disableYaw: false, + disableRoll: true, + clearDisabledAxis: true, + rotationOffset: { x: 0.0, y: 180.0, z: 0.0} + }, + Boppo: { + type: 'boppo', + gameParentID: _entityID + }, + grabbableKey: { + grabbable: false + } + }) + }); + updateBoppoEntities(); + _boppoEntities['boppo'] = _boppoClownID; + }, + laugh: function() { + if (_laughingInjector !== undefined && _laughingInjector.isPlaying()) { + return; + } + var randomLaughIndex = Math.floor(Math.random() * _clownLaughs.length); + _laughingInjector = Audio.playSound(_clownLaughs[randomLaughIndex], { + position: Entities.getEntityProperties(_boppoClownID, ['position']).position + }); + }, + hit: function() { + if (_coolDown) { + return; + } + if (!_isGameRunning) { + var boxingRingBoppoData = getOwnBoppoUserData(); + _updateInterval = Script.setInterval(onUpdate, MILLISECONDS_PER_SECOND); + _timeLeft = boxingRingBoppoData.playTimeSeconds ? parseInt(boxingRingBoppoData.playTimeSeconds) : + DEFAULT_PLAYTIME; + _isGameRunning = true; + _hits = 0; + playSoundAtBoxingRing(_boxingBellRingStart); + _musicInjector = playSoundAtBoxingRing(_music, {loop: true, volume: 0.6}); + } + _hits++; + updateTimerDisplay(); + updateScoreDisplay(); + _this.laugh(); + }, + unload: function() { + print('unload called'); + if (_updateInterval) { + Script.clearInterval(_updateInterval); + } + Messages.messageReceived.disconnect(onMessage); + Messages.unsubscribe(_channel); + Entities.deleteEntity(_boppoClownID); + print('endOfUnload'); + } + }; + + return new BoppoServer(); + }; + + return createBoppoServer(); +}); diff --git a/unpublishedScripts/marketplace/boppo/clownGloveDispenser.js b/unpublishedScripts/marketplace/boppo/clownGloveDispenser.js new file mode 100644 index 0000000000..cd0a0c0614 --- /dev/null +++ b/unpublishedScripts/marketplace/boppo/clownGloveDispenser.js @@ -0,0 +1,154 @@ +// +// clownGloveDispenser.js +// +// Created by Thijs Wenker on 8/2/16. +// Copyright 2016 High Fidelity, Inc. +// +// Based on examples/winterSmashUp/targetPractice/shooterPlatform.js +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +(function() { + var _this = this; + + var CHANNEL_PREFIX = 'io.highfidelity.boppo_server_'; + + var leftBoxingGlove = undefined; + var rightBoxingGlove = undefined; + + var inZone = false; + + var wearGloves = function() { + leftBoxingGlove = Entities.addEntity({ + position: MyAvatar.position, + collisionsWillMove: true, + dimensions: { + x: 0.24890634417533875, + y: 0.28214839100837708, + z: 0.21127720177173615 + }, + dynamic: true, + gravity: { + x: 0, + y: -9.8, + z: 0 + }, + modelURL: "https://hifi-content.s3.amazonaws.com/caitlyn/production/elBoppo/LFT_glove_VR3.fbx", + name: "Boxing Glove - Left", + registrationPoint: { + x: 0.5, + y: 0, + z: 0.5 + }, + shapeType: "simple-hull", + type: "Model", + userData: JSON.stringify({ + grabbableKey: { + invertSolidWhileHeld: true + }, + wearable: { + joints: { + LeftHand: [ + {x: 0, y: 0.0, z: 0.02 }, + Quat.fromVec3Degrees({x: 0, y: 0, z: 0}) + ] + } + } + }) + }); + Messages.sendLocalMessage('Hifi-Hand-Grab', JSON.stringify({hand: 'left', entityID: leftBoxingGlove})); + // Allows teleporting while glove is wielded + Messages.sendLocalMessage('Hifi-Teleport-Ignore-Add', leftBoxingGlove); + + rightBoxingGlove = Entities.addEntity({ + position: MyAvatar.position, + collisionsWillMove: true, + dimensions: { + x: 0.24890634417533875, + y: 0.28214839100837708, + z: 0.21127720177173615 + }, + dynamic: true, + gravity: { + x: 0, + y: -9.8, + z: 0 + }, + modelURL: "https://hifi-content.s3.amazonaws.com/caitlyn/production/elBoppo/RT_glove_VR2.fbx", + name: "Boxing Glove - Right", + registrationPoint: { + x: 0.5, + y: 0, + z: 0.5 + }, + shapeType: "simple-hull", + type: "Model", + userData: JSON.stringify({ + grabbableKey: { + invertSolidWhileHeld: true + }, + wearable: { + joints: { + RightHand: [ + {x: 0, y: 0.0, z: 0.02 }, + Quat.fromVec3Degrees({x: 0, y: 0, z: 0}) + ] + } + } + }) + }); + Messages.sendLocalMessage('Hifi-Hand-Grab', JSON.stringify({hand: 'right', entityID: rightBoxingGlove})); + // Allows teleporting while glove is wielded + Messages.sendLocalMessage('Hifi-Teleport-Ignore-Add', rightBoxingGlove); + }; + + var cleanUpGloves = function() { + if (leftBoxingGlove !== undefined) { + Entities.deleteEntity(leftBoxingGlove); + leftBoxingGlove = undefined; + } + if (rightBoxingGlove !== undefined) { + Entities.deleteEntity(rightBoxingGlove); + rightBoxingGlove = undefined; + } + }; + + var wearGlovesIfHMD = function() { + // cleanup your old gloves if they're still there (unlikely) + cleanUpGloves(); + if (HMD.active) { + wearGloves(); + } + }; + + _this.preload = function(entityID) { + HMD.displayModeChanged.connect(function() { + if (inZone) { + wearGlovesIfHMD(); + } + }); + }; + + _this.unload = function() { + cleanUpGloves(); + }; + + _this.enterEntity = function(entityID) { + inZone = true; + print('entered boxing glove dispenser entity'); + wearGlovesIfHMD(); + + // Reset boppo if game is not running: + var parentID = Entities.getEntityProperties(entityID, ['parentID']).parentID; + Messages.sendMessage(CHANNEL_PREFIX + parentID, 'enter-zone'); + }; + + _this.leaveEntity = function(entityID) { + inZone = false; + cleanUpGloves(); + }; + + _this.unload = _this.leaveEntity; +}); diff --git a/unpublishedScripts/marketplace/boppo/createElBoppo.js b/unpublishedScripts/marketplace/boppo/createElBoppo.js new file mode 100644 index 0000000000..4df6a2acda --- /dev/null +++ b/unpublishedScripts/marketplace/boppo/createElBoppo.js @@ -0,0 +1,430 @@ +// +// createElBoppo.js +// +// Created by Thijs Wenker on 3/17/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +/* globals SCRIPT_IMPORT_PROPERTIES */ + +var MODELS_PATH = 'https://hifi-content.s3.amazonaws.com/DomainContent/Welcome%20Area/production/models/boxingRing/'; +var WANT_CLEANUP_ON_SCRIPT_ENDING = false; + +var getScriptPath = function(localPath) { + if (this.isCleanupAndSpawnScript) { + return 'https://hifi-content.s3.amazonaws.com/DomainContent/Welcome%20Area/Scripts/boppo/' + localPath; + } + return Script.resolvePath(localPath); +}; + +var getCreatePosition = function() { + // can either return position defined by resetScript or avatar position + if (this.isCleanupAndSpawnScript) { + return SCRIPT_IMPORT_PROPERTIES.rootPosition; + } + return Vec3.sum(MyAvatar.position, {x: 1, z: -2}); +}; + +var boxingRing = Entities.addEntity({ + dimensions: { + x: 4.0584001541137695, + y: 4.0418000221252441, + z: 3.0490000247955322 + }, + modelURL: MODELS_PATH + 'assembled/boppoBoxingRingAssembly.fbx', + name: 'Boxing Ring Assembly', + rotation: { + w: 0.9996337890625, + x: -1.52587890625e-05, + y: -0.026230275630950928, + z: -4.57763671875e-05 + }, + position: getCreatePosition(), + scriptTimestamp: 1489612158459, + serverScripts: getScriptPath('boppoServer.js'), + shapeType: 'static-mesh', + type: 'Model', + userData: JSON.stringify({ + Boppo: { + type: 'boxingring', + playTimeSeconds: 15 + } + }) +}); + +var boppoEntities = [ + { + dimensions: { + x: 0.36947935819625854, + y: 0.25536194443702698, + z: 0.059455446898937225 + }, + modelURL: MODELS_PATH + 'boxingGameSign/boppoSignFrame.fbx', + parentID: boxingRing, + localPosition: { + x: -1.0251024961471558, + y: 0.51661628484725952, + z: -1.1176263093948364 + }, + rotation: { + w: 0.996856689453125, + x: 0.013321161270141602, + y: 0.0024566650390625, + z: 0.078049898147583008 + }, + shapeType: 'box', + type: 'Model' + }, + { + dimensions: { + x: 0.33255371451377869, + y: 0.1812121719121933, + z: 0.0099999997764825821 + }, + lineHeight: 0.125, + name: 'Boxing Ring - High Score Board', + parentID: boxingRing, + localPosition: { + x: -1.0239436626434326, + y: 0.52212876081466675, + z: -1.0971509218215942 + }, + rotation: { + w: 0.9876401424407959, + x: 0.013046503067016602, + y: 0.0012359619140625, + z: 0.15605401992797852 + }, + text: '0:00', + textColor: { + blue: 0, + green: 0, + red: 255 + }, + type: 'Text', + userData: JSON.stringify({ + Boppo: { + type: 'timer' + } + }) + }, + { + dimensions: { + x: 0.50491130352020264, + y: 0.13274604082107544, + z: 0.0099999997764825821 + }, + lineHeight: 0.090000003576278687, + name: 'Boxing Ring - Score Board', + parentID: boxingRing, + localPosition: { + x: -0.77596306800842285, + y: 0.37797555327415466, + z: -1.0910623073577881 + }, + rotation: { + w: 0.9518122673034668, + x: 0.004237703513354063, + y: -0.0010041374480351806, + z: 0.30455198884010315 + }, + text: 'SCORE: 0', + textColor: { + blue: 0, + green: 0, + red: 255 + }, + type: 'Text', + userData: JSON.stringify({ + Boppo: { + type: 'score' + } + }) + }, + { + dimensions: { + x: 0.58153259754180908, + y: 0.1884911060333252, + z: 0.059455446898937225 + }, + modelURL: MODELS_PATH + 'boxingGameSign/boppoSignFrame.fbx', + parentID: boxingRing, + localPosition: { + x: -0.78200173377990723, + y: 0.35684797167778015, + z: -1.108180046081543 + }, + rotation: { + w: 0.97814905643463135, + x: 0.0040436983108520508, + y: -0.0005645751953125, + z: 0.20778214931488037 + }, + shapeType: 'box', + type: 'Model' + }, + { + dimensions: { + x: 4.1867804527282715, + y: 3.5065803527832031, + z: 5.6845207214355469 + }, + name: 'El Boppo the Clown boxing area & glove maker', + parentID: boxingRing, + localPosition: { + x: -0.012308252975344658, + y: 0.054641719907522202, + z: 0.98782551288604736 + }, + rotation: { + w: 1, + x: -1.52587890625e-05, + y: -1.52587890625e-05, + z: -1.52587890625e-05 + }, + script: getScriptPath('clownGloveDispenser.js'), + shapeType: 'box', + type: 'Zone', + visible: false + }, + { + color: { + blue: 255, + green: 5, + red: 255 + }, + dimensions: { + x: 0.20000000298023224, + y: 0.20000000298023224, + z: 0.20000000298023224 + }, + name: 'LookAtBox', + parentID: boxingRing, + localPosition: { + x: -0.1772226095199585, + y: -1.7072629928588867, + z: 1.3122396469116211 + }, + rotation: { + w: 0.999969482421875, + x: 1.52587890625e-05, + y: 0.0043793916702270508, + z: 1.52587890625e-05 + }, + shape: 'Cube', + type: 'Box', + userData: JSON.stringify({ + Boppo: { + type: 'lookAtThis' + } + }) + }, + { + color: { + blue: 209, + green: 157, + red: 209 + }, + dimensions: { + x: 1.6913000345230103, + y: 1.2124500274658203, + z: 0.2572999894618988 + }, + name: 'boppoBackBoard', + parentID: boxingRing, + localPosition: { + x: -0.19500596821308136, + y: -1.1044719219207764, + z: -0.55993378162384033 + }, + rotation: { + w: 0.9807126522064209, + x: -0.19511711597442627, + y: 0.0085297822952270508, + z: 0.0016937255859375 + }, + shape: 'Cube', + type: 'Box', + visible: false + }, + { + color: { + blue: 0, + green: 0, + red: 255 + }, + dimensions: { + x: 1.8155574798583984, + y: 0.92306196689605713, + z: 0.51203572750091553 + }, + name: 'boppoBackBoard', + parentID: boxingRing, + localPosition: { + x: -0.11036647111177444, + y: -0.051978692412376404, + z: -0.79054081439971924 + }, + rotation: { + w: 0.9807431697845459, + x: 0.19505608081817627, + y: 0.0085602998733520508, + z: -0.0017547607421875 + }, + shape: 'Cube', + type: 'Box', + visible: false + }, + { + color: { + blue: 209, + green: 157, + red: 209 + }, + dimensions: { + x: 1.9941408634185791, + y: 1.2124500274658203, + z: 0.2572999894618988 + }, + name: 'boppoBackBoard', + localPosition: { + x: 0.69560068845748901, + y: -1.3840068578720093, + z: 0.059689953923225403 + }, + rotation: { + w: 0.73458456993103027, + x: -0.24113833904266357, + y: -0.56545358896255493, + z: -0.28734266757965088 + }, + shape: 'Cube', + type: 'Box', + visible: false + }, + { + color: { + blue: 82, + green: 82, + red: 82 + }, + dimensions: { + x: 8.3777303695678711, + y: 0.87573593854904175, + z: 7.9759469032287598 + }, + parentID: boxingRing, + localPosition: { + x: -0.38302639126777649, + y: -2.121284008026123, + z: 0.3699878454208374 + }, + rotation: { + w: 0.70711839199066162, + x: -7.62939453125e-05, + y: 0.70705735683441162, + z: -1.52587890625e-05 + }, + shape: 'Triangle', + type: 'Shape' + }, + { + color: { + blue: 209, + green: 157, + red: 209 + }, + dimensions: { + x: 1.889795184135437, + y: 0.86068248748779297, + z: 0.2572999894618988 + }, + name: 'boppoBackBoard', + parentID: boxingRing, + localPosition: { + x: -0.95167744159698486, + y: -1.4756947755813599, + z: -0.042313352227210999 + }, + rotation: { + w: 0.74004733562469482, + x: -0.24461740255355835, + y: 0.56044864654541016, + z: 0.27998781204223633 + }, + shape: 'Cube', + type: 'Box', + visible: false + }, + { + color: { + blue: 0, + green: 0, + red: 255 + }, + dimensions: { + x: 4.0720257759094238, + y: 0.50657749176025391, + z: 1.4769613742828369 + }, + name: 'boppo-stepsRamp', + parentID: boxingRing, + localPosition: { + x: -0.002939039608463645, + y: -1.9770187139511108, + z: 2.2165381908416748 + }, + rotation: { + w: 0.99252307415008545, + x: 0.12184333801269531, + y: -1.52587890625e-05, + z: -1.52587890625e-05 + }, + shape: 'Cube', + type: 'Box', + visible: false + }, + { + color: { + blue: 150, + green: 150, + red: 150 + }, + cutoff: 90, + dimensions: { + x: 5.2220535278320312, + y: 5.2220535278320312, + z: 5.2220535278320312 + }, + falloffRadius: 2, + intensity: 15, + name: 'boxing ring light', + parentID: boxingRing, + localPosition: { + x: -1.4094564914703369, + y: -0.36021926999092102, + z: 0.81797939538955688 + }, + rotation: { + w: 0.9807431697845459, + x: 1.52587890625e-05, + y: -0.19520866870880127, + z: -1.52587890625e-05 + }, + type: 'Light' + } +]; + +boppoEntities.forEach(function(entityProperties) { + entityProperties['parentID'] = boxingRing; + Entities.addEntity(entityProperties); +}); + +if (WANT_CLEANUP_ON_SCRIPT_ENDING) { + Script.scriptEnding.connect(function() { + Entities.deleteEntity(boxingRing); + }); +} diff --git a/unpublishedScripts/marketplace/boppo/lookAtEntity.js b/unpublishedScripts/marketplace/boppo/lookAtEntity.js new file mode 100644 index 0000000000..ba072814f2 --- /dev/null +++ b/unpublishedScripts/marketplace/boppo/lookAtEntity.js @@ -0,0 +1,98 @@ +// +// lookAtTarget.js +// +// Created by Thijs Wenker on 3/15/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +/* globals LookAtTarget:true */ + +LookAtTarget = function(sourceEntityID) { + /* private variables */ + var _this, + _options, + _sourceEntityID, + _sourceEntityProperties, + REQUIRED_PROPERTIES = ['position', 'rotation', 'userData'], + LOOK_AT_TAG = 'lookAtTarget'; + + LookAtTarget = function(sourceEntityID) { + _this = this; + _sourceEntityID = sourceEntityID; + _this.updateOptions(); + }; + + /* private functions */ + var updateEntitySourceProperties = function() { + _sourceEntityProperties = Entities.getEntityProperties(_sourceEntityID, REQUIRED_PROPERTIES); + }; + + var getUpdatedActionProperties = function() { + return { + targetRotation: _this.getLookAtRotation(), + angularTimeScale: 0.1, + ttl: 10 + }; + }; + + var getNewActionProperties = function() { + var newActionProperties = getUpdatedActionProperties(); + newActionProperties.tag = LOOK_AT_TAG; + return newActionProperties; + }; + + LookAtTarget.prototype = { + /* public functions */ + updateOptions: function() { + updateEntitySourceProperties(); + _options = JSON.parse(_sourceEntityProperties.userData).lookAt; + }, + getTargetPosition: function() { + return Entities.getEntityProperties(_options.targetID).position; + }, + getLookAtRotation: function() { + _this.updateOptions(); + + var newRotation = Quat.lookAt(_sourceEntityProperties.position, _this.getTargetPosition(), Vec3.UP); + if (_options.rotationOffset !== undefined) { + newRotation = Quat.multiply(newRotation, Quat.fromVec3Degrees(_options.rotationOffset)); + } + if (_options.disablePitch || _options.disableYaw || _options.disablePitch) { + var disabledAxis = _options.clearDisabledAxis ? Vec3.ZERO : + Quat.safeEulerAngles(_sourceEntityProperties.rotation); + var newEulers = Quat.safeEulerAngles(newRotation); + newRotation = Quat.fromVec3Degrees({ + x: _options.disablePitch ? disabledAxis.x : newEulers.x, + y: _options.disableYaw ? disabledAxis.y : newEulers.y, + z: _options.disableRoll ? disabledAxis.z : newEulers.z + }); + } + return newRotation; + }, + lookAtDirectly: function() { + Entities.editEntity(_sourceEntityID, {rotation: _this.getLookAtRotation()}); + }, + lookAtByAction: function() { + var actionIDs = Entities.getActionIDs(_sourceEntityID); + var actionFound = false; + actionIDs.forEach(function(actionID) { + if (actionFound) { + return; + } + var actionArguments = Entities.getActionArguments(_sourceEntityID, actionID); + if (actionArguments.tag === LOOK_AT_TAG) { + actionFound = true; + Entities.updateAction(_sourceEntityID, actionID, getUpdatedActionProperties()); + } + }); + if (!actionFound) { + Entities.addAction('spring', _sourceEntityID, getNewActionProperties()); + } + } + }; + + return new LookAtTarget(sourceEntityID); +}; From a08346719ce7e231225786effa8b3d8c54d41d23 Mon Sep 17 00:00:00 2001 From: David Kelly Date: Thu, 23 Mar 2017 10:29:45 -0700 Subject: [PATCH 041/118] one bit of defensive code, plus some temporary logging --- scripts/system/makeUserConnection.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/system/makeUserConnection.js b/scripts/system/makeUserConnection.js index a9d2653146..e750c7179f 100644 --- a/scripts/system/makeUserConnection.js +++ b/scripts/system/makeUserConnection.js @@ -234,6 +234,8 @@ function updateVisualization() { if (state == STATES.inactive) { deleteParticleEffect(); deleteMakeConnectionParticleEffect(); + // this should always be true if inactive, but just in case: + currentHand = undefined; return; } @@ -414,6 +416,7 @@ function updateTriggers(value, fromKeyboard, hand) { // ok now, we are either initiating or quitting... var isGripping = value > GRIP_MIN; if (isGripping) { + debug("updateTriggers called - gripping", handToString(hand)); if (state != STATES.inactive) { return; } else { @@ -421,6 +424,7 @@ function updateTriggers(value, fromKeyboard, hand) { } } else { // TODO: should we end handshake even when inactive? Ponder + debug("updateTriggers called -- no longer gripping", handToString(hand)); if (state != STATES.inactive) { endHandshake(); } else { From 0edcdde74680bbfd3c918c521437618195964c06 Mon Sep 17 00:00:00 2001 From: David Kelly Date: Thu, 23 Mar 2017 10:40:34 -0700 Subject: [PATCH 042/118] better key mapping --- scripts/system/makeUserConnection.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/system/makeUserConnection.js b/scripts/system/makeUserConnection.js index e750c7179f..0b73beaca5 100644 --- a/scripts/system/makeUserConnection.js +++ b/scripts/system/makeUserConnection.js @@ -688,12 +688,12 @@ function makeGripHandler(hand, animate) { } function keyPressEvent(event) { - if ((event.text === "x") && !event.isAutoRepeat) { + if ((event.text === "x") && !event.isAutoRepeat && !event.isShifted && !event.isMeta && !event.isControl && !event.isAlt) { updateTriggers(1.0, true, Controller.Standard.RightHand); } } function keyReleaseEvent(event) { - if ((event.text === "x") && !event.isAutoRepeat) { + if ((event.text === "x") && !event.isAutoRepeat && !event.isShifted && !event.isMeta && !event.isControl && !event.isAlt) { updateTriggers(0.0, true, Controller.Standard.RightHand); } } From a7623dcac8717749e505613f9c207068a053f07e Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Thu, 23 Mar 2017 10:48:44 -0700 Subject: [PATCH 043/118] Use OpacityMask instead of hack; fix connectionsTab --- interface/resources/qml/hifi/Pal.qml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index 837b804c7b..75aa670eba 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -301,6 +301,8 @@ Rectangle { anchors.fill: parent; onClicked: { if (activeTab != "connectionsTab") { + connectionsLoading.visible = false; + connectionsLoading.visible = true; pal.sendToScript({method: 'refreshConnections'}); } activeTab = "connectionsTab"; From 98ee02f84f337398fd0ef2eb6833c6487db40e20 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Thu, 23 Mar 2017 10:46:20 -0700 Subject: [PATCH 044/118] Use OpacityMask instead of hack; fix connectionsTab --- interface/resources/qml/hifi/NameCard.qml | 29 +++++++++++------------ interface/resources/qml/hifi/Pal.qml | 10 +++----- 2 files changed, 17 insertions(+), 22 deletions(-) diff --git a/interface/resources/qml/hifi/NameCard.qml b/interface/resources/qml/hifi/NameCard.qml index 0ef86dd342..4424c2a268 100644 --- a/interface/resources/qml/hifi/NameCard.qml +++ b/interface/resources/qml/hifi/NameCard.qml @@ -42,8 +42,7 @@ Item { property bool selected: false property bool isAdmin: false property bool isPresent: true - property string imageMaskColor: pal.color; - property string profilePicBorderColor: (connectionStatus == "connection" ? hifi.colors.indigoAccent : (connectionStatus == "friend" ? hifi.colors.greenHighlight : imageMaskColor)) + property string profilePicBorderColor: (connectionStatus == "connection" ? hifi.colors.indigoAccent : (connectionStatus == "friend" ? hifi.colors.greenHighlight : "transparent")) Item { id: avatarImage @@ -61,25 +60,25 @@ Item { mipmap: true; // Anchors anchors.fill: parent + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Item { + width: userImage.width; + height: userImage.height; + Rectangle { + anchors.centerIn: parent; + width: userImage.width; // This works because userImage is square + height: width; + radius: width; + } + } + } } AnimatedImage { source: "../../icons/profilePicLoading.gif" anchors.fill: parent; visible: userImage.status != Image.Ready; } - // Circular mask - Rectangle { - id: avatarImageMask; - visible: avatarImage.visible; - anchors.verticalCenter: avatarImage.verticalCenter; - anchors.horizontalCenter: avatarImage.horizontalCenter; - width: avatarImage.width * 2; - height: avatarImage.height * 2; - color: "transparent" - radius: avatarImage.height; - border.color: imageMaskColor; - border.width: avatarImage.height/2; - } StateImage { id: infoHoverImage; visible: false; diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index 75aa670eba..eba1a92cc6 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -126,7 +126,6 @@ Rectangle { id: myCard; // Properties profileUrl: myData.profileUrl; - imageMaskColor: pal.color; displayName: myData.displayName; userName: myData.userName; audioLevel: myData.audioLevel; @@ -300,15 +299,14 @@ Rectangle { MouseArea { anchors.fill: parent; onClicked: { + connectionsLoading.visible = false; + connectionsLoading.visible = true; if (activeTab != "connectionsTab") { connectionsLoading.visible = false; connectionsLoading.visible = true; pal.sendToScript({method: 'refreshConnections'}); } activeTab = "connectionsTab"; - connectionsLoading.visible = false; - connectionsLoading.visible = true; - connectionsRefreshProblemText.visible = false; } } @@ -589,7 +587,7 @@ Rectangle { property bool isCheckBox: styleData.role === "personalMute" || styleData.role === "ignore"; property bool isButton: styleData.role === "mute" || styleData.role === "kick"; property bool isAvgAudio: styleData.role === "avgAudioLevel"; - opacity: model && model.isPresent ? 1.0 : 0.4; + opacity: !isButton ? (model && model.isPresent ? 1.0 : 0.4) : 1.0; // Admin actions shouldn't turn gray // This NameCard refers to the cell that contains an avatar's // DisplayName and UserName @@ -597,7 +595,6 @@ Rectangle { id: nameCard; // Properties profileUrl: (model && model.profileUrl) || ""; - imageMaskColor: rowColor(styleData.selected, styleData.row % 2); displayName: styleData.value; userName: model ? model.userName : ""; connectionStatus: model ? model.connection : ""; @@ -918,7 +915,6 @@ Rectangle { // Properties visible: styleData.role === "userName"; profileUrl: (model && model.profileUrl) || ""; - imageMaskColor: rowColor(styleData.selected, styleData.row % 2); displayName: ""; userName: model ? model.userName : ""; connectionStatus : model ? model.connection : ""; From 3f95982f348dbdb18ee93230a209e92829132ed9 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Thu, 23 Mar 2017 14:00:16 -0700 Subject: [PATCH 045/118] Fix bug due to incorrect merge... --- interface/resources/qml/hifi/Pal.qml | 2 -- 1 file changed, 2 deletions(-) diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index eba1a92cc6..d5e26e22cf 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -299,8 +299,6 @@ Rectangle { MouseArea { anchors.fill: parent; onClicked: { - connectionsLoading.visible = false; - connectionsLoading.visible = true; if (activeTab != "connectionsTab") { connectionsLoading.visible = false; connectionsLoading.visible = true; From 665b21f2f788a0ec649bf214196b2891c5cc461e Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Thu, 23 Mar 2017 17:08:32 -0700 Subject: [PATCH 046/118] Checkpoint --- .../src/scripting/HMDScriptingInterface.h | 1 + scripts/system/selectAudioDevice.js | 160 ++++++++++-------- 2 files changed, 90 insertions(+), 71 deletions(-) diff --git a/interface/src/scripting/HMDScriptingInterface.h b/interface/src/scripting/HMDScriptingInterface.h index 276e23d2d5..846aa13017 100644 --- a/interface/src/scripting/HMDScriptingInterface.h +++ b/interface/src/scripting/HMDScriptingInterface.h @@ -80,6 +80,7 @@ public: signals: bool shouldShowHandControllersChanged(); + void mountedChanged(); public: HMDScriptingInterface(); diff --git a/scripts/system/selectAudioDevice.js b/scripts/system/selectAudioDevice.js index f5929ce151..04215c966d 100644 --- a/scripts/system/selectAudioDevice.js +++ b/scripts/system/selectAudioDevice.js @@ -45,26 +45,43 @@ if (typeof String.prototype.trimEndsWith != 'function') { }; } +/**************************************** + VAR DEFINITIONS +****************************************/ const INPUT_DEVICE_SETTING = "audio_input_device"; const OUTPUT_DEVICE_SETTING = "audio_output_device"; - var selectedInputMenu = ""; var selectedOutputMenu = ""; +var audioDevicesList = []; +// Some HMDs (like Oculus CV1) have a built in audio device. If they +// do, then this function will handle switching to that device automatically +// when you goActive with the HMD active. +var wasHmdActive = false; // assume it's not active to start +var switchedAudioInputToHMD = false; +var switchedAudioOutputToHMD = false; +var previousSelectedInputAudioDevice = ""; +var previousSelectedOutputAudioDevice = ""; - var audioDevicesList = []; +/**************************************** + BEGIN FUNCTION DEFINITIONS +****************************************/ function setupAudioMenus() { removeAudioMenus(); - Menu.addSeparator("Audio", "Input Audio Device"); + /* Setup audio input devices */ + Menu.addSeparator("Audio", "Input Audio Device"); var inputDeviceSetting = Settings.getValue(INPUT_DEVICE_SETTING); var inputDevices = AudioDevice.getInputDevices(); var selectedInputDevice = AudioDevice.getInputDevice(); if (inputDevices.indexOf(inputDeviceSetting) != -1 && selectedInputDevice != inputDeviceSetting) { + print ("Audio input device SETTING does not match Input AudioDevice. Attempting to change Input AudioDevice...") if (AudioDevice.setInputDevice(inputDeviceSetting)) { selectedInputDevice = inputDeviceSetting; + } else { + print("Error setting audio input device!") } } - print("audio input devices: " + inputDevices); + print("Audio input devices: " + inputDevices); for(var i = 0; i < inputDevices.length; i++) { var thisDeviceSelected = (inputDevices[i] == selectedInputDevice); var menuItem = "Use " + inputDevices[i] + " for Input"; @@ -80,17 +97,20 @@ function setupAudioMenus() { } } + /* Setup audio output devices */ Menu.addSeparator("Audio", "Output Audio Device"); - var outputDeviceSetting = Settings.getValue(OUTPUT_DEVICE_SETTING); var outputDevices = AudioDevice.getOutputDevices(); var selectedOutputDevice = AudioDevice.getOutputDevice(); if (outputDevices.indexOf(outputDeviceSetting) != -1 && selectedOutputDevice != outputDeviceSetting) { + print("Audio output device SETTING does not match Output AudioDevice. Attempting to change Output AudioDevice...") if (AudioDevice.setOutputDevice(outputDeviceSetting)) { selectedOutputDevice = outputDeviceSetting; + } else { + print("Error setting audio output device!") } } - print("audio output devices: " + outputDevices); + print("Audio output devices: " + outputDevices); for (var i = 0; i < outputDevices.length; i++) { var thisDeviceSelected = (outputDevices[i] == selectedOutputDevice); var menuItem = "Use " + outputDevices[i] + " for Output"; @@ -115,127 +135,125 @@ function removeAudioMenus() { Menu.removeMenuItem("Audio", audioDevicesList[index]); } + Menu.removeMenu("Audio > Devices"); + audioDevicesList = []; } function onDevicechanged() { - print("audio devices changed, removing Audio > Devices menu..."); - Menu.removeMenu("Audio > Devices"); - print("now setting up Audio > Devices menu"); + print("System audio device changed. Removing and replacing Audio Menus..."); setupAudioMenus(); } -// Have a small delay before the menu's get setup and the audio devices can switch to the last selected ones -Script.setTimeout(function () { - print("connecting deviceChanged"); - AudioDevice.deviceChanged.connect(onDevicechanged); - print("setting up Audio > Devices menu for first time"); - setupAudioMenus(); -}, 5000); - -function scriptEnding() { - Menu.removeMenu("Audio > Devices"); -} -Script.scriptEnding.connect(scriptEnding); - - function menuItemEvent(menuItem) { if (menuItem.startsWith("Use ")) { - if (menuItem.endsWith(" for Output")) { - var selectedDevice = menuItem.trimStartsWith("Use ").trimEndsWith(" for Output"); - print("output audio selection..." + selectedDevice); - Menu.menuItemEvent.disconnect(menuItemEvent); - Menu.setIsOptionChecked(selectedOutputMenu, false); - selectedOutputMenu = menuItem; - Menu.setIsOptionChecked(selectedOutputMenu, true); - if (AudioDevice.setOutputDevice(selectedDevice)) { - Settings.setValue(OUTPUT_DEVICE_SETTING, selectedDevice); - } - Menu.menuItemEvent.connect(menuItemEvent); - } else if (menuItem.endsWith(" for Input")) { + if (menuItem.endsWith(" for Input")) { var selectedDevice = menuItem.trimStartsWith("Use ").trimEndsWith(" for Input"); - print("input audio selection..." + selectedDevice); + print("User selected a new Audio Input Device: " + selectedDevice); Menu.menuItemEvent.disconnect(menuItemEvent); Menu.setIsOptionChecked(selectedInputMenu, false); selectedInputMenu = menuItem; Menu.setIsOptionChecked(selectedInputMenu, true); if (AudioDevice.setInputDevice(selectedDevice)) { Settings.setValue(INPUT_DEVICE_SETTING, selectedDevice); + } else { + print("Error setting audio input device!") + } + Menu.menuItemEvent.connect(menuItemEvent); + } else if (menuItem.endsWith(" for Output")) { + var selectedDevice = menuItem.trimStartsWith("Use ").trimEndsWith(" for Output"); + print("User selected a new Audio Output Device: " + selectedDevice); + Menu.menuItemEvent.disconnect(menuItemEvent); + Menu.setIsOptionChecked(selectedOutputMenu, false); + selectedOutputMenu = menuItem; + Menu.setIsOptionChecked(selectedOutputMenu, true); + if (AudioDevice.setOutputDevice(selectedDevice)) { + Settings.setValue(OUTPUT_DEVICE_SETTING, selectedDevice); + } else { + print("Error setting audio output device!") } Menu.menuItemEvent.connect(menuItemEvent); } } } -Menu.menuItemEvent.connect(menuItemEvent); - -// Some HMDs (like Oculus CV1) have a built in audio device. If they -// do, then this function will handle switching to that device automatically -// when you goActive with the HMD active. -var wasHmdMounted = false; // assume it's un-mounted to start -var switchedAudioInputToHMD = false; -var switchedAudioOutputToHMD = false; -var previousSelectedInputAudioDevice = ""; -var previousSelectedOutputAudioDevice = ""; - function restoreAudio() { if (switchedAudioInputToHMD) { - print("switching back from HMD preferred audio input to:" + previousSelectedInputAudioDevice); + print("Switching back from HMD preferred audio input to: " + previousSelectedInputAudioDevice); menuItemEvent("Use " + previousSelectedInputAudioDevice + " for Input"); + switchedAudioInputToHMD = false; } if (switchedAudioOutputToHMD) { - print("switching back from HMD preferred audio output to:" + previousSelectedOutputAudioDevice); + print("Switching back from HMD preferred audio output to: " + previousSelectedOutputAudioDevice); menuItemEvent("Use " + previousSelectedOutputAudioDevice + " for Output"); + switchedAudioOutputToHMD = false; } } function checkHMDAudio() { - // Mounted state is changing... handle switching - if (HMD.mounted != wasHmdMounted) { - print("HMD mounted changed..."); + // HMD Active state is changing; handle switching + if (HMD.active != wasHmdActive) { + print("HMD Active state changed!"); - // We're putting the HMD on... switch to those devices - if (HMD.mounted) { - print("NOW mounted..."); + // We're putting the HMD on; switch to those devices + if (HMD.active) { + print("HMD is now Active."); var hmdPreferredAudioInput = HMD.preferredAudioInput(); var hmdPreferredAudioOutput = HMD.preferredAudioOutput(); - print("hmdPreferredAudioInput:" + hmdPreferredAudioInput); - print("hmdPreferredAudioOutput:" + hmdPreferredAudioOutput); + print("hmdPreferredAudioInput: " + hmdPreferredAudioInput); + print("hmdPreferredAudioOutput: " + hmdPreferredAudioOutput); - var hmdHasPreferredAudio = (hmdPreferredAudioInput !== "") || (hmdPreferredAudioOutput !== ""); - if (hmdHasPreferredAudio) { - print("HMD has preferred audio!"); + if (hmdPreferredAudioInput !== "") { + print("HMD has preferred audio input device."); previousSelectedInputAudioDevice = Settings.getValue(INPUT_DEVICE_SETTING); - previousSelectedOutputAudioDevice = Settings.getValue(OUTPUT_DEVICE_SETTING); - print("previousSelectedInputAudioDevice:" + previousSelectedInputAudioDevice); - print("previousSelectedOutputAudioDevice:" + previousSelectedOutputAudioDevice); - if (hmdPreferredAudioInput != previousSelectedInputAudioDevice && hmdPreferredAudioInput !== "") { - print("switching to HMD preferred audio input to:" + hmdPreferredAudioInput); + print("previousSelectedInputAudioDevice: " + previousSelectedInputAudioDevice); + if (hmdPreferredAudioInput != previousSelectedInputAudioDevice) { + print("Switching Audio Input device to HMD preferred device: " + hmdPreferredAudioInput); switchedAudioInputToHMD = true; menuItemEvent("Use " + hmdPreferredAudioInput + " for Input"); } - if (hmdPreferredAudioOutput != previousSelectedOutputAudioDevice && hmdPreferredAudioOutput !== "") { - print("switching to HMD preferred audio output to:" + hmdPreferredAudioOutput); + } + if (hmdPreferredAudioOutput !== "") { + print("HMD has preferred audio output device."); + previousSelectedOutputAudioDevice = Settings.getValue(OUTPUT_DEVICE_SETTING); + print("previousSelectedOutputAudioDevice: " + previousSelectedOutputAudioDevice); + if (hmdPreferredAudioOutput != previousSelectedOutputAudioDevice) { + print("Switching Audio Output device to HMD preferred device: " + hmdPreferredAudioOutput); switchedAudioOutputToHMD = true; menuItemEvent("Use " + hmdPreferredAudioOutput + " for Output"); } } } else { - print("HMD NOW un-mounted..."); + print("HMD no longer active. Restoring audio I/O devices..."); restoreAudio(); } } - wasHmdMounted = HMD.mounted; + wasHmdActive = HMD.active; } +/**************************************** + END FUNCTION DEFINITIONS +****************************************/ -Script.update.connect(checkHMDAudio); +/**************************************** + BEGIN SCRIPT BODY +****************************************/ +// Have a small delay before the menus get setup so the audio devices can switch to the last selected ones +Script.setTimeout(function () { + print("Connecting deviceChanged() and displayModeChanged()"); + AudioDevice.deviceChanged.connect(onDevicechanged); + HMD.displayModeChanged.connect(checkHMDAudio); + print("Setting up Audio > Devices menu for the first time"); + setupAudioMenus(); +}, 2000); +print("Connecting menuItemEvent() and scriptEnding()"); +Menu.menuItemEvent.connect(menuItemEvent); Script.scriptEnding.connect(function () { restoreAudio(); removeAudioMenus(); Menu.menuItemEvent.disconnect(menuItemEvent); - Script.update.disconnect(checkHMDAudio); + HMD.displayModeChanged.disconnect(checkHMDAudio); }); }()); // END LOCAL_SCOPE From 4473e6ba38b7830343d717a20a908888fdda9452 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Fri, 24 Mar 2017 09:50:35 -0700 Subject: [PATCH 047/118] Remove dead code --- interface/src/scripting/HMDScriptingInterface.h | 1 - 1 file changed, 1 deletion(-) diff --git a/interface/src/scripting/HMDScriptingInterface.h b/interface/src/scripting/HMDScriptingInterface.h index 846aa13017..276e23d2d5 100644 --- a/interface/src/scripting/HMDScriptingInterface.h +++ b/interface/src/scripting/HMDScriptingInterface.h @@ -80,7 +80,6 @@ public: signals: bool shouldShowHandControllersChanged(); - void mountedChanged(); public: HMDScriptingInterface(); From 915ace0087213c0eb30f1ce0954c6ebe6e9b6a70 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Fri, 24 Mar 2017 10:04:51 -0700 Subject: [PATCH 048/118] Revert "Merge from Master" This reverts commit e48123b5bbc171e7df32d93b652f4077367df6d8. --- BUILD_WIN.md | 153 +-- assignment-client/src/Agent.cpp | 32 +- assignment-client/src/Agent.h | 8 - assignment-client/src/assets/AssetServer.cpp | 4 +- assignment-client/src/octree/OctreeServer.cpp | 9 +- .../src/scripts/EntityScriptServer.cpp | 30 +- domain-server/src/DomainServer.cpp | 4 +- .../src/DomainServerSettingsManager.cpp | 37 +- interface/CMakeLists.txt | 2 +- interface/resources/controllers/standard.json | 44 +- interface/resources/html/img/devices.png | Bin 0 -> 7492 bytes interface/resources/html/img/models.png | Bin 0 -> 8664 bytes interface/resources/html/img/move.png | Bin 0 -> 6121 bytes interface/resources/html/img/run-script.png | Bin 0 -> 4873 bytes interface/resources/html/img/talk.png | Bin 0 -> 2611 bytes interface/resources/html/img/write-script.png | Bin 0 -> 2006 bytes .../resources/html/interface-welcome.html | 187 +++ interface/resources/icons/load-script.svg | 125 ++ interface/resources/icons/new-script.svg | 129 ++ interface/resources/icons/save-script.svg | 674 +++++++++++ interface/resources/icons/start-script.svg | 550 +++++++++ interface/resources/icons/stop-script.svg | 163 +++ interface/resources/qml/AvatarInputs.qml | 57 +- interface/resources/qml/Stats.qml | 12 +- interface/resources/styles/log_dialog.qss | 4 +- interface/src/Application.cpp | 214 ++-- interface/src/Application.h | 7 +- interface/src/Menu.cpp | 21 +- interface/src/Menu.h | 6 +- interface/src/avatar/AvatarManager.cpp | 2 +- interface/src/avatar/AvatarManager.h | 2 +- .../src/avatar/CauterizedMeshPartPayload.cpp | 53 +- .../src/avatar/CauterizedMeshPartPayload.h | 7 +- interface/src/avatar/CauterizedModel.cpp | 38 +- interface/src/avatar/MyAvatar.cpp | 109 +- interface/src/avatar/MyAvatar.h | 56 +- interface/src/ui/ApplicationOverlay.cpp | 49 +- interface/src/ui/ApplicationOverlay.h | 3 + interface/src/ui/AvatarInputs.cpp | 20 + interface/src/ui/AvatarInputs.h | 6 + interface/src/ui/BaseLogDialog.cpp | 48 +- interface/src/ui/BaseLogDialog.h | 4 +- interface/src/ui/CachesSizeDialog.cpp | 84 ++ interface/src/ui/CachesSizeDialog.h | 45 + interface/src/ui/DialogsManager.cpp | 24 + interface/src/ui/DialogsManager.h | 6 + interface/src/ui/DiskCacheEditor.cpp | 146 +++ interface/src/ui/DiskCacheEditor.h | 49 + interface/src/ui/ScriptEditBox.cpp | 111 ++ interface/src/ui/ScriptEditBox.h | 38 + interface/src/ui/ScriptEditorWidget.cpp | 256 ++++ interface/src/ui/ScriptEditorWidget.h | 64 + interface/src/ui/ScriptEditorWindow.cpp | 259 +++++ interface/src/ui/ScriptEditorWindow.h | 64 + interface/src/ui/ScriptLineNumberArea.cpp | 28 + interface/src/ui/ScriptLineNumberArea.h | 32 + interface/src/ui/ScriptsTableWidget.cpp | 49 + interface/src/ui/ScriptsTableWidget.h | 28 + interface/src/ui/Stats.cpp | 10 +- interface/src/ui/Stats.h | 8 +- interface/src/ui/overlays/Overlays.cpp | 4 +- interface/src/ui/overlays/Web3DOverlay.cpp | 2 +- interface/ui/scriptEditorWidget.ui | 142 +++ interface/ui/scriptEditorWindow.ui | 324 ++++++ libraries/audio-client/src/AudioClient.cpp | 244 ++-- libraries/audio-client/src/AudioClient.h | 16 +- .../display-plugins/OpenGLDisplayPlugin.cpp | 6 +- .../display-plugins/hmd/HmdDisplayPlugin.cpp | 35 +- .../src/EntityTreeRenderer.cpp | 3 +- .../src/RenderablePolyVoxEntityItem.cpp | 78 +- .../src/RenderablePolyVoxEntityItem.h | 9 +- .../src/RenderableShapeEntityItem.cpp | 13 +- .../src/RenderableWebEntityItem.cpp | 2 +- .../src/EntitiesScriptEngineProvider.h | 4 +- libraries/entities/src/EntityItem.cpp | 8 +- .../entities/src/EntityItemProperties.cpp | 21 + libraries/entities/src/EntityItemProperties.h | 4 + .../entities/src/EntityScriptingInterface.cpp | 176 +-- .../entities/src/EntityScriptingInterface.h | 54 +- libraries/entities/src/PolyVoxEntityItem.cpp | 4 - libraries/entities/src/PolyVoxEntityItem.h | 6 +- libraries/entities/src/PropertyGroup.h | 29 +- libraries/fbx/src/FBXReader.cpp | 19 +- libraries/fbx/src/FBXReader.h | 20 + libraries/fbx/src/FBXReader_Node.cpp | 3 +- libraries/fbx/src/OBJReader.cpp | 10 +- libraries/fbx/src/OBJReader.h | 2 +- libraries/fbx/src/OBJWriter.cpp | 148 --- libraries/fbx/src/OBJWriter.h | 26 - libraries/gpu-gl/src/gpu/gl/GLBackend.cpp | 11 +- libraries/gpu-gl/src/gpu/gl/GLBackend.h | 13 +- .../gpu-gl/src/gpu/gl/GLBackendTexture.cpp | 54 +- libraries/gpu-gl/src/gpu/gl/GLFramebuffer.cpp | 9 +- libraries/gpu-gl/src/gpu/gl/GLFramebuffer.h | 2 +- libraries/gpu-gl/src/gpu/gl/GLTexelFormat.cpp | 5 - libraries/gpu-gl/src/gpu/gl/GLTexture.cpp | 233 +++- libraries/gpu-gl/src/gpu/gl/GLTexture.h | 203 +++- .../gpu-gl/src/gpu/gl/GLTextureTransfer.cpp | 208 ++++ .../gpu-gl/src/gpu/gl/GLTextureTransfer.h | 78 ++ libraries/gpu-gl/src/gpu/gl41/GL41Backend.h | 31 +- .../gpu-gl/src/gpu/gl41/GL41BackendOutput.cpp | 10 +- .../src/gpu/gl41/GL41BackendTexture.cpp | 192 ++- libraries/gpu-gl/src/gpu/gl45/GL45Backend.cpp | 11 +- libraries/gpu-gl/src/gpu/gl45/GL45Backend.h | 254 +--- .../gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp | 10 +- .../src/gpu/gl45/GL45BackendTexture.cpp | 542 +++++++-- .../gpu/gl45/GL45BackendVariableTexture.cpp | 1033 ----------------- libraries/gpu/CMakeLists.txt | 2 +- libraries/gpu/src/gpu/Batch.cpp | 7 + libraries/gpu/src/gpu/Buffer.h | 2 +- libraries/gpu/src/gpu/Context.cpp | 17 - libraries/gpu/src/gpu/Context.h | 4 - libraries/gpu/src/gpu/Format.cpp | 7 - libraries/gpu/src/gpu/Format.h | 6 - libraries/gpu/src/gpu/Framebuffer.cpp | 12 +- libraries/gpu/src/gpu/Texture.cpp | 261 +++-- libraries/gpu/src/gpu/Texture.h | 178 +-- libraries/gpu/src/gpu/Texture_ktx.cpp | 289 ----- libraries/ktx/CMakeLists.txt | 3 - libraries/ktx/src/ktx/KTX.cpp | 165 --- libraries/ktx/src/ktx/KTX.h | 494 -------- libraries/ktx/src/ktx/Reader.cpp | 195 ---- libraries/ktx/src/ktx/Writer.cpp | 171 --- libraries/model-networking/CMakeLists.txt | 2 +- .../src/model-networking/KTXCache.cpp | 47 - .../src/model-networking/KTXCache.h | 51 - .../src/model-networking/TextureCache.cpp | 492 +++----- .../src/model-networking/TextureCache.h | 26 +- libraries/model/CMakeLists.txt | 2 +- libraries/model/src/model/Geometry.cpp | 112 +- libraries/model/src/model/Geometry.h | 14 +- libraries/model/src/model/TextureMap.cpp | 149 +-- libraries/model/src/model/TextureMap.h | 3 +- libraries/networking/src/Assignment.cpp | 1 + libraries/networking/src/FileCache.cpp | 243 ---- libraries/networking/src/FileCache.h | 158 --- libraries/networking/src/NodePermissions.h | 48 +- libraries/networking/src/udt/PacketQueue.cpp | 23 +- libraries/networking/src/udt/PacketQueue.h | 5 +- libraries/physics/src/EntityMotionState.cpp | 24 +- libraries/physics/src/EntityMotionState.h | 1 - .../physics/src/PhysicalEntitySimulation.cpp | 18 +- .../physics/src/PhysicalEntitySimulation.h | 5 +- libraries/physics/src/PhysicsEngine.cpp | 2 +- libraries/physics/src/PhysicsEngine.h | 3 +- .../physics/src/ThreadSafeDynamicsWorld.cpp | 27 +- .../physics/src/ThreadSafeDynamicsWorld.h | 4 - libraries/recording/src/recording/Deck.cpp | 5 +- libraries/render-utils/CMakeLists.txt | 2 +- .../render-utils/src/AntialiasingEffect.cpp | 2 +- .../render-utils/src/DeferredFramebuffer.cpp | 10 +- .../src/DeferredLightingEffect.cpp | 4 +- .../render-utils/src/FramebufferCache.cpp | 16 + libraries/render-utils/src/FramebufferCache.h | 5 + libraries/render-utils/src/LightAmbient.slh | 13 +- libraries/render-utils/src/LightingModel.cpp | 10 - libraries/render-utils/src/LightingModel.h | 12 +- libraries/render-utils/src/LightingModel.slh | 9 +- .../render-utils/src/MaterialTextures.slh | 2 +- .../render-utils/src/MeshPartPayload.cpp | 26 +- libraries/render-utils/src/MeshPartPayload.h | 6 +- libraries/render-utils/src/Model.cpp | 18 +- .../render-utils/src/RenderDeferredTask.cpp | 27 +- .../render-utils/src/RenderPipelines.cpp | 2 +- .../render-utils/src/SubsurfaceScattering.cpp | 6 +- .../render-utils/src/SurfaceGeometryPass.cpp | 12 +- libraries/render-utils/src/text/Font.cpp | 3 +- libraries/render/CMakeLists.txt | 2 +- libraries/render/src/render/DrawTask.cpp | 12 +- libraries/render/src/render/DrawTask.h | 4 +- libraries/render/src/render/ShapePipeline.h | 8 +- .../render/src/render/drawItemStatus.slv | 4 +- .../src/AudioScriptingInterface.cpp | 5 + .../src/AudioScriptingInterface.h | 9 +- .../src/BaseScriptEngine.cpp | 130 +-- .../script-engine/src/BaseScriptEngine.h | 67 ++ libraries/script-engine/src/MeshProxy.h | 41 - .../src/ModelScriptingInterface.cpp | 159 --- .../src/ModelScriptingInterface.h | 45 - libraries/script-engine/src/ScriptEngine.cpp | 561 +-------- libraries/script-engine/src/ScriptEngine.h | 37 +- .../script-engine/src/ScriptEngineLogging.cpp | 1 - .../script-engine/src/ScriptEngineLogging.h | 1 - libraries/script-engine/src/ScriptEngines.cpp | 20 +- libraries/script-engine/src/ScriptEngines.h | 10 +- libraries/shared/src/BaseScriptEngine.h | 90 -- libraries/shared/src/HifiConfigVariantMap.cpp | 6 +- libraries/shared/src/PathUtils.cpp | 22 +- libraries/shared/src/PathUtils.h | 7 +- libraries/shared/src/RenderArgs.h | 1 - libraries/shared/src/ServerPathUtils.cpp | 31 + libraries/shared/src/ServerPathUtils.h | 22 + libraries/shared/src/shared/Storage.cpp | 92 -- libraries/shared/src/shared/Storage.h | 82 -- libraries/ui/src/ui/Menu.cpp | 2 +- .../src/OculusLegacyDisplayPlugin.cpp | 2 +- plugins/openvr/src/OpenVrDisplayPlugin.cpp | 8 +- .../developer/libraries/jasmine/hifi-boot.js | 13 +- scripts/developer/tests/.gitignore | 1 - scripts/developer/tests/scaling.png | Bin 3172 -> 0 bytes .../tests/unit_tests/moduleTests/cycles/a.js | 10 - .../tests/unit_tests/moduleTests/cycles/b.js | 10 - .../unit_tests/moduleTests/cycles/main.js | 17 - .../entity/entityConstructorAPIException.js | 13 - .../entity/entityConstructorModule.js | 23 - .../entity/entityConstructorNested.js | 14 - .../entity/entityConstructorNested2.js | 25 - .../entityConstructorRequireException.js | 10 - .../entity/entityPreloadAPIError.js | 13 - .../entity/entityPreloadRequire.js | 11 - .../tests/unit_tests/moduleTests/example.json | 9 - .../moduleTests/exceptions/exception.js | 4 - .../exceptions/exceptionInFunction.js | 38 - .../tests/unit_tests/moduleUnitTests.js | 378 ------ .../developer/tests/unit_tests/package.json | 6 - .../tests/unit_tests/scriptUnitTests.js | 18 +- .../developer/utilities/record/recorder.js | 97 +- .../utilities/render/deferredLighting.qml | 3 +- scripts/modules/vec3.js | 69 -- .../system/assets/images/icon-particles.svg | 29 - .../system/assets/images/icon-point-light.svg | 57 - .../system/assets/images/icon-spot-light.svg | 37 - scripts/system/controllers/teleport.js | 19 +- ...oggleAdvancedMovementForHandControllers.js | 13 +- scripts/system/edit.js | 136 +-- .../system/libraries/entitySelectionTool.js | 8 +- ...erlayManager.js => lightOverlayManager.js} | 51 +- scripts/system/libraries/toolBars.js | 4 - scripts/tutorials/entity_scripts/sit.js | 230 ++-- tests/ktx/CMakeLists.txt | 15 - tests/ktx/src/main.cpp | 150 --- tests/render-perf/CMakeLists.txt | 2 +- tests/render-perf/src/main.cpp | 1 + tests/render-texture-load/src/main.cpp | 1 - tests/shared/src/StorageTests.cpp | 75 -- tests/shared/src/StorageTests.h | 32 - tools/CMakeLists.txt | 2 - tools/atp-get/CMakeLists.txt | 3 - tools/atp-get/src/ATPGetApp.cpp | 269 ----- tools/atp-get/src/ATPGetApp.h | 52 - tools/atp-get/src/main.cpp | 31 - .../marketplace/boppo/boppoClownEntity.js | 80 -- .../marketplace/boppo/boppoServer.js | 303 ----- .../marketplace/boppo/clownGloveDispenser.js | 154 --- .../marketplace/boppo/createElBoppo.js | 430 ------- .../marketplace/boppo/lookAtEntity.js | 98 -- 246 files changed, 6789 insertions(+), 9700 deletions(-) create mode 100644 interface/resources/html/img/devices.png create mode 100644 interface/resources/html/img/models.png create mode 100644 interface/resources/html/img/move.png create mode 100644 interface/resources/html/img/run-script.png create mode 100644 interface/resources/html/img/talk.png create mode 100644 interface/resources/html/img/write-script.png create mode 100644 interface/resources/html/interface-welcome.html create mode 100644 interface/resources/icons/load-script.svg create mode 100644 interface/resources/icons/new-script.svg create mode 100644 interface/resources/icons/save-script.svg create mode 100644 interface/resources/icons/start-script.svg create mode 100644 interface/resources/icons/stop-script.svg create mode 100644 interface/src/ui/CachesSizeDialog.cpp create mode 100644 interface/src/ui/CachesSizeDialog.h create mode 100644 interface/src/ui/DiskCacheEditor.cpp create mode 100644 interface/src/ui/DiskCacheEditor.h create mode 100644 interface/src/ui/ScriptEditBox.cpp create mode 100644 interface/src/ui/ScriptEditBox.h create mode 100644 interface/src/ui/ScriptEditorWidget.cpp create mode 100644 interface/src/ui/ScriptEditorWidget.h create mode 100644 interface/src/ui/ScriptEditorWindow.cpp create mode 100644 interface/src/ui/ScriptEditorWindow.h create mode 100644 interface/src/ui/ScriptLineNumberArea.cpp create mode 100644 interface/src/ui/ScriptLineNumberArea.h create mode 100644 interface/src/ui/ScriptsTableWidget.cpp create mode 100644 interface/src/ui/ScriptsTableWidget.h create mode 100644 interface/ui/scriptEditorWidget.ui create mode 100644 interface/ui/scriptEditorWindow.ui delete mode 100644 libraries/fbx/src/OBJWriter.cpp delete mode 100644 libraries/fbx/src/OBJWriter.h create mode 100644 libraries/gpu-gl/src/gpu/gl/GLTextureTransfer.cpp create mode 100644 libraries/gpu-gl/src/gpu/gl/GLTextureTransfer.h delete mode 100644 libraries/gpu-gl/src/gpu/gl45/GL45BackendVariableTexture.cpp delete mode 100644 libraries/gpu/src/gpu/Texture_ktx.cpp delete mode 100644 libraries/ktx/CMakeLists.txt delete mode 100644 libraries/ktx/src/ktx/KTX.cpp delete mode 100644 libraries/ktx/src/ktx/KTX.h delete mode 100644 libraries/ktx/src/ktx/Reader.cpp delete mode 100644 libraries/ktx/src/ktx/Writer.cpp delete mode 100644 libraries/model-networking/src/model-networking/KTXCache.cpp delete mode 100644 libraries/model-networking/src/model-networking/KTXCache.h delete mode 100644 libraries/networking/src/FileCache.cpp delete mode 100644 libraries/networking/src/FileCache.h rename libraries/{shared => script-engine}/src/BaseScriptEngine.cpp (68%) create mode 100644 libraries/script-engine/src/BaseScriptEngine.h delete mode 100644 libraries/script-engine/src/MeshProxy.h delete mode 100644 libraries/script-engine/src/ModelScriptingInterface.cpp delete mode 100644 libraries/script-engine/src/ModelScriptingInterface.h delete mode 100644 libraries/shared/src/BaseScriptEngine.h create mode 100644 libraries/shared/src/ServerPathUtils.cpp create mode 100644 libraries/shared/src/ServerPathUtils.h delete mode 100644 libraries/shared/src/shared/Storage.cpp delete mode 100644 libraries/shared/src/shared/Storage.h delete mode 100644 scripts/developer/tests/.gitignore delete mode 100644 scripts/developer/tests/scaling.png delete mode 100644 scripts/developer/tests/unit_tests/moduleTests/cycles/a.js delete mode 100644 scripts/developer/tests/unit_tests/moduleTests/cycles/b.js delete mode 100644 scripts/developer/tests/unit_tests/moduleTests/cycles/main.js delete mode 100644 scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorAPIException.js delete mode 100644 scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorModule.js delete mode 100644 scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested.js delete mode 100644 scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested2.js delete mode 100644 scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorRequireException.js delete mode 100644 scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadAPIError.js delete mode 100644 scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadRequire.js delete mode 100644 scripts/developer/tests/unit_tests/moduleTests/example.json delete mode 100644 scripts/developer/tests/unit_tests/moduleTests/exceptions/exception.js delete mode 100644 scripts/developer/tests/unit_tests/moduleTests/exceptions/exceptionInFunction.js delete mode 100644 scripts/developer/tests/unit_tests/moduleUnitTests.js delete mode 100644 scripts/developer/tests/unit_tests/package.json delete mode 100644 scripts/modules/vec3.js delete mode 100644 scripts/system/assets/images/icon-particles.svg delete mode 100644 scripts/system/assets/images/icon-point-light.svg delete mode 100644 scripts/system/assets/images/icon-spot-light.svg rename scripts/system/libraries/{entityIconOverlayManager.js => lightOverlayManager.js} (67%) delete mode 100644 tests/ktx/CMakeLists.txt delete mode 100644 tests/ktx/src/main.cpp delete mode 100644 tests/shared/src/StorageTests.cpp delete mode 100644 tests/shared/src/StorageTests.h delete mode 100644 tools/atp-get/CMakeLists.txt delete mode 100644 tools/atp-get/src/ATPGetApp.cpp delete mode 100644 tools/atp-get/src/ATPGetApp.h delete mode 100644 tools/atp-get/src/main.cpp delete mode 100644 unpublishedScripts/marketplace/boppo/boppoClownEntity.js delete mode 100644 unpublishedScripts/marketplace/boppo/boppoServer.js delete mode 100644 unpublishedScripts/marketplace/boppo/clownGloveDispenser.js delete mode 100644 unpublishedScripts/marketplace/boppo/createElBoppo.js delete mode 100644 unpublishedScripts/marketplace/boppo/lookAtEntity.js diff --git a/BUILD_WIN.md b/BUILD_WIN.md index e37bf27503..45373d3093 100644 --- a/BUILD_WIN.md +++ b/BUILD_WIN.md @@ -1,81 +1,104 @@ -This is a stand-alone guide for creating your first High Fidelity build for Windows 64-bit. +Please read the [general build guide](BUILD.md) for information on dependencies required for all platforms. Only Windows specific instructions are found in this file. -###Step 1. Installing Visual Studio 2013 +Interface can be built as 32 or 64 bit. -If you don't already have the Community or Professional edition of Visual Studio 2013, download and install [Visual Studio Community 2013](https://www.visualstudio.com/en-us/news/releasenotes/vs2013-community-vs). You do not need to install any of the optional components when going through the installer. +###Visual Studio 2013 -Note: Newer versions of Visual Studio are not yet compatible. +You can use the Community or Professional editions of Visual Studio 2013. -###Step 2. Installing CMake +You can start a Visual Studio 2013 command prompt using the shortcut provided in the Visual Studio Tools folder installed as part of Visual Studio 2013. -Download and install the CMake 3.8.0-rc2 "win64-x64 Installer" from the [CMake Website](https://cmake.org/download/). Make sure "Add CMake to system PATH for all users" is checked when going through the installer. +Or you can start a regular command prompt and then run: -###Step 3. Installing Qt + "%VS120COMNTOOLS%\vsvars32.bat" -Download and install the [Qt 5.6.1 Installer](https://download.qt.io/official_releases/qt/5.6/5.6.1-1/qt-opensource-windows-x86-msvc2013_64-5.6.1-1.exe). Please note that the download file is large (850MB) and may take some time. +####Windows SDK 8.1 -Make sure to select all components when going through the installer. +If using Visual Studio 2013 and building as a Visual Studio 2013 project you need the Windows 8 SDK which you should already have as part of installing Visual Studio 2013. You should be able to see it at `C:\Program Files (x86)\Windows Kits\8.1\Lib\winv6.3\um\x86`. -###Step 4. Setting Qt Environment Variable +####nmake -Go to "Control Panel > System > Advanced System Settings > Environment Variables > New..." (or search “Environment Variables” in Start Search). -* Set "Variable name": QT_CMAKE_PREFIX_PATH -* Set "Variable value": `C:\Qt\Qt5.6.1\5.6\msvc2013_64\lib\cmake` +Some of the external projects may require nmake to compile and install. If it is not installed at the location listed below, please ensure that it is in your PATH so CMake can find it when required. -###Step 5. Installing OpenSSL +We expect nmake.exe to be located at the following path. -Download and install the "Win64 OpenSSL v1.0.2k" Installer from [this website](https://slproweb.com/products/Win32OpenSSL.html). - -###Step 6. Running CMake to Generate Build Files - -Run Command Prompt from Start and run the following commands: - cd "%HIFI_DIR%" - mkdir build - cd build - cmake .. -G "Visual Studio 12 Win64" - -Where %HIFI_DIR% is the directory for the highfidelity repository. - -###Step 7. Making a Build - -Open '%HIFI_DIR%\build\hifi.sln' using Visual Studio. - -Change the Solution Configuration (next to the green play button) from "Debug" to "Release" for best performance. - -Run Build > Build Solution. - -###Step 8. Testing Interface - -Create another environment variable (see Step #4) -* Set "Variable name": _NO_DEBUG_HEAP -* Set "Variable value": 1 - -In Visual Studio, right+click "interface" under the Apps folder in Solution Explorer and select "Set as Startup Project". Run Debug > Start Debugging. - -Now, you should have a full build of High Fidelity and be able to run the Interface using Visual Studio. Please check our [Docs](https://wiki.highfidelity.com/wiki/Main_Page) for more information regarding the programming workflow. - -Note: You can also run Interface by launching it from command line or File Explorer from %HIFI_DIR%\build\interface\Release\interface.exe - -###Troubleshooting - -For any problems after Step #6, first try this: -* Delete your locally cloned copy of the highfidelity repository -* Restart your computer -* Redownload the [repository](https://github.com/highfidelity/hifi) -* Restart directions from Step #6 - -####CMake gives you the same error message repeatedly after the build fails - -Remove `CMakeCache.txt` found in the '%HIFI_DIR%\build' directory - -####nmake cannot be found - -Make sure nmake.exe is located at the following path: C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\bin - -If not, add the directory where nmake is located to the PATH environment variable. -####Qt is throwing an error +###Qt +You can use the online installer or the offline installer. If you use the offline installer, be sure to select the "OpenGL" version. -Make sure you have the correct version (5.6.1-1) installed and 'QT_CMAKE_PREFIX_PATH' environment variable is set correctly. +* [Download the online installer](http://www.qt.io/download-open-source/#section-2) + * When it asks you to select components, ONLY select one of the following, 32- or 64-bit to match your build preference: + * Qt > Qt 5.6.1 > **msvc2013 32-bit** + * Qt > Qt 5.6.1 > **msvc2013 64-bit** +* Download the offline installer, 32- or 64-bit to match your build preference: + * [32-bit](https://download.qt.io/official_releases/qt/5.6/5.6.1-1/qt-opensource-windows-x86-msvc2013-5.6.1-1.exe) + * [64-bit](https://download.qt.io/official_releases/qt/5.6/5.6.1-1/qt-opensource-windows-x86-msvc2013_64-5.6.1-1.exe) + +Once Qt is installed, you need to manually configure the following: +* Set the QT_CMAKE_PREFIX_PATH environment variable to your `Qt\5.6.1\msvc2013\lib\cmake` or `Qt\5.6.1\msvc2013_64\lib\cmake` directory. + * You can set an environment variable from Control Panel > System > Advanced System Settings > Environment Variables > New + +###External Libraries + +All libraries should be 32- or 64-bit to match your build preference. + +CMake will need to know where the headers and libraries for required external dependencies are. + +We use CMake's `fixup_bundle` to find the DLLs all of our executable targets require, and then copy them beside the executable in a post-build step. If `fixup_bundle` is having problems finding a DLL, you can fix it manually on your end by adding the folder containing that DLL to your path. Let us know which DLL CMake had trouble finding, as it is possible a tweak to our CMake files is required. + +The recommended route for CMake to find the external dependencies is to place all of the dependencies in one folder and set one ENV variable - HIFI_LIB_DIR. That ENV variable should point to a directory with the following structure: + + root_lib_dir + -> openssl + -> bin + -> include + -> lib + +For many of the external libraries where precompiled binaries are readily available you should be able to simply copy the extracted folder that you get from the download links provided at the top of the guide. Otherwise you may need to build from source and install the built product to this directory. The `root_lib_dir` in the above example can be wherever you choose on your system - as long as the environment variable HIFI_LIB_DIR is set to it. From here on, whenever you see %HIFI_LIB_DIR% you should substitute the directory that you chose. + +####OpenSSL + +Qt will use OpenSSL if it's available, but it doesn't install it, so you must install it separately. + +Your system may already have several versions of the OpenSSL DLL's (ssleay32.dll, libeay32.dll) lying around, but they may be the wrong version. If these DLL's are in the PATH then QT will try to use them, and if they're the wrong version then you will see the following errors in the console: + + QSslSocket: cannot resolve TLSv1_1_client_method + QSslSocket: cannot resolve TLSv1_2_client_method + QSslSocket: cannot resolve TLSv1_1_server_method + QSslSocket: cannot resolve TLSv1_2_server_method + QSslSocket: cannot resolve SSL_select_next_proto + QSslSocket: cannot resolve SSL_CTX_set_next_proto_select_cb + QSslSocket: cannot resolve SSL_get0_next_proto_negotiated + +To prevent these problems, install OpenSSL yourself. Download one of the following binary packages [from this website](https://slproweb.com/products/Win32OpenSSL.html): +* Win32 OpenSSL v1.0.1q +* Win64 OpenSSL v1.0.1q + +Install OpenSSL into the Windows system directory, to make sure that Qt uses the version that you've just installed, and not some other version. + +###Build High Fidelity using Visual Studio +Follow the same build steps from the CMake section of [BUILD.md](BUILD.md), but pass a different generator to CMake. + +For 32-bit builds: + + cmake .. -G "Visual Studio 12" + +For 64-bit builds: + + cmake .. -G "Visual Studio 12 Win64" + +Open %HIFI_DIR%\build\hifi.sln and compile. + +###Running Interface +If you need to debug Interface, you can run interface from within Visual Studio (see the section below). You can also run Interface by launching it from command line or File Explorer from %HIFI_DIR%\build\interface\Debug\interface.exe + +###Debugging Interface +* In the Solution Explorer, right click interface and click Set as StartUp Project +* Set the "Working Directory" for the Interface debugging sessions to the Debug output directory so that your application can load resources. Do this: right click interface and click Properties, choose Debugging from Configuration Properties, set Working Directory to .\Debug +* Now you can run and debug interface through Visual Studio + +For better performance when running debug builds, set the environment variable ```_NO_DEBUG_HEAP``` to ```1``` + +http://preshing.com/20110717/the-windows-heap-is-slow-when-launched-from-the-debugger/ diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index a0c80453e0..be23dcfa25 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -371,39 +371,25 @@ void Agent::executeScript() { using namespace recording; static const FrameType AUDIO_FRAME_TYPE = Frame::registerFrameType(AudioConstants::getAudioFrameName()); Frame::registerFrameHandler(AUDIO_FRAME_TYPE, [this, &scriptedAvatar](Frame::ConstPointer frame) { + const QByteArray& audio = frame->data; static quint16 audioSequenceNumber{ 0 }; - - QByteArray audio(frame->data); - - if (_isNoiseGateEnabled) { - static int numSamples = AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL; - _noiseGate.gateSamples(reinterpret_cast(audio.data()), numSamples); - } - - computeLoudness(&audio, scriptedAvatar); - - // the codec needs a flush frame before sending silent packets, so - // do not send one if the gate closed in this block (eventually this can be crossfaded). - auto packetType = PacketType::MicrophoneAudioNoEcho; - if (scriptedAvatar->getAudioLoudness() == 0.0f && !_noiseGate.closedInLastBlock()) { - packetType = PacketType::SilentAudioFrame; - } - Transform audioTransform; + auto headOrientation = scriptedAvatar->getHeadOrientation(); audioTransform.setTranslation(scriptedAvatar->getPosition()); audioTransform.setRotation(headOrientation); + computeLoudness(&audio, scriptedAvatar); + QByteArray encodedBuffer; if (_encoder) { _encoder->encode(audio, encodedBuffer); } else { encodedBuffer = audio; } - AbstractAudioInterface::emitAudioPacket(encodedBuffer.data(), encodedBuffer.size(), audioSequenceNumber, audioTransform, scriptedAvatar->getPosition(), glm::vec3(0), - packetType, _selectedCodecName); + PacketType::MicrophoneAudioNoEcho, _selectedCodecName); }); auto avatarHashMap = DependencyManager::set(); @@ -497,14 +483,6 @@ void Agent::setIsListeningToAudioStream(bool isListeningToAudioStream) { _isListeningToAudioStream = isListeningToAudioStream; } -void Agent::setIsNoiseGateEnabled(bool isNoiseGateEnabled) { - if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "setIsNoiseGateEnabled", Q_ARG(bool, isNoiseGateEnabled)); - return; - } - _isNoiseGateEnabled = isNoiseGateEnabled; -} - void Agent::setIsAvatar(bool isAvatar) { // this must happen on Agent's main thread if (QThread::currentThread() != thread()) { diff --git a/assignment-client/src/Agent.h b/assignment-client/src/Agent.h index 620ac8e047..0ce7b71d5d 100644 --- a/assignment-client/src/Agent.h +++ b/assignment-client/src/Agent.h @@ -29,7 +29,6 @@ #include -#include "AudioNoiseGate.h" #include "MixedAudioStream.h" #include "avatars/ScriptableAvatar.h" @@ -39,7 +38,6 @@ class Agent : public ThreadedAssignment { Q_PROPERTY(bool isAvatar READ isAvatar WRITE setIsAvatar) Q_PROPERTY(bool isPlayingAvatarSound READ isPlayingAvatarSound) Q_PROPERTY(bool isListeningToAudioStream READ isListeningToAudioStream WRITE setIsListeningToAudioStream) - Q_PROPERTY(bool isNoiseGateEnabled READ isNoiseGateEnabled WRITE setIsNoiseGateEnabled) Q_PROPERTY(float lastReceivedAudioLoudness READ getLastReceivedAudioLoudness) Q_PROPERTY(QUuid sessionUUID READ getSessionUUID) @@ -54,9 +52,6 @@ public: bool isListeningToAudioStream() const { return _isListeningToAudioStream; } void setIsListeningToAudioStream(bool isListeningToAudioStream); - bool isNoiseGateEnabled() const { return _isNoiseGateEnabled; } - void setIsNoiseGateEnabled(bool isNoiseGateEnabled); - float getLastReceivedAudioLoudness() const { return _lastReceivedAudioLoudness; } QUuid getSessionUUID() const; @@ -111,9 +106,6 @@ private: QTimer* _avatarIdentityTimer = nullptr; QHash _outgoingScriptAudioSequenceNumbers; - AudioNoiseGate _noiseGate; - bool _isNoiseGateEnabled { false }; - CodecPluginPointer _codec; QString _selectedCodecName; Encoder* _encoder { nullptr }; diff --git a/assignment-client/src/assets/AssetServer.cpp b/assignment-client/src/assets/AssetServer.cpp index 3886ff8d92..82dd23a9de 100644 --- a/assignment-client/src/assets/AssetServer.cpp +++ b/assignment-client/src/assets/AssetServer.cpp @@ -24,7 +24,7 @@ #include #include -#include +#include #include "NetworkLogging.h" #include "NodeType.h" @@ -162,7 +162,7 @@ void AssetServer::completeSetup() { if (assetsPath.isRelative()) { // if the domain settings passed us a relative path, make an absolute path that is relative to the // default data directory - absoluteFilePath = PathUtils::getAppDataFilePath("assets/" + assetsPathString); + absoluteFilePath = ServerPathUtils::getDataFilePath("assets/" + assetsPathString); } _resourcesDirectory = QDir(absoluteFilePath); diff --git a/assignment-client/src/octree/OctreeServer.cpp b/assignment-client/src/octree/OctreeServer.cpp index f2dbe5d1d2..2eee2ee229 100644 --- a/assignment-client/src/octree/OctreeServer.cpp +++ b/assignment-client/src/octree/OctreeServer.cpp @@ -29,7 +29,7 @@ #include "OctreeQueryNode.h" #include "OctreeServerConsts.h" #include -#include +#include #include int OctreeServer::_clientCount = 0; @@ -279,7 +279,8 @@ OctreeServer::~OctreeServer() { void OctreeServer::initHTTPManager(int port) { // setup the embedded web server - QString documentRoot = QString("%1/web").arg(PathUtils::getAppDataPath()); + + QString documentRoot = QString("%1/web").arg(ServerPathUtils::getDataDirectory()); // setup an httpManager with us as the request handler and the parent _httpManager = new HTTPManager(QHostAddress::AnyIPv4, port, documentRoot, this, this); @@ -1178,7 +1179,7 @@ void OctreeServer::domainSettingsRequestComplete() { if (persistPath.isRelative()) { // if the domain settings passed us a relative path, make an absolute path that is relative to the // default data directory - persistAbsoluteFilePath = QDir(PathUtils::getAppDataFilePath("entities/")).absoluteFilePath(_persistFilePath); + persistAbsoluteFilePath = QDir(ServerPathUtils::getDataFilePath("entities/")).absoluteFilePath(_persistFilePath); } static const QString ENTITY_PERSIST_EXTENSION = ".json.gz"; @@ -1244,7 +1245,7 @@ void OctreeServer::domainSettingsRequestComplete() { QDir backupDirectory { _backupDirectoryPath }; QString absoluteBackupDirectory; if (backupDirectory.isRelative()) { - absoluteBackupDirectory = QDir(PathUtils::getAppDataFilePath("entities/")).absoluteFilePath(_backupDirectoryPath); + absoluteBackupDirectory = QDir(ServerPathUtils::getDataFilePath("entities/")).absoluteFilePath(_backupDirectoryPath); absoluteBackupDirectory = QDir(absoluteBackupDirectory).absolutePath(); } else { absoluteBackupDirectory = backupDirectory.absolutePath(); diff --git a/assignment-client/src/scripts/EntityScriptServer.cpp b/assignment-client/src/scripts/EntityScriptServer.cpp index 954c25a342..47071b10b7 100644 --- a/assignment-client/src/scripts/EntityScriptServer.cpp +++ b/assignment-client/src/scripts/EntityScriptServer.cpp @@ -58,8 +58,6 @@ EntityScriptServer::EntityScriptServer(ReceivedMessage& message) : ThreadedAssig DependencyManager::registerInheritance(); - DependencyManager::set(); - DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); @@ -326,26 +324,7 @@ void EntityScriptServer::nodeActivated(SharedNodePointer activatedNode) { void EntityScriptServer::nodeKilled(SharedNodePointer killedNode) { switch (killedNode->getType()) { case NodeType::EntityServer: { - // Before we clear, make sure this was our only entity server. - // Otherwise we're assuming that we have "trading" entity servers - // (an old one going away and a new one coming onboard) - // and that we shouldn't clear here because we're still doing work. - bool hasAnotherEntityServer = false; - auto nodeList = DependencyManager::get(); - - nodeList->eachNodeBreakable([&hasAnotherEntityServer, &killedNode](const SharedNodePointer& node){ - if (node->getType() == NodeType::EntityServer && node->getUUID() != killedNode->getUUID()) { - // we're talking to > 1 entity servers, we know we won't clear - hasAnotherEntityServer = true; - return false; - } - - return true; - }); - - if (!hasAnotherEntityServer) { - clear(); - } + clear(); break; } @@ -416,8 +395,7 @@ void EntityScriptServer::selectAudioFormat(const QString& selectedCodecName) { void EntityScriptServer::resetEntitiesScriptEngine() { auto engineName = QString("about:Entities %1").arg(++_entitiesScriptEngineCount); - auto newEngine = QSharedPointer(new ScriptEngine(ScriptEngine::ENTITY_SERVER_SCRIPT, NO_SCRIPT, engineName), - &ScriptEngine::deleteLater); + auto newEngine = QSharedPointer(new ScriptEngine(ScriptEngine::ENTITY_SERVER_SCRIPT, NO_SCRIPT, engineName)); auto webSocketServerConstructorValue = newEngine->newFunction(WebSocketServerClass::constructor); newEngine->globalObject().setProperty("WebSocketServer", webSocketServerConstructorValue); @@ -477,13 +455,13 @@ void EntityScriptServer::addingEntity(const EntityItemID& entityID) { void EntityScriptServer::deletingEntity(const EntityItemID& entityID) { if (_entityViewer.getTree() && !_shuttingDown && _entitiesScriptEngine) { - _entitiesScriptEngine->unloadEntityScript(entityID, true); + _entitiesScriptEngine->unloadEntityScript(entityID); } } void EntityScriptServer::entityServerScriptChanging(const EntityItemID& entityID, const bool reload) { if (_entityViewer.getTree() && !_shuttingDown) { - _entitiesScriptEngine->unloadEntityScript(entityID, true); + _entitiesScriptEngine->unloadEntityScript(entityID); checkAndCallPreload(entityID, reload); } } diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 620b11d8ad..c741c22b83 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -38,7 +38,7 @@ #include #include #include -#include +#include #include #include "DomainServerNodeData.h" @@ -1618,7 +1618,7 @@ QJsonObject DomainServer::jsonObjectForNode(const SharedNodePointer& node) { QDir pathForAssignmentScriptsDirectory() { static const QString SCRIPTS_DIRECTORY_NAME = "/scripts/"; - QDir directory(PathUtils::getAppDataPath() + SCRIPTS_DIRECTORY_NAME); + QDir directory(ServerPathUtils::getDataDirectory() + SCRIPTS_DIRECTORY_NAME); if (!directory.exists()) { directory.mkpath("."); qInfo() << "Created path to " << directory.path(); diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index d6b57b450a..661a6213b8 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -246,13 +246,10 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList _agentPermissions[editorKey]->set(NodePermissions::Permission::canAdjustLocks); } - std::list> permissionsSets{ - _standardAgentPermissions.get(), - _agentPermissions.get() - }; + QList> permissionsSets; + permissionsSets << _standardAgentPermissions.get() << _agentPermissions.get(); foreach (auto permissionsSet, permissionsSets) { - for (auto entry : permissionsSet) { - const auto& userKey = entry.first; + foreach (NodePermissionsKey userKey, permissionsSet.keys()) { if (onlyEditorsAreRezzers) { if (permissionsSet[userKey]->can(NodePermissions::Permission::canAdjustLocks)) { permissionsSet[userKey]->set(NodePermissions::Permission::canRezPermanentEntities); @@ -303,6 +300,7 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList } QVariantMap& DomainServerSettingsManager::getDescriptorsMap() { + static const QString DESCRIPTORS{ "descriptors" }; auto& settingsMap = getSettingsMap(); @@ -1357,12 +1355,18 @@ QStringList DomainServerSettingsManager::getAllKnownGroupNames() { // extract all the group names from the group-permissions and group-forbiddens settings QSet result; - for (const auto& entry : _groupPermissions.get()) { - result += entry.first.first; + QHashIterator i(_groupPermissions.get()); + while (i.hasNext()) { + i.next(); + NodePermissionsKey key = i.key(); + result += key.first; } - for (const auto& entry : _groupForbiddens.get()) { - result += entry.first.first; + QHashIterator j(_groupForbiddens.get()); + while (j.hasNext()) { + j.next(); + NodePermissionsKey key = j.key(); + result += key.first; } return result.toList(); @@ -1373,17 +1377,20 @@ bool DomainServerSettingsManager::setGroupID(const QString& groupName, const QUu _groupIDs[groupName.toLower()] = groupID; _groupNames[groupID] = groupName; - - for (const auto& entry : _groupPermissions.get()) { - auto& perms = entry.second; + QHashIterator i(_groupPermissions.get()); + while (i.hasNext()) { + i.next(); + NodePermissionsPointer perms = i.value(); if (perms->getID().toLower() == groupName.toLower() && !perms->isGroup()) { changed = true; perms->setGroupID(groupID); } } - for (const auto& entry : _groupForbiddens.get()) { - auto& perms = entry.second; + QHashIterator j(_groupForbiddens.get()); + while (j.hasNext()) { + j.next(); + NodePermissionsPointer perms = j.value(); if (perms->getID().toLower() == groupName.toLower() && !perms->isGroup()) { changed = true; perms->setGroupID(groupID); diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index 87048d752c..dbc484d0b9 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -189,7 +189,7 @@ endif() # link required hifi libraries link_hifi_libraries( - shared octree ktx gpu gl gpu-gl procedural model render + shared octree gpu gl gpu-gl procedural model render recording fbx networking model-networking entities avatars audio audio-client animation script-engine physics render-utils entities-renderer ui auto-updater diff --git a/interface/resources/controllers/standard.json b/interface/resources/controllers/standard.json index 9e3b2f4d13..04a3f560b6 100644 --- a/interface/resources/controllers/standard.json +++ b/interface/resources/controllers/standard.json @@ -2,27 +2,7 @@ "name": "Standard to Action", "channels": [ { "from": "Standard.LY", "to": "Actions.TranslateZ" }, - - { "from": "Standard.LX", - "when": [ - "Application.InHMD", "!Application.AdvancedMovement", - "Application.SnapTurn", "!Standard.RX" - ], - "to": "Actions.StepYaw", - "filters": - [ - { "type": "deadZone", "min": 0.15 }, - "constrainToInteger", - { "type": "pulse", "interval": 0.25 }, - { "type": "scale", "scale": 22.5 } - ] - }, - { "from": "Standard.LX", "to": "Actions.TranslateX", - "when": [ "Application.AdvancedMovement" ] - }, - { "from": "Standard.LX", "to": "Actions.Yaw", - "when": [ "!Application.AdvancedMovement", "!Application.SnapTurn" ] - }, + { "from": "Standard.LX", "to": "Actions.TranslateX" }, { "from": "Standard.RX", "when": [ "Application.InHMD", "Application.SnapTurn" ], @@ -35,29 +15,29 @@ { "type": "scale", "scale": 22.5 } ] }, - { "from": "Standard.RX", "to": "Actions.Yaw", - "when": [ "!Application.SnapTurn" ] - }, - { "from": "Standard.RY", - "when": "Application.Grounded", - "to": "Actions.Up", - "filters": + { "from": "Standard.RX", "to": "Actions.Yaw" }, + { "from": "Standard.RY", + "when": "Application.Grounded", + "to": "Actions.Up", + "filters": [ { "type": "deadZone", "min": 0.6 }, "invert" ] - }, + }, - { "from": "Standard.RY", "to": "Actions.Up", "filters": "invert"}, + { "from": "Standard.RY", "to": "Actions.Up", "filters": "invert"}, { "from": "Standard.Back", "to": "Actions.CycleCamera" }, { "from": "Standard.Start", "to": "Actions.ContextMenu" }, - { "from": "Standard.LT", "to": "Actions.LeftHandClick" }, + { "from": "Standard.LT", "to": "Actions.LeftHandClick" }, { "from": "Standard.RT", "to": "Actions.RightHandClick" }, - { "from": "Standard.LeftHand", "to": "Actions.LeftHand" }, + { "from": "Standard.LeftHand", "to": "Actions.LeftHand" }, { "from": "Standard.RightHand", "to": "Actions.RightHand" } ] } + + diff --git a/interface/resources/html/img/devices.png b/interface/resources/html/img/devices.png new file mode 100644 index 0000000000000000000000000000000000000000..fc4231e96e25732a0659c911e7c15ded5b54911b GIT binary patch literal 7492 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 literal 0 HcmV?d00001 diff --git a/interface/resources/html/img/models.png b/interface/resources/html/img/models.png new file mode 100644 index 0000000000000000000000000000000000000000..b09c36011d540759da5326ed70bda97592e9636b GIT binary patch literal 8664 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 literal 0 HcmV?d00001 diff --git a/interface/resources/html/img/run-script.png b/interface/resources/html/img/run-script.png new file mode 100644 index 0000000000000000000000000000000000000000..941b8ee9f13664fc2b2ec51a75273ba3e553d3a5 GIT binary patch literal 4873 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 literal 0 HcmV?d00001 diff --git a/interface/resources/html/img/write-script.png b/interface/resources/html/img/write-script.png new file mode 100644 index 0000000000000000000000000000000000000000..dae97e59b14bae50013b583cd9cb3b5bdf6ad464 GIT binary patch literal 2006 zcmb`HX*Ao38pi)h(Ww^WC~;|$Yo=Oy6}h!H%2>+{#aNnJBh9EKC6-vKrnL4lqlR8f z+CxLd)*4blsO1QWsHU;S(vqMgmZY&S_c%TCW#-;9=gheuo-gltK0NP--!H=jVRz~) zwXXmGIOSk(?Fs;Z5CDKBS!n`!Um!t4q*Wpv@1Fw zC@!KOZ4Ll3cn51scl-o@sspQp0s1~_21*qu6P+5Qx8)(@64A${yN;nQG-&a!vK46|YxiCC+UCW# z?78c1pDn_!(s`qA{2QK}Osh^O6w)&nq|Y#8K%!6GZ2QSKTb`%$ z3fZ>{+WbzhbKE8?t5!s}?(f#PZ^l`Cy89ua>7VGH@y;Zlu%JU%-FpS2``0zP)S?M~ zYMcE+3MG7Uj=OMBeYTuBMD$bP8*mLO*t0C(S41!!jdI!=H4CS}wHew%@uOnH0Z!6q zos_8@nng~FU`KAV#e8;03_6sDa8IZg`}c(Z%?KG9_pr}Qda$=!#5pMW2uHg4cCb2Rtjhd*rqmR@Ph0qaA_#**$5H#z#5Q?MJ=)Ym$FZY9?% zS|;<*8MQ}Nr2o58>dbfJE24Op%Qi~X%CMlTv`)|WljBXHaM;DiJZ4^&B916Q8tH`B z-RQPdyR4W#97+6;nfFACb8g?Ma&$S41a8QYlLaXlK!T2Pm6n`mNlVU?fJ$0PA0<^z z7IYK^1HETh;ov5s#Q3_pMbh3JFLrfW89PH2b8Kmi@!LWkWApCXhc|bgSl5s0FiXum zOH6P?>2-|Y#)3HGd^xH%B$d%Dc>UAX!Tv6x z))ph)9&g0hbj*MckS!%+&ZVZ86Ki7DK>eB|4uq?38_etv!)W7hRZ-DI{NAdSd0U7JdT(r6szh|#Qp-YgZ1M|+?autgHT9)N;V z*=+WZE+#tuUAJQfygZURO})$U1FYaQ#g_IR^PIiC=Z=LB^;7cX?RSkMaK*8%=h7iu z$&u;eD}s+&3PSZCR;TJP(Ld&?*l!q;$yn^<>-AoQo`O)_u}9EtJ04EOEg&L9^MlNm z5eV1hasGwKu@{1_2Pb>;wVWSXVm4G{EX(7_nALToi@caH&RYn`95W@l$;-i zzxg}V%;`a%N@yzc?8*kM#E35mz2wCZcn2UTS7|4!+U+>gA*tq?>zzglk-I){VpB1! zv@IRN)o`N7;Ek}O`C$`Ii$&tzr`v{YKBS2?pR^Js(v*Jz`me?trK~fp%{Z8;)?)BvYwS>KaG-Vg!;)==52-4{=z4A@{ucqO zr$jL4JyU49Nq10&i%vjed#)3d+T8q2FnBT)9piD=py#!GLdYRlBx)gCVJAACE-2@9 z_|1WtF~PR8^M)a_FACc|?wxgSmv$j-rr)dWLl4yybP%YO0g=3!ZBa!thC}mtHPA*# zi(Yc}ks^)!S2T~f{lcyUH02Assd8V~RWSGh_g^XeM*g3w;SU8j + + + + + Welcome to Interface + + + + + +
+
+

Move around

+ Move around +

+ Move around with WASD & fly
+ up or down with E & C.
+ Cmnd/Ctrl+G will send you
+ home. Hitting Enter will let you
+ teleport to a user or location. +

+
+
+

Listen & talk

+ Talk +

+ Use your best headphones
+ and microphone for high
+ fidelity audio. +

+
+
+

Connect devices

+ Connect devices +

+ Have an Oculus Rift, a Razer
+ Hydra, or a PrimeSense 3D
+ camera? We support them all. +

+
+
+

Run a script

+ Run a script +

+ Cmnd/Cntrl+J will launch a
+ Running Scripts dialog to help
+ manage your scripts and search
+ for new ones to run. +

+
+
+

Script something

+ Write a script +

+ Write a script; we're always
+ adding new features.
+ Cmnd/Cntrl+J will launch a
+ Running Scripts dialog to help
+ manage your scripts. +

+
+
+

Import models

+ Import models +

+ Use the edit.js script to
+ add FBX models in-world. You
+ can use grids and fine tune
+ placement-related parameters
+ with ease. +

+
+
+
+

Read the docs

+

+ We are writing documentation on
+ just about everything. Please,
+ devour all we've written and make
+ suggestions where necessary.
+ Documentation is always at
+ docs.highfidelity.com +

+
+
+
+ + + + + diff --git a/interface/resources/icons/load-script.svg b/interface/resources/icons/load-script.svg new file mode 100644 index 0000000000..21be61c321 --- /dev/null +++ b/interface/resources/icons/load-script.svg @@ -0,0 +1,125 @@ + + + + + + + + + + image/svg+xml + + + + + T.Hofmeister + + + + + + + + + + + + + + + + + + + + + + diff --git a/interface/resources/icons/new-script.svg b/interface/resources/icons/new-script.svg new file mode 100644 index 0000000000..f68fcfa967 --- /dev/null +++ b/interface/resources/icons/new-script.svg @@ -0,0 +1,129 @@ + + + + + + + + + + image/svg+xml + + + + + T.Hofmeister + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/interface/resources/icons/save-script.svg b/interface/resources/icons/save-script.svg new file mode 100644 index 0000000000..04d41b8302 --- /dev/null +++ b/interface/resources/icons/save-script.svg @@ -0,0 +1,674 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + T.Hofmeister + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/interface/resources/icons/start-script.svg b/interface/resources/icons/start-script.svg new file mode 100644 index 0000000000..994eb61efe --- /dev/null +++ b/interface/resources/icons/start-script.svg @@ -0,0 +1,550 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + Maximillian Merlin + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/interface/resources/icons/stop-script.svg b/interface/resources/icons/stop-script.svg new file mode 100644 index 0000000000..31cdcee749 --- /dev/null +++ b/interface/resources/icons/stop-script.svg @@ -0,0 +1,163 @@ + + + + + + + + + + image/svg+xml + + + + + Maximillian Merlin + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/interface/resources/qml/AvatarInputs.qml b/interface/resources/qml/AvatarInputs.qml index 28f3c0c7b9..384504aaa0 100644 --- a/interface/resources/qml/AvatarInputs.qml +++ b/interface/resources/qml/AvatarInputs.qml @@ -15,11 +15,12 @@ import Qt.labs.settings 1.0 Hifi.AvatarInputs { id: root objectName: "AvatarInputs" - width: rootWidth - height: controls.height + width: mirrorWidth + height: controls.height + mirror.height x: 10; y: 5 - readonly property int rootWidth: 265 + readonly property int mirrorHeight: 215 + readonly property int mirrorWidth: 265 readonly property int iconSize: 24 readonly property int iconPadding: 5 @@ -38,15 +39,61 @@ Hifi.AvatarInputs { anchors.fill: parent } + Item { + id: mirror + width: root.mirrorWidth + height: root.mirrorVisible ? root.mirrorHeight : 0 + visible: root.mirrorVisible + anchors.left: parent.left + clip: true + + Image { + id: closeMirror + visible: hover.containsMouse + width: root.iconSize + height: root.iconSize + anchors.top: parent.top + anchors.topMargin: root.iconPadding + anchors.left: parent.left + anchors.leftMargin: root.iconPadding + source: "../images/close.svg" + MouseArea { + anchors.fill: parent + onClicked: { + root.closeMirror(); + } + } + } + + Image { + id: zoomIn + visible: hover.containsMouse + width: root.iconSize + height: root.iconSize + anchors.bottom: parent.bottom + anchors.bottomMargin: root.iconPadding + anchors.left: parent.left + anchors.leftMargin: root.iconPadding + source: root.mirrorZoomed ? "../images/minus.svg" : "../images/plus.svg" + MouseArea { + anchors.fill: parent + onClicked: { + root.toggleZoom(); + } + } + } + } + Item { id: controls - width: root.rootWidth + width: root.mirrorWidth height: 44 visible: root.showAudioTools + anchors.top: mirror.bottom Rectangle { anchors.fill: parent - color: "#00000000" + color: root.mirrorVisible ? (root.audioClipping ? "red" : "#696969") : "#00000000" Item { id: audioMeter diff --git a/interface/resources/qml/Stats.qml b/interface/resources/qml/Stats.qml index 17e6578e4d..564c74b526 100644 --- a/interface/resources/qml/Stats.qml +++ b/interface/resources/qml/Stats.qml @@ -198,7 +198,7 @@ Item { } StatText { visible: root.expanded; - text: "Audio Out Mic: " + root.audioOutboundPPS + " pps, " + + text: "Audio Out Mic: " + root.audioMicOutboundPPS + " pps, " + "Silent: " + root.audioSilentOutboundPPS + " pps"; } StatText { @@ -266,7 +266,7 @@ Item { text: "GPU Textures: "; } StatText { - text: " Pressure State: " + root.gpuTextureMemoryPressureState; + text: " Sparse Enabled: " + (0 == root.gpuSparseTextureEnabled ? "false" : "true"); } StatText { text: " Count: " + root.gpuTextures; @@ -278,10 +278,14 @@ Item { text: " Decimated: " + root.decimatedTextureCount; } StatText { - text: " Pending Transfer: " + root.texturePendingTransfers + " MB"; + text: " Sparse Count: " + root.gpuTexturesSparse; + visible: 0 != root.gpuSparseTextureEnabled; } StatText { - text: " Resource Memory: " + root.gpuTextureMemory + " MB"; + text: " Virtual Memory: " + root.gpuTextureVirtualMemory + " MB"; + } + StatText { + text: " Commited Memory: " + root.gpuTextureMemory + " MB"; } StatText { text: " Framebuffer Memory: " + root.gpuTextureFramebufferMemory + " MB"; diff --git a/interface/resources/styles/log_dialog.qss b/interface/resources/styles/log_dialog.qss index d3ae4e0a00..1fc4df0717 100644 --- a/interface/resources/styles/log_dialog.qss +++ b/interface/resources/styles/log_dialog.qss @@ -1,6 +1,6 @@ QPlainTextEdit { - font-family: Inconsolata, Consolas, Courier New, monospace; + font-family: Inconsolata, Lucida Console, Andale Mono, Monaco; font-size: 16px; padding-left: 28px; padding-top: 7px; @@ -11,7 +11,7 @@ QPlainTextEdit { } QLineEdit { - font-family: Inconsolata, Consolas, Courier New, monospace; + font-family: Inconsolata, Lucida Console, Andale Mono, Monaco; padding-left: 7px; background-color: #CCCCCC; border-width: 0; diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index f1e771866f..1bb4c64884 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -177,8 +177,6 @@ #include "FrameTimingsScriptingInterface.h" #include #include -#include -#include // On Windows PC, NVidia Optimus laptop, we want to enable NVIDIA GPU // FIXME seems to be broken. @@ -215,10 +213,18 @@ static const QString FBX_EXTENSION = ".fbx"; static const QString OBJ_EXTENSION = ".obj"; static const QString AVA_JSON_EXTENSION = ".ava.json"; +static const int MIRROR_VIEW_TOP_PADDING = 5; +static const int MIRROR_VIEW_LEFT_PADDING = 10; +static const int MIRROR_VIEW_WIDTH = 265; +static const int MIRROR_VIEW_HEIGHT = 215; static const float MIRROR_FULLSCREEN_DISTANCE = 0.389f; +static const float MIRROR_REARVIEW_DISTANCE = 0.722f; +static const float MIRROR_REARVIEW_BODY_DISTANCE = 2.56f; +static const float MIRROR_FIELD_OF_VIEW = 30.0f; static const quint64 TOO_LONG_SINCE_LAST_SEND_DOWNSTREAM_AUDIO_STATS = 1 * USECS_PER_SECOND; +static const QString INFO_WELCOME_PATH = "html/interface-welcome.html"; static const QString INFO_EDIT_ENTITIES_PATH = "html/edit-commands.html"; static const QString INFO_HELP_PATH = "html/help.html"; @@ -417,7 +423,6 @@ static const QString STATE_CAMERA_THIRD_PERSON = "CameraThirdPerson"; static const QString STATE_CAMERA_ENTITY = "CameraEntity"; static const QString STATE_CAMERA_INDEPENDENT = "CameraIndependent"; static const QString STATE_SNAP_TURN = "SnapTurn"; -static const QString STATE_ADVANCED_MOVEMENT_CONTROLS = "AdvancedMovement"; static const QString STATE_GROUNDED = "Grounded"; static const QString STATE_NAV_FOCUSED = "NavigationFocused"; @@ -508,7 +513,7 @@ bool setupEssentials(int& argc, char** argv) { DependencyManager::set(); controller::StateController::setStateVariables({ { STATE_IN_HMD, STATE_CAMERA_FULL_SCREEN_MIRROR, STATE_CAMERA_FIRST_PERSON, STATE_CAMERA_THIRD_PERSON, STATE_CAMERA_ENTITY, STATE_CAMERA_INDEPENDENT, - STATE_SNAP_TURN, STATE_ADVANCED_MOVEMENT_CONTROLS, STATE_GROUNDED, STATE_NAV_FOCUSED } }); + STATE_SNAP_TURN, STATE_GROUNDED, STATE_NAV_FOCUSED } }); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); @@ -560,6 +565,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo _entityClipboardRenderer(false, this, this), _entityClipboard(new EntityTree()), _lastQueriedTime(usecTimestampNow()), + _mirrorViewRect(QRect(MIRROR_VIEW_LEFT_PADDING, MIRROR_VIEW_TOP_PADDING, MIRROR_VIEW_WIDTH, MIRROR_VIEW_HEIGHT)), _previousScriptLocation("LastScriptLocation", DESKTOP_LOCATION), _fieldOfView("fieldOfView", DEFAULT_FIELD_OF_VIEW_DEGREES), _hmdTabletScale("hmdTabletScale", DEFAULT_HMD_TABLET_SCALE_PERCENT), @@ -740,24 +746,23 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo } }); - auto audioScriptingInterface = DependencyManager::set(); + auto& audioScriptingInterface = AudioScriptingInterface::getInstance(); connect(audioThread, &QThread::started, audioIO.data(), &AudioClient::start); connect(audioIO.data(), &AudioClient::destroyed, audioThread, &QThread::quit); connect(audioThread, &QThread::finished, audioThread, &QThread::deleteLater); connect(audioIO.data(), &AudioClient::muteToggled, this, &Application::audioMuteToggled); - connect(audioIO.data(), &AudioClient::mutedByMixer, audioScriptingInterface.data(), &AudioScriptingInterface::mutedByMixer); - connect(audioIO.data(), &AudioClient::receivedFirstPacket, audioScriptingInterface.data(), &AudioScriptingInterface::receivedFirstPacket); - connect(audioIO.data(), &AudioClient::disconnected, audioScriptingInterface.data(), &AudioScriptingInterface::disconnected); + connect(audioIO.data(), &AudioClient::mutedByMixer, &audioScriptingInterface, &AudioScriptingInterface::mutedByMixer); + connect(audioIO.data(), &AudioClient::receivedFirstPacket, &audioScriptingInterface, &AudioScriptingInterface::receivedFirstPacket); + connect(audioIO.data(), &AudioClient::disconnected, &audioScriptingInterface, &AudioScriptingInterface::disconnected); connect(audioIO.data(), &AudioClient::muteEnvironmentRequested, [](glm::vec3 position, float radius) { auto audioClient = DependencyManager::get(); - auto audioScriptingInterface = DependencyManager::get(); auto myAvatarPosition = DependencyManager::get()->getMyAvatar()->getPosition(); float distance = glm::distance(myAvatarPosition, position); bool shouldMute = !audioClient->isMuted() && (distance < radius); if (shouldMute) { audioClient->toggleMute(); - audioScriptingInterface->environmentMuted(); + AudioScriptingInterface::getInstance().environmentMuted(); } }); @@ -1124,10 +1129,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo _applicationStateDevice->setInputVariant(STATE_SNAP_TURN, []() -> float { return qApp->getMyAvatar()->getSnapTurn() ? 1 : 0; }); - _applicationStateDevice->setInputVariant(STATE_ADVANCED_MOVEMENT_CONTROLS, []() -> float { - return qApp->getMyAvatar()->useAdvancedMovementControls() ? 1 : 0; - }); - _applicationStateDevice->setInputVariant(STATE_GROUNDED, []() -> float { return qApp->getMyAvatar()->getCharacterController()->onGround() ? 1 : 0; }); @@ -1182,10 +1183,10 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo // set the local loopback interface for local sounds AudioInjector::setLocalAudioInterface(audioIO.data()); - audioScriptingInterface->setLocalAudioInterface(audioIO.data()); - connect(audioIO.data(), &AudioClient::noiseGateOpened, audioScriptingInterface.data(), &AudioScriptingInterface::noiseGateOpened); - connect(audioIO.data(), &AudioClient::noiseGateClosed, audioScriptingInterface.data(), &AudioScriptingInterface::noiseGateClosed); - connect(audioIO.data(), &AudioClient::inputReceived, audioScriptingInterface.data(), &AudioScriptingInterface::inputReceived); + AudioScriptingInterface::getInstance().setLocalAudioInterface(audioIO.data()); + connect(audioIO.data(), &AudioClient::noiseGateOpened, &AudioScriptingInterface::getInstance(), &AudioScriptingInterface::noiseGateOpened); + connect(audioIO.data(), &AudioClient::noiseGateClosed, &AudioScriptingInterface::getInstance(), &AudioScriptingInterface::noiseGateClosed); + connect(audioIO.data(), &AudioClient::inputReceived, &AudioScriptingInterface::getInstance(), &AudioScriptingInterface::inputReceived); this->installEventFilter(this); @@ -1444,7 +1445,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo scriptEngines->loadScript(testScript, false); } else { // Get sandbox content set version, if available - auto acDirPath = PathUtils::getAppDataPath() + "../../" + BuildInfo::MODIFIED_ORGANIZATION + "/assignment-client/"; + auto acDirPath = PathUtils::getRootDataDirectory() + BuildInfo::MODIFIED_ORGANIZATION + "/assignment-client/"; auto contentVersionPath = acDirPath + "content-version.txt"; qCDebug(interfaceapp) << "Checking " << contentVersionPath << " for content version"; auto contentVersion = 0; @@ -1950,7 +1951,7 @@ void Application::initializeUi() { // For some reason there is already an "Application" object in the QML context, // though I can't find it. Hence, "ApplicationInterface" rootContext->setContextProperty("ApplicationInterface", this); - rootContext->setContextProperty("Audio", DependencyManager::get().data()); + rootContext->setContextProperty("Audio", &AudioScriptingInterface::getInstance()); rootContext->setContextProperty("AudioStats", DependencyManager::get()->getStats().data()); rootContext->setContextProperty("AudioScope", DependencyManager::get().data()); @@ -2118,6 +2119,21 @@ void Application::paintGL() { batch.resetStages(); }); + auto inputs = AvatarInputs::getInstance(); + if (inputs->mirrorVisible()) { + PerformanceTimer perfTimer("Mirror"); + + renderArgs._renderMode = RenderArgs::MIRROR_RENDER_MODE; + renderArgs._blitFramebuffer = DependencyManager::get()->getSelfieFramebuffer(); + + _mirrorViewRect.moveTo(inputs->x(), inputs->y()); + + renderRearViewMirror(&renderArgs, _mirrorViewRect, inputs->mirrorZoomed()); + + renderArgs._blitFramebuffer.reset(); + renderArgs._renderMode = RenderArgs::DEFAULT_RENDER_MODE; + } + { PerformanceTimer perfTimer("renderOverlay"); // NOTE: There is no batch associated with this renderArgs @@ -2365,6 +2381,10 @@ void Application::setSettingConstrainToolbarPosition(bool setting) { DependencyManager::get()->setConstrainToolbarToCenterX(setting); } +void Application::aboutApp() { + InfoView::show(INFO_WELCOME_PATH); +} + void Application::showHelp() { static const QString HAND_CONTROLLER_NAME_VIVE = "vive"; static const QString HAND_CONTROLLER_NAME_OCULUS_TOUCH = "oculus"; @@ -2746,6 +2766,8 @@ void Application::keyPressEvent(QKeyEvent* event) { case Qt::Key_S: if (isShifted && isMeta && !isOption) { Menu::getInstance()->triggerOption(MenuOption::SuppressShortTimings); + } else if (isOption && !isShifted && !isMeta) { + Menu::getInstance()->triggerOption(MenuOption::ScriptEditor); } else if (!isOption && !isShifted && isMeta) { takeSnapshot(true); } @@ -2864,49 +2886,51 @@ void Application::keyPressEvent(QKeyEvent* event) { break; #endif - case Qt::Key_H: { - // whenever switching to/from full screen mirror from the keyboard, remember - // the state you were in before full screen mirror, and return to that. - auto previousMode = _myCamera.getMode(); - if (previousMode != CAMERA_MODE_MIRROR) { - switch (previousMode) { - case CAMERA_MODE_FIRST_PERSON: - _returnFromFullScreenMirrorTo = MenuOption::FirstPerson; - break; - case CAMERA_MODE_THIRD_PERSON: - _returnFromFullScreenMirrorTo = MenuOption::ThirdPerson; - break; + case Qt::Key_H: + if (isShifted) { + Menu::getInstance()->triggerOption(MenuOption::MiniMirror); + } else { + // whenever switching to/from full screen mirror from the keyboard, remember + // the state you were in before full screen mirror, and return to that. + auto previousMode = _myCamera.getMode(); + if (previousMode != CAMERA_MODE_MIRROR) { + switch (previousMode) { + case CAMERA_MODE_FIRST_PERSON: + _returnFromFullScreenMirrorTo = MenuOption::FirstPerson; + break; + case CAMERA_MODE_THIRD_PERSON: + _returnFromFullScreenMirrorTo = MenuOption::ThirdPerson; + break; - // FIXME - it's not clear that these modes make sense to return to... - case CAMERA_MODE_INDEPENDENT: - _returnFromFullScreenMirrorTo = MenuOption::IndependentMode; - break; - case CAMERA_MODE_ENTITY: - _returnFromFullScreenMirrorTo = MenuOption::CameraEntityMode; - break; + // FIXME - it's not clear that these modes make sense to return to... + case CAMERA_MODE_INDEPENDENT: + _returnFromFullScreenMirrorTo = MenuOption::IndependentMode; + break; + case CAMERA_MODE_ENTITY: + _returnFromFullScreenMirrorTo = MenuOption::CameraEntityMode; + break; - default: - _returnFromFullScreenMirrorTo = MenuOption::ThirdPerson; - break; + default: + _returnFromFullScreenMirrorTo = MenuOption::ThirdPerson; + break; + } } - } - bool isMirrorChecked = Menu::getInstance()->isOptionChecked(MenuOption::FullscreenMirror); - Menu::getInstance()->setIsOptionChecked(MenuOption::FullscreenMirror, !isMirrorChecked); - if (isMirrorChecked) { + bool isMirrorChecked = Menu::getInstance()->isOptionChecked(MenuOption::FullscreenMirror); + Menu::getInstance()->setIsOptionChecked(MenuOption::FullscreenMirror, !isMirrorChecked); + if (isMirrorChecked) { - // if we got here without coming in from a non-Full Screen mirror case, then our - // _returnFromFullScreenMirrorTo is unknown. In that case we'll go to the old - // behavior of returning to ThirdPerson - if (_returnFromFullScreenMirrorTo.isEmpty()) { - _returnFromFullScreenMirrorTo = MenuOption::ThirdPerson; + // if we got here without coming in from a non-Full Screen mirror case, then our + // _returnFromFullScreenMirrorTo is unknown. In that case we'll go to the old + // behavior of returning to ThirdPerson + if (_returnFromFullScreenMirrorTo.isEmpty()) { + _returnFromFullScreenMirrorTo = MenuOption::ThirdPerson; + } + Menu::getInstance()->setIsOptionChecked(_returnFromFullScreenMirrorTo, true); } - Menu::getInstance()->setIsOptionChecked(_returnFromFullScreenMirrorTo, true); + cameraMenuChanged(); } - cameraMenuChanged(); break; - } - case Qt::Key_P: { if (!(isShifted || isMeta || isOption)) { bool isFirstPersonChecked = Menu::getInstance()->isOptionChecked(MenuOption::FirstPerson); @@ -3821,6 +3845,8 @@ void Application::init() { DependencyManager::get()->init(); _myCamera.setMode(CAMERA_MODE_FIRST_PERSON); + _mirrorCamera.setMode(CAMERA_MODE_MIRROR); + _timerStart.start(); _lastTimeUpdated.start(); @@ -4357,16 +4383,16 @@ void Application::update(float deltaTime) { myAvatar->clearDriveKeys(); if (_myCamera.getMode() != CAMERA_MODE_INDEPENDENT) { if (!_controllerScriptingInterface->areActionsCaptured()) { - myAvatar->setDriveKey(MyAvatar::TRANSLATE_Z, -1.0f * userInputMapper->getActionState(controller::Action::TRANSLATE_Z)); - myAvatar->setDriveKey(MyAvatar::TRANSLATE_Y, userInputMapper->getActionState(controller::Action::TRANSLATE_Y)); - myAvatar->setDriveKey(MyAvatar::TRANSLATE_X, userInputMapper->getActionState(controller::Action::TRANSLATE_X)); + myAvatar->setDriveKeys(TRANSLATE_Z, -1.0f * userInputMapper->getActionState(controller::Action::TRANSLATE_Z)); + myAvatar->setDriveKeys(TRANSLATE_Y, userInputMapper->getActionState(controller::Action::TRANSLATE_Y)); + myAvatar->setDriveKeys(TRANSLATE_X, userInputMapper->getActionState(controller::Action::TRANSLATE_X)); if (deltaTime > FLT_EPSILON) { - myAvatar->setDriveKey(MyAvatar::PITCH, -1.0f * userInputMapper->getActionState(controller::Action::PITCH)); - myAvatar->setDriveKey(MyAvatar::YAW, -1.0f * userInputMapper->getActionState(controller::Action::YAW)); - myAvatar->setDriveKey(MyAvatar::STEP_YAW, -1.0f * userInputMapper->getActionState(controller::Action::STEP_YAW)); + myAvatar->setDriveKeys(PITCH, -1.0f * userInputMapper->getActionState(controller::Action::PITCH)); + myAvatar->setDriveKeys(YAW, -1.0f * userInputMapper->getActionState(controller::Action::YAW)); + myAvatar->setDriveKeys(STEP_YAW, -1.0f * userInputMapper->getActionState(controller::Action::STEP_YAW)); } } - myAvatar->setDriveKey(MyAvatar::ZOOM, userInputMapper->getActionState(controller::Action::TRANSLATE_CAMERA_Z)); + myAvatar->setDriveKeys(ZOOM, userInputMapper->getActionState(controller::Action::TRANSLATE_CAMERA_Z)); } controller::Pose leftHandPose = userInputMapper->getPoseState(controller::Action::LEFT_HAND); @@ -4437,12 +4463,9 @@ void Application::update(float deltaTime) { getEntities()->getTree()->withWriteLock([&] { PerformanceTimer perfTimer("handleOutgoingChanges"); - const VectorOfMotionStates& deactivations = _physicsEngine->getDeactivatedMotionStates(); - _entitySimulation->handleDeactivatedMotionStates(deactivations); - - const VectorOfMotionStates& outgoingChanges = _physicsEngine->getChangedMotionStates(); - _entitySimulation->handleChangedMotionStates(outgoingChanges); - avatarManager->handleChangedMotionStates(outgoingChanges); + const VectorOfMotionStates& outgoingChanges = _physicsEngine->getOutgoingChanges(); + _entitySimulation->handleOutgoingChanges(outgoingChanges); + avatarManager->handleOutgoingChanges(outgoingChanges); }); if (!_aboutToQuit) { @@ -5099,6 +5122,58 @@ void Application::displaySide(RenderArgs* renderArgs, Camera& theCamera, bool se activeRenderingThread = nullptr; } +void Application::renderRearViewMirror(RenderArgs* renderArgs, const QRect& region, bool isZoomed) { + auto originalViewport = renderArgs->_viewport; + // Grab current viewport to reset it at the end + + float aspect = (float)region.width() / region.height(); + float fov = MIRROR_FIELD_OF_VIEW; + + auto myAvatar = getMyAvatar(); + + // bool eyeRelativeCamera = false; + if (!isZoomed) { + _mirrorCamera.setPosition(myAvatar->getChestPosition() + + myAvatar->getOrientation() * glm::vec3(0.0f, 0.0f, -1.0f) * MIRROR_REARVIEW_BODY_DISTANCE * myAvatar->getScale()); + + } else { // HEAD zoom level + // FIXME note that the positioning of the camera relative to the avatar can suffer limited + // precision as the user's position moves further away from the origin. Thus at + // /1e7,1e7,1e7 (well outside the buildable volume) the mirror camera veers and sways + // wildly as you rotate your avatar because the floating point values are becoming + // larger, squeezing out the available digits of precision you have available at the + // human scale for camera positioning. + + // Previously there was a hack to correct this using the mechanism of repositioning + // the avatar at the origin of the world for the purposes of rendering the mirror, + // but it resulted in failing to render the avatar's head model in the mirror view + // when in first person mode. Presumably this was because of some missed culling logic + // that was not accounted for in the hack. + + // This was removed in commit 71e59cfa88c6563749594e25494102fe01db38e9 but could be further + // investigated in order to adapt the technique while fixing the head rendering issue, + // but the complexity of the hack suggests that a better approach + _mirrorCamera.setPosition(myAvatar->getDefaultEyePosition() + + myAvatar->getOrientation() * glm::vec3(0.0f, 0.0f, -1.0f) * MIRROR_REARVIEW_DISTANCE * myAvatar->getScale()); + } + _mirrorCamera.setProjection(glm::perspective(glm::radians(fov), aspect, DEFAULT_NEAR_CLIP, DEFAULT_FAR_CLIP)); + _mirrorCamera.setOrientation(myAvatar->getWorldAlignedOrientation() * glm::quat(glm::vec3(0.0f, PI, 0.0f))); + + + // set the bounds of rear mirror view + // the region is in device independent coordinates; must convert to device + float ratio = (float)QApplication::desktop()->windowHandle()->devicePixelRatio() * getRenderResolutionScale(); + int width = region.width() * ratio; + int height = region.height() * ratio; + gpu::Vec4i viewport = gpu::Vec4i(0, 0, width, height); + renderArgs->_viewport = viewport; + + // render rear mirror view + displaySide(renderArgs, _mirrorCamera, true); + + renderArgs->_viewport = originalViewport; +} + void Application::resetSensors(bool andReload) { DependencyManager::get()->reset(); DependencyManager::get()->reset(); @@ -5428,7 +5503,8 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri scriptEngine->registerGlobalObject("Rates", new RatesScriptingInterface(this)); // hook our avatar and avatar hash map object into this script engine - getMyAvatar()->registerMetaTypes(scriptEngine); + scriptEngine->registerGlobalObject("MyAvatar", getMyAvatar().get()); + qScriptRegisterMetaType(scriptEngine, audioListenModeToScriptValue, audioListenModeFromScriptValue); scriptEngine->registerGlobalObject("AvatarList", DependencyManager::get().data()); diff --git a/interface/src/Application.h b/interface/src/Application.h index 7ae4160f8b..98080783a6 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -72,8 +72,6 @@ #include #include -#include - class OffscreenGLCanvas; class GLCanvas; @@ -278,6 +276,8 @@ public: virtual void pushPostUpdateLambda(void* key, std::function func) override; + const QRect& getMirrorViewRect() const { return _mirrorViewRect; } + void updateMyAvatarLookAtPosition(); float getAvatarSimrate() const { return _avatarSimCounter.rate(); } @@ -368,6 +368,7 @@ public slots: void calibrateEyeTracker5Points(); #endif + void aboutApp(); static void showHelp(); void cycleCamera(); @@ -556,6 +557,8 @@ private: int _avatarSimsPerSecondReport {0}; quint64 _lastAvatarSimsPerSecondUpdate {0}; Camera _myCamera; // My view onto the world + Camera _mirrorCamera; // Camera for mirror view + QRect _mirrorViewRect; Setting::Handle _previousScriptLocation; Setting::Handle _fieldOfView; diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index a48ee4e7db..beacbaccab 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -74,6 +74,9 @@ Menu::Menu() { // File > Help addActionToQMenuAndActionHash(fileMenu, MenuOption::Help, 0, qApp, SLOT(showHelp())); + // File > About + addActionToQMenuAndActionHash(fileMenu, MenuOption::AboutApp, 0, qApp, SLOT(aboutApp()), QAction::AboutRole); + // File > Quit addActionToQMenuAndActionHash(fileMenu, MenuOption::Quit, Qt::CTRL | Qt::Key_Q, qApp, SLOT(quit()), QAction::QuitRole); @@ -117,6 +120,11 @@ Menu::Menu() { scriptEngines.data(), SLOT(reloadAllScripts()), QAction::NoRole, UNSPECIFIED_POSITION, "Advanced"); + // Edit > Scripts Editor... [advanced] + addActionToQMenuAndActionHash(editMenu, MenuOption::ScriptEditor, Qt::ALT | Qt::Key_S, + dialogsManager.data(), SLOT(showScriptEditor()), + QAction::NoRole, UNSPECIFIED_POSITION, "Advanced"); + // Edit > Console... [advanced] addActionToQMenuAndActionHash(editMenu, MenuOption::Console, Qt::CTRL | Qt::ALT | Qt::Key_J, DependencyManager::get().data(), @@ -241,6 +249,9 @@ Menu::Menu() { viewMenu->addSeparator(); + // View > Mini Mirror + addCheckableActionToQMenuAndActionHash(viewMenu, MenuOption::MiniMirror, 0, false); + // View > Center Player In View addCheckableActionToQMenuAndActionHash(viewMenu, MenuOption::CenterPlayerInView, 0, true, qApp, SLOT(rotationModeChanged()), @@ -406,9 +417,6 @@ Menu::Menu() { } // Developer > Assets >>> - // Menu item is not currently needed but code should be kept in case it proves useful again at some stage. -//#define WANT_ASSET_MIGRATION -#ifdef WANT_ASSET_MIGRATION MenuWrapper* assetDeveloperMenu = developerMenu->addMenu("Assets"); auto& atpMigrator = ATPAssetMigrator::getInstance(); atpMigrator.setDialogParent(this); @@ -416,7 +424,6 @@ Menu::Menu() { addActionToQMenuAndActionHash(assetDeveloperMenu, MenuOption::AssetMigration, 0, &atpMigrator, SLOT(loadEntityServerFile())); -#endif // Developer > Avatar >>> MenuWrapper* avatarDebugMenu = developerMenu->addMenu("Avatar"); @@ -547,14 +554,16 @@ Menu::Menu() { "NetworkingPreferencesDialog"); }); addActionToQMenuAndActionHash(networkMenu, MenuOption::ReloadContent, 0, qApp, SLOT(reloadResourceCaches())); - addActionToQMenuAndActionHash(networkMenu, MenuOption::ClearDiskCache, 0, - DependencyManager::get().data(), SLOT(clearCache())); addCheckableActionToQMenuAndActionHash(networkMenu, MenuOption::DisableActivityLogger, 0, false, &UserActivityLogger::getInstance(), SLOT(disable(bool))); + addActionToQMenuAndActionHash(networkMenu, MenuOption::CachesSize, 0, + dialogsManager.data(), SLOT(cachesSizeDialog())); + addActionToQMenuAndActionHash(networkMenu, MenuOption::DiskCacheEditor, 0, + dialogsManager.data(), SLOT(toggleDiskCacheEditor())); addActionToQMenuAndActionHash(networkMenu, MenuOption::ShowDSConnectTable, 0, dialogsManager.data(), SLOT(showDomainConnectionDialog())); diff --git a/interface/src/Menu.h b/interface/src/Menu.h index b4eaf56758..c806ffa9ee 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -26,6 +26,7 @@ public: }; namespace MenuOption { + const QString AboutApp = "About Interface"; const QString AddRemoveFriends = "Add/Remove Friends..."; const QString AddressBar = "Show Address Bar"; const QString Animations = "Animations..."; @@ -51,11 +52,11 @@ namespace MenuOption { const QString BinaryEyelidControl = "Binary Eyelid Control"; const QString BookmarkLocation = "Bookmark Location"; const QString Bookmarks = "Bookmarks"; + const QString CachesSize = "RAM Caches Size"; const QString CalibrateCamera = "Calibrate Camera"; const QString CameraEntityMode = "Entity Mode"; const QString CenterPlayerInView = "Center Player In View"; const QString Chat = "Chat..."; - const QString ClearDiskCache = "Clear Disk Cache"; const QString Collisions = "Collisions"; const QString Connexion = "Activate 3D Connexion Devices"; const QString Console = "Console..."; @@ -82,6 +83,7 @@ namespace MenuOption { const QString DisableActivityLogger = "Disable Activity Logger"; const QString DisableEyelidAdjustment = "Disable Eyelid Adjustment"; const QString DisableLightEntities = "Disable Light Entities"; + const QString DiskCacheEditor = "Disk Cache Editor"; const QString DisplayCrashOptions = "Display Crash Options"; const QString DisplayHandTargets = "Show Hand Targets"; const QString DisplayModelBounds = "Display Model Bounds"; @@ -122,6 +124,7 @@ namespace MenuOption { const QString LogExtraTimings = "Log Extra Timing Details"; const QString LowVelocityFilter = "Low Velocity Filter"; const QString MeshVisible = "Draw Mesh"; + const QString MiniMirror = "Mini Mirror"; const QString MuteAudio = "Mute Microphone"; const QString MuteEnvironment = "Mute Environment"; const QString MuteFaceTracking = "Mute Face Tracking"; @@ -166,6 +169,7 @@ namespace MenuOption { const QString RunningScripts = "Running Scripts..."; const QString RunClientScriptTests = "Run Client Script Tests"; const QString RunTimingTests = "Run Timing Tests"; + const QString ScriptEditor = "Script Editor..."; const QString ScriptedMotorControl = "Enable Scripted Motor Control"; const QString SendWrongDSConnectVersion = "Send wrong DS connect version"; const QString SendWrongProtocolVersion = "Send wrong protocol version"; diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 6152148887..94ce444416 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -424,7 +424,7 @@ void AvatarManager::getObjectsToChange(VectorOfMotionStates& result) { } } -void AvatarManager::handleChangedMotionStates(const VectorOfMotionStates& motionStates) { +void AvatarManager::handleOutgoingChanges(const VectorOfMotionStates& motionStates) { // TODO: extract the MyAvatar results once we use a MotionState for it. } diff --git a/interface/src/avatar/AvatarManager.h b/interface/src/avatar/AvatarManager.h index b94f9e6a96..e1f5a3b411 100644 --- a/interface/src/avatar/AvatarManager.h +++ b/interface/src/avatar/AvatarManager.h @@ -70,7 +70,7 @@ public: void getObjectsToRemoveFromPhysics(VectorOfMotionStates& motionStates); void getObjectsToAddToPhysics(VectorOfMotionStates& motionStates); void getObjectsToChange(VectorOfMotionStates& motionStates); - void handleChangedMotionStates(const VectorOfMotionStates& motionStates); + void handleOutgoingChanges(const VectorOfMotionStates& motionStates); void handleCollisionEvents(const CollisionEvents& collisionEvents); Q_INVOKABLE float getAvatarDataRate(const QUuid& sessionID, const QString& rateName = QString("")) const; diff --git a/interface/src/avatar/CauterizedMeshPartPayload.cpp b/interface/src/avatar/CauterizedMeshPartPayload.cpp index c11f92083b..c8ec90dcee 100644 --- a/interface/src/avatar/CauterizedMeshPartPayload.cpp +++ b/interface/src/avatar/CauterizedMeshPartPayload.cpp @@ -20,28 +20,55 @@ using namespace render; CauterizedMeshPartPayload::CauterizedMeshPartPayload(Model* model, int meshIndex, int partIndex, int shapeIndex, const Transform& transform, const Transform& offsetTransform) : ModelMeshPartPayload(model, meshIndex, partIndex, shapeIndex, transform, offsetTransform) {} -void CauterizedMeshPartPayload::updateTransformForCauterizedMesh( - const Transform& renderTransform, - const gpu::BufferPointer& buffer) { - _cauterizedTransform = renderTransform; - _cauterizedClusterBuffer = buffer; +void CauterizedMeshPartPayload::updateTransformForSkinnedCauterizedMesh(const Transform& transform, + const QVector& clusterMatrices, + const QVector& cauterizedClusterMatrices) { + _transform = transform; + _cauterizedTransform = transform; + + if (clusterMatrices.size() > 0) { + _worldBound = AABox(); + for (auto& clusterMatrix : clusterMatrices) { + AABox clusterBound = _localBound; + clusterBound.transform(clusterMatrix); + _worldBound += clusterBound; + } + + _worldBound.transform(transform); + if (clusterMatrices.size() == 1) { + _transform = _transform.worldTransform(Transform(clusterMatrices[0])); + if (cauterizedClusterMatrices.size() != 0) { + _cauterizedTransform = _cauterizedTransform.worldTransform(Transform(cauterizedClusterMatrices[0])); + } else { + _cauterizedTransform = _transform; + } + } + } else { + _worldBound = _localBound; + _worldBound.transform(_drawTransform); + } } void CauterizedMeshPartPayload::bindTransform(gpu::Batch& batch, const render::ShapePipeline::LocationsPointer locations, RenderArgs::RenderMode renderMode) const { // Still relying on the raw data from the model + const Model::MeshState& state = _model->getMeshState(_meshIndex); SkeletonModel* skeleton = static_cast(_model); bool useCauterizedMesh = (renderMode != RenderArgs::RenderMode::SHADOW_RENDER_MODE) && skeleton->getEnableCauterization(); - if (useCauterizedMesh) { - if (_cauterizedClusterBuffer) { - batch.setUniformBuffer(ShapePipeline::Slot::BUFFER::SKINNING, _cauterizedClusterBuffer); - } - batch.setModelTransform(_cauterizedTransform); - } else { - if (_clusterBuffer) { - batch.setUniformBuffer(ShapePipeline::Slot::BUFFER::SKINNING, _clusterBuffer); + if (state.clusterBuffer) { + if (useCauterizedMesh) { + const Model::MeshState& cState = skeleton->getCauterizeMeshState(_meshIndex); + batch.setUniformBuffer(ShapePipeline::Slot::BUFFER::SKINNING, cState.clusterBuffer); + } else { + batch.setUniformBuffer(ShapePipeline::Slot::BUFFER::SKINNING, state.clusterBuffer); } batch.setModelTransform(_transform); + } else { + if (useCauterizedMesh) { + batch.setModelTransform(_cauterizedTransform); + } else { + batch.setModelTransform(_transform); + } } } diff --git a/interface/src/avatar/CauterizedMeshPartPayload.h b/interface/src/avatar/CauterizedMeshPartPayload.h index dc88e950c1..f4319ead6f 100644 --- a/interface/src/avatar/CauterizedMeshPartPayload.h +++ b/interface/src/avatar/CauterizedMeshPartPayload.h @@ -17,13 +17,12 @@ class CauterizedMeshPartPayload : public ModelMeshPartPayload { public: CauterizedMeshPartPayload(Model* model, int meshIndex, int partIndex, int shapeIndex, const Transform& transform, const Transform& offsetTransform); - - void updateTransformForCauterizedMesh(const Transform& renderTransform, const gpu::BufferPointer& buffer); + void updateTransformForSkinnedCauterizedMesh(const Transform& transform, + const QVector& clusterMatrices, + const QVector& cauterizedClusterMatrices); void bindTransform(gpu::Batch& batch, const render::ShapePipeline::LocationsPointer locations, RenderArgs::RenderMode renderMode) const override; - private: - gpu::BufferPointer _cauterizedClusterBuffer; Transform _cauterizedTransform; }; diff --git a/interface/src/avatar/CauterizedModel.cpp b/interface/src/avatar/CauterizedModel.cpp index d8db83fbb7..1ca87a498a 100644 --- a/interface/src/avatar/CauterizedModel.cpp +++ b/interface/src/avatar/CauterizedModel.cpp @@ -26,8 +26,8 @@ CauterizedModel::~CauterizedModel() { } void CauterizedModel::deleteGeometry() { - Model::deleteGeometry(); - _cauterizeMeshStates.clear(); + Model::deleteGeometry(); + _cauterizeMeshStates.clear(); } bool CauterizedModel::updateGeometry() { @@ -41,7 +41,7 @@ bool CauterizedModel::updateGeometry() { _cauterizeMeshStates.append(state); } } - return needsFullUpdate; + return needsFullUpdate; } void CauterizedModel::createVisibleRenderItemSet() { @@ -86,13 +86,13 @@ void CauterizedModel::createVisibleRenderItemSet() { } } } else { - Model::createVisibleRenderItemSet(); + Model::createVisibleRenderItemSet(); } } void CauterizedModel::createCollisionRenderItemSet() { // Temporary HACK: use base class method for now - Model::createCollisionRenderItemSet(); + Model::createCollisionRenderItemSet(); } void CauterizedModel::updateClusterMatrices() { @@ -122,8 +122,8 @@ void CauterizedModel::updateClusterMatrices() { state.clusterBuffer->setSubData(0, state.clusterMatrices.size() * sizeof(glm::mat4), (const gpu::Byte*) state.clusterMatrices.constData()); } - } - } + } + } // as an optimization, don't build cautrizedClusterMatrices if the boneSet is empty. if (!_cauterizeBoneSet.empty()) { @@ -191,9 +191,6 @@ void CauterizedModel::updateRenderItems() { return; } - // lazy update of cluster matrices used for rendering. We need to update them here, so we can correctly update the bounding box. - self->updateClusterMatrices(); - render::ScenePointer scene = AbstractViewStateInterface::instance()->getMain3DScene(); Transform modelTransform; @@ -212,22 +209,15 @@ void CauterizedModel::updateRenderItems() { if (data._model && data._model->isLoaded()) { // Ensure the model geometry was not reset between frames if (deleteGeometryCounter == data._model->getGeometryCounter()) { - // this stuff identical to what happens in regular Model - const Model::MeshState& state = data._model->getMeshState(data._meshIndex); - Transform renderTransform = modelTransform; - if (state.clusterMatrices.size() == 1) { - renderTransform = modelTransform.worldTransform(Transform(state.clusterMatrices[0])); - } - data.updateTransformForSkinnedMesh(renderTransform, modelTransform, state.clusterBuffer); + // lazy update of cluster matrices used for rendering. We need to update them here, so we can correctly update the bounding box. + data._model->updateClusterMatrices(); - // this stuff for cauterized mesh + // update the model transform and bounding box for this render item. + const Model::MeshState& state = data._model->getMeshState(data._meshIndex); CauterizedModel* cModel = static_cast(data._model); - const Model::MeshState& cState = cModel->getCauterizeMeshState(data._meshIndex); - renderTransform = modelTransform; - if (cState.clusterMatrices.size() == 1) { - renderTransform = modelTransform.worldTransform(Transform(cState.clusterMatrices[0])); - } - data.updateTransformForCauterizedMesh(renderTransform, cState.clusterBuffer); + assert(data._meshIndex < cModel->_cauterizeMeshStates.size()); + const Model::MeshState& cState = cModel->_cauterizeMeshStates.at(data._meshIndex); + data.updateTransformForSkinnedCauterizedMesh(modelTransform, state.clusterMatrices, cState.clusterMatrices); } } }); diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index b40ef601ea..969268c549 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -104,7 +104,6 @@ MyAvatar::MyAvatar(RigPointer rig) : _eyeContactTarget(LEFT_EYE), _realWorldFieldOfView("realWorldFieldOfView", DEFAULT_REAL_WORLD_FIELD_OF_VIEW_DEGREES), - _useAdvancedMovementControls("advancedMovementForHandControllersIsChecked", false), _hmdSensorMatrix(), _hmdSensorOrientation(), _hmdSensorPosition(), @@ -120,7 +119,9 @@ MyAvatar::MyAvatar(RigPointer rig) : using namespace recording; _skeletonModel->flagAsCauterized(); - clearDriveKeys(); + for (int i = 0; i < MAX_DRIVE_KEYS; i++) { + _driveKeys[i] = 0.0f; + } // Necessary to select the correct slot using SlotType = void(MyAvatar::*)(const glm::vec3&, bool, const glm::quat&, bool); @@ -153,12 +154,9 @@ MyAvatar::MyAvatar(RigPointer rig) : if (recordingInterface->getPlayFromCurrentLocation()) { setRecordingBasis(); } - _wasCharacterControllerEnabled = _characterController.isEnabled(); - _characterController.setEnabled(false); } else { clearRecordingBasis(); useFullAvatarURL(_fullAvatarURLFromPreferences, _fullAvatarModelName); - _characterController.setEnabled(_wasCharacterControllerEnabled); } auto audioIO = DependencyManager::get(); @@ -229,21 +227,6 @@ MyAvatar::~MyAvatar() { _lookAtTargetAvatar.reset(); } -void MyAvatar::registerMetaTypes(QScriptEngine* engine) { - QScriptValue value = engine->newQObject(this, QScriptEngine::QtOwnership, QScriptEngine::ExcludeDeleteLater | QScriptEngine::ExcludeChildObjects); - engine->globalObject().setProperty("MyAvatar", value); - - QScriptValue driveKeys = engine->newObject(); - auto metaEnum = QMetaEnum::fromType(); - for (int i = 0; i < MAX_DRIVE_KEYS; ++i) { - driveKeys.setProperty(metaEnum.key(i), metaEnum.value(i)); - } - engine->globalObject().setProperty("DriveKeys", driveKeys); - - qScriptRegisterMetaType(engine, audioListenModeToScriptValue, audioListenModeFromScriptValue); - qScriptRegisterMetaType(engine, driveKeysToScriptValue, driveKeysFromScriptValue); -} - void MyAvatar::setOrientationVar(const QVariant& newOrientationVar) { Avatar::setOrientation(quatFromVariant(newOrientationVar)); } @@ -476,7 +459,7 @@ void MyAvatar::simulate(float deltaTime) { // When there are no step values, we zero out the last step pulse. // This allows a user to do faster snapping by tapping a control for (int i = STEP_TRANSLATE_X; !stepAction && i <= STEP_YAW; ++i) { - if (getDriveKey((DriveKeys)i) != 0.0f) { + if (_driveKeys[i] != 0.0f) { stepAction = true; } } @@ -1669,7 +1652,7 @@ bool MyAvatar::shouldRenderHead(const RenderArgs* renderArgs) const { void MyAvatar::updateOrientation(float deltaTime) { // Smoothly rotate body with arrow keys - float targetSpeed = getDriveKey(YAW) * _yawSpeed; + float targetSpeed = _driveKeys[YAW] * _yawSpeed; if (targetSpeed != 0.0f) { const float ROTATION_RAMP_TIMESCALE = 0.1f; float blend = deltaTime / ROTATION_RAMP_TIMESCALE; @@ -1698,8 +1681,8 @@ void MyAvatar::updateOrientation(float deltaTime) { // Comfort Mode: If you press any of the left/right rotation drive keys or input, you'll // get an instantaneous 15 degree turn. If you keep holding the key down you'll get another // snap turn every half second. - if (getDriveKey(STEP_YAW) != 0.0f) { - totalBodyYaw += getDriveKey(STEP_YAW); + if (_driveKeys[STEP_YAW] != 0.0f) { + totalBodyYaw += _driveKeys[STEP_YAW]; } // use head/HMD orientation to turn while flying @@ -1736,7 +1719,7 @@ void MyAvatar::updateOrientation(float deltaTime) { // update body orientation by movement inputs setOrientation(getOrientation() * glm::quat(glm::radians(glm::vec3(0.0f, totalBodyYaw, 0.0f)))); - getHead()->setBasePitch(getHead()->getBasePitch() + getDriveKey(PITCH) * _pitchSpeed * deltaTime); + getHead()->setBasePitch(getHead()->getBasePitch() + _driveKeys[PITCH] * _pitchSpeed * deltaTime); if (qApp->isHMDMode()) { glm::quat orientation = glm::quat_cast(getSensorToWorldMatrix()) * getHMDSensorOrientation(); @@ -1770,14 +1753,14 @@ void MyAvatar::updateActionMotor(float deltaTime) { } // compute action input - glm::vec3 front = (getDriveKey(TRANSLATE_Z)) * IDENTITY_FRONT; - glm::vec3 right = (getDriveKey(TRANSLATE_X)) * IDENTITY_RIGHT; + glm::vec3 front = (_driveKeys[TRANSLATE_Z]) * IDENTITY_FRONT; + glm::vec3 right = (_driveKeys[TRANSLATE_X]) * IDENTITY_RIGHT; glm::vec3 direction = front + right; CharacterController::State state = _characterController.getState(); if (state == CharacterController::State::Hover) { // we're flying --> support vertical motion - glm::vec3 up = (getDriveKey(TRANSLATE_Y)) * IDENTITY_UP; + glm::vec3 up = (_driveKeys[TRANSLATE_Y]) * IDENTITY_UP; direction += up; } @@ -1816,7 +1799,7 @@ void MyAvatar::updateActionMotor(float deltaTime) { _actionMotorVelocity = MAX_WALKING_SPEED * direction; } - float boomChange = getDriveKey(ZOOM); + float boomChange = _driveKeys[ZOOM]; _boomLength += 2.0f * _boomLength * boomChange + boomChange * boomChange; _boomLength = glm::clamp(_boomLength, ZOOM_MIN, ZOOM_MAX); } @@ -1847,11 +1830,11 @@ void MyAvatar::updatePosition(float deltaTime) { } // capture the head rotation, in sensor space, when the user first indicates they would like to move/fly. - if (!_hoverReferenceCameraFacingIsCaptured && (fabs(getDriveKey(TRANSLATE_Z)) > 0.1f || fabs(getDriveKey(TRANSLATE_X)) > 0.1f)) { + if (!_hoverReferenceCameraFacingIsCaptured && (fabs(_driveKeys[TRANSLATE_Z]) > 0.1f || fabs(_driveKeys[TRANSLATE_X]) > 0.1f)) { _hoverReferenceCameraFacingIsCaptured = true; // transform the camera facing vector into sensor space. _hoverReferenceCameraFacing = transformVectorFast(glm::inverse(_sensorToWorldMatrix), getHead()->getCameraOrientation() * Vectors::UNIT_Z); - } else if (_hoverReferenceCameraFacingIsCaptured && (fabs(getDriveKey(TRANSLATE_Z)) <= 0.1f && fabs(getDriveKey(TRANSLATE_X)) <= 0.1f)) { + } else if (_hoverReferenceCameraFacingIsCaptured && (fabs(_driveKeys[TRANSLATE_Z]) <= 0.1f && fabs(_driveKeys[TRANSLATE_X]) <= 0.1f)) { _hoverReferenceCameraFacingIsCaptured = false; } } @@ -2107,61 +2090,17 @@ bool MyAvatar::getCharacterControllerEnabled() { } void MyAvatar::clearDriveKeys() { - _driveKeys.fill(0.0f); -} - -void MyAvatar::setDriveKey(DriveKeys key, float val) { - try { - _driveKeys.at(key) = val; - } catch (const std::exception&) { - qCCritical(interfaceapp) << Q_FUNC_INFO << ": Index out of bounds"; - } -} - -float MyAvatar::getDriveKey(DriveKeys key) const { - return isDriveKeyDisabled(key) ? 0.0f : getRawDriveKey(key); -} - -float MyAvatar::getRawDriveKey(DriveKeys key) const { - try { - return _driveKeys.at(key); - } catch (const std::exception&) { - qCCritical(interfaceapp) << Q_FUNC_INFO << ": Index out of bounds"; - return 0.0f; + for (int i = 0; i < MAX_DRIVE_KEYS; ++i) { + _driveKeys[i] = 0.0f; } } void MyAvatar::relayDriveKeysToCharacterController() { - if (getDriveKey(TRANSLATE_Y) > 0.0f) { + if (_driveKeys[TRANSLATE_Y] > 0.0f) { _characterController.jump(); } } -void MyAvatar::disableDriveKey(DriveKeys key) { - try { - _disabledDriveKeys.set(key); - } catch (const std::exception&) { - qCCritical(interfaceapp) << Q_FUNC_INFO << ": Index out of bounds"; - } -} - -void MyAvatar::enableDriveKey(DriveKeys key) { - try { - _disabledDriveKeys.reset(key); - } catch (const std::exception&) { - qCCritical(interfaceapp) << Q_FUNC_INFO << ": Index out of bounds"; - } -} - -bool MyAvatar::isDriveKeyDisabled(DriveKeys key) const { - try { - return _disabledDriveKeys.test(key); - } catch (const std::exception&) { - qCCritical(interfaceapp) << Q_FUNC_INFO << ": Index out of bounds"; - return true; - } -} - glm::vec3 MyAvatar::getWorldBodyPosition() const { return transformPoint(_sensorToWorldMatrix, extractTranslation(_bodySensorMatrix)); } @@ -2247,15 +2186,7 @@ QScriptValue audioListenModeToScriptValue(QScriptEngine* engine, const AudioList } void audioListenModeFromScriptValue(const QScriptValue& object, AudioListenerMode& audioListenerMode) { - audioListenerMode = static_cast(object.toUInt16()); -} - -QScriptValue driveKeysToScriptValue(QScriptEngine* engine, const MyAvatar::DriveKeys& driveKeys) { - return driveKeys; -} - -void driveKeysFromScriptValue(const QScriptValue& object, MyAvatar::DriveKeys& driveKeys) { - driveKeys = static_cast(object.toUInt16()); + audioListenerMode = (AudioListenerMode)object.toUInt16(); } @@ -2448,7 +2379,7 @@ bool MyAvatar::didTeleport() { } bool MyAvatar::hasDriveInput() const { - return fabsf(getDriveKey(TRANSLATE_X)) > 0.0f || fabsf(getDriveKey(TRANSLATE_Y)) > 0.0f || fabsf(getDriveKey(TRANSLATE_Z)) > 0.0f; + return fabsf(_driveKeys[TRANSLATE_X]) > 0.0f || fabsf(_driveKeys[TRANSLATE_Y]) > 0.0f || fabsf(_driveKeys[TRANSLATE_Z]) > 0.0f; } void MyAvatar::setAway(bool value) { @@ -2564,7 +2495,7 @@ bool MyAvatar::pinJoint(int index, const glm::vec3& position, const glm::quat& o return false; } - slamPosition(position); + setPosition(position); setOrientation(orientation); _rig->setMaxHipsOffsetLength(0.05f); diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 5f812f1f99..3cc665b533 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -12,8 +12,6 @@ #ifndef hifi_MyAvatar_h #define hifi_MyAvatar_h -#include - #include #include @@ -31,6 +29,20 @@ class AvatarActionHold; class ModelItemID; +enum DriveKeys { + TRANSLATE_X = 0, + TRANSLATE_Y, + TRANSLATE_Z, + YAW, + STEP_TRANSLATE_X, + STEP_TRANSLATE_Y, + STEP_TRANSLATE_Z, + STEP_YAW, + PITCH, + ZOOM, + MAX_DRIVE_KEYS +}; + enum eyeContactTarget { LEFT_EYE, RIGHT_EYE, @@ -74,29 +86,11 @@ class MyAvatar : public Avatar { Q_PROPERTY(bool hmdLeanRecenterEnabled READ getHMDLeanRecenterEnabled WRITE setHMDLeanRecenterEnabled) Q_PROPERTY(bool characterControllerEnabled READ getCharacterControllerEnabled WRITE setCharacterControllerEnabled) - Q_PROPERTY(bool useAdvancedMovementControls READ useAdvancedMovementControls WRITE setUseAdvancedMovementControls) public: - enum DriveKeys { - TRANSLATE_X = 0, - TRANSLATE_Y, - TRANSLATE_Z, - YAW, - STEP_TRANSLATE_X, - STEP_TRANSLATE_Y, - STEP_TRANSLATE_Z, - STEP_YAW, - PITCH, - ZOOM, - MAX_DRIVE_KEYS - }; - Q_ENUM(DriveKeys) - explicit MyAvatar(RigPointer rig); ~MyAvatar(); - void registerMetaTypes(QScriptEngine* engine); - virtual void simulateAttachments(float deltaTime) override; AudioListenerMode getAudioListenerModeHead() const { return FROM_HEAD; } @@ -177,10 +171,6 @@ public: Q_INVOKABLE void setHMDLeanRecenterEnabled(bool value) { _hmdLeanRecenterEnabled = value; } Q_INVOKABLE bool getHMDLeanRecenterEnabled() const { return _hmdLeanRecenterEnabled; } - bool useAdvancedMovementControls() const { return _useAdvancedMovementControls.get(); } - void setUseAdvancedMovementControls(bool useAdvancedMovementControls) - { _useAdvancedMovementControls.set(useAdvancedMovementControls); } - // get/set avatar data void saveData(); void loadData(); @@ -190,15 +180,9 @@ public: // Set what driving keys are being pressed to control thrust levels void clearDriveKeys(); - void setDriveKey(DriveKeys key, float val); - float getDriveKey(DriveKeys key) const; - Q_INVOKABLE float getRawDriveKey(DriveKeys key) const; + void setDriveKeys(int key, float val) { _driveKeys[key] = val; }; void relayDriveKeysToCharacterController(); - Q_INVOKABLE void disableDriveKey(DriveKeys key); - Q_INVOKABLE void enableDriveKey(DriveKeys key); - Q_INVOKABLE bool isDriveKeyDisabled(DriveKeys key) const; - eyeContactTarget getEyeContactTarget(); Q_INVOKABLE glm::vec3 getTrackedHeadPosition() const { return _trackedHeadPosition; } @@ -368,6 +352,7 @@ private: virtual bool shouldRenderHead(const RenderArgs* renderArgs) const override; void setShouldRenderLocally(bool shouldRender) { _shouldRender = shouldRender; setEnableMeshVisible(shouldRender); } bool getShouldRenderLocally() const { return _shouldRender; } + bool getDriveKeys(int key) { return _driveKeys[key] != 0.0f; }; bool isMyAvatar() const override { return true; } virtual int parseDataFromBuffer(const QByteArray& buffer) override; virtual glm::vec3 getSkeletonPosition() const override; @@ -403,9 +388,7 @@ private: void clampScaleChangeToDomainLimits(float desiredScale); glm::mat4 computeCameraRelativeHandControllerMatrix(const glm::mat4& controllerSensorMatrix) const; - std::array _driveKeys; - std::bitset _disabledDriveKeys; - + float _driveKeys[MAX_DRIVE_KEYS]; bool _wasPushing; bool _isPushing; bool _isBeingPushed; @@ -428,7 +411,6 @@ private: SharedSoundPointer _collisionSound; MyCharacterController _characterController; - bool _wasCharacterControllerEnabled { true }; AvatarWeakPointer _lookAtTargetAvatar; glm::vec3 _targetAvatarPosition; @@ -441,7 +423,6 @@ private: glm::vec3 _trackedHeadPosition; Setting::Handle _realWorldFieldOfView; - Setting::Handle _useAdvancedMovementControls; // private methods void updateOrientation(float deltaTime); @@ -559,7 +540,4 @@ private: QScriptValue audioListenModeToScriptValue(QScriptEngine* engine, const AudioListenerMode& audioListenerMode); void audioListenModeFromScriptValue(const QScriptValue& object, AudioListenerMode& audioListenerMode); -QScriptValue driveKeysToScriptValue(QScriptEngine* engine, const MyAvatar::DriveKeys& driveKeys); -void driveKeysFromScriptValue(const QScriptValue& object, MyAvatar::DriveKeys& driveKeys); - #endif // hifi_MyAvatar_h diff --git a/interface/src/ui/ApplicationOverlay.cpp b/interface/src/ui/ApplicationOverlay.cpp index f2d97a0137..364dff52a3 100644 --- a/interface/src/ui/ApplicationOverlay.cpp +++ b/interface/src/ui/ApplicationOverlay.cpp @@ -13,6 +13,7 @@ #include #include +#include #include #include #include @@ -41,6 +42,7 @@ ApplicationOverlay::ApplicationOverlay() _domainStatusBorder = geometryCache->allocateID(); _magnifierBorder = geometryCache->allocateID(); _qmlGeometryId = geometryCache->allocateID(); + _rearViewGeometryId = geometryCache->allocateID(); } ApplicationOverlay::~ApplicationOverlay() { @@ -49,6 +51,7 @@ ApplicationOverlay::~ApplicationOverlay() { geometryCache->releaseID(_domainStatusBorder); geometryCache->releaseID(_magnifierBorder); geometryCache->releaseID(_qmlGeometryId); + geometryCache->releaseID(_rearViewGeometryId); } } @@ -83,6 +86,7 @@ void ApplicationOverlay::renderOverlay(RenderArgs* renderArgs) { // Now render the overlay components together into a single texture renderDomainConnectionStatusBorder(renderArgs); // renders the connected domain line renderAudioScope(renderArgs); // audio scope in the very back - NOTE: this is the debug audio scope, not the VU meter + renderRearView(renderArgs); // renders the mirror view selfie renderOverlays(renderArgs); // renders Scripts Overlay and AudioScope renderQmlUi(renderArgs); // renders a unit quad with the QML UI texture, and the text overlays from scripts renderStatsAndLogs(renderArgs); // currently renders nothing @@ -95,7 +99,7 @@ void ApplicationOverlay::renderQmlUi(RenderArgs* renderArgs) { PROFILE_RANGE(app, __FUNCTION__); if (!_uiTexture) { - _uiTexture = gpu::TexturePointer(gpu::Texture::createExternal(OffscreenQmlSurface::getDiscardLambda())); + _uiTexture = gpu::TexturePointer(gpu::Texture::createExternal2D(OffscreenQmlSurface::getDiscardLambda())); _uiTexture->setSource(__FUNCTION__); } // Once we move UI rendering and screen rendering to different @@ -159,6 +163,45 @@ void ApplicationOverlay::renderOverlays(RenderArgs* renderArgs) { qApp->getOverlays().renderHUD(renderArgs); } +void ApplicationOverlay::renderRearViewToFbo(RenderArgs* renderArgs) { +} + +void ApplicationOverlay::renderRearView(RenderArgs* renderArgs) { + if (!qApp->isHMDMode() && Menu::getInstance()->isOptionChecked(MenuOption::MiniMirror) && + !Menu::getInstance()->isOptionChecked(MenuOption::FullscreenMirror)) { + gpu::Batch& batch = *renderArgs->_batch; + + auto geometryCache = DependencyManager::get(); + + auto framebuffer = DependencyManager::get(); + auto selfieTexture = framebuffer->getSelfieFramebuffer()->getRenderBuffer(0); + + int width = renderArgs->_viewport.z; + int height = renderArgs->_viewport.w; + mat4 legacyProjection = glm::ortho(0, width, height, 0, ORTHO_NEAR_CLIP, ORTHO_FAR_CLIP); + batch.setProjectionTransform(legacyProjection); + batch.setModelTransform(Transform()); + batch.resetViewTransform(); + + float screenRatio = ((float)qApp->getDevicePixelRatio()); + float renderRatio = ((float)qApp->getRenderResolutionScale()); + + auto viewport = qApp->getMirrorViewRect(); + glm::vec2 bottomLeft(viewport.left(), viewport.top() + viewport.height()); + glm::vec2 topRight(viewport.left() + viewport.width(), viewport.top()); + bottomLeft *= screenRatio; + topRight *= screenRatio; + glm::vec2 texCoordMinCorner(0.0f, 0.0f); + glm::vec2 texCoordMaxCorner(viewport.width() * renderRatio / float(selfieTexture->getWidth()), viewport.height() * renderRatio / float(selfieTexture->getHeight())); + + batch.setResourceTexture(0, selfieTexture); + float alpha = DependencyManager::get()->getDesktop()->property("unpinnedAlpha").toFloat(); + geometryCache->renderQuad(batch, bottomLeft, topRight, texCoordMinCorner, texCoordMaxCorner, glm::vec4(1.0f, 1.0f, 1.0f, alpha), _rearViewGeometryId); + + batch.setResourceTexture(0, renderArgs->_whiteTexture); + } +} + void ApplicationOverlay::renderStatsAndLogs(RenderArgs* renderArgs) { // Display stats and log text onscreen @@ -229,13 +272,13 @@ void ApplicationOverlay::buildFramebufferObject() { auto width = uiSize.x; auto height = uiSize.y; if (!_overlayFramebuffer->getDepthStencilBuffer()) { - auto overlayDepthTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(DEPTH_FORMAT, width, height, DEFAULT_SAMPLER)); + auto overlayDepthTexture = gpu::TexturePointer(gpu::Texture::create2D(DEPTH_FORMAT, width, height, DEFAULT_SAMPLER)); _overlayFramebuffer->setDepthStencilBuffer(overlayDepthTexture, DEPTH_FORMAT); } if (!_overlayFramebuffer->getRenderBuffer(0)) { const gpu::Sampler OVERLAY_SAMPLER(gpu::Sampler::FILTER_MIN_MAG_LINEAR, gpu::Sampler::WRAP_CLAMP); - auto colorBuffer = gpu::TexturePointer(gpu::Texture::createRenderBuffer(COLOR_FORMAT, width, height, OVERLAY_SAMPLER)); + auto colorBuffer = gpu::TexturePointer(gpu::Texture::create2D(COLOR_FORMAT, width, height, OVERLAY_SAMPLER)); _overlayFramebuffer->setRenderBuffer(0, colorBuffer); } } diff --git a/interface/src/ui/ApplicationOverlay.h b/interface/src/ui/ApplicationOverlay.h index af4d8779d4..7ace5ee885 100644 --- a/interface/src/ui/ApplicationOverlay.h +++ b/interface/src/ui/ApplicationOverlay.h @@ -31,6 +31,8 @@ public: private: void renderStatsAndLogs(RenderArgs* renderArgs); void renderDomainConnectionStatusBorder(RenderArgs* renderArgs); + void renderRearViewToFbo(RenderArgs* renderArgs); + void renderRearView(RenderArgs* renderArgs); void renderQmlUi(RenderArgs* renderArgs); void renderAudioScope(RenderArgs* renderArgs); void renderOverlays(RenderArgs* renderArgs); @@ -49,6 +51,7 @@ private: gpu::TexturePointer _overlayColorTexture; gpu::FramebufferPointer _overlayFramebuffer; int _qmlGeometryId { 0 }; + int _rearViewGeometryId { 0 }; }; #endif // hifi_ApplicationOverlay_h diff --git a/interface/src/ui/AvatarInputs.cpp b/interface/src/ui/AvatarInputs.cpp index 944be4bf9e..b09289c78a 100644 --- a/interface/src/ui/AvatarInputs.cpp +++ b/interface/src/ui/AvatarInputs.cpp @@ -20,6 +20,10 @@ HIFI_QML_DEF(AvatarInputs) static AvatarInputs* INSTANCE{ nullptr }; +static const char SETTINGS_GROUP_NAME[] = "Rear View Tools"; +static const char ZOOM_LEVEL_SETTINGS[] = "ZoomLevel"; + +static Setting::Handle rearViewZoomLevel(QStringList() << SETTINGS_GROUP_NAME << ZOOM_LEVEL_SETTINGS, 0); AvatarInputs* AvatarInputs::getInstance() { if (!INSTANCE) { @@ -32,6 +36,8 @@ AvatarInputs* AvatarInputs::getInstance() { AvatarInputs::AvatarInputs(QQuickItem* parent) : QQuickItem(parent) { INSTANCE = this; + int zoomSetting = rearViewZoomLevel.get(); + _mirrorZoomed = zoomSetting == 0; } #define AI_UPDATE(name, src) \ @@ -56,6 +62,8 @@ void AvatarInputs::update() { if (!Menu::getInstance()) { return; } + AI_UPDATE(mirrorVisible, Menu::getInstance()->isOptionChecked(MenuOption::MiniMirror) && !qApp->isHMDMode() + && !Menu::getInstance()->isOptionChecked(MenuOption::FullscreenMirror)); AI_UPDATE(cameraEnabled, !Menu::getInstance()->isOptionChecked(MenuOption::NoFaceTracking)); AI_UPDATE(cameraMuted, Menu::getInstance()->isOptionChecked(MenuOption::MuteFaceTracking)); AI_UPDATE(isHMD, qApp->isHMDMode()); @@ -114,3 +122,15 @@ void AvatarInputs::toggleAudioMute() { void AvatarInputs::resetSensors() { qApp->resetSensors(); } + +void AvatarInputs::toggleZoom() { + _mirrorZoomed = !_mirrorZoomed; + rearViewZoomLevel.set(_mirrorZoomed ? 0 : 1); + emit mirrorZoomedChanged(); +} + +void AvatarInputs::closeMirror() { + if (Menu::getInstance()->isOptionChecked(MenuOption::MiniMirror)) { + Menu::getInstance()->triggerOption(MenuOption::MiniMirror); + } +} diff --git a/interface/src/ui/AvatarInputs.h b/interface/src/ui/AvatarInputs.h index 5535469445..85570ecd3c 100644 --- a/interface/src/ui/AvatarInputs.h +++ b/interface/src/ui/AvatarInputs.h @@ -28,6 +28,8 @@ class AvatarInputs : public QQuickItem { AI_PROPERTY(bool, audioMuted, false) AI_PROPERTY(bool, audioClipping, false) AI_PROPERTY(float, audioLevel, 0) + AI_PROPERTY(bool, mirrorVisible, false) + AI_PROPERTY(bool, mirrorZoomed, true) AI_PROPERTY(bool, isHMD, false) AI_PROPERTY(bool, showAudioTools, true) @@ -42,6 +44,8 @@ signals: void audioMutedChanged(); void audioClippingChanged(); void audioLevelChanged(); + void mirrorVisibleChanged(); + void mirrorZoomedChanged(); void isHMDChanged(); void showAudioToolsChanged(); @@ -49,6 +53,8 @@ protected: Q_INVOKABLE void resetSensors(); Q_INVOKABLE void toggleCameraMute(); Q_INVOKABLE void toggleAudioMute(); + Q_INVOKABLE void toggleZoom(); + Q_INVOKABLE void closeMirror(); private: float _trailingAudioLoudness{ 0 }; diff --git a/interface/src/ui/BaseLogDialog.cpp b/interface/src/ui/BaseLogDialog.cpp index 571d3ac403..7e0027e0a8 100644 --- a/interface/src/ui/BaseLogDialog.cpp +++ b/interface/src/ui/BaseLogDialog.cpp @@ -28,23 +28,17 @@ const int SEARCH_BUTTON_LEFT = 25; const int SEARCH_BUTTON_WIDTH = 20; const int SEARCH_TOGGLE_BUTTON_WIDTH = 50; const int SEARCH_TEXT_WIDTH = 240; -const int TIME_STAMP_LENGTH = 16; -const int FONT_WEIGHT = 75; const QColor HIGHLIGHT_COLOR = QColor("#3366CC"); -const QColor BOLD_COLOR = QColor("#445c8c"); -const QString BOLD_PATTERN = "\\[\\d*\\/.*:\\d*:\\d*\\]"; -class Highlighter : public QSyntaxHighlighter { +class KeywordHighlighter : public QSyntaxHighlighter { public: - Highlighter(QTextDocument* parent = nullptr); - void setBold(int indexToBold); + KeywordHighlighter(QTextDocument* parent = nullptr); QString keyword; protected: void highlightBlock(const QString& text) override; private: - QTextCharFormat boldFormat; QTextCharFormat keywordFormat; }; @@ -95,7 +89,7 @@ void BaseLogDialog::initControls() { _leftPad += SEARCH_TOGGLE_BUTTON_WIDTH + BUTTON_MARGIN; _searchPrevButton->show(); connect(_searchPrevButton, SIGNAL(clicked()), SLOT(toggleSearchPrev())); - + _searchNextButton = new QPushButton(this); _searchNextButton->setObjectName("searchNextButton"); _searchNextButton->setGeometry(_leftPad, ELEMENT_MARGIN, SEARCH_TOGGLE_BUTTON_WIDTH, ELEMENT_HEIGHT); @@ -107,8 +101,9 @@ void BaseLogDialog::initControls() { _logTextBox = new QPlainTextEdit(this); _logTextBox->setReadOnly(true); _logTextBox->show(); - _highlighter = new Highlighter(_logTextBox->document()); + _highlighter = new KeywordHighlighter(_logTextBox->document()); connect(_logTextBox, SIGNAL(selectionChanged()), SLOT(updateSelection())); + } void BaseLogDialog::showEvent(QShowEvent* event) { @@ -121,9 +116,7 @@ void BaseLogDialog::resizeEvent(QResizeEvent* event) { void BaseLogDialog::appendLogLine(QString logLine) { if (logLine.contains(_searchTerm, Qt::CaseInsensitive)) { - int indexToBold = _logTextBox->document()->characterCount(); _logTextBox->appendPlainText(logLine.trimmed()); - _highlighter->setBold(indexToBold); } } @@ -135,7 +128,7 @@ void BaseLogDialog::handleSearchTextChanged(QString searchText) { if (searchText.isEmpty()) { return; } - + QTextCursor cursor = _logTextBox->textCursor(); if (cursor.hasSelection()) { QString selectedTerm = cursor.selectedText(); @@ -143,16 +136,16 @@ void BaseLogDialog::handleSearchTextChanged(QString searchText) { return; } } - + cursor.setPosition(0, QTextCursor::MoveAnchor); _logTextBox->setTextCursor(cursor); bool foundTerm = _logTextBox->find(searchText); - + if (!foundTerm) { cursor.movePosition(QTextCursor::End, QTextCursor::MoveAnchor); _logTextBox->setTextCursor(cursor); } - + _searchTerm = searchText; _highlighter->keyword = searchText; _highlighter->rehighlight(); @@ -182,7 +175,6 @@ void BaseLogDialog::showLogData() { _logTextBox->clear(); _logTextBox->appendPlainText(getCurrentLog()); _logTextBox->ensureCursorVisible(); - _highlighter->rehighlight(); } void BaseLogDialog::updateSelection() { @@ -195,28 +187,16 @@ void BaseLogDialog::updateSelection() { } } -Highlighter::Highlighter(QTextDocument* parent) : QSyntaxHighlighter(parent) { - boldFormat.setFontWeight(FONT_WEIGHT); - boldFormat.setForeground(BOLD_COLOR); +KeywordHighlighter::KeywordHighlighter(QTextDocument* parent) : QSyntaxHighlighter(parent) { keywordFormat.setForeground(HIGHLIGHT_COLOR); } -void Highlighter::highlightBlock(const QString& text) { - QRegExp expression(BOLD_PATTERN); - - int index = text.indexOf(expression, 0); - - while (index >= 0) { - int length = expression.matchedLength(); - setFormat(index, length, boldFormat); - index = text.indexOf(expression, index + length); - } - +void KeywordHighlighter::highlightBlock(const QString& text) { if (keyword.isNull() || keyword.isEmpty()) { return; } - index = text.indexOf(keyword, 0, Qt::CaseInsensitive); + int index = text.indexOf(keyword, 0, Qt::CaseInsensitive); int length = keyword.length(); while (index >= 0) { @@ -224,7 +204,3 @@ void Highlighter::highlightBlock(const QString& text) { index = text.indexOf(keyword, index + length, Qt::CaseInsensitive); } } - -void Highlighter::setBold(int indexToBold) { - setFormat(indexToBold, TIME_STAMP_LENGTH, boldFormat); -} diff --git a/interface/src/ui/BaseLogDialog.h b/interface/src/ui/BaseLogDialog.h index e18d23937f..d097010bae 100644 --- a/interface/src/ui/BaseLogDialog.h +++ b/interface/src/ui/BaseLogDialog.h @@ -23,7 +23,7 @@ const int BUTTON_MARGIN = 8; class QPushButton; class QLineEdit; class QPlainTextEdit; -class Highlighter; +class KeywordHighlighter; class BaseLogDialog : public QDialog { Q_OBJECT @@ -56,7 +56,7 @@ private: QPushButton* _searchPrevButton { nullptr }; QPushButton* _searchNextButton { nullptr }; QString _searchTerm; - Highlighter* _highlighter { nullptr }; + KeywordHighlighter* _highlighter { nullptr }; void initControls(); void showLogData(); diff --git a/interface/src/ui/CachesSizeDialog.cpp b/interface/src/ui/CachesSizeDialog.cpp new file mode 100644 index 0000000000..935a6d126e --- /dev/null +++ b/interface/src/ui/CachesSizeDialog.cpp @@ -0,0 +1,84 @@ +// +// CachesSizeDialog.cpp +// +// +// Created by Clement on 1/12/15. +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "CachesSizeDialog.h" + + +QDoubleSpinBox* createDoubleSpinBox(QWidget* parent) { + QDoubleSpinBox* box = new QDoubleSpinBox(parent); + box->setDecimals(0); + box->setRange(MIN_UNUSED_MAX_SIZE / BYTES_PER_MEGABYTES, MAX_UNUSED_MAX_SIZE / BYTES_PER_MEGABYTES); + + return box; +} + +CachesSizeDialog::CachesSizeDialog(QWidget* parent) : + QDialog(parent, Qt::Window | Qt::WindowCloseButtonHint) +{ + setWindowTitle("Caches Size"); + + // Create layouter + QFormLayout* form = new QFormLayout(this); + setLayout(form); + + form->addRow("Animations cache size (MB):", _animations = createDoubleSpinBox(this)); + form->addRow("Geometries cache size (MB):", _geometries = createDoubleSpinBox(this)); + form->addRow("Sounds cache size (MB):", _sounds = createDoubleSpinBox(this)); + form->addRow("Textures cache size (MB):", _textures = createDoubleSpinBox(this)); + + resetClicked(true); + + // Add a button to reset + QPushButton* confirmButton = new QPushButton("Confirm", this); + QPushButton* resetButton = new QPushButton("Reset", this); + form->addRow(confirmButton, resetButton); + connect(confirmButton, SIGNAL(clicked(bool)), this, SLOT(confirmClicked(bool))); + connect(resetButton, SIGNAL(clicked(bool)), this, SLOT(resetClicked(bool))); +} + +void CachesSizeDialog::confirmClicked(bool checked) { + DependencyManager::get()->setUnusedResourceCacheSize(_animations->value() * BYTES_PER_MEGABYTES); + DependencyManager::get()->setUnusedResourceCacheSize(_geometries->value() * BYTES_PER_MEGABYTES); + DependencyManager::get()->setUnusedResourceCacheSize(_sounds->value() * BYTES_PER_MEGABYTES); + // Disabling the texture cache because it's a liability in cases where we're overcommiting GPU memory +#if 0 + DependencyManager::get()->setUnusedResourceCacheSize(_textures->value() * BYTES_PER_MEGABYTES); +#endif + + QDialog::close(); +} + +void CachesSizeDialog::resetClicked(bool checked) { + _animations->setValue(DependencyManager::get()->getUnusedResourceCacheSize() / BYTES_PER_MEGABYTES); + _geometries->setValue(DependencyManager::get()->getUnusedResourceCacheSize() / BYTES_PER_MEGABYTES); + _sounds->setValue(DependencyManager::get()->getUnusedResourceCacheSize() / BYTES_PER_MEGABYTES); + _textures->setValue(DependencyManager::get()->getUnusedResourceCacheSize() / BYTES_PER_MEGABYTES); +} + +void CachesSizeDialog::reject() { + // Just regularly close upon ESC + QDialog::close(); +} + +void CachesSizeDialog::closeEvent(QCloseEvent* event) { + QDialog::closeEvent(event); + emit closed(); +} diff --git a/interface/src/ui/CachesSizeDialog.h b/interface/src/ui/CachesSizeDialog.h new file mode 100644 index 0000000000..025d0f2bac --- /dev/null +++ b/interface/src/ui/CachesSizeDialog.h @@ -0,0 +1,45 @@ +// +// CachesSizeDialog.h +// +// +// Created by Clement on 1/12/15. +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_CachesSizeDialog_h +#define hifi_CachesSizeDialog_h + +#include + +class QDoubleSpinBox; + +class CachesSizeDialog : public QDialog { + Q_OBJECT +public: + // Sets up the UI + CachesSizeDialog(QWidget* parent); + +signals: + void closed(); + +public slots: + void reject() override; + void confirmClicked(bool checked); + void resetClicked(bool checked); + +protected: + // Emits a 'closed' signal when this dialog is closed. + void closeEvent(QCloseEvent* event) override; + +private: + QDoubleSpinBox* _animations = nullptr; + QDoubleSpinBox* _geometries = nullptr; + QDoubleSpinBox* _scripts = nullptr; + QDoubleSpinBox* _sounds = nullptr; + QDoubleSpinBox* _textures = nullptr; +}; + +#endif // hifi_CachesSizeDialog_h diff --git a/interface/src/ui/DialogsManager.cpp b/interface/src/ui/DialogsManager.cpp index f1d6f585d7..3252fef4f0 100644 --- a/interface/src/ui/DialogsManager.cpp +++ b/interface/src/ui/DialogsManager.cpp @@ -19,13 +19,16 @@ #include #include "AddressBarDialog.h" +#include "CachesSizeDialog.h" #include "ConnectionFailureDialog.h" +#include "DiskCacheEditor.h" #include "DomainConnectionDialog.h" #include "HMDToolsDialog.h" #include "LodToolsDialog.h" #include "LoginDialog.h" #include "OctreeStatsDialog.h" #include "PreferencesDialog.h" +#include "ScriptEditorWindow.h" #include "UpdateDialog.h" template @@ -64,6 +67,11 @@ void DialogsManager::setDomainConnectionFailureVisibility(bool visible) { } } +void DialogsManager::toggleDiskCacheEditor() { + maybeCreateDialog(_diskCacheEditor); + _diskCacheEditor->toggle(); +} + void DialogsManager::toggleLoginDialog() { LoginDialog::toggleAction(); } @@ -89,6 +97,16 @@ void DialogsManager::octreeStatsDetails() { _octreeStatsDialog->raise(); } +void DialogsManager::cachesSizeDialog() { + if (!_cachesSizeDialog) { + maybeCreateDialog(_cachesSizeDialog); + + connect(_cachesSizeDialog, SIGNAL(closed()), _cachesSizeDialog, SLOT(deleteLater())); + _cachesSizeDialog->show(); + } + _cachesSizeDialog->raise(); +} + void DialogsManager::lodTools() { if (!_lodToolsDialog) { maybeCreateDialog(_lodToolsDialog); @@ -119,6 +137,12 @@ void DialogsManager::hmdToolsClosed() { } } +void DialogsManager::showScriptEditor() { + maybeCreateDialog(_scriptEditor); + _scriptEditor->show(); + _scriptEditor->raise(); +} + void DialogsManager::showTestingResults() { if (!_testingDialog) { _testingDialog = new TestingDialog(qApp->getWindow()); diff --git a/interface/src/ui/DialogsManager.h b/interface/src/ui/DialogsManager.h index 608195aca7..54aef38984 100644 --- a/interface/src/ui/DialogsManager.h +++ b/interface/src/ui/DialogsManager.h @@ -22,6 +22,7 @@ class AnimationsDialog; class AttachmentsDialog; class CachesSizeDialog; +class DiskCacheEditor; class LodToolsDialog; class OctreeStatsDialog; class ScriptEditorWindow; @@ -45,11 +46,14 @@ public slots: void showAddressBar(); void showFeed(); void setDomainConnectionFailureVisibility(bool visible); + void toggleDiskCacheEditor(); void toggleLoginDialog(); void showLoginDialog(); void octreeStatsDetails(); + void cachesSizeDialog(); void lodTools(); void hmdTools(bool showTools); + void showScriptEditor(); void showDomainConnectionDialog(); void showTestingResults(); @@ -73,10 +77,12 @@ private: QPointer _animationsDialog; QPointer _attachmentsDialog; QPointer _cachesSizeDialog; + QPointer _diskCacheEditor; QPointer _ircInfoBox; QPointer _hmdToolsDialog; QPointer _lodToolsDialog; QPointer _octreeStatsDialog; + QPointer _scriptEditor; QPointer _testingDialog; QPointer _domainConnectionDialog; }; diff --git a/interface/src/ui/DiskCacheEditor.cpp b/interface/src/ui/DiskCacheEditor.cpp new file mode 100644 index 0000000000..1a7be8642b --- /dev/null +++ b/interface/src/ui/DiskCacheEditor.cpp @@ -0,0 +1,146 @@ +// +// DiskCacheEditor.cpp +// +// +// Created by Clement on 3/4/15. +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "DiskCacheEditor.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "OffscreenUi.h" + +DiskCacheEditor::DiskCacheEditor(QWidget* parent) : QObject(parent) { +} + +QWindow* DiskCacheEditor::windowHandle() { + return (_dialog) ? _dialog->windowHandle() : nullptr; +} + +void DiskCacheEditor::toggle() { + if (!_dialog) { + makeDialog(); + } + + if (!_dialog->isActiveWindow()) { + _dialog->show(); + _dialog->raise(); + _dialog->activateWindow(); + } else { + _dialog->close(); + } +} + +void DiskCacheEditor::makeDialog() { + _dialog = new QDialog(static_cast(parent())); + Q_CHECK_PTR(_dialog); + _dialog->setAttribute(Qt::WA_DeleteOnClose); + _dialog->setWindowTitle("Disk Cache Editor"); + + QGridLayout* layout = new QGridLayout(_dialog); + Q_CHECK_PTR(layout); + _dialog->setLayout(layout); + + + QLabel* path = new QLabel("Path : ", _dialog); + Q_CHECK_PTR(path); + path->setAlignment(Qt::AlignRight); + layout->addWidget(path, 0, 0); + + QLabel* size = new QLabel("Current Size : ", _dialog); + Q_CHECK_PTR(size); + size->setAlignment(Qt::AlignRight); + layout->addWidget(size, 1, 0); + + QLabel* maxSize = new QLabel("Max Size : ", _dialog); + Q_CHECK_PTR(maxSize); + maxSize->setAlignment(Qt::AlignRight); + layout->addWidget(maxSize, 2, 0); + + + _path = new QLabel(_dialog); + Q_CHECK_PTR(_path); + _path->setAlignment(Qt::AlignLeft); + layout->addWidget(_path, 0, 1, 1, 3); + + _size = new QLabel(_dialog); + Q_CHECK_PTR(_size); + _size->setAlignment(Qt::AlignLeft); + layout->addWidget(_size, 1, 1, 1, 3); + + _maxSize = new QLabel(_dialog); + Q_CHECK_PTR(_maxSize); + _maxSize->setAlignment(Qt::AlignLeft); + layout->addWidget(_maxSize, 2, 1, 1, 3); + + refresh(); + + + static const int REFRESH_INTERVAL = 100; // msec + _refreshTimer = new QTimer(_dialog); + _refreshTimer->setInterval(REFRESH_INTERVAL); // Qt::CoarseTimer acceptable, no need for real time accuracy + _refreshTimer->setSingleShot(false); + QObject::connect(_refreshTimer.data(), &QTimer::timeout, this, &DiskCacheEditor::refresh); + _refreshTimer->start(); + + QPushButton* clearCacheButton = new QPushButton(_dialog); + Q_CHECK_PTR(clearCacheButton); + clearCacheButton->setText("Clear"); + clearCacheButton->setToolTip("Erases the entire content of the disk cache."); + connect(clearCacheButton, SIGNAL(clicked()), SLOT(clear())); + layout->addWidget(clearCacheButton, 3, 3); +} + +void DiskCacheEditor::refresh() { + DependencyManager::get()->cacheInfoRequest(this, "cacheInfoCallback"); +} + +void DiskCacheEditor::cacheInfoCallback(QString cacheDirectory, qint64 cacheSize, qint64 maximumCacheSize) { + static const auto stringify = [](qint64 number) { + static const QStringList UNITS = QStringList() << "B" << "KB" << "MB" << "GB"; + static const qint64 CHUNK = 1024; + QString unit; + int i = 0; + for (i = 0; i < 4; ++i) { + if (number / CHUNK > 0) { + number /= CHUNK; + } else { + break; + } + } + return QString("%0 %1").arg(number).arg(UNITS[i]); + }; + + if (_path) { + _path->setText(cacheDirectory); + } + if (_size) { + _size->setText(stringify(cacheSize)); + } + if (_maxSize) { + _maxSize->setText(stringify(maximumCacheSize)); + } +} + +void DiskCacheEditor::clear() { + auto buttonClicked = OffscreenUi::question(_dialog, "Clearing disk cache", + "You are about to erase all the content of the disk cache, " + "are you sure you want to do that?", + QMessageBox::Ok | QMessageBox::Cancel); + if (buttonClicked == QMessageBox::Ok) { + DependencyManager::get()->clearCache(); + } +} diff --git a/interface/src/ui/DiskCacheEditor.h b/interface/src/ui/DiskCacheEditor.h new file mode 100644 index 0000000000..3f8fa1a883 --- /dev/null +++ b/interface/src/ui/DiskCacheEditor.h @@ -0,0 +1,49 @@ +// +// DiskCacheEditor.h +// +// +// Created by Clement on 3/4/15. +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_DiskCacheEditor_h +#define hifi_DiskCacheEditor_h + +#include +#include + +class QDialog; +class QLabel; +class QWindow; +class QTimer; + +class DiskCacheEditor : public QObject { + Q_OBJECT + +public: + DiskCacheEditor(QWidget* parent = nullptr); + + QWindow* windowHandle(); + +public slots: + void toggle(); + +private slots: + void refresh(); + void cacheInfoCallback(QString cacheDirectory, qint64 cacheSize, qint64 maximumCacheSize); + void clear(); + +private: + void makeDialog(); + + QPointer _dialog; + QPointer _path; + QPointer _size; + QPointer _maxSize; + QPointer _refreshTimer; +}; + +#endif // hifi_DiskCacheEditor_h \ No newline at end of file diff --git a/interface/src/ui/ScriptEditBox.cpp b/interface/src/ui/ScriptEditBox.cpp new file mode 100644 index 0000000000..2aea225b17 --- /dev/null +++ b/interface/src/ui/ScriptEditBox.cpp @@ -0,0 +1,111 @@ +// +// ScriptEditBox.cpp +// interface/src/ui +// +// Created by Thijs Wenker on 4/30/14. +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "ScriptEditBox.h" + +#include +#include + +#include "ScriptLineNumberArea.h" + +ScriptEditBox::ScriptEditBox(QWidget* parent) : + QPlainTextEdit(parent) +{ + _scriptLineNumberArea = new ScriptLineNumberArea(this); + + connect(this, &ScriptEditBox::blockCountChanged, this, &ScriptEditBox::updateLineNumberAreaWidth); + connect(this, &ScriptEditBox::updateRequest, this, &ScriptEditBox::updateLineNumberArea); + connect(this, &ScriptEditBox::cursorPositionChanged, this, &ScriptEditBox::highlightCurrentLine); + + updateLineNumberAreaWidth(0); + highlightCurrentLine(); +} + +int ScriptEditBox::lineNumberAreaWidth() { + int digits = 1; + const int SPACER_PIXELS = 3; + const int BASE_TEN = 10; + int max = qMax(1, blockCount()); + while (max >= BASE_TEN) { + max /= BASE_TEN; + digits++; + } + return SPACER_PIXELS + fontMetrics().width(QLatin1Char('H')) * digits; +} + +void ScriptEditBox::updateLineNumberAreaWidth(int blockCount) { + setViewportMargins(lineNumberAreaWidth(), 0, 0, 0); +} + +void ScriptEditBox::updateLineNumberArea(const QRect& rect, int deltaY) { + if (deltaY) { + _scriptLineNumberArea->scroll(0, deltaY); + } else { + _scriptLineNumberArea->update(0, rect.y(), _scriptLineNumberArea->width(), rect.height()); + } + + if (rect.contains(viewport()->rect())) { + updateLineNumberAreaWidth(0); + } +} + +void ScriptEditBox::resizeEvent(QResizeEvent* event) { + QPlainTextEdit::resizeEvent(event); + + QRect localContentsRect = contentsRect(); + _scriptLineNumberArea->setGeometry(QRect(localContentsRect.left(), localContentsRect.top(), lineNumberAreaWidth(), + localContentsRect.height())); +} + +void ScriptEditBox::highlightCurrentLine() { + QList extraSelections; + + if (!isReadOnly()) { + QTextEdit::ExtraSelection selection; + + QColor lineColor = QColor(Qt::gray).lighter(); + + selection.format.setBackground(lineColor); + selection.format.setProperty(QTextFormat::FullWidthSelection, true); + selection.cursor = textCursor(); + selection.cursor.clearSelection(); + extraSelections.append(selection); + } + + setExtraSelections(extraSelections); +} + +void ScriptEditBox::lineNumberAreaPaintEvent(QPaintEvent* event) +{ + QPainter painter(_scriptLineNumberArea); + painter.fillRect(event->rect(), Qt::lightGray); + QTextBlock block = firstVisibleBlock(); + int blockNumber = block.blockNumber(); + int top = (int) blockBoundingGeometry(block).translated(contentOffset()).top(); + int bottom = top + (int) blockBoundingRect(block).height(); + + while (block.isValid() && top <= event->rect().bottom()) { + if (block.isVisible() && bottom >= event->rect().top()) { + QFont font = painter.font(); + font.setBold(this->textCursor().blockNumber() == block.blockNumber()); + painter.setFont(font); + QString number = QString::number(blockNumber + 1); + painter.setPen(Qt::black); + painter.drawText(0, top, _scriptLineNumberArea->width(), fontMetrics().height(), + Qt::AlignRight, number); + } + + block = block.next(); + top = bottom; + bottom = top + (int) blockBoundingRect(block).height(); + blockNumber++; + } +} diff --git a/interface/src/ui/ScriptEditBox.h b/interface/src/ui/ScriptEditBox.h new file mode 100644 index 0000000000..0b037db16a --- /dev/null +++ b/interface/src/ui/ScriptEditBox.h @@ -0,0 +1,38 @@ +// +// ScriptEditBox.h +// interface/src/ui +// +// Created by Thijs Wenker on 4/30/14. +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_ScriptEditBox_h +#define hifi_ScriptEditBox_h + +#include + +class ScriptEditBox : public QPlainTextEdit { + Q_OBJECT + +public: + ScriptEditBox(QWidget* parent = NULL); + + void lineNumberAreaPaintEvent(QPaintEvent* event); + int lineNumberAreaWidth(); + +protected: + void resizeEvent(QResizeEvent* event) override; + +private slots: + void updateLineNumberAreaWidth(int blockCount); + void highlightCurrentLine(); + void updateLineNumberArea(const QRect& rect, int deltaY); + +private: + QWidget* _scriptLineNumberArea; +}; + +#endif // hifi_ScriptEditBox_h diff --git a/interface/src/ui/ScriptEditorWidget.cpp b/interface/src/ui/ScriptEditorWidget.cpp new file mode 100644 index 0000000000..ada6b11355 --- /dev/null +++ b/interface/src/ui/ScriptEditorWidget.cpp @@ -0,0 +1,256 @@ +// +// ScriptEditorWidget.cpp +// interface/src/ui +// +// Created by Thijs Wenker on 4/14/14. +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "ui_scriptEditorWidget.h" +#include "ScriptEditorWidget.h" +#include "ScriptEditorWindow.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "Application.h" +#include "ScriptHighlighting.h" + +ScriptEditorWidget::ScriptEditorWidget() : + _scriptEditorWidgetUI(new Ui::ScriptEditorWidget), + _scriptEngine(NULL), + _isRestarting(false), + _isReloading(false) +{ + setAttribute(Qt::WA_DeleteOnClose); + + _scriptEditorWidgetUI->setupUi(this); + + connect(_scriptEditorWidgetUI->scriptEdit->document(), &QTextDocument::modificationChanged, this, + &ScriptEditorWidget::scriptModified); + connect(_scriptEditorWidgetUI->scriptEdit->document(), &QTextDocument::contentsChanged, this, + &ScriptEditorWidget::onScriptModified); + + // remove the title bar (see the Qt docs on setTitleBarWidget) + setTitleBarWidget(new QWidget()); + QFontMetrics fm(_scriptEditorWidgetUI->scriptEdit->font()); + _scriptEditorWidgetUI->scriptEdit->setTabStopWidth(fm.width('0') * 4); + // We create a new ScriptHighligting QObject and provide it with a parent so this is NOT a memory leak. + new ScriptHighlighting(_scriptEditorWidgetUI->scriptEdit->document()); + QTimer::singleShot(0, _scriptEditorWidgetUI->scriptEdit, SLOT(setFocus())); + + _console = new JSConsole(this); + _console->setFixedHeight(CONSOLE_HEIGHT); + _scriptEditorWidgetUI->verticalLayout->addWidget(_console); + connect(_scriptEditorWidgetUI->clearButton, &QPushButton::clicked, _console, &JSConsole::clear); +} + +ScriptEditorWidget::~ScriptEditorWidget() { + delete _scriptEditorWidgetUI; + delete _console; +} + +void ScriptEditorWidget::onScriptModified() { + if(_scriptEditorWidgetUI->onTheFlyCheckBox->isChecked() && isModified() && isRunning() && !_isReloading) { + _isRestarting = true; + setRunning(false); + // Script is restarted once current script instance finishes. + } +} + +void ScriptEditorWidget::onScriptFinished(const QString& scriptPath) { + _scriptEngine = NULL; + _console->setScriptEngine(NULL); + if (_isRestarting) { + _isRestarting = false; + setRunning(true); + } +} + +bool ScriptEditorWidget::isModified() { + return _scriptEditorWidgetUI->scriptEdit->document()->isModified(); +} + +bool ScriptEditorWidget::isRunning() { + return (_scriptEngine != NULL) ? _scriptEngine->isRunning() : false; +} + +bool ScriptEditorWidget::setRunning(bool run) { + if (run && isModified() && !save()) { + return false; + } + + if (_scriptEngine != NULL) { + disconnect(_scriptEngine, &ScriptEngine::runningStateChanged, this, &ScriptEditorWidget::runningStateChanged); + disconnect(_scriptEngine, &ScriptEngine::update, this, &ScriptEditorWidget::onScriptModified); + disconnect(_scriptEngine, &ScriptEngine::finished, this, &ScriptEditorWidget::onScriptFinished); + } + + auto scriptEngines = DependencyManager::get(); + if (run) { + const QString& scriptURLString = QUrl(_currentScript).toString(); + // Reload script so that an out of date copy is not retrieved from the cache + _scriptEngine = scriptEngines->loadScript(scriptURLString, true, true, false, true); + connect(_scriptEngine, &ScriptEngine::runningStateChanged, this, &ScriptEditorWidget::runningStateChanged); + connect(_scriptEngine, &ScriptEngine::update, this, &ScriptEditorWidget::onScriptModified); + connect(_scriptEngine, &ScriptEngine::finished, this, &ScriptEditorWidget::onScriptFinished); + } else { + connect(_scriptEngine, &ScriptEngine::finished, this, &ScriptEditorWidget::onScriptFinished); + const QString& scriptURLString = QUrl(_currentScript).toString(); + scriptEngines->stopScript(scriptURLString); + _scriptEngine = NULL; + } + _console->setScriptEngine(_scriptEngine); + return true; +} + +bool ScriptEditorWidget::saveFile(const QString &scriptPath) { + QFile file(scriptPath); + if (!file.open(QFile::WriteOnly | QFile::Text)) { + OffscreenUi::warning(this, tr("Interface"), tr("Cannot write script %1:\n%2.").arg(scriptPath) + .arg(file.errorString())); + return false; + } + + QTextStream out(&file); + out << _scriptEditorWidgetUI->scriptEdit->toPlainText(); + file.close(); + + setScriptFile(scriptPath); + return true; +} + +void ScriptEditorWidget::loadFile(const QString& scriptPath) { + QUrl url(scriptPath); + + // if the scheme length is one or lower, maybe they typed in a file, let's try + const int WINDOWS_DRIVE_LETTER_SIZE = 1; + if (url.scheme().size() <= WINDOWS_DRIVE_LETTER_SIZE) { + QFile file(scriptPath); + if (!file.open(QFile::ReadOnly | QFile::Text)) { + OffscreenUi::warning(this, tr("Interface"), tr("Cannot read script %1:\n%2.").arg(scriptPath) + .arg(file.errorString())); + return; + } + QTextStream in(&file); + _scriptEditorWidgetUI->scriptEdit->setPlainText(in.readAll()); + file.close(); + setScriptFile(scriptPath); + + if (_scriptEngine != NULL) { + disconnect(_scriptEngine, &ScriptEngine::runningStateChanged, this, &ScriptEditorWidget::runningStateChanged); + disconnect(_scriptEngine, &ScriptEngine::update, this, &ScriptEditorWidget::onScriptModified); + disconnect(_scriptEngine, &ScriptEngine::finished, this, &ScriptEditorWidget::onScriptFinished); + } + } else { + QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); + QNetworkRequest networkRequest = QNetworkRequest(url); + networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); + networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); + QNetworkReply* reply = networkAccessManager.get(networkRequest); + qDebug() << "Downloading included script at" << scriptPath; + QEventLoop loop; + QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); + loop.exec(); + _scriptEditorWidgetUI->scriptEdit->setPlainText(reply->readAll()); + delete reply; + + if (!saveAs()) { + static_cast(this->parent()->parent()->parent())->terminateCurrentTab(); + } + } + const QString& scriptURLString = QUrl(_currentScript).toString(); + _scriptEngine = DependencyManager::get()->getScriptEngine(scriptURLString); + if (_scriptEngine != NULL) { + connect(_scriptEngine, &ScriptEngine::runningStateChanged, this, &ScriptEditorWidget::runningStateChanged); + connect(_scriptEngine, &ScriptEngine::update, this, &ScriptEditorWidget::onScriptModified); + connect(_scriptEngine, &ScriptEngine::finished, this, &ScriptEditorWidget::onScriptFinished); + } + _console->setScriptEngine(_scriptEngine); +} + +bool ScriptEditorWidget::save() { + return _currentScript.isEmpty() ? saveAs() : saveFile(_currentScript); +} + +bool ScriptEditorWidget::saveAs() { + auto scriptEngines = DependencyManager::get(); + QString fileName = QFileDialog::getSaveFileName(this, tr("Save script"), + qApp->getPreviousScriptLocation(), + tr("JavaScript Files (*.js)")); + if (!fileName.isEmpty()) { + qApp->setPreviousScriptLocation(fileName); + return saveFile(fileName); + } else { + return false; + } +} + +void ScriptEditorWidget::setScriptFile(const QString& scriptPath) { + _currentScript = scriptPath; + _currentScriptModified = QFileInfo(_currentScript).lastModified(); + _scriptEditorWidgetUI->scriptEdit->document()->setModified(false); + setWindowModified(false); + + emit scriptnameChanged(); +} + +bool ScriptEditorWidget::questionSave() { + if (_scriptEditorWidgetUI->scriptEdit->document()->isModified()) { + QMessageBox::StandardButton button = OffscreenUi::warning(this, tr("Interface"), + tr("The script has been modified.\nDo you want to save your changes?"), QMessageBox::Save | QMessageBox::Discard | + QMessageBox::Cancel, QMessageBox::Save); + return button == QMessageBox::Save ? save() : (button == QMessageBox::Discard); + } + return true; +} + +void ScriptEditorWidget::onWindowActivated() { + if (!_isReloading) { + _isReloading = true; + + QDateTime fileStamp = QFileInfo(_currentScript).lastModified(); + if (fileStamp > _currentScriptModified) { + bool doReload = false; + auto window = static_cast(this->parent()->parent()->parent()); + window->inModalDialog = true; + if (window->autoReloadScripts() + || OffscreenUi::question(this, tr("Reload Script"), + tr("The following file has been modified outside of the Interface editor:") + "\n" + _currentScript + "\n" + + (isModified() + ? tr("Do you want to reload it and lose the changes you've made in the Interface editor?") + : tr("Do you want to reload it?")), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + doReload = true; + } + window->inModalDialog = false; + if (doReload) { + loadFile(_currentScript); + if (_scriptEditorWidgetUI->onTheFlyCheckBox->isChecked() && isRunning()) { + _isRestarting = true; + setRunning(false); + // Script is restarted once current script instance finishes. + } + } else { + _currentScriptModified = fileStamp; // Asked and answered. Don't ask again until the external file is changed again. + } + } + _isReloading = false; + } +} diff --git a/interface/src/ui/ScriptEditorWidget.h b/interface/src/ui/ScriptEditorWidget.h new file mode 100644 index 0000000000..f53fd7b718 --- /dev/null +++ b/interface/src/ui/ScriptEditorWidget.h @@ -0,0 +1,64 @@ +// +// ScriptEditorWidget.h +// interface/src/ui +// +// Created by Thijs Wenker on 4/14/14. +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_ScriptEditorWidget_h +#define hifi_ScriptEditorWidget_h + +#include + +#include "JSConsole.h" +#include "ScriptEngine.h" + +namespace Ui { + class ScriptEditorWidget; +} + +class ScriptEditorWidget : public QDockWidget { + Q_OBJECT + +public: + ScriptEditorWidget(); + ~ScriptEditorWidget(); + + bool isModified(); + bool isRunning(); + bool setRunning(bool run); + bool saveFile(const QString& scriptPath); + void loadFile(const QString& scriptPath); + void setScriptFile(const QString& scriptPath); + bool save(); + bool saveAs(); + bool questionSave(); + const QString getScriptName() const { return _currentScript; }; + +signals: + void runningStateChanged(); + void scriptnameChanged(); + void scriptModified(); + +public slots: + void onWindowActivated(); + +private slots: + void onScriptModified(); + void onScriptFinished(const QString& scriptName); + +private: + JSConsole* _console; + Ui::ScriptEditorWidget* _scriptEditorWidgetUI; + ScriptEngine* _scriptEngine; + QString _currentScript; + QDateTime _currentScriptModified; + bool _isRestarting; + bool _isReloading; +}; + +#endif // hifi_ScriptEditorWidget_h diff --git a/interface/src/ui/ScriptEditorWindow.cpp b/interface/src/ui/ScriptEditorWindow.cpp new file mode 100644 index 0000000000..58abd23979 --- /dev/null +++ b/interface/src/ui/ScriptEditorWindow.cpp @@ -0,0 +1,259 @@ +// +// ScriptEditorWindow.cpp +// interface/src/ui +// +// Created by Thijs Wenker on 4/14/14. +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include + +#include "ui_scriptEditorWindow.h" +#include "ScriptEditorWindow.h" +#include "ScriptEditorWidget.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include "Application.h" +#include "PathUtils.h" + +ScriptEditorWindow::ScriptEditorWindow(QWidget* parent) : + QWidget(parent), + _ScriptEditorWindowUI(new Ui::ScriptEditorWindow), + _loadMenu(new QMenu), + _saveMenu(new QMenu) +{ + setAttribute(Qt::WA_DeleteOnClose); + + _ScriptEditorWindowUI->setupUi(this); + + this->setWindowFlags(Qt::Tool); + addScriptEditorWidget("New script"); + connect(_loadMenu, &QMenu::aboutToShow, this, &ScriptEditorWindow::loadMenuAboutToShow); + _ScriptEditorWindowUI->loadButton->setMenu(_loadMenu); + + _saveMenu->addAction("Save as..", this, SLOT(saveScriptAsClicked()), Qt::CTRL | Qt::SHIFT | Qt::Key_S); + + _ScriptEditorWindowUI->saveButton->setMenu(_saveMenu); + + connect(new QShortcut(QKeySequence("Ctrl+N"), this), &QShortcut::activated, this, &ScriptEditorWindow::newScriptClicked); + connect(new QShortcut(QKeySequence("Ctrl+S"), this), &QShortcut::activated, this,&ScriptEditorWindow::saveScriptClicked); + connect(new QShortcut(QKeySequence("Ctrl+O"), this), &QShortcut::activated, this, &ScriptEditorWindow::loadScriptClicked); + connect(new QShortcut(QKeySequence("F5"), this), &QShortcut::activated, this, &ScriptEditorWindow::toggleRunScriptClicked); + + _ScriptEditorWindowUI->loadButton->setIcon(QIcon(QPixmap(PathUtils::resourcesPath() + "icons/load-script.svg"))); + _ScriptEditorWindowUI->newButton->setIcon(QIcon(QPixmap(PathUtils::resourcesPath() + "icons/new-script.svg"))); + _ScriptEditorWindowUI->saveButton->setIcon(QIcon(QPixmap(PathUtils::resourcesPath() + "icons/save-script.svg"))); + _ScriptEditorWindowUI->toggleRunButton->setIcon(QIcon(QPixmap(PathUtils::resourcesPath() + "icons/start-script.svg"))); +} + +ScriptEditorWindow::~ScriptEditorWindow() { + delete _ScriptEditorWindowUI; +} + +void ScriptEditorWindow::setRunningState(bool run) { + if (_ScriptEditorWindowUI->tabWidget->currentIndex() != -1) { + static_cast(_ScriptEditorWindowUI->tabWidget->currentWidget())->setRunning(run); + } + this->updateButtons(); +} + +void ScriptEditorWindow::updateButtons() { + bool isRunning = _ScriptEditorWindowUI->tabWidget->currentIndex() != -1 && + static_cast(_ScriptEditorWindowUI->tabWidget->currentWidget())->isRunning(); + _ScriptEditorWindowUI->toggleRunButton->setEnabled(_ScriptEditorWindowUI->tabWidget->currentIndex() != -1); + _ScriptEditorWindowUI->toggleRunButton->setIcon(QIcon(QPixmap(PathUtils::resourcesPath() + ((isRunning ? + "icons/stop-script.svg" : "icons/start-script.svg"))))); +} + +void ScriptEditorWindow::loadScriptMenu(const QString& scriptName) { + addScriptEditorWidget("loading...")->loadFile(scriptName); + updateButtons(); +} + +void ScriptEditorWindow::loadScriptClicked() { + QString scriptName = QFileDialog::getOpenFileName(this, tr("Interface"), + qApp->getPreviousScriptLocation(), + tr("JavaScript Files (*.js)")); + if (!scriptName.isEmpty()) { + qApp->setPreviousScriptLocation(scriptName); + addScriptEditorWidget("loading...")->loadFile(scriptName); + updateButtons(); + } +} + +void ScriptEditorWindow::loadMenuAboutToShow() { + _loadMenu->clear(); + QStringList runningScripts = DependencyManager::get()->getRunningScripts(); + if (runningScripts.count() > 0) { + QSignalMapper* signalMapper = new QSignalMapper(this); + foreach (const QString& runningScript, runningScripts) { + QAction* runningScriptAction = new QAction(runningScript, _loadMenu); + connect(runningScriptAction, SIGNAL(triggered()), signalMapper, SLOT(map())); + signalMapper->setMapping(runningScriptAction, runningScript); + _loadMenu->addAction(runningScriptAction); + } + connect(signalMapper, SIGNAL(mapped(const QString &)), this, SLOT(loadScriptMenu(const QString&))); + } else { + QAction* naAction = new QAction("(no running scripts)", _loadMenu); + naAction->setDisabled(true); + _loadMenu->addAction(naAction); + } +} + +void ScriptEditorWindow::newScriptClicked() { + addScriptEditorWidget(QString("New script")); +} + +void ScriptEditorWindow::toggleRunScriptClicked() { + this->setRunningState(!(_ScriptEditorWindowUI->tabWidget->currentIndex() !=-1 + && static_cast(_ScriptEditorWindowUI->tabWidget->currentWidget())->isRunning())); +} + +void ScriptEditorWindow::saveScriptClicked() { + if (_ScriptEditorWindowUI->tabWidget->currentIndex() != -1) { + ScriptEditorWidget* currentScriptWidget = static_cast(_ScriptEditorWindowUI->tabWidget + ->currentWidget()); + currentScriptWidget->save(); + } +} + +void ScriptEditorWindow::saveScriptAsClicked() { + if (_ScriptEditorWindowUI->tabWidget->currentIndex() != -1) { + ScriptEditorWidget* currentScriptWidget = static_cast(_ScriptEditorWindowUI->tabWidget + ->currentWidget()); + currentScriptWidget->saveAs(); + } +} + +ScriptEditorWidget* ScriptEditorWindow::addScriptEditorWidget(QString title) { + ScriptEditorWidget* newScriptEditorWidget = new ScriptEditorWidget(); + connect(newScriptEditorWidget, &ScriptEditorWidget::scriptnameChanged, this, &ScriptEditorWindow::updateScriptNameOrStatus); + connect(newScriptEditorWidget, &ScriptEditorWidget::scriptModified, this, &ScriptEditorWindow::updateScriptNameOrStatus); + connect(newScriptEditorWidget, &ScriptEditorWidget::runningStateChanged, this, &ScriptEditorWindow::updateButtons); + connect(this, &ScriptEditorWindow::windowActivated, newScriptEditorWidget, &ScriptEditorWidget::onWindowActivated); + _ScriptEditorWindowUI->tabWidget->addTab(newScriptEditorWidget, title); + _ScriptEditorWindowUI->tabWidget->setCurrentWidget(newScriptEditorWidget); + newScriptEditorWidget->setUpdatesEnabled(true); + newScriptEditorWidget->adjustSize(); + return newScriptEditorWidget; +} + +void ScriptEditorWindow::tabSwitched(int tabIndex) { + this->updateButtons(); + if (_ScriptEditorWindowUI->tabWidget->currentIndex() != -1) { + ScriptEditorWidget* currentScriptWidget = static_cast(_ScriptEditorWindowUI->tabWidget + ->currentWidget()); + QString modifiedStar = (currentScriptWidget->isModified() ? "*" : ""); + if (currentScriptWidget->getScriptName().length() > 0) { + this->setWindowTitle("Script Editor [" + currentScriptWidget->getScriptName() + modifiedStar + "]"); + } else { + this->setWindowTitle("Script Editor [New script" + modifiedStar + "]"); + } + } else { + this->setWindowTitle("Script Editor"); + } +} + +void ScriptEditorWindow::tabCloseRequested(int tabIndex) { + if (ignoreCloseForModal(nullptr)) { + return; + } + ScriptEditorWidget* closingScriptWidget = static_cast(_ScriptEditorWindowUI->tabWidget + ->widget(tabIndex)); + if(closingScriptWidget->questionSave()) { + _ScriptEditorWindowUI->tabWidget->removeTab(tabIndex); + } +} + +// If this operating system window causes a qml overlay modal dialog (which might not even be seen by the user), closing this window +// will crash the code that was waiting on the dialog result. So that code whousl set inModalDialog to true while the question is up. +// This code will not be necessary when switch out all operating system windows for qml overlays. +bool ScriptEditorWindow::ignoreCloseForModal(QCloseEvent* event) { + if (!inModalDialog) { + return false; + } + // Deliberately not using OffscreenUi, so that the dialog is seen. + QMessageBox::information(this, tr("Interface"), tr("There is a modal dialog that must be answered before closing."), + QMessageBox::Discard, QMessageBox::Discard); + if (event) { + event->ignore(); // don't close + } + return true; +} + +void ScriptEditorWindow::closeEvent(QCloseEvent *event) { + if (ignoreCloseForModal(event)) { + return; + } + bool unsaved_docs_warning = false; + for (int i = 0; i < _ScriptEditorWindowUI->tabWidget->count(); i++){ + if(static_cast(_ScriptEditorWindowUI->tabWidget->widget(i))->isModified()){ + unsaved_docs_warning = true; + break; + } + } + + if (!unsaved_docs_warning || QMessageBox::warning(this, tr("Interface"), + tr("There are some unsaved scripts, are you sure you want to close the editor? Changes will be lost!"), + QMessageBox::Discard | QMessageBox::Cancel, QMessageBox::Cancel) == QMessageBox::Discard) { + event->accept(); + } else { + event->ignore(); + } +} + +void ScriptEditorWindow::updateScriptNameOrStatus() { + ScriptEditorWidget* source = static_cast(QObject::sender()); + QString modifiedStar = (source->isModified()? "*" : ""); + if (source->getScriptName().length() > 0) { + for (int i = 0; i < _ScriptEditorWindowUI->tabWidget->count(); i++){ + if (_ScriptEditorWindowUI->tabWidget->widget(i) == source) { + _ScriptEditorWindowUI->tabWidget->setTabText(i, modifiedStar + QFileInfo(source->getScriptName()).fileName()); + _ScriptEditorWindowUI->tabWidget->setTabToolTip(i, source->getScriptName()); + } + } + } + + if (_ScriptEditorWindowUI->tabWidget->currentWidget() == source) { + if (source->getScriptName().length() > 0) { + this->setWindowTitle("Script Editor [" + source->getScriptName() + modifiedStar + "]"); + } else { + this->setWindowTitle("Script Editor [New script" + modifiedStar + "]"); + } + } +} + +void ScriptEditorWindow::terminateCurrentTab() { + if (_ScriptEditorWindowUI->tabWidget->currentIndex() != -1) { + _ScriptEditorWindowUI->tabWidget->removeTab(_ScriptEditorWindowUI->tabWidget->currentIndex()); + this->raise(); + } +} + +bool ScriptEditorWindow::autoReloadScripts() { + return _ScriptEditorWindowUI->autoReloadCheckBox->isChecked(); +} + +bool ScriptEditorWindow::event(QEvent* event) { + if (event->type() == QEvent::WindowActivate) { + emit windowActivated(); + } + return QWidget::event(event); +} + diff --git a/interface/src/ui/ScriptEditorWindow.h b/interface/src/ui/ScriptEditorWindow.h new file mode 100644 index 0000000000..af9863d136 --- /dev/null +++ b/interface/src/ui/ScriptEditorWindow.h @@ -0,0 +1,64 @@ +// +// ScriptEditorWindow.h +// interface/src/ui +// +// Created by Thijs Wenker on 4/14/14. +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_ScriptEditorWindow_h +#define hifi_ScriptEditorWindow_h + +#include "ScriptEditorWidget.h" + +namespace Ui { + class ScriptEditorWindow; +} + +class ScriptEditorWindow : public QWidget { + Q_OBJECT + +public: + ScriptEditorWindow(QWidget* parent = nullptr); + ~ScriptEditorWindow(); + + void terminateCurrentTab(); + bool autoReloadScripts(); + + bool inModalDialog { false }; + bool ignoreCloseForModal(QCloseEvent* event); + +signals: + void windowActivated(); + +protected: + void closeEvent(QCloseEvent* event) override; + virtual bool event(QEvent* event) override; + +private: + Ui::ScriptEditorWindow* _ScriptEditorWindowUI; + QMenu* _loadMenu; + QMenu* _saveMenu; + + ScriptEditorWidget* addScriptEditorWidget(QString title); + void setRunningState(bool run); + void setScriptName(const QString& scriptName); + +private slots: + void loadScriptMenu(const QString& scriptName); + void loadScriptClicked(); + void newScriptClicked(); + void toggleRunScriptClicked(); + void saveScriptClicked(); + void saveScriptAsClicked(); + void loadMenuAboutToShow(); + void tabSwitched(int tabIndex); + void tabCloseRequested(int tabIndex); + void updateScriptNameOrStatus(); + void updateButtons(); +}; + +#endif // hifi_ScriptEditorWindow_h diff --git a/interface/src/ui/ScriptLineNumberArea.cpp b/interface/src/ui/ScriptLineNumberArea.cpp new file mode 100644 index 0000000000..6d7e9185ea --- /dev/null +++ b/interface/src/ui/ScriptLineNumberArea.cpp @@ -0,0 +1,28 @@ +// +// ScriptLineNumberArea.cpp +// interface/src/ui +// +// Created by Thijs Wenker on 4/30/14. +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "ScriptLineNumberArea.h" + +#include "ScriptEditBox.h" + +ScriptLineNumberArea::ScriptLineNumberArea(ScriptEditBox* scriptEditBox) : + QWidget(scriptEditBox) +{ + _scriptEditBox = scriptEditBox; +} + +QSize ScriptLineNumberArea::sizeHint() const { + return QSize(_scriptEditBox->lineNumberAreaWidth(), 0); +} + +void ScriptLineNumberArea::paintEvent(QPaintEvent* event) { + _scriptEditBox->lineNumberAreaPaintEvent(event); +} diff --git a/interface/src/ui/ScriptLineNumberArea.h b/interface/src/ui/ScriptLineNumberArea.h new file mode 100644 index 0000000000..77de8244ce --- /dev/null +++ b/interface/src/ui/ScriptLineNumberArea.h @@ -0,0 +1,32 @@ +// +// ScriptLineNumberArea.h +// interface/src/ui +// +// Created by Thijs Wenker on 4/30/14. +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_ScriptLineNumberArea_h +#define hifi_ScriptLineNumberArea_h + +#include + +class ScriptEditBox; + +class ScriptLineNumberArea : public QWidget { + +public: + ScriptLineNumberArea(ScriptEditBox* scriptEditBox); + QSize sizeHint() const override; + +protected: + void paintEvent(QPaintEvent* event) override; + +private: + ScriptEditBox* _scriptEditBox; +}; + +#endif // hifi_ScriptLineNumberArea_h diff --git a/interface/src/ui/ScriptsTableWidget.cpp b/interface/src/ui/ScriptsTableWidget.cpp new file mode 100644 index 0000000000..7b4f9e6b1f --- /dev/null +++ b/interface/src/ui/ScriptsTableWidget.cpp @@ -0,0 +1,49 @@ +// +// ScriptsTableWidget.cpp +// interface +// +// Created by Mohammed Nafees on 04/03/2014. +// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include +#include +#include +#include + +#include "ScriptsTableWidget.h" + +ScriptsTableWidget::ScriptsTableWidget(QWidget* parent) : + QTableWidget(parent) { + verticalHeader()->setVisible(false); + horizontalHeader()->setVisible(false); + setShowGrid(false); + setSelectionMode(QAbstractItemView::NoSelection); + setEditTriggers(QAbstractItemView::NoEditTriggers); + setStyleSheet("QTableWidget { border: none; background: transparent; color: #333333; } QToolTip { color: #000000; background: #f9f6e4; padding: 2px; }"); + setToolTipDuration(200); + setWordWrap(true); + setGeometry(0, 0, parent->width(), parent->height()); +} + +void ScriptsTableWidget::paintEvent(QPaintEvent* event) { + QPainter painter(viewport()); + painter.setPen(QColor::fromRgb(225, 225, 225)); // #e1e1e1 + + int y = 0; + for (int i = 0; i < rowCount(); i++) { + painter.drawLine(5, rowHeight(i) + y, width(), rowHeight(i) + y); + y += rowHeight(i); + } + painter.end(); + + QTableWidget::paintEvent(event); +} + +void ScriptsTableWidget::keyPressEvent(QKeyEvent* event) { + // Ignore keys so they will propagate correctly + event->ignore(); +} diff --git a/interface/src/ui/ScriptsTableWidget.h b/interface/src/ui/ScriptsTableWidget.h new file mode 100644 index 0000000000..f5e3407e97 --- /dev/null +++ b/interface/src/ui/ScriptsTableWidget.h @@ -0,0 +1,28 @@ +// +// ScriptsTableWidget.h +// interface +// +// Created by Mohammed Nafees on 04/03/2014. +// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi__ScriptsTableWidget_h +#define hifi__ScriptsTableWidget_h + +#include +#include + +class ScriptsTableWidget : public QTableWidget { + Q_OBJECT +public: + explicit ScriptsTableWidget(QWidget* parent); + +protected: + virtual void paintEvent(QPaintEvent* event) override; + virtual void keyPressEvent(QKeyEvent* event) override; +}; + +#endif // hifi__ScriptsTableWidget_h diff --git a/interface/src/ui/Stats.cpp b/interface/src/ui/Stats.cpp index cedcb923d9..923d9f642d 100644 --- a/interface/src/ui/Stats.cpp +++ b/interface/src/ui/Stats.cpp @@ -38,8 +38,6 @@ using namespace std; static Stats* INSTANCE{ nullptr }; -QString getTextureMemoryPressureModeString(); - Stats* Stats::getInstance() { if (!INSTANCE) { Stats::registerType(); @@ -222,10 +220,10 @@ void Stats::updateStats(bool force) { STAT_UPDATE(audioMixerInPps, roundf(bandwidthRecorder->getAverageInputPacketsPerSecond(NodeType::AudioMixer))); STAT_UPDATE(audioMixerOutKbps, roundf(bandwidthRecorder->getAverageOutputKilobitsPerSecond(NodeType::AudioMixer))); STAT_UPDATE(audioMixerOutPps, roundf(bandwidthRecorder->getAverageOutputPacketsPerSecond(NodeType::AudioMixer))); + STAT_UPDATE(audioMicOutboundPPS, audioClient->getMicAudioOutboundPPS()); + STAT_UPDATE(audioSilentOutboundPPS, audioClient->getSilentOutboundPPS()); STAT_UPDATE(audioAudioInboundPPS, audioClient->getAudioInboundPPS()); STAT_UPDATE(audioSilentInboundPPS, audioClient->getSilentInboundPPS()); - STAT_UPDATE(audioOutboundPPS, audioClient->getAudioOutboundPPS()); - STAT_UPDATE(audioSilentOutboundPPS, audioClient->getSilentOutboundPPS()); } else { STAT_UPDATE(audioMixerKbps, -1); STAT_UPDATE(audioMixerPps, -1); @@ -233,7 +231,7 @@ void Stats::updateStats(bool force) { STAT_UPDATE(audioMixerInPps, -1); STAT_UPDATE(audioMixerOutKbps, -1); STAT_UPDATE(audioMixerOutPps, -1); - STAT_UPDATE(audioOutboundPPS, -1); + STAT_UPDATE(audioMicOutboundPPS, -1); STAT_UPDATE(audioSilentOutboundPPS, -1); STAT_UPDATE(audioAudioInboundPPS, -1); STAT_UPDATE(audioSilentInboundPPS, -1); @@ -342,12 +340,10 @@ void Stats::updateStats(bool force) { STAT_UPDATE(glContextSwapchainMemory, (int)BYTES_TO_MB(gl::Context::getSwapchainMemoryUsage())); STAT_UPDATE(qmlTextureMemory, (int)BYTES_TO_MB(OffscreenQmlSurface::getUsedTextureMemory())); - STAT_UPDATE(texturePendingTransfers, (int)BYTES_TO_MB(gpu::Texture::getTextureTransferPendingSize())); STAT_UPDATE(gpuTextureMemory, (int)BYTES_TO_MB(gpu::Texture::getTextureGPUMemoryUsage())); STAT_UPDATE(gpuTextureVirtualMemory, (int)BYTES_TO_MB(gpu::Texture::getTextureGPUVirtualMemoryUsage())); STAT_UPDATE(gpuTextureFramebufferMemory, (int)BYTES_TO_MB(gpu::Texture::getTextureGPUFramebufferMemoryUsage())); STAT_UPDATE(gpuTextureSparseMemory, (int)BYTES_TO_MB(gpu::Texture::getTextureGPUSparseMemoryUsage())); - STAT_UPDATE(gpuTextureMemoryPressureState, getTextureMemoryPressureModeString()); STAT_UPDATE(gpuSparseTextureEnabled, gpuContext->getBackend()->isTextureManagementSparseEnabled() ? 1 : 0); STAT_UPDATE(gpuFreeMemory, (int)BYTES_TO_MB(gpu::Context::getFreeGPUMemory())); STAT_UPDATE(rectifiedTextureCount, (int)RECTIFIED_TEXTURE_COUNT.load()); diff --git a/interface/src/ui/Stats.h b/interface/src/ui/Stats.h index a93a255a06..0ce113e0a0 100644 --- a/interface/src/ui/Stats.h +++ b/interface/src/ui/Stats.h @@ -77,7 +77,7 @@ class Stats : public QQuickItem { STATS_PROPERTY(int, audioMixerOutPps, 0) STATS_PROPERTY(int, audioMixerKbps, 0) STATS_PROPERTY(int, audioMixerPps, 0) - STATS_PROPERTY(int, audioOutboundPPS, 0) + STATS_PROPERTY(int, audioMicOutboundPPS, 0) STATS_PROPERTY(int, audioSilentOutboundPPS, 0) STATS_PROPERTY(int, audioAudioInboundPPS, 0) STATS_PROPERTY(int, audioSilentInboundPPS, 0) @@ -117,13 +117,11 @@ class Stats : public QQuickItem { STATS_PROPERTY(int, gpuTexturesSparse, 0) STATS_PROPERTY(int, glContextSwapchainMemory, 0) STATS_PROPERTY(int, qmlTextureMemory, 0) - STATS_PROPERTY(int, texturePendingTransfers, 0) STATS_PROPERTY(int, gpuTextureMemory, 0) STATS_PROPERTY(int, gpuTextureVirtualMemory, 0) STATS_PROPERTY(int, gpuTextureFramebufferMemory, 0) STATS_PROPERTY(int, gpuTextureSparseMemory, 0) STATS_PROPERTY(int, gpuSparseTextureEnabled, 0) - STATS_PROPERTY(QString, gpuTextureMemoryPressureState, QString()) STATS_PROPERTY(int, gpuFreeMemory, 0) STATS_PROPERTY(float, gpuFrameTime, 0) STATS_PROPERTY(float, batchFrameTime, 0) @@ -200,7 +198,7 @@ signals: void audioMixerOutPpsChanged(); void audioMixerKbpsChanged(); void audioMixerPpsChanged(); - void audioOutboundPPSChanged(); + void audioMicOutboundPPSChanged(); void audioSilentOutboundPPSChanged(); void audioAudioInboundPPSChanged(); void audioSilentInboundPPSChanged(); @@ -234,7 +232,6 @@ signals: void timingStatsChanged(); void glContextSwapchainMemoryChanged(); void qmlTextureMemoryChanged(); - void texturePendingTransfersChanged(); void gpuBuffersChanged(); void gpuBufferMemoryChanged(); void gpuTexturesChanged(); @@ -243,7 +240,6 @@ signals: void gpuTextureVirtualMemoryChanged(); void gpuTextureFramebufferMemoryChanged(); void gpuTextureSparseMemoryChanged(); - void gpuTextureMemoryPressureStateChanged(); void gpuSparseTextureEnabledChanged(); void gpuFreeMemoryChanged(); void gpuFrameTimeChanged(); diff --git a/interface/src/ui/overlays/Overlays.cpp b/interface/src/ui/overlays/Overlays.cpp index 6514052d26..f40dd522c4 100644 --- a/interface/src/ui/overlays/Overlays.cpp +++ b/interface/src/ui/overlays/Overlays.cpp @@ -431,9 +431,7 @@ RayToOverlayIntersectionResult Overlays::findRayIntersectionInternal(const PickR if (thisOverlay->findRayIntersectionExtraInfo(ray.origin, ray.direction, thisDistance, thisFace, thisSurfaceNormal, thisExtraInfo)) { bool isDrawInFront = thisOverlay->getDrawInFront(); - if ((bestIsFront && isDrawInFront && thisDistance < bestDistance) - || (!bestIsFront && (isDrawInFront || thisDistance < bestDistance))) { - + if (thisDistance < bestDistance && (!bestIsFront || isDrawInFront)) { bestIsFront = isDrawInFront; bestDistance = thisDistance; result.intersects = true; diff --git a/interface/src/ui/overlays/Web3DOverlay.cpp b/interface/src/ui/overlays/Web3DOverlay.cpp index 97e5344062..ba864d2c5c 100644 --- a/interface/src/ui/overlays/Web3DOverlay.cpp +++ b/interface/src/ui/overlays/Web3DOverlay.cpp @@ -270,7 +270,7 @@ void Web3DOverlay::render(RenderArgs* args) { if (!_texture) { auto webSurface = _webSurface; - _texture = gpu::TexturePointer(gpu::Texture::createExternal(OffscreenQmlSurface::getDiscardLambda())); + _texture = gpu::TexturePointer(gpu::Texture::createExternal2D(OffscreenQmlSurface::getDiscardLambda())); _texture->setSource(__FUNCTION__); } OffscreenQmlSurface::TextureAndFence newTextureAndFence; diff --git a/interface/ui/scriptEditorWidget.ui b/interface/ui/scriptEditorWidget.ui new file mode 100644 index 0000000000..e2e538a595 --- /dev/null +++ b/interface/ui/scriptEditorWidget.ui @@ -0,0 +1,142 @@ + + + ScriptEditorWidget + + + + 0 + 0 + 691 + 549 + + + + + 0 + 0 + + + + + 690 + 328 + + + + font-family: Helvetica, Arial, sans-serif; + + + QDockWidget::DockWidgetFloatable|QDockWidget::DockWidgetMovable + + + Qt::NoDockWidgetArea + + + Edit Script + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + Courier + -1 + 50 + false + false + + + + font: 16px "Courier"; + + + + + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + font: 13px "Helvetica","Arial","sans-serif"; + + + Debug Log: + + + + + + + + Helvetica,Arial,sans-serif + -1 + 50 + false + false + + + + font: 13px "Helvetica","Arial","sans-serif"; + + + Run on the fly (Careful: Any valid change made to the code will run immediately) + + + + + + + Clear + + + + 16 + 16 + + + + + + + + + + + + ScriptEditBox + QTextEdit +
ui/ScriptEditBox.h
+
+
+ +
diff --git a/interface/ui/scriptEditorWindow.ui b/interface/ui/scriptEditorWindow.ui new file mode 100644 index 0000000000..1e50aaef0b --- /dev/null +++ b/interface/ui/scriptEditorWindow.ui @@ -0,0 +1,324 @@ + + + ScriptEditorWindow + + + Qt::NonModal + + + + 0 + 0 + 780 + 717 + + + + + 400 + 250 + + + + Script Editor + + + font-family: Helvetica, Arial, sans-serif; + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 3 + + + QLayout::SetNoConstraint + + + 0 + + + 0 + + + + + New Script (Ctrl+N) + + + New + + + + 32 + 32 + + + + + + + + + 30 + 0 + + + + + 25 + 0 + + + + Load Script (Ctrl+O) + + + Load + + + + 32 + 32 + + + + false + + + QToolButton::MenuButtonPopup + + + Qt::ToolButtonIconOnly + + + + + + + + 30 + 0 + + + + + 32 + 0 + + + + Qt::NoFocus + + + Qt::NoContextMenu + + + Save Script (Ctrl+S) + + + Save + + + + 32 + 32 + + + + 316 + + + QToolButton::MenuButtonPopup + + + + + + + Toggle Run Script (F5) + + + Run/Stop + + + + 32 + 32 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + font: 13px "Helvetica","Arial","sans-serif"; + + + Automatically reload externally changed files + + + + + + + + + true + + + + 250 + 80 + + + + QTabWidget::West + + + QTabWidget::Triangular + + + -1 + + + Qt::ElideNone + + + true + + + true + + + + + + + + + saveButton + clicked() + ScriptEditorWindow + saveScriptClicked() + + + 236 + 10 + + + 199 + 264 + + + + + toggleRunButton + clicked() + ScriptEditorWindow + toggleRunScriptClicked() + + + 330 + 10 + + + 199 + 264 + + + + + newButton + clicked() + ScriptEditorWindow + newScriptClicked() + + + 58 + 10 + + + 199 + 264 + + + + + loadButton + clicked() + ScriptEditorWindow + loadScriptClicked() + + + 85 + 10 + + + 199 + 264 + + + + + tabWidget + currentChanged(int) + ScriptEditorWindow + tabSwitched(int) + + + 352 + 360 + + + 352 + 340 + + + + + tabWidget + tabCloseRequested(int) + ScriptEditorWindow + tabCloseRequested(int) + + + 352 + 360 + + + 352 + 340 + + + + + diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 4a2de0a64b..c32b5600d9 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -160,7 +160,7 @@ AudioClient::AudioClient() : _loopbackAudioOutput(NULL), _loopbackOutputDevice(NULL), _inputRingBuffer(0), - _localInjectorsStream(0, 1), + _localInjectorsStream(0), _receivedAudioStream(RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES), _isStereoInput(false), _outputStarveDetectionStartTimeMsec(0), @@ -184,6 +184,7 @@ AudioClient::AudioClient() : _outgoingAvatarAudioSequenceNumber(0), _audioOutputIODevice(_localInjectorsStream, _receivedAudioStream, this), _stats(&_receivedAudioStream), + _inputGate(), _positionGetter(DEFAULT_POSITION_GETTER), _orientationGetter(DEFAULT_ORIENTATION_GETTER) { // avoid putting a lock in the device callback @@ -970,87 +971,14 @@ void AudioClient::handleLocalEchoAndReverb(QByteArray& inputByteArray) { } } -void AudioClient::handleAudioInput(QByteArray& audioBuffer) { - if (_muted) { - _lastInputLoudness = 0.0f; - _timeSinceLastClip = 0.0f; - } else { - int16_t* samples = reinterpret_cast(audioBuffer.data()); - int numSamples = audioBuffer.size() / sizeof(AudioConstants::SAMPLE_SIZE); - bool didClip = false; - - bool shouldRemoveDCOffset = !_isPlayingBackRecording && !_isStereoInput; - if (shouldRemoveDCOffset) { - _noiseGate.removeDCOffset(samples, numSamples); - } - - bool shouldNoiseGate = (_isPlayingBackRecording || !_isStereoInput) && _isNoiseGateEnabled; - if (shouldNoiseGate) { - _noiseGate.gateSamples(samples, numSamples); - _lastInputLoudness = _noiseGate.getLastLoudness(); - didClip = _noiseGate.clippedInLastBlock(); - } else { - float loudness = 0.0f; - for (int i = 0; i < numSamples; ++i) { - int16_t sample = std::abs(samples[i]); - loudness += (float)sample; - didClip = didClip || - (sample > (AudioConstants::MAX_SAMPLE_VALUE * AudioNoiseGate::CLIPPING_THRESHOLD)); - } - _lastInputLoudness = fabs(loudness / numSamples); - } - - if (didClip) { - _timeSinceLastClip = 0.0f; - } else if (_timeSinceLastClip >= 0.0f) { - _timeSinceLastClip += (float)numSamples / (float)AudioConstants::SAMPLE_RATE; - } - - emit inputReceived({ audioBuffer.data(), numSamples }); - - if (_noiseGate.openedInLastBlock()) { - emit noiseGateOpened(); - } else if (_noiseGate.closedInLastBlock()) { - emit noiseGateClosed(); - } - } - - // the codec needs a flush frame before sending silent packets, so - // do not send one if the gate closed in this block (eventually this can be crossfaded). - auto packetType = _shouldEchoToServer ? - PacketType::MicrophoneAudioWithEcho : PacketType::MicrophoneAudioNoEcho; - if (_lastInputLoudness == 0.0f && !_noiseGate.closedInLastBlock()) { - packetType = PacketType::SilentAudioFrame; - _silentOutbound.increment(); - } else { - _audioOutbound.increment(); - } - - Transform audioTransform; - audioTransform.setTranslation(_positionGetter()); - audioTransform.setRotation(_orientationGetter()); - - QByteArray encodedBuffer; - if (_encoder) { - _encoder->encode(audioBuffer, encodedBuffer); - } else { - encodedBuffer = audioBuffer; - } - - emitAudioPacket(encodedBuffer.data(), encodedBuffer.size(), _outgoingAvatarAudioSequenceNumber, - audioTransform, avatarBoundingBoxCorner, avatarBoundingBoxScale, - packetType, _selectedCodecName); - _stats.sentPacket(); -} - -void AudioClient::handleMicAudioInput() { +void AudioClient::handleAudioInput() { if (!_inputDevice || _isPlayingBackRecording) { return; } // input samples required to produce exactly NETWORK_FRAME_SAMPLES of output - const int inputSamplesRequired = (_inputToNetworkResampler ? - _inputToNetworkResampler->getMinInput(AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL) : + const int inputSamplesRequired = (_inputToNetworkResampler ? + _inputToNetworkResampler->getMinInput(AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL) : AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL) * _inputFormat.channelCount(); const auto inputAudioSamples = std::unique_ptr(new int16_t[inputSamplesRequired]); @@ -1073,27 +1001,126 @@ void AudioClient::handleMicAudioInput() { static int16_t networkAudioSamples[AudioConstants::NETWORK_FRAME_SAMPLES_STEREO]; while (_inputRingBuffer.samplesAvailable() >= inputSamplesRequired) { - if (_muted) { - _inputRingBuffer.shiftReadPosition(inputSamplesRequired); - } else { + + if (!_muted) { + + + // Increment the time since the last clip + if (_timeSinceLastClip >= 0.0f) { + _timeSinceLastClip += (float)numNetworkSamples / (float)AudioConstants::SAMPLE_RATE; + } + _inputRingBuffer.readSamples(inputAudioSamples.get(), inputSamplesRequired); possibleResampling(_inputToNetworkResampler, inputAudioSamples.get(), networkAudioSamples, inputSamplesRequired, numNetworkSamples, _inputFormat.channelCount(), _desiredInputFormat.channelCount()); + + // Remove DC offset + if (!_isStereoInput) { + _inputGate.removeDCOffset(networkAudioSamples, numNetworkSamples); + } + + // only impose the noise gate and perform tone injection if we are sending mono audio + if (!_isStereoInput && _isNoiseGateEnabled) { + _inputGate.gateSamples(networkAudioSamples, numNetworkSamples); + + // if we performed the noise gate we can get values from it instead of enumerating the samples again + _lastInputLoudness = _inputGate.getLastLoudness(); + + if (_inputGate.clippedInLastBlock()) { + _timeSinceLastClip = 0.0f; + } + + } else { + float loudness = 0.0f; + + for (int i = 0; i < numNetworkSamples; i++) { + int thisSample = std::abs(networkAudioSamples[i]); + loudness += (float)thisSample; + + if (thisSample > (AudioConstants::MAX_SAMPLE_VALUE * AudioNoiseGate::CLIPPING_THRESHOLD)) { + _timeSinceLastClip = 0.0f; + } + } + + _lastInputLoudness = fabs(loudness / numNetworkSamples); + } + + emit inputReceived({ reinterpret_cast(networkAudioSamples), numNetworkBytes }); + + if (_inputGate.openedInLastBlock()) { + emit noiseGateOpened(); + } else if (_inputGate.closedInLastBlock()) { + emit noiseGateClosed(); + } + + } else { + // our input loudness is 0, since we're muted + _lastInputLoudness = 0; + _timeSinceLastClip = 0.0f; + + _inputRingBuffer.shiftReadPosition(inputSamplesRequired); } + + auto packetType = _shouldEchoToServer ? + PacketType::MicrophoneAudioWithEcho : PacketType::MicrophoneAudioNoEcho; + + // if the _inputGate closed in this last frame, then we don't actually want + // to send a silent packet, instead, we want to go ahead and encode and send + // the output from the input gate (eventually, this could be crossfaded) + // and allow the codec to properly encode down to silent/zero. If we still + // have _lastInputLoudness of 0 in our NEXT frame, we will send a silent packet + if (_lastInputLoudness == 0 && !_inputGate.closedInLastBlock()) { + packetType = PacketType::SilentAudioFrame; + _silentOutbound.increment(); + } else { + _micAudioOutbound.increment(); + } + + Transform audioTransform; + audioTransform.setTranslation(_positionGetter()); + audioTransform.setRotation(_orientationGetter()); + // FIXME find a way to properly handle both playback audio and user audio concurrently + + QByteArray decodedBuffer(reinterpret_cast(networkAudioSamples), numNetworkBytes); + QByteArray encodedBuffer; + if (_encoder) { + _encoder->encode(decodedBuffer, encodedBuffer); + } else { + encodedBuffer = decodedBuffer; + } + + emitAudioPacket(encodedBuffer.constData(), encodedBuffer.size(), _outgoingAvatarAudioSequenceNumber, + audioTransform, avatarBoundingBoxCorner, avatarBoundingBoxScale, + packetType, _selectedCodecName); + _stats.sentPacket(); + int bytesInInputRingBuffer = _inputRingBuffer.samplesAvailable() * AudioConstants::SAMPLE_SIZE; float msecsInInputRingBuffer = bytesInInputRingBuffer / (float)(_inputFormat.bytesForDuration(USECS_PER_MSEC)); _stats.updateInputMsUnplayed(msecsInInputRingBuffer); - - QByteArray audioBuffer(reinterpret_cast(networkAudioSamples), numNetworkBytes); - handleAudioInput(audioBuffer); } } +// FIXME - should this go through the noise gate and honor mute and echo? void AudioClient::handleRecordedAudioInput(const QByteArray& audio) { - QByteArray audioBuffer(audio); - handleAudioInput(audioBuffer); + Transform audioTransform; + audioTransform.setTranslation(_positionGetter()); + audioTransform.setRotation(_orientationGetter()); + + QByteArray encodedBuffer; + if (_encoder) { + _encoder->encode(audio, encodedBuffer); + } else { + encodedBuffer = audio; + } + + _micAudioOutbound.increment(); + + // FIXME check a flag to see if we should echo audio? + emitAudioPacket(encodedBuffer.data(), encodedBuffer.size(), _outgoingAvatarAudioSequenceNumber, + audioTransform, avatarBoundingBoxCorner, avatarBoundingBoxScale, + PacketType::MicrophoneAudioWithEcho, _selectedCodecName); } void AudioClient::prepareLocalAudioInjectors() { @@ -1407,7 +1434,7 @@ bool AudioClient::switchInputToAudioDevice(const QAudioDeviceInfo& inputDeviceIn lock.unlock(); if (_inputDevice) { - connect(_inputDevice, SIGNAL(readyRead()), this, SLOT(handleMicAudioInput())); + connect(_inputDevice, SIGNAL(readyRead()), this, SLOT(handleAudioInput())); supportedFormat = true; } else { qCDebug(audioclient) << "Error starting audio input -" << _audioInput->error(); @@ -1513,39 +1540,12 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDevice // setup our general output device for audio-mixer audio _audioOutput = new QAudioOutput(outputDeviceInfo, _outputFormat, this); + int osDefaultBufferSize = _audioOutput->bufferSize(); int deviceChannelCount = _outputFormat.channelCount(); - int frameSize = (AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * deviceChannelCount * _outputFormat.sampleRate()) / _desiredOutputFormat.sampleRate(); - int requestedSize = _sessionOutputBufferSizeFrames * frameSize * AudioConstants::SAMPLE_SIZE; + int deviceFrameSize = (AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * deviceChannelCount * _outputFormat.sampleRate()) / _desiredOutputFormat.sampleRate(); + int requestedSize = _sessionOutputBufferSizeFrames * deviceFrameSize * AudioConstants::SAMPLE_SIZE; _audioOutput->setBufferSize(requestedSize); - // initialize mix buffers on the _audioOutput thread to avoid races - connect(_audioOutput, &QAudioOutput::stateChanged, [&, frameSize, requestedSize](QAudio::State state) { - if (state == QAudio::ActiveState) { - // restrict device callback to _outputPeriod samples - _outputPeriod = (_audioOutput->periodSize() / AudioConstants::SAMPLE_SIZE) * 2; - _outputMixBuffer = new float[_outputPeriod]; - _outputScratchBuffer = new int16_t[_outputPeriod]; - - // size local output mix buffer based on resampled network frame size - _networkPeriod = _localToOutputResampler->getMaxOutput(AudioConstants::NETWORK_FRAME_SAMPLES_STEREO); - _localOutputMixBuffer = new float[_networkPeriod]; - int localPeriod = _outputPeriod * 2; - _localInjectorsStream.resizeForFrameSize(localPeriod); - - int bufferSize = _audioOutput->bufferSize(); - int bufferSamples = bufferSize / AudioConstants::SAMPLE_SIZE; - int bufferFrames = bufferSamples / (float)frameSize; - qCDebug(audioclient) << "frame (samples):" << frameSize; - qCDebug(audioclient) << "buffer (frames):" << bufferFrames; - qCDebug(audioclient) << "buffer (samples):" << bufferSamples; - qCDebug(audioclient) << "buffer (bytes):" << bufferSize; - qCDebug(audioclient) << "requested (bytes):" << requestedSize; - qCDebug(audioclient) << "period (samples):" << _outputPeriod; - qCDebug(audioclient) << "local buffer (samples):" << localPeriod; - - disconnect(_audioOutput, &QAudioOutput::stateChanged, 0, 0); - } - }); connect(_audioOutput, &QAudioOutput::notify, this, &AudioClient::outputNotify); _audioOutputIODevice.start(); @@ -1555,6 +1555,18 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDevice _audioOutput->start(&_audioOutputIODevice); lock.unlock(); + int periodSampleSize = _audioOutput->periodSize() / AudioConstants::SAMPLE_SIZE; + // device callback is not restricted to periodSampleSize, so double the mix/scratch buffer sizes + _outputPeriod = periodSampleSize * 2; + _outputMixBuffer = new float[_outputPeriod]; + _outputScratchBuffer = new int16_t[_outputPeriod]; + _localOutputMixBuffer = new float[_outputPeriod]; + _localInjectorsStream.resizeForFrameSize(_outputPeriod * 2); + + qCDebug(audioclient) << "Output Buffer capacity in frames: " << _audioOutput->bufferSize() / AudioConstants::SAMPLE_SIZE / (float)deviceFrameSize << + "requested bytes:" << requestedSize << "actual bytes:" << _audioOutput->bufferSize() << + "os default:" << osDefaultBufferSize << "period size:" << _audioOutput->periodSize(); + // setup a loopback audio output device _loopbackAudioOutput = new QAudioOutput(outputDeviceInfo, _outputFormat, this); diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index 139749e8e8..7e9acc0586 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -124,16 +124,16 @@ public: void selectAudioFormat(const QString& selectedCodecName); Q_INVOKABLE QString getSelectedAudioFormat() const { return _selectedCodecName; } - Q_INVOKABLE bool getNoiseGateOpen() const { return _noiseGate.isOpen(); } + Q_INVOKABLE bool getNoiseGateOpen() const { return _inputGate.isOpen(); } + Q_INVOKABLE float getSilentOutboundPPS() const { return _silentOutbound.rate(); } + Q_INVOKABLE float getMicAudioOutboundPPS() const { return _micAudioOutbound.rate(); } Q_INVOKABLE float getSilentInboundPPS() const { return _silentInbound.rate(); } Q_INVOKABLE float getAudioInboundPPS() const { return _audioInbound.rate(); } - Q_INVOKABLE float getSilentOutboundPPS() const { return _silentOutbound.rate(); } - Q_INVOKABLE float getAudioOutboundPPS() const { return _audioOutbound.rate(); } const MixedProcessedAudioStream& getReceivedAudioStream() const { return _receivedAudioStream; } MixedProcessedAudioStream& getReceivedAudioStream() { return _receivedAudioStream; } - float getLastInputLoudness() const { return glm::max(_lastInputLoudness - _noiseGate.getMeasuredFloor(), 0.0f); } + float getLastInputLoudness() const { return glm::max(_lastInputLoudness - _inputGate.getMeasuredFloor(), 0.0f); } float getTimeSinceLastClip() const { return _timeSinceLastClip; } float getAudioAverageInputLoudness() const { return _lastInputLoudness; } @@ -180,7 +180,7 @@ public slots: void handleMismatchAudioFormat(SharedNodePointer node, const QString& currentCodec, const QString& recievedCodec); void sendDownstreamAudioStatsPacket() { _stats.publish(); } - void handleMicAudioInput(); + void handleAudioInput(); void handleRecordedAudioInput(const QByteArray& audio); void reset(); void audioMixerKilled(); @@ -250,7 +250,6 @@ protected: private: void outputFormatChanged(); - void handleAudioInput(QByteArray& audioBuffer); bool mixLocalAudioInjectors(float* mixBuffer); float azimuthForSource(const glm::vec3& relativePosition); float gainForSource(float distance, float volume); @@ -340,7 +339,6 @@ private: int16_t _networkScratchBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_AMBISONIC]; // for local audio (used by audio injectors thread) - int _networkPeriod { 0 }; float _localMixBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_STEREO]; int16_t _localScratchBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_AMBISONIC]; float* _localOutputMixBuffer { NULL }; @@ -373,7 +371,7 @@ private: AudioIOStats _stats; - AudioNoiseGate _noiseGate; + AudioNoiseGate _inputGate; AudioPositionGetter _positionGetter; AudioOrientationGetter _orientationGetter; @@ -397,7 +395,7 @@ private: QThread* _checkDevicesThread { nullptr }; RateCounter<> _silentOutbound; - RateCounter<> _audioOutbound; + RateCounter<> _micAudioOutbound; RateCounter<> _silentInbound; RateCounter<> _audioInbound; }; diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index 5a317f64bc..b23b59d3f0 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -355,16 +355,14 @@ void OpenGLDisplayPlugin::customizeContext() { if ((image.width() > 0) && (image.height() > 0)) { cursorData.texture.reset( - gpu::Texture::createStrict( + gpu::Texture::create2D( gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA), image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); cursorData.texture->setSource("cursor texture"); auto usage = gpu::Texture::Usage::Builder().withColor().withAlpha(); cursorData.texture->setUsage(usage.build()); - cursorData.texture->setStoredMipFormat(gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA)); - cursorData.texture->assignStoredMip(0, image.byteCount(), image.constBits()); - cursorData.texture->autoGenerateMips(-1); + cursorData.texture->assignStoredMip(0, gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA), image.byteCount(), image.constBits()); } } } diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index c55d985a62..a8b8ba3618 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -296,32 +296,33 @@ void HmdDisplayPlugin::internalPresent() { image = image.convertToFormat(QImage::Format_RGBA8888); if (!_previewTexture) { _previewTexture.reset( - gpu::Texture::createStrict( + gpu::Texture::create2D( gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA), image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); _previewTexture->setSource("HMD Preview Texture"); _previewTexture->setUsage(gpu::Texture::Usage::Builder().withColor().build()); - _previewTexture->setStoredMipFormat(gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA)); - _previewTexture->assignStoredMip(0, image.byteCount(), image.constBits()); + _previewTexture->assignStoredMip(0, gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA), image.byteCount(), image.constBits()); _previewTexture->autoGenerateMips(-1); } - auto viewport = getViewportForSourceSize(uvec2(_previewTexture->getDimensions())); + if (getGLBackend()->isTextureReady(_previewTexture)) { + auto viewport = getViewportForSourceSize(uvec2(_previewTexture->getDimensions())); - render([&](gpu::Batch& batch) { - batch.enableStereo(false); - batch.resetViewTransform(); - batch.setFramebuffer(gpu::FramebufferPointer()); - batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, vec4(0)); - batch.setStateScissorRect(viewport); - batch.setViewportTransform(viewport); - batch.setResourceTexture(0, _previewTexture); - batch.setPipeline(_presentPipeline); - batch.draw(gpu::TRIANGLE_STRIP, 4); - }); - _clearPreviewFlag = false; - swapBuffers(); + render([&](gpu::Batch& batch) { + batch.enableStereo(false); + batch.resetViewTransform(); + batch.setFramebuffer(gpu::FramebufferPointer()); + batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, vec4(0)); + batch.setStateScissorRect(viewport); + batch.setViewportTransform(viewport); + batch.setResourceTexture(0, _previewTexture); + batch.setPipeline(_presentPipeline); + batch.draw(gpu::TRIANGLE_STRIP, 4); + }); + _clearPreviewFlag = false; + swapBuffers(); + } } postPreview(); diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index 8c4498edc6..27e00b47c6 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -146,7 +146,6 @@ void EntityTreeRenderer::clear() { void EntityTreeRenderer::reloadEntityScripts() { _entitiesScriptEngine->unloadAllEntityScripts(); - _entitiesScriptEngine->resetModuleCache(); foreach(auto entity, _entitiesInScene) { if (!entity->getScript().isEmpty()) { _entitiesScriptEngine->loadEntityScript(entity->getEntityItemID(), entity->getScript(), true); @@ -941,7 +940,7 @@ void EntityTreeRenderer::mouseMoveEvent(QMouseEvent* event) { void EntityTreeRenderer::deletingEntity(const EntityItemID& entityID) { if (_tree && !_shuttingDown && _entitiesScriptEngine) { - _entitiesScriptEngine->unloadEntityScript(entityID, true); + _entitiesScriptEngine->unloadEntityScript(entityID); } forceRecheckEntities(); // reset our state to force checking our inside/outsideness of entities diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp index 1d58527427..7359a548fc 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp @@ -14,7 +14,6 @@ #include #include #include -#include "ModelScriptingInterface.h" #if defined(__GNUC__) && !defined(__clang__) #pragma GCC diagnostic push @@ -54,8 +53,6 @@ #include "PhysicalEntitySimulation.h" gpu::PipelinePointer RenderablePolyVoxEntityItem::_pipeline = nullptr; -gpu::PipelinePointer RenderablePolyVoxEntityItem::_wireframePipeline = nullptr; - const float MARCHING_CUBE_COLLISION_HULL_OFFSET = 0.5; @@ -76,7 +73,7 @@ const float MARCHING_CUBE_COLLISION_HULL_OFFSET = 0.5; _meshDirty In RenderablePolyVoxEntityItem::render, these flags are checked and changes are propagated along the chain. - decompressVolumeData() is called to decompress _voxelData into _volData. recomputeMesh() is called to invoke the + decompressVolumeData() is called to decompress _voxelData into _volData. getMesh() is called to invoke the polyVox surface extractor to create _mesh (as well as set Simulation _dirtyFlags). Because Simulation::DIRTY_SHAPE is set, isReadyToComputeShape() gets called and _shape is created either from _volData or _shape, depending on the surface style. @@ -84,7 +81,7 @@ const float MARCHING_CUBE_COLLISION_HULL_OFFSET = 0.5; When a script changes _volData, compressVolumeDataAndSendEditPacket is called to update _voxelData and to send a packet to the entity-server. - decompressVolumeData, recomputeMesh, computeShapeInfoWorker, and compressVolumeDataAndSendEditPacket are too expensive + decompressVolumeData, getMesh, computeShapeInfoWorker, and compressVolumeDataAndSendEditPacket are too expensive to run on a thread that has other things to do. These use QtConcurrent::run to spawn a thread. As each thread finishes, it adjusts the dirty flags so that the next call to render() will kick off the next step. @@ -666,8 +663,11 @@ void RenderablePolyVoxEntityItem::setZTextureURL(QString zTextureURL) { } } +void RenderablePolyVoxEntityItem::render(RenderArgs* args) { + PerformanceTimer perfTimer("RenderablePolyVoxEntityItem::render"); + assert(getType() == EntityTypes::PolyVox); + Q_ASSERT(args->_batch); -bool RenderablePolyVoxEntityItem::updateDependents() { bool voxelDataDirty; bool volDataDirty; withWriteLock([&] { @@ -682,20 +682,9 @@ bool RenderablePolyVoxEntityItem::updateDependents() { if (voxelDataDirty) { decompressVolumeData(); } else if (volDataDirty) { - recomputeMesh(); + getMesh(); } - return !volDataDirty; -} - - -void RenderablePolyVoxEntityItem::render(RenderArgs* args) { - PerformanceTimer perfTimer("RenderablePolyVoxEntityItem::render"); - assert(getType() == EntityTypes::PolyVox); - Q_ASSERT(args->_batch); - - updateDependents(); - model::MeshPointer mesh; glm::vec3 voxelVolumeSize; withReadLock([&] { @@ -707,7 +696,7 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) { !mesh->getIndexBuffer()._buffer) { return; } - + if (!_pipeline) { gpu::ShaderPointer vertexShader = gpu::Shader::createVertex(std::string(polyvox_vert)); gpu::ShaderPointer pixelShader = gpu::Shader::createPixel(std::string(polyvox_frag)); @@ -726,13 +715,6 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) { state->setDepthTest(true, true, gpu::LESS_EQUAL); _pipeline = gpu::Pipeline::create(program, state); - - auto wireframeState = std::make_shared(); - wireframeState->setCullMode(gpu::State::CULL_BACK); - wireframeState->setDepthTest(true, true, gpu::LESS_EQUAL); - wireframeState->setFillMode(gpu::State::FILL_LINE); - - _wireframePipeline = gpu::Pipeline::create(program, wireframeState); } if (!_vertexFormat) { @@ -743,11 +725,7 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) { } gpu::Batch& batch = *args->_batch; - - // Pick correct Pipeline - bool wireframe = (render::ShapeKey(args->_globalShapeKey).isWireframe()); - auto pipeline = (wireframe ? _wireframePipeline : _pipeline); - batch.setPipeline(pipeline); + batch.setPipeline(_pipeline); Transform transform(voxelToWorldMatrix()); batch.setModelTransform(transform); @@ -784,7 +762,7 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) { batch.setResourceTexture(2, DependencyManager::get()->getWhiteTexture()); } - int voxelVolumeSizeLocation = pipeline->getProgram()->getUniforms().findLocation("voxelVolumeSize"); + int voxelVolumeSizeLocation = _pipeline->getProgram()->getUniforms().findLocation("voxelVolumeSize"); batch._glUniform3f(voxelVolumeSizeLocation, voxelVolumeSize.x, voxelVolumeSize.y, voxelVolumeSize.z); batch.drawIndexed(gpu::TRIANGLES, (gpu::uint32)mesh->getNumIndices(), 0); @@ -1221,7 +1199,7 @@ void RenderablePolyVoxEntityItem::copyUpperEdgesFromNeighbors() { } } -void RenderablePolyVoxEntityItem::recomputeMesh() { +void RenderablePolyVoxEntityItem::getMesh() { // use _volData to make a renderable mesh PolyVoxSurfaceStyle voxelSurfaceStyle; withReadLock([&] { @@ -1291,20 +1269,12 @@ void RenderablePolyVoxEntityItem::recomputeMesh() { vertexBufferPtr->getSize() , sizeof(PolyVox::PositionMaterialNormal), gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::RAW))); - - std::vector parts; - parts.emplace_back(model::Mesh::Part((model::Index)0, // startIndex - (model::Index)vecIndices.size(), // numIndices - (model::Index)0, // baseVertex - model::Mesh::TRIANGLES)); // topology - mesh->setPartBuffer(gpu::BufferView(new gpu::Buffer(parts.size() * sizeof(model::Mesh::Part), - (gpu::Byte*) parts.data()), gpu::Element::PART_DRAWCALL)); entity->setMesh(mesh); }); } void RenderablePolyVoxEntityItem::setMesh(model::MeshPointer mesh) { - // this catches the payload from recomputeMesh + // this catches the payload from getMesh bool neighborsNeedUpdate; withWriteLock([&] { if (!_collisionless) { @@ -1561,6 +1531,7 @@ std::shared_ptr RenderablePolyVoxEntityItem::getZPN return std::dynamic_pointer_cast(_zPNeighbor.lock()); } + void RenderablePolyVoxEntityItem::bonkNeighbors() { // flag neighbors to the negative of this entity as needing to rebake their meshes. cacheNeighbors(); @@ -1580,6 +1551,7 @@ void RenderablePolyVoxEntityItem::bonkNeighbors() { } } + void RenderablePolyVoxEntityItem::locationChanged(bool tellPhysics) { EntityItem::locationChanged(tellPhysics); if (!_pipeline || !render::Item::isValidID(_myItem)) { @@ -1591,25 +1563,3 @@ void RenderablePolyVoxEntityItem::locationChanged(bool tellPhysics) { scene->enqueuePendingChanges(pendingChanges); } - -bool RenderablePolyVoxEntityItem::getMeshAsScriptValue(QScriptEngine *engine, QScriptValue& result) { - if (!updateDependents()) { - return false; - } - - bool success = false; - MeshProxy* meshProxy = nullptr; - glm::mat4 transform = voxelToLocalMatrix(); - withReadLock([&] { - if (_meshInitialized) { - success = true; - // the mesh will be in voxel-space. transform it into object-space - meshProxy = new MeshProxy( - _mesh->map([=](glm::vec3 position){ return glm::vec3(transform * glm::vec4(position, 1.0f)); }, - [=](glm::vec3 normal){ return glm::vec3(transform * glm::vec4(normal, 0.0f)); }, - [](uint32_t index){ return index; })); - } - }); - result = meshToScriptValue(engine, meshProxy); - return success; -} diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h index cf4672f068..45842c2fb9 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h @@ -61,8 +61,6 @@ public: virtual uint8_t getVoxel(int x, int y, int z) override; virtual bool setVoxel(int x, int y, int z, uint8_t toValue) override; - int getOnCount() const override { return _onCount; } - void render(RenderArgs* args) override; virtual bool supportsDetailedRayIntersection() const override { return true; } virtual bool findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, @@ -135,7 +133,6 @@ public: QByteArray volDataToArray(quint16 voxelXSize, quint16 voxelYSize, quint16 voxelZSize) const; void setMesh(model::MeshPointer mesh); - bool getMeshAsScriptValue(QScriptEngine *engine, QScriptValue& result) override; void setCollisionPoints(ShapeInfo::PointCollection points, AABox box); PolyVox::SimpleVolume* getVolData() { return _volData; } @@ -166,12 +163,11 @@ private: const int MATERIAL_GPU_SLOT = 3; render::ItemID _myItem{ render::Item::INVALID_ITEM_ID }; static gpu::PipelinePointer _pipeline; - static gpu::PipelinePointer _wireframePipeline; ShapeInfo _shapeInfo; PolyVox::SimpleVolume* _volData = nullptr; - bool _volDataDirty = false; // does recomputeMesh need to be called? + bool _volDataDirty = false; // does getMesh need to be called? int _onCount; // how many non-zero voxels are in _volData bool _neighborsNeedUpdate { false }; @@ -182,7 +178,7 @@ private: // these are run off the main thread void decompressVolumeData(); void compressVolumeDataAndSendEditPacket(); - virtual void recomputeMesh() override; // recompute mesh + virtual void getMesh() override; // recompute mesh void computeShapeInfoWorker(); // these are cached lookups of _xNNeighborID, _yNNeighborID, _zNNeighborID, _xPNeighborID, _yPNeighborID, _zPNeighborID @@ -195,7 +191,6 @@ private: void cacheNeighbors(); void copyUpperEdgesFromNeighbors(); void bonkNeighbors(); - bool updateDependents(); }; bool inUserBounds(const PolyVox::SimpleVolume* vol, PolyVoxEntityItem::PolyVoxSurfaceStyle surfaceStyle, diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp index 1ad60bf7c6..c3e097382c 100644 --- a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp @@ -114,22 +114,13 @@ void RenderableShapeEntityItem::render(RenderArgs* args) { auto outColor = _procedural->getColor(color); outColor.a *= _procedural->isFading() ? Interpolate::calculateFadeRatio(_procedural->getFadeStartTime()) : 1.0f; batch._glColor4f(outColor.r, outColor.g, outColor.b, outColor.a); - if (render::ShapeKey(args->_globalShapeKey).isWireframe()) { - DependencyManager::get()->renderWireShape(batch, MAPPING[_shape]); - } else { - DependencyManager::get()->renderShape(batch, MAPPING[_shape]); - } + DependencyManager::get()->renderShape(batch, MAPPING[_shape]); } else { // FIXME, support instanced multi-shape rendering using multidraw indirect color.a *= _isFading ? Interpolate::calculateFadeRatio(_fadeStartTime) : 1.0f; auto geometryCache = DependencyManager::get(); auto pipeline = color.a < 1.0f ? geometryCache->getTransparentShapePipeline() : geometryCache->getOpaqueShapePipeline(); - - if (render::ShapeKey(args->_globalShapeKey).isWireframe()) { - geometryCache->renderWireShapeInstance(batch, MAPPING[_shape], color, pipeline); - } else { - geometryCache->renderSolidShapeInstance(batch, MAPPING[_shape], color, pipeline); - } + geometryCache->renderSolidShapeInstance(batch, MAPPING[_shape], color, pipeline); } static const auto triCount = DependencyManager::get()->getShapeTriangleCount(MAPPING[_shape]); diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp index c4ae0db1aa..d7d7013f59 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp @@ -216,7 +216,7 @@ void RenderableWebEntityItem::render(RenderArgs* args) { if (!_texture) { auto webSurface = _webSurface; - _texture = gpu::TexturePointer(gpu::Texture::createExternal(OffscreenQmlSurface::getDiscardLambda())); + _texture = gpu::TexturePointer(gpu::Texture::createExternal2D(OffscreenQmlSurface::getDiscardLambda())); _texture->setSource(__FUNCTION__); } OffscreenQmlSurface::TextureAndFence newTextureAndFence; diff --git a/libraries/entities/src/EntitiesScriptEngineProvider.h b/libraries/entities/src/EntitiesScriptEngineProvider.h index d87dd105c2..69bf73e688 100644 --- a/libraries/entities/src/EntitiesScriptEngineProvider.h +++ b/libraries/entities/src/EntitiesScriptEngineProvider.h @@ -15,13 +15,11 @@ #define hifi_EntitiesScriptEngineProvider_h #include -#include #include "EntityItemID.h" class EntitiesScriptEngineProvider { public: virtual void callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const QStringList& params = QStringList()) = 0; - virtual QFuture getLocalEntityScriptDetails(const EntityItemID& entityID) = 0; }; -#endif // hifi_EntitiesScriptEngineProvider_h +#endif // hifi_EntitiesScriptEngineProvider_h \ No newline at end of file diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index 0bb085459e..3ef1648fae 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -655,11 +655,13 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef // pack SimulationOwner and terse update properties near each other + // NOTE: the server is authoritative for changes to simOwnerID so we always unpack ownership data // even when we would otherwise ignore the rest of the packet. bool filterRejection = false; if (propertyFlags.getHasProperty(PROP_SIMULATION_OWNER)) { + QByteArray simOwnerData; int bytes = OctreePacketData::unpackDataFromBytes(dataAt, simOwnerData); SimulationOwner newSimOwner; @@ -1877,7 +1879,6 @@ void EntityItem::setSimulationOwner(const SimulationOwner& owner) { } void EntityItem::updateSimulationOwner(const SimulationOwner& owner) { - // NOTE: this method only used by EntityServer. The Interface uses special code in readEntityDataFromBuffer(). if (wantTerseEditLogging() && _simulationOwner != owner) { qCDebug(entities) << "sim ownership for" << getDebugName() << "is now" << owner; } @@ -1893,9 +1894,8 @@ void EntityItem::clearSimulationOwnership() { } _simulationOwner.clear(); - // don't bother setting the DIRTY_SIMULATOR_ID flag because: - // (a) when entity-server calls clearSimulationOwnership() the dirty-flags are meaningless (only used by interface) - // (b) the interface only calls clearSimulationOwnership() in a context that already knows best about dirty flags + // don't bother setting the DIRTY_SIMULATOR_ID flag because clearSimulationOwnership() + // is only ever called on the entity-server and the flags are only used client-side //_dirtyFlags |= Simulation::DIRTY_SIMULATOR_ID; } diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index 1ed020e592..ea81df3801 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -49,6 +49,13 @@ EntityItemProperties::EntityItemProperties(EntityPropertyFlags desiredProperties } +void EntityItemProperties::setSittingPoints(const QVector& sittingPoints) { + _sittingPoints.clear(); + foreach (SittingPoint sitPoint, sittingPoints) { + _sittingPoints.append(sitPoint); + } +} + void EntityItemProperties::calculateNaturalPosition(const glm::vec3& min, const glm::vec3& max) { glm::vec3 halfDimension = (max - min) / 2.0f; _naturalPosition = max - halfDimension; @@ -539,6 +546,20 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_TEXTURES, textures); } + // Sitting properties support + if (!skipDefaults && !strictSemantics) { + QScriptValue sittingPoints = engine->newObject(); + for (int i = 0; i < _sittingPoints.size(); ++i) { + QScriptValue sittingPoint = engine->newObject(); + sittingPoint.setProperty("name", _sittingPoints.at(i).name); + sittingPoint.setProperty("position", vec3toScriptValue(engine, _sittingPoints.at(i).position)); + sittingPoint.setProperty("rotation", quatToScriptValue(engine, _sittingPoints.at(i).rotation)); + sittingPoints.setProperty(i, sittingPoint); + } + sittingPoints.setProperty("length", _sittingPoints.size()); + COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER_ALWAYS(sittingPoints, sittingPoints); // gettable, but not settable + } + if (!skipDefaults && !strictSemantics) { AABox aaBox = getAABox(); QScriptValue boundingBox = engine->newObject(); diff --git a/libraries/entities/src/EntityItemProperties.h b/libraries/entities/src/EntityItemProperties.h index 590298e102..419740e4ea 100644 --- a/libraries/entities/src/EntityItemProperties.h +++ b/libraries/entities/src/EntityItemProperties.h @@ -22,6 +22,7 @@ #include #include +#include // for SittingPoint #include #include #include @@ -254,6 +255,8 @@ public: void clearID() { _id = UNKNOWN_ENTITY_ID; _idSet = false; } void markAllChanged(); + void setSittingPoints(const QVector& sittingPoints); + const glm::vec3& getNaturalDimensions() const { return _naturalDimensions; } void setNaturalDimensions(const glm::vec3& value) { _naturalDimensions = value; } @@ -322,6 +325,7 @@ private: // NOTE: The following are pseudo client only properties. They are only used in clients which can access // properties of model geometry. But these properties are not serialized like other properties. + QVector _sittingPoints; QVariantMap _textureNames; glm::vec3 _naturalDimensions; glm::vec3 _naturalPosition; diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index 1ab5438e53..540eba4511 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -8,15 +8,8 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // - -#include -#include - #include "EntityScriptingInterface.h" -#include -#include - #include "EntityItemID.h" #include #include @@ -296,11 +289,13 @@ EntityItemProperties EntityScriptingInterface::getEntityProperties(QUuid identit results = entity->getProperties(desiredProperties); - // TODO: improve naturalDimensions in the future, - // for now we've added this hack for setting natural dimensions of models + // TODO: improve sitting points and naturalDimensions in the future, + // for now we've included the old sitting points model behavior for entity types that are models + // we've also added this hack for setting natural dimensions of models if (entity->getType() == EntityTypes::Model) { const FBXGeometry* geometry = _entityTree->getGeometryForEntity(entity); if (geometry) { + results.setSittingPoints(geometry->sittingPoints); Extents meshExtents = geometry->getUnscaledMeshExtents(); results.setNaturalDimensions(meshExtents.maximum - meshExtents.minimum); results.calculateNaturalPosition(meshExtents.minimum, meshExtents.maximum); @@ -685,118 +680,6 @@ bool EntityScriptingInterface::reloadServerScripts(QUuid entityID) { return client->reloadServerScript(entityID); } -bool EntityPropertyMetadataRequest::script(EntityItemID entityID, QScriptValue handler) { - using LocalScriptStatusRequest = QFutureWatcher; - - LocalScriptStatusRequest* request = new LocalScriptStatusRequest; - QObject::connect(request, &LocalScriptStatusRequest::finished, _engine, [=]() mutable { - auto details = request->result().toMap(); - QScriptValue err, result; - if (details.contains("isError")) { - if (!details.contains("message")) { - details["message"] = details["errorInfo"]; - } - err = _engine->makeError(_engine->toScriptValue(details)); - } else { - details["success"] = true; - result = _engine->toScriptValue(details); - } - callScopedHandlerObject(handler, err, result); - request->deleteLater(); - }); - auto entityScriptingInterface = DependencyManager::get(); - entityScriptingInterface->withEntitiesScriptEngine([&](EntitiesScriptEngineProvider* entitiesScriptEngine) { - if (entitiesScriptEngine) { - request->setFuture(entitiesScriptEngine->getLocalEntityScriptDetails(entityID)); - } - }); - if (!request->isStarted()) { - request->deleteLater(); - callScopedHandlerObject(handler, _engine->makeError("Entities Scripting Provider unavailable", "InternalError"), QScriptValue()); - return false; - } - return true; -} - -bool EntityPropertyMetadataRequest::serverScripts(EntityItemID entityID, QScriptValue handler) { - auto client = DependencyManager::get(); - auto request = client->createScriptStatusRequest(entityID); - QPointer engine = _engine; - QObject::connect(request, &GetScriptStatusRequest::finished, _engine, [=](GetScriptStatusRequest* request) mutable { - auto engine = _engine; - if (!engine) { - qCDebug(entities) << __FUNCTION__ << " -- engine destroyed while inflight" << entityID; - return; - } - QVariantMap details; - details["success"] = request->getResponseReceived(); - details["isRunning"] = request->getIsRunning(); - details["status"] = EntityScriptStatus_::valueToKey(request->getStatus()).toLower(); - details["errorInfo"] = request->getErrorInfo(); - - QScriptValue err, result; - if (!details["success"].toBool()) { - if (!details.contains("message") && details.contains("errorInfo")) { - details["message"] = details["errorInfo"]; - } - if (details["message"].toString().isEmpty()) { - details["message"] = "entity server script details not found"; - } - err = engine->makeError(engine->toScriptValue(details)); - } else { - result = engine->toScriptValue(details); - } - callScopedHandlerObject(handler, err, result); - request->deleteLater(); - }); - request->start(); - return true; -} - -bool EntityScriptingInterface::queryPropertyMetadata(QUuid entityID, QScriptValue property, QScriptValue scopeOrCallback, QScriptValue methodOrName) { - auto name = property.toString(); - auto handler = makeScopedHandlerObject(scopeOrCallback, methodOrName); - QPointer engine = dynamic_cast(handler.engine()); - if (!engine) { - qCDebug(entities) << "queryPropertyMetadata without detectable engine" << entityID << name; - return false; - } -#ifdef DEBUG_ENGINE_STATE - connect(engine, &QObject::destroyed, this, [=]() { - qDebug() << "queryPropertyMetadata -- engine destroyed!" << (!engine ? "nullptr" : "engine"); - }); -#endif - if (!handler.property("callback").isFunction()) { - qDebug() << "!handler.callback.isFunction" << engine; - engine->raiseException(engine->makeError("callback is not a function", "TypeError")); - return false; - } - - // NOTE: this approach is a work-in-progress and for now just meant to work 100% correctly and provide - // some initial structure for organizing metadata adapters around. - - // The extra layer of indirection is *essential* because in real world conditions errors are often introduced - // by accident and sometimes without exact memory of "what just changed." - - // Here the scripter only needs to know an entityID and a property name -- which means all scripters can - // level this method when stuck in dead-end scenarios or to learn more about "magic" Entity properties - // like .script that work in terms of side-effects. - - // This is an async callback pattern -- so if needed C++ can easily throttle or restrict queries later. - - EntityPropertyMetadataRequest request(engine); - - if (name == "script") { - return request.script(entityID, handler); - } else if (name == "serverScripts") { - return request.serverScripts(entityID, handler); - } else { - engine->raiseException(engine->makeError("metadata for property " + name + " is not yet queryable")); - engine->maybeEmitUncaughtException(__FUNCTION__); - return false; - } -} - bool EntityScriptingInterface::getServerScriptStatus(QUuid entityID, QScriptValue callback) { auto client = DependencyManager::get(); auto request = client->createScriptStatusRequest(entityID); @@ -932,7 +815,8 @@ void RayToEntityIntersectionResultFromScriptValue(const QScriptValue& object, Ra } } -bool EntityScriptingInterface::polyVoxWorker(QUuid entityID, std::function actor) { +bool EntityScriptingInterface::setVoxels(QUuid entityID, + std::function actor) { PROFILE_RANGE(script_entities, __FUNCTION__); if (!_entityTree) { @@ -998,9 +882,11 @@ bool EntityScriptingInterface::setPoints(QUuid entityID, std::function& points) { PROFILE_RANGE(script_entities, __FUNCTION__); @@ -1674,20 +1541,3 @@ bool EntityScriptingInterface::AABoxIntersectsCapsule(const glm::vec3& low, cons AABox aaBox(low, dimensions); return aaBox.findCapsulePenetration(start, end, radius, penetration); } - -glm::mat4 EntityScriptingInterface::getEntityTransform(const QUuid& entityID) { - glm::mat4 result; - if (_entityTree) { - _entityTree->withReadLock([&] { - EntityItemPointer entity = _entityTree->findEntityByEntityItemID(EntityItemID(entityID)); - if (entity) { - glm::mat4 translation = glm::translate(entity->getPosition()); - glm::mat4 rotation = glm::mat4_cast(entity->getRotation()); - glm::mat4 registration = glm::translate(ENTITY_ITEM_DEFAULT_REGISTRATION_POINT - - entity->getRegistrationPoint()); - result = translation * rotation * registration; - } - }); - } - return result; -} diff --git a/libraries/entities/src/EntityScriptingInterface.h b/libraries/entities/src/EntityScriptingInterface.h index 63b5771e60..e9f0637830 100644 --- a/libraries/entities/src/EntityScriptingInterface.h +++ b/libraries/entities/src/EntityScriptingInterface.h @@ -34,23 +34,7 @@ #include "EntitiesScriptEngineProvider.h" #include "EntityItemProperties.h" -#include "BaseScriptEngine.h" - class EntityTree; -class MeshProxy; - -// helper factory to compose standardized, async metadata queries for "magic" Entity properties -// like .script and .serverScripts. This is used for automated testing of core scripting features -// as well as to provide early adopters a self-discoverable, consistent way to diagnose common -// problems with their own Entity scripts. -class EntityPropertyMetadataRequest { -public: - EntityPropertyMetadataRequest(BaseScriptEngine* engine) : _engine(engine) {}; - bool script(EntityItemID entityID, QScriptValue handler); - bool serverScripts(EntityItemID entityID, QScriptValue handler); -private: - QPointer _engine; -}; class RayToEntityIntersectionResult { public: @@ -83,7 +67,6 @@ class EntityScriptingInterface : public OctreeScriptingInterface, public Depende Q_PROPERTY(float costMultiplier READ getCostMultiplier WRITE setCostMultiplier) Q_PROPERTY(QUuid keyboardFocusEntity READ getKeyboardFocusEntity WRITE setKeyboardFocusEntity) - friend EntityPropertyMetadataRequest; public: EntityScriptingInterface(bool bidOnSimulationOwnership); @@ -228,26 +211,6 @@ public slots: Q_INVOKABLE RayToEntityIntersectionResult findRayIntersectionBlocking(const PickRay& ray, bool precisionPicking = false, const QScriptValue& entityIdsToInclude = QScriptValue(), const QScriptValue& entityIdsToDiscard = QScriptValue()); Q_INVOKABLE bool reloadServerScripts(QUuid entityID); - - /**jsdoc - * Query additional metadata for "magic" Entity properties like `script` and `serverScripts`. - * - * @function Entities.queryPropertyMetadata - * @param {EntityID} entityID The ID of the entity. - * @param {string} property The name of the property extended metadata is wanted for. - * @param {ResultCallback} callback Executes callback(err, result) with the query results. - */ - /**jsdoc - * Query additional metadata for "magic" Entity properties like `script` and `serverScripts`. - * - * @function Entities.queryPropertyMetadata - * @param {EntityID} entityID The ID of the entity. - * @param {string} property The name of the property extended metadata is wanted for. - * @param {Object} thisObject The scoping "this" context that callback will be executed within. - * @param {ResultCallback} callbackOrMethodName Executes thisObject[callbackOrMethodName](err, result) with the query results. - */ - Q_INVOKABLE bool queryPropertyMetadata(QUuid entityID, QScriptValue property, QScriptValue scopeOrCallback, QScriptValue methodOrName = QScriptValue()); - Q_INVOKABLE bool getServerScriptStatus(QUuid entityID, QScriptValue callback); Q_INVOKABLE void setLightsArePickable(bool value); @@ -266,7 +229,6 @@ public slots: Q_INVOKABLE bool setAllVoxels(QUuid entityID, int value); Q_INVOKABLE bool setVoxelsInCuboid(QUuid entityID, const glm::vec3& lowPosition, const glm::vec3& cuboidSize, int value); - Q_INVOKABLE void voxelsToMesh(QUuid entityID, QScriptValue callback); Q_INVOKABLE bool setAllPoints(QUuid entityID, const QVector& points); Q_INVOKABLE bool appendPoint(QUuid entityID, const glm::vec3& point); @@ -331,15 +293,6 @@ public slots: const glm::vec3& start, const glm::vec3& end, float radius); - /**jsdoc - * Returns object to world transform, excluding scale - * - * @function Entities.getEntityTransform - * @param {EntityID} entityID The ID of the entity whose transform is to be returned - * @return {Mat4} Entity's object to world transform, excluding scale - */ - Q_INVOKABLE glm::mat4 getEntityTransform(const QUuid& entityID); - signals: void collisionWithEntity(const EntityItemID& idA, const EntityItemID& idB, const Collision& collision); @@ -370,14 +323,9 @@ signals: void webEventReceived(const EntityItemID& entityItemID, const QVariant& message); -protected: - void withEntitiesScriptEngine(std::function function) { - std::lock_guard lock(_entitiesScriptEngineLock); - function(_entitiesScriptEngine); - }; private: bool actionWorker(const QUuid& entityID, std::function actor); - bool polyVoxWorker(QUuid entityID, std::function actor); + bool setVoxels(QUuid entityID, std::function actor); bool setPoints(QUuid entityID, std::function actor); void queueEntityMessage(PacketType packetType, EntityItemID entityID, const EntityItemProperties& properties); diff --git a/libraries/entities/src/PolyVoxEntityItem.cpp b/libraries/entities/src/PolyVoxEntityItem.cpp index 90344d6c4b..2a374c1d17 100644 --- a/libraries/entities/src/PolyVoxEntityItem.cpp +++ b/libraries/entities/src/PolyVoxEntityItem.cpp @@ -242,7 +242,3 @@ const QByteArray PolyVoxEntityItem::getVoxelData() const { }); return voxelDataCopy; } - -bool PolyVoxEntityItem::getMeshAsScriptValue(QScriptEngine *engine, QScriptValue& result) { - return false; -} diff --git a/libraries/entities/src/PolyVoxEntityItem.h b/libraries/entities/src/PolyVoxEntityItem.h index 311a002a4a..910d8eff88 100644 --- a/libraries/entities/src/PolyVoxEntityItem.h +++ b/libraries/entities/src/PolyVoxEntityItem.h @@ -57,8 +57,6 @@ class PolyVoxEntityItem : public EntityItem { virtual void setVoxelData(QByteArray voxelData); virtual const QByteArray getVoxelData() const; - virtual int getOnCount() const { return 0; } - enum PolyVoxSurfaceStyle { SURFACE_MARCHING_CUBES, SURFACE_CUBIC, @@ -133,9 +131,7 @@ class PolyVoxEntityItem : public EntityItem { virtual void rebakeMesh() {}; void setVoxelDataDirty(bool value) { withWriteLock([&] { _voxelDataDirty = value; }); } - virtual void recomputeMesh() {}; - - virtual bool getMeshAsScriptValue(QScriptEngine *engine, QScriptValue& result); + virtual void getMesh() {}; // recompute mesh protected: glm::vec3 _voxelVolumeSize; // this is always 3 bytes diff --git a/libraries/entities/src/PropertyGroup.h b/libraries/entities/src/PropertyGroup.h index f45d19f5eb..38b1e5f599 100644 --- a/libraries/entities/src/PropertyGroup.h +++ b/libraries/entities/src/PropertyGroup.h @@ -14,11 +14,9 @@ #include -#include - +//#include "EntityItemProperties.h" #include "EntityPropertyFlags.h" - class EntityItemProperties; class EncodeBitstreamParams; class OctreePacketData; @@ -26,6 +24,31 @@ class EntityTreeElementExtraEncodeData; class ReadBitstreamToTreeParams; using EntityTreeElementExtraEncodeDataPointer = std::shared_ptr; +#include + +/* +#include + +#include +#include + +#include +#include +#include + +#include +#include // for SittingPoint +#include +#include +#include + +#include "EntityItemID.h" +#include "PropertyGroupMacros.h" +#include "EntityTypes.h" +*/ + +//typedef PropertyFlags EntityPropertyFlags; + class PropertyGroup { public: diff --git a/libraries/fbx/src/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp index 718793fefa..fcaef90527 100644 --- a/libraries/fbx/src/FBXReader.cpp +++ b/libraries/fbx/src/FBXReader.cpp @@ -1468,9 +1468,6 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS // Create the Material Library consolidateFBXMaterials(mapping); - // We can't allow the scaling of a given image to different sizes, because the hash used for the KTX cache is based on the original image - // Allowing scaling of the same image to different sizes would cause different KTX files to target the same cache key -#if 0 // HACK: until we get proper LOD management we're going to cap model textures // according to how many unique textures the model uses: // 1 - 8 textures --> 2048 @@ -1484,7 +1481,6 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS int numTextures = uniqueTextures.size(); const int MAX_NUM_TEXTURES_AT_MAX_RESOLUTION = 8; int maxWidth = sqrt(MAX_NUM_PIXELS_FOR_FBX_TEXTURE); - if (numTextures > MAX_NUM_TEXTURES_AT_MAX_RESOLUTION) { int numTextureThreshold = MAX_NUM_TEXTURES_AT_MAX_RESOLUTION; const int MIN_MIP_TEXTURE_WIDTH = 64; @@ -1498,7 +1494,7 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS material.setMaxNumPixelsPerTexture(maxWidth * maxWidth); } } -#endif + geometry.materials = _fbxMaterials; // see if any materials have texture children @@ -1799,6 +1795,19 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS } geometry.palmDirection = parseVec3(mapping.value("palmDirection", "0, -1, 0").toString()); + // Add sitting points + QVariantHash sittingPoints = mapping.value("sit").toHash(); + for (QVariantHash::const_iterator it = sittingPoints.constBegin(); it != sittingPoints.constEnd(); it++) { + SittingPoint sittingPoint; + sittingPoint.name = it.key(); + + QVariantList properties = it->toList(); + sittingPoint.position = parseVec3(properties.at(0).toString()); + sittingPoint.rotation = glm::quat(glm::radians(parseVec3(properties.at(1).toString()))); + + geometry.sittingPoints.append(sittingPoint); + } + // attempt to map any meshes to a named model for (QHash::const_iterator m = meshIDsToMeshIndices.constBegin(); m != meshIDsToMeshIndices.constEnd(); m++) { diff --git a/libraries/fbx/src/FBXReader.h b/libraries/fbx/src/FBXReader.h index fa047e512f..6e51c413dc 100644 --- a/libraries/fbx/src/FBXReader.h +++ b/libraries/fbx/src/FBXReader.h @@ -265,6 +265,24 @@ public: Q_DECLARE_METATYPE(FBXAnimationFrame) Q_DECLARE_METATYPE(QVector) +/// A point where an avatar can sit +class SittingPoint { +public: + QString name; + glm::vec3 position; // relative postion + glm::quat rotation; // relative orientation +}; + +inline bool operator==(const SittingPoint& lhs, const SittingPoint& rhs) +{ + return (lhs.name == rhs.name) && (lhs.position == rhs.position) && (lhs.rotation == rhs.rotation); +} + +inline bool operator!=(const SittingPoint& lhs, const SittingPoint& rhs) +{ + return (lhs.name != rhs.name) || (lhs.position != rhs.position) || (lhs.rotation != rhs.rotation); +} + /// A set of meshes extracted from an FBX document. class FBXGeometry { public: @@ -302,6 +320,8 @@ public: glm::vec3 palmDirection; + QVector sittingPoints; + glm::vec3 neckPivot; Extents bindExtents; diff --git a/libraries/fbx/src/FBXReader_Node.cpp b/libraries/fbx/src/FBXReader_Node.cpp index d987f885eb..d814f58dab 100644 --- a/libraries/fbx/src/FBXReader_Node.cpp +++ b/libraries/fbx/src/FBXReader_Node.cpp @@ -54,8 +54,7 @@ template QVariant readBinaryArray(QDataStream& in, int& position) { in.readRawData(compressed.data() + sizeof(quint32), compressedLength); position += compressedLength; arrayData = qUncompress(compressed); - if (arrayData.isEmpty() || - (unsigned int)arrayData.size() != (sizeof(T) * arrayLength)) { // answers empty byte array if corrupt + if (arrayData.isEmpty() || arrayData.size() != (sizeof(T) * arrayLength)) { // answers empty byte array if corrupt throw QString("corrupt fbx file"); } } else { diff --git a/libraries/fbx/src/OBJReader.cpp b/libraries/fbx/src/OBJReader.cpp index c1bb72dff8..73cf7a520e 100644 --- a/libraries/fbx/src/OBJReader.cpp +++ b/libraries/fbx/src/OBJReader.cpp @@ -267,7 +267,7 @@ void OBJReader::parseMaterialLibrary(QIODevice* device) { } if (token == "map_Kd") { currentMaterial.diffuseTextureFilename = filename; - } else if( token == "map_Ks" ) { + } else { currentMaterial.specularTextureFilename = filename; } } @@ -546,7 +546,6 @@ FBXGeometry* OBJReader::readOBJ(QByteArray& model, const QVariantHash& mapping, QString queryPart = _url.query(); bool suppressMaterialsHack = queryPart.contains("hifiusemat"); // If this appears in query string, don't fetch mtl even if used. OBJMaterial& preDefinedMaterial = materials[SMART_DEFAULT_MATERIAL_NAME]; - preDefinedMaterial.used = true; if (suppressMaterialsHack) { needsMaterialLibrary = preDefinedMaterial.userSpecifiesUV = false; // I said it was a hack... } @@ -595,8 +594,8 @@ FBXGeometry* OBJReader::readOBJ(QByteArray& model, const QVariantHash& mapping, } foreach (QString materialID, materials.keys()) { - OBJMaterial& objMaterial = materials[materialID]; - if (!objMaterial.used) { + OBJMaterial& objMaterial = materials[materialID]; + if (!objMaterial.used) { continue; } geometry.materials[materialID] = FBXMaterial(objMaterial.diffuseColor, @@ -612,9 +611,6 @@ FBXGeometry* OBJReader::readOBJ(QByteArray& model, const QVariantHash& mapping, if (!objMaterial.diffuseTextureFilename.isEmpty()) { fbxMaterial.albedoTexture.filename = objMaterial.diffuseTextureFilename; } - if (!objMaterial.specularTextureFilename.isEmpty()) { - fbxMaterial.specularTexture.filename = objMaterial.specularTextureFilename; - } modelMaterial->setEmissive(fbxMaterial.emissiveColor); modelMaterial->setAlbedo(fbxMaterial.diffuseColor); diff --git a/libraries/fbx/src/OBJReader.h b/libraries/fbx/src/OBJReader.h index b4a48c570e..200f11548d 100644 --- a/libraries/fbx/src/OBJReader.h +++ b/libraries/fbx/src/OBJReader.h @@ -58,7 +58,7 @@ public: QByteArray specularTextureFilename; bool used { false }; bool userSpecifiesUV { false }; - OBJMaterial() : shininess(0.0f), opacity(1.0f), diffuseColor(0.9f), specularColor(0.9f) {} + OBJMaterial() : shininess(96.0f), opacity(1.0f), diffuseColor(1.0f), specularColor(1.0f) {} }; class OBJReader: public QObject { // QObject so we can make network requests. diff --git a/libraries/fbx/src/OBJWriter.cpp b/libraries/fbx/src/OBJWriter.cpp deleted file mode 100644 index 5ee04c5718..0000000000 --- a/libraries/fbx/src/OBJWriter.cpp +++ /dev/null @@ -1,148 +0,0 @@ -// -// OBJWriter.cpp -// libraries/fbx/src/ -// -// Created by Seth Alves on 2017-1-27. -// Copyright 2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include -#include -#include "model/Geometry.h" -#include "OBJWriter.h" -#include "ModelFormatLogging.h" - -static QString formatFloat(double n) { - // limit precision to 6, but don't output trailing zeros. - QString s = QString::number(n, 'f', 6); - while (s.endsWith("0")) { - s.remove(s.size() - 1, 1); - } - if (s.endsWith(".")) { - s.remove(s.size() - 1, 1); - } - - // check for non-numbers. if we get NaN or inf or scientific notation, just return 0 - for (int i = 0; i < s.length(); i++) { - auto c = s.at(i).toLatin1(); - if (c != '-' && - c != '.' && - (c < '0' || c > '9')) { - qCDebug(modelformat) << "OBJWriter zeroing bad vertex coordinate:" << s << "because of" << c; - return QString("0"); - } - } - - return s; -} - -bool writeOBJToTextStream(QTextStream& out, QList meshes) { - // each mesh's vertices are numbered from zero. We're combining all their vertices into one list here, - // so keep track of the start index for each mesh. - QList meshVertexStartOffset; - int currentVertexStartOffset = 0; - - // write out all vertices - foreach (const MeshPointer& mesh, meshes) { - meshVertexStartOffset.append(currentVertexStartOffset); - const gpu::BufferView& vertexBuffer = mesh->getVertexBuffer(); - int vertexCount = 0; - gpu::BufferView::Iterator vertexItr = vertexBuffer.cbegin(); - while (vertexItr != vertexBuffer.cend()) { - glm::vec3 v = *vertexItr; - out << "v "; - out << formatFloat(v[0]) << " "; - out << formatFloat(v[1]) << " "; - out << formatFloat(v[2]) << "\n"; - vertexItr++; - vertexCount++; - } - currentVertexStartOffset += vertexCount; - } - out << "\n"; - - // write out faces - int nth = 0; - foreach (const MeshPointer& mesh, meshes) { - currentVertexStartOffset = meshVertexStartOffset.takeFirst(); - - const gpu::BufferView& partBuffer = mesh->getPartBuffer(); - const gpu::BufferView& indexBuffer = mesh->getIndexBuffer(); - - model::Index partCount = (model::Index)mesh->getNumParts(); - for (int partIndex = 0; partIndex < partCount; partIndex++) { - const model::Mesh::Part& part = partBuffer.get(partIndex); - - out << "g part-" << nth++ << "\n"; - - // model::Mesh::TRIANGLES - // TODO -- handle other formats - gpu::BufferView::Iterator indexItr = indexBuffer.cbegin(); - indexItr += part._startIndex; - - int indexCount = 0; - while (indexItr != indexBuffer.cend() && indexCount < part._numIndices) { - uint32_t index0 = *indexItr; - indexItr++; - indexCount++; - if (indexItr == indexBuffer.cend() || indexCount >= part._numIndices) { - qCDebug(modelformat) << "OBJWriter -- index buffer length isn't multiple of 3"; - break; - } - uint32_t index1 = *indexItr; - indexItr++; - indexCount++; - if (indexItr == indexBuffer.cend() || indexCount >= part._numIndices) { - qCDebug(modelformat) << "OBJWriter -- index buffer length isn't multiple of 3"; - break; - } - uint32_t index2 = *indexItr; - indexItr++; - indexCount++; - - out << "f "; - out << currentVertexStartOffset + index0 + 1 << " "; - out << currentVertexStartOffset + index1 + 1 << " "; - out << currentVertexStartOffset + index2 + 1 << "\n"; - } - out << "\n"; - } - } - - return true; -} - -bool writeOBJToFile(QString path, QList meshes) { - if (QFileInfo(path).exists() && !QFile::remove(path)) { - qCDebug(modelformat) << "OBJ writer failed, file exists:" << path; - return false; - } - - QFile file(path); - if (!file.open(QIODevice::WriteOnly)) { - qCDebug(modelformat) << "OBJ writer failed to open output file:" << path; - return false; - } - - QTextStream outStream(&file); - - bool success; - success = writeOBJToTextStream(outStream, meshes); - - file.close(); - return success; -} - -QString writeOBJToString(QList meshes) { - QString result; - QTextStream outStream(&result, QIODevice::ReadWrite); - bool success; - success = writeOBJToTextStream(outStream, meshes); - if (success) { - return result; - } - return QString(""); -} diff --git a/libraries/fbx/src/OBJWriter.h b/libraries/fbx/src/OBJWriter.h deleted file mode 100644 index b6e20e1ae6..0000000000 --- a/libraries/fbx/src/OBJWriter.h +++ /dev/null @@ -1,26 +0,0 @@ -// -// OBJWriter.h -// libraries/fbx/src/ -// -// Created by Seth Alves on 2017-1-27. -// Copyright 2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#ifndef hifi_objwriter_h -#define hifi_objwriter_h - - -#include -#include -#include - -using MeshPointer = std::shared_ptr; - -bool writeOBJToTextStream(QTextStream& out, QList meshes); -bool writeOBJToFile(QString path, QList meshes); -QString writeOBJToString(QList meshes); - -#endif // hifi_objwriter_h diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp index 0800c27839..c51f468908 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp @@ -62,6 +62,8 @@ BackendPointer GLBackend::createBackend() { INSTANCE = result.get(); void* voidInstance = &(*result); qApp->setProperty(hifi::properties::gl::BACKEND, QVariant::fromValue(voidInstance)); + + gl::GLTexture::initTextureTransferHelper(); return result; } @@ -207,7 +209,7 @@ void GLBackend::renderPassTransfer(const Batch& batch) { } } - { // Sync all the transform states + { // Sync all the buffers PROFILE_RANGE(render_gpu_gl_detail, "syncCPUTransform"); _transform._cameras.clear(); _transform._cameraOffsets.clear(); @@ -275,7 +277,7 @@ void GLBackend::renderPassDraw(const Batch& batch) { updateInput(); updateTransform(batch); updatePipeline(); - + CommandCall call = _commandCalls[(*command)]; (this->*(call))(batch, *offset); break; @@ -621,7 +623,6 @@ void GLBackend::queueLambda(const std::function lambda) const { } void GLBackend::recycle() const { - PROFILE_RANGE(render_gpu_gl, __FUNCTION__) { std::list> lamdbasTrash; { @@ -744,6 +745,10 @@ void GLBackend::recycle() const { glDeleteQueries((GLsizei)ids.size(), ids.data()); } } + +#ifndef THREADED_TEXTURE_TRANSFER + gl::GLTexture::_textureTransferHelper->process(); +#endif } void GLBackend::setCameraCorrection(const Mat4& correction) { diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackend.h b/libraries/gpu-gl/src/gpu/gl/GLBackend.h index 76c950ec2b..950ac65a3f 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackend.h +++ b/libraries/gpu-gl/src/gpu/gl/GLBackend.h @@ -187,15 +187,10 @@ public: virtual void do_setStateScissorRect(const Batch& batch, size_t paramOffset) final; virtual GLuint getFramebufferID(const FramebufferPointer& framebuffer) = 0; - virtual GLuint getTextureID(const TexturePointer& texture) final; + virtual GLuint getTextureID(const TexturePointer& texture, bool needTransfer = true) = 0; virtual GLuint getBufferID(const Buffer& buffer) = 0; virtual GLuint getQueryID(const QueryPointer& query) = 0; - - virtual GLFramebuffer* syncGPUObject(const Framebuffer& framebuffer) = 0; - virtual GLBuffer* syncGPUObject(const Buffer& buffer) = 0; - virtual GLTexture* syncGPUObject(const TexturePointer& texture); - virtual GLQuery* syncGPUObject(const Query& query) = 0; - //virtual bool isTextureReady(const TexturePointer& texture); + virtual bool isTextureReady(const TexturePointer& texture); virtual void releaseBuffer(GLuint id, Size size) const; virtual void releaseExternalTexture(GLuint id, const Texture::ExternalRecycler& recycler) const; @@ -211,6 +206,10 @@ public: protected: void recycle() const override; + virtual GLFramebuffer* syncGPUObject(const Framebuffer& framebuffer) = 0; + virtual GLBuffer* syncGPUObject(const Buffer& buffer) = 0; + virtual GLTexture* syncGPUObject(const TexturePointer& texture, bool sync = true) = 0; + virtual GLQuery* syncGPUObject(const Query& query) = 0; static const size_t INVALID_OFFSET = (size_t)-1; bool _inRenderTransferPass { false }; diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackendTexture.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackendTexture.cpp index ca4e328612..f51eac0e33 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackendTexture.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackendTexture.cpp @@ -14,56 +14,12 @@ using namespace gpu; using namespace gpu::gl; - -GLuint GLBackend::getTextureID(const TexturePointer& texture) { - GLTexture* object = syncGPUObject(texture); - - if (!object) { - return 0; - } - - return object->_id; +bool GLBackend::isTextureReady(const TexturePointer& texture) { + // DO not transfer the texture, this call is expected for rendering texture + GLTexture* object = syncGPUObject(texture, true); + return object && object->isReady(); } -GLTexture* GLBackend::syncGPUObject(const TexturePointer& texturePointer) { - const Texture& texture = *texturePointer; - // Special case external textures - if (TextureUsageType::EXTERNAL == texture.getUsageType()) { - Texture::ExternalUpdates updates = texture.getUpdates(); - if (!updates.empty()) { - Texture::ExternalRecycler recycler = texture.getExternalRecycler(); - Q_ASSERT(recycler); - // Discard any superfluous updates - while (updates.size() > 1) { - const auto& update = updates.front(); - // Superfluous updates will never have been read, but we want to ensure the previous - // writes to them are complete before they're written again, so return them with the - // same fences they arrived with. This can happen on any thread because no GL context - // work is involved - recycler(update.first, update.second); - updates.pop_front(); - } - - // The last texture remaining is the one we'll use to create the GLTexture - const auto& update = updates.front(); - // Check for a fence, and if it exists, inject a wait into the command stream, then destroy the fence - if (update.second) { - GLsync fence = static_cast(update.second); - glWaitSync(fence, 0, GL_TIMEOUT_IGNORED); - glDeleteSync(fence); - } - - // Create the new texture object (replaces any previous texture object) - new GLExternalTexture(shared_from_this(), texture, update.first); - } - - // Return the texture object (if any) associated with the texture, without extensive logic - // (external textures are - return Backend::getGPUObject(texture); - } - - return nullptr; -} void GLBackend::do_generateTextureMips(const Batch& batch, size_t paramOffset) { TexturePointer resourceTexture = batch._textures.get(batch._params[paramOffset + 0]._uint); @@ -72,7 +28,7 @@ void GLBackend::do_generateTextureMips(const Batch& batch, size_t paramOffset) { } // DO not transfer the texture, this call is expected for rendering texture - GLTexture* object = syncGPUObject(resourceTexture); + GLTexture* object = syncGPUObject(resourceTexture, false); if (!object) { return; } diff --git a/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.cpp b/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.cpp index 2ac7e9d060..85cf069062 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.cpp @@ -21,12 +21,13 @@ GLFramebuffer::~GLFramebuffer() { } } -bool GLFramebuffer::checkStatus() const { +bool GLFramebuffer::checkStatus(GLenum target) const { + bool result = false; switch (_status) { case GL_FRAMEBUFFER_COMPLETE: // Success ! - return true; - + result = true; + break; case GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT: qCWarning(gpugllogging) << "GLFramebuffer::syncGPUObject : Framebuffer not valid, GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT."; break; @@ -43,5 +44,5 @@ bool GLFramebuffer::checkStatus() const { qCWarning(gpugllogging) << "GLFramebuffer::syncGPUObject : Framebuffer not valid, GL_FRAMEBUFFER_UNSUPPORTED."; break; } - return false; + return result; } diff --git a/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.h b/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.h index c0633cfdef..9b4f9703fc 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.h +++ b/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.h @@ -64,7 +64,7 @@ public: protected: GLenum _status { GL_FRAMEBUFFER_COMPLETE }; virtual void update() = 0; - bool checkStatus() const; + bool checkStatus(GLenum target) const; GLFramebuffer(const std::weak_ptr& backend, const Framebuffer& framebuffer, GLuint id) : GLObject(backend, framebuffer, id) {} ~GLFramebuffer(); diff --git a/libraries/gpu-gl/src/gpu/gl/GLTexelFormat.cpp b/libraries/gpu-gl/src/gpu/gl/GLTexelFormat.cpp index 7e26e65e02..bd945cbaaa 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLTexelFormat.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLTexelFormat.cpp @@ -17,7 +17,6 @@ GLenum GLTexelFormat::evalGLTexelFormatInternal(const gpu::Element& dstFormat) { switch (dstFormat.getDimension()) { case gpu::SCALAR: { switch (dstFormat.getSemantic()) { - case gpu::RED: case gpu::RGB: case gpu::RGBA: case gpu::SRGB: @@ -263,7 +262,6 @@ GLTexelFormat GLTexelFormat::evalGLTexelFormat(const Element& dstFormat, const E texel.type = ELEMENT_TYPE_TO_GL[dstFormat.getType()]; switch (dstFormat.getSemantic()) { - case gpu::RED: case gpu::RGB: case gpu::RGBA: texel.internalFormat = GL_R8; @@ -274,10 +272,8 @@ GLTexelFormat GLTexelFormat::evalGLTexelFormat(const Element& dstFormat, const E break; case gpu::DEPTH: - texel.format = GL_DEPTH_COMPONENT; texel.internalFormat = GL_DEPTH_COMPONENT32; break; - case gpu::DEPTH_STENCIL: texel.type = GL_UNSIGNED_INT_24_8; texel.format = GL_DEPTH_STENCIL; @@ -407,7 +403,6 @@ GLTexelFormat GLTexelFormat::evalGLTexelFormat(const Element& dstFormat, const E texel.internalFormat = GL_COMPRESSED_RED_RGTC1; break; } - case gpu::RED: case gpu::RGB: case gpu::RGBA: case gpu::SRGB: diff --git a/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp b/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp index 1de820e1df..1e0dd08ae1 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp @@ -10,13 +10,15 @@ #include +#include "GLTextureTransfer.h" #include "GLBackend.h" using namespace gpu; using namespace gpu::gl; +std::shared_ptr GLTexture::_textureTransferHelper; -const GLenum GLTexture::CUBE_FACE_LAYOUT[GLTexture::TEXTURE_CUBE_NUM_FACES] = { +const GLenum GLTexture::CUBE_FACE_LAYOUT[6] = { GL_TEXTURE_CUBE_MAP_POSITIVE_X, GL_TEXTURE_CUBE_MAP_NEGATIVE_X, GL_TEXTURE_CUBE_MAP_POSITIVE_Y, GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, GL_TEXTURE_CUBE_MAP_POSITIVE_Z, GL_TEXTURE_CUBE_MAP_NEGATIVE_Z @@ -65,17 +67,6 @@ GLenum GLTexture::getGLTextureType(const Texture& texture) { } -uint8_t GLTexture::getFaceCount(GLenum target) { - switch (target) { - case GL_TEXTURE_2D: - return TEXTURE_2D_NUM_FACES; - case GL_TEXTURE_CUBE_MAP: - return TEXTURE_CUBE_NUM_FACES; - default: - Q_UNREACHABLE(); - break; - } -} const std::vector& GLTexture::getFaceTargets(GLenum target) { static std::vector cubeFaceTargets { GL_TEXTURE_CUBE_MAP_POSITIVE_X, GL_TEXTURE_CUBE_MAP_NEGATIVE_X, @@ -98,34 +89,216 @@ const std::vector& GLTexture::getFaceTargets(GLenum target) { return faceTargets; } +// Default texture memory = GPU total memory - 2GB +#define GPU_MEMORY_RESERVE_BYTES MB_TO_BYTES(2048) +// Minimum texture memory = 1GB +#define TEXTURE_MEMORY_MIN_BYTES MB_TO_BYTES(1024) + + +float GLTexture::getMemoryPressure() { + // Check for an explicit memory limit + auto availableTextureMemory = Texture::getAllowedGPUMemoryUsage(); + + + // If no memory limit has been set, use a percentage of the total dedicated memory + if (!availableTextureMemory) { +#if 0 + auto totalMemory = getDedicatedMemory(); + if ((GPU_MEMORY_RESERVE_BYTES + TEXTURE_MEMORY_MIN_BYTES) > totalMemory) { + availableTextureMemory = TEXTURE_MEMORY_MIN_BYTES; + } else { + availableTextureMemory = totalMemory - GPU_MEMORY_RESERVE_BYTES; + } +#else + // Hardcode texture limit for sparse textures at 1 GB for now + availableTextureMemory = TEXTURE_MEMORY_MIN_BYTES; +#endif + } + + // Return the consumed texture memory divided by the available texture memory. + auto consumedGpuMemory = Context::getTextureGPUMemoryUsage() - Context::getTextureGPUFramebufferMemoryUsage(); + float memoryPressure = (float)consumedGpuMemory / (float)availableTextureMemory; + static Context::Size lastConsumedGpuMemory = 0; + if (memoryPressure > 1.0f && lastConsumedGpuMemory != consumedGpuMemory) { + lastConsumedGpuMemory = consumedGpuMemory; + qCDebug(gpugllogging) << "Exceeded max allowed texture memory: " << consumedGpuMemory << " / " << availableTextureMemory; + } + return memoryPressure; +} + + +// Create the texture and allocate storage +GLTexture::GLTexture(const std::weak_ptr& backend, const Texture& texture, GLuint id, bool transferrable) : + GLObject(backend, texture, id), + _external(false), + _source(texture.source()), + _storageStamp(texture.getStamp()), + _target(getGLTextureType(texture)), + _internalFormat(gl::GLTexelFormat::evalGLTexelFormatInternal(texture.getTexelFormat())), + _maxMip(texture.maxMip()), + _minMip(texture.minMip()), + _virtualSize(texture.evalTotalSize()), + _transferrable(transferrable) +{ + auto strongBackend = _backend.lock(); + strongBackend->recycle(); + Backend::incrementTextureGPUCount(); + Backend::updateTextureGPUVirtualMemoryUsage(0, _virtualSize); + Backend::setGPUObject(texture, this); +} + GLTexture::GLTexture(const std::weak_ptr& backend, const Texture& texture, GLuint id) : GLObject(backend, texture, id), + _external(true), _source(texture.source()), - _target(getGLTextureType(texture)) + _storageStamp(0), + _target(getGLTextureType(texture)), + _internalFormat(GL_RGBA8), + // FIXME force mips to 0? + _maxMip(texture.maxMip()), + _minMip(texture.minMip()), + _virtualSize(0), + _transferrable(false) { Backend::setGPUObject(texture, this); + + // FIXME Is this necessary? + //withPreservedTexture([this] { + // syncSampler(); + // if (_gpuObject.isAutogenerateMips()) { + // generateMips(); + // } + //}); } GLTexture::~GLTexture() { - auto backend = _backend.lock(); - if (backend && _id) { - backend->releaseTexture(_id, 0); - } -} - - -GLExternalTexture::GLExternalTexture(const std::weak_ptr& backend, const Texture& texture, GLuint id) - : Parent(backend, texture, id) { } - -GLExternalTexture::~GLExternalTexture() { auto backend = _backend.lock(); if (backend) { - auto recycler = _gpuObject.getExternalRecycler(); - if (recycler) { - backend->releaseExternalTexture(_id, recycler); - } else { - qWarning() << "No recycler available for texture " << _id << " possible leak"; + if (_external) { + auto recycler = _gpuObject.getExternalRecycler(); + if (recycler) { + backend->releaseExternalTexture(_id, recycler); + } else { + qWarning() << "No recycler available for texture " << _id << " possible leak"; + } + } else if (_id) { + // WARNING! Sparse textures do not use this code path. See GL45BackendTexture for + // the GL45Texture destructor for doing any required work tracking GPU stats + backend->releaseTexture(_id, _size); } - const_cast(_id) = 0; + + if (!_external && !_transferrable) { + Backend::updateTextureGPUFramebufferMemoryUsage(_size, 0); + } + } + Backend::updateTextureGPUVirtualMemoryUsage(_virtualSize, 0); +} + +void GLTexture::createTexture() { + withPreservedTexture([&] { + allocateStorage(); + (void)CHECK_GL_ERROR(); + syncSampler(); + (void)CHECK_GL_ERROR(); + }); +} + +void GLTexture::withPreservedTexture(std::function f) const { + GLint boundTex = -1; + switch (_target) { + case GL_TEXTURE_2D: + glGetIntegerv(GL_TEXTURE_BINDING_2D, &boundTex); + break; + + case GL_TEXTURE_CUBE_MAP: + glGetIntegerv(GL_TEXTURE_BINDING_CUBE_MAP, &boundTex); + break; + + default: + qFatal("Unsupported texture type"); + } + (void)CHECK_GL_ERROR(); + + glBindTexture(_target, _texture); + f(); + glBindTexture(_target, boundTex); + (void)CHECK_GL_ERROR(); +} + +void GLTexture::setSize(GLuint size) const { + if (!_external && !_transferrable) { + Backend::updateTextureGPUFramebufferMemoryUsage(_size, size); + } + Backend::updateTextureGPUMemoryUsage(_size, size); + const_cast(_size) = size; +} + +bool GLTexture::isInvalid() const { + return _storageStamp < _gpuObject.getStamp(); +} + +bool GLTexture::isOutdated() const { + return GLSyncState::Idle == _syncState && _contentStamp < _gpuObject.getDataStamp(); +} + +bool GLTexture::isReady() const { + // If we have an invalid texture, we're never ready + if (isInvalid()) { + return false; + } + + auto syncState = _syncState.load(); + if (isOutdated() || Idle != syncState) { + return false; + } + + return true; +} + + +// Do any post-transfer operations that might be required on the main context / rendering thread +void GLTexture::postTransfer() { + setSyncState(GLSyncState::Idle); + ++_transferCount; + + // At this point the mip pixels have been loaded, we can notify the gpu texture to abandon it's memory + switch (_gpuObject.getType()) { + case Texture::TEX_2D: + for (uint16_t i = 0; i < Sampler::MAX_MIP_LEVEL; ++i) { + if (_gpuObject.isStoredMipFaceAvailable(i)) { + _gpuObject.notifyMipFaceGPULoaded(i); + } + } + break; + + case Texture::TEX_CUBE: + // transfer pixels from each faces + for (uint8_t f = 0; f < CUBE_NUM_FACES; f++) { + for (uint16_t i = 0; i < Sampler::MAX_MIP_LEVEL; ++i) { + if (_gpuObject.isStoredMipFaceAvailable(i, f)) { + _gpuObject.notifyMipFaceGPULoaded(i, f); + } + } + } + break; + + default: + qCWarning(gpugllogging) << __FUNCTION__ << " case for Texture Type " << _gpuObject.getType() << " not supported"; + break; } } + +void GLTexture::initTextureTransferHelper() { + _textureTransferHelper = std::make_shared(); +} + +void GLTexture::startTransfer() { + createTexture(); +} + +void GLTexture::finishTransfer() { + if (_gpuObject.isAutogenerateMips()) { + generateMips(); + } +} + diff --git a/libraries/gpu-gl/src/gpu/gl/GLTexture.h b/libraries/gpu-gl/src/gpu/gl/GLTexture.h index 1f91e17157..0f75a6fe51 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLTexture.h +++ b/libraries/gpu-gl/src/gpu/gl/GLTexture.h @@ -9,6 +9,7 @@ #define hifi_gpu_gl_GLTexture_h #include "GLShared.h" +#include "GLTextureTransfer.h" #include "GLBackend.h" #include "GLTexelFormat.h" @@ -19,47 +20,209 @@ struct GLFilterMode { GLint magFilter; }; + class GLTexture : public GLObject { - using Parent = GLObject; - friend class GLBackend; public: static const uint16_t INVALID_MIP { (uint16_t)-1 }; static const uint8_t INVALID_FACE { (uint8_t)-1 }; + static void initTextureTransferHelper(); + static std::shared_ptr _textureTransferHelper; + + template + static GLTexture* sync(GLBackend& backend, const TexturePointer& texturePointer, bool needTransfer) { + const Texture& texture = *texturePointer; + + // Special case external textures + if (texture.getUsage().isExternal()) { + Texture::ExternalUpdates updates = texture.getUpdates(); + if (!updates.empty()) { + Texture::ExternalRecycler recycler = texture.getExternalRecycler(); + Q_ASSERT(recycler); + // Discard any superfluous updates + while (updates.size() > 1) { + const auto& update = updates.front(); + // Superfluous updates will never have been read, but we want to ensure the previous + // writes to them are complete before they're written again, so return them with the + // same fences they arrived with. This can happen on any thread because no GL context + // work is involved + recycler(update.first, update.second); + updates.pop_front(); + } + + // The last texture remaining is the one we'll use to create the GLTexture + const auto& update = updates.front(); + // Check for a fence, and if it exists, inject a wait into the command stream, then destroy the fence + if (update.second) { + GLsync fence = static_cast(update.second); + glWaitSync(fence, 0, GL_TIMEOUT_IGNORED); + glDeleteSync(fence); + } + + // Create the new texture object (replaces any previous texture object) + new GLTextureType(backend.shared_from_this(), texture, update.first); + } + + // Return the texture object (if any) associated with the texture, without extensive logic + // (external textures are + return Backend::getGPUObject(texture); + } + + if (!texture.isDefined()) { + // NO texture definition yet so let's avoid thinking + return nullptr; + } + + // If the object hasn't been created, or the object definition is out of date, drop and re-create + GLTexture* object = Backend::getGPUObject(texture); + + // Create the texture if need be (force re-creation if the storage stamp changes + // for easier use of immutable storage) + if (!object || object->isInvalid()) { + // This automatically any previous texture + object = new GLTextureType(backend.shared_from_this(), texture, needTransfer); + if (!object->_transferrable) { + object->createTexture(); + object->_contentStamp = texture.getDataStamp(); + object->updateSize(); + object->postTransfer(); + } + } + + // Object maybe doens't neet to be tranasferred after creation + if (!object->_transferrable) { + return object; + } + + // If we just did a transfer, return the object after doing post-transfer work + if (GLSyncState::Transferred == object->getSyncState()) { + object->postTransfer(); + } + + if (object->isOutdated()) { + // Object might be outdated, if so, start the transfer + // (outdated objects that are already in transfer will have reported 'true' for ready() + _textureTransferHelper->transferTexture(texturePointer); + return nullptr; + } + + if (!object->isReady()) { + return nullptr; + } + + ((GLTexture*)object)->updateMips(); + + return object; + } + + template + static GLuint getId(GLBackend& backend, const TexturePointer& texture, bool shouldSync) { + if (!texture) { + return 0; + } + GLTexture* object { nullptr }; + if (shouldSync) { + object = sync(backend, texture, shouldSync); + } else { + object = Backend::getGPUObject(*texture); + } + + if (!object) { + return 0; + } + + if (!shouldSync) { + return object->_id; + } + + // Don't return textures that are in transfer state + if ((object->getSyncState() != GLSyncState::Idle) || + // Don't return transferrable textures that have never completed transfer + (!object->_transferrable || 0 != object->_transferCount)) { + return 0; + } + + return object->_id; + } + ~GLTexture(); + // Is this texture generated outside the GPU library? + const bool _external; const GLuint& _texture { _id }; const std::string _source; + const Stamp _storageStamp; const GLenum _target; + const GLenum _internalFormat; + const uint16 _maxMip; + uint16 _minMip; + const GLuint _virtualSize; // theoretical size as expected + Stamp _contentStamp { 0 }; + const bool _transferrable; + Size _transferCount { 0 }; + GLuint size() const { return _size; } + GLSyncState getSyncState() const { return _syncState; } - static const std::vector& getFaceTargets(GLenum textureType); - static uint8_t getFaceCount(GLenum textureType); - static GLenum getGLTextureType(const Texture& texture); + // Is the storage out of date relative to the gpu texture? + bool isInvalid() const; - static const uint8_t TEXTURE_2D_NUM_FACES = 1; - static const uint8_t TEXTURE_CUBE_NUM_FACES = 6; - static const GLenum CUBE_FACE_LAYOUT[TEXTURE_CUBE_NUM_FACES]; + // Is the content out of date relative to the gpu texture? + bool isOutdated() const; + + // Is the texture in a state where it can be rendered with no work? + bool isReady() const; + + // Execute any post-move operations that must occur only on the main thread + virtual void postTransfer(); + + uint16 usedMipLevels() const { return (_maxMip - _minMip) + 1; } + + static const size_t CUBE_NUM_FACES = 6; + static const GLenum CUBE_FACE_LAYOUT[6]; static const GLFilterMode FILTER_MODES[Sampler::NUM_FILTERS]; static const GLenum WRAP_MODES[Sampler::NUM_WRAP_MODES]; + // Return a floating point value indicating how much of the allowed + // texture memory we are currently consuming. A value of 0 indicates + // no texture memory usage, while a value of 1 indicates all available / allowed memory + // is consumed. A value above 1 indicates that there is a problem. + static float getMemoryPressure(); protected: - virtual uint32 size() const = 0; - virtual void generateMips() const = 0; + static const std::vector& getFaceTargets(GLenum textureType); + + static GLenum getGLTextureType(const Texture& texture); + + + const GLuint _size { 0 }; // true size as reported by the gl api + std::atomic _syncState { GLSyncState::Idle }; + + GLTexture(const std::weak_ptr& backend, const Texture& texture, GLuint id, bool transferrable); GLTexture(const std::weak_ptr& backend, const Texture& texture, GLuint id); -}; -class GLExternalTexture : public GLTexture { - using Parent = GLTexture; - friend class GLBackend; -public: - ~GLExternalTexture(); + void setSyncState(GLSyncState syncState) { _syncState = syncState; } + + void createTexture(); + + virtual void updateMips() {} + virtual void allocateStorage() const = 0; + virtual void updateSize() const = 0; + virtual void syncSampler() const = 0; + virtual void generateMips() const = 0; + virtual void withPreservedTexture(std::function f) const; + protected: - GLExternalTexture(const std::weak_ptr& backend, const Texture& texture, GLuint id); - void generateMips() const override {} - uint32 size() const override { return 0; } -}; + void setSize(GLuint size) const; + virtual void startTransfer(); + // Returns true if this is the last block required to complete transfer + virtual bool continueTransfer() { return false; } + virtual void finishTransfer(); + +private: + friend class GLTextureTransferHelper; + friend class GLBackend; +}; } } diff --git a/libraries/gpu-gl/src/gpu/gl/GLTextureTransfer.cpp b/libraries/gpu-gl/src/gpu/gl/GLTextureTransfer.cpp new file mode 100644 index 0000000000..9dac2986e3 --- /dev/null +++ b/libraries/gpu-gl/src/gpu/gl/GLTextureTransfer.cpp @@ -0,0 +1,208 @@ +// +// Created by Bradley Austin Davis on 2016/04/03 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#include "GLTextureTransfer.h" + +#include +#include + +#include + +#include "GLShared.h" +#include "GLTexture.h" + +#ifdef HAVE_NSIGHT +#include "nvToolsExt.h" +std::unordered_map _map; +#endif + + +#ifdef TEXTURE_TRANSFER_PBOS +#define TEXTURE_TRANSFER_BLOCK_SIZE (64 * 1024) +#define TEXTURE_TRANSFER_PBO_COUNT 128 +#endif + +using namespace gpu; +using namespace gpu::gl; + +GLTextureTransferHelper::GLTextureTransferHelper() { +#ifdef THREADED_TEXTURE_TRANSFER + setObjectName("TextureTransferThread"); + _context.create(); + initialize(true, QThread::LowPriority); + // Clean shutdown on UNIX, otherwise _canvas is freed early + connect(qApp, &QCoreApplication::aboutToQuit, [&] { terminate(); }); +#else + initialize(false, QThread::LowPriority); +#endif +} + +GLTextureTransferHelper::~GLTextureTransferHelper() { +#ifdef THREADED_TEXTURE_TRANSFER + if (isStillRunning()) { + terminate(); + } +#else + terminate(); +#endif +} + +void GLTextureTransferHelper::transferTexture(const gpu::TexturePointer& texturePointer) { + GLTexture* object = Backend::getGPUObject(*texturePointer); + + Backend::incrementTextureGPUTransferCount(); + object->setSyncState(GLSyncState::Pending); + Lock lock(_mutex); + _pendingTextures.push_back(texturePointer); +} + +void GLTextureTransferHelper::setup() { +#ifdef THREADED_TEXTURE_TRANSFER + _context.makeCurrent(); + +#ifdef TEXTURE_TRANSFER_FORCE_DRAW + // FIXME don't use opengl 4.5 DSA functionality without verifying it's present + glCreateRenderbuffers(1, &_drawRenderbuffer); + glNamedRenderbufferStorage(_drawRenderbuffer, GL_RGBA8, 128, 128); + glCreateFramebuffers(1, &_drawFramebuffer); + glNamedFramebufferRenderbuffer(_drawFramebuffer, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, _drawRenderbuffer); + glCreateFramebuffers(1, &_readFramebuffer); +#endif + +#ifdef TEXTURE_TRANSFER_PBOS + std::array pbos; + glCreateBuffers(TEXTURE_TRANSFER_PBO_COUNT, &pbos[0]); + for (uint32_t i = 0; i < TEXTURE_TRANSFER_PBO_COUNT; ++i) { + TextureTransferBlock newBlock; + newBlock._pbo = pbos[i]; + glNamedBufferStorage(newBlock._pbo, TEXTURE_TRANSFER_BLOCK_SIZE, 0, GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT); + newBlock._mapped = glMapNamedBufferRange(newBlock._pbo, 0, TEXTURE_TRANSFER_BLOCK_SIZE, GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT); + _readyQueue.push(newBlock); + } +#endif +#endif +} + +void GLTextureTransferHelper::shutdown() { +#ifdef THREADED_TEXTURE_TRANSFER + _context.makeCurrent(); +#endif + +#ifdef TEXTURE_TRANSFER_FORCE_DRAW + glNamedFramebufferRenderbuffer(_drawFramebuffer, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, 0); + glDeleteFramebuffers(1, &_drawFramebuffer); + _drawFramebuffer = 0; + glDeleteFramebuffers(1, &_readFramebuffer); + _readFramebuffer = 0; + + glNamedFramebufferTexture(_readFramebuffer, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0); + glDeleteRenderbuffers(1, &_drawRenderbuffer); + _drawRenderbuffer = 0; +#endif +} + +void GLTextureTransferHelper::queueExecution(VoidLambda lambda) { + Lock lock(_mutex); + _pendingCommands.push_back(lambda); +} + +#define MAX_TRANSFERS_PER_PASS 2 + +bool GLTextureTransferHelper::process() { + // Take any new textures or commands off the queue + VoidLambdaList pendingCommands; + TextureList newTransferTextures; + { + Lock lock(_mutex); + newTransferTextures.swap(_pendingTextures); + pendingCommands.swap(_pendingCommands); + } + + if (!pendingCommands.empty()) { + for (auto command : pendingCommands) { + command(); + } + glFlush(); + } + + if (!newTransferTextures.empty()) { + for (auto& texturePointer : newTransferTextures) { +#ifdef HAVE_NSIGHT + _map[texturePointer] = nvtxRangeStart("TextureTansfer"); +#endif + GLTexture* object = Backend::getGPUObject(*texturePointer); + object->startTransfer(); + _transferringTextures.push_back(texturePointer); + _textureIterator = _transferringTextures.begin(); + } + _transferringTextures.sort([](const gpu::TexturePointer& a, const gpu::TexturePointer& b)->bool { + return a->getSize() < b->getSize(); + }); + } + + // No transfers in progress, sleep + if (_transferringTextures.empty()) { +#ifdef THREADED_TEXTURE_TRANSFER + QThread::usleep(1); +#endif + return true; + } + PROFILE_COUNTER_IF_CHANGED(render_gpu_gl, "transferringTextures", int, (int) _transferringTextures.size()) + + static auto lastReport = usecTimestampNow(); + auto now = usecTimestampNow(); + auto lastReportInterval = now - lastReport; + if (lastReportInterval > USECS_PER_SECOND * 4) { + lastReport = now; + qCDebug(gpulogging) << "Texture list " << _transferringTextures.size(); + } + + size_t transferCount = 0; + for (_textureIterator = _transferringTextures.begin(); _textureIterator != _transferringTextures.end();) { + if (++transferCount > MAX_TRANSFERS_PER_PASS) { + break; + } + auto texture = *_textureIterator; + GLTexture* gltexture = Backend::getGPUObject(*texture); + if (gltexture->continueTransfer()) { + ++_textureIterator; + continue; + } + + gltexture->finishTransfer(); + +#ifdef TEXTURE_TRANSFER_FORCE_DRAW + // FIXME force a draw on the texture transfer thread before passing the texture to the main thread for use +#endif + +#ifdef THREADED_TEXTURE_TRANSFER + clientWait(); +#endif + gltexture->_contentStamp = gltexture->_gpuObject.getDataStamp(); + gltexture->updateSize(); + gltexture->setSyncState(gpu::gl::GLSyncState::Transferred); + Backend::decrementTextureGPUTransferCount(); +#ifdef HAVE_NSIGHT + // Mark the texture as transferred + nvtxRangeEnd(_map[texture]); + _map.erase(texture); +#endif + _textureIterator = _transferringTextures.erase(_textureIterator); + } + +#ifdef THREADED_TEXTURE_TRANSFER + if (!_transferringTextures.empty()) { + // Don't saturate the GPU + clientWait(); + } else { + // Don't saturate the CPU + QThread::msleep(1); + } +#endif + + return true; +} diff --git a/libraries/gpu-gl/src/gpu/gl/GLTextureTransfer.h b/libraries/gpu-gl/src/gpu/gl/GLTextureTransfer.h new file mode 100644 index 0000000000..a23c282fd4 --- /dev/null +++ b/libraries/gpu-gl/src/gpu/gl/GLTextureTransfer.h @@ -0,0 +1,78 @@ +// +// Created by Bradley Austin Davis on 2016/04/03 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#ifndef hifi_gpu_gl_GLTextureTransfer_h +#define hifi_gpu_gl_GLTextureTransfer_h + +#include +#include + +#include + +#include + +#include "GLShared.h" + +#ifdef Q_OS_WIN +#define THREADED_TEXTURE_TRANSFER +#endif + +#ifdef THREADED_TEXTURE_TRANSFER +// FIXME when sparse textures are enabled, it's harder to force a draw on the transfer thread +// also, the current draw code is implicitly using OpenGL 4.5 functionality +//#define TEXTURE_TRANSFER_FORCE_DRAW +// FIXME PBO's increase the complexity and don't seem to work reliably +//#define TEXTURE_TRANSFER_PBOS +#endif + +namespace gpu { namespace gl { + +using TextureList = std::list; +using TextureListIterator = TextureList::iterator; + +class GLTextureTransferHelper : public GenericThread { +public: + using VoidLambda = std::function; + using VoidLambdaList = std::list; + using Pointer = std::shared_ptr; + GLTextureTransferHelper(); + ~GLTextureTransferHelper(); + void transferTexture(const gpu::TexturePointer& texturePointer); + void queueExecution(VoidLambda lambda); + + void setup() override; + void shutdown() override; + bool process() override; + +private: +#ifdef THREADED_TEXTURE_TRANSFER + ::gl::OffscreenContext _context; +#endif + +#ifdef TEXTURE_TRANSFER_FORCE_DRAW + // Framebuffers / renderbuffers for forcing access to the texture on the transfer thread + GLuint _drawRenderbuffer { 0 }; + GLuint _drawFramebuffer { 0 }; + GLuint _readFramebuffer { 0 }; +#endif + + // A mutex for protecting items access on the render and transfer threads + Mutex _mutex; + // Commands that have been submitted for execution on the texture transfer thread + VoidLambdaList _pendingCommands; + // Textures that have been submitted for transfer + TextureList _pendingTextures; + // Textures currently in the transfer process + // Only used on the transfer thread + TextureList _transferringTextures; + TextureListIterator _textureIterator; + +}; + +} } + +#endif \ No newline at end of file diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h b/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h index 6d2f91c436..72e2f5a804 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h +++ b/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h @@ -40,28 +40,18 @@ public: class GL41Texture : public GLTexture { using Parent = GLTexture; - static GLuint allocate(); - + GLuint allocate(); public: - ~GL41Texture(); - - private: - GL41Texture(const std::weak_ptr& backend, const Texture& buffer); + GL41Texture(const std::weak_ptr& backend, const Texture& buffer, GLuint externalId); + GL41Texture(const std::weak_ptr& backend, const Texture& buffer, bool transferrable); + protected: + void transferMip(uint16_t mipLevel, uint8_t face) const; + void startTransfer() override; + void allocateStorage() const override; + void updateSize() const override; + void syncSampler() const override; void generateMips() const override; - uint32 size() const override; - - friend class GL41Backend; - const Stamp _storageStamp; - mutable Stamp _contentStamp { 0 }; - mutable Stamp _samplerStamp { 0 }; - const uint32 _size; - - - bool isOutdated() const; - void withPreservedTexture(std::function f) const; - void syncContent() const; - void syncSampler() const; }; @@ -72,7 +62,8 @@ protected: GLuint getBufferID(const Buffer& buffer) override; GLBuffer* syncGPUObject(const Buffer& buffer) override; - GLTexture* syncGPUObject(const TexturePointer& texture) override; + GLuint getTextureID(const TexturePointer& texture, bool needTransfer = true) override; + GLTexture* syncGPUObject(const TexturePointer& texture, bool sync = true) override; GLuint getQueryID(const QueryPointer& query) override; GLQuery* syncGPUObject(const Query& query) override; diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41BackendOutput.cpp b/libraries/gpu-gl/src/gpu/gl41/GL41BackendOutput.cpp index 195b155bf3..6d11a52035 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41BackendOutput.cpp +++ b/libraries/gpu-gl/src/gpu/gl41/GL41BackendOutput.cpp @@ -53,12 +53,10 @@ public: GL_COLOR_ATTACHMENT15 }; int unit = 0; - auto backend = _backend.lock(); for (auto& b : _gpuObject.getRenderBuffers()) { surface = b._texture; if (surface) { - Q_ASSERT(TextureUsageType::RENDERBUFFER == surface->getUsageType()); - gltexture = backend->syncGPUObject(surface); + gltexture = gl::GLTexture::sync(*_backend.lock().get(), surface, false); // Grab the gltexture and don't transfer } else { gltexture = nullptr; } @@ -83,11 +81,9 @@ public: } if (_gpuObject.getDepthStamp() != _depthStamp) { - auto backend = _backend.lock(); auto surface = _gpuObject.getDepthStencilBuffer(); if (_gpuObject.hasDepthStencil() && surface) { - Q_ASSERT(TextureUsageType::RENDERBUFFER == surface->getUsageType()); - gltexture = backend->syncGPUObject(surface); + gltexture = gl::GLTexture::sync(*_backend.lock().get(), surface, false); // Grab the gltexture and don't transfer } if (gltexture) { @@ -114,7 +110,7 @@ public: glBindFramebuffer(GL_DRAW_FRAMEBUFFER, currentFBO); } - checkStatus(); + checkStatus(GL_DRAW_FRAMEBUFFER); } diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp b/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp index 8dbef09f06..65c45111db 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp +++ b/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp @@ -29,102 +29,20 @@ GLuint GL41Texture::allocate() { return result; } -GLTexture* GL41Backend::syncGPUObject(const TexturePointer& texturePointer) { - if (!texturePointer) { - return nullptr; - } - const Texture& texture = *texturePointer; - if (TextureUsageType::EXTERNAL == texture.getUsageType()) { - return Parent::syncGPUObject(texturePointer); - } - - if (!texture.isDefined()) { - // NO texture definition yet so let's avoid thinking - return nullptr; - } - - // If the object hasn't been created, or the object definition is out of date, drop and re-create - GL41Texture* object = Backend::getGPUObject(texture); - if (!object || object->_storageStamp < texture.getStamp()) { - // This automatically any previous texture - object = new GL41Texture(shared_from_this(), texture); - } - - // FIXME internalize to GL41Texture 'sync' function - if (object->isOutdated()) { - object->withPreservedTexture([&] { - if (object->_contentStamp <= texture.getDataStamp()) { - // FIXME implement synchronous texture transfer here - object->syncContent(); - } - - if (object->_samplerStamp <= texture.getSamplerStamp()) { - object->syncSampler(); - } - }); - } - - return object; +GLuint GL41Backend::getTextureID(const TexturePointer& texture, bool transfer) { + return GL41Texture::getId(*this, texture, transfer); } -GL41Texture::GL41Texture(const std::weak_ptr& backend, const Texture& texture) - : GLTexture(backend, texture, allocate()), _storageStamp { texture.getStamp() }, _size(texture.evalTotalSize()) { - incrementTextureGPUCount(); - withPreservedTexture([&] { - GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat(), _gpuObject.getStoredMipFormat()); - auto numMips = _gpuObject.evalNumMips(); - for (uint16_t mipLevel = 0; mipLevel < numMips; ++mipLevel) { - // Get the mip level dimensions, accounting for the downgrade level - Vec3u dimensions = _gpuObject.evalMipDimensions(mipLevel); - uint8_t face = 0; - for (GLenum target : getFaceTargets(_target)) { - const Byte* mipData = nullptr; - if (_gpuObject.isStoredMipFaceAvailable(mipLevel, face)) { - auto mip = _gpuObject.accessStoredMipFace(mipLevel, face); - mipData = mip->readData(); - } - glTexImage2D(target, mipLevel, texelFormat.internalFormat, dimensions.x, dimensions.y, 0, texelFormat.format, texelFormat.type, mipData); - (void)CHECK_GL_ERROR(); - ++face; - } - } - }); +GLTexture* GL41Backend::syncGPUObject(const TexturePointer& texture, bool transfer) { + return GL41Texture::sync(*this, texture, transfer); } -GL41Texture::~GL41Texture() { - +GL41Texture::GL41Texture(const std::weak_ptr& backend, const Texture& texture, GLuint externalId) + : GLTexture(backend, texture, externalId) { } -bool GL41Texture::isOutdated() const { - if (_samplerStamp <= _gpuObject.getSamplerStamp()) { - return true; - } - if (TextureUsageType::RESOURCE == _gpuObject.getUsageType() && _contentStamp <= _gpuObject.getDataStamp()) { - return true; - } - return false; -} - -void GL41Texture::withPreservedTexture(std::function f) const { - GLint boundTex = -1; - switch (_target) { - case GL_TEXTURE_2D: - glGetIntegerv(GL_TEXTURE_BINDING_2D, &boundTex); - break; - - case GL_TEXTURE_CUBE_MAP: - glGetIntegerv(GL_TEXTURE_BINDING_CUBE_MAP, &boundTex); - break; - - default: - qFatal("Unsupported texture type"); - } - (void)CHECK_GL_ERROR(); - - glBindTexture(_target, _texture); - f(); - glBindTexture(_target, boundTex); - (void)CHECK_GL_ERROR(); +GL41Texture::GL41Texture(const std::weak_ptr& backend, const Texture& texture, bool transferrable) + : GLTexture(backend, texture, allocate(), transferrable) { } void GL41Texture::generateMips() const { @@ -134,12 +52,94 @@ void GL41Texture::generateMips() const { (void)CHECK_GL_ERROR(); } -void GL41Texture::syncContent() const { - // FIXME actually copy the texture data - _contentStamp = _gpuObject.getDataStamp() + 1; +void GL41Texture::allocateStorage() const { + GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat()); + glTexParameteri(_target, GL_TEXTURE_BASE_LEVEL, 0); + (void)CHECK_GL_ERROR(); + glTexParameteri(_target, GL_TEXTURE_MAX_LEVEL, _maxMip - _minMip); + (void)CHECK_GL_ERROR(); + if (GLEW_VERSION_4_2 && !_gpuObject.getTexelFormat().isCompressed()) { + // Get the dimensions, accounting for the downgrade level + Vec3u dimensions = _gpuObject.evalMipDimensions(_minMip); + glTexStorage2D(_target, usedMipLevels(), texelFormat.internalFormat, dimensions.x, dimensions.y); + (void)CHECK_GL_ERROR(); + } else { + for (uint16_t l = _minMip; l <= _maxMip; l++) { + // Get the mip level dimensions, accounting for the downgrade level + Vec3u dimensions = _gpuObject.evalMipDimensions(l); + for (GLenum target : getFaceTargets(_target)) { + glTexImage2D(target, l - _minMip, texelFormat.internalFormat, dimensions.x, dimensions.y, 0, texelFormat.format, texelFormat.type, NULL); + (void)CHECK_GL_ERROR(); + } + } + } } -void GL41Texture::syncSampler() const { +void GL41Texture::updateSize() const { + setSize(_virtualSize); + if (!_id) { + return; + } + + if (_gpuObject.getTexelFormat().isCompressed()) { + GLenum proxyType = GL_TEXTURE_2D; + GLuint numFaces = 1; + if (_gpuObject.getType() == gpu::Texture::TEX_CUBE) { + proxyType = CUBE_FACE_LAYOUT[0]; + numFaces = (GLuint)CUBE_NUM_FACES; + } + GLint gpuSize{ 0 }; + glGetTexLevelParameteriv(proxyType, 0, GL_TEXTURE_COMPRESSED, &gpuSize); + (void)CHECK_GL_ERROR(); + + if (gpuSize) { + for (GLuint level = _minMip; level < _maxMip; level++) { + GLint levelSize{ 0 }; + glGetTexLevelParameteriv(proxyType, level, GL_TEXTURE_COMPRESSED_IMAGE_SIZE, &levelSize); + levelSize *= numFaces; + + if (levelSize <= 0) { + break; + } + gpuSize += levelSize; + } + (void)CHECK_GL_ERROR(); + setSize(gpuSize); + return; + } + } +} + +// Move content bits from the CPU to the GPU for a given mip / face +void GL41Texture::transferMip(uint16_t mipLevel, uint8_t face) const { + auto mip = _gpuObject.accessStoredMipFace(mipLevel, face); + GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat(), mip->getFormat()); + //GLenum target = getFaceTargets()[face]; + GLenum target = _target == GL_TEXTURE_2D ? GL_TEXTURE_2D : CUBE_FACE_LAYOUT[face]; + auto size = _gpuObject.evalMipDimensions(mipLevel); + glTexSubImage2D(target, mipLevel, 0, 0, size.x, size.y, texelFormat.format, texelFormat.type, mip->readData()); + (void)CHECK_GL_ERROR(); +} + +void GL41Texture::startTransfer() { + PROFILE_RANGE(render_gpu_gl, __FUNCTION__); + Parent::startTransfer(); + + glBindTexture(_target, _id); + (void)CHECK_GL_ERROR(); + + // transfer pixels from each faces + uint8_t numFaces = (Texture::TEX_CUBE == _gpuObject.getType()) ? CUBE_NUM_FACES : 1; + for (uint8_t f = 0; f < numFaces; f++) { + for (uint16_t i = 0; i < Sampler::MAX_MIP_LEVEL; ++i) { + if (_gpuObject.isStoredMipFaceAvailable(i, f)) { + transferMip(i, f); + } + } + } +} + +void GL41Backend::GL41Texture::syncSampler() const { const Sampler& sampler = _gpuObject.getSampler(); const auto& fm = FILTER_MODES[sampler.getFilter()]; glTexParameteri(_target, GL_TEXTURE_MIN_FILTER, fm.minFilter); @@ -161,9 +161,5 @@ void GL41Texture::syncSampler() const { glTexParameterf(_target, GL_TEXTURE_MIN_LOD, (float)sampler.getMinMip()); glTexParameterf(_target, GL_TEXTURE_MAX_LOD, (sampler.getMaxMip() == Sampler::MAX_MIP_LEVEL ? 1000.f : sampler.getMaxMip())); glTexParameterf(_target, GL_TEXTURE_MAX_ANISOTROPY_EXT, sampler.getMaxAnisotropy()); - _samplerStamp = _gpuObject.getSamplerStamp() + 1; } -uint32 GL41Texture::size() const { - return _size; -} diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45Backend.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45Backend.cpp index 12c4b818f7..d7dde8b7d6 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45Backend.cpp +++ b/libraries/gpu-gl/src/gpu/gl45/GL45Backend.cpp @@ -18,12 +18,6 @@ Q_LOGGING_CATEGORY(gpugl45logging, "hifi.gpu.gl45") using namespace gpu; using namespace gpu::gl45; -void GL45Backend::recycle() const { - Parent::recycle(); - GL45VariableAllocationTexture::manageMemory(); - GL45VariableAllocationTexture::_frameTexturesCreated = 0; -} - void GL45Backend::do_draw(const Batch& batch, size_t paramOffset) { Primitive primitiveType = (Primitive)batch._params[paramOffset + 2]._uint; GLenum mode = gl::PRIMITIVE_TO_GL[primitiveType]; @@ -169,3 +163,8 @@ void GL45Backend::do_multiDrawIndexedIndirect(const Batch& batch, size_t paramOf _stats._DSNumAPIDrawcalls++; (void)CHECK_GL_ERROR(); } + +void GL45Backend::recycle() const { + Parent::recycle(); + derezTextures(); +} diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h b/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h index 6a9811b055..2242bba5d9 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h +++ b/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h @@ -8,21 +8,17 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#pragma once #ifndef hifi_gpu_45_GL45Backend_h #define hifi_gpu_45_GL45Backend_h #include "../gl/GLBackend.h" #include "../gl/GLTexture.h" -#include #define INCREMENTAL_TRANSFER 0 -#define THREADED_TEXTURE_BUFFERING 1 namespace gpu { namespace gl45 { using namespace gpu::gl; -using TextureWeakPointer = std::weak_ptr; class GL45Backend : public GLBackend { using Parent = GLBackend; @@ -35,219 +31,60 @@ public: class GL45Texture : public GLTexture { using Parent = GLTexture; - friend class GL45Backend; static GLuint allocate(const Texture& texture); - protected: - GL45Texture(const std::weak_ptr& backend, const Texture& texture); - void generateMips() const override; - void copyMipFaceFromTexture(uint16_t sourceMip, uint16_t targetMip, uint8_t face) const; - void copyMipFaceLinesFromTexture(uint16_t mip, uint8_t face, const uvec3& size, uint32_t yOffset, GLenum format, GLenum type, const void* sourcePointer) const; - virtual void syncSampler() const; - }; - - // - // Textures that have fixed allocation sizes and cannot be managed at runtime - // - - class GL45FixedAllocationTexture : public GL45Texture { - using Parent = GL45Texture; - friend class GL45Backend; - - public: - GL45FixedAllocationTexture(const std::weak_ptr& backend, const Texture& texture); - ~GL45FixedAllocationTexture(); - - protected: - uint32 size() const override { return _size; } - void allocateStorage() const; - void syncSampler() const override; - const uint32 _size { 0 }; - }; - - class GL45AttachmentTexture : public GL45FixedAllocationTexture { - using Parent = GL45FixedAllocationTexture; - friend class GL45Backend; - protected: - GL45AttachmentTexture(const std::weak_ptr& backend, const Texture& texture); - ~GL45AttachmentTexture(); - }; - - class GL45StrictResourceTexture : public GL45FixedAllocationTexture { - using Parent = GL45FixedAllocationTexture; - friend class GL45Backend; - protected: - GL45StrictResourceTexture(const std::weak_ptr& backend, const Texture& texture); - }; - - // - // Textures that can be managed at runtime to increase or decrease their memory load - // - - class GL45VariableAllocationTexture : public GL45Texture { - using Parent = GL45Texture; - friend class GL45Backend; - using PromoteLambda = std::function; - - public: - enum class MemoryPressureState { - Idle, - Transfer, - Oversubscribed, - Undersubscribed, - }; - - using QueuePair = std::pair; - struct QueuePairLess { - bool operator()(const QueuePair& a, const QueuePair& b) { - return a.second < b.second; - } - }; - using WorkQueue = std::priority_queue, QueuePairLess>; - - class TransferJob { - using VoidLambda = std::function; - using VoidLambdaQueue = std::queue; - using ThreadPointer = std::shared_ptr; - const GL45VariableAllocationTexture& _parent; - // Holds the contents to transfer to the GPU in CPU memory - std::vector _buffer; - // Indicates if a transfer from backing storage to interal storage has started - bool _bufferingStarted { false }; - bool _bufferingCompleted { false }; - VoidLambda _transferLambda; - VoidLambda _bufferingLambda; -#if THREADED_TEXTURE_BUFFERING - static Mutex _mutex; - static VoidLambdaQueue _bufferLambdaQueue; - static ThreadPointer _bufferThread; - static std::atomic _shutdownBufferingThread; - static void bufferLoop(); -#endif - - public: - TransferJob(const TransferJob& other) = delete; - TransferJob(const GL45VariableAllocationTexture& parent, std::function transferLambda); - TransferJob(const GL45VariableAllocationTexture& parent, uint16_t sourceMip, uint16_t targetMip, uint8_t face, uint32_t lines = 0, uint32_t lineOffset = 0); - ~TransferJob(); - bool tryTransfer(); - -#if THREADED_TEXTURE_BUFFERING - static void startTransferLoop(); - static void stopTransferLoop(); -#endif - - private: - size_t _transferSize { 0 }; -#if THREADED_TEXTURE_BUFFERING - void startBuffering(); -#endif - void transfer(); - }; - - using TransferQueue = std::queue>; - static MemoryPressureState _memoryPressureState; - protected: - static size_t _frameTexturesCreated; - static std::atomic _memoryPressureStateStale; - static std::list _memoryManagedTextures; - static WorkQueue _transferQueue; - static WorkQueue _promoteQueue; - static WorkQueue _demoteQueue; - static TexturePointer _currentTransferTexture; - static const uvec3 INITIAL_MIP_TRANSFER_DIMENSIONS; - - - static void updateMemoryPressure(); - static void processWorkQueues(); - static void addMemoryManagedTexture(const TexturePointer& texturePointer); - static void addToWorkQueue(const TexturePointer& texture); - static WorkQueue& getActiveWorkQueue(); - - static void manageMemory(); - - protected: - GL45VariableAllocationTexture(const std::weak_ptr& backend, const Texture& texture); - ~GL45VariableAllocationTexture(); - //bool canPromoteNoAllocate() const { return _allocatedMip < _populatedMip; } - bool canPromote() const { return _allocatedMip > 0; } - bool canDemote() const { return _allocatedMip < _maxAllocatedMip; } - bool hasPendingTransfers() const { return _populatedMip > _allocatedMip; } - void executeNextTransfer(const TexturePointer& currentTexture); - uint32 size() const override { return _size; } - virtual void populateTransferQueue() = 0; - virtual void promote() = 0; - virtual void demote() = 0; - - // The allocated mip level, relative to the number of mips in the gpu::Texture object - // The relationship between a given glMip to the original gpu::Texture mip is always - // glMip + _allocatedMip - uint16 _allocatedMip { 0 }; - // The populated mip level, relative to the number of mips in the gpu::Texture object - // This must always be >= the allocated mip - uint16 _populatedMip { 0 }; - // The highest (lowest resolution) mip that we will support, relative to the number - // of mips in the gpu::Texture object - uint16 _maxAllocatedMip { 0 }; - uint32 _size { 0 }; - // Contains a series of lambdas that when executed will transfer data to the GPU, modify - // the _populatedMip and update the sampler in order to fully populate the allocated texture - // until _populatedMip == _allocatedMip - TransferQueue _pendingTransfers; - }; - - class GL45ResourceTexture : public GL45VariableAllocationTexture { - using Parent = GL45VariableAllocationTexture; - friend class GL45Backend; - protected: - GL45ResourceTexture(const std::weak_ptr& backend, const Texture& texture); - - void syncSampler() const override; - void promote() override; - void demote() override; - void populateTransferQueue() override; - - void allocateStorage(uint16 mip); - void copyMipsFromTexture(); - }; - -#if 0 - class GL45SparseResourceTexture : public GL45VariableAllocationTexture { - using Parent = GL45VariableAllocationTexture; - friend class GL45Backend; - using TextureTypeFormat = std::pair; - using PageDimensions = std::vector; - using PageDimensionsMap = std::map; - static PageDimensionsMap pageDimensionsByFormat; - static Mutex pageDimensionsMutex; - - static bool isSparseEligible(const Texture& texture); - static PageDimensions getPageDimensionsForFormat(const TextureTypeFormat& typeFormat); - static PageDimensions getPageDimensionsForFormat(GLenum type, GLenum format); static const uint32_t DEFAULT_PAGE_DIMENSION = 128; static const uint32_t DEFAULT_MAX_SPARSE_LEVEL = 0xFFFF; + public: + GL45Texture(const std::weak_ptr& backend, const Texture& texture, GLuint externalId); + GL45Texture(const std::weak_ptr& backend, const Texture& texture, bool transferrable); + ~GL45Texture(); + + void postTransfer() override; + + struct SparseInfo { + SparseInfo(GL45Texture& texture); + void maybeMakeSparse(); + void update(); + uvec3 getPageCounts(const uvec3& dimensions) const; + uint32_t getPageCount(const uvec3& dimensions) const; + uint32_t getSize() const; + + GL45Texture& texture; + bool sparse { false }; + uvec3 pageDimensions { DEFAULT_PAGE_DIMENSION }; + GLuint maxSparseLevel { DEFAULT_MAX_SPARSE_LEVEL }; + uint32_t allocatedPages { 0 }; + uint32_t maxPages { 0 }; + uint32_t pageBytes { 0 }; + GLint pageDimensionsIndex { 0 }; + }; + protected: - GL45SparseResourceTexture(const std::weak_ptr& backend, const Texture& texture); - ~GL45SparseResourceTexture(); - uint32 size() const override { return _allocatedPages * _pageBytes; } - void promote() override; - void demote() override; + void updateMips() override; + void stripToMip(uint16_t newMinMip); + void startTransfer() override; + bool continueTransfer() override; + void finishTransfer() override; + void incrementalTransfer(const uvec3& size, const gpu::Texture::PixelsPointer& mip, std::function f) const; + void transferMip(uint16_t mipLevel, uint8_t face = 0) const; + void allocateMip(uint16_t mipLevel, uint8_t face = 0) const; + void allocateStorage() const override; + void updateSize() const override; + void syncSampler() const override; + void generateMips() const override; + void withPreservedTexture(std::function f) const override; + void derez(); - private: - uvec3 getPageCounts(const uvec3& dimensions) const; - uint32_t getPageCount(const uvec3& dimensions) const; - - uint32_t _allocatedPages { 0 }; - uint32_t _pageBytes { 0 }; - uvec3 _pageDimensions { DEFAULT_PAGE_DIMENSION }; - GLuint _maxSparseLevel { DEFAULT_MAX_SPARSE_LEVEL }; + SparseInfo _sparseInfo; + uint16_t _mipOffset { 0 }; + friend class GL45Backend; }; -#endif protected: - void recycle() const override; + void derezTextures() const; GLuint getFramebufferID(const FramebufferPointer& framebuffer) override; GLFramebuffer* syncGPUObject(const Framebuffer& framebuffer) override; @@ -255,7 +92,8 @@ protected: GLuint getBufferID(const Buffer& buffer) override; GLBuffer* syncGPUObject(const Buffer& buffer) override; - GLTexture* syncGPUObject(const TexturePointer& texture) override; + GLuint getTextureID(const TexturePointer& texture, bool needTransfer = true) override; + GLTexture* syncGPUObject(const TexturePointer& texture, bool sync = true) override; GLuint getQueryID(const QueryPointer& query) override; GLQuery* syncGPUObject(const Query& query) override; @@ -288,5 +126,5 @@ protected: Q_DECLARE_LOGGING_CATEGORY(gpugl45logging) -#endif +#endif diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp index 9648af9b21..c5b84b7deb 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp @@ -49,12 +49,10 @@ public: GL_COLOR_ATTACHMENT15 }; int unit = 0; - auto backend = _backend.lock(); for (auto& b : _gpuObject.getRenderBuffers()) { surface = b._texture; if (surface) { - Q_ASSERT(TextureUsageType::RENDERBUFFER == surface->getUsageType()); - gltexture = backend->syncGPUObject(surface); + gltexture = gl::GLTexture::sync(*_backend.lock().get(), surface, false); // Grab the gltexture and don't transfer } else { gltexture = nullptr; } @@ -80,10 +78,8 @@ public: if (_gpuObject.getDepthStamp() != _depthStamp) { auto surface = _gpuObject.getDepthStencilBuffer(); - auto backend = _backend.lock(); if (_gpuObject.hasDepthStencil() && surface) { - Q_ASSERT(TextureUsageType::RENDERBUFFER == surface->getUsageType()); - gltexture = backend->syncGPUObject(surface); + gltexture = gl::GLTexture::sync(*_backend.lock().get(), surface, false); // Grab the gltexture and don't transfer } if (gltexture) { @@ -106,7 +102,7 @@ public: _status = glCheckNamedFramebufferStatus(_id, GL_DRAW_FRAMEBUFFER); // restore the current framebuffer - checkStatus(); + checkStatus(GL_DRAW_FRAMEBUFFER); } diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp index 36aaf75e81..6948a045a2 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp @@ -8,10 +8,9 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // - #include "GL45Backend.h" + #include -#include #include #include #include @@ -20,70 +19,142 @@ #include #include -#include #include "../gl/GLTexelFormat.h" using namespace gpu; using namespace gpu::gl; using namespace gpu::gl45; -#define SPARSE_PAGE_SIZE_OVERHEAD_ESTIMATE 1.3f -#define MAX_RESOURCE_TEXTURES_PER_FRAME 2 +// Allocate 1 MB of buffer space for paged transfers +#define DEFAULT_PAGE_BUFFER_SIZE (1024*1024) +#define DEFAULT_GL_PIXEL_ALIGNMENT 4 -GLTexture* GL45Backend::syncGPUObject(const TexturePointer& texturePointer) { - if (!texturePointer) { - return nullptr; +using GL45Texture = GL45Backend::GL45Texture; + +static std::map> texturesByMipCounts; +static Mutex texturesByMipCountsMutex; +using TextureTypeFormat = std::pair; +std::map> sparsePageDimensionsByFormat; +Mutex sparsePageDimensionsByFormatMutex; + +static std::vector getPageDimensionsForFormat(const TextureTypeFormat& typeFormat) { + { + Lock lock(sparsePageDimensionsByFormatMutex); + if (sparsePageDimensionsByFormat.count(typeFormat)) { + return sparsePageDimensionsByFormat[typeFormat]; + } } + GLint count = 0; + glGetInternalformativ(typeFormat.first, typeFormat.second, GL_NUM_VIRTUAL_PAGE_SIZES_ARB, 1, &count); - const Texture& texture = *texturePointer; - if (TextureUsageType::EXTERNAL == texture.getUsageType()) { - return Parent::syncGPUObject(texturePointer); - } + std::vector result; + if (count > 0) { + std::vector x, y, z; + x.resize(count); + glGetInternalformativ(typeFormat.first, typeFormat.second, GL_VIRTUAL_PAGE_SIZE_X_ARB, 1, &x[0]); + y.resize(count); + glGetInternalformativ(typeFormat.first, typeFormat.second, GL_VIRTUAL_PAGE_SIZE_Y_ARB, 1, &y[0]); + z.resize(count); + glGetInternalformativ(typeFormat.first, typeFormat.second, GL_VIRTUAL_PAGE_SIZE_Z_ARB, 1, &z[0]); - if (!texture.isDefined()) { - // NO texture definition yet so let's avoid thinking - return nullptr; - } - - GL45Texture* object = Backend::getGPUObject(texture); - if (!object) { - switch (texture.getUsageType()) { - case TextureUsageType::RENDERBUFFER: - object = new GL45AttachmentTexture(shared_from_this(), texture); - break; - - case TextureUsageType::STRICT_RESOURCE: - qCDebug(gpugllogging) << "Strict texture " << texture.source().c_str(); - object = new GL45StrictResourceTexture(shared_from_this(), texture); - break; - - case TextureUsageType::RESOURCE: { - if (GL45VariableAllocationTexture::_frameTexturesCreated < MAX_RESOURCE_TEXTURES_PER_FRAME) { -#if 0 - if (isTextureManagementSparseEnabled() && GL45Texture::isSparseEligible(texture)) { - object = new GL45SparseResourceTexture(shared_from_this(), texture); - } else { - object = new GL45ResourceTexture(shared_from_this(), texture); - } -#else - object = new GL45ResourceTexture(shared_from_this(), texture); -#endif - GL45VariableAllocationTexture::addMemoryManagedTexture(texturePointer); - } else { - auto fallback = texturePointer->getFallbackTexture(); - if (fallback) { - object = static_cast(syncGPUObject(fallback)); - } - } - break; - } - - default: - Q_UNREACHABLE(); + result.resize(count); + for (GLint i = 0; i < count; ++i) { + result[i] = uvec3(x[i], y[i], z[i]); } } - return object; + { + Lock lock(sparsePageDimensionsByFormatMutex); + if (0 == sparsePageDimensionsByFormat.count(typeFormat)) { + sparsePageDimensionsByFormat[typeFormat] = result; + } + } + + return result; +} + +static std::vector getPageDimensionsForFormat(GLenum target, GLenum format) { + return getPageDimensionsForFormat({ target, format }); +} + +GLTexture* GL45Backend::syncGPUObject(const TexturePointer& texture, bool transfer) { + return GL45Texture::sync(*this, texture, transfer); +} + +using SparseInfo = GL45Backend::GL45Texture::SparseInfo; + +SparseInfo::SparseInfo(GL45Texture& texture) + : texture(texture) { +} + +void SparseInfo::maybeMakeSparse() { + // Don't enable sparse for objects with explicitly managed mip levels + if (!texture._gpuObject.isAutogenerateMips()) { + return; + } + return; + + const uvec3 dimensions = texture._gpuObject.getDimensions(); + auto allowedPageDimensions = getPageDimensionsForFormat(texture._target, texture._internalFormat); + // In order to enable sparse the texture size must be an integer multiple of the page size + for (size_t i = 0; i < allowedPageDimensions.size(); ++i) { + pageDimensionsIndex = (uint32_t) i; + pageDimensions = allowedPageDimensions[i]; + // Is this texture an integer multiple of page dimensions? + if (uvec3(0) == (dimensions % pageDimensions)) { + qCDebug(gpugl45logging) << "Enabling sparse for texture " << texture._source.c_str(); + sparse = true; + break; + } + } + + if (sparse) { + glTextureParameteri(texture._id, GL_TEXTURE_SPARSE_ARB, GL_TRUE); + glTextureParameteri(texture._id, GL_VIRTUAL_PAGE_SIZE_INDEX_ARB, pageDimensionsIndex); + } else { + qCDebug(gpugl45logging) << "Size " << dimensions.x << " x " << dimensions.y << + " is not supported by any sparse page size for texture" << texture._source.c_str(); + } +} + +#define SPARSE_PAGE_SIZE_OVERHEAD_ESTIMATE 1.3f + +// This can only be called after we've established our storage size +void SparseInfo::update() { + if (!sparse) { + return; + } + glGetTextureParameterIuiv(texture._id, GL_NUM_SPARSE_LEVELS_ARB, &maxSparseLevel); + pageBytes = texture._gpuObject.getTexelFormat().getSize(); + pageBytes *= pageDimensions.x * pageDimensions.y * pageDimensions.z; + // Testing with a simple texture allocating app shows an estimated 20% GPU memory overhead for + // sparse textures as compared to non-sparse, so we acount for that here. + pageBytes = (uint32_t)(pageBytes * SPARSE_PAGE_SIZE_OVERHEAD_ESTIMATE); + + for (uint16_t mipLevel = 0; mipLevel <= maxSparseLevel; ++mipLevel) { + auto mipDimensions = texture._gpuObject.evalMipDimensions(mipLevel); + auto mipPageCount = getPageCount(mipDimensions); + maxPages += mipPageCount; + } + if (texture._target == GL_TEXTURE_CUBE_MAP) { + maxPages *= GLTexture::CUBE_NUM_FACES; + } +} + +uvec3 SparseInfo::getPageCounts(const uvec3& dimensions) const { + auto result = (dimensions / pageDimensions) + + glm::clamp(dimensions % pageDimensions, glm::uvec3(0), glm::uvec3(1)); + return result; +} + +uint32_t SparseInfo::getPageCount(const uvec3& dimensions) const { + auto pageCounts = getPageCounts(dimensions); + return pageCounts.x * pageCounts.y * pageCounts.z; +} + + +uint32_t SparseInfo::getSize() const { + return allocatedPages * pageBytes; } void GL45Backend::initTextureManagementStage() { @@ -100,12 +171,6 @@ void GL45Backend::initTextureManagementStage() { } } -using GL45Texture = GL45Backend::GL45Texture; - -GL45Texture::GL45Texture(const std::weak_ptr& backend, const Texture& texture) - : GLTexture(backend, texture, allocate(texture)) { - incrementTextureGPUCount(); -} GLuint GL45Texture::allocate(const Texture& texture) { GLuint result; @@ -113,43 +178,164 @@ GLuint GL45Texture::allocate(const Texture& texture) { return result; } +GLuint GL45Backend::getTextureID(const TexturePointer& texture, bool transfer) { + return GL45Texture::getId(*this, texture, transfer); +} + +GL45Texture::GL45Texture(const std::weak_ptr& backend, const Texture& texture, GLuint externalId) + : GLTexture(backend, texture, externalId), _sparseInfo(*this) +{ +} + +GL45Texture::GL45Texture(const std::weak_ptr& backend, const Texture& texture, bool transferrable) + : GLTexture(backend, texture, allocate(texture), transferrable), _sparseInfo(*this) + { + + auto theBackend = _backend.lock(); + if (_transferrable && theBackend && theBackend->isTextureManagementSparseEnabled()) { + _sparseInfo.maybeMakeSparse(); + if (_sparseInfo.sparse) { + Backend::incrementTextureGPUSparseCount(); + } + } +} + +GL45Texture::~GL45Texture() { + // Remove this texture from the candidate list of derezzable textures + if (_transferrable) { + auto mipLevels = usedMipLevels(); + Lock lock(texturesByMipCountsMutex); + if (texturesByMipCounts.count(mipLevels)) { + auto& textures = texturesByMipCounts[mipLevels]; + textures.erase(this); + if (textures.empty()) { + texturesByMipCounts.erase(mipLevels); + } + } + } + + if (_sparseInfo.sparse) { + Backend::decrementTextureGPUSparseCount(); + + // Experimenation suggests that allocating sparse textures on one context/thread and deallocating + // them on another is buggy. So for sparse textures we need to queue a lambda with the deallocation + // callls to the transfer thread + auto id = _id; + // Set the class _id to 0 so we don't try to double delete + const_cast(_id) = 0; + std::list> destructionFunctions; + + uint8_t maxFace = (uint8_t)((_target == GL_TEXTURE_CUBE_MAP) ? GLTexture::CUBE_NUM_FACES : 1); + auto maxSparseMip = std::min(_maxMip, _sparseInfo.maxSparseLevel); + for (uint16_t mipLevel = _minMip; mipLevel <= maxSparseMip; ++mipLevel) { + auto mipDimensions = _gpuObject.evalMipDimensions(mipLevel); + destructionFunctions.push_back([id, maxFace, mipLevel, mipDimensions] { + glTexturePageCommitmentEXT(id, mipLevel, 0, 0, 0, mipDimensions.x, mipDimensions.y, maxFace, GL_FALSE); + }); + + auto deallocatedPages = _sparseInfo.getPageCount(mipDimensions) * maxFace; + assert(deallocatedPages <= _sparseInfo.allocatedPages); + _sparseInfo.allocatedPages -= deallocatedPages; + } + + if (0 != _sparseInfo.allocatedPages) { + qCWarning(gpugl45logging) << "Allocated pages remaining " << _id << " " << _sparseInfo.allocatedPages; + } + + auto size = _size; + const_cast(_size) = 0; + _textureTransferHelper->queueExecution([id, size, destructionFunctions] { + for (auto function : destructionFunctions) { + function(); + } + glDeleteTextures(1, &id); + Backend::decrementTextureGPUCount(); + Backend::updateTextureGPUMemoryUsage(size, 0); + Backend::updateTextureGPUSparseMemoryUsage(size, 0); + }); + } +} + +void GL45Texture::withPreservedTexture(std::function f) const { + f(); +} + void GL45Texture::generateMips() const { glGenerateTextureMipmap(_id); (void)CHECK_GL_ERROR(); } -void GL45Texture::copyMipFaceLinesFromTexture(uint16_t mip, uint8_t face, const uvec3& size, uint32_t yOffset, GLenum format, GLenum type, const void* sourcePointer) const { - if (GL_TEXTURE_2D == _target) { - glTextureSubImage2D(_id, mip, 0, yOffset, size.x, size.y, format, type, sourcePointer); - } else if (GL_TEXTURE_CUBE_MAP == _target) { - // DSA ARB does not work on AMD, so use EXT - // unless EXT is not available on the driver - if (glTextureSubImage2DEXT) { - auto target = GLTexture::CUBE_FACE_LAYOUT[face]; - glTextureSubImage2DEXT(_id, target, mip, 0, yOffset, size.x, size.y, format, type, sourcePointer); - } else { - glTextureSubImage3D(_id, mip, 0, yOffset, face, size.x, size.y, 1, format, type, sourcePointer); - } - } else { - Q_ASSERT(false); +void GL45Texture::allocateStorage() const { + if (_gpuObject.getTexelFormat().isCompressed()) { + qFatal("Compressed textures not yet supported"); } + glTextureParameteri(_id, GL_TEXTURE_BASE_LEVEL, 0); + glTextureParameteri(_id, GL_TEXTURE_MAX_LEVEL, _maxMip - _minMip); + // Get the dimensions, accounting for the downgrade level + Vec3u dimensions = _gpuObject.evalMipDimensions(_minMip + _mipOffset); + glTextureStorage2D(_id, usedMipLevels(), _internalFormat, dimensions.x, dimensions.y); (void)CHECK_GL_ERROR(); } -void GL45Texture::copyMipFaceFromTexture(uint16_t sourceMip, uint16_t targetMip, uint8_t face) const { - if (!_gpuObject.isStoredMipFaceAvailable(sourceMip)) { - return; +void GL45Texture::updateSize() const { + if (_gpuObject.getTexelFormat().isCompressed()) { + qFatal("Compressed textures not yet supported"); } - auto size = _gpuObject.evalMipDimensions(sourceMip); - auto mipData = _gpuObject.accessStoredMipFace(sourceMip, face); - if (mipData) { - GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat(), _gpuObject.getStoredMipFormat()); - copyMipFaceLinesFromTexture(targetMip, face, size, 0, texelFormat.format, texelFormat.type, mipData->readData()); + + if (_transferrable && _sparseInfo.sparse) { + auto size = _sparseInfo.getSize(); + Backend::updateTextureGPUSparseMemoryUsage(_size, size); + setSize(size); } else { - qCDebug(gpugllogging) << "Missing mipData level=" << sourceMip << " face=" << (int)face << " for texture " << _gpuObject.source().c_str(); + setSize(_gpuObject.evalTotalSize(_mipOffset)); } } +void GL45Texture::startTransfer() { + Parent::startTransfer(); + _sparseInfo.update(); +} + +bool GL45Texture::continueTransfer() { + PROFILE_RANGE(render_gpu_gl, "continueTransfer") + size_t maxFace = GL_TEXTURE_CUBE_MAP == _target ? CUBE_NUM_FACES : 1; + for (uint8_t face = 0; face < maxFace; ++face) { + for (uint16_t mipLevel = _minMip; mipLevel <= _maxMip; ++mipLevel) { + auto size = _gpuObject.evalMipDimensions(mipLevel); + if (_sparseInfo.sparse && mipLevel <= _sparseInfo.maxSparseLevel) { + glTexturePageCommitmentEXT(_id, mipLevel, 0, 0, face, size.x, size.y, 1, GL_TRUE); + _sparseInfo.allocatedPages += _sparseInfo.getPageCount(size); + } + if (_gpuObject.isStoredMipFaceAvailable(mipLevel, face)) { + PROFILE_RANGE_EX(render_gpu_gl, "texSubImage", 0x0000ffff, (size.x * size.y * maxFace / 1024)); + + auto mip = _gpuObject.accessStoredMipFace(mipLevel, face); + GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat(), mip->getFormat()); + if (GL_TEXTURE_2D == _target) { + glTextureSubImage2D(_id, mipLevel, 0, 0, size.x, size.y, texelFormat.format, texelFormat.type, mip->readData()); + } else if (GL_TEXTURE_CUBE_MAP == _target) { + // DSA ARB does not work on AMD, so use EXT + // unless EXT is not available on the driver + if (glTextureSubImage2DEXT) { + auto target = CUBE_FACE_LAYOUT[face]; + glTextureSubImage2DEXT(_id, target, mipLevel, 0, 0, size.x, size.y, texelFormat.format, texelFormat.type, mip->readData()); + } else { + glTextureSubImage3D(_id, mipLevel, 0, 0, face, size.x, size.y, 1, texelFormat.format, texelFormat.type, mip->readData()); + } + } else { + Q_ASSERT(false); + } + (void)CHECK_GL_ERROR(); + } + } + } + return false; +} + +void GL45Texture::finishTransfer() { + Parent::finishTransfer(); +} + void GL45Texture::syncSampler() const { const Sampler& sampler = _gpuObject.getSampler(); @@ -167,63 +353,163 @@ void GL45Texture::syncSampler() const { glTextureParameteri(_id, GL_TEXTURE_WRAP_S, WRAP_MODES[sampler.getWrapModeU()]); glTextureParameteri(_id, GL_TEXTURE_WRAP_T, WRAP_MODES[sampler.getWrapModeV()]); glTextureParameteri(_id, GL_TEXTURE_WRAP_R, WRAP_MODES[sampler.getWrapModeW()]); - glTextureParameterf(_id, GL_TEXTURE_MAX_ANISOTROPY_EXT, sampler.getMaxAnisotropy()); glTextureParameterfv(_id, GL_TEXTURE_BORDER_COLOR, (const float*)&sampler.getBorderColor()); - glTextureParameterf(_id, GL_TEXTURE_MIN_LOD, sampler.getMinMip()); - glTextureParameterf(_id, GL_TEXTURE_MAX_LOD, (sampler.getMaxMip() == Sampler::MAX_MIP_LEVEL ? 1000.f : sampler.getMaxMip())); -} - -using GL45FixedAllocationTexture = GL45Backend::GL45FixedAllocationTexture; - -GL45FixedAllocationTexture::GL45FixedAllocationTexture(const std::weak_ptr& backend, const Texture& texture) : GL45Texture(backend, texture), _size(texture.evalTotalSize()) { - allocateStorage(); - syncSampler(); -} - -GL45FixedAllocationTexture::~GL45FixedAllocationTexture() { -} - -void GL45FixedAllocationTexture::allocateStorage() const { - const GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat()); - const auto dimensions = _gpuObject.getDimensions(); - const auto mips = _gpuObject.evalNumMips(); - glTextureStorage2D(_id, mips, texelFormat.internalFormat, dimensions.x, dimensions.y); -} - -void GL45FixedAllocationTexture::syncSampler() const { - Parent::syncSampler(); - const Sampler& sampler = _gpuObject.getSampler(); - auto baseMip = std::max(sampler.getMipOffset(), sampler.getMinMip()); + // FIXME account for mip offsets here + auto baseMip = std::max(sampler.getMipOffset(), _minMip); glTextureParameteri(_id, GL_TEXTURE_BASE_LEVEL, baseMip); glTextureParameterf(_id, GL_TEXTURE_MIN_LOD, (float)sampler.getMinMip()); - glTextureParameterf(_id, GL_TEXTURE_MAX_LOD, (sampler.getMaxMip() == Sampler::MAX_MIP_LEVEL ? 1000.f : sampler.getMaxMip())); + glTextureParameterf(_id, GL_TEXTURE_MAX_LOD, (sampler.getMaxMip() == Sampler::MAX_MIP_LEVEL ? 1000.f : sampler.getMaxMip() - _mipOffset)); + glTextureParameterf(_id, GL_TEXTURE_MAX_ANISOTROPY_EXT, sampler.getMaxAnisotropy()); } -// Renderbuffer attachment textures -using GL45AttachmentTexture = GL45Backend::GL45AttachmentTexture; - -GL45AttachmentTexture::GL45AttachmentTexture(const std::weak_ptr& backend, const Texture& texture) : GL45FixedAllocationTexture(backend, texture) { - Backend::updateTextureGPUFramebufferMemoryUsage(0, size()); +void GL45Texture::postTransfer() { + Parent::postTransfer(); + auto mipLevels = usedMipLevels(); + if (_transferrable && mipLevels > 1 && _minMip < _sparseInfo.maxSparseLevel) { + Lock lock(texturesByMipCountsMutex); + texturesByMipCounts[mipLevels].insert(this); + } } -GL45AttachmentTexture::~GL45AttachmentTexture() { - Backend::updateTextureGPUFramebufferMemoryUsage(size(), 0); -} +void GL45Texture::stripToMip(uint16_t newMinMip) { + if (newMinMip < _minMip) { + qCWarning(gpugl45logging) << "Cannot decrease the min mip"; + return; + } -// Strict resource textures -using GL45StrictResourceTexture = GL45Backend::GL45StrictResourceTexture; + if (_sparseInfo.sparse && newMinMip > _sparseInfo.maxSparseLevel) { + qCWarning(gpugl45logging) << "Cannot increase the min mip into the mip tail"; + return; + } -GL45StrictResourceTexture::GL45StrictResourceTexture(const std::weak_ptr& backend, const Texture& texture) : GL45FixedAllocationTexture(backend, texture) { - auto mipLevels = _gpuObject.evalNumMips(); - for (uint16_t sourceMip = 0; sourceMip < mipLevels; ++sourceMip) { - uint16_t targetMip = sourceMip; - size_t maxFace = GLTexture::getFaceCount(_target); - for (uint8_t face = 0; face < maxFace; ++face) { - copyMipFaceFromTexture(sourceMip, targetMip, face); + PROFILE_RANGE(render_gpu_gl, "GL45Texture::stripToMip"); + + auto mipLevels = usedMipLevels(); + { + Lock lock(texturesByMipCountsMutex); + assert(0 != texturesByMipCounts.count(mipLevels)); + assert(0 != texturesByMipCounts[mipLevels].count(this)); + texturesByMipCounts[mipLevels].erase(this); + if (texturesByMipCounts[mipLevels].empty()) { + texturesByMipCounts.erase(mipLevels); } } - if (texture.isAutogenerateMips()) { - generateMips(); + + // If we weren't generating mips before, we need to now that we're stripping down mip levels. + if (!_gpuObject.isAutogenerateMips()) { + qCDebug(gpugl45logging) << "Force mip generation for texture"; + glGenerateTextureMipmap(_id); + } + + + uint8_t maxFace = (uint8_t)((_target == GL_TEXTURE_CUBE_MAP) ? GLTexture::CUBE_NUM_FACES : 1); + if (_sparseInfo.sparse) { + for (uint16_t mip = _minMip; mip < newMinMip; ++mip) { + auto id = _id; + auto mipDimensions = _gpuObject.evalMipDimensions(mip); + _textureTransferHelper->queueExecution([id, mip, mipDimensions, maxFace] { + glTexturePageCommitmentEXT(id, mip, 0, 0, 0, mipDimensions.x, mipDimensions.y, maxFace, GL_FALSE); + }); + + auto deallocatedPages = _sparseInfo.getPageCount(mipDimensions) * maxFace; + assert(deallocatedPages < _sparseInfo.allocatedPages); + _sparseInfo.allocatedPages -= deallocatedPages; + } + _minMip = newMinMip; + } else { + GLuint oldId = _id; + // Find the distance between the old min mip and the new one + uint16 mipDelta = newMinMip - _minMip; + _mipOffset += mipDelta; + const_cast(_maxMip) -= mipDelta; + auto newLevels = usedMipLevels(); + + // Create and setup the new texture (allocate) + { + Vec3u newDimensions = _gpuObject.evalMipDimensions(_mipOffset); + PROFILE_RANGE_EX(render_gpu_gl, "Re-Allocate", 0xff0000ff, (newDimensions.x * newDimensions.y)); + + glCreateTextures(_target, 1, &const_cast(_id)); + glTextureParameteri(_id, GL_TEXTURE_BASE_LEVEL, 0); + glTextureParameteri(_id, GL_TEXTURE_MAX_LEVEL, _maxMip - _minMip); + glTextureStorage2D(_id, newLevels, _internalFormat, newDimensions.x, newDimensions.y); + } + + // Copy the contents of the old texture to the new + { + PROFILE_RANGE(render_gpu_gl, "Blit"); + // Preferred path only available in 4.3 + for (uint16 targetMip = _minMip; targetMip <= _maxMip; ++targetMip) { + uint16 sourceMip = targetMip + mipDelta; + Vec3u mipDimensions = _gpuObject.evalMipDimensions(targetMip + _mipOffset); + for (GLenum target : getFaceTargets(_target)) { + glCopyImageSubData( + oldId, target, sourceMip, 0, 0, 0, + _id, target, targetMip, 0, 0, 0, + mipDimensions.x, mipDimensions.y, 1 + ); + (void)CHECK_GL_ERROR(); + } + } + + glDeleteTextures(1, &oldId); + } + } + + // Re-sync the sampler to force access to the new mip level + syncSampler(); + updateSize(); + + // Re-insert into the texture-by-mips map if appropriate + mipLevels = usedMipLevels(); + if (mipLevels > 1 && (!_sparseInfo.sparse || _minMip < _sparseInfo.maxSparseLevel)) { + Lock lock(texturesByMipCountsMutex); + texturesByMipCounts[mipLevels].insert(this); } } +void GL45Texture::updateMips() { + if (!_sparseInfo.sparse) { + return; + } + auto newMinMip = std::min(_gpuObject.minMip(), _sparseInfo.maxSparseLevel); + if (_minMip < newMinMip) { + stripToMip(newMinMip); + } +} + +void GL45Texture::derez() { + if (_sparseInfo.sparse) { + assert(_minMip < _sparseInfo.maxSparseLevel); + } + assert(_minMip < _maxMip); + assert(_transferrable); + stripToMip(_minMip + 1); +} + +void GL45Backend::derezTextures() const { + if (GLTexture::getMemoryPressure() < 1.0f) { + return; + } + + Lock lock(texturesByMipCountsMutex); + if (texturesByMipCounts.empty()) { + // No available textures to derez + return; + } + + auto mipLevel = texturesByMipCounts.rbegin()->first; + if (mipLevel <= 1) { + // No mips available to remove + return; + } + + GL45Texture* targetTexture = nullptr; + { + auto& textures = texturesByMipCounts[mipLevel]; + assert(!textures.empty()); + targetTexture = *textures.begin(); + } + lock.unlock(); + targetTexture->derez(); +} diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendVariableTexture.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendVariableTexture.cpp deleted file mode 100644 index d54ad1ea4b..0000000000 --- a/libraries/gpu-gl/src/gpu/gl45/GL45BackendVariableTexture.cpp +++ /dev/null @@ -1,1033 +0,0 @@ -// -// GL45BackendTexture.cpp -// libraries/gpu/src/gpu -// -// Created by Sam Gateau on 1/19/2015. -// Copyright 2014 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include "GL45Backend.h" -#include -#include -#include -#include -#include -#include - -#include -#include - -#include -#include "../gl/GLTexelFormat.h" - -using namespace gpu; -using namespace gpu::gl; -using namespace gpu::gl45; - -// Variable sized textures -using GL45VariableAllocationTexture = GL45Backend::GL45VariableAllocationTexture; -using MemoryPressureState = GL45VariableAllocationTexture::MemoryPressureState; -using WorkQueue = GL45VariableAllocationTexture::WorkQueue; - -std::list GL45VariableAllocationTexture::_memoryManagedTextures; -MemoryPressureState GL45VariableAllocationTexture::_memoryPressureState = MemoryPressureState::Idle; -std::atomic GL45VariableAllocationTexture::_memoryPressureStateStale { false }; -const uvec3 GL45VariableAllocationTexture::INITIAL_MIP_TRANSFER_DIMENSIONS { 64, 64, 1 }; -WorkQueue GL45VariableAllocationTexture::_transferQueue; -WorkQueue GL45VariableAllocationTexture::_promoteQueue; -WorkQueue GL45VariableAllocationTexture::_demoteQueue; -TexturePointer GL45VariableAllocationTexture::_currentTransferTexture; - -#define OVERSUBSCRIBED_PRESSURE_VALUE 0.95f -#define UNDERSUBSCRIBED_PRESSURE_VALUE 0.85f -#define DEFAULT_ALLOWED_TEXTURE_MEMORY_MB ((size_t)1024) - -static const size_t DEFAULT_ALLOWED_TEXTURE_MEMORY = MB_TO_BYTES(DEFAULT_ALLOWED_TEXTURE_MEMORY_MB); - -using TransferJob = GL45VariableAllocationTexture::TransferJob; - -static const uvec3 MAX_TRANSFER_DIMENSIONS { 1024, 1024, 1 }; -static const size_t MAX_TRANSFER_SIZE = MAX_TRANSFER_DIMENSIONS.x * MAX_TRANSFER_DIMENSIONS.y * 4; - -#if THREADED_TEXTURE_BUFFERING -std::shared_ptr TransferJob::_bufferThread { nullptr }; -std::atomic TransferJob::_shutdownBufferingThread { false }; -Mutex TransferJob::_mutex; -TransferJob::VoidLambdaQueue TransferJob::_bufferLambdaQueue; - -void TransferJob::startTransferLoop() { - if (_bufferThread) { - return; - } - _shutdownBufferingThread = false; - _bufferThread = std::make_shared([] { - TransferJob::bufferLoop(); - }); -} - -void TransferJob::stopTransferLoop() { - if (!_bufferThread) { - return; - } - _shutdownBufferingThread = true; - _bufferThread->join(); - _bufferThread.reset(); - _shutdownBufferingThread = false; -} -#endif - -TransferJob::TransferJob(const GL45VariableAllocationTexture& parent, uint16_t sourceMip, uint16_t targetMip, uint8_t face, uint32_t lines, uint32_t lineOffset) - : _parent(parent) { - - auto transferDimensions = _parent._gpuObject.evalMipDimensions(sourceMip); - GLenum format; - GLenum type; - auto mipData = _parent._gpuObject.accessStoredMipFace(sourceMip, face); - GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_parent._gpuObject.getTexelFormat(), _parent._gpuObject.getStoredMipFormat()); - format = texelFormat.format; - type = texelFormat.type; - - if (0 == lines) { - _transferSize = mipData->getSize(); - _bufferingLambda = [=] { - _buffer.resize(_transferSize); - memcpy(&_buffer[0], mipData->readData(), _transferSize); - _bufferingCompleted = true; - }; - - } else { - transferDimensions.y = lines; - auto dimensions = _parent._gpuObject.evalMipDimensions(sourceMip); - auto mipSize = mipData->getSize(); - auto bytesPerLine = (uint32_t)mipSize / dimensions.y; - _transferSize = bytesPerLine * lines; - auto sourceOffset = bytesPerLine * lineOffset; - _bufferingLambda = [=] { - _buffer.resize(_transferSize); - memcpy(&_buffer[0], mipData->readData() + sourceOffset, _transferSize); - _bufferingCompleted = true; - }; - } - - Backend::updateTextureTransferPendingSize(0, _transferSize); - - _transferLambda = [=] { - _parent.copyMipFaceLinesFromTexture(targetMip, face, transferDimensions, lineOffset, format, type, _buffer.data()); - std::vector emptyVector; - _buffer.swap(emptyVector); - }; -} - -TransferJob::TransferJob(const GL45VariableAllocationTexture& parent, std::function transferLambda) - : _parent(parent), _bufferingCompleted(true), _transferLambda(transferLambda) { -} - -TransferJob::~TransferJob() { - Backend::updateTextureTransferPendingSize(_transferSize, 0); -} - - -bool TransferJob::tryTransfer() { - // Disable threaded texture transfer for now -#if THREADED_TEXTURE_BUFFERING - // Are we ready to transfer - if (_bufferingCompleted) { - _transferLambda(); - return true; - } - - startBuffering(); - return false; -#else - if (!_bufferingCompleted) { - _bufferingLambda(); - _bufferingCompleted = true; - } - _transferLambda(); - return true; -#endif -} - -#if THREADED_TEXTURE_BUFFERING - -void TransferJob::startBuffering() { - if (_bufferingStarted) { - return; - } - _bufferingStarted = true; - { - Lock lock(_mutex); - _bufferLambdaQueue.push(_bufferingLambda); - } -} - -void TransferJob::bufferLoop() { - while (!_shutdownBufferingThread) { - VoidLambdaQueue workingQueue; - { - Lock lock(_mutex); - _bufferLambdaQueue.swap(workingQueue); - } - - if (workingQueue.empty()) { - QThread::msleep(5); - continue; - } - - while (!workingQueue.empty()) { - workingQueue.front()(); - workingQueue.pop(); - } - } -} -#endif - - -void GL45VariableAllocationTexture::addMemoryManagedTexture(const TexturePointer& texturePointer) { - _memoryManagedTextures.push_back(texturePointer); - addToWorkQueue(texturePointer); -} - -void GL45VariableAllocationTexture::addToWorkQueue(const TexturePointer& texturePointer) { - GL45VariableAllocationTexture* object = Backend::getGPUObject(*texturePointer); - switch (_memoryPressureState) { - case MemoryPressureState::Oversubscribed: - if (object->canDemote()) { - // Demote largest first - _demoteQueue.push({ texturePointer, (float)object->size() }); - } - break; - - case MemoryPressureState::Undersubscribed: - if (object->canPromote()) { - // Promote smallest first - _promoteQueue.push({ texturePointer, 1.0f / (float)object->size() }); - } - break; - - case MemoryPressureState::Transfer: - if (object->hasPendingTransfers()) { - // Transfer priority given to smaller mips first - _transferQueue.push({ texturePointer, 1.0f / (float)object->_gpuObject.evalMipSize(object->_populatedMip) }); - } - break; - - case MemoryPressureState::Idle: - break; - - default: - Q_UNREACHABLE(); - } -} - -WorkQueue& GL45VariableAllocationTexture::getActiveWorkQueue() { - static WorkQueue empty; - switch (_memoryPressureState) { - case MemoryPressureState::Oversubscribed: - return _demoteQueue; - - case MemoryPressureState::Undersubscribed: - return _promoteQueue; - - case MemoryPressureState::Transfer: - return _transferQueue; - - default: - break; - } - Q_UNREACHABLE(); - return empty; -} - -// FIXME hack for stats display -QString getTextureMemoryPressureModeString() { - switch (GL45VariableAllocationTexture::_memoryPressureState) { - case MemoryPressureState::Oversubscribed: - return "Oversubscribed"; - - case MemoryPressureState::Undersubscribed: - return "Undersubscribed"; - - case MemoryPressureState::Transfer: - return "Transfer"; - - case MemoryPressureState::Idle: - return "Idle"; - } - Q_UNREACHABLE(); - return "Unknown"; -} - -void GL45VariableAllocationTexture::updateMemoryPressure() { - static size_t lastAllowedMemoryAllocation = gpu::Texture::getAllowedGPUMemoryUsage(); - - size_t allowedMemoryAllocation = gpu::Texture::getAllowedGPUMemoryUsage(); - if (0 == allowedMemoryAllocation) { - allowedMemoryAllocation = DEFAULT_ALLOWED_TEXTURE_MEMORY; - } - - // If the user explicitly changed the allowed memory usage, we need to mark ourselves stale - // so that we react - if (allowedMemoryAllocation != lastAllowedMemoryAllocation) { - _memoryPressureStateStale = true; - lastAllowedMemoryAllocation = allowedMemoryAllocation; - } - - if (!_memoryPressureStateStale.exchange(false)) { - return; - } - - PROFILE_RANGE(render_gpu_gl, __FUNCTION__); - - // Clear any defunct textures (weak pointers that no longer have a valid texture) - _memoryManagedTextures.remove_if([&](const TextureWeakPointer& weakPointer) { - return weakPointer.expired(); - }); - - // Convert weak pointers to strong. This new list may still contain nulls if a texture was - // deleted on another thread between the previous line and this one - std::vector strongTextures; { - strongTextures.reserve(_memoryManagedTextures.size()); - std::transform( - _memoryManagedTextures.begin(), _memoryManagedTextures.end(), - std::back_inserter(strongTextures), - [](const TextureWeakPointer& p) { return p.lock(); }); - } - - size_t totalVariableMemoryAllocation = 0; - size_t idealMemoryAllocation = 0; - bool canDemote = false; - bool canPromote = false; - bool hasTransfers = false; - for (const auto& texture : strongTextures) { - // Race conditions can still leave nulls in the list, so we need to check - if (!texture) { - continue; - } - GL45VariableAllocationTexture* object = Backend::getGPUObject(*texture); - // Track how much the texture thinks it should be using - idealMemoryAllocation += texture->evalTotalSize(); - // Track how much we're actually using - totalVariableMemoryAllocation += object->size(); - canDemote |= object->canDemote(); - canPromote |= object->canPromote(); - hasTransfers |= object->hasPendingTransfers(); - } - - size_t unallocated = idealMemoryAllocation - totalVariableMemoryAllocation; - float pressure = (float)totalVariableMemoryAllocation / (float)allowedMemoryAllocation; - - auto newState = MemoryPressureState::Idle; - if (pressure > OVERSUBSCRIBED_PRESSURE_VALUE && canDemote) { - newState = MemoryPressureState::Oversubscribed; - } else if (pressure < UNDERSUBSCRIBED_PRESSURE_VALUE && unallocated != 0 && canPromote) { - newState = MemoryPressureState::Undersubscribed; - } else if (hasTransfers) { - newState = MemoryPressureState::Transfer; - } - - if (newState != _memoryPressureState) { -#if THREADED_TEXTURE_BUFFERING - if (MemoryPressureState::Transfer == _memoryPressureState) { - TransferJob::stopTransferLoop(); - } - _memoryPressureState = newState; - if (MemoryPressureState::Transfer == _memoryPressureState) { - TransferJob::startTransferLoop(); - } -#else - _memoryPressureState = newState; -#endif - // Clear the existing queue - _transferQueue = WorkQueue(); - _promoteQueue = WorkQueue(); - _demoteQueue = WorkQueue(); - - // Populate the existing textures into the queue - for (const auto& texture : strongTextures) { - addToWorkQueue(texture); - } - } -} - -void GL45VariableAllocationTexture::processWorkQueues() { - if (MemoryPressureState::Idle == _memoryPressureState) { - return; - } - - auto& workQueue = getActiveWorkQueue(); - PROFILE_RANGE(render_gpu_gl, __FUNCTION__); - while (!workQueue.empty()) { - auto workTarget = workQueue.top(); - workQueue.pop(); - auto texture = workTarget.first.lock(); - if (!texture) { - continue; - } - - // Grab the first item off the demote queue - GL45VariableAllocationTexture* object = Backend::getGPUObject(*texture); - if (MemoryPressureState::Oversubscribed == _memoryPressureState) { - if (!object->canDemote()) { - continue; - } - object->demote(); - } else if (MemoryPressureState::Undersubscribed == _memoryPressureState) { - if (!object->canPromote()) { - continue; - } - object->promote(); - } else if (MemoryPressureState::Transfer == _memoryPressureState) { - if (!object->hasPendingTransfers()) { - continue; - } - object->executeNextTransfer(texture); - } else { - Q_UNREACHABLE(); - } - - // Reinject into the queue if more work to be done - addToWorkQueue(texture); - break; - } - - if (workQueue.empty()) { - _memoryPressureStateStale = true; - } -} - -void GL45VariableAllocationTexture::manageMemory() { - PROFILE_RANGE(render_gpu_gl, __FUNCTION__); - updateMemoryPressure(); - processWorkQueues(); -} - -size_t GL45VariableAllocationTexture::_frameTexturesCreated { 0 }; - -GL45VariableAllocationTexture::GL45VariableAllocationTexture(const std::weak_ptr& backend, const Texture& texture) : GL45Texture(backend, texture) { - ++_frameTexturesCreated; -} - -GL45VariableAllocationTexture::~GL45VariableAllocationTexture() { - _memoryPressureStateStale = true; - Backend::updateTextureGPUMemoryUsage(_size, 0); -} - -void GL45VariableAllocationTexture::executeNextTransfer(const TexturePointer& currentTexture) { - if (_populatedMip <= _allocatedMip) { - return; - } - - if (_pendingTransfers.empty()) { - populateTransferQueue(); - } - - if (!_pendingTransfers.empty()) { - // Keeping hold of a strong pointer during the transfer ensures that the transfer thread cannot try to access a destroyed texture - _currentTransferTexture = currentTexture; - if (_pendingTransfers.front()->tryTransfer()) { - _pendingTransfers.pop(); - _currentTransferTexture.reset(); - } - } -} - -// Managed size resource textures -using GL45ResourceTexture = GL45Backend::GL45ResourceTexture; - -GL45ResourceTexture::GL45ResourceTexture(const std::weak_ptr& backend, const Texture& texture) : GL45VariableAllocationTexture(backend, texture) { - auto mipLevels = texture.evalNumMips(); - _allocatedMip = mipLevels; - uvec3 mipDimensions; - for (uint16_t mip = 0; mip < mipLevels; ++mip) { - if (glm::all(glm::lessThanEqual(texture.evalMipDimensions(mip), INITIAL_MIP_TRANSFER_DIMENSIONS))) { - _maxAllocatedMip = _populatedMip = mip; - break; - } - } - - uint16_t allocatedMip = _populatedMip - std::min(_populatedMip, 2); - allocateStorage(allocatedMip); - _memoryPressureStateStale = true; - copyMipsFromTexture(); - syncSampler(); - -} - -void GL45ResourceTexture::allocateStorage(uint16 allocatedMip) { - _allocatedMip = allocatedMip; - const GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat()); - const auto dimensions = _gpuObject.evalMipDimensions(_allocatedMip); - const auto totalMips = _gpuObject.evalNumMips(); - const auto mips = totalMips - _allocatedMip; - glTextureStorage2D(_id, mips, texelFormat.internalFormat, dimensions.x, dimensions.y); - auto mipLevels = _gpuObject.evalNumMips(); - _size = 0; - for (uint16_t mip = _allocatedMip; mip < mipLevels; ++mip) { - _size += _gpuObject.evalMipSize(mip); - } - Backend::updateTextureGPUMemoryUsage(0, _size); - -} - -void GL45ResourceTexture::copyMipsFromTexture() { - auto mipLevels = _gpuObject.evalNumMips(); - size_t maxFace = GLTexture::getFaceCount(_target); - for (uint16_t sourceMip = _populatedMip; sourceMip < mipLevels; ++sourceMip) { - uint16_t targetMip = sourceMip - _allocatedMip; - for (uint8_t face = 0; face < maxFace; ++face) { - copyMipFaceFromTexture(sourceMip, targetMip, face); - } - } -} - -void GL45ResourceTexture::syncSampler() const { - Parent::syncSampler(); - glTextureParameteri(_id, GL_TEXTURE_BASE_LEVEL, _populatedMip - _allocatedMip); -} - -void GL45ResourceTexture::promote() { - PROFILE_RANGE(render_gpu_gl, __FUNCTION__); - Q_ASSERT(_allocatedMip > 0); - GLuint oldId = _id; - uint32_t oldSize = _size; - // create new texture - const_cast(_id) = allocate(_gpuObject); - uint16_t oldAllocatedMip = _allocatedMip; - // allocate storage for new level - allocateStorage(_allocatedMip - std::min(_allocatedMip, 2)); - uint16_t mips = _gpuObject.evalNumMips(); - // copy pre-existing mips - for (uint16_t mip = _populatedMip; mip < mips; ++mip) { - auto mipDimensions = _gpuObject.evalMipDimensions(mip); - uint16_t targetMip = mip - _allocatedMip; - uint16_t sourceMip = mip - oldAllocatedMip; - auto faces = getFaceCount(_target); - for (uint8_t face = 0; face < faces; ++face) { - glCopyImageSubData( - oldId, _target, sourceMip, 0, 0, face, - _id, _target, targetMip, 0, 0, face, - mipDimensions.x, mipDimensions.y, 1 - ); - (void)CHECK_GL_ERROR(); - } - } - // destroy the old texture - glDeleteTextures(1, &oldId); - // update the memory usage - Backend::updateTextureGPUMemoryUsage(oldSize, 0); - _memoryPressureStateStale = true; - syncSampler(); - populateTransferQueue(); -} - -void GL45ResourceTexture::demote() { - PROFILE_RANGE(render_gpu_gl, __FUNCTION__); - Q_ASSERT(_allocatedMip < _maxAllocatedMip); - auto oldId = _id; - auto oldSize = _size; - const_cast(_id) = allocate(_gpuObject); - allocateStorage(_allocatedMip + 1); - _populatedMip = std::max(_populatedMip, _allocatedMip); - uint16_t mips = _gpuObject.evalNumMips(); - // copy pre-existing mips - for (uint16_t mip = _populatedMip; mip < mips; ++mip) { - auto mipDimensions = _gpuObject.evalMipDimensions(mip); - uint16_t targetMip = mip - _allocatedMip; - uint16_t sourceMip = targetMip + 1; - auto faces = getFaceCount(_target); - for (uint8_t face = 0; face < faces; ++face) { - glCopyImageSubData( - oldId, _target, sourceMip, 0, 0, face, - _id, _target, targetMip, 0, 0, face, - mipDimensions.x, mipDimensions.y, 1 - ); - (void)CHECK_GL_ERROR(); - } - } - // destroy the old texture - glDeleteTextures(1, &oldId); - // update the memory usage - Backend::updateTextureGPUMemoryUsage(oldSize, 0); - _memoryPressureStateStale = true; - syncSampler(); - populateTransferQueue(); -} - - -void GL45ResourceTexture::populateTransferQueue() { - PROFILE_RANGE(render_gpu_gl, __FUNCTION__); - if (_populatedMip <= _allocatedMip) { - return; - } - _pendingTransfers = TransferQueue(); - - const uint8_t maxFace = GLTexture::getFaceCount(_target); - uint16_t sourceMip = _populatedMip; - do { - --sourceMip; - auto targetMip = sourceMip - _allocatedMip; - auto mipDimensions = _gpuObject.evalMipDimensions(sourceMip); - for (uint8_t face = 0; face < maxFace; ++face) { - if (!_gpuObject.isStoredMipFaceAvailable(sourceMip, face)) { - continue; - } - - // If the mip is less than the max transfer size, then just do it in one transfer - if (glm::all(glm::lessThanEqual(mipDimensions, MAX_TRANSFER_DIMENSIONS))) { - // Can the mip be transferred in one go - _pendingTransfers.emplace(new TransferJob(*this, sourceMip, targetMip, face)); - continue; - } - - // break down the transfers into chunks so that no single transfer is - // consuming more than X bandwidth - auto mipData = _gpuObject.accessStoredMipFace(sourceMip, face); - const auto lines = mipDimensions.y; - auto bytesPerLine = (uint32_t)mipData->getSize() / lines; - Q_ASSERT(0 == (mipData->getSize() % lines)); - uint32_t linesPerTransfer = (uint32_t)(MAX_TRANSFER_SIZE / bytesPerLine); - uint32_t lineOffset = 0; - while (lineOffset < lines) { - uint32_t linesToCopy = std::min(lines - lineOffset, linesPerTransfer); - _pendingTransfers.emplace(new TransferJob(*this, sourceMip, targetMip, face, linesToCopy, lineOffset)); - lineOffset += linesToCopy; - } - } - - // queue up the sampler and populated mip change for after the transfer has completed - _pendingTransfers.emplace(new TransferJob(*this, [=] { - _populatedMip = sourceMip; - syncSampler(); - })); - } while (sourceMip != _allocatedMip); -} - -// Sparsely allocated, managed size resource textures -#if 0 -#define SPARSE_PAGE_SIZE_OVERHEAD_ESTIMATE 1.3f - -using GL45SparseResourceTexture = GL45Backend::GL45SparseResourceTexture; - -GL45Texture::PageDimensionsMap GL45Texture::pageDimensionsByFormat; -Mutex GL45Texture::pageDimensionsMutex; - -GL45Texture::PageDimensions GL45Texture::getPageDimensionsForFormat(const TextureTypeFormat& typeFormat) { - { - Lock lock(pageDimensionsMutex); - if (pageDimensionsByFormat.count(typeFormat)) { - return pageDimensionsByFormat[typeFormat]; - } - } - - GLint count = 0; - glGetInternalformativ(typeFormat.first, typeFormat.second, GL_NUM_VIRTUAL_PAGE_SIZES_ARB, 1, &count); - - std::vector result; - if (count > 0) { - std::vector x, y, z; - x.resize(count); - glGetInternalformativ(typeFormat.first, typeFormat.second, GL_VIRTUAL_PAGE_SIZE_X_ARB, 1, &x[0]); - y.resize(count); - glGetInternalformativ(typeFormat.first, typeFormat.second, GL_VIRTUAL_PAGE_SIZE_Y_ARB, 1, &y[0]); - z.resize(count); - glGetInternalformativ(typeFormat.first, typeFormat.second, GL_VIRTUAL_PAGE_SIZE_Z_ARB, 1, &z[0]); - - result.resize(count); - for (GLint i = 0; i < count; ++i) { - result[i] = uvec3(x[i], y[i], z[i]); - } - } - - { - Lock lock(pageDimensionsMutex); - if (0 == pageDimensionsByFormat.count(typeFormat)) { - pageDimensionsByFormat[typeFormat] = result; - } - } - - return result; -} - -GL45Texture::PageDimensions GL45Texture::getPageDimensionsForFormat(GLenum target, GLenum format) { - return getPageDimensionsForFormat({ target, format }); -} -bool GL45Texture::isSparseEligible(const Texture& texture) { - Q_ASSERT(TextureUsageType::RESOURCE == texture.getUsageType()); - - // Disabling sparse for the momemnt - return false; - - const auto allowedPageDimensions = getPageDimensionsForFormat(getGLTextureType(texture), - gl::GLTexelFormat::evalGLTexelFormatInternal(texture.getTexelFormat())); - const auto textureDimensions = texture.getDimensions(); - for (const auto& pageDimensions : allowedPageDimensions) { - if (uvec3(0) == (textureDimensions % pageDimensions)) { - return true; - } - } - - return false; -} - - -GL45SparseResourceTexture::GL45SparseResourceTexture(const std::weak_ptr& backend, const Texture& texture) : GL45VariableAllocationTexture(backend, texture) { - const GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat()); - const uvec3 dimensions = _gpuObject.getDimensions(); - auto allowedPageDimensions = getPageDimensionsForFormat(_target, texelFormat.internalFormat); - uint32_t pageDimensionsIndex = 0; - // In order to enable sparse the texture size must be an integer multiple of the page size - for (size_t i = 0; i < allowedPageDimensions.size(); ++i) { - pageDimensionsIndex = (uint32_t)i; - _pageDimensions = allowedPageDimensions[i]; - // Is this texture an integer multiple of page dimensions? - if (uvec3(0) == (dimensions % _pageDimensions)) { - qCDebug(gpugl45logging) << "Enabling sparse for texture " << _gpuObject.source().c_str(); - break; - } - } - glTextureParameteri(_id, GL_TEXTURE_SPARSE_ARB, GL_TRUE); - glTextureParameteri(_id, GL_VIRTUAL_PAGE_SIZE_INDEX_ARB, pageDimensionsIndex); - glGetTextureParameterIuiv(_id, GL_NUM_SPARSE_LEVELS_ARB, &_maxSparseLevel); - - _pageBytes = _gpuObject.getTexelFormat().getSize(); - _pageBytes *= _pageDimensions.x * _pageDimensions.y * _pageDimensions.z; - // Testing with a simple texture allocating app shows an estimated 20% GPU memory overhead for - // sparse textures as compared to non-sparse, so we acount for that here. - _pageBytes = (uint32_t)(_pageBytes * SPARSE_PAGE_SIZE_OVERHEAD_ESTIMATE); - - //allocateStorage(); - syncSampler(); -} - -GL45SparseResourceTexture::~GL45SparseResourceTexture() { - Backend::updateTextureGPUVirtualMemoryUsage(size(), 0); -} - -uvec3 GL45SparseResourceTexture::getPageCounts(const uvec3& dimensions) const { - auto result = (dimensions / _pageDimensions) + - glm::clamp(dimensions % _pageDimensions, glm::uvec3(0), glm::uvec3(1)); - return result; -} - -uint32_t GL45SparseResourceTexture::getPageCount(const uvec3& dimensions) const { - auto pageCounts = getPageCounts(dimensions); - return pageCounts.x * pageCounts.y * pageCounts.z; -} - -void GL45SparseResourceTexture::promote() { -} - -void GL45SparseResourceTexture::demote() { -} - -SparseInfo::SparseInfo(GL45Texture& texture) - : texture(texture) { -} - -void SparseInfo::maybeMakeSparse() { - // Don't enable sparse for objects with explicitly managed mip levels - if (!texture._gpuObject.isAutogenerateMips()) { - return; - } - - const uvec3 dimensions = texture._gpuObject.getDimensions(); - auto allowedPageDimensions = getPageDimensionsForFormat(texture._target, texture._internalFormat); - // In order to enable sparse the texture size must be an integer multiple of the page size - for (size_t i = 0; i < allowedPageDimensions.size(); ++i) { - pageDimensionsIndex = (uint32_t)i; - pageDimensions = allowedPageDimensions[i]; - // Is this texture an integer multiple of page dimensions? - if (uvec3(0) == (dimensions % pageDimensions)) { - qCDebug(gpugl45logging) << "Enabling sparse for texture " << texture._source.c_str(); - sparse = true; - break; - } - } - - if (sparse) { - glTextureParameteri(texture._id, GL_TEXTURE_SPARSE_ARB, GL_TRUE); - glTextureParameteri(texture._id, GL_VIRTUAL_PAGE_SIZE_INDEX_ARB, pageDimensionsIndex); - } else { - qCDebug(gpugl45logging) << "Size " << dimensions.x << " x " << dimensions.y << - " is not supported by any sparse page size for texture" << texture._source.c_str(); - } -} - - -// This can only be called after we've established our storage size -void SparseInfo::update() { - if (!sparse) { - return; - } - glGetTextureParameterIuiv(texture._id, GL_NUM_SPARSE_LEVELS_ARB, &maxSparseLevel); - - for (uint16_t mipLevel = 0; mipLevel <= maxSparseLevel; ++mipLevel) { - auto mipDimensions = texture._gpuObject.evalMipDimensions(mipLevel); - auto mipPageCount = getPageCount(mipDimensions); - maxPages += mipPageCount; - } - if (texture._target == GL_TEXTURE_CUBE_MAP) { - maxPages *= GLTexture::CUBE_NUM_FACES; - } -} - - -void SparseInfo::allocateToMip(uint16_t targetMip) { - // Not sparse, do nothing - if (!sparse) { - return; - } - - if (allocatedMip == INVALID_MIP) { - allocatedMip = maxSparseLevel + 1; - } - - // Don't try to allocate below the maximum sparse level - if (targetMip > maxSparseLevel) { - targetMip = maxSparseLevel; - } - - // Already allocated this level - if (allocatedMip <= targetMip) { - return; - } - - uint32_t maxFace = (uint32_t)(GL_TEXTURE_CUBE_MAP == texture._target ? CUBE_NUM_FACES : 1); - for (uint16_t mip = targetMip; mip < allocatedMip; ++mip) { - auto size = texture._gpuObject.evalMipDimensions(mip); - glTexturePageCommitmentEXT(texture._id, mip, 0, 0, 0, size.x, size.y, maxFace, GL_TRUE); - allocatedPages += getPageCount(size); - } - allocatedMip = targetMip; -} - -uint32_t SparseInfo::getSize() const { - return allocatedPages * pageBytes; -} -using SparseInfo = GL45Backend::GL45Texture::SparseInfo; - -void GL45Texture::updateSize() const { - if (_gpuObject.getTexelFormat().isCompressed()) { - qFatal("Compressed textures not yet supported"); - } - - if (_transferrable && _sparseInfo.sparse) { - auto size = _sparseInfo.getSize(); - Backend::updateTextureGPUSparseMemoryUsage(_size, size); - setSize(size); - } else { - setSize(_gpuObject.evalTotalSize(_mipOffset)); - } -} - -void GL45Texture::startTransfer() { - Parent::startTransfer(); - _sparseInfo.update(); - _populatedMip = _maxMip + 1; -} - -bool GL45Texture::continueTransfer() { - size_t maxFace = GL_TEXTURE_CUBE_MAP == _target ? CUBE_NUM_FACES : 1; - if (_populatedMip == _minMip) { - return false; - } - - uint16_t targetMip = _populatedMip - 1; - while (targetMip > 0 && !_gpuObject.isStoredMipFaceAvailable(targetMip)) { - --targetMip; - } - - _sparseInfo.allocateToMip(targetMip); - for (uint8_t face = 0; face < maxFace; ++face) { - auto size = _gpuObject.evalMipDimensions(targetMip); - if (_gpuObject.isStoredMipFaceAvailable(targetMip, face)) { - auto mip = _gpuObject.accessStoredMipFace(targetMip, face); - GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat(), mip->getFormat()); - if (GL_TEXTURE_2D == _target) { - glTextureSubImage2D(_id, targetMip, 0, 0, size.x, size.y, texelFormat.format, texelFormat.type, mip->readData()); - } else if (GL_TEXTURE_CUBE_MAP == _target) { - // DSA ARB does not work on AMD, so use EXT - // unless EXT is not available on the driver - if (glTextureSubImage2DEXT) { - auto target = CUBE_FACE_LAYOUT[face]; - glTextureSubImage2DEXT(_id, target, targetMip, 0, 0, size.x, size.y, texelFormat.format, texelFormat.type, mip->readData()); - } else { - glTextureSubImage3D(_id, targetMip, 0, 0, face, size.x, size.y, 1, texelFormat.format, texelFormat.type, mip->readData()); - } - } else { - Q_ASSERT(false); - } - (void)CHECK_GL_ERROR(); - break; - } - } - _populatedMip = targetMip; - return _populatedMip != _minMip; -} - -void GL45Texture::finishTransfer() { - Parent::finishTransfer(); -} - -void GL45Texture::postTransfer() { - Parent::postTransfer(); -} - -void GL45Texture::stripToMip(uint16_t newMinMip) { - if (newMinMip < _minMip) { - qCWarning(gpugl45logging) << "Cannot decrease the min mip"; - return; - } - - if (_sparseInfo.sparse && newMinMip > _sparseInfo.maxSparseLevel) { - qCWarning(gpugl45logging) << "Cannot increase the min mip into the mip tail"; - return; - } - - // If we weren't generating mips before, we need to now that we're stripping down mip levels. - if (!_gpuObject.isAutogenerateMips()) { - qCDebug(gpugl45logging) << "Force mip generation for texture"; - glGenerateTextureMipmap(_id); - } - - - uint8_t maxFace = (uint8_t)((_target == GL_TEXTURE_CUBE_MAP) ? GLTexture::CUBE_NUM_FACES : 1); - if (_sparseInfo.sparse) { - for (uint16_t mip = _minMip; mip < newMinMip; ++mip) { - auto id = _id; - auto mipDimensions = _gpuObject.evalMipDimensions(mip); - glTexturePageCommitmentEXT(id, mip, 0, 0, 0, mipDimensions.x, mipDimensions.y, maxFace, GL_FALSE); - auto deallocatedPages = _sparseInfo.getPageCount(mipDimensions) * maxFace; - assert(deallocatedPages < _sparseInfo.allocatedPages); - _sparseInfo.allocatedPages -= deallocatedPages; - } - _minMip = newMinMip; - } else { - GLuint oldId = _id; - // Find the distance between the old min mip and the new one - uint16 mipDelta = newMinMip - _minMip; - _mipOffset += mipDelta; - const_cast(_maxMip) -= mipDelta; - auto newLevels = usedMipLevels(); - - // Create and setup the new texture (allocate) - glCreateTextures(_target, 1, &const_cast(_id)); - glTextureParameteri(_id, GL_TEXTURE_BASE_LEVEL, 0); - glTextureParameteri(_id, GL_TEXTURE_MAX_LEVEL, _maxMip - _minMip); - Vec3u newDimensions = _gpuObject.evalMipDimensions(_mipOffset); - glTextureStorage2D(_id, newLevels, _internalFormat, newDimensions.x, newDimensions.y); - - // Copy the contents of the old texture to the new - GLuint fbo { 0 }; - glCreateFramebuffers(1, &fbo); - glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo); - for (uint16 targetMip = _minMip; targetMip <= _maxMip; ++targetMip) { - uint16 sourceMip = targetMip + mipDelta; - Vec3u mipDimensions = _gpuObject.evalMipDimensions(targetMip + _mipOffset); - for (GLenum target : getFaceTargets(_target)) { - glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, target, oldId, sourceMip); - (void)CHECK_GL_ERROR(); - glCopyTextureSubImage2D(_id, targetMip, 0, 0, 0, 0, mipDimensions.x, mipDimensions.y); - (void)CHECK_GL_ERROR(); - } - } - glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); - glDeleteFramebuffers(1, &fbo); - glDeleteTextures(1, &oldId); - } - - // Re-sync the sampler to force access to the new mip level - syncSampler(); - updateSize(); -} - -bool GL45Texture::derezable() const { - if (_external) { - return false; - } - auto maxMinMip = _sparseInfo.sparse ? _sparseInfo.maxSparseLevel : _maxMip; - return _transferrable && (_targetMinMip < maxMinMip); -} - -size_t GL45Texture::getMipByteCount(uint16_t mip) const { - if (!_sparseInfo.sparse) { - return Parent::getMipByteCount(mip); - } - - auto dimensions = _gpuObject.evalMipDimensions(_targetMinMip); - return _sparseInfo.getPageCount(dimensions) * _sparseInfo.pageBytes; -} - -std::pair GL45Texture::preDerez() { - assert(!_sparseInfo.sparse || _targetMinMip < _sparseInfo.maxSparseLevel); - size_t freedMemory = getMipByteCount(_targetMinMip); - bool liveMip = _populatedMip != INVALID_MIP && _populatedMip <= _targetMinMip; - ++_targetMinMip; - return { freedMemory, liveMip }; -} - -void GL45Texture::derez() { - if (_sparseInfo.sparse) { - assert(_minMip < _sparseInfo.maxSparseLevel); - } - assert(_minMip < _maxMip); - assert(_transferrable); - stripToMip(_minMip + 1); -} - -size_t GL45Texture::getCurrentGpuSize() const { - if (!_sparseInfo.sparse) { - return Parent::getCurrentGpuSize(); - } - - return _sparseInfo.getSize(); -} - -size_t GL45Texture::getTargetGpuSize() const { - if (!_sparseInfo.sparse) { - return Parent::getTargetGpuSize(); - } - - size_t result = 0; - for (auto mip = _targetMinMip; mip <= _sparseInfo.maxSparseLevel; ++mip) { - result += (_sparseInfo.pageBytes * _sparseInfo.getPageCount(_gpuObject.evalMipDimensions(mip))); - } - - return result; -} - -GL45Texture::~GL45Texture() { - if (_sparseInfo.sparse) { - uint8_t maxFace = (uint8_t)((_target == GL_TEXTURE_CUBE_MAP) ? GLTexture::CUBE_NUM_FACES : 1); - auto maxSparseMip = std::min(_maxMip, _sparseInfo.maxSparseLevel); - for (uint16_t mipLevel = _minMip; mipLevel <= maxSparseMip; ++mipLevel) { - auto mipDimensions = _gpuObject.evalMipDimensions(mipLevel); - glTexturePageCommitmentEXT(_texture, mipLevel, 0, 0, 0, mipDimensions.x, mipDimensions.y, maxFace, GL_FALSE); - auto deallocatedPages = _sparseInfo.getPageCount(mipDimensions) * maxFace; - assert(deallocatedPages <= _sparseInfo.allocatedPages); - _sparseInfo.allocatedPages -= deallocatedPages; - } - - if (0 != _sparseInfo.allocatedPages) { - qCWarning(gpugl45logging) << "Allocated pages remaining " << _id << " " << _sparseInfo.allocatedPages; - } - Backend::decrementTextureGPUSparseCount(); - } -} -GL45Texture::GL45Texture(const std::weak_ptr& backend, const Texture& texture) - : GLTexture(backend, texture, allocate(texture)), _sparseInfo(*this), _targetMinMip(_minMip) -{ - - auto theBackend = _backend.lock(); - if (_transferrable && theBackend && theBackend->isTextureManagementSparseEnabled()) { - _sparseInfo.maybeMakeSparse(); - if (_sparseInfo.sparse) { - Backend::incrementTextureGPUSparseCount(); - } - } -} -#endif diff --git a/libraries/gpu/CMakeLists.txt b/libraries/gpu/CMakeLists.txt index 207431d8c7..384c5709ee 100644 --- a/libraries/gpu/CMakeLists.txt +++ b/libraries/gpu/CMakeLists.txt @@ -1,6 +1,6 @@ set(TARGET_NAME gpu) autoscribe_shader_lib(gpu) setup_hifi_library() -link_hifi_libraries(shared ktx) +link_hifi_libraries(shared) target_nsight() diff --git a/libraries/gpu/src/gpu/Batch.cpp b/libraries/gpu/src/gpu/Batch.cpp index f822da129b..c15da61800 100644 --- a/libraries/gpu/src/gpu/Batch.cpp +++ b/libraries/gpu/src/gpu/Batch.cpp @@ -292,8 +292,15 @@ void Batch::setUniformBuffer(uint32 slot, const BufferView& view) { setUniformBuffer(slot, view._buffer, view._offset, view._size); } + void Batch::setResourceTexture(uint32 slot, const TexturePointer& texture) { + if (texture && texture->getUsage().isExternal()) { + auto recycler = texture->getExternalRecycler(); + Q_ASSERT(recycler); + } + ADD_COMMAND(setResourceTexture); + _params.emplace_back(_textures.cache(texture)); _params.emplace_back(slot); } diff --git a/libraries/gpu/src/gpu/Buffer.h b/libraries/gpu/src/gpu/Buffer.h index 290b84bef0..2507e8e0a6 100644 --- a/libraries/gpu/src/gpu/Buffer.h +++ b/libraries/gpu/src/gpu/Buffer.h @@ -198,7 +198,7 @@ public: BufferView(const BufferPointer& buffer, Size offset, Size size, const Element& element = DEFAULT_ELEMENT); BufferView(const BufferPointer& buffer, Size offset, Size size, uint16 stride, const Element& element = DEFAULT_ELEMENT); - Size getNumElements() const { return (_size - _offset) / _stride; } + Size getNumElements() const { return _size / _element.getSize(); } //Template iterator with random access on the buffer sysmem template diff --git a/libraries/gpu/src/gpu/Context.cpp b/libraries/gpu/src/gpu/Context.cpp index cc570f696f..78b472bdae 100644 --- a/libraries/gpu/src/gpu/Context.cpp +++ b/libraries/gpu/src/gpu/Context.cpp @@ -241,7 +241,6 @@ std::atomic Context::_bufferGPUMemoryUsage { 0 }; std::atomic Context::_textureGPUCount{ 0 }; std::atomic Context::_textureGPUSparseCount { 0 }; -std::atomic Context::_textureTransferPendingSize { 0 }; std::atomic Context::_textureGPUMemoryUsage { 0 }; std::atomic Context::_textureGPUVirtualMemoryUsage { 0 }; std::atomic Context::_textureGPUFramebufferMemoryUsage { 0 }; @@ -318,17 +317,6 @@ void Context::decrementTextureGPUSparseCount() { --_textureGPUSparseCount; } -void Context::updateTextureTransferPendingSize(Size prevObjectSize, Size newObjectSize) { - if (prevObjectSize == newObjectSize) { - return; - } - if (newObjectSize > prevObjectSize) { - _textureTransferPendingSize.fetch_add(newObjectSize - prevObjectSize); - } else { - _textureTransferPendingSize.fetch_sub(prevObjectSize - newObjectSize); - } -} - void Context::updateTextureGPUMemoryUsage(Size prevObjectSize, Size newObjectSize) { if (prevObjectSize == newObjectSize) { return; @@ -402,10 +390,6 @@ uint32_t Context::getTextureGPUSparseCount() { return _textureGPUSparseCount.load(); } -Context::Size Context::getTextureTransferPendingSize() { - return _textureTransferPendingSize.load(); -} - Context::Size Context::getTextureGPUMemoryUsage() { return _textureGPUMemoryUsage.load(); } @@ -435,7 +419,6 @@ void Backend::incrementTextureGPUCount() { Context::incrementTextureGPUCount(); void Backend::decrementTextureGPUCount() { Context::decrementTextureGPUCount(); } void Backend::incrementTextureGPUSparseCount() { Context::incrementTextureGPUSparseCount(); } void Backend::decrementTextureGPUSparseCount() { Context::decrementTextureGPUSparseCount(); } -void Backend::updateTextureTransferPendingSize(Resource::Size prevObjectSize, Resource::Size newObjectSize) { Context::updateTextureTransferPendingSize(prevObjectSize, newObjectSize); } void Backend::updateTextureGPUMemoryUsage(Resource::Size prevObjectSize, Resource::Size newObjectSize) { Context::updateTextureGPUMemoryUsage(prevObjectSize, newObjectSize); } void Backend::updateTextureGPUVirtualMemoryUsage(Resource::Size prevObjectSize, Resource::Size newObjectSize) { Context::updateTextureGPUVirtualMemoryUsage(prevObjectSize, newObjectSize); } void Backend::updateTextureGPUFramebufferMemoryUsage(Resource::Size prevObjectSize, Resource::Size newObjectSize) { Context::updateTextureGPUFramebufferMemoryUsage(prevObjectSize, newObjectSize); } diff --git a/libraries/gpu/src/gpu/Context.h b/libraries/gpu/src/gpu/Context.h index 102c754cd7..01c841992d 100644 --- a/libraries/gpu/src/gpu/Context.h +++ b/libraries/gpu/src/gpu/Context.h @@ -101,7 +101,6 @@ public: static void decrementTextureGPUCount(); static void incrementTextureGPUSparseCount(); static void decrementTextureGPUSparseCount(); - static void updateTextureTransferPendingSize(Resource::Size prevObjectSize, Resource::Size newObjectSize); static void updateTextureGPUMemoryUsage(Resource::Size prevObjectSize, Resource::Size newObjectSize); static void updateTextureGPUSparseMemoryUsage(Resource::Size prevObjectSize, Resource::Size newObjectSize); static void updateTextureGPUVirtualMemoryUsage(Resource::Size prevObjectSize, Resource::Size newObjectSize); @@ -221,7 +220,6 @@ public: static uint32_t getTextureGPUSparseCount(); static Size getFreeGPUMemory(); static Size getUsedGPUMemory(); - static Size getTextureTransferPendingSize(); static Size getTextureGPUMemoryUsage(); static Size getTextureGPUVirtualMemoryUsage(); static Size getTextureGPUFramebufferMemoryUsage(); @@ -265,7 +263,6 @@ protected: static void decrementTextureGPUCount(); static void incrementTextureGPUSparseCount(); static void decrementTextureGPUSparseCount(); - static void updateTextureTransferPendingSize(Size prevObjectSize, Size newObjectSize); static void updateTextureGPUMemoryUsage(Size prevObjectSize, Size newObjectSize); static void updateTextureGPUSparseMemoryUsage(Size prevObjectSize, Size newObjectSize); static void updateTextureGPUVirtualMemoryUsage(Size prevObjectSize, Size newObjectSize); @@ -282,7 +279,6 @@ protected: static std::atomic _textureGPUCount; static std::atomic _textureGPUSparseCount; - static std::atomic _textureTransferPendingSize; static std::atomic _textureGPUMemoryUsage; static std::atomic _textureGPUSparseMemoryUsage; static std::atomic _textureGPUVirtualMemoryUsage; diff --git a/libraries/gpu/src/gpu/Format.cpp b/libraries/gpu/src/gpu/Format.cpp index de202911e3..2a8185bf94 100644 --- a/libraries/gpu/src/gpu/Format.cpp +++ b/libraries/gpu/src/gpu/Format.cpp @@ -10,15 +10,8 @@ using namespace gpu; -const Element Element::COLOR_R_8 { SCALAR, NUINT8, RED }; -const Element Element::COLOR_SR_8 { SCALAR, NUINT8, SRED }; - const Element Element::COLOR_RGBA_32{ VEC4, NUINT8, RGBA }; const Element Element::COLOR_SRGBA_32{ VEC4, NUINT8, SRGBA }; - -const Element Element::COLOR_BGRA_32{ VEC4, NUINT8, BGRA }; -const Element Element::COLOR_SBGRA_32{ VEC4, NUINT8, SBGRA }; - const Element Element::COLOR_R11G11B10{ SCALAR, FLOAT, R11G11B10 }; const Element Element::VEC4F_COLOR_RGBA{ VEC4, FLOAT, RGBA }; const Element Element::VEC2F_UV{ VEC2, FLOAT, UV }; diff --git a/libraries/gpu/src/gpu/Format.h b/libraries/gpu/src/gpu/Format.h index 493a2de3c2..13809a41e6 100644 --- a/libraries/gpu/src/gpu/Format.h +++ b/libraries/gpu/src/gpu/Format.h @@ -133,7 +133,6 @@ static const int SCALAR_COUNT[NUM_DIMENSIONS] = { enum Semantic { RAW = 0, // used as RAW memory - RED, RGB, RGBA, BGRA, @@ -150,7 +149,6 @@ enum Semantic { STENCIL, // Stencil only buffer DEPTH_STENCIL, // Depth Stencil buffer - SRED, SRGB, SRGBA, SBGRA, @@ -229,12 +227,8 @@ public: return getRaw() != right.getRaw(); } - static const Element COLOR_R_8; - static const Element COLOR_SR_8; static const Element COLOR_RGBA_32; static const Element COLOR_SRGBA_32; - static const Element COLOR_BGRA_32; - static const Element COLOR_SBGRA_32; static const Element COLOR_R11G11B10; static const Element VEC4F_COLOR_RGBA; static const Element VEC2F_UV; diff --git a/libraries/gpu/src/gpu/Framebuffer.cpp b/libraries/gpu/src/gpu/Framebuffer.cpp index 0d3291a74d..e8ccfce3b2 100755 --- a/libraries/gpu/src/gpu/Framebuffer.cpp +++ b/libraries/gpu/src/gpu/Framebuffer.cpp @@ -32,7 +32,7 @@ Framebuffer* Framebuffer::create(const std::string& name) { Framebuffer* Framebuffer::create(const std::string& name, const Format& colorBufferFormat, uint16 width, uint16 height) { auto framebuffer = Framebuffer::create(name); - auto colorTexture = TexturePointer(Texture::createRenderBuffer(colorBufferFormat, width, height, Sampler(Sampler::FILTER_MIN_MAG_POINT))); + auto colorTexture = TexturePointer(Texture::create2D(colorBufferFormat, width, height, Sampler(Sampler::FILTER_MIN_MAG_POINT))); colorTexture->setSource("Framebuffer::colorTexture"); framebuffer->setRenderBuffer(0, colorTexture); @@ -43,8 +43,8 @@ Framebuffer* Framebuffer::create(const std::string& name, const Format& colorBuf Framebuffer* Framebuffer::create(const std::string& name, const Format& colorBufferFormat, const Format& depthStencilBufferFormat, uint16 width, uint16 height) { auto framebuffer = Framebuffer::create(name); - auto colorTexture = TexturePointer(Texture::createRenderBuffer(colorBufferFormat, width, height, Sampler(Sampler::FILTER_MIN_MAG_POINT))); - auto depthTexture = TexturePointer(Texture::createRenderBuffer(depthStencilBufferFormat, width, height, Sampler(Sampler::FILTER_MIN_MAG_POINT))); + auto colorTexture = TexturePointer(Texture::create2D(colorBufferFormat, width, height, Sampler(Sampler::FILTER_MIN_MAG_POINT))); + auto depthTexture = TexturePointer(Texture::create2D(depthStencilBufferFormat, width, height, Sampler(Sampler::FILTER_MIN_MAG_POINT))); framebuffer->setRenderBuffer(0, colorTexture); framebuffer->setDepthStencilBuffer(depthTexture, depthStencilBufferFormat); @@ -55,7 +55,7 @@ Framebuffer* Framebuffer::createShadowmap(uint16 width) { auto framebuffer = Framebuffer::create("Shadowmap"); auto depthFormat = Element(gpu::SCALAR, gpu::FLOAT, gpu::DEPTH); // Depth32 texel format - auto depthTexture = TexturePointer(Texture::createRenderBuffer(depthFormat, width, width)); + auto depthTexture = TexturePointer(Texture::create2D(depthFormat, width, width)); Sampler::Desc samplerDesc; samplerDesc._borderColor = glm::vec4(1.0f); samplerDesc._wrapModeU = Sampler::WRAP_BORDER; @@ -143,8 +143,6 @@ int Framebuffer::setRenderBuffer(uint32 slot, const TexturePointer& texture, uin return -1; } - Q_ASSERT(!texture || TextureUsageType::RENDERBUFFER == texture->getUsageType()); - // Check for the slot if (slot >= getMaxNumRenderBuffers()) { return -1; @@ -224,8 +222,6 @@ bool Framebuffer::setDepthStencilBuffer(const TexturePointer& texture, const For return false; } - Q_ASSERT(!texture || TextureUsageType::RENDERBUFFER == texture->getUsageType()); - // Check for the compatibility of size if (texture) { if (!validateTargetCompatibility(*texture)) { diff --git a/libraries/gpu/src/gpu/Texture.cpp b/libraries/gpu/src/gpu/Texture.cpp index 1f66b2900e..5b0c4c876a 100755 --- a/libraries/gpu/src/gpu/Texture.cpp +++ b/libraries/gpu/src/gpu/Texture.cpp @@ -19,7 +19,6 @@ #include #include -#include #include #include "GPULogging.h" @@ -89,10 +88,6 @@ uint32_t Texture::getTextureGPUSparseCount() { return Context::getTextureGPUSparseCount(); } -Texture::Size Texture::getTextureTransferPendingSize() { - return Context::getTextureTransferPendingSize(); -} - Texture::Size Texture::getTextureGPUMemoryUsage() { return Context::getTextureGPUMemoryUsage(); } @@ -125,23 +120,62 @@ void Texture::setAllowedGPUMemoryUsage(Size size) { uint8 Texture::NUM_FACES_PER_TYPE[NUM_TYPES] = { 1, 1, 1, 6 }; -using Storage = Texture::Storage; -using PixelsPointer = Texture::PixelsPointer; -using MemoryStorage = Texture::MemoryStorage; +Texture::Pixels::Pixels(const Element& format, Size size, const Byte* bytes) : + _format(format), + _sysmem(size, bytes), + _isGPULoaded(false) { + Texture::updateTextureCPUMemoryUsage(0, _sysmem.getSize()); +} -void Storage::assignTexture(Texture* texture) { +Texture::Pixels::~Pixels() { + Texture::updateTextureCPUMemoryUsage(_sysmem.getSize(), 0); +} + +Texture::Size Texture::Pixels::resize(Size pSize) { + auto prevSize = _sysmem.getSize(); + auto newSize = _sysmem.resize(pSize); + Texture::updateTextureCPUMemoryUsage(prevSize, newSize); + return newSize; +} + +Texture::Size Texture::Pixels::setData(const Element& format, Size size, const Byte* bytes ) { + _format = format; + auto prevSize = _sysmem.getSize(); + auto newSize = _sysmem.setData(size, bytes); + Texture::updateTextureCPUMemoryUsage(prevSize, newSize); + _isGPULoaded = false; + return newSize; +} + +void Texture::Pixels::notifyGPULoaded() { + _isGPULoaded = true; + auto prevSize = _sysmem.getSize(); + auto newSize = _sysmem.resize(0); + Texture::updateTextureCPUMemoryUsage(prevSize, newSize); +} + +void Texture::Storage::assignTexture(Texture* texture) { _texture = texture; if (_texture) { _type = _texture->getType(); } } -void MemoryStorage::reset() { +void Texture::Storage::reset() { _mips.clear(); bumpStamp(); } -PixelsPointer MemoryStorage::getMipFace(uint16 level, uint8 face) const { +Texture::PixelsPointer Texture::Storage::editMipFace(uint16 level, uint8 face) { + if (level < _mips.size()) { + assert(face < _mips[level].size()); + bumpStamp(); + return _mips[level][face]; + } + return PixelsPointer(); +} + +const Texture::PixelsPointer Texture::Storage::getMipFace(uint16 level, uint8 face) const { if (level < _mips.size()) { assert(face < _mips[level].size()); return _mips[level][face]; @@ -149,12 +183,20 @@ PixelsPointer MemoryStorage::getMipFace(uint16 level, uint8 face) const { return PixelsPointer(); } -bool MemoryStorage::isMipAvailable(uint16 level, uint8 face) const { +void Texture::Storage::notifyMipFaceGPULoaded(uint16 level, uint8 face) const { + PixelsPointer mipFace = getMipFace(level, face); + // Free the mips + if (mipFace) { + mipFace->notifyGPULoaded(); + } +} + +bool Texture::Storage::isMipAvailable(uint16 level, uint8 face) const { PixelsPointer mipFace = getMipFace(level, face); return (mipFace && mipFace->getSize()); } -bool MemoryStorage::allocateMip(uint16 level) { +bool Texture::Storage::allocateMip(uint16 level) { bool changed = false; if (level >= _mips.size()) { _mips.resize(level+1, std::vector(Texture::NUM_FACES_PER_TYPE[getType()])); @@ -164,6 +206,7 @@ bool MemoryStorage::allocateMip(uint16 level) { auto& mip = _mips[level]; for (auto& face : mip) { if (!face) { + face = std::make_shared(); changed = true; } } @@ -173,7 +216,7 @@ bool MemoryStorage::allocateMip(uint16 level) { return changed; } -void MemoryStorage::assignMipData(uint16 level, const storage::StoragePointer& storagePointer) { +bool Texture::Storage::assignMipData(uint16 level, const Element& format, Size size, const Byte* bytes) { allocateMip(level); auto& mip = _mips[level]; @@ -182,63 +225,64 @@ void MemoryStorage::assignMipData(uint16 level, const storage::StoragePointer& s // The bytes assigned here are supposed to contain all the faces bytes of the mip. // For tex1D, 2D, 3D there is only one face // For Cube, we expect the 6 faces in the order X+, X-, Y+, Y-, Z+, Z- - auto sizePerFace = storagePointer->size() / mip.size(); - size_t offset = 0; + auto sizePerFace = size / mip.size(); + auto faceBytes = bytes; + Size allocated = 0; for (auto& face : mip) { - face = storagePointer->createView(sizePerFace, offset); - offset += sizePerFace; + allocated += face->setData(format, sizePerFace, faceBytes); + faceBytes += sizePerFace; } bumpStamp(); + + return allocated == size; } -void Texture::MemoryStorage::assignMipFaceData(uint16 level, uint8 face, const storage::StoragePointer& storagePointer) { +bool Texture::Storage::assignMipFaceData(uint16 level, const Element& format, Size size, const Byte* bytes, uint8 face) { + allocateMip(level); - auto& mip = _mips[level]; + auto mip = _mips[level]; + Size allocated = 0; if (face < mip.size()) { - mip[face] = storagePointer; + auto mipFace = mip[face]; + allocated += mipFace->setData(format, size, bytes); bumpStamp(); } + + return allocated == size; } -Texture* Texture::createExternal(const ExternalRecycler& recycler, const Sampler& sampler) { - Texture* tex = new Texture(TextureUsageType::EXTERNAL); +Texture* Texture::createExternal2D(const ExternalRecycler& recycler, const Sampler& sampler) { + Texture* tex = new Texture(); tex->_type = TEX_2D; tex->_maxMip = 0; tex->_sampler = sampler; + tex->setUsage(Usage::Builder().withExternal().withColor()); tex->setExternalRecycler(recycler); return tex; } -Texture* Texture::createRenderBuffer(const Element& texelFormat, uint16 width, uint16 height, const Sampler& sampler) { - return create(TextureUsageType::RENDERBUFFER, TEX_2D, texelFormat, width, height, 1, 1, 0, sampler); -} - Texture* Texture::create1D(const Element& texelFormat, uint16 width, const Sampler& sampler) { - return create(TextureUsageType::RESOURCE, TEX_1D, texelFormat, width, 1, 1, 1, 0, sampler); + return create(TEX_1D, texelFormat, width, 1, 1, 1, 1, sampler); } Texture* Texture::create2D(const Element& texelFormat, uint16 width, uint16 height, const Sampler& sampler) { - return create(TextureUsageType::RESOURCE, TEX_2D, texelFormat, width, height, 1, 1, 0, sampler); -} - -Texture* Texture::createStrict(const Element& texelFormat, uint16 width, uint16 height, const Sampler& sampler) { - return create(TextureUsageType::STRICT_RESOURCE, TEX_2D, texelFormat, width, height, 1, 1, 0, sampler); + return create(TEX_2D, texelFormat, width, height, 1, 1, 1, sampler); } Texture* Texture::create3D(const Element& texelFormat, uint16 width, uint16 height, uint16 depth, const Sampler& sampler) { - return create(TextureUsageType::RESOURCE, TEX_3D, texelFormat, width, height, depth, 1, 0, sampler); + return create(TEX_3D, texelFormat, width, height, depth, 1, 1, sampler); } Texture* Texture::createCube(const Element& texelFormat, uint16 width, const Sampler& sampler) { - return create(TextureUsageType::RESOURCE, TEX_CUBE, texelFormat, width, width, 1, 1, 0, sampler); + return create(TEX_CUBE, texelFormat, width, width, 1, 1, 1, sampler); } -Texture* Texture::create(TextureUsageType usageType, Type type, const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numSamples, uint16 numSlices, const Sampler& sampler) +Texture* Texture::create(Type type, const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numSamples, uint16 numSlices, const Sampler& sampler) { - Texture* tex = new Texture(usageType); - tex->_storage.reset(new MemoryStorage()); + Texture* tex = new Texture(); + tex->_storage.reset(new Storage()); tex->_type = type; tex->_storage->assignTexture(tex); tex->_maxMip = 0; @@ -249,14 +293,16 @@ Texture* Texture::create(TextureUsageType usageType, Type type, const Element& t return tex; } -Texture::Texture(TextureUsageType usageType) : - Resource(), _usageType(usageType) { +Texture::Texture(): + Resource() +{ _textureCPUCount++; } -Texture::~Texture() { +Texture::~Texture() +{ _textureCPUCount--; - if (_usageType == TextureUsageType::EXTERNAL) { + if (getUsage().isExternal()) { Texture::ExternalUpdates externalUpdates; { Lock lock(_externalMutex); @@ -275,7 +321,7 @@ Texture::~Texture() { } Texture::Size Texture::resize(Type type, const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numSamples, uint16 numSlices) { - if (width && height && depth && numSamples) { + if (width && height && depth && numSamples && numSlices) { bool changed = false; if ( _type != type) { @@ -336,20 +382,20 @@ Texture::Size Texture::resize(Type type, const Element& texelFormat, uint16 widt } Texture::Size Texture::resize1D(uint16 width, uint16 numSamples) { - return resize(TEX_1D, getTexelFormat(), width, 1, 1, numSamples, 0); + return resize(TEX_1D, getTexelFormat(), width, 1, 1, numSamples, 1); } Texture::Size Texture::resize2D(uint16 width, uint16 height, uint16 numSamples) { - return resize(TEX_2D, getTexelFormat(), width, height, 1, numSamples, 0); + return resize(TEX_2D, getTexelFormat(), width, height, 1, numSamples, 1); } Texture::Size Texture::resize3D(uint16 width, uint16 height, uint16 depth, uint16 numSamples) { - return resize(TEX_3D, getTexelFormat(), width, height, depth, numSamples, 0); + return resize(TEX_3D, getTexelFormat(), width, height, depth, numSamples, 1); } Texture::Size Texture::resizeCube(uint16 width, uint16 numSamples) { - return resize(TEX_CUBE, getTexelFormat(), width, 1, 1, numSamples, 0); + return resize(TEX_CUBE, getTexelFormat(), width, 1, 1, numSamples, 1); } Texture::Size Texture::reformat(const Element& texelFormat) { - return resize(_type, texelFormat, getWidth(), getHeight(), getDepth(), getNumSamples(), _numSlices); + return resize(_type, texelFormat, getWidth(), getHeight(), getDepth(), getNumSamples(), getNumSlices()); } bool Texture::isColorRenderTarget() const { @@ -380,83 +426,69 @@ uint16 Texture::evalNumMips() const { return evalNumMips({ _width, _height, _depth }); } -void Texture::setStoredMipFormat(const Element& format) { - _storage->setFormat(format); -} - -const Element& Texture::getStoredMipFormat() const { - return _storage->getFormat(); -} - -void Texture::assignStoredMip(uint16 level, Size size, const Byte* bytes) { - storage::StoragePointer storage = std::make_shared(size, bytes); - assignStoredMip(level, storage); -} - -void Texture::assignStoredMipFace(uint16 level, uint8 face, Size size, const Byte* bytes) { - storage::StoragePointer storage = std::make_shared(size, bytes); - assignStoredMipFace(level, face, storage); -} - -void Texture::assignStoredMip(uint16 level, storage::StoragePointer& storage) { +bool Texture::assignStoredMip(uint16 level, const Element& format, Size size, const Byte* bytes) { // Check that level accessed make sense if (level != 0) { if (_autoGenerateMips) { - return; + return false; } if (level >= evalNumMips()) { - return; + return false; } } // THen check that the mem texture passed make sense with its format - Size expectedSize = evalStoredMipSize(level, getStoredMipFormat()); - auto size = storage->size(); - if (storage->size() == expectedSize) { - _storage->assignMipData(level, storage); - _maxMip = std::max(_maxMip, level); - _stamp++; - } else if (size > expectedSize) { - // NOTE: We are facing this case sometime because apparently QImage (from where we get the bits) is generating images - // and alligning the line of pixels to 32 bits. - // We should probably consider something a bit more smart to get the correct result but for now (UI elements) - // it seems to work... - _storage->assignMipData(level, storage); - _maxMip = std::max(_maxMip, level); - _stamp++; - } -} - -void Texture::assignStoredMipFace(uint16 level, uint8 face, storage::StoragePointer& storage) { - // Check that level accessed make sense - if (level != 0) { - if (_autoGenerateMips) { - return; - } - if (level >= evalNumMips()) { - return; - } - } - - // THen check that the mem texture passed make sense with its format - Size expectedSize = evalStoredMipFaceSize(level, getStoredMipFormat()); - auto size = storage->size(); + Size expectedSize = evalStoredMipSize(level, format); if (size == expectedSize) { - _storage->assignMipFaceData(level, face, storage); + _storage->assignMipData(level, format, size, bytes); _maxMip = std::max(_maxMip, level); _stamp++; + return true; } else if (size > expectedSize) { // NOTE: We are facing this case sometime because apparently QImage (from where we get the bits) is generating images // and alligning the line of pixels to 32 bits. // We should probably consider something a bit more smart to get the correct result but for now (UI elements) // it seems to work... - _storage->assignMipFaceData(level, face, storage); + _storage->assignMipData(level, format, size, bytes); _maxMip = std::max(_maxMip, level); _stamp++; + return true; } + + return false; } +bool Texture::assignStoredMipFace(uint16 level, const Element& format, Size size, const Byte* bytes, uint8 face) { + // Check that level accessed make sense + if (level != 0) { + if (_autoGenerateMips) { + return false; + } + if (level >= evalNumMips()) { + return false; + } + } + + // THen check that the mem texture passed make sense with its format + Size expectedSize = evalStoredMipFaceSize(level, format); + if (size == expectedSize) { + _storage->assignMipFaceData(level, format, size, bytes, face); + _stamp++; + return true; + } else if (size > expectedSize) { + // NOTE: We are facing this case sometime because apparently QImage (from where we get the bits) is generating images + // and alligning the line of pixels to 32 bits. + // We should probably consider something a bit more smart to get the correct result but for now (UI elements) + // it seems to work... + _storage->assignMipFaceData(level, format, size, bytes, face); + _stamp++; + return true; + } + + return false; +} + uint16 Texture::autoGenerateMips(uint16 maxMip) { bool changed = false; if (!_autoGenerateMips) { @@ -490,7 +522,7 @@ uint16 Texture::getStoredMipHeight(uint16 level) const { if (mip && mip->getSize()) { return evalMipHeight(level); } - return 0; + return 0; } uint16 Texture::getStoredMipDepth(uint16 level) const { @@ -762,16 +794,7 @@ bool sphericalHarmonicsFromTexture(const gpu::Texture& cubeTexture, std::vector< for(int face=0; face < gpu::Texture::NUM_CUBE_FACES; face++) { PROFILE_RANGE(render_gpu, "ProcessFace"); - auto mipFormat = cubeTexture.getStoredMipFormat(); - auto numComponents = mipFormat.getScalarCount(); - int roffset { 0 }; - int goffset { 1 }; - int boffset { 2 }; - if ((mipFormat.getSemantic() == gpu::BGRA) || (mipFormat.getSemantic() == gpu::SBGRA)) { - roffset = 2; - boffset = 0; - } - + auto numComponents = cubeTexture.accessStoredMipFace(0,face)->getFormat().getScalarCount(); auto data = cubeTexture.accessStoredMipFace(0,face)->readData(); if (data == nullptr) { continue; @@ -859,9 +882,9 @@ bool sphericalHarmonicsFromTexture(const gpu::Texture& cubeTexture, std::vector< for (int i = 0; i < stride; ++i) { for (int j = 0; j < stride; ++j) { int k = (int)(x + i - halfStride + (y + j - halfStride) * width) * numComponents; - red += ColorUtils::sRGB8ToLinearFloat(data[k + roffset]); - green += ColorUtils::sRGB8ToLinearFloat(data[k + goffset]); - blue += ColorUtils::sRGB8ToLinearFloat(data[k + boffset]); + red += ColorUtils::sRGB8ToLinearFloat(data[k]); + green += ColorUtils::sRGB8ToLinearFloat(data[k + 1]); + blue += ColorUtils::sRGB8ToLinearFloat(data[k + 2]); } } glm::vec3 clr(red, green, blue); @@ -888,6 +911,8 @@ bool sphericalHarmonicsFromTexture(const gpu::Texture& cubeTexture, std::vector< // save result for(uint i=0; i < sqOrder; i++) { + // gamma Correct + // output[i] = linearTosRGB(glm::vec3(resultR[i], resultG[i], resultB[i])); output[i] = glm::vec3(resultR[i], resultG[i], resultB[i]); } @@ -976,7 +1001,3 @@ Texture::ExternalUpdates Texture::getUpdates() const { } return result; } - -void Texture::setStorage(std::unique_ptr& newStorage) { - _storage.swap(newStorage); -} diff --git a/libraries/gpu/src/gpu/Texture.h b/libraries/gpu/src/gpu/Texture.h index f7297b3280..856bd4983d 100755 --- a/libraries/gpu/src/gpu/Texture.h +++ b/libraries/gpu/src/gpu/Texture.h @@ -17,17 +17,9 @@ #include #include -#include - #include "Forward.h" #include "Resource.h" -namespace ktx { - class KTX; - using KTXUniquePointer = std::unique_ptr; - struct Header; -} - namespace gpu { // THe spherical harmonics is a nice tool for cubemap, so if required, the irradiance SH can be automatically generated @@ -143,18 +135,10 @@ public: uint8 getMinMip() const { return _desc._minMip; } uint8 getMaxMip() const { return _desc._maxMip; } - const Desc& getDesc() const { return _desc; } protected: Desc _desc; }; -enum class TextureUsageType { - RENDERBUFFER, // Used as attachments to a framebuffer - RESOURCE, // Resource textures, like materials... subject to memory manipulation - STRICT_RESOURCE, // Resource textures not subject to manipulation, like the normal fitting texture - EXTERNAL, -}; - class Texture : public Resource { static std::atomic _textureCPUCount; static std::atomic _textureCPUMemoryUsage; @@ -163,12 +147,10 @@ class Texture : public Resource { static void updateTextureCPUMemoryUsage(Size prevObjectSize, Size newObjectSize); public: - static const uint32_t CUBE_FACE_COUNT { 6 }; static uint32_t getTextureCPUCount(); static Size getTextureCPUMemoryUsage(); static uint32_t getTextureGPUCount(); static uint32_t getTextureGPUSparseCount(); - static Size getTextureTransferPendingSize(); static Size getTextureGPUMemoryUsage(); static Size getTextureGPUVirtualMemoryUsage(); static Size getTextureGPUFramebufferMemoryUsage(); @@ -191,9 +173,9 @@ public: NORMAL, // Texture is a normal map ALPHA, // Texture has an alpha channel ALPHA_MASK, // Texture alpha channel is a Mask 0/1 + EXTERNAL, NUM_FLAGS, }; - typedef std::bitset Flags; // The key is the Flags @@ -217,6 +199,7 @@ public: Builder& withNormal() { _flags.set(NORMAL); return (*this); } Builder& withAlpha() { _flags.set(ALPHA); return (*this); } Builder& withAlphaMask() { _flags.set(ALPHA_MASK); return (*this); } + Builder& withExternal() { _flags.set(EXTERNAL); return (*this); } }; Usage(const Builder& builder) : Usage(builder._flags) {} @@ -225,12 +208,37 @@ public: bool isAlpha() const { return _flags[ALPHA]; } bool isAlphaMask() const { return _flags[ALPHA_MASK]; } + bool isExternal() const { return _flags[EXTERNAL]; } + bool operator==(const Usage& usage) { return (_flags == usage._flags); } bool operator!=(const Usage& usage) { return (_flags != usage._flags); } }; - using PixelsPointer = storage::StoragePointer; + class Pixels { + public: + Pixels() {} + Pixels(const Pixels& pixels) = default; + Pixels(const Element& format, Size size, const Byte* bytes); + ~Pixels(); + + const Byte* readData() const { return _sysmem.readData(); } + Size getSize() const { return _sysmem.getSize(); } + Size resize(Size pSize); + Size setData(const Element& format, Size size, const Byte* bytes ); + + const Element& getFormat() const { return _format; } + + void notifyGPULoaded(); + + protected: + Element _format; + Sysmem _sysmem; + bool _isGPULoaded; + + friend class Texture; + }; + typedef std::shared_ptr< Pixels > PixelsPointer; enum Type { TEX_1D = 0, @@ -253,78 +261,46 @@ public: NUM_CUBE_FACES, // Not a valid vace index }; - class Storage { public: Storage() {} virtual ~Storage() {} + virtual void reset(); + virtual PixelsPointer editMipFace(uint16 level, uint8 face = 0); + virtual const PixelsPointer getMipFace(uint16 level, uint8 face = 0) const; + virtual bool allocateMip(uint16 level); + virtual bool assignMipData(uint16 level, const Element& format, Size size, const Byte* bytes); + virtual bool assignMipFaceData(uint16 level, const Element& format, Size size, const Byte* bytes, uint8 face); + virtual bool isMipAvailable(uint16 level, uint8 face = 0) const; - virtual void reset() = 0; - virtual PixelsPointer getMipFace(uint16 level, uint8 face = 0) const = 0; - virtual void assignMipData(uint16 level, const storage::StoragePointer& storage) = 0; - virtual void assignMipFaceData(uint16 level, uint8 face, const storage::StoragePointer& storage) = 0; - virtual bool isMipAvailable(uint16 level, uint8 face = 0) const = 0; Texture::Type getType() const { return _type; } - + Stamp getStamp() const { return _stamp; } Stamp bumpStamp() { return ++_stamp; } + protected: + Stamp _stamp = 0; + Texture* _texture = nullptr; // Points to the parent texture (not owned) + Texture::Type _type = Texture::TEX_2D; // The type of texture is needed to know the number of faces to expect + std::vector> _mips; // an array of mips, each mip is an array of faces - void setFormat(const Element& format) { _format = format; } - const Element& getFormat() const { return _format; } - - private: - Stamp _stamp { 0 }; - Element _format; - Texture::Type _type { Texture::TEX_2D }; // The type of texture is needed to know the number of faces to expect - Texture* _texture { nullptr }; // Points to the parent texture (not owned) virtual void assignTexture(Texture* tex); // Texture storage is pointing to ONE corrresponding Texture. const Texture* getTexture() const { return _texture; } + friend class Texture; + + // THis should be only called by the Texture from the Backend to notify the storage that the specified mip face pixels + // have been uploaded to the GPU memory. IT is possible for the storage to free the system memory then + virtual void notifyMipFaceGPULoaded(uint16 level, uint8 face) const; }; - class MemoryStorage : public Storage { - public: - void reset() override; - PixelsPointer getMipFace(uint16 level, uint8 face = 0) const override; - void assignMipData(uint16 level, const storage::StoragePointer& storage) override; - void assignMipFaceData(uint16 level, uint8 face, const storage::StoragePointer& storage) override; - bool isMipAvailable(uint16 level, uint8 face = 0) const override; - - protected: - bool allocateMip(uint16 level); - std::vector> _mips; // an array of mips, each mip is an array of faces - }; - - class KtxStorage : public Storage { - public: - KtxStorage(ktx::KTXUniquePointer& ktxData); - PixelsPointer getMipFace(uint16 level, uint8 face = 0) const override; - // By convention, all mip levels and faces MUST be populated when using KTX backing - bool isMipAvailable(uint16 level, uint8 face = 0) const override { return true; } - - void assignMipData(uint16 level, const storage::StoragePointer& storage) override { - throw std::runtime_error("Invalid call"); - } - - void assignMipFaceData(uint16 level, uint8 face, const storage::StoragePointer& storage) override { - throw std::runtime_error("Invalid call"); - } - void reset() override { } - - protected: - ktx::KTXUniquePointer _ktxData; - friend class Texture; - }; - + static Texture* create1D(const Element& texelFormat, uint16 width, const Sampler& sampler = Sampler()); static Texture* create2D(const Element& texelFormat, uint16 width, uint16 height, const Sampler& sampler = Sampler()); static Texture* create3D(const Element& texelFormat, uint16 width, uint16 height, uint16 depth, const Sampler& sampler = Sampler()); static Texture* createCube(const Element& texelFormat, uint16 width, const Sampler& sampler = Sampler()); - static Texture* createRenderBuffer(const Element& texelFormat, uint16 width, uint16 height, const Sampler& sampler = Sampler()); - static Texture* createStrict(const Element& texelFormat, uint16 width, uint16 height, const Sampler& sampler = Sampler()); - static Texture* createExternal(const ExternalRecycler& recycler, const Sampler& sampler = Sampler()); + static Texture* createExternal2D(const ExternalRecycler& recycler, const Sampler& sampler = Sampler()); - Texture(TextureUsageType usageType); + Texture(); Texture(const Texture& buf); // deep copy of the sysmem texture Texture& operator=(const Texture& buf); // deep copy of the sysmem texture ~Texture(); @@ -349,7 +325,6 @@ public: // Size and format Type getType() const { return _type; } - TextureUsageType getUsageType() const { return _usageType; } bool isColorRenderTarget() const; bool isDepthStencilRenderTarget() const; @@ -372,12 +347,7 @@ public: uint32 getNumTexels() const { return _width * _height * _depth * getNumFaces(); } - // The texture is an array if the _numSlices is not 0. - // otherwise, if _numSLices is 0, then the texture is NOT an array - // The number of slices returned is 1 at the minimum (if not an array) or the actual _numSlices. - bool isArray() const { return _numSlices > 0; } - uint16 getNumSlices() const { return (isArray() ? _numSlices : 1); } - + uint16 getNumSlices() const { return _numSlices; } uint16 getNumSamples() const { return _numSamples; } @@ -459,29 +429,18 @@ public: // Managing Storage and mips - // Mip storage format is constant across all mips - void setStoredMipFormat(const Element& format); - const Element& getStoredMipFormat() const; - // Manually allocate the mips down until the specified maxMip // this is just allocating the sysmem version of it // in case autoGen is on, this doesn't allocate // Explicitely assign mip data for a certain level // If Bytes is NULL then simply allocate the space so mip sysmem can be accessed - - void assignStoredMip(uint16 level, Size size, const Byte* bytes); - void assignStoredMipFace(uint16 level, uint8 face, Size size, const Byte* bytes); - - void assignStoredMip(uint16 level, storage::StoragePointer& storage); - void assignStoredMipFace(uint16 level, uint8 face, storage::StoragePointer& storage); + bool assignStoredMip(uint16 level, const Element& format, Size size, const Byte* bytes); + bool assignStoredMipFace(uint16 level, const Element& format, Size size, const Byte* bytes, uint8 face); // Access the the sub mips bool isStoredMipFaceAvailable(uint16 level, uint8 face = 0) const { return _storage->isMipAvailable(level, face); } const PixelsPointer accessStoredMipFace(uint16 level, uint8 face = 0) const { return _storage->getMipFace(level, face); } - void setStorage(std::unique_ptr& newStorage); - void setKtxBacking(ktx::KTXUniquePointer& newBacking); - // access sizes for the stored mips uint16 getStoredMipWidth(uint16 level) const; uint16 getStoredMipHeight(uint16 level) const; @@ -505,8 +464,8 @@ public: const Sampler& getSampler() const { return _sampler; } Stamp getSamplerStamp() const { return _samplerStamp; } - void setFallbackTexture(const TexturePointer& fallback) { _fallback = fallback; } - TexturePointer getFallbackTexture() const { return _fallback.lock(); } + // Only callable by the Backend + void notifyMipFaceGPULoaded(uint16 level, uint8 face = 0) const { return _storage->notifyMipFaceGPULoaded(level, face); } void setExternalTexture(uint32 externalId, void* externalFence); void setExternalRecycler(const ExternalRecycler& recycler); @@ -516,45 +475,36 @@ public: ExternalUpdates getUpdates() const; - // Textures can be serialized directly to ktx data file, here is how - static ktx::KTXUniquePointer serialize(const Texture& texture); - static Texture* unserialize(const ktx::KTXUniquePointer& srcData, TextureUsageType usageType = TextureUsageType::RESOURCE, Usage usage = Usage(), const Sampler::Desc& sampler = Sampler::Desc()); - static bool evalKTXFormat(const Element& mipFormat, const Element& texelFormat, ktx::Header& header); - static bool evalTextureFormat(const ktx::Header& header, Element& mipFormat, Element& texelFormat); - protected: - const TextureUsageType _usageType; - // Should only be accessed internally or by the backend sync function mutable Mutex _externalMutex; mutable std::list _externalUpdates; ExternalRecycler _externalRecycler; - std::weak_ptr _fallback; // Not strictly necessary, but incredibly useful for debugging std::string _source; std::unique_ptr< Storage > _storage; - Stamp _stamp { 0 }; + Stamp _stamp = 0; Sampler _sampler; - Stamp _samplerStamp { 0 }; + Stamp _samplerStamp; - uint32 _size { 0 }; + uint32 _size = 0; Element _texelFormat; - uint16 _width { 1 }; - uint16 _height { 1 }; - uint16 _depth { 1 }; + uint16 _width = 1; + uint16 _height = 1; + uint16 _depth = 1; - uint16 _numSamples { 1 }; - uint16 _numSlices { 0 }; // if _numSlices is 0, the texture is not an "Array", the getNumSlices reported is 1 + uint16 _numSamples = 1; + uint16 _numSlices = 1; uint16 _maxMip { 0 }; uint16 _minMip { 0 }; - Type _type { TEX_1D }; + Type _type = TEX_1D; Usage _usage; @@ -563,7 +513,7 @@ protected: bool _isIrradianceValid = false; bool _defined = false; - static Texture* create(TextureUsageType usageType, Type type, const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numSamples, uint16 numSlices, const Sampler& sampler); + static Texture* create(Type type, const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numSamples, uint16 numSlices, const Sampler& sampler); Size resize(Type type, const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numSamples, uint16 numSlices); }; diff --git a/libraries/gpu/src/gpu/Texture_ktx.cpp b/libraries/gpu/src/gpu/Texture_ktx.cpp deleted file mode 100644 index 5f0ededee7..0000000000 --- a/libraries/gpu/src/gpu/Texture_ktx.cpp +++ /dev/null @@ -1,289 +0,0 @@ -// -// Texture_ktx.cpp -// libraries/gpu/src/gpu -// -// Created by Sam Gateau on 2/16/2017. -// Copyright 2014 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - - -#include "Texture.h" - -#include -using namespace gpu; - -using PixelsPointer = Texture::PixelsPointer; -using KtxStorage = Texture::KtxStorage; - -struct GPUKTXPayload { - Sampler::Desc _samplerDesc; - Texture::Usage _usage; - TextureUsageType _usageType; - - static std::string KEY; - static bool isGPUKTX(const ktx::KeyValue& val) { - return (val._key.compare(KEY) == 0); - } - - static bool findInKeyValues(const ktx::KeyValues& keyValues, GPUKTXPayload& payload) { - auto found = std::find_if(keyValues.begin(), keyValues.end(), isGPUKTX); - if (found != keyValues.end()) { - if ((*found)._value.size() == sizeof(GPUKTXPayload)) { - memcpy(&payload, (*found)._value.data(), sizeof(GPUKTXPayload)); - return true; - } - } - return false; - } -}; - -std::string GPUKTXPayload::KEY { "hifi.gpu" }; - -KtxStorage::KtxStorage(ktx::KTXUniquePointer& ktxData) { - - // if the source ktx is valid let's config this KtxStorage correctly - if (ktxData && ktxData->getHeader()) { - - // now that we know the ktx, let's get the header info to configure this Texture::Storage: - Format mipFormat = Format::COLOR_BGRA_32; - Format texelFormat = Format::COLOR_SRGBA_32; - if (Texture::evalTextureFormat(*ktxData->getHeader(), mipFormat, texelFormat)) { - _format = mipFormat; - } - - - } - - _ktxData.reset(ktxData.release()); -} - -PixelsPointer KtxStorage::getMipFace(uint16 level, uint8 face) const { - return _ktxData->getMipFaceTexelsData(level, face); -} - -void Texture::setKtxBacking(ktx::KTXUniquePointer& ktxBacking) { - auto newBacking = std::unique_ptr(new KtxStorage(ktxBacking)); - setStorage(newBacking); -} - -ktx::KTXUniquePointer Texture::serialize(const Texture& texture) { - ktx::Header header; - - // From texture format to ktx format description - auto texelFormat = texture.getTexelFormat(); - auto mipFormat = texture.getStoredMipFormat(); - - if (!Texture::evalKTXFormat(mipFormat, texelFormat, header)) { - return nullptr; - } - - // Set Dimensions - uint32_t numFaces = 1; - switch (texture.getType()) { - case TEX_1D: { - if (texture.isArray()) { - header.set1DArray(texture.getWidth(), texture.getNumSlices()); - } else { - header.set1D(texture.getWidth()); - } - break; - } - case TEX_2D: { - if (texture.isArray()) { - header.set2DArray(texture.getWidth(), texture.getHeight(), texture.getNumSlices()); - } else { - header.set2D(texture.getWidth(), texture.getHeight()); - } - break; - } - case TEX_3D: { - if (texture.isArray()) { - header.set3DArray(texture.getWidth(), texture.getHeight(), texture.getDepth(), texture.getNumSlices()); - } else { - header.set3D(texture.getWidth(), texture.getHeight(), texture.getDepth()); - } - break; - } - case TEX_CUBE: { - if (texture.isArray()) { - header.setCubeArray(texture.getWidth(), texture.getHeight(), texture.getNumSlices()); - } else { - header.setCube(texture.getWidth(), texture.getHeight()); - } - numFaces = Texture::CUBE_FACE_COUNT; - break; - } - default: - return nullptr; - } - - // Number level of mips coming - header.numberOfMipmapLevels = texture.maxMip() + 1; - - ktx::Images images; - for (uint32_t level = 0; level < header.numberOfMipmapLevels; level++) { - auto mip = texture.accessStoredMipFace(level); - if (mip) { - if (numFaces == 1) { - images.emplace_back(ktx::Image((uint32_t)mip->getSize(), 0, mip->readData())); - } else { - ktx::Image::FaceBytes cubeFaces(Texture::CUBE_FACE_COUNT); - cubeFaces[0] = mip->readData(); - for (uint32_t face = 1; face < Texture::CUBE_FACE_COUNT; face++) { - cubeFaces[face] = texture.accessStoredMipFace(level, face)->readData(); - } - images.emplace_back(ktx::Image((uint32_t)mip->getSize(), 0, cubeFaces)); - } - } - } - - GPUKTXPayload keyval; - keyval._samplerDesc = texture.getSampler().getDesc(); - keyval._usage = texture.getUsage(); - keyval._usageType = texture.getUsageType(); - ktx::KeyValues keyValues; - keyValues.emplace_back(ktx::KeyValue(GPUKTXPayload::KEY, sizeof(GPUKTXPayload), (ktx::Byte*) &keyval)); - - auto ktxBuffer = ktx::KTX::create(header, images, keyValues); -#if 0 - auto expectedMipCount = texture.evalNumMips(); - assert(expectedMipCount == ktxBuffer->_images.size()); - assert(expectedMipCount == header.numberOfMipmapLevels); - - assert(0 == memcmp(&header, ktxBuffer->getHeader(), sizeof(ktx::Header))); - assert(ktxBuffer->_images.size() == images.size()); - auto start = ktxBuffer->_storage->data(); - for (size_t i = 0; i < images.size(); ++i) { - auto expected = images[i]; - auto actual = ktxBuffer->_images[i]; - assert(expected._padding == actual._padding); - assert(expected._numFaces == actual._numFaces); - assert(expected._imageSize == actual._imageSize); - assert(expected._faceSize == actual._faceSize); - assert(actual._faceBytes.size() == actual._numFaces); - for (uint32_t face = 0; face < expected._numFaces; ++face) { - auto expectedFace = expected._faceBytes[face]; - auto actualFace = actual._faceBytes[face]; - auto offset = actualFace - start; - assert(offset % 4 == 0); - assert(expectedFace != actualFace); - assert(0 == memcmp(expectedFace, actualFace, expected._faceSize)); - } - } -#endif - return ktxBuffer; -} - -Texture* Texture::unserialize(const ktx::KTXUniquePointer& srcData, TextureUsageType usageType, Usage usage, const Sampler::Desc& sampler) { - if (!srcData) { - return nullptr; - } - const auto& header = *srcData->getHeader(); - - Format mipFormat = Format::COLOR_BGRA_32; - Format texelFormat = Format::COLOR_SRGBA_32; - - if (!Texture::evalTextureFormat(header, mipFormat, texelFormat)) { - return nullptr; - } - - // Find Texture Type based on dimensions - Type type = TEX_1D; - if (header.pixelWidth == 0) { - return nullptr; - } else if (header.pixelHeight == 0) { - type = TEX_1D; - } else if (header.pixelDepth == 0) { - if (header.numberOfFaces == ktx::NUM_CUBEMAPFACES) { - type = TEX_CUBE; - } else { - type = TEX_2D; - } - } else { - type = TEX_3D; - } - - - // If found, use the - GPUKTXPayload gpuktxKeyValue; - bool isGPUKTXPayload = GPUKTXPayload::findInKeyValues(srcData->_keyValues, gpuktxKeyValue); - - auto tex = Texture::create( (isGPUKTXPayload ? gpuktxKeyValue._usageType : usageType), - type, - texelFormat, - header.getPixelWidth(), - header.getPixelHeight(), - header.getPixelDepth(), - 1, // num Samples - header.getNumberOfSlices(), - (isGPUKTXPayload ? gpuktxKeyValue._samplerDesc : sampler)); - - tex->setUsage((isGPUKTXPayload ? gpuktxKeyValue._usage : usage)); - - // Assing the mips availables - tex->setStoredMipFormat(mipFormat); - uint16_t level = 0; - for (auto& image : srcData->_images) { - for (uint32_t face = 0; face < image._numFaces; face++) { - tex->assignStoredMipFace(level, face, image._faceSize, image._faceBytes[face]); - } - level++; - } - - return tex; -} - -bool Texture::evalKTXFormat(const Element& mipFormat, const Element& texelFormat, ktx::Header& header) { - if (texelFormat == Format::COLOR_RGBA_32 && mipFormat == Format::COLOR_BGRA_32) { - header.setUncompressed(ktx::GLType::UNSIGNED_BYTE, 1, ktx::GLFormat::BGRA, ktx::GLInternalFormat_Uncompressed::RGBA8, ktx::GLBaseInternalFormat::RGBA); - } else if (texelFormat == Format::COLOR_RGBA_32 && mipFormat == Format::COLOR_RGBA_32) { - header.setUncompressed(ktx::GLType::UNSIGNED_BYTE, 1, ktx::GLFormat::RGBA, ktx::GLInternalFormat_Uncompressed::RGBA8, ktx::GLBaseInternalFormat::RGBA); - } else if (texelFormat == Format::COLOR_SRGBA_32 && mipFormat == Format::COLOR_SBGRA_32) { - header.setUncompressed(ktx::GLType::UNSIGNED_BYTE, 1, ktx::GLFormat::BGRA, ktx::GLInternalFormat_Uncompressed::SRGB8_ALPHA8, ktx::GLBaseInternalFormat::RGBA); - } else if (texelFormat == Format::COLOR_SRGBA_32 && mipFormat == Format::COLOR_SRGBA_32) { - header.setUncompressed(ktx::GLType::UNSIGNED_BYTE, 1, ktx::GLFormat::RGBA, ktx::GLInternalFormat_Uncompressed::SRGB8_ALPHA8, ktx::GLBaseInternalFormat::RGBA); - } else if (texelFormat == Format::COLOR_R_8 && mipFormat == Format::COLOR_R_8) { - header.setUncompressed(ktx::GLType::UNSIGNED_BYTE, 1, ktx::GLFormat::RED, ktx::GLInternalFormat_Uncompressed::R8, ktx::GLBaseInternalFormat::RED); - } else { - return false; - } - - return true; -} - -bool Texture::evalTextureFormat(const ktx::Header& header, Element& mipFormat, Element& texelFormat) { - if (header.getGLFormat() == ktx::GLFormat::BGRA && header.getGLType() == ktx::GLType::UNSIGNED_BYTE && header.getTypeSize() == 1) { - if (header.getGLInternaFormat_Uncompressed() == ktx::GLInternalFormat_Uncompressed::RGBA8) { - mipFormat = Format::COLOR_BGRA_32; - texelFormat = Format::COLOR_RGBA_32; - } else if (header.getGLInternaFormat_Uncompressed() == ktx::GLInternalFormat_Uncompressed::SRGB8_ALPHA8) { - mipFormat = Format::COLOR_SBGRA_32; - texelFormat = Format::COLOR_SRGBA_32; - } else { - return false; - } - } else if (header.getGLFormat() == ktx::GLFormat::RGBA && header.getGLType() == ktx::GLType::UNSIGNED_BYTE && header.getTypeSize() == 1) { - if (header.getGLInternaFormat_Uncompressed() == ktx::GLInternalFormat_Uncompressed::RGBA8) { - mipFormat = Format::COLOR_RGBA_32; - texelFormat = Format::COLOR_RGBA_32; - } else if (header.getGLInternaFormat_Uncompressed() == ktx::GLInternalFormat_Uncompressed::SRGB8_ALPHA8) { - mipFormat = Format::COLOR_SRGBA_32; - texelFormat = Format::COLOR_SRGBA_32; - } else { - return false; - } - } else if (header.getGLFormat() == ktx::GLFormat::RED && header.getGLType() == ktx::GLType::UNSIGNED_BYTE && header.getTypeSize() == 1) { - mipFormat = Format::COLOR_R_8; - if (header.getGLInternaFormat_Uncompressed() == ktx::GLInternalFormat_Uncompressed::R8) { - texelFormat = Format::COLOR_R_8; - } else { - return false; - } - } else { - return false; - } - return true; -} diff --git a/libraries/ktx/CMakeLists.txt b/libraries/ktx/CMakeLists.txt deleted file mode 100644 index 404660b247..0000000000 --- a/libraries/ktx/CMakeLists.txt +++ /dev/null @@ -1,3 +0,0 @@ -set(TARGET_NAME ktx) -setup_hifi_library() -link_hifi_libraries() \ No newline at end of file diff --git a/libraries/ktx/src/ktx/KTX.cpp b/libraries/ktx/src/ktx/KTX.cpp deleted file mode 100644 index bbd4e1bc86..0000000000 --- a/libraries/ktx/src/ktx/KTX.cpp +++ /dev/null @@ -1,165 +0,0 @@ -// -// KTX.cpp -// ktx/src/ktx -// -// Created by Zach Pomerantz on 2/08/2017. -// Copyright 2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include "KTX.h" - -#include //min max and more - -using namespace ktx; - -uint32_t Header::evalPadding(size_t byteSize) { - //auto padding = byteSize % PACKING_SIZE; - // return (uint32_t) (padding ? PACKING_SIZE - padding : 0); - return (uint32_t) (3 - (byteSize + 3) % PACKING_SIZE);// padding ? PACKING_SIZE - padding : 0); -} - - -const Header::Identifier ktx::Header::IDENTIFIER {{ - 0xAB, 0x4B, 0x54, 0x58, 0x20, 0x31, 0x31, 0xBB, 0x0D, 0x0A, 0x1A, 0x0A -}}; - -Header::Header() { - memcpy(identifier, IDENTIFIER.data(), IDENTIFIER_LENGTH); -} - -uint32_t Header::evalMaxDimension() const { - return std::max(getPixelWidth(), std::max(getPixelHeight(), getPixelDepth())); -} - -uint32_t Header::evalPixelWidth(uint32_t level) const { - return std::max(getPixelWidth() >> level, 1U); -} -uint32_t Header::evalPixelHeight(uint32_t level) const { - return std::max(getPixelHeight() >> level, 1U); -} -uint32_t Header::evalPixelDepth(uint32_t level) const { - return std::max(getPixelDepth() >> level, 1U); -} - -size_t Header::evalPixelSize() const { - return glTypeSize; // Really we should generate the size from the FOrmat etc -} - -size_t Header::evalRowSize(uint32_t level) const { - auto pixWidth = evalPixelWidth(level); - auto pixSize = evalPixelSize(); - auto netSize = pixWidth * pixSize; - auto padding = evalPadding(netSize); - return netSize + padding; -} -size_t Header::evalFaceSize(uint32_t level) const { - auto pixHeight = evalPixelHeight(level); - auto pixDepth = evalPixelDepth(level); - auto rowSize = evalRowSize(level); - return pixDepth * pixHeight * rowSize; -} -size_t Header::evalImageSize(uint32_t level) const { - auto faceSize = evalFaceSize(level); - if (numberOfFaces == NUM_CUBEMAPFACES && numberOfArrayElements == 0) { - return faceSize; - } else { - return (getNumberOfSlices() * numberOfFaces * faceSize); - } -} - - -KeyValue::KeyValue(const std::string& key, uint32_t valueByteSize, const Byte* value) : - _byteSize((uint32_t) key.size() + 1 + valueByteSize), // keyString size + '\0' ending char + the value size - _key(key), - _value(valueByteSize) -{ - if (_value.size() && value) { - memcpy(_value.data(), value, valueByteSize); - } -} - -KeyValue::KeyValue(const std::string& key, const std::string& value) : - KeyValue(key, (uint32_t) value.size(), (const Byte*) value.data()) -{ - -} - -uint32_t KeyValue::serializedByteSize() const { - return (uint32_t) (sizeof(uint32_t) + _byteSize + Header::evalPadding(_byteSize)); -} - -uint32_t KeyValue::serializedKeyValuesByteSize(const KeyValues& keyValues) { - uint32_t keyValuesSize = 0; - for (auto& keyval : keyValues) { - keyValuesSize += keyval.serializedByteSize(); - } - return (keyValuesSize + Header::evalPadding(keyValuesSize)); -} - - -KTX::KTX() { -} - -KTX::~KTX() { -} - -void KTX::resetStorage(const StoragePointer& storage) { - _storage = storage; -} - -const Header* KTX::getHeader() const { - if (!_storage) { - return nullptr; - } - return reinterpret_cast(_storage->data()); -} - - -size_t KTX::getKeyValueDataSize() const { - if (_storage) { - return getHeader()->bytesOfKeyValueData; - } else { - return 0; - } -} - -size_t KTX::getTexelsDataSize() const { - if (_storage) { - //return _storage->size() - (sizeof(Header) + getKeyValueDataSize()); - return (_storage->data() + _storage->size()) - getTexelsData(); - } else { - return 0; - } -} - -const Byte* KTX::getKeyValueData() const { - if (_storage) { - return (_storage->data() + sizeof(Header)); - } else { - return nullptr; - } -} - -const Byte* KTX::getTexelsData() const { - if (_storage) { - return (_storage->data() + sizeof(Header) + getKeyValueDataSize()); - } else { - return nullptr; - } -} - -storage::StoragePointer KTX::getMipFaceTexelsData(uint16_t mip, uint8_t face) const { - storage::StoragePointer result; - if (mip < _images.size()) { - const auto& faces = _images[mip]; - if (face < faces._numFaces) { - auto faceOffset = faces._faceBytes[face] - _storage->data(); - auto faceSize = faces._faceSize; - result = _storage->createView(faceSize, faceOffset); - } - } - return result; -} diff --git a/libraries/ktx/src/ktx/KTX.h b/libraries/ktx/src/ktx/KTX.h deleted file mode 100644 index 8e901b1105..0000000000 --- a/libraries/ktx/src/ktx/KTX.h +++ /dev/null @@ -1,494 +0,0 @@ -// -// KTX.h -// ktx/src/ktx -// -// Created by Zach Pomerantz on 2/08/2017. -// Copyright 2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// -#pragma once -#ifndef hifi_ktx_KTX_h -#define hifi_ktx_KTX_h - -#include -#include -#include -#include -#include -#include -#include - -#include - -/* KTX Spec: - -Byte[12] identifier -UInt32 endianness -UInt32 glType -UInt32 glTypeSize -UInt32 glFormat -Uint32 glInternalFormat -Uint32 glBaseInternalFormat -UInt32 pixelWidth -UInt32 pixelHeight -UInt32 pixelDepth -UInt32 numberOfArrayElements -UInt32 numberOfFaces -UInt32 numberOfMipmapLevels -UInt32 bytesOfKeyValueData - -for each keyValuePair that fits in bytesOfKeyValueData - UInt32 keyAndValueByteSize - Byte keyAndValue[keyAndValueByteSize] - Byte valuePadding[3 - ((keyAndValueByteSize + 3) % 4)] -end - -for each mipmap_level in numberOfMipmapLevels* - UInt32 imageSize; - for each array_element in numberOfArrayElements* - for each face in numberOfFaces - for each z_slice in pixelDepth* - for each row or row_of_blocks in pixelHeight* - for each pixel or block_of_pixels in pixelWidth - Byte data[format-specific-number-of-bytes]** - end - end - end - Byte cubePadding[0-3] - end - end - Byte mipPadding[3 - ((imageSize + 3) % 4)] -end - -* Replace with 1 if this field is 0. - -** Uncompressed texture data matches a GL_UNPACK_ALIGNMENT of 4. -*/ - - - -namespace ktx { - const uint32_t PACKING_SIZE { sizeof(uint32_t) }; - using Byte = uint8_t; - - enum class GLType : uint32_t { - COMPRESSED_TYPE = 0, - - // GL 4.4 Table 8.2 - UNSIGNED_BYTE = 0x1401, - BYTE = 0x1400, - UNSIGNED_SHORT = 0x1403, - SHORT = 0x1402, - UNSIGNED_INT = 0x1405, - INT = 0x1404, - HALF_FLOAT = 0x140B, - FLOAT = 0x1406, - UNSIGNED_BYTE_3_3_2 = 0x8032, - UNSIGNED_BYTE_2_3_3_REV = 0x8362, - UNSIGNED_SHORT_5_6_5 = 0x8363, - UNSIGNED_SHORT_5_6_5_REV = 0x8364, - UNSIGNED_SHORT_4_4_4_4 = 0x8033, - UNSIGNED_SHORT_4_4_4_4_REV = 0x8365, - UNSIGNED_SHORT_5_5_5_1 = 0x8034, - UNSIGNED_SHORT_1_5_5_5_REV = 0x8366, - UNSIGNED_INT_8_8_8_8 = 0x8035, - UNSIGNED_INT_8_8_8_8_REV = 0x8367, - UNSIGNED_INT_10_10_10_2 = 0x8036, - UNSIGNED_INT_2_10_10_10_REV = 0x8368, - UNSIGNED_INT_24_8 = 0x84FA, - UNSIGNED_INT_10F_11F_11F_REV = 0x8C3B, - UNSIGNED_INT_5_9_9_9_REV = 0x8C3E, - FLOAT_32_UNSIGNED_INT_24_8_REV = 0x8DAD, - - NUM_GLTYPES = 25, - }; - - enum class GLFormat : uint32_t { - COMPRESSED_FORMAT = 0, - - // GL 4.4 Table 8.3 - STENCIL_INDEX = 0x1901, - DEPTH_COMPONENT = 0x1902, - DEPTH_STENCIL = 0x84F9, - - RED = 0x1903, - GREEN = 0x1904, - BLUE = 0x1905, - RG = 0x8227, - RGB = 0x1907, - RGBA = 0x1908, - BGR = 0x80E0, - BGRA = 0x80E1, - - RG_INTEGER = 0x8228, - RED_INTEGER = 0x8D94, - GREEN_INTEGER = 0x8D95, - BLUE_INTEGER = 0x8D96, - RGB_INTEGER = 0x8D98, - RGBA_INTEGER = 0x8D99, - BGR_INTEGER = 0x8D9A, - BGRA_INTEGER = 0x8D9B, - - NUM_GLFORMATS = 20, - }; - - enum class GLInternalFormat_Uncompressed : uint32_t { - // GL 4.4 Table 8.12 - R8 = 0x8229, - R8_SNORM = 0x8F94, - - R16 = 0x822A, - R16_SNORM = 0x8F98, - - RG8 = 0x822B, - RG8_SNORM = 0x8F95, - - RG16 = 0x822C, - RG16_SNORM = 0x8F99, - - R3_G3_B2 = 0x2A10, - RGB4 = 0x804F, - RGB5 = 0x8050, - RGB565 = 0x8D62, - - RGB8 = 0x8051, - RGB8_SNORM = 0x8F96, - RGB10 = 0x8052, - RGB12 = 0x8053, - - RGB16 = 0x8054, - RGB16_SNORM = 0x8F9A, - - RGBA2 = 0x8055, - RGBA4 = 0x8056, - RGB5_A1 = 0x8057, - RGBA8 = 0x8058, - RGBA8_SNORM = 0x8F97, - - RGB10_A2 = 0x8059, - RGB10_A2UI = 0x906F, - - RGBA12 = 0x805A, - RGBA16 = 0x805B, - RGBA16_SNORM = 0x8F9B, - - SRGB8 = 0x8C41, - SRGB8_ALPHA8 = 0x8C43, - - R16F = 0x822D, - RG16F = 0x822F, - RGB16F = 0x881B, - RGBA16F = 0x881A, - - R32F = 0x822E, - RG32F = 0x8230, - RGB32F = 0x8815, - RGBA32F = 0x8814, - - R11F_G11F_B10F = 0x8C3A, - RGB9_E5 = 0x8C3D, - - - R8I = 0x8231, - R8UI = 0x8232, - R16I = 0x8233, - R16UI = 0x8234, - R32I = 0x8235, - R32UI = 0x8236, - RG8I = 0x8237, - RG8UI = 0x8238, - RG16I = 0x8239, - RG16UI = 0x823A, - RG32I = 0x823B, - RG32UI = 0x823C, - - RGB8I = 0x8D8F, - RGB8UI = 0x8D7D, - RGB16I = 0x8D89, - RGB16UI = 0x8D77, - - RGB32I = 0x8D83, - RGB32UI = 0x8D71, - RGBA8I = 0x8D8E, - RGBA8UI = 0x8D7C, - RGBA16I = 0x8D88, - RGBA16UI = 0x8D76, - RGBA32I = 0x8D82, - - RGBA32UI = 0x8D70, - - // GL 4.4 Table 8.13 - DEPTH_COMPONENT16 = 0x81A5, - DEPTH_COMPONENT24 = 0x81A6, - DEPTH_COMPONENT32 = 0x81A7, - - DEPTH_COMPONENT32F = 0x8CAC, - DEPTH24_STENCIL8 = 0x88F0, - DEPTH32F_STENCIL8 = 0x8CAD, - - STENCIL_INDEX1 = 0x8D46, - STENCIL_INDEX4 = 0x8D47, - STENCIL_INDEX8 = 0x8D48, - STENCIL_INDEX16 = 0x8D49, - - NUM_UNCOMPRESSED_GLINTERNALFORMATS = 74, - }; - - enum class GLInternalFormat_Compressed : uint32_t { - // GL 4.4 Table 8.14 - COMPRESSED_RED = 0x8225, - COMPRESSED_RG = 0x8226, - COMPRESSED_RGB = 0x84ED, - COMPRESSED_RGBA = 0x84EE, - - COMPRESSED_SRGB = 0x8C48, - COMPRESSED_SRGB_ALPHA = 0x8C49, - - COMPRESSED_RED_RGTC1 = 0x8DBB, - COMPRESSED_SIGNED_RED_RGTC1 = 0x8DBC, - COMPRESSED_RG_RGTC2 = 0x8DBD, - COMPRESSED_SIGNED_RG_RGTC2 = 0x8DBE, - - COMPRESSED_RGBA_BPTC_UNORM = 0x8E8C, - COMPRESSED_SRGB_ALPHA_BPTC_UNORM = 0x8E8D, - COMPRESSED_RGB_BPTC_SIGNED_FLOAT = 0x8E8E, - COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT = 0x8E8F, - - COMPRESSED_RGB8_ETC2 = 0x9274, - COMPRESSED_SRGB8_ETC2 = 0x9275, - COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2 = 0x9276, - COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2 = 0x9277, - COMPRESSED_RGBA8_ETC2_EAC = 0x9278, - COMPRESSED_SRGB8_ALPHA8_ETC2_EAC = 0x9279, - - COMPRESSED_R11_EAC = 0x9270, - COMPRESSED_SIGNED_R11_EAC = 0x9271, - COMPRESSED_RG11_EAC = 0x9272, - COMPRESSED_SIGNED_RG11_EAC = 0x9273, - - NUM_COMPRESSED_GLINTERNALFORMATS = 24, - }; - - enum class GLBaseInternalFormat : uint32_t { - // GL 4.4 Table 8.11 - DEPTH_COMPONENT = 0x1902, - DEPTH_STENCIL = 0x84F9, - RED = 0x1903, - RG = 0x8227, - RGB = 0x1907, - RGBA = 0x1908, - STENCIL_INDEX = 0x1901, - - NUM_GLBASEINTERNALFORMATS = 7, - }; - - enum CubeMapFace { - POS_X = 0, - NEG_X = 1, - POS_Y = 2, - NEG_Y = 3, - POS_Z = 4, - NEG_Z = 5, - NUM_CUBEMAPFACES = 6, - }; - - using Storage = storage::Storage; - using StoragePointer = std::shared_ptr; - - // Header - struct Header { - static const size_t IDENTIFIER_LENGTH = 12; - using Identifier = std::array; - static const Identifier IDENTIFIER; - - static const uint32_t ENDIAN_TEST = 0x04030201; - static const uint32_t REVERSE_ENDIAN_TEST = 0x01020304; - - static uint32_t evalPadding(size_t byteSize); - - Header(); - - Byte identifier[IDENTIFIER_LENGTH]; - uint32_t endianness { ENDIAN_TEST }; - - uint32_t glType; - uint32_t glTypeSize { 0 }; - uint32_t glFormat; - uint32_t glInternalFormat; - uint32_t glBaseInternalFormat; - - uint32_t pixelWidth { 1 }; - uint32_t pixelHeight { 0 }; - uint32_t pixelDepth { 0 }; - uint32_t numberOfArrayElements { 0 }; - uint32_t numberOfFaces { 1 }; - uint32_t numberOfMipmapLevels { 1 }; - - uint32_t bytesOfKeyValueData { 0 }; - - uint32_t getPixelWidth() const { return (pixelWidth ? pixelWidth : 1); } - uint32_t getPixelHeight() const { return (pixelHeight ? pixelHeight : 1); } - uint32_t getPixelDepth() const { return (pixelDepth ? pixelDepth : 1); } - uint32_t getNumberOfSlices() const { return (numberOfArrayElements ? numberOfArrayElements : 1); } - uint32_t getNumberOfLevels() const { return (numberOfMipmapLevels ? numberOfMipmapLevels : 1); } - - uint32_t evalMaxDimension() const; - uint32_t evalPixelWidth(uint32_t level) const; - uint32_t evalPixelHeight(uint32_t level) const; - uint32_t evalPixelDepth(uint32_t level) const; - - size_t evalPixelSize() const; - size_t evalRowSize(uint32_t level) const; - size_t evalFaceSize(uint32_t level) const; - size_t evalImageSize(uint32_t level) const; - - void setUncompressed(GLType type, uint32_t typeSize, GLFormat format, GLInternalFormat_Uncompressed internalFormat, GLBaseInternalFormat baseInternalFormat) { - glType = (uint32_t) type; - glTypeSize = typeSize; - glFormat = (uint32_t) format; - glInternalFormat = (uint32_t) internalFormat; - glBaseInternalFormat = (uint32_t) baseInternalFormat; - } - void setCompressed(GLInternalFormat_Compressed internalFormat, GLBaseInternalFormat baseInternalFormat) { - glType = (uint32_t) GLType::COMPRESSED_TYPE; - glTypeSize = 1; - glFormat = (uint32_t) GLFormat::COMPRESSED_FORMAT; - glInternalFormat = (uint32_t) internalFormat; - glBaseInternalFormat = (uint32_t) baseInternalFormat; - } - - GLType getGLType() const { return (GLType)glType; } - uint32_t getTypeSize() const { return glTypeSize; } - GLFormat getGLFormat() const { return (GLFormat)glFormat; } - GLInternalFormat_Uncompressed getGLInternaFormat_Uncompressed() const { return (GLInternalFormat_Uncompressed)glInternalFormat; } - GLInternalFormat_Compressed getGLInternaFormat_Compressed() const { return (GLInternalFormat_Compressed)glInternalFormat; } - GLBaseInternalFormat getGLBaseInternalFormat() const { return (GLBaseInternalFormat)glBaseInternalFormat; } - - - void setDimensions(uint32_t width, uint32_t height = 0, uint32_t depth = 0, uint32_t numSlices = 0, uint32_t numFaces = 1) { - pixelWidth = (width > 0 ? width : 1); - pixelHeight = height; - pixelDepth = depth; - numberOfArrayElements = numSlices; - numberOfFaces = ((numFaces == 1) || (numFaces == NUM_CUBEMAPFACES) ? numFaces : 1); - } - void set1D(uint32_t width) { setDimensions(width); } - void set1DArray(uint32_t width, uint32_t numSlices) { setDimensions(width, 0, 0, (numSlices > 0 ? numSlices : 1)); } - void set2D(uint32_t width, uint32_t height) { setDimensions(width, height); } - void set2DArray(uint32_t width, uint32_t height, uint32_t numSlices) { setDimensions(width, height, 0, (numSlices > 0 ? numSlices : 1)); } - void set3D(uint32_t width, uint32_t height, uint32_t depth) { setDimensions(width, height, depth); } - void set3DArray(uint32_t width, uint32_t height, uint32_t depth, uint32_t numSlices) { setDimensions(width, height, depth, (numSlices > 0 ? numSlices : 1)); } - void setCube(uint32_t width, uint32_t height) { setDimensions(width, height, 0, 0, NUM_CUBEMAPFACES); } - void setCubeArray(uint32_t width, uint32_t height, uint32_t numSlices) { setDimensions(width, height, 0, (numSlices > 0 ? numSlices : 1), NUM_CUBEMAPFACES); } - - }; - - // Key Values - struct KeyValue { - uint32_t _byteSize { 0 }; - std::string _key; - std::vector _value; - - - KeyValue(const std::string& key, uint32_t valueByteSize, const Byte* value); - - KeyValue(const std::string& key, const std::string& value); - - uint32_t serializedByteSize() const; - - static KeyValue parseSerializedKeyAndValue(uint32_t srcSize, const Byte* srcBytes); - static uint32_t writeSerializedKeyAndValue(Byte* destBytes, uint32_t destByteSize, const KeyValue& keyval); - - using KeyValues = std::list; - static uint32_t serializedKeyValuesByteSize(const KeyValues& keyValues); - - }; - using KeyValues = KeyValue::KeyValues; - - - struct Image { - using FaceBytes = std::vector; - - uint32_t _numFaces{ 1 }; - uint32_t _imageSize; - uint32_t _faceSize; - uint32_t _padding; - FaceBytes _faceBytes; - - - Image(uint32_t imageSize, uint32_t padding, const Byte* bytes) : - _numFaces(1), - _imageSize(imageSize), - _faceSize(imageSize), - _padding(padding), - _faceBytes(1, bytes) {} - - Image(uint32_t pageSize, uint32_t padding, const FaceBytes& cubeFaceBytes) : - _numFaces(NUM_CUBEMAPFACES), - _imageSize(pageSize * NUM_CUBEMAPFACES), - _faceSize(pageSize), - _padding(padding) - { - if (cubeFaceBytes.size() == NUM_CUBEMAPFACES) { - _faceBytes = cubeFaceBytes; - } - } - }; - using Images = std::vector; - - class KTX { - void resetStorage(const StoragePointer& src); - - KTX(); - public: - - ~KTX(); - - // Define a KTX object manually to write it somewhere (in a file on disk?) - // This path allocate the Storage where to store header, keyvalues and copy mips - // Then COPY all the data - static std::unique_ptr create(const Header& header, const Images& images, const KeyValues& keyValues = KeyValues()); - - // Instead of creating a full Copy of the src data in a KTX object, the write serialization can be performed with the - // following two functions - // size_t sizeNeeded = KTX::evalStorageSize(header, images); - // - // //allocate a buffer of size "sizeNeeded" or map a file with enough capacity - // Byte* destBytes = new Byte[sizeNeeded]; - // - // // THen perform the writing of the src data to the destinnation buffer - // write(destBytes, sizeNeeded, header, images); - // - // This is exactly what is done in the create function - static size_t evalStorageSize(const Header& header, const Images& images, const KeyValues& keyValues = KeyValues()); - static size_t write(Byte* destBytes, size_t destByteSize, const Header& header, const Images& images, const KeyValues& keyValues = KeyValues()); - static size_t writeKeyValues(Byte* destBytes, size_t destByteSize, const KeyValues& keyValues); - static Images writeImages(Byte* destBytes, size_t destByteSize, const Images& images); - - // Parse a block of memory and create a KTX object from it - static std::unique_ptr create(const StoragePointer& src); - - static bool checkHeaderFromStorage(size_t srcSize, const Byte* srcBytes); - static KeyValues parseKeyValues(size_t srcSize, const Byte* srcBytes); - static Images parseImages(const Header& header, size_t srcSize, const Byte* srcBytes); - - // Access raw pointers to the main sections of the KTX - const Header* getHeader() const; - const Byte* getKeyValueData() const; - const Byte* getTexelsData() const; - storage::StoragePointer getMipFaceTexelsData(uint16_t mip = 0, uint8_t face = 0) const; - const StoragePointer& getStorage() const { return _storage; } - - size_t getKeyValueDataSize() const; - size_t getTexelsDataSize() const; - - StoragePointer _storage; - KeyValues _keyValues; - Images _images; - }; - -} - -#endif // hifi_ktx_KTX_h diff --git a/libraries/ktx/src/ktx/Reader.cpp b/libraries/ktx/src/ktx/Reader.cpp deleted file mode 100644 index 277ce42e69..0000000000 --- a/libraries/ktx/src/ktx/Reader.cpp +++ /dev/null @@ -1,195 +0,0 @@ -// -// Reader.cpp -// ktx/src/ktx -// -// Created by Zach Pomerantz on 2/08/2017. -// Copyright 2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// -#include "KTX.h" - -#include -#include -#include - -#ifndef _MSC_VER -#define NOEXCEPT noexcept -#else -#define NOEXCEPT -#endif - -namespace ktx { - class ReaderException: public std::exception { - public: - ReaderException(const std::string& explanation) : _explanation("KTX deserialization error: " + explanation) {} - const char* what() const NOEXCEPT override { return _explanation.c_str(); } - private: - const std::string _explanation; - }; - - bool checkEndianness(uint32_t endianness, bool& matching) { - switch (endianness) { - case Header::ENDIAN_TEST: { - matching = true; - return true; - } - break; - case Header::REVERSE_ENDIAN_TEST: - { - matching = false; - return true; - } - break; - default: - throw ReaderException("endianness field has invalid value"); - return false; - } - } - - bool checkIdentifier(const Byte* identifier) { - if (!(0 == memcmp(identifier, Header::IDENTIFIER.data(), Header::IDENTIFIER_LENGTH))) { - throw ReaderException("identifier field invalid"); - return false; - } - return true; - } - - bool KTX::checkHeaderFromStorage(size_t srcSize, const Byte* srcBytes) { - try { - // validation - if (srcSize < sizeof(Header)) { - throw ReaderException("length is too short for header"); - } - const Header* header = reinterpret_cast(srcBytes); - - checkIdentifier(header->identifier); - - bool endianMatch { true }; - checkEndianness(header->endianness, endianMatch); - - // TODO: endian conversion if !endianMatch - for now, this is for local use and is unnecessary - - - // TODO: calculated bytesOfTexData - if (srcSize < (sizeof(Header) + header->bytesOfKeyValueData)) { - throw ReaderException("length is too short for metadata"); - } - - size_t bytesOfTexData = 0; - if (srcSize < (sizeof(Header) + header->bytesOfKeyValueData + bytesOfTexData)) { - - throw ReaderException("length is too short for data"); - } - - return true; - } - catch (const ReaderException& e) { - qWarning() << e.what(); - return false; - } - } - - KeyValue KeyValue::parseSerializedKeyAndValue(uint32_t srcSize, const Byte* srcBytes) { - uint32_t keyAndValueByteSize; - memcpy(&keyAndValueByteSize, srcBytes, sizeof(uint32_t)); - if (keyAndValueByteSize + sizeof(uint32_t) > srcSize) { - throw ReaderException("invalid key-value size"); - } - auto keyValueBytes = srcBytes + sizeof(uint32_t); - - // find the first null character \0 and extract the key - uint32_t keyLength = 0; - while (reinterpret_cast(keyValueBytes)[++keyLength] != '\0') { - if (keyLength == keyAndValueByteSize) { - // key must be null-terminated, and there must be space for the value - throw ReaderException("invalid key-value " + std::string(reinterpret_cast(keyValueBytes), keyLength)); - } - } - uint32_t valueStartOffset = keyLength + 1; - - // parse the key-value - return KeyValue(std::string(reinterpret_cast(keyValueBytes), keyLength), - keyAndValueByteSize - valueStartOffset, keyValueBytes + valueStartOffset); - } - - KeyValues KTX::parseKeyValues(size_t srcSize, const Byte* srcBytes) { - KeyValues keyValues; - try { - auto src = srcBytes; - uint32_t length = (uint32_t) srcSize; - uint32_t offset = 0; - while (offset < length) { - auto keyValue = KeyValue::parseSerializedKeyAndValue(length - offset, src); - keyValues.emplace_back(keyValue); - - // advance offset/src - offset += keyValue.serializedByteSize(); - src += keyValue.serializedByteSize(); - } - } - catch (const ReaderException& e) { - qWarning() << e.what(); - } - return keyValues; - } - - Images KTX::parseImages(const Header& header, size_t srcSize, const Byte* srcBytes) { - Images images; - auto currentPtr = srcBytes; - auto numFaces = header.numberOfFaces; - - // Keep identifying new mip as long as we can at list query the next imageSize - while ((currentPtr - srcBytes) + sizeof(uint32_t) <= (srcSize)) { - - // Grab the imageSize coming up - size_t imageSize = *reinterpret_cast(currentPtr); - currentPtr += sizeof(uint32_t); - - // If enough data ahead then capture the pointer - if ((currentPtr - srcBytes) + imageSize <= (srcSize)) { - auto padding = Header::evalPadding(imageSize); - - if (numFaces == NUM_CUBEMAPFACES) { - size_t faceSize = imageSize / NUM_CUBEMAPFACES; - Image::FaceBytes faces(NUM_CUBEMAPFACES); - for (uint32_t face = 0; face < NUM_CUBEMAPFACES; face++) { - faces[face] = currentPtr; - currentPtr += faceSize; - } - images.emplace_back(Image((uint32_t) faceSize, padding, faces)); - currentPtr += padding; - } else { - images.emplace_back(Image((uint32_t) imageSize, padding, currentPtr)); - currentPtr += imageSize + padding; - } - } else { - break; - } - } - - return images; - } - - std::unique_ptr KTX::create(const StoragePointer& src) { - if (!src) { - return nullptr; - } - - if (!checkHeaderFromStorage(src->size(), src->data())) { - return nullptr; - } - - std::unique_ptr result(new KTX()); - result->resetStorage(src); - - // read metadata - result->_keyValues = parseKeyValues(result->getHeader()->bytesOfKeyValueData, result->getKeyValueData()); - - // populate image table - result->_images = parseImages(*result->getHeader(), result->getTexelsDataSize(), result->getTexelsData()); - - return result; - } -} diff --git a/libraries/ktx/src/ktx/Writer.cpp b/libraries/ktx/src/ktx/Writer.cpp deleted file mode 100644 index 25b363d31b..0000000000 --- a/libraries/ktx/src/ktx/Writer.cpp +++ /dev/null @@ -1,171 +0,0 @@ -// -// Writer.cpp -// ktx/src/ktx -// -// Created by Zach Pomerantz on 2/08/2017. -// Copyright 2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// -#include "KTX.h" - - -#include -#include -#ifndef _MSC_VER -#define NOEXCEPT noexcept -#else -#define NOEXCEPT -#endif - -namespace ktx { - - class WriterException : public std::exception { - public: - WriterException(const std::string& explanation) : _explanation("KTX serialization error: " + explanation) {} - const char* what() const NOEXCEPT override { return _explanation.c_str(); } - private: - const std::string _explanation; - }; - - std::unique_ptr KTX::create(const Header& header, const Images& images, const KeyValues& keyValues) { - StoragePointer storagePointer; - { - auto storageSize = ktx::KTX::evalStorageSize(header, images, keyValues); - auto memoryStorage = new storage::MemoryStorage(storageSize); - ktx::KTX::write(memoryStorage->data(), memoryStorage->size(), header, images, keyValues); - storagePointer.reset(memoryStorage); - } - return create(storagePointer); - } - - size_t KTX::evalStorageSize(const Header& header, const Images& images, const KeyValues& keyValues) { - size_t storageSize = sizeof(Header); - - if (!keyValues.empty()) { - size_t keyValuesSize = KeyValue::serializedKeyValuesByteSize(keyValues); - storageSize += keyValuesSize; - } - - auto numMips = header.getNumberOfLevels(); - for (uint32_t l = 0; l < numMips; l++) { - if (images.size() > l) { - storageSize += sizeof(uint32_t); - storageSize += images[l]._imageSize; - storageSize += Header::evalPadding(images[l]._imageSize); - } - } - return storageSize; - } - - size_t KTX::write(Byte* destBytes, size_t destByteSize, const Header& header, const Images& srcImages, const KeyValues& keyValues) { - // Check again that we have enough destination capacity - if (!destBytes || (destByteSize < evalStorageSize(header, srcImages, keyValues))) { - return 0; - } - - auto currentDestPtr = destBytes; - // Header - auto destHeader = reinterpret_cast(currentDestPtr); - memcpy(currentDestPtr, &header, sizeof(Header)); - currentDestPtr += sizeof(Header); - - // KeyValues - if (!keyValues.empty()) { - destHeader->bytesOfKeyValueData = (uint32_t) writeKeyValues(currentDestPtr, destByteSize - sizeof(Header), keyValues); - } else { - // Make sure the header contains the right bytesOfKeyValueData size - destHeader->bytesOfKeyValueData = 0; - } - currentDestPtr += destHeader->bytesOfKeyValueData; - - // Images - auto destImages = writeImages(currentDestPtr, destByteSize - sizeof(Header) - destHeader->bytesOfKeyValueData, srcImages); - // We chould check here that the amoutn of dest IMages generated is the same as the source - - return destByteSize; - } - - uint32_t KeyValue::writeSerializedKeyAndValue(Byte* destBytes, uint32_t destByteSize, const KeyValue& keyval) { - uint32_t keyvalSize = keyval.serializedByteSize(); - if (keyvalSize > destByteSize) { - throw WriterException("invalid key-value size"); - } - - *((uint32_t*) destBytes) = keyval._byteSize; - - auto dest = destBytes + sizeof(uint32_t); - - auto keySize = keyval._key.size() + 1; // Add 1 for the '\0' character at the end of the string - memcpy(dest, keyval._key.data(), keySize); - dest += keySize; - - memcpy(dest, keyval._value.data(), keyval._value.size()); - - return keyvalSize; - } - - size_t KTX::writeKeyValues(Byte* destBytes, size_t destByteSize, const KeyValues& keyValues) { - size_t writtenByteSize = 0; - try { - auto dest = destBytes; - for (auto& keyval : keyValues) { - size_t keyvalSize = KeyValue::writeSerializedKeyAndValue(dest, (uint32_t) (destByteSize - writtenByteSize), keyval); - writtenByteSize += keyvalSize; - dest += keyvalSize; - } - } - catch (const WriterException& e) { - qWarning() << e.what(); - } - return writtenByteSize; - } - - Images KTX::writeImages(Byte* destBytes, size_t destByteSize, const Images& srcImages) { - Images destImages; - auto imagesDataPtr = destBytes; - if (!imagesDataPtr) { - return destImages; - } - auto allocatedImagesDataSize = destByteSize; - size_t currentDataSize = 0; - auto currentPtr = imagesDataPtr; - - for (uint32_t l = 0; l < srcImages.size(); l++) { - if (currentDataSize + sizeof(uint32_t) < allocatedImagesDataSize) { - size_t imageSize = srcImages[l]._imageSize; - *(reinterpret_cast (currentPtr)) = (uint32_t) imageSize; - currentPtr += sizeof(uint32_t); - currentDataSize += sizeof(uint32_t); - - // If enough data ahead then capture the copy source pointer - if (currentDataSize + imageSize <= (allocatedImagesDataSize)) { - auto padding = Header::evalPadding(imageSize); - - // Single face vs cubes - if (srcImages[l]._numFaces == 1) { - memcpy(currentPtr, srcImages[l]._faceBytes[0], imageSize); - destImages.emplace_back(Image((uint32_t) imageSize, padding, currentPtr)); - currentPtr += imageSize; - } else { - Image::FaceBytes faceBytes(NUM_CUBEMAPFACES); - auto faceSize = srcImages[l]._faceSize; - for (int face = 0; face < NUM_CUBEMAPFACES; face++) { - memcpy(currentPtr, srcImages[l]._faceBytes[face], faceSize); - faceBytes[face] = currentPtr; - currentPtr += faceSize; - } - destImages.emplace_back(Image(faceSize, padding, faceBytes)); - } - - currentPtr += padding; - currentDataSize += imageSize + padding; - } - } - } - - return destImages; - } - -} diff --git a/libraries/model-networking/CMakeLists.txt b/libraries/model-networking/CMakeLists.txt index 00aa17ff57..ed8cd7b5f9 100644 --- a/libraries/model-networking/CMakeLists.txt +++ b/libraries/model-networking/CMakeLists.txt @@ -1,4 +1,4 @@ set(TARGET_NAME model-networking) setup_hifi_library() -link_hifi_libraries(shared networking model fbx ktx) +link_hifi_libraries(shared networking model fbx) diff --git a/libraries/model-networking/src/model-networking/KTXCache.cpp b/libraries/model-networking/src/model-networking/KTXCache.cpp deleted file mode 100644 index 63d35fe4a4..0000000000 --- a/libraries/model-networking/src/model-networking/KTXCache.cpp +++ /dev/null @@ -1,47 +0,0 @@ -// -// KTXCache.cpp -// libraries/model-networking/src -// -// Created by Zach Pomerantz on 2/22/2017. -// Copyright 2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include "KTXCache.h" - -#include - -using File = cache::File; -using FilePointer = cache::FilePointer; - -KTXCache::KTXCache(const std::string& dir, const std::string& ext) : - FileCache(dir, ext) { - initialize(); -} - -KTXFilePointer KTXCache::writeFile(const char* data, Metadata&& metadata) { - FilePointer file = FileCache::writeFile(data, std::move(metadata)); - return std::static_pointer_cast(file); -} - -KTXFilePointer KTXCache::getFile(const Key& key) { - return std::static_pointer_cast(FileCache::getFile(key)); -} - -std::unique_ptr KTXCache::createFile(Metadata&& metadata, const std::string& filepath) { - qCInfo(file_cache) << "Wrote KTX" << metadata.key.c_str(); - return std::unique_ptr(new KTXFile(std::move(metadata), filepath)); -} - -KTXFile::KTXFile(Metadata&& metadata, const std::string& filepath) : - cache::File(std::move(metadata), filepath) {} - -std::unique_ptr KTXFile::getKTX() const { - ktx::StoragePointer storage = std::make_shared(getFilepath().c_str()); - if (*storage) { - return ktx::KTX::create(storage); - } - return {}; -} diff --git a/libraries/model-networking/src/model-networking/KTXCache.h b/libraries/model-networking/src/model-networking/KTXCache.h deleted file mode 100644 index 4ef5e52721..0000000000 --- a/libraries/model-networking/src/model-networking/KTXCache.h +++ /dev/null @@ -1,51 +0,0 @@ -// -// KTXCache.h -// libraries/model-networking/src -// -// Created by Zach Pomerantz 2/22/2017. -// Copyright 2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#ifndef hifi_KTXCache_h -#define hifi_KTXCache_h - -#include - -#include - -namespace ktx { - class KTX; -} - -class KTXFile; -using KTXFilePointer = std::shared_ptr; - -class KTXCache : public cache::FileCache { - Q_OBJECT - -public: - KTXCache(const std::string& dir, const std::string& ext); - - KTXFilePointer writeFile(const char* data, Metadata&& metadata); - KTXFilePointer getFile(const Key& key); - -protected: - std::unique_ptr createFile(Metadata&& metadata, const std::string& filepath) override final; -}; - -class KTXFile : public cache::File { - Q_OBJECT - -public: - std::unique_ptr getKTX() const; - -protected: - friend class KTXCache; - - KTXFile(Metadata&& metadata, const std::string& filepath); -}; - -#endif // hifi_KTXCache_h diff --git a/libraries/model-networking/src/model-networking/TextureCache.cpp b/libraries/model-networking/src/model-networking/TextureCache.cpp index 5dfaddd471..8a4e85cfe6 100644 --- a/libraries/model-networking/src/model-networking/TextureCache.cpp +++ b/libraries/model-networking/src/model-networking/TextureCache.cpp @@ -18,37 +18,27 @@ #include #include #include - -#if DEBUG_DUMP_TEXTURE_LOADS #include #include -#endif #include #include #include -#include - #include #include #include +#include #include "ModelNetworkingLogging.h" #include #include Q_LOGGING_CATEGORY(trace_resource_parse_image, "trace.resource.parse.image") -Q_LOGGING_CATEGORY(trace_resource_parse_image_raw, "trace.resource.parse.image.raw") -Q_LOGGING_CATEGORY(trace_resource_parse_image_ktx, "trace.resource.parse.image.ktx") -const std::string TextureCache::KTX_DIRNAME { "ktx_cache" }; -const std::string TextureCache::KTX_EXT { "ktx" }; - -TextureCache::TextureCache() : - _ktxCache(KTX_DIRNAME, KTX_EXT) { +TextureCache::TextureCache() { setUnusedResourceCacheSize(0); setObjectName("TextureCache"); @@ -71,7 +61,7 @@ TextureCache::~TextureCache() { // this list taken from Ken Perlin's Improved Noise reference implementation (orig. in Java) at // http://mrl.nyu.edu/~perlin/noise/ -const int permutation[256] = +const int permutation[256] = { 151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23, 190, 6, 148, @@ -118,8 +108,7 @@ const gpu::TexturePointer& TextureCache::getPermutationNormalTexture() { } _permutationNormalTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element(gpu::VEC3, gpu::NUINT8, gpu::RGB), 256, 2)); - _permutationNormalTexture->setStoredMipFormat(_permutationNormalTexture->getTexelFormat()); - _permutationNormalTexture->assignStoredMip(0, sizeof(data), data); + _permutationNormalTexture->assignStoredMip(0, _blueTexture->getTexelFormat(), sizeof(data), data); } return _permutationNormalTexture; } @@ -131,40 +120,36 @@ const unsigned char OPAQUE_BLACK[] = { 0x00, 0x00, 0x00, 0xFF }; const gpu::TexturePointer& TextureCache::getWhiteTexture() { if (!_whiteTexture) { - _whiteTexture = gpu::TexturePointer(gpu::Texture::createStrict(gpu::Element::COLOR_RGBA_32, 1, 1)); + _whiteTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, 1, 1)); _whiteTexture->setSource("TextureCache::_whiteTexture"); - _whiteTexture->setStoredMipFormat(_whiteTexture->getTexelFormat()); - _whiteTexture->assignStoredMip(0, sizeof(OPAQUE_WHITE), OPAQUE_WHITE); + _whiteTexture->assignStoredMip(0, _whiteTexture->getTexelFormat(), sizeof(OPAQUE_WHITE), OPAQUE_WHITE); } return _whiteTexture; } const gpu::TexturePointer& TextureCache::getGrayTexture() { if (!_grayTexture) { - _grayTexture = gpu::TexturePointer(gpu::Texture::createStrict(gpu::Element::COLOR_RGBA_32, 1, 1)); + _grayTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, 1, 1)); _grayTexture->setSource("TextureCache::_grayTexture"); - _grayTexture->setStoredMipFormat(_grayTexture->getTexelFormat()); - _grayTexture->assignStoredMip(0, sizeof(OPAQUE_GRAY), OPAQUE_GRAY); + _grayTexture->assignStoredMip(0, _grayTexture->getTexelFormat(), sizeof(OPAQUE_GRAY), OPAQUE_GRAY); } return _grayTexture; } const gpu::TexturePointer& TextureCache::getBlueTexture() { if (!_blueTexture) { - _blueTexture = gpu::TexturePointer(gpu::Texture::createStrict(gpu::Element::COLOR_RGBA_32, 1, 1)); + _blueTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, 1, 1)); _blueTexture->setSource("TextureCache::_blueTexture"); - _blueTexture->setStoredMipFormat(_blueTexture->getTexelFormat()); - _blueTexture->assignStoredMip(0, sizeof(OPAQUE_BLUE), OPAQUE_BLUE); + _blueTexture->assignStoredMip(0, _blueTexture->getTexelFormat(), sizeof(OPAQUE_BLUE), OPAQUE_BLUE); } return _blueTexture; } const gpu::TexturePointer& TextureCache::getBlackTexture() { if (!_blackTexture) { - _blackTexture = gpu::TexturePointer(gpu::Texture::createStrict(gpu::Element::COLOR_RGBA_32, 1, 1)); + _blackTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, 1, 1)); _blackTexture->setSource("TextureCache::_blackTexture"); - _blackTexture->setStoredMipFormat(_blackTexture->getTexelFormat()); - _blackTexture->assignStoredMip(0, sizeof(OPAQUE_BLACK), OPAQUE_BLACK); + _blackTexture->assignStoredMip(0, _blackTexture->getTexelFormat(), sizeof(OPAQUE_BLACK), OPAQUE_BLACK); } return _blackTexture; } @@ -188,72 +173,6 @@ NetworkTexturePointer TextureCache::getTexture(const QUrl& url, Type type, const return ResourceCache::getResource(url, QUrl(), &extra).staticCast(); } -gpu::TexturePointer TextureCache::getTextureByHash(const std::string& hash) { - std::weak_ptr weakPointer; - { - std::unique_lock lock(_texturesByHashesMutex); - weakPointer = _texturesByHashes[hash]; - } - auto result = weakPointer.lock(); - if (result) { - qCWarning(modelnetworking) << "QQQ Returning live texture for hash " << hash.c_str(); - } - return result; -} - -gpu::TexturePointer TextureCache::cacheTextureByHash(const std::string& hash, const gpu::TexturePointer& texture) { - gpu::TexturePointer result; - { - std::unique_lock lock(_texturesByHashesMutex); - result = _texturesByHashes[hash].lock(); - if (!result) { - _texturesByHashes[hash] = texture; - result = texture; - } else { - qCWarning(modelnetworking) << "QQQ Swapping out texture with previous live texture in hash " << hash.c_str(); - } - } - return result; -} - - -gpu::TexturePointer getFallbackTextureForType(NetworkTexture::Type type) { - gpu::TexturePointer result; - auto textureCache = DependencyManager::get(); - // Since this can be called on a background thread, there's a chance that the cache - // will be destroyed by the time we request it - if (!textureCache) { - return result; - } - switch (type) { - case NetworkTexture::DEFAULT_TEXTURE: - case NetworkTexture::ALBEDO_TEXTURE: - case NetworkTexture::ROUGHNESS_TEXTURE: - case NetworkTexture::OCCLUSION_TEXTURE: - result = textureCache->getWhiteTexture(); - break; - - case NetworkTexture::NORMAL_TEXTURE: - result = textureCache->getBlueTexture(); - break; - - case NetworkTexture::EMISSIVE_TEXTURE: - case NetworkTexture::LIGHTMAP_TEXTURE: - result = textureCache->getBlackTexture(); - break; - - case NetworkTexture::BUMP_TEXTURE: - case NetworkTexture::SPECULAR_TEXTURE: - case NetworkTexture::GLOSS_TEXTURE: - case NetworkTexture::CUBE_TEXTURE: - case NetworkTexture::CUSTOM_TEXTURE: - case NetworkTexture::STRICT_TEXTURE: - default: - break; - } - return result; -} - NetworkTexture::TextureLoaderFunc getTextureLoaderForType(NetworkTexture::Type type, const QVariantMap& options = QVariantMap()) { @@ -300,16 +219,11 @@ NetworkTexture::TextureLoaderFunc getTextureLoaderForType(NetworkTexture::Type t return model::TextureUsage::createMetallicTextureFromImage; break; } - case Type::STRICT_TEXTURE: { - return model::TextureUsage::createStrict2DTextureFromImage; - break; - } case Type::CUSTOM_TEXTURE: { Q_ASSERT(false); return NetworkTexture::TextureLoaderFunc(); break; } - case Type::DEFAULT_TEXTURE: default: { return model::TextureUsage::create2DTextureFromImage; @@ -331,8 +245,8 @@ QSharedPointer TextureCache::createResource(const QUrl& url, const QSh auto type = textureExtra ? textureExtra->type : Type::DEFAULT_TEXTURE; auto content = textureExtra ? textureExtra->content : QByteArray(); auto maxNumPixels = textureExtra ? textureExtra->maxNumPixels : ABSOLUTE_MAX_TEXTURE_NUM_PIXELS; - NetworkTexture* texture = new NetworkTexture(url, type, content, maxNumPixels); - return QSharedPointer(texture, &Resource::deleter); + return QSharedPointer(new NetworkTexture(url, type, content, maxNumPixels), + &Resource::deleter); } NetworkTexture::NetworkTexture(const QUrl& url, Type type, const QByteArray& content, int maxNumPixels) : @@ -346,6 +260,7 @@ NetworkTexture::NetworkTexture(const QUrl& url, Type type, const QByteArray& con _loaded = true; } + std::string theName = url.toString().toStdString(); // if we have content, load it after we have our self pointer if (!content.isEmpty()) { _startedLoading = true; @@ -353,6 +268,12 @@ NetworkTexture::NetworkTexture(const QUrl& url, Type type, const QByteArray& con } } +NetworkTexture::NetworkTexture(const QUrl& url, const TextureLoaderFunc& textureLoader, const QByteArray& content) : + NetworkTexture(url, CUSTOM_TEXTURE, content, ABSOLUTE_MAX_TEXTURE_NUM_PIXELS) +{ + _textureLoader = textureLoader; +} + NetworkTexture::TextureLoaderFunc NetworkTexture::getTextureLoader() const { if (_type == CUSTOM_TEXTURE) { return _textureLoader; @@ -360,6 +281,149 @@ NetworkTexture::TextureLoaderFunc NetworkTexture::getTextureLoader() const { return getTextureLoaderForType(_type); } + +class ImageReader : public QRunnable { +public: + + ImageReader(const QWeakPointer& resource, const QByteArray& data, + const QUrl& url = QUrl(), int maxNumPixels = ABSOLUTE_MAX_TEXTURE_NUM_PIXELS); + + virtual void run() override; + +private: + static void listSupportedImageFormats(); + + QWeakPointer _resource; + QUrl _url; + QByteArray _content; + int _maxNumPixels; +}; + +void NetworkTexture::downloadFinished(const QByteArray& data) { + // send the reader off to the thread pool + QThreadPool::globalInstance()->start(new ImageReader(_self, data, _url)); +} + +void NetworkTexture::loadContent(const QByteArray& content) { + QThreadPool::globalInstance()->start(new ImageReader(_self, content, _url, _maxNumPixels)); +} + +ImageReader::ImageReader(const QWeakPointer& resource, const QByteArray& data, + const QUrl& url, int maxNumPixels) : + _resource(resource), + _url(url), + _content(data), + _maxNumPixels(maxNumPixels) +{ +#if DEBUG_DUMP_TEXTURE_LOADS + static auto start = usecTimestampNow() / USECS_PER_MSEC; + auto now = usecTimestampNow() / USECS_PER_MSEC - start; + QString urlStr = _url.toString(); + auto dot = urlStr.lastIndexOf("."); + QString outFileName = QString(QCryptographicHash::hash(urlStr.toLocal8Bit(), QCryptographicHash::Md5).toHex()) + urlStr.right(urlStr.length() - dot); + QFile loadRecord("h:/textures/loads.txt"); + loadRecord.open(QFile::Text | QFile::Append | QFile::ReadWrite); + loadRecord.write(QString("%1 %2\n").arg(now).arg(outFileName).toLocal8Bit()); + outFileName = "h:/textures/" + outFileName; + QFileInfo outInfo(outFileName); + if (!outInfo.exists()) { + QFile outFile(outFileName); + outFile.open(QFile::WriteOnly | QFile::Truncate); + outFile.write(data); + outFile.close(); + } +#endif + DependencyManager::get()->incrementStat("PendingProcessing"); +} + +void ImageReader::listSupportedImageFormats() { + static std::once_flag once; + std::call_once(once, []{ + auto supportedFormats = QImageReader::supportedImageFormats(); + qCDebug(modelnetworking) << "List of supported Image formats:" << supportedFormats.join(", "); + }); +} + +void ImageReader::run() { + DependencyManager::get()->decrementStat("PendingProcessing"); + + CounterStat counter("Processing"); + + PROFILE_RANGE_EX(resource_parse_image, __FUNCTION__, 0xffff0000, 0, { { "url", _url.toString() } }); + auto originalPriority = QThread::currentThread()->priority(); + if (originalPriority == QThread::InheritPriority) { + originalPriority = QThread::NormalPriority; + } + QThread::currentThread()->setPriority(QThread::LowPriority); + Finally restorePriority([originalPriority]{ + QThread::currentThread()->setPriority(originalPriority); + }); + + if (!_resource.data()) { + qCWarning(modelnetworking) << "Abandoning load of" << _url << "; could not get strong ref"; + return; + } + listSupportedImageFormats(); + + // Help the QImage loader by extracting the image file format from the url filename ext. + // Some tga are not created properly without it. + auto filename = _url.fileName().toStdString(); + auto filenameExtension = filename.substr(filename.find_last_of('.') + 1); + QImage image = QImage::fromData(_content, filenameExtension.c_str()); + + // Note that QImage.format is the pixel format which is different from the "format" of the image file... + auto imageFormat = image.format(); + int imageWidth = image.width(); + int imageHeight = image.height(); + + if (imageWidth == 0 || imageHeight == 0 || imageFormat == QImage::Format_Invalid) { + if (filenameExtension.empty()) { + qCDebug(modelnetworking) << "QImage failed to create from content, no file extension:" << _url; + } else { + qCDebug(modelnetworking) << "QImage failed to create from content" << _url; + } + return; + } + + if (imageWidth * imageHeight > _maxNumPixels) { + float scaleFactor = sqrtf(_maxNumPixels / (float)(imageWidth * imageHeight)); + int originalWidth = imageWidth; + int originalHeight = imageHeight; + imageWidth = (int)(scaleFactor * (float)imageWidth + 0.5f); + imageHeight = (int)(scaleFactor * (float)imageHeight + 0.5f); + QImage newImage = image.scaled(QSize(imageWidth, imageHeight), Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + image.swap(newImage); + qCDebug(modelnetworking) << "Downscale image" << _url + << "from" << originalWidth << "x" << originalHeight + << "to" << imageWidth << "x" << imageHeight; + } + + gpu::TexturePointer texture = nullptr; + { + // Double-check the resource still exists between long operations. + auto resource = _resource.toStrongRef(); + if (!resource) { + qCWarning(modelnetworking) << "Abandoning load of" << _url << "; could not get strong ref"; + return; + } + + auto url = _url.toString().toStdString(); + + PROFILE_RANGE_EX(resource_parse_image, __FUNCTION__, 0xffffff00, 0); + texture.reset(resource.dynamicCast()->getTextureLoader()(image, url)); + } + + // Ensure the resource has not been deleted + auto resource = _resource.toStrongRef(); + if (!resource) { + qCWarning(modelnetworking) << "Abandoning load of" << _url << "; could not get strong ref"; + } else { + QMetaObject::invokeMethod(resource.data(), "setImage", + Q_ARG(gpu::TexturePointer, texture), + Q_ARG(int, imageWidth), Q_ARG(int, imageHeight)); + } +} + void NetworkTexture::setImage(gpu::TexturePointer texture, int originalWidth, int originalHeight) { _originalWidth = originalWidth; @@ -382,231 +446,3 @@ void NetworkTexture::setImage(gpu::TexturePointer texture, int originalWidth, emit networkTextureCreated(qWeakPointerCast (_self)); } - -gpu::TexturePointer NetworkTexture::getFallbackTexture() const { - if (_type == CUSTOM_TEXTURE) { - return gpu::TexturePointer(); - } - return getFallbackTextureForType(_type); -} - -class Reader : public QRunnable { -public: - Reader(const QWeakPointer& resource, const QUrl& url); - void run() override final; - virtual void read() = 0; - -protected: - QWeakPointer _resource; - QUrl _url; -}; - -class ImageReader : public Reader { -public: - ImageReader(const QWeakPointer& resource, const QUrl& url, - const QByteArray& data, const std::string& hash, int maxNumPixels); - void read() override final; - -private: - static void listSupportedImageFormats(); - - QByteArray _content; - std::string _hash; - int _maxNumPixels; -}; - -void NetworkTexture::downloadFinished(const QByteArray& data) { - loadContent(data); -} - -void NetworkTexture::loadContent(const QByteArray& content) { - // Hash the source image to for KTX caching - std::string hash; - { - QCryptographicHash hasher(QCryptographicHash::Md5); - hasher.addData(content); - hash = hasher.result().toHex().toStdString(); - } - - auto textureCache = static_cast(_cache.data()); - - if (textureCache != nullptr) { - // If we already have a live texture with the same hash, use it - auto texture = textureCache->getTextureByHash(hash); - - // If there is no live texture, check if there's an existing KTX file - if (!texture) { - KTXFilePointer ktxFile = textureCache->_ktxCache.getFile(hash); - if (ktxFile) { - // Ensure that the KTX deserialization worked - auto ktx = ktxFile->getKTX(); - if (ktx) { - texture.reset(gpu::Texture::unserialize(ktx)); - // Ensure that the texture population worked - if (texture) { - texture->setKtxBacking(ktx); - texture = textureCache->cacheTextureByHash(hash, texture); - } - } - } - } - - // If we found the texture either because it's in use or via KTX deserialization, - // set the image and return immediately. - if (texture) { - setImage(texture, texture->getWidth(), texture->getHeight()); - return; - } - } - - // We failed to find an existing live or KTX texture, so trigger an image reader - QThreadPool::globalInstance()->start(new ImageReader(_self, _url, content, hash, _maxNumPixels)); -} - -Reader::Reader(const QWeakPointer& resource, const QUrl& url) : - _resource(resource), _url(url) { - DependencyManager::get()->incrementStat("PendingProcessing"); -} - -void Reader::run() { - PROFILE_RANGE_EX(resource_parse_image, __FUNCTION__, 0xffff0000, 0, { { "url", _url.toString() } }); - DependencyManager::get()->decrementStat("PendingProcessing"); - CounterStat counter("Processing"); - - auto originalPriority = QThread::currentThread()->priority(); - if (originalPriority == QThread::InheritPriority) { - originalPriority = QThread::NormalPriority; - } - QThread::currentThread()->setPriority(QThread::LowPriority); - Finally restorePriority([originalPriority]{ QThread::currentThread()->setPriority(originalPriority); }); - - if (!_resource.data()) { - qCWarning(modelnetworking) << "Abandoning load of" << _url << "; could not get strong ref"; - return; - } - - read(); -} - -ImageReader::ImageReader(const QWeakPointer& resource, const QUrl& url, - const QByteArray& data, const std::string& hash, int maxNumPixels) : - Reader(resource, url), _content(data), _hash(hash), _maxNumPixels(maxNumPixels) { - listSupportedImageFormats(); - -#if DEBUG_DUMP_TEXTURE_LOADS - static auto start = usecTimestampNow() / USECS_PER_MSEC; - auto now = usecTimestampNow() / USECS_PER_MSEC - start; - QString urlStr = _url.toString(); - auto dot = urlStr.lastIndexOf("."); - QString outFileName = QString(QCryptographicHash::hash(urlStr.toLocal8Bit(), QCryptographicHash::Md5).toHex()) + urlStr.right(urlStr.length() - dot); - QFile loadRecord("h:/textures/loads.txt"); - loadRecord.open(QFile::Text | QFile::Append | QFile::ReadWrite); - loadRecord.write(QString("%1 %2\n").arg(now).arg(outFileName).toLocal8Bit()); - outFileName = "h:/textures/" + outFileName; - QFileInfo outInfo(outFileName); - if (!outInfo.exists()) { - QFile outFile(outFileName); - outFile.open(QFile::WriteOnly | QFile::Truncate); - outFile.write(data); - outFile.close(); - } -#endif -} - -void ImageReader::listSupportedImageFormats() { - static std::once_flag once; - std::call_once(once, []{ - auto supportedFormats = QImageReader::supportedImageFormats(); - qCDebug(modelnetworking) << "List of supported Image formats:" << supportedFormats.join(", "); - }); -} - -void ImageReader::read() { - // Help the QImage loader by extracting the image file format from the url filename ext. - // Some tga are not created properly without it. - auto filename = _url.fileName().toStdString(); - auto filenameExtension = filename.substr(filename.find_last_of('.') + 1); - QImage image = QImage::fromData(_content, filenameExtension.c_str()); - int imageWidth = image.width(); - int imageHeight = image.height(); - - // Validate that the image loaded - if (imageWidth == 0 || imageHeight == 0 || image.format() == QImage::Format_Invalid) { - QString reason(filenameExtension.empty() ? "" : "(no file extension)"); - qCWarning(modelnetworking) << "Failed to load" << _url << reason; - return; - } - - // Validate the image is less than _maxNumPixels, and downscale if necessary - if (imageWidth * imageHeight > _maxNumPixels) { - float scaleFactor = sqrtf(_maxNumPixels / (float)(imageWidth * imageHeight)); - int originalWidth = imageWidth; - int originalHeight = imageHeight; - imageWidth = (int)(scaleFactor * (float)imageWidth + 0.5f); - imageHeight = (int)(scaleFactor * (float)imageHeight + 0.5f); - QImage newImage = image.scaled(QSize(imageWidth, imageHeight), Qt::IgnoreAspectRatio, Qt::SmoothTransformation); - image.swap(newImage); - qCDebug(modelnetworking).nospace() << "Downscaled " << _url << " (" << - QSize(originalWidth, originalHeight) << " to " << - QSize(imageWidth, imageHeight) << ")"; - } - - gpu::TexturePointer texture = nullptr; - { - auto resource = _resource.lock(); // to ensure the resource is still needed - if (!resource) { - qCDebug(modelnetworking) << _url << "loading stopped; resource out of scope"; - return; - } - - auto url = _url.toString().toStdString(); - - PROFILE_RANGE_EX(resource_parse_image_raw, __FUNCTION__, 0xffff0000, 0); - // Load the image into a gpu::Texture - auto networkTexture = resource.staticCast(); - texture.reset(networkTexture->getTextureLoader()(image, url)); - texture->setSource(url); - if (texture) { - texture->setFallbackTexture(networkTexture->getFallbackTexture()); - } - - auto textureCache = DependencyManager::get(); - // Save the image into a KTXFile - auto memKtx = gpu::Texture::serialize(*texture); - if (!memKtx) { - qCWarning(modelnetworking) << "Unable to serialize texture to KTX " << _url; - } - - if (memKtx && textureCache) { - const char* data = reinterpret_cast(memKtx->_storage->data()); - size_t length = memKtx->_storage->size(); - KTXFilePointer file; - auto& ktxCache = textureCache->_ktxCache; - if (!memKtx || !(file = ktxCache.writeFile(data, KTXCache::Metadata(_hash, length)))) { - qCWarning(modelnetworking) << _url << "file cache failed"; - } else { - resource.staticCast()->_file = file; - auto fileKtx = file->getKTX(); - if (fileKtx) { - texture->setKtxBacking(fileKtx); - } - } - } - - // We replace the texture with the one stored in the cache. This deals with the possible race condition of two different - // images with the same hash being loaded concurrently. Only one of them will make it into the cache by hash first and will - // be the winner - if (textureCache) { - texture = textureCache->cacheTextureByHash(_hash, texture); - } - } - - auto resource = _resource.lock(); // to ensure the resource is still needed - if (resource) { - QMetaObject::invokeMethod(resource.data(), "setImage", - Q_ARG(gpu::TexturePointer, texture), - Q_ARG(int, imageWidth), Q_ARG(int, imageHeight)); - } else { - qCDebug(modelnetworking) << _url << "loading stopped; resource out of scope"; - } -} diff --git a/libraries/model-networking/src/model-networking/TextureCache.h b/libraries/model-networking/src/model-networking/TextureCache.h index 6005cc1226..77311afae6 100644 --- a/libraries/model-networking/src/model-networking/TextureCache.h +++ b/libraries/model-networking/src/model-networking/TextureCache.h @@ -23,8 +23,6 @@ #include #include -#include "KTXCache.h" - const int ABSOLUTE_MAX_TEXTURE_NUM_PIXELS = 8192 * 8192; namespace gpu { @@ -45,7 +43,6 @@ class NetworkTexture : public Resource, public Texture { public: enum Type { DEFAULT_TEXTURE, - STRICT_TEXTURE, ALBEDO_TEXTURE, NORMAL_TEXTURE, BUMP_TEXTURE, @@ -66,6 +63,7 @@ public: using TextureLoaderFunc = std::function; NetworkTexture(const QUrl& url, Type type, const QByteArray& content, int maxNumPixels); + NetworkTexture(const QUrl& url, const TextureLoaderFunc& textureLoader, const QByteArray& content); QString getType() const override { return "NetworkTexture"; } @@ -76,12 +74,12 @@ public: Type getTextureType() const { return _type; } TextureLoaderFunc getTextureLoader() const; - gpu::TexturePointer getFallbackTexture() const; signals: void networkTextureCreated(const QWeakPointer& self); protected: + virtual bool isCacheable() const override { return _loaded; } virtual void downloadFinished(const QByteArray& data) override; @@ -90,12 +88,8 @@ protected: Q_INVOKABLE void setImage(gpu::TexturePointer texture, int originalWidth, int originalHeight); private: - friend class KTXReader; - friend class ImageReader; - Type _type; TextureLoaderFunc _textureLoader { [](const QImage&, const std::string&){ return nullptr; } }; - KTXFilePointer _file; int _originalWidth { 0 }; int _originalHeight { 0 }; int _width { 0 }; @@ -137,10 +131,6 @@ public: NetworkTexturePointer getTexture(const QUrl& url, Type type = Type::DEFAULT_TEXTURE, const QByteArray& content = QByteArray(), int maxNumPixels = ABSOLUTE_MAX_TEXTURE_NUM_PIXELS); - - gpu::TexturePointer getTextureByHash(const std::string& hash); - gpu::TexturePointer cacheTextureByHash(const std::string& hash, const gpu::TexturePointer& texture); - protected: // Overload ResourceCache::prefetch to allow specifying texture type for loads Q_INVOKABLE ScriptableResource* prefetch(const QUrl& url, int type, int maxNumPixels = ABSOLUTE_MAX_TEXTURE_NUM_PIXELS); @@ -149,19 +139,9 @@ protected: const void* extra) override; private: - friend class ImageReader; - friend class NetworkTexture; - friend class DilatableNetworkTexture; - TextureCache(); virtual ~TextureCache(); - - static const std::string KTX_DIRNAME; - static const std::string KTX_EXT; - KTXCache _ktxCache; - // Map from image hashes to texture weak pointers - std::unordered_map> _texturesByHashes; - std::mutex _texturesByHashesMutex; + friend class DilatableNetworkTexture; gpu::TexturePointer _permutationNormalTexture; gpu::TexturePointer _whiteTexture; diff --git a/libraries/model/CMakeLists.txt b/libraries/model/CMakeLists.txt index 021aa3d027..63f632e484 100755 --- a/libraries/model/CMakeLists.txt +++ b/libraries/model/CMakeLists.txt @@ -1,5 +1,5 @@ set(TARGET_NAME model) AUTOSCRIBE_SHADER_LIB(gpu model) setup_hifi_library() -link_hifi_libraries(shared ktx gpu) +link_hifi_libraries(shared gpu) diff --git a/libraries/model/src/model/Geometry.cpp b/libraries/model/src/model/Geometry.cpp index 04b0db92d3..2bb6cfa436 100755 --- a/libraries/model/src/model/Geometry.cpp +++ b/libraries/model/src/model/Geometry.cpp @@ -117,7 +117,7 @@ Box Mesh::evalPartsBound(int partStart, int partEnd) const { auto partItEnd = _partBuffer.cbegin() + partEnd; for (;part != partItEnd; part++) { - + Box partBound; auto index = _indexBuffer.cbegin() + (*part)._startIndex; auto endIndex = index + (*part)._numIndices; @@ -134,115 +134,6 @@ Box Mesh::evalPartsBound(int partStart, int partEnd) const { return totalBound; } - -model::MeshPointer Mesh::map(std::function vertexFunc, - std::function normalFunc, - std::function indexFunc) { - // vertex data - const gpu::BufferView& vertexBufferView = getVertexBuffer(); - gpu::BufferView::Index numVertices = (gpu::BufferView::Index)getNumVertices(); - gpu::Resource::Size vertexSize = numVertices * sizeof(glm::vec3); - unsigned char* resultVertexData = new unsigned char[vertexSize]; - unsigned char* vertexDataCursor = resultVertexData; - - for (gpu::BufferView::Index i = 0; i < numVertices; i ++) { - glm::vec3 pos = vertexFunc(vertexBufferView.get(i)); - memcpy(vertexDataCursor, &pos, sizeof(pos)); - vertexDataCursor += sizeof(pos); - } - - // normal data - int attributeTypeNormal = gpu::Stream::InputSlot::NORMAL; // libraries/gpu/src/gpu/Stream.h - const gpu::BufferView& normalsBufferView = getAttributeBuffer(attributeTypeNormal); - gpu::BufferView::Index numNormals = (gpu::BufferView::Index)normalsBufferView.getNumElements(); - gpu::Resource::Size normalSize = numNormals * sizeof(glm::vec3); - unsigned char* resultNormalData = new unsigned char[normalSize]; - unsigned char* normalDataCursor = resultNormalData; - - for (gpu::BufferView::Index i = 0; i < numNormals; i ++) { - glm::vec3 normal = normalFunc(normalsBufferView.get(i)); - memcpy(normalDataCursor, &normal, sizeof(normal)); - normalDataCursor += sizeof(normal); - } - // TODO -- other attributes - - // face data - const gpu::BufferView& indexBufferView = getIndexBuffer(); - gpu::BufferView::Index numIndexes = (gpu::BufferView::Index)getNumIndices(); - gpu::Resource::Size indexSize = numIndexes * sizeof(uint32_t); - unsigned char* resultIndexData = new unsigned char[indexSize]; - unsigned char* indexDataCursor = resultIndexData; - - for (gpu::BufferView::Index i = 0; i < numIndexes; i ++) { - uint32_t index = indexFunc(indexBufferView.get(i)); - memcpy(indexDataCursor, &index, sizeof(index)); - indexDataCursor += sizeof(index); - } - - model::MeshPointer result(new model::Mesh()); - - gpu::Element vertexElement = gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ); - gpu::Buffer* resultVertexBuffer = new gpu::Buffer(vertexSize, resultVertexData); - gpu::BufferPointer resultVertexBufferPointer(resultVertexBuffer); - gpu::BufferView resultVertexBufferView(resultVertexBufferPointer, vertexElement); - result->setVertexBuffer(resultVertexBufferView); - - gpu::Element normalElement = gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ); - gpu::Buffer* resultNormalsBuffer = new gpu::Buffer(normalSize, resultNormalData); - gpu::BufferPointer resultNormalsBufferPointer(resultNormalsBuffer); - gpu::BufferView resultNormalsBufferView(resultNormalsBufferPointer, normalElement); - result->addAttribute(attributeTypeNormal, resultNormalsBufferView); - - gpu::Element indexElement = gpu::Element(gpu::SCALAR, gpu::UINT32, gpu::RAW); - gpu::Buffer* resultIndexesBuffer = new gpu::Buffer(indexSize, resultIndexData); - gpu::BufferPointer resultIndexesBufferPointer(resultIndexesBuffer); - gpu::BufferView resultIndexesBufferView(resultIndexesBufferPointer, indexElement); - result->setIndexBuffer(resultIndexesBufferView); - - - // TODO -- shouldn't assume just one part - - std::vector parts; - parts.emplace_back(model::Mesh::Part((model::Index)0, // startIndex - (model::Index)result->getNumIndices(), // numIndices - (model::Index)0, // baseVertex - model::Mesh::TRIANGLES)); // topology - result->setPartBuffer(gpu::BufferView(new gpu::Buffer(parts.size() * sizeof(model::Mesh::Part), - (gpu::Byte*) parts.data()), gpu::Element::PART_DRAWCALL)); - - return result; -} - - -void Mesh::forEach(std::function vertexFunc, - std::function normalFunc, - std::function indexFunc) { - int attributeTypeNormal = gpu::Stream::InputSlot::NORMAL; // libraries/gpu/src/gpu/Stream.h - - // vertex data - const gpu::BufferView& vertexBufferView = getVertexBuffer(); - gpu::BufferView::Index numVertices = (gpu::BufferView::Index)getNumVertices(); - for (gpu::BufferView::Index i = 0; i < numVertices; i ++) { - vertexFunc(vertexBufferView.get(i)); - } - - // normal data - const gpu::BufferView& normalsBufferView = getAttributeBuffer(attributeTypeNormal); - gpu::BufferView::Index numNormals = (gpu::BufferView::Index) normalsBufferView.getNumElements(); - for (gpu::BufferView::Index i = 0; i < numNormals; i ++) { - normalFunc(normalsBufferView.get(i)); - } - // TODO -- other attributes - - // face data - const gpu::BufferView& indexBufferView = getIndexBuffer(); - gpu::BufferView::Index numIndexes = (gpu::BufferView::Index)getNumIndices(); - for (gpu::BufferView::Index i = 0; i < numIndexes; i ++) { - indexFunc(indexBufferView.get(i)); - } -} - - Geometry::Geometry() { } @@ -257,3 +148,4 @@ Geometry::~Geometry() { void Geometry::setMesh(const MeshPointer& mesh) { _mesh = mesh; } + diff --git a/libraries/model/src/model/Geometry.h b/libraries/model/src/model/Geometry.h index 7ba3e83407..4256f0be03 100755 --- a/libraries/model/src/model/Geometry.h +++ b/libraries/model/src/model/Geometry.h @@ -25,10 +25,6 @@ typedef AABox Box; typedef std::vector< Box > Boxes; typedef glm::vec3 Vec3; -class Mesh; -using MeshPointer = std::shared_ptr< Mesh >; - - class Mesh { public: const static Index PRIMITIVE_RESTART_INDEX = -1; @@ -118,15 +114,6 @@ public: static gpu::Primitive topologyToPrimitive(Topology topo) { return static_cast(topo); } - // create a copy of this mesh after passing its vertices, normals, and indexes though the provided functions - MeshPointer map(std::function vertexFunc, - std::function normalFunc, - std::function indexFunc); - - void forEach(std::function vertexFunc, - std::function normalFunc, - std::function indexFunc); - protected: gpu::Stream::FormatPointer _vertexFormat; @@ -143,6 +130,7 @@ protected: void evalVertexStream(); }; +using MeshPointer = std::shared_ptr< Mesh >; class Geometry { diff --git a/libraries/model/src/model/TextureMap.cpp b/libraries/model/src/model/TextureMap.cpp index d07eae2166..7ac8083d9c 100755 --- a/libraries/model/src/model/TextureMap.cpp +++ b/libraries/model/src/model/TextureMap.cpp @@ -10,15 +10,10 @@ // #include "TextureMap.h" -#include - #include #include #include -#include -#include -#include -#include + #include #include "ModelLogging.h" @@ -154,7 +149,7 @@ const QImage TextureUsage::process2DImageColor(const QImage& srcImage, bool& val return image; } -void TextureUsage::defineColorTexelFormats(gpu::Element& formatGPU, gpu::Element& formatMip, +void TextureUsage::defineColorTexelFormats(gpu::Element& formatGPU, gpu::Element& formatMip, const QImage& image, bool isLinear, bool doCompress) { #ifdef COMPRESS_TEXTURES @@ -207,7 +202,7 @@ const QImage& image, bool isLinear, bool doCompress) { #define CPU_MIPMAPS 1 -void generateMips(gpu::Texture* texture, QImage& image, bool fastResize) { +void generateMips(gpu::Texture* texture, QImage& image, gpu::Element formatMip, bool fastResize) { #if CPU_MIPMAPS PROFILE_RANGE(resource_parse, "generateMips"); auto numMips = texture->evalNumMips(); @@ -215,33 +210,32 @@ void generateMips(gpu::Texture* texture, QImage& image, bool fastResize) { QSize mipSize(texture->evalMipWidth(level), texture->evalMipHeight(level)); if (fastResize) { image = image.scaled(mipSize); - texture->assignStoredMip(level, image.byteCount(), image.constBits()); + texture->assignStoredMip(level, formatMip, image.byteCount(), image.constBits()); } else { QImage mipImage = image.scaled(mipSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); - texture->assignStoredMip(level, mipImage.byteCount(), mipImage.constBits()); + texture->assignStoredMip(level, formatMip, mipImage.byteCount(), mipImage.constBits()); } } - #else texture->autoGenerateMips(-1); #endif } -void generateFaceMips(gpu::Texture* texture, QImage& image, uint8 face) { +void generateFaceMips(gpu::Texture* texture, QImage& image, gpu::Element formatMip, uint8 face) { #if CPU_MIPMAPS PROFILE_RANGE(resource_parse, "generateFaceMips"); auto numMips = texture->evalNumMips(); for (uint16 level = 1; level < numMips; ++level) { QSize mipSize(texture->evalMipWidth(level), texture->evalMipHeight(level)); QImage mipImage = image.scaled(mipSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); - texture->assignStoredMipFace(level, face, mipImage.byteCount(), mipImage.constBits()); + texture->assignStoredMipFace(level, formatMip, mipImage.byteCount(), mipImage.constBits(), face); } #else texture->autoGenerateMips(-1); #endif } -gpu::Texture* TextureUsage::process2DTextureColorFromImage(const QImage& srcImage, const std::string& srcImageName, bool isLinear, bool doCompress, bool generateMips, bool isStrict) { +gpu::Texture* TextureUsage::process2DTextureColorFromImage(const QImage& srcImage, const std::string& srcImageName, bool isLinear, bool doCompress, bool generateMips) { PROFILE_RANGE(resource_parse, "process2DTextureColorFromImage"); bool validAlpha = false; bool alphaAsMask = true; @@ -254,11 +248,7 @@ gpu::Texture* TextureUsage::process2DTextureColorFromImage(const QImage& srcImag gpu::Element formatMip; defineColorTexelFormats(formatGPU, formatMip, image, isLinear, doCompress); - if (isStrict) { - theTexture = (gpu::Texture::createStrict(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); - } else { - theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); - } + theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); theTexture->setSource(srcImageName); auto usage = gpu::Texture::Usage::Builder().withColor(); if (validAlpha) { @@ -268,26 +258,22 @@ gpu::Texture* TextureUsage::process2DTextureColorFromImage(const QImage& srcImag } } theTexture->setUsage(usage.build()); - theTexture->setStoredMipFormat(formatMip); - theTexture->assignStoredMip(0, image.byteCount(), image.constBits()); + + theTexture->assignStoredMip(0, formatMip, image.byteCount(), image.constBits()); if (generateMips) { - ::generateMips(theTexture, image, false); + ::generateMips(theTexture, image, formatMip, false); } - theTexture->setSource(srcImageName); } return theTexture; } -gpu::Texture* TextureUsage::createStrict2DTextureFromImage(const QImage& srcImage, const std::string& srcImageName) { - return process2DTextureColorFromImage(srcImage, srcImageName, false, false, true, true); -} - gpu::Texture* TextureUsage::create2DTextureFromImage(const QImage& srcImage, const std::string& srcImageName) { return process2DTextureColorFromImage(srcImage, srcImageName, false, false, true); } + gpu::Texture* TextureUsage::createAlbedoTextureFromImage(const QImage& srcImage, const std::string& srcImageName) { return process2DTextureColorFromImage(srcImage, srcImageName, false, true, true); } @@ -305,25 +291,21 @@ gpu::Texture* TextureUsage::createNormalTextureFromNormalImage(const QImage& src PROFILE_RANGE(resource_parse, "createNormalTextureFromNormalImage"); QImage image = processSourceImage(srcImage, false); - // Make sure the normal map source image is ARGB32 - if (image.format() != QImage::Format_ARGB32) { - image = image.convertToFormat(QImage::Format_ARGB32); + // Make sure the normal map source image is RGBA32 + if (image.format() != QImage::Format_RGBA8888) { + image = image.convertToFormat(QImage::Format_RGBA8888); } - gpu::Texture* theTexture = nullptr; if ((image.width() > 0) && (image.height() > 0)) { - gpu::Element formatMip = gpu::Element::COLOR_BGRA_32; - gpu::Element formatGPU = gpu::Element::COLOR_RGBA_32; + gpu::Element formatGPU = gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA); + gpu::Element formatMip = gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA); theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); theTexture->setSource(srcImageName); - theTexture->setStoredMipFormat(formatMip); - theTexture->assignStoredMip(0, image.byteCount(), image.constBits()); - generateMips(theTexture, image, true); - - theTexture->setSource(srcImageName); + theTexture->assignStoredMip(0, formatMip, image.byteCount(), image.constBits()); + generateMips(theTexture, image, formatMip, true); } return theTexture; @@ -354,17 +336,16 @@ gpu::Texture* TextureUsage::createNormalTextureFromBumpImage(const QImage& srcIm const double pStrength = 2.0; int width = image.width(); int height = image.height(); - - QImage result(width, height, QImage::Format_ARGB32); - + QImage result(width, height, QImage::Format_RGB888); + for (int i = 0; i < width; i++) { const int iNextClamped = clampPixelCoordinate(i + 1, width - 1); const int iPrevClamped = clampPixelCoordinate(i - 1, width - 1); - + for (int j = 0; j < height; j++) { const int jNextClamped = clampPixelCoordinate(j + 1, height - 1); const int jPrevClamped = clampPixelCoordinate(j - 1, height - 1); - + // surrounding pixels const QRgb topLeft = image.pixel(iPrevClamped, jPrevClamped); const QRgb top = image.pixel(iPrevClamped, j); @@ -374,7 +355,7 @@ gpu::Texture* TextureUsage::createNormalTextureFromBumpImage(const QImage& srcIm const QRgb bottom = image.pixel(iNextClamped, j); const QRgb bottomLeft = image.pixel(iNextClamped, jPrevClamped); const QRgb left = image.pixel(i, jPrevClamped); - + // take their gray intensities // since it's a grayscale image, the value of each component RGB is the same const double tl = qRed(topLeft); @@ -385,15 +366,15 @@ gpu::Texture* TextureUsage::createNormalTextureFromBumpImage(const QImage& srcIm const double b = qRed(bottom); const double bl = qRed(bottomLeft); const double l = qRed(left); - + // apply the sobel filter const double dX = (tr + pStrength * r + br) - (tl + pStrength * l + bl); const double dY = (bl + pStrength * b + br) - (tl + pStrength * t + tr); const double dZ = RGBA_MAX / pStrength; - + glm::vec3 v(dX, dY, dZ); glm::normalize(v); - + // convert to rgb from the value obtained computing the filter QRgb qRgbValue = qRgba(mapComponent(v.x), mapComponent(v.y), mapComponent(v.z), 1.0); result.setPixel(i, j, qRgbValue); @@ -401,19 +382,13 @@ gpu::Texture* TextureUsage::createNormalTextureFromBumpImage(const QImage& srcIm } gpu::Texture* theTexture = nullptr; - if ((result.width() > 0) && (result.height() > 0)) { - - gpu::Element formatMip = gpu::Element::COLOR_BGRA_32; - gpu::Element formatGPU = gpu::Element::COLOR_RGBA_32; - - - theTexture = (gpu::Texture::create2D(formatGPU, result.width(), result.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); - theTexture->setSource(srcImageName); - theTexture->setStoredMipFormat(formatMip); - theTexture->assignStoredMip(0, result.byteCount(), result.constBits()); - generateMips(theTexture, result, true); + if ((image.width() > 0) && (image.height() > 0)) { + gpu::Element formatGPU = gpu::Element(gpu::VEC3, gpu::NUINT8, gpu::RGB); + gpu::Element formatMip = gpu::Element(gpu::VEC3, gpu::NUINT8, gpu::RGB); + theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); theTexture->setSource(srcImageName); + theTexture->assignStoredMip(0, formatMip, image.byteCount(), image.constBits()); } return theTexture; @@ -439,17 +414,16 @@ gpu::Texture* TextureUsage::createRoughnessTextureFromImage(const QImage& srcIma #ifdef COMPRESS_TEXTURES gpu::Element formatGPU = gpu::Element(gpu::SCALAR, gpu::NUINT8, gpu::COMPRESSED_R); #else - gpu::Element formatGPU = gpu::Element::COLOR_R_8; + gpu::Element formatGPU = gpu::Element(gpu::SCALAR, gpu::NUINT8, gpu::RGB); #endif - gpu::Element formatMip = gpu::Element::COLOR_R_8; + gpu::Element formatMip = gpu::Element(gpu::SCALAR, gpu::NUINT8, gpu::RGB); theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); theTexture->setSource(srcImageName); - theTexture->setStoredMipFormat(formatMip); - theTexture->assignStoredMip(0, image.byteCount(), image.constBits()); - generateMips(theTexture, image, true); + theTexture->assignStoredMip(0, formatMip, image.byteCount(), image.constBits()); + generateMips(theTexture, image, formatMip, true); - theTexture->setSource(srcImageName); + // FIXME queue for transfer to GPU and block on completion } return theTexture; @@ -470,28 +444,27 @@ gpu::Texture* TextureUsage::createRoughnessTextureFromGlossImage(const QImage& s // Gloss turned into Rough image.invertPixels(QImage::InvertRgba); - + image = image.convertToFormat(QImage::Format_Grayscale8); - + gpu::Texture* theTexture = nullptr; if ((image.width() > 0) && (image.height() > 0)) { - + #ifdef COMPRESS_TEXTURES gpu::Element formatGPU = gpu::Element(gpu::SCALAR, gpu::NUINT8, gpu::COMPRESSED_R); #else - gpu::Element formatGPU = gpu::Element::COLOR_R_8; + gpu::Element formatGPU = gpu::Element(gpu::SCALAR, gpu::NUINT8, gpu::RGB); #endif - gpu::Element formatMip = gpu::Element::COLOR_R_8; + gpu::Element formatMip = gpu::Element(gpu::SCALAR, gpu::NUINT8, gpu::RGB); theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); theTexture->setSource(srcImageName); - theTexture->setStoredMipFormat(formatMip); - theTexture->assignStoredMip(0, image.byteCount(), image.constBits()); - generateMips(theTexture, image, true); - - theTexture->setSource(srcImageName); + theTexture->assignStoredMip(0, formatMip, image.byteCount(), image.constBits()); + generateMips(theTexture, image, formatMip, true); + + // FIXME queue for transfer to GPU and block on completion } - + return theTexture; } @@ -516,17 +489,16 @@ gpu::Texture* TextureUsage::createMetallicTextureFromImage(const QImage& srcImag #ifdef COMPRESS_TEXTURES gpu::Element formatGPU = gpu::Element(gpu::SCALAR, gpu::NUINT8, gpu::COMPRESSED_R); #else - gpu::Element formatGPU = gpu::Element::COLOR_R_8; + gpu::Element formatGPU = gpu::Element(gpu::SCALAR, gpu::NUINT8, gpu::RGB); #endif - gpu::Element formatMip = gpu::Element::COLOR_R_8; + gpu::Element formatMip = gpu::Element(gpu::SCALAR, gpu::NUINT8, gpu::RGB); theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); theTexture->setSource(srcImageName); - theTexture->setStoredMipFormat(formatMip); - theTexture->assignStoredMip(0, image.byteCount(), image.constBits()); - generateMips(theTexture, image, true); + theTexture->assignStoredMip(0, formatMip, image.byteCount(), image.constBits()); + generateMips(theTexture, image, formatMip, true); - theTexture->setSource(srcImageName); + // FIXME queue for transfer to GPU and block on completion } return theTexture; @@ -549,18 +521,18 @@ public: int _y = 0; bool _horizontalMirror = false; bool _verticalMirror = false; - + Face() {} Face(int x, int y, bool horizontalMirror, bool verticalMirror) : _x(x), _y(y), _horizontalMirror(horizontalMirror), _verticalMirror(verticalMirror) {} }; - + Face _faceXPos; Face _faceXNeg; Face _faceYPos; Face _faceYNeg; Face _faceZPos; Face _faceZNeg; - + CubeLayout(int wr, int hr, Face fXP, Face fXN, Face fYP, Face fYN, Face fZP, Face fZN) : _type(FLAT), _widthRatio(wr), @@ -803,7 +775,7 @@ gpu::Texture* TextureUsage::processCubeTextureColorFromImage(const QImage& srcIm defineColorTexelFormats(formatGPU, formatMip, image, isLinear, doCompress); // Find the layout of the cubemap in the 2D image - // Use the original image size since processSourceImage may have altered the size / aspect ratio + // Use the original image size since processSourceImage may have altered the size / aspect ratio int foundLayout = CubeLayout::findLayout(srcImage.width(), srcImage.height()); std::vector faces; @@ -838,12 +810,11 @@ gpu::Texture* TextureUsage::processCubeTextureColorFromImage(const QImage& srcIm if (faces.size() == gpu::Texture::NUM_FACES_PER_TYPE[gpu::Texture::TEX_CUBE]) { theTexture = gpu::Texture::createCube(formatGPU, faces[0].width(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP)); theTexture->setSource(srcImageName); - theTexture->setStoredMipFormat(formatMip); int f = 0; for (auto& face : faces) { - theTexture->assignStoredMipFace(0, f, face.byteCount(), face.constBits()); + theTexture->assignStoredMipFace(0, formatMip, face.byteCount(), face.constBits(), f); if (generateMips) { - generateFaceMips(theTexture, face, f); + generateFaceMips(theTexture, face, formatMip, f); } f++; } @@ -858,8 +829,6 @@ gpu::Texture* TextureUsage::processCubeTextureColorFromImage(const QImage& srcIm PROFILE_RANGE(resource_parse, "generateIrradiance"); theTexture->generateIrradiance(); } - - theTexture->setSource(srcImageName); } } diff --git a/libraries/model/src/model/TextureMap.h b/libraries/model/src/model/TextureMap.h index a4bb861502..220ee57a97 100755 --- a/libraries/model/src/model/TextureMap.h +++ b/libraries/model/src/model/TextureMap.h @@ -32,7 +32,6 @@ public: int _environmentUsage = 0; static gpu::Texture* create2DTextureFromImage(const QImage& image, const std::string& srcImageName); - static gpu::Texture* createStrict2DTextureFromImage(const QImage& image, const std::string& srcImageName); static gpu::Texture* createAlbedoTextureFromImage(const QImage& image, const std::string& srcImageName); static gpu::Texture* createEmissiveTextureFromImage(const QImage& image, const std::string& srcImageName); static gpu::Texture* createNormalTextureFromNormalImage(const QImage& image, const std::string& srcImageName); @@ -48,7 +47,7 @@ public: static const QImage process2DImageColor(const QImage& srcImage, bool& validAlpha, bool& alphaAsMask); static void defineColorTexelFormats(gpu::Element& formatGPU, gpu::Element& formatMip, const QImage& srcImage, bool isLinear, bool doCompress); - static gpu::Texture* process2DTextureColorFromImage(const QImage& srcImage, const std::string& srcImageName, bool isLinear, bool doCompress, bool generateMips, bool isStrict = false); + static gpu::Texture* process2DTextureColorFromImage(const QImage& srcImage, const std::string& srcImageName, bool isLinear, bool doCompress, bool generateMips); static gpu::Texture* processCubeTextureColorFromImage(const QImage& srcImage, const std::string& srcImageName, bool isLinear, bool doCompress, bool generateMips, bool generateIrradiance); }; diff --git a/libraries/networking/src/Assignment.cpp b/libraries/networking/src/Assignment.cpp index 27d4a31ccf..9efad15398 100644 --- a/libraries/networking/src/Assignment.cpp +++ b/libraries/networking/src/Assignment.cpp @@ -12,6 +12,7 @@ #include "udt/PacketHeaders.h" #include "SharedUtil.h" #include "UUID.h" +#include "ServerPathUtils.h" #include diff --git a/libraries/networking/src/FileCache.cpp b/libraries/networking/src/FileCache.cpp deleted file mode 100644 index f8a86903cb..0000000000 --- a/libraries/networking/src/FileCache.cpp +++ /dev/null @@ -1,243 +0,0 @@ -// -// FileCache.cpp -// libraries/model-networking/src -// -// Created by Zach Pomerantz on 2/21/2017. -// Copyright 2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include "FileCache.h" - -#include -#include -#include -#include - -#include - -#include - -Q_LOGGING_CATEGORY(file_cache, "hifi.file_cache", QtWarningMsg) - -using namespace cache; - -static const std::string MANIFEST_NAME = "manifest"; - -static const size_t BYTES_PER_MEGABYTES = 1024 * 1024; -static const size_t BYTES_PER_GIGABYTES = 1024 * BYTES_PER_MEGABYTES; -const size_t FileCache::DEFAULT_UNUSED_MAX_SIZE = 5 * BYTES_PER_GIGABYTES; // 5GB -const size_t FileCache::MAX_UNUSED_MAX_SIZE = 100 * BYTES_PER_GIGABYTES; // 100GB -const size_t FileCache::DEFAULT_OFFLINE_MAX_SIZE = 2 * BYTES_PER_GIGABYTES; // 2GB - -void FileCache::setUnusedFileCacheSize(size_t unusedFilesMaxSize) { - _unusedFilesMaxSize = std::min(unusedFilesMaxSize, MAX_UNUSED_MAX_SIZE); - reserve(0); - emit dirty(); -} - -void FileCache::setOfflineFileCacheSize(size_t offlineFilesMaxSize) { - _offlineFilesMaxSize = std::min(offlineFilesMaxSize, MAX_UNUSED_MAX_SIZE); -} - -FileCache::FileCache(const std::string& dirname, const std::string& ext, QObject* parent) : - QObject(parent), - _ext(ext), - _dirname(dirname), - _dirpath(PathUtils::getAppLocalDataFilePath(dirname.c_str()).toStdString()) {} - -FileCache::~FileCache() { - clear(); -} - -void fileDeleter(File* file) { - file->deleter(); -} - -void FileCache::initialize() { - QDir dir(_dirpath.c_str()); - - if (dir.exists()) { - auto nameFilters = QStringList(("*." + _ext).c_str()); - auto filters = QDir::Filters(QDir::NoDotAndDotDot | QDir::Files); - auto sort = QDir::SortFlags(QDir::Time); - auto files = dir.entryList(nameFilters, filters, sort); - - // load persisted files - foreach(QString filename, files) { - const Key key = filename.section('.', 0, 1).toStdString(); - const std::string filepath = dir.filePath(filename).toStdString(); - const size_t length = std::ifstream(filepath, std::ios::binary | std::ios::ate).tellg(); - addFile(Metadata(key, length), filepath); - } - - qCDebug(file_cache, "[%s] Initialized %s", _dirname.c_str(), _dirpath.c_str()); - } else { - dir.mkpath(_dirpath.c_str()); - qCDebug(file_cache, "[%s] Created %s", _dirname.c_str(), _dirpath.c_str()); - } - - _initialized = true; -} - -FilePointer FileCache::addFile(Metadata&& metadata, const std::string& filepath) { - FilePointer file(createFile(std::move(metadata), filepath).release(), &fileDeleter); - if (file) { - _numTotalFiles += 1; - _totalFilesSize += file->getLength(); - file->_cache = this; - emit dirty(); - - Lock lock(_filesMutex); - _files[file->getKey()] = file; - } - return file; -} - -FilePointer FileCache::writeFile(const char* data, File::Metadata&& metadata) { - assert(_initialized); - - std::string filepath = getFilepath(metadata.key); - - Lock lock(_filesMutex); - - // if file already exists, return it - FilePointer file = getFile(metadata.key); - if (file) { - qCWarning(file_cache, "[%s] Attempted to overwrite %s", _dirname.c_str(), metadata.key.c_str()); - return file; - } - - // write the new file - FILE* saveFile = fopen(filepath.c_str(), "wb"); - if (saveFile != nullptr && fwrite(data, metadata.length, 1, saveFile) && fclose(saveFile) == 0) { - file = addFile(std::move(metadata), filepath); - } else { - qCWarning(file_cache, "[%s] Failed to write %s (%s)", _dirname.c_str(), metadata.key.c_str(), strerror(errno)); - errno = 0; - } - - return file; -} - -FilePointer FileCache::getFile(const Key& key) { - assert(_initialized); - - FilePointer file; - - Lock lock(_filesMutex); - - // check if file exists - const auto it = _files.find(key); - if (it != _files.cend()) { - file = it->second.lock(); - if (file) { - // if it exists, it is active - remove it from the cache - removeUnusedFile(file); - qCDebug(file_cache, "[%s] Found %s", _dirname.c_str(), key.c_str()); - emit dirty(); - } else { - // if not, remove the weak_ptr - _files.erase(it); - } - } - - return file; -} - -std::string FileCache::getFilepath(const Key& key) { - return _dirpath + '/' + key + '.' + _ext; -} - -void FileCache::addUnusedFile(const FilePointer file) { - { - Lock lock(_filesMutex); - _files[file->getKey()] = file; - } - - reserve(file->getLength()); - file->_LRUKey = ++_lastLRUKey; - - { - Lock lock(_unusedFilesMutex); - _unusedFiles.insert({ file->_LRUKey, file }); - _numUnusedFiles += 1; - _unusedFilesSize += file->getLength(); - } - - emit dirty(); -} - -void FileCache::removeUnusedFile(const FilePointer file) { - Lock lock(_unusedFilesMutex); - const auto it = _unusedFiles.find(file->_LRUKey); - if (it != _unusedFiles.cend()) { - _unusedFiles.erase(it); - _numUnusedFiles -= 1; - _unusedFilesSize -= file->getLength(); - } -} - -void FileCache::reserve(size_t length) { - Lock unusedLock(_unusedFilesMutex); - while (!_unusedFiles.empty() && - _unusedFilesSize + length > _unusedFilesMaxSize) { - auto it = _unusedFiles.begin(); - auto file = it->second; - auto length = file->getLength(); - - unusedLock.unlock(); - { - file->_cache = nullptr; - Lock lock(_filesMutex); - _files.erase(file->getKey()); - } - unusedLock.lock(); - - _unusedFiles.erase(it); - _numTotalFiles -= 1; - _numUnusedFiles -= 1; - _totalFilesSize -= length; - _unusedFilesSize -= length; - } -} - -void FileCache::clear() { - Lock unusedFilesLock(_unusedFilesMutex); - for (const auto& pair : _unusedFiles) { - auto& file = pair.second; - file->_cache = nullptr; - - if (_totalFilesSize > _offlineFilesMaxSize) { - _totalFilesSize -= file->getLength(); - } else { - file->_shouldPersist = true; - qCDebug(file_cache, "[%s] Persisting %s", _dirname.c_str(), file->getKey().c_str()); - } - } - _unusedFiles.clear(); -} - -void File::deleter() { - if (_cache) { - FilePointer self(this, &fileDeleter); - _cache->addUnusedFile(self); - } else { - deleteLater(); - } -} - -File::File(Metadata&& metadata, const std::string& filepath) : - _key(std::move(metadata.key)), - _length(metadata.length), - _filepath(filepath) {} - -File::~File() { - QFile file(getFilepath().c_str()); - if (file.exists() && !_shouldPersist) { - qCInfo(file_cache, "Unlinked %s", getFilepath().c_str()); - file.remove(); - } -} diff --git a/libraries/networking/src/FileCache.h b/libraries/networking/src/FileCache.h deleted file mode 100644 index f77db555bc..0000000000 --- a/libraries/networking/src/FileCache.h +++ /dev/null @@ -1,158 +0,0 @@ -// -// FileCache.h -// libraries/networking/src -// -// Created by Zach Pomerantz on 2/21/2017. -// Copyright 2017 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#ifndef hifi_FileCache_h -#define hifi_FileCache_h - -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -Q_DECLARE_LOGGING_CATEGORY(file_cache) - -namespace cache { - -class File; -using FilePointer = std::shared_ptr; - -class FileCache : public QObject { - Q_OBJECT - Q_PROPERTY(size_t numTotal READ getNumTotalFiles NOTIFY dirty) - Q_PROPERTY(size_t numCached READ getNumCachedFiles NOTIFY dirty) - Q_PROPERTY(size_t sizeTotal READ getSizeTotalFiles NOTIFY dirty) - Q_PROPERTY(size_t sizeCached READ getSizeCachedFiles NOTIFY dirty) - - static const size_t DEFAULT_UNUSED_MAX_SIZE; - static const size_t MAX_UNUSED_MAX_SIZE; - static const size_t DEFAULT_OFFLINE_MAX_SIZE; - -public: - size_t getNumTotalFiles() const { return _numTotalFiles; } - size_t getNumCachedFiles() const { return _numUnusedFiles; } - size_t getSizeTotalFiles() const { return _totalFilesSize; } - size_t getSizeCachedFiles() const { return _unusedFilesSize; } - - void setUnusedFileCacheSize(size_t unusedFilesMaxSize); - size_t getUnusedFileCacheSize() const { return _unusedFilesSize; } - - void setOfflineFileCacheSize(size_t offlineFilesMaxSize); - - // initialize FileCache with a directory name (not a path, ex.: "temp_jpgs") and an ext (ex.: "jpg") - FileCache(const std::string& dirname, const std::string& ext, QObject* parent = nullptr); - virtual ~FileCache(); - - using Key = std::string; - struct Metadata { - Metadata(const Key& key, size_t length) : - key(key), length(length) {} - Key key; - size_t length; - }; - - // derived classes should implement a setter/getter, for example, for a FileCache backing a network cache: - // - // DerivedFilePointer writeFile(const char* data, DerivedMetadata&& metadata) { - // return writeFile(data, std::forward(metadata)); - // } - // - // DerivedFilePointer getFile(const QUrl& url) { - // auto key = lookup_hash_for(url); // assuming hashing url in create/evictedFile overrides - // return getFile(key); - // } - -signals: - void dirty(); - -protected: - /// must be called after construction to create the cache on the fs and restore persisted files - void initialize(); - - FilePointer writeFile(const char* data, Metadata&& metadata); - FilePointer getFile(const Key& key); - - /// create a file - virtual std::unique_ptr createFile(Metadata&& metadata, const std::string& filepath) = 0; - -private: - using Mutex = std::recursive_mutex; - using Lock = std::unique_lock; - - friend class File; - - std::string getFilepath(const Key& key); - - FilePointer addFile(Metadata&& metadata, const std::string& filepath); - void addUnusedFile(const FilePointer file); - void removeUnusedFile(const FilePointer file); - void reserve(size_t length); - void clear(); - - std::atomic _numTotalFiles { 0 }; - std::atomic _numUnusedFiles { 0 }; - std::atomic _totalFilesSize { 0 }; - std::atomic _unusedFilesSize { 0 }; - - std::string _ext; - std::string _dirname; - std::string _dirpath; - bool _initialized { false }; - - std::unordered_map> _files; - Mutex _filesMutex; - - std::map _unusedFiles; - Mutex _unusedFilesMutex; - size_t _unusedFilesMaxSize { DEFAULT_UNUSED_MAX_SIZE }; - int _lastLRUKey { 0 }; - - size_t _offlineFilesMaxSize { DEFAULT_OFFLINE_MAX_SIZE }; -}; - -class File : public QObject { - Q_OBJECT - -public: - using Key = FileCache::Key; - using Metadata = FileCache::Metadata; - - Key getKey() const { return _key; } - size_t getLength() const { return _length; } - std::string getFilepath() const { return _filepath; } - - virtual ~File(); - /// overrides should call File::deleter to maintain caching behavior - virtual void deleter(); - -protected: - /// when constructed, the file has already been created/written - File(Metadata&& metadata, const std::string& filepath); - -private: - friend class FileCache; - - const Key _key; - const size_t _length; - const std::string _filepath; - - FileCache* _cache; - int _LRUKey { 0 }; - - bool _shouldPersist { false }; -}; - -} - -#endif // hifi_FileCache_h diff --git a/libraries/networking/src/NodePermissions.h b/libraries/networking/src/NodePermissions.h index 6fa005e360..5d2755f9b5 100644 --- a/libraries/networking/src/NodePermissions.h +++ b/libraries/networking/src/NodePermissions.h @@ -13,31 +13,18 @@ #define hifi_NodePermissions_h #include -#include #include #include #include #include -#include -#include + #include "GroupRank.h" class NodePermissions; using NodePermissionsPointer = std::shared_ptr; -using NodePermissionsKey = std::pair; // name, rankID +using NodePermissionsKey = QPair; // name, rankID using NodePermissionsKeyList = QList>; -namespace std { - template<> - struct hash { - size_t operator()(const NodePermissionsKey& key) const { - size_t result = qHash(key.first); - result <<= 32; - result |= qHash(key.second); - return result; - } - }; -} class NodePermissions { public: @@ -113,40 +100,27 @@ public: NodePermissionsMap() { } NodePermissionsPointer& operator[](const NodePermissionsKey& key) { NodePermissionsKey dataKey(key.first.toLower(), key.second); - if (0 == _data.count(dataKey)) { + if (!_data.contains(dataKey)) { _data[dataKey] = NodePermissionsPointer(new NodePermissions(key)); } return _data[dataKey]; } NodePermissionsPointer operator[](const NodePermissionsKey& key) const { - NodePermissionsPointer result; - auto itr = _data.find(NodePermissionsKey(key.first.toLower(), key.second)); - if (_data.end() != itr) { - result = itr->second; - } - return result; + return _data.value(NodePermissionsKey(key.first.toLower(), key.second)); } bool contains(const NodePermissionsKey& key) const { - return 0 != _data.count(NodePermissionsKey(key.first.toLower(), key.second)); + return _data.contains(NodePermissionsKey(key.first.toLower(), key.second)); } - bool contains(const QString& keyFirst, const QUuid& keySecond) const { - return 0 != _data.count(NodePermissionsKey(keyFirst.toLower(), keySecond)); + bool contains(const QString& keyFirst, QUuid keySecond) const { + return _data.contains(NodePermissionsKey(keyFirst.toLower(), keySecond)); } - - QList keys() const { - QList result; - for (const auto& entry : _data) { - result.push_back(entry.first); - } - return result; - } - - const std::unordered_map& get() { return _data; } + QList keys() const { return _data.keys(); } + QHash get() { return _data; } void clear() { _data.clear(); } - void remove(const NodePermissionsKey& key) { _data.erase(key); } + void remove(const NodePermissionsKey& key) { _data.remove(key); } private: - std::unordered_map _data; + QHash _data; }; diff --git a/libraries/networking/src/udt/PacketQueue.cpp b/libraries/networking/src/udt/PacketQueue.cpp index 9560f2f187..bb20982ca4 100644 --- a/libraries/networking/src/udt/PacketQueue.cpp +++ b/libraries/networking/src/udt/PacketQueue.cpp @@ -15,10 +15,6 @@ using namespace udt; -PacketQueue::PacketQueue() { - _channels.emplace_back(new std::list()); -} - MessageNumber PacketQueue::getNextMessageNumber() { static const MessageNumber MAX_MESSAGE_NUMBER = MessageNumber(1) << MESSAGE_NUMBER_SIZE; _currentMessageNumber = (_currentMessageNumber + 1) % MAX_MESSAGE_NUMBER; @@ -28,7 +24,7 @@ MessageNumber PacketQueue::getNextMessageNumber() { bool PacketQueue::isEmpty() const { LockGuard locker(_packetsLock); // Only the main channel and it is empty - return (_channels.size() == 1) && _channels.front()->empty(); + return (_channels.size() == 1) && _channels.front().empty(); } PacketQueue::PacketPointer PacketQueue::takePacket() { @@ -38,19 +34,19 @@ PacketQueue::PacketPointer PacketQueue::takePacket() { } // Find next non empty channel - if (_channels[nextIndex()]->empty()) { + if (_channels[nextIndex()].empty()) { nextIndex(); } auto& channel = _channels[_currentIndex]; - Q_ASSERT(!channel->empty()); + Q_ASSERT(!channel.empty()); // Take front packet - auto packet = std::move(channel->front()); - channel->pop_front(); + auto packet = std::move(channel.front()); + channel.pop_front(); // Remove now empty channel (Don't remove the main channel) - if (channel->empty() && _currentIndex != 0) { - channel->swap(*_channels.back()); + if (channel.empty() && _currentIndex != 0) { + channel.swap(_channels.back()); _channels.pop_back(); --_currentIndex; } @@ -65,7 +61,7 @@ unsigned int PacketQueue::nextIndex() { void PacketQueue::queuePacket(PacketPointer packet) { LockGuard locker(_packetsLock); - _channels.front()->push_back(std::move(packet)); + _channels.front().push_back(std::move(packet)); } void PacketQueue::queuePacketList(PacketListPointer packetList) { @@ -74,6 +70,5 @@ void PacketQueue::queuePacketList(PacketListPointer packetList) { } LockGuard locker(_packetsLock); - _channels.emplace_back(new std::list()); - _channels.back()->swap(packetList->_packets); + _channels.push_back(std::move(packetList->_packets)); } diff --git a/libraries/networking/src/udt/PacketQueue.h b/libraries/networking/src/udt/PacketQueue.h index 2b3d3a4b5b..69784fd8db 100644 --- a/libraries/networking/src/udt/PacketQueue.h +++ b/libraries/networking/src/udt/PacketQueue.h @@ -30,11 +30,10 @@ class PacketQueue { using LockGuard = std::lock_guard; using PacketPointer = std::unique_ptr; using PacketListPointer = std::unique_ptr; - using Channel = std::unique_ptr>; + using Channel = std::list; using Channels = std::vector; public: - PacketQueue(); void queuePacket(PacketPointer packet); void queuePacketList(PacketListPointer packetList); @@ -50,7 +49,7 @@ private: MessageNumber _currentMessageNumber { 0 }; mutable Mutex _packetsLock; // Protects the packets to be sent. - Channels _channels; // One channel per packet list + Main channel + Channels _channels = Channels(1); // One channel per packet list + Main channel unsigned int _currentIndex { 0 }; }; diff --git a/libraries/physics/src/EntityMotionState.cpp b/libraries/physics/src/EntityMotionState.cpp index d383f4c199..c175a836cc 100644 --- a/libraries/physics/src/EntityMotionState.cpp +++ b/libraries/physics/src/EntityMotionState.cpp @@ -97,21 +97,6 @@ void EntityMotionState::updateServerPhysicsVariables() { _serverActionData = _entity->getActionData(); } -void EntityMotionState::handleDeactivation() { - // copy _server data to entity - bool success; - _entity->setPosition(_serverPosition, success, false); - _entity->setOrientation(_serverRotation, success, false); - _entity->setVelocity(ENTITY_ITEM_ZERO_VEC3); - _entity->setAngularVelocity(ENTITY_ITEM_ZERO_VEC3); - // and also to RigidBody - btTransform worldTrans; - worldTrans.setOrigin(glmToBullet(_serverPosition)); - worldTrans.setRotation(glmToBullet(_serverRotation)); - _body->setWorldTransform(worldTrans); - // no need to update velocities... should already be zero -} - // virtual void EntityMotionState::handleEasyChanges(uint32_t& flags) { assert(entityTreeIsLocked()); @@ -126,8 +111,6 @@ void EntityMotionState::handleEasyChanges(uint32_t& flags) { flags &= ~Simulation::DIRTY_PHYSICS_ACTIVATION; _body->setActivationState(WANTS_DEACTIVATION); _outgoingPriority = 0; - const float ACTIVATION_EXPIRY = 3.0f; // something larger than the 2.0 hard coded in Bullet - _body->setDeactivationTime(ACTIVATION_EXPIRY); } else { // disowned object is still moving --> start timer for ownership bid // TODO? put a delay in here proportional to distance from object? @@ -238,9 +221,12 @@ void EntityMotionState::getWorldTransform(btTransform& worldTrans) const { } // This callback is invoked by the physics simulation at the end of each simulation step... -// iff the corresponding RigidBody is DYNAMIC and ACTIVE. +// iff the corresponding RigidBody is DYNAMIC and has moved. void EntityMotionState::setWorldTransform(const btTransform& worldTrans) { - assert(_entity); + if (!_entity) { + return; + } + assert(entityTreeIsLocked()); measureBodyAcceleration(); bool positionSuccess; diff --git a/libraries/physics/src/EntityMotionState.h b/libraries/physics/src/EntityMotionState.h index 380edf3927..feac47d8ec 100644 --- a/libraries/physics/src/EntityMotionState.h +++ b/libraries/physics/src/EntityMotionState.h @@ -29,7 +29,6 @@ public: virtual ~EntityMotionState(); void updateServerPhysicsVariables(); - void handleDeactivation(); virtual void handleEasyChanges(uint32_t& flags) override; virtual bool handleHardAndEasyChanges(uint32_t& flags, PhysicsEngine* engine) override; diff --git a/libraries/physics/src/PhysicalEntitySimulation.cpp b/libraries/physics/src/PhysicalEntitySimulation.cpp index bd76b2d70f..903b160a5e 100644 --- a/libraries/physics/src/PhysicalEntitySimulation.cpp +++ b/libraries/physics/src/PhysicalEntitySimulation.cpp @@ -259,27 +259,13 @@ void PhysicalEntitySimulation::getObjectsToChange(VectorOfMotionStates& result) _pendingChanges.clear(); } -void PhysicalEntitySimulation::handleDeactivatedMotionStates(const VectorOfMotionStates& motionStates) { - for (auto stateItr : motionStates) { - ObjectMotionState* state = &(*stateItr); - assert(state); - if (state->getType() == MOTIONSTATE_TYPE_ENTITY) { - EntityMotionState* entityState = static_cast(state); - entityState->handleDeactivation(); - EntityItemPointer entity = entityState->getEntity(); - _entitiesToSort.insert(entity); - } - } -} - -void PhysicalEntitySimulation::handleChangedMotionStates(const VectorOfMotionStates& motionStates) { +void PhysicalEntitySimulation::handleOutgoingChanges(const VectorOfMotionStates& motionStates) { QMutexLocker lock(&_mutex); // walk the motionStates looking for those that correspond to entities for (auto stateItr : motionStates) { ObjectMotionState* state = &(*stateItr); - assert(state); - if (state->getType() == MOTIONSTATE_TYPE_ENTITY) { + if (state && state->getType() == MOTIONSTATE_TYPE_ENTITY) { EntityMotionState* entityState = static_cast(state); EntityItemPointer entity = entityState->getEntity(); assert(entity.get()); diff --git a/libraries/physics/src/PhysicalEntitySimulation.h b/libraries/physics/src/PhysicalEntitySimulation.h index 5f6185add3..af5def9775 100644 --- a/libraries/physics/src/PhysicalEntitySimulation.h +++ b/libraries/physics/src/PhysicalEntitySimulation.h @@ -56,8 +56,7 @@ public: void setObjectsToChange(const VectorOfMotionStates& objectsToChange); void getObjectsToChange(VectorOfMotionStates& result); - void handleDeactivatedMotionStates(const VectorOfMotionStates& motionStates); - void handleChangedMotionStates(const VectorOfMotionStates& motionStates); + void handleOutgoingChanges(const VectorOfMotionStates& motionStates); void handleCollisionEvents(const CollisionEvents& collisionEvents); EntityEditPacketSender* getPacketSender() { return _entityPacketSender; } @@ -68,7 +67,7 @@ private: SetOfEntities _entitiesToAddToPhysics; SetOfEntityMotionStates _pendingChanges; // EntityMotionStates already in PhysicsEngine that need their physics changed - SetOfEntityMotionStates _outgoingChanges; // EntityMotionStates for which we may need to send updates to entity-server + SetOfEntityMotionStates _outgoingChanges; // EntityMotionStates for which we need to send updates to entity-server SetOfMotionStates _physicalObjects; // MotionStates of entities in PhysicsEngine diff --git a/libraries/physics/src/PhysicsEngine.cpp b/libraries/physics/src/PhysicsEngine.cpp index a8a8e6acfd..363887de25 100644 --- a/libraries/physics/src/PhysicsEngine.cpp +++ b/libraries/physics/src/PhysicsEngine.cpp @@ -472,7 +472,7 @@ const CollisionEvents& PhysicsEngine::getCollisionEvents() { return _collisionEvents; } -const VectorOfMotionStates& PhysicsEngine::getChangedMotionStates() { +const VectorOfMotionStates& PhysicsEngine::getOutgoingChanges() { BT_PROFILE("copyOutgoingChanges"); // Bullet will not deactivate static objects (it doesn't expect them to be active) // so we must deactivate them ourselves diff --git a/libraries/physics/src/PhysicsEngine.h b/libraries/physics/src/PhysicsEngine.h index b2ebe58f08..bbafbb06b6 100644 --- a/libraries/physics/src/PhysicsEngine.h +++ b/libraries/physics/src/PhysicsEngine.h @@ -65,8 +65,7 @@ public: bool hasOutgoingChanges() const { return _hasOutgoingChanges; } /// \return reference to list of changed MotionStates. The list is only valid until beginning of next simulation loop. - const VectorOfMotionStates& getChangedMotionStates(); - const VectorOfMotionStates& getDeactivatedMotionStates() const { return _dynamicsWorld->getDeactivatedMotionStates(); } + const VectorOfMotionStates& getOutgoingChanges(); /// \return reference to list of Collision events. The list is only valid until beginning of next simulation loop. const CollisionEvents& getCollisionEvents(); diff --git a/libraries/physics/src/ThreadSafeDynamicsWorld.cpp b/libraries/physics/src/ThreadSafeDynamicsWorld.cpp index 24cfbc2609..5fe99f137c 100644 --- a/libraries/physics/src/ThreadSafeDynamicsWorld.cpp +++ b/libraries/physics/src/ThreadSafeDynamicsWorld.cpp @@ -120,41 +120,30 @@ void ThreadSafeDynamicsWorld::synchronizeMotionState(btRigidBody* body) { void ThreadSafeDynamicsWorld::synchronizeMotionStates() { BT_PROFILE("synchronizeMotionStates"); _changedMotionStates.clear(); - - // NOTE: m_synchronizeAllMotionStates is 'false' by default for optimization. - // See PhysicsEngine::init() where we call _dynamicsWorld->setForceUpdateAllAabbs(false) if (m_synchronizeAllMotionStates) { //iterate over all collision objects for (int i=0;igetMotionState()) { - synchronizeMotionState(body); - _changedMotionStates.push_back(static_cast(body->getMotionState())); + if (body) { + if (body->getMotionState()) { + synchronizeMotionState(body); + _changedMotionStates.push_back(static_cast(body->getMotionState())); + } } } } else { //iterate over all active rigid bodies - // TODO? if this becomes a performance bottleneck we could derive our own SimulationIslandManager - // that remembers a list of objects deactivated last step - _activeStates.clear(); - _deactivatedStates.clear(); for (int i=0;i(body->getMotionState()); - if (motionState) { - if (body->isActive()) { + if (body->isActive()) { + if (body->getMotionState()) { synchronizeMotionState(body); - _changedMotionStates.push_back(motionState); - _activeStates.insert(motionState); - } else if (_lastActiveStates.find(motionState) != _lastActiveStates.end()) { - // this object was active last frame but is no longer - _deactivatedStates.push_back(motionState); + _changedMotionStates.push_back(static_cast(body->getMotionState())); } } } } - _activeStates.swap(_lastActiveStates); } void ThreadSafeDynamicsWorld::saveKinematicState(btScalar timeStep) { diff --git a/libraries/physics/src/ThreadSafeDynamicsWorld.h b/libraries/physics/src/ThreadSafeDynamicsWorld.h index b4fcca8cdb..68062d8d29 100644 --- a/libraries/physics/src/ThreadSafeDynamicsWorld.h +++ b/libraries/physics/src/ThreadSafeDynamicsWorld.h @@ -49,16 +49,12 @@ public: float getLocalTimeAccumulation() const { return m_localTime; } const VectorOfMotionStates& getChangedMotionStates() const { return _changedMotionStates; } - const VectorOfMotionStates& getDeactivatedMotionStates() const { return _deactivatedStates; } private: // call this instead of non-virtual btDiscreteDynamicsWorld::synchronizeSingleMotionState() void synchronizeMotionState(btRigidBody* body); VectorOfMotionStates _changedMotionStates; - VectorOfMotionStates _deactivatedStates; - SetOfMotionStates _activeStates; - SetOfMotionStates _lastActiveStates; }; #endif // hifi_ThreadSafeDynamicsWorld_h diff --git a/libraries/recording/src/recording/Deck.cpp b/libraries/recording/src/recording/Deck.cpp index 186516e01c..61eb86c91f 100644 --- a/libraries/recording/src/recording/Deck.cpp +++ b/libraries/recording/src/recording/Deck.cpp @@ -33,7 +33,6 @@ void Deck::queueClip(ClipPointer clip, float timeOffset) { // FIXME disabling multiple clips for now _clips.clear(); - _length = 0.0f; // if the time offset is not zero, wrap in an OffsetClip if (timeOffset != 0.0f) { @@ -154,8 +153,8 @@ void Deck::processFrames() { // if doing relative movement emit looped(); } else { - // otherwise stop playback - stop(); + // otherwise pause playback + pause(); } return; } diff --git a/libraries/render-utils/CMakeLists.txt b/libraries/render-utils/CMakeLists.txt index 3bf389973a..ecafb8f565 100644 --- a/libraries/render-utils/CMakeLists.txt +++ b/libraries/render-utils/CMakeLists.txt @@ -3,7 +3,7 @@ AUTOSCRIBE_SHADER_LIB(gpu model render) # pull in the resources.qrc file qt5_add_resources(QT_RESOURCES_FILE "${CMAKE_CURRENT_SOURCE_DIR}/res/fonts/fonts.qrc") setup_hifi_library(Widgets OpenGL Network Qml Quick Script) -link_hifi_libraries(shared ktx gpu model model-networking render animation fbx entities) +link_hifi_libraries(shared gpu model model-networking render animation fbx entities) if (NOT ANDROID) target_nsight() diff --git a/libraries/render-utils/src/AntialiasingEffect.cpp b/libraries/render-utils/src/AntialiasingEffect.cpp index f95d45de04..2941197e6d 100644 --- a/libraries/render-utils/src/AntialiasingEffect.cpp +++ b/libraries/render-utils/src/AntialiasingEffect.cpp @@ -52,7 +52,7 @@ const gpu::PipelinePointer& Antialiasing::getAntialiasingPipeline() { _antialiasingBuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("antialiasing")); auto format = gpu::Element::COLOR_SRGBA_32; // DependencyManager::get()->getLightingTexture()->getTexelFormat(); auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT); - _antialiasingTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(format, width, height, defaultSampler)); + _antialiasingTexture = gpu::TexturePointer(gpu::Texture::create2D(format, width, height, defaultSampler)); _antialiasingBuffer->setRenderBuffer(0, _antialiasingTexture); } diff --git a/libraries/render-utils/src/DeferredFramebuffer.cpp b/libraries/render-utils/src/DeferredFramebuffer.cpp index 40c22beba4..e8783e0e0d 100644 --- a/libraries/render-utils/src/DeferredFramebuffer.cpp +++ b/libraries/render-utils/src/DeferredFramebuffer.cpp @@ -53,9 +53,9 @@ void DeferredFramebuffer::allocate() { auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT); - _deferredColorTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(colorFormat, width, height, defaultSampler)); - _deferredNormalTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(linearFormat, width, height, defaultSampler)); - _deferredSpecularTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(colorFormat, width, height, defaultSampler)); + _deferredColorTexture = gpu::TexturePointer(gpu::Texture::create2D(colorFormat, width, height, defaultSampler)); + _deferredNormalTexture = gpu::TexturePointer(gpu::Texture::create2D(linearFormat, width, height, defaultSampler)); + _deferredSpecularTexture = gpu::TexturePointer(gpu::Texture::create2D(colorFormat, width, height, defaultSampler)); _deferredFramebuffer->setRenderBuffer(0, _deferredColorTexture); _deferredFramebuffer->setRenderBuffer(1, _deferredNormalTexture); @@ -65,7 +65,7 @@ void DeferredFramebuffer::allocate() { auto depthFormat = gpu::Element(gpu::SCALAR, gpu::UINT32, gpu::DEPTH_STENCIL); // Depth24_Stencil8 texel format if (!_primaryDepthTexture) { - _primaryDepthTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(depthFormat, width, height, defaultSampler)); + _primaryDepthTexture = gpu::TexturePointer(gpu::Texture::create2D(depthFormat, width, height, defaultSampler)); } _deferredFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, depthFormat); @@ -75,7 +75,7 @@ void DeferredFramebuffer::allocate() { auto smoothSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR); - _lightingTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::R11G11B10), width, height, defaultSampler)); + _lightingTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::R11G11B10), width, height, defaultSampler)); _lightingFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("lighting")); _lightingFramebuffer->setRenderBuffer(0, _lightingTexture); _lightingFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, depthFormat); diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp index ce340583ee..6f1152ac16 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.cpp +++ b/libraries/render-utils/src/DeferredLightingEffect.cpp @@ -496,14 +496,14 @@ void PreparePrimaryFramebuffer::run(const SceneContextPointer& sceneContext, con auto colorFormat = gpu::Element::COLOR_SRGBA_32; auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT); - auto primaryColorTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(colorFormat, frameSize.x, frameSize.y, defaultSampler)); + auto primaryColorTexture = gpu::TexturePointer(gpu::Texture::create2D(colorFormat, frameSize.x, frameSize.y, defaultSampler)); _primaryFramebuffer->setRenderBuffer(0, primaryColorTexture); auto depthFormat = gpu::Element(gpu::SCALAR, gpu::UINT32, gpu::DEPTH_STENCIL); // Depth24_Stencil8 texel format - auto primaryDepthTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(depthFormat, frameSize.x, frameSize.y, defaultSampler)); + auto primaryDepthTexture = gpu::TexturePointer(gpu::Texture::create2D(depthFormat, frameSize.x, frameSize.y, defaultSampler)); _primaryFramebuffer->setDepthStencilBuffer(primaryDepthTexture, depthFormat); } diff --git a/libraries/render-utils/src/FramebufferCache.cpp b/libraries/render-utils/src/FramebufferCache.cpp index 72b3c2ceb4..27429595b4 100644 --- a/libraries/render-utils/src/FramebufferCache.cpp +++ b/libraries/render-utils/src/FramebufferCache.cpp @@ -21,6 +21,7 @@ void FramebufferCache::setFrameBufferSize(QSize frameBufferSize) { //If the size changed, we need to delete our FBOs if (_frameBufferSize != frameBufferSize) { _frameBufferSize = frameBufferSize; + _selfieFramebuffer.reset(); { std::unique_lock lock(_mutex); _cachedFramebuffers.clear(); @@ -29,8 +30,16 @@ void FramebufferCache::setFrameBufferSize(QSize frameBufferSize) { } void FramebufferCache::createPrimaryFramebuffer() { + auto colorFormat = gpu::Element::COLOR_SRGBA_32; + auto width = _frameBufferSize.width(); + auto height = _frameBufferSize.height(); + auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT); + _selfieFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("selfie")); + auto tex = gpu::TexturePointer(gpu::Texture::create2D(colorFormat, width * 0.5, height * 0.5, defaultSampler)); + _selfieFramebuffer->setRenderBuffer(0, tex); + auto smoothSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR); } @@ -51,3 +60,10 @@ void FramebufferCache::releaseFramebuffer(const gpu::FramebufferPointer& framebu _cachedFramebuffers.push_back(framebuffer); } } + +gpu::FramebufferPointer FramebufferCache::getSelfieFramebuffer() { + if (!_selfieFramebuffer) { + createPrimaryFramebuffer(); + } + return _selfieFramebuffer; +} diff --git a/libraries/render-utils/src/FramebufferCache.h b/libraries/render-utils/src/FramebufferCache.h index 8065357615..f74d224a61 100644 --- a/libraries/render-utils/src/FramebufferCache.h +++ b/libraries/render-utils/src/FramebufferCache.h @@ -27,6 +27,9 @@ public: void setFrameBufferSize(QSize frameBufferSize); const QSize& getFrameBufferSize() const { return _frameBufferSize; } + /// Returns the framebuffer object used to render selfie maps; + gpu::FramebufferPointer getSelfieFramebuffer(); + /// Returns a free framebuffer with a single color attachment for temp or intra-frame operations gpu::FramebufferPointer getFramebuffer(); @@ -39,6 +42,8 @@ private: gpu::FramebufferPointer _shadowFramebuffer; + gpu::FramebufferPointer _selfieFramebuffer; + QSize _frameBufferSize{ 100, 100 }; std::mutex _mutex; diff --git a/libraries/render-utils/src/LightAmbient.slh b/libraries/render-utils/src/LightAmbient.slh index e343d8c239..15e23015cb 100644 --- a/libraries/render-utils/src/LightAmbient.slh +++ b/libraries/render-utils/src/LightAmbient.slh @@ -30,8 +30,9 @@ vec3 fresnelSchlickAmbient(vec3 fresnelColor, vec3 lightDir, vec3 halfDir, float <$declareSkyboxMap()$> <@endif@> -vec3 evalAmbientSpecularIrradiance(LightAmbient ambient, vec3 fragEyeDir, vec3 fragNormal, float roughness) { +vec3 evalAmbientSpecularIrradiance(LightAmbient ambient, vec3 fragEyeDir, vec3 fragNormal, float roughness, vec3 fresnel) { vec3 direction = -reflect(fragEyeDir, fragNormal); + vec3 ambientFresnel = fresnelSchlickAmbient(fresnel, fragEyeDir, fragNormal, 1.0 - roughness); vec3 specularLight; <@if supportIfAmbientMapElseAmbientSphere@> if (getLightHasAmbientMap(ambient)) @@ -52,7 +53,7 @@ vec3 evalAmbientSpecularIrradiance(LightAmbient ambient, vec3 fragEyeDir, vec3 f } <@endif@> - return specularLight; + return specularLight * ambientFresnel; } <@endfunc@> @@ -73,14 +74,12 @@ void evalLightingAmbient(out vec3 diffuse, out vec3 specular, LightAmbient ambie <@endif@> ) { - // Fresnel - vec3 ambientFresnel = fresnelSchlickAmbient(fresnel, eyeDir, normal, 1.0 - roughness); - // Diffuse from ambient - diffuse = (1.0 - metallic) * (vec3(1.0) - ambientFresnel) * sphericalHarmonics_evalSphericalLight(getLightAmbientSphere(ambient), normal).xyz; + diffuse = (1.0 - metallic) * sphericalHarmonics_evalSphericalLight(getLightAmbientSphere(ambient), normal).xyz; // Specular highlight from ambient - specular = evalAmbientSpecularIrradiance(ambient, eyeDir, normal, roughness) * ambientFresnel; + specular = evalAmbientSpecularIrradiance(ambient, eyeDir, normal, roughness, fresnel) * obscurance * getLightAmbientIntensity(ambient); + <@if supportScattering@> float ambientOcclusion = curvatureAO(lowNormalCurvature.w * 20.0f) * 0.5f; diff --git a/libraries/render-utils/src/LightingModel.cpp b/libraries/render-utils/src/LightingModel.cpp index bd321bad95..47af83da36 100644 --- a/libraries/render-utils/src/LightingModel.cpp +++ b/libraries/render-utils/src/LightingModel.cpp @@ -133,7 +133,6 @@ void LightingModel::setSpotLight(bool enable) { bool LightingModel::isSpotLightEnabled() const { return (bool)_parametersBuffer.get().enableSpotLight; } - void LightingModel::setShowLightContour(bool enable) { if (enable != isShowLightContourEnabled()) { _parametersBuffer.edit().showLightContour = (float)enable; @@ -143,14 +142,6 @@ bool LightingModel::isShowLightContourEnabled() const { return (bool)_parametersBuffer.get().showLightContour; } -void LightingModel::setWireframe(bool enable) { - if (enable != isWireframeEnabled()) { - _parametersBuffer.edit().enableWireframe = (float)enable; - } -} -bool LightingModel::isWireframeEnabled() const { - return (bool)_parametersBuffer.get().enableWireframe; -} MakeLightingModel::MakeLightingModel() { _lightingModel = std::make_shared(); } @@ -176,7 +167,6 @@ void MakeLightingModel::configure(const Config& config) { _lightingModel->setSpotLight(config.enableSpotLight); _lightingModel->setShowLightContour(config.showLightContour); - _lightingModel->setWireframe(config.enableWireframe); } void MakeLightingModel::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, LightingModelPointer& lightingModel) { diff --git a/libraries/render-utils/src/LightingModel.h b/libraries/render-utils/src/LightingModel.h index c1189d5160..45514654f2 100644 --- a/libraries/render-utils/src/LightingModel.h +++ b/libraries/render-utils/src/LightingModel.h @@ -64,9 +64,6 @@ public: void setShowLightContour(bool enable); bool isShowLightContourEnabled() const; - void setWireframe(bool enable); - bool isWireframeEnabled() const; - UniformBufferView getParametersBuffer() const { return _parametersBuffer; } protected: @@ -92,12 +89,13 @@ protected: float enablePointLight{ 1.0f }; float enableSpotLight{ 1.0f }; - float showLightContour { 0.0f }; // false by default + float showLightContour{ 0.0f }; // false by default float enableObscurance{ 1.0f }; float enableMaterialTexturing { 1.0f }; - float enableWireframe { 0.0f }; // false by default + + float spares{ 0.0f }; Parameters() {} }; @@ -131,7 +129,6 @@ class MakeLightingModelConfig : public render::Job::Config { Q_PROPERTY(bool enablePointLight MEMBER enablePointLight NOTIFY dirty) Q_PROPERTY(bool enableSpotLight MEMBER enableSpotLight NOTIFY dirty) - Q_PROPERTY(bool enableWireframe MEMBER enableWireframe NOTIFY dirty) Q_PROPERTY(bool showLightContour MEMBER showLightContour NOTIFY dirty) public: @@ -155,9 +152,8 @@ public: bool enablePointLight{ true }; bool enableSpotLight{ true }; - bool showLightContour { false }; // false by default - bool enableWireframe { false }; // false by default + bool showLightContour { false }; // false by default signals: void dirty(); diff --git a/libraries/render-utils/src/LightingModel.slh b/libraries/render-utils/src/LightingModel.slh index 209a1f38d6..74285aa6a9 100644 --- a/libraries/render-utils/src/LightingModel.slh +++ b/libraries/render-utils/src/LightingModel.slh @@ -17,7 +17,7 @@ struct LightingModel { vec4 _UnlitEmissiveLightmapBackground; vec4 _ScatteringDiffuseSpecularAlbedo; vec4 _AmbientDirectionalPointSpot; - vec4 _ShowContourObscuranceWireframe; + vec4 _ShowContourObscuranceSpare2; }; uniform lightingModelBuffer{ @@ -37,7 +37,7 @@ float isBackgroundEnabled() { return lightingModel._UnlitEmissiveLightmapBackground.w; } float isObscuranceEnabled() { - return lightingModel._ShowContourObscuranceWireframe.y; + return lightingModel._ShowContourObscuranceSpare2.y; } float isScatteringEnabled() { @@ -67,12 +67,9 @@ float isSpotEnabled() { } float isShowLightContour() { - return lightingModel._ShowContourObscuranceWireframe.x; + return lightingModel._ShowContourObscuranceSpare2.x; } -float isWireframeEnabled() { - return lightingModel._ShowContourObscuranceWireframe.z; -} <@endfunc@> <$declareLightingModel()$> diff --git a/libraries/render-utils/src/MaterialTextures.slh b/libraries/render-utils/src/MaterialTextures.slh index 7b73896cc5..6d2ad23c21 100644 --- a/libraries/render-utils/src/MaterialTextures.slh +++ b/libraries/render-utils/src/MaterialTextures.slh @@ -64,7 +64,7 @@ float fetchRoughnessMap(vec2 uv) { uniform sampler2D normalMap; vec3 fetchNormalMap(vec2 uv) { // unpack normal, swizzle to get into hifi tangent space with Y axis pointing out - return normalize(texture(normalMap, uv).rbg -vec3(0.5, 0.5, 0.5)); + return normalize(texture(normalMap, uv).xzy -vec3(0.5, 0.5, 0.5)); } <@endif@> diff --git a/libraries/render-utils/src/MeshPartPayload.cpp b/libraries/render-utils/src/MeshPartPayload.cpp index 41a1bb4c74..5b3d285b47 100644 --- a/libraries/render-utils/src/MeshPartPayload.cpp +++ b/libraries/render-utils/src/MeshPartPayload.cpp @@ -372,12 +372,19 @@ void ModelMeshPartPayload::notifyLocationChanged() { } -void ModelMeshPartPayload::updateTransformForSkinnedMesh(const Transform& renderTransform, const Transform& boundTransform, - const gpu::BufferPointer& buffer) { - _transform = renderTransform; - _worldBound = _adjustedLocalBound; - _worldBound.transform(boundTransform); - _clusterBuffer = buffer; +void ModelMeshPartPayload::updateTransformForSkinnedMesh(const Transform& transform, const QVector& clusterMatrices) { + _transform = transform; + + if (clusterMatrices.size() > 0) { + _worldBound = _adjustedLocalBound; + _worldBound.transform(_transform); + if (clusterMatrices.size() == 1) { + _transform = _transform.worldTransform(Transform(clusterMatrices[0])); + } + } else { + _worldBound = _localBound; + _worldBound.transform(_transform); + } } ItemKey ModelMeshPartPayload::getKey() const { @@ -525,8 +532,9 @@ void ModelMeshPartPayload::bindMesh(gpu::Batch& batch) const { void ModelMeshPartPayload::bindTransform(gpu::Batch& batch, const ShapePipeline::LocationsPointer locations, RenderArgs::RenderMode renderMode) const { // Still relying on the raw data from the model - if (_clusterBuffer) { - batch.setUniformBuffer(ShapePipeline::Slot::BUFFER::SKINNING, _clusterBuffer); + const Model::MeshState& state = _model->getMeshState(_meshIndex); + if (state.clusterBuffer) { + batch.setUniformBuffer(ShapePipeline::Slot::BUFFER::SKINNING, state.clusterBuffer); } batch.setModelTransform(_transform); } @@ -582,6 +590,8 @@ void ModelMeshPartPayload::render(RenderArgs* args) const { auto locations = args->_pipeline->locations; assert(locations); + // Bind the model transform and the skinCLusterMatrices if needed + _model->updateClusterMatrices(); bindTransform(batch, locations, args->_renderMode); //Bind the index buffer and vertex buffer and Blend shapes if needed diff --git a/libraries/render-utils/src/MeshPartPayload.h b/libraries/render-utils/src/MeshPartPayload.h index ef74011c40..c585c95025 100644 --- a/libraries/render-utils/src/MeshPartPayload.h +++ b/libraries/render-utils/src/MeshPartPayload.h @@ -89,9 +89,8 @@ public: typedef Payload::DataPointer Pointer; void notifyLocationChanged() override; - void updateTransformForSkinnedMesh(const Transform& renderTransform, - const Transform& boundTransform, - const gpu::BufferPointer& buffer); + void updateTransformForSkinnedMesh(const Transform& transform, + const QVector& clusterMatrices); float computeFadeAlpha() const; @@ -109,7 +108,6 @@ public: void computeAdjustedLocalBound(const QVector& clusterMatrices); - gpu::BufferPointer _clusterBuffer; Model* _model; int _meshIndex; diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index c584b0bc21..48c1d29b68 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -227,10 +227,6 @@ void Model::updateRenderItems() { return; } - // lazy update of cluster matrices used for rendering. - // We need to update them here so we can correctly update the bounding box. - self->updateClusterMatrices(); - render::ScenePointer scene = AbstractViewStateInterface::instance()->getMain3DScene(); uint32_t deleteGeometryCounter = self->_deleteGeometryCounter; @@ -244,12 +240,12 @@ void Model::updateRenderItems() { Transform modelTransform = data._model->getTransform(); modelTransform.setScale(glm::vec3(1.0f)); - const Model::MeshState& state = data._model->getMeshState(data._meshIndex); - Transform renderTransform = modelTransform; - if (state.clusterMatrices.size() == 1) { - renderTransform = modelTransform.worldTransform(Transform(state.clusterMatrices[0])); - } - data.updateTransformForSkinnedMesh(renderTransform, modelTransform, state.clusterBuffer); + // lazy update of cluster matrices used for rendering. We need to update them here, so we can correctly update the bounding box. + data._model->updateClusterMatrices(); + + // update the model transform and bounding box for this render item. + const Model::MeshState& state = data._model->_meshStates.at(data._meshIndex); + data.updateTransformForSkinnedMesh(modelTransform, state.clusterMatrices); } } }); @@ -1052,7 +1048,7 @@ void Model::updateRig(float deltaTime, glm::mat4 parentTransform) { } void Model::computeMeshPartLocalBounds() { - for (auto& part : _modelMeshRenderItemsSet) { + for (auto& part : _modelMeshRenderItemsSet) { assert(part->_meshIndex < _modelMeshRenderItemsSet.size()); const Model::MeshState& state = _meshStates.at(part->_meshIndex); part->computeAdjustedLocalBound(state.clusterMatrices); diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index 22aa95090c..676d176cca 100644 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -194,7 +194,7 @@ RenderDeferredTask::RenderDeferredTask(RenderFetchCullSortTask::Output items) { { // Grab a texture map representing the different status icons and assign that to the drawStatsuJob auto iconMapPath = PathUtils::resourcesPath() + "icons/statusIconAtlas.svg"; - auto statusIconMap = DependencyManager::get()->getImageTexture(iconMapPath, NetworkTexture::STRICT_TEXTURE); + auto statusIconMap = DependencyManager::get()->getImageTexture(iconMapPath); addJob("DrawStatus", opaques, DrawStatus(statusIconMap)); } } @@ -259,18 +259,8 @@ void DrawDeferred::run(const SceneContextPointer& sceneContext, const RenderCont // Setup lighting model for all items; batch.setUniformBuffer(render::ShapePipeline::Slot::LIGHTING_MODEL, lightingModel->getParametersBuffer()); - // From the lighting model define a global shapKey ORED with individiual keys - ShapeKey::Builder keyBuilder; - if (lightingModel->isWireframeEnabled()) { - keyBuilder.withWireframe(); - } - ShapeKey globalKey = keyBuilder.build(); - args->_globalShapeKey = globalKey._flags.to_ulong(); - - renderShapes(sceneContext, renderContext, _shapePlumber, inItems, _maxDrawn, globalKey); - + renderShapes(sceneContext, renderContext, _shapePlumber, inItems, _maxDrawn); args->_batch = nullptr; - args->_globalShapeKey = 0; }); config->setNumDrawn((int)inItems.size()); @@ -305,21 +295,12 @@ void DrawStateSortDeferred::run(const SceneContextPointer& sceneContext, const R // Setup lighting model for all items; batch.setUniformBuffer(render::ShapePipeline::Slot::LIGHTING_MODEL, lightingModel->getParametersBuffer()); - // From the lighting model define a global shapKey ORED with individiual keys - ShapeKey::Builder keyBuilder; - if (lightingModel->isWireframeEnabled()) { - keyBuilder.withWireframe(); - } - ShapeKey globalKey = keyBuilder.build(); - args->_globalShapeKey = globalKey._flags.to_ulong(); - if (_stateSort) { - renderStateSortShapes(sceneContext, renderContext, _shapePlumber, inItems, _maxDrawn, globalKey); + renderStateSortShapes(sceneContext, renderContext, _shapePlumber, inItems, _maxDrawn); } else { - renderShapes(sceneContext, renderContext, _shapePlumber, inItems, _maxDrawn, globalKey); + renderShapes(sceneContext, renderContext, _shapePlumber, inItems, _maxDrawn); } args->_batch = nullptr; - args->_globalShapeKey = 0; }); config->setNumDrawn((int)inItems.size()); diff --git a/libraries/render-utils/src/RenderPipelines.cpp b/libraries/render-utils/src/RenderPipelines.cpp index 414bcf0d63..4fbac4170e 100644 --- a/libraries/render-utils/src/RenderPipelines.cpp +++ b/libraries/render-utils/src/RenderPipelines.cpp @@ -307,7 +307,7 @@ void initForwardPipelines(render::ShapePlumber& plumber) { void addPlumberPipeline(ShapePlumber& plumber, const ShapeKey& key, const gpu::ShaderPointer& vertex, const gpu::ShaderPointer& pixel) { // These key-values' pipelines are added by this functor in addition to the key passed - assert(!key.isWireframe()); + assert(!key.isWireFrame()); assert(!key.isDepthBiased()); assert(key.isCullFace()); diff --git a/libraries/render-utils/src/SubsurfaceScattering.cpp b/libraries/render-utils/src/SubsurfaceScattering.cpp index 25a01bff1b..188381b822 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.cpp +++ b/libraries/render-utils/src/SubsurfaceScattering.cpp @@ -414,7 +414,7 @@ gpu::TexturePointer SubsurfaceScatteringResource::generateScatteringProfile(Rend const int PROFILE_RESOLUTION = 512; // const auto pixelFormat = gpu::Element::COLOR_SRGBA_32; const auto pixelFormat = gpu::Element::COLOR_R11G11B10; - auto profileMap = gpu::TexturePointer(gpu::Texture::createRenderBuffer(pixelFormat, PROFILE_RESOLUTION, 1, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP))); + auto profileMap = gpu::TexturePointer(gpu::Texture::create2D(pixelFormat, PROFILE_RESOLUTION, 1, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP))); profileMap->setSource("Generated Scattering Profile"); diffuseProfileGPU(profileMap, args); return profileMap; @@ -425,7 +425,7 @@ gpu::TexturePointer SubsurfaceScatteringResource::generatePreIntegratedScatterin const int TABLE_RESOLUTION = 512; // const auto pixelFormat = gpu::Element::COLOR_SRGBA_32; const auto pixelFormat = gpu::Element::COLOR_R11G11B10; - auto scatteringLUT = gpu::TexturePointer(gpu::Texture::createRenderBuffer(pixelFormat, TABLE_RESOLUTION, TABLE_RESOLUTION, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP))); + auto scatteringLUT = gpu::TexturePointer(gpu::Texture::create2D(pixelFormat, TABLE_RESOLUTION, TABLE_RESOLUTION, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP))); //diffuseScatter(scatteringLUT); scatteringLUT->setSource("Generated pre-integrated scattering"); diffuseScatterGPU(profile, scatteringLUT, args); @@ -434,7 +434,7 @@ gpu::TexturePointer SubsurfaceScatteringResource::generatePreIntegratedScatterin gpu::TexturePointer SubsurfaceScatteringResource::generateScatteringSpecularBeckmann(RenderArgs* args) { const int SPECULAR_RESOLUTION = 256; - auto beckmannMap = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32 /*gpu::Element(gpu::SCALAR, gpu::HALF, gpu::RGB)*/, SPECULAR_RESOLUTION, SPECULAR_RESOLUTION, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP))); + auto beckmannMap = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32 /*gpu::Element(gpu::SCALAR, gpu::HALF, gpu::RGB)*/, SPECULAR_RESOLUTION, SPECULAR_RESOLUTION, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP))); beckmannMap->setSource("Generated beckmannMap"); computeSpecularBeckmannGPU(beckmannMap, args); return beckmannMap; diff --git a/libraries/render-utils/src/SurfaceGeometryPass.cpp b/libraries/render-utils/src/SurfaceGeometryPass.cpp index 3a23e70664..f0ac56ac26 100644 --- a/libraries/render-utils/src/SurfaceGeometryPass.cpp +++ b/libraries/render-utils/src/SurfaceGeometryPass.cpp @@ -72,18 +72,18 @@ void LinearDepthFramebuffer::allocate() { auto height = _frameSize.y; // For Linear Depth: - _linearDepthTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::RGB), width, height, + _linearDepthTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::RGB), width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); _linearDepthFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("linearDepth")); _linearDepthFramebuffer->setRenderBuffer(0, _linearDepthTexture); _linearDepthFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, _primaryDepthTexture->getTexelFormat()); // For Downsampling: - _halfLinearDepthTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::RGB), _halfFrameSize.x, _halfFrameSize.y, + _halfLinearDepthTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::RGB), _halfFrameSize.x, _halfFrameSize.y, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); _halfLinearDepthTexture->autoGenerateMips(5); - _halfNormalTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element(gpu::VEC3, gpu::NUINT8, gpu::RGB), _halfFrameSize.x, _halfFrameSize.y, + _halfNormalTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element(gpu::VEC3, gpu::NUINT8, gpu::RGB), _halfFrameSize.x, _halfFrameSize.y, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); _downsampleFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("halfLinearDepth")); @@ -304,15 +304,15 @@ void SurfaceGeometryFramebuffer::allocate() { auto width = _frameSize.x; auto height = _frameSize.y; - _curvatureTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32, width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); + _curvatureTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); _curvatureFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("surfaceGeometry::curvature")); _curvatureFramebuffer->setRenderBuffer(0, _curvatureTexture); - _lowCurvatureTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32, width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); + _lowCurvatureTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); _lowCurvatureFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("surfaceGeometry::lowCurvature")); _lowCurvatureFramebuffer->setRenderBuffer(0, _lowCurvatureTexture); - _blurringTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32, width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); + _blurringTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); _blurringFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("surfaceGeometry::blurring")); _blurringFramebuffer->setRenderBuffer(0, _blurringTexture); } diff --git a/libraries/render-utils/src/text/Font.cpp b/libraries/render-utils/src/text/Font.cpp index c405f6d6ae..4f4ee12622 100644 --- a/libraries/render-utils/src/text/Font.cpp +++ b/libraries/render-utils/src/text/Font.cpp @@ -209,8 +209,7 @@ void Font::read(QIODevice& in) { } _texture = gpu::TexturePointer(gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_POINT_MAG_LINEAR))); - _texture->setStoredMipFormat(formatMip); - _texture->assignStoredMip(0, image.byteCount(), image.constBits()); + _texture->assignStoredMip(0, formatMip, image.byteCount(), image.constBits()); } void Font::setupGPU() { diff --git a/libraries/render/CMakeLists.txt b/libraries/render/CMakeLists.txt index 8fd05bd320..735bb7f086 100644 --- a/libraries/render/CMakeLists.txt +++ b/libraries/render/CMakeLists.txt @@ -3,6 +3,6 @@ AUTOSCRIBE_SHADER_LIB(gpu model) setup_hifi_library() # render needs octree only for getAccuracyAngle(float, int) -link_hifi_libraries(shared ktx gpu model octree) +link_hifi_libraries(shared gpu model octree) target_nsight() diff --git a/libraries/render/src/render/DrawTask.cpp b/libraries/render/src/render/DrawTask.cpp index e8537e3452..2829c6f8e7 100755 --- a/libraries/render/src/render/DrawTask.cpp +++ b/libraries/render/src/render/DrawTask.cpp @@ -39,9 +39,9 @@ void render::renderItems(const SceneContextPointer& sceneContext, const RenderCo } } -void renderShape(RenderArgs* args, const ShapePlumberPointer& shapeContext, const Item& item, const ShapeKey& globalKey) { +void renderShape(RenderArgs* args, const ShapePlumberPointer& shapeContext, const Item& item) { assert(item.getKey().isShape()); - auto key = item.getShapeKey() | globalKey; + const auto& key = item.getShapeKey(); if (key.isValid() && !key.hasOwnPipeline()) { args->_pipeline = shapeContext->pickPipeline(args, key); if (args->_pipeline) { @@ -56,7 +56,7 @@ void renderShape(RenderArgs* args, const ShapePlumberPointer& shapeContext, cons } void render::renderShapes(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, - const ShapePlumberPointer& shapeContext, const ItemBounds& inItems, int maxDrawnItems, const ShapeKey& globalKey) { + const ShapePlumberPointer& shapeContext, const ItemBounds& inItems, int maxDrawnItems) { auto& scene = sceneContext->_scene; RenderArgs* args = renderContext->args; @@ -66,12 +66,12 @@ void render::renderShapes(const SceneContextPointer& sceneContext, const RenderC } for (auto i = 0; i < numItemsToDraw; ++i) { auto& item = scene->getItem(inItems[i].id); - renderShape(args, shapeContext, item, globalKey); + renderShape(args, shapeContext, item); } } void render::renderStateSortShapes(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, - const ShapePlumberPointer& shapeContext, const ItemBounds& inItems, int maxDrawnItems, const ShapeKey& globalKey) { + const ShapePlumberPointer& shapeContext, const ItemBounds& inItems, int maxDrawnItems) { auto& scene = sceneContext->_scene; RenderArgs* args = renderContext->args; @@ -91,7 +91,7 @@ void render::renderStateSortShapes(const SceneContextPointer& sceneContext, cons { assert(item.getKey().isShape()); - auto key = item.getShapeKey() | globalKey; + const auto key = item.getShapeKey(); if (key.isValid() && !key.hasOwnPipeline()) { auto& bucket = sortedShapes[key]; if (bucket.empty()) { diff --git a/libraries/render/src/render/DrawTask.h b/libraries/render/src/render/DrawTask.h index a9c5f3a4d8..6e0e5ba10b 100755 --- a/libraries/render/src/render/DrawTask.h +++ b/libraries/render/src/render/DrawTask.h @@ -17,8 +17,8 @@ namespace render { void renderItems(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const ItemBounds& inItems, int maxDrawnItems = -1); -void renderShapes(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const ShapePlumberPointer& shapeContext, const ItemBounds& inItems, int maxDrawnItems = -1, const ShapeKey& globalKey = ShapeKey()); -void renderStateSortShapes(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const ShapePlumberPointer& shapeContext, const ItemBounds& inItems, int maxDrawnItems = -1, const ShapeKey& globalKey = ShapeKey()); +void renderShapes(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const ShapePlumberPointer& shapeContext, const ItemBounds& inItems, int maxDrawnItems = -1); +void renderStateSortShapes(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const ShapePlumberPointer& shapeContext, const ItemBounds& inItems, int maxDrawnItems = -1); class DrawLightConfig : public Job::Config { Q_OBJECT diff --git a/libraries/render/src/render/ShapePipeline.h b/libraries/render/src/render/ShapePipeline.h index 73e8f82f24..0c77a15184 100644 --- a/libraries/render/src/render/ShapePipeline.h +++ b/libraries/render/src/render/ShapePipeline.h @@ -46,10 +46,6 @@ public: ShapeKey() : _flags{ 0 } {} ShapeKey(const Flags& flags) : _flags{flags} {} - friend ShapeKey operator&(const ShapeKey& _Left, const ShapeKey& _Right) { return ShapeKey(_Left._flags & _Right._flags); } - friend ShapeKey operator|(const ShapeKey& _Left, const ShapeKey& _Right) { return ShapeKey(_Left._flags | _Right._flags); } - friend ShapeKey operator^(const ShapeKey& _Left, const ShapeKey& _Right) { return ShapeKey(_Left._flags ^ _Right._flags); } - class Builder { public: Builder() {} @@ -148,7 +144,7 @@ public: bool isSkinned() const { return _flags[SKINNED]; } bool isDepthOnly() const { return _flags[DEPTH_ONLY]; } bool isDepthBiased() const { return _flags[DEPTH_BIAS]; } - bool isWireframe() const { return _flags[WIREFRAME]; } + bool isWireFrame() const { return _flags[WIREFRAME]; } bool isCullFace() const { return !_flags[NO_CULL_FACE]; } bool hasOwnPipeline() const { return _flags[OWN_PIPELINE]; } @@ -184,7 +180,7 @@ inline QDebug operator<<(QDebug debug, const ShapeKey& key) { << "isSkinned:" << key.isSkinned() << "isDepthOnly:" << key.isDepthOnly() << "isDepthBiased:" << key.isDepthBiased() - << "isWireframe:" << key.isWireframe() + << "isWireFrame:" << key.isWireFrame() << "isCullFace:" << key.isCullFace() << "]"; } diff --git a/libraries/render/src/render/drawItemStatus.slv b/libraries/render/src/render/drawItemStatus.slv index 792f2733c5..cb4ae7ebd2 100644 --- a/libraries/render/src/render/drawItemStatus.slv +++ b/libraries/render/src/render/drawItemStatus.slv @@ -75,7 +75,7 @@ void main(void) { vec4(1.0, 1.0, 0.0, 1.0) ); - const vec2 ICON_PIXEL_SIZE = vec2(36, 36); + const vec2 ICON_PIXEL_SIZE = vec2(20, 20); const vec2 MARGIN_PIXEL_SIZE = vec2(2, 2); const vec2 ICON_GRID_SLOTS[MAX_NUM_ICONS] = vec2[MAX_NUM_ICONS](vec2(-1.5, 0.5), vec2(-0.5, 0.5), @@ -114,7 +114,7 @@ void main(void) { varColor = vec4(paintRainbow(abs(iconStatus.y)), 1.0); // Pass the texcoord and the z texcoord is representing the texture icon - varTexcoord = vec3( (quadPos.x + 1.0) * 0.5, (quadPos.y + 1.0) * -0.5, iconStatus.z); + varTexcoord = vec3((quadPos.xy + 1.0) * 0.5, iconStatus.z); // Also changes the size of the notification vec2 iconScale = ICON_PIXEL_SIZE; diff --git a/libraries/script-engine/src/AudioScriptingInterface.cpp b/libraries/script-engine/src/AudioScriptingInterface.cpp index 8452494d95..fcc1f201f9 100644 --- a/libraries/script-engine/src/AudioScriptingInterface.cpp +++ b/libraries/script-engine/src/AudioScriptingInterface.cpp @@ -19,6 +19,11 @@ void registerAudioMetaTypes(QScriptEngine* engine) { qScriptRegisterMetaType(engine, soundSharedPointerToScriptValue, soundSharedPointerFromScriptValue); } +AudioScriptingInterface& AudioScriptingInterface::getInstance() { + static AudioScriptingInterface staticInstance; + return staticInstance; +} + AudioScriptingInterface::AudioScriptingInterface() : _localAudioInterface(NULL) { diff --git a/libraries/script-engine/src/AudioScriptingInterface.h b/libraries/script-engine/src/AudioScriptingInterface.h index e97bc329c6..6cce78d48f 100644 --- a/libraries/script-engine/src/AudioScriptingInterface.h +++ b/libraries/script-engine/src/AudioScriptingInterface.h @@ -14,20 +14,18 @@ #include #include -#include #include class ScriptAudioInjector; -class AudioScriptingInterface : public QObject, public Dependency { +class AudioScriptingInterface : public QObject { Q_OBJECT - SINGLETON_DEPENDENCY - public: + static AudioScriptingInterface& getInstance(); + void setLocalAudioInterface(AbstractAudioInterface* audioInterface) { _localAudioInterface = audioInterface; } protected: - // this method is protected to stop C++ callers from calling, but invokable from script Q_INVOKABLE ScriptAudioInjector* playSound(SharedSoundPointer sound, const AudioInjectorOptions& injectorOptions = AudioInjectorOptions()); @@ -44,7 +42,6 @@ signals: private: AudioScriptingInterface(); - AbstractAudioInterface* _localAudioInterface; }; diff --git a/libraries/shared/src/BaseScriptEngine.cpp b/libraries/script-engine/src/BaseScriptEngine.cpp similarity index 68% rename from libraries/shared/src/BaseScriptEngine.cpp rename to libraries/script-engine/src/BaseScriptEngine.cpp index c92d629b75..16308c0650 100644 --- a/libraries/shared/src/BaseScriptEngine.cpp +++ b/libraries/script-engine/src/BaseScriptEngine.cpp @@ -10,7 +10,6 @@ // #include "BaseScriptEngine.h" -#include "SharedLogging.h" #include #include @@ -19,27 +18,18 @@ #include #include +#include "ScriptEngineLogging.h" #include "Profile.h" +const QString BaseScriptEngine::_SETTINGS_ENABLE_EXTENDED_EXCEPTIONS { + "com.highfidelity.experimental.enableExtendedJSExceptions" +}; + const QString BaseScriptEngine::SCRIPT_EXCEPTION_FORMAT { "[%0] %1 in %2:%3" }; const QString BaseScriptEngine::SCRIPT_BACKTRACE_SEP { "\n " }; -bool BaseScriptEngine::IS_THREADSAFE_INVOCATION(const QThread *thread, const QString& method) { - if (QThread::currentThread() == thread) { - return true; - } - qCCritical(shared) << QString("Scripting::%1 @ %2 -- ignoring thread-unsafe call from %3") - .arg(method).arg(thread ? thread->objectName() : "(!thread)").arg(QThread::currentThread()->objectName()); - qCDebug(shared) << "(please resolve on the calling side by using invokeMethod, executeOnScriptThread, etc.)"; - Q_ASSERT(false); - return false; -} - // engine-aware JS Error copier and factory QScriptValue BaseScriptEngine::makeError(const QScriptValue& _other, const QString& type) { - if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { - return unboundNullValue(); - } auto other = _other; if (other.isString()) { other = newObject(); @@ -51,7 +41,7 @@ QScriptValue BaseScriptEngine::makeError(const QScriptValue& _other, const QStri } if (!proto.isFunction()) { #ifdef DEBUG_JS_EXCEPTIONS - qCDebug(shared) << "BaseScriptEngine::makeError -- couldn't find constructor for" << type << " -- using Error instead"; + qCDebug(scriptengine) << "BaseScriptEngine::makeError -- couldn't find constructor for" << type << " -- using Error instead"; #endif proto = globalObject().property("Error"); } @@ -74,9 +64,6 @@ QScriptValue BaseScriptEngine::makeError(const QScriptValue& _other, const QStri // check syntax and when there are issues returns an actual "SyntaxError" with the details QScriptValue BaseScriptEngine::lintScript(const QString& sourceCode, const QString& fileName, const int lineNumber) { - if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { - return unboundNullValue(); - } const auto syntaxCheck = checkSyntax(sourceCode); if (syntaxCheck.state() != QScriptSyntaxCheckResult::Valid) { auto err = globalObject().property("SyntaxError") @@ -95,16 +82,13 @@ QScriptValue BaseScriptEngine::lintScript(const QString& sourceCode, const QStri } return err; } - return QScriptValue(); + return undefinedValue(); } // this pulls from the best available information to create a detailed snapshot of the current exception QScriptValue BaseScriptEngine::cloneUncaughtException(const QString& extraDetail) { - if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { - return unboundNullValue(); - } if (!hasUncaughtException()) { - return unboundNullValue(); + return QScriptValue(); } auto exception = uncaughtException(); // ensure the error object is engine-local @@ -160,10 +144,7 @@ QScriptValue BaseScriptEngine::cloneUncaughtException(const QString& extraDetail return err; } -QString BaseScriptEngine::formatException(const QScriptValue& exception, bool includeExtendedDetails) { - if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { - return QString(); - } +QString BaseScriptEngine::formatException(const QScriptValue& exception) { QString note { "UncaughtException" }; QString result; @@ -175,8 +156,8 @@ QString BaseScriptEngine::formatException(const QScriptValue& exception, bool in const auto lineNumber = exception.property("lineNumber").toString(); const auto stacktrace = exception.property("stack").toString(); - if (includeExtendedDetails) { - // Display additional exception / troubleshooting hints that can be added via the custom Error .detail property + if (_enableExtendedJSExceptions.get()) { + // This setting toggles display of the hints now being added during the loading process. // Example difference: // [UncaughtExceptions] Error: Can't find variable: foobar in atp:/myentity.js\n... // [UncaughtException (construct {1eb5d3fa-23b1-411c-af83-163af7220e3f})] Error: Can't find variable: foobar in atp:/myentity.js\n... @@ -192,39 +173,14 @@ QString BaseScriptEngine::formatException(const QScriptValue& exception, bool in return result; } -bool BaseScriptEngine::raiseException(const QScriptValue& exception) { - if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { - return false; - } - if (currentContext()) { - // we have an active context / JS stack frame so throw the exception per usual - currentContext()->throwValue(makeError(exception)); - return true; - } else { - // we are within a pure C++ stack frame (ie: being called directly by other C++ code) - // in this case no context information is available so just emit the exception for reporting - emit unhandledException(makeError(exception)); - } - return false; -} - -bool BaseScriptEngine::maybeEmitUncaughtException(const QString& debugHint) { - if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { - return false; - } - if (!isEvaluating() && hasUncaughtException()) { - emit unhandledException(cloneUncaughtException(debugHint)); - clearExceptions(); - return true; - } - return false; -} - QScriptValue BaseScriptEngine::evaluateInClosure(const QScriptValue& closure, const QScriptProgram& program) { PROFILE_RANGE(script, "evaluateInClosure"); - if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { - return unboundNullValue(); + if (QThread::currentThread() != thread()) { + qCCritical(scriptengine) << "*** CRITICAL *** ScriptEngine::evaluateInClosure() is meant to be called from engine thread only."; + // note: a recursive mutex might be needed around below code if this method ever becomes Q_INVOKABLE + return QScriptValue(); } + const auto fileName = program.fileName(); const auto shortName = QUrl(fileName).fileName(); @@ -233,7 +189,7 @@ QScriptValue BaseScriptEngine::evaluateInClosure(const QScriptValue& closure, co auto global = closure.property("global"); if (global.isObject()) { #ifdef DEBUG_JS - qCDebug(shared) << " setting global = closure.global" << shortName; + qCDebug(scriptengine) << " setting global = closure.global" << shortName; #endif oldGlobal = globalObject(); setGlobalObject(global); @@ -244,34 +200,34 @@ QScriptValue BaseScriptEngine::evaluateInClosure(const QScriptValue& closure, co auto thiz = closure.property("this"); if (thiz.isObject()) { #ifdef DEBUG_JS - qCDebug(shared) << " setting this = closure.this" << shortName; + qCDebug(scriptengine) << " setting this = closure.this" << shortName; #endif context->setThisObject(thiz); } context->pushScope(closure); #ifdef DEBUG_JS - qCDebug(shared) << QString("[%1] evaluateInClosure %2").arg(isEvaluating()).arg(shortName); + qCDebug(scriptengine) << QString("[%1] evaluateInClosure %2").arg(isEvaluating()).arg(shortName); #endif { result = BaseScriptEngine::evaluate(program); if (hasUncaughtException()) { auto err = cloneUncaughtException(__FUNCTION__); #ifdef DEBUG_JS_EXCEPTIONS - qCWarning(shared) << __FUNCTION__ << "---------- hasCaught:" << err.toString() << result.toString(); + qCWarning(scriptengine) << __FUNCTION__ << "---------- hasCaught:" << err.toString() << result.toString(); err.setProperty("_result", result); #endif result = err; } } #ifdef DEBUG_JS - qCDebug(shared) << QString("[%1] //evaluateInClosure %2").arg(isEvaluating()).arg(shortName); + qCDebug(scriptengine) << QString("[%1] //evaluateInClosure %2").arg(isEvaluating()).arg(shortName); #endif popContext(); if (oldGlobal.isValid()) { #ifdef DEBUG_JS - qCDebug(shared) << " restoring global" << shortName; + qCDebug(scriptengine) << " restoring global" << shortName; #endif setGlobalObject(oldGlobal); } @@ -280,6 +236,7 @@ QScriptValue BaseScriptEngine::evaluateInClosure(const QScriptValue& closure, co } // Lambda + QScriptValue BaseScriptEngine::newLambdaFunction(std::function operation, const QScriptValue& data, const QScriptEngine::ValueOwnership& ownership) { auto lambda = new Lambda(this, operation, data); auto object = newQObject(lambda, ownership); @@ -305,57 +262,26 @@ Lambda::Lambda(QScriptEngine *engine, std::functionthread(), __FUNCTION__)) { - return BaseScriptEngine::unboundNullValue(); - } return operation(engine->currentContext(), engine); } -QScriptValue makeScopedHandlerObject(QScriptValue scopeOrCallback, QScriptValue methodOrName) { - auto engine = scopeOrCallback.engine(); - if (!engine) { - return scopeOrCallback; - } - auto scope = QScriptValue(); - auto callback = scopeOrCallback; - if (scopeOrCallback.isObject()) { - if (methodOrName.isString()) { - scope = scopeOrCallback; - callback = scope.property(methodOrName.toString()); - } else if (methodOrName.isFunction()) { - scope = scopeOrCallback; - callback = methodOrName; - } - } - auto handler = engine->newObject(); - handler.setProperty("scope", scope); - handler.setProperty("callback", callback); - return handler; -} - -QScriptValue callScopedHandlerObject(QScriptValue handler, QScriptValue err, QScriptValue result) { - return handler.property("callback").call(handler.property("scope"), QScriptValueList({ err, result })); -} - #ifdef DEBUG_JS void BaseScriptEngine::_debugDump(const QString& header, const QScriptValue& object, const QString& footer) { - if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { - return; - } if (!header.isEmpty()) { - qCDebug(shared) << header; + qCDebug(scriptengine) << header; } if (!object.isObject()) { - qCDebug(shared) << "(!isObject)" << object.toVariant().toString() << object.toString(); + qCDebug(scriptengine) << "(!isObject)" << object.toVariant().toString() << object.toString(); return; } QScriptValueIterator it(object); while (it.hasNext()) { it.next(); - qCDebug(shared) << it.name() << ":" << it.value().toString(); + qCDebug(scriptengine) << it.name() << ":" << it.value().toString(); } if (!footer.isEmpty()) { - qCDebug(shared) << footer; + qCDebug(scriptengine) << footer; } } #endif + diff --git a/libraries/script-engine/src/BaseScriptEngine.h b/libraries/script-engine/src/BaseScriptEngine.h new file mode 100644 index 0000000000..27a6eff33d --- /dev/null +++ b/libraries/script-engine/src/BaseScriptEngine.h @@ -0,0 +1,67 @@ +// +// BaseScriptEngine.h +// libraries/script-engine/src +// +// Created by Timothy Dedischew on 02/01/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_BaseScriptEngine_h +#define hifi_BaseScriptEngine_h + +#include +#include +#include + +#include "SettingHandle.h" + +// common base class for extending QScriptEngine itself +class BaseScriptEngine : public QScriptEngine { + Q_OBJECT +public: + static const QString SCRIPT_EXCEPTION_FORMAT; + static const QString SCRIPT_BACKTRACE_SEP; + + BaseScriptEngine() {} + + Q_INVOKABLE QScriptValue evaluateInClosure(const QScriptValue& locals, const QScriptProgram& program); + + Q_INVOKABLE QScriptValue lintScript(const QString& sourceCode, const QString& fileName, const int lineNumber = 1); + Q_INVOKABLE QScriptValue makeError(const QScriptValue& other = QScriptValue(), const QString& type = "Error"); + Q_INVOKABLE QString formatException(const QScriptValue& exception); + QScriptValue cloneUncaughtException(const QString& detail = QString()); + +signals: + void unhandledException(const QScriptValue& exception); + +protected: + void _emitUnhandledException(const QScriptValue& exception); + QScriptValue newLambdaFunction(std::function operation, const QScriptValue& data = QScriptValue(), const QScriptEngine::ValueOwnership& ownership = QScriptEngine::AutoOwnership); + + static const QString _SETTINGS_ENABLE_EXTENDED_EXCEPTIONS; + Setting::Handle _enableExtendedJSExceptions { _SETTINGS_ENABLE_EXTENDED_EXCEPTIONS, true }; +#ifdef DEBUG_JS + static void _debugDump(const QString& header, const QScriptValue& object, const QString& footer = QString()); +#endif +}; + +// Lambda helps create callable QScriptValues out of std::functions: +// (just meant for use from within the script engine itself) +class Lambda : public QObject { + Q_OBJECT +public: + Lambda(QScriptEngine *engine, std::function operation, QScriptValue data); + ~Lambda(); + public slots: + QScriptValue call(); + QString toString() const; +private: + QScriptEngine* engine; + std::function operation; + QScriptValue data; +}; + +#endif // hifi_BaseScriptEngine_h diff --git a/libraries/script-engine/src/MeshProxy.h b/libraries/script-engine/src/MeshProxy.h deleted file mode 100644 index 82f5038348..0000000000 --- a/libraries/script-engine/src/MeshProxy.h +++ /dev/null @@ -1,41 +0,0 @@ -// -// MeshProxy.h -// libraries/script-engine/src -// -// Created by Seth Alves on 2017-1-27. -// Copyright 2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#ifndef hifi_MeshProxy_h -#define hifi_MeshProxy_h - -#include - -using MeshPointer = std::shared_ptr; - -class MeshProxy : public QObject { - Q_OBJECT - -public: - MeshProxy(MeshPointer mesh) : _mesh(mesh) {} - ~MeshProxy() {} - - MeshPointer getMeshPointer() const { return _mesh; } - - Q_INVOKABLE int getNumVertices() const { return (int)_mesh->getNumVertices(); } - Q_INVOKABLE glm::vec3 getPos3(int index) const { return _mesh->getPos3(index); } - - -protected: - MeshPointer _mesh; -}; - -Q_DECLARE_METATYPE(MeshProxy*); - -class MeshProxyList : public QList {}; // typedef and using fight with the Qt macros/templates, do this instead -Q_DECLARE_METATYPE(MeshProxyList); - -#endif // hifi_MeshProxy_h diff --git a/libraries/script-engine/src/ModelScriptingInterface.cpp b/libraries/script-engine/src/ModelScriptingInterface.cpp deleted file mode 100644 index 833ac5b64d..0000000000 --- a/libraries/script-engine/src/ModelScriptingInterface.cpp +++ /dev/null @@ -1,159 +0,0 @@ -// -// ModelScriptingInterface.cpp -// libraries/script-engine/src -// -// Created by Seth Alves on 2017-1-27. -// Copyright 2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include -#include -#include -#include "ScriptEngine.h" -#include "ModelScriptingInterface.h" -#include "OBJWriter.h" - -ModelScriptingInterface::ModelScriptingInterface(QObject* parent) : QObject(parent) { - _modelScriptEngine = qobject_cast(parent); -} - -QScriptValue meshToScriptValue(QScriptEngine* engine, MeshProxy* const &in) { - return engine->newQObject(in, QScriptEngine::QtOwnership, - QScriptEngine::ExcludeDeleteLater | QScriptEngine::ExcludeChildObjects); -} - -void meshFromScriptValue(const QScriptValue& value, MeshProxy* &out) { - out = qobject_cast(value.toQObject()); -} - -QScriptValue meshesToScriptValue(QScriptEngine* engine, const MeshProxyList &in) { - return engine->toScriptValue(in); -} - -void meshesFromScriptValue(const QScriptValue& value, MeshProxyList &out) { - QScriptValueIterator itr(value); - while(itr.hasNext()) { - itr.next(); - MeshProxy* meshProxy = qscriptvalue_cast(itr.value()); - if (meshProxy) { - out.append(meshProxy); - } - } -} - -QString ModelScriptingInterface::meshToOBJ(MeshProxyList in) { - QList meshes; - foreach (const MeshProxy* meshProxy, in) { - meshes.append(meshProxy->getMeshPointer()); - } - - return writeOBJToString(meshes); -} - -QScriptValue ModelScriptingInterface::appendMeshes(MeshProxyList in) { - // figure out the size of the resulting mesh - size_t totalVertexCount { 0 }; - size_t totalAttributeCount { 0 }; - size_t totalIndexCount { 0 }; - foreach (const MeshProxy* meshProxy, in) { - MeshPointer mesh = meshProxy->getMeshPointer(); - totalVertexCount += mesh->getNumVertices(); - - int attributeTypeNormal = gpu::Stream::InputSlot::NORMAL; // libraries/gpu/src/gpu/Stream.h - const gpu::BufferView& normalsBufferView = mesh->getAttributeBuffer(attributeTypeNormal); - gpu::BufferView::Index numNormals = (gpu::BufferView::Index)normalsBufferView.getNumElements(); - totalAttributeCount += numNormals; - - totalIndexCount += mesh->getNumIndices(); - } - - // alloc the resulting mesh - gpu::Resource::Size combinedVertexSize = totalVertexCount * sizeof(glm::vec3); - unsigned char* combinedVertexData = new unsigned char[combinedVertexSize]; - unsigned char* combinedVertexDataCursor = combinedVertexData; - - gpu::Resource::Size combinedNormalSize = totalAttributeCount * sizeof(glm::vec3); - unsigned char* combinedNormalData = new unsigned char[combinedNormalSize]; - unsigned char* combinedNormalDataCursor = combinedNormalData; - - gpu::Resource::Size combinedIndexSize = totalIndexCount * sizeof(uint32_t); - unsigned char* combinedIndexData = new unsigned char[combinedIndexSize]; - unsigned char* combinedIndexDataCursor = combinedIndexData; - - uint32_t indexStartOffset { 0 }; - - foreach (const MeshProxy* meshProxy, in) { - MeshPointer mesh = meshProxy->getMeshPointer(); - mesh->forEach( - [&](glm::vec3 position){ - memcpy(combinedVertexDataCursor, &position, sizeof(position)); - combinedVertexDataCursor += sizeof(position); - }, - [&](glm::vec3 normal){ - memcpy(combinedNormalDataCursor, &normal, sizeof(normal)); - combinedNormalDataCursor += sizeof(normal); - }, - [&](uint32_t index){ - index += indexStartOffset; - memcpy(combinedIndexDataCursor, &index, sizeof(index)); - combinedIndexDataCursor += sizeof(index); - }); - - gpu::BufferView::Index numVertices = (gpu::BufferView::Index)mesh->getNumVertices(); - indexStartOffset += numVertices; - } - - model::MeshPointer result(new model::Mesh()); - - gpu::Element vertexElement = gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ); - gpu::Buffer* combinedVertexBuffer = new gpu::Buffer(combinedVertexSize, combinedVertexData); - gpu::BufferPointer combinedVertexBufferPointer(combinedVertexBuffer); - gpu::BufferView combinedVertexBufferView(combinedVertexBufferPointer, vertexElement); - result->setVertexBuffer(combinedVertexBufferView); - - int attributeTypeNormal = gpu::Stream::InputSlot::NORMAL; // libraries/gpu/src/gpu/Stream.h - gpu::Element normalElement = gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ); - gpu::Buffer* combinedNormalsBuffer = new gpu::Buffer(combinedNormalSize, combinedNormalData); - gpu::BufferPointer combinedNormalsBufferPointer(combinedNormalsBuffer); - gpu::BufferView combinedNormalsBufferView(combinedNormalsBufferPointer, normalElement); - result->addAttribute(attributeTypeNormal, combinedNormalsBufferView); - - gpu::Element indexElement = gpu::Element(gpu::SCALAR, gpu::UINT32, gpu::RAW); - gpu::Buffer* combinedIndexesBuffer = new gpu::Buffer(combinedIndexSize, combinedIndexData); - gpu::BufferPointer combinedIndexesBufferPointer(combinedIndexesBuffer); - gpu::BufferView combinedIndexesBufferView(combinedIndexesBufferPointer, indexElement); - result->setIndexBuffer(combinedIndexesBufferView); - - std::vector parts; - parts.emplace_back(model::Mesh::Part((model::Index)0, // startIndex - (model::Index)result->getNumIndices(), // numIndices - (model::Index)0, // baseVertex - model::Mesh::TRIANGLES)); // topology - result->setPartBuffer(gpu::BufferView(new gpu::Buffer(parts.size() * sizeof(model::Mesh::Part), - (gpu::Byte*) parts.data()), gpu::Element::PART_DRAWCALL)); - - - MeshProxy* resultProxy = new MeshProxy(result); - return meshToScriptValue(_modelScriptEngine, resultProxy); -} - - - -QScriptValue ModelScriptingInterface::transformMesh(glm::mat4 transform, MeshProxy* meshProxy) { - if (!meshProxy) { - return QScriptValue(false); - } - MeshPointer mesh = meshProxy->getMeshPointer(); - if (!mesh) { - return QScriptValue(false); - } - - model::MeshPointer result = mesh->map([&](glm::vec3 position){ return glm::vec3(transform * glm::vec4(position, 1.0f)); }, - [&](glm::vec3 normal){ return glm::vec3(transform * glm::vec4(normal, 0.0f)); }, - [&](uint32_t index){ return index; }); - MeshProxy* resultProxy = new MeshProxy(result); - return meshToScriptValue(_modelScriptEngine, resultProxy); -} diff --git a/libraries/script-engine/src/ModelScriptingInterface.h b/libraries/script-engine/src/ModelScriptingInterface.h deleted file mode 100644 index 14789943e3..0000000000 --- a/libraries/script-engine/src/ModelScriptingInterface.h +++ /dev/null @@ -1,45 +0,0 @@ -// -// ModelScriptingInterface.h -// libraries/script-engine/src -// -// Created by Seth Alves on 2017-1-27. -// Copyright 2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - - -#ifndef hifi_ModelScriptingInterface_h -#define hifi_ModelScriptingInterface_h - -#include -#include -#include -#include -#include "MeshProxy.h" - -using MeshPointer = std::shared_ptr; -class ScriptEngine; - -class ModelScriptingInterface : public QObject { - Q_OBJECT - -public: - ModelScriptingInterface(QObject* parent); - - Q_INVOKABLE QString meshToOBJ(MeshProxyList in); - Q_INVOKABLE QScriptValue appendMeshes(MeshProxyList in); - Q_INVOKABLE QScriptValue transformMesh(glm::mat4 transform, MeshProxy* meshProxy); - -private: - ScriptEngine* _modelScriptEngine { nullptr }; -}; - -QScriptValue meshToScriptValue(QScriptEngine* engine, MeshProxy* const &in); -void meshFromScriptValue(const QScriptValue& value, MeshProxy* &out); - -QScriptValue meshesToScriptValue(QScriptEngine* engine, const MeshProxyList &in); -void meshesFromScriptValue(const QScriptValue& value, MeshProxyList &out); - -#endif // hifi_ModelScriptingInterface_h diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index a5c94c1bb4..d721d1c86f 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -19,9 +19,6 @@ #include #include -#include -#include - #include #include @@ -68,25 +65,18 @@ #include "RecordingScriptingInterface.h" #include "ScriptEngines.h" #include "TabletScriptingInterface.h" -#include "ModelScriptingInterface.h" - #include #include "MIDIEvent.h" -const QString ScriptEngine::_SETTINGS_ENABLE_EXTENDED_EXCEPTIONS { - "com.highfidelity.experimental.enableExtendedJSExceptions" -}; - -static const int MAX_MODULE_ID_LENGTH { 4096 }; -static const int MAX_DEBUG_VALUE_LENGTH { 80 }; - static const QScriptEngine::QObjectWrapOptions DEFAULT_QOBJECT_WRAP_OPTIONS = QScriptEngine::ExcludeDeleteLater | QScriptEngine::ExcludeChildObjects; static const QScriptValue::PropertyFlags READONLY_PROP_FLAGS { QScriptValue::ReadOnly | QScriptValue::Undeletable }; static const QScriptValue::PropertyFlags READONLY_HIDDEN_PROP_FLAGS { READONLY_PROP_FLAGS | QScriptValue::SkipInEnumeration }; + + static const bool HIFI_AUTOREFRESH_FILE_SCRIPTS { true }; Q_DECLARE_METATYPE(QScriptEngine::FunctionSignature) @@ -94,7 +84,7 @@ int functionSignatureMetaID = qRegisterMetaTypeargumentCount(); i++) { if (i > 0) { @@ -151,7 +141,7 @@ QString encodeEntityIdIntoEntityUrl(const QString& url, const QString& entityID) } QString ScriptEngine::logException(const QScriptValue& exception) { - auto message = formatException(exception, _enableExtendedJSExceptions.get()); + auto message = formatException(exception); scriptErrorMessage(message); return message; } @@ -343,7 +333,7 @@ void ScriptEngine::runInThread() { // The thread interface cannot live on itself, and we want to move this into the thread, so // the thread cannot have this as a parent. QThread* workerThread = new QThread(); - workerThread->setObjectName(QString("js:") + getFilename().replace("about:","")); + workerThread->setObjectName(QString("Script Thread:") + getFilename()); moveToThread(workerThread); // NOTE: If you connect any essential signals for proper shutdown or cleanup of @@ -464,17 +454,17 @@ void ScriptEngine::loadURL(const QUrl& scriptURL, bool reload) { void ScriptEngine::scriptErrorMessage(const QString& message) { qCCritical(scriptengine) << qPrintable(message); - emit errorMessage(message, getFilename()); + emit errorMessage(message); } void ScriptEngine::scriptWarningMessage(const QString& message) { qCWarning(scriptengine) << message; - emit warningMessage(message, getFilename()); + emit warningMessage(message); } void ScriptEngine::scriptInfoMessage(const QString& message) { qCInfo(scriptengine) << message; - emit infoMessage(message, getFilename()); + emit infoMessage(message); } // Even though we never pass AnimVariantMap directly to and from javascript, the queued invokeMethod of @@ -542,40 +532,6 @@ static QScriptValue createScriptableResourcePrototype(QScriptEngine* engine) { return prototype; } -void ScriptEngine::resetModuleCache(bool deleteScriptCache) { - if (QThread::currentThread() != thread()) { - executeOnScriptThread([=]() { resetModuleCache(deleteScriptCache); }); - return; - } - auto jsRequire = globalObject().property("Script").property("require"); - auto cache = jsRequire.property("cache"); - auto cacheMeta = jsRequire.data(); - - if (deleteScriptCache) { - QScriptValueIterator it(cache); - while (it.hasNext()) { - it.next(); - if (it.flags() & QScriptValue::SkipInEnumeration) { - continue; - } - qCDebug(scriptengine) << "resetModuleCache(true) -- staging " << it.name() << " for cache reset at next require"; - cacheMeta.setProperty(it.name(), true); - } - } - cache = newObject(); - if (!cacheMeta.isObject()) { - cacheMeta = newObject(); - cacheMeta.setProperty("id", "Script.require.cacheMeta"); - cacheMeta.setProperty("type", "cacheMeta"); - jsRequire.setData(cacheMeta); - } - cache.setProperty("__created__", (double)QDateTime::currentMSecsSinceEpoch(), QScriptValue::SkipInEnumeration); -#if DEBUG_JS_MODULES - cache.setProperty("__meta__", cacheMeta, READONLY_HIDDEN_PROP_FLAGS); -#endif - jsRequire.setProperty("cache", cache, READONLY_PROP_FLAGS); -} - void ScriptEngine::init() { if (_isInitialized) { return; // only initialize once @@ -585,6 +541,16 @@ void ScriptEngine::init() { auto entityScriptingInterface = DependencyManager::get(); entityScriptingInterface->init(); + connect(entityScriptingInterface.data(), &EntityScriptingInterface::deletingEntity, this, [this](const EntityItemID& entityID) { + if (_entityScripts.contains(entityID)) { + if (isEntityScriptRunning(entityID)) { + qCWarning(scriptengine) << "deletingEntity while entity script is still running!" << entityID; + } + _entityScripts.remove(entityID); + emit entityScriptDetailsUpdated(); + } + }); + // register various meta-types registerMetaTypes(this); @@ -627,22 +593,9 @@ void ScriptEngine::init() { qScriptRegisterMetaType(this, qWSCloseCodeToScriptValue, qWSCloseCodeFromScriptValue); qScriptRegisterMetaType(this, wscReadyStateToScriptValue, wscReadyStateFromScriptValue); - // NOTE: You do not want to end up creating new instances of singletons here. They will be on the ScriptEngine thread - // and are likely to be unusable if we "reset" the ScriptEngine by creating a new one (on a whole new thread). - registerGlobalObject("Script", this); - { - // set up Script.require.resolve and Script.require.cache - auto Script = globalObject().property("Script"); - auto require = Script.property("require"); - auto resolve = Script.property("_requireResolve"); - require.setProperty("resolve", resolve, READONLY_PROP_FLAGS); - resetModuleCache(); - } - - registerGlobalObject("Audio", DependencyManager::get().data()); - + registerGlobalObject("Audio", &AudioScriptingInterface::getInstance()); registerGlobalObject("Entities", entityScriptingInterface.data()); registerGlobalObject("Quat", &_quatLibrary); registerGlobalObject("Vec3", &_vec3Library); @@ -651,7 +604,7 @@ void ScriptEngine::init() { registerGlobalObject("Messages", DependencyManager::get().data()); registerGlobalObject("File", new FileScriptingInterface(this)); - + qScriptRegisterMetaType(this, animVarMapToScriptValue, animVarMapFromScriptValue); qScriptRegisterMetaType(this, resultHandlerToScriptValue, resultHandlerFromScriptValue); @@ -669,10 +622,6 @@ void ScriptEngine::init() { registerGlobalObject("Resources", DependencyManager::get().data()); registerGlobalObject("DebugDraw", &DebugDraw::getInstance()); - - registerGlobalObject("Model", new ModelScriptingInterface(this)); - qScriptRegisterMetaType(this, meshToScriptValue, meshFromScriptValue); - qScriptRegisterMetaType(this, meshesToScriptValue, meshesFromScriptValue); } void ScriptEngine::registerValue(const QString& valueName, QScriptValue value) { @@ -914,11 +863,6 @@ void ScriptEngine::addEventHandler(const EntityItemID& entityID, const QString& handlersForEvent << handlerData; // Note that the same handler can be added many times. See removeEntityEventHandler(). } -// this is not redundant -- the version in BaseScriptEngine is specifically not Q_INVOKABLE -QScriptValue ScriptEngine::evaluateInClosure(const QScriptValue& closure, const QScriptProgram& program) { - return BaseScriptEngine::evaluateInClosure(closure, program); -} - QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fileName, int lineNumber) { if (DependencyManager::get()->isStopped()) { return QScriptValue(); // bail early @@ -941,26 +885,29 @@ QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fi // Check syntax auto syntaxError = lintScript(sourceCode, fileName); if (syntaxError.isError()) { - if (!isEvaluating()) { + if (isEvaluating()) { + currentContext()->throwValue(syntaxError); + } else { syntaxError.setProperty("detail", "evaluate"); + emit unhandledException(syntaxError); } - raiseException(syntaxError); - maybeEmitUncaughtException("lint"); return syntaxError; } QScriptProgram program { sourceCode, fileName, lineNumber }; if (program.isNull()) { // can this happen? auto err = makeError("could not create QScriptProgram for " + fileName); - raiseException(err); - maybeEmitUncaughtException("compile"); + emit unhandledException(err); return err; } QScriptValue result; { result = BaseScriptEngine::evaluate(program); - maybeEmitUncaughtException("evaluate"); + if (!isEvaluating() && hasUncaughtException()) { + emit unhandledException(cloneUncaughtException(__FUNCTION__)); + clearExceptions(); + } } return result; } @@ -983,7 +930,10 @@ void ScriptEngine::run() { { evaluate(_scriptContents, _fileNameString); - maybeEmitUncaughtException(__FUNCTION__); + if (!isEvaluating() && hasUncaughtException()) { + emit unhandledException(cloneUncaughtException(__FUNCTION__)); + clearExceptions(); + } } #ifdef _WIN32 // VS13 does not sleep_until unless it uses the system_clock, see: @@ -1351,354 +1301,7 @@ QUrl ScriptEngine::resourcesPath() const { } void ScriptEngine::print(const QString& message) { - emit printedMessage(message, getFilename()); -} - -// Script.require.resolve -- like resolvePath, but performs more validation and throws exceptions on invalid module identifiers (for consistency with Node.js) -QString ScriptEngine::_requireResolve(const QString& moduleId, const QString& relativeTo) { - if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { - return QString(); - } - QUrl defaultScriptsLoc = defaultScriptsLocation(); - QUrl url(moduleId); - - auto displayId = moduleId; - if (displayId.length() > MAX_DEBUG_VALUE_LENGTH) { - displayId = displayId.mid(0, MAX_DEBUG_VALUE_LENGTH) + "..."; - } - auto message = QString("Cannot find module '%1' (%2)").arg(displayId); - - auto throwResolveError = [&](const QScriptValue& error) -> QString { - raiseException(error); - maybeEmitUncaughtException("require.resolve"); - return QString(); - }; - - // de-fuzz the input a little by restricting to rational sizes - auto idLength = url.toString().length(); - if (idLength < 1 || idLength > MAX_MODULE_ID_LENGTH) { - auto details = QString("rejecting invalid module id size (%1 chars [1,%2])") - .arg(idLength).arg(MAX_MODULE_ID_LENGTH); - return throwResolveError(makeError(message.arg(details), "RangeError")); - } - - // this regex matches: absolute, dotted or path-like URLs - // (ie: the kind of stuff ScriptEngine::resolvePath already handles) - QRegularExpression qualified ("^\\w+:|^/|^[.]{1,2}(/|$)"); - - // this is for module.require (which is a bound version of require that's always relative to the module path) - if (!relativeTo.isEmpty()) { - url = QUrl(relativeTo).resolved(moduleId); - url = resolvePath(url.toString()); - } else if (qualified.match(moduleId).hasMatch()) { - url = resolvePath(moduleId); - } else { - // check if the moduleId refers to a "system" module - QString systemPath = defaultScriptsLoc.path(); - QString systemModulePath = QString("%1/modules/%2.js").arg(systemPath).arg(moduleId); - url = defaultScriptsLoc; - url.setPath(systemModulePath); - if (!QFileInfo(url.toLocalFile()).isFile()) { - if (!moduleId.contains("./")) { - // the user might be trying to refer to a relative file without anchoring it - // let's do them a favor and test for that case -- offering specific advice if detected - auto unanchoredUrl = resolvePath("./" + moduleId); - if (QFileInfo(unanchoredUrl.toLocalFile()).isFile()) { - auto msg = QString("relative module ids must be anchored; use './%1' instead") - .arg(moduleId); - return throwResolveError(makeError(message.arg(msg))); - } - } - return throwResolveError(makeError(message.arg("system module not found"))); - } - } - - if (url.isRelative()) { - return throwResolveError(makeError(message.arg("could not resolve module id"))); - } - - // if it looks like a local file, verify that it's an allowed path and really a file - if (url.isLocalFile()) { - QFileInfo file(url.toLocalFile()); - QUrl canonical = url; - if (file.exists()) { - canonical.setPath(file.canonicalFilePath()); - } - - bool disallowOutsideFiles = !defaultScriptsLocation().isParentOf(canonical) && !currentSandboxURL.isLocalFile(); - if (disallowOutsideFiles && !PathUtils::isDescendantOf(canonical, currentSandboxURL)) { - return throwResolveError(makeError(message.arg( - QString("path '%1' outside of origin script '%2' '%3'") - .arg(PathUtils::stripFilename(url)) - .arg(PathUtils::stripFilename(currentSandboxURL)) - .arg(canonical.toString()) - ))); - } - if (!file.exists()) { - return throwResolveError(makeError(message.arg("path does not exist: " + url.toLocalFile()))); - } - if (!file.isFile()) { - return throwResolveError(makeError(message.arg("path is not a file: " + url.toLocalFile()))); - } - } - - maybeEmitUncaughtException(__FUNCTION__); - return url.toString(); -} - -// retrieves the current parent module from the JS scope chain -QScriptValue ScriptEngine::currentModule() { - if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { - return unboundNullValue(); - } - auto jsRequire = globalObject().property("Script").property("require"); - auto cache = jsRequire.property("cache"); - auto candidate = QScriptValue(); - for (auto c = currentContext(); c && !candidate.isObject(); c = c->parentContext()) { - QScriptContextInfo contextInfo { c }; - candidate = cache.property(contextInfo.fileName()); - } - if (!candidate.isObject()) { - return QScriptValue(); - } - return candidate; -} - -// replaces or adds "module" to "parent.children[]" array -// (for consistency with Node.js and userscript cache invalidation without "cache busters") -bool ScriptEngine::registerModuleWithParent(const QScriptValue& module, const QScriptValue& parent) { - auto children = parent.property("children"); - if (children.isArray()) { - auto key = module.property("id"); - auto length = children.property("length").toInt32(); - for (int i = 0; i < length; i++) { - if (children.property(i).property("id").strictlyEquals(key)) { - qCDebug(scriptengine_module) << key.toString() << " updating parent.children[" << i << "] = module"; - children.setProperty(i, module); - return true; - } - } - qCDebug(scriptengine_module) << key.toString() << " appending parent.children[" << length << "] = module"; - children.setProperty(length, module); - return true; - } else if (parent.isValid()) { - qCDebug(scriptengine_module) << "registerModuleWithParent -- unrecognized parent" << parent.toVariant().toString(); - } - return false; -} - -// creates a new JS "module" Object with default metadata properties -QScriptValue ScriptEngine::newModule(const QString& modulePath, const QScriptValue& parent) { - auto closure = newObject(); - auto exports = newObject(); - auto module = newObject(); - qCDebug(scriptengine_module) << "newModule" << modulePath << parent.property("filename").toString(); - - closure.setProperty("module", module, READONLY_PROP_FLAGS); - - // note: this becomes the "exports" free variable, so should not be set read only - closure.setProperty("exports", exports); - - // make the closure available to module instantiation - module.setProperty("__closure__", closure, READONLY_HIDDEN_PROP_FLAGS); - - // for consistency with Node.js Module - module.setProperty("id", modulePath, READONLY_PROP_FLAGS); - module.setProperty("filename", modulePath, READONLY_PROP_FLAGS); - module.setProperty("exports", exports); // not readonly - module.setProperty("loaded", false, READONLY_PROP_FLAGS); - module.setProperty("parent", parent, READONLY_PROP_FLAGS); - module.setProperty("children", newArray(), READONLY_PROP_FLAGS); - - // module.require is a bound version of require that always resolves relative to that module's path - auto boundRequire = QScriptEngine::evaluate("(function(id) { return Script.require(Script.require.resolve(id, this.filename)); })", "(boundRequire)"); - module.setProperty("require", boundRequire, READONLY_PROP_FLAGS); - - return module; -} - -// synchronously fetch a module's source code using BatchLoader -QVariantMap ScriptEngine::fetchModuleSource(const QString& modulePath, const bool forceDownload) { - using UrlMap = QMap; - auto scriptCache = DependencyManager::get(); - QVariantMap req; - qCDebug(scriptengine_module) << "require.fetchModuleSource: " << QUrl(modulePath).fileName() << QThread::currentThread(); - - auto onload = [=, &req](const UrlMap& data, const UrlMap& _status) { - auto url = modulePath; - auto status = _status[url]; - auto contents = data[url]; - qCDebug(scriptengine_module) << "require.fetchModuleSource.onload: " << QUrl(url).fileName() << status << QThread::currentThread(); - if (isStopping()) { - req["status"] = "Stopped"; - req["success"] = false; - } else { - req["url"] = url; - req["status"] = status; - req["success"] = ScriptCache::isSuccessStatus(status); - req["contents"] = contents; - } - }; - - if (forceDownload) { - qCDebug(scriptengine_module) << "require.requestScript -- clearing cache for" << modulePath; - scriptCache->deleteScript(modulePath); - } - BatchLoader* loader = new BatchLoader(QList({ modulePath })); - connect(loader, &BatchLoader::finished, this, onload); - connect(this, &QObject::destroyed, loader, &QObject::deleteLater); - // fail faster? (since require() blocks the engine thread while resolving dependencies) - const int MAX_RETRIES = 1; - - loader->start(MAX_RETRIES); - - if (!loader->isFinished()) { - QTimer monitor; - QEventLoop loop; - QObject::connect(loader, &BatchLoader::finished, this, [this, &monitor, &loop]{ - monitor.stop(); - loop.quit(); - }); - - // this helps detect the case where stop() is invoked during the download - // but not seen in time to abort processing in onload()... - connect(&monitor, &QTimer::timeout, this, [this, &loop, &loader]{ - if (isStopping()) { - loop.exit(-1); - } - }); - monitor.start(500); - loop.exec(); - } - loader->deleteLater(); - return req; -} - -// evaluate a pending module object using the fetched source code -QScriptValue ScriptEngine::instantiateModule(const QScriptValue& module, const QString& sourceCode) { - QScriptValue result; - auto modulePath = module.property("filename").toString(); - auto closure = module.property("__closure__"); - - qCDebug(scriptengine_module) << QString("require.instantiateModule: %1 / %2 bytes") - .arg(QUrl(modulePath).fileName()).arg(sourceCode.length()); - - if (module.property("content-type").toString() == "application/json") { - qCDebug(scriptengine_module) << "... parsing as JSON"; - closure.setProperty("__json", sourceCode); - result = evaluateInClosure(closure, { "module.exports = JSON.parse(__json)", modulePath }); - } else { - // scoped vars for consistency with Node.js - closure.setProperty("require", module.property("require")); - closure.setProperty("__filename", modulePath, READONLY_HIDDEN_PROP_FLAGS); - closure.setProperty("__dirname", QString(modulePath).replace(QRegExp("/[^/]*$"), ""), READONLY_HIDDEN_PROP_FLAGS); - result = evaluateInClosure(closure, { sourceCode, modulePath }); - } - maybeEmitUncaughtException(__FUNCTION__); - return result; -} - -// CommonJS/Node.js like require/module support -QScriptValue ScriptEngine::require(const QString& moduleId) { - qCDebug(scriptengine_module) << "ScriptEngine::require(" << moduleId.left(MAX_DEBUG_VALUE_LENGTH) << ")"; - if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { - return unboundNullValue(); - } - - auto jsRequire = globalObject().property("Script").property("require"); - auto cacheMeta = jsRequire.data(); - auto cache = jsRequire.property("cache"); - auto parent = currentModule(); - - auto throwModuleError = [&](const QString& modulePath, const QScriptValue& error) { - cache.setProperty(modulePath, nullValue()); - if (!error.isNull()) { -#ifdef DEBUG_JS_MODULES - qCWarning(scriptengine_module) << "throwing module error:" << error.toString() << modulePath << error.property("stack").toString(); -#endif - raiseException(error); - } - maybeEmitUncaughtException("module"); - return unboundNullValue(); - }; - - // start by resolving the moduleId into a fully-qualified path/URL - QString modulePath = _requireResolve(moduleId); - if (modulePath.isNull() || hasUncaughtException()) { - // the resolver already threw an exception -- bail early - maybeEmitUncaughtException(__FUNCTION__); - return unboundNullValue(); - } - - // check the resolved path against the cache - auto module = cache.property(modulePath); - - // modules get cached in `Script.require.cache` and (similar to Node.js) users can access it - // to inspect particular entries and invalidate them by deleting the key: - // `delete Script.require.cache[Script.require.resolve(moduleId)];` - - // cacheMeta is just used right now to tell deleted keys apart from undefined ones - bool invalidateCache = module.isUndefined() && cacheMeta.property(moduleId).isValid(); - - // reset the cacheMeta record so invalidation won't apply next time, even if the module fails to load - cacheMeta.setProperty(modulePath, QScriptValue()); - - auto exports = module.property("exports"); - if (!invalidateCache && exports.isObject()) { - // we have found a cached module -- just need to possibly register it with current parent - qCDebug(scriptengine_module) << QString("require - using cached module '%1' for '%2' (loaded: %3)") - .arg(modulePath).arg(moduleId).arg(module.property("loaded").toString()); - registerModuleWithParent(module, parent); - maybeEmitUncaughtException("cached module"); - return exports; - } - - // bootstrap / register new empty module - module = newModule(modulePath, parent); - registerModuleWithParent(module, parent); - - // add it to the cache (this is done early so any cyclic dependencies pick up) - cache.setProperty(modulePath, module); - - // download the module source - auto req = fetchModuleSource(modulePath, invalidateCache); - - if (!req.contains("success") || !req["success"].toBool()) { - auto error = QString("error retrieving script (%1)").arg(req["status"].toString()); - return throwModuleError(modulePath, error); - } - -#if DEBUG_JS_MODULES - qCDebug(scriptengine_module) << "require.loaded: " << - QUrl(req["url"].toString()).fileName() << req["status"].toString(); -#endif - - auto sourceCode = req["contents"].toString(); - - if (QUrl(modulePath).fileName().endsWith(".json", Qt::CaseInsensitive)) { - module.setProperty("content-type", "application/json"); - } else { - module.setProperty("content-type", "application/javascript"); - } - - // evaluate the module - auto result = instantiateModule(module, sourceCode); - - if (result.isError() && !result.strictlyEquals(module.property("exports"))) { - qCWarning(scriptengine_module) << "-- result.isError --" << result.toString(); - return throwModuleError(modulePath, result); - } - - // mark as fully-loaded - module.setProperty("loaded", true, READONLY_PROP_FLAGS); - - // set up a new reference point for detecting cache key deletion - cacheMeta.setProperty(modulePath, module); - - qCDebug(scriptengine_module) << "//ScriptEngine::require(" << moduleId << ")"; - - maybeEmitUncaughtException(__FUNCTION__); - return module.property("exports"); + emit printedMessage(message); } // If a callback is specified, the included files will be loaded asynchronously and the callback will be called @@ -1706,9 +1309,6 @@ QScriptValue ScriptEngine::require(const QString& moduleId) { // If no callback is specified, the included files will be loaded synchronously and will block execution until // all of the files have finished loading. void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callback) { - if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { - return; - } if (DependencyManager::get()->isStopped()) { scriptWarningMessage("Script.include() while shutting down is ignored... includeFiles:" + includeFiles.join(",") + "parent script:" + getFilename()); @@ -1771,7 +1371,7 @@ void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callbac doWithEnvironment(capturedEntityIdentifier, capturedSandboxURL, operation); if (hasUncaughtException()) { - emit unhandledException(cloneUncaughtException("evaluateInclude")); + emit unhandledException(cloneUncaughtException(__FUNCTION__)); clearExceptions(); } } else { @@ -1818,9 +1418,6 @@ void ScriptEngine::include(const QString& includeFile, QScriptValue callback) { // as a stand-alone script. To accomplish this, the ScriptEngine class just emits a signal which // the Application or other context will connect to in order to know to actually load the script void ScriptEngine::load(const QString& loadFile) { - if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { - return; - } if (DependencyManager::get()->isStopped()) { scriptWarningMessage("Script.load() while shutting down is ignored... loadFile:" + loadFile + "parent script:" + getFilename()); @@ -1890,52 +1487,6 @@ void ScriptEngine::updateEntityScriptStatus(const EntityItemID& entityID, const emit entityScriptDetailsUpdated(); } -QVariant ScriptEngine::cloneEntityScriptDetails(const EntityItemID& entityID) { - static const QVariant NULL_VARIANT { qVariantFromValue((QObject*)nullptr) }; - QVariantMap map; - if (entityID.isNull()) { - // TODO: find better way to report JS Error across thread/process boundaries - map["isError"] = true; - map["errorInfo"] = "Error: getEntityScriptDetails -- invalid entityID"; - } else { -#ifdef DEBUG_ENTITY_STATES - qDebug() << "cloneEntityScriptDetails" << entityID << QThread::currentThread(); -#endif - EntityScriptDetails scriptDetails; - if (getEntityScriptDetails(entityID, scriptDetails)) { -#ifdef DEBUG_ENTITY_STATES - qDebug() << "gotEntityScriptDetails" << scriptDetails.status << QThread::currentThread(); -#endif - map["isRunning"] = isEntityScriptRunning(entityID); - map["status"] = EntityScriptStatus_::valueToKey(scriptDetails.status).toLower(); - map["errorInfo"] = scriptDetails.errorInfo; - map["entityID"] = entityID.toString(); -#ifdef DEBUG_ENTITY_STATES - { - auto debug = QVariantMap(); - debug["script"] = scriptDetails.scriptText; - debug["scriptObject"] = scriptDetails.scriptObject.toVariant(); - debug["lastModified"] = (qlonglong)scriptDetails.lastModified; - debug["sandboxURL"] = scriptDetails.definingSandboxURL; - map["debug"] = debug; - } -#endif - } else { -#ifdef DEBUG_ENTITY_STATES - qDebug() << "!gotEntityScriptDetails" << QThread::currentThread(); -#endif - map["isError"] = true; - map["errorInfo"] = "Entity script details unavailable"; - map["entityID"] = entityID.toString(); - } - } - return map; -} - -QFuture ScriptEngine::getLocalEntityScriptDetails(const EntityItemID& entityID) { - return QtConcurrent::run(this, &ScriptEngine::cloneEntityScriptDetails, entityID); -} - bool ScriptEngine::getEntityScriptDetails(const EntityItemID& entityID, EntityScriptDetails &details) const { auto it = _entityScripts.constFind(entityID); if (it == _entityScripts.constEnd()) { @@ -2074,10 +1625,10 @@ void ScriptEngine::loadEntityScript(const EntityItemID& entityID, const QString& auto scriptCache = DependencyManager::get(); // note: see EntityTreeRenderer.cpp for shared pointer lifecycle management - QWeakPointer weakRef(sharedFromThis()); + QWeakPointer weakRef(sharedFromThis()); scriptCache->getScriptContents(entityScript, [this, weakRef, entityScript, entityID](const QString& url, const QString& contents, bool isURL, bool success, const QString& status) { - QSharedPointer strongRef(weakRef); + QSharedPointer strongRef(weakRef); if (!strongRef) { qCWarning(scriptengine) << "loadEntityScript.contentAvailable -- ScriptEngine was deleted during getScriptContents!!"; return; @@ -2196,12 +1747,13 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co timeout.setSingleShot(true); timeout.start(SANDBOX_TIMEOUT); connect(&timeout, &QTimer::timeout, [&sandbox, SANDBOX_TIMEOUT, scriptOrURL]{ + auto context = sandbox.currentContext(); + if (context) { qCDebug(scriptengine) << "ScriptEngine::entityScriptContentAvailable timeout(" << scriptOrURL << ")"; // Guard against infinite loops and non-performant code - sandbox.raiseException( - sandbox.makeError(QString("Timed out (entity constructors are limited to %1ms)").arg(SANDBOX_TIMEOUT)) - ); + context->throwError(QString("Timed out (entity constructors are limited to %1ms)").arg(SANDBOX_TIMEOUT)); + } }); testConstructor = sandbox.evaluate(program); @@ -2217,7 +1769,7 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co if (exception.isError()) { // create a local copy using makeError to decouple from the sandbox engine exception = makeError(exception); - setError(formatException(exception, _enableExtendedJSExceptions.get()), EntityScriptStatus::ERROR_RUNNING_SCRIPT); + setError(formatException(exception), EntityScriptStatus::ERROR_RUNNING_SCRIPT); emit unhandledException(exception); return; } @@ -2229,8 +1781,9 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co testConstructorType = "empty"; } QString testConstructorValue = testConstructor.toString(); - if (testConstructorValue.size() > MAX_DEBUG_VALUE_LENGTH) { - testConstructorValue = testConstructorValue.mid(0, MAX_DEBUG_VALUE_LENGTH) + "..."; + const int maxTestConstructorValueSize = 80; + if (testConstructorValue.size() > maxTestConstructorValueSize) { + testConstructorValue = testConstructorValue.mid(0, maxTestConstructorValueSize) + "..."; } auto message = QString("failed to load entity script -- expected a function, got %1, %2") .arg(testConstructorType).arg(testConstructorValue); @@ -2268,7 +1821,7 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co if (entityScriptObject.isError()) { auto exception = entityScriptObject; - setError(formatException(exception, _enableExtendedJSExceptions.get()), EntityScriptStatus::ERROR_RUNNING_SCRIPT); + setError(formatException(exception), EntityScriptStatus::ERROR_RUNNING_SCRIPT); emit unhandledException(exception); return; } @@ -2291,7 +1844,7 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co processDeferredEntityLoads(entityScript, entityID); } -void ScriptEngine::unloadEntityScript(const EntityItemID& entityID, bool shouldRemoveFromMap) { +void ScriptEngine::unloadEntityScript(const EntityItemID& entityID) { if (QThread::currentThread() != thread()) { #ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "*** WARNING *** ScriptEngine::unloadEntityScript() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] " @@ -2299,8 +1852,7 @@ void ScriptEngine::unloadEntityScript(const EntityItemID& entityID, bool shouldR #endif QMetaObject::invokeMethod(this, "unloadEntityScript", - Q_ARG(const EntityItemID&, entityID), - Q_ARG(bool, shouldRemoveFromMap)); + Q_ARG(const EntityItemID&, entityID)); return; } #ifdef THREAD_DEBUGGING @@ -2312,17 +1864,10 @@ void ScriptEngine::unloadEntityScript(const EntityItemID& entityID, bool shouldR const EntityScriptDetails &oldDetails = _entityScripts[entityID]; if (isEntityScriptRunning(entityID)) { callEntityScriptMethod(entityID, "unload"); - } -#ifdef DEBUG_ENTITY_STATES - else { + } else { qCDebug(scriptengine) << "unload called while !running" << entityID << oldDetails.status; } -#endif - if (shouldRemoveFromMap) { - // this was a deleted entity, we've been asked to remove it from the map - _entityScripts.remove(entityID); - emit entityScriptDetailsUpdated(); - } else if (oldDetails.status != EntityScriptStatus::UNLOADED) { + if (oldDetails.status != EntityScriptStatus::UNLOADED) { EntityScriptDetails newDetails; newDetails.status = EntityScriptStatus::UNLOADED; newDetails.lastModified = QDateTime::currentMSecsSinceEpoch(); @@ -2330,7 +1875,6 @@ void ScriptEngine::unloadEntityScript(const EntityItemID& entityID, bool shouldR newDetails.scriptText = oldDetails.scriptText; setEntityScriptDetails(entityID, newDetails); } - stopAllTimersForEntityScript(entityID); { // FIXME: shouldn't have to do this here, but currently something seems to be firing unloads moments after firing initial load requests @@ -2409,7 +1953,10 @@ void ScriptEngine::doWithEnvironment(const EntityItemID& entityID, const QUrl& s #else operation(); #endif - maybeEmitUncaughtException(!entityID.isNull() ? entityID.toString() : __FUNCTION__); + if (!isEvaluating() && hasUncaughtException()) { + emit unhandledException(cloneUncaughtException(!entityID.isNull() ? entityID.toString() : __FUNCTION__)); + clearExceptions(); + } currentEntityIdentifier = oldIdentifier; currentSandboxURL = oldSandboxURL; } diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index 5ea8d052e9..b988ccfe90 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -41,7 +41,6 @@ #include "ScriptCache.h" #include "ScriptUUID.h" #include "Vec3.h" -#include "SettingHandle.h" class QScriptEngineDebugger; @@ -79,7 +78,7 @@ public: QUrl definingSandboxURL { QUrl("about:EntityScript") }; }; -class ScriptEngine : public BaseScriptEngine, public EntitiesScriptEngineProvider { +class ScriptEngine : public BaseScriptEngine, public EntitiesScriptEngineProvider, public QEnableSharedFromThis { Q_OBJECT Q_PROPERTY(QString context READ getContext) public: @@ -138,8 +137,6 @@ public: /// evaluate some code in the context of the ScriptEngine and return the result Q_INVOKABLE QScriptValue evaluate(const QString& program, const QString& fileName, int lineNumber = 1); // this is also used by the script tool widget - Q_INVOKABLE QScriptValue evaluateInClosure(const QScriptValue& locals, const QScriptProgram& program); - /// if the script engine is not already running, this will download the URL and start the process of seting it up /// to run... NOTE - this is used by Application currently to load the url. We don't really want it to be exposed /// to scripts. we may not need this to be invokable @@ -160,16 +157,6 @@ public: Q_INVOKABLE void include(const QStringList& includeFiles, QScriptValue callback = QScriptValue()); Q_INVOKABLE void include(const QString& includeFile, QScriptValue callback = QScriptValue()); - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // MODULE related methods - Q_INVOKABLE QScriptValue require(const QString& moduleId); - Q_INVOKABLE void resetModuleCache(bool deleteScriptCache = false); - QScriptValue currentModule(); - bool registerModuleWithParent(const QScriptValue& module, const QScriptValue& parent); - QScriptValue newModule(const QString& modulePath, const QScriptValue& parent = QScriptValue()); - QVariantMap fetchModuleSource(const QString& modulePath, const bool forceDownload = false); - QScriptValue instantiateModule(const QScriptValue& module, const QString& sourceCode); - Q_INVOKABLE QObject* setInterval(const QScriptValue& function, int intervalMS); Q_INVOKABLE QObject* setTimeout(const QScriptValue& function, int timeoutMS); Q_INVOKABLE void clearInterval(QObject* timer) { stopTimer(reinterpret_cast(timer)); } @@ -183,10 +170,8 @@ public: Q_INVOKABLE bool isEntityScriptRunning(const EntityItemID& entityID) { return _entityScripts.contains(entityID) && _entityScripts[entityID].status == EntityScriptStatus::RUNNING; } - QVariant cloneEntityScriptDetails(const EntityItemID& entityID); - QFuture getLocalEntityScriptDetails(const EntityItemID& entityID) override; Q_INVOKABLE void loadEntityScript(const EntityItemID& entityID, const QString& entityScript, bool forceRedownload); - Q_INVOKABLE void unloadEntityScript(const EntityItemID& entityID, bool shouldRemoveFromMap = false); // will call unload method + Q_INVOKABLE void unloadEntityScript(const EntityItemID& entityID); // will call unload method Q_INVOKABLE void unloadAllEntityScripts(); Q_INVOKABLE void callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const QStringList& params = QStringList()) override; @@ -236,10 +221,10 @@ signals: void scriptEnding(); void finished(const QString& fileNameString, ScriptEngine* engine); void cleanupMenuItem(const QString& menuItemString); - void printedMessage(const QString& message, const QString& scriptName); - void errorMessage(const QString& message, const QString& scriptName); - void warningMessage(const QString& message, const QString& scriptName); - void infoMessage(const QString& message, const QString& scriptName); + void printedMessage(const QString& message); + void errorMessage(const QString& message); + void warningMessage(const QString& message); + void infoMessage(const QString& message); void runningStateChanged(); void loadScript(const QString& scriptName, bool isUserLoaded); void reloadScript(const QString& scriptName, bool isUserLoaded); @@ -252,9 +237,6 @@ signals: protected: void init(); Q_INVOKABLE void executeOnScriptThread(std::function function, const Qt::ConnectionType& type = Qt::QueuedConnection ); - // note: this is not meant to be called directly, but just to have QMetaObject take care of wiring it up in general; - // then inside of init() we just have to do "Script.require.resolve = Script._requireResolve;" - Q_INVOKABLE QString _requireResolve(const QString& moduleId, const QString& relativeTo = QString()); QString logException(const QScriptValue& exception); void timerFired(); @@ -308,16 +290,11 @@ protected: AssetScriptingInterface _assetScriptingInterface{ this }; - std::function _emitScriptUpdates{ []() { return true; } }; + std::function _emitScriptUpdates{ [](){ return true; } }; std::recursive_mutex _lock; std::chrono::microseconds _totalTimerExecution { 0 }; - - static const QString _SETTINGS_ENABLE_EXTENDED_MODULE_COMPAT; - static const QString _SETTINGS_ENABLE_EXTENDED_EXCEPTIONS; - - Setting::Handle _enableExtendedJSExceptions { _SETTINGS_ENABLE_EXTENDED_EXCEPTIONS, true }; }; #endif // hifi_ScriptEngine_h diff --git a/libraries/script-engine/src/ScriptEngineLogging.cpp b/libraries/script-engine/src/ScriptEngineLogging.cpp index 392bc05129..2e5d293728 100644 --- a/libraries/script-engine/src/ScriptEngineLogging.cpp +++ b/libraries/script-engine/src/ScriptEngineLogging.cpp @@ -12,4 +12,3 @@ #include "ScriptEngineLogging.h" Q_LOGGING_CATEGORY(scriptengine, "hifi.scriptengine") -Q_LOGGING_CATEGORY(scriptengine_module, "hifi.scriptengine.module") diff --git a/libraries/script-engine/src/ScriptEngineLogging.h b/libraries/script-engine/src/ScriptEngineLogging.h index 62e46632a6..0e614dd5bf 100644 --- a/libraries/script-engine/src/ScriptEngineLogging.h +++ b/libraries/script-engine/src/ScriptEngineLogging.h @@ -15,7 +15,6 @@ #include Q_DECLARE_LOGGING_CATEGORY(scriptengine) -Q_DECLARE_LOGGING_CATEGORY(scriptengine_module) #endif // hifi_ScriptEngineLogging_h diff --git a/libraries/script-engine/src/ScriptEngines.cpp b/libraries/script-engine/src/ScriptEngines.cpp index 88b0e0b7b5..57887d2d96 100644 --- a/libraries/script-engine/src/ScriptEngines.cpp +++ b/libraries/script-engine/src/ScriptEngines.cpp @@ -34,24 +34,34 @@ ScriptsModel& getScriptsModel() { return scriptsModel; } -void ScriptEngines::onPrintedMessage(const QString& message, const QString& scriptName) { +void ScriptEngines::onPrintedMessage(const QString& message) { + auto scriptEngine = qobject_cast(sender()); + auto scriptName = scriptEngine ? scriptEngine->getFilename() : ""; emit printedMessage(message, scriptName); } -void ScriptEngines::onErrorMessage(const QString& message, const QString& scriptName) { +void ScriptEngines::onErrorMessage(const QString& message) { + auto scriptEngine = qobject_cast(sender()); + auto scriptName = scriptEngine ? scriptEngine->getFilename() : ""; emit errorMessage(message, scriptName); } -void ScriptEngines::onWarningMessage(const QString& message, const QString& scriptName) { +void ScriptEngines::onWarningMessage(const QString& message) { + auto scriptEngine = qobject_cast(sender()); + auto scriptName = scriptEngine ? scriptEngine->getFilename() : ""; emit warningMessage(message, scriptName); } -void ScriptEngines::onInfoMessage(const QString& message, const QString& scriptName) { +void ScriptEngines::onInfoMessage(const QString& message) { + auto scriptEngine = qobject_cast(sender()); + auto scriptName = scriptEngine ? scriptEngine->getFilename() : ""; emit infoMessage(message, scriptName); } void ScriptEngines::onErrorLoadingScript(const QString& url) { - emit errorLoadingScript(url); + auto scriptEngine = qobject_cast(sender()); + auto scriptName = scriptEngine ? scriptEngine->getFilename() : ""; + emit errorLoadingScript(url, scriptName); } ScriptEngines::ScriptEngines(ScriptEngine::Context context) diff --git a/libraries/script-engine/src/ScriptEngines.h b/libraries/script-engine/src/ScriptEngines.h index 63b7e8f11c..2fadfc81f8 100644 --- a/libraries/script-engine/src/ScriptEngines.h +++ b/libraries/script-engine/src/ScriptEngines.h @@ -79,13 +79,13 @@ signals: void errorMessage(const QString& message, const QString& engineName); void warningMessage(const QString& message, const QString& engineName); void infoMessage(const QString& message, const QString& engineName); - void errorLoadingScript(const QString& url); + void errorLoadingScript(const QString& url, const QString& engineName); public slots: - void onPrintedMessage(const QString& message, const QString& scriptName); - void onErrorMessage(const QString& message, const QString& scriptName); - void onWarningMessage(const QString& message, const QString& scriptName); - void onInfoMessage(const QString& message, const QString& scriptName); + void onPrintedMessage(const QString& message); + void onErrorMessage(const QString& message); + void onWarningMessage(const QString& message); + void onInfoMessage(const QString& message); void onErrorLoadingScript(const QString& url); protected slots: diff --git a/libraries/shared/src/BaseScriptEngine.h b/libraries/shared/src/BaseScriptEngine.h deleted file mode 100644 index 138e46fafa..0000000000 --- a/libraries/shared/src/BaseScriptEngine.h +++ /dev/null @@ -1,90 +0,0 @@ -// -// BaseScriptEngine.h -// libraries/script-engine/src -// -// Created by Timothy Dedischew on 02/01/17. -// Copyright 2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#ifndef hifi_BaseScriptEngine_h -#define hifi_BaseScriptEngine_h - -#include -#include -#include - -// common base class for extending QScriptEngine itself -class BaseScriptEngine : public QScriptEngine, public QEnableSharedFromThis { - Q_OBJECT -public: - static const QString SCRIPT_EXCEPTION_FORMAT; - static const QString SCRIPT_BACKTRACE_SEP; - - // threadsafe "unbound" version of QScriptEngine::nullValue() - static const QScriptValue unboundNullValue() { return QScriptValue(0, QScriptValue::NullValue); } - - BaseScriptEngine() {} - - Q_INVOKABLE QScriptValue lintScript(const QString& sourceCode, const QString& fileName, const int lineNumber = 1); - Q_INVOKABLE QScriptValue makeError(const QScriptValue& other = QScriptValue(), const QString& type = "Error"); - Q_INVOKABLE QString formatException(const QScriptValue& exception, bool includeExtendedDetails); - - QScriptValue cloneUncaughtException(const QString& detail = QString()); - QScriptValue evaluateInClosure(const QScriptValue& locals, const QScriptProgram& program); - - // if there is a pending exception and we are at the top level (non-recursive) stack frame, this emits and resets it - bool maybeEmitUncaughtException(const QString& debugHint = QString()); - - // if the currentContext() is valid then throw the passed exception; otherwise, immediately emit it. - // note: this is used in cases where C++ code might call into JS API methods directly - bool raiseException(const QScriptValue& exception); - - // helper to detect and log warnings when other code invokes QScriptEngine/BaseScriptEngine in thread-unsafe ways - static bool IS_THREADSAFE_INVOCATION(const QThread *thread, const QString& method); -signals: - void unhandledException(const QScriptValue& exception); - -protected: - // like `newFunction`, but allows mapping inline C++ lambdas with captures as callable QScriptValues - // even though the context/engine parameters are redundant in most cases, the function signature matches `newFunction` - // anyway so that newLambdaFunction can be used to rapidly prototype / test utility APIs and then if becoming - // permanent more easily promoted into regular static newFunction scenarios. - QScriptValue newLambdaFunction(std::function operation, const QScriptValue& data = QScriptValue(), const QScriptEngine::ValueOwnership& ownership = QScriptEngine::AutoOwnership); - -#ifdef DEBUG_JS - static void _debugDump(const QString& header, const QScriptValue& object, const QString& footer = QString()); -#endif -}; - -// Standardized CPS callback helpers (see: http://fredkschott.com/post/2014/03/understanding-error-first-callbacks-in-node-js/) -// These two helpers allow async JS APIs that use a callback parameter to be more friendly to scripters by accepting thisObject -// context and adopting a consistent and intuitable callback signature: -// function callback(err, result) { if (err) { ... } else { /* do stuff with result */ } } -// -// To use, first pass the user-specified callback args in the same order used with optionally-scoped Qt signal connections: -// auto handler = makeScopedHandlerObject(scopeOrCallback, optionalMethodOrName); -// And then invoke the scoped handler later per CPS conventions: -// auto result = callScopedHandlerObject(handler, err, result); -QScriptValue makeScopedHandlerObject(QScriptValue scopeOrCallback, QScriptValue methodOrName); -QScriptValue callScopedHandlerObject(QScriptValue handler, QScriptValue err, QScriptValue result); - -// Lambda helps create callable QScriptValues out of std::functions: -// (just meant for use from within the script engine itself) -class Lambda : public QObject { - Q_OBJECT -public: - Lambda(QScriptEngine *engine, std::function operation, QScriptValue data); - ~Lambda(); - public slots: - QScriptValue call(); - QString toString() const; -private: - QScriptEngine* engine; - std::function operation; - QScriptValue data; -}; - -#endif // hifi_BaseScriptEngine_h diff --git a/libraries/shared/src/HifiConfigVariantMap.cpp b/libraries/shared/src/HifiConfigVariantMap.cpp index d0fb14e104..5be6b2cd74 100644 --- a/libraries/shared/src/HifiConfigVariantMap.cpp +++ b/libraries/shared/src/HifiConfigVariantMap.cpp @@ -21,7 +21,7 @@ #include #include -#include "PathUtils.h" +#include "ServerPathUtils.h" #include "SharedLogging.h" QVariantMap HifiConfigVariantMap::mergeCLParametersWithJSONConfig(const QStringList& argumentList) { @@ -127,7 +127,7 @@ void HifiConfigVariantMap::loadConfig(const QStringList& argumentList) { _userConfigFilename = argumentList[userConfigIndex + 1]; } else { // we weren't passed a user config path - _userConfigFilename = PathUtils::getAppDataFilePath(USER_CONFIG_FILE_NAME); + _userConfigFilename = ServerPathUtils::getDataFilePath(USER_CONFIG_FILE_NAME); // as of 1/19/2016 this path was moved so we attempt a migration for first run post migration here @@ -153,7 +153,7 @@ void HifiConfigVariantMap::loadConfig(const QStringList& argumentList) { // we have the old file and not the new file - time to copy the file // make the destination directory if it doesn't exist - auto dataDirectory = PathUtils::getAppDataPath(); + auto dataDirectory = ServerPathUtils::getDataDirectory(); if (QDir().mkpath(dataDirectory)) { if (oldConfigFile.copy(_userConfigFilename)) { qCDebug(shared) << "Migrated config file from" << oldConfigFilename << "to" << _userConfigFilename; diff --git a/libraries/shared/src/PathUtils.cpp b/libraries/shared/src/PathUtils.cpp index 6e3acc5e99..265eaaa5b6 100644 --- a/libraries/shared/src/PathUtils.cpp +++ b/libraries/shared/src/PathUtils.cpp @@ -30,20 +30,18 @@ const QString& PathUtils::resourcesPath() { return staticResourcePath; } -QString PathUtils::getAppDataPath() { - return QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/"; -} +QString PathUtils::getRootDataDirectory() { + auto dataPath = QStandardPaths::writableLocation(QStandardPaths::HomeLocation); -QString PathUtils::getAppLocalDataPath() { - return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/"; -} +#ifdef Q_OS_WIN + dataPath += "/AppData/Roaming/"; +#elif defined(Q_OS_OSX) + dataPath += "/Library/Application Support/"; +#else + dataPath += "/.local/share/"; +#endif -QString PathUtils::getAppDataFilePath(const QString& filename) { - return QDir(getAppDataPath()).absoluteFilePath(filename); -} - -QString PathUtils::getAppLocalDataFilePath(const QString& filename) { - return QDir(getAppLocalDataPath()).absoluteFilePath(filename); + return dataPath; } QString fileNameWithoutExtension(const QString& fileName, const QVector possibleExtensions) { diff --git a/libraries/shared/src/PathUtils.h b/libraries/shared/src/PathUtils.h index a7af44221c..1f7dcbe466 100644 --- a/libraries/shared/src/PathUtils.h +++ b/libraries/shared/src/PathUtils.h @@ -27,12 +27,7 @@ class PathUtils : public QObject, public Dependency { Q_PROPERTY(QString resources READ resourcesPath) public: static const QString& resourcesPath(); - - static QString getAppDataPath(); - static QString getAppLocalDataPath(); - - static QString getAppDataFilePath(const QString& filename); - static QString getAppLocalDataFilePath(const QString& filename); + static QString getRootDataDirectory(); static Qt::CaseSensitivity getFSCaseSensitivity(); static QString stripFilename(const QUrl& url); diff --git a/libraries/shared/src/RenderArgs.h b/libraries/shared/src/RenderArgs.h index 50722c0deb..b2c05b0548 100644 --- a/libraries/shared/src/RenderArgs.h +++ b/libraries/shared/src/RenderArgs.h @@ -122,7 +122,6 @@ public: gpu::Batch* _batch = nullptr; std::shared_ptr _whiteTexture; - uint32_t _globalShapeKey { 0 }; bool _enableTexturing { true }; RenderDetails _details; diff --git a/libraries/shared/src/ServerPathUtils.cpp b/libraries/shared/src/ServerPathUtils.cpp new file mode 100644 index 0000000000..cf52875c5f --- /dev/null +++ b/libraries/shared/src/ServerPathUtils.cpp @@ -0,0 +1,31 @@ +// +// ServerPathUtils.cpp +// libraries/shared/src +// +// Created by Ryan Huffman on 01/12/16. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#include "ServerPathUtils.h" + +#include +#include +#include +#include + +#include "PathUtils.h" + +QString ServerPathUtils::getDataDirectory() { + auto dataPath = PathUtils::getRootDataDirectory(); + + dataPath += qApp->organizationName() + "/" + qApp->applicationName(); + + return QDir::cleanPath(dataPath); +} + +QString ServerPathUtils::getDataFilePath(QString filename) { + return QDir(getDataDirectory()).absoluteFilePath(filename); +} + diff --git a/libraries/shared/src/ServerPathUtils.h b/libraries/shared/src/ServerPathUtils.h new file mode 100644 index 0000000000..28a9a71f0d --- /dev/null +++ b/libraries/shared/src/ServerPathUtils.h @@ -0,0 +1,22 @@ +// +// ServerPathUtils.h +// libraries/shared/src +// +// Created by Ryan Huffman on 01/12/16. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_ServerPathUtils_h +#define hifi_ServerPathUtils_h + +#include + +namespace ServerPathUtils { + QString getDataDirectory(); + QString getDataFilePath(QString filename); +} + +#endif // hifi_ServerPathUtils_h \ No newline at end of file diff --git a/libraries/shared/src/shared/Storage.cpp b/libraries/shared/src/shared/Storage.cpp deleted file mode 100644 index 3c46347a49..0000000000 --- a/libraries/shared/src/shared/Storage.cpp +++ /dev/null @@ -1,92 +0,0 @@ -// -// Created by Bradley Austin Davis on 2016/02/17 -// Copyright 2013-2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include "Storage.h" - -#include -#include -#include - -Q_LOGGING_CATEGORY(storagelogging, "hifi.core.storage") - -using namespace storage; - -ViewStorage::ViewStorage(const storage::StoragePointer& owner, size_t size, const uint8_t* data) - : _owner(owner), _size(size), _data(data) {} - -StoragePointer Storage::createView(size_t viewSize, size_t offset) const { - auto selfSize = size(); - if (0 == viewSize) { - viewSize = selfSize; - } - if ((viewSize + offset) > selfSize) { - throw std::runtime_error("Invalid mapping range"); - } - return std::make_shared(shared_from_this(), viewSize, data() + offset); -} - -StoragePointer Storage::toMemoryStorage() const { - return std::make_shared(size(), data()); -} - -StoragePointer Storage::toFileStorage(const QString& filename) const { - return FileStorage::create(filename, size(), data()); -} - -MemoryStorage::MemoryStorage(size_t size, const uint8_t* data) { - _data.resize(size); - if (data) { - memcpy(_data.data(), data, size); - } -} - -StoragePointer FileStorage::create(const QString& filename, size_t size, const uint8_t* data) { - QFile file(filename); - if (!file.open(QFile::ReadWrite | QIODevice::Truncate)) { - throw std::runtime_error("Unable to open file for writing"); - } - if (!file.resize(size)) { - throw std::runtime_error("Unable to resize file"); - } - { - auto mapped = file.map(0, size); - if (!mapped) { - throw std::runtime_error("Unable to map file"); - } - memcpy(mapped, data, size); - if (!file.unmap(mapped)) { - throw std::runtime_error("Unable to unmap file"); - } - } - file.close(); - return std::make_shared(filename); -} - -FileStorage::FileStorage(const QString& filename) : _file(filename) { - if (_file.open(QFile::ReadOnly)) { - _mapped = _file.map(0, _file.size()); - if (_mapped) { - _valid = true; - } else { - qCWarning(storagelogging) << "Failed to map file " << filename; - } - } else { - qCWarning(storagelogging) << "Failed to open file " << filename; - } -} - -FileStorage::~FileStorage() { - if (_mapped) { - if (!_file.unmap(_mapped)) { - throw std::runtime_error("Unable to unmap file"); - } - } - if (_file.isOpen()) { - _file.close(); - } -} diff --git a/libraries/shared/src/shared/Storage.h b/libraries/shared/src/shared/Storage.h deleted file mode 100644 index 306984040f..0000000000 --- a/libraries/shared/src/shared/Storage.h +++ /dev/null @@ -1,82 +0,0 @@ -// -// Created by Bradley Austin Davis on 2016/02/17 -// Copyright 2013-2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#pragma once -#ifndef hifi_Storage_h -#define hifi_Storage_h - -#include -#include -#include -#include -#include - -namespace storage { - class Storage; - using StoragePointer = std::shared_ptr; - - class Storage : public std::enable_shared_from_this { - public: - virtual ~Storage() {} - virtual const uint8_t* data() const = 0; - virtual size_t size() const = 0; - virtual operator bool() const { return true; } - - StoragePointer createView(size_t size = 0, size_t offset = 0) const; - StoragePointer toFileStorage(const QString& filename) const; - StoragePointer toMemoryStorage() const; - - // Aliases to prevent having to re-write a ton of code - inline size_t getSize() const { return size(); } - inline const uint8_t* readData() const { return data(); } - }; - - class MemoryStorage : public Storage { - public: - MemoryStorage(size_t size, const uint8_t* data = nullptr); - const uint8_t* data() const override { return _data.data(); } - uint8_t* data() { return _data.data(); } - size_t size() const override { return _data.size(); } - operator bool() const override { return true; } - private: - std::vector _data; - }; - - class FileStorage : public Storage { - public: - static StoragePointer create(const QString& filename, size_t size, const uint8_t* data); - FileStorage(const QString& filename); - ~FileStorage(); - // Prevent copying - FileStorage(const FileStorage& other) = delete; - FileStorage& operator=(const FileStorage& other) = delete; - - const uint8_t* data() const override { return _mapped; } - size_t size() const override { return _file.size(); } - operator bool() const override { return _valid; } - private: - bool _valid { false }; - QFile _file; - uint8_t* _mapped { nullptr }; - }; - - class ViewStorage : public Storage { - public: - ViewStorage(const storage::StoragePointer& owner, size_t size, const uint8_t* data); - const uint8_t* data() const override { return _data; } - size_t size() const override { return _size; } - operator bool() const override { return *_owner; } - private: - const storage::StoragePointer _owner; - const size_t _size; - const uint8_t* _data; - }; - -} - -#endif // hifi_Storage_h diff --git a/libraries/ui/src/ui/Menu.cpp b/libraries/ui/src/ui/Menu.cpp index a793942056..f68fff0204 100644 --- a/libraries/ui/src/ui/Menu.cpp +++ b/libraries/ui/src/ui/Menu.cpp @@ -470,8 +470,8 @@ void Menu::removeSeparator(const QString& menuName, const QString& separatorName if (menu) { int textAt = findPositionOfMenuItem(menu, separatorName); QList menuActions = menu->actions(); + QAction* separatorText = menuActions[textAt]; if (textAt > 0 && textAt < menuActions.size()) { - QAction* separatorText = menuActions[textAt]; QAction* separatorLine = menuActions[textAt - 1]; if (separatorLine) { if (separatorLine->isSeparator()) { diff --git a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp index b759a06aee..09f3e6dc8c 100644 --- a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp +++ b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp @@ -255,7 +255,7 @@ void OculusLegacyDisplayPlugin::hmdPresent() { memset(eyePoses, 0, sizeof(ovrPosef) * 2); eyePoses[0].Orientation = eyePoses[1].Orientation = ovrRotation; - GLint texture = getGLBackend()->getTextureID(_compositeFramebuffer->getRenderBuffer(0)); + GLint texture = getGLBackend()->getTextureID(_compositeFramebuffer->getRenderBuffer(0), false); auto sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); glFlush(); if (_hmdWindow->makeCurrent()) { diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.cpp b/plugins/openvr/src/OpenVrDisplayPlugin.cpp index 46c2cf3ff2..6d503a208a 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.cpp +++ b/plugins/openvr/src/OpenVrDisplayPlugin.cpp @@ -494,9 +494,9 @@ void OpenVrDisplayPlugin::customizeContext() { _compositeInfos[0].texture = _compositeFramebuffer->getRenderBuffer(0); for (size_t i = 0; i < COMPOSITING_BUFFER_SIZE; ++i) { if (0 != i) { - _compositeInfos[i].texture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32, _renderTargetSize.x, _renderTargetSize.y, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT))); + _compositeInfos[i].texture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, _renderTargetSize.x, _renderTargetSize.y, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT))); } - _compositeInfos[i].textureID = getGLBackend()->getTextureID(_compositeInfos[i].texture); + _compositeInfos[i].textureID = getGLBackend()->getTextureID(_compositeInfos[i].texture, false); } _submitThread->_canvas = _submitCanvas; _submitThread->start(QThread::HighPriority); @@ -624,7 +624,7 @@ void OpenVrDisplayPlugin::compositeLayers() { glFlush(); if (!newComposite.textureID) { - newComposite.textureID = getGLBackend()->getTextureID(newComposite.texture); + newComposite.textureID = getGLBackend()->getTextureID(newComposite.texture, false); } withPresentThreadLock([&] { _submitThread->update(newComposite); @@ -638,7 +638,7 @@ void OpenVrDisplayPlugin::hmdPresent() { if (_threadedSubmit) { _submitThread->waitForPresent(); } else { - GLuint glTexId = getGLBackend()->getTextureID(_compositeFramebuffer->getRenderBuffer(0)); + GLuint glTexId = getGLBackend()->getTextureID(_compositeFramebuffer->getRenderBuffer(0), false); vr::Texture_t vrTexture { (void*)glTexId, vr::API_OpenGL, vr::ColorSpace_Auto }; vr::VRCompositor()->Submit(vr::Eye_Left, &vrTexture, &OPENVR_TEXTURE_BOUNDS_LEFT); vr::VRCompositor()->Submit(vr::Eye_Right, &vrTexture, &OPENVR_TEXTURE_BOUNDS_RIGHT); diff --git a/scripts/developer/libraries/jasmine/hifi-boot.js b/scripts/developer/libraries/jasmine/hifi-boot.js index 772dd8c17e..f490a3618f 100644 --- a/scripts/developer/libraries/jasmine/hifi-boot.js +++ b/scripts/developer/libraries/jasmine/hifi-boot.js @@ -6,7 +6,7 @@ var lastSpecStartTime; function ConsoleReporter(options) { var startTime = new Date().getTime(); - var errorCount = 0, pending = []; + var errorCount = 0; this.jasmineStarted = function (obj) { print('Jasmine started with ' + obj.totalSpecsDefined + ' tests.'); }; @@ -15,14 +15,11 @@ var endTime = new Date().getTime(); print('
'); if (errorCount === 0) { - print ('All enabled tests passed!'); + print ('All tests passed!'); } else { print('Tests completed with ' + errorCount + ' ' + ERROR + '.'); } - if (pending.length) - print ('disabled:
   '+ - pending.join('
   ')+'
'); print('Tests completed in ' + (endTime - startTime) + 'ms.'); }; this.suiteStarted = function(obj) { @@ -35,10 +32,6 @@ lastSpecStartTime = new Date().getTime(); }; this.specDone = function(obj) { - if (obj.status === 'pending') { - pending.push(obj.fullName); - return print('...(pending ' + obj.fullName +')'); - } var specEndTime = new Date().getTime(); var symbol = obj.status === PASSED ? '' + CHECKMARK + '' : @@ -62,7 +55,7 @@ clearTimeout = Script.clearTimeout; clearInterval = Script.clearInterval; - var jasmine = this.jasmine = jasmineRequire.core(jasmineRequire); + var jasmine = jasmineRequire.core(jasmineRequire); var env = jasmine.getEnv(); diff --git a/scripts/developer/tests/.gitignore b/scripts/developer/tests/.gitignore deleted file mode 100644 index 7cacbf042c..0000000000 --- a/scripts/developer/tests/.gitignore +++ /dev/null @@ -1 +0,0 @@ -cube_texture.ktx \ No newline at end of file diff --git a/scripts/developer/tests/scaling.png b/scripts/developer/tests/scaling.png deleted file mode 100644 index 1e6a7df45d8440cc52d3b45501b909419fed2f1b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3172 zcmd5;c~p~E7XQ63A%rEG0zw6aOe>MPK~IBJisaL=qAd_A)CCaGwo@pwR8gZC_|%SD z7o4J$st8ugBC@Cz1PuvBYq45TM5xH3-~b|o9YRPlCwSUFX3m*2?O*fPeed4;yT5ne zyYHTRFu>o3XKrr}fVXnRvQ+>DfPl*ZaO3bV9|M+iS1wx;Bqh%uyiNeFQgFJcjPsAZ zQ+zK$=}JukxPSm)@JBWj{=Z@I&oh>M-7es>42Jw255u%;9b7_Ar+anY4|u#h;LQ2T zXWDU%Msp|eqeX1oR5;Ed z%2}=L#||eS>yaPipfV=$G*|spnb+iuN)}3+#M!v_fuM66=g`gr46y9i{f9JUb}sR=GCq% zR(P3YRJ{JO!mW$0%9Jz@eYk?q7U{t?rcyCMhi z6{Z`TtYfCa`{>n@WsR%lQ;+N=tEm?r;P=t9QTaV#{6N zh1Pmom+C}u<3gMv&9tVn=P0E-_pG{G3+9@Qtto#hLS%otuAK(%ys9nwp}|Zmc#fx{ z@4e;VgALMJ*0VC@D40W;6Y8yVt(VamH@OrP?uF{bEryGZ>yr$b7q$3XH8k5~RT|1?GJ0JWv*9}?nbf>SGfY8XT`;`MnhRsYzrz*~= z*A&XdI*PFZ)w71p)L&UCq1xqkU@kKXfq;M)EJ!w0C{PvwOE%?t%W$GO* zQv5VVq%*X-k;SH=>(8gm)6xC*3s9bQ+sx>5RT7 zd4@9yS6et(OIvWcdDMMG9_j>>&9K$pDm%-Ru&KU$#B5$#Qtn?aEa-mH*Htd;G0_(7mw3bheDNh9_>{ z^Qy-bHmNZfzG!*8rbotKzSn=mCMS0TmwUqEoo9E0*ZsPG0VuUUW9$J8a9P8m5Z~r{ znNSEYOmn9J|Bl24<@*GEGdIz|qp>9b*Z}=!1OfPyUG2Y#)@lD4`*&c4Ogob4Bu+Xq z_pKif4jc5(+ssD9x+{L8cDah|fwsBUq1F8w&M`yF?qNW+?YVekiy}GmSVryVJ^eS| zh;`M+4z^hYRd^Q_9@K?wTb$A{7Xri|9nTYI6X~FIpsJ+#E>F2LwJt$rXE|;LH{VE| z;Xo7yWI3rZm-dT5(ROTHRc9U&zROcKs$;mhfnn zo9B75R?*7_8w>h>xgX>%Gl`By(!f8X>z@^CVZb)o~%L;_f%|NAB(nv;`jxp6AniR}RWt zOD#%0&=$Nt`J&_#-1=(EQqWKi8Lkb8!WuZfDcvVI0&)GgVqLa^bHM_2#0)@K3 zdjq1dUpco+wDwFy&qUnnvkH^scz73k? z*0^wSGR<<^RCVlhggmC)f!R|PKFR46xws_6p1H3d{JG8pRmZZE`8aUG%TL(o%%i$p34%Ee%*y;?&!bVsIIHVCU`&x67@V=oz80bPkRU#~ z>gCI6fJ#&z?)_yH7DU1RsqTCaah zxRduvIgME=l>hJbq$|g`^ed--g)kOKgQTUv^iBC-opi?Po#V+ zm|^$~;%#`!C&53@XE@4QRL7AQRrS7~vVf;t!Q#)z7t-jQjVzM8iRz8TkG3?+?X(#m z<7SA&gQxS2ubgGVr}+3Khj9P?+aBJ>0aLF{GTn+ZpK29FWaXm}S4>*R@TAF$j? zP`PuvJC1>5odjZPH}|b$E>yXWsgLIvw%Rj3?!mY;P72a{OGk~S zzi8qDb@f4?vTADBHjg5jCSF2k=JgQ|XpW2w2Br6wKArRmOB~8C&>lM*hdIYVKp2Y| z6JeT!ai>eA(2mL#^M^8v9P}5>P@G3x7M0vnt4+sq z^pA=!`D%4M<-@eFMq`Z_C+h!Q7-w(pixK<}i+|%%4w63`O{v~IOI3Pm{_;5hu<~vH KWra&4_WTP0c9{JD diff --git a/scripts/developer/tests/unit_tests/moduleTests/cycles/a.js b/scripts/developer/tests/unit_tests/moduleTests/cycles/a.js deleted file mode 100644 index 265cfaa2df..0000000000 --- a/scripts/developer/tests/unit_tests/moduleTests/cycles/a.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-env node */ -var a = exports; -a.done = false; -var b = require('./b.js'); -a.done = true; -a.name = 'a'; -a['a.done?'] = a.done; -a['b.done?'] = b.done; - -print('from a.js a.done =', a.done, '/ b.done =', b.done); diff --git a/scripts/developer/tests/unit_tests/moduleTests/cycles/b.js b/scripts/developer/tests/unit_tests/moduleTests/cycles/b.js deleted file mode 100644 index c46c872828..0000000000 --- a/scripts/developer/tests/unit_tests/moduleTests/cycles/b.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-env node */ -var b = exports; -b.done = false; -var a = require('./a.js'); -b.done = true; -b.name = 'b'; -b['a.done?'] = a.done; -b['b.done?'] = b.done; - -print('from b.js a.done =', a.done, '/ b.done =', b.done); diff --git a/scripts/developer/tests/unit_tests/moduleTests/cycles/main.js b/scripts/developer/tests/unit_tests/moduleTests/cycles/main.js deleted file mode 100644 index 0ec39cd656..0000000000 --- a/scripts/developer/tests/unit_tests/moduleTests/cycles/main.js +++ /dev/null @@ -1,17 +0,0 @@ -/* eslint-env node */ -/* global print */ -/* eslint-disable comma-dangle */ - -print('main.js'); -var a = require('./a.js'), - b = require('./b.js'); - -print('from main.js a.done =', a.done, 'and b.done =', b.done); - -module.exports = { - name: 'main', - a: a, - b: b, - 'a.done?': a.done, - 'b.done?': b.done, -}; diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorAPIException.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorAPIException.js deleted file mode 100644 index bbe694b578..0000000000 --- a/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorAPIException.js +++ /dev/null @@ -1,13 +0,0 @@ -/* eslint-disable comma-dangle */ -// test module method exception being thrown within main constructor -(function() { - var apiMethod = Script.require('../exceptions/exceptionInFunction.js'); - print(Script.resolvePath(''), "apiMethod", apiMethod); - // this next line throws from within apiMethod - print(apiMethod()); - return { - preload: function(uuid) { - print("entityConstructorAPIException::preload -- never seen --", uuid, Script.resolvePath('')); - }, - }; -}); diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorModule.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorModule.js deleted file mode 100644 index a4e8c17ab6..0000000000 --- a/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorModule.js +++ /dev/null @@ -1,23 +0,0 @@ -/* global module */ -/* eslint-disable comma-dangle */ -// test dual-purpose module and standalone Entity script -function MyEntity(filename) { - return { - preload: function(uuid) { - print("entityConstructorModule.js::preload"); - if (typeof module === 'object') { - print("module.filename", module.filename); - print("module.parent.filename", module.parent && module.parent.filename); - } - }, - clickDownOnEntity: function(uuid, evt) { - print("entityConstructorModule.js::clickDownOnEntity"); - }, - }; -} - -try { - module.exports = MyEntity; -} catch (e) {} // eslint-disable-line no-empty -print('entityConstructorModule::MyEntity', typeof MyEntity); -(MyEntity); diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested.js deleted file mode 100644 index a90d979877..0000000000 --- a/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested.js +++ /dev/null @@ -1,14 +0,0 @@ -/* global module */ -// test Entity constructor based on inherited constructor from a module -function constructor() { - print("entityConstructorNested::constructor"); - var MyEntity = Script.require('./entityConstructorModule.js'); - return new MyEntity("-- created from entityConstructorNested --"); -} - -try { - module.exports = constructor; -} catch (e) { - constructor; -} - diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested2.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested2.js deleted file mode 100644 index 29e0ed65b1..0000000000 --- a/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested2.js +++ /dev/null @@ -1,25 +0,0 @@ -/* global module */ -// test Entity constructor based on nested, inherited module constructors -function constructor() { - print("entityConstructorNested2::constructor"); - - // inherit from entityConstructorNested - var MyEntity = Script.require('./entityConstructorNested.js'); - function SubEntity() {} - SubEntity.prototype = new MyEntity('-- created from entityConstructorNested2 --'); - - // create new instance - var entity = new SubEntity(); - // "override" clickDownOnEntity for just this new instance - entity.clickDownOnEntity = function(uuid, evt) { - print("entityConstructorNested2::clickDownOnEntity"); - SubEntity.prototype.clickDownOnEntity.apply(this, arguments); - }; - return entity; -} - -try { - module.exports = constructor; -} catch (e) { - constructor; -} diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorRequireException.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorRequireException.js deleted file mode 100644 index 5872bce529..0000000000 --- a/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorRequireException.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable comma-dangle */ -// test module-related exception from within "require" evaluation itself -(function() { - var mod = Script.require('../exceptions/exception.js'); - return { - preload: function(uuid) { - print("entityConstructorRequireException::preload (never happens)", uuid, Script.resolvePath(''), mod); - }, - }; -}); diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadAPIError.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadAPIError.js deleted file mode 100644 index eaee178b0a..0000000000 --- a/scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadAPIError.js +++ /dev/null @@ -1,13 +0,0 @@ -/* eslint-disable comma-dangle */ -// test module method exception being thrown within preload -(function() { - var apiMethod = Script.require('../exceptions/exceptionInFunction.js'); - print(Script.resolvePath(''), "apiMethod", apiMethod); - return { - preload: function(uuid) { - // this next line throws from within apiMethod - print(apiMethod()); - print("entityPreloadAPIException::preload -- never seen --", uuid, Script.resolvePath('')); - }, - }; -}); diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadRequire.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadRequire.js deleted file mode 100644 index 50dab9fa7c..0000000000 --- a/scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadRequire.js +++ /dev/null @@ -1,11 +0,0 @@ -/* eslint-disable comma-dangle */ -// test requiring a module from within preload -(function constructor() { - return { - preload: function(uuid) { - print("entityPreloadRequire::preload"); - var example = Script.require('../example.json'); - print("entityPreloadRequire::example::name", example.name); - }, - }; -}); diff --git a/scripts/developer/tests/unit_tests/moduleTests/example.json b/scripts/developer/tests/unit_tests/moduleTests/example.json deleted file mode 100644 index 42d7fe07da..0000000000 --- a/scripts/developer/tests/unit_tests/moduleTests/example.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "Example JSON Module", - "last-modified": 1485789862, - "config": { - "title": "My Title", - "width": 800, - "height": 600 - } -} diff --git a/scripts/developer/tests/unit_tests/moduleTests/exceptions/exception.js b/scripts/developer/tests/unit_tests/moduleTests/exceptions/exception.js deleted file mode 100644 index 8d25d6b7a4..0000000000 --- a/scripts/developer/tests/unit_tests/moduleTests/exceptions/exception.js +++ /dev/null @@ -1,4 +0,0 @@ -/* eslint-env node */ -module.exports = "n/a"; -throw new Error('exception on line 2'); - diff --git a/scripts/developer/tests/unit_tests/moduleTests/exceptions/exceptionInFunction.js b/scripts/developer/tests/unit_tests/moduleTests/exceptions/exceptionInFunction.js deleted file mode 100644 index 69415a0741..0000000000 --- a/scripts/developer/tests/unit_tests/moduleTests/exceptions/exceptionInFunction.js +++ /dev/null @@ -1,38 +0,0 @@ -/* eslint-env node */ -// dummy lines to make sure exception line number is well below parent test script -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// - - -function myfunc() { - throw new Error('exception on line 32 in myfunc'); -} -module.exports = myfunc; -if (Script[module.filename] === 'throw') { - myfunc(); -} diff --git a/scripts/developer/tests/unit_tests/moduleUnitTests.js b/scripts/developer/tests/unit_tests/moduleUnitTests.js deleted file mode 100644 index 6810dd8b6d..0000000000 --- a/scripts/developer/tests/unit_tests/moduleUnitTests.js +++ /dev/null @@ -1,378 +0,0 @@ -/* eslint-env jasmine, node */ -/* global print:true, Script:true, global:true, require:true */ -/* eslint-disable comma-dangle */ -var isNode = instrumentTestrunner(), - runInterfaceTests = !isNode, - runNetworkTests = true; - -// describe wrappers (note: `xdescribe` indicates a disabled or "pending" jasmine test) -var INTERFACE = { describe: runInterfaceTests ? describe : xdescribe }, - NETWORK = { describe: runNetworkTests ? describe : xdescribe }; - -describe('require', function() { - describe('resolve', function() { - it('should resolve relative filenames', function() { - var expected = Script.resolvePath('./moduleTests/example.json'); - expect(require.resolve('./moduleTests/example.json')).toEqual(expected); - }); - describe('exceptions', function() { - it('should reject blank "" module identifiers', function() { - expect(function() { - require.resolve(''); - }).toThrowError(/Cannot find/); - }); - it('should reject excessive identifier sizes', function() { - expect(function() { - require.resolve(new Array(8193).toString()); - }).toThrowError(/Cannot find/); - }); - it('should reject implicitly-relative filenames', function() { - expect(function() { - var mod = require.resolve('example.js'); - mod.exists; - }).toThrowError(/Cannot find/); - }); - it('should reject unanchored, existing filenames with advice', function() { - expect(function() { - var mod = require.resolve('moduleTests/example.json'); - mod.exists; - }).toThrowError(/use '.\/moduleTests\/example\.json'/); - }); - it('should reject unanchored, non-existing filenames', function() { - expect(function() { - var mod = require.resolve('asdfssdf/example.json'); - mod.exists; - }).toThrowError(/Cannot find.*system module not found/); - }); - it('should reject non-existent filenames', function() { - expect(function() { - require.resolve('./404error.js'); - }).toThrowError(/Cannot find/); - }); - it('should reject identifiers resolving to a directory', function() { - expect(function() { - var mod = require.resolve('.'); - mod.exists; - // console.warn('resolved(.)', mod); - }).toThrowError(/Cannot find/); - expect(function() { - var mod = require.resolve('..'); - mod.exists; - // console.warn('resolved(..)', mod); - }).toThrowError(/Cannot find/); - expect(function() { - var mod = require.resolve('../'); - mod.exists; - // console.warn('resolved(../)', mod); - }).toThrowError(/Cannot find/); - }); - (isNode ? xit : it)('should reject non-system, extensionless identifiers', function() { - expect(function() { - require.resolve('./example'); - }).toThrowError(/Cannot find/); - }); - }); - }); - - describe('JSON', function() { - it('should import .json modules', function() { - var example = require('./moduleTests/example.json'); - expect(example.name).toEqual('Example JSON Module'); - }); - // noet: support for loading JSON via content type workarounds reverted - // (leaving these tests intact in case ever revisited later) - // INTERFACE.describe('interface', function() { - // NETWORK.describe('network', function() { - // xit('should import #content-type=application/json modules', function() { - // var results = require('https://jsonip.com#content-type=application/json'); - // expect(results.ip).toMatch(/^[.0-9]+$/); - // }); - // xit('should import content-type: application/json modules', function() { - // var scope = { 'content-type': 'application/json' }; - // var results = require.call(scope, 'https://jsonip.com'); - // expect(results.ip).toMatch(/^[.0-9]+$/); - // }); - // }); - // }); - - }); - - INTERFACE.describe('system', function() { - it('require("vec3")', function() { - expect(require('vec3')).toEqual(jasmine.any(Function)); - }); - it('require("vec3").method', function() { - expect(require('vec3')().isValid).toEqual(jasmine.any(Function)); - }); - it('require("vec3") as constructor', function() { - var vec3 = require('vec3'); - var v = vec3(1.1, 2.2, 3.3); - expect(v).toEqual(jasmine.any(Object)); - expect(v.isValid).toEqual(jasmine.any(Function)); - expect(v.isValid()).toBe(true); - expect(v.toString()).toEqual('[Vec3 (1.100,2.200,3.300)]'); - }); - }); - - describe('cache', function() { - it('should cache modules by resolved module id', function() { - var value = new Date; - var example = require('./moduleTests/example.json'); - // earmark the module object with a unique value - example['.test'] = value; - var example2 = require('../../tests/unit_tests/moduleTests/example.json'); - expect(example2).toBe(example); - // verify earmark is still the same after a second require() - expect(example2['.test']).toBe(example['.test']); - }); - it('should reload cached modules set to null', function() { - var value = new Date; - var example = require('./moduleTests/example.json'); - example['.test'] = value; - require.cache[require.resolve('../../tests/unit_tests/moduleTests/example.json')] = null; - var example2 = require('../../tests/unit_tests/moduleTests/example.json'); - // verify the earmark is *not* the same as before - expect(example2).not.toBe(example); - expect(example2['.test']).not.toBe(example['.test']); - }); - it('should reload when module property is deleted', function() { - var value = new Date; - var example = require('./moduleTests/example.json'); - example['.test'] = value; - delete require.cache[require.resolve('../../tests/unit_tests/moduleTests/example.json')]; - var example2 = require('../../tests/unit_tests/moduleTests/example.json'); - // verify the earmark is *not* the same as before - expect(example2).not.toBe(example); - expect(example2['.test']).not.toBe(example['.test']); - }); - }); - - describe('cyclic dependencies', function() { - describe('should allow lazy-ref cyclic module resolution', function() { - var main; - beforeEach(function() { - // eslint-disable-next-line - try { this._print = print; } catch (e) {} - // during these tests print() is no-op'd so that it doesn't disrupt the reporter output - print = function() {}; - Script.resetModuleCache(); - }); - afterEach(function() { - print = this._print; - }); - it('main is requirable', function() { - main = require('./moduleTests/cycles/main.js'); - expect(main).toEqual(jasmine.any(Object)); - }); - it('transient a and b done values', function() { - expect(main.a['b.done?']).toBe(true); - expect(main.b['a.done?']).toBe(false); - }); - it('ultimate a.done?', function() { - expect(main['a.done?']).toBe(true); - }); - it('ultimate b.done?', function() { - expect(main['b.done?']).toBe(true); - }); - }); - }); - - describe('JS', function() { - it('should throw catchable local file errors', function() { - expect(function() { - require('file:///dev/null/non-existent-file.js'); - }).toThrowError(/path not found|Cannot find.*non-existent-file/); - }); - it('should throw catchable invalid id errors', function() { - expect(function() { - require(new Array(4096 * 2).toString()); - }).toThrowError(/invalid.*size|Cannot find.*,{30}/); - }); - it('should throw catchable unresolved id errors', function() { - expect(function() { - require('foobar:/baz.js'); - }).toThrowError(/could not resolve|Cannot find.*foobar:/); - }); - - NETWORK.describe('network', function() { - // note: depending on retries these tests can take up to 60 seconds each to timeout - var timeout = 75 * 1000; - it('should throw catchable host errors', function() { - expect(function() { - var mod = require('http://non.existent.highfidelity.io/moduleUnitTest.js'); - print("mod", Object.keys(mod)); - }).toThrowError(/error retrieving script .ServerUnavailable.|Cannot find.*non.existent/); - }, timeout); - it('should throw catchable network timeouts', function() { - expect(function() { - require('http://ping.highfidelity.io:1024'); - }).toThrowError(/error retrieving script .Timeout.|Cannot find.*ping.highfidelity/); - }, timeout); - }); - }); - - INTERFACE.describe('entity', function() { - var sampleScripts = [ - 'entityConstructorAPIException.js', - 'entityConstructorModule.js', - 'entityConstructorNested2.js', - 'entityConstructorNested.js', - 'entityConstructorRequireException.js', - 'entityPreloadAPIError.js', - 'entityPreloadRequire.js', - ].filter(Boolean).map(function(id) { - return Script.require.resolve('./moduleTests/entity/'+id); - }); - - var uuids = []; - function cleanup() { - uuids.splice(0,uuids.length).forEach(function(uuid) { - Entities.deleteEntity(uuid); - }); - } - afterAll(cleanup); - // extra sanity check to avoid lingering entities - Script.scriptEnding.connect(cleanup); - - for (var i=0; i < sampleScripts.length; i++) { - maketest(i); - } - - function maketest(i) { - var script = sampleScripts[ i % sampleScripts.length ]; - var shortname = '['+i+'] ' + script.split('/').pop(); - var position = MyAvatar.position; - position.y -= i/2; - // define a unique jasmine test for the current entity script - it(shortname, function(done) { - var uuid = Entities.addEntity({ - text: shortname, - description: Script.resolvePath('').split('/').pop(), - type: 'Text', - position: position, - rotation: MyAvatar.orientation, - script: script, - scriptTimestamp: +new Date, - lifetime: 20, - lineHeight: 1/8, - dimensions: { x: 2, y: 0.5, z: 0.01 }, - backgroundColor: { red: 0, green: 0, blue: 0 }, - color: { red: 0xff, green: 0xff, blue: 0xff }, - }, !Entities.serversExist() || !Entities.canRezTmp()); - uuids.push(uuid); - function stopChecking() { - if (ii) { - Script.clearInterval(ii); - ii = 0; - } - } - var ii = Script.setInterval(function() { - Entities.queryPropertyMetadata(uuid, "script", function(err, result) { - if (err) { - stopChecking(); - throw new Error(err); - } - if (result.success) { - stopChecking(); - if (/Exception/.test(script)) { - expect(result.status).toMatch(/^error_(loading|running)_script$/); - } else { - expect(result.status).toEqual("running"); - } - Entities.deleteEntity(uuid); - done(); - } else { - print('!result.success', JSON.stringify(result)); - } - }); - }, 100); - Script.setTimeout(stopChecking, 4900); - }, 5000 /* jasmine async timeout */); - } - }); -}); - -// support for isomorphic Node.js / Interface unit testing -// note: run `npm install` from unit_tests/ and then `node moduleUnitTests.js` -function run() {} -function instrumentTestrunner() { - var isNode = typeof process === 'object' && process.title === 'node'; - if (typeof describe === 'function') { - // already running within a test runner; assume jasmine is ready-to-go - return isNode; - } - if (isNode) { - /* eslint-disable no-console */ - // Node.js test mode - // to keep things consistent Node.js uses the local jasmine.js library (instead of an npm version) - var jasmineRequire = require('../../libraries/jasmine/jasmine.js'); - var jasmine = jasmineRequire.core(jasmineRequire); - var env = jasmine.getEnv(); - var jasmineInterface = jasmineRequire.interface(jasmine, env); - for (var p in jasmineInterface) { - global[p] = jasmineInterface[p]; - } - env.addReporter(new (require('jasmine-console-reporter'))); - // testing mocks - Script = { - resetModuleCache: function() { - module.require.cache = {}; - }, - setTimeout: setTimeout, - clearTimeout: clearTimeout, - resolvePath: function(id) { - // this attempts to accurately emulate how Script.resolvePath works - var trace = {}; Error.captureStackTrace(trace); - var base = trace.stack.split('\n')[2].replace(/^.*[(]|[)].*$/g,'').replace(/:[0-9]+:[0-9]+.*$/,''); - if (!id) { - return base; - } - var rel = base.replace(/[^\/]+$/, id); - console.info('rel', rel); - return require.resolve(rel); - }, - require: function(mod) { - return require(Script.require.resolve(mod)); - }, - }; - Script.require.cache = require.cache; - Script.require.resolve = function(mod) { - if (mod === '.' || /^\.\.($|\/)/.test(mod)) { - throw new Error("Cannot find module '"+mod+"' (is dir)"); - } - var path = require.resolve(mod); - // console.info('node-require-reoslved', mod, path); - try { - if (require('fs').lstatSync(path).isDirectory()) { - throw new Error("Cannot find module '"+path+"' (is directory)"); - } - // console.info('!path', path); - } catch (e) { - console.error(e); - } - return path; - }; - print = console.info.bind(console, '[print]'); - /* eslint-enable no-console */ - } else { - // Interface test mode - global = this; - Script.require('../../../system/libraries/utils.js'); - this.jasmineRequire = Script.require('../../libraries/jasmine/jasmine.js'); - Script.require('../../libraries/jasmine/hifi-boot.js'); - require = Script.require; - // polyfill console - /* global console:true */ - console = { - log: print, - info: print.bind(this, '[info]'), - warn: print.bind(this, '[warn]'), - error: print.bind(this, '[error]'), - debug: print.bind(this, '[debug]'), - }; - } - // eslint-disable-next-line - run = function() { global.jasmine.getEnv().execute(); }; - return isNode; -} -run(); diff --git a/scripts/developer/tests/unit_tests/package.json b/scripts/developer/tests/unit_tests/package.json deleted file mode 100644 index 91d719b687..0000000000 --- a/scripts/developer/tests/unit_tests/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "unit_tests", - "devDependencies": { - "jasmine-console-reporter": "^1.2.7" - } -} diff --git a/scripts/developer/tests/unit_tests/scriptUnitTests.js b/scripts/developer/tests/unit_tests/scriptUnitTests.js index fa8cb44608..63b451e97f 100644 --- a/scripts/developer/tests/unit_tests/scriptUnitTests.js +++ b/scripts/developer/tests/unit_tests/scriptUnitTests.js @@ -15,20 +15,10 @@ describe('Script', function () { // characterization tests // initially these are just to capture how the app works currently var testCases = { - // special relative resolves '': filename, '.': dirname, '..': parentdir, - - // local file "magic" tilde path expansion - '/~/defaultScripts.js': ScriptDiscoveryService.defaultScriptsPath + '/defaultScripts.js', - - // these schemes appear to always get resolved to empty URLs - 'qrc://test': '', 'about:Entities 1': '', - 'ftp://host:port/path': '', - 'data:text/html;text,foo': '', - 'Entities 1': dirname + 'Entities 1', './file.js': dirname + 'file.js', 'c:/temp/': 'file:///c:/temp/', @@ -41,12 +31,6 @@ describe('Script', function () { '/~/libraries/utils.js': 'file:///~/libraries/utils.js', '/temp/file.js': 'file:///temp/file.js', '/~/': 'file:///~/', - - // these schemes appear to always get resolved to the same URL again - 'http://highfidelity.com': 'http://highfidelity.com', - 'atp:/highfidelity': 'atp:/highfidelity', - 'atp:c2d7e3a48cadf9ba75e4f8d9f4d80e75276774880405a093fdee36543aa04f': - 'atp:c2d7e3a48cadf9ba75e4f8d9f4d80e75276774880405a093fdee36543aa04f', }; describe('resolvePath', function () { Object.keys(testCases).forEach(function(input) { @@ -58,7 +42,7 @@ describe('Script', function () { describe('include', function () { var old_cache_buster; - var cache_buster = '#' + new Date().getTime().toString(36); + var cache_buster = '#' + +new Date; beforeAll(function() { old_cache_buster = Settings.getValue('cache_buster'); Settings.setValue('cache_buster', cache_buster); diff --git a/scripts/developer/utilities/record/recorder.js b/scripts/developer/utilities/record/recorder.js index ba1c8b0393..0e335116d5 100644 --- a/scripts/developer/utilities/record/recorder.js +++ b/scripts/developer/utilities/record/recorder.js @@ -9,14 +9,12 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -/* globals HIFI_PUBLIC_BUCKET:true, Tool, ToolBar */ - HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/"; Script.include("/~/system/libraries/toolBars.js"); var recordingFile = "recording.hfr"; -function setDefaultPlayerOptions() { +function setPlayerOptions() { Recording.setPlayFromCurrentLocation(true); Recording.setPlayerUseDisplayName(false); Recording.setPlayerUseAttachments(false); @@ -40,16 +38,16 @@ var saveIcon; var loadIcon; var spacing; var timerOffset; +setupToolBar(); + var timer = null; var slider = null; - -setupToolBar(); setupTimer(); var watchStop = false; function setupToolBar() { - if (toolBar !== null) { + if (toolBar != null) { print("Multiple calls to Recorder.js:setupToolBar()"); return; } @@ -58,8 +56,6 @@ function setupToolBar() { toolBar = new ToolBar(0, 0, ToolBar.HORIZONTAL); - toolBar.onMove = onToolbarMove; - toolBar.setBack(COLOR_TOOL_BAR, ALPHA_OFF); recordIcon = toolBar.addTool({ @@ -90,7 +86,7 @@ function setupToolBar() { visible: true }, false); - timerOffset = toolBar.width + ToolBar.SPACING; + timerOffset = toolBar.width; spacing = toolBar.addSpacing(0); saveIcon = toolBar.addTool({ @@ -116,15 +112,15 @@ function setupTimer() { text: (0.00).toFixed(3), backgroundColor: COLOR_OFF, x: 0, y: 0, - width: 200, height: 25, - leftMargin: 5, topMargin: 3, + width: 0, height: 0, + leftMargin: 10, topMargin: 10, alpha: 1.0, backgroundAlpha: 1.0, visible: true }); slider = { x: 0, y: 0, w: 200, h: 20, - pos: 0.0 // 0.0 <= pos <= 1.0 + pos: 0.0, // 0.0 <= pos <= 1.0 }; slider.background = Overlays.addOverlay("text", { text: "", @@ -148,40 +144,20 @@ function setupTimer() { }); } -function onToolbarMove(newX, newY, deltaX, deltaY) { - Overlays.editOverlay(timer, { - x: newX + timerOffset - ToolBar.SPACING, - y: newY - }); - - slider.x = newX - ToolBar.SPACING; - slider.y = newY - slider.h - ToolBar.SPACING; - - Overlays.editOverlay(slider.background, { - x: slider.x, - y: slider.y - }); - Overlays.editOverlay(slider.foreground, { - x: slider.x, - y: slider.y - }); -} - function updateTimer() { var text = ""; if (Recording.isRecording()) { text = formatTime(Recording.recorderElapsed()); + } else { - text = formatTime(Recording.playerElapsed()) + " / " + formatTime(Recording.playerLength()); + text = formatTime(Recording.playerElapsed()) + " / " + + formatTime(Recording.playerLength()); } - var timerWidth = text.length * 8 + ((Recording.isRecording()) ? 15 : 0); - Overlays.editOverlay(timer, { - text: text, - width: timerWidth - }); - toolBar.changeSpacing(timerWidth + ToolBar.SPACING, spacing); + text: text + }) + toolBar.changeSpacing(text.length * 8 + ((Recording.isRecording()) ? 15 : 0), spacing); if (Recording.isRecording()) { slider.pos = 1.0; @@ -197,7 +173,7 @@ function updateTimer() { function formatTime(time) { var MIN_PER_HOUR = 60; var SEC_PER_MIN = 60; - var MSEC_DIGITS = 3; + var MSEC_PER_SEC = 1000; var hours = Math.floor(time / (SEC_PER_MIN * MIN_PER_HOUR)); time -= hours * (SEC_PER_MIN * MIN_PER_HOUR); @@ -208,19 +184,37 @@ function formatTime(time) { var seconds = time; var text = ""; - text += (hours > 0) ? hours + ":" : ""; - text += (minutes > 0) ? ((minutes < 10 && text !== "") ? "0" : "") + minutes + ":" : ""; - text += ((seconds < 10 && text !== "") ? "0" : "") + seconds.toFixed(MSEC_DIGITS); + text += (hours > 0) ? hours + ":" : + ""; + text += (minutes > 0) ? ((minutes < 10 && text != "") ? "0" : "") + minutes + ":" : + ""; + text += ((seconds < 10 && text != "") ? "0" : "") + seconds.toFixed(3); return text; } function moveUI() { var relative = { x: 70, y: 40 }; toolBar.move(relative.x, windowDimensions.y - relative.y); + Overlays.editOverlay(timer, { + x: relative.x + timerOffset - ToolBar.SPACING, + y: windowDimensions.y - relative.y - ToolBar.SPACING + }); + + slider.x = relative.x - ToolBar.SPACING; + slider.y = windowDimensions.y - relative.y - slider.h - ToolBar.SPACING; + + Overlays.editOverlay(slider.background, { + x: slider.x, + y: slider.y, + }); + Overlays.editOverlay(slider.foreground, { + x: slider.x, + y: slider.y, + }); } function mousePressEvent(event) { - var clickedOverlay = Overlays.getOverlayAtPoint({ x: event.x, y: event.y }); + clickedOverlay = Overlays.getOverlayAtPoint({ x: event.x, y: event.y }); if (recordIcon === toolBar.clicked(clickedOverlay, false) && !Recording.isPlaying()) { if (!Recording.isRecording()) { @@ -232,11 +226,7 @@ function mousePressEvent(event) { toolBar.setAlpha(ALPHA_OFF, loadIcon); } else { Recording.stopRecording(); - toolBar.selectTool(recordIcon, true); - setDefaultPlayerOptions(); - // Plays the recording at the same spot as you recorded it - Recording.setPlayFromCurrentLocation(false); - Recording.setPlayerTime(0); + toolBar.selectTool(recordIcon, true ); Recording.loadLastRecording(); toolBar.setAlpha(ALPHA_ON, playIcon); toolBar.setAlpha(ALPHA_ON, playLoopIcon); @@ -250,6 +240,7 @@ function mousePressEvent(event) { toolBar.setAlpha(ALPHA_ON, saveIcon); toolBar.setAlpha(ALPHA_ON, loadIcon); } else if (Recording.playerLength() > 0) { + setPlayerOptions(); Recording.setPlayerLoop(false); Recording.startPlaying(); toolBar.setAlpha(ALPHA_OFF, recordIcon); @@ -264,6 +255,7 @@ function mousePressEvent(event) { toolBar.setAlpha(ALPHA_ON, saveIcon); toolBar.setAlpha(ALPHA_ON, loadIcon); } else if (Recording.playerLength() > 0) { + setPlayerOptions(); Recording.setPlayerLoop(true); Recording.startPlaying(); toolBar.setAlpha(ALPHA_OFF, recordIcon); @@ -271,7 +263,7 @@ function mousePressEvent(event) { toolBar.setAlpha(ALPHA_OFF, loadIcon); } } else if (saveIcon === toolBar.clicked(clickedOverlay)) { - if (!Recording.isRecording() && !Recording.isPlaying() && Recording.playerLength() !== 0) { + if (!Recording.isRecording() && !Recording.isPlaying() && Recording.playerLength() != 0) { recordingFile = Window.save("Save recording to file", ".", "Recordings (*.hfr)"); if (!(recordingFile === "null" || recordingFile === null || recordingFile === "")) { Recording.saveRecording(recordingFile); @@ -282,7 +274,6 @@ function mousePressEvent(event) { recordingFile = Window.browse("Load recording from file", ".", "Recordings (*.hfr *.rec *.HFR *.REC)"); if (!(recordingFile === "null" || recordingFile === null || recordingFile === "")) { Recording.loadRecording(recordingFile); - setDefaultPlayerOptions(); } if (Recording.playerLength() > 0) { toolBar.setAlpha(ALPHA_ON, playIcon); @@ -291,8 +282,8 @@ function mousePressEvent(event) { } } } else if (Recording.playerLength() > 0 && - slider.x < event.x && event.x < slider.x + slider.w && - slider.y < event.y && event.y < slider.y + slider.h) { + slider.x < event.x && event.x < slider.x + slider.w && + slider.y < event.y && event.y < slider.y + slider.h) { isSliding = true; slider.pos = (event.x - slider.x) / slider.w; Recording.setPlayerTime(slider.pos * Recording.playerLength()); @@ -317,7 +308,7 @@ function mouseReleaseEvent(event) { function update() { var newDimensions = Controller.getViewportDimensions(); - if (windowDimensions.x !== newDimensions.x || windowDimensions.y !== newDimensions.y) { + if (windowDimensions.x != newDimensions.x || windowDimensions.y != newDimensions.y) { windowDimensions = newDimensions; moveUI(); } diff --git a/scripts/developer/utilities/render/deferredLighting.qml b/scripts/developer/utilities/render/deferredLighting.qml index c7ec8e1153..99a9f258e3 100644 --- a/scripts/developer/utilities/render/deferredLighting.qml +++ b/scripts/developer/utilities/render/deferredLighting.qml @@ -25,7 +25,7 @@ Column { "Lightmap:LightingModel:enableLightmap", "Background:LightingModel:enableBackground", "ssao:AmbientOcclusion:enabled", - "Textures:LightingModel:enableMaterialTexturing" + "Textures:LightingModel:enableMaterialTexturing", ] CheckBox { text: modelData.split(":")[0] @@ -45,7 +45,6 @@ Column { "Diffuse:LightingModel:enableDiffuse", "Specular:LightingModel:enableSpecular", "Albedo:LightingModel:enableAlbedo", - "Wireframe:LightingModel:enableWireframe" ] CheckBox { text: modelData.split(":")[0] diff --git a/scripts/modules/vec3.js b/scripts/modules/vec3.js deleted file mode 100644 index f164f01374..0000000000 --- a/scripts/modules/vec3.js +++ /dev/null @@ -1,69 +0,0 @@ -// Example of using a "system module" to decouple Vec3's implementation details. -// -// Users would bring Vec3 support in as a module: -// var vec3 = Script.require('vec3'); -// - -// (this example is compatible with using as a Script.include and as a Script.require module) -try { - // Script.require - module.exports = vec3; -} catch(e) { - // Script.include - Script.registerValue("vec3", vec3); -} - -vec3.fromObject = function(v) { - //return new vec3(v.x, v.y, v.z); - //... this is even faster and achieves the same effect - v.__proto__ = vec3.prototype; - return v; -}; - -vec3.prototype = { - multiply: function(v2) { - // later on could support overrides like so: - // if (v2 instanceof quat) { [...] } - // which of the below is faster (C++ or JS)? - // (dunno -- but could systematically find out and go with that version) - - // pure JS option - // return new vec3(this.x * v2.x, this.y * v2.y, this.z * v2.z); - - // hybrid C++ option - return vec3.fromObject(Vec3.multiply(this, v2)); - }, - // detects any NaN and Infinity values - isValid: function() { - return isFinite(this.x) && isFinite(this.y) && isFinite(this.z); - }, - // format Vec3's, eg: - // var v = vec3(); - // print(v); // outputs [Vec3 (0.000, 0.000, 0.000)] - toString: function() { - if (this === vec3.prototype) { - return "{Vec3 prototype}"; - } - function fixed(n) { return n.toFixed(3); } - return "[Vec3 (" + [this.x, this.y, this.z].map(fixed) + ")]"; - }, -}; - -vec3.DEBUG = true; - -function vec3(x, y, z) { - if (!(this instanceof vec3)) { - // if vec3 is called as a function then re-invoke as a constructor - // (so that `value instanceof vec3` holds true for created values) - return new vec3(x, y, z); - } - - // unfold default arguments (vec3(), vec3(.5), vec3(0,1), etc.) - this.x = x !== undefined ? x : 0; - this.y = y !== undefined ? y : this.x; - this.z = z !== undefined ? z : this.y; - - if (vec3.DEBUG && !this.isValid()) - throw new Error('vec3() -- invalid initial values ['+[].slice.call(arguments)+']'); -}; - diff --git a/scripts/system/assets/images/icon-particles.svg b/scripts/system/assets/images/icon-particles.svg deleted file mode 100644 index 5e0105d7cd..0000000000 --- a/scripts/system/assets/images/icon-particles.svg +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/scripts/system/assets/images/icon-point-light.svg b/scripts/system/assets/images/icon-point-light.svg deleted file mode 100644 index 896c35b63b..0000000000 --- a/scripts/system/assets/images/icon-point-light.svg +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/scripts/system/assets/images/icon-spot-light.svg b/scripts/system/assets/images/icon-spot-light.svg deleted file mode 100644 index ac2f87bb27..0000000000 --- a/scripts/system/assets/images/icon-spot-light.svg +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index 1c6c9af272..c058f046db 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -85,7 +85,6 @@ function Trigger(hand) { } var coolInTimeout = null; -var ignoredEntities = []; var TELEPORTER_STATES = { IDLE: 'idle', @@ -240,11 +239,11 @@ function Teleporter() { // We might hit an invisible entity that is not a seat, so we need to do a second pass. // * In the second pass we pick against visible entities only. // - var intersection = Entities.findRayIntersection(pickRay, true, [], [this.targetEntity].concat(ignoredEntities), false, true); + var intersection = Entities.findRayIntersection(pickRay, true, [], [this.targetEntity], false, true); var teleportLocationType = getTeleportTargetType(intersection); if (teleportLocationType === TARGET.INVISIBLE) { - intersection = Entities.findRayIntersection(pickRay, true, [], [this.targetEntity].concat(ignoredEntities), true, true); + intersection = Entities.findRayIntersection(pickRay, true, [], [this.targetEntity], true, true); teleportLocationType = getTeleportTargetType(intersection); } @@ -514,7 +513,7 @@ function cleanup() { Script.scriptEnding.connect(cleanup); var isDisabled = false; -var handleTeleportMessages = function(channel, message, sender) { +var handleHandMessages = function(channel, message, sender) { var data; if (sender === MyAvatar.sessionUUID) { if (channel === 'Hifi-Teleport-Disabler') { @@ -530,20 +529,12 @@ var handleTeleportMessages = function(channel, message, sender) { if (message === 'none') { isDisabled = false; } - } else if (channel === 'Hifi-Teleport-Ignore-Add' && !Uuid.isNull(message) && ignoredEntities.indexOf(message) === -1) { - ignoredEntities.push(message); - } else if (channel === 'Hifi-Teleport-Ignore-Remove' && !Uuid.isNull(message)) { - var removeIndex = ignoredEntities.indexOf(message); - if (removeIndex > -1) { - ignoredEntities.splice(removeIndex, 1); - } + } } } Messages.subscribe('Hifi-Teleport-Disabler'); -Messages.subscribe('Hifi-Teleport-Ignore-Add'); -Messages.subscribe('Hifi-Teleport-Ignore-Remove'); -Messages.messageReceived.connect(handleTeleportMessages); +Messages.messageReceived.connect(handleHandMessages); }()); // END LOCAL_SCOPE diff --git a/scripts/system/controllers/toggleAdvancedMovementForHandControllers.js b/scripts/system/controllers/toggleAdvancedMovementForHandControllers.js index e6c9b0aee0..46464dc2e1 100644 --- a/scripts/system/controllers/toggleAdvancedMovementForHandControllers.js +++ b/scripts/system/controllers/toggleAdvancedMovementForHandControllers.js @@ -17,14 +17,15 @@ var mappingName, basicMapping, isChecked; var TURN_RATE = 1000; var MENU_ITEM_NAME = "Advanced Movement For Hand Controllers"; +var SETTINGS_KEY = 'advancedMovementForHandControllersIsChecked'; var isDisabled = false; -var previousSetting = MyAvatar.useAdvancedMovementControls; -if (previousSetting === false) { +var previousSetting = Settings.getValue(SETTINGS_KEY); +if (previousSetting === '' || previousSetting === false || previousSetting === 'false') { previousSetting = false; isChecked = false; } -if (previousSetting === true) { +if (previousSetting === true || previousSetting === 'true') { previousSetting = true; isChecked = true; } @@ -36,6 +37,7 @@ function addAdvancedMovementItemToSettingsMenu() { isCheckable: true, isChecked: previousSetting }); + } function rotate180() { @@ -70,6 +72,7 @@ function registerBasicMapping() { } return; }); + basicMapping.from(Controller.Standard.LX).to(Controller.Standard.RX); basicMapping.from(Controller.Standard.RY).to(function(value) { if (isDisabled) { return; @@ -109,10 +112,10 @@ function menuItemEvent(menuItem) { if (menuItem == MENU_ITEM_NAME) { isChecked = Menu.isOptionChecked(MENU_ITEM_NAME); if (isChecked === true) { - MyAvatar.useAdvancedMovementControls = true; + Settings.setValue(SETTINGS_KEY, true); disableMappings(); } else if (isChecked === false) { - MyAvatar.useAdvancedMovementControls = false; + Settings.setValue(SETTINGS_KEY, false); enableMappings(); } } diff --git a/scripts/system/edit.js b/scripts/system/edit.js index fb5a3c2f73..a440fec1ac 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -33,27 +33,13 @@ Script.include([ "libraries/gridTool.js", "libraries/entityList.js", "particle_explorer/particleExplorerTool.js", - "libraries/entityIconOverlayManager.js" + "libraries/lightOverlayManager.js" ]); var selectionDisplay = SelectionDisplay; var selectionManager = SelectionManager; -const PARTICLE_SYSTEM_URL = Script.resolvePath("assets/images/icon-particles.svg"); -const POINT_LIGHT_URL = Script.resolvePath("assets/images/icon-point-light.svg"); -const SPOT_LIGHT_URL = Script.resolvePath("assets/images/icon-spot-light.svg"); -entityIconOverlayManager = new EntityIconOverlayManager(['Light', 'ParticleEffect'], function(entityID) { - var properties = Entities.getEntityProperties(entityID, ['type', 'isSpotlight']); - if (properties.type === 'Light') { - return { - url: properties.isSpotlight ? SPOT_LIGHT_URL : POINT_LIGHT_URL, - } - } else { - return { - url: PARTICLE_SYSTEM_URL, - } - } -}); +var lightOverlayManager = new LightOverlayManager(); var cameraManager = new CameraManager(); @@ -67,45 +53,7 @@ var entityListTool = new EntityListTool(); selectionManager.addEventListener(function () { selectionDisplay.updateHandles(); - entityIconOverlayManager.updatePositions(); - - // Update particle explorer - var needToDestroyParticleExplorer = false; - if (selectionManager.selections.length === 1) { - var selectedEntityID = selectionManager.selections[0]; - if (selectedEntityID === selectedParticleEntityID) { - return; - } - var type = Entities.getEntityProperties(selectedEntityID, "type").type; - if (type === "ParticleEffect") { - // Destroy the old particles web view first - particleExplorerTool.destroyWebView(); - particleExplorerTool.createWebView(); - var properties = Entities.getEntityProperties(selectedEntityID); - var particleData = { - messageType: "particle_settings", - currentProperties: properties - }; - selectedParticleEntityID = selectedEntityID; - particleExplorerTool.setActiveParticleEntity(selectedParticleEntityID); - - particleExplorerTool.webView.webEventReceived.connect(function (data) { - data = JSON.parse(data); - if (data.messageType === "page_loaded") { - particleExplorerTool.webView.emitScriptEvent(JSON.stringify(particleData)); - } - }); - } else { - needToDestroyParticleExplorer = true; - } - } else { - needToDestroyParticleExplorer = true; - } - - if (needToDestroyParticleExplorer && selectedParticleEntityID !== null) { - selectedParticleEntityID = null; - particleExplorerTool.destroyWebView(); - } + lightOverlayManager.updatePositions(); }); const KEY_P = 80; //Key code for letter p used for Parenting hotkey. @@ -134,13 +82,13 @@ var DEFAULT_LIGHT_DIMENSIONS = Vec3.multiply(20, DEFAULT_DIMENSIONS); var MENU_AUTO_FOCUS_ON_SELECT = "Auto Focus on Select"; var MENU_EASE_ON_FOCUS = "Ease Orientation on Focus"; -var MENU_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE = "Show Lights and Particle Systems in Edit Mode"; +var MENU_SHOW_LIGHTS_IN_EDIT_MODE = "Show Lights in Edit Mode"; var MENU_SHOW_ZONES_IN_EDIT_MODE = "Show Zones in Edit Mode"; var SETTING_INSPECT_TOOL_ENABLED = "inspectToolEnabled"; var SETTING_AUTO_FOCUS_ON_SELECT = "autoFocusOnSelect"; var SETTING_EASE_ON_FOCUS = "cameraEaseOnFocus"; -var SETTING_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE = "showLightsAndParticlesInEditMode"; +var SETTING_SHOW_LIGHTS_IN_EDIT_MODE = "showLightsInEditMode"; var SETTING_SHOW_ZONES_IN_EDIT_MODE = "showZonesInEditMode"; @@ -558,7 +506,7 @@ var toolBar = (function () { toolBar.writeProperty("shown", false); toolBar.writeProperty("shown", true); } - entityIconOverlayManager.setVisible(isActive && Menu.isOptionChecked(MENU_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE)); + lightOverlayManager.setVisible(isActive && Menu.isOptionChecked(MENU_SHOW_LIGHTS_IN_EDIT_MODE)); Entities.setDrawZoneBoundaries(isActive && Menu.isOptionChecked(MENU_SHOW_ZONES_IN_EDIT_MODE)); }; @@ -623,8 +571,8 @@ function findClickedEntity(event) { } var entityResult = Entities.findRayIntersection(pickRay, true); // want precision picking - var iconResult = entityIconOverlayManager.findRayIntersection(pickRay); - iconResult.accurate = true; + var lightResult = lightOverlayManager.findRayIntersection(pickRay); + lightResult.accurate = true; if (pickZones) { Entities.setZonesArePickable(false); @@ -632,12 +580,18 @@ function findClickedEntity(event) { var result; - if (iconResult.intersects) { - result = iconResult; - } else if (entityResult.intersects) { - result = entityResult; - } else { + if (!entityResult.intersects && !lightResult.intersects) { return null; + } else if (entityResult.intersects && !lightResult.intersects) { + result = entityResult; + } else if (!entityResult.intersects && lightResult.intersects) { + result = lightResult; + } else { + if (entityResult.distance < lightResult.distance) { + result = entityResult; + } else { + result = lightResult; + } } if (!result.accurate) { @@ -991,18 +945,18 @@ function setupModelMenus() { }); Menu.addMenuItem({ menuName: "Edit", - menuItemName: MENU_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE, + menuItemName: MENU_SHOW_LIGHTS_IN_EDIT_MODE, afterItem: MENU_EASE_ON_FOCUS, isCheckable: true, - isChecked: Settings.getValue(SETTING_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE) !== "false", + isChecked: Settings.getValue(SETTING_SHOW_LIGHTS_IN_EDIT_MODE) === "true", grouping: "Advanced" }); Menu.addMenuItem({ menuName: "Edit", menuItemName: MENU_SHOW_ZONES_IN_EDIT_MODE, - afterItem: MENU_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE, + afterItem: MENU_SHOW_LIGHTS_IN_EDIT_MODE, isCheckable: true, - isChecked: Settings.getValue(SETTING_SHOW_ZONES_IN_EDIT_MODE) !== "false", + isChecked: Settings.getValue(SETTING_SHOW_ZONES_IN_EDIT_MODE) === "true", grouping: "Advanced" }); @@ -1033,7 +987,7 @@ function cleanupModelMenus() { Menu.removeMenuItem("Edit", MENU_AUTO_FOCUS_ON_SELECT); Menu.removeMenuItem("Edit", MENU_EASE_ON_FOCUS); - Menu.removeMenuItem("Edit", MENU_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE); + Menu.removeMenuItem("Edit", MENU_SHOW_LIGHTS_IN_EDIT_MODE); Menu.removeMenuItem("Edit", MENU_SHOW_ZONES_IN_EDIT_MODE); } @@ -1041,7 +995,7 @@ Script.scriptEnding.connect(function () { toolBar.setActive(false); Settings.setValue(SETTING_AUTO_FOCUS_ON_SELECT, Menu.isOptionChecked(MENU_AUTO_FOCUS_ON_SELECT)); Settings.setValue(SETTING_EASE_ON_FOCUS, Menu.isOptionChecked(MENU_EASE_ON_FOCUS)); - Settings.setValue(SETTING_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE, Menu.isOptionChecked(MENU_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE)); + Settings.setValue(SETTING_SHOW_LIGHTS_IN_EDIT_MODE, Menu.isOptionChecked(MENU_SHOW_LIGHTS_IN_EDIT_MODE)); Settings.setValue(SETTING_SHOW_ZONES_IN_EDIT_MODE, Menu.isOptionChecked(MENU_SHOW_ZONES_IN_EDIT_MODE)); progressDialog.cleanup(); @@ -1230,7 +1184,7 @@ function parentSelectedEntities() { } function deleteSelectedEntities() { if (SelectionManager.hasSelection()) { - selectedParticleEntityID = null; + selectedParticleEntity = 0; particleExplorerTool.destroyWebView(); SelectionManager.saveProperties(); var savedProperties = []; @@ -1329,8 +1283,8 @@ function handeMenuEvent(menuItem) { selectAllEtitiesInCurrentSelectionBox(false); } else if (menuItem === "Select All Entities Touching Box") { selectAllEtitiesInCurrentSelectionBox(true); - } else if (menuItem === MENU_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE) { - entityIconOverlayManager.setVisible(isActive && Menu.isOptionChecked(MENU_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE)); + } else if (menuItem === MENU_SHOW_LIGHTS_IN_EDIT_MODE) { + lightOverlayManager.setVisible(isActive && Menu.isOptionChecked(MENU_SHOW_LIGHTS_IN_EDIT_MODE)); } else if (menuItem === MENU_SHOW_ZONES_IN_EDIT_MODE) { Entities.setDrawZoneBoundaries(isActive && Menu.isOptionChecked(MENU_SHOW_ZONES_IN_EDIT_MODE)); } @@ -2005,13 +1959,43 @@ var showMenuItem = propertyMenu.addMenuItem("Show in Marketplace"); var propertiesTool = new PropertiesTool(); var particleExplorerTool = new ParticleExplorerTool(); -var selectedParticleEntityID = null; +var selectedParticleEntity = 0; entityListTool.webView.webEventReceived.connect(function (data) { data = JSON.parse(data); - if (data.type === 'parent') { + if(data.type === 'parent') { parentSelectedEntities(); } else if(data.type === 'unparent') { unparentSelectedEntities(); + } else if (data.type === "selectionUpdate") { + var ids = data.entityIds; + if (ids.length === 1) { + if (Entities.getEntityProperties(ids[0], "type").type === "ParticleEffect") { + if (JSON.stringify(selectedParticleEntity) === JSON.stringify(ids[0])) { + // This particle entity is already selected, so return + return; + } + // Destroy the old particles web view first + particleExplorerTool.destroyWebView(); + particleExplorerTool.createWebView(); + var properties = Entities.getEntityProperties(ids[0]); + var particleData = { + messageType: "particle_settings", + currentProperties: properties + }; + selectedParticleEntity = ids[0]; + particleExplorerTool.setActiveParticleEntity(ids[0]); + + particleExplorerTool.webView.webEventReceived.connect(function (data) { + data = JSON.parse(data); + if (data.messageType === "page_loaded") { + particleExplorerTool.webView.emitScriptEvent(JSON.stringify(particleData)); + } + }); + } else { + selectedParticleEntity = 0; + particleExplorerTool.destroyWebView(); + } + } } }); diff --git a/scripts/system/libraries/entitySelectionTool.js b/scripts/system/libraries/entitySelectionTool.js index a8c4300fbe..d68a525458 100644 --- a/scripts/system/libraries/entitySelectionTool.js +++ b/scripts/system/libraries/entitySelectionTool.js @@ -1032,12 +1032,10 @@ SelectionDisplay = (function() { var pickRay = controllerComputePickRay(); if (pickRay) { var entityIntersection = Entities.findRayIntersection(pickRay, true); - var iconIntersection = entityIconOverlayManager.findRayIntersection(pickRay); - var overlayIntersection = Overlays.findRayIntersection(pickRay); - if (iconIntersection.intersects) { - selectionManager.setSelections([iconIntersection.entityID]); - } else if (entityIntersection.intersects && + + var overlayIntersection = Overlays.findRayIntersection(pickRay); + if (entityIntersection.intersects && (!overlayIntersection.intersects || (entityIntersection.distance < overlayIntersection.distance))) { if (HMD.tabletID === entityIntersection.entityID) { diff --git a/scripts/system/libraries/entityIconOverlayManager.js b/scripts/system/libraries/lightOverlayManager.js similarity index 67% rename from scripts/system/libraries/entityIconOverlayManager.js rename to scripts/system/libraries/lightOverlayManager.js index 7f7a293bc3..2d3618096b 100644 --- a/scripts/system/libraries/entityIconOverlayManager.js +++ b/scripts/system/libraries/lightOverlayManager.js @@ -1,6 +1,9 @@ -/* globals EntityIconOverlayManager:true */ +var POINT_LIGHT_URL = "http://s3.amazonaws.com/hifi-public/images/tools/point-light.svg"; +var SPOT_LIGHT_URL = "http://s3.amazonaws.com/hifi-public/images/tools/spot-light.svg"; + +LightOverlayManager = function() { + var self = this; -EntityIconOverlayManager = function(entityTypes, getOverlayPropertiesFunc) { var visible = false; // List of all created overlays @@ -19,16 +22,9 @@ EntityIconOverlayManager = function(entityTypes, getOverlayPropertiesFunc) { for (var id in entityIDs) { var entityID = entityIDs[id]; var properties = Entities.getEntityProperties(entityID); - var overlayProperties = { + Overlays.editOverlay(entityOverlays[entityID], { position: properties.position - }; - if (getOverlayPropertiesFunc) { - var customProperties = getOverlayPropertiesFunc(entityID, properties); - for (var key in customProperties) { - overlayProperties[key] = customProperties[key]; - } - } - Overlays.editOverlay(entityOverlays[entityID], overlayProperties); + }); } }; @@ -38,7 +34,7 @@ EntityIconOverlayManager = function(entityTypes, getOverlayPropertiesFunc) { if (result.intersects) { for (var id in entityOverlays) { - if (result.overlayID === entityOverlays[id]) { + if (result.overlayID == entityOverlays[id]) { result.entityID = entityIDs[id]; found = true; break; @@ -54,7 +50,7 @@ EntityIconOverlayManager = function(entityTypes, getOverlayPropertiesFunc) { }; this.setVisible = function(isVisible) { - if (visible !== isVisible) { + if (visible != isVisible) { visible = isVisible; for (var id in entityOverlays) { Overlays.editOverlay(entityOverlays[id], { @@ -66,13 +62,12 @@ EntityIconOverlayManager = function(entityTypes, getOverlayPropertiesFunc) { // Allocate or get an unused overlay function getOverlay() { - var overlay; - if (unusedOverlays.length === 0) { - overlay = Overlays.addOverlay("image3d", {}); + if (unusedOverlays.length == 0) { + var overlay = Overlays.addOverlay("image3d", {}); allOverlays.push(overlay); } else { - overlay = unusedOverlays.pop(); - } + var overlay = unusedOverlays.pop(); + }; return overlay; } @@ -84,32 +79,24 @@ EntityIconOverlayManager = function(entityTypes, getOverlayPropertiesFunc) { } function addEntity(entityID) { - var properties = Entities.getEntityProperties(entityID, ['position', 'type']); - if (entityTypes.indexOf(properties.type) > -1 && !(entityID in entityOverlays)) { + var properties = Entities.getEntityProperties(entityID); + if (properties.type == "Light" && !(entityID in entityOverlays)) { var overlay = getOverlay(); entityOverlays[entityID] = overlay; entityIDs[entityID] = entityID; - var overlayProperties = { + Overlays.editOverlay(overlay, { position: properties.position, + url: properties.isSpotlight ? SPOT_LIGHT_URL : POINT_LIGHT_URL, rotation: Quat.fromPitchYawRollDegrees(0, 0, 270), visible: visible, alpha: 0.9, scale: 0.5, - drawInFront: true, - isFacingAvatar: true, color: { red: 255, green: 255, blue: 255 } - }; - if (getOverlayPropertiesFunc) { - var customProperties = getOverlayPropertiesFunc(entityID, properties); - for (var key in customProperties) { - overlayProperties[key] = customProperties[key]; - } - } - Overlays.editOverlay(overlay, overlayProperties); + }); } } @@ -143,4 +130,4 @@ EntityIconOverlayManager = function(entityTypes, getOverlayPropertiesFunc) { Overlays.deleteOverlay(allOverlays[i]); } }); -}; +}; \ No newline at end of file diff --git a/scripts/system/libraries/toolBars.js b/scripts/system/libraries/toolBars.js index 351f10e7bd..e49f8c4004 100644 --- a/scripts/system/libraries/toolBars.js +++ b/scripts/system/libraries/toolBars.js @@ -160,7 +160,6 @@ ToolBar = function(x, y, direction, optionalPersistenceKey, optionalInitialPosit visible: false }); this.spacing = []; - this.onMove = null; this.addTool = function(properties, selectable, selected) { if (direction == ToolBar.HORIZONTAL) { @@ -255,9 +254,6 @@ ToolBar = function(x, y, direction, optionalPersistenceKey, optionalInitialPosit y: y - ToolBar.SPACING }); } - if (this.onMove !== null) { - this.onMove(x, y, dx, dy); - }; } this.setAlpha = function(alpha, tool) { diff --git a/scripts/tutorials/entity_scripts/sit.js b/scripts/tutorials/entity_scripts/sit.js index 82afdc8974..2ba19231e0 100644 --- a/scripts/tutorials/entity_scripts/sit.js +++ b/scripts/tutorials/entity_scripts/sit.js @@ -2,41 +2,31 @@ Script.include("/~/system/libraries/utils.js"); var SETTING_KEY = "com.highfidelity.avatar.isSitting"; + var ROLE = "fly"; var ANIMATION_URL = "https://s3-us-west-1.amazonaws.com/hifi-content/clement/production/animations/sitting_idle.fbx"; var ANIMATION_FPS = 30; var ANIMATION_FIRST_FRAME = 1; var ANIMATION_LAST_FRAME = 10; + var RELEASE_KEYS = ['w', 'a', 's', 'd', 'UP', 'LEFT', 'DOWN', 'RIGHT']; var RELEASE_TIME = 500; // ms var RELEASE_DISTANCE = 0.2; // meters - var MAX_IK_ERROR = 30; - var IK_SETTLE_TIME = 250; // ms - var DESKTOP_UI_CHECK_INTERVAL = 100; + var MAX_IK_ERROR = 20; + var DESKTOP_UI_CHECK_INTERVAL = 250; var DESKTOP_MAX_DISTANCE = 5; - var SIT_DELAY = 25; - var MAX_RESET_DISTANCE = 0.5; // meters - var OVERRIDEN_DRIVE_KEYS = [ - DriveKeys.TRANSLATE_X, - DriveKeys.TRANSLATE_Y, - DriveKeys.TRANSLATE_Z, - DriveKeys.STEP_TRANSLATE_X, - DriveKeys.STEP_TRANSLATE_Y, - DriveKeys.STEP_TRANSLATE_Z, - ]; + var SIT_DELAY = 25 this.entityID = null; + this.timers = {}; this.animStateHandlerID = null; - this.interval = null; - this.sitDownSettlePeriod = null; - this.lastTimeNoDriveKeys = null; this.preload = function(entityID) { this.entityID = entityID; } this.unload = function() { - if (Settings.getValue(SETTING_KEY) === this.entityID) { - this.standUp(); + if (MyAvatar.sessionUUID === this.getSeatUser()) { + this.sitUp(this.entityID); } - if (this.interval !== null) { + if (this.interval) { Script.clearInterval(this.interval); this.interval = null; } @@ -44,60 +34,42 @@ } this.setSeatUser = function(user) { - try { - var userData = Entities.getEntityProperties(this.entityID, ["userData"]).userData; - userData = JSON.parse(userData); + var userData = Entities.getEntityProperties(this.entityID, ["userData"]).userData; + userData = JSON.parse(userData); - if (user !== null) { - userData.seat.user = user; - } else { - delete userData.seat.user; - } - - Entities.editEntity(this.entityID, { - userData: JSON.stringify(userData) - }); - } catch (e) { - // Do Nothing + if (user) { + userData.seat.user = user; + } else { + delete userData.seat.user; } + + Entities.editEntity(this.entityID, { + userData: JSON.stringify(userData) + }); } this.getSeatUser = function() { - try { - var properties = Entities.getEntityProperties(this.entityID, ["userData", "position"]); - var userData = JSON.parse(properties.userData); + var properties = Entities.getEntityProperties(this.entityID, ["userData", "position"]); + var userData = JSON.parse(properties.userData); - // If MyAvatar return my uuid - if (userData.seat.user === MyAvatar.sessionUUID) { - return userData.seat.user; + if (userData.seat.user && userData.seat.user !== MyAvatar.sessionUUID) { + var avatar = AvatarList.getAvatar(userData.seat.user); + if (avatar && Vec3.distance(avatar.position, properties.position) > RELEASE_DISTANCE) { + return null; } - - - // If Avatar appears to be sitting - if (userData.seat.user) { - var avatar = AvatarList.getAvatar(userData.seat.user); - if (avatar && (Vec3.distance(avatar.position, properties.position) < RELEASE_DISTANCE)) { - return userData.seat.user; - } - } - } catch (e) { - // Do nothing } - - // Nobody on the seat - return null; + return userData.seat.user; } - // Is the seat used this.checkSeatForAvatar = function() { var seatUser = this.getSeatUser(); - - // If MyAvatar appears to be sitting - if (seatUser === MyAvatar.sessionUUID) { - var properties = Entities.getEntityProperties(this.entityID, ["position"]); - return Vec3.distance(MyAvatar.position, properties.position) < RELEASE_DISTANCE; + var avatarIdentifiers = AvatarList.getAvatarIdentifiers(); + for (var i in avatarIdentifiers) { + var avatar = AvatarList.getAvatar(avatarIdentifiers[i]); + if (avatar && avatar.sessionUUID === seatUser) { + return true; + } } - - return seatUser !== null; + return false; } this.sitDown = function() { @@ -105,53 +77,41 @@ print("Someone is already sitting in that chair."); return; } - print("Sitting down (" + this.entityID + ")"); - var now = Date.now(); - this.sitDownSettlePeriod = now + IK_SETTLE_TIME; - this.lastTimeNoDriveKeys = now; + this.setSeatUser(MyAvatar.sessionUUID); var previousValue = Settings.getValue(SETTING_KEY); Settings.setValue(SETTING_KEY, this.entityID); - this.setSeatUser(MyAvatar.sessionUUID); if (previousValue === "") { MyAvatar.characterControllerEnabled = false; MyAvatar.hmdLeanRecenterEnabled = false; - var ROLES = MyAvatar.getAnimationRoles(); - for (i in ROLES) { - MyAvatar.overrideRoleAnimation(ROLES[i], ANIMATION_URL, ANIMATION_FPS, true, ANIMATION_FIRST_FRAME, ANIMATION_LAST_FRAME); - } + MyAvatar.overrideRoleAnimation(ROLE, ANIMATION_URL, ANIMATION_FPS, true, ANIMATION_FIRST_FRAME, ANIMATION_LAST_FRAME); MyAvatar.resetSensorsAndBody(); } - var properties = Entities.getEntityProperties(this.entityID, ["position", "rotation"]); - var index = MyAvatar.getJointIndex("Hips"); - MyAvatar.pinJoint(index, properties.position, properties.rotation); + var that = this; + Script.setTimeout(function() { + var properties = Entities.getEntityProperties(that.entityID, ["position", "rotation"]); + var index = MyAvatar.getJointIndex("Hips"); + MyAvatar.pinJoint(index, properties.position, properties.rotation); - this.animStateHandlerID = MyAvatar.addAnimationStateHandler(function(properties) { - return { headType: 0 }; - }, ["headType"]); - Script.update.connect(this, this.update); - for (var i in OVERRIDEN_DRIVE_KEYS) { - MyAvatar.disableDriveKey(OVERRIDEN_DRIVE_KEYS[i]); - } + that.animStateHandlerID = MyAvatar.addAnimationStateHandler(function(properties) { + return { headType: 0 }; + }, ["headType"]); + Script.update.connect(that, that.update); + Controller.keyPressEvent.connect(that, that.keyPressed); + Controller.keyReleaseEvent.connect(that, that.keyReleased); + for (var i in RELEASE_KEYS) { + Controller.captureKeyEvents({ text: RELEASE_KEYS[i] }); + } + }, SIT_DELAY); } - this.standUp = function() { - print("Standing up (" + this.entityID + ")"); - MyAvatar.removeAnimationStateHandler(this.animStateHandlerID); - Script.update.disconnect(this, this.update); - for (var i in OVERRIDEN_DRIVE_KEYS) { - MyAvatar.enableDriveKey(OVERRIDEN_DRIVE_KEYS[i]); - } - + this.sitUp = function() { this.setSeatUser(null); + if (Settings.getValue(SETTING_KEY) === this.entityID) { - Settings.setValue(SETTING_KEY, ""); - var ROLES = MyAvatar.getAnimationRoles(); - for (i in ROLES) { - MyAvatar.restoreRoleAnimation(ROLES[i]); - } + MyAvatar.restoreRoleAnimation(ROLE); MyAvatar.characterControllerEnabled = true; MyAvatar.hmdLeanRecenterEnabled = true; @@ -164,10 +124,19 @@ MyAvatar.bodyPitch = 0.0; MyAvatar.bodyRoll = 0.0; }, SIT_DELAY); + + Settings.setValue(SETTING_KEY, ""); + } + + MyAvatar.removeAnimationStateHandler(this.animStateHandlerID); + Script.update.disconnect(this, this.update); + Controller.keyPressEvent.disconnect(this, this.keyPressed); + Controller.keyReleaseEvent.disconnect(this, this.keyReleased); + for (var i in RELEASE_KEYS) { + Controller.releaseKeyEvents({ text: RELEASE_KEYS[i] }); } } - // function called by teleport.js if it detects the appropriate userData this.sit = function () { this.sitDown(); } @@ -214,52 +183,39 @@ } } + this.update = function(dt) { if (MyAvatar.sessionUUID === this.getSeatUser()) { - var properties = Entities.getEntityProperties(this.entityID); + var properties = Entities.getEntityProperties(this.entityID, ["position"]); var avatarDistance = Vec3.distance(MyAvatar.position, properties.position); var ikError = MyAvatar.getIKErrorOnLastSolve(); - var now = Date.now(); - var shouldStandUp = false; - - // Check if a drive key is pressed - var hasActiveDriveKey = false; - for (var i in OVERRIDEN_DRIVE_KEYS) { - if (MyAvatar.getRawDriveKey(OVERRIDEN_DRIVE_KEYS[i]) != 0.0) { - hasActiveDriveKey = true; - break; - } - } - - // Only standup if user has been pushing a drive key for RELEASE_TIME - if (hasActiveDriveKey) { - var elapsed = now - this.lastTimeNoDriveKeys; - shouldStandUp = elapsed > RELEASE_TIME; - } else { - this.lastTimeNoDriveKeys = Date.now(); - } - - // Allow some time for the IK to settle - if (ikError > MAX_IK_ERROR && now > this.sitDownSettlePeriod) { - shouldStandUp = true; - } - - - if (shouldStandUp || avatarDistance > RELEASE_DISTANCE) { + if (avatarDistance > RELEASE_DISTANCE || ikError > MAX_IK_ERROR) { print("IK error: " + ikError + ", distance from chair: " + avatarDistance); - - // Move avatar in front of the chair to avoid getting stuck in collision hulls - if (avatarDistance < MAX_RESET_DISTANCE) { - var offset = { x: 0, y: 1.0, z: -0.5 - properties.dimensions.z * properties.registrationPoint.z }; - var position = Vec3.sum(properties.position, Vec3.multiplyQbyV(properties.rotation, offset)); - MyAvatar.position = position; - print("Moving Avatar in front of the chair.") - } - - this.standUp(); + this.sitUp(this.entityID); } } } + this.keyPressed = function(event) { + if (isInEditMode()) { + return; + } + + if (RELEASE_KEYS.indexOf(event.text) !== -1) { + var that = this; + this.timers[event.text] = Script.setTimeout(function() { + that.sitUp(); + }, RELEASE_TIME); + } + } + this.keyReleased = function(event) { + if (RELEASE_KEYS.indexOf(event.text) !== -1) { + if (this.timers[event.text]) { + Script.clearTimeout(this.timers[event.text]); + delete this.timers[event.text]; + } + } + } + this.canSitDesktop = function() { var properties = Entities.getEntityProperties(this.entityID, ["position"]); var distanceFromSeat = Vec3.distance(MyAvatar.position, properties.position); @@ -267,7 +223,7 @@ } this.hoverEnterEntity = function(event) { - if (isInEditMode() || this.interval !== null) { + if (isInEditMode() || (MyAvatar.sessionUUID === this.getSeatUser())) { return; } @@ -283,18 +239,18 @@ }, DESKTOP_UI_CHECK_INTERVAL); } this.hoverLeaveEntity = function(event) { - if (this.interval !== null) { + if (this.interval) { Script.clearInterval(this.interval); this.interval = null; } this.cleanupOverlay(); } - this.clickDownOnEntity = function (id, event) { - if (isInEditMode()) { + this.clickDownOnEntity = function () { + if (isInEditMode() || (MyAvatar.sessionUUID === this.getSeatUser())) { return; } - if (event.isPrimaryButton && this.canSitDesktop()) { + if (this.canSitDesktop()) { this.sitDown(); } } diff --git a/tests/ktx/CMakeLists.txt b/tests/ktx/CMakeLists.txt deleted file mode 100644 index d72379efd6..0000000000 --- a/tests/ktx/CMakeLists.txt +++ /dev/null @@ -1,15 +0,0 @@ - -set(TARGET_NAME ktx-test) - -if (WIN32) - SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /ignore:4049 /ignore:4217") -endif() - -# This is not a testcase -- just set it up as a regular hifi project -setup_hifi_project(Quick Gui OpenGL) -set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/") - -# link in the shared libraries -link_hifi_libraries(shared octree ktx gl gpu gpu-gl render model model-networking networking render-utils fbx entities entities-renderer animation audio avatars script-engine physics) - -package_libraries_for_deployment() diff --git a/tests/ktx/src/main.cpp b/tests/ktx/src/main.cpp deleted file mode 100644 index aa6795e17b..0000000000 --- a/tests/ktx/src/main.cpp +++ /dev/null @@ -1,150 +0,0 @@ -// -// Created by Bradley Austin Davis on 2016/07/01 -// Copyright 2014 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include -#include -#include -#include - -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include - - -#include -#include -#include -#include - - -QSharedPointer logger; - -gpu::Texture* cacheTexture(const std::string& name, gpu::Texture* srcTexture, bool write = true, bool read = true); - - -void messageHandler(QtMsgType type, const QMessageLogContext& context, const QString& message) { - QString logMessage = LogHandler::getInstance().printMessage((LogMsgType)type, context, message); - - if (!logMessage.isEmpty()) { -#ifdef Q_OS_WIN - OutputDebugStringA(logMessage.toLocal8Bit().constData()); - OutputDebugStringA("\n"); -#endif - logger->addMessage(qPrintable(logMessage + "\n")); - } -} - -const char * LOG_FILTER_RULES = R"V0G0N( -hifi.gpu=true -)V0G0N"; - -QString getRootPath() { - static std::once_flag once; - static QString result; - std::call_once(once, [&] { - QFileInfo file(__FILE__); - QDir parent = file.absolutePath(); - result = QDir::cleanPath(parent.currentPath() + "/../../.."); - }); - return result; -} - -const QString TEST_IMAGE = getRootPath() + "/scripts/developer/tests/cube_texture.png"; -const QString TEST_IMAGE_KTX = getRootPath() + "/scripts/developer/tests/cube_texture.ktx"; - -int main(int argc, char** argv) { - QApplication app(argc, argv); - QCoreApplication::setApplicationName("KTX"); - QCoreApplication::setOrganizationName("High Fidelity"); - QCoreApplication::setOrganizationDomain("highfidelity.com"); - logger.reset(new FileLogger()); - - Q_ASSERT(sizeof(ktx::Header) == 12 + (sizeof(uint32_t) * 13)); - - DependencyManager::set(); - qInstallMessageHandler(messageHandler); - QLoggingCategory::setFilterRules(LOG_FILTER_RULES); - - QImage image(TEST_IMAGE); - gpu::Texture* testTexture = model::TextureUsage::process2DTextureColorFromImage(image, TEST_IMAGE.toStdString(), true, false, true); - - auto ktxMemory = gpu::Texture::serialize(*testTexture); - { - const auto& ktxStorage = ktxMemory->getStorage(); - QFile outFile(TEST_IMAGE_KTX); - if (!outFile.open(QFile::Truncate | QFile::ReadWrite)) { - throw std::runtime_error("Unable to open file"); - } - auto ktxSize = ktxStorage->size(); - outFile.resize(ktxSize); - auto dest = outFile.map(0, ktxSize); - memcpy(dest, ktxStorage->data(), ktxSize); - outFile.unmap(dest); - outFile.close(); - } - - auto ktxFile = ktx::KTX::create(std::shared_ptr(new storage::FileStorage(TEST_IMAGE_KTX))); - { - const auto& memStorage = ktxMemory->getStorage(); - const auto& fileStorage = ktxFile->getStorage(); - Q_ASSERT(memStorage->size() == fileStorage->size()); - Q_ASSERT(memStorage->data() != fileStorage->data()); - Q_ASSERT(0 == memcmp(memStorage->data(), fileStorage->data(), memStorage->size())); - Q_ASSERT(ktxFile->_images.size() == ktxMemory->_images.size()); - auto imageCount = ktxFile->_images.size(); - auto startMemory = ktxMemory->_storage->data(); - auto startFile = ktxFile->_storage->data(); - for (size_t i = 0; i < imageCount; ++i) { - auto memImages = ktxMemory->_images[i]; - auto fileImages = ktxFile->_images[i]; - Q_ASSERT(memImages._padding == fileImages._padding); - Q_ASSERT(memImages._numFaces == fileImages._numFaces); - Q_ASSERT(memImages._imageSize == fileImages._imageSize); - Q_ASSERT(memImages._faceSize == fileImages._faceSize); - Q_ASSERT(memImages._faceBytes.size() == memImages._numFaces); - Q_ASSERT(fileImages._faceBytes.size() == fileImages._numFaces); - auto faceCount = fileImages._numFaces; - for (uint32_t face = 0; face < faceCount; ++face) { - auto memFace = memImages._faceBytes[face]; - auto memOffset = memFace - startMemory; - auto fileFace = fileImages._faceBytes[face]; - auto fileOffset = fileFace - startFile; - Q_ASSERT(memOffset % 4 == 0); - Q_ASSERT(memOffset == fileOffset); - } - } - } - testTexture->setKtxBacking(ktxFile); - return 0; -} - -#include "main.moc" - diff --git a/tests/render-perf/CMakeLists.txt b/tests/render-perf/CMakeLists.txt index 96cede9c43..d4f90fdace 100644 --- a/tests/render-perf/CMakeLists.txt +++ b/tests/render-perf/CMakeLists.txt @@ -10,7 +10,7 @@ setup_hifi_project(Quick Gui OpenGL) set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/") # link in the shared libraries -link_hifi_libraries(shared octree ktx gl gpu gpu-gl render model model-networking networking render-utils fbx entities entities-renderer animation audio avatars script-engine physics) +link_hifi_libraries(shared octree gl gpu gpu-gl render model model-networking networking render-utils fbx entities entities-renderer animation audio avatars script-engine physics) package_libraries_for_deployment() diff --git a/tests/render-perf/src/main.cpp b/tests/render-perf/src/main.cpp index 522fe79b10..7e9d2c426f 100644 --- a/tests/render-perf/src/main.cpp +++ b/tests/render-perf/src/main.cpp @@ -642,6 +642,7 @@ protected: gpu::Texture::setAllowedGPUMemoryUsage(MB_TO_BYTES(64)); return; + default: break; } diff --git a/tests/render-texture-load/src/main.cpp b/tests/render-texture-load/src/main.cpp index d924f76232..09a420f018 100644 --- a/tests/render-texture-load/src/main.cpp +++ b/tests/render-texture-load/src/main.cpp @@ -48,7 +48,6 @@ #include #include -#include #include #include #include diff --git a/tests/shared/src/StorageTests.cpp b/tests/shared/src/StorageTests.cpp deleted file mode 100644 index fa538f6911..0000000000 --- a/tests/shared/src/StorageTests.cpp +++ /dev/null @@ -1,75 +0,0 @@ -// -// Created by Bradley Austin Davis on 2016/02/17 -// Copyright 2013-2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include "StorageTests.h" - -QTEST_MAIN(StorageTests) - -using namespace storage; - -StorageTests::StorageTests() { - for (size_t i = 0; i < _testData.size(); ++i) { - _testData[i] = (uint8_t)rand(); - } - _testFile = QDir::tempPath() + "/" + QUuid::createUuid().toString(); -} - -StorageTests::~StorageTests() { - QFileInfo fileInfo(_testFile); - if (fileInfo.exists()) { - QFile(_testFile).remove(); - } -} - - -void StorageTests::testConversion() { - { - QFileInfo fileInfo(_testFile); - QCOMPARE(fileInfo.exists(), false); - } - StoragePointer storagePointer = std::make_unique(_testData.size(), _testData.data()); - QCOMPARE(storagePointer->size(), (quint64)_testData.size()); - QCOMPARE(memcmp(_testData.data(), storagePointer->data(), _testData.size()), 0); - // Convert to a file - storagePointer = storagePointer->toFileStorage(_testFile); - { - QFileInfo fileInfo(_testFile); - QCOMPARE(fileInfo.exists(), true); - QCOMPARE(fileInfo.size(), (qint64)_testData.size()); - } - QCOMPARE(storagePointer->size(), (quint64)_testData.size()); - QCOMPARE(memcmp(_testData.data(), storagePointer->data(), _testData.size()), 0); - - // Convert to memory - storagePointer = storagePointer->toMemoryStorage(); - QCOMPARE(storagePointer->size(), (quint64)_testData.size()); - QCOMPARE(memcmp(_testData.data(), storagePointer->data(), _testData.size()), 0); - { - // ensure the file is unaffected - QFileInfo fileInfo(_testFile); - QCOMPARE(fileInfo.exists(), true); - QCOMPARE(fileInfo.size(), (qint64)_testData.size()); - } - - // truncate the data as a new memory object - auto newSize = _testData.size() / 2; - storagePointer = std::make_unique(newSize, storagePointer->data()); - QCOMPARE(storagePointer->size(), (quint64)newSize); - QCOMPARE(memcmp(_testData.data(), storagePointer->data(), newSize), 0); - - // Convert back to file - storagePointer = storagePointer->toFileStorage(_testFile); - QCOMPARE(storagePointer->size(), (quint64)newSize); - QCOMPARE(memcmp(_testData.data(), storagePointer->data(), newSize), 0); - { - // ensure the file is truncated - QFileInfo fileInfo(_testFile); - QCOMPARE(fileInfo.exists(), true); - QCOMPARE(fileInfo.size(), (qint64)newSize); - } -} diff --git a/tests/shared/src/StorageTests.h b/tests/shared/src/StorageTests.h deleted file mode 100644 index 6a2c153223..0000000000 --- a/tests/shared/src/StorageTests.h +++ /dev/null @@ -1,32 +0,0 @@ -// -// Created by Bradley Austin Davis on 2016/02/17 -// Copyright 2013-2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#ifndef hifi_StorageTests_h -#define hifi_StorageTests_h - -#include - -#include -#include - -class StorageTests : public QObject { - Q_OBJECT - -public: - StorageTests(); - ~StorageTests(); - -private slots: - void testConversion(); - -private: - std::array _testData; - QString _testFile; -}; - -#endif // hifi_StorageTests_h diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index 8dc993e6fe..a85a112bf5 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -17,5 +17,3 @@ set_target_properties(ac-client PROPERTIES FOLDER "Tools") add_subdirectory(skeleton-dump) set_target_properties(skeleton-dump PROPERTIES FOLDER "Tools") -add_subdirectory(atp-get) -set_target_properties(atp-get PROPERTIES FOLDER "Tools") diff --git a/tools/atp-get/CMakeLists.txt b/tools/atp-get/CMakeLists.txt deleted file mode 100644 index b1646dc023..0000000000 --- a/tools/atp-get/CMakeLists.txt +++ /dev/null @@ -1,3 +0,0 @@ -set(TARGET_NAME atp-get) -setup_hifi_project(Core Widgets) -link_hifi_libraries(shared networking) diff --git a/tools/atp-get/src/ATPGetApp.cpp b/tools/atp-get/src/ATPGetApp.cpp deleted file mode 100644 index 30054fffea..0000000000 --- a/tools/atp-get/src/ATPGetApp.cpp +++ /dev/null @@ -1,269 +0,0 @@ -// -// ATPGetApp.cpp -// tools/atp-get/src -// -// Created by Seth Alves on 2017-3-15 -// Copyright 2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "ATPGetApp.h" - -ATPGetApp::ATPGetApp(int argc, char* argv[]) : - QCoreApplication(argc, argv) -{ - // parse command-line - QCommandLineParser parser; - parser.setApplicationDescription("High Fidelity ATP-Get"); - - const QCommandLineOption helpOption = parser.addHelpOption(); - - const QCommandLineOption verboseOutput("v", "verbose output"); - parser.addOption(verboseOutput); - - const QCommandLineOption domainAddressOption("d", "domain-server address", "127.0.0.1"); - parser.addOption(domainAddressOption); - - const QCommandLineOption cacheSTUNOption("s", "cache stun-server response"); - parser.addOption(cacheSTUNOption); - - const QCommandLineOption listenPortOption("listenPort", "listen port", QString::number(INVALID_PORT)); - parser.addOption(listenPortOption); - - - if (!parser.parse(QCoreApplication::arguments())) { - qCritical() << parser.errorText() << endl; - parser.showHelp(); - Q_UNREACHABLE(); - } - - if (parser.isSet(helpOption)) { - parser.showHelp(); - Q_UNREACHABLE(); - } - - _verbose = parser.isSet(verboseOutput); - if (!_verbose) { - QLoggingCategory::setFilterRules("qt.network.ssl.warning=false"); - - const_cast(&networking())->setEnabled(QtDebugMsg, false); - const_cast(&networking())->setEnabled(QtInfoMsg, false); - const_cast(&networking())->setEnabled(QtWarningMsg, false); - - const_cast(&shared())->setEnabled(QtDebugMsg, false); - const_cast(&shared())->setEnabled(QtInfoMsg, false); - const_cast(&shared())->setEnabled(QtWarningMsg, false); - } - - - QStringList filenames = parser.positionalArguments(); - if (filenames.empty() || filenames.size() > 2) { - qDebug() << "give remote url and optional local filename as arguments"; - parser.showHelp(); - Q_UNREACHABLE(); - } - - _url = QUrl(filenames[0]); - if (_url.scheme() != "atp") { - qDebug() << "url should start with atp:"; - parser.showHelp(); - Q_UNREACHABLE(); - } - - if (filenames.size() == 2) { - _localOutputFile = filenames[1]; - } - - QString domainServerAddress = "127.0.0.1:40103"; - if (parser.isSet(domainAddressOption)) { - domainServerAddress = parser.value(domainAddressOption); - } - - if (_verbose) { - qDebug() << "domain-server address is" << domainServerAddress; - } - - int listenPort = INVALID_PORT; - if (parser.isSet(listenPortOption)) { - listenPort = parser.value(listenPortOption).toInt(); - } - - Setting::init(); - DependencyManager::registerInheritance(); - - DependencyManager::set([&]{ return QString("Mozilla/5.0 (HighFidelityATPGet)"); }); - DependencyManager::set(); - DependencyManager::set(NodeType::Agent, listenPort); - - - auto nodeList = DependencyManager::get(); - - // start the nodeThread so its event loop is running - QThread* nodeThread = new QThread(this); - nodeThread->setObjectName("NodeList Thread"); - nodeThread->start(); - - // make sure the node thread is given highest priority - nodeThread->setPriority(QThread::TimeCriticalPriority); - - // setup a timer for domain-server check ins - QTimer* domainCheckInTimer = new QTimer(nodeList.data()); - connect(domainCheckInTimer, &QTimer::timeout, nodeList.data(), &NodeList::sendDomainServerCheckIn); - domainCheckInTimer->start(DOMAIN_SERVER_CHECK_IN_MSECS); - - // put the NodeList and datagram processing on the node thread - nodeList->moveToThread(nodeThread); - - const DomainHandler& domainHandler = nodeList->getDomainHandler(); - - connect(&domainHandler, SIGNAL(hostnameChanged(const QString&)), SLOT(domainChanged(const QString&))); - // connect(&domainHandler, SIGNAL(resetting()), SLOT(resettingDomain())); - // connect(&domainHandler, SIGNAL(disconnectedFromDomain()), SLOT(clearDomainOctreeDetails())); - connect(&domainHandler, &DomainHandler::domainConnectionRefused, this, &ATPGetApp::domainConnectionRefused); - - connect(nodeList.data(), &NodeList::nodeAdded, this, &ATPGetApp::nodeAdded); - connect(nodeList.data(), &NodeList::nodeKilled, this, &ATPGetApp::nodeKilled); - connect(nodeList.data(), &NodeList::nodeActivated, this, &ATPGetApp::nodeActivated); - // connect(nodeList.data(), &NodeList::uuidChanged, getMyAvatar(), &MyAvatar::setSessionUUID); - // connect(nodeList.data(), &NodeList::uuidChanged, this, &ATPGetApp::setSessionUUID); - connect(nodeList.data(), &NodeList::packetVersionMismatch, this, &ATPGetApp::notifyPacketVersionMismatch); - - nodeList->addSetOfNodeTypesToNodeInterestSet(NodeSet() << NodeType::AudioMixer << NodeType::AvatarMixer - << NodeType::EntityServer << NodeType::AssetServer << NodeType::MessagesMixer); - - DependencyManager::get()->handleLookupString(domainServerAddress, false); - - auto assetClient = DependencyManager::set(); - assetClient->init(); - - QTimer* doTimer = new QTimer(this); - doTimer->setSingleShot(true); - connect(doTimer, &QTimer::timeout, this, &ATPGetApp::timedOut); - doTimer->start(4000); -} - -ATPGetApp::~ATPGetApp() { -} - - -void ATPGetApp::domainConnectionRefused(const QString& reasonMessage, int reasonCodeInt, const QString& extraInfo) { - qDebug() << "domainConnectionRefused"; -} - -void ATPGetApp::domainChanged(const QString& domainHostname) { - if (_verbose) { - qDebug() << "domainChanged"; - } -} - -void ATPGetApp::nodeAdded(SharedNodePointer node) { - if (_verbose) { - qDebug() << "node added: " << node->getType(); - } -} - -void ATPGetApp::nodeActivated(SharedNodePointer node) { - if (node->getType() == NodeType::AssetServer) { - lookup(); - } -} - -void ATPGetApp::nodeKilled(SharedNodePointer node) { - qDebug() << "nodeKilled"; -} - -void ATPGetApp::timedOut() { - finish(1); -} - -void ATPGetApp::notifyPacketVersionMismatch() { - if (_verbose) { - qDebug() << "packet version mismatch"; - } - finish(1); -} - -void ATPGetApp::lookup() { - - auto path = _url.path(); - qDebug() << "path is " << path; - - auto request = DependencyManager::get()->createGetMappingRequest(path); - QObject::connect(request, &GetMappingRequest::finished, this, [=](GetMappingRequest* request) mutable { - auto result = request->getError(); - if (result == GetMappingRequest::NotFound) { - qDebug() << "not found"; - } else if (result == GetMappingRequest::NoError) { - qDebug() << "found, hash is " << request->getHash(); - download(request->getHash()); - } else { - qDebug() << "error -- " << request->getError() << " -- " << request->getErrorString(); - } - request->deleteLater(); - }); - request->start(); -} - -void ATPGetApp::download(AssetHash hash) { - auto assetClient = DependencyManager::get(); - auto assetRequest = new AssetRequest(hash); - - connect(assetRequest, &AssetRequest::finished, this, [this](AssetRequest* request) mutable { - Q_ASSERT(request->getState() == AssetRequest::Finished); - - if (request->getError() == AssetRequest::Error::NoError) { - QString data = QString::fromUtf8(request->getData()); - if (_localOutputFile == "") { - QTextStream cout(stdout); - cout << data; - } else { - QFile outputHandle(_localOutputFile); - if (outputHandle.open(QIODevice::ReadWrite)) { - QTextStream stream( &outputHandle ); - stream << data; - } else { - qDebug() << "couldn't open output file:" << _localOutputFile; - } - } - } - - request->deleteLater(); - finish(0); - }); - - assetRequest->start(); -} - -void ATPGetApp::finish(int exitCode) { - auto nodeList = DependencyManager::get(); - - // send the domain a disconnect packet, force stoppage of domain-server check-ins - nodeList->getDomainHandler().disconnect(); - nodeList->setIsShuttingDown(true); - - // tell the packet receiver we're shutting down, so it can drop packets - nodeList->getPacketReceiver().setShouldDropPackets(true); - - QThread* nodeThread = DependencyManager::get()->thread(); - // remove the NodeList from the DependencyManager - DependencyManager::destroy(); - // ask the node thread to quit and wait until it is done - nodeThread->quit(); - nodeThread->wait(); - - QCoreApplication::exit(exitCode); -} diff --git a/tools/atp-get/src/ATPGetApp.h b/tools/atp-get/src/ATPGetApp.h deleted file mode 100644 index 5507d2aa62..0000000000 --- a/tools/atp-get/src/ATPGetApp.h +++ /dev/null @@ -1,52 +0,0 @@ -// -// ATPGetApp.h -// tools/atp-get/src -// -// Created by Seth Alves on 2017-3-15 -// Copyright 2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - - -#ifndef hifi_ATPGetApp_h -#define hifi_ATPGetApp_h - -#include -#include -#include -#include -#include -#include -#include -#include - - -class ATPGetApp : public QCoreApplication { - Q_OBJECT -public: - ATPGetApp(int argc, char* argv[]); - ~ATPGetApp(); - -private slots: - void domainConnectionRefused(const QString& reasonMessage, int reasonCodeInt, const QString& extraInfo); - void domainChanged(const QString& domainHostname); - void nodeAdded(SharedNodePointer node); - void nodeActivated(SharedNodePointer node); - void nodeKilled(SharedNodePointer node); - void notifyPacketVersionMismatch(); - -private: - NodeList* _nodeList; - void timedOut(); - void lookup(); - void download(AssetHash hash); - void finish(int exitCode); - bool _verbose; - - QUrl _url; - QString _localOutputFile; -}; - -#endif // hifi_ATPGetApp_h diff --git a/tools/atp-get/src/main.cpp b/tools/atp-get/src/main.cpp deleted file mode 100644 index bddf30c666..0000000000 --- a/tools/atp-get/src/main.cpp +++ /dev/null @@ -1,31 +0,0 @@ -// -// main.cpp -// tools/atp-get/src -// -// Created by Seth Alves on 2017-3-15 -// Copyright 2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html - -#include -#include -#include -#include - -#include - -#include "ATPGetApp.h" - -using namespace std; - -int main(int argc, char * argv[]) { - QCoreApplication::setApplicationName(BuildInfo::AC_CLIENT_SERVER_NAME); - QCoreApplication::setOrganizationName(BuildInfo::MODIFIED_ORGANIZATION); - QCoreApplication::setOrganizationDomain(BuildInfo::ORGANIZATION_DOMAIN); - QCoreApplication::setApplicationVersion(BuildInfo::VERSION); - - ATPGetApp app(argc, argv); - - return app.exec(); -} diff --git a/unpublishedScripts/marketplace/boppo/boppoClownEntity.js b/unpublishedScripts/marketplace/boppo/boppoClownEntity.js deleted file mode 100644 index 36f2bf5ab0..0000000000 --- a/unpublishedScripts/marketplace/boppo/boppoClownEntity.js +++ /dev/null @@ -1,80 +0,0 @@ -// -// boppoClownEntity.js -// -// Created by Thijs Wenker on 3/15/17. -// Copyright 2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -/* globals LookAtTarget */ - -(function() { - var SFX_PREFIX = 'https://hifi-content.s3-us-west-1.amazonaws.com/caitlyn/production/elBoppo/sfx/'; - var CHANNEL_PREFIX = 'io.highfidelity.boppo_server_'; - var PUNCH_SOUNDS = [ - 'punch_1.wav', - 'punch_2.wav' - ]; - var PUNCH_COOLDOWN = 300; - - Script.include('lookAtEntity.js'); - - var createBoppoClownEntity = function() { - var _this, - _entityID, - _boppoUserData, - _lookAtTarget, - _punchSounds = [], - _lastPlayedPunch = {}; - - var getOwnBoppoUserData = function() { - try { - return JSON.parse(Entities.getEntityProperties(_entityID, ['userData']).userData).Boppo; - } catch (e) { - // e - } - return {}; - }; - - var BoppoClownEntity = function () { - _this = this; - PUNCH_SOUNDS.forEach(function(punch) { - _punchSounds.push(SoundCache.getSound(SFX_PREFIX + punch)); - }); - }; - - BoppoClownEntity.prototype = { - preload: function(entityID) { - _entityID = entityID; - _boppoUserData = getOwnBoppoUserData(); - _lookAtTarget = new LookAtTarget(_entityID); - }, - collisionWithEntity: function(boppoEntity, collidingEntity, collisionInfo) { - if (collisionInfo.type === 0 && - Entities.getEntityProperties(collidingEntity, ['name']).name.indexOf('Boxing Glove ') === 0) { - - if (_lastPlayedPunch[collidingEntity] === undefined || - Date.now() - _lastPlayedPunch[collidingEntity] > PUNCH_COOLDOWN) { - - // If boxing glove detected here: - Messages.sendMessage(CHANNEL_PREFIX + _boppoUserData.gameParentID, 'hit'); - - _lookAtTarget.lookAtByAction(); - var randomPunchIndex = Math.floor(Math.random() * _punchSounds.length); - Audio.playSound(_punchSounds[randomPunchIndex], { - position: collisionInfo.contactPoint - }); - _lastPlayedPunch[collidingEntity] = Date.now(); - } - } - } - - }; - - return new BoppoClownEntity(); - }; - - return createBoppoClownEntity(); -}); diff --git a/unpublishedScripts/marketplace/boppo/boppoServer.js b/unpublishedScripts/marketplace/boppo/boppoServer.js deleted file mode 100644 index f03154573c..0000000000 --- a/unpublishedScripts/marketplace/boppo/boppoServer.js +++ /dev/null @@ -1,303 +0,0 @@ -// -// boppoServer.js -// -// Created by Thijs Wenker on 3/15/17. -// Copyright 2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -(function() { - var SFX_PREFIX = 'https://hifi-content.s3-us-west-1.amazonaws.com/caitlyn/production/elBoppo/sfx/'; - var CLOWN_LAUGHS = [ - 'clown_laugh_1.wav', - 'clown_laugh_2.wav', - 'clown_laugh_3.wav', - 'clown_laugh_4.wav' - ]; - var TICK_TOCK_SOUND = 'ticktock%20-%20tock.wav'; - var BOXING_RING_BELL_START = 'boxingRingBell.wav'; - var BOXING_RING_BELL_END = 'boxingRingBell-end.wav'; - var BOPPO_MUSIC = 'boppoMusic.wav'; - var CHANNEL_PREFIX = 'io.highfidelity.boppo_server_'; - var MESSAGE_HIT = 'hit'; - var MESSAGE_ENTER_ZONE = 'enter-zone'; - var MESSAGE_UNLOAD_FIX = 'unload-fix'; - - var DEFAULT_SOUND_VOLUME = 0.6; - - // don't set the search radius too high, it might remove boppo's from other nearby instances - var BOPPO_SEARCH_RADIUS = 4.0; - - var MILLISECONDS_PER_SECOND = 1000; - // Make sure the entities are loaded at startup (TODO: more solid fix) - var LOAD_TIMEOUT = 5000; - var SECONDS_PER_MINUTE = 60; - var DEFAULT_PLAYTIME = 30; // seconds - var BASE_TEN = 10; - var TICK_TOCK_FROM = 3; // seconds - var COOLDOWN_TIME_MS = MILLISECONDS_PER_SECOND * 3; - - var createBoppoServer = function() { - var _this, - _isInitialized = false, - _clownLaughs = [], - _musicInjector, - _music, - _laughingInjector, - _tickTockSound, - _boxingBellRingStart, - _boxingBellRingEnd, - _entityID, - _boppoClownID, - _channel, - _boppoEntities, - _isGameRunning, - _updateInterval, - _timeLeft, - _hits, - _coolDown; - - var getOwnBoppoUserData = function() { - try { - return JSON.parse(Entities.getEntityProperties(_entityID, ['userData']).userData).Boppo; - } catch (e) { - // e - } - return {}; - }; - - var updateBoppoEntities = function() { - Entities.getChildrenIDs(_entityID).forEach(function(entityID) { - try { - var userData = JSON.parse(Entities.getEntityProperties(entityID, ['userData']).userData); - if (userData.Boppo.type !== undefined) { - _boppoEntities[userData.Boppo.type] = entityID; - } - } catch (e) { - // e - } - }); - }; - - var clearUntrackedBoppos = function() { - var position = Entities.getEntityProperties(_entityID, ['position']).position; - Entities.findEntities(position, BOPPO_SEARCH_RADIUS).forEach(function(entityID) { - try { - if (JSON.parse(Entities.getEntityProperties(entityID, ['userData']).userData).Boppo.type === 'boppo') { - Entities.deleteEntity(entityID); - } - } catch (e) { - // e - } - }); - }; - - var updateTimerDisplay = function() { - if (_boppoEntities['timer']) { - var secondsString = _timeLeft % SECONDS_PER_MINUTE; - if (secondsString < BASE_TEN) { - secondsString = '0' + secondsString; - } - var minutesString = Math.floor(_timeLeft / SECONDS_PER_MINUTE); - Entities.editEntity(_boppoEntities['timer'], { - text: minutesString + ':' + secondsString - }); - } - }; - - var updateScoreDisplay = function() { - if (_boppoEntities['score']) { - Entities.editEntity(_boppoEntities['score'], { - text: 'SCORE: ' + _hits - }); - } - }; - - var playSoundAtBoxingRing = function(sound, properties) { - var _properties = properties ? properties : {}; - if (_properties['volume'] === undefined) { - _properties['volume'] = DEFAULT_SOUND_VOLUME; - } - _properties['position'] = Entities.getEntityProperties(_entityID, ['position']).position; - // play beep - return Audio.playSound(sound, _properties); - }; - - var onUpdate = function() { - _timeLeft--; - - if (_timeLeft > 0 && _timeLeft <= TICK_TOCK_FROM) { - // play beep - playSoundAtBoxingRing(_tickTockSound); - } - if (_timeLeft === 0) { - if (_musicInjector !== undefined && _musicInjector.isPlaying()) { - _musicInjector.stop(); - _musicInjector = undefined; - } - playSoundAtBoxingRing(_boxingBellRingEnd); - _isGameRunning = false; - Script.clearInterval(_updateInterval); - _updateInterval = null; - _coolDown = true; - Script.setTimeout(function() { - _coolDown = false; - _this.resetBoppo(); - }, COOLDOWN_TIME_MS); - } - updateTimerDisplay(); - }; - - var onMessage = function(channel, message, sender) { - if (channel === _channel) { - if (message === MESSAGE_HIT) { - _this.hit(); - } else if (message === MESSAGE_ENTER_ZONE && !_isGameRunning) { - _this.resetBoppo(); - } else if (message === MESSAGE_UNLOAD_FIX && _isInitialized) { - _this.unload(); - } - } - }; - - var BoppoServer = function () { - _this = this; - _hits = 0; - _boppoClownID = null; - _coolDown = false; - CLOWN_LAUGHS.forEach(function(clownLaugh) { - _clownLaughs.push(SoundCache.getSound(SFX_PREFIX + clownLaugh)); - }); - _tickTockSound = SoundCache.getSound(SFX_PREFIX + TICK_TOCK_SOUND); - _boxingBellRingStart = SoundCache.getSound(SFX_PREFIX + BOXING_RING_BELL_START); - _boxingBellRingEnd = SoundCache.getSound(SFX_PREFIX + BOXING_RING_BELL_END); - _music = SoundCache.getSound(SFX_PREFIX + BOPPO_MUSIC); - _boppoEntities = {}; - }; - - BoppoServer.prototype = { - preload: function(entityID) { - _entityID = entityID; - _channel = CHANNEL_PREFIX + entityID; - - Messages.sendLocalMessage(_channel, MESSAGE_UNLOAD_FIX); - Script.setTimeout(function() { - clearUntrackedBoppos(); - updateBoppoEntities(); - Messages.subscribe(_channel); - Messages.messageReceived.connect(onMessage); - _this.resetBoppo(); - _isInitialized = true; - }, LOAD_TIMEOUT); - }, - resetBoppo: function() { - if (_boppoClownID !== null) { - print('deleting boppo: ' + _boppoClownID); - Entities.deleteEntity(_boppoClownID); - } - var boppoBaseProperties = Entities.getEntityProperties(_entityID, ['position', 'rotation']); - _boppoClownID = Entities.addEntity({ - angularDamping: 0.0, - collisionSoundURL: 'https://hifi-content.s3.amazonaws.com/caitlyn/production/elBoppo/51460__andre-rocha-nascimento__basket-ball-01-bounce.wav', - collisionsWillMove: true, - compoundShapeURL: 'https://hifi-content.s3.amazonaws.com/caitlyn/production/elBoppo/bopo_phys.obj', - damping: 1.0, - density: 10000, - dimensions: { - x: 1.2668079137802124, - y: 2.0568051338195801, - z: 0.88563752174377441 - }, - dynamic: 1.0, - friction: 1.0, - gravity: { - x: 0, - y: -25, - z: 0 - }, - modelURL: 'https://hifi-content.s3.amazonaws.com/caitlyn/production/elBoppo/elBoppo3_VR.fbx', - name: 'El Boppo the Punching Bag Clown', - registrationPoint: { - x: 0.5, - y: 0, - z: 0.3 - }, - restitution: 0.99, - rotation: boppoBaseProperties.rotation, - position: Vec3.sum(boppoBaseProperties.position, - Vec3.multiplyQbyV(boppoBaseProperties.rotation, { - x: 0.08666179329156876, - y: -1.5698202848434448, - z: 0.1847127377986908 - })), - script: Script.resolvePath('boppoClownEntity.js'), - shapeType: 'compound', - type: 'Model', - userData: JSON.stringify({ - lookAt: { - targetID: _boppoEntities['lookAtThis'], - disablePitch: true, - disableYaw: false, - disableRoll: true, - clearDisabledAxis: true, - rotationOffset: { x: 0.0, y: 180.0, z: 0.0} - }, - Boppo: { - type: 'boppo', - gameParentID: _entityID - }, - grabbableKey: { - grabbable: false - } - }) - }); - updateBoppoEntities(); - _boppoEntities['boppo'] = _boppoClownID; - }, - laugh: function() { - if (_laughingInjector !== undefined && _laughingInjector.isPlaying()) { - return; - } - var randomLaughIndex = Math.floor(Math.random() * _clownLaughs.length); - _laughingInjector = Audio.playSound(_clownLaughs[randomLaughIndex], { - position: Entities.getEntityProperties(_boppoClownID, ['position']).position - }); - }, - hit: function() { - if (_coolDown) { - return; - } - if (!_isGameRunning) { - var boxingRingBoppoData = getOwnBoppoUserData(); - _updateInterval = Script.setInterval(onUpdate, MILLISECONDS_PER_SECOND); - _timeLeft = boxingRingBoppoData.playTimeSeconds ? parseInt(boxingRingBoppoData.playTimeSeconds) : - DEFAULT_PLAYTIME; - _isGameRunning = true; - _hits = 0; - playSoundAtBoxingRing(_boxingBellRingStart); - _musicInjector = playSoundAtBoxingRing(_music, {loop: true, volume: 0.6}); - } - _hits++; - updateTimerDisplay(); - updateScoreDisplay(); - _this.laugh(); - }, - unload: function() { - print('unload called'); - if (_updateInterval) { - Script.clearInterval(_updateInterval); - } - Messages.messageReceived.disconnect(onMessage); - Messages.unsubscribe(_channel); - Entities.deleteEntity(_boppoClownID); - print('endOfUnload'); - } - }; - - return new BoppoServer(); - }; - - return createBoppoServer(); -}); diff --git a/unpublishedScripts/marketplace/boppo/clownGloveDispenser.js b/unpublishedScripts/marketplace/boppo/clownGloveDispenser.js deleted file mode 100644 index cd0a0c0614..0000000000 --- a/unpublishedScripts/marketplace/boppo/clownGloveDispenser.js +++ /dev/null @@ -1,154 +0,0 @@ -// -// clownGloveDispenser.js -// -// Created by Thijs Wenker on 8/2/16. -// Copyright 2016 High Fidelity, Inc. -// -// Based on examples/winterSmashUp/targetPractice/shooterPlatform.js -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -(function() { - var _this = this; - - var CHANNEL_PREFIX = 'io.highfidelity.boppo_server_'; - - var leftBoxingGlove = undefined; - var rightBoxingGlove = undefined; - - var inZone = false; - - var wearGloves = function() { - leftBoxingGlove = Entities.addEntity({ - position: MyAvatar.position, - collisionsWillMove: true, - dimensions: { - x: 0.24890634417533875, - y: 0.28214839100837708, - z: 0.21127720177173615 - }, - dynamic: true, - gravity: { - x: 0, - y: -9.8, - z: 0 - }, - modelURL: "https://hifi-content.s3.amazonaws.com/caitlyn/production/elBoppo/LFT_glove_VR3.fbx", - name: "Boxing Glove - Left", - registrationPoint: { - x: 0.5, - y: 0, - z: 0.5 - }, - shapeType: "simple-hull", - type: "Model", - userData: JSON.stringify({ - grabbableKey: { - invertSolidWhileHeld: true - }, - wearable: { - joints: { - LeftHand: [ - {x: 0, y: 0.0, z: 0.02 }, - Quat.fromVec3Degrees({x: 0, y: 0, z: 0}) - ] - } - } - }) - }); - Messages.sendLocalMessage('Hifi-Hand-Grab', JSON.stringify({hand: 'left', entityID: leftBoxingGlove})); - // Allows teleporting while glove is wielded - Messages.sendLocalMessage('Hifi-Teleport-Ignore-Add', leftBoxingGlove); - - rightBoxingGlove = Entities.addEntity({ - position: MyAvatar.position, - collisionsWillMove: true, - dimensions: { - x: 0.24890634417533875, - y: 0.28214839100837708, - z: 0.21127720177173615 - }, - dynamic: true, - gravity: { - x: 0, - y: -9.8, - z: 0 - }, - modelURL: "https://hifi-content.s3.amazonaws.com/caitlyn/production/elBoppo/RT_glove_VR2.fbx", - name: "Boxing Glove - Right", - registrationPoint: { - x: 0.5, - y: 0, - z: 0.5 - }, - shapeType: "simple-hull", - type: "Model", - userData: JSON.stringify({ - grabbableKey: { - invertSolidWhileHeld: true - }, - wearable: { - joints: { - RightHand: [ - {x: 0, y: 0.0, z: 0.02 }, - Quat.fromVec3Degrees({x: 0, y: 0, z: 0}) - ] - } - } - }) - }); - Messages.sendLocalMessage('Hifi-Hand-Grab', JSON.stringify({hand: 'right', entityID: rightBoxingGlove})); - // Allows teleporting while glove is wielded - Messages.sendLocalMessage('Hifi-Teleport-Ignore-Add', rightBoxingGlove); - }; - - var cleanUpGloves = function() { - if (leftBoxingGlove !== undefined) { - Entities.deleteEntity(leftBoxingGlove); - leftBoxingGlove = undefined; - } - if (rightBoxingGlove !== undefined) { - Entities.deleteEntity(rightBoxingGlove); - rightBoxingGlove = undefined; - } - }; - - var wearGlovesIfHMD = function() { - // cleanup your old gloves if they're still there (unlikely) - cleanUpGloves(); - if (HMD.active) { - wearGloves(); - } - }; - - _this.preload = function(entityID) { - HMD.displayModeChanged.connect(function() { - if (inZone) { - wearGlovesIfHMD(); - } - }); - }; - - _this.unload = function() { - cleanUpGloves(); - }; - - _this.enterEntity = function(entityID) { - inZone = true; - print('entered boxing glove dispenser entity'); - wearGlovesIfHMD(); - - // Reset boppo if game is not running: - var parentID = Entities.getEntityProperties(entityID, ['parentID']).parentID; - Messages.sendMessage(CHANNEL_PREFIX + parentID, 'enter-zone'); - }; - - _this.leaveEntity = function(entityID) { - inZone = false; - cleanUpGloves(); - }; - - _this.unload = _this.leaveEntity; -}); diff --git a/unpublishedScripts/marketplace/boppo/createElBoppo.js b/unpublishedScripts/marketplace/boppo/createElBoppo.js deleted file mode 100644 index 4df6a2acda..0000000000 --- a/unpublishedScripts/marketplace/boppo/createElBoppo.js +++ /dev/null @@ -1,430 +0,0 @@ -// -// createElBoppo.js -// -// Created by Thijs Wenker on 3/17/17. -// Copyright 2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -/* globals SCRIPT_IMPORT_PROPERTIES */ - -var MODELS_PATH = 'https://hifi-content.s3.amazonaws.com/DomainContent/Welcome%20Area/production/models/boxingRing/'; -var WANT_CLEANUP_ON_SCRIPT_ENDING = false; - -var getScriptPath = function(localPath) { - if (this.isCleanupAndSpawnScript) { - return 'https://hifi-content.s3.amazonaws.com/DomainContent/Welcome%20Area/Scripts/boppo/' + localPath; - } - return Script.resolvePath(localPath); -}; - -var getCreatePosition = function() { - // can either return position defined by resetScript or avatar position - if (this.isCleanupAndSpawnScript) { - return SCRIPT_IMPORT_PROPERTIES.rootPosition; - } - return Vec3.sum(MyAvatar.position, {x: 1, z: -2}); -}; - -var boxingRing = Entities.addEntity({ - dimensions: { - x: 4.0584001541137695, - y: 4.0418000221252441, - z: 3.0490000247955322 - }, - modelURL: MODELS_PATH + 'assembled/boppoBoxingRingAssembly.fbx', - name: 'Boxing Ring Assembly', - rotation: { - w: 0.9996337890625, - x: -1.52587890625e-05, - y: -0.026230275630950928, - z: -4.57763671875e-05 - }, - position: getCreatePosition(), - scriptTimestamp: 1489612158459, - serverScripts: getScriptPath('boppoServer.js'), - shapeType: 'static-mesh', - type: 'Model', - userData: JSON.stringify({ - Boppo: { - type: 'boxingring', - playTimeSeconds: 15 - } - }) -}); - -var boppoEntities = [ - { - dimensions: { - x: 0.36947935819625854, - y: 0.25536194443702698, - z: 0.059455446898937225 - }, - modelURL: MODELS_PATH + 'boxingGameSign/boppoSignFrame.fbx', - parentID: boxingRing, - localPosition: { - x: -1.0251024961471558, - y: 0.51661628484725952, - z: -1.1176263093948364 - }, - rotation: { - w: 0.996856689453125, - x: 0.013321161270141602, - y: 0.0024566650390625, - z: 0.078049898147583008 - }, - shapeType: 'box', - type: 'Model' - }, - { - dimensions: { - x: 0.33255371451377869, - y: 0.1812121719121933, - z: 0.0099999997764825821 - }, - lineHeight: 0.125, - name: 'Boxing Ring - High Score Board', - parentID: boxingRing, - localPosition: { - x: -1.0239436626434326, - y: 0.52212876081466675, - z: -1.0971509218215942 - }, - rotation: { - w: 0.9876401424407959, - x: 0.013046503067016602, - y: 0.0012359619140625, - z: 0.15605401992797852 - }, - text: '0:00', - textColor: { - blue: 0, - green: 0, - red: 255 - }, - type: 'Text', - userData: JSON.stringify({ - Boppo: { - type: 'timer' - } - }) - }, - { - dimensions: { - x: 0.50491130352020264, - y: 0.13274604082107544, - z: 0.0099999997764825821 - }, - lineHeight: 0.090000003576278687, - name: 'Boxing Ring - Score Board', - parentID: boxingRing, - localPosition: { - x: -0.77596306800842285, - y: 0.37797555327415466, - z: -1.0910623073577881 - }, - rotation: { - w: 0.9518122673034668, - x: 0.004237703513354063, - y: -0.0010041374480351806, - z: 0.30455198884010315 - }, - text: 'SCORE: 0', - textColor: { - blue: 0, - green: 0, - red: 255 - }, - type: 'Text', - userData: JSON.stringify({ - Boppo: { - type: 'score' - } - }) - }, - { - dimensions: { - x: 0.58153259754180908, - y: 0.1884911060333252, - z: 0.059455446898937225 - }, - modelURL: MODELS_PATH + 'boxingGameSign/boppoSignFrame.fbx', - parentID: boxingRing, - localPosition: { - x: -0.78200173377990723, - y: 0.35684797167778015, - z: -1.108180046081543 - }, - rotation: { - w: 0.97814905643463135, - x: 0.0040436983108520508, - y: -0.0005645751953125, - z: 0.20778214931488037 - }, - shapeType: 'box', - type: 'Model' - }, - { - dimensions: { - x: 4.1867804527282715, - y: 3.5065803527832031, - z: 5.6845207214355469 - }, - name: 'El Boppo the Clown boxing area & glove maker', - parentID: boxingRing, - localPosition: { - x: -0.012308252975344658, - y: 0.054641719907522202, - z: 0.98782551288604736 - }, - rotation: { - w: 1, - x: -1.52587890625e-05, - y: -1.52587890625e-05, - z: -1.52587890625e-05 - }, - script: getScriptPath('clownGloveDispenser.js'), - shapeType: 'box', - type: 'Zone', - visible: false - }, - { - color: { - blue: 255, - green: 5, - red: 255 - }, - dimensions: { - x: 0.20000000298023224, - y: 0.20000000298023224, - z: 0.20000000298023224 - }, - name: 'LookAtBox', - parentID: boxingRing, - localPosition: { - x: -0.1772226095199585, - y: -1.7072629928588867, - z: 1.3122396469116211 - }, - rotation: { - w: 0.999969482421875, - x: 1.52587890625e-05, - y: 0.0043793916702270508, - z: 1.52587890625e-05 - }, - shape: 'Cube', - type: 'Box', - userData: JSON.stringify({ - Boppo: { - type: 'lookAtThis' - } - }) - }, - { - color: { - blue: 209, - green: 157, - red: 209 - }, - dimensions: { - x: 1.6913000345230103, - y: 1.2124500274658203, - z: 0.2572999894618988 - }, - name: 'boppoBackBoard', - parentID: boxingRing, - localPosition: { - x: -0.19500596821308136, - y: -1.1044719219207764, - z: -0.55993378162384033 - }, - rotation: { - w: 0.9807126522064209, - x: -0.19511711597442627, - y: 0.0085297822952270508, - z: 0.0016937255859375 - }, - shape: 'Cube', - type: 'Box', - visible: false - }, - { - color: { - blue: 0, - green: 0, - red: 255 - }, - dimensions: { - x: 1.8155574798583984, - y: 0.92306196689605713, - z: 0.51203572750091553 - }, - name: 'boppoBackBoard', - parentID: boxingRing, - localPosition: { - x: -0.11036647111177444, - y: -0.051978692412376404, - z: -0.79054081439971924 - }, - rotation: { - w: 0.9807431697845459, - x: 0.19505608081817627, - y: 0.0085602998733520508, - z: -0.0017547607421875 - }, - shape: 'Cube', - type: 'Box', - visible: false - }, - { - color: { - blue: 209, - green: 157, - red: 209 - }, - dimensions: { - x: 1.9941408634185791, - y: 1.2124500274658203, - z: 0.2572999894618988 - }, - name: 'boppoBackBoard', - localPosition: { - x: 0.69560068845748901, - y: -1.3840068578720093, - z: 0.059689953923225403 - }, - rotation: { - w: 0.73458456993103027, - x: -0.24113833904266357, - y: -0.56545358896255493, - z: -0.28734266757965088 - }, - shape: 'Cube', - type: 'Box', - visible: false - }, - { - color: { - blue: 82, - green: 82, - red: 82 - }, - dimensions: { - x: 8.3777303695678711, - y: 0.87573593854904175, - z: 7.9759469032287598 - }, - parentID: boxingRing, - localPosition: { - x: -0.38302639126777649, - y: -2.121284008026123, - z: 0.3699878454208374 - }, - rotation: { - w: 0.70711839199066162, - x: -7.62939453125e-05, - y: 0.70705735683441162, - z: -1.52587890625e-05 - }, - shape: 'Triangle', - type: 'Shape' - }, - { - color: { - blue: 209, - green: 157, - red: 209 - }, - dimensions: { - x: 1.889795184135437, - y: 0.86068248748779297, - z: 0.2572999894618988 - }, - name: 'boppoBackBoard', - parentID: boxingRing, - localPosition: { - x: -0.95167744159698486, - y: -1.4756947755813599, - z: -0.042313352227210999 - }, - rotation: { - w: 0.74004733562469482, - x: -0.24461740255355835, - y: 0.56044864654541016, - z: 0.27998781204223633 - }, - shape: 'Cube', - type: 'Box', - visible: false - }, - { - color: { - blue: 0, - green: 0, - red: 255 - }, - dimensions: { - x: 4.0720257759094238, - y: 0.50657749176025391, - z: 1.4769613742828369 - }, - name: 'boppo-stepsRamp', - parentID: boxingRing, - localPosition: { - x: -0.002939039608463645, - y: -1.9770187139511108, - z: 2.2165381908416748 - }, - rotation: { - w: 0.99252307415008545, - x: 0.12184333801269531, - y: -1.52587890625e-05, - z: -1.52587890625e-05 - }, - shape: 'Cube', - type: 'Box', - visible: false - }, - { - color: { - blue: 150, - green: 150, - red: 150 - }, - cutoff: 90, - dimensions: { - x: 5.2220535278320312, - y: 5.2220535278320312, - z: 5.2220535278320312 - }, - falloffRadius: 2, - intensity: 15, - name: 'boxing ring light', - parentID: boxingRing, - localPosition: { - x: -1.4094564914703369, - y: -0.36021926999092102, - z: 0.81797939538955688 - }, - rotation: { - w: 0.9807431697845459, - x: 1.52587890625e-05, - y: -0.19520866870880127, - z: -1.52587890625e-05 - }, - type: 'Light' - } -]; - -boppoEntities.forEach(function(entityProperties) { - entityProperties['parentID'] = boxingRing; - Entities.addEntity(entityProperties); -}); - -if (WANT_CLEANUP_ON_SCRIPT_ENDING) { - Script.scriptEnding.connect(function() { - Entities.deleteEntity(boxingRing); - }); -} diff --git a/unpublishedScripts/marketplace/boppo/lookAtEntity.js b/unpublishedScripts/marketplace/boppo/lookAtEntity.js deleted file mode 100644 index ba072814f2..0000000000 --- a/unpublishedScripts/marketplace/boppo/lookAtEntity.js +++ /dev/null @@ -1,98 +0,0 @@ -// -// lookAtTarget.js -// -// Created by Thijs Wenker on 3/15/17. -// Copyright 2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -/* globals LookAtTarget:true */ - -LookAtTarget = function(sourceEntityID) { - /* private variables */ - var _this, - _options, - _sourceEntityID, - _sourceEntityProperties, - REQUIRED_PROPERTIES = ['position', 'rotation', 'userData'], - LOOK_AT_TAG = 'lookAtTarget'; - - LookAtTarget = function(sourceEntityID) { - _this = this; - _sourceEntityID = sourceEntityID; - _this.updateOptions(); - }; - - /* private functions */ - var updateEntitySourceProperties = function() { - _sourceEntityProperties = Entities.getEntityProperties(_sourceEntityID, REQUIRED_PROPERTIES); - }; - - var getUpdatedActionProperties = function() { - return { - targetRotation: _this.getLookAtRotation(), - angularTimeScale: 0.1, - ttl: 10 - }; - }; - - var getNewActionProperties = function() { - var newActionProperties = getUpdatedActionProperties(); - newActionProperties.tag = LOOK_AT_TAG; - return newActionProperties; - }; - - LookAtTarget.prototype = { - /* public functions */ - updateOptions: function() { - updateEntitySourceProperties(); - _options = JSON.parse(_sourceEntityProperties.userData).lookAt; - }, - getTargetPosition: function() { - return Entities.getEntityProperties(_options.targetID).position; - }, - getLookAtRotation: function() { - _this.updateOptions(); - - var newRotation = Quat.lookAt(_sourceEntityProperties.position, _this.getTargetPosition(), Vec3.UP); - if (_options.rotationOffset !== undefined) { - newRotation = Quat.multiply(newRotation, Quat.fromVec3Degrees(_options.rotationOffset)); - } - if (_options.disablePitch || _options.disableYaw || _options.disablePitch) { - var disabledAxis = _options.clearDisabledAxis ? Vec3.ZERO : - Quat.safeEulerAngles(_sourceEntityProperties.rotation); - var newEulers = Quat.safeEulerAngles(newRotation); - newRotation = Quat.fromVec3Degrees({ - x: _options.disablePitch ? disabledAxis.x : newEulers.x, - y: _options.disableYaw ? disabledAxis.y : newEulers.y, - z: _options.disableRoll ? disabledAxis.z : newEulers.z - }); - } - return newRotation; - }, - lookAtDirectly: function() { - Entities.editEntity(_sourceEntityID, {rotation: _this.getLookAtRotation()}); - }, - lookAtByAction: function() { - var actionIDs = Entities.getActionIDs(_sourceEntityID); - var actionFound = false; - actionIDs.forEach(function(actionID) { - if (actionFound) { - return; - } - var actionArguments = Entities.getActionArguments(_sourceEntityID, actionID); - if (actionArguments.tag === LOOK_AT_TAG) { - actionFound = true; - Entities.updateAction(_sourceEntityID, actionID, getUpdatedActionProperties()); - } - }); - if (!actionFound) { - Entities.addAction('spring', _sourceEntityID, getNewActionProperties()); - } - } - }; - - return new LookAtTarget(sourceEntityID); -}; From 056d6fbe4f384386f3c35d0f142a1cf9a6dd532a Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Fri, 24 Mar 2017 10:10:17 -0700 Subject: [PATCH 049/118] Actually merge from master --- BUILD_WIN.md | 145 +-- assignment-client/src/Agent.cpp | 33 +- assignment-client/src/Agent.h | 8 + assignment-client/src/assets/AssetServer.cpp | 4 +- assignment-client/src/octree/OctreeServer.cpp | 9 +- .../src/scripts/EntityScriptServer.cpp | 30 +- .../PackageLibrariesForDeployment.cmake | 28 +- .../SymlinkOrCopyDirectoryBesideTarget.cmake | 6 +- domain-server/src/DomainServer.cpp | 4 +- .../src/DomainServerSettingsManager.cpp | 37 +- interface/CMakeLists.txt | 10 +- interface/resources/controllers/standard.json | 44 +- interface/resources/html/img/devices.png | Bin 7492 -> 0 bytes interface/resources/html/img/models.png | Bin 8664 -> 0 bytes interface/resources/html/img/move.png | Bin 6121 -> 0 bytes interface/resources/html/img/run-script.png | Bin 4873 -> 0 bytes interface/resources/html/img/talk.png | Bin 2611 -> 0 bytes interface/resources/html/img/write-script.png | Bin 2006 -> 0 bytes .../resources/html/interface-welcome.html | 187 --- interface/resources/icons/load-script.svg | 125 -- interface/resources/icons/new-script.svg | 129 -- interface/resources/icons/save-script.svg | 674 ----------- interface/resources/icons/start-script.svg | 550 --------- interface/resources/icons/stop-script.svg | 163 --- interface/resources/qml/AssetServer.qml | 2 +- interface/resources/qml/AvatarInputs.qml | 57 +- interface/resources/qml/Stats.qml | 12 +- interface/resources/styles/log_dialog.qss | 4 +- interface/src/Application.cpp | 224 ++-- interface/src/Application.h | 7 +- interface/src/Menu.cpp | 21 +- interface/src/Menu.h | 6 +- interface/src/avatar/Avatar.h | 1 - interface/src/avatar/AvatarManager.cpp | 2 +- interface/src/avatar/AvatarManager.h | 2 +- .../src/avatar/CauterizedMeshPartPayload.cpp | 53 +- .../src/avatar/CauterizedMeshPartPayload.h | 7 +- interface/src/avatar/CauterizedModel.cpp | 44 +- interface/src/avatar/Head.cpp | 4 +- interface/src/avatar/Head.h | 6 +- interface/src/avatar/MyAvatar.cpp | 115 +- interface/src/avatar/MyAvatar.h | 56 +- interface/src/ui/ApplicationOverlay.cpp | 49 +- interface/src/ui/ApplicationOverlay.h | 3 - interface/src/ui/AvatarInputs.cpp | 20 - interface/src/ui/AvatarInputs.h | 6 - interface/src/ui/BaseLogDialog.cpp | 48 +- interface/src/ui/BaseLogDialog.h | 4 +- interface/src/ui/CachesSizeDialog.cpp | 84 -- interface/src/ui/CachesSizeDialog.h | 45 - interface/src/ui/DialogsManager.cpp | 24 - interface/src/ui/DialogsManager.h | 6 - interface/src/ui/DiskCacheEditor.cpp | 146 --- interface/src/ui/DiskCacheEditor.h | 49 - interface/src/ui/ScriptEditBox.cpp | 111 -- interface/src/ui/ScriptEditBox.h | 38 - interface/src/ui/ScriptEditorWidget.cpp | 256 ---- interface/src/ui/ScriptEditorWidget.h | 64 - interface/src/ui/ScriptEditorWindow.cpp | 259 ----- interface/src/ui/ScriptEditorWindow.h | 64 - interface/src/ui/ScriptLineNumberArea.cpp | 28 - interface/src/ui/ScriptLineNumberArea.h | 32 - interface/src/ui/ScriptsTableWidget.cpp | 49 - interface/src/ui/ScriptsTableWidget.h | 28 - interface/src/ui/Stats.cpp | 10 +- interface/src/ui/Stats.h | 8 +- interface/src/ui/overlays/Overlays.cpp | 4 +- interface/src/ui/overlays/Web3DOverlay.cpp | 2 +- interface/ui/scriptEditorWidget.ui | 142 --- interface/ui/scriptEditorWindow.ui | 324 ------ libraries/animation/src/Rig.cpp | 8 +- libraries/animation/src/Rig.h | 2 +- libraries/audio-client/src/AudioClient.cpp | 244 ++-- libraries/audio-client/src/AudioClient.h | 16 +- libraries/avatars/src/HeadData.cpp | 4 +- .../display-plugins/OpenGLDisplayPlugin.cpp | 6 +- .../display-plugins/hmd/HmdDisplayPlugin.cpp | 35 +- .../src/EntityTreeRenderer.cpp | 5 +- .../src/RenderablePolyVoxEntityItem.cpp | 78 +- .../src/RenderablePolyVoxEntityItem.h | 9 +- .../src/RenderableShapeEntityItem.cpp | 13 +- .../src/RenderableWebEntityItem.cpp | 2 +- .../src/EntitiesScriptEngineProvider.h | 4 +- libraries/entities/src/EntityItem.cpp | 8 +- .../entities/src/EntityItemProperties.cpp | 21 - libraries/entities/src/EntityItemProperties.h | 4 - .../entities/src/EntityScriptingInterface.cpp | 176 ++- .../entities/src/EntityScriptingInterface.h | 54 +- libraries/entities/src/PolyVoxEntityItem.cpp | 4 + libraries/entities/src/PolyVoxEntityItem.h | 6 +- libraries/entities/src/PropertyGroup.h | 29 +- libraries/fbx/src/FBXReader.cpp | 19 +- libraries/fbx/src/FBXReader.h | 20 - libraries/fbx/src/FBXReader_Node.cpp | 3 +- libraries/fbx/src/OBJReader.cpp | 10 +- libraries/fbx/src/OBJReader.h | 2 +- libraries/fbx/src/OBJWriter.cpp | 148 +++ libraries/fbx/src/OBJWriter.h | 26 + libraries/gpu-gl/src/gpu/gl/GLBackend.cpp | 11 +- libraries/gpu-gl/src/gpu/gl/GLBackend.h | 13 +- .../gpu-gl/src/gpu/gl/GLBackendTexture.cpp | 54 +- libraries/gpu-gl/src/gpu/gl/GLFramebuffer.cpp | 9 +- libraries/gpu-gl/src/gpu/gl/GLFramebuffer.h | 2 +- libraries/gpu-gl/src/gpu/gl/GLTexelFormat.cpp | 5 + libraries/gpu-gl/src/gpu/gl/GLTexture.cpp | 233 +--- libraries/gpu-gl/src/gpu/gl/GLTexture.h | 207 +--- .../gpu-gl/src/gpu/gl/GLTextureTransfer.cpp | 208 ---- .../gpu-gl/src/gpu/gl/GLTextureTransfer.h | 78 -- libraries/gpu-gl/src/gpu/gl41/GL41Backend.h | 33 +- .../gpu-gl/src/gpu/gl41/GL41BackendOutput.cpp | 10 +- .../src/gpu/gl41/GL41BackendTexture.cpp | 192 +-- libraries/gpu-gl/src/gpu/gl45/GL45Backend.cpp | 11 +- libraries/gpu-gl/src/gpu/gl45/GL45Backend.h | 254 +++- .../gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp | 10 +- .../src/gpu/gl45/GL45BackendTexture.cpp | 536 ++------- .../gpu/gl45/GL45BackendVariableTexture.cpp | 1033 +++++++++++++++++ libraries/gpu/CMakeLists.txt | 2 +- libraries/gpu/src/gpu/Batch.cpp | 7 - libraries/gpu/src/gpu/Buffer.h | 2 +- libraries/gpu/src/gpu/Context.cpp | 17 + libraries/gpu/src/gpu/Context.h | 4 + libraries/gpu/src/gpu/Format.cpp | 7 + libraries/gpu/src/gpu/Format.h | 6 + libraries/gpu/src/gpu/Framebuffer.cpp | 12 +- libraries/gpu/src/gpu/Texture.cpp | 225 ++-- libraries/gpu/src/gpu/Texture.h | 178 ++- libraries/gpu/src/gpu/Texture_ktx.cpp | 289 +++++ libraries/ktx/CMakeLists.txt | 3 + libraries/ktx/src/ktx/KTX.cpp | 165 +++ libraries/ktx/src/ktx/KTX.h | 494 ++++++++ libraries/ktx/src/ktx/Reader.cpp | 195 ++++ libraries/ktx/src/ktx/Writer.cpp | 171 +++ libraries/model-networking/CMakeLists.txt | 2 +- .../src/model-networking/KTXCache.cpp | 47 + .../src/model-networking/KTXCache.h | 51 + .../src/model-networking/TextureCache.cpp | 492 +++++--- .../src/model-networking/TextureCache.h | 26 +- libraries/model/CMakeLists.txt | 2 +- libraries/model/src/model/Geometry.cpp | 112 +- libraries/model/src/model/Geometry.h | 14 +- libraries/model/src/model/TextureMap.cpp | 149 ++- libraries/model/src/model/TextureMap.h | 3 +- libraries/networking/src/Assignment.cpp | 1 - libraries/networking/src/FileCache.cpp | 243 ++++ libraries/networking/src/FileCache.h | 158 +++ libraries/networking/src/NodePermissions.h | 48 +- libraries/networking/src/udt/PacketQueue.cpp | 23 +- libraries/networking/src/udt/PacketQueue.h | 5 +- libraries/octree/src/OctreeQuery.cpp | 2 +- libraries/physics/src/EntityMotionState.cpp | 24 +- libraries/physics/src/EntityMotionState.h | 1 + .../physics/src/PhysicalEntitySimulation.cpp | 18 +- .../physics/src/PhysicalEntitySimulation.h | 5 +- libraries/physics/src/PhysicsEngine.cpp | 2 +- libraries/physics/src/PhysicsEngine.h | 3 +- .../physics/src/ThreadSafeDynamicsWorld.cpp | 27 +- .../physics/src/ThreadSafeDynamicsWorld.h | 4 + libraries/recording/src/recording/Deck.cpp | 5 +- libraries/render-utils/CMakeLists.txt | 2 +- .../render-utils/src/AntialiasingEffect.cpp | 2 +- .../render-utils/src/DeferredFramebuffer.cpp | 10 +- .../src/DeferredLightingEffect.cpp | 4 +- .../render-utils/src/FramebufferCache.cpp | 16 - libraries/render-utils/src/FramebufferCache.h | 5 - libraries/render-utils/src/LightAmbient.slh | 13 +- libraries/render-utils/src/LightStage.cpp | 4 +- libraries/render-utils/src/LightingModel.cpp | 10 + libraries/render-utils/src/LightingModel.h | 12 +- libraries/render-utils/src/LightingModel.slh | 9 +- .../render-utils/src/MaterialTextures.slh | 2 +- .../render-utils/src/MeshPartPayload.cpp | 26 +- libraries/render-utils/src/MeshPartPayload.h | 6 +- libraries/render-utils/src/Model.cpp | 98 +- libraries/render-utils/src/Model.h | 10 +- .../render-utils/src/RenderDeferredTask.cpp | 27 +- .../render-utils/src/RenderPipelines.cpp | 2 +- .../render-utils/src/SubsurfaceScattering.cpp | 6 +- .../render-utils/src/SurfaceGeometryPass.cpp | 12 +- libraries/render-utils/src/text/Font.cpp | 3 +- libraries/render/CMakeLists.txt | 2 +- libraries/render/src/render/DrawTask.cpp | 12 +- libraries/render/src/render/DrawTask.h | 4 +- libraries/render/src/render/ShapePipeline.h | 8 +- .../render/src/render/drawItemStatus.slv | 4 +- .../src/AudioScriptingInterface.cpp | 5 - .../src/AudioScriptingInterface.h | 9 +- .../script-engine/src/BaseScriptEngine.h | 67 -- libraries/script-engine/src/Mat4.cpp | 2 +- libraries/script-engine/src/Mat4.h | 4 +- libraries/script-engine/src/MeshProxy.h | 41 + .../src/ModelScriptingInterface.cpp | 159 +++ .../src/ModelScriptingInterface.h | 45 + libraries/script-engine/src/Quat.cpp | 2 +- libraries/script-engine/src/Quat.h | 4 +- libraries/script-engine/src/ScriptEngine.cpp | 561 ++++++++- libraries/script-engine/src/ScriptEngine.h | 37 +- .../script-engine/src/ScriptEngineLogging.cpp | 1 + .../script-engine/src/ScriptEngineLogging.h | 1 + libraries/script-engine/src/ScriptEngines.cpp | 20 +- libraries/script-engine/src/ScriptEngines.h | 10 +- .../src/BaseScriptEngine.cpp | 130 ++- libraries/shared/src/BaseScriptEngine.h | 90 ++ libraries/shared/src/GLMHelpers.h | 2 +- libraries/shared/src/HifiConfigVariantMap.cpp | 6 +- libraries/shared/src/PathUtils.cpp | 22 +- libraries/shared/src/PathUtils.h | 7 +- libraries/shared/src/RenderArgs.h | 1 + libraries/shared/src/ServerPathUtils.cpp | 31 - libraries/shared/src/ServerPathUtils.h | 22 - libraries/shared/src/ViewFrustum.cpp | 2 +- libraries/shared/src/ViewFrustum.h | 2 +- libraries/shared/src/shared/Storage.cpp | 92 ++ libraries/shared/src/shared/Storage.h | 82 ++ libraries/ui/src/ui/Menu.cpp | 2 +- .../src/OculusLegacyDisplayPlugin.cpp | 2 +- plugins/openvr/src/OpenVrDisplayPlugin.cpp | 8 +- .../developer/libraries/jasmine/hifi-boot.js | 13 +- scripts/developer/tests/.gitignore | 1 + scripts/developer/tests/ambientSoundTest.js | 2 +- .../tests/basicEntityTest/entitySpawner.js | 2 +- .../batonSoundTestEntitySpawner.js | 2 +- .../tests/entityServerStampedeTest.js | 2 +- scripts/developer/tests/entityStampedeTest.js | 2 +- scripts/developer/tests/lodTest.js | 2 +- scripts/developer/tests/mat4test.js | 8 +- .../developer/tests/performance/tribbles.js | 2 +- .../rapidProceduralChangeTest.js | 4 +- scripts/developer/tests/scaling.png | Bin 0 -> 3172 bytes scripts/developer/tests/sphereLODTest.js | 2 +- scripts/developer/tests/testInterval.js | 2 +- .../tests/unit_tests/entityUnitTests.js | 2 +- .../tests/unit_tests/moduleTests/cycles/a.js | 10 + .../tests/unit_tests/moduleTests/cycles/b.js | 10 + .../unit_tests/moduleTests/cycles/main.js | 17 + .../entity/entityConstructorAPIException.js | 13 + .../entity/entityConstructorModule.js | 23 + .../entity/entityConstructorNested.js | 14 + .../entity/entityConstructorNested2.js | 25 + .../entityConstructorRequireException.js | 10 + .../entity/entityPreloadAPIError.js | 13 + .../entity/entityPreloadRequire.js | 11 + .../tests/unit_tests/moduleTests/example.json | 9 + .../moduleTests/exceptions/exception.js | 4 + .../exceptions/exceptionInFunction.js | 38 + .../tests/unit_tests/moduleUnitTests.js | 378 ++++++ .../developer/tests/unit_tests/package.json | 6 + .../tests/unit_tests/scriptUnitTests.js | 18 +- scripts/developer/tests/viveTouchpadTest.js | 8 +- .../developer/utilities/record/recorder.js | 97 +- .../utilities/render/deferredLighting.qml | 3 +- .../utilities/render/photobooth/photobooth.js | 9 +- scripts/modules/vec3.js | 69 ++ .../system/assets/images/icon-particles.svg | 29 + .../system/assets/images/icon-point-light.svg | 57 + .../system/assets/images/icon-spot-light.svg | 37 + scripts/system/away.js | 4 +- scripts/system/controllers/grab.js | 4 +- .../system/controllers/handControllerGrab.js | 2 +- .../controllers/handControllerPointer.js | 2 +- scripts/system/controllers/teleport.js | 19 +- ...oggleAdvancedMovementForHandControllers.js | 13 +- scripts/system/edit.js | 144 ++- scripts/system/libraries/WebTablet.js | 6 +- scripts/system/libraries/entityCameraTool.js | 4 +- ...Manager.js => entityIconOverlayManager.js} | 51 +- .../system/libraries/entitySelectionTool.js | 10 +- scripts/system/libraries/soundArray.js | 2 +- scripts/system/libraries/toolBars.js | 4 + scripts/system/nameTag.js | 4 +- scripts/system/pal.js | 6 +- scripts/system/voxels.js | 2 +- scripts/tutorials/NBody/makePlanets.js | 2 +- scripts/tutorials/butterflies.js | 6 +- scripts/tutorials/createCow.js | 2 +- scripts/tutorials/createDice.js | 4 +- scripts/tutorials/createFlashlight.js | 2 +- scripts/tutorials/createGolfClub.js | 2 +- scripts/tutorials/createPictureFrame.js | 2 +- scripts/tutorials/createPingPongGun.js | 2 +- scripts/tutorials/createPistol.js | 2 +- scripts/tutorials/createSoundMaker.js | 2 +- scripts/tutorials/entity_scripts/golfClub.js | 4 +- .../tutorials/entity_scripts/pingPongGun.js | 14 +- scripts/tutorials/entity_scripts/pistol.js | 2 +- scripts/tutorials/entity_scripts/sit.js | 230 ++-- scripts/tutorials/makeBlocks.js | 8 +- tests/ktx/CMakeLists.txt | 15 + tests/ktx/src/main.cpp | 150 +++ tests/render-perf/CMakeLists.txt | 2 +- tests/render-perf/src/Camera.hpp | 6 +- tests/render-perf/src/main.cpp | 1 - tests/render-texture-load/src/main.cpp | 1 + tests/shared/src/StorageTests.cpp | 75 ++ tests/shared/src/StorageTests.h | 32 + tools/CMakeLists.txt | 2 + tools/atp-get/CMakeLists.txt | 3 + tools/atp-get/src/ATPGetApp.cpp | 269 +++++ tools/atp-get/src/ATPGetApp.h | 52 + tools/atp-get/src/main.cpp | 31 + .../marketplace/boppo/boppoClownEntity.js | 80 ++ .../marketplace/boppo/boppoServer.js | 303 +++++ .../marketplace/boppo/clownGloveDispenser.js | 154 +++ .../marketplace/boppo/createElBoppo.js | 430 +++++++ .../marketplace/boppo/lookAtEntity.js | 98 ++ 304 files changed, 9862 insertions(+), 6946 deletions(-) delete mode 100644 interface/resources/html/img/devices.png delete mode 100644 interface/resources/html/img/models.png delete mode 100644 interface/resources/html/img/move.png delete mode 100644 interface/resources/html/img/run-script.png delete mode 100644 interface/resources/html/img/talk.png delete mode 100644 interface/resources/html/img/write-script.png delete mode 100644 interface/resources/html/interface-welcome.html delete mode 100644 interface/resources/icons/load-script.svg delete mode 100644 interface/resources/icons/new-script.svg delete mode 100644 interface/resources/icons/save-script.svg delete mode 100644 interface/resources/icons/start-script.svg delete mode 100644 interface/resources/icons/stop-script.svg delete mode 100644 interface/src/ui/CachesSizeDialog.cpp delete mode 100644 interface/src/ui/CachesSizeDialog.h delete mode 100644 interface/src/ui/DiskCacheEditor.cpp delete mode 100644 interface/src/ui/DiskCacheEditor.h delete mode 100644 interface/src/ui/ScriptEditBox.cpp delete mode 100644 interface/src/ui/ScriptEditBox.h delete mode 100644 interface/src/ui/ScriptEditorWidget.cpp delete mode 100644 interface/src/ui/ScriptEditorWidget.h delete mode 100644 interface/src/ui/ScriptEditorWindow.cpp delete mode 100644 interface/src/ui/ScriptEditorWindow.h delete mode 100644 interface/src/ui/ScriptLineNumberArea.cpp delete mode 100644 interface/src/ui/ScriptLineNumberArea.h delete mode 100644 interface/src/ui/ScriptsTableWidget.cpp delete mode 100644 interface/src/ui/ScriptsTableWidget.h delete mode 100644 interface/ui/scriptEditorWidget.ui delete mode 100644 interface/ui/scriptEditorWindow.ui create mode 100644 libraries/fbx/src/OBJWriter.cpp create mode 100644 libraries/fbx/src/OBJWriter.h delete mode 100644 libraries/gpu-gl/src/gpu/gl/GLTextureTransfer.cpp delete mode 100644 libraries/gpu-gl/src/gpu/gl/GLTextureTransfer.h create mode 100644 libraries/gpu-gl/src/gpu/gl45/GL45BackendVariableTexture.cpp create mode 100644 libraries/gpu/src/gpu/Texture_ktx.cpp create mode 100644 libraries/ktx/CMakeLists.txt create mode 100644 libraries/ktx/src/ktx/KTX.cpp create mode 100644 libraries/ktx/src/ktx/KTX.h create mode 100644 libraries/ktx/src/ktx/Reader.cpp create mode 100644 libraries/ktx/src/ktx/Writer.cpp create mode 100644 libraries/model-networking/src/model-networking/KTXCache.cpp create mode 100644 libraries/model-networking/src/model-networking/KTXCache.h create mode 100644 libraries/networking/src/FileCache.cpp create mode 100644 libraries/networking/src/FileCache.h delete mode 100644 libraries/script-engine/src/BaseScriptEngine.h create mode 100644 libraries/script-engine/src/MeshProxy.h create mode 100644 libraries/script-engine/src/ModelScriptingInterface.cpp create mode 100644 libraries/script-engine/src/ModelScriptingInterface.h rename libraries/{script-engine => shared}/src/BaseScriptEngine.cpp (68%) create mode 100644 libraries/shared/src/BaseScriptEngine.h delete mode 100644 libraries/shared/src/ServerPathUtils.cpp delete mode 100644 libraries/shared/src/ServerPathUtils.h create mode 100644 libraries/shared/src/shared/Storage.cpp create mode 100644 libraries/shared/src/shared/Storage.h create mode 100644 scripts/developer/tests/.gitignore create mode 100644 scripts/developer/tests/scaling.png create mode 100644 scripts/developer/tests/unit_tests/moduleTests/cycles/a.js create mode 100644 scripts/developer/tests/unit_tests/moduleTests/cycles/b.js create mode 100644 scripts/developer/tests/unit_tests/moduleTests/cycles/main.js create mode 100644 scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorAPIException.js create mode 100644 scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorModule.js create mode 100644 scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested.js create mode 100644 scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested2.js create mode 100644 scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorRequireException.js create mode 100644 scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadAPIError.js create mode 100644 scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadRequire.js create mode 100644 scripts/developer/tests/unit_tests/moduleTests/example.json create mode 100644 scripts/developer/tests/unit_tests/moduleTests/exceptions/exception.js create mode 100644 scripts/developer/tests/unit_tests/moduleTests/exceptions/exceptionInFunction.js create mode 100644 scripts/developer/tests/unit_tests/moduleUnitTests.js create mode 100644 scripts/developer/tests/unit_tests/package.json create mode 100644 scripts/modules/vec3.js create mode 100644 scripts/system/assets/images/icon-particles.svg create mode 100644 scripts/system/assets/images/icon-point-light.svg create mode 100644 scripts/system/assets/images/icon-spot-light.svg rename scripts/system/libraries/{lightOverlayManager.js => entityIconOverlayManager.js} (67%) create mode 100644 tests/ktx/CMakeLists.txt create mode 100644 tests/ktx/src/main.cpp create mode 100644 tests/shared/src/StorageTests.cpp create mode 100644 tests/shared/src/StorageTests.h create mode 100644 tools/atp-get/CMakeLists.txt create mode 100644 tools/atp-get/src/ATPGetApp.cpp create mode 100644 tools/atp-get/src/ATPGetApp.h create mode 100644 tools/atp-get/src/main.cpp create mode 100644 unpublishedScripts/marketplace/boppo/boppoClownEntity.js create mode 100644 unpublishedScripts/marketplace/boppo/boppoServer.js create mode 100644 unpublishedScripts/marketplace/boppo/clownGloveDispenser.js create mode 100644 unpublishedScripts/marketplace/boppo/createElBoppo.js create mode 100644 unpublishedScripts/marketplace/boppo/lookAtEntity.js diff --git a/BUILD_WIN.md b/BUILD_WIN.md index 45373d3093..e37bf27503 100644 --- a/BUILD_WIN.md +++ b/BUILD_WIN.md @@ -1,104 +1,81 @@ -Please read the [general build guide](BUILD.md) for information on dependencies required for all platforms. Only Windows specific instructions are found in this file. +This is a stand-alone guide for creating your first High Fidelity build for Windows 64-bit. -Interface can be built as 32 or 64 bit. +###Step 1. Installing Visual Studio 2013 -###Visual Studio 2013 +If you don't already have the Community or Professional edition of Visual Studio 2013, download and install [Visual Studio Community 2013](https://www.visualstudio.com/en-us/news/releasenotes/vs2013-community-vs). You do not need to install any of the optional components when going through the installer. -You can use the Community or Professional editions of Visual Studio 2013. +Note: Newer versions of Visual Studio are not yet compatible. -You can start a Visual Studio 2013 command prompt using the shortcut provided in the Visual Studio Tools folder installed as part of Visual Studio 2013. +###Step 2. Installing CMake -Or you can start a regular command prompt and then run: +Download and install the CMake 3.8.0-rc2 "win64-x64 Installer" from the [CMake Website](https://cmake.org/download/). Make sure "Add CMake to system PATH for all users" is checked when going through the installer. - "%VS120COMNTOOLS%\vsvars32.bat" +###Step 3. Installing Qt -####Windows SDK 8.1 +Download and install the [Qt 5.6.1 Installer](https://download.qt.io/official_releases/qt/5.6/5.6.1-1/qt-opensource-windows-x86-msvc2013_64-5.6.1-1.exe). Please note that the download file is large (850MB) and may take some time. -If using Visual Studio 2013 and building as a Visual Studio 2013 project you need the Windows 8 SDK which you should already have as part of installing Visual Studio 2013. You should be able to see it at `C:\Program Files (x86)\Windows Kits\8.1\Lib\winv6.3\um\x86`. +Make sure to select all components when going through the installer. -####nmake +###Step 4. Setting Qt Environment Variable -Some of the external projects may require nmake to compile and install. If it is not installed at the location listed below, please ensure that it is in your PATH so CMake can find it when required. +Go to "Control Panel > System > Advanced System Settings > Environment Variables > New..." (or search “Environment Variables” in Start Search). +* Set "Variable name": QT_CMAKE_PREFIX_PATH +* Set "Variable value": `C:\Qt\Qt5.6.1\5.6\msvc2013_64\lib\cmake` -We expect nmake.exe to be located at the following path. +###Step 5. Installing OpenSSL - C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\bin +Download and install the "Win64 OpenSSL v1.0.2k" Installer from [this website](https://slproweb.com/products/Win32OpenSSL.html). -###Qt -You can use the online installer or the offline installer. If you use the offline installer, be sure to select the "OpenGL" version. - -* [Download the online installer](http://www.qt.io/download-open-source/#section-2) - * When it asks you to select components, ONLY select one of the following, 32- or 64-bit to match your build preference: - * Qt > Qt 5.6.1 > **msvc2013 32-bit** - * Qt > Qt 5.6.1 > **msvc2013 64-bit** - -* Download the offline installer, 32- or 64-bit to match your build preference: - * [32-bit](https://download.qt.io/official_releases/qt/5.6/5.6.1-1/qt-opensource-windows-x86-msvc2013-5.6.1-1.exe) - * [64-bit](https://download.qt.io/official_releases/qt/5.6/5.6.1-1/qt-opensource-windows-x86-msvc2013_64-5.6.1-1.exe) - -Once Qt is installed, you need to manually configure the following: -* Set the QT_CMAKE_PREFIX_PATH environment variable to your `Qt\5.6.1\msvc2013\lib\cmake` or `Qt\5.6.1\msvc2013_64\lib\cmake` directory. - * You can set an environment variable from Control Panel > System > Advanced System Settings > Environment Variables > New - -###External Libraries - -All libraries should be 32- or 64-bit to match your build preference. - -CMake will need to know where the headers and libraries for required external dependencies are. - -We use CMake's `fixup_bundle` to find the DLLs all of our executable targets require, and then copy them beside the executable in a post-build step. If `fixup_bundle` is having problems finding a DLL, you can fix it manually on your end by adding the folder containing that DLL to your path. Let us know which DLL CMake had trouble finding, as it is possible a tweak to our CMake files is required. - -The recommended route for CMake to find the external dependencies is to place all of the dependencies in one folder and set one ENV variable - HIFI_LIB_DIR. That ENV variable should point to a directory with the following structure: - - root_lib_dir - -> openssl - -> bin - -> include - -> lib - -For many of the external libraries where precompiled binaries are readily available you should be able to simply copy the extracted folder that you get from the download links provided at the top of the guide. Otherwise you may need to build from source and install the built product to this directory. The `root_lib_dir` in the above example can be wherever you choose on your system - as long as the environment variable HIFI_LIB_DIR is set to it. From here on, whenever you see %HIFI_LIB_DIR% you should substitute the directory that you chose. - -####OpenSSL - -Qt will use OpenSSL if it's available, but it doesn't install it, so you must install it separately. - -Your system may already have several versions of the OpenSSL DLL's (ssleay32.dll, libeay32.dll) lying around, but they may be the wrong version. If these DLL's are in the PATH then QT will try to use them, and if they're the wrong version then you will see the following errors in the console: - - QSslSocket: cannot resolve TLSv1_1_client_method - QSslSocket: cannot resolve TLSv1_2_client_method - QSslSocket: cannot resolve TLSv1_1_server_method - QSslSocket: cannot resolve TLSv1_2_server_method - QSslSocket: cannot resolve SSL_select_next_proto - QSslSocket: cannot resolve SSL_CTX_set_next_proto_select_cb - QSslSocket: cannot resolve SSL_get0_next_proto_negotiated - -To prevent these problems, install OpenSSL yourself. Download one of the following binary packages [from this website](https://slproweb.com/products/Win32OpenSSL.html): -* Win32 OpenSSL v1.0.1q -* Win64 OpenSSL v1.0.1q - -Install OpenSSL into the Windows system directory, to make sure that Qt uses the version that you've just installed, and not some other version. - -###Build High Fidelity using Visual Studio -Follow the same build steps from the CMake section of [BUILD.md](BUILD.md), but pass a different generator to CMake. - -For 32-bit builds: - - cmake .. -G "Visual Studio 12" - -For 64-bit builds: +###Step 6. Running CMake to Generate Build Files +Run Command Prompt from Start and run the following commands: + cd "%HIFI_DIR%" + mkdir build + cd build cmake .. -G "Visual Studio 12 Win64" + +Where %HIFI_DIR% is the directory for the highfidelity repository. -Open %HIFI_DIR%\build\hifi.sln and compile. +###Step 7. Making a Build -###Running Interface -If you need to debug Interface, you can run interface from within Visual Studio (see the section below). You can also run Interface by launching it from command line or File Explorer from %HIFI_DIR%\build\interface\Debug\interface.exe +Open '%HIFI_DIR%\build\hifi.sln' using Visual Studio. -###Debugging Interface -* In the Solution Explorer, right click interface and click Set as StartUp Project -* Set the "Working Directory" for the Interface debugging sessions to the Debug output directory so that your application can load resources. Do this: right click interface and click Properties, choose Debugging from Configuration Properties, set Working Directory to .\Debug -* Now you can run and debug interface through Visual Studio +Change the Solution Configuration (next to the green play button) from "Debug" to "Release" for best performance. -For better performance when running debug builds, set the environment variable ```_NO_DEBUG_HEAP``` to ```1``` +Run Build > Build Solution. + +###Step 8. Testing Interface + +Create another environment variable (see Step #4) +* Set "Variable name": _NO_DEBUG_HEAP +* Set "Variable value": 1 + +In Visual Studio, right+click "interface" under the Apps folder in Solution Explorer and select "Set as Startup Project". Run Debug > Start Debugging. + +Now, you should have a full build of High Fidelity and be able to run the Interface using Visual Studio. Please check our [Docs](https://wiki.highfidelity.com/wiki/Main_Page) for more information regarding the programming workflow. + +Note: You can also run Interface by launching it from command line or File Explorer from %HIFI_DIR%\build\interface\Release\interface.exe + +###Troubleshooting + +For any problems after Step #6, first try this: +* Delete your locally cloned copy of the highfidelity repository +* Restart your computer +* Redownload the [repository](https://github.com/highfidelity/hifi) +* Restart directions from Step #6 + +####CMake gives you the same error message repeatedly after the build fails + +Remove `CMakeCache.txt` found in the '%HIFI_DIR%\build' directory + +####nmake cannot be found + +Make sure nmake.exe is located at the following path: + C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\bin + +If not, add the directory where nmake is located to the PATH environment variable. + +####Qt is throwing an error + +Make sure you have the correct version (5.6.1-1) installed and 'QT_CMAKE_PREFIX_PATH' environment variable is set correctly. -http://preshing.com/20110717/the-windows-heap-is-slow-when-launched-from-the-debugger/ diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index be23dcfa25..a5063b09b6 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -62,6 +62,7 @@ Agent::Agent(ReceivedMessage& message) : DependencyManager::set(); DependencyManager::set(); + DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); @@ -371,25 +372,39 @@ void Agent::executeScript() { using namespace recording; static const FrameType AUDIO_FRAME_TYPE = Frame::registerFrameType(AudioConstants::getAudioFrameName()); Frame::registerFrameHandler(AUDIO_FRAME_TYPE, [this, &scriptedAvatar](Frame::ConstPointer frame) { - const QByteArray& audio = frame->data; static quint16 audioSequenceNumber{ 0 }; - Transform audioTransform; + QByteArray audio(frame->data); + + if (_isNoiseGateEnabled) { + static int numSamples = AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL; + _noiseGate.gateSamples(reinterpret_cast(audio.data()), numSamples); + } + + computeLoudness(&audio, scriptedAvatar); + + // the codec needs a flush frame before sending silent packets, so + // do not send one if the gate closed in this block (eventually this can be crossfaded). + auto packetType = PacketType::MicrophoneAudioNoEcho; + if (scriptedAvatar->getAudioLoudness() == 0.0f && !_noiseGate.closedInLastBlock()) { + packetType = PacketType::SilentAudioFrame; + } + + Transform audioTransform; auto headOrientation = scriptedAvatar->getHeadOrientation(); audioTransform.setTranslation(scriptedAvatar->getPosition()); audioTransform.setRotation(headOrientation); - computeLoudness(&audio, scriptedAvatar); - QByteArray encodedBuffer; if (_encoder) { _encoder->encode(audio, encodedBuffer); } else { encodedBuffer = audio; } + AbstractAudioInterface::emitAudioPacket(encodedBuffer.data(), encodedBuffer.size(), audioSequenceNumber, audioTransform, scriptedAvatar->getPosition(), glm::vec3(0), - PacketType::MicrophoneAudioNoEcho, _selectedCodecName); + packetType, _selectedCodecName); }); auto avatarHashMap = DependencyManager::set(); @@ -483,6 +498,14 @@ void Agent::setIsListeningToAudioStream(bool isListeningToAudioStream) { _isListeningToAudioStream = isListeningToAudioStream; } +void Agent::setIsNoiseGateEnabled(bool isNoiseGateEnabled) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setIsNoiseGateEnabled", Q_ARG(bool, isNoiseGateEnabled)); + return; + } + _isNoiseGateEnabled = isNoiseGateEnabled; +} + void Agent::setIsAvatar(bool isAvatar) { // this must happen on Agent's main thread if (QThread::currentThread() != thread()) { diff --git a/assignment-client/src/Agent.h b/assignment-client/src/Agent.h index 0ce7b71d5d..620ac8e047 100644 --- a/assignment-client/src/Agent.h +++ b/assignment-client/src/Agent.h @@ -29,6 +29,7 @@ #include +#include "AudioNoiseGate.h" #include "MixedAudioStream.h" #include "avatars/ScriptableAvatar.h" @@ -38,6 +39,7 @@ class Agent : public ThreadedAssignment { Q_PROPERTY(bool isAvatar READ isAvatar WRITE setIsAvatar) Q_PROPERTY(bool isPlayingAvatarSound READ isPlayingAvatarSound) Q_PROPERTY(bool isListeningToAudioStream READ isListeningToAudioStream WRITE setIsListeningToAudioStream) + Q_PROPERTY(bool isNoiseGateEnabled READ isNoiseGateEnabled WRITE setIsNoiseGateEnabled) Q_PROPERTY(float lastReceivedAudioLoudness READ getLastReceivedAudioLoudness) Q_PROPERTY(QUuid sessionUUID READ getSessionUUID) @@ -52,6 +54,9 @@ public: bool isListeningToAudioStream() const { return _isListeningToAudioStream; } void setIsListeningToAudioStream(bool isListeningToAudioStream); + bool isNoiseGateEnabled() const { return _isNoiseGateEnabled; } + void setIsNoiseGateEnabled(bool isNoiseGateEnabled); + float getLastReceivedAudioLoudness() const { return _lastReceivedAudioLoudness; } QUuid getSessionUUID() const; @@ -106,6 +111,9 @@ private: QTimer* _avatarIdentityTimer = nullptr; QHash _outgoingScriptAudioSequenceNumbers; + AudioNoiseGate _noiseGate; + bool _isNoiseGateEnabled { false }; + CodecPluginPointer _codec; QString _selectedCodecName; Encoder* _encoder { nullptr }; diff --git a/assignment-client/src/assets/AssetServer.cpp b/assignment-client/src/assets/AssetServer.cpp index 82dd23a9de..3886ff8d92 100644 --- a/assignment-client/src/assets/AssetServer.cpp +++ b/assignment-client/src/assets/AssetServer.cpp @@ -24,7 +24,7 @@ #include #include -#include +#include #include "NetworkLogging.h" #include "NodeType.h" @@ -162,7 +162,7 @@ void AssetServer::completeSetup() { if (assetsPath.isRelative()) { // if the domain settings passed us a relative path, make an absolute path that is relative to the // default data directory - absoluteFilePath = ServerPathUtils::getDataFilePath("assets/" + assetsPathString); + absoluteFilePath = PathUtils::getAppDataFilePath("assets/" + assetsPathString); } _resourcesDirectory = QDir(absoluteFilePath); diff --git a/assignment-client/src/octree/OctreeServer.cpp b/assignment-client/src/octree/OctreeServer.cpp index 2eee2ee229..f2dbe5d1d2 100644 --- a/assignment-client/src/octree/OctreeServer.cpp +++ b/assignment-client/src/octree/OctreeServer.cpp @@ -29,7 +29,7 @@ #include "OctreeQueryNode.h" #include "OctreeServerConsts.h" #include -#include +#include #include int OctreeServer::_clientCount = 0; @@ -279,8 +279,7 @@ OctreeServer::~OctreeServer() { void OctreeServer::initHTTPManager(int port) { // setup the embedded web server - - QString documentRoot = QString("%1/web").arg(ServerPathUtils::getDataDirectory()); + QString documentRoot = QString("%1/web").arg(PathUtils::getAppDataPath()); // setup an httpManager with us as the request handler and the parent _httpManager = new HTTPManager(QHostAddress::AnyIPv4, port, documentRoot, this, this); @@ -1179,7 +1178,7 @@ void OctreeServer::domainSettingsRequestComplete() { if (persistPath.isRelative()) { // if the domain settings passed us a relative path, make an absolute path that is relative to the // default data directory - persistAbsoluteFilePath = QDir(ServerPathUtils::getDataFilePath("entities/")).absoluteFilePath(_persistFilePath); + persistAbsoluteFilePath = QDir(PathUtils::getAppDataFilePath("entities/")).absoluteFilePath(_persistFilePath); } static const QString ENTITY_PERSIST_EXTENSION = ".json.gz"; @@ -1245,7 +1244,7 @@ void OctreeServer::domainSettingsRequestComplete() { QDir backupDirectory { _backupDirectoryPath }; QString absoluteBackupDirectory; if (backupDirectory.isRelative()) { - absoluteBackupDirectory = QDir(ServerPathUtils::getDataFilePath("entities/")).absoluteFilePath(_backupDirectoryPath); + absoluteBackupDirectory = QDir(PathUtils::getAppDataFilePath("entities/")).absoluteFilePath(_backupDirectoryPath); absoluteBackupDirectory = QDir(absoluteBackupDirectory).absolutePath(); } else { absoluteBackupDirectory = backupDirectory.absolutePath(); diff --git a/assignment-client/src/scripts/EntityScriptServer.cpp b/assignment-client/src/scripts/EntityScriptServer.cpp index 47071b10b7..954c25a342 100644 --- a/assignment-client/src/scripts/EntityScriptServer.cpp +++ b/assignment-client/src/scripts/EntityScriptServer.cpp @@ -58,6 +58,8 @@ EntityScriptServer::EntityScriptServer(ReceivedMessage& message) : ThreadedAssig DependencyManager::registerInheritance(); + DependencyManager::set(); + DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); @@ -324,7 +326,26 @@ void EntityScriptServer::nodeActivated(SharedNodePointer activatedNode) { void EntityScriptServer::nodeKilled(SharedNodePointer killedNode) { switch (killedNode->getType()) { case NodeType::EntityServer: { - clear(); + // Before we clear, make sure this was our only entity server. + // Otherwise we're assuming that we have "trading" entity servers + // (an old one going away and a new one coming onboard) + // and that we shouldn't clear here because we're still doing work. + bool hasAnotherEntityServer = false; + auto nodeList = DependencyManager::get(); + + nodeList->eachNodeBreakable([&hasAnotherEntityServer, &killedNode](const SharedNodePointer& node){ + if (node->getType() == NodeType::EntityServer && node->getUUID() != killedNode->getUUID()) { + // we're talking to > 1 entity servers, we know we won't clear + hasAnotherEntityServer = true; + return false; + } + + return true; + }); + + if (!hasAnotherEntityServer) { + clear(); + } break; } @@ -395,7 +416,8 @@ void EntityScriptServer::selectAudioFormat(const QString& selectedCodecName) { void EntityScriptServer::resetEntitiesScriptEngine() { auto engineName = QString("about:Entities %1").arg(++_entitiesScriptEngineCount); - auto newEngine = QSharedPointer(new ScriptEngine(ScriptEngine::ENTITY_SERVER_SCRIPT, NO_SCRIPT, engineName)); + auto newEngine = QSharedPointer(new ScriptEngine(ScriptEngine::ENTITY_SERVER_SCRIPT, NO_SCRIPT, engineName), + &ScriptEngine::deleteLater); auto webSocketServerConstructorValue = newEngine->newFunction(WebSocketServerClass::constructor); newEngine->globalObject().setProperty("WebSocketServer", webSocketServerConstructorValue); @@ -455,13 +477,13 @@ void EntityScriptServer::addingEntity(const EntityItemID& entityID) { void EntityScriptServer::deletingEntity(const EntityItemID& entityID) { if (_entityViewer.getTree() && !_shuttingDown && _entitiesScriptEngine) { - _entitiesScriptEngine->unloadEntityScript(entityID); + _entitiesScriptEngine->unloadEntityScript(entityID, true); } } void EntityScriptServer::entityServerScriptChanging(const EntityItemID& entityID, const bool reload) { if (_entityViewer.getTree() && !_shuttingDown) { - _entitiesScriptEngine->unloadEntityScript(entityID); + _entitiesScriptEngine->unloadEntityScript(entityID, true); checkAndCallPreload(entityID, reload); } } diff --git a/cmake/macros/PackageLibrariesForDeployment.cmake b/cmake/macros/PackageLibrariesForDeployment.cmake index 795e3642a5..d324776572 100644 --- a/cmake/macros/PackageLibrariesForDeployment.cmake +++ b/cmake/macros/PackageLibrariesForDeployment.cmake @@ -24,9 +24,9 @@ macro(PACKAGE_LIBRARIES_FOR_DEPLOYMENT) TARGET ${TARGET_NAME} POST_BUILD COMMAND ${CMAKE_COMMAND} - -DBUNDLE_EXECUTABLE=$ - -DBUNDLE_PLUGIN_DIR=$/${PLUGIN_PATH} - -P ${CMAKE_CURRENT_BINARY_DIR}/FixupBundlePostBuild.cmake + -DBUNDLE_EXECUTABLE="$" + -DBUNDLE_PLUGIN_DIR="$/${PLUGIN_PATH}" + -P "${CMAKE_CURRENT_BINARY_DIR}/FixupBundlePostBuild.cmake" ) find_program(WINDEPLOYQT_COMMAND windeployqt PATHS ${QT_DIR}/bin NO_DEFAULT_PATH) @@ -39,27 +39,27 @@ macro(PACKAGE_LIBRARIES_FOR_DEPLOYMENT) add_custom_command( TARGET ${TARGET_NAME} POST_BUILD - COMMAND CMD /C "SET PATH=%PATH%;${QT_DIR}/bin && ${WINDEPLOYQT_COMMAND} ${EXTRA_DEPLOY_OPTIONS} $<$,$,$>:--release> $" + COMMAND CMD /C "SET PATH=%PATH%;${QT_DIR}/bin && ${WINDEPLOYQT_COMMAND} ${EXTRA_DEPLOY_OPTIONS} $<$,$,$>:--release> \"$\"" ) - set(QTAUDIO_PATH $/audio) - set(QTAUDIO_WIN7_PATH $/audioWin7/audio) - set(QTAUDIO_WIN8_PATH $/audioWin8/audio) + set(QTAUDIO_PATH "$/audio") + set(QTAUDIO_WIN7_PATH "$/audioWin7/audio") + set(QTAUDIO_WIN8_PATH "$/audioWin8/audio") # copy qtaudio_wasapi.dll and qtaudio_windows.dll in the correct directories for runtime selection add_custom_command( TARGET ${TARGET_NAME} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E make_directory ${QTAUDIO_WIN7_PATH} - COMMAND ${CMAKE_COMMAND} -E make_directory ${QTAUDIO_WIN8_PATH} + COMMAND ${CMAKE_COMMAND} -E make_directory "${QTAUDIO_WIN7_PATH}" + COMMAND ${CMAKE_COMMAND} -E make_directory "${QTAUDIO_WIN8_PATH}" # copy release DLLs - COMMAND if exist ${QTAUDIO_PATH}/qtaudio_windows.dll ( ${CMAKE_COMMAND} -E copy ${QTAUDIO_PATH}/qtaudio_windows.dll ${QTAUDIO_WIN7_PATH} ) - COMMAND if exist ${QTAUDIO_PATH}/qtaudio_windows.dll ( ${CMAKE_COMMAND} -E copy ${WASAPI_DLL_PATH}/qtaudio_wasapi.dll ${QTAUDIO_WIN8_PATH} ) + COMMAND if exist "${QTAUDIO_PATH}/qtaudio_windows.dll" ( ${CMAKE_COMMAND} -E copy "${QTAUDIO_PATH}/qtaudio_windows.dll" "${QTAUDIO_WIN7_PATH}" ) + COMMAND if exist "${QTAUDIO_PATH}/qtaudio_windows.dll" ( ${CMAKE_COMMAND} -E copy "${WASAPI_DLL_PATH}/qtaudio_wasapi.dll" "${QTAUDIO_WIN8_PATH}" ) # copy debug DLLs - COMMAND if exist ${QTAUDIO_PATH}/qtaudio_windowsd.dll ( ${CMAKE_COMMAND} -E copy ${QTAUDIO_PATH}/qtaudio_windowsd.dll ${QTAUDIO_WIN7_PATH} ) - COMMAND if exist ${QTAUDIO_PATH}/qtaudio_windowsd.dll ( ${CMAKE_COMMAND} -E copy ${WASAPI_DLL_PATH}/qtaudio_wasapid.dll ${QTAUDIO_WIN8_PATH} ) + COMMAND if exist "${QTAUDIO_PATH}/qtaudio_windowsd.dll" ( ${CMAKE_COMMAND} -E copy "${QTAUDIO_PATH}/qtaudio_windowsd.dll" "${QTAUDIO_WIN7_PATH}" ) + COMMAND if exist "${QTAUDIO_PATH}/qtaudio_windowsd.dll" ( ${CMAKE_COMMAND} -E copy "${WASAPI_DLL_PATH}/qtaudio_wasapid.dll" "${QTAUDIO_WIN8_PATH}" ) # remove directory - COMMAND ${CMAKE_COMMAND} -E remove_directory ${QTAUDIO_PATH} + COMMAND ${CMAKE_COMMAND} -E remove_directory "${QTAUDIO_PATH}" ) endif () diff --git a/cmake/macros/SymlinkOrCopyDirectoryBesideTarget.cmake b/cmake/macros/SymlinkOrCopyDirectoryBesideTarget.cmake index 37a7a9caa0..9ae47aad82 100644 --- a/cmake/macros/SymlinkOrCopyDirectoryBesideTarget.cmake +++ b/cmake/macros/SymlinkOrCopyDirectoryBesideTarget.cmake @@ -14,7 +14,7 @@ macro(SYMLINK_OR_COPY_DIRECTORY_BESIDE_TARGET _SHOULD_SYMLINK _DIRECTORY _DESTIN # remove the current directory add_custom_command( TARGET ${TARGET_NAME} POST_BUILD - COMMAND "${CMAKE_COMMAND}" -E remove_directory $/${_DESTINATION} + COMMAND "${CMAKE_COMMAND}" -E remove_directory "$/${_DESTINATION}" ) if (${_SHOULD_SYMLINK}) @@ -48,8 +48,8 @@ macro(SYMLINK_OR_COPY_DIRECTORY_BESIDE_TARGET _SHOULD_SYMLINK _DIRECTORY _DESTIN # copy the directory add_custom_command( TARGET ${TARGET_NAME} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_directory ${_DIRECTORY} - $/${_DESTINATION} + COMMAND ${CMAKE_COMMAND} -E copy_directory "${_DIRECTORY}" + "$/${_DESTINATION}" ) endif () # glob everything in this directory - add a custom command to copy any files diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index c741c22b83..620b11d8ad 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -38,7 +38,7 @@ #include #include #include -#include +#include #include #include "DomainServerNodeData.h" @@ -1618,7 +1618,7 @@ QJsonObject DomainServer::jsonObjectForNode(const SharedNodePointer& node) { QDir pathForAssignmentScriptsDirectory() { static const QString SCRIPTS_DIRECTORY_NAME = "/scripts/"; - QDir directory(ServerPathUtils::getDataDirectory() + SCRIPTS_DIRECTORY_NAME); + QDir directory(PathUtils::getAppDataPath() + SCRIPTS_DIRECTORY_NAME); if (!directory.exists()) { directory.mkpath("."); qInfo() << "Created path to " << directory.path(); diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index 661a6213b8..d6b57b450a 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -246,10 +246,13 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList _agentPermissions[editorKey]->set(NodePermissions::Permission::canAdjustLocks); } - QList> permissionsSets; - permissionsSets << _standardAgentPermissions.get() << _agentPermissions.get(); + std::list> permissionsSets{ + _standardAgentPermissions.get(), + _agentPermissions.get() + }; foreach (auto permissionsSet, permissionsSets) { - foreach (NodePermissionsKey userKey, permissionsSet.keys()) { + for (auto entry : permissionsSet) { + const auto& userKey = entry.first; if (onlyEditorsAreRezzers) { if (permissionsSet[userKey]->can(NodePermissions::Permission::canAdjustLocks)) { permissionsSet[userKey]->set(NodePermissions::Permission::canRezPermanentEntities); @@ -300,7 +303,6 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList } QVariantMap& DomainServerSettingsManager::getDescriptorsMap() { - static const QString DESCRIPTORS{ "descriptors" }; auto& settingsMap = getSettingsMap(); @@ -1355,18 +1357,12 @@ QStringList DomainServerSettingsManager::getAllKnownGroupNames() { // extract all the group names from the group-permissions and group-forbiddens settings QSet result; - QHashIterator i(_groupPermissions.get()); - while (i.hasNext()) { - i.next(); - NodePermissionsKey key = i.key(); - result += key.first; + for (const auto& entry : _groupPermissions.get()) { + result += entry.first.first; } - QHashIterator j(_groupForbiddens.get()); - while (j.hasNext()) { - j.next(); - NodePermissionsKey key = j.key(); - result += key.first; + for (const auto& entry : _groupForbiddens.get()) { + result += entry.first.first; } return result.toList(); @@ -1377,20 +1373,17 @@ bool DomainServerSettingsManager::setGroupID(const QString& groupName, const QUu _groupIDs[groupName.toLower()] = groupID; _groupNames[groupID] = groupName; - QHashIterator i(_groupPermissions.get()); - while (i.hasNext()) { - i.next(); - NodePermissionsPointer perms = i.value(); + + for (const auto& entry : _groupPermissions.get()) { + auto& perms = entry.second; if (perms->getID().toLower() == groupName.toLower() && !perms->isGroup()) { changed = true; perms->setGroupID(groupID); } } - QHashIterator j(_groupForbiddens.get()); - while (j.hasNext()) { - j.next(); - NodePermissionsPointer perms = j.value(); + for (const auto& entry : _groupForbiddens.get()) { + auto& perms = entry.second; if (perms->getID().toLower() == groupName.toLower() && !perms->isGroup()) { changed = true; perms->setGroupID(groupID); diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index dbc484d0b9..726aa7ef84 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -189,7 +189,7 @@ endif() # link required hifi libraries link_hifi_libraries( - shared octree gpu gl gpu-gl procedural model render + shared octree ktx gpu gl gpu-gl procedural model render recording fbx networking model-networking entities avatars audio audio-client animation script-engine physics render-utils entities-renderer ui auto-updater @@ -288,7 +288,7 @@ if (APPLE) add_custom_command(TARGET ${TARGET_NAME} POST_BUILD COMMAND "${CMAKE_COMMAND}" -E copy_directory "${CMAKE_SOURCE_DIR}/scripts" - $/../Resources/scripts + "$/../Resources/scripts" ) # call the fixup_interface macro to add required bundling commands for installation @@ -299,10 +299,10 @@ else (APPLE) add_custom_command(TARGET ${TARGET_NAME} POST_BUILD COMMAND "${CMAKE_COMMAND}" -E copy_directory "${PROJECT_SOURCE_DIR}/resources" - $/resources + "$/resources" COMMAND "${CMAKE_COMMAND}" -E copy_directory "${CMAKE_SOURCE_DIR}/scripts" - $/scripts + "$/scripts" ) # link target to external libraries @@ -337,7 +337,7 @@ endif() add_bugsplat() if (WIN32) - set(EXTRA_DEPLOY_OPTIONS "--qmldir ${PROJECT_SOURCE_DIR}/resources/qml") + set(EXTRA_DEPLOY_OPTIONS "--qmldir \"${PROJECT_SOURCE_DIR}/resources/qml\"") set(TARGET_INSTALL_DIR ${INTERFACE_INSTALL_DIR}) set(TARGET_INSTALL_COMPONENT ${CLIENT_COMPONENT}) diff --git a/interface/resources/controllers/standard.json b/interface/resources/controllers/standard.json index 04a3f560b6..9e3b2f4d13 100644 --- a/interface/resources/controllers/standard.json +++ b/interface/resources/controllers/standard.json @@ -2,7 +2,27 @@ "name": "Standard to Action", "channels": [ { "from": "Standard.LY", "to": "Actions.TranslateZ" }, - { "from": "Standard.LX", "to": "Actions.TranslateX" }, + + { "from": "Standard.LX", + "when": [ + "Application.InHMD", "!Application.AdvancedMovement", + "Application.SnapTurn", "!Standard.RX" + ], + "to": "Actions.StepYaw", + "filters": + [ + { "type": "deadZone", "min": 0.15 }, + "constrainToInteger", + { "type": "pulse", "interval": 0.25 }, + { "type": "scale", "scale": 22.5 } + ] + }, + { "from": "Standard.LX", "to": "Actions.TranslateX", + "when": [ "Application.AdvancedMovement" ] + }, + { "from": "Standard.LX", "to": "Actions.Yaw", + "when": [ "!Application.AdvancedMovement", "!Application.SnapTurn" ] + }, { "from": "Standard.RX", "when": [ "Application.InHMD", "Application.SnapTurn" ], @@ -15,29 +35,29 @@ { "type": "scale", "scale": 22.5 } ] }, + { "from": "Standard.RX", "to": "Actions.Yaw", + "when": [ "!Application.SnapTurn" ] + }, - { "from": "Standard.RX", "to": "Actions.Yaw" }, - { "from": "Standard.RY", - "when": "Application.Grounded", - "to": "Actions.Up", - "filters": + { "from": "Standard.RY", + "when": "Application.Grounded", + "to": "Actions.Up", + "filters": [ { "type": "deadZone", "min": 0.6 }, "invert" ] - }, + }, - { "from": "Standard.RY", "to": "Actions.Up", "filters": "invert"}, + { "from": "Standard.RY", "to": "Actions.Up", "filters": "invert"}, { "from": "Standard.Back", "to": "Actions.CycleCamera" }, { "from": "Standard.Start", "to": "Actions.ContextMenu" }, - { "from": "Standard.LT", "to": "Actions.LeftHandClick" }, + { "from": "Standard.LT", "to": "Actions.LeftHandClick" }, { "from": "Standard.RT", "to": "Actions.RightHandClick" }, - { "from": "Standard.LeftHand", "to": "Actions.LeftHand" }, + { "from": "Standard.LeftHand", "to": "Actions.LeftHand" }, { "from": "Standard.RightHand", "to": "Actions.RightHand" } ] } - - diff --git a/interface/resources/html/img/devices.png b/interface/resources/html/img/devices.png deleted file mode 100644 index fc4231e96e25732a0659c911e7c15ded5b54911b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7492 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 diff --git a/interface/resources/html/img/models.png b/interface/resources/html/img/models.png deleted file mode 100644 index b09c36011d540759da5326ed70bda97592e9636b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8664 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 diff --git a/interface/resources/html/img/run-script.png b/interface/resources/html/img/run-script.png deleted file mode 100644 index 941b8ee9f13664fc2b2ec51a75273ba3e553d3a5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4873 zcmcK8c{tQ>zrgV?q0!GWLkk-FGG(jyg&5hUg)+9nWM78L*s>=}j9nB+un6J+0DzWuyA%KbLEmeZzGj{-z5x!8(14D!rz2WS-`&9#ZHjhq4)X3o z!vTP=R3D*pCvbE*R}%vr6pHSQ<&g3XxyH^{wH@UC!>xW2H;-voM z3h|)4jo9HWC5cTvJyjG`L+g)JNtWJfOe^clAh;x}4Z(h2Q*_h_Jm~!{;!UuDLc|k` zp&mD&5xeKQHNL53sY&Px#IRX)h1wY{%a2%sJ1=!WAgwhu2+(!DKfVoruoP(XnH?KaM>Ki@gK|@D(9lGBm95(ku8y-BI5)xsZK5>su1 z>a9$bT;1_1D=N`|SNAo6oR{b1ynjS&s+67kjJ%HG0&pGGj&uW$exv)d85SbcJ^DG4 z@=__(BkiWI`Y^udWc~N)h>)O>NzWnIiD2OhxJ9t)=AldWvpTxW1bI#kNP&phzn7P* zqPUC-&Xx6?fUSTt3lqIAfYbT!cgp3Gu9}G!MNf>KIK1jEdFAT67rl;OJY(|Z06_pR9Ae;FfVRJcM|euweT5=bG$caQb; z^|gFc3s@cVfvG|lFH~JJRJ3jQS;hO=^9JAGZc=siWn>~yydmB_eD>c&UP?tWFh2+#EW~WQUxFq!a};IG=5a8 z8y#q$V3`<)tqN7M#1i z1MugSA=6hio`Pb2p=)ZnuT?CNTJH2A_ajZw_cDm{v$1R?Q7rv8Zn%E+`*I$dsJMad zqP8T$V=r=cPsQnZ1c%1@S)@ji263V3#8rezdRAi4Zl>|$Zp1#LwS~I3Jd>>T{iC|0 z`IDW+!4||R3Pw5JJBQfHJy8Hg3mjT%qh%H4p0iOR?^4|2y@Zd5JPCp_tJ7+Ig}04e zM;Gj*C9zJ38IA*DL^r&$GMV5Y;_%?0Pj#{e6X3P3OXNBQLyyH5h~DTjvKV_~l=4Ub z(#rXgXODW7N$^7I2*0}rLkJe~%zA048VkgKMopRU7_lNiH*f{{xmXf)mix)j)BP@3 zK3Dex3!ZvRhbDt}yO3Yu@bPqUFmS`cG017A>*YSmI!6rJ*Zy&d(At5?hOp0Rinbbbqznt7A=Ti(HH8`0JqfxVbc@S@6lX1{iWgsfZ+aSaLh zW?AvurT1WqKFXcS$;rXC-V)blI__`UK~={{QXYc4eH65W4%8_o5e$IE#s5Eb{wMSy z>k{Iz_^7C;^q?B&^d!_MH}kP3smP_4SX(;+OWH}66ckU?i=g#VoJjJQ*QX4Z1HJ?V zW18i>3Et4ozP8Pd4t`CPopp{juL;QO3w+FNUdJkZr1!s&4G*+&@kv%2QcQ{kcbM|L zm*mk2J~=1s4K@5dLA{7F=jIBSe>z4!c1B<(7UW=UEx{%k$L@`>mj^q*eL*cZ9d)2* z1^z$?@Ve_>d|?UhpnVEb!k~3|Qgy!@mxrqM~3eRtJ53eQ(9dp3+Pb zC$p&JnPBLNn3*}e9daUsF+SUysT}#E`Ebu|bN=(IBH9x7AhLd3q-vw%tfy4i(E;P- zGtsbp?{z2uIX$5C$nEU&yPAvNf5JqC`KDq-L)E}9^^}5-4j9KJn0reVx2~1AYaMJ4 zwD=bYP;)i#PZ;9?$HaA2R6Qr)>l5t>XE<+Ti#MXghjQogpZM}E``1(T`)@ebKbkXG z{eg_Q2gAKQs@*LJVG(PEI0x;D@CzSy-7w>k7DS&4%>T$j%>Qdr#8E>ffY|nOa#@UeizND;dvrdm!3fixH?ic7Rt!PpgI4tZmDIoij zhdl=-VLIYjq?VCHCl40GP+p+(k%PAPUcycNE@apdjIw?{ysgInExX*M!m4*3(Tbaq3Gxt zKtf(d;e0Z$5BaCN+?!wWoR1~s49xbM=dRmokSl!pTvE4QOV8Ii&Mpp?cJXTMd&%Ql z-Wpbyg?MEO8w=rm=;@Wn$5vN#M@t|1(^Ac*>&OHJS>j~(bHAa(^8Ahuw~U2$3#FkG z-|&<@c(2Iiq7G49V^h=N_AjlYeM%QZ8>^SzxwvIU_S3$-J4<(2}eqV^aJ z`x{?Z%_>Y4^pJ?Ac<_dUN{Q~IR{b4Lp$)YK8#M%KneE)$aw+Tdd{ru@CaaDA?b6Xev1e%vj;6O(JG7KmweW=B)>8|u)CYKF-{NBG;_7HhBwvF7N@6Qy5&CiHw$i8c_IU~7C~yW;5ziL zZS+c0KaG~4_=Qfs!%LpiiX7OXOv2dlJ0q8(S3QLY`N~uEOPo(EA0*$6$hvcvWFp#$ z1wAx##~ZGWB6K%g(O*%hs#F<74#dF!x-8usS*DlXjzp9v8(Tbvlh+QYO{%%GIf$+r z_i%%;VDi_Y$w6Y+%mv%;i+Gyb;fJNVPgf;sgIkv6 zszVH}Yi|P2DJpWr$INC_E40}Nx$0L~_{Ld3r#Roxw!dOVK#5M|H7#qyCJ__!DgwepN<*1*9Yh2 zAOvmCj1?zj#bYwuGjbxn{8XEx8%1Y)uG0-}l!3kp6+CN-_RzI4T7JbZ;c$BPJwH9u zi6MGB?W{#DR~El?EEoyiVbmUKaOax9n!><)6!ci%s9sgmRiERvWRtiR*W5#{!AnVU zIIf+>$>6m{9h&yAbb-KMf9swM9^*6u($Vh8M*EbeaLx;Se%C4CqP^Z7zXuw^^N)?_ zOg>+y6HbUrJD$<(vEYYygknN0Q4B?sjzX5tLG#L8)SV;!a1=v_hkEl^V%8ZD3sgl; zUfqiDq8wv-SGCs7cBGVA&1;|~CW*b*vp!9UG3vRO*6p#{^4H9Wl*ThQvh|>q z1t7*>^b?%k?BJNY1hm0gciP*VEJUafJ|OmzQt74|sbXfN@E0$P6>)IaC1o(gyBaRI{5 z88~$YSxMZArhy;;0uLD_81p@`xAVwG=kePzXw6X4Tzr``+Qju}XXpCuuXpf_u>CM|4;wt`CoT;bwtRi$N>O= zaKfKC2LKW<003N21^|GuJ1)Ni0Dz8h@QLw^BFDr9MUw!#kf>mimeb{+P|`V4P)Gu; zhlB%wy`xTN?7ZS9KY#e(q3WxWI#aAaU3JXoWE#o=P(;b+v~VaZsnLKf;Mbxa#{gSU zTC%oAXdiU8TI-7FZmB~Xc>J3^cV+P@S5nGi_-4Y%C-tM>zAlfBUJEt=RJiBu0T4<> zO8(zqOGq-6Zg}$H!w1(y_F2CZ*FI^6FT03_yIyYh^A8MLB*w?bW79zBhb(_ba>oxDwR%6Wfg zI|`;7n%hKwrx=@yS$2g~ol3CmCW5f`9Z2 z`mk?c;-S8eR?^?C^j)rVqumy@NGSA!3C_~>35q^7z9T8c>ajMxAjHyEcJ2}5vYwUJ z25g8@wCSrgVCT8b1}JU=HyxnTe+t%>+H6h^>j)^BdMn+ywbpo1jL1NuvdgK9>dlvN z7z$R9LJXNH=@4F@;BmHHhgUk1-DY1L$%GkTW#5bVx?f$0hBLI{#XXCQCS8h3Q4A^d z<;f`Y^sK*pMjn}4sh@^wE;nK1?NTDQU#hzh$A7EI)XOQgUY&-)@Vvv!`oje&f7FF$ z$wzN}yHwXWYpU_&oqCNZ#KHMf;hvShekBMkBl<0kWfNp-vb)r7HU8v4?d9bd3?yY2 zapWkwj;O=1ieU*{{i)(8`JBMz#)FraL!3^c+F2YC(BJT8+YJdbRa>VH_HpWQ`P`_s zWT%>0Y~w@^i=EIBj(xJ@2tor{fojS=Yjln!e?|C8J?KW?M%QO-0n%TdA5xWW+rm462 zY{Tamat{b8FcF>Cdr0T+EG;Het~#VR4LnMFQZVoaekfK8%Ii zfe9pwaRX6xy3BsUM8#^>t__U8LQYpA_#AXAQrLf%oWa}rtV+*g4$4eNr#$e5V0`(V zfv|6cCgt#Ag*Qw@oY3SDJx|Wy-oJ?|kA}@wIjb-}qo9Kpl;O)Ns@Gjny>DS)EZX8{ z;=!l<$kk4yjZOOF!sMZ{<@+#HR!25k;w)W>%lx$4bSJ6l5`&({HOhL2>ErzD!MudC z`fsba8r^@weyw1hHsOFdg~mW-i79^3G9lh!-|Nx6rYbbw-z@F$9AGNEQZq%t+&{7F z!*|%*L{7mz7=MF&>TV~tQ`X-7f`~!AuOieRJU1!wq}jBxPY@$^o_G-B@7i?FUM)Fo z(cA39`IvubBqJ{l4BP@V6#Guk_hnP-9JJr|{+!Lj*?hnK2vVy36T8qTrEJX|6dHyo z3|>`7k2d>xG$e+LZ)%t*{iFbUqg&N%Py37>WZ9cYV(1m7X*u5Bo`YGod!fEVtz2t? zRS(?FVYPaqD%|7|KO?KhZ*30YN4_7ca0iP5)RYA=m5G8Z0$7x!xtVh9OBQ2~lI zSrcQZGUkt>@*28xRs+h>HXxbjWGRZCdpH(*_N~Hc!=pyB%g{&Q)_P>{UYv`7rFkBlpl`KpZ1#5X z$^s%?Hl|jOqh5QK{*b(ml7z!R0Fp++f5%bo33~t@kGX6Q^wGW+MWD-ga~)k)_4V~U zg1^`}dDlCURq|?@{gx@L>QacN5S!$nkm4|xL#VoOcj>E310qQYwdKb=y=`lhXs~i2 zyWGrt9{-*o8kv#NiElh39Uvoc?5EfagAnO)W2&e1p1HLLI^zQx%iu15==V*+XpEz#n7g;dqd1!{24lZS$JzF&Ml}Y}^J{N$yEsSm+ zASXFh#am(KE%w`1tOoYh+3US*FgW5hrj@Br6|7X0)`}AhzKl`ot#b{@bi-rjTSS-X zP@m&8O=Fv9Ae7Z^O7_saWO~nPYEJkV1wYel^Oy`K*Pd_H>U3Gv8l>2PzE+HMwe~`& z^X7c3qRGA4Sck3HGkFBsi?S)ZY|Mg#8I8m=vTmk^p|g?IHzyu>0L~ZxZkPYbY(a#C z79D`Wf6h<~GNrLD0=@s?Q%G%Y!#YzcECO_dL97jZS|~1&mmOLwr;Sm88}c*FJDam+ zjp~n|fBnjR7K%^ReFL@;Qp6p3009wntpVrCf}KMiCO|HIvpy;TbGa8(6(3$Ve@*qq zJvhsDWxe*am%?_Kh7@u3PNxhp)& zGlKJ1*G4diHKvDqD~tDT__tH1-6aT2lxx<^b16OB3){z*^F_nN#qj0%0|<8PM=iOb zLZqv-+1<{ugjk=sA`WSC<;!Q(!zt)~p}IynkE+dpa w8mdJ$F@o>^$TljjB#j>>_U`-F!(BO3fT`8&WfDTw1ONbVa&SFU2?wVA2MC*sc>n+a diff --git a/interface/resources/html/img/write-script.png b/interface/resources/html/img/write-script.png deleted file mode 100644 index dae97e59b14bae50013b583cd9cb3b5bdf6ad464..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2006 zcmb`HX*Ao38pi)h(Ww^WC~;|$Yo=Oy6}h!H%2>+{#aNnJBh9EKC6-vKrnL4lqlR8f z+CxLd)*4blsO1QWsHU;S(vqMgmZY&S_c%TCW#-;9=gheuo-gltK0NP--!H=jVRz~) zwXXmGIOSk(?Fs;Z5CDKBS!n`!Um!t4q*Wpv@1Fw zC@!KOZ4Ll3cn51scl-o@sspQp0s1~_21*qu6P+5Qx8)(@64A${yN;nQG-&a!vK46|YxiCC+UCW# z?78c1pDn_!(s`qA{2QK}Osh^O6w)&nq|Y#8K%!6GZ2QSKTb`%$ z3fZ>{+WbzhbKE8?t5!s}?(f#PZ^l`Cy89ua>7VGH@y;Zlu%JU%-FpS2``0zP)S?M~ zYMcE+3MG7Uj=OMBeYTuBMD$bP8*mLO*t0C(S41!!jdI!=H4CS}wHew%@uOnH0Z!6q zos_8@nng~FU`KAV#e8;03_6sDa8IZg`}c(Z%?KG9_pr}Qda$=!#5pMW2uHg4cCb2Rtjhd*rqmR@Ph0qaA_#**$5H#z#5Q?MJ=)Ym$FZY9?% zS|;<*8MQ}Nr2o58>dbfJE24Op%Qi~X%CMlTv`)|WljBXHaM;DiJZ4^&B916Q8tH`B z-RQPdyR4W#97+6;nfFACb8g?Ma&$S41a8QYlLaXlK!T2Pm6n`mNlVU?fJ$0PA0<^z z7IYK^1HETh;ov5s#Q3_pMbh3JFLrfW89PH2b8Kmi@!LWkWApCXhc|bgSl5s0FiXum zOH6P?>2-|Y#)3HGd^xH%B$d%Dc>UAX!Tv6x z))ph)9&g0hbj*MckS!%+&ZVZ86Ki7DK>eB|4uq?38_etv!)W7hRZ-DI{NAdSd0U7JdT(r6szh|#Qp-YgZ1M|+?autgHT9)N;V z*=+WZE+#tuUAJQfygZURO})$U1FYaQ#g_IR^PIiC=Z=LB^;7cX?RSkMaK*8%=h7iu z$&u;eD}s+&3PSZCR;TJP(Ld&?*l!q;$yn^<>-AoQo`O)_u}9EtJ04EOEg&L9^MlNm z5eV1hasGwKu@{1_2Pb>;wVWSXVm4G{EX(7_nALToi@caH&RYn`95W@l$;-i zzxg}V%;`a%N@yzc?8*kM#E35mz2wCZcn2UTS7|4!+U+>gA*tq?>zzglk-I){VpB1! zv@IRN)o`N7;Ek}O`C$`Ii$&tzr`v{YKBS2?pR^Js(v*Jz`me?trK~fp%{Z8;)?)BvYwS>KaG-Vg!;)==52-4{=z4A@{ucqO zr$jL4JyU49Nq10&i%vjed#)3d+T8q2FnBT)9piD=py#!GLdYRlBx)gCVJAACE-2@9 z_|1WtF~PR8^M)a_FACc|?wxgSmv$j-rr)dWLl4yybP%YO0g=3!ZBa!thC}mtHPA*# zi(Yc}ks^)!S2T~f{lcyUH02Assd8V~RWSGh_g^XeM*g3w;SU8j - - - - - Welcome to Interface - - - - - -
-
-

Move around

- Move around -

- Move around with WASD & fly
- up or down with E & C.
- Cmnd/Ctrl+G will send you
- home. Hitting Enter will let you
- teleport to a user or location. -

-
-
-

Listen & talk

- Talk -

- Use your best headphones
- and microphone for high
- fidelity audio. -

-
-
-

Connect devices

- Connect devices -

- Have an Oculus Rift, a Razer
- Hydra, or a PrimeSense 3D
- camera? We support them all. -

-
-
-

Run a script

- Run a script -

- Cmnd/Cntrl+J will launch a
- Running Scripts dialog to help
- manage your scripts and search
- for new ones to run. -

-
-
-

Script something

- Write a script -

- Write a script; we're always
- adding new features.
- Cmnd/Cntrl+J will launch a
- Running Scripts dialog to help
- manage your scripts. -

-
-
-

Import models

- Import models -

- Use the edit.js script to
- add FBX models in-world. You
- can use grids and fine tune
- placement-related parameters
- with ease. -

-
-
-
-

Read the docs

-

- We are writing documentation on
- just about everything. Please,
- devour all we've written and make
- suggestions where necessary.
- Documentation is always at
- docs.highfidelity.com -

-
-
-
- - - - - diff --git a/interface/resources/icons/load-script.svg b/interface/resources/icons/load-script.svg deleted file mode 100644 index 21be61c321..0000000000 --- a/interface/resources/icons/load-script.svg +++ /dev/null @@ -1,125 +0,0 @@ - - - - - - - - - - image/svg+xml - - - - - T.Hofmeister - - - - - - - - - - - - - - - - - - - - - - diff --git a/interface/resources/icons/new-script.svg b/interface/resources/icons/new-script.svg deleted file mode 100644 index f68fcfa967..0000000000 --- a/interface/resources/icons/new-script.svg +++ /dev/null @@ -1,129 +0,0 @@ - - - - - - - - - - image/svg+xml - - - - - T.Hofmeister - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/interface/resources/icons/save-script.svg b/interface/resources/icons/save-script.svg deleted file mode 100644 index 04d41b8302..0000000000 --- a/interface/resources/icons/save-script.svg +++ /dev/null @@ -1,674 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - T.Hofmeister - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/interface/resources/icons/start-script.svg b/interface/resources/icons/start-script.svg deleted file mode 100644 index 994eb61efe..0000000000 --- a/interface/resources/icons/start-script.svg +++ /dev/null @@ -1,550 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - Maximillian Merlin - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/interface/resources/icons/stop-script.svg b/interface/resources/icons/stop-script.svg deleted file mode 100644 index 31cdcee749..0000000000 --- a/interface/resources/icons/stop-script.svg +++ /dev/null @@ -1,163 +0,0 @@ - - - - - - - - - - image/svg+xml - - - - - Maximillian Merlin - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/interface/resources/qml/AssetServer.qml b/interface/resources/qml/AssetServer.qml index cf61a2ae4a..6f3076b408 100644 --- a/interface/resources/qml/AssetServer.qml +++ b/interface/resources/qml/AssetServer.qml @@ -206,7 +206,7 @@ ScrollingWindow { print("Error: model cannot be both static mesh and dynamic. This should never happen."); } else if (url) { var name = assetProxyModel.data(treeView.selection.currentIndex); - var addPosition = Vec3.sum(MyAvatar.position, Vec3.multiply(2, Quat.getFront(MyAvatar.orientation))); + var addPosition = Vec3.sum(MyAvatar.position, Vec3.multiply(2, Quat.getForward(MyAvatar.orientation))); var gravity; if (dynamic) { // Create a vector <0, -10, 0>. { x: 0, y: -10, z: 0 } won't work because Qt is dumb and this is a diff --git a/interface/resources/qml/AvatarInputs.qml b/interface/resources/qml/AvatarInputs.qml index 384504aaa0..28f3c0c7b9 100644 --- a/interface/resources/qml/AvatarInputs.qml +++ b/interface/resources/qml/AvatarInputs.qml @@ -15,12 +15,11 @@ import Qt.labs.settings 1.0 Hifi.AvatarInputs { id: root objectName: "AvatarInputs" - width: mirrorWidth - height: controls.height + mirror.height + width: rootWidth + height: controls.height x: 10; y: 5 - readonly property int mirrorHeight: 215 - readonly property int mirrorWidth: 265 + readonly property int rootWidth: 265 readonly property int iconSize: 24 readonly property int iconPadding: 5 @@ -39,61 +38,15 @@ Hifi.AvatarInputs { anchors.fill: parent } - Item { - id: mirror - width: root.mirrorWidth - height: root.mirrorVisible ? root.mirrorHeight : 0 - visible: root.mirrorVisible - anchors.left: parent.left - clip: true - - Image { - id: closeMirror - visible: hover.containsMouse - width: root.iconSize - height: root.iconSize - anchors.top: parent.top - anchors.topMargin: root.iconPadding - anchors.left: parent.left - anchors.leftMargin: root.iconPadding - source: "../images/close.svg" - MouseArea { - anchors.fill: parent - onClicked: { - root.closeMirror(); - } - } - } - - Image { - id: zoomIn - visible: hover.containsMouse - width: root.iconSize - height: root.iconSize - anchors.bottom: parent.bottom - anchors.bottomMargin: root.iconPadding - anchors.left: parent.left - anchors.leftMargin: root.iconPadding - source: root.mirrorZoomed ? "../images/minus.svg" : "../images/plus.svg" - MouseArea { - anchors.fill: parent - onClicked: { - root.toggleZoom(); - } - } - } - } - Item { id: controls - width: root.mirrorWidth + width: root.rootWidth height: 44 visible: root.showAudioTools - anchors.top: mirror.bottom Rectangle { anchors.fill: parent - color: root.mirrorVisible ? (root.audioClipping ? "red" : "#696969") : "#00000000" + color: "#00000000" Item { id: audioMeter diff --git a/interface/resources/qml/Stats.qml b/interface/resources/qml/Stats.qml index 564c74b526..17e6578e4d 100644 --- a/interface/resources/qml/Stats.qml +++ b/interface/resources/qml/Stats.qml @@ -198,7 +198,7 @@ Item { } StatText { visible: root.expanded; - text: "Audio Out Mic: " + root.audioMicOutboundPPS + " pps, " + + text: "Audio Out Mic: " + root.audioOutboundPPS + " pps, " + "Silent: " + root.audioSilentOutboundPPS + " pps"; } StatText { @@ -266,7 +266,7 @@ Item { text: "GPU Textures: "; } StatText { - text: " Sparse Enabled: " + (0 == root.gpuSparseTextureEnabled ? "false" : "true"); + text: " Pressure State: " + root.gpuTextureMemoryPressureState; } StatText { text: " Count: " + root.gpuTextures; @@ -278,14 +278,10 @@ Item { text: " Decimated: " + root.decimatedTextureCount; } StatText { - text: " Sparse Count: " + root.gpuTexturesSparse; - visible: 0 != root.gpuSparseTextureEnabled; + text: " Pending Transfer: " + root.texturePendingTransfers + " MB"; } StatText { - text: " Virtual Memory: " + root.gpuTextureVirtualMemory + " MB"; - } - StatText { - text: " Commited Memory: " + root.gpuTextureMemory + " MB"; + text: " Resource Memory: " + root.gpuTextureMemory + " MB"; } StatText { text: " Framebuffer Memory: " + root.gpuTextureFramebufferMemory + " MB"; diff --git a/interface/resources/styles/log_dialog.qss b/interface/resources/styles/log_dialog.qss index 1fc4df0717..d3ae4e0a00 100644 --- a/interface/resources/styles/log_dialog.qss +++ b/interface/resources/styles/log_dialog.qss @@ -1,6 +1,6 @@ QPlainTextEdit { - font-family: Inconsolata, Lucida Console, Andale Mono, Monaco; + font-family: Inconsolata, Consolas, Courier New, monospace; font-size: 16px; padding-left: 28px; padding-top: 7px; @@ -11,7 +11,7 @@ QPlainTextEdit { } QLineEdit { - font-family: Inconsolata, Lucida Console, Andale Mono, Monaco; + font-family: Inconsolata, Consolas, Courier New, monospace; padding-left: 7px; background-color: #CCCCCC; border-width: 0; diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 1bb4c64884..78707ee635 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -177,6 +177,8 @@ #include "FrameTimingsScriptingInterface.h" #include #include +#include +#include // On Windows PC, NVidia Optimus laptop, we want to enable NVIDIA GPU // FIXME seems to be broken. @@ -213,18 +215,10 @@ static const QString FBX_EXTENSION = ".fbx"; static const QString OBJ_EXTENSION = ".obj"; static const QString AVA_JSON_EXTENSION = ".ava.json"; -static const int MIRROR_VIEW_TOP_PADDING = 5; -static const int MIRROR_VIEW_LEFT_PADDING = 10; -static const int MIRROR_VIEW_WIDTH = 265; -static const int MIRROR_VIEW_HEIGHT = 215; static const float MIRROR_FULLSCREEN_DISTANCE = 0.389f; -static const float MIRROR_REARVIEW_DISTANCE = 0.722f; -static const float MIRROR_REARVIEW_BODY_DISTANCE = 2.56f; -static const float MIRROR_FIELD_OF_VIEW = 30.0f; static const quint64 TOO_LONG_SINCE_LAST_SEND_DOWNSTREAM_AUDIO_STATS = 1 * USECS_PER_SECOND; -static const QString INFO_WELCOME_PATH = "html/interface-welcome.html"; static const QString INFO_EDIT_ENTITIES_PATH = "html/edit-commands.html"; static const QString INFO_HELP_PATH = "html/help.html"; @@ -423,6 +417,7 @@ static const QString STATE_CAMERA_THIRD_PERSON = "CameraThirdPerson"; static const QString STATE_CAMERA_ENTITY = "CameraEntity"; static const QString STATE_CAMERA_INDEPENDENT = "CameraIndependent"; static const QString STATE_SNAP_TURN = "SnapTurn"; +static const QString STATE_ADVANCED_MOVEMENT_CONTROLS = "AdvancedMovement"; static const QString STATE_GROUNDED = "Grounded"; static const QString STATE_NAV_FOCUSED = "NavigationFocused"; @@ -513,7 +508,7 @@ bool setupEssentials(int& argc, char** argv) { DependencyManager::set(); controller::StateController::setStateVariables({ { STATE_IN_HMD, STATE_CAMERA_FULL_SCREEN_MIRROR, STATE_CAMERA_FIRST_PERSON, STATE_CAMERA_THIRD_PERSON, STATE_CAMERA_ENTITY, STATE_CAMERA_INDEPENDENT, - STATE_SNAP_TURN, STATE_GROUNDED, STATE_NAV_FOCUSED } }); + STATE_SNAP_TURN, STATE_ADVANCED_MOVEMENT_CONTROLS, STATE_GROUNDED, STATE_NAV_FOCUSED } }); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); @@ -565,7 +560,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo _entityClipboardRenderer(false, this, this), _entityClipboard(new EntityTree()), _lastQueriedTime(usecTimestampNow()), - _mirrorViewRect(QRect(MIRROR_VIEW_LEFT_PADDING, MIRROR_VIEW_TOP_PADDING, MIRROR_VIEW_WIDTH, MIRROR_VIEW_HEIGHT)), _previousScriptLocation("LastScriptLocation", DESKTOP_LOCATION), _fieldOfView("fieldOfView", DEFAULT_FIELD_OF_VIEW_DEGREES), _hmdTabletScale("hmdTabletScale", DEFAULT_HMD_TABLET_SCALE_PERCENT), @@ -746,23 +740,24 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo } }); - auto& audioScriptingInterface = AudioScriptingInterface::getInstance(); + auto audioScriptingInterface = DependencyManager::set(); connect(audioThread, &QThread::started, audioIO.data(), &AudioClient::start); connect(audioIO.data(), &AudioClient::destroyed, audioThread, &QThread::quit); connect(audioThread, &QThread::finished, audioThread, &QThread::deleteLater); connect(audioIO.data(), &AudioClient::muteToggled, this, &Application::audioMuteToggled); - connect(audioIO.data(), &AudioClient::mutedByMixer, &audioScriptingInterface, &AudioScriptingInterface::mutedByMixer); - connect(audioIO.data(), &AudioClient::receivedFirstPacket, &audioScriptingInterface, &AudioScriptingInterface::receivedFirstPacket); - connect(audioIO.data(), &AudioClient::disconnected, &audioScriptingInterface, &AudioScriptingInterface::disconnected); + connect(audioIO.data(), &AudioClient::mutedByMixer, audioScriptingInterface.data(), &AudioScriptingInterface::mutedByMixer); + connect(audioIO.data(), &AudioClient::receivedFirstPacket, audioScriptingInterface.data(), &AudioScriptingInterface::receivedFirstPacket); + connect(audioIO.data(), &AudioClient::disconnected, audioScriptingInterface.data(), &AudioScriptingInterface::disconnected); connect(audioIO.data(), &AudioClient::muteEnvironmentRequested, [](glm::vec3 position, float radius) { auto audioClient = DependencyManager::get(); + auto audioScriptingInterface = DependencyManager::get(); auto myAvatarPosition = DependencyManager::get()->getMyAvatar()->getPosition(); float distance = glm::distance(myAvatarPosition, position); bool shouldMute = !audioClient->isMuted() && (distance < radius); if (shouldMute) { audioClient->toggleMute(); - AudioScriptingInterface::getInstance().environmentMuted(); + audioScriptingInterface->environmentMuted(); } }); @@ -1129,6 +1124,10 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo _applicationStateDevice->setInputVariant(STATE_SNAP_TURN, []() -> float { return qApp->getMyAvatar()->getSnapTurn() ? 1 : 0; }); + _applicationStateDevice->setInputVariant(STATE_ADVANCED_MOVEMENT_CONTROLS, []() -> float { + return qApp->getMyAvatar()->useAdvancedMovementControls() ? 1 : 0; + }); + _applicationStateDevice->setInputVariant(STATE_GROUNDED, []() -> float { return qApp->getMyAvatar()->getCharacterController()->onGround() ? 1 : 0; }); @@ -1183,10 +1182,10 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo // set the local loopback interface for local sounds AudioInjector::setLocalAudioInterface(audioIO.data()); - AudioScriptingInterface::getInstance().setLocalAudioInterface(audioIO.data()); - connect(audioIO.data(), &AudioClient::noiseGateOpened, &AudioScriptingInterface::getInstance(), &AudioScriptingInterface::noiseGateOpened); - connect(audioIO.data(), &AudioClient::noiseGateClosed, &AudioScriptingInterface::getInstance(), &AudioScriptingInterface::noiseGateClosed); - connect(audioIO.data(), &AudioClient::inputReceived, &AudioScriptingInterface::getInstance(), &AudioScriptingInterface::inputReceived); + audioScriptingInterface->setLocalAudioInterface(audioIO.data()); + connect(audioIO.data(), &AudioClient::noiseGateOpened, audioScriptingInterface.data(), &AudioScriptingInterface::noiseGateOpened); + connect(audioIO.data(), &AudioClient::noiseGateClosed, audioScriptingInterface.data(), &AudioScriptingInterface::noiseGateClosed); + connect(audioIO.data(), &AudioClient::inputReceived, audioScriptingInterface.data(), &AudioScriptingInterface::inputReceived); this->installEventFilter(this); @@ -1445,7 +1444,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo scriptEngines->loadScript(testScript, false); } else { // Get sandbox content set version, if available - auto acDirPath = PathUtils::getRootDataDirectory() + BuildInfo::MODIFIED_ORGANIZATION + "/assignment-client/"; + auto acDirPath = PathUtils::getAppDataPath() + "../../" + BuildInfo::MODIFIED_ORGANIZATION + "/assignment-client/"; auto contentVersionPath = acDirPath + "content-version.txt"; qCDebug(interfaceapp) << "Checking " << contentVersionPath << " for content version"; auto contentVersion = 0; @@ -1951,7 +1950,7 @@ void Application::initializeUi() { // For some reason there is already an "Application" object in the QML context, // though I can't find it. Hence, "ApplicationInterface" rootContext->setContextProperty("ApplicationInterface", this); - rootContext->setContextProperty("Audio", &AudioScriptingInterface::getInstance()); + rootContext->setContextProperty("Audio", DependencyManager::get().data()); rootContext->setContextProperty("AudioStats", DependencyManager::get()->getStats().data()); rootContext->setContextProperty("AudioScope", DependencyManager::get().data()); @@ -2119,21 +2118,6 @@ void Application::paintGL() { batch.resetStages(); }); - auto inputs = AvatarInputs::getInstance(); - if (inputs->mirrorVisible()) { - PerformanceTimer perfTimer("Mirror"); - - renderArgs._renderMode = RenderArgs::MIRROR_RENDER_MODE; - renderArgs._blitFramebuffer = DependencyManager::get()->getSelfieFramebuffer(); - - _mirrorViewRect.moveTo(inputs->x(), inputs->y()); - - renderRearViewMirror(&renderArgs, _mirrorViewRect, inputs->mirrorZoomed()); - - renderArgs._blitFramebuffer.reset(); - renderArgs._renderMode = RenderArgs::DEFAULT_RENDER_MODE; - } - { PerformanceTimer perfTimer("renderOverlay"); // NOTE: There is no batch associated with this renderArgs @@ -2148,7 +2132,7 @@ void Application::paintGL() { PerformanceTimer perfTimer("CameraUpdates"); auto myAvatar = getMyAvatar(); - boomOffset = myAvatar->getScale() * myAvatar->getBoomLength() * -IDENTITY_FRONT; + boomOffset = myAvatar->getScale() * myAvatar->getBoomLength() * -IDENTITY_FORWARD; if (_myCamera.getMode() == CAMERA_MODE_FIRST_PERSON || _myCamera.getMode() == CAMERA_MODE_THIRD_PERSON) { Menu::getInstance()->setIsOptionChecked(MenuOption::FirstPerson, myAvatar->getBoomLength() <= MyAvatar::ZOOM_MIN); @@ -2381,10 +2365,6 @@ void Application::setSettingConstrainToolbarPosition(bool setting) { DependencyManager::get()->setConstrainToolbarToCenterX(setting); } -void Application::aboutApp() { - InfoView::show(INFO_WELCOME_PATH); -} - void Application::showHelp() { static const QString HAND_CONTROLLER_NAME_VIVE = "vive"; static const QString HAND_CONTROLLER_NAME_OCULUS_TOUCH = "oculus"; @@ -2766,8 +2746,6 @@ void Application::keyPressEvent(QKeyEvent* event) { case Qt::Key_S: if (isShifted && isMeta && !isOption) { Menu::getInstance()->triggerOption(MenuOption::SuppressShortTimings); - } else if (isOption && !isShifted && !isMeta) { - Menu::getInstance()->triggerOption(MenuOption::ScriptEditor); } else if (!isOption && !isShifted && isMeta) { takeSnapshot(true); } @@ -2886,51 +2864,49 @@ void Application::keyPressEvent(QKeyEvent* event) { break; #endif - case Qt::Key_H: - if (isShifted) { - Menu::getInstance()->triggerOption(MenuOption::MiniMirror); - } else { - // whenever switching to/from full screen mirror from the keyboard, remember - // the state you were in before full screen mirror, and return to that. - auto previousMode = _myCamera.getMode(); - if (previousMode != CAMERA_MODE_MIRROR) { - switch (previousMode) { - case CAMERA_MODE_FIRST_PERSON: - _returnFromFullScreenMirrorTo = MenuOption::FirstPerson; - break; - case CAMERA_MODE_THIRD_PERSON: - _returnFromFullScreenMirrorTo = MenuOption::ThirdPerson; - break; + case Qt::Key_H: { + // whenever switching to/from full screen mirror from the keyboard, remember + // the state you were in before full screen mirror, and return to that. + auto previousMode = _myCamera.getMode(); + if (previousMode != CAMERA_MODE_MIRROR) { + switch (previousMode) { + case CAMERA_MODE_FIRST_PERSON: + _returnFromFullScreenMirrorTo = MenuOption::FirstPerson; + break; + case CAMERA_MODE_THIRD_PERSON: + _returnFromFullScreenMirrorTo = MenuOption::ThirdPerson; + break; - // FIXME - it's not clear that these modes make sense to return to... - case CAMERA_MODE_INDEPENDENT: - _returnFromFullScreenMirrorTo = MenuOption::IndependentMode; - break; - case CAMERA_MODE_ENTITY: - _returnFromFullScreenMirrorTo = MenuOption::CameraEntityMode; - break; + // FIXME - it's not clear that these modes make sense to return to... + case CAMERA_MODE_INDEPENDENT: + _returnFromFullScreenMirrorTo = MenuOption::IndependentMode; + break; + case CAMERA_MODE_ENTITY: + _returnFromFullScreenMirrorTo = MenuOption::CameraEntityMode; + break; - default: - _returnFromFullScreenMirrorTo = MenuOption::ThirdPerson; - break; - } + default: + _returnFromFullScreenMirrorTo = MenuOption::ThirdPerson; + break; } - - bool isMirrorChecked = Menu::getInstance()->isOptionChecked(MenuOption::FullscreenMirror); - Menu::getInstance()->setIsOptionChecked(MenuOption::FullscreenMirror, !isMirrorChecked); - if (isMirrorChecked) { - - // if we got here without coming in from a non-Full Screen mirror case, then our - // _returnFromFullScreenMirrorTo is unknown. In that case we'll go to the old - // behavior of returning to ThirdPerson - if (_returnFromFullScreenMirrorTo.isEmpty()) { - _returnFromFullScreenMirrorTo = MenuOption::ThirdPerson; - } - Menu::getInstance()->setIsOptionChecked(_returnFromFullScreenMirrorTo, true); - } - cameraMenuChanged(); } + + bool isMirrorChecked = Menu::getInstance()->isOptionChecked(MenuOption::FullscreenMirror); + Menu::getInstance()->setIsOptionChecked(MenuOption::FullscreenMirror, !isMirrorChecked); + if (isMirrorChecked) { + + // if we got here without coming in from a non-Full Screen mirror case, then our + // _returnFromFullScreenMirrorTo is unknown. In that case we'll go to the old + // behavior of returning to ThirdPerson + if (_returnFromFullScreenMirrorTo.isEmpty()) { + _returnFromFullScreenMirrorTo = MenuOption::ThirdPerson; + } + Menu::getInstance()->setIsOptionChecked(_returnFromFullScreenMirrorTo, true); + } + cameraMenuChanged(); break; + } + case Qt::Key_P: { if (!(isShifted || isMeta || isOption)) { bool isFirstPersonChecked = Menu::getInstance()->isOptionChecked(MenuOption::FirstPerson); @@ -3845,8 +3821,6 @@ void Application::init() { DependencyManager::get()->init(); _myCamera.setMode(CAMERA_MODE_FIRST_PERSON); - _mirrorCamera.setMode(CAMERA_MODE_MIRROR); - _timerStart.start(); _lastTimeUpdated.start(); @@ -3981,7 +3955,7 @@ void Application::updateMyAvatarLookAtPosition() { auto lookingAtHead = static_pointer_cast(lookingAt)->getHead(); const float MAXIMUM_FACE_ANGLE = 65.0f * RADIANS_PER_DEGREE; - glm::vec3 lookingAtFaceOrientation = lookingAtHead->getFinalOrientationInWorldFrame() * IDENTITY_FRONT; + glm::vec3 lookingAtFaceOrientation = lookingAtHead->getFinalOrientationInWorldFrame() * IDENTITY_FORWARD; glm::vec3 fromLookingAtToMe = glm::normalize(myAvatar->getHead()->getEyePosition() - lookingAtHead->getEyePosition()); float faceAngle = glm::angle(lookingAtFaceOrientation, fromLookingAtToMe); @@ -4383,16 +4357,16 @@ void Application::update(float deltaTime) { myAvatar->clearDriveKeys(); if (_myCamera.getMode() != CAMERA_MODE_INDEPENDENT) { if (!_controllerScriptingInterface->areActionsCaptured()) { - myAvatar->setDriveKeys(TRANSLATE_Z, -1.0f * userInputMapper->getActionState(controller::Action::TRANSLATE_Z)); - myAvatar->setDriveKeys(TRANSLATE_Y, userInputMapper->getActionState(controller::Action::TRANSLATE_Y)); - myAvatar->setDriveKeys(TRANSLATE_X, userInputMapper->getActionState(controller::Action::TRANSLATE_X)); + myAvatar->setDriveKey(MyAvatar::TRANSLATE_Z, -1.0f * userInputMapper->getActionState(controller::Action::TRANSLATE_Z)); + myAvatar->setDriveKey(MyAvatar::TRANSLATE_Y, userInputMapper->getActionState(controller::Action::TRANSLATE_Y)); + myAvatar->setDriveKey(MyAvatar::TRANSLATE_X, userInputMapper->getActionState(controller::Action::TRANSLATE_X)); if (deltaTime > FLT_EPSILON) { - myAvatar->setDriveKeys(PITCH, -1.0f * userInputMapper->getActionState(controller::Action::PITCH)); - myAvatar->setDriveKeys(YAW, -1.0f * userInputMapper->getActionState(controller::Action::YAW)); - myAvatar->setDriveKeys(STEP_YAW, -1.0f * userInputMapper->getActionState(controller::Action::STEP_YAW)); + myAvatar->setDriveKey(MyAvatar::PITCH, -1.0f * userInputMapper->getActionState(controller::Action::PITCH)); + myAvatar->setDriveKey(MyAvatar::YAW, -1.0f * userInputMapper->getActionState(controller::Action::YAW)); + myAvatar->setDriveKey(MyAvatar::STEP_YAW, -1.0f * userInputMapper->getActionState(controller::Action::STEP_YAW)); } } - myAvatar->setDriveKeys(ZOOM, userInputMapper->getActionState(controller::Action::TRANSLATE_CAMERA_Z)); + myAvatar->setDriveKey(MyAvatar::ZOOM, userInputMapper->getActionState(controller::Action::TRANSLATE_CAMERA_Z)); } controller::Pose leftHandPose = userInputMapper->getPoseState(controller::Action::LEFT_HAND); @@ -4463,9 +4437,12 @@ void Application::update(float deltaTime) { getEntities()->getTree()->withWriteLock([&] { PerformanceTimer perfTimer("handleOutgoingChanges"); - const VectorOfMotionStates& outgoingChanges = _physicsEngine->getOutgoingChanges(); - _entitySimulation->handleOutgoingChanges(outgoingChanges); - avatarManager->handleOutgoingChanges(outgoingChanges); + const VectorOfMotionStates& deactivations = _physicsEngine->getDeactivatedMotionStates(); + _entitySimulation->handleDeactivatedMotionStates(deactivations); + + const VectorOfMotionStates& outgoingChanges = _physicsEngine->getChangedMotionStates(); + _entitySimulation->handleChangedMotionStates(outgoingChanges); + avatarManager->handleChangedMotionStates(outgoingChanges); }); if (!_aboutToQuit) { @@ -5122,58 +5099,6 @@ void Application::displaySide(RenderArgs* renderArgs, Camera& theCamera, bool se activeRenderingThread = nullptr; } -void Application::renderRearViewMirror(RenderArgs* renderArgs, const QRect& region, bool isZoomed) { - auto originalViewport = renderArgs->_viewport; - // Grab current viewport to reset it at the end - - float aspect = (float)region.width() / region.height(); - float fov = MIRROR_FIELD_OF_VIEW; - - auto myAvatar = getMyAvatar(); - - // bool eyeRelativeCamera = false; - if (!isZoomed) { - _mirrorCamera.setPosition(myAvatar->getChestPosition() + - myAvatar->getOrientation() * glm::vec3(0.0f, 0.0f, -1.0f) * MIRROR_REARVIEW_BODY_DISTANCE * myAvatar->getScale()); - - } else { // HEAD zoom level - // FIXME note that the positioning of the camera relative to the avatar can suffer limited - // precision as the user's position moves further away from the origin. Thus at - // /1e7,1e7,1e7 (well outside the buildable volume) the mirror camera veers and sways - // wildly as you rotate your avatar because the floating point values are becoming - // larger, squeezing out the available digits of precision you have available at the - // human scale for camera positioning. - - // Previously there was a hack to correct this using the mechanism of repositioning - // the avatar at the origin of the world for the purposes of rendering the mirror, - // but it resulted in failing to render the avatar's head model in the mirror view - // when in first person mode. Presumably this was because of some missed culling logic - // that was not accounted for in the hack. - - // This was removed in commit 71e59cfa88c6563749594e25494102fe01db38e9 but could be further - // investigated in order to adapt the technique while fixing the head rendering issue, - // but the complexity of the hack suggests that a better approach - _mirrorCamera.setPosition(myAvatar->getDefaultEyePosition() + - myAvatar->getOrientation() * glm::vec3(0.0f, 0.0f, -1.0f) * MIRROR_REARVIEW_DISTANCE * myAvatar->getScale()); - } - _mirrorCamera.setProjection(glm::perspective(glm::radians(fov), aspect, DEFAULT_NEAR_CLIP, DEFAULT_FAR_CLIP)); - _mirrorCamera.setOrientation(myAvatar->getWorldAlignedOrientation() * glm::quat(glm::vec3(0.0f, PI, 0.0f))); - - - // set the bounds of rear mirror view - // the region is in device independent coordinates; must convert to device - float ratio = (float)QApplication::desktop()->windowHandle()->devicePixelRatio() * getRenderResolutionScale(); - int width = region.width() * ratio; - int height = region.height() * ratio; - gpu::Vec4i viewport = gpu::Vec4i(0, 0, width, height); - renderArgs->_viewport = viewport; - - // render rear mirror view - displaySide(renderArgs, _mirrorCamera, true); - - renderArgs->_viewport = originalViewport; -} - void Application::resetSensors(bool andReload) { DependencyManager::get()->reset(); DependencyManager::get()->reset(); @@ -5503,8 +5428,7 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri scriptEngine->registerGlobalObject("Rates", new RatesScriptingInterface(this)); // hook our avatar and avatar hash map object into this script engine - scriptEngine->registerGlobalObject("MyAvatar", getMyAvatar().get()); - qScriptRegisterMetaType(scriptEngine, audioListenModeToScriptValue, audioListenModeFromScriptValue); + getMyAvatar()->registerMetaTypes(scriptEngine); scriptEngine->registerGlobalObject("AvatarList", DependencyManager::get().data()); diff --git a/interface/src/Application.h b/interface/src/Application.h index 98080783a6..7ae4160f8b 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -72,6 +72,8 @@ #include #include +#include + class OffscreenGLCanvas; class GLCanvas; @@ -276,8 +278,6 @@ public: virtual void pushPostUpdateLambda(void* key, std::function func) override; - const QRect& getMirrorViewRect() const { return _mirrorViewRect; } - void updateMyAvatarLookAtPosition(); float getAvatarSimrate() const { return _avatarSimCounter.rate(); } @@ -368,7 +368,6 @@ public slots: void calibrateEyeTracker5Points(); #endif - void aboutApp(); static void showHelp(); void cycleCamera(); @@ -557,8 +556,6 @@ private: int _avatarSimsPerSecondReport {0}; quint64 _lastAvatarSimsPerSecondUpdate {0}; Camera _myCamera; // My view onto the world - Camera _mirrorCamera; // Camera for mirror view - QRect _mirrorViewRect; Setting::Handle _previousScriptLocation; Setting::Handle _fieldOfView; diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index beacbaccab..a48ee4e7db 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -74,9 +74,6 @@ Menu::Menu() { // File > Help addActionToQMenuAndActionHash(fileMenu, MenuOption::Help, 0, qApp, SLOT(showHelp())); - // File > About - addActionToQMenuAndActionHash(fileMenu, MenuOption::AboutApp, 0, qApp, SLOT(aboutApp()), QAction::AboutRole); - // File > Quit addActionToQMenuAndActionHash(fileMenu, MenuOption::Quit, Qt::CTRL | Qt::Key_Q, qApp, SLOT(quit()), QAction::QuitRole); @@ -120,11 +117,6 @@ Menu::Menu() { scriptEngines.data(), SLOT(reloadAllScripts()), QAction::NoRole, UNSPECIFIED_POSITION, "Advanced"); - // Edit > Scripts Editor... [advanced] - addActionToQMenuAndActionHash(editMenu, MenuOption::ScriptEditor, Qt::ALT | Qt::Key_S, - dialogsManager.data(), SLOT(showScriptEditor()), - QAction::NoRole, UNSPECIFIED_POSITION, "Advanced"); - // Edit > Console... [advanced] addActionToQMenuAndActionHash(editMenu, MenuOption::Console, Qt::CTRL | Qt::ALT | Qt::Key_J, DependencyManager::get().data(), @@ -249,9 +241,6 @@ Menu::Menu() { viewMenu->addSeparator(); - // View > Mini Mirror - addCheckableActionToQMenuAndActionHash(viewMenu, MenuOption::MiniMirror, 0, false); - // View > Center Player In View addCheckableActionToQMenuAndActionHash(viewMenu, MenuOption::CenterPlayerInView, 0, true, qApp, SLOT(rotationModeChanged()), @@ -417,6 +406,9 @@ Menu::Menu() { } // Developer > Assets >>> + // Menu item is not currently needed but code should be kept in case it proves useful again at some stage. +//#define WANT_ASSET_MIGRATION +#ifdef WANT_ASSET_MIGRATION MenuWrapper* assetDeveloperMenu = developerMenu->addMenu("Assets"); auto& atpMigrator = ATPAssetMigrator::getInstance(); atpMigrator.setDialogParent(this); @@ -424,6 +416,7 @@ Menu::Menu() { addActionToQMenuAndActionHash(assetDeveloperMenu, MenuOption::AssetMigration, 0, &atpMigrator, SLOT(loadEntityServerFile())); +#endif // Developer > Avatar >>> MenuWrapper* avatarDebugMenu = developerMenu->addMenu("Avatar"); @@ -554,16 +547,14 @@ Menu::Menu() { "NetworkingPreferencesDialog"); }); addActionToQMenuAndActionHash(networkMenu, MenuOption::ReloadContent, 0, qApp, SLOT(reloadResourceCaches())); + addActionToQMenuAndActionHash(networkMenu, MenuOption::ClearDiskCache, 0, + DependencyManager::get().data(), SLOT(clearCache())); addCheckableActionToQMenuAndActionHash(networkMenu, MenuOption::DisableActivityLogger, 0, false, &UserActivityLogger::getInstance(), SLOT(disable(bool))); - addActionToQMenuAndActionHash(networkMenu, MenuOption::CachesSize, 0, - dialogsManager.data(), SLOT(cachesSizeDialog())); - addActionToQMenuAndActionHash(networkMenu, MenuOption::DiskCacheEditor, 0, - dialogsManager.data(), SLOT(toggleDiskCacheEditor())); addActionToQMenuAndActionHash(networkMenu, MenuOption::ShowDSConnectTable, 0, dialogsManager.data(), SLOT(showDomainConnectionDialog())); diff --git a/interface/src/Menu.h b/interface/src/Menu.h index c806ffa9ee..b4eaf56758 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -26,7 +26,6 @@ public: }; namespace MenuOption { - const QString AboutApp = "About Interface"; const QString AddRemoveFriends = "Add/Remove Friends..."; const QString AddressBar = "Show Address Bar"; const QString Animations = "Animations..."; @@ -52,11 +51,11 @@ namespace MenuOption { const QString BinaryEyelidControl = "Binary Eyelid Control"; const QString BookmarkLocation = "Bookmark Location"; const QString Bookmarks = "Bookmarks"; - const QString CachesSize = "RAM Caches Size"; const QString CalibrateCamera = "Calibrate Camera"; const QString CameraEntityMode = "Entity Mode"; const QString CenterPlayerInView = "Center Player In View"; const QString Chat = "Chat..."; + const QString ClearDiskCache = "Clear Disk Cache"; const QString Collisions = "Collisions"; const QString Connexion = "Activate 3D Connexion Devices"; const QString Console = "Console..."; @@ -83,7 +82,6 @@ namespace MenuOption { const QString DisableActivityLogger = "Disable Activity Logger"; const QString DisableEyelidAdjustment = "Disable Eyelid Adjustment"; const QString DisableLightEntities = "Disable Light Entities"; - const QString DiskCacheEditor = "Disk Cache Editor"; const QString DisplayCrashOptions = "Display Crash Options"; const QString DisplayHandTargets = "Show Hand Targets"; const QString DisplayModelBounds = "Display Model Bounds"; @@ -124,7 +122,6 @@ namespace MenuOption { const QString LogExtraTimings = "Log Extra Timing Details"; const QString LowVelocityFilter = "Low Velocity Filter"; const QString MeshVisible = "Draw Mesh"; - const QString MiniMirror = "Mini Mirror"; const QString MuteAudio = "Mute Microphone"; const QString MuteEnvironment = "Mute Environment"; const QString MuteFaceTracking = "Mute Face Tracking"; @@ -169,7 +166,6 @@ namespace MenuOption { const QString RunningScripts = "Running Scripts..."; const QString RunClientScriptTests = "Run Client Script Tests"; const QString RunTimingTests = "Run Timing Tests"; - const QString ScriptEditor = "Script Editor..."; const QString ScriptedMotorControl = "Enable Scripted Motor Control"; const QString SendWrongDSConnectVersion = "Send wrong DS connect version"; const QString SendWrongProtocolVersion = "Send wrong protocol version"; diff --git a/interface/src/avatar/Avatar.h b/interface/src/avatar/Avatar.h index ca4dbd2af8..d4bd03367e 100644 --- a/interface/src/avatar/Avatar.h +++ b/interface/src/avatar/Avatar.h @@ -236,7 +236,6 @@ protected: glm::vec3 getBodyRightDirection() const { return getOrientation() * IDENTITY_RIGHT; } glm::vec3 getBodyUpDirection() const { return getOrientation() * IDENTITY_UP; } - glm::vec3 getBodyFrontDirection() const { return getOrientation() * IDENTITY_FRONT; } glm::quat computeRotationFromBodyToWorldUp(float proportion = 1.0f) const; void measureMotionDerivatives(float deltaTime); diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 94ce444416..6152148887 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -424,7 +424,7 @@ void AvatarManager::getObjectsToChange(VectorOfMotionStates& result) { } } -void AvatarManager::handleOutgoingChanges(const VectorOfMotionStates& motionStates) { +void AvatarManager::handleChangedMotionStates(const VectorOfMotionStates& motionStates) { // TODO: extract the MyAvatar results once we use a MotionState for it. } diff --git a/interface/src/avatar/AvatarManager.h b/interface/src/avatar/AvatarManager.h index e1f5a3b411..b94f9e6a96 100644 --- a/interface/src/avatar/AvatarManager.h +++ b/interface/src/avatar/AvatarManager.h @@ -70,7 +70,7 @@ public: void getObjectsToRemoveFromPhysics(VectorOfMotionStates& motionStates); void getObjectsToAddToPhysics(VectorOfMotionStates& motionStates); void getObjectsToChange(VectorOfMotionStates& motionStates); - void handleOutgoingChanges(const VectorOfMotionStates& motionStates); + void handleChangedMotionStates(const VectorOfMotionStates& motionStates); void handleCollisionEvents(const CollisionEvents& collisionEvents); Q_INVOKABLE float getAvatarDataRate(const QUuid& sessionID, const QString& rateName = QString("")) const; diff --git a/interface/src/avatar/CauterizedMeshPartPayload.cpp b/interface/src/avatar/CauterizedMeshPartPayload.cpp index c8ec90dcee..c11f92083b 100644 --- a/interface/src/avatar/CauterizedMeshPartPayload.cpp +++ b/interface/src/avatar/CauterizedMeshPartPayload.cpp @@ -20,55 +20,28 @@ using namespace render; CauterizedMeshPartPayload::CauterizedMeshPartPayload(Model* model, int meshIndex, int partIndex, int shapeIndex, const Transform& transform, const Transform& offsetTransform) : ModelMeshPartPayload(model, meshIndex, partIndex, shapeIndex, transform, offsetTransform) {} -void CauterizedMeshPartPayload::updateTransformForSkinnedCauterizedMesh(const Transform& transform, - const QVector& clusterMatrices, - const QVector& cauterizedClusterMatrices) { - _transform = transform; - _cauterizedTransform = transform; - - if (clusterMatrices.size() > 0) { - _worldBound = AABox(); - for (auto& clusterMatrix : clusterMatrices) { - AABox clusterBound = _localBound; - clusterBound.transform(clusterMatrix); - _worldBound += clusterBound; - } - - _worldBound.transform(transform); - if (clusterMatrices.size() == 1) { - _transform = _transform.worldTransform(Transform(clusterMatrices[0])); - if (cauterizedClusterMatrices.size() != 0) { - _cauterizedTransform = _cauterizedTransform.worldTransform(Transform(cauterizedClusterMatrices[0])); - } else { - _cauterizedTransform = _transform; - } - } - } else { - _worldBound = _localBound; - _worldBound.transform(_drawTransform); - } +void CauterizedMeshPartPayload::updateTransformForCauterizedMesh( + const Transform& renderTransform, + const gpu::BufferPointer& buffer) { + _cauterizedTransform = renderTransform; + _cauterizedClusterBuffer = buffer; } void CauterizedMeshPartPayload::bindTransform(gpu::Batch& batch, const render::ShapePipeline::LocationsPointer locations, RenderArgs::RenderMode renderMode) const { // Still relying on the raw data from the model - const Model::MeshState& state = _model->getMeshState(_meshIndex); SkeletonModel* skeleton = static_cast(_model); bool useCauterizedMesh = (renderMode != RenderArgs::RenderMode::SHADOW_RENDER_MODE) && skeleton->getEnableCauterization(); - if (state.clusterBuffer) { - if (useCauterizedMesh) { - const Model::MeshState& cState = skeleton->getCauterizeMeshState(_meshIndex); - batch.setUniformBuffer(ShapePipeline::Slot::BUFFER::SKINNING, cState.clusterBuffer); - } else { - batch.setUniformBuffer(ShapePipeline::Slot::BUFFER::SKINNING, state.clusterBuffer); + if (useCauterizedMesh) { + if (_cauterizedClusterBuffer) { + batch.setUniformBuffer(ShapePipeline::Slot::BUFFER::SKINNING, _cauterizedClusterBuffer); + } + batch.setModelTransform(_cauterizedTransform); + } else { + if (_clusterBuffer) { + batch.setUniformBuffer(ShapePipeline::Slot::BUFFER::SKINNING, _clusterBuffer); } batch.setModelTransform(_transform); - } else { - if (useCauterizedMesh) { - batch.setModelTransform(_cauterizedTransform); - } else { - batch.setModelTransform(_transform); - } } } diff --git a/interface/src/avatar/CauterizedMeshPartPayload.h b/interface/src/avatar/CauterizedMeshPartPayload.h index f4319ead6f..dc88e950c1 100644 --- a/interface/src/avatar/CauterizedMeshPartPayload.h +++ b/interface/src/avatar/CauterizedMeshPartPayload.h @@ -17,12 +17,13 @@ class CauterizedMeshPartPayload : public ModelMeshPartPayload { public: CauterizedMeshPartPayload(Model* model, int meshIndex, int partIndex, int shapeIndex, const Transform& transform, const Transform& offsetTransform); - void updateTransformForSkinnedCauterizedMesh(const Transform& transform, - const QVector& clusterMatrices, - const QVector& cauterizedClusterMatrices); + + void updateTransformForCauterizedMesh(const Transform& renderTransform, const gpu::BufferPointer& buffer); void bindTransform(gpu::Batch& batch, const render::ShapePipeline::LocationsPointer locations, RenderArgs::RenderMode renderMode) const override; + private: + gpu::BufferPointer _cauterizedClusterBuffer; Transform _cauterizedTransform; }; diff --git a/interface/src/avatar/CauterizedModel.cpp b/interface/src/avatar/CauterizedModel.cpp index 1ca87a498a..f479ed9a35 100644 --- a/interface/src/avatar/CauterizedModel.cpp +++ b/interface/src/avatar/CauterizedModel.cpp @@ -26,8 +26,8 @@ CauterizedModel::~CauterizedModel() { } void CauterizedModel::deleteGeometry() { - Model::deleteGeometry(); - _cauterizeMeshStates.clear(); + Model::deleteGeometry(); + _cauterizeMeshStates.clear(); } bool CauterizedModel::updateGeometry() { @@ -41,7 +41,7 @@ bool CauterizedModel::updateGeometry() { _cauterizeMeshStates.append(state); } } - return needsFullUpdate; + return needsFullUpdate; } void CauterizedModel::createVisibleRenderItemSet() { @@ -56,9 +56,9 @@ void CauterizedModel::createVisibleRenderItemSet() { } // We should not have any existing renderItems if we enter this section of code - Q_ASSERT(_modelMeshRenderItemsSet.isEmpty()); + Q_ASSERT(_modelMeshRenderItems.isEmpty()); - _modelMeshRenderItemsSet.clear(); + _modelMeshRenderItems.clear(); Transform transform; transform.setTranslation(_translation); @@ -81,18 +81,18 @@ void CauterizedModel::createVisibleRenderItemSet() { int numParts = (int)mesh->getNumParts(); for (int partIndex = 0; partIndex < numParts; partIndex++) { auto ptr = std::make_shared(this, i, partIndex, shapeID, transform, offset); - _modelMeshRenderItemsSet << std::static_pointer_cast(ptr); + _modelMeshRenderItems << std::static_pointer_cast(ptr); shapeID++; } } } else { - Model::createVisibleRenderItemSet(); + Model::createVisibleRenderItemSet(); } } void CauterizedModel::createCollisionRenderItemSet() { // Temporary HACK: use base class method for now - Model::createCollisionRenderItemSet(); + Model::createCollisionRenderItemSet(); } void CauterizedModel::updateClusterMatrices() { @@ -122,8 +122,8 @@ void CauterizedModel::updateClusterMatrices() { state.clusterBuffer->setSubData(0, state.clusterMatrices.size() * sizeof(glm::mat4), (const gpu::Byte*) state.clusterMatrices.constData()); } - } - } + } + } // as an optimization, don't build cautrizedClusterMatrices if the boneSet is empty. if (!_cauterizeBoneSet.empty()) { @@ -191,6 +191,9 @@ void CauterizedModel::updateRenderItems() { return; } + // lazy update of cluster matrices used for rendering. We need to update them here, so we can correctly update the bounding box. + self->updateClusterMatrices(); + render::ScenePointer scene = AbstractViewStateInterface::instance()->getMain3DScene(); Transform modelTransform; @@ -209,15 +212,22 @@ void CauterizedModel::updateRenderItems() { if (data._model && data._model->isLoaded()) { // Ensure the model geometry was not reset between frames if (deleteGeometryCounter == data._model->getGeometryCounter()) { - // lazy update of cluster matrices used for rendering. We need to update them here, so we can correctly update the bounding box. - data._model->updateClusterMatrices(); - - // update the model transform and bounding box for this render item. + // this stuff identical to what happens in regular Model const Model::MeshState& state = data._model->getMeshState(data._meshIndex); + Transform renderTransform = modelTransform; + if (state.clusterMatrices.size() == 1) { + renderTransform = modelTransform.worldTransform(Transform(state.clusterMatrices[0])); + } + data.updateTransformForSkinnedMesh(renderTransform, modelTransform, state.clusterBuffer); + + // this stuff for cauterized mesh CauterizedModel* cModel = static_cast(data._model); - assert(data._meshIndex < cModel->_cauterizeMeshStates.size()); - const Model::MeshState& cState = cModel->_cauterizeMeshStates.at(data._meshIndex); - data.updateTransformForSkinnedCauterizedMesh(modelTransform, state.clusterMatrices, cState.clusterMatrices); + const Model::MeshState& cState = cModel->getCauterizeMeshState(data._meshIndex); + renderTransform = modelTransform; + if (cState.clusterMatrices.size() == 1) { + renderTransform = modelTransform.worldTransform(Transform(cState.clusterMatrices[0])); + } + data.updateTransformForCauterizedMesh(renderTransform, cState.clusterBuffer); } } }); diff --git a/interface/src/avatar/Head.cpp b/interface/src/avatar/Head.cpp index d7bf2b79bf..f4fb844d9b 100644 --- a/interface/src/avatar/Head.cpp +++ b/interface/src/avatar/Head.cpp @@ -268,7 +268,7 @@ void Head::applyEyelidOffset(glm::quat headOrientation) { return; } - glm::quat eyeRotation = rotationBetween(headOrientation * IDENTITY_FRONT, getLookAtPosition() - _eyePosition); + glm::quat eyeRotation = rotationBetween(headOrientation * IDENTITY_FORWARD, getLookAtPosition() - _eyePosition); eyeRotation = eyeRotation * glm::angleAxis(safeEulerAngles(headOrientation).y, IDENTITY_UP); // Rotation w.r.t. head float eyePitch = safeEulerAngles(eyeRotation).x; @@ -375,7 +375,7 @@ glm::quat Head::getCameraOrientation() const { glm::quat Head::getEyeRotation(const glm::vec3& eyePosition) const { glm::quat orientation = getOrientation(); glm::vec3 lookAtDelta = _lookAtPosition - eyePosition; - return rotationBetween(orientation * IDENTITY_FRONT, lookAtDelta + glm::length(lookAtDelta) * _saccade) * orientation; + return rotationBetween(orientation * IDENTITY_FORWARD, lookAtDelta + glm::length(lookAtDelta) * _saccade) * orientation; } void Head::setFinalPitch(float finalPitch) { diff --git a/interface/src/avatar/Head.h b/interface/src/avatar/Head.h index 3d25c79087..aa801e5eb5 100644 --- a/interface/src/avatar/Head.h +++ b/interface/src/avatar/Head.h @@ -58,14 +58,14 @@ public: const glm::vec3& getSaccade() const { return _saccade; } glm::vec3 getRightDirection() const { return getOrientation() * IDENTITY_RIGHT; } glm::vec3 getUpDirection() const { return getOrientation() * IDENTITY_UP; } - glm::vec3 getFrontDirection() const { return getOrientation() * IDENTITY_FRONT; } + glm::vec3 getForwardDirection() const { return getOrientation() * IDENTITY_FORWARD; } glm::quat getEyeRotation(const glm::vec3& eyePosition) const; const glm::vec3& getRightEyePosition() const { return _rightEyePosition; } const glm::vec3& getLeftEyePosition() const { return _leftEyePosition; } - glm::vec3 getRightEarPosition() const { return _rightEyePosition + (getRightDirection() * EYE_EAR_GAP) + (getFrontDirection() * -EYE_EAR_GAP); } - glm::vec3 getLeftEarPosition() const { return _leftEyePosition + (getRightDirection() * -EYE_EAR_GAP) + (getFrontDirection() * -EYE_EAR_GAP); } + glm::vec3 getRightEarPosition() const { return _rightEyePosition + (getRightDirection() * EYE_EAR_GAP) + (getForwardDirection() * -EYE_EAR_GAP); } + glm::vec3 getLeftEarPosition() const { return _leftEyePosition + (getRightDirection() * -EYE_EAR_GAP) + (getForwardDirection() * -EYE_EAR_GAP); } glm::vec3 getMouthPosition() const { return _eyePosition - getUpDirection() * glm::length(_rightEyePosition - _leftEyePosition); } bool getReturnToCenter() const { return _returnHeadToCenter; } // Do you want head to try to return to center (depends on interface detected) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 969268c549..e0f4b55393 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -104,6 +104,7 @@ MyAvatar::MyAvatar(RigPointer rig) : _eyeContactTarget(LEFT_EYE), _realWorldFieldOfView("realWorldFieldOfView", DEFAULT_REAL_WORLD_FIELD_OF_VIEW_DEGREES), + _useAdvancedMovementControls("advancedMovementForHandControllersIsChecked", false), _hmdSensorMatrix(), _hmdSensorOrientation(), _hmdSensorPosition(), @@ -119,9 +120,7 @@ MyAvatar::MyAvatar(RigPointer rig) : using namespace recording; _skeletonModel->flagAsCauterized(); - for (int i = 0; i < MAX_DRIVE_KEYS; i++) { - _driveKeys[i] = 0.0f; - } + clearDriveKeys(); // Necessary to select the correct slot using SlotType = void(MyAvatar::*)(const glm::vec3&, bool, const glm::quat&, bool); @@ -154,9 +153,12 @@ MyAvatar::MyAvatar(RigPointer rig) : if (recordingInterface->getPlayFromCurrentLocation()) { setRecordingBasis(); } + _wasCharacterControllerEnabled = _characterController.isEnabled(); + _characterController.setEnabled(false); } else { clearRecordingBasis(); useFullAvatarURL(_fullAvatarURLFromPreferences, _fullAvatarModelName); + _characterController.setEnabled(_wasCharacterControllerEnabled); } auto audioIO = DependencyManager::get(); @@ -227,6 +229,21 @@ MyAvatar::~MyAvatar() { _lookAtTargetAvatar.reset(); } +void MyAvatar::registerMetaTypes(QScriptEngine* engine) { + QScriptValue value = engine->newQObject(this, QScriptEngine::QtOwnership, QScriptEngine::ExcludeDeleteLater | QScriptEngine::ExcludeChildObjects); + engine->globalObject().setProperty("MyAvatar", value); + + QScriptValue driveKeys = engine->newObject(); + auto metaEnum = QMetaEnum::fromType(); + for (int i = 0; i < MAX_DRIVE_KEYS; ++i) { + driveKeys.setProperty(metaEnum.key(i), metaEnum.value(i)); + } + engine->globalObject().setProperty("DriveKeys", driveKeys); + + qScriptRegisterMetaType(engine, audioListenModeToScriptValue, audioListenModeFromScriptValue); + qScriptRegisterMetaType(engine, driveKeysToScriptValue, driveKeysFromScriptValue); +} + void MyAvatar::setOrientationVar(const QVariant& newOrientationVar) { Avatar::setOrientation(quatFromVariant(newOrientationVar)); } @@ -459,7 +476,7 @@ void MyAvatar::simulate(float deltaTime) { // When there are no step values, we zero out the last step pulse. // This allows a user to do faster snapping by tapping a control for (int i = STEP_TRANSLATE_X; !stepAction && i <= STEP_YAW; ++i) { - if (_driveKeys[i] != 0.0f) { + if (getDriveKey((DriveKeys)i) != 0.0f) { stepAction = true; } } @@ -1051,7 +1068,7 @@ void MyAvatar::updateLookAtTargetAvatar() { _lookAtTargetAvatar.reset(); _targetAvatarPosition = glm::vec3(0.0f); - glm::vec3 lookForward = getHead()->getFinalOrientationInWorldFrame() * IDENTITY_FRONT; + glm::vec3 lookForward = getHead()->getFinalOrientationInWorldFrame() * IDENTITY_FORWARD; glm::vec3 cameraPosition = qApp->getCamera()->getPosition(); float smallestAngleTo = glm::radians(DEFAULT_FIELD_OF_VIEW_DEGREES) / 2.0f; @@ -1652,7 +1669,7 @@ bool MyAvatar::shouldRenderHead(const RenderArgs* renderArgs) const { void MyAvatar::updateOrientation(float deltaTime) { // Smoothly rotate body with arrow keys - float targetSpeed = _driveKeys[YAW] * _yawSpeed; + float targetSpeed = getDriveKey(YAW) * _yawSpeed; if (targetSpeed != 0.0f) { const float ROTATION_RAMP_TIMESCALE = 0.1f; float blend = deltaTime / ROTATION_RAMP_TIMESCALE; @@ -1681,8 +1698,8 @@ void MyAvatar::updateOrientation(float deltaTime) { // Comfort Mode: If you press any of the left/right rotation drive keys or input, you'll // get an instantaneous 15 degree turn. If you keep holding the key down you'll get another // snap turn every half second. - if (_driveKeys[STEP_YAW] != 0.0f) { - totalBodyYaw += _driveKeys[STEP_YAW]; + if (getDriveKey(STEP_YAW) != 0.0f) { + totalBodyYaw += getDriveKey(STEP_YAW); } // use head/HMD orientation to turn while flying @@ -1719,7 +1736,7 @@ void MyAvatar::updateOrientation(float deltaTime) { // update body orientation by movement inputs setOrientation(getOrientation() * glm::quat(glm::radians(glm::vec3(0.0f, totalBodyYaw, 0.0f)))); - getHead()->setBasePitch(getHead()->getBasePitch() + _driveKeys[PITCH] * _pitchSpeed * deltaTime); + getHead()->setBasePitch(getHead()->getBasePitch() + getDriveKey(PITCH) * _pitchSpeed * deltaTime); if (qApp->isHMDMode()) { glm::quat orientation = glm::quat_cast(getSensorToWorldMatrix()) * getHMDSensorOrientation(); @@ -1753,14 +1770,14 @@ void MyAvatar::updateActionMotor(float deltaTime) { } // compute action input - glm::vec3 front = (_driveKeys[TRANSLATE_Z]) * IDENTITY_FRONT; - glm::vec3 right = (_driveKeys[TRANSLATE_X]) * IDENTITY_RIGHT; + glm::vec3 forward = (getDriveKey(TRANSLATE_Z)) * IDENTITY_FORWARD; + glm::vec3 right = (getDriveKey(TRANSLATE_X)) * IDENTITY_RIGHT; - glm::vec3 direction = front + right; + glm::vec3 direction = forward + right; CharacterController::State state = _characterController.getState(); if (state == CharacterController::State::Hover) { // we're flying --> support vertical motion - glm::vec3 up = (_driveKeys[TRANSLATE_Y]) * IDENTITY_UP; + glm::vec3 up = (getDriveKey(TRANSLATE_Y)) * IDENTITY_UP; direction += up; } @@ -1799,7 +1816,7 @@ void MyAvatar::updateActionMotor(float deltaTime) { _actionMotorVelocity = MAX_WALKING_SPEED * direction; } - float boomChange = _driveKeys[ZOOM]; + float boomChange = getDriveKey(ZOOM); _boomLength += 2.0f * _boomLength * boomChange + boomChange * boomChange; _boomLength = glm::clamp(_boomLength, ZOOM_MIN, ZOOM_MAX); } @@ -1830,11 +1847,11 @@ void MyAvatar::updatePosition(float deltaTime) { } // capture the head rotation, in sensor space, when the user first indicates they would like to move/fly. - if (!_hoverReferenceCameraFacingIsCaptured && (fabs(_driveKeys[TRANSLATE_Z]) > 0.1f || fabs(_driveKeys[TRANSLATE_X]) > 0.1f)) { + if (!_hoverReferenceCameraFacingIsCaptured && (fabs(getDriveKey(TRANSLATE_Z)) > 0.1f || fabs(getDriveKey(TRANSLATE_X)) > 0.1f)) { _hoverReferenceCameraFacingIsCaptured = true; // transform the camera facing vector into sensor space. _hoverReferenceCameraFacing = transformVectorFast(glm::inverse(_sensorToWorldMatrix), getHead()->getCameraOrientation() * Vectors::UNIT_Z); - } else if (_hoverReferenceCameraFacingIsCaptured && (fabs(_driveKeys[TRANSLATE_Z]) <= 0.1f && fabs(_driveKeys[TRANSLATE_X]) <= 0.1f)) { + } else if (_hoverReferenceCameraFacingIsCaptured && (fabs(getDriveKey(TRANSLATE_Z)) <= 0.1f && fabs(getDriveKey(TRANSLATE_X)) <= 0.1f)) { _hoverReferenceCameraFacingIsCaptured = false; } } @@ -2036,7 +2053,7 @@ void MyAvatar::goToLocation(const glm::vec3& newPosition, // move the user a couple units away const float DISTANCE_TO_USER = 2.0f; - _goToPosition = newPosition - quatOrientation * IDENTITY_FRONT * DISTANCE_TO_USER; + _goToPosition = newPosition - quatOrientation * IDENTITY_FORWARD * DISTANCE_TO_USER; } _goToOrientation = quatOrientation; @@ -2090,17 +2107,61 @@ bool MyAvatar::getCharacterControllerEnabled() { } void MyAvatar::clearDriveKeys() { - for (int i = 0; i < MAX_DRIVE_KEYS; ++i) { - _driveKeys[i] = 0.0f; + _driveKeys.fill(0.0f); +} + +void MyAvatar::setDriveKey(DriveKeys key, float val) { + try { + _driveKeys.at(key) = val; + } catch (const std::exception&) { + qCCritical(interfaceapp) << Q_FUNC_INFO << ": Index out of bounds"; + } +} + +float MyAvatar::getDriveKey(DriveKeys key) const { + return isDriveKeyDisabled(key) ? 0.0f : getRawDriveKey(key); +} + +float MyAvatar::getRawDriveKey(DriveKeys key) const { + try { + return _driveKeys.at(key); + } catch (const std::exception&) { + qCCritical(interfaceapp) << Q_FUNC_INFO << ": Index out of bounds"; + return 0.0f; } } void MyAvatar::relayDriveKeysToCharacterController() { - if (_driveKeys[TRANSLATE_Y] > 0.0f) { + if (getDriveKey(TRANSLATE_Y) > 0.0f) { _characterController.jump(); } } +void MyAvatar::disableDriveKey(DriveKeys key) { + try { + _disabledDriveKeys.set(key); + } catch (const std::exception&) { + qCCritical(interfaceapp) << Q_FUNC_INFO << ": Index out of bounds"; + } +} + +void MyAvatar::enableDriveKey(DriveKeys key) { + try { + _disabledDriveKeys.reset(key); + } catch (const std::exception&) { + qCCritical(interfaceapp) << Q_FUNC_INFO << ": Index out of bounds"; + } +} + +bool MyAvatar::isDriveKeyDisabled(DriveKeys key) const { + try { + return _disabledDriveKeys.test(key); + } catch (const std::exception&) { + qCCritical(interfaceapp) << Q_FUNC_INFO << ": Index out of bounds"; + return true; + } +} + glm::vec3 MyAvatar::getWorldBodyPosition() const { return transformPoint(_sensorToWorldMatrix, extractTranslation(_bodySensorMatrix)); } @@ -2186,7 +2247,15 @@ QScriptValue audioListenModeToScriptValue(QScriptEngine* engine, const AudioList } void audioListenModeFromScriptValue(const QScriptValue& object, AudioListenerMode& audioListenerMode) { - audioListenerMode = (AudioListenerMode)object.toUInt16(); + audioListenerMode = static_cast(object.toUInt16()); +} + +QScriptValue driveKeysToScriptValue(QScriptEngine* engine, const MyAvatar::DriveKeys& driveKeys) { + return driveKeys; +} + +void driveKeysFromScriptValue(const QScriptValue& object, MyAvatar::DriveKeys& driveKeys) { + driveKeys = static_cast(object.toUInt16()); } @@ -2379,7 +2448,7 @@ bool MyAvatar::didTeleport() { } bool MyAvatar::hasDriveInput() const { - return fabsf(_driveKeys[TRANSLATE_X]) > 0.0f || fabsf(_driveKeys[TRANSLATE_Y]) > 0.0f || fabsf(_driveKeys[TRANSLATE_Z]) > 0.0f; + return fabsf(getDriveKey(TRANSLATE_X)) > 0.0f || fabsf(getDriveKey(TRANSLATE_Y)) > 0.0f || fabsf(getDriveKey(TRANSLATE_Z)) > 0.0f; } void MyAvatar::setAway(bool value) { @@ -2495,7 +2564,7 @@ bool MyAvatar::pinJoint(int index, const glm::vec3& position, const glm::quat& o return false; } - setPosition(position); + slamPosition(position); setOrientation(orientation); _rig->setMaxHipsOffsetLength(0.05f); diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 3cc665b533..5f812f1f99 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -12,6 +12,8 @@ #ifndef hifi_MyAvatar_h #define hifi_MyAvatar_h +#include + #include #include @@ -29,20 +31,6 @@ class AvatarActionHold; class ModelItemID; -enum DriveKeys { - TRANSLATE_X = 0, - TRANSLATE_Y, - TRANSLATE_Z, - YAW, - STEP_TRANSLATE_X, - STEP_TRANSLATE_Y, - STEP_TRANSLATE_Z, - STEP_YAW, - PITCH, - ZOOM, - MAX_DRIVE_KEYS -}; - enum eyeContactTarget { LEFT_EYE, RIGHT_EYE, @@ -86,11 +74,29 @@ class MyAvatar : public Avatar { Q_PROPERTY(bool hmdLeanRecenterEnabled READ getHMDLeanRecenterEnabled WRITE setHMDLeanRecenterEnabled) Q_PROPERTY(bool characterControllerEnabled READ getCharacterControllerEnabled WRITE setCharacterControllerEnabled) + Q_PROPERTY(bool useAdvancedMovementControls READ useAdvancedMovementControls WRITE setUseAdvancedMovementControls) public: + enum DriveKeys { + TRANSLATE_X = 0, + TRANSLATE_Y, + TRANSLATE_Z, + YAW, + STEP_TRANSLATE_X, + STEP_TRANSLATE_Y, + STEP_TRANSLATE_Z, + STEP_YAW, + PITCH, + ZOOM, + MAX_DRIVE_KEYS + }; + Q_ENUM(DriveKeys) + explicit MyAvatar(RigPointer rig); ~MyAvatar(); + void registerMetaTypes(QScriptEngine* engine); + virtual void simulateAttachments(float deltaTime) override; AudioListenerMode getAudioListenerModeHead() const { return FROM_HEAD; } @@ -171,6 +177,10 @@ public: Q_INVOKABLE void setHMDLeanRecenterEnabled(bool value) { _hmdLeanRecenterEnabled = value; } Q_INVOKABLE bool getHMDLeanRecenterEnabled() const { return _hmdLeanRecenterEnabled; } + bool useAdvancedMovementControls() const { return _useAdvancedMovementControls.get(); } + void setUseAdvancedMovementControls(bool useAdvancedMovementControls) + { _useAdvancedMovementControls.set(useAdvancedMovementControls); } + // get/set avatar data void saveData(); void loadData(); @@ -180,9 +190,15 @@ public: // Set what driving keys are being pressed to control thrust levels void clearDriveKeys(); - void setDriveKeys(int key, float val) { _driveKeys[key] = val; }; + void setDriveKey(DriveKeys key, float val); + float getDriveKey(DriveKeys key) const; + Q_INVOKABLE float getRawDriveKey(DriveKeys key) const; void relayDriveKeysToCharacterController(); + Q_INVOKABLE void disableDriveKey(DriveKeys key); + Q_INVOKABLE void enableDriveKey(DriveKeys key); + Q_INVOKABLE bool isDriveKeyDisabled(DriveKeys key) const; + eyeContactTarget getEyeContactTarget(); Q_INVOKABLE glm::vec3 getTrackedHeadPosition() const { return _trackedHeadPosition; } @@ -352,7 +368,6 @@ private: virtual bool shouldRenderHead(const RenderArgs* renderArgs) const override; void setShouldRenderLocally(bool shouldRender) { _shouldRender = shouldRender; setEnableMeshVisible(shouldRender); } bool getShouldRenderLocally() const { return _shouldRender; } - bool getDriveKeys(int key) { return _driveKeys[key] != 0.0f; }; bool isMyAvatar() const override { return true; } virtual int parseDataFromBuffer(const QByteArray& buffer) override; virtual glm::vec3 getSkeletonPosition() const override; @@ -388,7 +403,9 @@ private: void clampScaleChangeToDomainLimits(float desiredScale); glm::mat4 computeCameraRelativeHandControllerMatrix(const glm::mat4& controllerSensorMatrix) const; - float _driveKeys[MAX_DRIVE_KEYS]; + std::array _driveKeys; + std::bitset _disabledDriveKeys; + bool _wasPushing; bool _isPushing; bool _isBeingPushed; @@ -411,6 +428,7 @@ private: SharedSoundPointer _collisionSound; MyCharacterController _characterController; + bool _wasCharacterControllerEnabled { true }; AvatarWeakPointer _lookAtTargetAvatar; glm::vec3 _targetAvatarPosition; @@ -423,6 +441,7 @@ private: glm::vec3 _trackedHeadPosition; Setting::Handle _realWorldFieldOfView; + Setting::Handle _useAdvancedMovementControls; // private methods void updateOrientation(float deltaTime); @@ -540,4 +559,7 @@ private: QScriptValue audioListenModeToScriptValue(QScriptEngine* engine, const AudioListenerMode& audioListenerMode); void audioListenModeFromScriptValue(const QScriptValue& object, AudioListenerMode& audioListenerMode); +QScriptValue driveKeysToScriptValue(QScriptEngine* engine, const MyAvatar::DriveKeys& driveKeys); +void driveKeysFromScriptValue(const QScriptValue& object, MyAvatar::DriveKeys& driveKeys); + #endif // hifi_MyAvatar_h diff --git a/interface/src/ui/ApplicationOverlay.cpp b/interface/src/ui/ApplicationOverlay.cpp index 364dff52a3..f2d97a0137 100644 --- a/interface/src/ui/ApplicationOverlay.cpp +++ b/interface/src/ui/ApplicationOverlay.cpp @@ -13,7 +13,6 @@ #include #include -#include #include #include #include @@ -42,7 +41,6 @@ ApplicationOverlay::ApplicationOverlay() _domainStatusBorder = geometryCache->allocateID(); _magnifierBorder = geometryCache->allocateID(); _qmlGeometryId = geometryCache->allocateID(); - _rearViewGeometryId = geometryCache->allocateID(); } ApplicationOverlay::~ApplicationOverlay() { @@ -51,7 +49,6 @@ ApplicationOverlay::~ApplicationOverlay() { geometryCache->releaseID(_domainStatusBorder); geometryCache->releaseID(_magnifierBorder); geometryCache->releaseID(_qmlGeometryId); - geometryCache->releaseID(_rearViewGeometryId); } } @@ -86,7 +83,6 @@ void ApplicationOverlay::renderOverlay(RenderArgs* renderArgs) { // Now render the overlay components together into a single texture renderDomainConnectionStatusBorder(renderArgs); // renders the connected domain line renderAudioScope(renderArgs); // audio scope in the very back - NOTE: this is the debug audio scope, not the VU meter - renderRearView(renderArgs); // renders the mirror view selfie renderOverlays(renderArgs); // renders Scripts Overlay and AudioScope renderQmlUi(renderArgs); // renders a unit quad with the QML UI texture, and the text overlays from scripts renderStatsAndLogs(renderArgs); // currently renders nothing @@ -99,7 +95,7 @@ void ApplicationOverlay::renderQmlUi(RenderArgs* renderArgs) { PROFILE_RANGE(app, __FUNCTION__); if (!_uiTexture) { - _uiTexture = gpu::TexturePointer(gpu::Texture::createExternal2D(OffscreenQmlSurface::getDiscardLambda())); + _uiTexture = gpu::TexturePointer(gpu::Texture::createExternal(OffscreenQmlSurface::getDiscardLambda())); _uiTexture->setSource(__FUNCTION__); } // Once we move UI rendering and screen rendering to different @@ -163,45 +159,6 @@ void ApplicationOverlay::renderOverlays(RenderArgs* renderArgs) { qApp->getOverlays().renderHUD(renderArgs); } -void ApplicationOverlay::renderRearViewToFbo(RenderArgs* renderArgs) { -} - -void ApplicationOverlay::renderRearView(RenderArgs* renderArgs) { - if (!qApp->isHMDMode() && Menu::getInstance()->isOptionChecked(MenuOption::MiniMirror) && - !Menu::getInstance()->isOptionChecked(MenuOption::FullscreenMirror)) { - gpu::Batch& batch = *renderArgs->_batch; - - auto geometryCache = DependencyManager::get(); - - auto framebuffer = DependencyManager::get(); - auto selfieTexture = framebuffer->getSelfieFramebuffer()->getRenderBuffer(0); - - int width = renderArgs->_viewport.z; - int height = renderArgs->_viewport.w; - mat4 legacyProjection = glm::ortho(0, width, height, 0, ORTHO_NEAR_CLIP, ORTHO_FAR_CLIP); - batch.setProjectionTransform(legacyProjection); - batch.setModelTransform(Transform()); - batch.resetViewTransform(); - - float screenRatio = ((float)qApp->getDevicePixelRatio()); - float renderRatio = ((float)qApp->getRenderResolutionScale()); - - auto viewport = qApp->getMirrorViewRect(); - glm::vec2 bottomLeft(viewport.left(), viewport.top() + viewport.height()); - glm::vec2 topRight(viewport.left() + viewport.width(), viewport.top()); - bottomLeft *= screenRatio; - topRight *= screenRatio; - glm::vec2 texCoordMinCorner(0.0f, 0.0f); - glm::vec2 texCoordMaxCorner(viewport.width() * renderRatio / float(selfieTexture->getWidth()), viewport.height() * renderRatio / float(selfieTexture->getHeight())); - - batch.setResourceTexture(0, selfieTexture); - float alpha = DependencyManager::get()->getDesktop()->property("unpinnedAlpha").toFloat(); - geometryCache->renderQuad(batch, bottomLeft, topRight, texCoordMinCorner, texCoordMaxCorner, glm::vec4(1.0f, 1.0f, 1.0f, alpha), _rearViewGeometryId); - - batch.setResourceTexture(0, renderArgs->_whiteTexture); - } -} - void ApplicationOverlay::renderStatsAndLogs(RenderArgs* renderArgs) { // Display stats and log text onscreen @@ -272,13 +229,13 @@ void ApplicationOverlay::buildFramebufferObject() { auto width = uiSize.x; auto height = uiSize.y; if (!_overlayFramebuffer->getDepthStencilBuffer()) { - auto overlayDepthTexture = gpu::TexturePointer(gpu::Texture::create2D(DEPTH_FORMAT, width, height, DEFAULT_SAMPLER)); + auto overlayDepthTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(DEPTH_FORMAT, width, height, DEFAULT_SAMPLER)); _overlayFramebuffer->setDepthStencilBuffer(overlayDepthTexture, DEPTH_FORMAT); } if (!_overlayFramebuffer->getRenderBuffer(0)) { const gpu::Sampler OVERLAY_SAMPLER(gpu::Sampler::FILTER_MIN_MAG_LINEAR, gpu::Sampler::WRAP_CLAMP); - auto colorBuffer = gpu::TexturePointer(gpu::Texture::create2D(COLOR_FORMAT, width, height, OVERLAY_SAMPLER)); + auto colorBuffer = gpu::TexturePointer(gpu::Texture::createRenderBuffer(COLOR_FORMAT, width, height, OVERLAY_SAMPLER)); _overlayFramebuffer->setRenderBuffer(0, colorBuffer); } } diff --git a/interface/src/ui/ApplicationOverlay.h b/interface/src/ui/ApplicationOverlay.h index 7ace5ee885..af4d8779d4 100644 --- a/interface/src/ui/ApplicationOverlay.h +++ b/interface/src/ui/ApplicationOverlay.h @@ -31,8 +31,6 @@ public: private: void renderStatsAndLogs(RenderArgs* renderArgs); void renderDomainConnectionStatusBorder(RenderArgs* renderArgs); - void renderRearViewToFbo(RenderArgs* renderArgs); - void renderRearView(RenderArgs* renderArgs); void renderQmlUi(RenderArgs* renderArgs); void renderAudioScope(RenderArgs* renderArgs); void renderOverlays(RenderArgs* renderArgs); @@ -51,7 +49,6 @@ private: gpu::TexturePointer _overlayColorTexture; gpu::FramebufferPointer _overlayFramebuffer; int _qmlGeometryId { 0 }; - int _rearViewGeometryId { 0 }; }; #endif // hifi_ApplicationOverlay_h diff --git a/interface/src/ui/AvatarInputs.cpp b/interface/src/ui/AvatarInputs.cpp index b09289c78a..944be4bf9e 100644 --- a/interface/src/ui/AvatarInputs.cpp +++ b/interface/src/ui/AvatarInputs.cpp @@ -20,10 +20,6 @@ HIFI_QML_DEF(AvatarInputs) static AvatarInputs* INSTANCE{ nullptr }; -static const char SETTINGS_GROUP_NAME[] = "Rear View Tools"; -static const char ZOOM_LEVEL_SETTINGS[] = "ZoomLevel"; - -static Setting::Handle rearViewZoomLevel(QStringList() << SETTINGS_GROUP_NAME << ZOOM_LEVEL_SETTINGS, 0); AvatarInputs* AvatarInputs::getInstance() { if (!INSTANCE) { @@ -36,8 +32,6 @@ AvatarInputs* AvatarInputs::getInstance() { AvatarInputs::AvatarInputs(QQuickItem* parent) : QQuickItem(parent) { INSTANCE = this; - int zoomSetting = rearViewZoomLevel.get(); - _mirrorZoomed = zoomSetting == 0; } #define AI_UPDATE(name, src) \ @@ -62,8 +56,6 @@ void AvatarInputs::update() { if (!Menu::getInstance()) { return; } - AI_UPDATE(mirrorVisible, Menu::getInstance()->isOptionChecked(MenuOption::MiniMirror) && !qApp->isHMDMode() - && !Menu::getInstance()->isOptionChecked(MenuOption::FullscreenMirror)); AI_UPDATE(cameraEnabled, !Menu::getInstance()->isOptionChecked(MenuOption::NoFaceTracking)); AI_UPDATE(cameraMuted, Menu::getInstance()->isOptionChecked(MenuOption::MuteFaceTracking)); AI_UPDATE(isHMD, qApp->isHMDMode()); @@ -122,15 +114,3 @@ void AvatarInputs::toggleAudioMute() { void AvatarInputs::resetSensors() { qApp->resetSensors(); } - -void AvatarInputs::toggleZoom() { - _mirrorZoomed = !_mirrorZoomed; - rearViewZoomLevel.set(_mirrorZoomed ? 0 : 1); - emit mirrorZoomedChanged(); -} - -void AvatarInputs::closeMirror() { - if (Menu::getInstance()->isOptionChecked(MenuOption::MiniMirror)) { - Menu::getInstance()->triggerOption(MenuOption::MiniMirror); - } -} diff --git a/interface/src/ui/AvatarInputs.h b/interface/src/ui/AvatarInputs.h index 85570ecd3c..5535469445 100644 --- a/interface/src/ui/AvatarInputs.h +++ b/interface/src/ui/AvatarInputs.h @@ -28,8 +28,6 @@ class AvatarInputs : public QQuickItem { AI_PROPERTY(bool, audioMuted, false) AI_PROPERTY(bool, audioClipping, false) AI_PROPERTY(float, audioLevel, 0) - AI_PROPERTY(bool, mirrorVisible, false) - AI_PROPERTY(bool, mirrorZoomed, true) AI_PROPERTY(bool, isHMD, false) AI_PROPERTY(bool, showAudioTools, true) @@ -44,8 +42,6 @@ signals: void audioMutedChanged(); void audioClippingChanged(); void audioLevelChanged(); - void mirrorVisibleChanged(); - void mirrorZoomedChanged(); void isHMDChanged(); void showAudioToolsChanged(); @@ -53,8 +49,6 @@ protected: Q_INVOKABLE void resetSensors(); Q_INVOKABLE void toggleCameraMute(); Q_INVOKABLE void toggleAudioMute(); - Q_INVOKABLE void toggleZoom(); - Q_INVOKABLE void closeMirror(); private: float _trailingAudioLoudness{ 0 }; diff --git a/interface/src/ui/BaseLogDialog.cpp b/interface/src/ui/BaseLogDialog.cpp index 7e0027e0a8..571d3ac403 100644 --- a/interface/src/ui/BaseLogDialog.cpp +++ b/interface/src/ui/BaseLogDialog.cpp @@ -28,17 +28,23 @@ const int SEARCH_BUTTON_LEFT = 25; const int SEARCH_BUTTON_WIDTH = 20; const int SEARCH_TOGGLE_BUTTON_WIDTH = 50; const int SEARCH_TEXT_WIDTH = 240; +const int TIME_STAMP_LENGTH = 16; +const int FONT_WEIGHT = 75; const QColor HIGHLIGHT_COLOR = QColor("#3366CC"); +const QColor BOLD_COLOR = QColor("#445c8c"); +const QString BOLD_PATTERN = "\\[\\d*\\/.*:\\d*:\\d*\\]"; -class KeywordHighlighter : public QSyntaxHighlighter { +class Highlighter : public QSyntaxHighlighter { public: - KeywordHighlighter(QTextDocument* parent = nullptr); + Highlighter(QTextDocument* parent = nullptr); + void setBold(int indexToBold); QString keyword; protected: void highlightBlock(const QString& text) override; private: + QTextCharFormat boldFormat; QTextCharFormat keywordFormat; }; @@ -89,7 +95,7 @@ void BaseLogDialog::initControls() { _leftPad += SEARCH_TOGGLE_BUTTON_WIDTH + BUTTON_MARGIN; _searchPrevButton->show(); connect(_searchPrevButton, SIGNAL(clicked()), SLOT(toggleSearchPrev())); - + _searchNextButton = new QPushButton(this); _searchNextButton->setObjectName("searchNextButton"); _searchNextButton->setGeometry(_leftPad, ELEMENT_MARGIN, SEARCH_TOGGLE_BUTTON_WIDTH, ELEMENT_HEIGHT); @@ -101,9 +107,8 @@ void BaseLogDialog::initControls() { _logTextBox = new QPlainTextEdit(this); _logTextBox->setReadOnly(true); _logTextBox->show(); - _highlighter = new KeywordHighlighter(_logTextBox->document()); + _highlighter = new Highlighter(_logTextBox->document()); connect(_logTextBox, SIGNAL(selectionChanged()), SLOT(updateSelection())); - } void BaseLogDialog::showEvent(QShowEvent* event) { @@ -116,7 +121,9 @@ void BaseLogDialog::resizeEvent(QResizeEvent* event) { void BaseLogDialog::appendLogLine(QString logLine) { if (logLine.contains(_searchTerm, Qt::CaseInsensitive)) { + int indexToBold = _logTextBox->document()->characterCount(); _logTextBox->appendPlainText(logLine.trimmed()); + _highlighter->setBold(indexToBold); } } @@ -128,7 +135,7 @@ void BaseLogDialog::handleSearchTextChanged(QString searchText) { if (searchText.isEmpty()) { return; } - + QTextCursor cursor = _logTextBox->textCursor(); if (cursor.hasSelection()) { QString selectedTerm = cursor.selectedText(); @@ -136,16 +143,16 @@ void BaseLogDialog::handleSearchTextChanged(QString searchText) { return; } } - + cursor.setPosition(0, QTextCursor::MoveAnchor); _logTextBox->setTextCursor(cursor); bool foundTerm = _logTextBox->find(searchText); - + if (!foundTerm) { cursor.movePosition(QTextCursor::End, QTextCursor::MoveAnchor); _logTextBox->setTextCursor(cursor); } - + _searchTerm = searchText; _highlighter->keyword = searchText; _highlighter->rehighlight(); @@ -175,6 +182,7 @@ void BaseLogDialog::showLogData() { _logTextBox->clear(); _logTextBox->appendPlainText(getCurrentLog()); _logTextBox->ensureCursorVisible(); + _highlighter->rehighlight(); } void BaseLogDialog::updateSelection() { @@ -187,16 +195,28 @@ void BaseLogDialog::updateSelection() { } } -KeywordHighlighter::KeywordHighlighter(QTextDocument* parent) : QSyntaxHighlighter(parent) { +Highlighter::Highlighter(QTextDocument* parent) : QSyntaxHighlighter(parent) { + boldFormat.setFontWeight(FONT_WEIGHT); + boldFormat.setForeground(BOLD_COLOR); keywordFormat.setForeground(HIGHLIGHT_COLOR); } -void KeywordHighlighter::highlightBlock(const QString& text) { +void Highlighter::highlightBlock(const QString& text) { + QRegExp expression(BOLD_PATTERN); + + int index = text.indexOf(expression, 0); + + while (index >= 0) { + int length = expression.matchedLength(); + setFormat(index, length, boldFormat); + index = text.indexOf(expression, index + length); + } + if (keyword.isNull() || keyword.isEmpty()) { return; } - int index = text.indexOf(keyword, 0, Qt::CaseInsensitive); + index = text.indexOf(keyword, 0, Qt::CaseInsensitive); int length = keyword.length(); while (index >= 0) { @@ -204,3 +224,7 @@ void KeywordHighlighter::highlightBlock(const QString& text) { index = text.indexOf(keyword, index + length, Qt::CaseInsensitive); } } + +void Highlighter::setBold(int indexToBold) { + setFormat(indexToBold, TIME_STAMP_LENGTH, boldFormat); +} diff --git a/interface/src/ui/BaseLogDialog.h b/interface/src/ui/BaseLogDialog.h index d097010bae..e18d23937f 100644 --- a/interface/src/ui/BaseLogDialog.h +++ b/interface/src/ui/BaseLogDialog.h @@ -23,7 +23,7 @@ const int BUTTON_MARGIN = 8; class QPushButton; class QLineEdit; class QPlainTextEdit; -class KeywordHighlighter; +class Highlighter; class BaseLogDialog : public QDialog { Q_OBJECT @@ -56,7 +56,7 @@ private: QPushButton* _searchPrevButton { nullptr }; QPushButton* _searchNextButton { nullptr }; QString _searchTerm; - KeywordHighlighter* _highlighter { nullptr }; + Highlighter* _highlighter { nullptr }; void initControls(); void showLogData(); diff --git a/interface/src/ui/CachesSizeDialog.cpp b/interface/src/ui/CachesSizeDialog.cpp deleted file mode 100644 index 935a6d126e..0000000000 --- a/interface/src/ui/CachesSizeDialog.cpp +++ /dev/null @@ -1,84 +0,0 @@ -// -// CachesSizeDialog.cpp -// -// -// Created by Clement on 1/12/15. -// Copyright 2015 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include -#include -#include - -#include -#include -#include -#include -#include - -#include "CachesSizeDialog.h" - - -QDoubleSpinBox* createDoubleSpinBox(QWidget* parent) { - QDoubleSpinBox* box = new QDoubleSpinBox(parent); - box->setDecimals(0); - box->setRange(MIN_UNUSED_MAX_SIZE / BYTES_PER_MEGABYTES, MAX_UNUSED_MAX_SIZE / BYTES_PER_MEGABYTES); - - return box; -} - -CachesSizeDialog::CachesSizeDialog(QWidget* parent) : - QDialog(parent, Qt::Window | Qt::WindowCloseButtonHint) -{ - setWindowTitle("Caches Size"); - - // Create layouter - QFormLayout* form = new QFormLayout(this); - setLayout(form); - - form->addRow("Animations cache size (MB):", _animations = createDoubleSpinBox(this)); - form->addRow("Geometries cache size (MB):", _geometries = createDoubleSpinBox(this)); - form->addRow("Sounds cache size (MB):", _sounds = createDoubleSpinBox(this)); - form->addRow("Textures cache size (MB):", _textures = createDoubleSpinBox(this)); - - resetClicked(true); - - // Add a button to reset - QPushButton* confirmButton = new QPushButton("Confirm", this); - QPushButton* resetButton = new QPushButton("Reset", this); - form->addRow(confirmButton, resetButton); - connect(confirmButton, SIGNAL(clicked(bool)), this, SLOT(confirmClicked(bool))); - connect(resetButton, SIGNAL(clicked(bool)), this, SLOT(resetClicked(bool))); -} - -void CachesSizeDialog::confirmClicked(bool checked) { - DependencyManager::get()->setUnusedResourceCacheSize(_animations->value() * BYTES_PER_MEGABYTES); - DependencyManager::get()->setUnusedResourceCacheSize(_geometries->value() * BYTES_PER_MEGABYTES); - DependencyManager::get()->setUnusedResourceCacheSize(_sounds->value() * BYTES_PER_MEGABYTES); - // Disabling the texture cache because it's a liability in cases where we're overcommiting GPU memory -#if 0 - DependencyManager::get()->setUnusedResourceCacheSize(_textures->value() * BYTES_PER_MEGABYTES); -#endif - - QDialog::close(); -} - -void CachesSizeDialog::resetClicked(bool checked) { - _animations->setValue(DependencyManager::get()->getUnusedResourceCacheSize() / BYTES_PER_MEGABYTES); - _geometries->setValue(DependencyManager::get()->getUnusedResourceCacheSize() / BYTES_PER_MEGABYTES); - _sounds->setValue(DependencyManager::get()->getUnusedResourceCacheSize() / BYTES_PER_MEGABYTES); - _textures->setValue(DependencyManager::get()->getUnusedResourceCacheSize() / BYTES_PER_MEGABYTES); -} - -void CachesSizeDialog::reject() { - // Just regularly close upon ESC - QDialog::close(); -} - -void CachesSizeDialog::closeEvent(QCloseEvent* event) { - QDialog::closeEvent(event); - emit closed(); -} diff --git a/interface/src/ui/CachesSizeDialog.h b/interface/src/ui/CachesSizeDialog.h deleted file mode 100644 index 025d0f2bac..0000000000 --- a/interface/src/ui/CachesSizeDialog.h +++ /dev/null @@ -1,45 +0,0 @@ -// -// CachesSizeDialog.h -// -// -// Created by Clement on 1/12/15. -// Copyright 2015 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#ifndef hifi_CachesSizeDialog_h -#define hifi_CachesSizeDialog_h - -#include - -class QDoubleSpinBox; - -class CachesSizeDialog : public QDialog { - Q_OBJECT -public: - // Sets up the UI - CachesSizeDialog(QWidget* parent); - -signals: - void closed(); - -public slots: - void reject() override; - void confirmClicked(bool checked); - void resetClicked(bool checked); - -protected: - // Emits a 'closed' signal when this dialog is closed. - void closeEvent(QCloseEvent* event) override; - -private: - QDoubleSpinBox* _animations = nullptr; - QDoubleSpinBox* _geometries = nullptr; - QDoubleSpinBox* _scripts = nullptr; - QDoubleSpinBox* _sounds = nullptr; - QDoubleSpinBox* _textures = nullptr; -}; - -#endif // hifi_CachesSizeDialog_h diff --git a/interface/src/ui/DialogsManager.cpp b/interface/src/ui/DialogsManager.cpp index 3252fef4f0..f1d6f585d7 100644 --- a/interface/src/ui/DialogsManager.cpp +++ b/interface/src/ui/DialogsManager.cpp @@ -19,16 +19,13 @@ #include #include "AddressBarDialog.h" -#include "CachesSizeDialog.h" #include "ConnectionFailureDialog.h" -#include "DiskCacheEditor.h" #include "DomainConnectionDialog.h" #include "HMDToolsDialog.h" #include "LodToolsDialog.h" #include "LoginDialog.h" #include "OctreeStatsDialog.h" #include "PreferencesDialog.h" -#include "ScriptEditorWindow.h" #include "UpdateDialog.h" template @@ -67,11 +64,6 @@ void DialogsManager::setDomainConnectionFailureVisibility(bool visible) { } } -void DialogsManager::toggleDiskCacheEditor() { - maybeCreateDialog(_diskCacheEditor); - _diskCacheEditor->toggle(); -} - void DialogsManager::toggleLoginDialog() { LoginDialog::toggleAction(); } @@ -97,16 +89,6 @@ void DialogsManager::octreeStatsDetails() { _octreeStatsDialog->raise(); } -void DialogsManager::cachesSizeDialog() { - if (!_cachesSizeDialog) { - maybeCreateDialog(_cachesSizeDialog); - - connect(_cachesSizeDialog, SIGNAL(closed()), _cachesSizeDialog, SLOT(deleteLater())); - _cachesSizeDialog->show(); - } - _cachesSizeDialog->raise(); -} - void DialogsManager::lodTools() { if (!_lodToolsDialog) { maybeCreateDialog(_lodToolsDialog); @@ -137,12 +119,6 @@ void DialogsManager::hmdToolsClosed() { } } -void DialogsManager::showScriptEditor() { - maybeCreateDialog(_scriptEditor); - _scriptEditor->show(); - _scriptEditor->raise(); -} - void DialogsManager::showTestingResults() { if (!_testingDialog) { _testingDialog = new TestingDialog(qApp->getWindow()); diff --git a/interface/src/ui/DialogsManager.h b/interface/src/ui/DialogsManager.h index 54aef38984..608195aca7 100644 --- a/interface/src/ui/DialogsManager.h +++ b/interface/src/ui/DialogsManager.h @@ -22,7 +22,6 @@ class AnimationsDialog; class AttachmentsDialog; class CachesSizeDialog; -class DiskCacheEditor; class LodToolsDialog; class OctreeStatsDialog; class ScriptEditorWindow; @@ -46,14 +45,11 @@ public slots: void showAddressBar(); void showFeed(); void setDomainConnectionFailureVisibility(bool visible); - void toggleDiskCacheEditor(); void toggleLoginDialog(); void showLoginDialog(); void octreeStatsDetails(); - void cachesSizeDialog(); void lodTools(); void hmdTools(bool showTools); - void showScriptEditor(); void showDomainConnectionDialog(); void showTestingResults(); @@ -77,12 +73,10 @@ private: QPointer _animationsDialog; QPointer _attachmentsDialog; QPointer _cachesSizeDialog; - QPointer _diskCacheEditor; QPointer _ircInfoBox; QPointer _hmdToolsDialog; QPointer _lodToolsDialog; QPointer _octreeStatsDialog; - QPointer _scriptEditor; QPointer _testingDialog; QPointer _domainConnectionDialog; }; diff --git a/interface/src/ui/DiskCacheEditor.cpp b/interface/src/ui/DiskCacheEditor.cpp deleted file mode 100644 index 1a7be8642b..0000000000 --- a/interface/src/ui/DiskCacheEditor.cpp +++ /dev/null @@ -1,146 +0,0 @@ -// -// DiskCacheEditor.cpp -// -// -// Created by Clement on 3/4/15. -// Copyright 2015 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include "DiskCacheEditor.h" - -#include -#include -#include -#include -#include -#include -#include - -#include - -#include "OffscreenUi.h" - -DiskCacheEditor::DiskCacheEditor(QWidget* parent) : QObject(parent) { -} - -QWindow* DiskCacheEditor::windowHandle() { - return (_dialog) ? _dialog->windowHandle() : nullptr; -} - -void DiskCacheEditor::toggle() { - if (!_dialog) { - makeDialog(); - } - - if (!_dialog->isActiveWindow()) { - _dialog->show(); - _dialog->raise(); - _dialog->activateWindow(); - } else { - _dialog->close(); - } -} - -void DiskCacheEditor::makeDialog() { - _dialog = new QDialog(static_cast(parent())); - Q_CHECK_PTR(_dialog); - _dialog->setAttribute(Qt::WA_DeleteOnClose); - _dialog->setWindowTitle("Disk Cache Editor"); - - QGridLayout* layout = new QGridLayout(_dialog); - Q_CHECK_PTR(layout); - _dialog->setLayout(layout); - - - QLabel* path = new QLabel("Path : ", _dialog); - Q_CHECK_PTR(path); - path->setAlignment(Qt::AlignRight); - layout->addWidget(path, 0, 0); - - QLabel* size = new QLabel("Current Size : ", _dialog); - Q_CHECK_PTR(size); - size->setAlignment(Qt::AlignRight); - layout->addWidget(size, 1, 0); - - QLabel* maxSize = new QLabel("Max Size : ", _dialog); - Q_CHECK_PTR(maxSize); - maxSize->setAlignment(Qt::AlignRight); - layout->addWidget(maxSize, 2, 0); - - - _path = new QLabel(_dialog); - Q_CHECK_PTR(_path); - _path->setAlignment(Qt::AlignLeft); - layout->addWidget(_path, 0, 1, 1, 3); - - _size = new QLabel(_dialog); - Q_CHECK_PTR(_size); - _size->setAlignment(Qt::AlignLeft); - layout->addWidget(_size, 1, 1, 1, 3); - - _maxSize = new QLabel(_dialog); - Q_CHECK_PTR(_maxSize); - _maxSize->setAlignment(Qt::AlignLeft); - layout->addWidget(_maxSize, 2, 1, 1, 3); - - refresh(); - - - static const int REFRESH_INTERVAL = 100; // msec - _refreshTimer = new QTimer(_dialog); - _refreshTimer->setInterval(REFRESH_INTERVAL); // Qt::CoarseTimer acceptable, no need for real time accuracy - _refreshTimer->setSingleShot(false); - QObject::connect(_refreshTimer.data(), &QTimer::timeout, this, &DiskCacheEditor::refresh); - _refreshTimer->start(); - - QPushButton* clearCacheButton = new QPushButton(_dialog); - Q_CHECK_PTR(clearCacheButton); - clearCacheButton->setText("Clear"); - clearCacheButton->setToolTip("Erases the entire content of the disk cache."); - connect(clearCacheButton, SIGNAL(clicked()), SLOT(clear())); - layout->addWidget(clearCacheButton, 3, 3); -} - -void DiskCacheEditor::refresh() { - DependencyManager::get()->cacheInfoRequest(this, "cacheInfoCallback"); -} - -void DiskCacheEditor::cacheInfoCallback(QString cacheDirectory, qint64 cacheSize, qint64 maximumCacheSize) { - static const auto stringify = [](qint64 number) { - static const QStringList UNITS = QStringList() << "B" << "KB" << "MB" << "GB"; - static const qint64 CHUNK = 1024; - QString unit; - int i = 0; - for (i = 0; i < 4; ++i) { - if (number / CHUNK > 0) { - number /= CHUNK; - } else { - break; - } - } - return QString("%0 %1").arg(number).arg(UNITS[i]); - }; - - if (_path) { - _path->setText(cacheDirectory); - } - if (_size) { - _size->setText(stringify(cacheSize)); - } - if (_maxSize) { - _maxSize->setText(stringify(maximumCacheSize)); - } -} - -void DiskCacheEditor::clear() { - auto buttonClicked = OffscreenUi::question(_dialog, "Clearing disk cache", - "You are about to erase all the content of the disk cache, " - "are you sure you want to do that?", - QMessageBox::Ok | QMessageBox::Cancel); - if (buttonClicked == QMessageBox::Ok) { - DependencyManager::get()->clearCache(); - } -} diff --git a/interface/src/ui/DiskCacheEditor.h b/interface/src/ui/DiskCacheEditor.h deleted file mode 100644 index 3f8fa1a883..0000000000 --- a/interface/src/ui/DiskCacheEditor.h +++ /dev/null @@ -1,49 +0,0 @@ -// -// DiskCacheEditor.h -// -// -// Created by Clement on 3/4/15. -// Copyright 2015 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#ifndef hifi_DiskCacheEditor_h -#define hifi_DiskCacheEditor_h - -#include -#include - -class QDialog; -class QLabel; -class QWindow; -class QTimer; - -class DiskCacheEditor : public QObject { - Q_OBJECT - -public: - DiskCacheEditor(QWidget* parent = nullptr); - - QWindow* windowHandle(); - -public slots: - void toggle(); - -private slots: - void refresh(); - void cacheInfoCallback(QString cacheDirectory, qint64 cacheSize, qint64 maximumCacheSize); - void clear(); - -private: - void makeDialog(); - - QPointer _dialog; - QPointer _path; - QPointer _size; - QPointer _maxSize; - QPointer _refreshTimer; -}; - -#endif // hifi_DiskCacheEditor_h \ No newline at end of file diff --git a/interface/src/ui/ScriptEditBox.cpp b/interface/src/ui/ScriptEditBox.cpp deleted file mode 100644 index 2aea225b17..0000000000 --- a/interface/src/ui/ScriptEditBox.cpp +++ /dev/null @@ -1,111 +0,0 @@ -// -// ScriptEditBox.cpp -// interface/src/ui -// -// Created by Thijs Wenker on 4/30/14. -// Copyright 2014 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include "ScriptEditBox.h" - -#include -#include - -#include "ScriptLineNumberArea.h" - -ScriptEditBox::ScriptEditBox(QWidget* parent) : - QPlainTextEdit(parent) -{ - _scriptLineNumberArea = new ScriptLineNumberArea(this); - - connect(this, &ScriptEditBox::blockCountChanged, this, &ScriptEditBox::updateLineNumberAreaWidth); - connect(this, &ScriptEditBox::updateRequest, this, &ScriptEditBox::updateLineNumberArea); - connect(this, &ScriptEditBox::cursorPositionChanged, this, &ScriptEditBox::highlightCurrentLine); - - updateLineNumberAreaWidth(0); - highlightCurrentLine(); -} - -int ScriptEditBox::lineNumberAreaWidth() { - int digits = 1; - const int SPACER_PIXELS = 3; - const int BASE_TEN = 10; - int max = qMax(1, blockCount()); - while (max >= BASE_TEN) { - max /= BASE_TEN; - digits++; - } - return SPACER_PIXELS + fontMetrics().width(QLatin1Char('H')) * digits; -} - -void ScriptEditBox::updateLineNumberAreaWidth(int blockCount) { - setViewportMargins(lineNumberAreaWidth(), 0, 0, 0); -} - -void ScriptEditBox::updateLineNumberArea(const QRect& rect, int deltaY) { - if (deltaY) { - _scriptLineNumberArea->scroll(0, deltaY); - } else { - _scriptLineNumberArea->update(0, rect.y(), _scriptLineNumberArea->width(), rect.height()); - } - - if (rect.contains(viewport()->rect())) { - updateLineNumberAreaWidth(0); - } -} - -void ScriptEditBox::resizeEvent(QResizeEvent* event) { - QPlainTextEdit::resizeEvent(event); - - QRect localContentsRect = contentsRect(); - _scriptLineNumberArea->setGeometry(QRect(localContentsRect.left(), localContentsRect.top(), lineNumberAreaWidth(), - localContentsRect.height())); -} - -void ScriptEditBox::highlightCurrentLine() { - QList extraSelections; - - if (!isReadOnly()) { - QTextEdit::ExtraSelection selection; - - QColor lineColor = QColor(Qt::gray).lighter(); - - selection.format.setBackground(lineColor); - selection.format.setProperty(QTextFormat::FullWidthSelection, true); - selection.cursor = textCursor(); - selection.cursor.clearSelection(); - extraSelections.append(selection); - } - - setExtraSelections(extraSelections); -} - -void ScriptEditBox::lineNumberAreaPaintEvent(QPaintEvent* event) -{ - QPainter painter(_scriptLineNumberArea); - painter.fillRect(event->rect(), Qt::lightGray); - QTextBlock block = firstVisibleBlock(); - int blockNumber = block.blockNumber(); - int top = (int) blockBoundingGeometry(block).translated(contentOffset()).top(); - int bottom = top + (int) blockBoundingRect(block).height(); - - while (block.isValid() && top <= event->rect().bottom()) { - if (block.isVisible() && bottom >= event->rect().top()) { - QFont font = painter.font(); - font.setBold(this->textCursor().blockNumber() == block.blockNumber()); - painter.setFont(font); - QString number = QString::number(blockNumber + 1); - painter.setPen(Qt::black); - painter.drawText(0, top, _scriptLineNumberArea->width(), fontMetrics().height(), - Qt::AlignRight, number); - } - - block = block.next(); - top = bottom; - bottom = top + (int) blockBoundingRect(block).height(); - blockNumber++; - } -} diff --git a/interface/src/ui/ScriptEditBox.h b/interface/src/ui/ScriptEditBox.h deleted file mode 100644 index 0b037db16a..0000000000 --- a/interface/src/ui/ScriptEditBox.h +++ /dev/null @@ -1,38 +0,0 @@ -// -// ScriptEditBox.h -// interface/src/ui -// -// Created by Thijs Wenker on 4/30/14. -// Copyright 2014 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#ifndef hifi_ScriptEditBox_h -#define hifi_ScriptEditBox_h - -#include - -class ScriptEditBox : public QPlainTextEdit { - Q_OBJECT - -public: - ScriptEditBox(QWidget* parent = NULL); - - void lineNumberAreaPaintEvent(QPaintEvent* event); - int lineNumberAreaWidth(); - -protected: - void resizeEvent(QResizeEvent* event) override; - -private slots: - void updateLineNumberAreaWidth(int blockCount); - void highlightCurrentLine(); - void updateLineNumberArea(const QRect& rect, int deltaY); - -private: - QWidget* _scriptLineNumberArea; -}; - -#endif // hifi_ScriptEditBox_h diff --git a/interface/src/ui/ScriptEditorWidget.cpp b/interface/src/ui/ScriptEditorWidget.cpp deleted file mode 100644 index ada6b11355..0000000000 --- a/interface/src/ui/ScriptEditorWidget.cpp +++ /dev/null @@ -1,256 +0,0 @@ -// -// ScriptEditorWidget.cpp -// interface/src/ui -// -// Created by Thijs Wenker on 4/14/14. -// Copyright 2014 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include "ui_scriptEditorWidget.h" -#include "ScriptEditorWidget.h" -#include "ScriptEditorWindow.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -#include "Application.h" -#include "ScriptHighlighting.h" - -ScriptEditorWidget::ScriptEditorWidget() : - _scriptEditorWidgetUI(new Ui::ScriptEditorWidget), - _scriptEngine(NULL), - _isRestarting(false), - _isReloading(false) -{ - setAttribute(Qt::WA_DeleteOnClose); - - _scriptEditorWidgetUI->setupUi(this); - - connect(_scriptEditorWidgetUI->scriptEdit->document(), &QTextDocument::modificationChanged, this, - &ScriptEditorWidget::scriptModified); - connect(_scriptEditorWidgetUI->scriptEdit->document(), &QTextDocument::contentsChanged, this, - &ScriptEditorWidget::onScriptModified); - - // remove the title bar (see the Qt docs on setTitleBarWidget) - setTitleBarWidget(new QWidget()); - QFontMetrics fm(_scriptEditorWidgetUI->scriptEdit->font()); - _scriptEditorWidgetUI->scriptEdit->setTabStopWidth(fm.width('0') * 4); - // We create a new ScriptHighligting QObject and provide it with a parent so this is NOT a memory leak. - new ScriptHighlighting(_scriptEditorWidgetUI->scriptEdit->document()); - QTimer::singleShot(0, _scriptEditorWidgetUI->scriptEdit, SLOT(setFocus())); - - _console = new JSConsole(this); - _console->setFixedHeight(CONSOLE_HEIGHT); - _scriptEditorWidgetUI->verticalLayout->addWidget(_console); - connect(_scriptEditorWidgetUI->clearButton, &QPushButton::clicked, _console, &JSConsole::clear); -} - -ScriptEditorWidget::~ScriptEditorWidget() { - delete _scriptEditorWidgetUI; - delete _console; -} - -void ScriptEditorWidget::onScriptModified() { - if(_scriptEditorWidgetUI->onTheFlyCheckBox->isChecked() && isModified() && isRunning() && !_isReloading) { - _isRestarting = true; - setRunning(false); - // Script is restarted once current script instance finishes. - } -} - -void ScriptEditorWidget::onScriptFinished(const QString& scriptPath) { - _scriptEngine = NULL; - _console->setScriptEngine(NULL); - if (_isRestarting) { - _isRestarting = false; - setRunning(true); - } -} - -bool ScriptEditorWidget::isModified() { - return _scriptEditorWidgetUI->scriptEdit->document()->isModified(); -} - -bool ScriptEditorWidget::isRunning() { - return (_scriptEngine != NULL) ? _scriptEngine->isRunning() : false; -} - -bool ScriptEditorWidget::setRunning(bool run) { - if (run && isModified() && !save()) { - return false; - } - - if (_scriptEngine != NULL) { - disconnect(_scriptEngine, &ScriptEngine::runningStateChanged, this, &ScriptEditorWidget::runningStateChanged); - disconnect(_scriptEngine, &ScriptEngine::update, this, &ScriptEditorWidget::onScriptModified); - disconnect(_scriptEngine, &ScriptEngine::finished, this, &ScriptEditorWidget::onScriptFinished); - } - - auto scriptEngines = DependencyManager::get(); - if (run) { - const QString& scriptURLString = QUrl(_currentScript).toString(); - // Reload script so that an out of date copy is not retrieved from the cache - _scriptEngine = scriptEngines->loadScript(scriptURLString, true, true, false, true); - connect(_scriptEngine, &ScriptEngine::runningStateChanged, this, &ScriptEditorWidget::runningStateChanged); - connect(_scriptEngine, &ScriptEngine::update, this, &ScriptEditorWidget::onScriptModified); - connect(_scriptEngine, &ScriptEngine::finished, this, &ScriptEditorWidget::onScriptFinished); - } else { - connect(_scriptEngine, &ScriptEngine::finished, this, &ScriptEditorWidget::onScriptFinished); - const QString& scriptURLString = QUrl(_currentScript).toString(); - scriptEngines->stopScript(scriptURLString); - _scriptEngine = NULL; - } - _console->setScriptEngine(_scriptEngine); - return true; -} - -bool ScriptEditorWidget::saveFile(const QString &scriptPath) { - QFile file(scriptPath); - if (!file.open(QFile::WriteOnly | QFile::Text)) { - OffscreenUi::warning(this, tr("Interface"), tr("Cannot write script %1:\n%2.").arg(scriptPath) - .arg(file.errorString())); - return false; - } - - QTextStream out(&file); - out << _scriptEditorWidgetUI->scriptEdit->toPlainText(); - file.close(); - - setScriptFile(scriptPath); - return true; -} - -void ScriptEditorWidget::loadFile(const QString& scriptPath) { - QUrl url(scriptPath); - - // if the scheme length is one or lower, maybe they typed in a file, let's try - const int WINDOWS_DRIVE_LETTER_SIZE = 1; - if (url.scheme().size() <= WINDOWS_DRIVE_LETTER_SIZE) { - QFile file(scriptPath); - if (!file.open(QFile::ReadOnly | QFile::Text)) { - OffscreenUi::warning(this, tr("Interface"), tr("Cannot read script %1:\n%2.").arg(scriptPath) - .arg(file.errorString())); - return; - } - QTextStream in(&file); - _scriptEditorWidgetUI->scriptEdit->setPlainText(in.readAll()); - file.close(); - setScriptFile(scriptPath); - - if (_scriptEngine != NULL) { - disconnect(_scriptEngine, &ScriptEngine::runningStateChanged, this, &ScriptEditorWidget::runningStateChanged); - disconnect(_scriptEngine, &ScriptEngine::update, this, &ScriptEditorWidget::onScriptModified); - disconnect(_scriptEngine, &ScriptEngine::finished, this, &ScriptEditorWidget::onScriptFinished); - } - } else { - QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); - QNetworkRequest networkRequest = QNetworkRequest(url); - networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); - networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); - QNetworkReply* reply = networkAccessManager.get(networkRequest); - qDebug() << "Downloading included script at" << scriptPath; - QEventLoop loop; - QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); - loop.exec(); - _scriptEditorWidgetUI->scriptEdit->setPlainText(reply->readAll()); - delete reply; - - if (!saveAs()) { - static_cast(this->parent()->parent()->parent())->terminateCurrentTab(); - } - } - const QString& scriptURLString = QUrl(_currentScript).toString(); - _scriptEngine = DependencyManager::get()->getScriptEngine(scriptURLString); - if (_scriptEngine != NULL) { - connect(_scriptEngine, &ScriptEngine::runningStateChanged, this, &ScriptEditorWidget::runningStateChanged); - connect(_scriptEngine, &ScriptEngine::update, this, &ScriptEditorWidget::onScriptModified); - connect(_scriptEngine, &ScriptEngine::finished, this, &ScriptEditorWidget::onScriptFinished); - } - _console->setScriptEngine(_scriptEngine); -} - -bool ScriptEditorWidget::save() { - return _currentScript.isEmpty() ? saveAs() : saveFile(_currentScript); -} - -bool ScriptEditorWidget::saveAs() { - auto scriptEngines = DependencyManager::get(); - QString fileName = QFileDialog::getSaveFileName(this, tr("Save script"), - qApp->getPreviousScriptLocation(), - tr("JavaScript Files (*.js)")); - if (!fileName.isEmpty()) { - qApp->setPreviousScriptLocation(fileName); - return saveFile(fileName); - } else { - return false; - } -} - -void ScriptEditorWidget::setScriptFile(const QString& scriptPath) { - _currentScript = scriptPath; - _currentScriptModified = QFileInfo(_currentScript).lastModified(); - _scriptEditorWidgetUI->scriptEdit->document()->setModified(false); - setWindowModified(false); - - emit scriptnameChanged(); -} - -bool ScriptEditorWidget::questionSave() { - if (_scriptEditorWidgetUI->scriptEdit->document()->isModified()) { - QMessageBox::StandardButton button = OffscreenUi::warning(this, tr("Interface"), - tr("The script has been modified.\nDo you want to save your changes?"), QMessageBox::Save | QMessageBox::Discard | - QMessageBox::Cancel, QMessageBox::Save); - return button == QMessageBox::Save ? save() : (button == QMessageBox::Discard); - } - return true; -} - -void ScriptEditorWidget::onWindowActivated() { - if (!_isReloading) { - _isReloading = true; - - QDateTime fileStamp = QFileInfo(_currentScript).lastModified(); - if (fileStamp > _currentScriptModified) { - bool doReload = false; - auto window = static_cast(this->parent()->parent()->parent()); - window->inModalDialog = true; - if (window->autoReloadScripts() - || OffscreenUi::question(this, tr("Reload Script"), - tr("The following file has been modified outside of the Interface editor:") + "\n" + _currentScript + "\n" - + (isModified() - ? tr("Do you want to reload it and lose the changes you've made in the Interface editor?") - : tr("Do you want to reload it?")), - QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { - doReload = true; - } - window->inModalDialog = false; - if (doReload) { - loadFile(_currentScript); - if (_scriptEditorWidgetUI->onTheFlyCheckBox->isChecked() && isRunning()) { - _isRestarting = true; - setRunning(false); - // Script is restarted once current script instance finishes. - } - } else { - _currentScriptModified = fileStamp; // Asked and answered. Don't ask again until the external file is changed again. - } - } - _isReloading = false; - } -} diff --git a/interface/src/ui/ScriptEditorWidget.h b/interface/src/ui/ScriptEditorWidget.h deleted file mode 100644 index f53fd7b718..0000000000 --- a/interface/src/ui/ScriptEditorWidget.h +++ /dev/null @@ -1,64 +0,0 @@ -// -// ScriptEditorWidget.h -// interface/src/ui -// -// Created by Thijs Wenker on 4/14/14. -// Copyright 2014 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#ifndef hifi_ScriptEditorWidget_h -#define hifi_ScriptEditorWidget_h - -#include - -#include "JSConsole.h" -#include "ScriptEngine.h" - -namespace Ui { - class ScriptEditorWidget; -} - -class ScriptEditorWidget : public QDockWidget { - Q_OBJECT - -public: - ScriptEditorWidget(); - ~ScriptEditorWidget(); - - bool isModified(); - bool isRunning(); - bool setRunning(bool run); - bool saveFile(const QString& scriptPath); - void loadFile(const QString& scriptPath); - void setScriptFile(const QString& scriptPath); - bool save(); - bool saveAs(); - bool questionSave(); - const QString getScriptName() const { return _currentScript; }; - -signals: - void runningStateChanged(); - void scriptnameChanged(); - void scriptModified(); - -public slots: - void onWindowActivated(); - -private slots: - void onScriptModified(); - void onScriptFinished(const QString& scriptName); - -private: - JSConsole* _console; - Ui::ScriptEditorWidget* _scriptEditorWidgetUI; - ScriptEngine* _scriptEngine; - QString _currentScript; - QDateTime _currentScriptModified; - bool _isRestarting; - bool _isReloading; -}; - -#endif // hifi_ScriptEditorWidget_h diff --git a/interface/src/ui/ScriptEditorWindow.cpp b/interface/src/ui/ScriptEditorWindow.cpp deleted file mode 100644 index 58abd23979..0000000000 --- a/interface/src/ui/ScriptEditorWindow.cpp +++ /dev/null @@ -1,259 +0,0 @@ -// -// ScriptEditorWindow.cpp -// interface/src/ui -// -// Created by Thijs Wenker on 4/14/14. -// Copyright 2014 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include - -#include "ui_scriptEditorWindow.h" -#include "ScriptEditorWindow.h" -#include "ScriptEditorWidget.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include "Application.h" -#include "PathUtils.h" - -ScriptEditorWindow::ScriptEditorWindow(QWidget* parent) : - QWidget(parent), - _ScriptEditorWindowUI(new Ui::ScriptEditorWindow), - _loadMenu(new QMenu), - _saveMenu(new QMenu) -{ - setAttribute(Qt::WA_DeleteOnClose); - - _ScriptEditorWindowUI->setupUi(this); - - this->setWindowFlags(Qt::Tool); - addScriptEditorWidget("New script"); - connect(_loadMenu, &QMenu::aboutToShow, this, &ScriptEditorWindow::loadMenuAboutToShow); - _ScriptEditorWindowUI->loadButton->setMenu(_loadMenu); - - _saveMenu->addAction("Save as..", this, SLOT(saveScriptAsClicked()), Qt::CTRL | Qt::SHIFT | Qt::Key_S); - - _ScriptEditorWindowUI->saveButton->setMenu(_saveMenu); - - connect(new QShortcut(QKeySequence("Ctrl+N"), this), &QShortcut::activated, this, &ScriptEditorWindow::newScriptClicked); - connect(new QShortcut(QKeySequence("Ctrl+S"), this), &QShortcut::activated, this,&ScriptEditorWindow::saveScriptClicked); - connect(new QShortcut(QKeySequence("Ctrl+O"), this), &QShortcut::activated, this, &ScriptEditorWindow::loadScriptClicked); - connect(new QShortcut(QKeySequence("F5"), this), &QShortcut::activated, this, &ScriptEditorWindow::toggleRunScriptClicked); - - _ScriptEditorWindowUI->loadButton->setIcon(QIcon(QPixmap(PathUtils::resourcesPath() + "icons/load-script.svg"))); - _ScriptEditorWindowUI->newButton->setIcon(QIcon(QPixmap(PathUtils::resourcesPath() + "icons/new-script.svg"))); - _ScriptEditorWindowUI->saveButton->setIcon(QIcon(QPixmap(PathUtils::resourcesPath() + "icons/save-script.svg"))); - _ScriptEditorWindowUI->toggleRunButton->setIcon(QIcon(QPixmap(PathUtils::resourcesPath() + "icons/start-script.svg"))); -} - -ScriptEditorWindow::~ScriptEditorWindow() { - delete _ScriptEditorWindowUI; -} - -void ScriptEditorWindow::setRunningState(bool run) { - if (_ScriptEditorWindowUI->tabWidget->currentIndex() != -1) { - static_cast(_ScriptEditorWindowUI->tabWidget->currentWidget())->setRunning(run); - } - this->updateButtons(); -} - -void ScriptEditorWindow::updateButtons() { - bool isRunning = _ScriptEditorWindowUI->tabWidget->currentIndex() != -1 && - static_cast(_ScriptEditorWindowUI->tabWidget->currentWidget())->isRunning(); - _ScriptEditorWindowUI->toggleRunButton->setEnabled(_ScriptEditorWindowUI->tabWidget->currentIndex() != -1); - _ScriptEditorWindowUI->toggleRunButton->setIcon(QIcon(QPixmap(PathUtils::resourcesPath() + ((isRunning ? - "icons/stop-script.svg" : "icons/start-script.svg"))))); -} - -void ScriptEditorWindow::loadScriptMenu(const QString& scriptName) { - addScriptEditorWidget("loading...")->loadFile(scriptName); - updateButtons(); -} - -void ScriptEditorWindow::loadScriptClicked() { - QString scriptName = QFileDialog::getOpenFileName(this, tr("Interface"), - qApp->getPreviousScriptLocation(), - tr("JavaScript Files (*.js)")); - if (!scriptName.isEmpty()) { - qApp->setPreviousScriptLocation(scriptName); - addScriptEditorWidget("loading...")->loadFile(scriptName); - updateButtons(); - } -} - -void ScriptEditorWindow::loadMenuAboutToShow() { - _loadMenu->clear(); - QStringList runningScripts = DependencyManager::get()->getRunningScripts(); - if (runningScripts.count() > 0) { - QSignalMapper* signalMapper = new QSignalMapper(this); - foreach (const QString& runningScript, runningScripts) { - QAction* runningScriptAction = new QAction(runningScript, _loadMenu); - connect(runningScriptAction, SIGNAL(triggered()), signalMapper, SLOT(map())); - signalMapper->setMapping(runningScriptAction, runningScript); - _loadMenu->addAction(runningScriptAction); - } - connect(signalMapper, SIGNAL(mapped(const QString &)), this, SLOT(loadScriptMenu(const QString&))); - } else { - QAction* naAction = new QAction("(no running scripts)", _loadMenu); - naAction->setDisabled(true); - _loadMenu->addAction(naAction); - } -} - -void ScriptEditorWindow::newScriptClicked() { - addScriptEditorWidget(QString("New script")); -} - -void ScriptEditorWindow::toggleRunScriptClicked() { - this->setRunningState(!(_ScriptEditorWindowUI->tabWidget->currentIndex() !=-1 - && static_cast(_ScriptEditorWindowUI->tabWidget->currentWidget())->isRunning())); -} - -void ScriptEditorWindow::saveScriptClicked() { - if (_ScriptEditorWindowUI->tabWidget->currentIndex() != -1) { - ScriptEditorWidget* currentScriptWidget = static_cast(_ScriptEditorWindowUI->tabWidget - ->currentWidget()); - currentScriptWidget->save(); - } -} - -void ScriptEditorWindow::saveScriptAsClicked() { - if (_ScriptEditorWindowUI->tabWidget->currentIndex() != -1) { - ScriptEditorWidget* currentScriptWidget = static_cast(_ScriptEditorWindowUI->tabWidget - ->currentWidget()); - currentScriptWidget->saveAs(); - } -} - -ScriptEditorWidget* ScriptEditorWindow::addScriptEditorWidget(QString title) { - ScriptEditorWidget* newScriptEditorWidget = new ScriptEditorWidget(); - connect(newScriptEditorWidget, &ScriptEditorWidget::scriptnameChanged, this, &ScriptEditorWindow::updateScriptNameOrStatus); - connect(newScriptEditorWidget, &ScriptEditorWidget::scriptModified, this, &ScriptEditorWindow::updateScriptNameOrStatus); - connect(newScriptEditorWidget, &ScriptEditorWidget::runningStateChanged, this, &ScriptEditorWindow::updateButtons); - connect(this, &ScriptEditorWindow::windowActivated, newScriptEditorWidget, &ScriptEditorWidget::onWindowActivated); - _ScriptEditorWindowUI->tabWidget->addTab(newScriptEditorWidget, title); - _ScriptEditorWindowUI->tabWidget->setCurrentWidget(newScriptEditorWidget); - newScriptEditorWidget->setUpdatesEnabled(true); - newScriptEditorWidget->adjustSize(); - return newScriptEditorWidget; -} - -void ScriptEditorWindow::tabSwitched(int tabIndex) { - this->updateButtons(); - if (_ScriptEditorWindowUI->tabWidget->currentIndex() != -1) { - ScriptEditorWidget* currentScriptWidget = static_cast(_ScriptEditorWindowUI->tabWidget - ->currentWidget()); - QString modifiedStar = (currentScriptWidget->isModified() ? "*" : ""); - if (currentScriptWidget->getScriptName().length() > 0) { - this->setWindowTitle("Script Editor [" + currentScriptWidget->getScriptName() + modifiedStar + "]"); - } else { - this->setWindowTitle("Script Editor [New script" + modifiedStar + "]"); - } - } else { - this->setWindowTitle("Script Editor"); - } -} - -void ScriptEditorWindow::tabCloseRequested(int tabIndex) { - if (ignoreCloseForModal(nullptr)) { - return; - } - ScriptEditorWidget* closingScriptWidget = static_cast(_ScriptEditorWindowUI->tabWidget - ->widget(tabIndex)); - if(closingScriptWidget->questionSave()) { - _ScriptEditorWindowUI->tabWidget->removeTab(tabIndex); - } -} - -// If this operating system window causes a qml overlay modal dialog (which might not even be seen by the user), closing this window -// will crash the code that was waiting on the dialog result. So that code whousl set inModalDialog to true while the question is up. -// This code will not be necessary when switch out all operating system windows for qml overlays. -bool ScriptEditorWindow::ignoreCloseForModal(QCloseEvent* event) { - if (!inModalDialog) { - return false; - } - // Deliberately not using OffscreenUi, so that the dialog is seen. - QMessageBox::information(this, tr("Interface"), tr("There is a modal dialog that must be answered before closing."), - QMessageBox::Discard, QMessageBox::Discard); - if (event) { - event->ignore(); // don't close - } - return true; -} - -void ScriptEditorWindow::closeEvent(QCloseEvent *event) { - if (ignoreCloseForModal(event)) { - return; - } - bool unsaved_docs_warning = false; - for (int i = 0; i < _ScriptEditorWindowUI->tabWidget->count(); i++){ - if(static_cast(_ScriptEditorWindowUI->tabWidget->widget(i))->isModified()){ - unsaved_docs_warning = true; - break; - } - } - - if (!unsaved_docs_warning || QMessageBox::warning(this, tr("Interface"), - tr("There are some unsaved scripts, are you sure you want to close the editor? Changes will be lost!"), - QMessageBox::Discard | QMessageBox::Cancel, QMessageBox::Cancel) == QMessageBox::Discard) { - event->accept(); - } else { - event->ignore(); - } -} - -void ScriptEditorWindow::updateScriptNameOrStatus() { - ScriptEditorWidget* source = static_cast(QObject::sender()); - QString modifiedStar = (source->isModified()? "*" : ""); - if (source->getScriptName().length() > 0) { - for (int i = 0; i < _ScriptEditorWindowUI->tabWidget->count(); i++){ - if (_ScriptEditorWindowUI->tabWidget->widget(i) == source) { - _ScriptEditorWindowUI->tabWidget->setTabText(i, modifiedStar + QFileInfo(source->getScriptName()).fileName()); - _ScriptEditorWindowUI->tabWidget->setTabToolTip(i, source->getScriptName()); - } - } - } - - if (_ScriptEditorWindowUI->tabWidget->currentWidget() == source) { - if (source->getScriptName().length() > 0) { - this->setWindowTitle("Script Editor [" + source->getScriptName() + modifiedStar + "]"); - } else { - this->setWindowTitle("Script Editor [New script" + modifiedStar + "]"); - } - } -} - -void ScriptEditorWindow::terminateCurrentTab() { - if (_ScriptEditorWindowUI->tabWidget->currentIndex() != -1) { - _ScriptEditorWindowUI->tabWidget->removeTab(_ScriptEditorWindowUI->tabWidget->currentIndex()); - this->raise(); - } -} - -bool ScriptEditorWindow::autoReloadScripts() { - return _ScriptEditorWindowUI->autoReloadCheckBox->isChecked(); -} - -bool ScriptEditorWindow::event(QEvent* event) { - if (event->type() == QEvent::WindowActivate) { - emit windowActivated(); - } - return QWidget::event(event); -} - diff --git a/interface/src/ui/ScriptEditorWindow.h b/interface/src/ui/ScriptEditorWindow.h deleted file mode 100644 index af9863d136..0000000000 --- a/interface/src/ui/ScriptEditorWindow.h +++ /dev/null @@ -1,64 +0,0 @@ -// -// ScriptEditorWindow.h -// interface/src/ui -// -// Created by Thijs Wenker on 4/14/14. -// Copyright 2014 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#ifndef hifi_ScriptEditorWindow_h -#define hifi_ScriptEditorWindow_h - -#include "ScriptEditorWidget.h" - -namespace Ui { - class ScriptEditorWindow; -} - -class ScriptEditorWindow : public QWidget { - Q_OBJECT - -public: - ScriptEditorWindow(QWidget* parent = nullptr); - ~ScriptEditorWindow(); - - void terminateCurrentTab(); - bool autoReloadScripts(); - - bool inModalDialog { false }; - bool ignoreCloseForModal(QCloseEvent* event); - -signals: - void windowActivated(); - -protected: - void closeEvent(QCloseEvent* event) override; - virtual bool event(QEvent* event) override; - -private: - Ui::ScriptEditorWindow* _ScriptEditorWindowUI; - QMenu* _loadMenu; - QMenu* _saveMenu; - - ScriptEditorWidget* addScriptEditorWidget(QString title); - void setRunningState(bool run); - void setScriptName(const QString& scriptName); - -private slots: - void loadScriptMenu(const QString& scriptName); - void loadScriptClicked(); - void newScriptClicked(); - void toggleRunScriptClicked(); - void saveScriptClicked(); - void saveScriptAsClicked(); - void loadMenuAboutToShow(); - void tabSwitched(int tabIndex); - void tabCloseRequested(int tabIndex); - void updateScriptNameOrStatus(); - void updateButtons(); -}; - -#endif // hifi_ScriptEditorWindow_h diff --git a/interface/src/ui/ScriptLineNumberArea.cpp b/interface/src/ui/ScriptLineNumberArea.cpp deleted file mode 100644 index 6d7e9185ea..0000000000 --- a/interface/src/ui/ScriptLineNumberArea.cpp +++ /dev/null @@ -1,28 +0,0 @@ -// -// ScriptLineNumberArea.cpp -// interface/src/ui -// -// Created by Thijs Wenker on 4/30/14. -// Copyright 2014 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include "ScriptLineNumberArea.h" - -#include "ScriptEditBox.h" - -ScriptLineNumberArea::ScriptLineNumberArea(ScriptEditBox* scriptEditBox) : - QWidget(scriptEditBox) -{ - _scriptEditBox = scriptEditBox; -} - -QSize ScriptLineNumberArea::sizeHint() const { - return QSize(_scriptEditBox->lineNumberAreaWidth(), 0); -} - -void ScriptLineNumberArea::paintEvent(QPaintEvent* event) { - _scriptEditBox->lineNumberAreaPaintEvent(event); -} diff --git a/interface/src/ui/ScriptLineNumberArea.h b/interface/src/ui/ScriptLineNumberArea.h deleted file mode 100644 index 77de8244ce..0000000000 --- a/interface/src/ui/ScriptLineNumberArea.h +++ /dev/null @@ -1,32 +0,0 @@ -// -// ScriptLineNumberArea.h -// interface/src/ui -// -// Created by Thijs Wenker on 4/30/14. -// Copyright 2014 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#ifndef hifi_ScriptLineNumberArea_h -#define hifi_ScriptLineNumberArea_h - -#include - -class ScriptEditBox; - -class ScriptLineNumberArea : public QWidget { - -public: - ScriptLineNumberArea(ScriptEditBox* scriptEditBox); - QSize sizeHint() const override; - -protected: - void paintEvent(QPaintEvent* event) override; - -private: - ScriptEditBox* _scriptEditBox; -}; - -#endif // hifi_ScriptLineNumberArea_h diff --git a/interface/src/ui/ScriptsTableWidget.cpp b/interface/src/ui/ScriptsTableWidget.cpp deleted file mode 100644 index 7b4f9e6b1f..0000000000 --- a/interface/src/ui/ScriptsTableWidget.cpp +++ /dev/null @@ -1,49 +0,0 @@ -// -// ScriptsTableWidget.cpp -// interface -// -// Created by Mohammed Nafees on 04/03/2014. -// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include -#include -#include -#include - -#include "ScriptsTableWidget.h" - -ScriptsTableWidget::ScriptsTableWidget(QWidget* parent) : - QTableWidget(parent) { - verticalHeader()->setVisible(false); - horizontalHeader()->setVisible(false); - setShowGrid(false); - setSelectionMode(QAbstractItemView::NoSelection); - setEditTriggers(QAbstractItemView::NoEditTriggers); - setStyleSheet("QTableWidget { border: none; background: transparent; color: #333333; } QToolTip { color: #000000; background: #f9f6e4; padding: 2px; }"); - setToolTipDuration(200); - setWordWrap(true); - setGeometry(0, 0, parent->width(), parent->height()); -} - -void ScriptsTableWidget::paintEvent(QPaintEvent* event) { - QPainter painter(viewport()); - painter.setPen(QColor::fromRgb(225, 225, 225)); // #e1e1e1 - - int y = 0; - for (int i = 0; i < rowCount(); i++) { - painter.drawLine(5, rowHeight(i) + y, width(), rowHeight(i) + y); - y += rowHeight(i); - } - painter.end(); - - QTableWidget::paintEvent(event); -} - -void ScriptsTableWidget::keyPressEvent(QKeyEvent* event) { - // Ignore keys so they will propagate correctly - event->ignore(); -} diff --git a/interface/src/ui/ScriptsTableWidget.h b/interface/src/ui/ScriptsTableWidget.h deleted file mode 100644 index f5e3407e97..0000000000 --- a/interface/src/ui/ScriptsTableWidget.h +++ /dev/null @@ -1,28 +0,0 @@ -// -// ScriptsTableWidget.h -// interface -// -// Created by Mohammed Nafees on 04/03/2014. -// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#ifndef hifi__ScriptsTableWidget_h -#define hifi__ScriptsTableWidget_h - -#include -#include - -class ScriptsTableWidget : public QTableWidget { - Q_OBJECT -public: - explicit ScriptsTableWidget(QWidget* parent); - -protected: - virtual void paintEvent(QPaintEvent* event) override; - virtual void keyPressEvent(QKeyEvent* event) override; -}; - -#endif // hifi__ScriptsTableWidget_h diff --git a/interface/src/ui/Stats.cpp b/interface/src/ui/Stats.cpp index 923d9f642d..cedcb923d9 100644 --- a/interface/src/ui/Stats.cpp +++ b/interface/src/ui/Stats.cpp @@ -38,6 +38,8 @@ using namespace std; static Stats* INSTANCE{ nullptr }; +QString getTextureMemoryPressureModeString(); + Stats* Stats::getInstance() { if (!INSTANCE) { Stats::registerType(); @@ -220,10 +222,10 @@ void Stats::updateStats(bool force) { STAT_UPDATE(audioMixerInPps, roundf(bandwidthRecorder->getAverageInputPacketsPerSecond(NodeType::AudioMixer))); STAT_UPDATE(audioMixerOutKbps, roundf(bandwidthRecorder->getAverageOutputKilobitsPerSecond(NodeType::AudioMixer))); STAT_UPDATE(audioMixerOutPps, roundf(bandwidthRecorder->getAverageOutputPacketsPerSecond(NodeType::AudioMixer))); - STAT_UPDATE(audioMicOutboundPPS, audioClient->getMicAudioOutboundPPS()); - STAT_UPDATE(audioSilentOutboundPPS, audioClient->getSilentOutboundPPS()); STAT_UPDATE(audioAudioInboundPPS, audioClient->getAudioInboundPPS()); STAT_UPDATE(audioSilentInboundPPS, audioClient->getSilentInboundPPS()); + STAT_UPDATE(audioOutboundPPS, audioClient->getAudioOutboundPPS()); + STAT_UPDATE(audioSilentOutboundPPS, audioClient->getSilentOutboundPPS()); } else { STAT_UPDATE(audioMixerKbps, -1); STAT_UPDATE(audioMixerPps, -1); @@ -231,7 +233,7 @@ void Stats::updateStats(bool force) { STAT_UPDATE(audioMixerInPps, -1); STAT_UPDATE(audioMixerOutKbps, -1); STAT_UPDATE(audioMixerOutPps, -1); - STAT_UPDATE(audioMicOutboundPPS, -1); + STAT_UPDATE(audioOutboundPPS, -1); STAT_UPDATE(audioSilentOutboundPPS, -1); STAT_UPDATE(audioAudioInboundPPS, -1); STAT_UPDATE(audioSilentInboundPPS, -1); @@ -340,10 +342,12 @@ void Stats::updateStats(bool force) { STAT_UPDATE(glContextSwapchainMemory, (int)BYTES_TO_MB(gl::Context::getSwapchainMemoryUsage())); STAT_UPDATE(qmlTextureMemory, (int)BYTES_TO_MB(OffscreenQmlSurface::getUsedTextureMemory())); + STAT_UPDATE(texturePendingTransfers, (int)BYTES_TO_MB(gpu::Texture::getTextureTransferPendingSize())); STAT_UPDATE(gpuTextureMemory, (int)BYTES_TO_MB(gpu::Texture::getTextureGPUMemoryUsage())); STAT_UPDATE(gpuTextureVirtualMemory, (int)BYTES_TO_MB(gpu::Texture::getTextureGPUVirtualMemoryUsage())); STAT_UPDATE(gpuTextureFramebufferMemory, (int)BYTES_TO_MB(gpu::Texture::getTextureGPUFramebufferMemoryUsage())); STAT_UPDATE(gpuTextureSparseMemory, (int)BYTES_TO_MB(gpu::Texture::getTextureGPUSparseMemoryUsage())); + STAT_UPDATE(gpuTextureMemoryPressureState, getTextureMemoryPressureModeString()); STAT_UPDATE(gpuSparseTextureEnabled, gpuContext->getBackend()->isTextureManagementSparseEnabled() ? 1 : 0); STAT_UPDATE(gpuFreeMemory, (int)BYTES_TO_MB(gpu::Context::getFreeGPUMemory())); STAT_UPDATE(rectifiedTextureCount, (int)RECTIFIED_TEXTURE_COUNT.load()); diff --git a/interface/src/ui/Stats.h b/interface/src/ui/Stats.h index 0ce113e0a0..a93a255a06 100644 --- a/interface/src/ui/Stats.h +++ b/interface/src/ui/Stats.h @@ -77,7 +77,7 @@ class Stats : public QQuickItem { STATS_PROPERTY(int, audioMixerOutPps, 0) STATS_PROPERTY(int, audioMixerKbps, 0) STATS_PROPERTY(int, audioMixerPps, 0) - STATS_PROPERTY(int, audioMicOutboundPPS, 0) + STATS_PROPERTY(int, audioOutboundPPS, 0) STATS_PROPERTY(int, audioSilentOutboundPPS, 0) STATS_PROPERTY(int, audioAudioInboundPPS, 0) STATS_PROPERTY(int, audioSilentInboundPPS, 0) @@ -117,11 +117,13 @@ class Stats : public QQuickItem { STATS_PROPERTY(int, gpuTexturesSparse, 0) STATS_PROPERTY(int, glContextSwapchainMemory, 0) STATS_PROPERTY(int, qmlTextureMemory, 0) + STATS_PROPERTY(int, texturePendingTransfers, 0) STATS_PROPERTY(int, gpuTextureMemory, 0) STATS_PROPERTY(int, gpuTextureVirtualMemory, 0) STATS_PROPERTY(int, gpuTextureFramebufferMemory, 0) STATS_PROPERTY(int, gpuTextureSparseMemory, 0) STATS_PROPERTY(int, gpuSparseTextureEnabled, 0) + STATS_PROPERTY(QString, gpuTextureMemoryPressureState, QString()) STATS_PROPERTY(int, gpuFreeMemory, 0) STATS_PROPERTY(float, gpuFrameTime, 0) STATS_PROPERTY(float, batchFrameTime, 0) @@ -198,7 +200,7 @@ signals: void audioMixerOutPpsChanged(); void audioMixerKbpsChanged(); void audioMixerPpsChanged(); - void audioMicOutboundPPSChanged(); + void audioOutboundPPSChanged(); void audioSilentOutboundPPSChanged(); void audioAudioInboundPPSChanged(); void audioSilentInboundPPSChanged(); @@ -232,6 +234,7 @@ signals: void timingStatsChanged(); void glContextSwapchainMemoryChanged(); void qmlTextureMemoryChanged(); + void texturePendingTransfersChanged(); void gpuBuffersChanged(); void gpuBufferMemoryChanged(); void gpuTexturesChanged(); @@ -240,6 +243,7 @@ signals: void gpuTextureVirtualMemoryChanged(); void gpuTextureFramebufferMemoryChanged(); void gpuTextureSparseMemoryChanged(); + void gpuTextureMemoryPressureStateChanged(); void gpuSparseTextureEnabledChanged(); void gpuFreeMemoryChanged(); void gpuFrameTimeChanged(); diff --git a/interface/src/ui/overlays/Overlays.cpp b/interface/src/ui/overlays/Overlays.cpp index f40dd522c4..6514052d26 100644 --- a/interface/src/ui/overlays/Overlays.cpp +++ b/interface/src/ui/overlays/Overlays.cpp @@ -431,7 +431,9 @@ RayToOverlayIntersectionResult Overlays::findRayIntersectionInternal(const PickR if (thisOverlay->findRayIntersectionExtraInfo(ray.origin, ray.direction, thisDistance, thisFace, thisSurfaceNormal, thisExtraInfo)) { bool isDrawInFront = thisOverlay->getDrawInFront(); - if (thisDistance < bestDistance && (!bestIsFront || isDrawInFront)) { + if ((bestIsFront && isDrawInFront && thisDistance < bestDistance) + || (!bestIsFront && (isDrawInFront || thisDistance < bestDistance))) { + bestIsFront = isDrawInFront; bestDistance = thisDistance; result.intersects = true; diff --git a/interface/src/ui/overlays/Web3DOverlay.cpp b/interface/src/ui/overlays/Web3DOverlay.cpp index ba864d2c5c..97e5344062 100644 --- a/interface/src/ui/overlays/Web3DOverlay.cpp +++ b/interface/src/ui/overlays/Web3DOverlay.cpp @@ -270,7 +270,7 @@ void Web3DOverlay::render(RenderArgs* args) { if (!_texture) { auto webSurface = _webSurface; - _texture = gpu::TexturePointer(gpu::Texture::createExternal2D(OffscreenQmlSurface::getDiscardLambda())); + _texture = gpu::TexturePointer(gpu::Texture::createExternal(OffscreenQmlSurface::getDiscardLambda())); _texture->setSource(__FUNCTION__); } OffscreenQmlSurface::TextureAndFence newTextureAndFence; diff --git a/interface/ui/scriptEditorWidget.ui b/interface/ui/scriptEditorWidget.ui deleted file mode 100644 index e2e538a595..0000000000 --- a/interface/ui/scriptEditorWidget.ui +++ /dev/null @@ -1,142 +0,0 @@ - - - ScriptEditorWidget - - - - 0 - 0 - 691 - 549 - - - - - 0 - 0 - - - - - 690 - 328 - - - - font-family: Helvetica, Arial, sans-serif; - - - QDockWidget::DockWidgetFloatable|QDockWidget::DockWidgetMovable - - - Qt::NoDockWidgetArea - - - Edit Script - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - Courier - -1 - 50 - false - false - - - - font: 16px "Courier"; - - - - - - - 0 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - font: 13px "Helvetica","Arial","sans-serif"; - - - Debug Log: - - - - - - - - Helvetica,Arial,sans-serif - -1 - 50 - false - false - - - - font: 13px "Helvetica","Arial","sans-serif"; - - - Run on the fly (Careful: Any valid change made to the code will run immediately) - - - - - - - Clear - - - - 16 - 16 - - - - - - - - - - - - ScriptEditBox - QTextEdit -
ui/ScriptEditBox.h
-
-
- -
diff --git a/interface/ui/scriptEditorWindow.ui b/interface/ui/scriptEditorWindow.ui deleted file mode 100644 index 1e50aaef0b..0000000000 --- a/interface/ui/scriptEditorWindow.ui +++ /dev/null @@ -1,324 +0,0 @@ - - - ScriptEditorWindow - - - Qt::NonModal - - - - 0 - 0 - 780 - 717 - - - - - 400 - 250 - - - - Script Editor - - - font-family: Helvetica, Arial, sans-serif; - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - 3 - - - QLayout::SetNoConstraint - - - 0 - - - 0 - - - - - New Script (Ctrl+N) - - - New - - - - 32 - 32 - - - - - - - - - 30 - 0 - - - - - 25 - 0 - - - - Load Script (Ctrl+O) - - - Load - - - - 32 - 32 - - - - false - - - QToolButton::MenuButtonPopup - - - Qt::ToolButtonIconOnly - - - - - - - - 30 - 0 - - - - - 32 - 0 - - - - Qt::NoFocus - - - Qt::NoContextMenu - - - Save Script (Ctrl+S) - - - Save - - - - 32 - 32 - - - - 316 - - - QToolButton::MenuButtonPopup - - - - - - - Toggle Run Script (F5) - - - Run/Stop - - - - 32 - 32 - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - font: 13px "Helvetica","Arial","sans-serif"; - - - Automatically reload externally changed files - - - - - - - - - true - - - - 250 - 80 - - - - QTabWidget::West - - - QTabWidget::Triangular - - - -1 - - - Qt::ElideNone - - - true - - - true - - - - - - - - - saveButton - clicked() - ScriptEditorWindow - saveScriptClicked() - - - 236 - 10 - - - 199 - 264 - - - - - toggleRunButton - clicked() - ScriptEditorWindow - toggleRunScriptClicked() - - - 330 - 10 - - - 199 - 264 - - - - - newButton - clicked() - ScriptEditorWindow - newScriptClicked() - - - 58 - 10 - - - 199 - 264 - - - - - loadButton - clicked() - ScriptEditorWindow - loadScriptClicked() - - - 85 - 10 - - - 199 - 264 - - - - - tabWidget - currentChanged(int) - ScriptEditorWindow - tabSwitched(int) - - - 352 - 360 - - - 352 - 340 - - - - - tabWidget - tabCloseRequested(int) - ScriptEditorWindow - tabCloseRequested(int) - - - 352 - 360 - - - 352 - 340 - - - - - diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 9ecd0f6352..0520e5c5a1 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -558,15 +558,15 @@ static const std::vector LATERAL_SPEEDS = { 0.2f, 0.65f }; // m/s void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPosition, const glm::vec3& worldVelocity, const glm::quat& worldRotation, CharacterControllerState ccState) { - glm::vec3 front = worldRotation * IDENTITY_FRONT; + glm::vec3 forward = worldRotation * IDENTITY_FORWARD; glm::vec3 workingVelocity = worldVelocity; { glm::vec3 localVel = glm::inverse(worldRotation) * workingVelocity; - float forwardSpeed = glm::dot(localVel, IDENTITY_FRONT); + float forwardSpeed = glm::dot(localVel, IDENTITY_FORWARD); float lateralSpeed = glm::dot(localVel, IDENTITY_RIGHT); - float turningSpeed = glm::orientedAngle(front, _lastFront, IDENTITY_UP) / deltaTime; + float turningSpeed = glm::orientedAngle(forward, _lastForward, IDENTITY_UP) / deltaTime; // filter speeds using a simple moving average. _averageForwardSpeed.updateAverage(forwardSpeed); @@ -852,7 +852,7 @@ void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPos _lastEnableInverseKinematics = _enableInverseKinematics; } - _lastFront = front; + _lastForward = forward; _lastPosition = worldPosition; _lastVelocity = workingVelocity; } diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index b2cc877460..41cc5cabc6 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -267,7 +267,7 @@ protected: int _rightElbowJointIndex { -1 }; int _rightShoulderJointIndex { -1 }; - glm::vec3 _lastFront; + glm::vec3 _lastForward; glm::vec3 _lastPosition; glm::vec3 _lastVelocity; diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index c32b5600d9..4a2de0a64b 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -160,7 +160,7 @@ AudioClient::AudioClient() : _loopbackAudioOutput(NULL), _loopbackOutputDevice(NULL), _inputRingBuffer(0), - _localInjectorsStream(0), + _localInjectorsStream(0, 1), _receivedAudioStream(RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES), _isStereoInput(false), _outputStarveDetectionStartTimeMsec(0), @@ -184,7 +184,6 @@ AudioClient::AudioClient() : _outgoingAvatarAudioSequenceNumber(0), _audioOutputIODevice(_localInjectorsStream, _receivedAudioStream, this), _stats(&_receivedAudioStream), - _inputGate(), _positionGetter(DEFAULT_POSITION_GETTER), _orientationGetter(DEFAULT_ORIENTATION_GETTER) { // avoid putting a lock in the device callback @@ -971,14 +970,87 @@ void AudioClient::handleLocalEchoAndReverb(QByteArray& inputByteArray) { } } -void AudioClient::handleAudioInput() { +void AudioClient::handleAudioInput(QByteArray& audioBuffer) { + if (_muted) { + _lastInputLoudness = 0.0f; + _timeSinceLastClip = 0.0f; + } else { + int16_t* samples = reinterpret_cast(audioBuffer.data()); + int numSamples = audioBuffer.size() / sizeof(AudioConstants::SAMPLE_SIZE); + bool didClip = false; + + bool shouldRemoveDCOffset = !_isPlayingBackRecording && !_isStereoInput; + if (shouldRemoveDCOffset) { + _noiseGate.removeDCOffset(samples, numSamples); + } + + bool shouldNoiseGate = (_isPlayingBackRecording || !_isStereoInput) && _isNoiseGateEnabled; + if (shouldNoiseGate) { + _noiseGate.gateSamples(samples, numSamples); + _lastInputLoudness = _noiseGate.getLastLoudness(); + didClip = _noiseGate.clippedInLastBlock(); + } else { + float loudness = 0.0f; + for (int i = 0; i < numSamples; ++i) { + int16_t sample = std::abs(samples[i]); + loudness += (float)sample; + didClip = didClip || + (sample > (AudioConstants::MAX_SAMPLE_VALUE * AudioNoiseGate::CLIPPING_THRESHOLD)); + } + _lastInputLoudness = fabs(loudness / numSamples); + } + + if (didClip) { + _timeSinceLastClip = 0.0f; + } else if (_timeSinceLastClip >= 0.0f) { + _timeSinceLastClip += (float)numSamples / (float)AudioConstants::SAMPLE_RATE; + } + + emit inputReceived({ audioBuffer.data(), numSamples }); + + if (_noiseGate.openedInLastBlock()) { + emit noiseGateOpened(); + } else if (_noiseGate.closedInLastBlock()) { + emit noiseGateClosed(); + } + } + + // the codec needs a flush frame before sending silent packets, so + // do not send one if the gate closed in this block (eventually this can be crossfaded). + auto packetType = _shouldEchoToServer ? + PacketType::MicrophoneAudioWithEcho : PacketType::MicrophoneAudioNoEcho; + if (_lastInputLoudness == 0.0f && !_noiseGate.closedInLastBlock()) { + packetType = PacketType::SilentAudioFrame; + _silentOutbound.increment(); + } else { + _audioOutbound.increment(); + } + + Transform audioTransform; + audioTransform.setTranslation(_positionGetter()); + audioTransform.setRotation(_orientationGetter()); + + QByteArray encodedBuffer; + if (_encoder) { + _encoder->encode(audioBuffer, encodedBuffer); + } else { + encodedBuffer = audioBuffer; + } + + emitAudioPacket(encodedBuffer.data(), encodedBuffer.size(), _outgoingAvatarAudioSequenceNumber, + audioTransform, avatarBoundingBoxCorner, avatarBoundingBoxScale, + packetType, _selectedCodecName); + _stats.sentPacket(); +} + +void AudioClient::handleMicAudioInput() { if (!_inputDevice || _isPlayingBackRecording) { return; } // input samples required to produce exactly NETWORK_FRAME_SAMPLES of output - const int inputSamplesRequired = (_inputToNetworkResampler ? - _inputToNetworkResampler->getMinInput(AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL) : + const int inputSamplesRequired = (_inputToNetworkResampler ? + _inputToNetworkResampler->getMinInput(AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL) : AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL) * _inputFormat.channelCount(); const auto inputAudioSamples = std::unique_ptr(new int16_t[inputSamplesRequired]); @@ -1001,126 +1073,27 @@ void AudioClient::handleAudioInput() { static int16_t networkAudioSamples[AudioConstants::NETWORK_FRAME_SAMPLES_STEREO]; while (_inputRingBuffer.samplesAvailable() >= inputSamplesRequired) { - - if (!_muted) { - - - // Increment the time since the last clip - if (_timeSinceLastClip >= 0.0f) { - _timeSinceLastClip += (float)numNetworkSamples / (float)AudioConstants::SAMPLE_RATE; - } - + if (_muted) { + _inputRingBuffer.shiftReadPosition(inputSamplesRequired); + } else { _inputRingBuffer.readSamples(inputAudioSamples.get(), inputSamplesRequired); possibleResampling(_inputToNetworkResampler, inputAudioSamples.get(), networkAudioSamples, inputSamplesRequired, numNetworkSamples, _inputFormat.channelCount(), _desiredInputFormat.channelCount()); - - // Remove DC offset - if (!_isStereoInput) { - _inputGate.removeDCOffset(networkAudioSamples, numNetworkSamples); - } - - // only impose the noise gate and perform tone injection if we are sending mono audio - if (!_isStereoInput && _isNoiseGateEnabled) { - _inputGate.gateSamples(networkAudioSamples, numNetworkSamples); - - // if we performed the noise gate we can get values from it instead of enumerating the samples again - _lastInputLoudness = _inputGate.getLastLoudness(); - - if (_inputGate.clippedInLastBlock()) { - _timeSinceLastClip = 0.0f; - } - - } else { - float loudness = 0.0f; - - for (int i = 0; i < numNetworkSamples; i++) { - int thisSample = std::abs(networkAudioSamples[i]); - loudness += (float)thisSample; - - if (thisSample > (AudioConstants::MAX_SAMPLE_VALUE * AudioNoiseGate::CLIPPING_THRESHOLD)) { - _timeSinceLastClip = 0.0f; - } - } - - _lastInputLoudness = fabs(loudness / numNetworkSamples); - } - - emit inputReceived({ reinterpret_cast(networkAudioSamples), numNetworkBytes }); - - if (_inputGate.openedInLastBlock()) { - emit noiseGateOpened(); - } else if (_inputGate.closedInLastBlock()) { - emit noiseGateClosed(); - } - - } else { - // our input loudness is 0, since we're muted - _lastInputLoudness = 0; - _timeSinceLastClip = 0.0f; - - _inputRingBuffer.shiftReadPosition(inputSamplesRequired); } - - auto packetType = _shouldEchoToServer ? - PacketType::MicrophoneAudioWithEcho : PacketType::MicrophoneAudioNoEcho; - - // if the _inputGate closed in this last frame, then we don't actually want - // to send a silent packet, instead, we want to go ahead and encode and send - // the output from the input gate (eventually, this could be crossfaded) - // and allow the codec to properly encode down to silent/zero. If we still - // have _lastInputLoudness of 0 in our NEXT frame, we will send a silent packet - if (_lastInputLoudness == 0 && !_inputGate.closedInLastBlock()) { - packetType = PacketType::SilentAudioFrame; - _silentOutbound.increment(); - } else { - _micAudioOutbound.increment(); - } - - Transform audioTransform; - audioTransform.setTranslation(_positionGetter()); - audioTransform.setRotation(_orientationGetter()); - // FIXME find a way to properly handle both playback audio and user audio concurrently - - QByteArray decodedBuffer(reinterpret_cast(networkAudioSamples), numNetworkBytes); - QByteArray encodedBuffer; - if (_encoder) { - _encoder->encode(decodedBuffer, encodedBuffer); - } else { - encodedBuffer = decodedBuffer; - } - - emitAudioPacket(encodedBuffer.constData(), encodedBuffer.size(), _outgoingAvatarAudioSequenceNumber, - audioTransform, avatarBoundingBoxCorner, avatarBoundingBoxScale, - packetType, _selectedCodecName); - _stats.sentPacket(); - int bytesInInputRingBuffer = _inputRingBuffer.samplesAvailable() * AudioConstants::SAMPLE_SIZE; float msecsInInputRingBuffer = bytesInInputRingBuffer / (float)(_inputFormat.bytesForDuration(USECS_PER_MSEC)); _stats.updateInputMsUnplayed(msecsInInputRingBuffer); + + QByteArray audioBuffer(reinterpret_cast(networkAudioSamples), numNetworkBytes); + handleAudioInput(audioBuffer); } } -// FIXME - should this go through the noise gate and honor mute and echo? void AudioClient::handleRecordedAudioInput(const QByteArray& audio) { - Transform audioTransform; - audioTransform.setTranslation(_positionGetter()); - audioTransform.setRotation(_orientationGetter()); - - QByteArray encodedBuffer; - if (_encoder) { - _encoder->encode(audio, encodedBuffer); - } else { - encodedBuffer = audio; - } - - _micAudioOutbound.increment(); - - // FIXME check a flag to see if we should echo audio? - emitAudioPacket(encodedBuffer.data(), encodedBuffer.size(), _outgoingAvatarAudioSequenceNumber, - audioTransform, avatarBoundingBoxCorner, avatarBoundingBoxScale, - PacketType::MicrophoneAudioWithEcho, _selectedCodecName); + QByteArray audioBuffer(audio); + handleAudioInput(audioBuffer); } void AudioClient::prepareLocalAudioInjectors() { @@ -1434,7 +1407,7 @@ bool AudioClient::switchInputToAudioDevice(const QAudioDeviceInfo& inputDeviceIn lock.unlock(); if (_inputDevice) { - connect(_inputDevice, SIGNAL(readyRead()), this, SLOT(handleAudioInput())); + connect(_inputDevice, SIGNAL(readyRead()), this, SLOT(handleMicAudioInput())); supportedFormat = true; } else { qCDebug(audioclient) << "Error starting audio input -" << _audioInput->error(); @@ -1540,12 +1513,39 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDevice // setup our general output device for audio-mixer audio _audioOutput = new QAudioOutput(outputDeviceInfo, _outputFormat, this); - int osDefaultBufferSize = _audioOutput->bufferSize(); int deviceChannelCount = _outputFormat.channelCount(); - int deviceFrameSize = (AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * deviceChannelCount * _outputFormat.sampleRate()) / _desiredOutputFormat.sampleRate(); - int requestedSize = _sessionOutputBufferSizeFrames * deviceFrameSize * AudioConstants::SAMPLE_SIZE; + int frameSize = (AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * deviceChannelCount * _outputFormat.sampleRate()) / _desiredOutputFormat.sampleRate(); + int requestedSize = _sessionOutputBufferSizeFrames * frameSize * AudioConstants::SAMPLE_SIZE; _audioOutput->setBufferSize(requestedSize); + // initialize mix buffers on the _audioOutput thread to avoid races + connect(_audioOutput, &QAudioOutput::stateChanged, [&, frameSize, requestedSize](QAudio::State state) { + if (state == QAudio::ActiveState) { + // restrict device callback to _outputPeriod samples + _outputPeriod = (_audioOutput->periodSize() / AudioConstants::SAMPLE_SIZE) * 2; + _outputMixBuffer = new float[_outputPeriod]; + _outputScratchBuffer = new int16_t[_outputPeriod]; + + // size local output mix buffer based on resampled network frame size + _networkPeriod = _localToOutputResampler->getMaxOutput(AudioConstants::NETWORK_FRAME_SAMPLES_STEREO); + _localOutputMixBuffer = new float[_networkPeriod]; + int localPeriod = _outputPeriod * 2; + _localInjectorsStream.resizeForFrameSize(localPeriod); + + int bufferSize = _audioOutput->bufferSize(); + int bufferSamples = bufferSize / AudioConstants::SAMPLE_SIZE; + int bufferFrames = bufferSamples / (float)frameSize; + qCDebug(audioclient) << "frame (samples):" << frameSize; + qCDebug(audioclient) << "buffer (frames):" << bufferFrames; + qCDebug(audioclient) << "buffer (samples):" << bufferSamples; + qCDebug(audioclient) << "buffer (bytes):" << bufferSize; + qCDebug(audioclient) << "requested (bytes):" << requestedSize; + qCDebug(audioclient) << "period (samples):" << _outputPeriod; + qCDebug(audioclient) << "local buffer (samples):" << localPeriod; + + disconnect(_audioOutput, &QAudioOutput::stateChanged, 0, 0); + } + }); connect(_audioOutput, &QAudioOutput::notify, this, &AudioClient::outputNotify); _audioOutputIODevice.start(); @@ -1555,18 +1555,6 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDevice _audioOutput->start(&_audioOutputIODevice); lock.unlock(); - int periodSampleSize = _audioOutput->periodSize() / AudioConstants::SAMPLE_SIZE; - // device callback is not restricted to periodSampleSize, so double the mix/scratch buffer sizes - _outputPeriod = periodSampleSize * 2; - _outputMixBuffer = new float[_outputPeriod]; - _outputScratchBuffer = new int16_t[_outputPeriod]; - _localOutputMixBuffer = new float[_outputPeriod]; - _localInjectorsStream.resizeForFrameSize(_outputPeriod * 2); - - qCDebug(audioclient) << "Output Buffer capacity in frames: " << _audioOutput->bufferSize() / AudioConstants::SAMPLE_SIZE / (float)deviceFrameSize << - "requested bytes:" << requestedSize << "actual bytes:" << _audioOutput->bufferSize() << - "os default:" << osDefaultBufferSize << "period size:" << _audioOutput->periodSize(); - // setup a loopback audio output device _loopbackAudioOutput = new QAudioOutput(outputDeviceInfo, _outputFormat, this); diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index 7e9acc0586..139749e8e8 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -124,16 +124,16 @@ public: void selectAudioFormat(const QString& selectedCodecName); Q_INVOKABLE QString getSelectedAudioFormat() const { return _selectedCodecName; } - Q_INVOKABLE bool getNoiseGateOpen() const { return _inputGate.isOpen(); } - Q_INVOKABLE float getSilentOutboundPPS() const { return _silentOutbound.rate(); } - Q_INVOKABLE float getMicAudioOutboundPPS() const { return _micAudioOutbound.rate(); } + Q_INVOKABLE bool getNoiseGateOpen() const { return _noiseGate.isOpen(); } Q_INVOKABLE float getSilentInboundPPS() const { return _silentInbound.rate(); } Q_INVOKABLE float getAudioInboundPPS() const { return _audioInbound.rate(); } + Q_INVOKABLE float getSilentOutboundPPS() const { return _silentOutbound.rate(); } + Q_INVOKABLE float getAudioOutboundPPS() const { return _audioOutbound.rate(); } const MixedProcessedAudioStream& getReceivedAudioStream() const { return _receivedAudioStream; } MixedProcessedAudioStream& getReceivedAudioStream() { return _receivedAudioStream; } - float getLastInputLoudness() const { return glm::max(_lastInputLoudness - _inputGate.getMeasuredFloor(), 0.0f); } + float getLastInputLoudness() const { return glm::max(_lastInputLoudness - _noiseGate.getMeasuredFloor(), 0.0f); } float getTimeSinceLastClip() const { return _timeSinceLastClip; } float getAudioAverageInputLoudness() const { return _lastInputLoudness; } @@ -180,7 +180,7 @@ public slots: void handleMismatchAudioFormat(SharedNodePointer node, const QString& currentCodec, const QString& recievedCodec); void sendDownstreamAudioStatsPacket() { _stats.publish(); } - void handleAudioInput(); + void handleMicAudioInput(); void handleRecordedAudioInput(const QByteArray& audio); void reset(); void audioMixerKilled(); @@ -250,6 +250,7 @@ protected: private: void outputFormatChanged(); + void handleAudioInput(QByteArray& audioBuffer); bool mixLocalAudioInjectors(float* mixBuffer); float azimuthForSource(const glm::vec3& relativePosition); float gainForSource(float distance, float volume); @@ -339,6 +340,7 @@ private: int16_t _networkScratchBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_AMBISONIC]; // for local audio (used by audio injectors thread) + int _networkPeriod { 0 }; float _localMixBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_STEREO]; int16_t _localScratchBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_AMBISONIC]; float* _localOutputMixBuffer { NULL }; @@ -371,7 +373,7 @@ private: AudioIOStats _stats; - AudioNoiseGate _inputGate; + AudioNoiseGate _noiseGate; AudioPositionGetter _positionGetter; AudioOrientationGetter _orientationGetter; @@ -395,7 +397,7 @@ private: QThread* _checkDevicesThread { nullptr }; RateCounter<> _silentOutbound; - RateCounter<> _micAudioOutbound; + RateCounter<> _audioOutbound; RateCounter<> _silentInbound; RateCounter<> _audioInbound; }; diff --git a/libraries/avatars/src/HeadData.cpp b/libraries/avatars/src/HeadData.cpp index 72516d9740..bf8593f1d9 100644 --- a/libraries/avatars/src/HeadData.cpp +++ b/libraries/avatars/src/HeadData.cpp @@ -65,8 +65,8 @@ glm::quat HeadData::getOrientation() const { void HeadData::setOrientation(const glm::quat& orientation) { // rotate body about vertical axis glm::quat bodyOrientation = _owningAvatar->getOrientation(); - glm::vec3 newFront = glm::inverse(bodyOrientation) * (orientation * IDENTITY_FRONT); - bodyOrientation = bodyOrientation * glm::angleAxis(atan2f(-newFront.x, -newFront.z), glm::vec3(0.0f, 1.0f, 0.0f)); + glm::vec3 newForward = glm::inverse(bodyOrientation) * (orientation * IDENTITY_FORWARD); + bodyOrientation = bodyOrientation * glm::angleAxis(atan2f(-newForward.x, -newForward.z), glm::vec3(0.0f, 1.0f, 0.0f)); _owningAvatar->setOrientation(bodyOrientation); // the rest goes to the head diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index b23b59d3f0..5a317f64bc 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -355,14 +355,16 @@ void OpenGLDisplayPlugin::customizeContext() { if ((image.width() > 0) && (image.height() > 0)) { cursorData.texture.reset( - gpu::Texture::create2D( + gpu::Texture::createStrict( gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA), image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); cursorData.texture->setSource("cursor texture"); auto usage = gpu::Texture::Usage::Builder().withColor().withAlpha(); cursorData.texture->setUsage(usage.build()); - cursorData.texture->assignStoredMip(0, gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA), image.byteCount(), image.constBits()); + cursorData.texture->setStoredMipFormat(gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA)); + cursorData.texture->assignStoredMip(0, image.byteCount(), image.constBits()); + cursorData.texture->autoGenerateMips(-1); } } } diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index a8b8ba3618..c55d985a62 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -296,33 +296,32 @@ void HmdDisplayPlugin::internalPresent() { image = image.convertToFormat(QImage::Format_RGBA8888); if (!_previewTexture) { _previewTexture.reset( - gpu::Texture::create2D( + gpu::Texture::createStrict( gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA), image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); _previewTexture->setSource("HMD Preview Texture"); _previewTexture->setUsage(gpu::Texture::Usage::Builder().withColor().build()); - _previewTexture->assignStoredMip(0, gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA), image.byteCount(), image.constBits()); + _previewTexture->setStoredMipFormat(gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA)); + _previewTexture->assignStoredMip(0, image.byteCount(), image.constBits()); _previewTexture->autoGenerateMips(-1); } - if (getGLBackend()->isTextureReady(_previewTexture)) { - auto viewport = getViewportForSourceSize(uvec2(_previewTexture->getDimensions())); + auto viewport = getViewportForSourceSize(uvec2(_previewTexture->getDimensions())); - render([&](gpu::Batch& batch) { - batch.enableStereo(false); - batch.resetViewTransform(); - batch.setFramebuffer(gpu::FramebufferPointer()); - batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, vec4(0)); - batch.setStateScissorRect(viewport); - batch.setViewportTransform(viewport); - batch.setResourceTexture(0, _previewTexture); - batch.setPipeline(_presentPipeline); - batch.draw(gpu::TRIANGLE_STRIP, 4); - }); - _clearPreviewFlag = false; - swapBuffers(); - } + render([&](gpu::Batch& batch) { + batch.enableStereo(false); + batch.resetViewTransform(); + batch.setFramebuffer(gpu::FramebufferPointer()); + batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, vec4(0)); + batch.setStateScissorRect(viewport); + batch.setViewportTransform(viewport); + batch.setResourceTexture(0, _previewTexture); + batch.setPipeline(_presentPipeline); + batch.draw(gpu::TRIANGLE_STRIP, 4); + }); + _clearPreviewFlag = false; + swapBuffers(); } postPreview(); diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index 27e00b47c6..fb6054a514 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -146,6 +146,7 @@ void EntityTreeRenderer::clear() { void EntityTreeRenderer::reloadEntityScripts() { _entitiesScriptEngine->unloadAllEntityScripts(); + _entitiesScriptEngine->resetModuleCache(); foreach(auto entity, _entitiesInScene) { if (!entity->getScript().isEmpty()) { _entitiesScriptEngine->loadEntityScript(entity->getEntityItemID(), entity->getScript(), true); @@ -940,7 +941,7 @@ void EntityTreeRenderer::mouseMoveEvent(QMouseEvent* event) { void EntityTreeRenderer::deletingEntity(const EntityItemID& entityID) { if (_tree && !_shuttingDown && _entitiesScriptEngine) { - _entitiesScriptEngine->unloadEntityScript(entityID); + _entitiesScriptEngine->unloadEntityScript(entityID, true); } forceRecheckEntities(); // reset our state to force checking our inside/outsideness of entities @@ -995,7 +996,7 @@ void EntityTreeRenderer::checkAndCallPreload(const EntityItemID& entityID, const } bool shouldLoad = entity->shouldPreloadScript() && _entitiesScriptEngine; QString scriptUrl = entity->getScript(); - if ((unloadFirst && shouldLoad) || scriptUrl.isEmpty()) { + if (shouldLoad && (unloadFirst || scriptUrl.isEmpty())) { _entitiesScriptEngine->unloadEntityScript(entityID); entity->scriptHasUnloaded(); } diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp index 7359a548fc..1d58527427 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp @@ -14,6 +14,7 @@ #include #include #include +#include "ModelScriptingInterface.h" #if defined(__GNUC__) && !defined(__clang__) #pragma GCC diagnostic push @@ -53,6 +54,8 @@ #include "PhysicalEntitySimulation.h" gpu::PipelinePointer RenderablePolyVoxEntityItem::_pipeline = nullptr; +gpu::PipelinePointer RenderablePolyVoxEntityItem::_wireframePipeline = nullptr; + const float MARCHING_CUBE_COLLISION_HULL_OFFSET = 0.5; @@ -73,7 +76,7 @@ const float MARCHING_CUBE_COLLISION_HULL_OFFSET = 0.5; _meshDirty In RenderablePolyVoxEntityItem::render, these flags are checked and changes are propagated along the chain. - decompressVolumeData() is called to decompress _voxelData into _volData. getMesh() is called to invoke the + decompressVolumeData() is called to decompress _voxelData into _volData. recomputeMesh() is called to invoke the polyVox surface extractor to create _mesh (as well as set Simulation _dirtyFlags). Because Simulation::DIRTY_SHAPE is set, isReadyToComputeShape() gets called and _shape is created either from _volData or _shape, depending on the surface style. @@ -81,7 +84,7 @@ const float MARCHING_CUBE_COLLISION_HULL_OFFSET = 0.5; When a script changes _volData, compressVolumeDataAndSendEditPacket is called to update _voxelData and to send a packet to the entity-server. - decompressVolumeData, getMesh, computeShapeInfoWorker, and compressVolumeDataAndSendEditPacket are too expensive + decompressVolumeData, recomputeMesh, computeShapeInfoWorker, and compressVolumeDataAndSendEditPacket are too expensive to run on a thread that has other things to do. These use QtConcurrent::run to spawn a thread. As each thread finishes, it adjusts the dirty flags so that the next call to render() will kick off the next step. @@ -663,11 +666,8 @@ void RenderablePolyVoxEntityItem::setZTextureURL(QString zTextureURL) { } } -void RenderablePolyVoxEntityItem::render(RenderArgs* args) { - PerformanceTimer perfTimer("RenderablePolyVoxEntityItem::render"); - assert(getType() == EntityTypes::PolyVox); - Q_ASSERT(args->_batch); +bool RenderablePolyVoxEntityItem::updateDependents() { bool voxelDataDirty; bool volDataDirty; withWriteLock([&] { @@ -682,9 +682,20 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) { if (voxelDataDirty) { decompressVolumeData(); } else if (volDataDirty) { - getMesh(); + recomputeMesh(); } + return !volDataDirty; +} + + +void RenderablePolyVoxEntityItem::render(RenderArgs* args) { + PerformanceTimer perfTimer("RenderablePolyVoxEntityItem::render"); + assert(getType() == EntityTypes::PolyVox); + Q_ASSERT(args->_batch); + + updateDependents(); + model::MeshPointer mesh; glm::vec3 voxelVolumeSize; withReadLock([&] { @@ -696,7 +707,7 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) { !mesh->getIndexBuffer()._buffer) { return; } - + if (!_pipeline) { gpu::ShaderPointer vertexShader = gpu::Shader::createVertex(std::string(polyvox_vert)); gpu::ShaderPointer pixelShader = gpu::Shader::createPixel(std::string(polyvox_frag)); @@ -715,6 +726,13 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) { state->setDepthTest(true, true, gpu::LESS_EQUAL); _pipeline = gpu::Pipeline::create(program, state); + + auto wireframeState = std::make_shared(); + wireframeState->setCullMode(gpu::State::CULL_BACK); + wireframeState->setDepthTest(true, true, gpu::LESS_EQUAL); + wireframeState->setFillMode(gpu::State::FILL_LINE); + + _wireframePipeline = gpu::Pipeline::create(program, wireframeState); } if (!_vertexFormat) { @@ -725,7 +743,11 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) { } gpu::Batch& batch = *args->_batch; - batch.setPipeline(_pipeline); + + // Pick correct Pipeline + bool wireframe = (render::ShapeKey(args->_globalShapeKey).isWireframe()); + auto pipeline = (wireframe ? _wireframePipeline : _pipeline); + batch.setPipeline(pipeline); Transform transform(voxelToWorldMatrix()); batch.setModelTransform(transform); @@ -762,7 +784,7 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) { batch.setResourceTexture(2, DependencyManager::get()->getWhiteTexture()); } - int voxelVolumeSizeLocation = _pipeline->getProgram()->getUniforms().findLocation("voxelVolumeSize"); + int voxelVolumeSizeLocation = pipeline->getProgram()->getUniforms().findLocation("voxelVolumeSize"); batch._glUniform3f(voxelVolumeSizeLocation, voxelVolumeSize.x, voxelVolumeSize.y, voxelVolumeSize.z); batch.drawIndexed(gpu::TRIANGLES, (gpu::uint32)mesh->getNumIndices(), 0); @@ -1199,7 +1221,7 @@ void RenderablePolyVoxEntityItem::copyUpperEdgesFromNeighbors() { } } -void RenderablePolyVoxEntityItem::getMesh() { +void RenderablePolyVoxEntityItem::recomputeMesh() { // use _volData to make a renderable mesh PolyVoxSurfaceStyle voxelSurfaceStyle; withReadLock([&] { @@ -1269,12 +1291,20 @@ void RenderablePolyVoxEntityItem::getMesh() { vertexBufferPtr->getSize() , sizeof(PolyVox::PositionMaterialNormal), gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::RAW))); + + std::vector parts; + parts.emplace_back(model::Mesh::Part((model::Index)0, // startIndex + (model::Index)vecIndices.size(), // numIndices + (model::Index)0, // baseVertex + model::Mesh::TRIANGLES)); // topology + mesh->setPartBuffer(gpu::BufferView(new gpu::Buffer(parts.size() * sizeof(model::Mesh::Part), + (gpu::Byte*) parts.data()), gpu::Element::PART_DRAWCALL)); entity->setMesh(mesh); }); } void RenderablePolyVoxEntityItem::setMesh(model::MeshPointer mesh) { - // this catches the payload from getMesh + // this catches the payload from recomputeMesh bool neighborsNeedUpdate; withWriteLock([&] { if (!_collisionless) { @@ -1531,7 +1561,6 @@ std::shared_ptr RenderablePolyVoxEntityItem::getZPN return std::dynamic_pointer_cast(_zPNeighbor.lock()); } - void RenderablePolyVoxEntityItem::bonkNeighbors() { // flag neighbors to the negative of this entity as needing to rebake their meshes. cacheNeighbors(); @@ -1551,7 +1580,6 @@ void RenderablePolyVoxEntityItem::bonkNeighbors() { } } - void RenderablePolyVoxEntityItem::locationChanged(bool tellPhysics) { EntityItem::locationChanged(tellPhysics); if (!_pipeline || !render::Item::isValidID(_myItem)) { @@ -1563,3 +1591,25 @@ void RenderablePolyVoxEntityItem::locationChanged(bool tellPhysics) { scene->enqueuePendingChanges(pendingChanges); } + +bool RenderablePolyVoxEntityItem::getMeshAsScriptValue(QScriptEngine *engine, QScriptValue& result) { + if (!updateDependents()) { + return false; + } + + bool success = false; + MeshProxy* meshProxy = nullptr; + glm::mat4 transform = voxelToLocalMatrix(); + withReadLock([&] { + if (_meshInitialized) { + success = true; + // the mesh will be in voxel-space. transform it into object-space + meshProxy = new MeshProxy( + _mesh->map([=](glm::vec3 position){ return glm::vec3(transform * glm::vec4(position, 1.0f)); }, + [=](glm::vec3 normal){ return glm::vec3(transform * glm::vec4(normal, 0.0f)); }, + [](uint32_t index){ return index; })); + } + }); + result = meshToScriptValue(engine, meshProxy); + return success; +} diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h index 45842c2fb9..cf4672f068 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h @@ -61,6 +61,8 @@ public: virtual uint8_t getVoxel(int x, int y, int z) override; virtual bool setVoxel(int x, int y, int z, uint8_t toValue) override; + int getOnCount() const override { return _onCount; } + void render(RenderArgs* args) override; virtual bool supportsDetailedRayIntersection() const override { return true; } virtual bool findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, @@ -133,6 +135,7 @@ public: QByteArray volDataToArray(quint16 voxelXSize, quint16 voxelYSize, quint16 voxelZSize) const; void setMesh(model::MeshPointer mesh); + bool getMeshAsScriptValue(QScriptEngine *engine, QScriptValue& result) override; void setCollisionPoints(ShapeInfo::PointCollection points, AABox box); PolyVox::SimpleVolume* getVolData() { return _volData; } @@ -163,11 +166,12 @@ private: const int MATERIAL_GPU_SLOT = 3; render::ItemID _myItem{ render::Item::INVALID_ITEM_ID }; static gpu::PipelinePointer _pipeline; + static gpu::PipelinePointer _wireframePipeline; ShapeInfo _shapeInfo; PolyVox::SimpleVolume* _volData = nullptr; - bool _volDataDirty = false; // does getMesh need to be called? + bool _volDataDirty = false; // does recomputeMesh need to be called? int _onCount; // how many non-zero voxels are in _volData bool _neighborsNeedUpdate { false }; @@ -178,7 +182,7 @@ private: // these are run off the main thread void decompressVolumeData(); void compressVolumeDataAndSendEditPacket(); - virtual void getMesh() override; // recompute mesh + virtual void recomputeMesh() override; // recompute mesh void computeShapeInfoWorker(); // these are cached lookups of _xNNeighborID, _yNNeighborID, _zNNeighborID, _xPNeighborID, _yPNeighborID, _zPNeighborID @@ -191,6 +195,7 @@ private: void cacheNeighbors(); void copyUpperEdgesFromNeighbors(); void bonkNeighbors(); + bool updateDependents(); }; bool inUserBounds(const PolyVox::SimpleVolume* vol, PolyVoxEntityItem::PolyVoxSurfaceStyle surfaceStyle, diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp index c3e097382c..1ad60bf7c6 100644 --- a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp @@ -114,13 +114,22 @@ void RenderableShapeEntityItem::render(RenderArgs* args) { auto outColor = _procedural->getColor(color); outColor.a *= _procedural->isFading() ? Interpolate::calculateFadeRatio(_procedural->getFadeStartTime()) : 1.0f; batch._glColor4f(outColor.r, outColor.g, outColor.b, outColor.a); - DependencyManager::get()->renderShape(batch, MAPPING[_shape]); + if (render::ShapeKey(args->_globalShapeKey).isWireframe()) { + DependencyManager::get()->renderWireShape(batch, MAPPING[_shape]); + } else { + DependencyManager::get()->renderShape(batch, MAPPING[_shape]); + } } else { // FIXME, support instanced multi-shape rendering using multidraw indirect color.a *= _isFading ? Interpolate::calculateFadeRatio(_fadeStartTime) : 1.0f; auto geometryCache = DependencyManager::get(); auto pipeline = color.a < 1.0f ? geometryCache->getTransparentShapePipeline() : geometryCache->getOpaqueShapePipeline(); - geometryCache->renderSolidShapeInstance(batch, MAPPING[_shape], color, pipeline); + + if (render::ShapeKey(args->_globalShapeKey).isWireframe()) { + geometryCache->renderWireShapeInstance(batch, MAPPING[_shape], color, pipeline); + } else { + geometryCache->renderSolidShapeInstance(batch, MAPPING[_shape], color, pipeline); + } } static const auto triCount = DependencyManager::get()->getShapeTriangleCount(MAPPING[_shape]); diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp index d7d7013f59..c4ae0db1aa 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp @@ -216,7 +216,7 @@ void RenderableWebEntityItem::render(RenderArgs* args) { if (!_texture) { auto webSurface = _webSurface; - _texture = gpu::TexturePointer(gpu::Texture::createExternal2D(OffscreenQmlSurface::getDiscardLambda())); + _texture = gpu::TexturePointer(gpu::Texture::createExternal(OffscreenQmlSurface::getDiscardLambda())); _texture->setSource(__FUNCTION__); } OffscreenQmlSurface::TextureAndFence newTextureAndFence; diff --git a/libraries/entities/src/EntitiesScriptEngineProvider.h b/libraries/entities/src/EntitiesScriptEngineProvider.h index 69bf73e688..d87dd105c2 100644 --- a/libraries/entities/src/EntitiesScriptEngineProvider.h +++ b/libraries/entities/src/EntitiesScriptEngineProvider.h @@ -15,11 +15,13 @@ #define hifi_EntitiesScriptEngineProvider_h #include +#include #include "EntityItemID.h" class EntitiesScriptEngineProvider { public: virtual void callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const QStringList& params = QStringList()) = 0; + virtual QFuture getLocalEntityScriptDetails(const EntityItemID& entityID) = 0; }; -#endif // hifi_EntitiesScriptEngineProvider_h \ No newline at end of file +#endif // hifi_EntitiesScriptEngineProvider_h diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index 3ef1648fae..0bb085459e 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -655,13 +655,11 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef // pack SimulationOwner and terse update properties near each other - // NOTE: the server is authoritative for changes to simOwnerID so we always unpack ownership data // even when we would otherwise ignore the rest of the packet. bool filterRejection = false; if (propertyFlags.getHasProperty(PROP_SIMULATION_OWNER)) { - QByteArray simOwnerData; int bytes = OctreePacketData::unpackDataFromBytes(dataAt, simOwnerData); SimulationOwner newSimOwner; @@ -1879,6 +1877,7 @@ void EntityItem::setSimulationOwner(const SimulationOwner& owner) { } void EntityItem::updateSimulationOwner(const SimulationOwner& owner) { + // NOTE: this method only used by EntityServer. The Interface uses special code in readEntityDataFromBuffer(). if (wantTerseEditLogging() && _simulationOwner != owner) { qCDebug(entities) << "sim ownership for" << getDebugName() << "is now" << owner; } @@ -1894,8 +1893,9 @@ void EntityItem::clearSimulationOwnership() { } _simulationOwner.clear(); - // don't bother setting the DIRTY_SIMULATOR_ID flag because clearSimulationOwnership() - // is only ever called on the entity-server and the flags are only used client-side + // don't bother setting the DIRTY_SIMULATOR_ID flag because: + // (a) when entity-server calls clearSimulationOwnership() the dirty-flags are meaningless (only used by interface) + // (b) the interface only calls clearSimulationOwnership() in a context that already knows best about dirty flags //_dirtyFlags |= Simulation::DIRTY_SIMULATOR_ID; } diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index ea81df3801..1ed020e592 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -49,13 +49,6 @@ EntityItemProperties::EntityItemProperties(EntityPropertyFlags desiredProperties } -void EntityItemProperties::setSittingPoints(const QVector& sittingPoints) { - _sittingPoints.clear(); - foreach (SittingPoint sitPoint, sittingPoints) { - _sittingPoints.append(sitPoint); - } -} - void EntityItemProperties::calculateNaturalPosition(const glm::vec3& min, const glm::vec3& max) { glm::vec3 halfDimension = (max - min) / 2.0f; _naturalPosition = max - halfDimension; @@ -546,20 +539,6 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_TEXTURES, textures); } - // Sitting properties support - if (!skipDefaults && !strictSemantics) { - QScriptValue sittingPoints = engine->newObject(); - for (int i = 0; i < _sittingPoints.size(); ++i) { - QScriptValue sittingPoint = engine->newObject(); - sittingPoint.setProperty("name", _sittingPoints.at(i).name); - sittingPoint.setProperty("position", vec3toScriptValue(engine, _sittingPoints.at(i).position)); - sittingPoint.setProperty("rotation", quatToScriptValue(engine, _sittingPoints.at(i).rotation)); - sittingPoints.setProperty(i, sittingPoint); - } - sittingPoints.setProperty("length", _sittingPoints.size()); - COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER_ALWAYS(sittingPoints, sittingPoints); // gettable, but not settable - } - if (!skipDefaults && !strictSemantics) { AABox aaBox = getAABox(); QScriptValue boundingBox = engine->newObject(); diff --git a/libraries/entities/src/EntityItemProperties.h b/libraries/entities/src/EntityItemProperties.h index 419740e4ea..590298e102 100644 --- a/libraries/entities/src/EntityItemProperties.h +++ b/libraries/entities/src/EntityItemProperties.h @@ -22,7 +22,6 @@ #include #include -#include // for SittingPoint #include #include #include @@ -255,8 +254,6 @@ public: void clearID() { _id = UNKNOWN_ENTITY_ID; _idSet = false; } void markAllChanged(); - void setSittingPoints(const QVector& sittingPoints); - const glm::vec3& getNaturalDimensions() const { return _naturalDimensions; } void setNaturalDimensions(const glm::vec3& value) { _naturalDimensions = value; } @@ -325,7 +322,6 @@ private: // NOTE: The following are pseudo client only properties. They are only used in clients which can access // properties of model geometry. But these properties are not serialized like other properties. - QVector _sittingPoints; QVariantMap _textureNames; glm::vec3 _naturalDimensions; glm::vec3 _naturalPosition; diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index 540eba4511..1ab5438e53 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -8,8 +8,15 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // + +#include +#include + #include "EntityScriptingInterface.h" +#include +#include + #include "EntityItemID.h" #include #include @@ -289,13 +296,11 @@ EntityItemProperties EntityScriptingInterface::getEntityProperties(QUuid identit results = entity->getProperties(desiredProperties); - // TODO: improve sitting points and naturalDimensions in the future, - // for now we've included the old sitting points model behavior for entity types that are models - // we've also added this hack for setting natural dimensions of models + // TODO: improve naturalDimensions in the future, + // for now we've added this hack for setting natural dimensions of models if (entity->getType() == EntityTypes::Model) { const FBXGeometry* geometry = _entityTree->getGeometryForEntity(entity); if (geometry) { - results.setSittingPoints(geometry->sittingPoints); Extents meshExtents = geometry->getUnscaledMeshExtents(); results.setNaturalDimensions(meshExtents.maximum - meshExtents.minimum); results.calculateNaturalPosition(meshExtents.minimum, meshExtents.maximum); @@ -680,6 +685,118 @@ bool EntityScriptingInterface::reloadServerScripts(QUuid entityID) { return client->reloadServerScript(entityID); } +bool EntityPropertyMetadataRequest::script(EntityItemID entityID, QScriptValue handler) { + using LocalScriptStatusRequest = QFutureWatcher; + + LocalScriptStatusRequest* request = new LocalScriptStatusRequest; + QObject::connect(request, &LocalScriptStatusRequest::finished, _engine, [=]() mutable { + auto details = request->result().toMap(); + QScriptValue err, result; + if (details.contains("isError")) { + if (!details.contains("message")) { + details["message"] = details["errorInfo"]; + } + err = _engine->makeError(_engine->toScriptValue(details)); + } else { + details["success"] = true; + result = _engine->toScriptValue(details); + } + callScopedHandlerObject(handler, err, result); + request->deleteLater(); + }); + auto entityScriptingInterface = DependencyManager::get(); + entityScriptingInterface->withEntitiesScriptEngine([&](EntitiesScriptEngineProvider* entitiesScriptEngine) { + if (entitiesScriptEngine) { + request->setFuture(entitiesScriptEngine->getLocalEntityScriptDetails(entityID)); + } + }); + if (!request->isStarted()) { + request->deleteLater(); + callScopedHandlerObject(handler, _engine->makeError("Entities Scripting Provider unavailable", "InternalError"), QScriptValue()); + return false; + } + return true; +} + +bool EntityPropertyMetadataRequest::serverScripts(EntityItemID entityID, QScriptValue handler) { + auto client = DependencyManager::get(); + auto request = client->createScriptStatusRequest(entityID); + QPointer engine = _engine; + QObject::connect(request, &GetScriptStatusRequest::finished, _engine, [=](GetScriptStatusRequest* request) mutable { + auto engine = _engine; + if (!engine) { + qCDebug(entities) << __FUNCTION__ << " -- engine destroyed while inflight" << entityID; + return; + } + QVariantMap details; + details["success"] = request->getResponseReceived(); + details["isRunning"] = request->getIsRunning(); + details["status"] = EntityScriptStatus_::valueToKey(request->getStatus()).toLower(); + details["errorInfo"] = request->getErrorInfo(); + + QScriptValue err, result; + if (!details["success"].toBool()) { + if (!details.contains("message") && details.contains("errorInfo")) { + details["message"] = details["errorInfo"]; + } + if (details["message"].toString().isEmpty()) { + details["message"] = "entity server script details not found"; + } + err = engine->makeError(engine->toScriptValue(details)); + } else { + result = engine->toScriptValue(details); + } + callScopedHandlerObject(handler, err, result); + request->deleteLater(); + }); + request->start(); + return true; +} + +bool EntityScriptingInterface::queryPropertyMetadata(QUuid entityID, QScriptValue property, QScriptValue scopeOrCallback, QScriptValue methodOrName) { + auto name = property.toString(); + auto handler = makeScopedHandlerObject(scopeOrCallback, methodOrName); + QPointer engine = dynamic_cast(handler.engine()); + if (!engine) { + qCDebug(entities) << "queryPropertyMetadata without detectable engine" << entityID << name; + return false; + } +#ifdef DEBUG_ENGINE_STATE + connect(engine, &QObject::destroyed, this, [=]() { + qDebug() << "queryPropertyMetadata -- engine destroyed!" << (!engine ? "nullptr" : "engine"); + }); +#endif + if (!handler.property("callback").isFunction()) { + qDebug() << "!handler.callback.isFunction" << engine; + engine->raiseException(engine->makeError("callback is not a function", "TypeError")); + return false; + } + + // NOTE: this approach is a work-in-progress and for now just meant to work 100% correctly and provide + // some initial structure for organizing metadata adapters around. + + // The extra layer of indirection is *essential* because in real world conditions errors are often introduced + // by accident and sometimes without exact memory of "what just changed." + + // Here the scripter only needs to know an entityID and a property name -- which means all scripters can + // level this method when stuck in dead-end scenarios or to learn more about "magic" Entity properties + // like .script that work in terms of side-effects. + + // This is an async callback pattern -- so if needed C++ can easily throttle or restrict queries later. + + EntityPropertyMetadataRequest request(engine); + + if (name == "script") { + return request.script(entityID, handler); + } else if (name == "serverScripts") { + return request.serverScripts(entityID, handler); + } else { + engine->raiseException(engine->makeError("metadata for property " + name + " is not yet queryable")); + engine->maybeEmitUncaughtException(__FUNCTION__); + return false; + } +} + bool EntityScriptingInterface::getServerScriptStatus(QUuid entityID, QScriptValue callback) { auto client = DependencyManager::get(); auto request = client->createScriptStatusRequest(entityID); @@ -815,8 +932,7 @@ void RayToEntityIntersectionResultFromScriptValue(const QScriptValue& object, Ra } } -bool EntityScriptingInterface::setVoxels(QUuid entityID, - std::function actor) { +bool EntityScriptingInterface::polyVoxWorker(QUuid entityID, std::function actor) { PROFILE_RANGE(script_entities, __FUNCTION__); if (!_entityTree) { @@ -882,11 +998,9 @@ bool EntityScriptingInterface::setPoints(QUuid entityID, std::function& points) { PROFILE_RANGE(script_entities, __FUNCTION__); @@ -1541,3 +1674,20 @@ bool EntityScriptingInterface::AABoxIntersectsCapsule(const glm::vec3& low, cons AABox aaBox(low, dimensions); return aaBox.findCapsulePenetration(start, end, radius, penetration); } + +glm::mat4 EntityScriptingInterface::getEntityTransform(const QUuid& entityID) { + glm::mat4 result; + if (_entityTree) { + _entityTree->withReadLock([&] { + EntityItemPointer entity = _entityTree->findEntityByEntityItemID(EntityItemID(entityID)); + if (entity) { + glm::mat4 translation = glm::translate(entity->getPosition()); + glm::mat4 rotation = glm::mat4_cast(entity->getRotation()); + glm::mat4 registration = glm::translate(ENTITY_ITEM_DEFAULT_REGISTRATION_POINT - + entity->getRegistrationPoint()); + result = translation * rotation * registration; + } + }); + } + return result; +} diff --git a/libraries/entities/src/EntityScriptingInterface.h b/libraries/entities/src/EntityScriptingInterface.h index e9f0637830..63b5771e60 100644 --- a/libraries/entities/src/EntityScriptingInterface.h +++ b/libraries/entities/src/EntityScriptingInterface.h @@ -34,7 +34,23 @@ #include "EntitiesScriptEngineProvider.h" #include "EntityItemProperties.h" +#include "BaseScriptEngine.h" + class EntityTree; +class MeshProxy; + +// helper factory to compose standardized, async metadata queries for "magic" Entity properties +// like .script and .serverScripts. This is used for automated testing of core scripting features +// as well as to provide early adopters a self-discoverable, consistent way to diagnose common +// problems with their own Entity scripts. +class EntityPropertyMetadataRequest { +public: + EntityPropertyMetadataRequest(BaseScriptEngine* engine) : _engine(engine) {}; + bool script(EntityItemID entityID, QScriptValue handler); + bool serverScripts(EntityItemID entityID, QScriptValue handler); +private: + QPointer _engine; +}; class RayToEntityIntersectionResult { public: @@ -67,6 +83,7 @@ class EntityScriptingInterface : public OctreeScriptingInterface, public Depende Q_PROPERTY(float costMultiplier READ getCostMultiplier WRITE setCostMultiplier) Q_PROPERTY(QUuid keyboardFocusEntity READ getKeyboardFocusEntity WRITE setKeyboardFocusEntity) + friend EntityPropertyMetadataRequest; public: EntityScriptingInterface(bool bidOnSimulationOwnership); @@ -211,6 +228,26 @@ public slots: Q_INVOKABLE RayToEntityIntersectionResult findRayIntersectionBlocking(const PickRay& ray, bool precisionPicking = false, const QScriptValue& entityIdsToInclude = QScriptValue(), const QScriptValue& entityIdsToDiscard = QScriptValue()); Q_INVOKABLE bool reloadServerScripts(QUuid entityID); + + /**jsdoc + * Query additional metadata for "magic" Entity properties like `script` and `serverScripts`. + * + * @function Entities.queryPropertyMetadata + * @param {EntityID} entityID The ID of the entity. + * @param {string} property The name of the property extended metadata is wanted for. + * @param {ResultCallback} callback Executes callback(err, result) with the query results. + */ + /**jsdoc + * Query additional metadata for "magic" Entity properties like `script` and `serverScripts`. + * + * @function Entities.queryPropertyMetadata + * @param {EntityID} entityID The ID of the entity. + * @param {string} property The name of the property extended metadata is wanted for. + * @param {Object} thisObject The scoping "this" context that callback will be executed within. + * @param {ResultCallback} callbackOrMethodName Executes thisObject[callbackOrMethodName](err, result) with the query results. + */ + Q_INVOKABLE bool queryPropertyMetadata(QUuid entityID, QScriptValue property, QScriptValue scopeOrCallback, QScriptValue methodOrName = QScriptValue()); + Q_INVOKABLE bool getServerScriptStatus(QUuid entityID, QScriptValue callback); Q_INVOKABLE void setLightsArePickable(bool value); @@ -229,6 +266,7 @@ public slots: Q_INVOKABLE bool setAllVoxels(QUuid entityID, int value); Q_INVOKABLE bool setVoxelsInCuboid(QUuid entityID, const glm::vec3& lowPosition, const glm::vec3& cuboidSize, int value); + Q_INVOKABLE void voxelsToMesh(QUuid entityID, QScriptValue callback); Q_INVOKABLE bool setAllPoints(QUuid entityID, const QVector& points); Q_INVOKABLE bool appendPoint(QUuid entityID, const glm::vec3& point); @@ -293,6 +331,15 @@ public slots: const glm::vec3& start, const glm::vec3& end, float radius); + /**jsdoc + * Returns object to world transform, excluding scale + * + * @function Entities.getEntityTransform + * @param {EntityID} entityID The ID of the entity whose transform is to be returned + * @return {Mat4} Entity's object to world transform, excluding scale + */ + Q_INVOKABLE glm::mat4 getEntityTransform(const QUuid& entityID); + signals: void collisionWithEntity(const EntityItemID& idA, const EntityItemID& idB, const Collision& collision); @@ -323,9 +370,14 @@ signals: void webEventReceived(const EntityItemID& entityItemID, const QVariant& message); +protected: + void withEntitiesScriptEngine(std::function function) { + std::lock_guard lock(_entitiesScriptEngineLock); + function(_entitiesScriptEngine); + }; private: bool actionWorker(const QUuid& entityID, std::function actor); - bool setVoxels(QUuid entityID, std::function actor); + bool polyVoxWorker(QUuid entityID, std::function actor); bool setPoints(QUuid entityID, std::function actor); void queueEntityMessage(PacketType packetType, EntityItemID entityID, const EntityItemProperties& properties); diff --git a/libraries/entities/src/PolyVoxEntityItem.cpp b/libraries/entities/src/PolyVoxEntityItem.cpp index 2a374c1d17..90344d6c4b 100644 --- a/libraries/entities/src/PolyVoxEntityItem.cpp +++ b/libraries/entities/src/PolyVoxEntityItem.cpp @@ -242,3 +242,7 @@ const QByteArray PolyVoxEntityItem::getVoxelData() const { }); return voxelDataCopy; } + +bool PolyVoxEntityItem::getMeshAsScriptValue(QScriptEngine *engine, QScriptValue& result) { + return false; +} diff --git a/libraries/entities/src/PolyVoxEntityItem.h b/libraries/entities/src/PolyVoxEntityItem.h index 910d8eff88..311a002a4a 100644 --- a/libraries/entities/src/PolyVoxEntityItem.h +++ b/libraries/entities/src/PolyVoxEntityItem.h @@ -57,6 +57,8 @@ class PolyVoxEntityItem : public EntityItem { virtual void setVoxelData(QByteArray voxelData); virtual const QByteArray getVoxelData() const; + virtual int getOnCount() const { return 0; } + enum PolyVoxSurfaceStyle { SURFACE_MARCHING_CUBES, SURFACE_CUBIC, @@ -131,7 +133,9 @@ class PolyVoxEntityItem : public EntityItem { virtual void rebakeMesh() {}; void setVoxelDataDirty(bool value) { withWriteLock([&] { _voxelDataDirty = value; }); } - virtual void getMesh() {}; // recompute mesh + virtual void recomputeMesh() {}; + + virtual bool getMeshAsScriptValue(QScriptEngine *engine, QScriptValue& result); protected: glm::vec3 _voxelVolumeSize; // this is always 3 bytes diff --git a/libraries/entities/src/PropertyGroup.h b/libraries/entities/src/PropertyGroup.h index 38b1e5f599..f45d19f5eb 100644 --- a/libraries/entities/src/PropertyGroup.h +++ b/libraries/entities/src/PropertyGroup.h @@ -14,9 +14,11 @@ #include -//#include "EntityItemProperties.h" +#include + #include "EntityPropertyFlags.h" + class EntityItemProperties; class EncodeBitstreamParams; class OctreePacketData; @@ -24,31 +26,6 @@ class EntityTreeElementExtraEncodeData; class ReadBitstreamToTreeParams; using EntityTreeElementExtraEncodeDataPointer = std::shared_ptr; -#include - -/* -#include - -#include -#include - -#include -#include -#include - -#include -#include // for SittingPoint -#include -#include -#include - -#include "EntityItemID.h" -#include "PropertyGroupMacros.h" -#include "EntityTypes.h" -*/ - -//typedef PropertyFlags EntityPropertyFlags; - class PropertyGroup { public: diff --git a/libraries/fbx/src/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp index fcaef90527..718793fefa 100644 --- a/libraries/fbx/src/FBXReader.cpp +++ b/libraries/fbx/src/FBXReader.cpp @@ -1468,6 +1468,9 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS // Create the Material Library consolidateFBXMaterials(mapping); + // We can't allow the scaling of a given image to different sizes, because the hash used for the KTX cache is based on the original image + // Allowing scaling of the same image to different sizes would cause different KTX files to target the same cache key +#if 0 // HACK: until we get proper LOD management we're going to cap model textures // according to how many unique textures the model uses: // 1 - 8 textures --> 2048 @@ -1481,6 +1484,7 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS int numTextures = uniqueTextures.size(); const int MAX_NUM_TEXTURES_AT_MAX_RESOLUTION = 8; int maxWidth = sqrt(MAX_NUM_PIXELS_FOR_FBX_TEXTURE); + if (numTextures > MAX_NUM_TEXTURES_AT_MAX_RESOLUTION) { int numTextureThreshold = MAX_NUM_TEXTURES_AT_MAX_RESOLUTION; const int MIN_MIP_TEXTURE_WIDTH = 64; @@ -1494,7 +1498,7 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS material.setMaxNumPixelsPerTexture(maxWidth * maxWidth); } } - +#endif geometry.materials = _fbxMaterials; // see if any materials have texture children @@ -1795,19 +1799,6 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS } geometry.palmDirection = parseVec3(mapping.value("palmDirection", "0, -1, 0").toString()); - // Add sitting points - QVariantHash sittingPoints = mapping.value("sit").toHash(); - for (QVariantHash::const_iterator it = sittingPoints.constBegin(); it != sittingPoints.constEnd(); it++) { - SittingPoint sittingPoint; - sittingPoint.name = it.key(); - - QVariantList properties = it->toList(); - sittingPoint.position = parseVec3(properties.at(0).toString()); - sittingPoint.rotation = glm::quat(glm::radians(parseVec3(properties.at(1).toString()))); - - geometry.sittingPoints.append(sittingPoint); - } - // attempt to map any meshes to a named model for (QHash::const_iterator m = meshIDsToMeshIndices.constBegin(); m != meshIDsToMeshIndices.constEnd(); m++) { diff --git a/libraries/fbx/src/FBXReader.h b/libraries/fbx/src/FBXReader.h index 6e51c413dc..fa047e512f 100644 --- a/libraries/fbx/src/FBXReader.h +++ b/libraries/fbx/src/FBXReader.h @@ -265,24 +265,6 @@ public: Q_DECLARE_METATYPE(FBXAnimationFrame) Q_DECLARE_METATYPE(QVector) -/// A point where an avatar can sit -class SittingPoint { -public: - QString name; - glm::vec3 position; // relative postion - glm::quat rotation; // relative orientation -}; - -inline bool operator==(const SittingPoint& lhs, const SittingPoint& rhs) -{ - return (lhs.name == rhs.name) && (lhs.position == rhs.position) && (lhs.rotation == rhs.rotation); -} - -inline bool operator!=(const SittingPoint& lhs, const SittingPoint& rhs) -{ - return (lhs.name != rhs.name) || (lhs.position != rhs.position) || (lhs.rotation != rhs.rotation); -} - /// A set of meshes extracted from an FBX document. class FBXGeometry { public: @@ -320,8 +302,6 @@ public: glm::vec3 palmDirection; - QVector sittingPoints; - glm::vec3 neckPivot; Extents bindExtents; diff --git a/libraries/fbx/src/FBXReader_Node.cpp b/libraries/fbx/src/FBXReader_Node.cpp index d814f58dab..d987f885eb 100644 --- a/libraries/fbx/src/FBXReader_Node.cpp +++ b/libraries/fbx/src/FBXReader_Node.cpp @@ -54,7 +54,8 @@ template QVariant readBinaryArray(QDataStream& in, int& position) { in.readRawData(compressed.data() + sizeof(quint32), compressedLength); position += compressedLength; arrayData = qUncompress(compressed); - if (arrayData.isEmpty() || arrayData.size() != (sizeof(T) * arrayLength)) { // answers empty byte array if corrupt + if (arrayData.isEmpty() || + (unsigned int)arrayData.size() != (sizeof(T) * arrayLength)) { // answers empty byte array if corrupt throw QString("corrupt fbx file"); } } else { diff --git a/libraries/fbx/src/OBJReader.cpp b/libraries/fbx/src/OBJReader.cpp index 73cf7a520e..c1bb72dff8 100644 --- a/libraries/fbx/src/OBJReader.cpp +++ b/libraries/fbx/src/OBJReader.cpp @@ -267,7 +267,7 @@ void OBJReader::parseMaterialLibrary(QIODevice* device) { } if (token == "map_Kd") { currentMaterial.diffuseTextureFilename = filename; - } else { + } else if( token == "map_Ks" ) { currentMaterial.specularTextureFilename = filename; } } @@ -546,6 +546,7 @@ FBXGeometry* OBJReader::readOBJ(QByteArray& model, const QVariantHash& mapping, QString queryPart = _url.query(); bool suppressMaterialsHack = queryPart.contains("hifiusemat"); // If this appears in query string, don't fetch mtl even if used. OBJMaterial& preDefinedMaterial = materials[SMART_DEFAULT_MATERIAL_NAME]; + preDefinedMaterial.used = true; if (suppressMaterialsHack) { needsMaterialLibrary = preDefinedMaterial.userSpecifiesUV = false; // I said it was a hack... } @@ -594,8 +595,8 @@ FBXGeometry* OBJReader::readOBJ(QByteArray& model, const QVariantHash& mapping, } foreach (QString materialID, materials.keys()) { - OBJMaterial& objMaterial = materials[materialID]; - if (!objMaterial.used) { + OBJMaterial& objMaterial = materials[materialID]; + if (!objMaterial.used) { continue; } geometry.materials[materialID] = FBXMaterial(objMaterial.diffuseColor, @@ -611,6 +612,9 @@ FBXGeometry* OBJReader::readOBJ(QByteArray& model, const QVariantHash& mapping, if (!objMaterial.diffuseTextureFilename.isEmpty()) { fbxMaterial.albedoTexture.filename = objMaterial.diffuseTextureFilename; } + if (!objMaterial.specularTextureFilename.isEmpty()) { + fbxMaterial.specularTexture.filename = objMaterial.specularTextureFilename; + } modelMaterial->setEmissive(fbxMaterial.emissiveColor); modelMaterial->setAlbedo(fbxMaterial.diffuseColor); diff --git a/libraries/fbx/src/OBJReader.h b/libraries/fbx/src/OBJReader.h index 200f11548d..b4a48c570e 100644 --- a/libraries/fbx/src/OBJReader.h +++ b/libraries/fbx/src/OBJReader.h @@ -58,7 +58,7 @@ public: QByteArray specularTextureFilename; bool used { false }; bool userSpecifiesUV { false }; - OBJMaterial() : shininess(96.0f), opacity(1.0f), diffuseColor(1.0f), specularColor(1.0f) {} + OBJMaterial() : shininess(0.0f), opacity(1.0f), diffuseColor(0.9f), specularColor(0.9f) {} }; class OBJReader: public QObject { // QObject so we can make network requests. diff --git a/libraries/fbx/src/OBJWriter.cpp b/libraries/fbx/src/OBJWriter.cpp new file mode 100644 index 0000000000..5ee04c5718 --- /dev/null +++ b/libraries/fbx/src/OBJWriter.cpp @@ -0,0 +1,148 @@ +// +// OBJWriter.cpp +// libraries/fbx/src/ +// +// Created by Seth Alves on 2017-1-27. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include +#include +#include "model/Geometry.h" +#include "OBJWriter.h" +#include "ModelFormatLogging.h" + +static QString formatFloat(double n) { + // limit precision to 6, but don't output trailing zeros. + QString s = QString::number(n, 'f', 6); + while (s.endsWith("0")) { + s.remove(s.size() - 1, 1); + } + if (s.endsWith(".")) { + s.remove(s.size() - 1, 1); + } + + // check for non-numbers. if we get NaN or inf or scientific notation, just return 0 + for (int i = 0; i < s.length(); i++) { + auto c = s.at(i).toLatin1(); + if (c != '-' && + c != '.' && + (c < '0' || c > '9')) { + qCDebug(modelformat) << "OBJWriter zeroing bad vertex coordinate:" << s << "because of" << c; + return QString("0"); + } + } + + return s; +} + +bool writeOBJToTextStream(QTextStream& out, QList meshes) { + // each mesh's vertices are numbered from zero. We're combining all their vertices into one list here, + // so keep track of the start index for each mesh. + QList meshVertexStartOffset; + int currentVertexStartOffset = 0; + + // write out all vertices + foreach (const MeshPointer& mesh, meshes) { + meshVertexStartOffset.append(currentVertexStartOffset); + const gpu::BufferView& vertexBuffer = mesh->getVertexBuffer(); + int vertexCount = 0; + gpu::BufferView::Iterator vertexItr = vertexBuffer.cbegin(); + while (vertexItr != vertexBuffer.cend()) { + glm::vec3 v = *vertexItr; + out << "v "; + out << formatFloat(v[0]) << " "; + out << formatFloat(v[1]) << " "; + out << formatFloat(v[2]) << "\n"; + vertexItr++; + vertexCount++; + } + currentVertexStartOffset += vertexCount; + } + out << "\n"; + + // write out faces + int nth = 0; + foreach (const MeshPointer& mesh, meshes) { + currentVertexStartOffset = meshVertexStartOffset.takeFirst(); + + const gpu::BufferView& partBuffer = mesh->getPartBuffer(); + const gpu::BufferView& indexBuffer = mesh->getIndexBuffer(); + + model::Index partCount = (model::Index)mesh->getNumParts(); + for (int partIndex = 0; partIndex < partCount; partIndex++) { + const model::Mesh::Part& part = partBuffer.get(partIndex); + + out << "g part-" << nth++ << "\n"; + + // model::Mesh::TRIANGLES + // TODO -- handle other formats + gpu::BufferView::Iterator indexItr = indexBuffer.cbegin(); + indexItr += part._startIndex; + + int indexCount = 0; + while (indexItr != indexBuffer.cend() && indexCount < part._numIndices) { + uint32_t index0 = *indexItr; + indexItr++; + indexCount++; + if (indexItr == indexBuffer.cend() || indexCount >= part._numIndices) { + qCDebug(modelformat) << "OBJWriter -- index buffer length isn't multiple of 3"; + break; + } + uint32_t index1 = *indexItr; + indexItr++; + indexCount++; + if (indexItr == indexBuffer.cend() || indexCount >= part._numIndices) { + qCDebug(modelformat) << "OBJWriter -- index buffer length isn't multiple of 3"; + break; + } + uint32_t index2 = *indexItr; + indexItr++; + indexCount++; + + out << "f "; + out << currentVertexStartOffset + index0 + 1 << " "; + out << currentVertexStartOffset + index1 + 1 << " "; + out << currentVertexStartOffset + index2 + 1 << "\n"; + } + out << "\n"; + } + } + + return true; +} + +bool writeOBJToFile(QString path, QList meshes) { + if (QFileInfo(path).exists() && !QFile::remove(path)) { + qCDebug(modelformat) << "OBJ writer failed, file exists:" << path; + return false; + } + + QFile file(path); + if (!file.open(QIODevice::WriteOnly)) { + qCDebug(modelformat) << "OBJ writer failed to open output file:" << path; + return false; + } + + QTextStream outStream(&file); + + bool success; + success = writeOBJToTextStream(outStream, meshes); + + file.close(); + return success; +} + +QString writeOBJToString(QList meshes) { + QString result; + QTextStream outStream(&result, QIODevice::ReadWrite); + bool success; + success = writeOBJToTextStream(outStream, meshes); + if (success) { + return result; + } + return QString(""); +} diff --git a/libraries/fbx/src/OBJWriter.h b/libraries/fbx/src/OBJWriter.h new file mode 100644 index 0000000000..b6e20e1ae6 --- /dev/null +++ b/libraries/fbx/src/OBJWriter.h @@ -0,0 +1,26 @@ +// +// OBJWriter.h +// libraries/fbx/src/ +// +// Created by Seth Alves on 2017-1-27. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_objwriter_h +#define hifi_objwriter_h + + +#include +#include +#include + +using MeshPointer = std::shared_ptr; + +bool writeOBJToTextStream(QTextStream& out, QList meshes); +bool writeOBJToFile(QString path, QList meshes); +QString writeOBJToString(QList meshes); + +#endif // hifi_objwriter_h diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp index c51f468908..0800c27839 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp @@ -62,8 +62,6 @@ BackendPointer GLBackend::createBackend() { INSTANCE = result.get(); void* voidInstance = &(*result); qApp->setProperty(hifi::properties::gl::BACKEND, QVariant::fromValue(voidInstance)); - - gl::GLTexture::initTextureTransferHelper(); return result; } @@ -209,7 +207,7 @@ void GLBackend::renderPassTransfer(const Batch& batch) { } } - { // Sync all the buffers + { // Sync all the transform states PROFILE_RANGE(render_gpu_gl_detail, "syncCPUTransform"); _transform._cameras.clear(); _transform._cameraOffsets.clear(); @@ -277,7 +275,7 @@ void GLBackend::renderPassDraw(const Batch& batch) { updateInput(); updateTransform(batch); updatePipeline(); - + CommandCall call = _commandCalls[(*command)]; (this->*(call))(batch, *offset); break; @@ -623,6 +621,7 @@ void GLBackend::queueLambda(const std::function lambda) const { } void GLBackend::recycle() const { + PROFILE_RANGE(render_gpu_gl, __FUNCTION__) { std::list> lamdbasTrash; { @@ -745,10 +744,6 @@ void GLBackend::recycle() const { glDeleteQueries((GLsizei)ids.size(), ids.data()); } } - -#ifndef THREADED_TEXTURE_TRANSFER - gl::GLTexture::_textureTransferHelper->process(); -#endif } void GLBackend::setCameraCorrection(const Mat4& correction) { diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackend.h b/libraries/gpu-gl/src/gpu/gl/GLBackend.h index 950ac65a3f..76c950ec2b 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackend.h +++ b/libraries/gpu-gl/src/gpu/gl/GLBackend.h @@ -187,10 +187,15 @@ public: virtual void do_setStateScissorRect(const Batch& batch, size_t paramOffset) final; virtual GLuint getFramebufferID(const FramebufferPointer& framebuffer) = 0; - virtual GLuint getTextureID(const TexturePointer& texture, bool needTransfer = true) = 0; + virtual GLuint getTextureID(const TexturePointer& texture) final; virtual GLuint getBufferID(const Buffer& buffer) = 0; virtual GLuint getQueryID(const QueryPointer& query) = 0; - virtual bool isTextureReady(const TexturePointer& texture); + + virtual GLFramebuffer* syncGPUObject(const Framebuffer& framebuffer) = 0; + virtual GLBuffer* syncGPUObject(const Buffer& buffer) = 0; + virtual GLTexture* syncGPUObject(const TexturePointer& texture); + virtual GLQuery* syncGPUObject(const Query& query) = 0; + //virtual bool isTextureReady(const TexturePointer& texture); virtual void releaseBuffer(GLuint id, Size size) const; virtual void releaseExternalTexture(GLuint id, const Texture::ExternalRecycler& recycler) const; @@ -206,10 +211,6 @@ public: protected: void recycle() const override; - virtual GLFramebuffer* syncGPUObject(const Framebuffer& framebuffer) = 0; - virtual GLBuffer* syncGPUObject(const Buffer& buffer) = 0; - virtual GLTexture* syncGPUObject(const TexturePointer& texture, bool sync = true) = 0; - virtual GLQuery* syncGPUObject(const Query& query) = 0; static const size_t INVALID_OFFSET = (size_t)-1; bool _inRenderTransferPass { false }; diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackendTexture.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackendTexture.cpp index f51eac0e33..ca4e328612 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackendTexture.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackendTexture.cpp @@ -14,12 +14,56 @@ using namespace gpu; using namespace gpu::gl; -bool GLBackend::isTextureReady(const TexturePointer& texture) { - // DO not transfer the texture, this call is expected for rendering texture - GLTexture* object = syncGPUObject(texture, true); - return object && object->isReady(); + +GLuint GLBackend::getTextureID(const TexturePointer& texture) { + GLTexture* object = syncGPUObject(texture); + + if (!object) { + return 0; + } + + return object->_id; } +GLTexture* GLBackend::syncGPUObject(const TexturePointer& texturePointer) { + const Texture& texture = *texturePointer; + // Special case external textures + if (TextureUsageType::EXTERNAL == texture.getUsageType()) { + Texture::ExternalUpdates updates = texture.getUpdates(); + if (!updates.empty()) { + Texture::ExternalRecycler recycler = texture.getExternalRecycler(); + Q_ASSERT(recycler); + // Discard any superfluous updates + while (updates.size() > 1) { + const auto& update = updates.front(); + // Superfluous updates will never have been read, but we want to ensure the previous + // writes to them are complete before they're written again, so return them with the + // same fences they arrived with. This can happen on any thread because no GL context + // work is involved + recycler(update.first, update.second); + updates.pop_front(); + } + + // The last texture remaining is the one we'll use to create the GLTexture + const auto& update = updates.front(); + // Check for a fence, and if it exists, inject a wait into the command stream, then destroy the fence + if (update.second) { + GLsync fence = static_cast(update.second); + glWaitSync(fence, 0, GL_TIMEOUT_IGNORED); + glDeleteSync(fence); + } + + // Create the new texture object (replaces any previous texture object) + new GLExternalTexture(shared_from_this(), texture, update.first); + } + + // Return the texture object (if any) associated with the texture, without extensive logic + // (external textures are + return Backend::getGPUObject(texture); + } + + return nullptr; +} void GLBackend::do_generateTextureMips(const Batch& batch, size_t paramOffset) { TexturePointer resourceTexture = batch._textures.get(batch._params[paramOffset + 0]._uint); @@ -28,7 +72,7 @@ void GLBackend::do_generateTextureMips(const Batch& batch, size_t paramOffset) { } // DO not transfer the texture, this call is expected for rendering texture - GLTexture* object = syncGPUObject(resourceTexture, false); + GLTexture* object = syncGPUObject(resourceTexture); if (!object) { return; } diff --git a/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.cpp b/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.cpp index 85cf069062..2ac7e9d060 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.cpp @@ -21,13 +21,12 @@ GLFramebuffer::~GLFramebuffer() { } } -bool GLFramebuffer::checkStatus(GLenum target) const { - bool result = false; +bool GLFramebuffer::checkStatus() const { switch (_status) { case GL_FRAMEBUFFER_COMPLETE: // Success ! - result = true; - break; + return true; + case GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT: qCWarning(gpugllogging) << "GLFramebuffer::syncGPUObject : Framebuffer not valid, GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT."; break; @@ -44,5 +43,5 @@ bool GLFramebuffer::checkStatus(GLenum target) const { qCWarning(gpugllogging) << "GLFramebuffer::syncGPUObject : Framebuffer not valid, GL_FRAMEBUFFER_UNSUPPORTED."; break; } - return result; + return false; } diff --git a/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.h b/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.h index 9b4f9703fc..c0633cfdef 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.h +++ b/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.h @@ -64,7 +64,7 @@ public: protected: GLenum _status { GL_FRAMEBUFFER_COMPLETE }; virtual void update() = 0; - bool checkStatus(GLenum target) const; + bool checkStatus() const; GLFramebuffer(const std::weak_ptr& backend, const Framebuffer& framebuffer, GLuint id) : GLObject(backend, framebuffer, id) {} ~GLFramebuffer(); diff --git a/libraries/gpu-gl/src/gpu/gl/GLTexelFormat.cpp b/libraries/gpu-gl/src/gpu/gl/GLTexelFormat.cpp index bd945cbaaa..7e26e65e02 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLTexelFormat.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLTexelFormat.cpp @@ -17,6 +17,7 @@ GLenum GLTexelFormat::evalGLTexelFormatInternal(const gpu::Element& dstFormat) { switch (dstFormat.getDimension()) { case gpu::SCALAR: { switch (dstFormat.getSemantic()) { + case gpu::RED: case gpu::RGB: case gpu::RGBA: case gpu::SRGB: @@ -262,6 +263,7 @@ GLTexelFormat GLTexelFormat::evalGLTexelFormat(const Element& dstFormat, const E texel.type = ELEMENT_TYPE_TO_GL[dstFormat.getType()]; switch (dstFormat.getSemantic()) { + case gpu::RED: case gpu::RGB: case gpu::RGBA: texel.internalFormat = GL_R8; @@ -272,8 +274,10 @@ GLTexelFormat GLTexelFormat::evalGLTexelFormat(const Element& dstFormat, const E break; case gpu::DEPTH: + texel.format = GL_DEPTH_COMPONENT; texel.internalFormat = GL_DEPTH_COMPONENT32; break; + case gpu::DEPTH_STENCIL: texel.type = GL_UNSIGNED_INT_24_8; texel.format = GL_DEPTH_STENCIL; @@ -403,6 +407,7 @@ GLTexelFormat GLTexelFormat::evalGLTexelFormat(const Element& dstFormat, const E texel.internalFormat = GL_COMPRESSED_RED_RGTC1; break; } + case gpu::RED: case gpu::RGB: case gpu::RGBA: case gpu::SRGB: diff --git a/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp b/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp index 1e0dd08ae1..1de820e1df 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp @@ -10,15 +10,13 @@ #include -#include "GLTextureTransfer.h" #include "GLBackend.h" using namespace gpu; using namespace gpu::gl; -std::shared_ptr GLTexture::_textureTransferHelper; -const GLenum GLTexture::CUBE_FACE_LAYOUT[6] = { +const GLenum GLTexture::CUBE_FACE_LAYOUT[GLTexture::TEXTURE_CUBE_NUM_FACES] = { GL_TEXTURE_CUBE_MAP_POSITIVE_X, GL_TEXTURE_CUBE_MAP_NEGATIVE_X, GL_TEXTURE_CUBE_MAP_POSITIVE_Y, GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, GL_TEXTURE_CUBE_MAP_POSITIVE_Z, GL_TEXTURE_CUBE_MAP_NEGATIVE_Z @@ -67,6 +65,17 @@ GLenum GLTexture::getGLTextureType(const Texture& texture) { } +uint8_t GLTexture::getFaceCount(GLenum target) { + switch (target) { + case GL_TEXTURE_2D: + return TEXTURE_2D_NUM_FACES; + case GL_TEXTURE_CUBE_MAP: + return TEXTURE_CUBE_NUM_FACES; + default: + Q_UNREACHABLE(); + break; + } +} const std::vector& GLTexture::getFaceTargets(GLenum target) { static std::vector cubeFaceTargets { GL_TEXTURE_CUBE_MAP_POSITIVE_X, GL_TEXTURE_CUBE_MAP_NEGATIVE_X, @@ -89,216 +98,34 @@ const std::vector& GLTexture::getFaceTargets(GLenum target) { return faceTargets; } -// Default texture memory = GPU total memory - 2GB -#define GPU_MEMORY_RESERVE_BYTES MB_TO_BYTES(2048) -// Minimum texture memory = 1GB -#define TEXTURE_MEMORY_MIN_BYTES MB_TO_BYTES(1024) - - -float GLTexture::getMemoryPressure() { - // Check for an explicit memory limit - auto availableTextureMemory = Texture::getAllowedGPUMemoryUsage(); - - - // If no memory limit has been set, use a percentage of the total dedicated memory - if (!availableTextureMemory) { -#if 0 - auto totalMemory = getDedicatedMemory(); - if ((GPU_MEMORY_RESERVE_BYTES + TEXTURE_MEMORY_MIN_BYTES) > totalMemory) { - availableTextureMemory = TEXTURE_MEMORY_MIN_BYTES; - } else { - availableTextureMemory = totalMemory - GPU_MEMORY_RESERVE_BYTES; - } -#else - // Hardcode texture limit for sparse textures at 1 GB for now - availableTextureMemory = TEXTURE_MEMORY_MIN_BYTES; -#endif - } - - // Return the consumed texture memory divided by the available texture memory. - auto consumedGpuMemory = Context::getTextureGPUMemoryUsage() - Context::getTextureGPUFramebufferMemoryUsage(); - float memoryPressure = (float)consumedGpuMemory / (float)availableTextureMemory; - static Context::Size lastConsumedGpuMemory = 0; - if (memoryPressure > 1.0f && lastConsumedGpuMemory != consumedGpuMemory) { - lastConsumedGpuMemory = consumedGpuMemory; - qCDebug(gpugllogging) << "Exceeded max allowed texture memory: " << consumedGpuMemory << " / " << availableTextureMemory; - } - return memoryPressure; -} - - -// Create the texture and allocate storage -GLTexture::GLTexture(const std::weak_ptr& backend, const Texture& texture, GLuint id, bool transferrable) : - GLObject(backend, texture, id), - _external(false), - _source(texture.source()), - _storageStamp(texture.getStamp()), - _target(getGLTextureType(texture)), - _internalFormat(gl::GLTexelFormat::evalGLTexelFormatInternal(texture.getTexelFormat())), - _maxMip(texture.maxMip()), - _minMip(texture.minMip()), - _virtualSize(texture.evalTotalSize()), - _transferrable(transferrable) -{ - auto strongBackend = _backend.lock(); - strongBackend->recycle(); - Backend::incrementTextureGPUCount(); - Backend::updateTextureGPUVirtualMemoryUsage(0, _virtualSize); - Backend::setGPUObject(texture, this); -} - GLTexture::GLTexture(const std::weak_ptr& backend, const Texture& texture, GLuint id) : GLObject(backend, texture, id), - _external(true), _source(texture.source()), - _storageStamp(0), - _target(getGLTextureType(texture)), - _internalFormat(GL_RGBA8), - // FIXME force mips to 0? - _maxMip(texture.maxMip()), - _minMip(texture.minMip()), - _virtualSize(0), - _transferrable(false) + _target(getGLTextureType(texture)) { Backend::setGPUObject(texture, this); - - // FIXME Is this necessary? - //withPreservedTexture([this] { - // syncSampler(); - // if (_gpuObject.isAutogenerateMips()) { - // generateMips(); - // } - //}); } GLTexture::~GLTexture() { + auto backend = _backend.lock(); + if (backend && _id) { + backend->releaseTexture(_id, 0); + } +} + + +GLExternalTexture::GLExternalTexture(const std::weak_ptr& backend, const Texture& texture, GLuint id) + : Parent(backend, texture, id) { } + +GLExternalTexture::~GLExternalTexture() { auto backend = _backend.lock(); if (backend) { - if (_external) { - auto recycler = _gpuObject.getExternalRecycler(); - if (recycler) { - backend->releaseExternalTexture(_id, recycler); - } else { - qWarning() << "No recycler available for texture " << _id << " possible leak"; - } - } else if (_id) { - // WARNING! Sparse textures do not use this code path. See GL45BackendTexture for - // the GL45Texture destructor for doing any required work tracking GPU stats - backend->releaseTexture(_id, _size); + auto recycler = _gpuObject.getExternalRecycler(); + if (recycler) { + backend->releaseExternalTexture(_id, recycler); + } else { + qWarning() << "No recycler available for texture " << _id << " possible leak"; } - - if (!_external && !_transferrable) { - Backend::updateTextureGPUFramebufferMemoryUsage(_size, 0); - } - } - Backend::updateTextureGPUVirtualMemoryUsage(_virtualSize, 0); -} - -void GLTexture::createTexture() { - withPreservedTexture([&] { - allocateStorage(); - (void)CHECK_GL_ERROR(); - syncSampler(); - (void)CHECK_GL_ERROR(); - }); -} - -void GLTexture::withPreservedTexture(std::function f) const { - GLint boundTex = -1; - switch (_target) { - case GL_TEXTURE_2D: - glGetIntegerv(GL_TEXTURE_BINDING_2D, &boundTex); - break; - - case GL_TEXTURE_CUBE_MAP: - glGetIntegerv(GL_TEXTURE_BINDING_CUBE_MAP, &boundTex); - break; - - default: - qFatal("Unsupported texture type"); - } - (void)CHECK_GL_ERROR(); - - glBindTexture(_target, _texture); - f(); - glBindTexture(_target, boundTex); - (void)CHECK_GL_ERROR(); -} - -void GLTexture::setSize(GLuint size) const { - if (!_external && !_transferrable) { - Backend::updateTextureGPUFramebufferMemoryUsage(_size, size); - } - Backend::updateTextureGPUMemoryUsage(_size, size); - const_cast(_size) = size; -} - -bool GLTexture::isInvalid() const { - return _storageStamp < _gpuObject.getStamp(); -} - -bool GLTexture::isOutdated() const { - return GLSyncState::Idle == _syncState && _contentStamp < _gpuObject.getDataStamp(); -} - -bool GLTexture::isReady() const { - // If we have an invalid texture, we're never ready - if (isInvalid()) { - return false; - } - - auto syncState = _syncState.load(); - if (isOutdated() || Idle != syncState) { - return false; - } - - return true; -} - - -// Do any post-transfer operations that might be required on the main context / rendering thread -void GLTexture::postTransfer() { - setSyncState(GLSyncState::Idle); - ++_transferCount; - - // At this point the mip pixels have been loaded, we can notify the gpu texture to abandon it's memory - switch (_gpuObject.getType()) { - case Texture::TEX_2D: - for (uint16_t i = 0; i < Sampler::MAX_MIP_LEVEL; ++i) { - if (_gpuObject.isStoredMipFaceAvailable(i)) { - _gpuObject.notifyMipFaceGPULoaded(i); - } - } - break; - - case Texture::TEX_CUBE: - // transfer pixels from each faces - for (uint8_t f = 0; f < CUBE_NUM_FACES; f++) { - for (uint16_t i = 0; i < Sampler::MAX_MIP_LEVEL; ++i) { - if (_gpuObject.isStoredMipFaceAvailable(i, f)) { - _gpuObject.notifyMipFaceGPULoaded(i, f); - } - } - } - break; - - default: - qCWarning(gpugllogging) << __FUNCTION__ << " case for Texture Type " << _gpuObject.getType() << " not supported"; - break; + const_cast(_id) = 0; } } - -void GLTexture::initTextureTransferHelper() { - _textureTransferHelper = std::make_shared(); -} - -void GLTexture::startTransfer() { - createTexture(); -} - -void GLTexture::finishTransfer() { - if (_gpuObject.isAutogenerateMips()) { - generateMips(); - } -} - diff --git a/libraries/gpu-gl/src/gpu/gl/GLTexture.h b/libraries/gpu-gl/src/gpu/gl/GLTexture.h index 0f75a6fe51..1f91e17157 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLTexture.h +++ b/libraries/gpu-gl/src/gpu/gl/GLTexture.h @@ -9,7 +9,6 @@ #define hifi_gpu_gl_GLTexture_h #include "GLShared.h" -#include "GLTextureTransfer.h" #include "GLBackend.h" #include "GLTexelFormat.h" @@ -20,210 +19,48 @@ struct GLFilterMode { GLint magFilter; }; - class GLTexture : public GLObject { + using Parent = GLObject; + friend class GLBackend; public: static const uint16_t INVALID_MIP { (uint16_t)-1 }; static const uint8_t INVALID_FACE { (uint8_t)-1 }; - static void initTextureTransferHelper(); - static std::shared_ptr _textureTransferHelper; - - template - static GLTexture* sync(GLBackend& backend, const TexturePointer& texturePointer, bool needTransfer) { - const Texture& texture = *texturePointer; - - // Special case external textures - if (texture.getUsage().isExternal()) { - Texture::ExternalUpdates updates = texture.getUpdates(); - if (!updates.empty()) { - Texture::ExternalRecycler recycler = texture.getExternalRecycler(); - Q_ASSERT(recycler); - // Discard any superfluous updates - while (updates.size() > 1) { - const auto& update = updates.front(); - // Superfluous updates will never have been read, but we want to ensure the previous - // writes to them are complete before they're written again, so return them with the - // same fences they arrived with. This can happen on any thread because no GL context - // work is involved - recycler(update.first, update.second); - updates.pop_front(); - } - - // The last texture remaining is the one we'll use to create the GLTexture - const auto& update = updates.front(); - // Check for a fence, and if it exists, inject a wait into the command stream, then destroy the fence - if (update.second) { - GLsync fence = static_cast(update.second); - glWaitSync(fence, 0, GL_TIMEOUT_IGNORED); - glDeleteSync(fence); - } - - // Create the new texture object (replaces any previous texture object) - new GLTextureType(backend.shared_from_this(), texture, update.first); - } - - // Return the texture object (if any) associated with the texture, without extensive logic - // (external textures are - return Backend::getGPUObject(texture); - } - - if (!texture.isDefined()) { - // NO texture definition yet so let's avoid thinking - return nullptr; - } - - // If the object hasn't been created, or the object definition is out of date, drop and re-create - GLTexture* object = Backend::getGPUObject(texture); - - // Create the texture if need be (force re-creation if the storage stamp changes - // for easier use of immutable storage) - if (!object || object->isInvalid()) { - // This automatically any previous texture - object = new GLTextureType(backend.shared_from_this(), texture, needTransfer); - if (!object->_transferrable) { - object->createTexture(); - object->_contentStamp = texture.getDataStamp(); - object->updateSize(); - object->postTransfer(); - } - } - - // Object maybe doens't neet to be tranasferred after creation - if (!object->_transferrable) { - return object; - } - - // If we just did a transfer, return the object after doing post-transfer work - if (GLSyncState::Transferred == object->getSyncState()) { - object->postTransfer(); - } - - if (object->isOutdated()) { - // Object might be outdated, if so, start the transfer - // (outdated objects that are already in transfer will have reported 'true' for ready() - _textureTransferHelper->transferTexture(texturePointer); - return nullptr; - } - - if (!object->isReady()) { - return nullptr; - } - - ((GLTexture*)object)->updateMips(); - - return object; - } - - template - static GLuint getId(GLBackend& backend, const TexturePointer& texture, bool shouldSync) { - if (!texture) { - return 0; - } - GLTexture* object { nullptr }; - if (shouldSync) { - object = sync(backend, texture, shouldSync); - } else { - object = Backend::getGPUObject(*texture); - } - - if (!object) { - return 0; - } - - if (!shouldSync) { - return object->_id; - } - - // Don't return textures that are in transfer state - if ((object->getSyncState() != GLSyncState::Idle) || - // Don't return transferrable textures that have never completed transfer - (!object->_transferrable || 0 != object->_transferCount)) { - return 0; - } - - return object->_id; - } - ~GLTexture(); - // Is this texture generated outside the GPU library? - const bool _external; const GLuint& _texture { _id }; const std::string _source; - const Stamp _storageStamp; const GLenum _target; - const GLenum _internalFormat; - const uint16 _maxMip; - uint16 _minMip; - const GLuint _virtualSize; // theoretical size as expected - Stamp _contentStamp { 0 }; - const bool _transferrable; - Size _transferCount { 0 }; - GLuint size() const { return _size; } - GLSyncState getSyncState() const { return _syncState; } - // Is the storage out of date relative to the gpu texture? - bool isInvalid() const; + static const std::vector& getFaceTargets(GLenum textureType); + static uint8_t getFaceCount(GLenum textureType); + static GLenum getGLTextureType(const Texture& texture); - // Is the content out of date relative to the gpu texture? - bool isOutdated() const; - - // Is the texture in a state where it can be rendered with no work? - bool isReady() const; - - // Execute any post-move operations that must occur only on the main thread - virtual void postTransfer(); - - uint16 usedMipLevels() const { return (_maxMip - _minMip) + 1; } - - static const size_t CUBE_NUM_FACES = 6; - static const GLenum CUBE_FACE_LAYOUT[6]; + static const uint8_t TEXTURE_2D_NUM_FACES = 1; + static const uint8_t TEXTURE_CUBE_NUM_FACES = 6; + static const GLenum CUBE_FACE_LAYOUT[TEXTURE_CUBE_NUM_FACES]; static const GLFilterMode FILTER_MODES[Sampler::NUM_FILTERS]; static const GLenum WRAP_MODES[Sampler::NUM_WRAP_MODES]; - // Return a floating point value indicating how much of the allowed - // texture memory we are currently consuming. A value of 0 indicates - // no texture memory usage, while a value of 1 indicates all available / allowed memory - // is consumed. A value above 1 indicates that there is a problem. - static float getMemoryPressure(); protected: - - static const std::vector& getFaceTargets(GLenum textureType); - - static GLenum getGLTextureType(const Texture& texture); - - - const GLuint _size { 0 }; // true size as reported by the gl api - std::atomic _syncState { GLSyncState::Idle }; - - GLTexture(const std::weak_ptr& backend, const Texture& texture, GLuint id, bool transferrable); - GLTexture(const std::weak_ptr& backend, const Texture& texture, GLuint id); - - void setSyncState(GLSyncState syncState) { _syncState = syncState; } - - void createTexture(); - - virtual void updateMips() {} - virtual void allocateStorage() const = 0; - virtual void updateSize() const = 0; - virtual void syncSampler() const = 0; + virtual uint32 size() const = 0; virtual void generateMips() const = 0; - virtual void withPreservedTexture(std::function f) const; -protected: - void setSize(GLuint size) const; - - virtual void startTransfer(); - // Returns true if this is the last block required to complete transfer - virtual bool continueTransfer() { return false; } - virtual void finishTransfer(); - -private: - friend class GLTextureTransferHelper; - friend class GLBackend; + GLTexture(const std::weak_ptr& backend, const Texture& texture, GLuint id); }; +class GLExternalTexture : public GLTexture { + using Parent = GLTexture; + friend class GLBackend; +public: + ~GLExternalTexture(); +protected: + GLExternalTexture(const std::weak_ptr& backend, const Texture& texture, GLuint id); + void generateMips() const override {} + uint32 size() const override { return 0; } +}; + + } } #endif diff --git a/libraries/gpu-gl/src/gpu/gl/GLTextureTransfer.cpp b/libraries/gpu-gl/src/gpu/gl/GLTextureTransfer.cpp deleted file mode 100644 index 9dac2986e3..0000000000 --- a/libraries/gpu-gl/src/gpu/gl/GLTextureTransfer.cpp +++ /dev/null @@ -1,208 +0,0 @@ -// -// Created by Bradley Austin Davis on 2016/04/03 -// Copyright 2013-2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// -#include "GLTextureTransfer.h" - -#include -#include - -#include - -#include "GLShared.h" -#include "GLTexture.h" - -#ifdef HAVE_NSIGHT -#include "nvToolsExt.h" -std::unordered_map _map; -#endif - - -#ifdef TEXTURE_TRANSFER_PBOS -#define TEXTURE_TRANSFER_BLOCK_SIZE (64 * 1024) -#define TEXTURE_TRANSFER_PBO_COUNT 128 -#endif - -using namespace gpu; -using namespace gpu::gl; - -GLTextureTransferHelper::GLTextureTransferHelper() { -#ifdef THREADED_TEXTURE_TRANSFER - setObjectName("TextureTransferThread"); - _context.create(); - initialize(true, QThread::LowPriority); - // Clean shutdown on UNIX, otherwise _canvas is freed early - connect(qApp, &QCoreApplication::aboutToQuit, [&] { terminate(); }); -#else - initialize(false, QThread::LowPriority); -#endif -} - -GLTextureTransferHelper::~GLTextureTransferHelper() { -#ifdef THREADED_TEXTURE_TRANSFER - if (isStillRunning()) { - terminate(); - } -#else - terminate(); -#endif -} - -void GLTextureTransferHelper::transferTexture(const gpu::TexturePointer& texturePointer) { - GLTexture* object = Backend::getGPUObject(*texturePointer); - - Backend::incrementTextureGPUTransferCount(); - object->setSyncState(GLSyncState::Pending); - Lock lock(_mutex); - _pendingTextures.push_back(texturePointer); -} - -void GLTextureTransferHelper::setup() { -#ifdef THREADED_TEXTURE_TRANSFER - _context.makeCurrent(); - -#ifdef TEXTURE_TRANSFER_FORCE_DRAW - // FIXME don't use opengl 4.5 DSA functionality without verifying it's present - glCreateRenderbuffers(1, &_drawRenderbuffer); - glNamedRenderbufferStorage(_drawRenderbuffer, GL_RGBA8, 128, 128); - glCreateFramebuffers(1, &_drawFramebuffer); - glNamedFramebufferRenderbuffer(_drawFramebuffer, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, _drawRenderbuffer); - glCreateFramebuffers(1, &_readFramebuffer); -#endif - -#ifdef TEXTURE_TRANSFER_PBOS - std::array pbos; - glCreateBuffers(TEXTURE_TRANSFER_PBO_COUNT, &pbos[0]); - for (uint32_t i = 0; i < TEXTURE_TRANSFER_PBO_COUNT; ++i) { - TextureTransferBlock newBlock; - newBlock._pbo = pbos[i]; - glNamedBufferStorage(newBlock._pbo, TEXTURE_TRANSFER_BLOCK_SIZE, 0, GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT); - newBlock._mapped = glMapNamedBufferRange(newBlock._pbo, 0, TEXTURE_TRANSFER_BLOCK_SIZE, GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT); - _readyQueue.push(newBlock); - } -#endif -#endif -} - -void GLTextureTransferHelper::shutdown() { -#ifdef THREADED_TEXTURE_TRANSFER - _context.makeCurrent(); -#endif - -#ifdef TEXTURE_TRANSFER_FORCE_DRAW - glNamedFramebufferRenderbuffer(_drawFramebuffer, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, 0); - glDeleteFramebuffers(1, &_drawFramebuffer); - _drawFramebuffer = 0; - glDeleteFramebuffers(1, &_readFramebuffer); - _readFramebuffer = 0; - - glNamedFramebufferTexture(_readFramebuffer, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0); - glDeleteRenderbuffers(1, &_drawRenderbuffer); - _drawRenderbuffer = 0; -#endif -} - -void GLTextureTransferHelper::queueExecution(VoidLambda lambda) { - Lock lock(_mutex); - _pendingCommands.push_back(lambda); -} - -#define MAX_TRANSFERS_PER_PASS 2 - -bool GLTextureTransferHelper::process() { - // Take any new textures or commands off the queue - VoidLambdaList pendingCommands; - TextureList newTransferTextures; - { - Lock lock(_mutex); - newTransferTextures.swap(_pendingTextures); - pendingCommands.swap(_pendingCommands); - } - - if (!pendingCommands.empty()) { - for (auto command : pendingCommands) { - command(); - } - glFlush(); - } - - if (!newTransferTextures.empty()) { - for (auto& texturePointer : newTransferTextures) { -#ifdef HAVE_NSIGHT - _map[texturePointer] = nvtxRangeStart("TextureTansfer"); -#endif - GLTexture* object = Backend::getGPUObject(*texturePointer); - object->startTransfer(); - _transferringTextures.push_back(texturePointer); - _textureIterator = _transferringTextures.begin(); - } - _transferringTextures.sort([](const gpu::TexturePointer& a, const gpu::TexturePointer& b)->bool { - return a->getSize() < b->getSize(); - }); - } - - // No transfers in progress, sleep - if (_transferringTextures.empty()) { -#ifdef THREADED_TEXTURE_TRANSFER - QThread::usleep(1); -#endif - return true; - } - PROFILE_COUNTER_IF_CHANGED(render_gpu_gl, "transferringTextures", int, (int) _transferringTextures.size()) - - static auto lastReport = usecTimestampNow(); - auto now = usecTimestampNow(); - auto lastReportInterval = now - lastReport; - if (lastReportInterval > USECS_PER_SECOND * 4) { - lastReport = now; - qCDebug(gpulogging) << "Texture list " << _transferringTextures.size(); - } - - size_t transferCount = 0; - for (_textureIterator = _transferringTextures.begin(); _textureIterator != _transferringTextures.end();) { - if (++transferCount > MAX_TRANSFERS_PER_PASS) { - break; - } - auto texture = *_textureIterator; - GLTexture* gltexture = Backend::getGPUObject(*texture); - if (gltexture->continueTransfer()) { - ++_textureIterator; - continue; - } - - gltexture->finishTransfer(); - -#ifdef TEXTURE_TRANSFER_FORCE_DRAW - // FIXME force a draw on the texture transfer thread before passing the texture to the main thread for use -#endif - -#ifdef THREADED_TEXTURE_TRANSFER - clientWait(); -#endif - gltexture->_contentStamp = gltexture->_gpuObject.getDataStamp(); - gltexture->updateSize(); - gltexture->setSyncState(gpu::gl::GLSyncState::Transferred); - Backend::decrementTextureGPUTransferCount(); -#ifdef HAVE_NSIGHT - // Mark the texture as transferred - nvtxRangeEnd(_map[texture]); - _map.erase(texture); -#endif - _textureIterator = _transferringTextures.erase(_textureIterator); - } - -#ifdef THREADED_TEXTURE_TRANSFER - if (!_transferringTextures.empty()) { - // Don't saturate the GPU - clientWait(); - } else { - // Don't saturate the CPU - QThread::msleep(1); - } -#endif - - return true; -} diff --git a/libraries/gpu-gl/src/gpu/gl/GLTextureTransfer.h b/libraries/gpu-gl/src/gpu/gl/GLTextureTransfer.h deleted file mode 100644 index a23c282fd4..0000000000 --- a/libraries/gpu-gl/src/gpu/gl/GLTextureTransfer.h +++ /dev/null @@ -1,78 +0,0 @@ -// -// Created by Bradley Austin Davis on 2016/04/03 -// Copyright 2013-2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// -#ifndef hifi_gpu_gl_GLTextureTransfer_h -#define hifi_gpu_gl_GLTextureTransfer_h - -#include -#include - -#include - -#include - -#include "GLShared.h" - -#ifdef Q_OS_WIN -#define THREADED_TEXTURE_TRANSFER -#endif - -#ifdef THREADED_TEXTURE_TRANSFER -// FIXME when sparse textures are enabled, it's harder to force a draw on the transfer thread -// also, the current draw code is implicitly using OpenGL 4.5 functionality -//#define TEXTURE_TRANSFER_FORCE_DRAW -// FIXME PBO's increase the complexity and don't seem to work reliably -//#define TEXTURE_TRANSFER_PBOS -#endif - -namespace gpu { namespace gl { - -using TextureList = std::list; -using TextureListIterator = TextureList::iterator; - -class GLTextureTransferHelper : public GenericThread { -public: - using VoidLambda = std::function; - using VoidLambdaList = std::list; - using Pointer = std::shared_ptr; - GLTextureTransferHelper(); - ~GLTextureTransferHelper(); - void transferTexture(const gpu::TexturePointer& texturePointer); - void queueExecution(VoidLambda lambda); - - void setup() override; - void shutdown() override; - bool process() override; - -private: -#ifdef THREADED_TEXTURE_TRANSFER - ::gl::OffscreenContext _context; -#endif - -#ifdef TEXTURE_TRANSFER_FORCE_DRAW - // Framebuffers / renderbuffers for forcing access to the texture on the transfer thread - GLuint _drawRenderbuffer { 0 }; - GLuint _drawFramebuffer { 0 }; - GLuint _readFramebuffer { 0 }; -#endif - - // A mutex for protecting items access on the render and transfer threads - Mutex _mutex; - // Commands that have been submitted for execution on the texture transfer thread - VoidLambdaList _pendingCommands; - // Textures that have been submitted for transfer - TextureList _pendingTextures; - // Textures currently in the transfer process - // Only used on the transfer thread - TextureList _transferringTextures; - TextureListIterator _textureIterator; - -}; - -} } - -#endif \ No newline at end of file diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h b/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h index 72e2f5a804..6d2f91c436 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h +++ b/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h @@ -40,18 +40,28 @@ public: class GL41Texture : public GLTexture { using Parent = GLTexture; - GLuint allocate(); - public: - GL41Texture(const std::weak_ptr& backend, const Texture& buffer, GLuint externalId); - GL41Texture(const std::weak_ptr& backend, const Texture& buffer, bool transferrable); + static GLuint allocate(); + + public: + ~GL41Texture(); + + private: + GL41Texture(const std::weak_ptr& backend, const Texture& buffer); - protected: - void transferMip(uint16_t mipLevel, uint8_t face) const; - void startTransfer() override; - void allocateStorage() const override; - void updateSize() const override; - void syncSampler() const override; void generateMips() const override; + uint32 size() const override; + + friend class GL41Backend; + const Stamp _storageStamp; + mutable Stamp _contentStamp { 0 }; + mutable Stamp _samplerStamp { 0 }; + const uint32 _size; + + + bool isOutdated() const; + void withPreservedTexture(std::function f) const; + void syncContent() const; + void syncSampler() const; }; @@ -62,8 +72,7 @@ protected: GLuint getBufferID(const Buffer& buffer) override; GLBuffer* syncGPUObject(const Buffer& buffer) override; - GLuint getTextureID(const TexturePointer& texture, bool needTransfer = true) override; - GLTexture* syncGPUObject(const TexturePointer& texture, bool sync = true) override; + GLTexture* syncGPUObject(const TexturePointer& texture) override; GLuint getQueryID(const QueryPointer& query) override; GLQuery* syncGPUObject(const Query& query) override; diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41BackendOutput.cpp b/libraries/gpu-gl/src/gpu/gl41/GL41BackendOutput.cpp index 6d11a52035..195b155bf3 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41BackendOutput.cpp +++ b/libraries/gpu-gl/src/gpu/gl41/GL41BackendOutput.cpp @@ -53,10 +53,12 @@ public: GL_COLOR_ATTACHMENT15 }; int unit = 0; + auto backend = _backend.lock(); for (auto& b : _gpuObject.getRenderBuffers()) { surface = b._texture; if (surface) { - gltexture = gl::GLTexture::sync(*_backend.lock().get(), surface, false); // Grab the gltexture and don't transfer + Q_ASSERT(TextureUsageType::RENDERBUFFER == surface->getUsageType()); + gltexture = backend->syncGPUObject(surface); } else { gltexture = nullptr; } @@ -81,9 +83,11 @@ public: } if (_gpuObject.getDepthStamp() != _depthStamp) { + auto backend = _backend.lock(); auto surface = _gpuObject.getDepthStencilBuffer(); if (_gpuObject.hasDepthStencil() && surface) { - gltexture = gl::GLTexture::sync(*_backend.lock().get(), surface, false); // Grab the gltexture and don't transfer + Q_ASSERT(TextureUsageType::RENDERBUFFER == surface->getUsageType()); + gltexture = backend->syncGPUObject(surface); } if (gltexture) { @@ -110,7 +114,7 @@ public: glBindFramebuffer(GL_DRAW_FRAMEBUFFER, currentFBO); } - checkStatus(GL_DRAW_FRAMEBUFFER); + checkStatus(); } diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp b/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp index 65c45111db..8dbef09f06 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp +++ b/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp @@ -29,20 +29,102 @@ GLuint GL41Texture::allocate() { return result; } -GLuint GL41Backend::getTextureID(const TexturePointer& texture, bool transfer) { - return GL41Texture::getId(*this, texture, transfer); +GLTexture* GL41Backend::syncGPUObject(const TexturePointer& texturePointer) { + if (!texturePointer) { + return nullptr; + } + const Texture& texture = *texturePointer; + if (TextureUsageType::EXTERNAL == texture.getUsageType()) { + return Parent::syncGPUObject(texturePointer); + } + + if (!texture.isDefined()) { + // NO texture definition yet so let's avoid thinking + return nullptr; + } + + // If the object hasn't been created, or the object definition is out of date, drop and re-create + GL41Texture* object = Backend::getGPUObject(texture); + if (!object || object->_storageStamp < texture.getStamp()) { + // This automatically any previous texture + object = new GL41Texture(shared_from_this(), texture); + } + + // FIXME internalize to GL41Texture 'sync' function + if (object->isOutdated()) { + object->withPreservedTexture([&] { + if (object->_contentStamp <= texture.getDataStamp()) { + // FIXME implement synchronous texture transfer here + object->syncContent(); + } + + if (object->_samplerStamp <= texture.getSamplerStamp()) { + object->syncSampler(); + } + }); + } + + return object; } -GLTexture* GL41Backend::syncGPUObject(const TexturePointer& texture, bool transfer) { - return GL41Texture::sync(*this, texture, transfer); +GL41Texture::GL41Texture(const std::weak_ptr& backend, const Texture& texture) + : GLTexture(backend, texture, allocate()), _storageStamp { texture.getStamp() }, _size(texture.evalTotalSize()) { + incrementTextureGPUCount(); + withPreservedTexture([&] { + GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat(), _gpuObject.getStoredMipFormat()); + auto numMips = _gpuObject.evalNumMips(); + for (uint16_t mipLevel = 0; mipLevel < numMips; ++mipLevel) { + // Get the mip level dimensions, accounting for the downgrade level + Vec3u dimensions = _gpuObject.evalMipDimensions(mipLevel); + uint8_t face = 0; + for (GLenum target : getFaceTargets(_target)) { + const Byte* mipData = nullptr; + if (_gpuObject.isStoredMipFaceAvailable(mipLevel, face)) { + auto mip = _gpuObject.accessStoredMipFace(mipLevel, face); + mipData = mip->readData(); + } + glTexImage2D(target, mipLevel, texelFormat.internalFormat, dimensions.x, dimensions.y, 0, texelFormat.format, texelFormat.type, mipData); + (void)CHECK_GL_ERROR(); + ++face; + } + } + }); } -GL41Texture::GL41Texture(const std::weak_ptr& backend, const Texture& texture, GLuint externalId) - : GLTexture(backend, texture, externalId) { +GL41Texture::~GL41Texture() { + } -GL41Texture::GL41Texture(const std::weak_ptr& backend, const Texture& texture, bool transferrable) - : GLTexture(backend, texture, allocate(), transferrable) { +bool GL41Texture::isOutdated() const { + if (_samplerStamp <= _gpuObject.getSamplerStamp()) { + return true; + } + if (TextureUsageType::RESOURCE == _gpuObject.getUsageType() && _contentStamp <= _gpuObject.getDataStamp()) { + return true; + } + return false; +} + +void GL41Texture::withPreservedTexture(std::function f) const { + GLint boundTex = -1; + switch (_target) { + case GL_TEXTURE_2D: + glGetIntegerv(GL_TEXTURE_BINDING_2D, &boundTex); + break; + + case GL_TEXTURE_CUBE_MAP: + glGetIntegerv(GL_TEXTURE_BINDING_CUBE_MAP, &boundTex); + break; + + default: + qFatal("Unsupported texture type"); + } + (void)CHECK_GL_ERROR(); + + glBindTexture(_target, _texture); + f(); + glBindTexture(_target, boundTex); + (void)CHECK_GL_ERROR(); } void GL41Texture::generateMips() const { @@ -52,94 +134,12 @@ void GL41Texture::generateMips() const { (void)CHECK_GL_ERROR(); } -void GL41Texture::allocateStorage() const { - GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat()); - glTexParameteri(_target, GL_TEXTURE_BASE_LEVEL, 0); - (void)CHECK_GL_ERROR(); - glTexParameteri(_target, GL_TEXTURE_MAX_LEVEL, _maxMip - _minMip); - (void)CHECK_GL_ERROR(); - if (GLEW_VERSION_4_2 && !_gpuObject.getTexelFormat().isCompressed()) { - // Get the dimensions, accounting for the downgrade level - Vec3u dimensions = _gpuObject.evalMipDimensions(_minMip); - glTexStorage2D(_target, usedMipLevels(), texelFormat.internalFormat, dimensions.x, dimensions.y); - (void)CHECK_GL_ERROR(); - } else { - for (uint16_t l = _minMip; l <= _maxMip; l++) { - // Get the mip level dimensions, accounting for the downgrade level - Vec3u dimensions = _gpuObject.evalMipDimensions(l); - for (GLenum target : getFaceTargets(_target)) { - glTexImage2D(target, l - _minMip, texelFormat.internalFormat, dimensions.x, dimensions.y, 0, texelFormat.format, texelFormat.type, NULL); - (void)CHECK_GL_ERROR(); - } - } - } +void GL41Texture::syncContent() const { + // FIXME actually copy the texture data + _contentStamp = _gpuObject.getDataStamp() + 1; } -void GL41Texture::updateSize() const { - setSize(_virtualSize); - if (!_id) { - return; - } - - if (_gpuObject.getTexelFormat().isCompressed()) { - GLenum proxyType = GL_TEXTURE_2D; - GLuint numFaces = 1; - if (_gpuObject.getType() == gpu::Texture::TEX_CUBE) { - proxyType = CUBE_FACE_LAYOUT[0]; - numFaces = (GLuint)CUBE_NUM_FACES; - } - GLint gpuSize{ 0 }; - glGetTexLevelParameteriv(proxyType, 0, GL_TEXTURE_COMPRESSED, &gpuSize); - (void)CHECK_GL_ERROR(); - - if (gpuSize) { - for (GLuint level = _minMip; level < _maxMip; level++) { - GLint levelSize{ 0 }; - glGetTexLevelParameteriv(proxyType, level, GL_TEXTURE_COMPRESSED_IMAGE_SIZE, &levelSize); - levelSize *= numFaces; - - if (levelSize <= 0) { - break; - } - gpuSize += levelSize; - } - (void)CHECK_GL_ERROR(); - setSize(gpuSize); - return; - } - } -} - -// Move content bits from the CPU to the GPU for a given mip / face -void GL41Texture::transferMip(uint16_t mipLevel, uint8_t face) const { - auto mip = _gpuObject.accessStoredMipFace(mipLevel, face); - GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat(), mip->getFormat()); - //GLenum target = getFaceTargets()[face]; - GLenum target = _target == GL_TEXTURE_2D ? GL_TEXTURE_2D : CUBE_FACE_LAYOUT[face]; - auto size = _gpuObject.evalMipDimensions(mipLevel); - glTexSubImage2D(target, mipLevel, 0, 0, size.x, size.y, texelFormat.format, texelFormat.type, mip->readData()); - (void)CHECK_GL_ERROR(); -} - -void GL41Texture::startTransfer() { - PROFILE_RANGE(render_gpu_gl, __FUNCTION__); - Parent::startTransfer(); - - glBindTexture(_target, _id); - (void)CHECK_GL_ERROR(); - - // transfer pixels from each faces - uint8_t numFaces = (Texture::TEX_CUBE == _gpuObject.getType()) ? CUBE_NUM_FACES : 1; - for (uint8_t f = 0; f < numFaces; f++) { - for (uint16_t i = 0; i < Sampler::MAX_MIP_LEVEL; ++i) { - if (_gpuObject.isStoredMipFaceAvailable(i, f)) { - transferMip(i, f); - } - } - } -} - -void GL41Backend::GL41Texture::syncSampler() const { +void GL41Texture::syncSampler() const { const Sampler& sampler = _gpuObject.getSampler(); const auto& fm = FILTER_MODES[sampler.getFilter()]; glTexParameteri(_target, GL_TEXTURE_MIN_FILTER, fm.minFilter); @@ -161,5 +161,9 @@ void GL41Backend::GL41Texture::syncSampler() const { glTexParameterf(_target, GL_TEXTURE_MIN_LOD, (float)sampler.getMinMip()); glTexParameterf(_target, GL_TEXTURE_MAX_LOD, (sampler.getMaxMip() == Sampler::MAX_MIP_LEVEL ? 1000.f : sampler.getMaxMip())); glTexParameterf(_target, GL_TEXTURE_MAX_ANISOTROPY_EXT, sampler.getMaxAnisotropy()); + _samplerStamp = _gpuObject.getSamplerStamp() + 1; } +uint32 GL41Texture::size() const { + return _size; +} diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45Backend.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45Backend.cpp index d7dde8b7d6..12c4b818f7 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45Backend.cpp +++ b/libraries/gpu-gl/src/gpu/gl45/GL45Backend.cpp @@ -18,6 +18,12 @@ Q_LOGGING_CATEGORY(gpugl45logging, "hifi.gpu.gl45") using namespace gpu; using namespace gpu::gl45; +void GL45Backend::recycle() const { + Parent::recycle(); + GL45VariableAllocationTexture::manageMemory(); + GL45VariableAllocationTexture::_frameTexturesCreated = 0; +} + void GL45Backend::do_draw(const Batch& batch, size_t paramOffset) { Primitive primitiveType = (Primitive)batch._params[paramOffset + 2]._uint; GLenum mode = gl::PRIMITIVE_TO_GL[primitiveType]; @@ -163,8 +169,3 @@ void GL45Backend::do_multiDrawIndexedIndirect(const Batch& batch, size_t paramOf _stats._DSNumAPIDrawcalls++; (void)CHECK_GL_ERROR(); } - -void GL45Backend::recycle() const { - Parent::recycle(); - derezTextures(); -} diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h b/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h index 2242bba5d9..6a9811b055 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h +++ b/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h @@ -8,17 +8,21 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#pragma once #ifndef hifi_gpu_45_GL45Backend_h #define hifi_gpu_45_GL45Backend_h #include "../gl/GLBackend.h" #include "../gl/GLTexture.h" +#include #define INCREMENTAL_TRANSFER 0 +#define THREADED_TEXTURE_BUFFERING 1 namespace gpu { namespace gl45 { using namespace gpu::gl; +using TextureWeakPointer = std::weak_ptr; class GL45Backend : public GLBackend { using Parent = GLBackend; @@ -31,60 +35,219 @@ public: class GL45Texture : public GLTexture { using Parent = GLTexture; + friend class GL45Backend; static GLuint allocate(const Texture& texture); + protected: + GL45Texture(const std::weak_ptr& backend, const Texture& texture); + void generateMips() const override; + void copyMipFaceFromTexture(uint16_t sourceMip, uint16_t targetMip, uint8_t face) const; + void copyMipFaceLinesFromTexture(uint16_t mip, uint8_t face, const uvec3& size, uint32_t yOffset, GLenum format, GLenum type, const void* sourcePointer) const; + virtual void syncSampler() const; + }; + + // + // Textures that have fixed allocation sizes and cannot be managed at runtime + // + + class GL45FixedAllocationTexture : public GL45Texture { + using Parent = GL45Texture; + friend class GL45Backend; + + public: + GL45FixedAllocationTexture(const std::weak_ptr& backend, const Texture& texture); + ~GL45FixedAllocationTexture(); + + protected: + uint32 size() const override { return _size; } + void allocateStorage() const; + void syncSampler() const override; + const uint32 _size { 0 }; + }; + + class GL45AttachmentTexture : public GL45FixedAllocationTexture { + using Parent = GL45FixedAllocationTexture; + friend class GL45Backend; + protected: + GL45AttachmentTexture(const std::weak_ptr& backend, const Texture& texture); + ~GL45AttachmentTexture(); + }; + + class GL45StrictResourceTexture : public GL45FixedAllocationTexture { + using Parent = GL45FixedAllocationTexture; + friend class GL45Backend; + protected: + GL45StrictResourceTexture(const std::weak_ptr& backend, const Texture& texture); + }; + + // + // Textures that can be managed at runtime to increase or decrease their memory load + // + + class GL45VariableAllocationTexture : public GL45Texture { + using Parent = GL45Texture; + friend class GL45Backend; + using PromoteLambda = std::function; + + public: + enum class MemoryPressureState { + Idle, + Transfer, + Oversubscribed, + Undersubscribed, + }; + + using QueuePair = std::pair; + struct QueuePairLess { + bool operator()(const QueuePair& a, const QueuePair& b) { + return a.second < b.second; + } + }; + using WorkQueue = std::priority_queue, QueuePairLess>; + + class TransferJob { + using VoidLambda = std::function; + using VoidLambdaQueue = std::queue; + using ThreadPointer = std::shared_ptr; + const GL45VariableAllocationTexture& _parent; + // Holds the contents to transfer to the GPU in CPU memory + std::vector _buffer; + // Indicates if a transfer from backing storage to interal storage has started + bool _bufferingStarted { false }; + bool _bufferingCompleted { false }; + VoidLambda _transferLambda; + VoidLambda _bufferingLambda; +#if THREADED_TEXTURE_BUFFERING + static Mutex _mutex; + static VoidLambdaQueue _bufferLambdaQueue; + static ThreadPointer _bufferThread; + static std::atomic _shutdownBufferingThread; + static void bufferLoop(); +#endif + + public: + TransferJob(const TransferJob& other) = delete; + TransferJob(const GL45VariableAllocationTexture& parent, std::function transferLambda); + TransferJob(const GL45VariableAllocationTexture& parent, uint16_t sourceMip, uint16_t targetMip, uint8_t face, uint32_t lines = 0, uint32_t lineOffset = 0); + ~TransferJob(); + bool tryTransfer(); + +#if THREADED_TEXTURE_BUFFERING + static void startTransferLoop(); + static void stopTransferLoop(); +#endif + + private: + size_t _transferSize { 0 }; +#if THREADED_TEXTURE_BUFFERING + void startBuffering(); +#endif + void transfer(); + }; + + using TransferQueue = std::queue>; + static MemoryPressureState _memoryPressureState; + protected: + static size_t _frameTexturesCreated; + static std::atomic _memoryPressureStateStale; + static std::list _memoryManagedTextures; + static WorkQueue _transferQueue; + static WorkQueue _promoteQueue; + static WorkQueue _demoteQueue; + static TexturePointer _currentTransferTexture; + static const uvec3 INITIAL_MIP_TRANSFER_DIMENSIONS; + + + static void updateMemoryPressure(); + static void processWorkQueues(); + static void addMemoryManagedTexture(const TexturePointer& texturePointer); + static void addToWorkQueue(const TexturePointer& texture); + static WorkQueue& getActiveWorkQueue(); + + static void manageMemory(); + + protected: + GL45VariableAllocationTexture(const std::weak_ptr& backend, const Texture& texture); + ~GL45VariableAllocationTexture(); + //bool canPromoteNoAllocate() const { return _allocatedMip < _populatedMip; } + bool canPromote() const { return _allocatedMip > 0; } + bool canDemote() const { return _allocatedMip < _maxAllocatedMip; } + bool hasPendingTransfers() const { return _populatedMip > _allocatedMip; } + void executeNextTransfer(const TexturePointer& currentTexture); + uint32 size() const override { return _size; } + virtual void populateTransferQueue() = 0; + virtual void promote() = 0; + virtual void demote() = 0; + + // The allocated mip level, relative to the number of mips in the gpu::Texture object + // The relationship between a given glMip to the original gpu::Texture mip is always + // glMip + _allocatedMip + uint16 _allocatedMip { 0 }; + // The populated mip level, relative to the number of mips in the gpu::Texture object + // This must always be >= the allocated mip + uint16 _populatedMip { 0 }; + // The highest (lowest resolution) mip that we will support, relative to the number + // of mips in the gpu::Texture object + uint16 _maxAllocatedMip { 0 }; + uint32 _size { 0 }; + // Contains a series of lambdas that when executed will transfer data to the GPU, modify + // the _populatedMip and update the sampler in order to fully populate the allocated texture + // until _populatedMip == _allocatedMip + TransferQueue _pendingTransfers; + }; + + class GL45ResourceTexture : public GL45VariableAllocationTexture { + using Parent = GL45VariableAllocationTexture; + friend class GL45Backend; + protected: + GL45ResourceTexture(const std::weak_ptr& backend, const Texture& texture); + + void syncSampler() const override; + void promote() override; + void demote() override; + void populateTransferQueue() override; + + void allocateStorage(uint16 mip); + void copyMipsFromTexture(); + }; + +#if 0 + class GL45SparseResourceTexture : public GL45VariableAllocationTexture { + using Parent = GL45VariableAllocationTexture; + friend class GL45Backend; + using TextureTypeFormat = std::pair; + using PageDimensions = std::vector; + using PageDimensionsMap = std::map; + static PageDimensionsMap pageDimensionsByFormat; + static Mutex pageDimensionsMutex; + + static bool isSparseEligible(const Texture& texture); + static PageDimensions getPageDimensionsForFormat(const TextureTypeFormat& typeFormat); + static PageDimensions getPageDimensionsForFormat(GLenum type, GLenum format); static const uint32_t DEFAULT_PAGE_DIMENSION = 128; static const uint32_t DEFAULT_MAX_SPARSE_LEVEL = 0xFFFF; - public: - GL45Texture(const std::weak_ptr& backend, const Texture& texture, GLuint externalId); - GL45Texture(const std::weak_ptr& backend, const Texture& texture, bool transferrable); - ~GL45Texture(); - - void postTransfer() override; - - struct SparseInfo { - SparseInfo(GL45Texture& texture); - void maybeMakeSparse(); - void update(); - uvec3 getPageCounts(const uvec3& dimensions) const; - uint32_t getPageCount(const uvec3& dimensions) const; - uint32_t getSize() const; - - GL45Texture& texture; - bool sparse { false }; - uvec3 pageDimensions { DEFAULT_PAGE_DIMENSION }; - GLuint maxSparseLevel { DEFAULT_MAX_SPARSE_LEVEL }; - uint32_t allocatedPages { 0 }; - uint32_t maxPages { 0 }; - uint32_t pageBytes { 0 }; - GLint pageDimensionsIndex { 0 }; - }; - protected: - void updateMips() override; - void stripToMip(uint16_t newMinMip); - void startTransfer() override; - bool continueTransfer() override; - void finishTransfer() override; - void incrementalTransfer(const uvec3& size, const gpu::Texture::PixelsPointer& mip, std::function f) const; - void transferMip(uint16_t mipLevel, uint8_t face = 0) const; - void allocateMip(uint16_t mipLevel, uint8_t face = 0) const; - void allocateStorage() const override; - void updateSize() const override; - void syncSampler() const override; - void generateMips() const override; - void withPreservedTexture(std::function f) const override; - void derez(); + GL45SparseResourceTexture(const std::weak_ptr& backend, const Texture& texture); + ~GL45SparseResourceTexture(); + uint32 size() const override { return _allocatedPages * _pageBytes; } + void promote() override; + void demote() override; - SparseInfo _sparseInfo; - uint16_t _mipOffset { 0 }; - friend class GL45Backend; + private: + uvec3 getPageCounts(const uvec3& dimensions) const; + uint32_t getPageCount(const uvec3& dimensions) const; + + uint32_t _allocatedPages { 0 }; + uint32_t _pageBytes { 0 }; + uvec3 _pageDimensions { DEFAULT_PAGE_DIMENSION }; + GLuint _maxSparseLevel { DEFAULT_MAX_SPARSE_LEVEL }; }; +#endif protected: + void recycle() const override; - void derezTextures() const; GLuint getFramebufferID(const FramebufferPointer& framebuffer) override; GLFramebuffer* syncGPUObject(const Framebuffer& framebuffer) override; @@ -92,8 +255,7 @@ protected: GLuint getBufferID(const Buffer& buffer) override; GLBuffer* syncGPUObject(const Buffer& buffer) override; - GLuint getTextureID(const TexturePointer& texture, bool needTransfer = true) override; - GLTexture* syncGPUObject(const TexturePointer& texture, bool sync = true) override; + GLTexture* syncGPUObject(const TexturePointer& texture) override; GLuint getQueryID(const QueryPointer& query) override; GLQuery* syncGPUObject(const Query& query) override; @@ -126,5 +288,5 @@ protected: Q_DECLARE_LOGGING_CATEGORY(gpugl45logging) - #endif + diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp index c5b84b7deb..9648af9b21 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp @@ -49,10 +49,12 @@ public: GL_COLOR_ATTACHMENT15 }; int unit = 0; + auto backend = _backend.lock(); for (auto& b : _gpuObject.getRenderBuffers()) { surface = b._texture; if (surface) { - gltexture = gl::GLTexture::sync(*_backend.lock().get(), surface, false); // Grab the gltexture and don't transfer + Q_ASSERT(TextureUsageType::RENDERBUFFER == surface->getUsageType()); + gltexture = backend->syncGPUObject(surface); } else { gltexture = nullptr; } @@ -78,8 +80,10 @@ public: if (_gpuObject.getDepthStamp() != _depthStamp) { auto surface = _gpuObject.getDepthStencilBuffer(); + auto backend = _backend.lock(); if (_gpuObject.hasDepthStencil() && surface) { - gltexture = gl::GLTexture::sync(*_backend.lock().get(), surface, false); // Grab the gltexture and don't transfer + Q_ASSERT(TextureUsageType::RENDERBUFFER == surface->getUsageType()); + gltexture = backend->syncGPUObject(surface); } if (gltexture) { @@ -102,7 +106,7 @@ public: _status = glCheckNamedFramebufferStatus(_id, GL_DRAW_FRAMEBUFFER); // restore the current framebuffer - checkStatus(GL_DRAW_FRAMEBUFFER); + checkStatus(); } diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp index 6948a045a2..36aaf75e81 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp @@ -8,9 +8,10 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#include "GL45Backend.h" +#include "GL45Backend.h" #include +#include #include #include #include @@ -19,142 +20,70 @@ #include #include +#include #include "../gl/GLTexelFormat.h" using namespace gpu; using namespace gpu::gl; using namespace gpu::gl45; -// Allocate 1 MB of buffer space for paged transfers -#define DEFAULT_PAGE_BUFFER_SIZE (1024*1024) -#define DEFAULT_GL_PIXEL_ALIGNMENT 4 - -using GL45Texture = GL45Backend::GL45Texture; - -static std::map> texturesByMipCounts; -static Mutex texturesByMipCountsMutex; -using TextureTypeFormat = std::pair; -std::map> sparsePageDimensionsByFormat; -Mutex sparsePageDimensionsByFormatMutex; - -static std::vector getPageDimensionsForFormat(const TextureTypeFormat& typeFormat) { - { - Lock lock(sparsePageDimensionsByFormatMutex); - if (sparsePageDimensionsByFormat.count(typeFormat)) { - return sparsePageDimensionsByFormat[typeFormat]; - } - } - GLint count = 0; - glGetInternalformativ(typeFormat.first, typeFormat.second, GL_NUM_VIRTUAL_PAGE_SIZES_ARB, 1, &count); - - std::vector result; - if (count > 0) { - std::vector x, y, z; - x.resize(count); - glGetInternalformativ(typeFormat.first, typeFormat.second, GL_VIRTUAL_PAGE_SIZE_X_ARB, 1, &x[0]); - y.resize(count); - glGetInternalformativ(typeFormat.first, typeFormat.second, GL_VIRTUAL_PAGE_SIZE_Y_ARB, 1, &y[0]); - z.resize(count); - glGetInternalformativ(typeFormat.first, typeFormat.second, GL_VIRTUAL_PAGE_SIZE_Z_ARB, 1, &z[0]); - - result.resize(count); - for (GLint i = 0; i < count; ++i) { - result[i] = uvec3(x[i], y[i], z[i]); - } - } - - { - Lock lock(sparsePageDimensionsByFormatMutex); - if (0 == sparsePageDimensionsByFormat.count(typeFormat)) { - sparsePageDimensionsByFormat[typeFormat] = result; - } - } - - return result; -} - -static std::vector getPageDimensionsForFormat(GLenum target, GLenum format) { - return getPageDimensionsForFormat({ target, format }); -} - -GLTexture* GL45Backend::syncGPUObject(const TexturePointer& texture, bool transfer) { - return GL45Texture::sync(*this, texture, transfer); -} - -using SparseInfo = GL45Backend::GL45Texture::SparseInfo; - -SparseInfo::SparseInfo(GL45Texture& texture) - : texture(texture) { -} - -void SparseInfo::maybeMakeSparse() { - // Don't enable sparse for objects with explicitly managed mip levels - if (!texture._gpuObject.isAutogenerateMips()) { - return; - } - return; - - const uvec3 dimensions = texture._gpuObject.getDimensions(); - auto allowedPageDimensions = getPageDimensionsForFormat(texture._target, texture._internalFormat); - // In order to enable sparse the texture size must be an integer multiple of the page size - for (size_t i = 0; i < allowedPageDimensions.size(); ++i) { - pageDimensionsIndex = (uint32_t) i; - pageDimensions = allowedPageDimensions[i]; - // Is this texture an integer multiple of page dimensions? - if (uvec3(0) == (dimensions % pageDimensions)) { - qCDebug(gpugl45logging) << "Enabling sparse for texture " << texture._source.c_str(); - sparse = true; - break; - } - } - - if (sparse) { - glTextureParameteri(texture._id, GL_TEXTURE_SPARSE_ARB, GL_TRUE); - glTextureParameteri(texture._id, GL_VIRTUAL_PAGE_SIZE_INDEX_ARB, pageDimensionsIndex); - } else { - qCDebug(gpugl45logging) << "Size " << dimensions.x << " x " << dimensions.y << - " is not supported by any sparse page size for texture" << texture._source.c_str(); - } -} - #define SPARSE_PAGE_SIZE_OVERHEAD_ESTIMATE 1.3f +#define MAX_RESOURCE_TEXTURES_PER_FRAME 2 -// This can only be called after we've established our storage size -void SparseInfo::update() { - if (!sparse) { - return; +GLTexture* GL45Backend::syncGPUObject(const TexturePointer& texturePointer) { + if (!texturePointer) { + return nullptr; } - glGetTextureParameterIuiv(texture._id, GL_NUM_SPARSE_LEVELS_ARB, &maxSparseLevel); - pageBytes = texture._gpuObject.getTexelFormat().getSize(); - pageBytes *= pageDimensions.x * pageDimensions.y * pageDimensions.z; - // Testing with a simple texture allocating app shows an estimated 20% GPU memory overhead for - // sparse textures as compared to non-sparse, so we acount for that here. - pageBytes = (uint32_t)(pageBytes * SPARSE_PAGE_SIZE_OVERHEAD_ESTIMATE); - for (uint16_t mipLevel = 0; mipLevel <= maxSparseLevel; ++mipLevel) { - auto mipDimensions = texture._gpuObject.evalMipDimensions(mipLevel); - auto mipPageCount = getPageCount(mipDimensions); - maxPages += mipPageCount; + const Texture& texture = *texturePointer; + if (TextureUsageType::EXTERNAL == texture.getUsageType()) { + return Parent::syncGPUObject(texturePointer); } - if (texture._target == GL_TEXTURE_CUBE_MAP) { - maxPages *= GLTexture::CUBE_NUM_FACES; + + if (!texture.isDefined()) { + // NO texture definition yet so let's avoid thinking + return nullptr; } -} -uvec3 SparseInfo::getPageCounts(const uvec3& dimensions) const { - auto result = (dimensions / pageDimensions) + - glm::clamp(dimensions % pageDimensions, glm::uvec3(0), glm::uvec3(1)); - return result; -} + GL45Texture* object = Backend::getGPUObject(texture); + if (!object) { + switch (texture.getUsageType()) { + case TextureUsageType::RENDERBUFFER: + object = new GL45AttachmentTexture(shared_from_this(), texture); + break; -uint32_t SparseInfo::getPageCount(const uvec3& dimensions) const { - auto pageCounts = getPageCounts(dimensions); - return pageCounts.x * pageCounts.y * pageCounts.z; -} + case TextureUsageType::STRICT_RESOURCE: + qCDebug(gpugllogging) << "Strict texture " << texture.source().c_str(); + object = new GL45StrictResourceTexture(shared_from_this(), texture); + break; + case TextureUsageType::RESOURCE: { + if (GL45VariableAllocationTexture::_frameTexturesCreated < MAX_RESOURCE_TEXTURES_PER_FRAME) { +#if 0 + if (isTextureManagementSparseEnabled() && GL45Texture::isSparseEligible(texture)) { + object = new GL45SparseResourceTexture(shared_from_this(), texture); + } else { + object = new GL45ResourceTexture(shared_from_this(), texture); + } +#else + object = new GL45ResourceTexture(shared_from_this(), texture); +#endif + GL45VariableAllocationTexture::addMemoryManagedTexture(texturePointer); + } else { + auto fallback = texturePointer->getFallbackTexture(); + if (fallback) { + object = static_cast(syncGPUObject(fallback)); + } + } + break; + } -uint32_t SparseInfo::getSize() const { - return allocatedPages * pageBytes; + default: + Q_UNREACHABLE(); + } + } + + return object; } void GL45Backend::initTextureManagementStage() { @@ -171,6 +100,12 @@ void GL45Backend::initTextureManagementStage() { } } +using GL45Texture = GL45Backend::GL45Texture; + +GL45Texture::GL45Texture(const std::weak_ptr& backend, const Texture& texture) + : GLTexture(backend, texture, allocate(texture)) { + incrementTextureGPUCount(); +} GLuint GL45Texture::allocate(const Texture& texture) { GLuint result; @@ -178,164 +113,43 @@ GLuint GL45Texture::allocate(const Texture& texture) { return result; } -GLuint GL45Backend::getTextureID(const TexturePointer& texture, bool transfer) { - return GL45Texture::getId(*this, texture, transfer); -} - -GL45Texture::GL45Texture(const std::weak_ptr& backend, const Texture& texture, GLuint externalId) - : GLTexture(backend, texture, externalId), _sparseInfo(*this) -{ -} - -GL45Texture::GL45Texture(const std::weak_ptr& backend, const Texture& texture, bool transferrable) - : GLTexture(backend, texture, allocate(texture), transferrable), _sparseInfo(*this) - { - - auto theBackend = _backend.lock(); - if (_transferrable && theBackend && theBackend->isTextureManagementSparseEnabled()) { - _sparseInfo.maybeMakeSparse(); - if (_sparseInfo.sparse) { - Backend::incrementTextureGPUSparseCount(); - } - } -} - -GL45Texture::~GL45Texture() { - // Remove this texture from the candidate list of derezzable textures - if (_transferrable) { - auto mipLevels = usedMipLevels(); - Lock lock(texturesByMipCountsMutex); - if (texturesByMipCounts.count(mipLevels)) { - auto& textures = texturesByMipCounts[mipLevels]; - textures.erase(this); - if (textures.empty()) { - texturesByMipCounts.erase(mipLevels); - } - } - } - - if (_sparseInfo.sparse) { - Backend::decrementTextureGPUSparseCount(); - - // Experimenation suggests that allocating sparse textures on one context/thread and deallocating - // them on another is buggy. So for sparse textures we need to queue a lambda with the deallocation - // callls to the transfer thread - auto id = _id; - // Set the class _id to 0 so we don't try to double delete - const_cast(_id) = 0; - std::list> destructionFunctions; - - uint8_t maxFace = (uint8_t)((_target == GL_TEXTURE_CUBE_MAP) ? GLTexture::CUBE_NUM_FACES : 1); - auto maxSparseMip = std::min(_maxMip, _sparseInfo.maxSparseLevel); - for (uint16_t mipLevel = _minMip; mipLevel <= maxSparseMip; ++mipLevel) { - auto mipDimensions = _gpuObject.evalMipDimensions(mipLevel); - destructionFunctions.push_back([id, maxFace, mipLevel, mipDimensions] { - glTexturePageCommitmentEXT(id, mipLevel, 0, 0, 0, mipDimensions.x, mipDimensions.y, maxFace, GL_FALSE); - }); - - auto deallocatedPages = _sparseInfo.getPageCount(mipDimensions) * maxFace; - assert(deallocatedPages <= _sparseInfo.allocatedPages); - _sparseInfo.allocatedPages -= deallocatedPages; - } - - if (0 != _sparseInfo.allocatedPages) { - qCWarning(gpugl45logging) << "Allocated pages remaining " << _id << " " << _sparseInfo.allocatedPages; - } - - auto size = _size; - const_cast(_size) = 0; - _textureTransferHelper->queueExecution([id, size, destructionFunctions] { - for (auto function : destructionFunctions) { - function(); - } - glDeleteTextures(1, &id); - Backend::decrementTextureGPUCount(); - Backend::updateTextureGPUMemoryUsage(size, 0); - Backend::updateTextureGPUSparseMemoryUsage(size, 0); - }); - } -} - -void GL45Texture::withPreservedTexture(std::function f) const { - f(); -} - void GL45Texture::generateMips() const { glGenerateTextureMipmap(_id); (void)CHECK_GL_ERROR(); } -void GL45Texture::allocateStorage() const { - if (_gpuObject.getTexelFormat().isCompressed()) { - qFatal("Compressed textures not yet supported"); +void GL45Texture::copyMipFaceLinesFromTexture(uint16_t mip, uint8_t face, const uvec3& size, uint32_t yOffset, GLenum format, GLenum type, const void* sourcePointer) const { + if (GL_TEXTURE_2D == _target) { + glTextureSubImage2D(_id, mip, 0, yOffset, size.x, size.y, format, type, sourcePointer); + } else if (GL_TEXTURE_CUBE_MAP == _target) { + // DSA ARB does not work on AMD, so use EXT + // unless EXT is not available on the driver + if (glTextureSubImage2DEXT) { + auto target = GLTexture::CUBE_FACE_LAYOUT[face]; + glTextureSubImage2DEXT(_id, target, mip, 0, yOffset, size.x, size.y, format, type, sourcePointer); + } else { + glTextureSubImage3D(_id, mip, 0, yOffset, face, size.x, size.y, 1, format, type, sourcePointer); + } + } else { + Q_ASSERT(false); } - glTextureParameteri(_id, GL_TEXTURE_BASE_LEVEL, 0); - glTextureParameteri(_id, GL_TEXTURE_MAX_LEVEL, _maxMip - _minMip); - // Get the dimensions, accounting for the downgrade level - Vec3u dimensions = _gpuObject.evalMipDimensions(_minMip + _mipOffset); - glTextureStorage2D(_id, usedMipLevels(), _internalFormat, dimensions.x, dimensions.y); (void)CHECK_GL_ERROR(); } -void GL45Texture::updateSize() const { - if (_gpuObject.getTexelFormat().isCompressed()) { - qFatal("Compressed textures not yet supported"); +void GL45Texture::copyMipFaceFromTexture(uint16_t sourceMip, uint16_t targetMip, uint8_t face) const { + if (!_gpuObject.isStoredMipFaceAvailable(sourceMip)) { + return; } - - if (_transferrable && _sparseInfo.sparse) { - auto size = _sparseInfo.getSize(); - Backend::updateTextureGPUSparseMemoryUsage(_size, size); - setSize(size); + auto size = _gpuObject.evalMipDimensions(sourceMip); + auto mipData = _gpuObject.accessStoredMipFace(sourceMip, face); + if (mipData) { + GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat(), _gpuObject.getStoredMipFormat()); + copyMipFaceLinesFromTexture(targetMip, face, size, 0, texelFormat.format, texelFormat.type, mipData->readData()); } else { - setSize(_gpuObject.evalTotalSize(_mipOffset)); + qCDebug(gpugllogging) << "Missing mipData level=" << sourceMip << " face=" << (int)face << " for texture " << _gpuObject.source().c_str(); } } -void GL45Texture::startTransfer() { - Parent::startTransfer(); - _sparseInfo.update(); -} - -bool GL45Texture::continueTransfer() { - PROFILE_RANGE(render_gpu_gl, "continueTransfer") - size_t maxFace = GL_TEXTURE_CUBE_MAP == _target ? CUBE_NUM_FACES : 1; - for (uint8_t face = 0; face < maxFace; ++face) { - for (uint16_t mipLevel = _minMip; mipLevel <= _maxMip; ++mipLevel) { - auto size = _gpuObject.evalMipDimensions(mipLevel); - if (_sparseInfo.sparse && mipLevel <= _sparseInfo.maxSparseLevel) { - glTexturePageCommitmentEXT(_id, mipLevel, 0, 0, face, size.x, size.y, 1, GL_TRUE); - _sparseInfo.allocatedPages += _sparseInfo.getPageCount(size); - } - if (_gpuObject.isStoredMipFaceAvailable(mipLevel, face)) { - PROFILE_RANGE_EX(render_gpu_gl, "texSubImage", 0x0000ffff, (size.x * size.y * maxFace / 1024)); - - auto mip = _gpuObject.accessStoredMipFace(mipLevel, face); - GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat(), mip->getFormat()); - if (GL_TEXTURE_2D == _target) { - glTextureSubImage2D(_id, mipLevel, 0, 0, size.x, size.y, texelFormat.format, texelFormat.type, mip->readData()); - } else if (GL_TEXTURE_CUBE_MAP == _target) { - // DSA ARB does not work on AMD, so use EXT - // unless EXT is not available on the driver - if (glTextureSubImage2DEXT) { - auto target = CUBE_FACE_LAYOUT[face]; - glTextureSubImage2DEXT(_id, target, mipLevel, 0, 0, size.x, size.y, texelFormat.format, texelFormat.type, mip->readData()); - } else { - glTextureSubImage3D(_id, mipLevel, 0, 0, face, size.x, size.y, 1, texelFormat.format, texelFormat.type, mip->readData()); - } - } else { - Q_ASSERT(false); - } - (void)CHECK_GL_ERROR(); - } - } - } - return false; -} - -void GL45Texture::finishTransfer() { - Parent::finishTransfer(); -} - void GL45Texture::syncSampler() const { const Sampler& sampler = _gpuObject.getSampler(); @@ -353,163 +167,63 @@ void GL45Texture::syncSampler() const { glTextureParameteri(_id, GL_TEXTURE_WRAP_S, WRAP_MODES[sampler.getWrapModeU()]); glTextureParameteri(_id, GL_TEXTURE_WRAP_T, WRAP_MODES[sampler.getWrapModeV()]); glTextureParameteri(_id, GL_TEXTURE_WRAP_R, WRAP_MODES[sampler.getWrapModeW()]); + glTextureParameterf(_id, GL_TEXTURE_MAX_ANISOTROPY_EXT, sampler.getMaxAnisotropy()); glTextureParameterfv(_id, GL_TEXTURE_BORDER_COLOR, (const float*)&sampler.getBorderColor()); - // FIXME account for mip offsets here - auto baseMip = std::max(sampler.getMipOffset(), _minMip); + glTextureParameterf(_id, GL_TEXTURE_MIN_LOD, sampler.getMinMip()); + glTextureParameterf(_id, GL_TEXTURE_MAX_LOD, (sampler.getMaxMip() == Sampler::MAX_MIP_LEVEL ? 1000.f : sampler.getMaxMip())); +} + +using GL45FixedAllocationTexture = GL45Backend::GL45FixedAllocationTexture; + +GL45FixedAllocationTexture::GL45FixedAllocationTexture(const std::weak_ptr& backend, const Texture& texture) : GL45Texture(backend, texture), _size(texture.evalTotalSize()) { + allocateStorage(); + syncSampler(); +} + +GL45FixedAllocationTexture::~GL45FixedAllocationTexture() { +} + +void GL45FixedAllocationTexture::allocateStorage() const { + const GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat()); + const auto dimensions = _gpuObject.getDimensions(); + const auto mips = _gpuObject.evalNumMips(); + glTextureStorage2D(_id, mips, texelFormat.internalFormat, dimensions.x, dimensions.y); +} + +void GL45FixedAllocationTexture::syncSampler() const { + Parent::syncSampler(); + const Sampler& sampler = _gpuObject.getSampler(); + auto baseMip = std::max(sampler.getMipOffset(), sampler.getMinMip()); glTextureParameteri(_id, GL_TEXTURE_BASE_LEVEL, baseMip); glTextureParameterf(_id, GL_TEXTURE_MIN_LOD, (float)sampler.getMinMip()); - glTextureParameterf(_id, GL_TEXTURE_MAX_LOD, (sampler.getMaxMip() == Sampler::MAX_MIP_LEVEL ? 1000.f : sampler.getMaxMip() - _mipOffset)); - glTextureParameterf(_id, GL_TEXTURE_MAX_ANISOTROPY_EXT, sampler.getMaxAnisotropy()); + glTextureParameterf(_id, GL_TEXTURE_MAX_LOD, (sampler.getMaxMip() == Sampler::MAX_MIP_LEVEL ? 1000.f : sampler.getMaxMip())); } -void GL45Texture::postTransfer() { - Parent::postTransfer(); - auto mipLevels = usedMipLevels(); - if (_transferrable && mipLevels > 1 && _minMip < _sparseInfo.maxSparseLevel) { - Lock lock(texturesByMipCountsMutex); - texturesByMipCounts[mipLevels].insert(this); - } +// Renderbuffer attachment textures +using GL45AttachmentTexture = GL45Backend::GL45AttachmentTexture; + +GL45AttachmentTexture::GL45AttachmentTexture(const std::weak_ptr& backend, const Texture& texture) : GL45FixedAllocationTexture(backend, texture) { + Backend::updateTextureGPUFramebufferMemoryUsage(0, size()); } -void GL45Texture::stripToMip(uint16_t newMinMip) { - if (newMinMip < _minMip) { - qCWarning(gpugl45logging) << "Cannot decrease the min mip"; - return; - } +GL45AttachmentTexture::~GL45AttachmentTexture() { + Backend::updateTextureGPUFramebufferMemoryUsage(size(), 0); +} - if (_sparseInfo.sparse && newMinMip > _sparseInfo.maxSparseLevel) { - qCWarning(gpugl45logging) << "Cannot increase the min mip into the mip tail"; - return; - } +// Strict resource textures +using GL45StrictResourceTexture = GL45Backend::GL45StrictResourceTexture; - PROFILE_RANGE(render_gpu_gl, "GL45Texture::stripToMip"); - - auto mipLevels = usedMipLevels(); - { - Lock lock(texturesByMipCountsMutex); - assert(0 != texturesByMipCounts.count(mipLevels)); - assert(0 != texturesByMipCounts[mipLevels].count(this)); - texturesByMipCounts[mipLevels].erase(this); - if (texturesByMipCounts[mipLevels].empty()) { - texturesByMipCounts.erase(mipLevels); +GL45StrictResourceTexture::GL45StrictResourceTexture(const std::weak_ptr& backend, const Texture& texture) : GL45FixedAllocationTexture(backend, texture) { + auto mipLevels = _gpuObject.evalNumMips(); + for (uint16_t sourceMip = 0; sourceMip < mipLevels; ++sourceMip) { + uint16_t targetMip = sourceMip; + size_t maxFace = GLTexture::getFaceCount(_target); + for (uint8_t face = 0; face < maxFace; ++face) { + copyMipFaceFromTexture(sourceMip, targetMip, face); } } - - // If we weren't generating mips before, we need to now that we're stripping down mip levels. - if (!_gpuObject.isAutogenerateMips()) { - qCDebug(gpugl45logging) << "Force mip generation for texture"; - glGenerateTextureMipmap(_id); - } - - - uint8_t maxFace = (uint8_t)((_target == GL_TEXTURE_CUBE_MAP) ? GLTexture::CUBE_NUM_FACES : 1); - if (_sparseInfo.sparse) { - for (uint16_t mip = _minMip; mip < newMinMip; ++mip) { - auto id = _id; - auto mipDimensions = _gpuObject.evalMipDimensions(mip); - _textureTransferHelper->queueExecution([id, mip, mipDimensions, maxFace] { - glTexturePageCommitmentEXT(id, mip, 0, 0, 0, mipDimensions.x, mipDimensions.y, maxFace, GL_FALSE); - }); - - auto deallocatedPages = _sparseInfo.getPageCount(mipDimensions) * maxFace; - assert(deallocatedPages < _sparseInfo.allocatedPages); - _sparseInfo.allocatedPages -= deallocatedPages; - } - _minMip = newMinMip; - } else { - GLuint oldId = _id; - // Find the distance between the old min mip and the new one - uint16 mipDelta = newMinMip - _minMip; - _mipOffset += mipDelta; - const_cast(_maxMip) -= mipDelta; - auto newLevels = usedMipLevels(); - - // Create and setup the new texture (allocate) - { - Vec3u newDimensions = _gpuObject.evalMipDimensions(_mipOffset); - PROFILE_RANGE_EX(render_gpu_gl, "Re-Allocate", 0xff0000ff, (newDimensions.x * newDimensions.y)); - - glCreateTextures(_target, 1, &const_cast(_id)); - glTextureParameteri(_id, GL_TEXTURE_BASE_LEVEL, 0); - glTextureParameteri(_id, GL_TEXTURE_MAX_LEVEL, _maxMip - _minMip); - glTextureStorage2D(_id, newLevels, _internalFormat, newDimensions.x, newDimensions.y); - } - - // Copy the contents of the old texture to the new - { - PROFILE_RANGE(render_gpu_gl, "Blit"); - // Preferred path only available in 4.3 - for (uint16 targetMip = _minMip; targetMip <= _maxMip; ++targetMip) { - uint16 sourceMip = targetMip + mipDelta; - Vec3u mipDimensions = _gpuObject.evalMipDimensions(targetMip + _mipOffset); - for (GLenum target : getFaceTargets(_target)) { - glCopyImageSubData( - oldId, target, sourceMip, 0, 0, 0, - _id, target, targetMip, 0, 0, 0, - mipDimensions.x, mipDimensions.y, 1 - ); - (void)CHECK_GL_ERROR(); - } - } - - glDeleteTextures(1, &oldId); - } - } - - // Re-sync the sampler to force access to the new mip level - syncSampler(); - updateSize(); - - // Re-insert into the texture-by-mips map if appropriate - mipLevels = usedMipLevels(); - if (mipLevels > 1 && (!_sparseInfo.sparse || _minMip < _sparseInfo.maxSparseLevel)) { - Lock lock(texturesByMipCountsMutex); - texturesByMipCounts[mipLevels].insert(this); + if (texture.isAutogenerateMips()) { + generateMips(); } } -void GL45Texture::updateMips() { - if (!_sparseInfo.sparse) { - return; - } - auto newMinMip = std::min(_gpuObject.minMip(), _sparseInfo.maxSparseLevel); - if (_minMip < newMinMip) { - stripToMip(newMinMip); - } -} - -void GL45Texture::derez() { - if (_sparseInfo.sparse) { - assert(_minMip < _sparseInfo.maxSparseLevel); - } - assert(_minMip < _maxMip); - assert(_transferrable); - stripToMip(_minMip + 1); -} - -void GL45Backend::derezTextures() const { - if (GLTexture::getMemoryPressure() < 1.0f) { - return; - } - - Lock lock(texturesByMipCountsMutex); - if (texturesByMipCounts.empty()) { - // No available textures to derez - return; - } - - auto mipLevel = texturesByMipCounts.rbegin()->first; - if (mipLevel <= 1) { - // No mips available to remove - return; - } - - GL45Texture* targetTexture = nullptr; - { - auto& textures = texturesByMipCounts[mipLevel]; - assert(!textures.empty()); - targetTexture = *textures.begin(); - } - lock.unlock(); - targetTexture->derez(); -} diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendVariableTexture.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendVariableTexture.cpp new file mode 100644 index 0000000000..d54ad1ea4b --- /dev/null +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendVariableTexture.cpp @@ -0,0 +1,1033 @@ +// +// GL45BackendTexture.cpp +// libraries/gpu/src/gpu +// +// Created by Sam Gateau on 1/19/2015. +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "GL45Backend.h" +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include "../gl/GLTexelFormat.h" + +using namespace gpu; +using namespace gpu::gl; +using namespace gpu::gl45; + +// Variable sized textures +using GL45VariableAllocationTexture = GL45Backend::GL45VariableAllocationTexture; +using MemoryPressureState = GL45VariableAllocationTexture::MemoryPressureState; +using WorkQueue = GL45VariableAllocationTexture::WorkQueue; + +std::list GL45VariableAllocationTexture::_memoryManagedTextures; +MemoryPressureState GL45VariableAllocationTexture::_memoryPressureState = MemoryPressureState::Idle; +std::atomic GL45VariableAllocationTexture::_memoryPressureStateStale { false }; +const uvec3 GL45VariableAllocationTexture::INITIAL_MIP_TRANSFER_DIMENSIONS { 64, 64, 1 }; +WorkQueue GL45VariableAllocationTexture::_transferQueue; +WorkQueue GL45VariableAllocationTexture::_promoteQueue; +WorkQueue GL45VariableAllocationTexture::_demoteQueue; +TexturePointer GL45VariableAllocationTexture::_currentTransferTexture; + +#define OVERSUBSCRIBED_PRESSURE_VALUE 0.95f +#define UNDERSUBSCRIBED_PRESSURE_VALUE 0.85f +#define DEFAULT_ALLOWED_TEXTURE_MEMORY_MB ((size_t)1024) + +static const size_t DEFAULT_ALLOWED_TEXTURE_MEMORY = MB_TO_BYTES(DEFAULT_ALLOWED_TEXTURE_MEMORY_MB); + +using TransferJob = GL45VariableAllocationTexture::TransferJob; + +static const uvec3 MAX_TRANSFER_DIMENSIONS { 1024, 1024, 1 }; +static const size_t MAX_TRANSFER_SIZE = MAX_TRANSFER_DIMENSIONS.x * MAX_TRANSFER_DIMENSIONS.y * 4; + +#if THREADED_TEXTURE_BUFFERING +std::shared_ptr TransferJob::_bufferThread { nullptr }; +std::atomic TransferJob::_shutdownBufferingThread { false }; +Mutex TransferJob::_mutex; +TransferJob::VoidLambdaQueue TransferJob::_bufferLambdaQueue; + +void TransferJob::startTransferLoop() { + if (_bufferThread) { + return; + } + _shutdownBufferingThread = false; + _bufferThread = std::make_shared([] { + TransferJob::bufferLoop(); + }); +} + +void TransferJob::stopTransferLoop() { + if (!_bufferThread) { + return; + } + _shutdownBufferingThread = true; + _bufferThread->join(); + _bufferThread.reset(); + _shutdownBufferingThread = false; +} +#endif + +TransferJob::TransferJob(const GL45VariableAllocationTexture& parent, uint16_t sourceMip, uint16_t targetMip, uint8_t face, uint32_t lines, uint32_t lineOffset) + : _parent(parent) { + + auto transferDimensions = _parent._gpuObject.evalMipDimensions(sourceMip); + GLenum format; + GLenum type; + auto mipData = _parent._gpuObject.accessStoredMipFace(sourceMip, face); + GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_parent._gpuObject.getTexelFormat(), _parent._gpuObject.getStoredMipFormat()); + format = texelFormat.format; + type = texelFormat.type; + + if (0 == lines) { + _transferSize = mipData->getSize(); + _bufferingLambda = [=] { + _buffer.resize(_transferSize); + memcpy(&_buffer[0], mipData->readData(), _transferSize); + _bufferingCompleted = true; + }; + + } else { + transferDimensions.y = lines; + auto dimensions = _parent._gpuObject.evalMipDimensions(sourceMip); + auto mipSize = mipData->getSize(); + auto bytesPerLine = (uint32_t)mipSize / dimensions.y; + _transferSize = bytesPerLine * lines; + auto sourceOffset = bytesPerLine * lineOffset; + _bufferingLambda = [=] { + _buffer.resize(_transferSize); + memcpy(&_buffer[0], mipData->readData() + sourceOffset, _transferSize); + _bufferingCompleted = true; + }; + } + + Backend::updateTextureTransferPendingSize(0, _transferSize); + + _transferLambda = [=] { + _parent.copyMipFaceLinesFromTexture(targetMip, face, transferDimensions, lineOffset, format, type, _buffer.data()); + std::vector emptyVector; + _buffer.swap(emptyVector); + }; +} + +TransferJob::TransferJob(const GL45VariableAllocationTexture& parent, std::function transferLambda) + : _parent(parent), _bufferingCompleted(true), _transferLambda(transferLambda) { +} + +TransferJob::~TransferJob() { + Backend::updateTextureTransferPendingSize(_transferSize, 0); +} + + +bool TransferJob::tryTransfer() { + // Disable threaded texture transfer for now +#if THREADED_TEXTURE_BUFFERING + // Are we ready to transfer + if (_bufferingCompleted) { + _transferLambda(); + return true; + } + + startBuffering(); + return false; +#else + if (!_bufferingCompleted) { + _bufferingLambda(); + _bufferingCompleted = true; + } + _transferLambda(); + return true; +#endif +} + +#if THREADED_TEXTURE_BUFFERING + +void TransferJob::startBuffering() { + if (_bufferingStarted) { + return; + } + _bufferingStarted = true; + { + Lock lock(_mutex); + _bufferLambdaQueue.push(_bufferingLambda); + } +} + +void TransferJob::bufferLoop() { + while (!_shutdownBufferingThread) { + VoidLambdaQueue workingQueue; + { + Lock lock(_mutex); + _bufferLambdaQueue.swap(workingQueue); + } + + if (workingQueue.empty()) { + QThread::msleep(5); + continue; + } + + while (!workingQueue.empty()) { + workingQueue.front()(); + workingQueue.pop(); + } + } +} +#endif + + +void GL45VariableAllocationTexture::addMemoryManagedTexture(const TexturePointer& texturePointer) { + _memoryManagedTextures.push_back(texturePointer); + addToWorkQueue(texturePointer); +} + +void GL45VariableAllocationTexture::addToWorkQueue(const TexturePointer& texturePointer) { + GL45VariableAllocationTexture* object = Backend::getGPUObject(*texturePointer); + switch (_memoryPressureState) { + case MemoryPressureState::Oversubscribed: + if (object->canDemote()) { + // Demote largest first + _demoteQueue.push({ texturePointer, (float)object->size() }); + } + break; + + case MemoryPressureState::Undersubscribed: + if (object->canPromote()) { + // Promote smallest first + _promoteQueue.push({ texturePointer, 1.0f / (float)object->size() }); + } + break; + + case MemoryPressureState::Transfer: + if (object->hasPendingTransfers()) { + // Transfer priority given to smaller mips first + _transferQueue.push({ texturePointer, 1.0f / (float)object->_gpuObject.evalMipSize(object->_populatedMip) }); + } + break; + + case MemoryPressureState::Idle: + break; + + default: + Q_UNREACHABLE(); + } +} + +WorkQueue& GL45VariableAllocationTexture::getActiveWorkQueue() { + static WorkQueue empty; + switch (_memoryPressureState) { + case MemoryPressureState::Oversubscribed: + return _demoteQueue; + + case MemoryPressureState::Undersubscribed: + return _promoteQueue; + + case MemoryPressureState::Transfer: + return _transferQueue; + + default: + break; + } + Q_UNREACHABLE(); + return empty; +} + +// FIXME hack for stats display +QString getTextureMemoryPressureModeString() { + switch (GL45VariableAllocationTexture::_memoryPressureState) { + case MemoryPressureState::Oversubscribed: + return "Oversubscribed"; + + case MemoryPressureState::Undersubscribed: + return "Undersubscribed"; + + case MemoryPressureState::Transfer: + return "Transfer"; + + case MemoryPressureState::Idle: + return "Idle"; + } + Q_UNREACHABLE(); + return "Unknown"; +} + +void GL45VariableAllocationTexture::updateMemoryPressure() { + static size_t lastAllowedMemoryAllocation = gpu::Texture::getAllowedGPUMemoryUsage(); + + size_t allowedMemoryAllocation = gpu::Texture::getAllowedGPUMemoryUsage(); + if (0 == allowedMemoryAllocation) { + allowedMemoryAllocation = DEFAULT_ALLOWED_TEXTURE_MEMORY; + } + + // If the user explicitly changed the allowed memory usage, we need to mark ourselves stale + // so that we react + if (allowedMemoryAllocation != lastAllowedMemoryAllocation) { + _memoryPressureStateStale = true; + lastAllowedMemoryAllocation = allowedMemoryAllocation; + } + + if (!_memoryPressureStateStale.exchange(false)) { + return; + } + + PROFILE_RANGE(render_gpu_gl, __FUNCTION__); + + // Clear any defunct textures (weak pointers that no longer have a valid texture) + _memoryManagedTextures.remove_if([&](const TextureWeakPointer& weakPointer) { + return weakPointer.expired(); + }); + + // Convert weak pointers to strong. This new list may still contain nulls if a texture was + // deleted on another thread between the previous line and this one + std::vector strongTextures; { + strongTextures.reserve(_memoryManagedTextures.size()); + std::transform( + _memoryManagedTextures.begin(), _memoryManagedTextures.end(), + std::back_inserter(strongTextures), + [](const TextureWeakPointer& p) { return p.lock(); }); + } + + size_t totalVariableMemoryAllocation = 0; + size_t idealMemoryAllocation = 0; + bool canDemote = false; + bool canPromote = false; + bool hasTransfers = false; + for (const auto& texture : strongTextures) { + // Race conditions can still leave nulls in the list, so we need to check + if (!texture) { + continue; + } + GL45VariableAllocationTexture* object = Backend::getGPUObject(*texture); + // Track how much the texture thinks it should be using + idealMemoryAllocation += texture->evalTotalSize(); + // Track how much we're actually using + totalVariableMemoryAllocation += object->size(); + canDemote |= object->canDemote(); + canPromote |= object->canPromote(); + hasTransfers |= object->hasPendingTransfers(); + } + + size_t unallocated = idealMemoryAllocation - totalVariableMemoryAllocation; + float pressure = (float)totalVariableMemoryAllocation / (float)allowedMemoryAllocation; + + auto newState = MemoryPressureState::Idle; + if (pressure > OVERSUBSCRIBED_PRESSURE_VALUE && canDemote) { + newState = MemoryPressureState::Oversubscribed; + } else if (pressure < UNDERSUBSCRIBED_PRESSURE_VALUE && unallocated != 0 && canPromote) { + newState = MemoryPressureState::Undersubscribed; + } else if (hasTransfers) { + newState = MemoryPressureState::Transfer; + } + + if (newState != _memoryPressureState) { +#if THREADED_TEXTURE_BUFFERING + if (MemoryPressureState::Transfer == _memoryPressureState) { + TransferJob::stopTransferLoop(); + } + _memoryPressureState = newState; + if (MemoryPressureState::Transfer == _memoryPressureState) { + TransferJob::startTransferLoop(); + } +#else + _memoryPressureState = newState; +#endif + // Clear the existing queue + _transferQueue = WorkQueue(); + _promoteQueue = WorkQueue(); + _demoteQueue = WorkQueue(); + + // Populate the existing textures into the queue + for (const auto& texture : strongTextures) { + addToWorkQueue(texture); + } + } +} + +void GL45VariableAllocationTexture::processWorkQueues() { + if (MemoryPressureState::Idle == _memoryPressureState) { + return; + } + + auto& workQueue = getActiveWorkQueue(); + PROFILE_RANGE(render_gpu_gl, __FUNCTION__); + while (!workQueue.empty()) { + auto workTarget = workQueue.top(); + workQueue.pop(); + auto texture = workTarget.first.lock(); + if (!texture) { + continue; + } + + // Grab the first item off the demote queue + GL45VariableAllocationTexture* object = Backend::getGPUObject(*texture); + if (MemoryPressureState::Oversubscribed == _memoryPressureState) { + if (!object->canDemote()) { + continue; + } + object->demote(); + } else if (MemoryPressureState::Undersubscribed == _memoryPressureState) { + if (!object->canPromote()) { + continue; + } + object->promote(); + } else if (MemoryPressureState::Transfer == _memoryPressureState) { + if (!object->hasPendingTransfers()) { + continue; + } + object->executeNextTransfer(texture); + } else { + Q_UNREACHABLE(); + } + + // Reinject into the queue if more work to be done + addToWorkQueue(texture); + break; + } + + if (workQueue.empty()) { + _memoryPressureStateStale = true; + } +} + +void GL45VariableAllocationTexture::manageMemory() { + PROFILE_RANGE(render_gpu_gl, __FUNCTION__); + updateMemoryPressure(); + processWorkQueues(); +} + +size_t GL45VariableAllocationTexture::_frameTexturesCreated { 0 }; + +GL45VariableAllocationTexture::GL45VariableAllocationTexture(const std::weak_ptr& backend, const Texture& texture) : GL45Texture(backend, texture) { + ++_frameTexturesCreated; +} + +GL45VariableAllocationTexture::~GL45VariableAllocationTexture() { + _memoryPressureStateStale = true; + Backend::updateTextureGPUMemoryUsage(_size, 0); +} + +void GL45VariableAllocationTexture::executeNextTransfer(const TexturePointer& currentTexture) { + if (_populatedMip <= _allocatedMip) { + return; + } + + if (_pendingTransfers.empty()) { + populateTransferQueue(); + } + + if (!_pendingTransfers.empty()) { + // Keeping hold of a strong pointer during the transfer ensures that the transfer thread cannot try to access a destroyed texture + _currentTransferTexture = currentTexture; + if (_pendingTransfers.front()->tryTransfer()) { + _pendingTransfers.pop(); + _currentTransferTexture.reset(); + } + } +} + +// Managed size resource textures +using GL45ResourceTexture = GL45Backend::GL45ResourceTexture; + +GL45ResourceTexture::GL45ResourceTexture(const std::weak_ptr& backend, const Texture& texture) : GL45VariableAllocationTexture(backend, texture) { + auto mipLevels = texture.evalNumMips(); + _allocatedMip = mipLevels; + uvec3 mipDimensions; + for (uint16_t mip = 0; mip < mipLevels; ++mip) { + if (glm::all(glm::lessThanEqual(texture.evalMipDimensions(mip), INITIAL_MIP_TRANSFER_DIMENSIONS))) { + _maxAllocatedMip = _populatedMip = mip; + break; + } + } + + uint16_t allocatedMip = _populatedMip - std::min(_populatedMip, 2); + allocateStorage(allocatedMip); + _memoryPressureStateStale = true; + copyMipsFromTexture(); + syncSampler(); + +} + +void GL45ResourceTexture::allocateStorage(uint16 allocatedMip) { + _allocatedMip = allocatedMip; + const GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat()); + const auto dimensions = _gpuObject.evalMipDimensions(_allocatedMip); + const auto totalMips = _gpuObject.evalNumMips(); + const auto mips = totalMips - _allocatedMip; + glTextureStorage2D(_id, mips, texelFormat.internalFormat, dimensions.x, dimensions.y); + auto mipLevels = _gpuObject.evalNumMips(); + _size = 0; + for (uint16_t mip = _allocatedMip; mip < mipLevels; ++mip) { + _size += _gpuObject.evalMipSize(mip); + } + Backend::updateTextureGPUMemoryUsage(0, _size); + +} + +void GL45ResourceTexture::copyMipsFromTexture() { + auto mipLevels = _gpuObject.evalNumMips(); + size_t maxFace = GLTexture::getFaceCount(_target); + for (uint16_t sourceMip = _populatedMip; sourceMip < mipLevels; ++sourceMip) { + uint16_t targetMip = sourceMip - _allocatedMip; + for (uint8_t face = 0; face < maxFace; ++face) { + copyMipFaceFromTexture(sourceMip, targetMip, face); + } + } +} + +void GL45ResourceTexture::syncSampler() const { + Parent::syncSampler(); + glTextureParameteri(_id, GL_TEXTURE_BASE_LEVEL, _populatedMip - _allocatedMip); +} + +void GL45ResourceTexture::promote() { + PROFILE_RANGE(render_gpu_gl, __FUNCTION__); + Q_ASSERT(_allocatedMip > 0); + GLuint oldId = _id; + uint32_t oldSize = _size; + // create new texture + const_cast(_id) = allocate(_gpuObject); + uint16_t oldAllocatedMip = _allocatedMip; + // allocate storage for new level + allocateStorage(_allocatedMip - std::min(_allocatedMip, 2)); + uint16_t mips = _gpuObject.evalNumMips(); + // copy pre-existing mips + for (uint16_t mip = _populatedMip; mip < mips; ++mip) { + auto mipDimensions = _gpuObject.evalMipDimensions(mip); + uint16_t targetMip = mip - _allocatedMip; + uint16_t sourceMip = mip - oldAllocatedMip; + auto faces = getFaceCount(_target); + for (uint8_t face = 0; face < faces; ++face) { + glCopyImageSubData( + oldId, _target, sourceMip, 0, 0, face, + _id, _target, targetMip, 0, 0, face, + mipDimensions.x, mipDimensions.y, 1 + ); + (void)CHECK_GL_ERROR(); + } + } + // destroy the old texture + glDeleteTextures(1, &oldId); + // update the memory usage + Backend::updateTextureGPUMemoryUsage(oldSize, 0); + _memoryPressureStateStale = true; + syncSampler(); + populateTransferQueue(); +} + +void GL45ResourceTexture::demote() { + PROFILE_RANGE(render_gpu_gl, __FUNCTION__); + Q_ASSERT(_allocatedMip < _maxAllocatedMip); + auto oldId = _id; + auto oldSize = _size; + const_cast(_id) = allocate(_gpuObject); + allocateStorage(_allocatedMip + 1); + _populatedMip = std::max(_populatedMip, _allocatedMip); + uint16_t mips = _gpuObject.evalNumMips(); + // copy pre-existing mips + for (uint16_t mip = _populatedMip; mip < mips; ++mip) { + auto mipDimensions = _gpuObject.evalMipDimensions(mip); + uint16_t targetMip = mip - _allocatedMip; + uint16_t sourceMip = targetMip + 1; + auto faces = getFaceCount(_target); + for (uint8_t face = 0; face < faces; ++face) { + glCopyImageSubData( + oldId, _target, sourceMip, 0, 0, face, + _id, _target, targetMip, 0, 0, face, + mipDimensions.x, mipDimensions.y, 1 + ); + (void)CHECK_GL_ERROR(); + } + } + // destroy the old texture + glDeleteTextures(1, &oldId); + // update the memory usage + Backend::updateTextureGPUMemoryUsage(oldSize, 0); + _memoryPressureStateStale = true; + syncSampler(); + populateTransferQueue(); +} + + +void GL45ResourceTexture::populateTransferQueue() { + PROFILE_RANGE(render_gpu_gl, __FUNCTION__); + if (_populatedMip <= _allocatedMip) { + return; + } + _pendingTransfers = TransferQueue(); + + const uint8_t maxFace = GLTexture::getFaceCount(_target); + uint16_t sourceMip = _populatedMip; + do { + --sourceMip; + auto targetMip = sourceMip - _allocatedMip; + auto mipDimensions = _gpuObject.evalMipDimensions(sourceMip); + for (uint8_t face = 0; face < maxFace; ++face) { + if (!_gpuObject.isStoredMipFaceAvailable(sourceMip, face)) { + continue; + } + + // If the mip is less than the max transfer size, then just do it in one transfer + if (glm::all(glm::lessThanEqual(mipDimensions, MAX_TRANSFER_DIMENSIONS))) { + // Can the mip be transferred in one go + _pendingTransfers.emplace(new TransferJob(*this, sourceMip, targetMip, face)); + continue; + } + + // break down the transfers into chunks so that no single transfer is + // consuming more than X bandwidth + auto mipData = _gpuObject.accessStoredMipFace(sourceMip, face); + const auto lines = mipDimensions.y; + auto bytesPerLine = (uint32_t)mipData->getSize() / lines; + Q_ASSERT(0 == (mipData->getSize() % lines)); + uint32_t linesPerTransfer = (uint32_t)(MAX_TRANSFER_SIZE / bytesPerLine); + uint32_t lineOffset = 0; + while (lineOffset < lines) { + uint32_t linesToCopy = std::min(lines - lineOffset, linesPerTransfer); + _pendingTransfers.emplace(new TransferJob(*this, sourceMip, targetMip, face, linesToCopy, lineOffset)); + lineOffset += linesToCopy; + } + } + + // queue up the sampler and populated mip change for after the transfer has completed + _pendingTransfers.emplace(new TransferJob(*this, [=] { + _populatedMip = sourceMip; + syncSampler(); + })); + } while (sourceMip != _allocatedMip); +} + +// Sparsely allocated, managed size resource textures +#if 0 +#define SPARSE_PAGE_SIZE_OVERHEAD_ESTIMATE 1.3f + +using GL45SparseResourceTexture = GL45Backend::GL45SparseResourceTexture; + +GL45Texture::PageDimensionsMap GL45Texture::pageDimensionsByFormat; +Mutex GL45Texture::pageDimensionsMutex; + +GL45Texture::PageDimensions GL45Texture::getPageDimensionsForFormat(const TextureTypeFormat& typeFormat) { + { + Lock lock(pageDimensionsMutex); + if (pageDimensionsByFormat.count(typeFormat)) { + return pageDimensionsByFormat[typeFormat]; + } + } + + GLint count = 0; + glGetInternalformativ(typeFormat.first, typeFormat.second, GL_NUM_VIRTUAL_PAGE_SIZES_ARB, 1, &count); + + std::vector result; + if (count > 0) { + std::vector x, y, z; + x.resize(count); + glGetInternalformativ(typeFormat.first, typeFormat.second, GL_VIRTUAL_PAGE_SIZE_X_ARB, 1, &x[0]); + y.resize(count); + glGetInternalformativ(typeFormat.first, typeFormat.second, GL_VIRTUAL_PAGE_SIZE_Y_ARB, 1, &y[0]); + z.resize(count); + glGetInternalformativ(typeFormat.first, typeFormat.second, GL_VIRTUAL_PAGE_SIZE_Z_ARB, 1, &z[0]); + + result.resize(count); + for (GLint i = 0; i < count; ++i) { + result[i] = uvec3(x[i], y[i], z[i]); + } + } + + { + Lock lock(pageDimensionsMutex); + if (0 == pageDimensionsByFormat.count(typeFormat)) { + pageDimensionsByFormat[typeFormat] = result; + } + } + + return result; +} + +GL45Texture::PageDimensions GL45Texture::getPageDimensionsForFormat(GLenum target, GLenum format) { + return getPageDimensionsForFormat({ target, format }); +} +bool GL45Texture::isSparseEligible(const Texture& texture) { + Q_ASSERT(TextureUsageType::RESOURCE == texture.getUsageType()); + + // Disabling sparse for the momemnt + return false; + + const auto allowedPageDimensions = getPageDimensionsForFormat(getGLTextureType(texture), + gl::GLTexelFormat::evalGLTexelFormatInternal(texture.getTexelFormat())); + const auto textureDimensions = texture.getDimensions(); + for (const auto& pageDimensions : allowedPageDimensions) { + if (uvec3(0) == (textureDimensions % pageDimensions)) { + return true; + } + } + + return false; +} + + +GL45SparseResourceTexture::GL45SparseResourceTexture(const std::weak_ptr& backend, const Texture& texture) : GL45VariableAllocationTexture(backend, texture) { + const GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat()); + const uvec3 dimensions = _gpuObject.getDimensions(); + auto allowedPageDimensions = getPageDimensionsForFormat(_target, texelFormat.internalFormat); + uint32_t pageDimensionsIndex = 0; + // In order to enable sparse the texture size must be an integer multiple of the page size + for (size_t i = 0; i < allowedPageDimensions.size(); ++i) { + pageDimensionsIndex = (uint32_t)i; + _pageDimensions = allowedPageDimensions[i]; + // Is this texture an integer multiple of page dimensions? + if (uvec3(0) == (dimensions % _pageDimensions)) { + qCDebug(gpugl45logging) << "Enabling sparse for texture " << _gpuObject.source().c_str(); + break; + } + } + glTextureParameteri(_id, GL_TEXTURE_SPARSE_ARB, GL_TRUE); + glTextureParameteri(_id, GL_VIRTUAL_PAGE_SIZE_INDEX_ARB, pageDimensionsIndex); + glGetTextureParameterIuiv(_id, GL_NUM_SPARSE_LEVELS_ARB, &_maxSparseLevel); + + _pageBytes = _gpuObject.getTexelFormat().getSize(); + _pageBytes *= _pageDimensions.x * _pageDimensions.y * _pageDimensions.z; + // Testing with a simple texture allocating app shows an estimated 20% GPU memory overhead for + // sparse textures as compared to non-sparse, so we acount for that here. + _pageBytes = (uint32_t)(_pageBytes * SPARSE_PAGE_SIZE_OVERHEAD_ESTIMATE); + + //allocateStorage(); + syncSampler(); +} + +GL45SparseResourceTexture::~GL45SparseResourceTexture() { + Backend::updateTextureGPUVirtualMemoryUsage(size(), 0); +} + +uvec3 GL45SparseResourceTexture::getPageCounts(const uvec3& dimensions) const { + auto result = (dimensions / _pageDimensions) + + glm::clamp(dimensions % _pageDimensions, glm::uvec3(0), glm::uvec3(1)); + return result; +} + +uint32_t GL45SparseResourceTexture::getPageCount(const uvec3& dimensions) const { + auto pageCounts = getPageCounts(dimensions); + return pageCounts.x * pageCounts.y * pageCounts.z; +} + +void GL45SparseResourceTexture::promote() { +} + +void GL45SparseResourceTexture::demote() { +} + +SparseInfo::SparseInfo(GL45Texture& texture) + : texture(texture) { +} + +void SparseInfo::maybeMakeSparse() { + // Don't enable sparse for objects with explicitly managed mip levels + if (!texture._gpuObject.isAutogenerateMips()) { + return; + } + + const uvec3 dimensions = texture._gpuObject.getDimensions(); + auto allowedPageDimensions = getPageDimensionsForFormat(texture._target, texture._internalFormat); + // In order to enable sparse the texture size must be an integer multiple of the page size + for (size_t i = 0; i < allowedPageDimensions.size(); ++i) { + pageDimensionsIndex = (uint32_t)i; + pageDimensions = allowedPageDimensions[i]; + // Is this texture an integer multiple of page dimensions? + if (uvec3(0) == (dimensions % pageDimensions)) { + qCDebug(gpugl45logging) << "Enabling sparse for texture " << texture._source.c_str(); + sparse = true; + break; + } + } + + if (sparse) { + glTextureParameteri(texture._id, GL_TEXTURE_SPARSE_ARB, GL_TRUE); + glTextureParameteri(texture._id, GL_VIRTUAL_PAGE_SIZE_INDEX_ARB, pageDimensionsIndex); + } else { + qCDebug(gpugl45logging) << "Size " << dimensions.x << " x " << dimensions.y << + " is not supported by any sparse page size for texture" << texture._source.c_str(); + } +} + + +// This can only be called after we've established our storage size +void SparseInfo::update() { + if (!sparse) { + return; + } + glGetTextureParameterIuiv(texture._id, GL_NUM_SPARSE_LEVELS_ARB, &maxSparseLevel); + + for (uint16_t mipLevel = 0; mipLevel <= maxSparseLevel; ++mipLevel) { + auto mipDimensions = texture._gpuObject.evalMipDimensions(mipLevel); + auto mipPageCount = getPageCount(mipDimensions); + maxPages += mipPageCount; + } + if (texture._target == GL_TEXTURE_CUBE_MAP) { + maxPages *= GLTexture::CUBE_NUM_FACES; + } +} + + +void SparseInfo::allocateToMip(uint16_t targetMip) { + // Not sparse, do nothing + if (!sparse) { + return; + } + + if (allocatedMip == INVALID_MIP) { + allocatedMip = maxSparseLevel + 1; + } + + // Don't try to allocate below the maximum sparse level + if (targetMip > maxSparseLevel) { + targetMip = maxSparseLevel; + } + + // Already allocated this level + if (allocatedMip <= targetMip) { + return; + } + + uint32_t maxFace = (uint32_t)(GL_TEXTURE_CUBE_MAP == texture._target ? CUBE_NUM_FACES : 1); + for (uint16_t mip = targetMip; mip < allocatedMip; ++mip) { + auto size = texture._gpuObject.evalMipDimensions(mip); + glTexturePageCommitmentEXT(texture._id, mip, 0, 0, 0, size.x, size.y, maxFace, GL_TRUE); + allocatedPages += getPageCount(size); + } + allocatedMip = targetMip; +} + +uint32_t SparseInfo::getSize() const { + return allocatedPages * pageBytes; +} +using SparseInfo = GL45Backend::GL45Texture::SparseInfo; + +void GL45Texture::updateSize() const { + if (_gpuObject.getTexelFormat().isCompressed()) { + qFatal("Compressed textures not yet supported"); + } + + if (_transferrable && _sparseInfo.sparse) { + auto size = _sparseInfo.getSize(); + Backend::updateTextureGPUSparseMemoryUsage(_size, size); + setSize(size); + } else { + setSize(_gpuObject.evalTotalSize(_mipOffset)); + } +} + +void GL45Texture::startTransfer() { + Parent::startTransfer(); + _sparseInfo.update(); + _populatedMip = _maxMip + 1; +} + +bool GL45Texture::continueTransfer() { + size_t maxFace = GL_TEXTURE_CUBE_MAP == _target ? CUBE_NUM_FACES : 1; + if (_populatedMip == _minMip) { + return false; + } + + uint16_t targetMip = _populatedMip - 1; + while (targetMip > 0 && !_gpuObject.isStoredMipFaceAvailable(targetMip)) { + --targetMip; + } + + _sparseInfo.allocateToMip(targetMip); + for (uint8_t face = 0; face < maxFace; ++face) { + auto size = _gpuObject.evalMipDimensions(targetMip); + if (_gpuObject.isStoredMipFaceAvailable(targetMip, face)) { + auto mip = _gpuObject.accessStoredMipFace(targetMip, face); + GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat(), mip->getFormat()); + if (GL_TEXTURE_2D == _target) { + glTextureSubImage2D(_id, targetMip, 0, 0, size.x, size.y, texelFormat.format, texelFormat.type, mip->readData()); + } else if (GL_TEXTURE_CUBE_MAP == _target) { + // DSA ARB does not work on AMD, so use EXT + // unless EXT is not available on the driver + if (glTextureSubImage2DEXT) { + auto target = CUBE_FACE_LAYOUT[face]; + glTextureSubImage2DEXT(_id, target, targetMip, 0, 0, size.x, size.y, texelFormat.format, texelFormat.type, mip->readData()); + } else { + glTextureSubImage3D(_id, targetMip, 0, 0, face, size.x, size.y, 1, texelFormat.format, texelFormat.type, mip->readData()); + } + } else { + Q_ASSERT(false); + } + (void)CHECK_GL_ERROR(); + break; + } + } + _populatedMip = targetMip; + return _populatedMip != _minMip; +} + +void GL45Texture::finishTransfer() { + Parent::finishTransfer(); +} + +void GL45Texture::postTransfer() { + Parent::postTransfer(); +} + +void GL45Texture::stripToMip(uint16_t newMinMip) { + if (newMinMip < _minMip) { + qCWarning(gpugl45logging) << "Cannot decrease the min mip"; + return; + } + + if (_sparseInfo.sparse && newMinMip > _sparseInfo.maxSparseLevel) { + qCWarning(gpugl45logging) << "Cannot increase the min mip into the mip tail"; + return; + } + + // If we weren't generating mips before, we need to now that we're stripping down mip levels. + if (!_gpuObject.isAutogenerateMips()) { + qCDebug(gpugl45logging) << "Force mip generation for texture"; + glGenerateTextureMipmap(_id); + } + + + uint8_t maxFace = (uint8_t)((_target == GL_TEXTURE_CUBE_MAP) ? GLTexture::CUBE_NUM_FACES : 1); + if (_sparseInfo.sparse) { + for (uint16_t mip = _minMip; mip < newMinMip; ++mip) { + auto id = _id; + auto mipDimensions = _gpuObject.evalMipDimensions(mip); + glTexturePageCommitmentEXT(id, mip, 0, 0, 0, mipDimensions.x, mipDimensions.y, maxFace, GL_FALSE); + auto deallocatedPages = _sparseInfo.getPageCount(mipDimensions) * maxFace; + assert(deallocatedPages < _sparseInfo.allocatedPages); + _sparseInfo.allocatedPages -= deallocatedPages; + } + _minMip = newMinMip; + } else { + GLuint oldId = _id; + // Find the distance between the old min mip and the new one + uint16 mipDelta = newMinMip - _minMip; + _mipOffset += mipDelta; + const_cast(_maxMip) -= mipDelta; + auto newLevels = usedMipLevels(); + + // Create and setup the new texture (allocate) + glCreateTextures(_target, 1, &const_cast(_id)); + glTextureParameteri(_id, GL_TEXTURE_BASE_LEVEL, 0); + glTextureParameteri(_id, GL_TEXTURE_MAX_LEVEL, _maxMip - _minMip); + Vec3u newDimensions = _gpuObject.evalMipDimensions(_mipOffset); + glTextureStorage2D(_id, newLevels, _internalFormat, newDimensions.x, newDimensions.y); + + // Copy the contents of the old texture to the new + GLuint fbo { 0 }; + glCreateFramebuffers(1, &fbo); + glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo); + for (uint16 targetMip = _minMip; targetMip <= _maxMip; ++targetMip) { + uint16 sourceMip = targetMip + mipDelta; + Vec3u mipDimensions = _gpuObject.evalMipDimensions(targetMip + _mipOffset); + for (GLenum target : getFaceTargets(_target)) { + glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, target, oldId, sourceMip); + (void)CHECK_GL_ERROR(); + glCopyTextureSubImage2D(_id, targetMip, 0, 0, 0, 0, mipDimensions.x, mipDimensions.y); + (void)CHECK_GL_ERROR(); + } + } + glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); + glDeleteFramebuffers(1, &fbo); + glDeleteTextures(1, &oldId); + } + + // Re-sync the sampler to force access to the new mip level + syncSampler(); + updateSize(); +} + +bool GL45Texture::derezable() const { + if (_external) { + return false; + } + auto maxMinMip = _sparseInfo.sparse ? _sparseInfo.maxSparseLevel : _maxMip; + return _transferrable && (_targetMinMip < maxMinMip); +} + +size_t GL45Texture::getMipByteCount(uint16_t mip) const { + if (!_sparseInfo.sparse) { + return Parent::getMipByteCount(mip); + } + + auto dimensions = _gpuObject.evalMipDimensions(_targetMinMip); + return _sparseInfo.getPageCount(dimensions) * _sparseInfo.pageBytes; +} + +std::pair GL45Texture::preDerez() { + assert(!_sparseInfo.sparse || _targetMinMip < _sparseInfo.maxSparseLevel); + size_t freedMemory = getMipByteCount(_targetMinMip); + bool liveMip = _populatedMip != INVALID_MIP && _populatedMip <= _targetMinMip; + ++_targetMinMip; + return { freedMemory, liveMip }; +} + +void GL45Texture::derez() { + if (_sparseInfo.sparse) { + assert(_minMip < _sparseInfo.maxSparseLevel); + } + assert(_minMip < _maxMip); + assert(_transferrable); + stripToMip(_minMip + 1); +} + +size_t GL45Texture::getCurrentGpuSize() const { + if (!_sparseInfo.sparse) { + return Parent::getCurrentGpuSize(); + } + + return _sparseInfo.getSize(); +} + +size_t GL45Texture::getTargetGpuSize() const { + if (!_sparseInfo.sparse) { + return Parent::getTargetGpuSize(); + } + + size_t result = 0; + for (auto mip = _targetMinMip; mip <= _sparseInfo.maxSparseLevel; ++mip) { + result += (_sparseInfo.pageBytes * _sparseInfo.getPageCount(_gpuObject.evalMipDimensions(mip))); + } + + return result; +} + +GL45Texture::~GL45Texture() { + if (_sparseInfo.sparse) { + uint8_t maxFace = (uint8_t)((_target == GL_TEXTURE_CUBE_MAP) ? GLTexture::CUBE_NUM_FACES : 1); + auto maxSparseMip = std::min(_maxMip, _sparseInfo.maxSparseLevel); + for (uint16_t mipLevel = _minMip; mipLevel <= maxSparseMip; ++mipLevel) { + auto mipDimensions = _gpuObject.evalMipDimensions(mipLevel); + glTexturePageCommitmentEXT(_texture, mipLevel, 0, 0, 0, mipDimensions.x, mipDimensions.y, maxFace, GL_FALSE); + auto deallocatedPages = _sparseInfo.getPageCount(mipDimensions) * maxFace; + assert(deallocatedPages <= _sparseInfo.allocatedPages); + _sparseInfo.allocatedPages -= deallocatedPages; + } + + if (0 != _sparseInfo.allocatedPages) { + qCWarning(gpugl45logging) << "Allocated pages remaining " << _id << " " << _sparseInfo.allocatedPages; + } + Backend::decrementTextureGPUSparseCount(); + } +} +GL45Texture::GL45Texture(const std::weak_ptr& backend, const Texture& texture) + : GLTexture(backend, texture, allocate(texture)), _sparseInfo(*this), _targetMinMip(_minMip) +{ + + auto theBackend = _backend.lock(); + if (_transferrable && theBackend && theBackend->isTextureManagementSparseEnabled()) { + _sparseInfo.maybeMakeSparse(); + if (_sparseInfo.sparse) { + Backend::incrementTextureGPUSparseCount(); + } + } +} +#endif diff --git a/libraries/gpu/CMakeLists.txt b/libraries/gpu/CMakeLists.txt index 384c5709ee..207431d8c7 100644 --- a/libraries/gpu/CMakeLists.txt +++ b/libraries/gpu/CMakeLists.txt @@ -1,6 +1,6 @@ set(TARGET_NAME gpu) autoscribe_shader_lib(gpu) setup_hifi_library() -link_hifi_libraries(shared) +link_hifi_libraries(shared ktx) target_nsight() diff --git a/libraries/gpu/src/gpu/Batch.cpp b/libraries/gpu/src/gpu/Batch.cpp index c15da61800..f822da129b 100644 --- a/libraries/gpu/src/gpu/Batch.cpp +++ b/libraries/gpu/src/gpu/Batch.cpp @@ -292,15 +292,8 @@ void Batch::setUniformBuffer(uint32 slot, const BufferView& view) { setUniformBuffer(slot, view._buffer, view._offset, view._size); } - void Batch::setResourceTexture(uint32 slot, const TexturePointer& texture) { - if (texture && texture->getUsage().isExternal()) { - auto recycler = texture->getExternalRecycler(); - Q_ASSERT(recycler); - } - ADD_COMMAND(setResourceTexture); - _params.emplace_back(_textures.cache(texture)); _params.emplace_back(slot); } diff --git a/libraries/gpu/src/gpu/Buffer.h b/libraries/gpu/src/gpu/Buffer.h index 2507e8e0a6..290b84bef0 100644 --- a/libraries/gpu/src/gpu/Buffer.h +++ b/libraries/gpu/src/gpu/Buffer.h @@ -198,7 +198,7 @@ public: BufferView(const BufferPointer& buffer, Size offset, Size size, const Element& element = DEFAULT_ELEMENT); BufferView(const BufferPointer& buffer, Size offset, Size size, uint16 stride, const Element& element = DEFAULT_ELEMENT); - Size getNumElements() const { return _size / _element.getSize(); } + Size getNumElements() const { return (_size - _offset) / _stride; } //Template iterator with random access on the buffer sysmem template diff --git a/libraries/gpu/src/gpu/Context.cpp b/libraries/gpu/src/gpu/Context.cpp index 78b472bdae..cc570f696f 100644 --- a/libraries/gpu/src/gpu/Context.cpp +++ b/libraries/gpu/src/gpu/Context.cpp @@ -241,6 +241,7 @@ std::atomic Context::_bufferGPUMemoryUsage { 0 }; std::atomic Context::_textureGPUCount{ 0 }; std::atomic Context::_textureGPUSparseCount { 0 }; +std::atomic Context::_textureTransferPendingSize { 0 }; std::atomic Context::_textureGPUMemoryUsage { 0 }; std::atomic Context::_textureGPUVirtualMemoryUsage { 0 }; std::atomic Context::_textureGPUFramebufferMemoryUsage { 0 }; @@ -317,6 +318,17 @@ void Context::decrementTextureGPUSparseCount() { --_textureGPUSparseCount; } +void Context::updateTextureTransferPendingSize(Size prevObjectSize, Size newObjectSize) { + if (prevObjectSize == newObjectSize) { + return; + } + if (newObjectSize > prevObjectSize) { + _textureTransferPendingSize.fetch_add(newObjectSize - prevObjectSize); + } else { + _textureTransferPendingSize.fetch_sub(prevObjectSize - newObjectSize); + } +} + void Context::updateTextureGPUMemoryUsage(Size prevObjectSize, Size newObjectSize) { if (prevObjectSize == newObjectSize) { return; @@ -390,6 +402,10 @@ uint32_t Context::getTextureGPUSparseCount() { return _textureGPUSparseCount.load(); } +Context::Size Context::getTextureTransferPendingSize() { + return _textureTransferPendingSize.load(); +} + Context::Size Context::getTextureGPUMemoryUsage() { return _textureGPUMemoryUsage.load(); } @@ -419,6 +435,7 @@ void Backend::incrementTextureGPUCount() { Context::incrementTextureGPUCount(); void Backend::decrementTextureGPUCount() { Context::decrementTextureGPUCount(); } void Backend::incrementTextureGPUSparseCount() { Context::incrementTextureGPUSparseCount(); } void Backend::decrementTextureGPUSparseCount() { Context::decrementTextureGPUSparseCount(); } +void Backend::updateTextureTransferPendingSize(Resource::Size prevObjectSize, Resource::Size newObjectSize) { Context::updateTextureTransferPendingSize(prevObjectSize, newObjectSize); } void Backend::updateTextureGPUMemoryUsage(Resource::Size prevObjectSize, Resource::Size newObjectSize) { Context::updateTextureGPUMemoryUsage(prevObjectSize, newObjectSize); } void Backend::updateTextureGPUVirtualMemoryUsage(Resource::Size prevObjectSize, Resource::Size newObjectSize) { Context::updateTextureGPUVirtualMemoryUsage(prevObjectSize, newObjectSize); } void Backend::updateTextureGPUFramebufferMemoryUsage(Resource::Size prevObjectSize, Resource::Size newObjectSize) { Context::updateTextureGPUFramebufferMemoryUsage(prevObjectSize, newObjectSize); } diff --git a/libraries/gpu/src/gpu/Context.h b/libraries/gpu/src/gpu/Context.h index 01c841992d..102c754cd7 100644 --- a/libraries/gpu/src/gpu/Context.h +++ b/libraries/gpu/src/gpu/Context.h @@ -101,6 +101,7 @@ public: static void decrementTextureGPUCount(); static void incrementTextureGPUSparseCount(); static void decrementTextureGPUSparseCount(); + static void updateTextureTransferPendingSize(Resource::Size prevObjectSize, Resource::Size newObjectSize); static void updateTextureGPUMemoryUsage(Resource::Size prevObjectSize, Resource::Size newObjectSize); static void updateTextureGPUSparseMemoryUsage(Resource::Size prevObjectSize, Resource::Size newObjectSize); static void updateTextureGPUVirtualMemoryUsage(Resource::Size prevObjectSize, Resource::Size newObjectSize); @@ -220,6 +221,7 @@ public: static uint32_t getTextureGPUSparseCount(); static Size getFreeGPUMemory(); static Size getUsedGPUMemory(); + static Size getTextureTransferPendingSize(); static Size getTextureGPUMemoryUsage(); static Size getTextureGPUVirtualMemoryUsage(); static Size getTextureGPUFramebufferMemoryUsage(); @@ -263,6 +265,7 @@ protected: static void decrementTextureGPUCount(); static void incrementTextureGPUSparseCount(); static void decrementTextureGPUSparseCount(); + static void updateTextureTransferPendingSize(Size prevObjectSize, Size newObjectSize); static void updateTextureGPUMemoryUsage(Size prevObjectSize, Size newObjectSize); static void updateTextureGPUSparseMemoryUsage(Size prevObjectSize, Size newObjectSize); static void updateTextureGPUVirtualMemoryUsage(Size prevObjectSize, Size newObjectSize); @@ -279,6 +282,7 @@ protected: static std::atomic _textureGPUCount; static std::atomic _textureGPUSparseCount; + static std::atomic _textureTransferPendingSize; static std::atomic _textureGPUMemoryUsage; static std::atomic _textureGPUSparseMemoryUsage; static std::atomic _textureGPUVirtualMemoryUsage; diff --git a/libraries/gpu/src/gpu/Format.cpp b/libraries/gpu/src/gpu/Format.cpp index 2a8185bf94..de202911e3 100644 --- a/libraries/gpu/src/gpu/Format.cpp +++ b/libraries/gpu/src/gpu/Format.cpp @@ -10,8 +10,15 @@ using namespace gpu; +const Element Element::COLOR_R_8 { SCALAR, NUINT8, RED }; +const Element Element::COLOR_SR_8 { SCALAR, NUINT8, SRED }; + const Element Element::COLOR_RGBA_32{ VEC4, NUINT8, RGBA }; const Element Element::COLOR_SRGBA_32{ VEC4, NUINT8, SRGBA }; + +const Element Element::COLOR_BGRA_32{ VEC4, NUINT8, BGRA }; +const Element Element::COLOR_SBGRA_32{ VEC4, NUINT8, SBGRA }; + const Element Element::COLOR_R11G11B10{ SCALAR, FLOAT, R11G11B10 }; const Element Element::VEC4F_COLOR_RGBA{ VEC4, FLOAT, RGBA }; const Element Element::VEC2F_UV{ VEC2, FLOAT, UV }; diff --git a/libraries/gpu/src/gpu/Format.h b/libraries/gpu/src/gpu/Format.h index 13809a41e6..493a2de3c2 100644 --- a/libraries/gpu/src/gpu/Format.h +++ b/libraries/gpu/src/gpu/Format.h @@ -133,6 +133,7 @@ static const int SCALAR_COUNT[NUM_DIMENSIONS] = { enum Semantic { RAW = 0, // used as RAW memory + RED, RGB, RGBA, BGRA, @@ -149,6 +150,7 @@ enum Semantic { STENCIL, // Stencil only buffer DEPTH_STENCIL, // Depth Stencil buffer + SRED, SRGB, SRGBA, SBGRA, @@ -227,8 +229,12 @@ public: return getRaw() != right.getRaw(); } + static const Element COLOR_R_8; + static const Element COLOR_SR_8; static const Element COLOR_RGBA_32; static const Element COLOR_SRGBA_32; + static const Element COLOR_BGRA_32; + static const Element COLOR_SBGRA_32; static const Element COLOR_R11G11B10; static const Element VEC4F_COLOR_RGBA; static const Element VEC2F_UV; diff --git a/libraries/gpu/src/gpu/Framebuffer.cpp b/libraries/gpu/src/gpu/Framebuffer.cpp index e8ccfce3b2..0d3291a74d 100755 --- a/libraries/gpu/src/gpu/Framebuffer.cpp +++ b/libraries/gpu/src/gpu/Framebuffer.cpp @@ -32,7 +32,7 @@ Framebuffer* Framebuffer::create(const std::string& name) { Framebuffer* Framebuffer::create(const std::string& name, const Format& colorBufferFormat, uint16 width, uint16 height) { auto framebuffer = Framebuffer::create(name); - auto colorTexture = TexturePointer(Texture::create2D(colorBufferFormat, width, height, Sampler(Sampler::FILTER_MIN_MAG_POINT))); + auto colorTexture = TexturePointer(Texture::createRenderBuffer(colorBufferFormat, width, height, Sampler(Sampler::FILTER_MIN_MAG_POINT))); colorTexture->setSource("Framebuffer::colorTexture"); framebuffer->setRenderBuffer(0, colorTexture); @@ -43,8 +43,8 @@ Framebuffer* Framebuffer::create(const std::string& name, const Format& colorBuf Framebuffer* Framebuffer::create(const std::string& name, const Format& colorBufferFormat, const Format& depthStencilBufferFormat, uint16 width, uint16 height) { auto framebuffer = Framebuffer::create(name); - auto colorTexture = TexturePointer(Texture::create2D(colorBufferFormat, width, height, Sampler(Sampler::FILTER_MIN_MAG_POINT))); - auto depthTexture = TexturePointer(Texture::create2D(depthStencilBufferFormat, width, height, Sampler(Sampler::FILTER_MIN_MAG_POINT))); + auto colorTexture = TexturePointer(Texture::createRenderBuffer(colorBufferFormat, width, height, Sampler(Sampler::FILTER_MIN_MAG_POINT))); + auto depthTexture = TexturePointer(Texture::createRenderBuffer(depthStencilBufferFormat, width, height, Sampler(Sampler::FILTER_MIN_MAG_POINT))); framebuffer->setRenderBuffer(0, colorTexture); framebuffer->setDepthStencilBuffer(depthTexture, depthStencilBufferFormat); @@ -55,7 +55,7 @@ Framebuffer* Framebuffer::createShadowmap(uint16 width) { auto framebuffer = Framebuffer::create("Shadowmap"); auto depthFormat = Element(gpu::SCALAR, gpu::FLOAT, gpu::DEPTH); // Depth32 texel format - auto depthTexture = TexturePointer(Texture::create2D(depthFormat, width, width)); + auto depthTexture = TexturePointer(Texture::createRenderBuffer(depthFormat, width, width)); Sampler::Desc samplerDesc; samplerDesc._borderColor = glm::vec4(1.0f); samplerDesc._wrapModeU = Sampler::WRAP_BORDER; @@ -143,6 +143,8 @@ int Framebuffer::setRenderBuffer(uint32 slot, const TexturePointer& texture, uin return -1; } + Q_ASSERT(!texture || TextureUsageType::RENDERBUFFER == texture->getUsageType()); + // Check for the slot if (slot >= getMaxNumRenderBuffers()) { return -1; @@ -222,6 +224,8 @@ bool Framebuffer::setDepthStencilBuffer(const TexturePointer& texture, const For return false; } + Q_ASSERT(!texture || TextureUsageType::RENDERBUFFER == texture->getUsageType()); + // Check for the compatibility of size if (texture) { if (!validateTargetCompatibility(*texture)) { diff --git a/libraries/gpu/src/gpu/Texture.cpp b/libraries/gpu/src/gpu/Texture.cpp index 5b0c4c876a..1f66b2900e 100755 --- a/libraries/gpu/src/gpu/Texture.cpp +++ b/libraries/gpu/src/gpu/Texture.cpp @@ -19,6 +19,7 @@ #include #include +#include #include #include "GPULogging.h" @@ -88,6 +89,10 @@ uint32_t Texture::getTextureGPUSparseCount() { return Context::getTextureGPUSparseCount(); } +Texture::Size Texture::getTextureTransferPendingSize() { + return Context::getTextureTransferPendingSize(); +} + Texture::Size Texture::getTextureGPUMemoryUsage() { return Context::getTextureGPUMemoryUsage(); } @@ -120,62 +125,23 @@ void Texture::setAllowedGPUMemoryUsage(Size size) { uint8 Texture::NUM_FACES_PER_TYPE[NUM_TYPES] = { 1, 1, 1, 6 }; -Texture::Pixels::Pixels(const Element& format, Size size, const Byte* bytes) : - _format(format), - _sysmem(size, bytes), - _isGPULoaded(false) { - Texture::updateTextureCPUMemoryUsage(0, _sysmem.getSize()); -} +using Storage = Texture::Storage; +using PixelsPointer = Texture::PixelsPointer; +using MemoryStorage = Texture::MemoryStorage; -Texture::Pixels::~Pixels() { - Texture::updateTextureCPUMemoryUsage(_sysmem.getSize(), 0); -} - -Texture::Size Texture::Pixels::resize(Size pSize) { - auto prevSize = _sysmem.getSize(); - auto newSize = _sysmem.resize(pSize); - Texture::updateTextureCPUMemoryUsage(prevSize, newSize); - return newSize; -} - -Texture::Size Texture::Pixels::setData(const Element& format, Size size, const Byte* bytes ) { - _format = format; - auto prevSize = _sysmem.getSize(); - auto newSize = _sysmem.setData(size, bytes); - Texture::updateTextureCPUMemoryUsage(prevSize, newSize); - _isGPULoaded = false; - return newSize; -} - -void Texture::Pixels::notifyGPULoaded() { - _isGPULoaded = true; - auto prevSize = _sysmem.getSize(); - auto newSize = _sysmem.resize(0); - Texture::updateTextureCPUMemoryUsage(prevSize, newSize); -} - -void Texture::Storage::assignTexture(Texture* texture) { +void Storage::assignTexture(Texture* texture) { _texture = texture; if (_texture) { _type = _texture->getType(); } } -void Texture::Storage::reset() { +void MemoryStorage::reset() { _mips.clear(); bumpStamp(); } -Texture::PixelsPointer Texture::Storage::editMipFace(uint16 level, uint8 face) { - if (level < _mips.size()) { - assert(face < _mips[level].size()); - bumpStamp(); - return _mips[level][face]; - } - return PixelsPointer(); -} - -const Texture::PixelsPointer Texture::Storage::getMipFace(uint16 level, uint8 face) const { +PixelsPointer MemoryStorage::getMipFace(uint16 level, uint8 face) const { if (level < _mips.size()) { assert(face < _mips[level].size()); return _mips[level][face]; @@ -183,20 +149,12 @@ const Texture::PixelsPointer Texture::Storage::getMipFace(uint16 level, uint8 fa return PixelsPointer(); } -void Texture::Storage::notifyMipFaceGPULoaded(uint16 level, uint8 face) const { - PixelsPointer mipFace = getMipFace(level, face); - // Free the mips - if (mipFace) { - mipFace->notifyGPULoaded(); - } -} - -bool Texture::Storage::isMipAvailable(uint16 level, uint8 face) const { +bool MemoryStorage::isMipAvailable(uint16 level, uint8 face) const { PixelsPointer mipFace = getMipFace(level, face); return (mipFace && mipFace->getSize()); } -bool Texture::Storage::allocateMip(uint16 level) { +bool MemoryStorage::allocateMip(uint16 level) { bool changed = false; if (level >= _mips.size()) { _mips.resize(level+1, std::vector(Texture::NUM_FACES_PER_TYPE[getType()])); @@ -206,7 +164,6 @@ bool Texture::Storage::allocateMip(uint16 level) { auto& mip = _mips[level]; for (auto& face : mip) { if (!face) { - face = std::make_shared(); changed = true; } } @@ -216,7 +173,7 @@ bool Texture::Storage::allocateMip(uint16 level) { return changed; } -bool Texture::Storage::assignMipData(uint16 level, const Element& format, Size size, const Byte* bytes) { +void MemoryStorage::assignMipData(uint16 level, const storage::StoragePointer& storagePointer) { allocateMip(level); auto& mip = _mips[level]; @@ -225,64 +182,63 @@ bool Texture::Storage::assignMipData(uint16 level, const Element& format, Size s // The bytes assigned here are supposed to contain all the faces bytes of the mip. // For tex1D, 2D, 3D there is only one face // For Cube, we expect the 6 faces in the order X+, X-, Y+, Y-, Z+, Z- - auto sizePerFace = size / mip.size(); - auto faceBytes = bytes; - Size allocated = 0; + auto sizePerFace = storagePointer->size() / mip.size(); + size_t offset = 0; for (auto& face : mip) { - allocated += face->setData(format, sizePerFace, faceBytes); - faceBytes += sizePerFace; + face = storagePointer->createView(sizePerFace, offset); + offset += sizePerFace; } bumpStamp(); - - return allocated == size; } -bool Texture::Storage::assignMipFaceData(uint16 level, const Element& format, Size size, const Byte* bytes, uint8 face) { - +void Texture::MemoryStorage::assignMipFaceData(uint16 level, uint8 face, const storage::StoragePointer& storagePointer) { allocateMip(level); - auto mip = _mips[level]; - Size allocated = 0; + auto& mip = _mips[level]; if (face < mip.size()) { - auto mipFace = mip[face]; - allocated += mipFace->setData(format, size, bytes); + mip[face] = storagePointer; bumpStamp(); } - - return allocated == size; } -Texture* Texture::createExternal2D(const ExternalRecycler& recycler, const Sampler& sampler) { - Texture* tex = new Texture(); +Texture* Texture::createExternal(const ExternalRecycler& recycler, const Sampler& sampler) { + Texture* tex = new Texture(TextureUsageType::EXTERNAL); tex->_type = TEX_2D; tex->_maxMip = 0; tex->_sampler = sampler; - tex->setUsage(Usage::Builder().withExternal().withColor()); tex->setExternalRecycler(recycler); return tex; } +Texture* Texture::createRenderBuffer(const Element& texelFormat, uint16 width, uint16 height, const Sampler& sampler) { + return create(TextureUsageType::RENDERBUFFER, TEX_2D, texelFormat, width, height, 1, 1, 0, sampler); +} + Texture* Texture::create1D(const Element& texelFormat, uint16 width, const Sampler& sampler) { - return create(TEX_1D, texelFormat, width, 1, 1, 1, 1, sampler); + return create(TextureUsageType::RESOURCE, TEX_1D, texelFormat, width, 1, 1, 1, 0, sampler); } Texture* Texture::create2D(const Element& texelFormat, uint16 width, uint16 height, const Sampler& sampler) { - return create(TEX_2D, texelFormat, width, height, 1, 1, 1, sampler); + return create(TextureUsageType::RESOURCE, TEX_2D, texelFormat, width, height, 1, 1, 0, sampler); +} + +Texture* Texture::createStrict(const Element& texelFormat, uint16 width, uint16 height, const Sampler& sampler) { + return create(TextureUsageType::STRICT_RESOURCE, TEX_2D, texelFormat, width, height, 1, 1, 0, sampler); } Texture* Texture::create3D(const Element& texelFormat, uint16 width, uint16 height, uint16 depth, const Sampler& sampler) { - return create(TEX_3D, texelFormat, width, height, depth, 1, 1, sampler); + return create(TextureUsageType::RESOURCE, TEX_3D, texelFormat, width, height, depth, 1, 0, sampler); } Texture* Texture::createCube(const Element& texelFormat, uint16 width, const Sampler& sampler) { - return create(TEX_CUBE, texelFormat, width, width, 1, 1, 1, sampler); + return create(TextureUsageType::RESOURCE, TEX_CUBE, texelFormat, width, width, 1, 1, 0, sampler); } -Texture* Texture::create(Type type, const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numSamples, uint16 numSlices, const Sampler& sampler) +Texture* Texture::create(TextureUsageType usageType, Type type, const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numSamples, uint16 numSlices, const Sampler& sampler) { - Texture* tex = new Texture(); - tex->_storage.reset(new Storage()); + Texture* tex = new Texture(usageType); + tex->_storage.reset(new MemoryStorage()); tex->_type = type; tex->_storage->assignTexture(tex); tex->_maxMip = 0; @@ -293,16 +249,14 @@ Texture* Texture::create(Type type, const Element& texelFormat, uint16 width, ui return tex; } -Texture::Texture(): - Resource() -{ +Texture::Texture(TextureUsageType usageType) : + Resource(), _usageType(usageType) { _textureCPUCount++; } -Texture::~Texture() -{ +Texture::~Texture() { _textureCPUCount--; - if (getUsage().isExternal()) { + if (_usageType == TextureUsageType::EXTERNAL) { Texture::ExternalUpdates externalUpdates; { Lock lock(_externalMutex); @@ -321,7 +275,7 @@ Texture::~Texture() } Texture::Size Texture::resize(Type type, const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numSamples, uint16 numSlices) { - if (width && height && depth && numSamples && numSlices) { + if (width && height && depth && numSamples) { bool changed = false; if ( _type != type) { @@ -382,20 +336,20 @@ Texture::Size Texture::resize(Type type, const Element& texelFormat, uint16 widt } Texture::Size Texture::resize1D(uint16 width, uint16 numSamples) { - return resize(TEX_1D, getTexelFormat(), width, 1, 1, numSamples, 1); + return resize(TEX_1D, getTexelFormat(), width, 1, 1, numSamples, 0); } Texture::Size Texture::resize2D(uint16 width, uint16 height, uint16 numSamples) { - return resize(TEX_2D, getTexelFormat(), width, height, 1, numSamples, 1); + return resize(TEX_2D, getTexelFormat(), width, height, 1, numSamples, 0); } Texture::Size Texture::resize3D(uint16 width, uint16 height, uint16 depth, uint16 numSamples) { - return resize(TEX_3D, getTexelFormat(), width, height, depth, numSamples, 1); + return resize(TEX_3D, getTexelFormat(), width, height, depth, numSamples, 0); } Texture::Size Texture::resizeCube(uint16 width, uint16 numSamples) { - return resize(TEX_CUBE, getTexelFormat(), width, 1, 1, numSamples, 1); + return resize(TEX_CUBE, getTexelFormat(), width, 1, 1, numSamples, 0); } Texture::Size Texture::reformat(const Element& texelFormat) { - return resize(_type, texelFormat, getWidth(), getHeight(), getDepth(), getNumSamples(), getNumSlices()); + return resize(_type, texelFormat, getWidth(), getHeight(), getDepth(), getNumSamples(), _numSlices); } bool Texture::isColorRenderTarget() const { @@ -426,69 +380,83 @@ uint16 Texture::evalNumMips() const { return evalNumMips({ _width, _height, _depth }); } -bool Texture::assignStoredMip(uint16 level, const Element& format, Size size, const Byte* bytes) { +void Texture::setStoredMipFormat(const Element& format) { + _storage->setFormat(format); +} + +const Element& Texture::getStoredMipFormat() const { + return _storage->getFormat(); +} + +void Texture::assignStoredMip(uint16 level, Size size, const Byte* bytes) { + storage::StoragePointer storage = std::make_shared(size, bytes); + assignStoredMip(level, storage); +} + +void Texture::assignStoredMipFace(uint16 level, uint8 face, Size size, const Byte* bytes) { + storage::StoragePointer storage = std::make_shared(size, bytes); + assignStoredMipFace(level, face, storage); +} + +void Texture::assignStoredMip(uint16 level, storage::StoragePointer& storage) { // Check that level accessed make sense if (level != 0) { if (_autoGenerateMips) { - return false; + return; } if (level >= evalNumMips()) { - return false; + return; } } // THen check that the mem texture passed make sense with its format - Size expectedSize = evalStoredMipSize(level, format); - if (size == expectedSize) { - _storage->assignMipData(level, format, size, bytes); + Size expectedSize = evalStoredMipSize(level, getStoredMipFormat()); + auto size = storage->size(); + if (storage->size() == expectedSize) { + _storage->assignMipData(level, storage); _maxMip = std::max(_maxMip, level); _stamp++; - return true; } else if (size > expectedSize) { // NOTE: We are facing this case sometime because apparently QImage (from where we get the bits) is generating images // and alligning the line of pixels to 32 bits. // We should probably consider something a bit more smart to get the correct result but for now (UI elements) // it seems to work... - _storage->assignMipData(level, format, size, bytes); + _storage->assignMipData(level, storage); _maxMip = std::max(_maxMip, level); _stamp++; - return true; } - - return false; } - -bool Texture::assignStoredMipFace(uint16 level, const Element& format, Size size, const Byte* bytes, uint8 face) { +void Texture::assignStoredMipFace(uint16 level, uint8 face, storage::StoragePointer& storage) { // Check that level accessed make sense if (level != 0) { if (_autoGenerateMips) { - return false; + return; } if (level >= evalNumMips()) { - return false; + return; } } // THen check that the mem texture passed make sense with its format - Size expectedSize = evalStoredMipFaceSize(level, format); + Size expectedSize = evalStoredMipFaceSize(level, getStoredMipFormat()); + auto size = storage->size(); if (size == expectedSize) { - _storage->assignMipFaceData(level, format, size, bytes, face); + _storage->assignMipFaceData(level, face, storage); + _maxMip = std::max(_maxMip, level); _stamp++; - return true; } else if (size > expectedSize) { // NOTE: We are facing this case sometime because apparently QImage (from where we get the bits) is generating images // and alligning the line of pixels to 32 bits. // We should probably consider something a bit more smart to get the correct result but for now (UI elements) // it seems to work... - _storage->assignMipFaceData(level, format, size, bytes, face); + _storage->assignMipFaceData(level, face, storage); + _maxMip = std::max(_maxMip, level); _stamp++; - return true; } - - return false; } + uint16 Texture::autoGenerateMips(uint16 maxMip) { bool changed = false; if (!_autoGenerateMips) { @@ -522,7 +490,7 @@ uint16 Texture::getStoredMipHeight(uint16 level) const { if (mip && mip->getSize()) { return evalMipHeight(level); } - return 0; + return 0; } uint16 Texture::getStoredMipDepth(uint16 level) const { @@ -794,7 +762,16 @@ bool sphericalHarmonicsFromTexture(const gpu::Texture& cubeTexture, std::vector< for(int face=0; face < gpu::Texture::NUM_CUBE_FACES; face++) { PROFILE_RANGE(render_gpu, "ProcessFace"); - auto numComponents = cubeTexture.accessStoredMipFace(0,face)->getFormat().getScalarCount(); + auto mipFormat = cubeTexture.getStoredMipFormat(); + auto numComponents = mipFormat.getScalarCount(); + int roffset { 0 }; + int goffset { 1 }; + int boffset { 2 }; + if ((mipFormat.getSemantic() == gpu::BGRA) || (mipFormat.getSemantic() == gpu::SBGRA)) { + roffset = 2; + boffset = 0; + } + auto data = cubeTexture.accessStoredMipFace(0,face)->readData(); if (data == nullptr) { continue; @@ -882,9 +859,9 @@ bool sphericalHarmonicsFromTexture(const gpu::Texture& cubeTexture, std::vector< for (int i = 0; i < stride; ++i) { for (int j = 0; j < stride; ++j) { int k = (int)(x + i - halfStride + (y + j - halfStride) * width) * numComponents; - red += ColorUtils::sRGB8ToLinearFloat(data[k]); - green += ColorUtils::sRGB8ToLinearFloat(data[k + 1]); - blue += ColorUtils::sRGB8ToLinearFloat(data[k + 2]); + red += ColorUtils::sRGB8ToLinearFloat(data[k + roffset]); + green += ColorUtils::sRGB8ToLinearFloat(data[k + goffset]); + blue += ColorUtils::sRGB8ToLinearFloat(data[k + boffset]); } } glm::vec3 clr(red, green, blue); @@ -911,8 +888,6 @@ bool sphericalHarmonicsFromTexture(const gpu::Texture& cubeTexture, std::vector< // save result for(uint i=0; i < sqOrder; i++) { - // gamma Correct - // output[i] = linearTosRGB(glm::vec3(resultR[i], resultG[i], resultB[i])); output[i] = glm::vec3(resultR[i], resultG[i], resultB[i]); } @@ -1001,3 +976,7 @@ Texture::ExternalUpdates Texture::getUpdates() const { } return result; } + +void Texture::setStorage(std::unique_ptr& newStorage) { + _storage.swap(newStorage); +} diff --git a/libraries/gpu/src/gpu/Texture.h b/libraries/gpu/src/gpu/Texture.h index 856bd4983d..f7297b3280 100755 --- a/libraries/gpu/src/gpu/Texture.h +++ b/libraries/gpu/src/gpu/Texture.h @@ -17,9 +17,17 @@ #include #include +#include + #include "Forward.h" #include "Resource.h" +namespace ktx { + class KTX; + using KTXUniquePointer = std::unique_ptr; + struct Header; +} + namespace gpu { // THe spherical harmonics is a nice tool for cubemap, so if required, the irradiance SH can be automatically generated @@ -135,10 +143,18 @@ public: uint8 getMinMip() const { return _desc._minMip; } uint8 getMaxMip() const { return _desc._maxMip; } + const Desc& getDesc() const { return _desc; } protected: Desc _desc; }; +enum class TextureUsageType { + RENDERBUFFER, // Used as attachments to a framebuffer + RESOURCE, // Resource textures, like materials... subject to memory manipulation + STRICT_RESOURCE, // Resource textures not subject to manipulation, like the normal fitting texture + EXTERNAL, +}; + class Texture : public Resource { static std::atomic _textureCPUCount; static std::atomic _textureCPUMemoryUsage; @@ -147,10 +163,12 @@ class Texture : public Resource { static void updateTextureCPUMemoryUsage(Size prevObjectSize, Size newObjectSize); public: + static const uint32_t CUBE_FACE_COUNT { 6 }; static uint32_t getTextureCPUCount(); static Size getTextureCPUMemoryUsage(); static uint32_t getTextureGPUCount(); static uint32_t getTextureGPUSparseCount(); + static Size getTextureTransferPendingSize(); static Size getTextureGPUMemoryUsage(); static Size getTextureGPUVirtualMemoryUsage(); static Size getTextureGPUFramebufferMemoryUsage(); @@ -173,9 +191,9 @@ public: NORMAL, // Texture is a normal map ALPHA, // Texture has an alpha channel ALPHA_MASK, // Texture alpha channel is a Mask 0/1 - EXTERNAL, NUM_FLAGS, }; + typedef std::bitset Flags; // The key is the Flags @@ -199,7 +217,6 @@ public: Builder& withNormal() { _flags.set(NORMAL); return (*this); } Builder& withAlpha() { _flags.set(ALPHA); return (*this); } Builder& withAlphaMask() { _flags.set(ALPHA_MASK); return (*this); } - Builder& withExternal() { _flags.set(EXTERNAL); return (*this); } }; Usage(const Builder& builder) : Usage(builder._flags) {} @@ -208,37 +225,12 @@ public: bool isAlpha() const { return _flags[ALPHA]; } bool isAlphaMask() const { return _flags[ALPHA_MASK]; } - bool isExternal() const { return _flags[EXTERNAL]; } - bool operator==(const Usage& usage) { return (_flags == usage._flags); } bool operator!=(const Usage& usage) { return (_flags != usage._flags); } }; - class Pixels { - public: - Pixels() {} - Pixels(const Pixels& pixels) = default; - Pixels(const Element& format, Size size, const Byte* bytes); - ~Pixels(); - - const Byte* readData() const { return _sysmem.readData(); } - Size getSize() const { return _sysmem.getSize(); } - Size resize(Size pSize); - Size setData(const Element& format, Size size, const Byte* bytes ); - - const Element& getFormat() const { return _format; } - - void notifyGPULoaded(); - - protected: - Element _format; - Sysmem _sysmem; - bool _isGPULoaded; - - friend class Texture; - }; - typedef std::shared_ptr< Pixels > PixelsPointer; + using PixelsPointer = storage::StoragePointer; enum Type { TEX_1D = 0, @@ -261,46 +253,78 @@ public: NUM_CUBE_FACES, // Not a valid vace index }; + class Storage { public: Storage() {} virtual ~Storage() {} - virtual void reset(); - virtual PixelsPointer editMipFace(uint16 level, uint8 face = 0); - virtual const PixelsPointer getMipFace(uint16 level, uint8 face = 0) const; - virtual bool allocateMip(uint16 level); - virtual bool assignMipData(uint16 level, const Element& format, Size size, const Byte* bytes); - virtual bool assignMipFaceData(uint16 level, const Element& format, Size size, const Byte* bytes, uint8 face); - virtual bool isMipAvailable(uint16 level, uint8 face = 0) const; + virtual void reset() = 0; + virtual PixelsPointer getMipFace(uint16 level, uint8 face = 0) const = 0; + virtual void assignMipData(uint16 level, const storage::StoragePointer& storage) = 0; + virtual void assignMipFaceData(uint16 level, uint8 face, const storage::StoragePointer& storage) = 0; + virtual bool isMipAvailable(uint16 level, uint8 face = 0) const = 0; Texture::Type getType() const { return _type; } - + Stamp getStamp() const { return _stamp; } Stamp bumpStamp() { return ++_stamp; } - protected: - Stamp _stamp = 0; - Texture* _texture = nullptr; // Points to the parent texture (not owned) - Texture::Type _type = Texture::TEX_2D; // The type of texture is needed to know the number of faces to expect - std::vector> _mips; // an array of mips, each mip is an array of faces + void setFormat(const Element& format) { _format = format; } + const Element& getFormat() const { return _format; } + + private: + Stamp _stamp { 0 }; + Element _format; + Texture::Type _type { Texture::TEX_2D }; // The type of texture is needed to know the number of faces to expect + Texture* _texture { nullptr }; // Points to the parent texture (not owned) virtual void assignTexture(Texture* tex); // Texture storage is pointing to ONE corrresponding Texture. const Texture* getTexture() const { return _texture; } - friend class Texture; - - // THis should be only called by the Texture from the Backend to notify the storage that the specified mip face pixels - // have been uploaded to the GPU memory. IT is possible for the storage to free the system memory then - virtual void notifyMipFaceGPULoaded(uint16 level, uint8 face) const; }; - + class MemoryStorage : public Storage { + public: + void reset() override; + PixelsPointer getMipFace(uint16 level, uint8 face = 0) const override; + void assignMipData(uint16 level, const storage::StoragePointer& storage) override; + void assignMipFaceData(uint16 level, uint8 face, const storage::StoragePointer& storage) override; + bool isMipAvailable(uint16 level, uint8 face = 0) const override; + + protected: + bool allocateMip(uint16 level); + std::vector> _mips; // an array of mips, each mip is an array of faces + }; + + class KtxStorage : public Storage { + public: + KtxStorage(ktx::KTXUniquePointer& ktxData); + PixelsPointer getMipFace(uint16 level, uint8 face = 0) const override; + // By convention, all mip levels and faces MUST be populated when using KTX backing + bool isMipAvailable(uint16 level, uint8 face = 0) const override { return true; } + + void assignMipData(uint16 level, const storage::StoragePointer& storage) override { + throw std::runtime_error("Invalid call"); + } + + void assignMipFaceData(uint16 level, uint8 face, const storage::StoragePointer& storage) override { + throw std::runtime_error("Invalid call"); + } + void reset() override { } + + protected: + ktx::KTXUniquePointer _ktxData; + friend class Texture; + }; + static Texture* create1D(const Element& texelFormat, uint16 width, const Sampler& sampler = Sampler()); static Texture* create2D(const Element& texelFormat, uint16 width, uint16 height, const Sampler& sampler = Sampler()); static Texture* create3D(const Element& texelFormat, uint16 width, uint16 height, uint16 depth, const Sampler& sampler = Sampler()); static Texture* createCube(const Element& texelFormat, uint16 width, const Sampler& sampler = Sampler()); - static Texture* createExternal2D(const ExternalRecycler& recycler, const Sampler& sampler = Sampler()); + static Texture* createRenderBuffer(const Element& texelFormat, uint16 width, uint16 height, const Sampler& sampler = Sampler()); + static Texture* createStrict(const Element& texelFormat, uint16 width, uint16 height, const Sampler& sampler = Sampler()); + static Texture* createExternal(const ExternalRecycler& recycler, const Sampler& sampler = Sampler()); - Texture(); + Texture(TextureUsageType usageType); Texture(const Texture& buf); // deep copy of the sysmem texture Texture& operator=(const Texture& buf); // deep copy of the sysmem texture ~Texture(); @@ -325,6 +349,7 @@ public: // Size and format Type getType() const { return _type; } + TextureUsageType getUsageType() const { return _usageType; } bool isColorRenderTarget() const; bool isDepthStencilRenderTarget() const; @@ -347,7 +372,12 @@ public: uint32 getNumTexels() const { return _width * _height * _depth * getNumFaces(); } - uint16 getNumSlices() const { return _numSlices; } + // The texture is an array if the _numSlices is not 0. + // otherwise, if _numSLices is 0, then the texture is NOT an array + // The number of slices returned is 1 at the minimum (if not an array) or the actual _numSlices. + bool isArray() const { return _numSlices > 0; } + uint16 getNumSlices() const { return (isArray() ? _numSlices : 1); } + uint16 getNumSamples() const { return _numSamples; } @@ -429,18 +459,29 @@ public: // Managing Storage and mips + // Mip storage format is constant across all mips + void setStoredMipFormat(const Element& format); + const Element& getStoredMipFormat() const; + // Manually allocate the mips down until the specified maxMip // this is just allocating the sysmem version of it // in case autoGen is on, this doesn't allocate // Explicitely assign mip data for a certain level // If Bytes is NULL then simply allocate the space so mip sysmem can be accessed - bool assignStoredMip(uint16 level, const Element& format, Size size, const Byte* bytes); - bool assignStoredMipFace(uint16 level, const Element& format, Size size, const Byte* bytes, uint8 face); + + void assignStoredMip(uint16 level, Size size, const Byte* bytes); + void assignStoredMipFace(uint16 level, uint8 face, Size size, const Byte* bytes); + + void assignStoredMip(uint16 level, storage::StoragePointer& storage); + void assignStoredMipFace(uint16 level, uint8 face, storage::StoragePointer& storage); // Access the the sub mips bool isStoredMipFaceAvailable(uint16 level, uint8 face = 0) const { return _storage->isMipAvailable(level, face); } const PixelsPointer accessStoredMipFace(uint16 level, uint8 face = 0) const { return _storage->getMipFace(level, face); } + void setStorage(std::unique_ptr& newStorage); + void setKtxBacking(ktx::KTXUniquePointer& newBacking); + // access sizes for the stored mips uint16 getStoredMipWidth(uint16 level) const; uint16 getStoredMipHeight(uint16 level) const; @@ -464,8 +505,8 @@ public: const Sampler& getSampler() const { return _sampler; } Stamp getSamplerStamp() const { return _samplerStamp; } - // Only callable by the Backend - void notifyMipFaceGPULoaded(uint16 level, uint8 face = 0) const { return _storage->notifyMipFaceGPULoaded(level, face); } + void setFallbackTexture(const TexturePointer& fallback) { _fallback = fallback; } + TexturePointer getFallbackTexture() const { return _fallback.lock(); } void setExternalTexture(uint32 externalId, void* externalFence); void setExternalRecycler(const ExternalRecycler& recycler); @@ -475,36 +516,45 @@ public: ExternalUpdates getUpdates() const; + // Textures can be serialized directly to ktx data file, here is how + static ktx::KTXUniquePointer serialize(const Texture& texture); + static Texture* unserialize(const ktx::KTXUniquePointer& srcData, TextureUsageType usageType = TextureUsageType::RESOURCE, Usage usage = Usage(), const Sampler::Desc& sampler = Sampler::Desc()); + static bool evalKTXFormat(const Element& mipFormat, const Element& texelFormat, ktx::Header& header); + static bool evalTextureFormat(const ktx::Header& header, Element& mipFormat, Element& texelFormat); + protected: + const TextureUsageType _usageType; + // Should only be accessed internally or by the backend sync function mutable Mutex _externalMutex; mutable std::list _externalUpdates; ExternalRecycler _externalRecycler; + std::weak_ptr _fallback; // Not strictly necessary, but incredibly useful for debugging std::string _source; std::unique_ptr< Storage > _storage; - Stamp _stamp = 0; + Stamp _stamp { 0 }; Sampler _sampler; - Stamp _samplerStamp; + Stamp _samplerStamp { 0 }; - uint32 _size = 0; + uint32 _size { 0 }; Element _texelFormat; - uint16 _width = 1; - uint16 _height = 1; - uint16 _depth = 1; + uint16 _width { 1 }; + uint16 _height { 1 }; + uint16 _depth { 1 }; - uint16 _numSamples = 1; - uint16 _numSlices = 1; + uint16 _numSamples { 1 }; + uint16 _numSlices { 0 }; // if _numSlices is 0, the texture is not an "Array", the getNumSlices reported is 1 uint16 _maxMip { 0 }; uint16 _minMip { 0 }; - Type _type = TEX_1D; + Type _type { TEX_1D }; Usage _usage; @@ -513,7 +563,7 @@ protected: bool _isIrradianceValid = false; bool _defined = false; - static Texture* create(Type type, const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numSamples, uint16 numSlices, const Sampler& sampler); + static Texture* create(TextureUsageType usageType, Type type, const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numSamples, uint16 numSlices, const Sampler& sampler); Size resize(Type type, const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numSamples, uint16 numSlices); }; diff --git a/libraries/gpu/src/gpu/Texture_ktx.cpp b/libraries/gpu/src/gpu/Texture_ktx.cpp new file mode 100644 index 0000000000..5f0ededee7 --- /dev/null +++ b/libraries/gpu/src/gpu/Texture_ktx.cpp @@ -0,0 +1,289 @@ +// +// Texture_ktx.cpp +// libraries/gpu/src/gpu +// +// Created by Sam Gateau on 2/16/2017. +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + + +#include "Texture.h" + +#include +using namespace gpu; + +using PixelsPointer = Texture::PixelsPointer; +using KtxStorage = Texture::KtxStorage; + +struct GPUKTXPayload { + Sampler::Desc _samplerDesc; + Texture::Usage _usage; + TextureUsageType _usageType; + + static std::string KEY; + static bool isGPUKTX(const ktx::KeyValue& val) { + return (val._key.compare(KEY) == 0); + } + + static bool findInKeyValues(const ktx::KeyValues& keyValues, GPUKTXPayload& payload) { + auto found = std::find_if(keyValues.begin(), keyValues.end(), isGPUKTX); + if (found != keyValues.end()) { + if ((*found)._value.size() == sizeof(GPUKTXPayload)) { + memcpy(&payload, (*found)._value.data(), sizeof(GPUKTXPayload)); + return true; + } + } + return false; + } +}; + +std::string GPUKTXPayload::KEY { "hifi.gpu" }; + +KtxStorage::KtxStorage(ktx::KTXUniquePointer& ktxData) { + + // if the source ktx is valid let's config this KtxStorage correctly + if (ktxData && ktxData->getHeader()) { + + // now that we know the ktx, let's get the header info to configure this Texture::Storage: + Format mipFormat = Format::COLOR_BGRA_32; + Format texelFormat = Format::COLOR_SRGBA_32; + if (Texture::evalTextureFormat(*ktxData->getHeader(), mipFormat, texelFormat)) { + _format = mipFormat; + } + + + } + + _ktxData.reset(ktxData.release()); +} + +PixelsPointer KtxStorage::getMipFace(uint16 level, uint8 face) const { + return _ktxData->getMipFaceTexelsData(level, face); +} + +void Texture::setKtxBacking(ktx::KTXUniquePointer& ktxBacking) { + auto newBacking = std::unique_ptr(new KtxStorage(ktxBacking)); + setStorage(newBacking); +} + +ktx::KTXUniquePointer Texture::serialize(const Texture& texture) { + ktx::Header header; + + // From texture format to ktx format description + auto texelFormat = texture.getTexelFormat(); + auto mipFormat = texture.getStoredMipFormat(); + + if (!Texture::evalKTXFormat(mipFormat, texelFormat, header)) { + return nullptr; + } + + // Set Dimensions + uint32_t numFaces = 1; + switch (texture.getType()) { + case TEX_1D: { + if (texture.isArray()) { + header.set1DArray(texture.getWidth(), texture.getNumSlices()); + } else { + header.set1D(texture.getWidth()); + } + break; + } + case TEX_2D: { + if (texture.isArray()) { + header.set2DArray(texture.getWidth(), texture.getHeight(), texture.getNumSlices()); + } else { + header.set2D(texture.getWidth(), texture.getHeight()); + } + break; + } + case TEX_3D: { + if (texture.isArray()) { + header.set3DArray(texture.getWidth(), texture.getHeight(), texture.getDepth(), texture.getNumSlices()); + } else { + header.set3D(texture.getWidth(), texture.getHeight(), texture.getDepth()); + } + break; + } + case TEX_CUBE: { + if (texture.isArray()) { + header.setCubeArray(texture.getWidth(), texture.getHeight(), texture.getNumSlices()); + } else { + header.setCube(texture.getWidth(), texture.getHeight()); + } + numFaces = Texture::CUBE_FACE_COUNT; + break; + } + default: + return nullptr; + } + + // Number level of mips coming + header.numberOfMipmapLevels = texture.maxMip() + 1; + + ktx::Images images; + for (uint32_t level = 0; level < header.numberOfMipmapLevels; level++) { + auto mip = texture.accessStoredMipFace(level); + if (mip) { + if (numFaces == 1) { + images.emplace_back(ktx::Image((uint32_t)mip->getSize(), 0, mip->readData())); + } else { + ktx::Image::FaceBytes cubeFaces(Texture::CUBE_FACE_COUNT); + cubeFaces[0] = mip->readData(); + for (uint32_t face = 1; face < Texture::CUBE_FACE_COUNT; face++) { + cubeFaces[face] = texture.accessStoredMipFace(level, face)->readData(); + } + images.emplace_back(ktx::Image((uint32_t)mip->getSize(), 0, cubeFaces)); + } + } + } + + GPUKTXPayload keyval; + keyval._samplerDesc = texture.getSampler().getDesc(); + keyval._usage = texture.getUsage(); + keyval._usageType = texture.getUsageType(); + ktx::KeyValues keyValues; + keyValues.emplace_back(ktx::KeyValue(GPUKTXPayload::KEY, sizeof(GPUKTXPayload), (ktx::Byte*) &keyval)); + + auto ktxBuffer = ktx::KTX::create(header, images, keyValues); +#if 0 + auto expectedMipCount = texture.evalNumMips(); + assert(expectedMipCount == ktxBuffer->_images.size()); + assert(expectedMipCount == header.numberOfMipmapLevels); + + assert(0 == memcmp(&header, ktxBuffer->getHeader(), sizeof(ktx::Header))); + assert(ktxBuffer->_images.size() == images.size()); + auto start = ktxBuffer->_storage->data(); + for (size_t i = 0; i < images.size(); ++i) { + auto expected = images[i]; + auto actual = ktxBuffer->_images[i]; + assert(expected._padding == actual._padding); + assert(expected._numFaces == actual._numFaces); + assert(expected._imageSize == actual._imageSize); + assert(expected._faceSize == actual._faceSize); + assert(actual._faceBytes.size() == actual._numFaces); + for (uint32_t face = 0; face < expected._numFaces; ++face) { + auto expectedFace = expected._faceBytes[face]; + auto actualFace = actual._faceBytes[face]; + auto offset = actualFace - start; + assert(offset % 4 == 0); + assert(expectedFace != actualFace); + assert(0 == memcmp(expectedFace, actualFace, expected._faceSize)); + } + } +#endif + return ktxBuffer; +} + +Texture* Texture::unserialize(const ktx::KTXUniquePointer& srcData, TextureUsageType usageType, Usage usage, const Sampler::Desc& sampler) { + if (!srcData) { + return nullptr; + } + const auto& header = *srcData->getHeader(); + + Format mipFormat = Format::COLOR_BGRA_32; + Format texelFormat = Format::COLOR_SRGBA_32; + + if (!Texture::evalTextureFormat(header, mipFormat, texelFormat)) { + return nullptr; + } + + // Find Texture Type based on dimensions + Type type = TEX_1D; + if (header.pixelWidth == 0) { + return nullptr; + } else if (header.pixelHeight == 0) { + type = TEX_1D; + } else if (header.pixelDepth == 0) { + if (header.numberOfFaces == ktx::NUM_CUBEMAPFACES) { + type = TEX_CUBE; + } else { + type = TEX_2D; + } + } else { + type = TEX_3D; + } + + + // If found, use the + GPUKTXPayload gpuktxKeyValue; + bool isGPUKTXPayload = GPUKTXPayload::findInKeyValues(srcData->_keyValues, gpuktxKeyValue); + + auto tex = Texture::create( (isGPUKTXPayload ? gpuktxKeyValue._usageType : usageType), + type, + texelFormat, + header.getPixelWidth(), + header.getPixelHeight(), + header.getPixelDepth(), + 1, // num Samples + header.getNumberOfSlices(), + (isGPUKTXPayload ? gpuktxKeyValue._samplerDesc : sampler)); + + tex->setUsage((isGPUKTXPayload ? gpuktxKeyValue._usage : usage)); + + // Assing the mips availables + tex->setStoredMipFormat(mipFormat); + uint16_t level = 0; + for (auto& image : srcData->_images) { + for (uint32_t face = 0; face < image._numFaces; face++) { + tex->assignStoredMipFace(level, face, image._faceSize, image._faceBytes[face]); + } + level++; + } + + return tex; +} + +bool Texture::evalKTXFormat(const Element& mipFormat, const Element& texelFormat, ktx::Header& header) { + if (texelFormat == Format::COLOR_RGBA_32 && mipFormat == Format::COLOR_BGRA_32) { + header.setUncompressed(ktx::GLType::UNSIGNED_BYTE, 1, ktx::GLFormat::BGRA, ktx::GLInternalFormat_Uncompressed::RGBA8, ktx::GLBaseInternalFormat::RGBA); + } else if (texelFormat == Format::COLOR_RGBA_32 && mipFormat == Format::COLOR_RGBA_32) { + header.setUncompressed(ktx::GLType::UNSIGNED_BYTE, 1, ktx::GLFormat::RGBA, ktx::GLInternalFormat_Uncompressed::RGBA8, ktx::GLBaseInternalFormat::RGBA); + } else if (texelFormat == Format::COLOR_SRGBA_32 && mipFormat == Format::COLOR_SBGRA_32) { + header.setUncompressed(ktx::GLType::UNSIGNED_BYTE, 1, ktx::GLFormat::BGRA, ktx::GLInternalFormat_Uncompressed::SRGB8_ALPHA8, ktx::GLBaseInternalFormat::RGBA); + } else if (texelFormat == Format::COLOR_SRGBA_32 && mipFormat == Format::COLOR_SRGBA_32) { + header.setUncompressed(ktx::GLType::UNSIGNED_BYTE, 1, ktx::GLFormat::RGBA, ktx::GLInternalFormat_Uncompressed::SRGB8_ALPHA8, ktx::GLBaseInternalFormat::RGBA); + } else if (texelFormat == Format::COLOR_R_8 && mipFormat == Format::COLOR_R_8) { + header.setUncompressed(ktx::GLType::UNSIGNED_BYTE, 1, ktx::GLFormat::RED, ktx::GLInternalFormat_Uncompressed::R8, ktx::GLBaseInternalFormat::RED); + } else { + return false; + } + + return true; +} + +bool Texture::evalTextureFormat(const ktx::Header& header, Element& mipFormat, Element& texelFormat) { + if (header.getGLFormat() == ktx::GLFormat::BGRA && header.getGLType() == ktx::GLType::UNSIGNED_BYTE && header.getTypeSize() == 1) { + if (header.getGLInternaFormat_Uncompressed() == ktx::GLInternalFormat_Uncompressed::RGBA8) { + mipFormat = Format::COLOR_BGRA_32; + texelFormat = Format::COLOR_RGBA_32; + } else if (header.getGLInternaFormat_Uncompressed() == ktx::GLInternalFormat_Uncompressed::SRGB8_ALPHA8) { + mipFormat = Format::COLOR_SBGRA_32; + texelFormat = Format::COLOR_SRGBA_32; + } else { + return false; + } + } else if (header.getGLFormat() == ktx::GLFormat::RGBA && header.getGLType() == ktx::GLType::UNSIGNED_BYTE && header.getTypeSize() == 1) { + if (header.getGLInternaFormat_Uncompressed() == ktx::GLInternalFormat_Uncompressed::RGBA8) { + mipFormat = Format::COLOR_RGBA_32; + texelFormat = Format::COLOR_RGBA_32; + } else if (header.getGLInternaFormat_Uncompressed() == ktx::GLInternalFormat_Uncompressed::SRGB8_ALPHA8) { + mipFormat = Format::COLOR_SRGBA_32; + texelFormat = Format::COLOR_SRGBA_32; + } else { + return false; + } + } else if (header.getGLFormat() == ktx::GLFormat::RED && header.getGLType() == ktx::GLType::UNSIGNED_BYTE && header.getTypeSize() == 1) { + mipFormat = Format::COLOR_R_8; + if (header.getGLInternaFormat_Uncompressed() == ktx::GLInternalFormat_Uncompressed::R8) { + texelFormat = Format::COLOR_R_8; + } else { + return false; + } + } else { + return false; + } + return true; +} diff --git a/libraries/ktx/CMakeLists.txt b/libraries/ktx/CMakeLists.txt new file mode 100644 index 0000000000..404660b247 --- /dev/null +++ b/libraries/ktx/CMakeLists.txt @@ -0,0 +1,3 @@ +set(TARGET_NAME ktx) +setup_hifi_library() +link_hifi_libraries() \ No newline at end of file diff --git a/libraries/ktx/src/ktx/KTX.cpp b/libraries/ktx/src/ktx/KTX.cpp new file mode 100644 index 0000000000..bbd4e1bc86 --- /dev/null +++ b/libraries/ktx/src/ktx/KTX.cpp @@ -0,0 +1,165 @@ +// +// KTX.cpp +// ktx/src/ktx +// +// Created by Zach Pomerantz on 2/08/2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "KTX.h" + +#include //min max and more + +using namespace ktx; + +uint32_t Header::evalPadding(size_t byteSize) { + //auto padding = byteSize % PACKING_SIZE; + // return (uint32_t) (padding ? PACKING_SIZE - padding : 0); + return (uint32_t) (3 - (byteSize + 3) % PACKING_SIZE);// padding ? PACKING_SIZE - padding : 0); +} + + +const Header::Identifier ktx::Header::IDENTIFIER {{ + 0xAB, 0x4B, 0x54, 0x58, 0x20, 0x31, 0x31, 0xBB, 0x0D, 0x0A, 0x1A, 0x0A +}}; + +Header::Header() { + memcpy(identifier, IDENTIFIER.data(), IDENTIFIER_LENGTH); +} + +uint32_t Header::evalMaxDimension() const { + return std::max(getPixelWidth(), std::max(getPixelHeight(), getPixelDepth())); +} + +uint32_t Header::evalPixelWidth(uint32_t level) const { + return std::max(getPixelWidth() >> level, 1U); +} +uint32_t Header::evalPixelHeight(uint32_t level) const { + return std::max(getPixelHeight() >> level, 1U); +} +uint32_t Header::evalPixelDepth(uint32_t level) const { + return std::max(getPixelDepth() >> level, 1U); +} + +size_t Header::evalPixelSize() const { + return glTypeSize; // Really we should generate the size from the FOrmat etc +} + +size_t Header::evalRowSize(uint32_t level) const { + auto pixWidth = evalPixelWidth(level); + auto pixSize = evalPixelSize(); + auto netSize = pixWidth * pixSize; + auto padding = evalPadding(netSize); + return netSize + padding; +} +size_t Header::evalFaceSize(uint32_t level) const { + auto pixHeight = evalPixelHeight(level); + auto pixDepth = evalPixelDepth(level); + auto rowSize = evalRowSize(level); + return pixDepth * pixHeight * rowSize; +} +size_t Header::evalImageSize(uint32_t level) const { + auto faceSize = evalFaceSize(level); + if (numberOfFaces == NUM_CUBEMAPFACES && numberOfArrayElements == 0) { + return faceSize; + } else { + return (getNumberOfSlices() * numberOfFaces * faceSize); + } +} + + +KeyValue::KeyValue(const std::string& key, uint32_t valueByteSize, const Byte* value) : + _byteSize((uint32_t) key.size() + 1 + valueByteSize), // keyString size + '\0' ending char + the value size + _key(key), + _value(valueByteSize) +{ + if (_value.size() && value) { + memcpy(_value.data(), value, valueByteSize); + } +} + +KeyValue::KeyValue(const std::string& key, const std::string& value) : + KeyValue(key, (uint32_t) value.size(), (const Byte*) value.data()) +{ + +} + +uint32_t KeyValue::serializedByteSize() const { + return (uint32_t) (sizeof(uint32_t) + _byteSize + Header::evalPadding(_byteSize)); +} + +uint32_t KeyValue::serializedKeyValuesByteSize(const KeyValues& keyValues) { + uint32_t keyValuesSize = 0; + for (auto& keyval : keyValues) { + keyValuesSize += keyval.serializedByteSize(); + } + return (keyValuesSize + Header::evalPadding(keyValuesSize)); +} + + +KTX::KTX() { +} + +KTX::~KTX() { +} + +void KTX::resetStorage(const StoragePointer& storage) { + _storage = storage; +} + +const Header* KTX::getHeader() const { + if (!_storage) { + return nullptr; + } + return reinterpret_cast(_storage->data()); +} + + +size_t KTX::getKeyValueDataSize() const { + if (_storage) { + return getHeader()->bytesOfKeyValueData; + } else { + return 0; + } +} + +size_t KTX::getTexelsDataSize() const { + if (_storage) { + //return _storage->size() - (sizeof(Header) + getKeyValueDataSize()); + return (_storage->data() + _storage->size()) - getTexelsData(); + } else { + return 0; + } +} + +const Byte* KTX::getKeyValueData() const { + if (_storage) { + return (_storage->data() + sizeof(Header)); + } else { + return nullptr; + } +} + +const Byte* KTX::getTexelsData() const { + if (_storage) { + return (_storage->data() + sizeof(Header) + getKeyValueDataSize()); + } else { + return nullptr; + } +} + +storage::StoragePointer KTX::getMipFaceTexelsData(uint16_t mip, uint8_t face) const { + storage::StoragePointer result; + if (mip < _images.size()) { + const auto& faces = _images[mip]; + if (face < faces._numFaces) { + auto faceOffset = faces._faceBytes[face] - _storage->data(); + auto faceSize = faces._faceSize; + result = _storage->createView(faceSize, faceOffset); + } + } + return result; +} diff --git a/libraries/ktx/src/ktx/KTX.h b/libraries/ktx/src/ktx/KTX.h new file mode 100644 index 0000000000..8e901b1105 --- /dev/null +++ b/libraries/ktx/src/ktx/KTX.h @@ -0,0 +1,494 @@ +// +// KTX.h +// ktx/src/ktx +// +// Created by Zach Pomerantz on 2/08/2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#pragma once +#ifndef hifi_ktx_KTX_h +#define hifi_ktx_KTX_h + +#include +#include +#include +#include +#include +#include +#include + +#include + +/* KTX Spec: + +Byte[12] identifier +UInt32 endianness +UInt32 glType +UInt32 glTypeSize +UInt32 glFormat +Uint32 glInternalFormat +Uint32 glBaseInternalFormat +UInt32 pixelWidth +UInt32 pixelHeight +UInt32 pixelDepth +UInt32 numberOfArrayElements +UInt32 numberOfFaces +UInt32 numberOfMipmapLevels +UInt32 bytesOfKeyValueData + +for each keyValuePair that fits in bytesOfKeyValueData + UInt32 keyAndValueByteSize + Byte keyAndValue[keyAndValueByteSize] + Byte valuePadding[3 - ((keyAndValueByteSize + 3) % 4)] +end + +for each mipmap_level in numberOfMipmapLevels* + UInt32 imageSize; + for each array_element in numberOfArrayElements* + for each face in numberOfFaces + for each z_slice in pixelDepth* + for each row or row_of_blocks in pixelHeight* + for each pixel or block_of_pixels in pixelWidth + Byte data[format-specific-number-of-bytes]** + end + end + end + Byte cubePadding[0-3] + end + end + Byte mipPadding[3 - ((imageSize + 3) % 4)] +end + +* Replace with 1 if this field is 0. + +** Uncompressed texture data matches a GL_UNPACK_ALIGNMENT of 4. +*/ + + + +namespace ktx { + const uint32_t PACKING_SIZE { sizeof(uint32_t) }; + using Byte = uint8_t; + + enum class GLType : uint32_t { + COMPRESSED_TYPE = 0, + + // GL 4.4 Table 8.2 + UNSIGNED_BYTE = 0x1401, + BYTE = 0x1400, + UNSIGNED_SHORT = 0x1403, + SHORT = 0x1402, + UNSIGNED_INT = 0x1405, + INT = 0x1404, + HALF_FLOAT = 0x140B, + FLOAT = 0x1406, + UNSIGNED_BYTE_3_3_2 = 0x8032, + UNSIGNED_BYTE_2_3_3_REV = 0x8362, + UNSIGNED_SHORT_5_6_5 = 0x8363, + UNSIGNED_SHORT_5_6_5_REV = 0x8364, + UNSIGNED_SHORT_4_4_4_4 = 0x8033, + UNSIGNED_SHORT_4_4_4_4_REV = 0x8365, + UNSIGNED_SHORT_5_5_5_1 = 0x8034, + UNSIGNED_SHORT_1_5_5_5_REV = 0x8366, + UNSIGNED_INT_8_8_8_8 = 0x8035, + UNSIGNED_INT_8_8_8_8_REV = 0x8367, + UNSIGNED_INT_10_10_10_2 = 0x8036, + UNSIGNED_INT_2_10_10_10_REV = 0x8368, + UNSIGNED_INT_24_8 = 0x84FA, + UNSIGNED_INT_10F_11F_11F_REV = 0x8C3B, + UNSIGNED_INT_5_9_9_9_REV = 0x8C3E, + FLOAT_32_UNSIGNED_INT_24_8_REV = 0x8DAD, + + NUM_GLTYPES = 25, + }; + + enum class GLFormat : uint32_t { + COMPRESSED_FORMAT = 0, + + // GL 4.4 Table 8.3 + STENCIL_INDEX = 0x1901, + DEPTH_COMPONENT = 0x1902, + DEPTH_STENCIL = 0x84F9, + + RED = 0x1903, + GREEN = 0x1904, + BLUE = 0x1905, + RG = 0x8227, + RGB = 0x1907, + RGBA = 0x1908, + BGR = 0x80E0, + BGRA = 0x80E1, + + RG_INTEGER = 0x8228, + RED_INTEGER = 0x8D94, + GREEN_INTEGER = 0x8D95, + BLUE_INTEGER = 0x8D96, + RGB_INTEGER = 0x8D98, + RGBA_INTEGER = 0x8D99, + BGR_INTEGER = 0x8D9A, + BGRA_INTEGER = 0x8D9B, + + NUM_GLFORMATS = 20, + }; + + enum class GLInternalFormat_Uncompressed : uint32_t { + // GL 4.4 Table 8.12 + R8 = 0x8229, + R8_SNORM = 0x8F94, + + R16 = 0x822A, + R16_SNORM = 0x8F98, + + RG8 = 0x822B, + RG8_SNORM = 0x8F95, + + RG16 = 0x822C, + RG16_SNORM = 0x8F99, + + R3_G3_B2 = 0x2A10, + RGB4 = 0x804F, + RGB5 = 0x8050, + RGB565 = 0x8D62, + + RGB8 = 0x8051, + RGB8_SNORM = 0x8F96, + RGB10 = 0x8052, + RGB12 = 0x8053, + + RGB16 = 0x8054, + RGB16_SNORM = 0x8F9A, + + RGBA2 = 0x8055, + RGBA4 = 0x8056, + RGB5_A1 = 0x8057, + RGBA8 = 0x8058, + RGBA8_SNORM = 0x8F97, + + RGB10_A2 = 0x8059, + RGB10_A2UI = 0x906F, + + RGBA12 = 0x805A, + RGBA16 = 0x805B, + RGBA16_SNORM = 0x8F9B, + + SRGB8 = 0x8C41, + SRGB8_ALPHA8 = 0x8C43, + + R16F = 0x822D, + RG16F = 0x822F, + RGB16F = 0x881B, + RGBA16F = 0x881A, + + R32F = 0x822E, + RG32F = 0x8230, + RGB32F = 0x8815, + RGBA32F = 0x8814, + + R11F_G11F_B10F = 0x8C3A, + RGB9_E5 = 0x8C3D, + + + R8I = 0x8231, + R8UI = 0x8232, + R16I = 0x8233, + R16UI = 0x8234, + R32I = 0x8235, + R32UI = 0x8236, + RG8I = 0x8237, + RG8UI = 0x8238, + RG16I = 0x8239, + RG16UI = 0x823A, + RG32I = 0x823B, + RG32UI = 0x823C, + + RGB8I = 0x8D8F, + RGB8UI = 0x8D7D, + RGB16I = 0x8D89, + RGB16UI = 0x8D77, + + RGB32I = 0x8D83, + RGB32UI = 0x8D71, + RGBA8I = 0x8D8E, + RGBA8UI = 0x8D7C, + RGBA16I = 0x8D88, + RGBA16UI = 0x8D76, + RGBA32I = 0x8D82, + + RGBA32UI = 0x8D70, + + // GL 4.4 Table 8.13 + DEPTH_COMPONENT16 = 0x81A5, + DEPTH_COMPONENT24 = 0x81A6, + DEPTH_COMPONENT32 = 0x81A7, + + DEPTH_COMPONENT32F = 0x8CAC, + DEPTH24_STENCIL8 = 0x88F0, + DEPTH32F_STENCIL8 = 0x8CAD, + + STENCIL_INDEX1 = 0x8D46, + STENCIL_INDEX4 = 0x8D47, + STENCIL_INDEX8 = 0x8D48, + STENCIL_INDEX16 = 0x8D49, + + NUM_UNCOMPRESSED_GLINTERNALFORMATS = 74, + }; + + enum class GLInternalFormat_Compressed : uint32_t { + // GL 4.4 Table 8.14 + COMPRESSED_RED = 0x8225, + COMPRESSED_RG = 0x8226, + COMPRESSED_RGB = 0x84ED, + COMPRESSED_RGBA = 0x84EE, + + COMPRESSED_SRGB = 0x8C48, + COMPRESSED_SRGB_ALPHA = 0x8C49, + + COMPRESSED_RED_RGTC1 = 0x8DBB, + COMPRESSED_SIGNED_RED_RGTC1 = 0x8DBC, + COMPRESSED_RG_RGTC2 = 0x8DBD, + COMPRESSED_SIGNED_RG_RGTC2 = 0x8DBE, + + COMPRESSED_RGBA_BPTC_UNORM = 0x8E8C, + COMPRESSED_SRGB_ALPHA_BPTC_UNORM = 0x8E8D, + COMPRESSED_RGB_BPTC_SIGNED_FLOAT = 0x8E8E, + COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT = 0x8E8F, + + COMPRESSED_RGB8_ETC2 = 0x9274, + COMPRESSED_SRGB8_ETC2 = 0x9275, + COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2 = 0x9276, + COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2 = 0x9277, + COMPRESSED_RGBA8_ETC2_EAC = 0x9278, + COMPRESSED_SRGB8_ALPHA8_ETC2_EAC = 0x9279, + + COMPRESSED_R11_EAC = 0x9270, + COMPRESSED_SIGNED_R11_EAC = 0x9271, + COMPRESSED_RG11_EAC = 0x9272, + COMPRESSED_SIGNED_RG11_EAC = 0x9273, + + NUM_COMPRESSED_GLINTERNALFORMATS = 24, + }; + + enum class GLBaseInternalFormat : uint32_t { + // GL 4.4 Table 8.11 + DEPTH_COMPONENT = 0x1902, + DEPTH_STENCIL = 0x84F9, + RED = 0x1903, + RG = 0x8227, + RGB = 0x1907, + RGBA = 0x1908, + STENCIL_INDEX = 0x1901, + + NUM_GLBASEINTERNALFORMATS = 7, + }; + + enum CubeMapFace { + POS_X = 0, + NEG_X = 1, + POS_Y = 2, + NEG_Y = 3, + POS_Z = 4, + NEG_Z = 5, + NUM_CUBEMAPFACES = 6, + }; + + using Storage = storage::Storage; + using StoragePointer = std::shared_ptr; + + // Header + struct Header { + static const size_t IDENTIFIER_LENGTH = 12; + using Identifier = std::array; + static const Identifier IDENTIFIER; + + static const uint32_t ENDIAN_TEST = 0x04030201; + static const uint32_t REVERSE_ENDIAN_TEST = 0x01020304; + + static uint32_t evalPadding(size_t byteSize); + + Header(); + + Byte identifier[IDENTIFIER_LENGTH]; + uint32_t endianness { ENDIAN_TEST }; + + uint32_t glType; + uint32_t glTypeSize { 0 }; + uint32_t glFormat; + uint32_t glInternalFormat; + uint32_t glBaseInternalFormat; + + uint32_t pixelWidth { 1 }; + uint32_t pixelHeight { 0 }; + uint32_t pixelDepth { 0 }; + uint32_t numberOfArrayElements { 0 }; + uint32_t numberOfFaces { 1 }; + uint32_t numberOfMipmapLevels { 1 }; + + uint32_t bytesOfKeyValueData { 0 }; + + uint32_t getPixelWidth() const { return (pixelWidth ? pixelWidth : 1); } + uint32_t getPixelHeight() const { return (pixelHeight ? pixelHeight : 1); } + uint32_t getPixelDepth() const { return (pixelDepth ? pixelDepth : 1); } + uint32_t getNumberOfSlices() const { return (numberOfArrayElements ? numberOfArrayElements : 1); } + uint32_t getNumberOfLevels() const { return (numberOfMipmapLevels ? numberOfMipmapLevels : 1); } + + uint32_t evalMaxDimension() const; + uint32_t evalPixelWidth(uint32_t level) const; + uint32_t evalPixelHeight(uint32_t level) const; + uint32_t evalPixelDepth(uint32_t level) const; + + size_t evalPixelSize() const; + size_t evalRowSize(uint32_t level) const; + size_t evalFaceSize(uint32_t level) const; + size_t evalImageSize(uint32_t level) const; + + void setUncompressed(GLType type, uint32_t typeSize, GLFormat format, GLInternalFormat_Uncompressed internalFormat, GLBaseInternalFormat baseInternalFormat) { + glType = (uint32_t) type; + glTypeSize = typeSize; + glFormat = (uint32_t) format; + glInternalFormat = (uint32_t) internalFormat; + glBaseInternalFormat = (uint32_t) baseInternalFormat; + } + void setCompressed(GLInternalFormat_Compressed internalFormat, GLBaseInternalFormat baseInternalFormat) { + glType = (uint32_t) GLType::COMPRESSED_TYPE; + glTypeSize = 1; + glFormat = (uint32_t) GLFormat::COMPRESSED_FORMAT; + glInternalFormat = (uint32_t) internalFormat; + glBaseInternalFormat = (uint32_t) baseInternalFormat; + } + + GLType getGLType() const { return (GLType)glType; } + uint32_t getTypeSize() const { return glTypeSize; } + GLFormat getGLFormat() const { return (GLFormat)glFormat; } + GLInternalFormat_Uncompressed getGLInternaFormat_Uncompressed() const { return (GLInternalFormat_Uncompressed)glInternalFormat; } + GLInternalFormat_Compressed getGLInternaFormat_Compressed() const { return (GLInternalFormat_Compressed)glInternalFormat; } + GLBaseInternalFormat getGLBaseInternalFormat() const { return (GLBaseInternalFormat)glBaseInternalFormat; } + + + void setDimensions(uint32_t width, uint32_t height = 0, uint32_t depth = 0, uint32_t numSlices = 0, uint32_t numFaces = 1) { + pixelWidth = (width > 0 ? width : 1); + pixelHeight = height; + pixelDepth = depth; + numberOfArrayElements = numSlices; + numberOfFaces = ((numFaces == 1) || (numFaces == NUM_CUBEMAPFACES) ? numFaces : 1); + } + void set1D(uint32_t width) { setDimensions(width); } + void set1DArray(uint32_t width, uint32_t numSlices) { setDimensions(width, 0, 0, (numSlices > 0 ? numSlices : 1)); } + void set2D(uint32_t width, uint32_t height) { setDimensions(width, height); } + void set2DArray(uint32_t width, uint32_t height, uint32_t numSlices) { setDimensions(width, height, 0, (numSlices > 0 ? numSlices : 1)); } + void set3D(uint32_t width, uint32_t height, uint32_t depth) { setDimensions(width, height, depth); } + void set3DArray(uint32_t width, uint32_t height, uint32_t depth, uint32_t numSlices) { setDimensions(width, height, depth, (numSlices > 0 ? numSlices : 1)); } + void setCube(uint32_t width, uint32_t height) { setDimensions(width, height, 0, 0, NUM_CUBEMAPFACES); } + void setCubeArray(uint32_t width, uint32_t height, uint32_t numSlices) { setDimensions(width, height, 0, (numSlices > 0 ? numSlices : 1), NUM_CUBEMAPFACES); } + + }; + + // Key Values + struct KeyValue { + uint32_t _byteSize { 0 }; + std::string _key; + std::vector _value; + + + KeyValue(const std::string& key, uint32_t valueByteSize, const Byte* value); + + KeyValue(const std::string& key, const std::string& value); + + uint32_t serializedByteSize() const; + + static KeyValue parseSerializedKeyAndValue(uint32_t srcSize, const Byte* srcBytes); + static uint32_t writeSerializedKeyAndValue(Byte* destBytes, uint32_t destByteSize, const KeyValue& keyval); + + using KeyValues = std::list; + static uint32_t serializedKeyValuesByteSize(const KeyValues& keyValues); + + }; + using KeyValues = KeyValue::KeyValues; + + + struct Image { + using FaceBytes = std::vector; + + uint32_t _numFaces{ 1 }; + uint32_t _imageSize; + uint32_t _faceSize; + uint32_t _padding; + FaceBytes _faceBytes; + + + Image(uint32_t imageSize, uint32_t padding, const Byte* bytes) : + _numFaces(1), + _imageSize(imageSize), + _faceSize(imageSize), + _padding(padding), + _faceBytes(1, bytes) {} + + Image(uint32_t pageSize, uint32_t padding, const FaceBytes& cubeFaceBytes) : + _numFaces(NUM_CUBEMAPFACES), + _imageSize(pageSize * NUM_CUBEMAPFACES), + _faceSize(pageSize), + _padding(padding) + { + if (cubeFaceBytes.size() == NUM_CUBEMAPFACES) { + _faceBytes = cubeFaceBytes; + } + } + }; + using Images = std::vector; + + class KTX { + void resetStorage(const StoragePointer& src); + + KTX(); + public: + + ~KTX(); + + // Define a KTX object manually to write it somewhere (in a file on disk?) + // This path allocate the Storage where to store header, keyvalues and copy mips + // Then COPY all the data + static std::unique_ptr create(const Header& header, const Images& images, const KeyValues& keyValues = KeyValues()); + + // Instead of creating a full Copy of the src data in a KTX object, the write serialization can be performed with the + // following two functions + // size_t sizeNeeded = KTX::evalStorageSize(header, images); + // + // //allocate a buffer of size "sizeNeeded" or map a file with enough capacity + // Byte* destBytes = new Byte[sizeNeeded]; + // + // // THen perform the writing of the src data to the destinnation buffer + // write(destBytes, sizeNeeded, header, images); + // + // This is exactly what is done in the create function + static size_t evalStorageSize(const Header& header, const Images& images, const KeyValues& keyValues = KeyValues()); + static size_t write(Byte* destBytes, size_t destByteSize, const Header& header, const Images& images, const KeyValues& keyValues = KeyValues()); + static size_t writeKeyValues(Byte* destBytes, size_t destByteSize, const KeyValues& keyValues); + static Images writeImages(Byte* destBytes, size_t destByteSize, const Images& images); + + // Parse a block of memory and create a KTX object from it + static std::unique_ptr create(const StoragePointer& src); + + static bool checkHeaderFromStorage(size_t srcSize, const Byte* srcBytes); + static KeyValues parseKeyValues(size_t srcSize, const Byte* srcBytes); + static Images parseImages(const Header& header, size_t srcSize, const Byte* srcBytes); + + // Access raw pointers to the main sections of the KTX + const Header* getHeader() const; + const Byte* getKeyValueData() const; + const Byte* getTexelsData() const; + storage::StoragePointer getMipFaceTexelsData(uint16_t mip = 0, uint8_t face = 0) const; + const StoragePointer& getStorage() const { return _storage; } + + size_t getKeyValueDataSize() const; + size_t getTexelsDataSize() const; + + StoragePointer _storage; + KeyValues _keyValues; + Images _images; + }; + +} + +#endif // hifi_ktx_KTX_h diff --git a/libraries/ktx/src/ktx/Reader.cpp b/libraries/ktx/src/ktx/Reader.cpp new file mode 100644 index 0000000000..277ce42e69 --- /dev/null +++ b/libraries/ktx/src/ktx/Reader.cpp @@ -0,0 +1,195 @@ +// +// Reader.cpp +// ktx/src/ktx +// +// Created by Zach Pomerantz on 2/08/2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#include "KTX.h" + +#include +#include +#include + +#ifndef _MSC_VER +#define NOEXCEPT noexcept +#else +#define NOEXCEPT +#endif + +namespace ktx { + class ReaderException: public std::exception { + public: + ReaderException(const std::string& explanation) : _explanation("KTX deserialization error: " + explanation) {} + const char* what() const NOEXCEPT override { return _explanation.c_str(); } + private: + const std::string _explanation; + }; + + bool checkEndianness(uint32_t endianness, bool& matching) { + switch (endianness) { + case Header::ENDIAN_TEST: { + matching = true; + return true; + } + break; + case Header::REVERSE_ENDIAN_TEST: + { + matching = false; + return true; + } + break; + default: + throw ReaderException("endianness field has invalid value"); + return false; + } + } + + bool checkIdentifier(const Byte* identifier) { + if (!(0 == memcmp(identifier, Header::IDENTIFIER.data(), Header::IDENTIFIER_LENGTH))) { + throw ReaderException("identifier field invalid"); + return false; + } + return true; + } + + bool KTX::checkHeaderFromStorage(size_t srcSize, const Byte* srcBytes) { + try { + // validation + if (srcSize < sizeof(Header)) { + throw ReaderException("length is too short for header"); + } + const Header* header = reinterpret_cast(srcBytes); + + checkIdentifier(header->identifier); + + bool endianMatch { true }; + checkEndianness(header->endianness, endianMatch); + + // TODO: endian conversion if !endianMatch - for now, this is for local use and is unnecessary + + + // TODO: calculated bytesOfTexData + if (srcSize < (sizeof(Header) + header->bytesOfKeyValueData)) { + throw ReaderException("length is too short for metadata"); + } + + size_t bytesOfTexData = 0; + if (srcSize < (sizeof(Header) + header->bytesOfKeyValueData + bytesOfTexData)) { + + throw ReaderException("length is too short for data"); + } + + return true; + } + catch (const ReaderException& e) { + qWarning() << e.what(); + return false; + } + } + + KeyValue KeyValue::parseSerializedKeyAndValue(uint32_t srcSize, const Byte* srcBytes) { + uint32_t keyAndValueByteSize; + memcpy(&keyAndValueByteSize, srcBytes, sizeof(uint32_t)); + if (keyAndValueByteSize + sizeof(uint32_t) > srcSize) { + throw ReaderException("invalid key-value size"); + } + auto keyValueBytes = srcBytes + sizeof(uint32_t); + + // find the first null character \0 and extract the key + uint32_t keyLength = 0; + while (reinterpret_cast(keyValueBytes)[++keyLength] != '\0') { + if (keyLength == keyAndValueByteSize) { + // key must be null-terminated, and there must be space for the value + throw ReaderException("invalid key-value " + std::string(reinterpret_cast(keyValueBytes), keyLength)); + } + } + uint32_t valueStartOffset = keyLength + 1; + + // parse the key-value + return KeyValue(std::string(reinterpret_cast(keyValueBytes), keyLength), + keyAndValueByteSize - valueStartOffset, keyValueBytes + valueStartOffset); + } + + KeyValues KTX::parseKeyValues(size_t srcSize, const Byte* srcBytes) { + KeyValues keyValues; + try { + auto src = srcBytes; + uint32_t length = (uint32_t) srcSize; + uint32_t offset = 0; + while (offset < length) { + auto keyValue = KeyValue::parseSerializedKeyAndValue(length - offset, src); + keyValues.emplace_back(keyValue); + + // advance offset/src + offset += keyValue.serializedByteSize(); + src += keyValue.serializedByteSize(); + } + } + catch (const ReaderException& e) { + qWarning() << e.what(); + } + return keyValues; + } + + Images KTX::parseImages(const Header& header, size_t srcSize, const Byte* srcBytes) { + Images images; + auto currentPtr = srcBytes; + auto numFaces = header.numberOfFaces; + + // Keep identifying new mip as long as we can at list query the next imageSize + while ((currentPtr - srcBytes) + sizeof(uint32_t) <= (srcSize)) { + + // Grab the imageSize coming up + size_t imageSize = *reinterpret_cast(currentPtr); + currentPtr += sizeof(uint32_t); + + // If enough data ahead then capture the pointer + if ((currentPtr - srcBytes) + imageSize <= (srcSize)) { + auto padding = Header::evalPadding(imageSize); + + if (numFaces == NUM_CUBEMAPFACES) { + size_t faceSize = imageSize / NUM_CUBEMAPFACES; + Image::FaceBytes faces(NUM_CUBEMAPFACES); + for (uint32_t face = 0; face < NUM_CUBEMAPFACES; face++) { + faces[face] = currentPtr; + currentPtr += faceSize; + } + images.emplace_back(Image((uint32_t) faceSize, padding, faces)); + currentPtr += padding; + } else { + images.emplace_back(Image((uint32_t) imageSize, padding, currentPtr)); + currentPtr += imageSize + padding; + } + } else { + break; + } + } + + return images; + } + + std::unique_ptr KTX::create(const StoragePointer& src) { + if (!src) { + return nullptr; + } + + if (!checkHeaderFromStorage(src->size(), src->data())) { + return nullptr; + } + + std::unique_ptr result(new KTX()); + result->resetStorage(src); + + // read metadata + result->_keyValues = parseKeyValues(result->getHeader()->bytesOfKeyValueData, result->getKeyValueData()); + + // populate image table + result->_images = parseImages(*result->getHeader(), result->getTexelsDataSize(), result->getTexelsData()); + + return result; + } +} diff --git a/libraries/ktx/src/ktx/Writer.cpp b/libraries/ktx/src/ktx/Writer.cpp new file mode 100644 index 0000000000..25b363d31b --- /dev/null +++ b/libraries/ktx/src/ktx/Writer.cpp @@ -0,0 +1,171 @@ +// +// Writer.cpp +// ktx/src/ktx +// +// Created by Zach Pomerantz on 2/08/2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#include "KTX.h" + + +#include +#include +#ifndef _MSC_VER +#define NOEXCEPT noexcept +#else +#define NOEXCEPT +#endif + +namespace ktx { + + class WriterException : public std::exception { + public: + WriterException(const std::string& explanation) : _explanation("KTX serialization error: " + explanation) {} + const char* what() const NOEXCEPT override { return _explanation.c_str(); } + private: + const std::string _explanation; + }; + + std::unique_ptr KTX::create(const Header& header, const Images& images, const KeyValues& keyValues) { + StoragePointer storagePointer; + { + auto storageSize = ktx::KTX::evalStorageSize(header, images, keyValues); + auto memoryStorage = new storage::MemoryStorage(storageSize); + ktx::KTX::write(memoryStorage->data(), memoryStorage->size(), header, images, keyValues); + storagePointer.reset(memoryStorage); + } + return create(storagePointer); + } + + size_t KTX::evalStorageSize(const Header& header, const Images& images, const KeyValues& keyValues) { + size_t storageSize = sizeof(Header); + + if (!keyValues.empty()) { + size_t keyValuesSize = KeyValue::serializedKeyValuesByteSize(keyValues); + storageSize += keyValuesSize; + } + + auto numMips = header.getNumberOfLevels(); + for (uint32_t l = 0; l < numMips; l++) { + if (images.size() > l) { + storageSize += sizeof(uint32_t); + storageSize += images[l]._imageSize; + storageSize += Header::evalPadding(images[l]._imageSize); + } + } + return storageSize; + } + + size_t KTX::write(Byte* destBytes, size_t destByteSize, const Header& header, const Images& srcImages, const KeyValues& keyValues) { + // Check again that we have enough destination capacity + if (!destBytes || (destByteSize < evalStorageSize(header, srcImages, keyValues))) { + return 0; + } + + auto currentDestPtr = destBytes; + // Header + auto destHeader = reinterpret_cast(currentDestPtr); + memcpy(currentDestPtr, &header, sizeof(Header)); + currentDestPtr += sizeof(Header); + + // KeyValues + if (!keyValues.empty()) { + destHeader->bytesOfKeyValueData = (uint32_t) writeKeyValues(currentDestPtr, destByteSize - sizeof(Header), keyValues); + } else { + // Make sure the header contains the right bytesOfKeyValueData size + destHeader->bytesOfKeyValueData = 0; + } + currentDestPtr += destHeader->bytesOfKeyValueData; + + // Images + auto destImages = writeImages(currentDestPtr, destByteSize - sizeof(Header) - destHeader->bytesOfKeyValueData, srcImages); + // We chould check here that the amoutn of dest IMages generated is the same as the source + + return destByteSize; + } + + uint32_t KeyValue::writeSerializedKeyAndValue(Byte* destBytes, uint32_t destByteSize, const KeyValue& keyval) { + uint32_t keyvalSize = keyval.serializedByteSize(); + if (keyvalSize > destByteSize) { + throw WriterException("invalid key-value size"); + } + + *((uint32_t*) destBytes) = keyval._byteSize; + + auto dest = destBytes + sizeof(uint32_t); + + auto keySize = keyval._key.size() + 1; // Add 1 for the '\0' character at the end of the string + memcpy(dest, keyval._key.data(), keySize); + dest += keySize; + + memcpy(dest, keyval._value.data(), keyval._value.size()); + + return keyvalSize; + } + + size_t KTX::writeKeyValues(Byte* destBytes, size_t destByteSize, const KeyValues& keyValues) { + size_t writtenByteSize = 0; + try { + auto dest = destBytes; + for (auto& keyval : keyValues) { + size_t keyvalSize = KeyValue::writeSerializedKeyAndValue(dest, (uint32_t) (destByteSize - writtenByteSize), keyval); + writtenByteSize += keyvalSize; + dest += keyvalSize; + } + } + catch (const WriterException& e) { + qWarning() << e.what(); + } + return writtenByteSize; + } + + Images KTX::writeImages(Byte* destBytes, size_t destByteSize, const Images& srcImages) { + Images destImages; + auto imagesDataPtr = destBytes; + if (!imagesDataPtr) { + return destImages; + } + auto allocatedImagesDataSize = destByteSize; + size_t currentDataSize = 0; + auto currentPtr = imagesDataPtr; + + for (uint32_t l = 0; l < srcImages.size(); l++) { + if (currentDataSize + sizeof(uint32_t) < allocatedImagesDataSize) { + size_t imageSize = srcImages[l]._imageSize; + *(reinterpret_cast (currentPtr)) = (uint32_t) imageSize; + currentPtr += sizeof(uint32_t); + currentDataSize += sizeof(uint32_t); + + // If enough data ahead then capture the copy source pointer + if (currentDataSize + imageSize <= (allocatedImagesDataSize)) { + auto padding = Header::evalPadding(imageSize); + + // Single face vs cubes + if (srcImages[l]._numFaces == 1) { + memcpy(currentPtr, srcImages[l]._faceBytes[0], imageSize); + destImages.emplace_back(Image((uint32_t) imageSize, padding, currentPtr)); + currentPtr += imageSize; + } else { + Image::FaceBytes faceBytes(NUM_CUBEMAPFACES); + auto faceSize = srcImages[l]._faceSize; + for (int face = 0; face < NUM_CUBEMAPFACES; face++) { + memcpy(currentPtr, srcImages[l]._faceBytes[face], faceSize); + faceBytes[face] = currentPtr; + currentPtr += faceSize; + } + destImages.emplace_back(Image(faceSize, padding, faceBytes)); + } + + currentPtr += padding; + currentDataSize += imageSize + padding; + } + } + } + + return destImages; + } + +} diff --git a/libraries/model-networking/CMakeLists.txt b/libraries/model-networking/CMakeLists.txt index ed8cd7b5f9..00aa17ff57 100644 --- a/libraries/model-networking/CMakeLists.txt +++ b/libraries/model-networking/CMakeLists.txt @@ -1,4 +1,4 @@ set(TARGET_NAME model-networking) setup_hifi_library() -link_hifi_libraries(shared networking model fbx) +link_hifi_libraries(shared networking model fbx ktx) diff --git a/libraries/model-networking/src/model-networking/KTXCache.cpp b/libraries/model-networking/src/model-networking/KTXCache.cpp new file mode 100644 index 0000000000..63d35fe4a4 --- /dev/null +++ b/libraries/model-networking/src/model-networking/KTXCache.cpp @@ -0,0 +1,47 @@ +// +// KTXCache.cpp +// libraries/model-networking/src +// +// Created by Zach Pomerantz on 2/22/2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "KTXCache.h" + +#include + +using File = cache::File; +using FilePointer = cache::FilePointer; + +KTXCache::KTXCache(const std::string& dir, const std::string& ext) : + FileCache(dir, ext) { + initialize(); +} + +KTXFilePointer KTXCache::writeFile(const char* data, Metadata&& metadata) { + FilePointer file = FileCache::writeFile(data, std::move(metadata)); + return std::static_pointer_cast(file); +} + +KTXFilePointer KTXCache::getFile(const Key& key) { + return std::static_pointer_cast(FileCache::getFile(key)); +} + +std::unique_ptr KTXCache::createFile(Metadata&& metadata, const std::string& filepath) { + qCInfo(file_cache) << "Wrote KTX" << metadata.key.c_str(); + return std::unique_ptr(new KTXFile(std::move(metadata), filepath)); +} + +KTXFile::KTXFile(Metadata&& metadata, const std::string& filepath) : + cache::File(std::move(metadata), filepath) {} + +std::unique_ptr KTXFile::getKTX() const { + ktx::StoragePointer storage = std::make_shared(getFilepath().c_str()); + if (*storage) { + return ktx::KTX::create(storage); + } + return {}; +} diff --git a/libraries/model-networking/src/model-networking/KTXCache.h b/libraries/model-networking/src/model-networking/KTXCache.h new file mode 100644 index 0000000000..4ef5e52721 --- /dev/null +++ b/libraries/model-networking/src/model-networking/KTXCache.h @@ -0,0 +1,51 @@ +// +// KTXCache.h +// libraries/model-networking/src +// +// Created by Zach Pomerantz 2/22/2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_KTXCache_h +#define hifi_KTXCache_h + +#include + +#include + +namespace ktx { + class KTX; +} + +class KTXFile; +using KTXFilePointer = std::shared_ptr; + +class KTXCache : public cache::FileCache { + Q_OBJECT + +public: + KTXCache(const std::string& dir, const std::string& ext); + + KTXFilePointer writeFile(const char* data, Metadata&& metadata); + KTXFilePointer getFile(const Key& key); + +protected: + std::unique_ptr createFile(Metadata&& metadata, const std::string& filepath) override final; +}; + +class KTXFile : public cache::File { + Q_OBJECT + +public: + std::unique_ptr getKTX() const; + +protected: + friend class KTXCache; + + KTXFile(Metadata&& metadata, const std::string& filepath); +}; + +#endif // hifi_KTXCache_h diff --git a/libraries/model-networking/src/model-networking/TextureCache.cpp b/libraries/model-networking/src/model-networking/TextureCache.cpp index 8a4e85cfe6..5dfaddd471 100644 --- a/libraries/model-networking/src/model-networking/TextureCache.cpp +++ b/libraries/model-networking/src/model-networking/TextureCache.cpp @@ -18,27 +18,37 @@ #include #include #include + +#if DEBUG_DUMP_TEXTURE_LOADS #include #include +#endif #include #include #include +#include + #include #include #include -#include #include "ModelNetworkingLogging.h" #include #include Q_LOGGING_CATEGORY(trace_resource_parse_image, "trace.resource.parse.image") +Q_LOGGING_CATEGORY(trace_resource_parse_image_raw, "trace.resource.parse.image.raw") +Q_LOGGING_CATEGORY(trace_resource_parse_image_ktx, "trace.resource.parse.image.ktx") -TextureCache::TextureCache() { +const std::string TextureCache::KTX_DIRNAME { "ktx_cache" }; +const std::string TextureCache::KTX_EXT { "ktx" }; + +TextureCache::TextureCache() : + _ktxCache(KTX_DIRNAME, KTX_EXT) { setUnusedResourceCacheSize(0); setObjectName("TextureCache"); @@ -61,7 +71,7 @@ TextureCache::~TextureCache() { // this list taken from Ken Perlin's Improved Noise reference implementation (orig. in Java) at // http://mrl.nyu.edu/~perlin/noise/ -const int permutation[256] = +const int permutation[256] = { 151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23, 190, 6, 148, @@ -108,7 +118,8 @@ const gpu::TexturePointer& TextureCache::getPermutationNormalTexture() { } _permutationNormalTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element(gpu::VEC3, gpu::NUINT8, gpu::RGB), 256, 2)); - _permutationNormalTexture->assignStoredMip(0, _blueTexture->getTexelFormat(), sizeof(data), data); + _permutationNormalTexture->setStoredMipFormat(_permutationNormalTexture->getTexelFormat()); + _permutationNormalTexture->assignStoredMip(0, sizeof(data), data); } return _permutationNormalTexture; } @@ -120,36 +131,40 @@ const unsigned char OPAQUE_BLACK[] = { 0x00, 0x00, 0x00, 0xFF }; const gpu::TexturePointer& TextureCache::getWhiteTexture() { if (!_whiteTexture) { - _whiteTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, 1, 1)); + _whiteTexture = gpu::TexturePointer(gpu::Texture::createStrict(gpu::Element::COLOR_RGBA_32, 1, 1)); _whiteTexture->setSource("TextureCache::_whiteTexture"); - _whiteTexture->assignStoredMip(0, _whiteTexture->getTexelFormat(), sizeof(OPAQUE_WHITE), OPAQUE_WHITE); + _whiteTexture->setStoredMipFormat(_whiteTexture->getTexelFormat()); + _whiteTexture->assignStoredMip(0, sizeof(OPAQUE_WHITE), OPAQUE_WHITE); } return _whiteTexture; } const gpu::TexturePointer& TextureCache::getGrayTexture() { if (!_grayTexture) { - _grayTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, 1, 1)); + _grayTexture = gpu::TexturePointer(gpu::Texture::createStrict(gpu::Element::COLOR_RGBA_32, 1, 1)); _grayTexture->setSource("TextureCache::_grayTexture"); - _grayTexture->assignStoredMip(0, _grayTexture->getTexelFormat(), sizeof(OPAQUE_GRAY), OPAQUE_GRAY); + _grayTexture->setStoredMipFormat(_grayTexture->getTexelFormat()); + _grayTexture->assignStoredMip(0, sizeof(OPAQUE_GRAY), OPAQUE_GRAY); } return _grayTexture; } const gpu::TexturePointer& TextureCache::getBlueTexture() { if (!_blueTexture) { - _blueTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, 1, 1)); + _blueTexture = gpu::TexturePointer(gpu::Texture::createStrict(gpu::Element::COLOR_RGBA_32, 1, 1)); _blueTexture->setSource("TextureCache::_blueTexture"); - _blueTexture->assignStoredMip(0, _blueTexture->getTexelFormat(), sizeof(OPAQUE_BLUE), OPAQUE_BLUE); + _blueTexture->setStoredMipFormat(_blueTexture->getTexelFormat()); + _blueTexture->assignStoredMip(0, sizeof(OPAQUE_BLUE), OPAQUE_BLUE); } return _blueTexture; } const gpu::TexturePointer& TextureCache::getBlackTexture() { if (!_blackTexture) { - _blackTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, 1, 1)); + _blackTexture = gpu::TexturePointer(gpu::Texture::createStrict(gpu::Element::COLOR_RGBA_32, 1, 1)); _blackTexture->setSource("TextureCache::_blackTexture"); - _blackTexture->assignStoredMip(0, _blackTexture->getTexelFormat(), sizeof(OPAQUE_BLACK), OPAQUE_BLACK); + _blackTexture->setStoredMipFormat(_blackTexture->getTexelFormat()); + _blackTexture->assignStoredMip(0, sizeof(OPAQUE_BLACK), OPAQUE_BLACK); } return _blackTexture; } @@ -173,6 +188,72 @@ NetworkTexturePointer TextureCache::getTexture(const QUrl& url, Type type, const return ResourceCache::getResource(url, QUrl(), &extra).staticCast(); } +gpu::TexturePointer TextureCache::getTextureByHash(const std::string& hash) { + std::weak_ptr weakPointer; + { + std::unique_lock lock(_texturesByHashesMutex); + weakPointer = _texturesByHashes[hash]; + } + auto result = weakPointer.lock(); + if (result) { + qCWarning(modelnetworking) << "QQQ Returning live texture for hash " << hash.c_str(); + } + return result; +} + +gpu::TexturePointer TextureCache::cacheTextureByHash(const std::string& hash, const gpu::TexturePointer& texture) { + gpu::TexturePointer result; + { + std::unique_lock lock(_texturesByHashesMutex); + result = _texturesByHashes[hash].lock(); + if (!result) { + _texturesByHashes[hash] = texture; + result = texture; + } else { + qCWarning(modelnetworking) << "QQQ Swapping out texture with previous live texture in hash " << hash.c_str(); + } + } + return result; +} + + +gpu::TexturePointer getFallbackTextureForType(NetworkTexture::Type type) { + gpu::TexturePointer result; + auto textureCache = DependencyManager::get(); + // Since this can be called on a background thread, there's a chance that the cache + // will be destroyed by the time we request it + if (!textureCache) { + return result; + } + switch (type) { + case NetworkTexture::DEFAULT_TEXTURE: + case NetworkTexture::ALBEDO_TEXTURE: + case NetworkTexture::ROUGHNESS_TEXTURE: + case NetworkTexture::OCCLUSION_TEXTURE: + result = textureCache->getWhiteTexture(); + break; + + case NetworkTexture::NORMAL_TEXTURE: + result = textureCache->getBlueTexture(); + break; + + case NetworkTexture::EMISSIVE_TEXTURE: + case NetworkTexture::LIGHTMAP_TEXTURE: + result = textureCache->getBlackTexture(); + break; + + case NetworkTexture::BUMP_TEXTURE: + case NetworkTexture::SPECULAR_TEXTURE: + case NetworkTexture::GLOSS_TEXTURE: + case NetworkTexture::CUBE_TEXTURE: + case NetworkTexture::CUSTOM_TEXTURE: + case NetworkTexture::STRICT_TEXTURE: + default: + break; + } + return result; +} + NetworkTexture::TextureLoaderFunc getTextureLoaderForType(NetworkTexture::Type type, const QVariantMap& options = QVariantMap()) { @@ -219,11 +300,16 @@ NetworkTexture::TextureLoaderFunc getTextureLoaderForType(NetworkTexture::Type t return model::TextureUsage::createMetallicTextureFromImage; break; } + case Type::STRICT_TEXTURE: { + return model::TextureUsage::createStrict2DTextureFromImage; + break; + } case Type::CUSTOM_TEXTURE: { Q_ASSERT(false); return NetworkTexture::TextureLoaderFunc(); break; } + case Type::DEFAULT_TEXTURE: default: { return model::TextureUsage::create2DTextureFromImage; @@ -245,8 +331,8 @@ QSharedPointer TextureCache::createResource(const QUrl& url, const QSh auto type = textureExtra ? textureExtra->type : Type::DEFAULT_TEXTURE; auto content = textureExtra ? textureExtra->content : QByteArray(); auto maxNumPixels = textureExtra ? textureExtra->maxNumPixels : ABSOLUTE_MAX_TEXTURE_NUM_PIXELS; - return QSharedPointer(new NetworkTexture(url, type, content, maxNumPixels), - &Resource::deleter); + NetworkTexture* texture = new NetworkTexture(url, type, content, maxNumPixels); + return QSharedPointer(texture, &Resource::deleter); } NetworkTexture::NetworkTexture(const QUrl& url, Type type, const QByteArray& content, int maxNumPixels) : @@ -260,7 +346,6 @@ NetworkTexture::NetworkTexture(const QUrl& url, Type type, const QByteArray& con _loaded = true; } - std::string theName = url.toString().toStdString(); // if we have content, load it after we have our self pointer if (!content.isEmpty()) { _startedLoading = true; @@ -268,12 +353,6 @@ NetworkTexture::NetworkTexture(const QUrl& url, Type type, const QByteArray& con } } -NetworkTexture::NetworkTexture(const QUrl& url, const TextureLoaderFunc& textureLoader, const QByteArray& content) : - NetworkTexture(url, CUSTOM_TEXTURE, content, ABSOLUTE_MAX_TEXTURE_NUM_PIXELS) -{ - _textureLoader = textureLoader; -} - NetworkTexture::TextureLoaderFunc NetworkTexture::getTextureLoader() const { if (_type == CUSTOM_TEXTURE) { return _textureLoader; @@ -281,149 +360,6 @@ NetworkTexture::TextureLoaderFunc NetworkTexture::getTextureLoader() const { return getTextureLoaderForType(_type); } - -class ImageReader : public QRunnable { -public: - - ImageReader(const QWeakPointer& resource, const QByteArray& data, - const QUrl& url = QUrl(), int maxNumPixels = ABSOLUTE_MAX_TEXTURE_NUM_PIXELS); - - virtual void run() override; - -private: - static void listSupportedImageFormats(); - - QWeakPointer _resource; - QUrl _url; - QByteArray _content; - int _maxNumPixels; -}; - -void NetworkTexture::downloadFinished(const QByteArray& data) { - // send the reader off to the thread pool - QThreadPool::globalInstance()->start(new ImageReader(_self, data, _url)); -} - -void NetworkTexture::loadContent(const QByteArray& content) { - QThreadPool::globalInstance()->start(new ImageReader(_self, content, _url, _maxNumPixels)); -} - -ImageReader::ImageReader(const QWeakPointer& resource, const QByteArray& data, - const QUrl& url, int maxNumPixels) : - _resource(resource), - _url(url), - _content(data), - _maxNumPixels(maxNumPixels) -{ -#if DEBUG_DUMP_TEXTURE_LOADS - static auto start = usecTimestampNow() / USECS_PER_MSEC; - auto now = usecTimestampNow() / USECS_PER_MSEC - start; - QString urlStr = _url.toString(); - auto dot = urlStr.lastIndexOf("."); - QString outFileName = QString(QCryptographicHash::hash(urlStr.toLocal8Bit(), QCryptographicHash::Md5).toHex()) + urlStr.right(urlStr.length() - dot); - QFile loadRecord("h:/textures/loads.txt"); - loadRecord.open(QFile::Text | QFile::Append | QFile::ReadWrite); - loadRecord.write(QString("%1 %2\n").arg(now).arg(outFileName).toLocal8Bit()); - outFileName = "h:/textures/" + outFileName; - QFileInfo outInfo(outFileName); - if (!outInfo.exists()) { - QFile outFile(outFileName); - outFile.open(QFile::WriteOnly | QFile::Truncate); - outFile.write(data); - outFile.close(); - } -#endif - DependencyManager::get()->incrementStat("PendingProcessing"); -} - -void ImageReader::listSupportedImageFormats() { - static std::once_flag once; - std::call_once(once, []{ - auto supportedFormats = QImageReader::supportedImageFormats(); - qCDebug(modelnetworking) << "List of supported Image formats:" << supportedFormats.join(", "); - }); -} - -void ImageReader::run() { - DependencyManager::get()->decrementStat("PendingProcessing"); - - CounterStat counter("Processing"); - - PROFILE_RANGE_EX(resource_parse_image, __FUNCTION__, 0xffff0000, 0, { { "url", _url.toString() } }); - auto originalPriority = QThread::currentThread()->priority(); - if (originalPriority == QThread::InheritPriority) { - originalPriority = QThread::NormalPriority; - } - QThread::currentThread()->setPriority(QThread::LowPriority); - Finally restorePriority([originalPriority]{ - QThread::currentThread()->setPriority(originalPriority); - }); - - if (!_resource.data()) { - qCWarning(modelnetworking) << "Abandoning load of" << _url << "; could not get strong ref"; - return; - } - listSupportedImageFormats(); - - // Help the QImage loader by extracting the image file format from the url filename ext. - // Some tga are not created properly without it. - auto filename = _url.fileName().toStdString(); - auto filenameExtension = filename.substr(filename.find_last_of('.') + 1); - QImage image = QImage::fromData(_content, filenameExtension.c_str()); - - // Note that QImage.format is the pixel format which is different from the "format" of the image file... - auto imageFormat = image.format(); - int imageWidth = image.width(); - int imageHeight = image.height(); - - if (imageWidth == 0 || imageHeight == 0 || imageFormat == QImage::Format_Invalid) { - if (filenameExtension.empty()) { - qCDebug(modelnetworking) << "QImage failed to create from content, no file extension:" << _url; - } else { - qCDebug(modelnetworking) << "QImage failed to create from content" << _url; - } - return; - } - - if (imageWidth * imageHeight > _maxNumPixels) { - float scaleFactor = sqrtf(_maxNumPixels / (float)(imageWidth * imageHeight)); - int originalWidth = imageWidth; - int originalHeight = imageHeight; - imageWidth = (int)(scaleFactor * (float)imageWidth + 0.5f); - imageHeight = (int)(scaleFactor * (float)imageHeight + 0.5f); - QImage newImage = image.scaled(QSize(imageWidth, imageHeight), Qt::IgnoreAspectRatio, Qt::SmoothTransformation); - image.swap(newImage); - qCDebug(modelnetworking) << "Downscale image" << _url - << "from" << originalWidth << "x" << originalHeight - << "to" << imageWidth << "x" << imageHeight; - } - - gpu::TexturePointer texture = nullptr; - { - // Double-check the resource still exists between long operations. - auto resource = _resource.toStrongRef(); - if (!resource) { - qCWarning(modelnetworking) << "Abandoning load of" << _url << "; could not get strong ref"; - return; - } - - auto url = _url.toString().toStdString(); - - PROFILE_RANGE_EX(resource_parse_image, __FUNCTION__, 0xffffff00, 0); - texture.reset(resource.dynamicCast()->getTextureLoader()(image, url)); - } - - // Ensure the resource has not been deleted - auto resource = _resource.toStrongRef(); - if (!resource) { - qCWarning(modelnetworking) << "Abandoning load of" << _url << "; could not get strong ref"; - } else { - QMetaObject::invokeMethod(resource.data(), "setImage", - Q_ARG(gpu::TexturePointer, texture), - Q_ARG(int, imageWidth), Q_ARG(int, imageHeight)); - } -} - void NetworkTexture::setImage(gpu::TexturePointer texture, int originalWidth, int originalHeight) { _originalWidth = originalWidth; @@ -446,3 +382,231 @@ void NetworkTexture::setImage(gpu::TexturePointer texture, int originalWidth, emit networkTextureCreated(qWeakPointerCast (_self)); } + +gpu::TexturePointer NetworkTexture::getFallbackTexture() const { + if (_type == CUSTOM_TEXTURE) { + return gpu::TexturePointer(); + } + return getFallbackTextureForType(_type); +} + +class Reader : public QRunnable { +public: + Reader(const QWeakPointer& resource, const QUrl& url); + void run() override final; + virtual void read() = 0; + +protected: + QWeakPointer _resource; + QUrl _url; +}; + +class ImageReader : public Reader { +public: + ImageReader(const QWeakPointer& resource, const QUrl& url, + const QByteArray& data, const std::string& hash, int maxNumPixels); + void read() override final; + +private: + static void listSupportedImageFormats(); + + QByteArray _content; + std::string _hash; + int _maxNumPixels; +}; + +void NetworkTexture::downloadFinished(const QByteArray& data) { + loadContent(data); +} + +void NetworkTexture::loadContent(const QByteArray& content) { + // Hash the source image to for KTX caching + std::string hash; + { + QCryptographicHash hasher(QCryptographicHash::Md5); + hasher.addData(content); + hash = hasher.result().toHex().toStdString(); + } + + auto textureCache = static_cast(_cache.data()); + + if (textureCache != nullptr) { + // If we already have a live texture with the same hash, use it + auto texture = textureCache->getTextureByHash(hash); + + // If there is no live texture, check if there's an existing KTX file + if (!texture) { + KTXFilePointer ktxFile = textureCache->_ktxCache.getFile(hash); + if (ktxFile) { + // Ensure that the KTX deserialization worked + auto ktx = ktxFile->getKTX(); + if (ktx) { + texture.reset(gpu::Texture::unserialize(ktx)); + // Ensure that the texture population worked + if (texture) { + texture->setKtxBacking(ktx); + texture = textureCache->cacheTextureByHash(hash, texture); + } + } + } + } + + // If we found the texture either because it's in use or via KTX deserialization, + // set the image and return immediately. + if (texture) { + setImage(texture, texture->getWidth(), texture->getHeight()); + return; + } + } + + // We failed to find an existing live or KTX texture, so trigger an image reader + QThreadPool::globalInstance()->start(new ImageReader(_self, _url, content, hash, _maxNumPixels)); +} + +Reader::Reader(const QWeakPointer& resource, const QUrl& url) : + _resource(resource), _url(url) { + DependencyManager::get()->incrementStat("PendingProcessing"); +} + +void Reader::run() { + PROFILE_RANGE_EX(resource_parse_image, __FUNCTION__, 0xffff0000, 0, { { "url", _url.toString() } }); + DependencyManager::get()->decrementStat("PendingProcessing"); + CounterStat counter("Processing"); + + auto originalPriority = QThread::currentThread()->priority(); + if (originalPriority == QThread::InheritPriority) { + originalPriority = QThread::NormalPriority; + } + QThread::currentThread()->setPriority(QThread::LowPriority); + Finally restorePriority([originalPriority]{ QThread::currentThread()->setPriority(originalPriority); }); + + if (!_resource.data()) { + qCWarning(modelnetworking) << "Abandoning load of" << _url << "; could not get strong ref"; + return; + } + + read(); +} + +ImageReader::ImageReader(const QWeakPointer& resource, const QUrl& url, + const QByteArray& data, const std::string& hash, int maxNumPixels) : + Reader(resource, url), _content(data), _hash(hash), _maxNumPixels(maxNumPixels) { + listSupportedImageFormats(); + +#if DEBUG_DUMP_TEXTURE_LOADS + static auto start = usecTimestampNow() / USECS_PER_MSEC; + auto now = usecTimestampNow() / USECS_PER_MSEC - start; + QString urlStr = _url.toString(); + auto dot = urlStr.lastIndexOf("."); + QString outFileName = QString(QCryptographicHash::hash(urlStr.toLocal8Bit(), QCryptographicHash::Md5).toHex()) + urlStr.right(urlStr.length() - dot); + QFile loadRecord("h:/textures/loads.txt"); + loadRecord.open(QFile::Text | QFile::Append | QFile::ReadWrite); + loadRecord.write(QString("%1 %2\n").arg(now).arg(outFileName).toLocal8Bit()); + outFileName = "h:/textures/" + outFileName; + QFileInfo outInfo(outFileName); + if (!outInfo.exists()) { + QFile outFile(outFileName); + outFile.open(QFile::WriteOnly | QFile::Truncate); + outFile.write(data); + outFile.close(); + } +#endif +} + +void ImageReader::listSupportedImageFormats() { + static std::once_flag once; + std::call_once(once, []{ + auto supportedFormats = QImageReader::supportedImageFormats(); + qCDebug(modelnetworking) << "List of supported Image formats:" << supportedFormats.join(", "); + }); +} + +void ImageReader::read() { + // Help the QImage loader by extracting the image file format from the url filename ext. + // Some tga are not created properly without it. + auto filename = _url.fileName().toStdString(); + auto filenameExtension = filename.substr(filename.find_last_of('.') + 1); + QImage image = QImage::fromData(_content, filenameExtension.c_str()); + int imageWidth = image.width(); + int imageHeight = image.height(); + + // Validate that the image loaded + if (imageWidth == 0 || imageHeight == 0 || image.format() == QImage::Format_Invalid) { + QString reason(filenameExtension.empty() ? "" : "(no file extension)"); + qCWarning(modelnetworking) << "Failed to load" << _url << reason; + return; + } + + // Validate the image is less than _maxNumPixels, and downscale if necessary + if (imageWidth * imageHeight > _maxNumPixels) { + float scaleFactor = sqrtf(_maxNumPixels / (float)(imageWidth * imageHeight)); + int originalWidth = imageWidth; + int originalHeight = imageHeight; + imageWidth = (int)(scaleFactor * (float)imageWidth + 0.5f); + imageHeight = (int)(scaleFactor * (float)imageHeight + 0.5f); + QImage newImage = image.scaled(QSize(imageWidth, imageHeight), Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + image.swap(newImage); + qCDebug(modelnetworking).nospace() << "Downscaled " << _url << " (" << + QSize(originalWidth, originalHeight) << " to " << + QSize(imageWidth, imageHeight) << ")"; + } + + gpu::TexturePointer texture = nullptr; + { + auto resource = _resource.lock(); // to ensure the resource is still needed + if (!resource) { + qCDebug(modelnetworking) << _url << "loading stopped; resource out of scope"; + return; + } + + auto url = _url.toString().toStdString(); + + PROFILE_RANGE_EX(resource_parse_image_raw, __FUNCTION__, 0xffff0000, 0); + // Load the image into a gpu::Texture + auto networkTexture = resource.staticCast(); + texture.reset(networkTexture->getTextureLoader()(image, url)); + texture->setSource(url); + if (texture) { + texture->setFallbackTexture(networkTexture->getFallbackTexture()); + } + + auto textureCache = DependencyManager::get(); + // Save the image into a KTXFile + auto memKtx = gpu::Texture::serialize(*texture); + if (!memKtx) { + qCWarning(modelnetworking) << "Unable to serialize texture to KTX " << _url; + } + + if (memKtx && textureCache) { + const char* data = reinterpret_cast(memKtx->_storage->data()); + size_t length = memKtx->_storage->size(); + KTXFilePointer file; + auto& ktxCache = textureCache->_ktxCache; + if (!memKtx || !(file = ktxCache.writeFile(data, KTXCache::Metadata(_hash, length)))) { + qCWarning(modelnetworking) << _url << "file cache failed"; + } else { + resource.staticCast()->_file = file; + auto fileKtx = file->getKTX(); + if (fileKtx) { + texture->setKtxBacking(fileKtx); + } + } + } + + // We replace the texture with the one stored in the cache. This deals with the possible race condition of two different + // images with the same hash being loaded concurrently. Only one of them will make it into the cache by hash first and will + // be the winner + if (textureCache) { + texture = textureCache->cacheTextureByHash(_hash, texture); + } + } + + auto resource = _resource.lock(); // to ensure the resource is still needed + if (resource) { + QMetaObject::invokeMethod(resource.data(), "setImage", + Q_ARG(gpu::TexturePointer, texture), + Q_ARG(int, imageWidth), Q_ARG(int, imageHeight)); + } else { + qCDebug(modelnetworking) << _url << "loading stopped; resource out of scope"; + } +} diff --git a/libraries/model-networking/src/model-networking/TextureCache.h b/libraries/model-networking/src/model-networking/TextureCache.h index 77311afae6..6005cc1226 100644 --- a/libraries/model-networking/src/model-networking/TextureCache.h +++ b/libraries/model-networking/src/model-networking/TextureCache.h @@ -23,6 +23,8 @@ #include #include +#include "KTXCache.h" + const int ABSOLUTE_MAX_TEXTURE_NUM_PIXELS = 8192 * 8192; namespace gpu { @@ -43,6 +45,7 @@ class NetworkTexture : public Resource, public Texture { public: enum Type { DEFAULT_TEXTURE, + STRICT_TEXTURE, ALBEDO_TEXTURE, NORMAL_TEXTURE, BUMP_TEXTURE, @@ -63,7 +66,6 @@ public: using TextureLoaderFunc = std::function; NetworkTexture(const QUrl& url, Type type, const QByteArray& content, int maxNumPixels); - NetworkTexture(const QUrl& url, const TextureLoaderFunc& textureLoader, const QByteArray& content); QString getType() const override { return "NetworkTexture"; } @@ -74,12 +76,12 @@ public: Type getTextureType() const { return _type; } TextureLoaderFunc getTextureLoader() const; + gpu::TexturePointer getFallbackTexture() const; signals: void networkTextureCreated(const QWeakPointer& self); protected: - virtual bool isCacheable() const override { return _loaded; } virtual void downloadFinished(const QByteArray& data) override; @@ -88,8 +90,12 @@ protected: Q_INVOKABLE void setImage(gpu::TexturePointer texture, int originalWidth, int originalHeight); private: + friend class KTXReader; + friend class ImageReader; + Type _type; TextureLoaderFunc _textureLoader { [](const QImage&, const std::string&){ return nullptr; } }; + KTXFilePointer _file; int _originalWidth { 0 }; int _originalHeight { 0 }; int _width { 0 }; @@ -131,6 +137,10 @@ public: NetworkTexturePointer getTexture(const QUrl& url, Type type = Type::DEFAULT_TEXTURE, const QByteArray& content = QByteArray(), int maxNumPixels = ABSOLUTE_MAX_TEXTURE_NUM_PIXELS); + + gpu::TexturePointer getTextureByHash(const std::string& hash); + gpu::TexturePointer cacheTextureByHash(const std::string& hash, const gpu::TexturePointer& texture); + protected: // Overload ResourceCache::prefetch to allow specifying texture type for loads Q_INVOKABLE ScriptableResource* prefetch(const QUrl& url, int type, int maxNumPixels = ABSOLUTE_MAX_TEXTURE_NUM_PIXELS); @@ -139,9 +149,19 @@ protected: const void* extra) override; private: + friend class ImageReader; + friend class NetworkTexture; + friend class DilatableNetworkTexture; + TextureCache(); virtual ~TextureCache(); - friend class DilatableNetworkTexture; + + static const std::string KTX_DIRNAME; + static const std::string KTX_EXT; + KTXCache _ktxCache; + // Map from image hashes to texture weak pointers + std::unordered_map> _texturesByHashes; + std::mutex _texturesByHashesMutex; gpu::TexturePointer _permutationNormalTexture; gpu::TexturePointer _whiteTexture; diff --git a/libraries/model/CMakeLists.txt b/libraries/model/CMakeLists.txt index 63f632e484..021aa3d027 100755 --- a/libraries/model/CMakeLists.txt +++ b/libraries/model/CMakeLists.txt @@ -1,5 +1,5 @@ set(TARGET_NAME model) AUTOSCRIBE_SHADER_LIB(gpu model) setup_hifi_library() -link_hifi_libraries(shared gpu) +link_hifi_libraries(shared ktx gpu) diff --git a/libraries/model/src/model/Geometry.cpp b/libraries/model/src/model/Geometry.cpp index 2bb6cfa436..04b0db92d3 100755 --- a/libraries/model/src/model/Geometry.cpp +++ b/libraries/model/src/model/Geometry.cpp @@ -117,7 +117,7 @@ Box Mesh::evalPartsBound(int partStart, int partEnd) const { auto partItEnd = _partBuffer.cbegin() + partEnd; for (;part != partItEnd; part++) { - + Box partBound; auto index = _indexBuffer.cbegin() + (*part)._startIndex; auto endIndex = index + (*part)._numIndices; @@ -134,6 +134,115 @@ Box Mesh::evalPartsBound(int partStart, int partEnd) const { return totalBound; } + +model::MeshPointer Mesh::map(std::function vertexFunc, + std::function normalFunc, + std::function indexFunc) { + // vertex data + const gpu::BufferView& vertexBufferView = getVertexBuffer(); + gpu::BufferView::Index numVertices = (gpu::BufferView::Index)getNumVertices(); + gpu::Resource::Size vertexSize = numVertices * sizeof(glm::vec3); + unsigned char* resultVertexData = new unsigned char[vertexSize]; + unsigned char* vertexDataCursor = resultVertexData; + + for (gpu::BufferView::Index i = 0; i < numVertices; i ++) { + glm::vec3 pos = vertexFunc(vertexBufferView.get(i)); + memcpy(vertexDataCursor, &pos, sizeof(pos)); + vertexDataCursor += sizeof(pos); + } + + // normal data + int attributeTypeNormal = gpu::Stream::InputSlot::NORMAL; // libraries/gpu/src/gpu/Stream.h + const gpu::BufferView& normalsBufferView = getAttributeBuffer(attributeTypeNormal); + gpu::BufferView::Index numNormals = (gpu::BufferView::Index)normalsBufferView.getNumElements(); + gpu::Resource::Size normalSize = numNormals * sizeof(glm::vec3); + unsigned char* resultNormalData = new unsigned char[normalSize]; + unsigned char* normalDataCursor = resultNormalData; + + for (gpu::BufferView::Index i = 0; i < numNormals; i ++) { + glm::vec3 normal = normalFunc(normalsBufferView.get(i)); + memcpy(normalDataCursor, &normal, sizeof(normal)); + normalDataCursor += sizeof(normal); + } + // TODO -- other attributes + + // face data + const gpu::BufferView& indexBufferView = getIndexBuffer(); + gpu::BufferView::Index numIndexes = (gpu::BufferView::Index)getNumIndices(); + gpu::Resource::Size indexSize = numIndexes * sizeof(uint32_t); + unsigned char* resultIndexData = new unsigned char[indexSize]; + unsigned char* indexDataCursor = resultIndexData; + + for (gpu::BufferView::Index i = 0; i < numIndexes; i ++) { + uint32_t index = indexFunc(indexBufferView.get(i)); + memcpy(indexDataCursor, &index, sizeof(index)); + indexDataCursor += sizeof(index); + } + + model::MeshPointer result(new model::Mesh()); + + gpu::Element vertexElement = gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ); + gpu::Buffer* resultVertexBuffer = new gpu::Buffer(vertexSize, resultVertexData); + gpu::BufferPointer resultVertexBufferPointer(resultVertexBuffer); + gpu::BufferView resultVertexBufferView(resultVertexBufferPointer, vertexElement); + result->setVertexBuffer(resultVertexBufferView); + + gpu::Element normalElement = gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ); + gpu::Buffer* resultNormalsBuffer = new gpu::Buffer(normalSize, resultNormalData); + gpu::BufferPointer resultNormalsBufferPointer(resultNormalsBuffer); + gpu::BufferView resultNormalsBufferView(resultNormalsBufferPointer, normalElement); + result->addAttribute(attributeTypeNormal, resultNormalsBufferView); + + gpu::Element indexElement = gpu::Element(gpu::SCALAR, gpu::UINT32, gpu::RAW); + gpu::Buffer* resultIndexesBuffer = new gpu::Buffer(indexSize, resultIndexData); + gpu::BufferPointer resultIndexesBufferPointer(resultIndexesBuffer); + gpu::BufferView resultIndexesBufferView(resultIndexesBufferPointer, indexElement); + result->setIndexBuffer(resultIndexesBufferView); + + + // TODO -- shouldn't assume just one part + + std::vector parts; + parts.emplace_back(model::Mesh::Part((model::Index)0, // startIndex + (model::Index)result->getNumIndices(), // numIndices + (model::Index)0, // baseVertex + model::Mesh::TRIANGLES)); // topology + result->setPartBuffer(gpu::BufferView(new gpu::Buffer(parts.size() * sizeof(model::Mesh::Part), + (gpu::Byte*) parts.data()), gpu::Element::PART_DRAWCALL)); + + return result; +} + + +void Mesh::forEach(std::function vertexFunc, + std::function normalFunc, + std::function indexFunc) { + int attributeTypeNormal = gpu::Stream::InputSlot::NORMAL; // libraries/gpu/src/gpu/Stream.h + + // vertex data + const gpu::BufferView& vertexBufferView = getVertexBuffer(); + gpu::BufferView::Index numVertices = (gpu::BufferView::Index)getNumVertices(); + for (gpu::BufferView::Index i = 0; i < numVertices; i ++) { + vertexFunc(vertexBufferView.get(i)); + } + + // normal data + const gpu::BufferView& normalsBufferView = getAttributeBuffer(attributeTypeNormal); + gpu::BufferView::Index numNormals = (gpu::BufferView::Index) normalsBufferView.getNumElements(); + for (gpu::BufferView::Index i = 0; i < numNormals; i ++) { + normalFunc(normalsBufferView.get(i)); + } + // TODO -- other attributes + + // face data + const gpu::BufferView& indexBufferView = getIndexBuffer(); + gpu::BufferView::Index numIndexes = (gpu::BufferView::Index)getNumIndices(); + for (gpu::BufferView::Index i = 0; i < numIndexes; i ++) { + indexFunc(indexBufferView.get(i)); + } +} + + Geometry::Geometry() { } @@ -148,4 +257,3 @@ Geometry::~Geometry() { void Geometry::setMesh(const MeshPointer& mesh) { _mesh = mesh; } - diff --git a/libraries/model/src/model/Geometry.h b/libraries/model/src/model/Geometry.h index 4256f0be03..7ba3e83407 100755 --- a/libraries/model/src/model/Geometry.h +++ b/libraries/model/src/model/Geometry.h @@ -25,6 +25,10 @@ typedef AABox Box; typedef std::vector< Box > Boxes; typedef glm::vec3 Vec3; +class Mesh; +using MeshPointer = std::shared_ptr< Mesh >; + + class Mesh { public: const static Index PRIMITIVE_RESTART_INDEX = -1; @@ -114,6 +118,15 @@ public: static gpu::Primitive topologyToPrimitive(Topology topo) { return static_cast(topo); } + // create a copy of this mesh after passing its vertices, normals, and indexes though the provided functions + MeshPointer map(std::function vertexFunc, + std::function normalFunc, + std::function indexFunc); + + void forEach(std::function vertexFunc, + std::function normalFunc, + std::function indexFunc); + protected: gpu::Stream::FormatPointer _vertexFormat; @@ -130,7 +143,6 @@ protected: void evalVertexStream(); }; -using MeshPointer = std::shared_ptr< Mesh >; class Geometry { diff --git a/libraries/model/src/model/TextureMap.cpp b/libraries/model/src/model/TextureMap.cpp index 7ac8083d9c..d07eae2166 100755 --- a/libraries/model/src/model/TextureMap.cpp +++ b/libraries/model/src/model/TextureMap.cpp @@ -10,10 +10,15 @@ // #include "TextureMap.h" +#include + #include #include #include - +#include +#include +#include +#include #include #include "ModelLogging.h" @@ -149,7 +154,7 @@ const QImage TextureUsage::process2DImageColor(const QImage& srcImage, bool& val return image; } -void TextureUsage::defineColorTexelFormats(gpu::Element& formatGPU, gpu::Element& formatMip, +void TextureUsage::defineColorTexelFormats(gpu::Element& formatGPU, gpu::Element& formatMip, const QImage& image, bool isLinear, bool doCompress) { #ifdef COMPRESS_TEXTURES @@ -202,7 +207,7 @@ const QImage& image, bool isLinear, bool doCompress) { #define CPU_MIPMAPS 1 -void generateMips(gpu::Texture* texture, QImage& image, gpu::Element formatMip, bool fastResize) { +void generateMips(gpu::Texture* texture, QImage& image, bool fastResize) { #if CPU_MIPMAPS PROFILE_RANGE(resource_parse, "generateMips"); auto numMips = texture->evalNumMips(); @@ -210,32 +215,33 @@ void generateMips(gpu::Texture* texture, QImage& image, gpu::Element formatMip, QSize mipSize(texture->evalMipWidth(level), texture->evalMipHeight(level)); if (fastResize) { image = image.scaled(mipSize); - texture->assignStoredMip(level, formatMip, image.byteCount(), image.constBits()); + texture->assignStoredMip(level, image.byteCount(), image.constBits()); } else { QImage mipImage = image.scaled(mipSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); - texture->assignStoredMip(level, formatMip, mipImage.byteCount(), mipImage.constBits()); + texture->assignStoredMip(level, mipImage.byteCount(), mipImage.constBits()); } } + #else texture->autoGenerateMips(-1); #endif } -void generateFaceMips(gpu::Texture* texture, QImage& image, gpu::Element formatMip, uint8 face) { +void generateFaceMips(gpu::Texture* texture, QImage& image, uint8 face) { #if CPU_MIPMAPS PROFILE_RANGE(resource_parse, "generateFaceMips"); auto numMips = texture->evalNumMips(); for (uint16 level = 1; level < numMips; ++level) { QSize mipSize(texture->evalMipWidth(level), texture->evalMipHeight(level)); QImage mipImage = image.scaled(mipSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); - texture->assignStoredMipFace(level, formatMip, mipImage.byteCount(), mipImage.constBits(), face); + texture->assignStoredMipFace(level, face, mipImage.byteCount(), mipImage.constBits()); } #else texture->autoGenerateMips(-1); #endif } -gpu::Texture* TextureUsage::process2DTextureColorFromImage(const QImage& srcImage, const std::string& srcImageName, bool isLinear, bool doCompress, bool generateMips) { +gpu::Texture* TextureUsage::process2DTextureColorFromImage(const QImage& srcImage, const std::string& srcImageName, bool isLinear, bool doCompress, bool generateMips, bool isStrict) { PROFILE_RANGE(resource_parse, "process2DTextureColorFromImage"); bool validAlpha = false; bool alphaAsMask = true; @@ -248,7 +254,11 @@ gpu::Texture* TextureUsage::process2DTextureColorFromImage(const QImage& srcImag gpu::Element formatMip; defineColorTexelFormats(formatGPU, formatMip, image, isLinear, doCompress); - theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); + if (isStrict) { + theTexture = (gpu::Texture::createStrict(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); + } else { + theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); + } theTexture->setSource(srcImageName); auto usage = gpu::Texture::Usage::Builder().withColor(); if (validAlpha) { @@ -258,22 +268,26 @@ gpu::Texture* TextureUsage::process2DTextureColorFromImage(const QImage& srcImag } } theTexture->setUsage(usage.build()); - - theTexture->assignStoredMip(0, formatMip, image.byteCount(), image.constBits()); + theTexture->setStoredMipFormat(formatMip); + theTexture->assignStoredMip(0, image.byteCount(), image.constBits()); if (generateMips) { - ::generateMips(theTexture, image, formatMip, false); + ::generateMips(theTexture, image, false); } + theTexture->setSource(srcImageName); } return theTexture; } +gpu::Texture* TextureUsage::createStrict2DTextureFromImage(const QImage& srcImage, const std::string& srcImageName) { + return process2DTextureColorFromImage(srcImage, srcImageName, false, false, true, true); +} + gpu::Texture* TextureUsage::create2DTextureFromImage(const QImage& srcImage, const std::string& srcImageName) { return process2DTextureColorFromImage(srcImage, srcImageName, false, false, true); } - gpu::Texture* TextureUsage::createAlbedoTextureFromImage(const QImage& srcImage, const std::string& srcImageName) { return process2DTextureColorFromImage(srcImage, srcImageName, false, true, true); } @@ -291,21 +305,25 @@ gpu::Texture* TextureUsage::createNormalTextureFromNormalImage(const QImage& src PROFILE_RANGE(resource_parse, "createNormalTextureFromNormalImage"); QImage image = processSourceImage(srcImage, false); - // Make sure the normal map source image is RGBA32 - if (image.format() != QImage::Format_RGBA8888) { - image = image.convertToFormat(QImage::Format_RGBA8888); + // Make sure the normal map source image is ARGB32 + if (image.format() != QImage::Format_ARGB32) { + image = image.convertToFormat(QImage::Format_ARGB32); } + gpu::Texture* theTexture = nullptr; if ((image.width() > 0) && (image.height() > 0)) { - gpu::Element formatGPU = gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA); - gpu::Element formatMip = gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA); + gpu::Element formatMip = gpu::Element::COLOR_BGRA_32; + gpu::Element formatGPU = gpu::Element::COLOR_RGBA_32; theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); theTexture->setSource(srcImageName); - theTexture->assignStoredMip(0, formatMip, image.byteCount(), image.constBits()); - generateMips(theTexture, image, formatMip, true); + theTexture->setStoredMipFormat(formatMip); + theTexture->assignStoredMip(0, image.byteCount(), image.constBits()); + generateMips(theTexture, image, true); + + theTexture->setSource(srcImageName); } return theTexture; @@ -336,16 +354,17 @@ gpu::Texture* TextureUsage::createNormalTextureFromBumpImage(const QImage& srcIm const double pStrength = 2.0; int width = image.width(); int height = image.height(); - QImage result(width, height, QImage::Format_RGB888); - + + QImage result(width, height, QImage::Format_ARGB32); + for (int i = 0; i < width; i++) { const int iNextClamped = clampPixelCoordinate(i + 1, width - 1); const int iPrevClamped = clampPixelCoordinate(i - 1, width - 1); - + for (int j = 0; j < height; j++) { const int jNextClamped = clampPixelCoordinate(j + 1, height - 1); const int jPrevClamped = clampPixelCoordinate(j - 1, height - 1); - + // surrounding pixels const QRgb topLeft = image.pixel(iPrevClamped, jPrevClamped); const QRgb top = image.pixel(iPrevClamped, j); @@ -355,7 +374,7 @@ gpu::Texture* TextureUsage::createNormalTextureFromBumpImage(const QImage& srcIm const QRgb bottom = image.pixel(iNextClamped, j); const QRgb bottomLeft = image.pixel(iNextClamped, jPrevClamped); const QRgb left = image.pixel(i, jPrevClamped); - + // take their gray intensities // since it's a grayscale image, the value of each component RGB is the same const double tl = qRed(topLeft); @@ -366,15 +385,15 @@ gpu::Texture* TextureUsage::createNormalTextureFromBumpImage(const QImage& srcIm const double b = qRed(bottom); const double bl = qRed(bottomLeft); const double l = qRed(left); - + // apply the sobel filter const double dX = (tr + pStrength * r + br) - (tl + pStrength * l + bl); const double dY = (bl + pStrength * b + br) - (tl + pStrength * t + tr); const double dZ = RGBA_MAX / pStrength; - + glm::vec3 v(dX, dY, dZ); glm::normalize(v); - + // convert to rgb from the value obtained computing the filter QRgb qRgbValue = qRgba(mapComponent(v.x), mapComponent(v.y), mapComponent(v.z), 1.0); result.setPixel(i, j, qRgbValue); @@ -382,13 +401,19 @@ gpu::Texture* TextureUsage::createNormalTextureFromBumpImage(const QImage& srcIm } gpu::Texture* theTexture = nullptr; - if ((image.width() > 0) && (image.height() > 0)) { - gpu::Element formatGPU = gpu::Element(gpu::VEC3, gpu::NUINT8, gpu::RGB); - gpu::Element formatMip = gpu::Element(gpu::VEC3, gpu::NUINT8, gpu::RGB); + if ((result.width() > 0) && (result.height() > 0)) { + + gpu::Element formatMip = gpu::Element::COLOR_BGRA_32; + gpu::Element formatGPU = gpu::Element::COLOR_RGBA_32; + + + theTexture = (gpu::Texture::create2D(formatGPU, result.width(), result.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); + theTexture->setSource(srcImageName); + theTexture->setStoredMipFormat(formatMip); + theTexture->assignStoredMip(0, result.byteCount(), result.constBits()); + generateMips(theTexture, result, true); - theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); theTexture->setSource(srcImageName); - theTexture->assignStoredMip(0, formatMip, image.byteCount(), image.constBits()); } return theTexture; @@ -414,16 +439,17 @@ gpu::Texture* TextureUsage::createRoughnessTextureFromImage(const QImage& srcIma #ifdef COMPRESS_TEXTURES gpu::Element formatGPU = gpu::Element(gpu::SCALAR, gpu::NUINT8, gpu::COMPRESSED_R); #else - gpu::Element formatGPU = gpu::Element(gpu::SCALAR, gpu::NUINT8, gpu::RGB); + gpu::Element formatGPU = gpu::Element::COLOR_R_8; #endif - gpu::Element formatMip = gpu::Element(gpu::SCALAR, gpu::NUINT8, gpu::RGB); + gpu::Element formatMip = gpu::Element::COLOR_R_8; theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); theTexture->setSource(srcImageName); - theTexture->assignStoredMip(0, formatMip, image.byteCount(), image.constBits()); - generateMips(theTexture, image, formatMip, true); + theTexture->setStoredMipFormat(formatMip); + theTexture->assignStoredMip(0, image.byteCount(), image.constBits()); + generateMips(theTexture, image, true); - // FIXME queue for transfer to GPU and block on completion + theTexture->setSource(srcImageName); } return theTexture; @@ -444,27 +470,28 @@ gpu::Texture* TextureUsage::createRoughnessTextureFromGlossImage(const QImage& s // Gloss turned into Rough image.invertPixels(QImage::InvertRgba); - + image = image.convertToFormat(QImage::Format_Grayscale8); - + gpu::Texture* theTexture = nullptr; if ((image.width() > 0) && (image.height() > 0)) { - + #ifdef COMPRESS_TEXTURES gpu::Element formatGPU = gpu::Element(gpu::SCALAR, gpu::NUINT8, gpu::COMPRESSED_R); #else - gpu::Element formatGPU = gpu::Element(gpu::SCALAR, gpu::NUINT8, gpu::RGB); + gpu::Element formatGPU = gpu::Element::COLOR_R_8; #endif - gpu::Element formatMip = gpu::Element(gpu::SCALAR, gpu::NUINT8, gpu::RGB); + gpu::Element formatMip = gpu::Element::COLOR_R_8; theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); theTexture->setSource(srcImageName); - theTexture->assignStoredMip(0, formatMip, image.byteCount(), image.constBits()); - generateMips(theTexture, image, formatMip, true); - - // FIXME queue for transfer to GPU and block on completion + theTexture->setStoredMipFormat(formatMip); + theTexture->assignStoredMip(0, image.byteCount(), image.constBits()); + generateMips(theTexture, image, true); + + theTexture->setSource(srcImageName); } - + return theTexture; } @@ -489,16 +516,17 @@ gpu::Texture* TextureUsage::createMetallicTextureFromImage(const QImage& srcImag #ifdef COMPRESS_TEXTURES gpu::Element formatGPU = gpu::Element(gpu::SCALAR, gpu::NUINT8, gpu::COMPRESSED_R); #else - gpu::Element formatGPU = gpu::Element(gpu::SCALAR, gpu::NUINT8, gpu::RGB); + gpu::Element formatGPU = gpu::Element::COLOR_R_8; #endif - gpu::Element formatMip = gpu::Element(gpu::SCALAR, gpu::NUINT8, gpu::RGB); + gpu::Element formatMip = gpu::Element::COLOR_R_8; theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); theTexture->setSource(srcImageName); - theTexture->assignStoredMip(0, formatMip, image.byteCount(), image.constBits()); - generateMips(theTexture, image, formatMip, true); + theTexture->setStoredMipFormat(formatMip); + theTexture->assignStoredMip(0, image.byteCount(), image.constBits()); + generateMips(theTexture, image, true); - // FIXME queue for transfer to GPU and block on completion + theTexture->setSource(srcImageName); } return theTexture; @@ -521,18 +549,18 @@ public: int _y = 0; bool _horizontalMirror = false; bool _verticalMirror = false; - + Face() {} Face(int x, int y, bool horizontalMirror, bool verticalMirror) : _x(x), _y(y), _horizontalMirror(horizontalMirror), _verticalMirror(verticalMirror) {} }; - + Face _faceXPos; Face _faceXNeg; Face _faceYPos; Face _faceYNeg; Face _faceZPos; Face _faceZNeg; - + CubeLayout(int wr, int hr, Face fXP, Face fXN, Face fYP, Face fYN, Face fZP, Face fZN) : _type(FLAT), _widthRatio(wr), @@ -775,7 +803,7 @@ gpu::Texture* TextureUsage::processCubeTextureColorFromImage(const QImage& srcIm defineColorTexelFormats(formatGPU, formatMip, image, isLinear, doCompress); // Find the layout of the cubemap in the 2D image - // Use the original image size since processSourceImage may have altered the size / aspect ratio + // Use the original image size since processSourceImage may have altered the size / aspect ratio int foundLayout = CubeLayout::findLayout(srcImage.width(), srcImage.height()); std::vector faces; @@ -810,11 +838,12 @@ gpu::Texture* TextureUsage::processCubeTextureColorFromImage(const QImage& srcIm if (faces.size() == gpu::Texture::NUM_FACES_PER_TYPE[gpu::Texture::TEX_CUBE]) { theTexture = gpu::Texture::createCube(formatGPU, faces[0].width(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP)); theTexture->setSource(srcImageName); + theTexture->setStoredMipFormat(formatMip); int f = 0; for (auto& face : faces) { - theTexture->assignStoredMipFace(0, formatMip, face.byteCount(), face.constBits(), f); + theTexture->assignStoredMipFace(0, f, face.byteCount(), face.constBits()); if (generateMips) { - generateFaceMips(theTexture, face, formatMip, f); + generateFaceMips(theTexture, face, f); } f++; } @@ -829,6 +858,8 @@ gpu::Texture* TextureUsage::processCubeTextureColorFromImage(const QImage& srcIm PROFILE_RANGE(resource_parse, "generateIrradiance"); theTexture->generateIrradiance(); } + + theTexture->setSource(srcImageName); } } diff --git a/libraries/model/src/model/TextureMap.h b/libraries/model/src/model/TextureMap.h index 220ee57a97..a4bb861502 100755 --- a/libraries/model/src/model/TextureMap.h +++ b/libraries/model/src/model/TextureMap.h @@ -32,6 +32,7 @@ public: int _environmentUsage = 0; static gpu::Texture* create2DTextureFromImage(const QImage& image, const std::string& srcImageName); + static gpu::Texture* createStrict2DTextureFromImage(const QImage& image, const std::string& srcImageName); static gpu::Texture* createAlbedoTextureFromImage(const QImage& image, const std::string& srcImageName); static gpu::Texture* createEmissiveTextureFromImage(const QImage& image, const std::string& srcImageName); static gpu::Texture* createNormalTextureFromNormalImage(const QImage& image, const std::string& srcImageName); @@ -47,7 +48,7 @@ public: static const QImage process2DImageColor(const QImage& srcImage, bool& validAlpha, bool& alphaAsMask); static void defineColorTexelFormats(gpu::Element& formatGPU, gpu::Element& formatMip, const QImage& srcImage, bool isLinear, bool doCompress); - static gpu::Texture* process2DTextureColorFromImage(const QImage& srcImage, const std::string& srcImageName, bool isLinear, bool doCompress, bool generateMips); + static gpu::Texture* process2DTextureColorFromImage(const QImage& srcImage, const std::string& srcImageName, bool isLinear, bool doCompress, bool generateMips, bool isStrict = false); static gpu::Texture* processCubeTextureColorFromImage(const QImage& srcImage, const std::string& srcImageName, bool isLinear, bool doCompress, bool generateMips, bool generateIrradiance); }; diff --git a/libraries/networking/src/Assignment.cpp b/libraries/networking/src/Assignment.cpp index 9efad15398..27d4a31ccf 100644 --- a/libraries/networking/src/Assignment.cpp +++ b/libraries/networking/src/Assignment.cpp @@ -12,7 +12,6 @@ #include "udt/PacketHeaders.h" #include "SharedUtil.h" #include "UUID.h" -#include "ServerPathUtils.h" #include diff --git a/libraries/networking/src/FileCache.cpp b/libraries/networking/src/FileCache.cpp new file mode 100644 index 0000000000..f8a86903cb --- /dev/null +++ b/libraries/networking/src/FileCache.cpp @@ -0,0 +1,243 @@ +// +// FileCache.cpp +// libraries/model-networking/src +// +// Created by Zach Pomerantz on 2/21/2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "FileCache.h" + +#include +#include +#include +#include + +#include + +#include + +Q_LOGGING_CATEGORY(file_cache, "hifi.file_cache", QtWarningMsg) + +using namespace cache; + +static const std::string MANIFEST_NAME = "manifest"; + +static const size_t BYTES_PER_MEGABYTES = 1024 * 1024; +static const size_t BYTES_PER_GIGABYTES = 1024 * BYTES_PER_MEGABYTES; +const size_t FileCache::DEFAULT_UNUSED_MAX_SIZE = 5 * BYTES_PER_GIGABYTES; // 5GB +const size_t FileCache::MAX_UNUSED_MAX_SIZE = 100 * BYTES_PER_GIGABYTES; // 100GB +const size_t FileCache::DEFAULT_OFFLINE_MAX_SIZE = 2 * BYTES_PER_GIGABYTES; // 2GB + +void FileCache::setUnusedFileCacheSize(size_t unusedFilesMaxSize) { + _unusedFilesMaxSize = std::min(unusedFilesMaxSize, MAX_UNUSED_MAX_SIZE); + reserve(0); + emit dirty(); +} + +void FileCache::setOfflineFileCacheSize(size_t offlineFilesMaxSize) { + _offlineFilesMaxSize = std::min(offlineFilesMaxSize, MAX_UNUSED_MAX_SIZE); +} + +FileCache::FileCache(const std::string& dirname, const std::string& ext, QObject* parent) : + QObject(parent), + _ext(ext), + _dirname(dirname), + _dirpath(PathUtils::getAppLocalDataFilePath(dirname.c_str()).toStdString()) {} + +FileCache::~FileCache() { + clear(); +} + +void fileDeleter(File* file) { + file->deleter(); +} + +void FileCache::initialize() { + QDir dir(_dirpath.c_str()); + + if (dir.exists()) { + auto nameFilters = QStringList(("*." + _ext).c_str()); + auto filters = QDir::Filters(QDir::NoDotAndDotDot | QDir::Files); + auto sort = QDir::SortFlags(QDir::Time); + auto files = dir.entryList(nameFilters, filters, sort); + + // load persisted files + foreach(QString filename, files) { + const Key key = filename.section('.', 0, 1).toStdString(); + const std::string filepath = dir.filePath(filename).toStdString(); + const size_t length = std::ifstream(filepath, std::ios::binary | std::ios::ate).tellg(); + addFile(Metadata(key, length), filepath); + } + + qCDebug(file_cache, "[%s] Initialized %s", _dirname.c_str(), _dirpath.c_str()); + } else { + dir.mkpath(_dirpath.c_str()); + qCDebug(file_cache, "[%s] Created %s", _dirname.c_str(), _dirpath.c_str()); + } + + _initialized = true; +} + +FilePointer FileCache::addFile(Metadata&& metadata, const std::string& filepath) { + FilePointer file(createFile(std::move(metadata), filepath).release(), &fileDeleter); + if (file) { + _numTotalFiles += 1; + _totalFilesSize += file->getLength(); + file->_cache = this; + emit dirty(); + + Lock lock(_filesMutex); + _files[file->getKey()] = file; + } + return file; +} + +FilePointer FileCache::writeFile(const char* data, File::Metadata&& metadata) { + assert(_initialized); + + std::string filepath = getFilepath(metadata.key); + + Lock lock(_filesMutex); + + // if file already exists, return it + FilePointer file = getFile(metadata.key); + if (file) { + qCWarning(file_cache, "[%s] Attempted to overwrite %s", _dirname.c_str(), metadata.key.c_str()); + return file; + } + + // write the new file + FILE* saveFile = fopen(filepath.c_str(), "wb"); + if (saveFile != nullptr && fwrite(data, metadata.length, 1, saveFile) && fclose(saveFile) == 0) { + file = addFile(std::move(metadata), filepath); + } else { + qCWarning(file_cache, "[%s] Failed to write %s (%s)", _dirname.c_str(), metadata.key.c_str(), strerror(errno)); + errno = 0; + } + + return file; +} + +FilePointer FileCache::getFile(const Key& key) { + assert(_initialized); + + FilePointer file; + + Lock lock(_filesMutex); + + // check if file exists + const auto it = _files.find(key); + if (it != _files.cend()) { + file = it->second.lock(); + if (file) { + // if it exists, it is active - remove it from the cache + removeUnusedFile(file); + qCDebug(file_cache, "[%s] Found %s", _dirname.c_str(), key.c_str()); + emit dirty(); + } else { + // if not, remove the weak_ptr + _files.erase(it); + } + } + + return file; +} + +std::string FileCache::getFilepath(const Key& key) { + return _dirpath + '/' + key + '.' + _ext; +} + +void FileCache::addUnusedFile(const FilePointer file) { + { + Lock lock(_filesMutex); + _files[file->getKey()] = file; + } + + reserve(file->getLength()); + file->_LRUKey = ++_lastLRUKey; + + { + Lock lock(_unusedFilesMutex); + _unusedFiles.insert({ file->_LRUKey, file }); + _numUnusedFiles += 1; + _unusedFilesSize += file->getLength(); + } + + emit dirty(); +} + +void FileCache::removeUnusedFile(const FilePointer file) { + Lock lock(_unusedFilesMutex); + const auto it = _unusedFiles.find(file->_LRUKey); + if (it != _unusedFiles.cend()) { + _unusedFiles.erase(it); + _numUnusedFiles -= 1; + _unusedFilesSize -= file->getLength(); + } +} + +void FileCache::reserve(size_t length) { + Lock unusedLock(_unusedFilesMutex); + while (!_unusedFiles.empty() && + _unusedFilesSize + length > _unusedFilesMaxSize) { + auto it = _unusedFiles.begin(); + auto file = it->second; + auto length = file->getLength(); + + unusedLock.unlock(); + { + file->_cache = nullptr; + Lock lock(_filesMutex); + _files.erase(file->getKey()); + } + unusedLock.lock(); + + _unusedFiles.erase(it); + _numTotalFiles -= 1; + _numUnusedFiles -= 1; + _totalFilesSize -= length; + _unusedFilesSize -= length; + } +} + +void FileCache::clear() { + Lock unusedFilesLock(_unusedFilesMutex); + for (const auto& pair : _unusedFiles) { + auto& file = pair.second; + file->_cache = nullptr; + + if (_totalFilesSize > _offlineFilesMaxSize) { + _totalFilesSize -= file->getLength(); + } else { + file->_shouldPersist = true; + qCDebug(file_cache, "[%s] Persisting %s", _dirname.c_str(), file->getKey().c_str()); + } + } + _unusedFiles.clear(); +} + +void File::deleter() { + if (_cache) { + FilePointer self(this, &fileDeleter); + _cache->addUnusedFile(self); + } else { + deleteLater(); + } +} + +File::File(Metadata&& metadata, const std::string& filepath) : + _key(std::move(metadata.key)), + _length(metadata.length), + _filepath(filepath) {} + +File::~File() { + QFile file(getFilepath().c_str()); + if (file.exists() && !_shouldPersist) { + qCInfo(file_cache, "Unlinked %s", getFilepath().c_str()); + file.remove(); + } +} diff --git a/libraries/networking/src/FileCache.h b/libraries/networking/src/FileCache.h new file mode 100644 index 0000000000..f77db555bc --- /dev/null +++ b/libraries/networking/src/FileCache.h @@ -0,0 +1,158 @@ +// +// FileCache.h +// libraries/networking/src +// +// Created by Zach Pomerantz on 2/21/2017. +// Copyright 2017 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_FileCache_h +#define hifi_FileCache_h + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +Q_DECLARE_LOGGING_CATEGORY(file_cache) + +namespace cache { + +class File; +using FilePointer = std::shared_ptr; + +class FileCache : public QObject { + Q_OBJECT + Q_PROPERTY(size_t numTotal READ getNumTotalFiles NOTIFY dirty) + Q_PROPERTY(size_t numCached READ getNumCachedFiles NOTIFY dirty) + Q_PROPERTY(size_t sizeTotal READ getSizeTotalFiles NOTIFY dirty) + Q_PROPERTY(size_t sizeCached READ getSizeCachedFiles NOTIFY dirty) + + static const size_t DEFAULT_UNUSED_MAX_SIZE; + static const size_t MAX_UNUSED_MAX_SIZE; + static const size_t DEFAULT_OFFLINE_MAX_SIZE; + +public: + size_t getNumTotalFiles() const { return _numTotalFiles; } + size_t getNumCachedFiles() const { return _numUnusedFiles; } + size_t getSizeTotalFiles() const { return _totalFilesSize; } + size_t getSizeCachedFiles() const { return _unusedFilesSize; } + + void setUnusedFileCacheSize(size_t unusedFilesMaxSize); + size_t getUnusedFileCacheSize() const { return _unusedFilesSize; } + + void setOfflineFileCacheSize(size_t offlineFilesMaxSize); + + // initialize FileCache with a directory name (not a path, ex.: "temp_jpgs") and an ext (ex.: "jpg") + FileCache(const std::string& dirname, const std::string& ext, QObject* parent = nullptr); + virtual ~FileCache(); + + using Key = std::string; + struct Metadata { + Metadata(const Key& key, size_t length) : + key(key), length(length) {} + Key key; + size_t length; + }; + + // derived classes should implement a setter/getter, for example, for a FileCache backing a network cache: + // + // DerivedFilePointer writeFile(const char* data, DerivedMetadata&& metadata) { + // return writeFile(data, std::forward(metadata)); + // } + // + // DerivedFilePointer getFile(const QUrl& url) { + // auto key = lookup_hash_for(url); // assuming hashing url in create/evictedFile overrides + // return getFile(key); + // } + +signals: + void dirty(); + +protected: + /// must be called after construction to create the cache on the fs and restore persisted files + void initialize(); + + FilePointer writeFile(const char* data, Metadata&& metadata); + FilePointer getFile(const Key& key); + + /// create a file + virtual std::unique_ptr createFile(Metadata&& metadata, const std::string& filepath) = 0; + +private: + using Mutex = std::recursive_mutex; + using Lock = std::unique_lock; + + friend class File; + + std::string getFilepath(const Key& key); + + FilePointer addFile(Metadata&& metadata, const std::string& filepath); + void addUnusedFile(const FilePointer file); + void removeUnusedFile(const FilePointer file); + void reserve(size_t length); + void clear(); + + std::atomic _numTotalFiles { 0 }; + std::atomic _numUnusedFiles { 0 }; + std::atomic _totalFilesSize { 0 }; + std::atomic _unusedFilesSize { 0 }; + + std::string _ext; + std::string _dirname; + std::string _dirpath; + bool _initialized { false }; + + std::unordered_map> _files; + Mutex _filesMutex; + + std::map _unusedFiles; + Mutex _unusedFilesMutex; + size_t _unusedFilesMaxSize { DEFAULT_UNUSED_MAX_SIZE }; + int _lastLRUKey { 0 }; + + size_t _offlineFilesMaxSize { DEFAULT_OFFLINE_MAX_SIZE }; +}; + +class File : public QObject { + Q_OBJECT + +public: + using Key = FileCache::Key; + using Metadata = FileCache::Metadata; + + Key getKey() const { return _key; } + size_t getLength() const { return _length; } + std::string getFilepath() const { return _filepath; } + + virtual ~File(); + /// overrides should call File::deleter to maintain caching behavior + virtual void deleter(); + +protected: + /// when constructed, the file has already been created/written + File(Metadata&& metadata, const std::string& filepath); + +private: + friend class FileCache; + + const Key _key; + const size_t _length; + const std::string _filepath; + + FileCache* _cache; + int _LRUKey { 0 }; + + bool _shouldPersist { false }; +}; + +} + +#endif // hifi_FileCache_h diff --git a/libraries/networking/src/NodePermissions.h b/libraries/networking/src/NodePermissions.h index 5d2755f9b5..6fa005e360 100644 --- a/libraries/networking/src/NodePermissions.h +++ b/libraries/networking/src/NodePermissions.h @@ -13,18 +13,31 @@ #define hifi_NodePermissions_h #include +#include #include #include #include #include - +#include +#include #include "GroupRank.h" class NodePermissions; using NodePermissionsPointer = std::shared_ptr; -using NodePermissionsKey = QPair; // name, rankID +using NodePermissionsKey = std::pair; // name, rankID using NodePermissionsKeyList = QList>; +namespace std { + template<> + struct hash { + size_t operator()(const NodePermissionsKey& key) const { + size_t result = qHash(key.first); + result <<= 32; + result |= qHash(key.second); + return result; + } + }; +} class NodePermissions { public: @@ -100,27 +113,40 @@ public: NodePermissionsMap() { } NodePermissionsPointer& operator[](const NodePermissionsKey& key) { NodePermissionsKey dataKey(key.first.toLower(), key.second); - if (!_data.contains(dataKey)) { + if (0 == _data.count(dataKey)) { _data[dataKey] = NodePermissionsPointer(new NodePermissions(key)); } return _data[dataKey]; } NodePermissionsPointer operator[](const NodePermissionsKey& key) const { - return _data.value(NodePermissionsKey(key.first.toLower(), key.second)); + NodePermissionsPointer result; + auto itr = _data.find(NodePermissionsKey(key.first.toLower(), key.second)); + if (_data.end() != itr) { + result = itr->second; + } + return result; } bool contains(const NodePermissionsKey& key) const { - return _data.contains(NodePermissionsKey(key.first.toLower(), key.second)); + return 0 != _data.count(NodePermissionsKey(key.first.toLower(), key.second)); } - bool contains(const QString& keyFirst, QUuid keySecond) const { - return _data.contains(NodePermissionsKey(keyFirst.toLower(), keySecond)); + bool contains(const QString& keyFirst, const QUuid& keySecond) const { + return 0 != _data.count(NodePermissionsKey(keyFirst.toLower(), keySecond)); } - QList keys() const { return _data.keys(); } - QHash get() { return _data; } + + QList keys() const { + QList result; + for (const auto& entry : _data) { + result.push_back(entry.first); + } + return result; + } + + const std::unordered_map& get() { return _data; } void clear() { _data.clear(); } - void remove(const NodePermissionsKey& key) { _data.remove(key); } + void remove(const NodePermissionsKey& key) { _data.erase(key); } private: - QHash _data; + std::unordered_map _data; }; diff --git a/libraries/networking/src/udt/PacketQueue.cpp b/libraries/networking/src/udt/PacketQueue.cpp index bb20982ca4..9560f2f187 100644 --- a/libraries/networking/src/udt/PacketQueue.cpp +++ b/libraries/networking/src/udt/PacketQueue.cpp @@ -15,6 +15,10 @@ using namespace udt; +PacketQueue::PacketQueue() { + _channels.emplace_back(new std::list()); +} + MessageNumber PacketQueue::getNextMessageNumber() { static const MessageNumber MAX_MESSAGE_NUMBER = MessageNumber(1) << MESSAGE_NUMBER_SIZE; _currentMessageNumber = (_currentMessageNumber + 1) % MAX_MESSAGE_NUMBER; @@ -24,7 +28,7 @@ MessageNumber PacketQueue::getNextMessageNumber() { bool PacketQueue::isEmpty() const { LockGuard locker(_packetsLock); // Only the main channel and it is empty - return (_channels.size() == 1) && _channels.front().empty(); + return (_channels.size() == 1) && _channels.front()->empty(); } PacketQueue::PacketPointer PacketQueue::takePacket() { @@ -34,19 +38,19 @@ PacketQueue::PacketPointer PacketQueue::takePacket() { } // Find next non empty channel - if (_channels[nextIndex()].empty()) { + if (_channels[nextIndex()]->empty()) { nextIndex(); } auto& channel = _channels[_currentIndex]; - Q_ASSERT(!channel.empty()); + Q_ASSERT(!channel->empty()); // Take front packet - auto packet = std::move(channel.front()); - channel.pop_front(); + auto packet = std::move(channel->front()); + channel->pop_front(); // Remove now empty channel (Don't remove the main channel) - if (channel.empty() && _currentIndex != 0) { - channel.swap(_channels.back()); + if (channel->empty() && _currentIndex != 0) { + channel->swap(*_channels.back()); _channels.pop_back(); --_currentIndex; } @@ -61,7 +65,7 @@ unsigned int PacketQueue::nextIndex() { void PacketQueue::queuePacket(PacketPointer packet) { LockGuard locker(_packetsLock); - _channels.front().push_back(std::move(packet)); + _channels.front()->push_back(std::move(packet)); } void PacketQueue::queuePacketList(PacketListPointer packetList) { @@ -70,5 +74,6 @@ void PacketQueue::queuePacketList(PacketListPointer packetList) { } LockGuard locker(_packetsLock); - _channels.push_back(std::move(packetList->_packets)); + _channels.emplace_back(new std::list()); + _channels.back()->swap(packetList->_packets); } diff --git a/libraries/networking/src/udt/PacketQueue.h b/libraries/networking/src/udt/PacketQueue.h index 69784fd8db..2b3d3a4b5b 100644 --- a/libraries/networking/src/udt/PacketQueue.h +++ b/libraries/networking/src/udt/PacketQueue.h @@ -30,10 +30,11 @@ class PacketQueue { using LockGuard = std::lock_guard; using PacketPointer = std::unique_ptr; using PacketListPointer = std::unique_ptr; - using Channel = std::list; + using Channel = std::unique_ptr>; using Channels = std::vector; public: + PacketQueue(); void queuePacket(PacketPointer packet); void queuePacketList(PacketListPointer packetList); @@ -49,7 +50,7 @@ private: MessageNumber _currentMessageNumber { 0 }; mutable Mutex _packetsLock; // Protects the packets to be sent. - Channels _channels = Channels(1); // One channel per packet list + Main channel + Channels _channels; // One channel per packet list + Main channel unsigned int _currentIndex { 0 }; }; diff --git a/libraries/octree/src/OctreeQuery.cpp b/libraries/octree/src/OctreeQuery.cpp index a639eccaba..7d9fc7d08c 100644 --- a/libraries/octree/src/OctreeQuery.cpp +++ b/libraries/octree/src/OctreeQuery.cpp @@ -142,6 +142,6 @@ int OctreeQuery::parseData(ReceivedMessage& message) { } glm::vec3 OctreeQuery::calculateCameraDirection() const { - glm::vec3 direction = glm::vec3(_cameraOrientation * glm::vec4(IDENTITY_FRONT, 0.0f)); + glm::vec3 direction = glm::vec3(_cameraOrientation * glm::vec4(IDENTITY_FORWARD, 0.0f)); return direction; } diff --git a/libraries/physics/src/EntityMotionState.cpp b/libraries/physics/src/EntityMotionState.cpp index c175a836cc..d383f4c199 100644 --- a/libraries/physics/src/EntityMotionState.cpp +++ b/libraries/physics/src/EntityMotionState.cpp @@ -97,6 +97,21 @@ void EntityMotionState::updateServerPhysicsVariables() { _serverActionData = _entity->getActionData(); } +void EntityMotionState::handleDeactivation() { + // copy _server data to entity + bool success; + _entity->setPosition(_serverPosition, success, false); + _entity->setOrientation(_serverRotation, success, false); + _entity->setVelocity(ENTITY_ITEM_ZERO_VEC3); + _entity->setAngularVelocity(ENTITY_ITEM_ZERO_VEC3); + // and also to RigidBody + btTransform worldTrans; + worldTrans.setOrigin(glmToBullet(_serverPosition)); + worldTrans.setRotation(glmToBullet(_serverRotation)); + _body->setWorldTransform(worldTrans); + // no need to update velocities... should already be zero +} + // virtual void EntityMotionState::handleEasyChanges(uint32_t& flags) { assert(entityTreeIsLocked()); @@ -111,6 +126,8 @@ void EntityMotionState::handleEasyChanges(uint32_t& flags) { flags &= ~Simulation::DIRTY_PHYSICS_ACTIVATION; _body->setActivationState(WANTS_DEACTIVATION); _outgoingPriority = 0; + const float ACTIVATION_EXPIRY = 3.0f; // something larger than the 2.0 hard coded in Bullet + _body->setDeactivationTime(ACTIVATION_EXPIRY); } else { // disowned object is still moving --> start timer for ownership bid // TODO? put a delay in here proportional to distance from object? @@ -221,12 +238,9 @@ void EntityMotionState::getWorldTransform(btTransform& worldTrans) const { } // This callback is invoked by the physics simulation at the end of each simulation step... -// iff the corresponding RigidBody is DYNAMIC and has moved. +// iff the corresponding RigidBody is DYNAMIC and ACTIVE. void EntityMotionState::setWorldTransform(const btTransform& worldTrans) { - if (!_entity) { - return; - } - + assert(_entity); assert(entityTreeIsLocked()); measureBodyAcceleration(); bool positionSuccess; diff --git a/libraries/physics/src/EntityMotionState.h b/libraries/physics/src/EntityMotionState.h index feac47d8ec..380edf3927 100644 --- a/libraries/physics/src/EntityMotionState.h +++ b/libraries/physics/src/EntityMotionState.h @@ -29,6 +29,7 @@ public: virtual ~EntityMotionState(); void updateServerPhysicsVariables(); + void handleDeactivation(); virtual void handleEasyChanges(uint32_t& flags) override; virtual bool handleHardAndEasyChanges(uint32_t& flags, PhysicsEngine* engine) override; diff --git a/libraries/physics/src/PhysicalEntitySimulation.cpp b/libraries/physics/src/PhysicalEntitySimulation.cpp index 903b160a5e..bd76b2d70f 100644 --- a/libraries/physics/src/PhysicalEntitySimulation.cpp +++ b/libraries/physics/src/PhysicalEntitySimulation.cpp @@ -259,13 +259,27 @@ void PhysicalEntitySimulation::getObjectsToChange(VectorOfMotionStates& result) _pendingChanges.clear(); } -void PhysicalEntitySimulation::handleOutgoingChanges(const VectorOfMotionStates& motionStates) { +void PhysicalEntitySimulation::handleDeactivatedMotionStates(const VectorOfMotionStates& motionStates) { + for (auto stateItr : motionStates) { + ObjectMotionState* state = &(*stateItr); + assert(state); + if (state->getType() == MOTIONSTATE_TYPE_ENTITY) { + EntityMotionState* entityState = static_cast(state); + entityState->handleDeactivation(); + EntityItemPointer entity = entityState->getEntity(); + _entitiesToSort.insert(entity); + } + } +} + +void PhysicalEntitySimulation::handleChangedMotionStates(const VectorOfMotionStates& motionStates) { QMutexLocker lock(&_mutex); // walk the motionStates looking for those that correspond to entities for (auto stateItr : motionStates) { ObjectMotionState* state = &(*stateItr); - if (state && state->getType() == MOTIONSTATE_TYPE_ENTITY) { + assert(state); + if (state->getType() == MOTIONSTATE_TYPE_ENTITY) { EntityMotionState* entityState = static_cast(state); EntityItemPointer entity = entityState->getEntity(); assert(entity.get()); diff --git a/libraries/physics/src/PhysicalEntitySimulation.h b/libraries/physics/src/PhysicalEntitySimulation.h index af5def9775..5f6185add3 100644 --- a/libraries/physics/src/PhysicalEntitySimulation.h +++ b/libraries/physics/src/PhysicalEntitySimulation.h @@ -56,7 +56,8 @@ public: void setObjectsToChange(const VectorOfMotionStates& objectsToChange); void getObjectsToChange(VectorOfMotionStates& result); - void handleOutgoingChanges(const VectorOfMotionStates& motionStates); + void handleDeactivatedMotionStates(const VectorOfMotionStates& motionStates); + void handleChangedMotionStates(const VectorOfMotionStates& motionStates); void handleCollisionEvents(const CollisionEvents& collisionEvents); EntityEditPacketSender* getPacketSender() { return _entityPacketSender; } @@ -67,7 +68,7 @@ private: SetOfEntities _entitiesToAddToPhysics; SetOfEntityMotionStates _pendingChanges; // EntityMotionStates already in PhysicsEngine that need their physics changed - SetOfEntityMotionStates _outgoingChanges; // EntityMotionStates for which we need to send updates to entity-server + SetOfEntityMotionStates _outgoingChanges; // EntityMotionStates for which we may need to send updates to entity-server SetOfMotionStates _physicalObjects; // MotionStates of entities in PhysicsEngine diff --git a/libraries/physics/src/PhysicsEngine.cpp b/libraries/physics/src/PhysicsEngine.cpp index 363887de25..a8a8e6acfd 100644 --- a/libraries/physics/src/PhysicsEngine.cpp +++ b/libraries/physics/src/PhysicsEngine.cpp @@ -472,7 +472,7 @@ const CollisionEvents& PhysicsEngine::getCollisionEvents() { return _collisionEvents; } -const VectorOfMotionStates& PhysicsEngine::getOutgoingChanges() { +const VectorOfMotionStates& PhysicsEngine::getChangedMotionStates() { BT_PROFILE("copyOutgoingChanges"); // Bullet will not deactivate static objects (it doesn't expect them to be active) // so we must deactivate them ourselves diff --git a/libraries/physics/src/PhysicsEngine.h b/libraries/physics/src/PhysicsEngine.h index bbafbb06b6..b2ebe58f08 100644 --- a/libraries/physics/src/PhysicsEngine.h +++ b/libraries/physics/src/PhysicsEngine.h @@ -65,7 +65,8 @@ public: bool hasOutgoingChanges() const { return _hasOutgoingChanges; } /// \return reference to list of changed MotionStates. The list is only valid until beginning of next simulation loop. - const VectorOfMotionStates& getOutgoingChanges(); + const VectorOfMotionStates& getChangedMotionStates(); + const VectorOfMotionStates& getDeactivatedMotionStates() const { return _dynamicsWorld->getDeactivatedMotionStates(); } /// \return reference to list of Collision events. The list is only valid until beginning of next simulation loop. const CollisionEvents& getCollisionEvents(); diff --git a/libraries/physics/src/ThreadSafeDynamicsWorld.cpp b/libraries/physics/src/ThreadSafeDynamicsWorld.cpp index 5fe99f137c..24cfbc2609 100644 --- a/libraries/physics/src/ThreadSafeDynamicsWorld.cpp +++ b/libraries/physics/src/ThreadSafeDynamicsWorld.cpp @@ -120,30 +120,41 @@ void ThreadSafeDynamicsWorld::synchronizeMotionState(btRigidBody* body) { void ThreadSafeDynamicsWorld::synchronizeMotionStates() { BT_PROFILE("synchronizeMotionStates"); _changedMotionStates.clear(); + + // NOTE: m_synchronizeAllMotionStates is 'false' by default for optimization. + // See PhysicsEngine::init() where we call _dynamicsWorld->setForceUpdateAllAabbs(false) if (m_synchronizeAllMotionStates) { //iterate over all collision objects for (int i=0;igetMotionState()) { - synchronizeMotionState(body); - _changedMotionStates.push_back(static_cast(body->getMotionState())); - } + if (body && body->getMotionState()) { + synchronizeMotionState(body); + _changedMotionStates.push_back(static_cast(body->getMotionState())); } } } else { //iterate over all active rigid bodies + // TODO? if this becomes a performance bottleneck we could derive our own SimulationIslandManager + // that remembers a list of objects deactivated last step + _activeStates.clear(); + _deactivatedStates.clear(); for (int i=0;iisActive()) { - if (body->getMotionState()) { + ObjectMotionState* motionState = static_cast(body->getMotionState()); + if (motionState) { + if (body->isActive()) { synchronizeMotionState(body); - _changedMotionStates.push_back(static_cast(body->getMotionState())); + _changedMotionStates.push_back(motionState); + _activeStates.insert(motionState); + } else if (_lastActiveStates.find(motionState) != _lastActiveStates.end()) { + // this object was active last frame but is no longer + _deactivatedStates.push_back(motionState); } } } } + _activeStates.swap(_lastActiveStates); } void ThreadSafeDynamicsWorld::saveKinematicState(btScalar timeStep) { diff --git a/libraries/physics/src/ThreadSafeDynamicsWorld.h b/libraries/physics/src/ThreadSafeDynamicsWorld.h index 68062d8d29..b4fcca8cdb 100644 --- a/libraries/physics/src/ThreadSafeDynamicsWorld.h +++ b/libraries/physics/src/ThreadSafeDynamicsWorld.h @@ -49,12 +49,16 @@ public: float getLocalTimeAccumulation() const { return m_localTime; } const VectorOfMotionStates& getChangedMotionStates() const { return _changedMotionStates; } + const VectorOfMotionStates& getDeactivatedMotionStates() const { return _deactivatedStates; } private: // call this instead of non-virtual btDiscreteDynamicsWorld::synchronizeSingleMotionState() void synchronizeMotionState(btRigidBody* body); VectorOfMotionStates _changedMotionStates; + VectorOfMotionStates _deactivatedStates; + SetOfMotionStates _activeStates; + SetOfMotionStates _lastActiveStates; }; #endif // hifi_ThreadSafeDynamicsWorld_h diff --git a/libraries/recording/src/recording/Deck.cpp b/libraries/recording/src/recording/Deck.cpp index 61eb86c91f..186516e01c 100644 --- a/libraries/recording/src/recording/Deck.cpp +++ b/libraries/recording/src/recording/Deck.cpp @@ -33,6 +33,7 @@ void Deck::queueClip(ClipPointer clip, float timeOffset) { // FIXME disabling multiple clips for now _clips.clear(); + _length = 0.0f; // if the time offset is not zero, wrap in an OffsetClip if (timeOffset != 0.0f) { @@ -153,8 +154,8 @@ void Deck::processFrames() { // if doing relative movement emit looped(); } else { - // otherwise pause playback - pause(); + // otherwise stop playback + stop(); } return; } diff --git a/libraries/render-utils/CMakeLists.txt b/libraries/render-utils/CMakeLists.txt index ecafb8f565..3bf389973a 100644 --- a/libraries/render-utils/CMakeLists.txt +++ b/libraries/render-utils/CMakeLists.txt @@ -3,7 +3,7 @@ AUTOSCRIBE_SHADER_LIB(gpu model render) # pull in the resources.qrc file qt5_add_resources(QT_RESOURCES_FILE "${CMAKE_CURRENT_SOURCE_DIR}/res/fonts/fonts.qrc") setup_hifi_library(Widgets OpenGL Network Qml Quick Script) -link_hifi_libraries(shared gpu model model-networking render animation fbx entities) +link_hifi_libraries(shared ktx gpu model model-networking render animation fbx entities) if (NOT ANDROID) target_nsight() diff --git a/libraries/render-utils/src/AntialiasingEffect.cpp b/libraries/render-utils/src/AntialiasingEffect.cpp index 2941197e6d..f95d45de04 100644 --- a/libraries/render-utils/src/AntialiasingEffect.cpp +++ b/libraries/render-utils/src/AntialiasingEffect.cpp @@ -52,7 +52,7 @@ const gpu::PipelinePointer& Antialiasing::getAntialiasingPipeline() { _antialiasingBuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("antialiasing")); auto format = gpu::Element::COLOR_SRGBA_32; // DependencyManager::get()->getLightingTexture()->getTexelFormat(); auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT); - _antialiasingTexture = gpu::TexturePointer(gpu::Texture::create2D(format, width, height, defaultSampler)); + _antialiasingTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(format, width, height, defaultSampler)); _antialiasingBuffer->setRenderBuffer(0, _antialiasingTexture); } diff --git a/libraries/render-utils/src/DeferredFramebuffer.cpp b/libraries/render-utils/src/DeferredFramebuffer.cpp index e8783e0e0d..40c22beba4 100644 --- a/libraries/render-utils/src/DeferredFramebuffer.cpp +++ b/libraries/render-utils/src/DeferredFramebuffer.cpp @@ -53,9 +53,9 @@ void DeferredFramebuffer::allocate() { auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT); - _deferredColorTexture = gpu::TexturePointer(gpu::Texture::create2D(colorFormat, width, height, defaultSampler)); - _deferredNormalTexture = gpu::TexturePointer(gpu::Texture::create2D(linearFormat, width, height, defaultSampler)); - _deferredSpecularTexture = gpu::TexturePointer(gpu::Texture::create2D(colorFormat, width, height, defaultSampler)); + _deferredColorTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(colorFormat, width, height, defaultSampler)); + _deferredNormalTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(linearFormat, width, height, defaultSampler)); + _deferredSpecularTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(colorFormat, width, height, defaultSampler)); _deferredFramebuffer->setRenderBuffer(0, _deferredColorTexture); _deferredFramebuffer->setRenderBuffer(1, _deferredNormalTexture); @@ -65,7 +65,7 @@ void DeferredFramebuffer::allocate() { auto depthFormat = gpu::Element(gpu::SCALAR, gpu::UINT32, gpu::DEPTH_STENCIL); // Depth24_Stencil8 texel format if (!_primaryDepthTexture) { - _primaryDepthTexture = gpu::TexturePointer(gpu::Texture::create2D(depthFormat, width, height, defaultSampler)); + _primaryDepthTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(depthFormat, width, height, defaultSampler)); } _deferredFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, depthFormat); @@ -75,7 +75,7 @@ void DeferredFramebuffer::allocate() { auto smoothSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR); - _lightingTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::R11G11B10), width, height, defaultSampler)); + _lightingTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::R11G11B10), width, height, defaultSampler)); _lightingFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("lighting")); _lightingFramebuffer->setRenderBuffer(0, _lightingTexture); _lightingFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, depthFormat); diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp index 6f1152ac16..ce340583ee 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.cpp +++ b/libraries/render-utils/src/DeferredLightingEffect.cpp @@ -496,14 +496,14 @@ void PreparePrimaryFramebuffer::run(const SceneContextPointer& sceneContext, con auto colorFormat = gpu::Element::COLOR_SRGBA_32; auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT); - auto primaryColorTexture = gpu::TexturePointer(gpu::Texture::create2D(colorFormat, frameSize.x, frameSize.y, defaultSampler)); + auto primaryColorTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(colorFormat, frameSize.x, frameSize.y, defaultSampler)); _primaryFramebuffer->setRenderBuffer(0, primaryColorTexture); auto depthFormat = gpu::Element(gpu::SCALAR, gpu::UINT32, gpu::DEPTH_STENCIL); // Depth24_Stencil8 texel format - auto primaryDepthTexture = gpu::TexturePointer(gpu::Texture::create2D(depthFormat, frameSize.x, frameSize.y, defaultSampler)); + auto primaryDepthTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(depthFormat, frameSize.x, frameSize.y, defaultSampler)); _primaryFramebuffer->setDepthStencilBuffer(primaryDepthTexture, depthFormat); } diff --git a/libraries/render-utils/src/FramebufferCache.cpp b/libraries/render-utils/src/FramebufferCache.cpp index 27429595b4..72b3c2ceb4 100644 --- a/libraries/render-utils/src/FramebufferCache.cpp +++ b/libraries/render-utils/src/FramebufferCache.cpp @@ -21,7 +21,6 @@ void FramebufferCache::setFrameBufferSize(QSize frameBufferSize) { //If the size changed, we need to delete our FBOs if (_frameBufferSize != frameBufferSize) { _frameBufferSize = frameBufferSize; - _selfieFramebuffer.reset(); { std::unique_lock lock(_mutex); _cachedFramebuffers.clear(); @@ -30,16 +29,8 @@ void FramebufferCache::setFrameBufferSize(QSize frameBufferSize) { } void FramebufferCache::createPrimaryFramebuffer() { - auto colorFormat = gpu::Element::COLOR_SRGBA_32; - auto width = _frameBufferSize.width(); - auto height = _frameBufferSize.height(); - auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT); - _selfieFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("selfie")); - auto tex = gpu::TexturePointer(gpu::Texture::create2D(colorFormat, width * 0.5, height * 0.5, defaultSampler)); - _selfieFramebuffer->setRenderBuffer(0, tex); - auto smoothSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR); } @@ -60,10 +51,3 @@ void FramebufferCache::releaseFramebuffer(const gpu::FramebufferPointer& framebu _cachedFramebuffers.push_back(framebuffer); } } - -gpu::FramebufferPointer FramebufferCache::getSelfieFramebuffer() { - if (!_selfieFramebuffer) { - createPrimaryFramebuffer(); - } - return _selfieFramebuffer; -} diff --git a/libraries/render-utils/src/FramebufferCache.h b/libraries/render-utils/src/FramebufferCache.h index f74d224a61..8065357615 100644 --- a/libraries/render-utils/src/FramebufferCache.h +++ b/libraries/render-utils/src/FramebufferCache.h @@ -27,9 +27,6 @@ public: void setFrameBufferSize(QSize frameBufferSize); const QSize& getFrameBufferSize() const { return _frameBufferSize; } - /// Returns the framebuffer object used to render selfie maps; - gpu::FramebufferPointer getSelfieFramebuffer(); - /// Returns a free framebuffer with a single color attachment for temp or intra-frame operations gpu::FramebufferPointer getFramebuffer(); @@ -42,8 +39,6 @@ private: gpu::FramebufferPointer _shadowFramebuffer; - gpu::FramebufferPointer _selfieFramebuffer; - QSize _frameBufferSize{ 100, 100 }; std::mutex _mutex; diff --git a/libraries/render-utils/src/LightAmbient.slh b/libraries/render-utils/src/LightAmbient.slh index 15e23015cb..e343d8c239 100644 --- a/libraries/render-utils/src/LightAmbient.slh +++ b/libraries/render-utils/src/LightAmbient.slh @@ -30,9 +30,8 @@ vec3 fresnelSchlickAmbient(vec3 fresnelColor, vec3 lightDir, vec3 halfDir, float <$declareSkyboxMap()$> <@endif@> -vec3 evalAmbientSpecularIrradiance(LightAmbient ambient, vec3 fragEyeDir, vec3 fragNormal, float roughness, vec3 fresnel) { +vec3 evalAmbientSpecularIrradiance(LightAmbient ambient, vec3 fragEyeDir, vec3 fragNormal, float roughness) { vec3 direction = -reflect(fragEyeDir, fragNormal); - vec3 ambientFresnel = fresnelSchlickAmbient(fresnel, fragEyeDir, fragNormal, 1.0 - roughness); vec3 specularLight; <@if supportIfAmbientMapElseAmbientSphere@> if (getLightHasAmbientMap(ambient)) @@ -53,7 +52,7 @@ vec3 evalAmbientSpecularIrradiance(LightAmbient ambient, vec3 fragEyeDir, vec3 f } <@endif@> - return specularLight * ambientFresnel; + return specularLight; } <@endfunc@> @@ -74,12 +73,14 @@ void evalLightingAmbient(out vec3 diffuse, out vec3 specular, LightAmbient ambie <@endif@> ) { + // Fresnel + vec3 ambientFresnel = fresnelSchlickAmbient(fresnel, eyeDir, normal, 1.0 - roughness); + // Diffuse from ambient - diffuse = (1.0 - metallic) * sphericalHarmonics_evalSphericalLight(getLightAmbientSphere(ambient), normal).xyz; + diffuse = (1.0 - metallic) * (vec3(1.0) - ambientFresnel) * sphericalHarmonics_evalSphericalLight(getLightAmbientSphere(ambient), normal).xyz; // Specular highlight from ambient - specular = evalAmbientSpecularIrradiance(ambient, eyeDir, normal, roughness, fresnel) * obscurance * getLightAmbientIntensity(ambient); - + specular = evalAmbientSpecularIrradiance(ambient, eyeDir, normal, roughness) * ambientFresnel; <@if supportScattering@> float ambientOcclusion = curvatureAO(lowNormalCurvature.w * 20.0f) * 0.5f; diff --git a/libraries/render-utils/src/LightStage.cpp b/libraries/render-utils/src/LightStage.cpp index 66a9797d3c..dd6a046dea 100644 --- a/libraries/render-utils/src/LightStage.cpp +++ b/libraries/render-utils/src/LightStage.cpp @@ -27,9 +27,9 @@ void LightStage::Shadow::setKeylightFrustum(const ViewFrustum& viewFrustum, floa const auto& direction = glm::normalize(_light->getDirection()); glm::quat orientation; if (direction == IDENTITY_UP) { - orientation = glm::quat(glm::mat3(-IDENTITY_RIGHT, IDENTITY_FRONT, -IDENTITY_UP)); + orientation = glm::quat(glm::mat3(-IDENTITY_RIGHT, IDENTITY_FORWARD, -IDENTITY_UP)); } else if (direction == -IDENTITY_UP) { - orientation = glm::quat(glm::mat3(IDENTITY_RIGHT, IDENTITY_FRONT, IDENTITY_UP)); + orientation = glm::quat(glm::mat3(IDENTITY_RIGHT, IDENTITY_FORWARD, IDENTITY_UP)); } else { auto side = glm::normalize(glm::cross(direction, IDENTITY_UP)); auto up = glm::normalize(glm::cross(side, direction)); diff --git a/libraries/render-utils/src/LightingModel.cpp b/libraries/render-utils/src/LightingModel.cpp index 47af83da36..bd321bad95 100644 --- a/libraries/render-utils/src/LightingModel.cpp +++ b/libraries/render-utils/src/LightingModel.cpp @@ -133,6 +133,7 @@ void LightingModel::setSpotLight(bool enable) { bool LightingModel::isSpotLightEnabled() const { return (bool)_parametersBuffer.get().enableSpotLight; } + void LightingModel::setShowLightContour(bool enable) { if (enable != isShowLightContourEnabled()) { _parametersBuffer.edit().showLightContour = (float)enable; @@ -142,6 +143,14 @@ bool LightingModel::isShowLightContourEnabled() const { return (bool)_parametersBuffer.get().showLightContour; } +void LightingModel::setWireframe(bool enable) { + if (enable != isWireframeEnabled()) { + _parametersBuffer.edit().enableWireframe = (float)enable; + } +} +bool LightingModel::isWireframeEnabled() const { + return (bool)_parametersBuffer.get().enableWireframe; +} MakeLightingModel::MakeLightingModel() { _lightingModel = std::make_shared(); } @@ -167,6 +176,7 @@ void MakeLightingModel::configure(const Config& config) { _lightingModel->setSpotLight(config.enableSpotLight); _lightingModel->setShowLightContour(config.showLightContour); + _lightingModel->setWireframe(config.enableWireframe); } void MakeLightingModel::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, LightingModelPointer& lightingModel) { diff --git a/libraries/render-utils/src/LightingModel.h b/libraries/render-utils/src/LightingModel.h index 45514654f2..c1189d5160 100644 --- a/libraries/render-utils/src/LightingModel.h +++ b/libraries/render-utils/src/LightingModel.h @@ -64,6 +64,9 @@ public: void setShowLightContour(bool enable); bool isShowLightContourEnabled() const; + void setWireframe(bool enable); + bool isWireframeEnabled() const; + UniformBufferView getParametersBuffer() const { return _parametersBuffer; } protected: @@ -89,13 +92,12 @@ protected: float enablePointLight{ 1.0f }; float enableSpotLight{ 1.0f }; - float showLightContour{ 0.0f }; // false by default + float showLightContour { 0.0f }; // false by default float enableObscurance{ 1.0f }; float enableMaterialTexturing { 1.0f }; - - float spares{ 0.0f }; + float enableWireframe { 0.0f }; // false by default Parameters() {} }; @@ -129,6 +131,7 @@ class MakeLightingModelConfig : public render::Job::Config { Q_PROPERTY(bool enablePointLight MEMBER enablePointLight NOTIFY dirty) Q_PROPERTY(bool enableSpotLight MEMBER enableSpotLight NOTIFY dirty) + Q_PROPERTY(bool enableWireframe MEMBER enableWireframe NOTIFY dirty) Q_PROPERTY(bool showLightContour MEMBER showLightContour NOTIFY dirty) public: @@ -152,9 +155,10 @@ public: bool enablePointLight{ true }; bool enableSpotLight{ true }; - bool showLightContour { false }; // false by default + bool enableWireframe { false }; // false by default + signals: void dirty(); }; diff --git a/libraries/render-utils/src/LightingModel.slh b/libraries/render-utils/src/LightingModel.slh index 74285aa6a9..209a1f38d6 100644 --- a/libraries/render-utils/src/LightingModel.slh +++ b/libraries/render-utils/src/LightingModel.slh @@ -17,7 +17,7 @@ struct LightingModel { vec4 _UnlitEmissiveLightmapBackground; vec4 _ScatteringDiffuseSpecularAlbedo; vec4 _AmbientDirectionalPointSpot; - vec4 _ShowContourObscuranceSpare2; + vec4 _ShowContourObscuranceWireframe; }; uniform lightingModelBuffer{ @@ -37,7 +37,7 @@ float isBackgroundEnabled() { return lightingModel._UnlitEmissiveLightmapBackground.w; } float isObscuranceEnabled() { - return lightingModel._ShowContourObscuranceSpare2.y; + return lightingModel._ShowContourObscuranceWireframe.y; } float isScatteringEnabled() { @@ -67,9 +67,12 @@ float isSpotEnabled() { } float isShowLightContour() { - return lightingModel._ShowContourObscuranceSpare2.x; + return lightingModel._ShowContourObscuranceWireframe.x; } +float isWireframeEnabled() { + return lightingModel._ShowContourObscuranceWireframe.z; +} <@endfunc@> <$declareLightingModel()$> diff --git a/libraries/render-utils/src/MaterialTextures.slh b/libraries/render-utils/src/MaterialTextures.slh index 6d2ad23c21..7b73896cc5 100644 --- a/libraries/render-utils/src/MaterialTextures.slh +++ b/libraries/render-utils/src/MaterialTextures.slh @@ -64,7 +64,7 @@ float fetchRoughnessMap(vec2 uv) { uniform sampler2D normalMap; vec3 fetchNormalMap(vec2 uv) { // unpack normal, swizzle to get into hifi tangent space with Y axis pointing out - return normalize(texture(normalMap, uv).xzy -vec3(0.5, 0.5, 0.5)); + return normalize(texture(normalMap, uv).rbg -vec3(0.5, 0.5, 0.5)); } <@endif@> diff --git a/libraries/render-utils/src/MeshPartPayload.cpp b/libraries/render-utils/src/MeshPartPayload.cpp index 5b3d285b47..41a1bb4c74 100644 --- a/libraries/render-utils/src/MeshPartPayload.cpp +++ b/libraries/render-utils/src/MeshPartPayload.cpp @@ -372,19 +372,12 @@ void ModelMeshPartPayload::notifyLocationChanged() { } -void ModelMeshPartPayload::updateTransformForSkinnedMesh(const Transform& transform, const QVector& clusterMatrices) { - _transform = transform; - - if (clusterMatrices.size() > 0) { - _worldBound = _adjustedLocalBound; - _worldBound.transform(_transform); - if (clusterMatrices.size() == 1) { - _transform = _transform.worldTransform(Transform(clusterMatrices[0])); - } - } else { - _worldBound = _localBound; - _worldBound.transform(_transform); - } +void ModelMeshPartPayload::updateTransformForSkinnedMesh(const Transform& renderTransform, const Transform& boundTransform, + const gpu::BufferPointer& buffer) { + _transform = renderTransform; + _worldBound = _adjustedLocalBound; + _worldBound.transform(boundTransform); + _clusterBuffer = buffer; } ItemKey ModelMeshPartPayload::getKey() const { @@ -532,9 +525,8 @@ void ModelMeshPartPayload::bindMesh(gpu::Batch& batch) const { void ModelMeshPartPayload::bindTransform(gpu::Batch& batch, const ShapePipeline::LocationsPointer locations, RenderArgs::RenderMode renderMode) const { // Still relying on the raw data from the model - const Model::MeshState& state = _model->getMeshState(_meshIndex); - if (state.clusterBuffer) { - batch.setUniformBuffer(ShapePipeline::Slot::BUFFER::SKINNING, state.clusterBuffer); + if (_clusterBuffer) { + batch.setUniformBuffer(ShapePipeline::Slot::BUFFER::SKINNING, _clusterBuffer); } batch.setModelTransform(_transform); } @@ -590,8 +582,6 @@ void ModelMeshPartPayload::render(RenderArgs* args) const { auto locations = args->_pipeline->locations; assert(locations); - // Bind the model transform and the skinCLusterMatrices if needed - _model->updateClusterMatrices(); bindTransform(batch, locations, args->_renderMode); //Bind the index buffer and vertex buffer and Blend shapes if needed diff --git a/libraries/render-utils/src/MeshPartPayload.h b/libraries/render-utils/src/MeshPartPayload.h index c585c95025..ef74011c40 100644 --- a/libraries/render-utils/src/MeshPartPayload.h +++ b/libraries/render-utils/src/MeshPartPayload.h @@ -89,8 +89,9 @@ public: typedef Payload::DataPointer Pointer; void notifyLocationChanged() override; - void updateTransformForSkinnedMesh(const Transform& transform, - const QVector& clusterMatrices); + void updateTransformForSkinnedMesh(const Transform& renderTransform, + const Transform& boundTransform, + const gpu::BufferPointer& buffer); float computeFadeAlpha() const; @@ -108,6 +109,7 @@ public: void computeAdjustedLocalBound(const QVector& clusterMatrices); + gpu::BufferPointer _clusterBuffer; Model* _model; int _meshIndex; diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 48c1d29b68..3448c9e8da 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -176,11 +176,11 @@ void Model::setOffset(const glm::vec3& offset) { } void Model::calculateTextureInfo() { - if (!_hasCalculatedTextureInfo && isLoaded() && getGeometry()->areTexturesLoaded() && !_modelMeshRenderItems.isEmpty()) { + if (!_hasCalculatedTextureInfo && isLoaded() && getGeometry()->areTexturesLoaded() && !_modelMeshRenderItemsMap.isEmpty()) { size_t textureSize = 0; int textureCount = 0; bool allTexturesLoaded = true; - foreach(auto renderItem, _modelMeshRenderItemsSet) { + foreach(auto renderItem, _modelMeshRenderItems) { auto meshPart = renderItem.get(); textureSize += meshPart->getMaterialTextureSize(); textureCount += meshPart->getMaterialTextureCount(); @@ -227,12 +227,16 @@ void Model::updateRenderItems() { return; } + // lazy update of cluster matrices used for rendering. + // We need to update them here so we can correctly update the bounding box. + self->updateClusterMatrices(); + render::ScenePointer scene = AbstractViewStateInterface::instance()->getMain3DScene(); uint32_t deleteGeometryCounter = self->_deleteGeometryCounter; render::PendingChanges pendingChanges; - foreach (auto itemID, self->_modelMeshRenderItems.keys()) { + foreach (auto itemID, self->_modelMeshRenderItemsMap.keys()) { pendingChanges.updateItem(itemID, [deleteGeometryCounter](ModelMeshPartPayload& data) { if (data._model && data._model->isLoaded()) { // Ensure the model geometry was not reset between frames @@ -240,12 +244,12 @@ void Model::updateRenderItems() { Transform modelTransform = data._model->getTransform(); modelTransform.setScale(glm::vec3(1.0f)); - // lazy update of cluster matrices used for rendering. We need to update them here, so we can correctly update the bounding box. - data._model->updateClusterMatrices(); - - // update the model transform and bounding box for this render item. - const Model::MeshState& state = data._model->_meshStates.at(data._meshIndex); - data.updateTransformForSkinnedMesh(modelTransform, state.clusterMatrices); + const Model::MeshState& state = data._model->getMeshState(data._meshIndex); + Transform renderTransform = modelTransform; + if (state.clusterMatrices.size() == 1) { + renderTransform = modelTransform.worldTransform(Transform(state.clusterMatrices[0])); + } + data.updateTransformForSkinnedMesh(renderTransform, modelTransform, state.clusterBuffer); } } }); @@ -255,7 +259,7 @@ void Model::updateRenderItems() { Transform collisionMeshOffset; collisionMeshOffset.setIdentity(); Transform modelTransform = self->getTransform(); - foreach (auto itemID, self->_collisionRenderItems.keys()) { + foreach(auto itemID, self->_collisionRenderItemsMap.keys()) { pendingChanges.updateItem(itemID, [modelTransform, collisionMeshOffset](MeshPartPayload& data) { // update the model transform for this render item. data.updateTransform(modelTransform, collisionMeshOffset); @@ -535,11 +539,11 @@ void Model::setVisibleInScene(bool newValue, std::shared_ptr scen _isVisible = newValue; render::PendingChanges pendingChanges; - foreach (auto item, _modelMeshRenderItems.keys()) { - pendingChanges.resetItem(item, _modelMeshRenderItems[item]); + foreach (auto item, _modelMeshRenderItemsMap.keys()) { + pendingChanges.resetItem(item, _modelMeshRenderItemsMap[item]); } - foreach (auto item, _collisionRenderItems.keys()) { - pendingChanges.resetItem(item, _collisionRenderItems[item]); + foreach(auto item, _collisionRenderItemsMap.keys()) { + pendingChanges.resetItem(item, _collisionRenderItemsMap[item]); } scene->enqueuePendingChanges(pendingChanges); } @@ -551,11 +555,11 @@ void Model::setLayeredInFront(bool layered, std::shared_ptr scene _isLayeredInFront = layered; render::PendingChanges pendingChanges; - foreach(auto item, _modelMeshRenderItems.keys()) { - pendingChanges.resetItem(item, _modelMeshRenderItems[item]); + foreach(auto item, _modelMeshRenderItemsMap.keys()) { + pendingChanges.resetItem(item, _modelMeshRenderItemsMap[item]); } - foreach(auto item, _collisionRenderItems.keys()) { - pendingChanges.resetItem(item, _collisionRenderItems[item]); + foreach(auto item, _collisionRenderItemsMap.keys()) { + pendingChanges.resetItem(item, _collisionRenderItemsMap[item]); } scene->enqueuePendingChanges(pendingChanges); } @@ -572,39 +576,39 @@ bool Model::addToScene(std::shared_ptr scene, bool somethingAdded = false; if (_collisionGeometry) { if (_collisionRenderItems.empty()) { - foreach (auto renderItem, _collisionRenderItemsSet) { + foreach (auto renderItem, _collisionRenderItems) { auto item = scene->allocateID(); auto renderPayload = std::make_shared(renderItem); - if (statusGetters.size()) { + if (_collisionRenderItems.empty() && statusGetters.size()) { renderPayload->addStatusGetters(statusGetters); } pendingChanges.resetItem(item, renderPayload); - _collisionRenderItems.insert(item, renderPayload); + _collisionRenderItemsMap.insert(item, renderPayload); } somethingAdded = !_collisionRenderItems.empty(); } } else { - if (_modelMeshRenderItems.empty()) { + if (_modelMeshRenderItemsMap.empty()) { bool hasTransparent = false; size_t verticesCount = 0; - foreach(auto renderItem, _modelMeshRenderItemsSet) { + foreach(auto renderItem, _modelMeshRenderItems) { auto item = scene->allocateID(); auto renderPayload = std::make_shared(renderItem); - if (statusGetters.size()) { + if (_modelMeshRenderItemsMap.empty() && statusGetters.size()) { renderPayload->addStatusGetters(statusGetters); } pendingChanges.resetItem(item, renderPayload); hasTransparent = hasTransparent || renderItem.get()->getShapeKey().isTranslucent(); verticesCount += renderItem.get()->getVerticesCount(); - _modelMeshRenderItems.insert(item, renderPayload); + _modelMeshRenderItemsMap.insert(item, renderPayload); _modelMeshRenderItemIDs.emplace_back(item); } - somethingAdded = !_modelMeshRenderItems.empty(); + somethingAdded = !_modelMeshRenderItemsMap.empty(); _renderInfoVertexCount = verticesCount; - _renderInfoDrawCalls = _modelMeshRenderItems.count(); + _renderInfoDrawCalls = _modelMeshRenderItemsMap.count(); _renderInfoHasTransparent = hasTransparent; } } @@ -619,18 +623,18 @@ bool Model::addToScene(std::shared_ptr scene, } void Model::removeFromScene(std::shared_ptr scene, render::PendingChanges& pendingChanges) { - foreach (auto item, _modelMeshRenderItems.keys()) { + foreach (auto item, _modelMeshRenderItemsMap.keys()) { pendingChanges.removeItem(item); } _modelMeshRenderItemIDs.clear(); + _modelMeshRenderItemsMap.clear(); _modelMeshRenderItems.clear(); - _modelMeshRenderItemsSet.clear(); - foreach (auto item, _collisionRenderItems.keys()) { + foreach(auto item, _collisionRenderItemsMap.keys()) { pendingChanges.removeItem(item); } _collisionRenderItems.clear(); - _collisionRenderItemsSet.clear(); + _collisionRenderItems.clear(); _addedToScene = false; _renderInfoVertexCount = 0; @@ -1048,8 +1052,8 @@ void Model::updateRig(float deltaTime, glm::mat4 parentTransform) { } void Model::computeMeshPartLocalBounds() { - for (auto& part : _modelMeshRenderItemsSet) { - assert(part->_meshIndex < _modelMeshRenderItemsSet.size()); + for (auto& part : _modelMeshRenderItems) { + assert(part->_meshIndex < _modelMeshRenderItems.size()); const Model::MeshState& state = _meshStates.at(part->_meshIndex); part->computeAdjustedLocalBound(state.clusterMatrices); } @@ -1163,7 +1167,7 @@ AABox Model::getRenderableMeshBound() const { } else { // Build a bound using the last known bound from all the renderItems. AABox totalBound; - for (auto& renderItem : _modelMeshRenderItemsSet) { + for (auto& renderItem : _modelMeshRenderItems) { totalBound += renderItem->getBound(); } return totalBound; @@ -1176,11 +1180,11 @@ const render::ItemIDs& Model::fetchRenderItemIDs() const { void Model::createRenderItemSet() { if (_collisionGeometry) { - if (_collisionRenderItemsSet.empty()) { + if (_collisionRenderItems.empty()) { createCollisionRenderItemSet(); } } else { - if (_modelMeshRenderItemsSet.empty()) { + if (_modelMeshRenderItems.empty()) { createVisibleRenderItemSet(); } } @@ -1197,9 +1201,9 @@ void Model::createVisibleRenderItemSet() { } // We should not have any existing renderItems if we enter this section of code - Q_ASSERT(_modelMeshRenderItemsSet.isEmpty()); + Q_ASSERT(_modelMeshRenderItems.isEmpty()); - _modelMeshRenderItemsSet.clear(); + _modelMeshRenderItems.clear(); Transform transform; transform.setTranslation(_translation); @@ -1221,7 +1225,7 @@ void Model::createVisibleRenderItemSet() { // Create the render payloads int numParts = (int)mesh->getNumParts(); for (int partIndex = 0; partIndex < numParts; partIndex++) { - _modelMeshRenderItemsSet << std::make_shared(this, i, partIndex, shapeID, transform, offset); + _modelMeshRenderItems << std::make_shared(this, i, partIndex, shapeID, transform, offset); shapeID++; } } @@ -1237,7 +1241,7 @@ void Model::createCollisionRenderItemSet() { const auto& meshes = _collisionGeometry->getMeshes(); // We should not have any existing renderItems if we enter this section of code - Q_ASSERT(_collisionRenderItemsSet.isEmpty()); + Q_ASSERT(_collisionRenderItems.isEmpty()); Transform identity; identity.setIdentity(); @@ -1258,7 +1262,7 @@ void Model::createCollisionRenderItemSet() { model::MaterialPointer& material = _collisionMaterials[partIndex % NUM_COLLISION_HULL_COLORS]; auto payload = std::make_shared(mesh, partIndex, material); payload->updateTransform(identity, offset); - _collisionRenderItemsSet << payload; + _collisionRenderItems << payload; } } } @@ -1279,28 +1283,28 @@ bool Model::initWhenReady(render::ScenePointer scene) { bool addedPendingChanges = false; if (_collisionGeometry) { - foreach (auto renderItem, _collisionRenderItemsSet) { + foreach (auto renderItem, _collisionRenderItems) { auto item = scene->allocateID(); auto renderPayload = std::make_shared(renderItem); - _collisionRenderItems.insert(item, renderPayload); + _collisionRenderItemsMap.insert(item, renderPayload); pendingChanges.resetItem(item, renderPayload); } addedPendingChanges = !_collisionRenderItems.empty(); } else { bool hasTransparent = false; size_t verticesCount = 0; - foreach (auto renderItem, _modelMeshRenderItemsSet) { + foreach (auto renderItem, _modelMeshRenderItems) { auto item = scene->allocateID(); auto renderPayload = std::make_shared(renderItem); hasTransparent = hasTransparent || renderItem.get()->getShapeKey().isTranslucent(); verticesCount += renderItem.get()->getVerticesCount(); - _modelMeshRenderItems.insert(item, renderPayload); + _modelMeshRenderItemsMap.insert(item, renderPayload); pendingChanges.resetItem(item, renderPayload); } - addedPendingChanges = !_modelMeshRenderItems.empty(); + addedPendingChanges = !_modelMeshRenderItemsMap.empty(); _renderInfoVertexCount = verticesCount; - _renderInfoDrawCalls = _modelMeshRenderItems.count(); + _renderInfoDrawCalls = _modelMeshRenderItemsMap.count(); _renderInfoHasTransparent = hasTransparent; } _addedToScene = addedPendingChanges; diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index 41821736f7..bb283cce1f 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -248,7 +248,7 @@ public: const MeshState& getMeshState(int index) { return _meshStates.at(index); } uint32_t getGeometryCounter() const { return _deleteGeometryCounter; } - const QMap& getRenderItems() const { return _modelMeshRenderItems; } + const QMap& getRenderItems() const { return _modelMeshRenderItemsMap; } void renderDebugMeshBoxes(gpu::Batch& batch); @@ -373,11 +373,11 @@ protected: static AbstractViewStateInterface* _viewState; - QSet> _collisionRenderItemsSet; - QMap _collisionRenderItems; + QVector> _collisionRenderItems; + QMap _collisionRenderItemsMap; - QSet> _modelMeshRenderItemsSet; - QMap _modelMeshRenderItems; + QVector> _modelMeshRenderItems; + QMap _modelMeshRenderItemsMap; render::ItemIDs _modelMeshRenderItemIDs; diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index 676d176cca..22aa95090c 100644 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -194,7 +194,7 @@ RenderDeferredTask::RenderDeferredTask(RenderFetchCullSortTask::Output items) { { // Grab a texture map representing the different status icons and assign that to the drawStatsuJob auto iconMapPath = PathUtils::resourcesPath() + "icons/statusIconAtlas.svg"; - auto statusIconMap = DependencyManager::get()->getImageTexture(iconMapPath); + auto statusIconMap = DependencyManager::get()->getImageTexture(iconMapPath, NetworkTexture::STRICT_TEXTURE); addJob("DrawStatus", opaques, DrawStatus(statusIconMap)); } } @@ -259,8 +259,18 @@ void DrawDeferred::run(const SceneContextPointer& sceneContext, const RenderCont // Setup lighting model for all items; batch.setUniformBuffer(render::ShapePipeline::Slot::LIGHTING_MODEL, lightingModel->getParametersBuffer()); - renderShapes(sceneContext, renderContext, _shapePlumber, inItems, _maxDrawn); + // From the lighting model define a global shapKey ORED with individiual keys + ShapeKey::Builder keyBuilder; + if (lightingModel->isWireframeEnabled()) { + keyBuilder.withWireframe(); + } + ShapeKey globalKey = keyBuilder.build(); + args->_globalShapeKey = globalKey._flags.to_ulong(); + + renderShapes(sceneContext, renderContext, _shapePlumber, inItems, _maxDrawn, globalKey); + args->_batch = nullptr; + args->_globalShapeKey = 0; }); config->setNumDrawn((int)inItems.size()); @@ -295,12 +305,21 @@ void DrawStateSortDeferred::run(const SceneContextPointer& sceneContext, const R // Setup lighting model for all items; batch.setUniformBuffer(render::ShapePipeline::Slot::LIGHTING_MODEL, lightingModel->getParametersBuffer()); + // From the lighting model define a global shapKey ORED with individiual keys + ShapeKey::Builder keyBuilder; + if (lightingModel->isWireframeEnabled()) { + keyBuilder.withWireframe(); + } + ShapeKey globalKey = keyBuilder.build(); + args->_globalShapeKey = globalKey._flags.to_ulong(); + if (_stateSort) { - renderStateSortShapes(sceneContext, renderContext, _shapePlumber, inItems, _maxDrawn); + renderStateSortShapes(sceneContext, renderContext, _shapePlumber, inItems, _maxDrawn, globalKey); } else { - renderShapes(sceneContext, renderContext, _shapePlumber, inItems, _maxDrawn); + renderShapes(sceneContext, renderContext, _shapePlumber, inItems, _maxDrawn, globalKey); } args->_batch = nullptr; + args->_globalShapeKey = 0; }); config->setNumDrawn((int)inItems.size()); diff --git a/libraries/render-utils/src/RenderPipelines.cpp b/libraries/render-utils/src/RenderPipelines.cpp index 4fbac4170e..414bcf0d63 100644 --- a/libraries/render-utils/src/RenderPipelines.cpp +++ b/libraries/render-utils/src/RenderPipelines.cpp @@ -307,7 +307,7 @@ void initForwardPipelines(render::ShapePlumber& plumber) { void addPlumberPipeline(ShapePlumber& plumber, const ShapeKey& key, const gpu::ShaderPointer& vertex, const gpu::ShaderPointer& pixel) { // These key-values' pipelines are added by this functor in addition to the key passed - assert(!key.isWireFrame()); + assert(!key.isWireframe()); assert(!key.isDepthBiased()); assert(key.isCullFace()); diff --git a/libraries/render-utils/src/SubsurfaceScattering.cpp b/libraries/render-utils/src/SubsurfaceScattering.cpp index 188381b822..25a01bff1b 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.cpp +++ b/libraries/render-utils/src/SubsurfaceScattering.cpp @@ -414,7 +414,7 @@ gpu::TexturePointer SubsurfaceScatteringResource::generateScatteringProfile(Rend const int PROFILE_RESOLUTION = 512; // const auto pixelFormat = gpu::Element::COLOR_SRGBA_32; const auto pixelFormat = gpu::Element::COLOR_R11G11B10; - auto profileMap = gpu::TexturePointer(gpu::Texture::create2D(pixelFormat, PROFILE_RESOLUTION, 1, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP))); + auto profileMap = gpu::TexturePointer(gpu::Texture::createRenderBuffer(pixelFormat, PROFILE_RESOLUTION, 1, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP))); profileMap->setSource("Generated Scattering Profile"); diffuseProfileGPU(profileMap, args); return profileMap; @@ -425,7 +425,7 @@ gpu::TexturePointer SubsurfaceScatteringResource::generatePreIntegratedScatterin const int TABLE_RESOLUTION = 512; // const auto pixelFormat = gpu::Element::COLOR_SRGBA_32; const auto pixelFormat = gpu::Element::COLOR_R11G11B10; - auto scatteringLUT = gpu::TexturePointer(gpu::Texture::create2D(pixelFormat, TABLE_RESOLUTION, TABLE_RESOLUTION, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP))); + auto scatteringLUT = gpu::TexturePointer(gpu::Texture::createRenderBuffer(pixelFormat, TABLE_RESOLUTION, TABLE_RESOLUTION, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP))); //diffuseScatter(scatteringLUT); scatteringLUT->setSource("Generated pre-integrated scattering"); diffuseScatterGPU(profile, scatteringLUT, args); @@ -434,7 +434,7 @@ gpu::TexturePointer SubsurfaceScatteringResource::generatePreIntegratedScatterin gpu::TexturePointer SubsurfaceScatteringResource::generateScatteringSpecularBeckmann(RenderArgs* args) { const int SPECULAR_RESOLUTION = 256; - auto beckmannMap = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32 /*gpu::Element(gpu::SCALAR, gpu::HALF, gpu::RGB)*/, SPECULAR_RESOLUTION, SPECULAR_RESOLUTION, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP))); + auto beckmannMap = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32 /*gpu::Element(gpu::SCALAR, gpu::HALF, gpu::RGB)*/, SPECULAR_RESOLUTION, SPECULAR_RESOLUTION, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP))); beckmannMap->setSource("Generated beckmannMap"); computeSpecularBeckmannGPU(beckmannMap, args); return beckmannMap; diff --git a/libraries/render-utils/src/SurfaceGeometryPass.cpp b/libraries/render-utils/src/SurfaceGeometryPass.cpp index f0ac56ac26..3a23e70664 100644 --- a/libraries/render-utils/src/SurfaceGeometryPass.cpp +++ b/libraries/render-utils/src/SurfaceGeometryPass.cpp @@ -72,18 +72,18 @@ void LinearDepthFramebuffer::allocate() { auto height = _frameSize.y; // For Linear Depth: - _linearDepthTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::RGB), width, height, + _linearDepthTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::RGB), width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); _linearDepthFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("linearDepth")); _linearDepthFramebuffer->setRenderBuffer(0, _linearDepthTexture); _linearDepthFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, _primaryDepthTexture->getTexelFormat()); // For Downsampling: - _halfLinearDepthTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::RGB), _halfFrameSize.x, _halfFrameSize.y, + _halfLinearDepthTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::RGB), _halfFrameSize.x, _halfFrameSize.y, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); _halfLinearDepthTexture->autoGenerateMips(5); - _halfNormalTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element(gpu::VEC3, gpu::NUINT8, gpu::RGB), _halfFrameSize.x, _halfFrameSize.y, + _halfNormalTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element(gpu::VEC3, gpu::NUINT8, gpu::RGB), _halfFrameSize.x, _halfFrameSize.y, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); _downsampleFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("halfLinearDepth")); @@ -304,15 +304,15 @@ void SurfaceGeometryFramebuffer::allocate() { auto width = _frameSize.x; auto height = _frameSize.y; - _curvatureTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); + _curvatureTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32, width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); _curvatureFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("surfaceGeometry::curvature")); _curvatureFramebuffer->setRenderBuffer(0, _curvatureTexture); - _lowCurvatureTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); + _lowCurvatureTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32, width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); _lowCurvatureFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("surfaceGeometry::lowCurvature")); _lowCurvatureFramebuffer->setRenderBuffer(0, _lowCurvatureTexture); - _blurringTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); + _blurringTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32, width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); _blurringFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("surfaceGeometry::blurring")); _blurringFramebuffer->setRenderBuffer(0, _blurringTexture); } diff --git a/libraries/render-utils/src/text/Font.cpp b/libraries/render-utils/src/text/Font.cpp index 4f4ee12622..c405f6d6ae 100644 --- a/libraries/render-utils/src/text/Font.cpp +++ b/libraries/render-utils/src/text/Font.cpp @@ -209,7 +209,8 @@ void Font::read(QIODevice& in) { } _texture = gpu::TexturePointer(gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_POINT_MAG_LINEAR))); - _texture->assignStoredMip(0, formatMip, image.byteCount(), image.constBits()); + _texture->setStoredMipFormat(formatMip); + _texture->assignStoredMip(0, image.byteCount(), image.constBits()); } void Font::setupGPU() { diff --git a/libraries/render/CMakeLists.txt b/libraries/render/CMakeLists.txt index 735bb7f086..8fd05bd320 100644 --- a/libraries/render/CMakeLists.txt +++ b/libraries/render/CMakeLists.txt @@ -3,6 +3,6 @@ AUTOSCRIBE_SHADER_LIB(gpu model) setup_hifi_library() # render needs octree only for getAccuracyAngle(float, int) -link_hifi_libraries(shared gpu model octree) +link_hifi_libraries(shared ktx gpu model octree) target_nsight() diff --git a/libraries/render/src/render/DrawTask.cpp b/libraries/render/src/render/DrawTask.cpp index 2829c6f8e7..e8537e3452 100755 --- a/libraries/render/src/render/DrawTask.cpp +++ b/libraries/render/src/render/DrawTask.cpp @@ -39,9 +39,9 @@ void render::renderItems(const SceneContextPointer& sceneContext, const RenderCo } } -void renderShape(RenderArgs* args, const ShapePlumberPointer& shapeContext, const Item& item) { +void renderShape(RenderArgs* args, const ShapePlumberPointer& shapeContext, const Item& item, const ShapeKey& globalKey) { assert(item.getKey().isShape()); - const auto& key = item.getShapeKey(); + auto key = item.getShapeKey() | globalKey; if (key.isValid() && !key.hasOwnPipeline()) { args->_pipeline = shapeContext->pickPipeline(args, key); if (args->_pipeline) { @@ -56,7 +56,7 @@ void renderShape(RenderArgs* args, const ShapePlumberPointer& shapeContext, cons } void render::renderShapes(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, - const ShapePlumberPointer& shapeContext, const ItemBounds& inItems, int maxDrawnItems) { + const ShapePlumberPointer& shapeContext, const ItemBounds& inItems, int maxDrawnItems, const ShapeKey& globalKey) { auto& scene = sceneContext->_scene; RenderArgs* args = renderContext->args; @@ -66,12 +66,12 @@ void render::renderShapes(const SceneContextPointer& sceneContext, const RenderC } for (auto i = 0; i < numItemsToDraw; ++i) { auto& item = scene->getItem(inItems[i].id); - renderShape(args, shapeContext, item); + renderShape(args, shapeContext, item, globalKey); } } void render::renderStateSortShapes(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, - const ShapePlumberPointer& shapeContext, const ItemBounds& inItems, int maxDrawnItems) { + const ShapePlumberPointer& shapeContext, const ItemBounds& inItems, int maxDrawnItems, const ShapeKey& globalKey) { auto& scene = sceneContext->_scene; RenderArgs* args = renderContext->args; @@ -91,7 +91,7 @@ void render::renderStateSortShapes(const SceneContextPointer& sceneContext, cons { assert(item.getKey().isShape()); - const auto key = item.getShapeKey(); + auto key = item.getShapeKey() | globalKey; if (key.isValid() && !key.hasOwnPipeline()) { auto& bucket = sortedShapes[key]; if (bucket.empty()) { diff --git a/libraries/render/src/render/DrawTask.h b/libraries/render/src/render/DrawTask.h index 6e0e5ba10b..a9c5f3a4d8 100755 --- a/libraries/render/src/render/DrawTask.h +++ b/libraries/render/src/render/DrawTask.h @@ -17,8 +17,8 @@ namespace render { void renderItems(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const ItemBounds& inItems, int maxDrawnItems = -1); -void renderShapes(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const ShapePlumberPointer& shapeContext, const ItemBounds& inItems, int maxDrawnItems = -1); -void renderStateSortShapes(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const ShapePlumberPointer& shapeContext, const ItemBounds& inItems, int maxDrawnItems = -1); +void renderShapes(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const ShapePlumberPointer& shapeContext, const ItemBounds& inItems, int maxDrawnItems = -1, const ShapeKey& globalKey = ShapeKey()); +void renderStateSortShapes(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const ShapePlumberPointer& shapeContext, const ItemBounds& inItems, int maxDrawnItems = -1, const ShapeKey& globalKey = ShapeKey()); class DrawLightConfig : public Job::Config { Q_OBJECT diff --git a/libraries/render/src/render/ShapePipeline.h b/libraries/render/src/render/ShapePipeline.h index 0c77a15184..73e8f82f24 100644 --- a/libraries/render/src/render/ShapePipeline.h +++ b/libraries/render/src/render/ShapePipeline.h @@ -46,6 +46,10 @@ public: ShapeKey() : _flags{ 0 } {} ShapeKey(const Flags& flags) : _flags{flags} {} + friend ShapeKey operator&(const ShapeKey& _Left, const ShapeKey& _Right) { return ShapeKey(_Left._flags & _Right._flags); } + friend ShapeKey operator|(const ShapeKey& _Left, const ShapeKey& _Right) { return ShapeKey(_Left._flags | _Right._flags); } + friend ShapeKey operator^(const ShapeKey& _Left, const ShapeKey& _Right) { return ShapeKey(_Left._flags ^ _Right._flags); } + class Builder { public: Builder() {} @@ -144,7 +148,7 @@ public: bool isSkinned() const { return _flags[SKINNED]; } bool isDepthOnly() const { return _flags[DEPTH_ONLY]; } bool isDepthBiased() const { return _flags[DEPTH_BIAS]; } - bool isWireFrame() const { return _flags[WIREFRAME]; } + bool isWireframe() const { return _flags[WIREFRAME]; } bool isCullFace() const { return !_flags[NO_CULL_FACE]; } bool hasOwnPipeline() const { return _flags[OWN_PIPELINE]; } @@ -180,7 +184,7 @@ inline QDebug operator<<(QDebug debug, const ShapeKey& key) { << "isSkinned:" << key.isSkinned() << "isDepthOnly:" << key.isDepthOnly() << "isDepthBiased:" << key.isDepthBiased() - << "isWireFrame:" << key.isWireFrame() + << "isWireframe:" << key.isWireframe() << "isCullFace:" << key.isCullFace() << "]"; } diff --git a/libraries/render/src/render/drawItemStatus.slv b/libraries/render/src/render/drawItemStatus.slv index cb4ae7ebd2..792f2733c5 100644 --- a/libraries/render/src/render/drawItemStatus.slv +++ b/libraries/render/src/render/drawItemStatus.slv @@ -75,7 +75,7 @@ void main(void) { vec4(1.0, 1.0, 0.0, 1.0) ); - const vec2 ICON_PIXEL_SIZE = vec2(20, 20); + const vec2 ICON_PIXEL_SIZE = vec2(36, 36); const vec2 MARGIN_PIXEL_SIZE = vec2(2, 2); const vec2 ICON_GRID_SLOTS[MAX_NUM_ICONS] = vec2[MAX_NUM_ICONS](vec2(-1.5, 0.5), vec2(-0.5, 0.5), @@ -114,7 +114,7 @@ void main(void) { varColor = vec4(paintRainbow(abs(iconStatus.y)), 1.0); // Pass the texcoord and the z texcoord is representing the texture icon - varTexcoord = vec3((quadPos.xy + 1.0) * 0.5, iconStatus.z); + varTexcoord = vec3( (quadPos.x + 1.0) * 0.5, (quadPos.y + 1.0) * -0.5, iconStatus.z); // Also changes the size of the notification vec2 iconScale = ICON_PIXEL_SIZE; diff --git a/libraries/script-engine/src/AudioScriptingInterface.cpp b/libraries/script-engine/src/AudioScriptingInterface.cpp index fcc1f201f9..8452494d95 100644 --- a/libraries/script-engine/src/AudioScriptingInterface.cpp +++ b/libraries/script-engine/src/AudioScriptingInterface.cpp @@ -19,11 +19,6 @@ void registerAudioMetaTypes(QScriptEngine* engine) { qScriptRegisterMetaType(engine, soundSharedPointerToScriptValue, soundSharedPointerFromScriptValue); } -AudioScriptingInterface& AudioScriptingInterface::getInstance() { - static AudioScriptingInterface staticInstance; - return staticInstance; -} - AudioScriptingInterface::AudioScriptingInterface() : _localAudioInterface(NULL) { diff --git a/libraries/script-engine/src/AudioScriptingInterface.h b/libraries/script-engine/src/AudioScriptingInterface.h index 6cce78d48f..e97bc329c6 100644 --- a/libraries/script-engine/src/AudioScriptingInterface.h +++ b/libraries/script-engine/src/AudioScriptingInterface.h @@ -14,18 +14,20 @@ #include #include +#include #include class ScriptAudioInjector; -class AudioScriptingInterface : public QObject { +class AudioScriptingInterface : public QObject, public Dependency { Q_OBJECT -public: - static AudioScriptingInterface& getInstance(); + SINGLETON_DEPENDENCY +public: void setLocalAudioInterface(AbstractAudioInterface* audioInterface) { _localAudioInterface = audioInterface; } protected: + // this method is protected to stop C++ callers from calling, but invokable from script Q_INVOKABLE ScriptAudioInjector* playSound(SharedSoundPointer sound, const AudioInjectorOptions& injectorOptions = AudioInjectorOptions()); @@ -42,6 +44,7 @@ signals: private: AudioScriptingInterface(); + AbstractAudioInterface* _localAudioInterface; }; diff --git a/libraries/script-engine/src/BaseScriptEngine.h b/libraries/script-engine/src/BaseScriptEngine.h deleted file mode 100644 index 27a6eff33d..0000000000 --- a/libraries/script-engine/src/BaseScriptEngine.h +++ /dev/null @@ -1,67 +0,0 @@ -// -// BaseScriptEngine.h -// libraries/script-engine/src -// -// Created by Timothy Dedischew on 02/01/17. -// Copyright 2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#ifndef hifi_BaseScriptEngine_h -#define hifi_BaseScriptEngine_h - -#include -#include -#include - -#include "SettingHandle.h" - -// common base class for extending QScriptEngine itself -class BaseScriptEngine : public QScriptEngine { - Q_OBJECT -public: - static const QString SCRIPT_EXCEPTION_FORMAT; - static const QString SCRIPT_BACKTRACE_SEP; - - BaseScriptEngine() {} - - Q_INVOKABLE QScriptValue evaluateInClosure(const QScriptValue& locals, const QScriptProgram& program); - - Q_INVOKABLE QScriptValue lintScript(const QString& sourceCode, const QString& fileName, const int lineNumber = 1); - Q_INVOKABLE QScriptValue makeError(const QScriptValue& other = QScriptValue(), const QString& type = "Error"); - Q_INVOKABLE QString formatException(const QScriptValue& exception); - QScriptValue cloneUncaughtException(const QString& detail = QString()); - -signals: - void unhandledException(const QScriptValue& exception); - -protected: - void _emitUnhandledException(const QScriptValue& exception); - QScriptValue newLambdaFunction(std::function operation, const QScriptValue& data = QScriptValue(), const QScriptEngine::ValueOwnership& ownership = QScriptEngine::AutoOwnership); - - static const QString _SETTINGS_ENABLE_EXTENDED_EXCEPTIONS; - Setting::Handle _enableExtendedJSExceptions { _SETTINGS_ENABLE_EXTENDED_EXCEPTIONS, true }; -#ifdef DEBUG_JS - static void _debugDump(const QString& header, const QScriptValue& object, const QString& footer = QString()); -#endif -}; - -// Lambda helps create callable QScriptValues out of std::functions: -// (just meant for use from within the script engine itself) -class Lambda : public QObject { - Q_OBJECT -public: - Lambda(QScriptEngine *engine, std::function operation, QScriptValue data); - ~Lambda(); - public slots: - QScriptValue call(); - QString toString() const; -private: - QScriptEngine* engine; - std::function operation; - QScriptValue data; -}; - -#endif // hifi_BaseScriptEngine_h diff --git a/libraries/script-engine/src/Mat4.cpp b/libraries/script-engine/src/Mat4.cpp index 52b9690321..6676d0cde1 100644 --- a/libraries/script-engine/src/Mat4.cpp +++ b/libraries/script-engine/src/Mat4.cpp @@ -54,7 +54,7 @@ glm::mat4 Mat4::inverse(const glm::mat4& m) const { return glm::inverse(m); } -glm::vec3 Mat4::getFront(const glm::mat4& m) const { +glm::vec3 Mat4::getForward(const glm::mat4& m) const { return glm::vec3(-m[0][2], -m[1][2], -m[2][2]); } diff --git a/libraries/script-engine/src/Mat4.h b/libraries/script-engine/src/Mat4.h index 8b2a8aa8c1..19bbbe178a 100644 --- a/libraries/script-engine/src/Mat4.h +++ b/libraries/script-engine/src/Mat4.h @@ -37,7 +37,9 @@ public slots: glm::mat4 inverse(const glm::mat4& m) const; - glm::vec3 getFront(const glm::mat4& m) const; + // redundant, calls getForward which better describes the returned vector as a direction + glm::vec3 getFront(const glm::mat4& m) const { return getForward(m); } + glm::vec3 getForward(const glm::mat4& m) const; glm::vec3 getRight(const glm::mat4& m) const; glm::vec3 getUp(const glm::mat4& m) const; diff --git a/libraries/script-engine/src/MeshProxy.h b/libraries/script-engine/src/MeshProxy.h new file mode 100644 index 0000000000..82f5038348 --- /dev/null +++ b/libraries/script-engine/src/MeshProxy.h @@ -0,0 +1,41 @@ +// +// MeshProxy.h +// libraries/script-engine/src +// +// Created by Seth Alves on 2017-1-27. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_MeshProxy_h +#define hifi_MeshProxy_h + +#include + +using MeshPointer = std::shared_ptr; + +class MeshProxy : public QObject { + Q_OBJECT + +public: + MeshProxy(MeshPointer mesh) : _mesh(mesh) {} + ~MeshProxy() {} + + MeshPointer getMeshPointer() const { return _mesh; } + + Q_INVOKABLE int getNumVertices() const { return (int)_mesh->getNumVertices(); } + Q_INVOKABLE glm::vec3 getPos3(int index) const { return _mesh->getPos3(index); } + + +protected: + MeshPointer _mesh; +}; + +Q_DECLARE_METATYPE(MeshProxy*); + +class MeshProxyList : public QList {}; // typedef and using fight with the Qt macros/templates, do this instead +Q_DECLARE_METATYPE(MeshProxyList); + +#endif // hifi_MeshProxy_h diff --git a/libraries/script-engine/src/ModelScriptingInterface.cpp b/libraries/script-engine/src/ModelScriptingInterface.cpp new file mode 100644 index 0000000000..833ac5b64d --- /dev/null +++ b/libraries/script-engine/src/ModelScriptingInterface.cpp @@ -0,0 +1,159 @@ +// +// ModelScriptingInterface.cpp +// libraries/script-engine/src +// +// Created by Seth Alves on 2017-1-27. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include +#include +#include +#include "ScriptEngine.h" +#include "ModelScriptingInterface.h" +#include "OBJWriter.h" + +ModelScriptingInterface::ModelScriptingInterface(QObject* parent) : QObject(parent) { + _modelScriptEngine = qobject_cast(parent); +} + +QScriptValue meshToScriptValue(QScriptEngine* engine, MeshProxy* const &in) { + return engine->newQObject(in, QScriptEngine::QtOwnership, + QScriptEngine::ExcludeDeleteLater | QScriptEngine::ExcludeChildObjects); +} + +void meshFromScriptValue(const QScriptValue& value, MeshProxy* &out) { + out = qobject_cast(value.toQObject()); +} + +QScriptValue meshesToScriptValue(QScriptEngine* engine, const MeshProxyList &in) { + return engine->toScriptValue(in); +} + +void meshesFromScriptValue(const QScriptValue& value, MeshProxyList &out) { + QScriptValueIterator itr(value); + while(itr.hasNext()) { + itr.next(); + MeshProxy* meshProxy = qscriptvalue_cast(itr.value()); + if (meshProxy) { + out.append(meshProxy); + } + } +} + +QString ModelScriptingInterface::meshToOBJ(MeshProxyList in) { + QList meshes; + foreach (const MeshProxy* meshProxy, in) { + meshes.append(meshProxy->getMeshPointer()); + } + + return writeOBJToString(meshes); +} + +QScriptValue ModelScriptingInterface::appendMeshes(MeshProxyList in) { + // figure out the size of the resulting mesh + size_t totalVertexCount { 0 }; + size_t totalAttributeCount { 0 }; + size_t totalIndexCount { 0 }; + foreach (const MeshProxy* meshProxy, in) { + MeshPointer mesh = meshProxy->getMeshPointer(); + totalVertexCount += mesh->getNumVertices(); + + int attributeTypeNormal = gpu::Stream::InputSlot::NORMAL; // libraries/gpu/src/gpu/Stream.h + const gpu::BufferView& normalsBufferView = mesh->getAttributeBuffer(attributeTypeNormal); + gpu::BufferView::Index numNormals = (gpu::BufferView::Index)normalsBufferView.getNumElements(); + totalAttributeCount += numNormals; + + totalIndexCount += mesh->getNumIndices(); + } + + // alloc the resulting mesh + gpu::Resource::Size combinedVertexSize = totalVertexCount * sizeof(glm::vec3); + unsigned char* combinedVertexData = new unsigned char[combinedVertexSize]; + unsigned char* combinedVertexDataCursor = combinedVertexData; + + gpu::Resource::Size combinedNormalSize = totalAttributeCount * sizeof(glm::vec3); + unsigned char* combinedNormalData = new unsigned char[combinedNormalSize]; + unsigned char* combinedNormalDataCursor = combinedNormalData; + + gpu::Resource::Size combinedIndexSize = totalIndexCount * sizeof(uint32_t); + unsigned char* combinedIndexData = new unsigned char[combinedIndexSize]; + unsigned char* combinedIndexDataCursor = combinedIndexData; + + uint32_t indexStartOffset { 0 }; + + foreach (const MeshProxy* meshProxy, in) { + MeshPointer mesh = meshProxy->getMeshPointer(); + mesh->forEach( + [&](glm::vec3 position){ + memcpy(combinedVertexDataCursor, &position, sizeof(position)); + combinedVertexDataCursor += sizeof(position); + }, + [&](glm::vec3 normal){ + memcpy(combinedNormalDataCursor, &normal, sizeof(normal)); + combinedNormalDataCursor += sizeof(normal); + }, + [&](uint32_t index){ + index += indexStartOffset; + memcpy(combinedIndexDataCursor, &index, sizeof(index)); + combinedIndexDataCursor += sizeof(index); + }); + + gpu::BufferView::Index numVertices = (gpu::BufferView::Index)mesh->getNumVertices(); + indexStartOffset += numVertices; + } + + model::MeshPointer result(new model::Mesh()); + + gpu::Element vertexElement = gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ); + gpu::Buffer* combinedVertexBuffer = new gpu::Buffer(combinedVertexSize, combinedVertexData); + gpu::BufferPointer combinedVertexBufferPointer(combinedVertexBuffer); + gpu::BufferView combinedVertexBufferView(combinedVertexBufferPointer, vertexElement); + result->setVertexBuffer(combinedVertexBufferView); + + int attributeTypeNormal = gpu::Stream::InputSlot::NORMAL; // libraries/gpu/src/gpu/Stream.h + gpu::Element normalElement = gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ); + gpu::Buffer* combinedNormalsBuffer = new gpu::Buffer(combinedNormalSize, combinedNormalData); + gpu::BufferPointer combinedNormalsBufferPointer(combinedNormalsBuffer); + gpu::BufferView combinedNormalsBufferView(combinedNormalsBufferPointer, normalElement); + result->addAttribute(attributeTypeNormal, combinedNormalsBufferView); + + gpu::Element indexElement = gpu::Element(gpu::SCALAR, gpu::UINT32, gpu::RAW); + gpu::Buffer* combinedIndexesBuffer = new gpu::Buffer(combinedIndexSize, combinedIndexData); + gpu::BufferPointer combinedIndexesBufferPointer(combinedIndexesBuffer); + gpu::BufferView combinedIndexesBufferView(combinedIndexesBufferPointer, indexElement); + result->setIndexBuffer(combinedIndexesBufferView); + + std::vector parts; + parts.emplace_back(model::Mesh::Part((model::Index)0, // startIndex + (model::Index)result->getNumIndices(), // numIndices + (model::Index)0, // baseVertex + model::Mesh::TRIANGLES)); // topology + result->setPartBuffer(gpu::BufferView(new gpu::Buffer(parts.size() * sizeof(model::Mesh::Part), + (gpu::Byte*) parts.data()), gpu::Element::PART_DRAWCALL)); + + + MeshProxy* resultProxy = new MeshProxy(result); + return meshToScriptValue(_modelScriptEngine, resultProxy); +} + + + +QScriptValue ModelScriptingInterface::transformMesh(glm::mat4 transform, MeshProxy* meshProxy) { + if (!meshProxy) { + return QScriptValue(false); + } + MeshPointer mesh = meshProxy->getMeshPointer(); + if (!mesh) { + return QScriptValue(false); + } + + model::MeshPointer result = mesh->map([&](glm::vec3 position){ return glm::vec3(transform * glm::vec4(position, 1.0f)); }, + [&](glm::vec3 normal){ return glm::vec3(transform * glm::vec4(normal, 0.0f)); }, + [&](uint32_t index){ return index; }); + MeshProxy* resultProxy = new MeshProxy(result); + return meshToScriptValue(_modelScriptEngine, resultProxy); +} diff --git a/libraries/script-engine/src/ModelScriptingInterface.h b/libraries/script-engine/src/ModelScriptingInterface.h new file mode 100644 index 0000000000..14789943e3 --- /dev/null +++ b/libraries/script-engine/src/ModelScriptingInterface.h @@ -0,0 +1,45 @@ +// +// ModelScriptingInterface.h +// libraries/script-engine/src +// +// Created by Seth Alves on 2017-1-27. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + + +#ifndef hifi_ModelScriptingInterface_h +#define hifi_ModelScriptingInterface_h + +#include +#include +#include +#include +#include "MeshProxy.h" + +using MeshPointer = std::shared_ptr; +class ScriptEngine; + +class ModelScriptingInterface : public QObject { + Q_OBJECT + +public: + ModelScriptingInterface(QObject* parent); + + Q_INVOKABLE QString meshToOBJ(MeshProxyList in); + Q_INVOKABLE QScriptValue appendMeshes(MeshProxyList in); + Q_INVOKABLE QScriptValue transformMesh(glm::mat4 transform, MeshProxy* meshProxy); + +private: + ScriptEngine* _modelScriptEngine { nullptr }; +}; + +QScriptValue meshToScriptValue(QScriptEngine* engine, MeshProxy* const &in); +void meshFromScriptValue(const QScriptValue& value, MeshProxy* &out); + +QScriptValue meshesToScriptValue(QScriptEngine* engine, const MeshProxyList &in); +void meshesFromScriptValue(const QScriptValue& value, MeshProxyList &out); + +#endif // hifi_ModelScriptingInterface_h diff --git a/libraries/script-engine/src/Quat.cpp b/libraries/script-engine/src/Quat.cpp index 6c2e7a349e..6d49ed27c1 100644 --- a/libraries/script-engine/src/Quat.cpp +++ b/libraries/script-engine/src/Quat.cpp @@ -68,7 +68,7 @@ glm::quat Quat::inverse(const glm::quat& q) { return glm::inverse(q); } -glm::vec3 Quat::getFront(const glm::quat& orientation) { +glm::vec3 Quat::getForward(const glm::quat& orientation) { return orientation * Vectors::FRONT; } diff --git a/libraries/script-engine/src/Quat.h b/libraries/script-engine/src/Quat.h index b51f1cb47e..8a88767a41 100644 --- a/libraries/script-engine/src/Quat.h +++ b/libraries/script-engine/src/Quat.h @@ -45,7 +45,9 @@ public slots: glm::quat fromPitchYawRollDegrees(float pitch, float yaw, float roll); // degrees glm::quat fromPitchYawRollRadians(float pitch, float yaw, float roll); // radians glm::quat inverse(const glm::quat& q); - glm::vec3 getFront(const glm::quat& orientation); + // redundant, calls getForward which better describes the returned vector as a direction + glm::vec3 getFront(const glm::quat& orientation) { return getForward(orientation); } + glm::vec3 getForward(const glm::quat& orientation); glm::vec3 getRight(const glm::quat& orientation); glm::vec3 getUp(const glm::quat& orientation); glm::vec3 safeEulerAngles(const glm::quat& orientation); // degrees diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index d721d1c86f..a5c94c1bb4 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -19,6 +19,9 @@ #include #include +#include +#include + #include #include @@ -65,18 +68,25 @@ #include "RecordingScriptingInterface.h" #include "ScriptEngines.h" #include "TabletScriptingInterface.h" +#include "ModelScriptingInterface.h" + #include #include "MIDIEvent.h" +const QString ScriptEngine::_SETTINGS_ENABLE_EXTENDED_EXCEPTIONS { + "com.highfidelity.experimental.enableExtendedJSExceptions" +}; + +static const int MAX_MODULE_ID_LENGTH { 4096 }; +static const int MAX_DEBUG_VALUE_LENGTH { 80 }; + static const QScriptEngine::QObjectWrapOptions DEFAULT_QOBJECT_WRAP_OPTIONS = QScriptEngine::ExcludeDeleteLater | QScriptEngine::ExcludeChildObjects; static const QScriptValue::PropertyFlags READONLY_PROP_FLAGS { QScriptValue::ReadOnly | QScriptValue::Undeletable }; static const QScriptValue::PropertyFlags READONLY_HIDDEN_PROP_FLAGS { READONLY_PROP_FLAGS | QScriptValue::SkipInEnumeration }; - - static const bool HIFI_AUTOREFRESH_FILE_SCRIPTS { true }; Q_DECLARE_METATYPE(QScriptEngine::FunctionSignature) @@ -84,7 +94,7 @@ int functionSignatureMetaID = qRegisterMetaTypeargumentCount(); i++) { if (i > 0) { @@ -141,7 +151,7 @@ QString encodeEntityIdIntoEntityUrl(const QString& url, const QString& entityID) } QString ScriptEngine::logException(const QScriptValue& exception) { - auto message = formatException(exception); + auto message = formatException(exception, _enableExtendedJSExceptions.get()); scriptErrorMessage(message); return message; } @@ -333,7 +343,7 @@ void ScriptEngine::runInThread() { // The thread interface cannot live on itself, and we want to move this into the thread, so // the thread cannot have this as a parent. QThread* workerThread = new QThread(); - workerThread->setObjectName(QString("Script Thread:") + getFilename()); + workerThread->setObjectName(QString("js:") + getFilename().replace("about:","")); moveToThread(workerThread); // NOTE: If you connect any essential signals for proper shutdown or cleanup of @@ -454,17 +464,17 @@ void ScriptEngine::loadURL(const QUrl& scriptURL, bool reload) { void ScriptEngine::scriptErrorMessage(const QString& message) { qCCritical(scriptengine) << qPrintable(message); - emit errorMessage(message); + emit errorMessage(message, getFilename()); } void ScriptEngine::scriptWarningMessage(const QString& message) { qCWarning(scriptengine) << message; - emit warningMessage(message); + emit warningMessage(message, getFilename()); } void ScriptEngine::scriptInfoMessage(const QString& message) { qCInfo(scriptengine) << message; - emit infoMessage(message); + emit infoMessage(message, getFilename()); } // Even though we never pass AnimVariantMap directly to and from javascript, the queued invokeMethod of @@ -532,6 +542,40 @@ static QScriptValue createScriptableResourcePrototype(QScriptEngine* engine) { return prototype; } +void ScriptEngine::resetModuleCache(bool deleteScriptCache) { + if (QThread::currentThread() != thread()) { + executeOnScriptThread([=]() { resetModuleCache(deleteScriptCache); }); + return; + } + auto jsRequire = globalObject().property("Script").property("require"); + auto cache = jsRequire.property("cache"); + auto cacheMeta = jsRequire.data(); + + if (deleteScriptCache) { + QScriptValueIterator it(cache); + while (it.hasNext()) { + it.next(); + if (it.flags() & QScriptValue::SkipInEnumeration) { + continue; + } + qCDebug(scriptengine) << "resetModuleCache(true) -- staging " << it.name() << " for cache reset at next require"; + cacheMeta.setProperty(it.name(), true); + } + } + cache = newObject(); + if (!cacheMeta.isObject()) { + cacheMeta = newObject(); + cacheMeta.setProperty("id", "Script.require.cacheMeta"); + cacheMeta.setProperty("type", "cacheMeta"); + jsRequire.setData(cacheMeta); + } + cache.setProperty("__created__", (double)QDateTime::currentMSecsSinceEpoch(), QScriptValue::SkipInEnumeration); +#if DEBUG_JS_MODULES + cache.setProperty("__meta__", cacheMeta, READONLY_HIDDEN_PROP_FLAGS); +#endif + jsRequire.setProperty("cache", cache, READONLY_PROP_FLAGS); +} + void ScriptEngine::init() { if (_isInitialized) { return; // only initialize once @@ -541,16 +585,6 @@ void ScriptEngine::init() { auto entityScriptingInterface = DependencyManager::get(); entityScriptingInterface->init(); - connect(entityScriptingInterface.data(), &EntityScriptingInterface::deletingEntity, this, [this](const EntityItemID& entityID) { - if (_entityScripts.contains(entityID)) { - if (isEntityScriptRunning(entityID)) { - qCWarning(scriptengine) << "deletingEntity while entity script is still running!" << entityID; - } - _entityScripts.remove(entityID); - emit entityScriptDetailsUpdated(); - } - }); - // register various meta-types registerMetaTypes(this); @@ -593,9 +627,22 @@ void ScriptEngine::init() { qScriptRegisterMetaType(this, qWSCloseCodeToScriptValue, qWSCloseCodeFromScriptValue); qScriptRegisterMetaType(this, wscReadyStateToScriptValue, wscReadyStateFromScriptValue); + // NOTE: You do not want to end up creating new instances of singletons here. They will be on the ScriptEngine thread + // and are likely to be unusable if we "reset" the ScriptEngine by creating a new one (on a whole new thread). + registerGlobalObject("Script", this); - registerGlobalObject("Audio", &AudioScriptingInterface::getInstance()); + { + // set up Script.require.resolve and Script.require.cache + auto Script = globalObject().property("Script"); + auto require = Script.property("require"); + auto resolve = Script.property("_requireResolve"); + require.setProperty("resolve", resolve, READONLY_PROP_FLAGS); + resetModuleCache(); + } + + registerGlobalObject("Audio", DependencyManager::get().data()); + registerGlobalObject("Entities", entityScriptingInterface.data()); registerGlobalObject("Quat", &_quatLibrary); registerGlobalObject("Vec3", &_vec3Library); @@ -604,7 +651,7 @@ void ScriptEngine::init() { registerGlobalObject("Messages", DependencyManager::get().data()); registerGlobalObject("File", new FileScriptingInterface(this)); - + qScriptRegisterMetaType(this, animVarMapToScriptValue, animVarMapFromScriptValue); qScriptRegisterMetaType(this, resultHandlerToScriptValue, resultHandlerFromScriptValue); @@ -622,6 +669,10 @@ void ScriptEngine::init() { registerGlobalObject("Resources", DependencyManager::get().data()); registerGlobalObject("DebugDraw", &DebugDraw::getInstance()); + + registerGlobalObject("Model", new ModelScriptingInterface(this)); + qScriptRegisterMetaType(this, meshToScriptValue, meshFromScriptValue); + qScriptRegisterMetaType(this, meshesToScriptValue, meshesFromScriptValue); } void ScriptEngine::registerValue(const QString& valueName, QScriptValue value) { @@ -863,6 +914,11 @@ void ScriptEngine::addEventHandler(const EntityItemID& entityID, const QString& handlersForEvent << handlerData; // Note that the same handler can be added many times. See removeEntityEventHandler(). } +// this is not redundant -- the version in BaseScriptEngine is specifically not Q_INVOKABLE +QScriptValue ScriptEngine::evaluateInClosure(const QScriptValue& closure, const QScriptProgram& program) { + return BaseScriptEngine::evaluateInClosure(closure, program); +} + QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fileName, int lineNumber) { if (DependencyManager::get()->isStopped()) { return QScriptValue(); // bail early @@ -885,29 +941,26 @@ QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fi // Check syntax auto syntaxError = lintScript(sourceCode, fileName); if (syntaxError.isError()) { - if (isEvaluating()) { - currentContext()->throwValue(syntaxError); - } else { + if (!isEvaluating()) { syntaxError.setProperty("detail", "evaluate"); - emit unhandledException(syntaxError); } + raiseException(syntaxError); + maybeEmitUncaughtException("lint"); return syntaxError; } QScriptProgram program { sourceCode, fileName, lineNumber }; if (program.isNull()) { // can this happen? auto err = makeError("could not create QScriptProgram for " + fileName); - emit unhandledException(err); + raiseException(err); + maybeEmitUncaughtException("compile"); return err; } QScriptValue result; { result = BaseScriptEngine::evaluate(program); - if (!isEvaluating() && hasUncaughtException()) { - emit unhandledException(cloneUncaughtException(__FUNCTION__)); - clearExceptions(); - } + maybeEmitUncaughtException("evaluate"); } return result; } @@ -930,10 +983,7 @@ void ScriptEngine::run() { { evaluate(_scriptContents, _fileNameString); - if (!isEvaluating() && hasUncaughtException()) { - emit unhandledException(cloneUncaughtException(__FUNCTION__)); - clearExceptions(); - } + maybeEmitUncaughtException(__FUNCTION__); } #ifdef _WIN32 // VS13 does not sleep_until unless it uses the system_clock, see: @@ -1301,7 +1351,354 @@ QUrl ScriptEngine::resourcesPath() const { } void ScriptEngine::print(const QString& message) { - emit printedMessage(message); + emit printedMessage(message, getFilename()); +} + +// Script.require.resolve -- like resolvePath, but performs more validation and throws exceptions on invalid module identifiers (for consistency with Node.js) +QString ScriptEngine::_requireResolve(const QString& moduleId, const QString& relativeTo) { + if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { + return QString(); + } + QUrl defaultScriptsLoc = defaultScriptsLocation(); + QUrl url(moduleId); + + auto displayId = moduleId; + if (displayId.length() > MAX_DEBUG_VALUE_LENGTH) { + displayId = displayId.mid(0, MAX_DEBUG_VALUE_LENGTH) + "..."; + } + auto message = QString("Cannot find module '%1' (%2)").arg(displayId); + + auto throwResolveError = [&](const QScriptValue& error) -> QString { + raiseException(error); + maybeEmitUncaughtException("require.resolve"); + return QString(); + }; + + // de-fuzz the input a little by restricting to rational sizes + auto idLength = url.toString().length(); + if (idLength < 1 || idLength > MAX_MODULE_ID_LENGTH) { + auto details = QString("rejecting invalid module id size (%1 chars [1,%2])") + .arg(idLength).arg(MAX_MODULE_ID_LENGTH); + return throwResolveError(makeError(message.arg(details), "RangeError")); + } + + // this regex matches: absolute, dotted or path-like URLs + // (ie: the kind of stuff ScriptEngine::resolvePath already handles) + QRegularExpression qualified ("^\\w+:|^/|^[.]{1,2}(/|$)"); + + // this is for module.require (which is a bound version of require that's always relative to the module path) + if (!relativeTo.isEmpty()) { + url = QUrl(relativeTo).resolved(moduleId); + url = resolvePath(url.toString()); + } else if (qualified.match(moduleId).hasMatch()) { + url = resolvePath(moduleId); + } else { + // check if the moduleId refers to a "system" module + QString systemPath = defaultScriptsLoc.path(); + QString systemModulePath = QString("%1/modules/%2.js").arg(systemPath).arg(moduleId); + url = defaultScriptsLoc; + url.setPath(systemModulePath); + if (!QFileInfo(url.toLocalFile()).isFile()) { + if (!moduleId.contains("./")) { + // the user might be trying to refer to a relative file without anchoring it + // let's do them a favor and test for that case -- offering specific advice if detected + auto unanchoredUrl = resolvePath("./" + moduleId); + if (QFileInfo(unanchoredUrl.toLocalFile()).isFile()) { + auto msg = QString("relative module ids must be anchored; use './%1' instead") + .arg(moduleId); + return throwResolveError(makeError(message.arg(msg))); + } + } + return throwResolveError(makeError(message.arg("system module not found"))); + } + } + + if (url.isRelative()) { + return throwResolveError(makeError(message.arg("could not resolve module id"))); + } + + // if it looks like a local file, verify that it's an allowed path and really a file + if (url.isLocalFile()) { + QFileInfo file(url.toLocalFile()); + QUrl canonical = url; + if (file.exists()) { + canonical.setPath(file.canonicalFilePath()); + } + + bool disallowOutsideFiles = !defaultScriptsLocation().isParentOf(canonical) && !currentSandboxURL.isLocalFile(); + if (disallowOutsideFiles && !PathUtils::isDescendantOf(canonical, currentSandboxURL)) { + return throwResolveError(makeError(message.arg( + QString("path '%1' outside of origin script '%2' '%3'") + .arg(PathUtils::stripFilename(url)) + .arg(PathUtils::stripFilename(currentSandboxURL)) + .arg(canonical.toString()) + ))); + } + if (!file.exists()) { + return throwResolveError(makeError(message.arg("path does not exist: " + url.toLocalFile()))); + } + if (!file.isFile()) { + return throwResolveError(makeError(message.arg("path is not a file: " + url.toLocalFile()))); + } + } + + maybeEmitUncaughtException(__FUNCTION__); + return url.toString(); +} + +// retrieves the current parent module from the JS scope chain +QScriptValue ScriptEngine::currentModule() { + if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { + return unboundNullValue(); + } + auto jsRequire = globalObject().property("Script").property("require"); + auto cache = jsRequire.property("cache"); + auto candidate = QScriptValue(); + for (auto c = currentContext(); c && !candidate.isObject(); c = c->parentContext()) { + QScriptContextInfo contextInfo { c }; + candidate = cache.property(contextInfo.fileName()); + } + if (!candidate.isObject()) { + return QScriptValue(); + } + return candidate; +} + +// replaces or adds "module" to "parent.children[]" array +// (for consistency with Node.js and userscript cache invalidation without "cache busters") +bool ScriptEngine::registerModuleWithParent(const QScriptValue& module, const QScriptValue& parent) { + auto children = parent.property("children"); + if (children.isArray()) { + auto key = module.property("id"); + auto length = children.property("length").toInt32(); + for (int i = 0; i < length; i++) { + if (children.property(i).property("id").strictlyEquals(key)) { + qCDebug(scriptengine_module) << key.toString() << " updating parent.children[" << i << "] = module"; + children.setProperty(i, module); + return true; + } + } + qCDebug(scriptengine_module) << key.toString() << " appending parent.children[" << length << "] = module"; + children.setProperty(length, module); + return true; + } else if (parent.isValid()) { + qCDebug(scriptengine_module) << "registerModuleWithParent -- unrecognized parent" << parent.toVariant().toString(); + } + return false; +} + +// creates a new JS "module" Object with default metadata properties +QScriptValue ScriptEngine::newModule(const QString& modulePath, const QScriptValue& parent) { + auto closure = newObject(); + auto exports = newObject(); + auto module = newObject(); + qCDebug(scriptengine_module) << "newModule" << modulePath << parent.property("filename").toString(); + + closure.setProperty("module", module, READONLY_PROP_FLAGS); + + // note: this becomes the "exports" free variable, so should not be set read only + closure.setProperty("exports", exports); + + // make the closure available to module instantiation + module.setProperty("__closure__", closure, READONLY_HIDDEN_PROP_FLAGS); + + // for consistency with Node.js Module + module.setProperty("id", modulePath, READONLY_PROP_FLAGS); + module.setProperty("filename", modulePath, READONLY_PROP_FLAGS); + module.setProperty("exports", exports); // not readonly + module.setProperty("loaded", false, READONLY_PROP_FLAGS); + module.setProperty("parent", parent, READONLY_PROP_FLAGS); + module.setProperty("children", newArray(), READONLY_PROP_FLAGS); + + // module.require is a bound version of require that always resolves relative to that module's path + auto boundRequire = QScriptEngine::evaluate("(function(id) { return Script.require(Script.require.resolve(id, this.filename)); })", "(boundRequire)"); + module.setProperty("require", boundRequire, READONLY_PROP_FLAGS); + + return module; +} + +// synchronously fetch a module's source code using BatchLoader +QVariantMap ScriptEngine::fetchModuleSource(const QString& modulePath, const bool forceDownload) { + using UrlMap = QMap; + auto scriptCache = DependencyManager::get(); + QVariantMap req; + qCDebug(scriptengine_module) << "require.fetchModuleSource: " << QUrl(modulePath).fileName() << QThread::currentThread(); + + auto onload = [=, &req](const UrlMap& data, const UrlMap& _status) { + auto url = modulePath; + auto status = _status[url]; + auto contents = data[url]; + qCDebug(scriptengine_module) << "require.fetchModuleSource.onload: " << QUrl(url).fileName() << status << QThread::currentThread(); + if (isStopping()) { + req["status"] = "Stopped"; + req["success"] = false; + } else { + req["url"] = url; + req["status"] = status; + req["success"] = ScriptCache::isSuccessStatus(status); + req["contents"] = contents; + } + }; + + if (forceDownload) { + qCDebug(scriptengine_module) << "require.requestScript -- clearing cache for" << modulePath; + scriptCache->deleteScript(modulePath); + } + BatchLoader* loader = new BatchLoader(QList({ modulePath })); + connect(loader, &BatchLoader::finished, this, onload); + connect(this, &QObject::destroyed, loader, &QObject::deleteLater); + // fail faster? (since require() blocks the engine thread while resolving dependencies) + const int MAX_RETRIES = 1; + + loader->start(MAX_RETRIES); + + if (!loader->isFinished()) { + QTimer monitor; + QEventLoop loop; + QObject::connect(loader, &BatchLoader::finished, this, [this, &monitor, &loop]{ + monitor.stop(); + loop.quit(); + }); + + // this helps detect the case where stop() is invoked during the download + // but not seen in time to abort processing in onload()... + connect(&monitor, &QTimer::timeout, this, [this, &loop, &loader]{ + if (isStopping()) { + loop.exit(-1); + } + }); + monitor.start(500); + loop.exec(); + } + loader->deleteLater(); + return req; +} + +// evaluate a pending module object using the fetched source code +QScriptValue ScriptEngine::instantiateModule(const QScriptValue& module, const QString& sourceCode) { + QScriptValue result; + auto modulePath = module.property("filename").toString(); + auto closure = module.property("__closure__"); + + qCDebug(scriptengine_module) << QString("require.instantiateModule: %1 / %2 bytes") + .arg(QUrl(modulePath).fileName()).arg(sourceCode.length()); + + if (module.property("content-type").toString() == "application/json") { + qCDebug(scriptengine_module) << "... parsing as JSON"; + closure.setProperty("__json", sourceCode); + result = evaluateInClosure(closure, { "module.exports = JSON.parse(__json)", modulePath }); + } else { + // scoped vars for consistency with Node.js + closure.setProperty("require", module.property("require")); + closure.setProperty("__filename", modulePath, READONLY_HIDDEN_PROP_FLAGS); + closure.setProperty("__dirname", QString(modulePath).replace(QRegExp("/[^/]*$"), ""), READONLY_HIDDEN_PROP_FLAGS); + result = evaluateInClosure(closure, { sourceCode, modulePath }); + } + maybeEmitUncaughtException(__FUNCTION__); + return result; +} + +// CommonJS/Node.js like require/module support +QScriptValue ScriptEngine::require(const QString& moduleId) { + qCDebug(scriptengine_module) << "ScriptEngine::require(" << moduleId.left(MAX_DEBUG_VALUE_LENGTH) << ")"; + if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { + return unboundNullValue(); + } + + auto jsRequire = globalObject().property("Script").property("require"); + auto cacheMeta = jsRequire.data(); + auto cache = jsRequire.property("cache"); + auto parent = currentModule(); + + auto throwModuleError = [&](const QString& modulePath, const QScriptValue& error) { + cache.setProperty(modulePath, nullValue()); + if (!error.isNull()) { +#ifdef DEBUG_JS_MODULES + qCWarning(scriptengine_module) << "throwing module error:" << error.toString() << modulePath << error.property("stack").toString(); +#endif + raiseException(error); + } + maybeEmitUncaughtException("module"); + return unboundNullValue(); + }; + + // start by resolving the moduleId into a fully-qualified path/URL + QString modulePath = _requireResolve(moduleId); + if (modulePath.isNull() || hasUncaughtException()) { + // the resolver already threw an exception -- bail early + maybeEmitUncaughtException(__FUNCTION__); + return unboundNullValue(); + } + + // check the resolved path against the cache + auto module = cache.property(modulePath); + + // modules get cached in `Script.require.cache` and (similar to Node.js) users can access it + // to inspect particular entries and invalidate them by deleting the key: + // `delete Script.require.cache[Script.require.resolve(moduleId)];` + + // cacheMeta is just used right now to tell deleted keys apart from undefined ones + bool invalidateCache = module.isUndefined() && cacheMeta.property(moduleId).isValid(); + + // reset the cacheMeta record so invalidation won't apply next time, even if the module fails to load + cacheMeta.setProperty(modulePath, QScriptValue()); + + auto exports = module.property("exports"); + if (!invalidateCache && exports.isObject()) { + // we have found a cached module -- just need to possibly register it with current parent + qCDebug(scriptengine_module) << QString("require - using cached module '%1' for '%2' (loaded: %3)") + .arg(modulePath).arg(moduleId).arg(module.property("loaded").toString()); + registerModuleWithParent(module, parent); + maybeEmitUncaughtException("cached module"); + return exports; + } + + // bootstrap / register new empty module + module = newModule(modulePath, parent); + registerModuleWithParent(module, parent); + + // add it to the cache (this is done early so any cyclic dependencies pick up) + cache.setProperty(modulePath, module); + + // download the module source + auto req = fetchModuleSource(modulePath, invalidateCache); + + if (!req.contains("success") || !req["success"].toBool()) { + auto error = QString("error retrieving script (%1)").arg(req["status"].toString()); + return throwModuleError(modulePath, error); + } + +#if DEBUG_JS_MODULES + qCDebug(scriptengine_module) << "require.loaded: " << + QUrl(req["url"].toString()).fileName() << req["status"].toString(); +#endif + + auto sourceCode = req["contents"].toString(); + + if (QUrl(modulePath).fileName().endsWith(".json", Qt::CaseInsensitive)) { + module.setProperty("content-type", "application/json"); + } else { + module.setProperty("content-type", "application/javascript"); + } + + // evaluate the module + auto result = instantiateModule(module, sourceCode); + + if (result.isError() && !result.strictlyEquals(module.property("exports"))) { + qCWarning(scriptengine_module) << "-- result.isError --" << result.toString(); + return throwModuleError(modulePath, result); + } + + // mark as fully-loaded + module.setProperty("loaded", true, READONLY_PROP_FLAGS); + + // set up a new reference point for detecting cache key deletion + cacheMeta.setProperty(modulePath, module); + + qCDebug(scriptengine_module) << "//ScriptEngine::require(" << moduleId << ")"; + + maybeEmitUncaughtException(__FUNCTION__); + return module.property("exports"); } // If a callback is specified, the included files will be loaded asynchronously and the callback will be called @@ -1309,6 +1706,9 @@ void ScriptEngine::print(const QString& message) { // If no callback is specified, the included files will be loaded synchronously and will block execution until // all of the files have finished loading. void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callback) { + if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { + return; + } if (DependencyManager::get()->isStopped()) { scriptWarningMessage("Script.include() while shutting down is ignored... includeFiles:" + includeFiles.join(",") + "parent script:" + getFilename()); @@ -1371,7 +1771,7 @@ void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callbac doWithEnvironment(capturedEntityIdentifier, capturedSandboxURL, operation); if (hasUncaughtException()) { - emit unhandledException(cloneUncaughtException(__FUNCTION__)); + emit unhandledException(cloneUncaughtException("evaluateInclude")); clearExceptions(); } } else { @@ -1418,6 +1818,9 @@ void ScriptEngine::include(const QString& includeFile, QScriptValue callback) { // as a stand-alone script. To accomplish this, the ScriptEngine class just emits a signal which // the Application or other context will connect to in order to know to actually load the script void ScriptEngine::load(const QString& loadFile) { + if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { + return; + } if (DependencyManager::get()->isStopped()) { scriptWarningMessage("Script.load() while shutting down is ignored... loadFile:" + loadFile + "parent script:" + getFilename()); @@ -1487,6 +1890,52 @@ void ScriptEngine::updateEntityScriptStatus(const EntityItemID& entityID, const emit entityScriptDetailsUpdated(); } +QVariant ScriptEngine::cloneEntityScriptDetails(const EntityItemID& entityID) { + static const QVariant NULL_VARIANT { qVariantFromValue((QObject*)nullptr) }; + QVariantMap map; + if (entityID.isNull()) { + // TODO: find better way to report JS Error across thread/process boundaries + map["isError"] = true; + map["errorInfo"] = "Error: getEntityScriptDetails -- invalid entityID"; + } else { +#ifdef DEBUG_ENTITY_STATES + qDebug() << "cloneEntityScriptDetails" << entityID << QThread::currentThread(); +#endif + EntityScriptDetails scriptDetails; + if (getEntityScriptDetails(entityID, scriptDetails)) { +#ifdef DEBUG_ENTITY_STATES + qDebug() << "gotEntityScriptDetails" << scriptDetails.status << QThread::currentThread(); +#endif + map["isRunning"] = isEntityScriptRunning(entityID); + map["status"] = EntityScriptStatus_::valueToKey(scriptDetails.status).toLower(); + map["errorInfo"] = scriptDetails.errorInfo; + map["entityID"] = entityID.toString(); +#ifdef DEBUG_ENTITY_STATES + { + auto debug = QVariantMap(); + debug["script"] = scriptDetails.scriptText; + debug["scriptObject"] = scriptDetails.scriptObject.toVariant(); + debug["lastModified"] = (qlonglong)scriptDetails.lastModified; + debug["sandboxURL"] = scriptDetails.definingSandboxURL; + map["debug"] = debug; + } +#endif + } else { +#ifdef DEBUG_ENTITY_STATES + qDebug() << "!gotEntityScriptDetails" << QThread::currentThread(); +#endif + map["isError"] = true; + map["errorInfo"] = "Entity script details unavailable"; + map["entityID"] = entityID.toString(); + } + } + return map; +} + +QFuture ScriptEngine::getLocalEntityScriptDetails(const EntityItemID& entityID) { + return QtConcurrent::run(this, &ScriptEngine::cloneEntityScriptDetails, entityID); +} + bool ScriptEngine::getEntityScriptDetails(const EntityItemID& entityID, EntityScriptDetails &details) const { auto it = _entityScripts.constFind(entityID); if (it == _entityScripts.constEnd()) { @@ -1625,10 +2074,10 @@ void ScriptEngine::loadEntityScript(const EntityItemID& entityID, const QString& auto scriptCache = DependencyManager::get(); // note: see EntityTreeRenderer.cpp for shared pointer lifecycle management - QWeakPointer weakRef(sharedFromThis()); + QWeakPointer weakRef(sharedFromThis()); scriptCache->getScriptContents(entityScript, [this, weakRef, entityScript, entityID](const QString& url, const QString& contents, bool isURL, bool success, const QString& status) { - QSharedPointer strongRef(weakRef); + QSharedPointer strongRef(weakRef); if (!strongRef) { qCWarning(scriptengine) << "loadEntityScript.contentAvailable -- ScriptEngine was deleted during getScriptContents!!"; return; @@ -1747,13 +2196,12 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co timeout.setSingleShot(true); timeout.start(SANDBOX_TIMEOUT); connect(&timeout, &QTimer::timeout, [&sandbox, SANDBOX_TIMEOUT, scriptOrURL]{ - auto context = sandbox.currentContext(); - if (context) { qCDebug(scriptengine) << "ScriptEngine::entityScriptContentAvailable timeout(" << scriptOrURL << ")"; // Guard against infinite loops and non-performant code - context->throwError(QString("Timed out (entity constructors are limited to %1ms)").arg(SANDBOX_TIMEOUT)); - } + sandbox.raiseException( + sandbox.makeError(QString("Timed out (entity constructors are limited to %1ms)").arg(SANDBOX_TIMEOUT)) + ); }); testConstructor = sandbox.evaluate(program); @@ -1769,7 +2217,7 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co if (exception.isError()) { // create a local copy using makeError to decouple from the sandbox engine exception = makeError(exception); - setError(formatException(exception), EntityScriptStatus::ERROR_RUNNING_SCRIPT); + setError(formatException(exception, _enableExtendedJSExceptions.get()), EntityScriptStatus::ERROR_RUNNING_SCRIPT); emit unhandledException(exception); return; } @@ -1781,9 +2229,8 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co testConstructorType = "empty"; } QString testConstructorValue = testConstructor.toString(); - const int maxTestConstructorValueSize = 80; - if (testConstructorValue.size() > maxTestConstructorValueSize) { - testConstructorValue = testConstructorValue.mid(0, maxTestConstructorValueSize) + "..."; + if (testConstructorValue.size() > MAX_DEBUG_VALUE_LENGTH) { + testConstructorValue = testConstructorValue.mid(0, MAX_DEBUG_VALUE_LENGTH) + "..."; } auto message = QString("failed to load entity script -- expected a function, got %1, %2") .arg(testConstructorType).arg(testConstructorValue); @@ -1821,7 +2268,7 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co if (entityScriptObject.isError()) { auto exception = entityScriptObject; - setError(formatException(exception), EntityScriptStatus::ERROR_RUNNING_SCRIPT); + setError(formatException(exception, _enableExtendedJSExceptions.get()), EntityScriptStatus::ERROR_RUNNING_SCRIPT); emit unhandledException(exception); return; } @@ -1844,7 +2291,7 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co processDeferredEntityLoads(entityScript, entityID); } -void ScriptEngine::unloadEntityScript(const EntityItemID& entityID) { +void ScriptEngine::unloadEntityScript(const EntityItemID& entityID, bool shouldRemoveFromMap) { if (QThread::currentThread() != thread()) { #ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "*** WARNING *** ScriptEngine::unloadEntityScript() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] " @@ -1852,7 +2299,8 @@ void ScriptEngine::unloadEntityScript(const EntityItemID& entityID) { #endif QMetaObject::invokeMethod(this, "unloadEntityScript", - Q_ARG(const EntityItemID&, entityID)); + Q_ARG(const EntityItemID&, entityID), + Q_ARG(bool, shouldRemoveFromMap)); return; } #ifdef THREAD_DEBUGGING @@ -1864,10 +2312,17 @@ void ScriptEngine::unloadEntityScript(const EntityItemID& entityID) { const EntityScriptDetails &oldDetails = _entityScripts[entityID]; if (isEntityScriptRunning(entityID)) { callEntityScriptMethod(entityID, "unload"); - } else { + } +#ifdef DEBUG_ENTITY_STATES + else { qCDebug(scriptengine) << "unload called while !running" << entityID << oldDetails.status; } - if (oldDetails.status != EntityScriptStatus::UNLOADED) { +#endif + if (shouldRemoveFromMap) { + // this was a deleted entity, we've been asked to remove it from the map + _entityScripts.remove(entityID); + emit entityScriptDetailsUpdated(); + } else if (oldDetails.status != EntityScriptStatus::UNLOADED) { EntityScriptDetails newDetails; newDetails.status = EntityScriptStatus::UNLOADED; newDetails.lastModified = QDateTime::currentMSecsSinceEpoch(); @@ -1875,6 +2330,7 @@ void ScriptEngine::unloadEntityScript(const EntityItemID& entityID) { newDetails.scriptText = oldDetails.scriptText; setEntityScriptDetails(entityID, newDetails); } + stopAllTimersForEntityScript(entityID); { // FIXME: shouldn't have to do this here, but currently something seems to be firing unloads moments after firing initial load requests @@ -1953,10 +2409,7 @@ void ScriptEngine::doWithEnvironment(const EntityItemID& entityID, const QUrl& s #else operation(); #endif - if (!isEvaluating() && hasUncaughtException()) { - emit unhandledException(cloneUncaughtException(!entityID.isNull() ? entityID.toString() : __FUNCTION__)); - clearExceptions(); - } + maybeEmitUncaughtException(!entityID.isNull() ? entityID.toString() : __FUNCTION__); currentEntityIdentifier = oldIdentifier; currentSandboxURL = oldSandboxURL; } diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index b988ccfe90..5ea8d052e9 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -41,6 +41,7 @@ #include "ScriptCache.h" #include "ScriptUUID.h" #include "Vec3.h" +#include "SettingHandle.h" class QScriptEngineDebugger; @@ -78,7 +79,7 @@ public: QUrl definingSandboxURL { QUrl("about:EntityScript") }; }; -class ScriptEngine : public BaseScriptEngine, public EntitiesScriptEngineProvider, public QEnableSharedFromThis { +class ScriptEngine : public BaseScriptEngine, public EntitiesScriptEngineProvider { Q_OBJECT Q_PROPERTY(QString context READ getContext) public: @@ -137,6 +138,8 @@ public: /// evaluate some code in the context of the ScriptEngine and return the result Q_INVOKABLE QScriptValue evaluate(const QString& program, const QString& fileName, int lineNumber = 1); // this is also used by the script tool widget + Q_INVOKABLE QScriptValue evaluateInClosure(const QScriptValue& locals, const QScriptProgram& program); + /// if the script engine is not already running, this will download the URL and start the process of seting it up /// to run... NOTE - this is used by Application currently to load the url. We don't really want it to be exposed /// to scripts. we may not need this to be invokable @@ -157,6 +160,16 @@ public: Q_INVOKABLE void include(const QStringList& includeFiles, QScriptValue callback = QScriptValue()); Q_INVOKABLE void include(const QString& includeFile, QScriptValue callback = QScriptValue()); + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // MODULE related methods + Q_INVOKABLE QScriptValue require(const QString& moduleId); + Q_INVOKABLE void resetModuleCache(bool deleteScriptCache = false); + QScriptValue currentModule(); + bool registerModuleWithParent(const QScriptValue& module, const QScriptValue& parent); + QScriptValue newModule(const QString& modulePath, const QScriptValue& parent = QScriptValue()); + QVariantMap fetchModuleSource(const QString& modulePath, const bool forceDownload = false); + QScriptValue instantiateModule(const QScriptValue& module, const QString& sourceCode); + Q_INVOKABLE QObject* setInterval(const QScriptValue& function, int intervalMS); Q_INVOKABLE QObject* setTimeout(const QScriptValue& function, int timeoutMS); Q_INVOKABLE void clearInterval(QObject* timer) { stopTimer(reinterpret_cast(timer)); } @@ -170,8 +183,10 @@ public: Q_INVOKABLE bool isEntityScriptRunning(const EntityItemID& entityID) { return _entityScripts.contains(entityID) && _entityScripts[entityID].status == EntityScriptStatus::RUNNING; } + QVariant cloneEntityScriptDetails(const EntityItemID& entityID); + QFuture getLocalEntityScriptDetails(const EntityItemID& entityID) override; Q_INVOKABLE void loadEntityScript(const EntityItemID& entityID, const QString& entityScript, bool forceRedownload); - Q_INVOKABLE void unloadEntityScript(const EntityItemID& entityID); // will call unload method + Q_INVOKABLE void unloadEntityScript(const EntityItemID& entityID, bool shouldRemoveFromMap = false); // will call unload method Q_INVOKABLE void unloadAllEntityScripts(); Q_INVOKABLE void callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const QStringList& params = QStringList()) override; @@ -221,10 +236,10 @@ signals: void scriptEnding(); void finished(const QString& fileNameString, ScriptEngine* engine); void cleanupMenuItem(const QString& menuItemString); - void printedMessage(const QString& message); - void errorMessage(const QString& message); - void warningMessage(const QString& message); - void infoMessage(const QString& message); + void printedMessage(const QString& message, const QString& scriptName); + void errorMessage(const QString& message, const QString& scriptName); + void warningMessage(const QString& message, const QString& scriptName); + void infoMessage(const QString& message, const QString& scriptName); void runningStateChanged(); void loadScript(const QString& scriptName, bool isUserLoaded); void reloadScript(const QString& scriptName, bool isUserLoaded); @@ -237,6 +252,9 @@ signals: protected: void init(); Q_INVOKABLE void executeOnScriptThread(std::function function, const Qt::ConnectionType& type = Qt::QueuedConnection ); + // note: this is not meant to be called directly, but just to have QMetaObject take care of wiring it up in general; + // then inside of init() we just have to do "Script.require.resolve = Script._requireResolve;" + Q_INVOKABLE QString _requireResolve(const QString& moduleId, const QString& relativeTo = QString()); QString logException(const QScriptValue& exception); void timerFired(); @@ -290,11 +308,16 @@ protected: AssetScriptingInterface _assetScriptingInterface{ this }; - std::function _emitScriptUpdates{ [](){ return true; } }; + std::function _emitScriptUpdates{ []() { return true; } }; std::recursive_mutex _lock; std::chrono::microseconds _totalTimerExecution { 0 }; + + static const QString _SETTINGS_ENABLE_EXTENDED_MODULE_COMPAT; + static const QString _SETTINGS_ENABLE_EXTENDED_EXCEPTIONS; + + Setting::Handle _enableExtendedJSExceptions { _SETTINGS_ENABLE_EXTENDED_EXCEPTIONS, true }; }; #endif // hifi_ScriptEngine_h diff --git a/libraries/script-engine/src/ScriptEngineLogging.cpp b/libraries/script-engine/src/ScriptEngineLogging.cpp index 2e5d293728..392bc05129 100644 --- a/libraries/script-engine/src/ScriptEngineLogging.cpp +++ b/libraries/script-engine/src/ScriptEngineLogging.cpp @@ -12,3 +12,4 @@ #include "ScriptEngineLogging.h" Q_LOGGING_CATEGORY(scriptengine, "hifi.scriptengine") +Q_LOGGING_CATEGORY(scriptengine_module, "hifi.scriptengine.module") diff --git a/libraries/script-engine/src/ScriptEngineLogging.h b/libraries/script-engine/src/ScriptEngineLogging.h index 0e614dd5bf..62e46632a6 100644 --- a/libraries/script-engine/src/ScriptEngineLogging.h +++ b/libraries/script-engine/src/ScriptEngineLogging.h @@ -15,6 +15,7 @@ #include Q_DECLARE_LOGGING_CATEGORY(scriptengine) +Q_DECLARE_LOGGING_CATEGORY(scriptengine_module) #endif // hifi_ScriptEngineLogging_h diff --git a/libraries/script-engine/src/ScriptEngines.cpp b/libraries/script-engine/src/ScriptEngines.cpp index 57887d2d96..88b0e0b7b5 100644 --- a/libraries/script-engine/src/ScriptEngines.cpp +++ b/libraries/script-engine/src/ScriptEngines.cpp @@ -34,34 +34,24 @@ ScriptsModel& getScriptsModel() { return scriptsModel; } -void ScriptEngines::onPrintedMessage(const QString& message) { - auto scriptEngine = qobject_cast(sender()); - auto scriptName = scriptEngine ? scriptEngine->getFilename() : ""; +void ScriptEngines::onPrintedMessage(const QString& message, const QString& scriptName) { emit printedMessage(message, scriptName); } -void ScriptEngines::onErrorMessage(const QString& message) { - auto scriptEngine = qobject_cast(sender()); - auto scriptName = scriptEngine ? scriptEngine->getFilename() : ""; +void ScriptEngines::onErrorMessage(const QString& message, const QString& scriptName) { emit errorMessage(message, scriptName); } -void ScriptEngines::onWarningMessage(const QString& message) { - auto scriptEngine = qobject_cast(sender()); - auto scriptName = scriptEngine ? scriptEngine->getFilename() : ""; +void ScriptEngines::onWarningMessage(const QString& message, const QString& scriptName) { emit warningMessage(message, scriptName); } -void ScriptEngines::onInfoMessage(const QString& message) { - auto scriptEngine = qobject_cast(sender()); - auto scriptName = scriptEngine ? scriptEngine->getFilename() : ""; +void ScriptEngines::onInfoMessage(const QString& message, const QString& scriptName) { emit infoMessage(message, scriptName); } void ScriptEngines::onErrorLoadingScript(const QString& url) { - auto scriptEngine = qobject_cast(sender()); - auto scriptName = scriptEngine ? scriptEngine->getFilename() : ""; - emit errorLoadingScript(url, scriptName); + emit errorLoadingScript(url); } ScriptEngines::ScriptEngines(ScriptEngine::Context context) diff --git a/libraries/script-engine/src/ScriptEngines.h b/libraries/script-engine/src/ScriptEngines.h index 2fadfc81f8..63b7e8f11c 100644 --- a/libraries/script-engine/src/ScriptEngines.h +++ b/libraries/script-engine/src/ScriptEngines.h @@ -79,13 +79,13 @@ signals: void errorMessage(const QString& message, const QString& engineName); void warningMessage(const QString& message, const QString& engineName); void infoMessage(const QString& message, const QString& engineName); - void errorLoadingScript(const QString& url, const QString& engineName); + void errorLoadingScript(const QString& url); public slots: - void onPrintedMessage(const QString& message); - void onErrorMessage(const QString& message); - void onWarningMessage(const QString& message); - void onInfoMessage(const QString& message); + void onPrintedMessage(const QString& message, const QString& scriptName); + void onErrorMessage(const QString& message, const QString& scriptName); + void onWarningMessage(const QString& message, const QString& scriptName); + void onInfoMessage(const QString& message, const QString& scriptName); void onErrorLoadingScript(const QString& url); protected slots: diff --git a/libraries/script-engine/src/BaseScriptEngine.cpp b/libraries/shared/src/BaseScriptEngine.cpp similarity index 68% rename from libraries/script-engine/src/BaseScriptEngine.cpp rename to libraries/shared/src/BaseScriptEngine.cpp index 16308c0650..c92d629b75 100644 --- a/libraries/script-engine/src/BaseScriptEngine.cpp +++ b/libraries/shared/src/BaseScriptEngine.cpp @@ -10,6 +10,7 @@ // #include "BaseScriptEngine.h" +#include "SharedLogging.h" #include #include @@ -18,18 +19,27 @@ #include #include -#include "ScriptEngineLogging.h" #include "Profile.h" -const QString BaseScriptEngine::_SETTINGS_ENABLE_EXTENDED_EXCEPTIONS { - "com.highfidelity.experimental.enableExtendedJSExceptions" -}; - const QString BaseScriptEngine::SCRIPT_EXCEPTION_FORMAT { "[%0] %1 in %2:%3" }; const QString BaseScriptEngine::SCRIPT_BACKTRACE_SEP { "\n " }; +bool BaseScriptEngine::IS_THREADSAFE_INVOCATION(const QThread *thread, const QString& method) { + if (QThread::currentThread() == thread) { + return true; + } + qCCritical(shared) << QString("Scripting::%1 @ %2 -- ignoring thread-unsafe call from %3") + .arg(method).arg(thread ? thread->objectName() : "(!thread)").arg(QThread::currentThread()->objectName()); + qCDebug(shared) << "(please resolve on the calling side by using invokeMethod, executeOnScriptThread, etc.)"; + Q_ASSERT(false); + return false; +} + // engine-aware JS Error copier and factory QScriptValue BaseScriptEngine::makeError(const QScriptValue& _other, const QString& type) { + if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { + return unboundNullValue(); + } auto other = _other; if (other.isString()) { other = newObject(); @@ -41,7 +51,7 @@ QScriptValue BaseScriptEngine::makeError(const QScriptValue& _other, const QStri } if (!proto.isFunction()) { #ifdef DEBUG_JS_EXCEPTIONS - qCDebug(scriptengine) << "BaseScriptEngine::makeError -- couldn't find constructor for" << type << " -- using Error instead"; + qCDebug(shared) << "BaseScriptEngine::makeError -- couldn't find constructor for" << type << " -- using Error instead"; #endif proto = globalObject().property("Error"); } @@ -64,6 +74,9 @@ QScriptValue BaseScriptEngine::makeError(const QScriptValue& _other, const QStri // check syntax and when there are issues returns an actual "SyntaxError" with the details QScriptValue BaseScriptEngine::lintScript(const QString& sourceCode, const QString& fileName, const int lineNumber) { + if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { + return unboundNullValue(); + } const auto syntaxCheck = checkSyntax(sourceCode); if (syntaxCheck.state() != QScriptSyntaxCheckResult::Valid) { auto err = globalObject().property("SyntaxError") @@ -82,13 +95,16 @@ QScriptValue BaseScriptEngine::lintScript(const QString& sourceCode, const QStri } return err; } - return undefinedValue(); + return QScriptValue(); } // this pulls from the best available information to create a detailed snapshot of the current exception QScriptValue BaseScriptEngine::cloneUncaughtException(const QString& extraDetail) { + if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { + return unboundNullValue(); + } if (!hasUncaughtException()) { - return QScriptValue(); + return unboundNullValue(); } auto exception = uncaughtException(); // ensure the error object is engine-local @@ -144,7 +160,10 @@ QScriptValue BaseScriptEngine::cloneUncaughtException(const QString& extraDetail return err; } -QString BaseScriptEngine::formatException(const QScriptValue& exception) { +QString BaseScriptEngine::formatException(const QScriptValue& exception, bool includeExtendedDetails) { + if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { + return QString(); + } QString note { "UncaughtException" }; QString result; @@ -156,8 +175,8 @@ QString BaseScriptEngine::formatException(const QScriptValue& exception) { const auto lineNumber = exception.property("lineNumber").toString(); const auto stacktrace = exception.property("stack").toString(); - if (_enableExtendedJSExceptions.get()) { - // This setting toggles display of the hints now being added during the loading process. + if (includeExtendedDetails) { + // Display additional exception / troubleshooting hints that can be added via the custom Error .detail property // Example difference: // [UncaughtExceptions] Error: Can't find variable: foobar in atp:/myentity.js\n... // [UncaughtException (construct {1eb5d3fa-23b1-411c-af83-163af7220e3f})] Error: Can't find variable: foobar in atp:/myentity.js\n... @@ -173,14 +192,39 @@ QString BaseScriptEngine::formatException(const QScriptValue& exception) { return result; } +bool BaseScriptEngine::raiseException(const QScriptValue& exception) { + if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { + return false; + } + if (currentContext()) { + // we have an active context / JS stack frame so throw the exception per usual + currentContext()->throwValue(makeError(exception)); + return true; + } else { + // we are within a pure C++ stack frame (ie: being called directly by other C++ code) + // in this case no context information is available so just emit the exception for reporting + emit unhandledException(makeError(exception)); + } + return false; +} + +bool BaseScriptEngine::maybeEmitUncaughtException(const QString& debugHint) { + if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { + return false; + } + if (!isEvaluating() && hasUncaughtException()) { + emit unhandledException(cloneUncaughtException(debugHint)); + clearExceptions(); + return true; + } + return false; +} + QScriptValue BaseScriptEngine::evaluateInClosure(const QScriptValue& closure, const QScriptProgram& program) { PROFILE_RANGE(script, "evaluateInClosure"); - if (QThread::currentThread() != thread()) { - qCCritical(scriptengine) << "*** CRITICAL *** ScriptEngine::evaluateInClosure() is meant to be called from engine thread only."; - // note: a recursive mutex might be needed around below code if this method ever becomes Q_INVOKABLE - return QScriptValue(); + if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { + return unboundNullValue(); } - const auto fileName = program.fileName(); const auto shortName = QUrl(fileName).fileName(); @@ -189,7 +233,7 @@ QScriptValue BaseScriptEngine::evaluateInClosure(const QScriptValue& closure, co auto global = closure.property("global"); if (global.isObject()) { #ifdef DEBUG_JS - qCDebug(scriptengine) << " setting global = closure.global" << shortName; + qCDebug(shared) << " setting global = closure.global" << shortName; #endif oldGlobal = globalObject(); setGlobalObject(global); @@ -200,34 +244,34 @@ QScriptValue BaseScriptEngine::evaluateInClosure(const QScriptValue& closure, co auto thiz = closure.property("this"); if (thiz.isObject()) { #ifdef DEBUG_JS - qCDebug(scriptengine) << " setting this = closure.this" << shortName; + qCDebug(shared) << " setting this = closure.this" << shortName; #endif context->setThisObject(thiz); } context->pushScope(closure); #ifdef DEBUG_JS - qCDebug(scriptengine) << QString("[%1] evaluateInClosure %2").arg(isEvaluating()).arg(shortName); + qCDebug(shared) << QString("[%1] evaluateInClosure %2").arg(isEvaluating()).arg(shortName); #endif { result = BaseScriptEngine::evaluate(program); if (hasUncaughtException()) { auto err = cloneUncaughtException(__FUNCTION__); #ifdef DEBUG_JS_EXCEPTIONS - qCWarning(scriptengine) << __FUNCTION__ << "---------- hasCaught:" << err.toString() << result.toString(); + qCWarning(shared) << __FUNCTION__ << "---------- hasCaught:" << err.toString() << result.toString(); err.setProperty("_result", result); #endif result = err; } } #ifdef DEBUG_JS - qCDebug(scriptengine) << QString("[%1] //evaluateInClosure %2").arg(isEvaluating()).arg(shortName); + qCDebug(shared) << QString("[%1] //evaluateInClosure %2").arg(isEvaluating()).arg(shortName); #endif popContext(); if (oldGlobal.isValid()) { #ifdef DEBUG_JS - qCDebug(scriptengine) << " restoring global" << shortName; + qCDebug(shared) << " restoring global" << shortName; #endif setGlobalObject(oldGlobal); } @@ -236,7 +280,6 @@ QScriptValue BaseScriptEngine::evaluateInClosure(const QScriptValue& closure, co } // Lambda - QScriptValue BaseScriptEngine::newLambdaFunction(std::function operation, const QScriptValue& data, const QScriptEngine::ValueOwnership& ownership) { auto lambda = new Lambda(this, operation, data); auto object = newQObject(lambda, ownership); @@ -262,26 +305,57 @@ Lambda::Lambda(QScriptEngine *engine, std::functionthread(), __FUNCTION__)) { + return BaseScriptEngine::unboundNullValue(); + } return operation(engine->currentContext(), engine); } +QScriptValue makeScopedHandlerObject(QScriptValue scopeOrCallback, QScriptValue methodOrName) { + auto engine = scopeOrCallback.engine(); + if (!engine) { + return scopeOrCallback; + } + auto scope = QScriptValue(); + auto callback = scopeOrCallback; + if (scopeOrCallback.isObject()) { + if (methodOrName.isString()) { + scope = scopeOrCallback; + callback = scope.property(methodOrName.toString()); + } else if (methodOrName.isFunction()) { + scope = scopeOrCallback; + callback = methodOrName; + } + } + auto handler = engine->newObject(); + handler.setProperty("scope", scope); + handler.setProperty("callback", callback); + return handler; +} + +QScriptValue callScopedHandlerObject(QScriptValue handler, QScriptValue err, QScriptValue result) { + return handler.property("callback").call(handler.property("scope"), QScriptValueList({ err, result })); +} + #ifdef DEBUG_JS void BaseScriptEngine::_debugDump(const QString& header, const QScriptValue& object, const QString& footer) { + if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { + return; + } if (!header.isEmpty()) { - qCDebug(scriptengine) << header; + qCDebug(shared) << header; } if (!object.isObject()) { - qCDebug(scriptengine) << "(!isObject)" << object.toVariant().toString() << object.toString(); + qCDebug(shared) << "(!isObject)" << object.toVariant().toString() << object.toString(); return; } QScriptValueIterator it(object); while (it.hasNext()) { it.next(); - qCDebug(scriptengine) << it.name() << ":" << it.value().toString(); + qCDebug(shared) << it.name() << ":" << it.value().toString(); } if (!footer.isEmpty()) { - qCDebug(scriptengine) << footer; + qCDebug(shared) << footer; } } #endif - diff --git a/libraries/shared/src/BaseScriptEngine.h b/libraries/shared/src/BaseScriptEngine.h new file mode 100644 index 0000000000..138e46fafa --- /dev/null +++ b/libraries/shared/src/BaseScriptEngine.h @@ -0,0 +1,90 @@ +// +// BaseScriptEngine.h +// libraries/script-engine/src +// +// Created by Timothy Dedischew on 02/01/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_BaseScriptEngine_h +#define hifi_BaseScriptEngine_h + +#include +#include +#include + +// common base class for extending QScriptEngine itself +class BaseScriptEngine : public QScriptEngine, public QEnableSharedFromThis { + Q_OBJECT +public: + static const QString SCRIPT_EXCEPTION_FORMAT; + static const QString SCRIPT_BACKTRACE_SEP; + + // threadsafe "unbound" version of QScriptEngine::nullValue() + static const QScriptValue unboundNullValue() { return QScriptValue(0, QScriptValue::NullValue); } + + BaseScriptEngine() {} + + Q_INVOKABLE QScriptValue lintScript(const QString& sourceCode, const QString& fileName, const int lineNumber = 1); + Q_INVOKABLE QScriptValue makeError(const QScriptValue& other = QScriptValue(), const QString& type = "Error"); + Q_INVOKABLE QString formatException(const QScriptValue& exception, bool includeExtendedDetails); + + QScriptValue cloneUncaughtException(const QString& detail = QString()); + QScriptValue evaluateInClosure(const QScriptValue& locals, const QScriptProgram& program); + + // if there is a pending exception and we are at the top level (non-recursive) stack frame, this emits and resets it + bool maybeEmitUncaughtException(const QString& debugHint = QString()); + + // if the currentContext() is valid then throw the passed exception; otherwise, immediately emit it. + // note: this is used in cases where C++ code might call into JS API methods directly + bool raiseException(const QScriptValue& exception); + + // helper to detect and log warnings when other code invokes QScriptEngine/BaseScriptEngine in thread-unsafe ways + static bool IS_THREADSAFE_INVOCATION(const QThread *thread, const QString& method); +signals: + void unhandledException(const QScriptValue& exception); + +protected: + // like `newFunction`, but allows mapping inline C++ lambdas with captures as callable QScriptValues + // even though the context/engine parameters are redundant in most cases, the function signature matches `newFunction` + // anyway so that newLambdaFunction can be used to rapidly prototype / test utility APIs and then if becoming + // permanent more easily promoted into regular static newFunction scenarios. + QScriptValue newLambdaFunction(std::function operation, const QScriptValue& data = QScriptValue(), const QScriptEngine::ValueOwnership& ownership = QScriptEngine::AutoOwnership); + +#ifdef DEBUG_JS + static void _debugDump(const QString& header, const QScriptValue& object, const QString& footer = QString()); +#endif +}; + +// Standardized CPS callback helpers (see: http://fredkschott.com/post/2014/03/understanding-error-first-callbacks-in-node-js/) +// These two helpers allow async JS APIs that use a callback parameter to be more friendly to scripters by accepting thisObject +// context and adopting a consistent and intuitable callback signature: +// function callback(err, result) { if (err) { ... } else { /* do stuff with result */ } } +// +// To use, first pass the user-specified callback args in the same order used with optionally-scoped Qt signal connections: +// auto handler = makeScopedHandlerObject(scopeOrCallback, optionalMethodOrName); +// And then invoke the scoped handler later per CPS conventions: +// auto result = callScopedHandlerObject(handler, err, result); +QScriptValue makeScopedHandlerObject(QScriptValue scopeOrCallback, QScriptValue methodOrName); +QScriptValue callScopedHandlerObject(QScriptValue handler, QScriptValue err, QScriptValue result); + +// Lambda helps create callable QScriptValues out of std::functions: +// (just meant for use from within the script engine itself) +class Lambda : public QObject { + Q_OBJECT +public: + Lambda(QScriptEngine *engine, std::function operation, QScriptValue data); + ~Lambda(); + public slots: + QScriptValue call(); + QString toString() const; +private: + QScriptEngine* engine; + std::function operation; + QScriptValue data; +}; + +#endif // hifi_BaseScriptEngine_h diff --git a/libraries/shared/src/GLMHelpers.h b/libraries/shared/src/GLMHelpers.h index 609c3ab08b..deb87930fc 100644 --- a/libraries/shared/src/GLMHelpers.h +++ b/libraries/shared/src/GLMHelpers.h @@ -50,7 +50,7 @@ using glm::quat; // this is where the coordinate system is represented const glm::vec3 IDENTITY_RIGHT = glm::vec3( 1.0f, 0.0f, 0.0f); const glm::vec3 IDENTITY_UP = glm::vec3( 0.0f, 1.0f, 0.0f); -const glm::vec3 IDENTITY_FRONT = glm::vec3( 0.0f, 0.0f,-1.0f); +const glm::vec3 IDENTITY_FORWARD = glm::vec3( 0.0f, 0.0f,-1.0f); glm::quat safeMix(const glm::quat& q1, const glm::quat& q2, float alpha); diff --git a/libraries/shared/src/HifiConfigVariantMap.cpp b/libraries/shared/src/HifiConfigVariantMap.cpp index 5be6b2cd74..d0fb14e104 100644 --- a/libraries/shared/src/HifiConfigVariantMap.cpp +++ b/libraries/shared/src/HifiConfigVariantMap.cpp @@ -21,7 +21,7 @@ #include #include -#include "ServerPathUtils.h" +#include "PathUtils.h" #include "SharedLogging.h" QVariantMap HifiConfigVariantMap::mergeCLParametersWithJSONConfig(const QStringList& argumentList) { @@ -127,7 +127,7 @@ void HifiConfigVariantMap::loadConfig(const QStringList& argumentList) { _userConfigFilename = argumentList[userConfigIndex + 1]; } else { // we weren't passed a user config path - _userConfigFilename = ServerPathUtils::getDataFilePath(USER_CONFIG_FILE_NAME); + _userConfigFilename = PathUtils::getAppDataFilePath(USER_CONFIG_FILE_NAME); // as of 1/19/2016 this path was moved so we attempt a migration for first run post migration here @@ -153,7 +153,7 @@ void HifiConfigVariantMap::loadConfig(const QStringList& argumentList) { // we have the old file and not the new file - time to copy the file // make the destination directory if it doesn't exist - auto dataDirectory = ServerPathUtils::getDataDirectory(); + auto dataDirectory = PathUtils::getAppDataPath(); if (QDir().mkpath(dataDirectory)) { if (oldConfigFile.copy(_userConfigFilename)) { qCDebug(shared) << "Migrated config file from" << oldConfigFilename << "to" << _userConfigFilename; diff --git a/libraries/shared/src/PathUtils.cpp b/libraries/shared/src/PathUtils.cpp index 265eaaa5b6..6e3acc5e99 100644 --- a/libraries/shared/src/PathUtils.cpp +++ b/libraries/shared/src/PathUtils.cpp @@ -30,18 +30,20 @@ const QString& PathUtils::resourcesPath() { return staticResourcePath; } -QString PathUtils::getRootDataDirectory() { - auto dataPath = QStandardPaths::writableLocation(QStandardPaths::HomeLocation); +QString PathUtils::getAppDataPath() { + return QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/"; +} -#ifdef Q_OS_WIN - dataPath += "/AppData/Roaming/"; -#elif defined(Q_OS_OSX) - dataPath += "/Library/Application Support/"; -#else - dataPath += "/.local/share/"; -#endif +QString PathUtils::getAppLocalDataPath() { + return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/"; +} - return dataPath; +QString PathUtils::getAppDataFilePath(const QString& filename) { + return QDir(getAppDataPath()).absoluteFilePath(filename); +} + +QString PathUtils::getAppLocalDataFilePath(const QString& filename) { + return QDir(getAppLocalDataPath()).absoluteFilePath(filename); } QString fileNameWithoutExtension(const QString& fileName, const QVector possibleExtensions) { diff --git a/libraries/shared/src/PathUtils.h b/libraries/shared/src/PathUtils.h index 1f7dcbe466..a7af44221c 100644 --- a/libraries/shared/src/PathUtils.h +++ b/libraries/shared/src/PathUtils.h @@ -27,7 +27,12 @@ class PathUtils : public QObject, public Dependency { Q_PROPERTY(QString resources READ resourcesPath) public: static const QString& resourcesPath(); - static QString getRootDataDirectory(); + + static QString getAppDataPath(); + static QString getAppLocalDataPath(); + + static QString getAppDataFilePath(const QString& filename); + static QString getAppLocalDataFilePath(const QString& filename); static Qt::CaseSensitivity getFSCaseSensitivity(); static QString stripFilename(const QUrl& url); diff --git a/libraries/shared/src/RenderArgs.h b/libraries/shared/src/RenderArgs.h index b2c05b0548..50722c0deb 100644 --- a/libraries/shared/src/RenderArgs.h +++ b/libraries/shared/src/RenderArgs.h @@ -122,6 +122,7 @@ public: gpu::Batch* _batch = nullptr; std::shared_ptr _whiteTexture; + uint32_t _globalShapeKey { 0 }; bool _enableTexturing { true }; RenderDetails _details; diff --git a/libraries/shared/src/ServerPathUtils.cpp b/libraries/shared/src/ServerPathUtils.cpp deleted file mode 100644 index cf52875c5f..0000000000 --- a/libraries/shared/src/ServerPathUtils.cpp +++ /dev/null @@ -1,31 +0,0 @@ -// -// ServerPathUtils.cpp -// libraries/shared/src -// -// Created by Ryan Huffman on 01/12/16. -// Copyright 2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// -#include "ServerPathUtils.h" - -#include -#include -#include -#include - -#include "PathUtils.h" - -QString ServerPathUtils::getDataDirectory() { - auto dataPath = PathUtils::getRootDataDirectory(); - - dataPath += qApp->organizationName() + "/" + qApp->applicationName(); - - return QDir::cleanPath(dataPath); -} - -QString ServerPathUtils::getDataFilePath(QString filename) { - return QDir(getDataDirectory()).absoluteFilePath(filename); -} - diff --git a/libraries/shared/src/ServerPathUtils.h b/libraries/shared/src/ServerPathUtils.h deleted file mode 100644 index 28a9a71f0d..0000000000 --- a/libraries/shared/src/ServerPathUtils.h +++ /dev/null @@ -1,22 +0,0 @@ -// -// ServerPathUtils.h -// libraries/shared/src -// -// Created by Ryan Huffman on 01/12/16. -// Copyright 2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#ifndef hifi_ServerPathUtils_h -#define hifi_ServerPathUtils_h - -#include - -namespace ServerPathUtils { - QString getDataDirectory(); - QString getDataFilePath(QString filename); -} - -#endif // hifi_ServerPathUtils_h \ No newline at end of file diff --git a/libraries/shared/src/ViewFrustum.cpp b/libraries/shared/src/ViewFrustum.cpp index a0b7d17e46..7e4f64686b 100644 --- a/libraries/shared/src/ViewFrustum.cpp +++ b/libraries/shared/src/ViewFrustum.cpp @@ -31,7 +31,7 @@ void ViewFrustum::setOrientation(const glm::quat& orientationAsQuaternion) { _orientation = orientationAsQuaternion; _right = glm::vec3(orientationAsQuaternion * glm::vec4(IDENTITY_RIGHT, 0.0f)); _up = glm::vec3(orientationAsQuaternion * glm::vec4(IDENTITY_UP, 0.0f)); - _direction = glm::vec3(orientationAsQuaternion * glm::vec4(IDENTITY_FRONT, 0.0f)); + _direction = glm::vec3(orientationAsQuaternion * glm::vec4(IDENTITY_FORWARD, 0.0f)); _view = glm::translate(mat4(), _position) * glm::mat4_cast(_orientation); } diff --git a/libraries/shared/src/ViewFrustum.h b/libraries/shared/src/ViewFrustum.h index 9a6cb9ab68..221b0b5a07 100644 --- a/libraries/shared/src/ViewFrustum.h +++ b/libraries/shared/src/ViewFrustum.h @@ -153,7 +153,7 @@ private: glm::quat _orientation; // orientation in world-frame // calculated from orientation - glm::vec3 _direction = IDENTITY_FRONT; + glm::vec3 _direction = IDENTITY_FORWARD; glm::vec3 _up = IDENTITY_UP; glm::vec3 _right = IDENTITY_RIGHT; diff --git a/libraries/shared/src/shared/Storage.cpp b/libraries/shared/src/shared/Storage.cpp new file mode 100644 index 0000000000..3c46347a49 --- /dev/null +++ b/libraries/shared/src/shared/Storage.cpp @@ -0,0 +1,92 @@ +// +// Created by Bradley Austin Davis on 2016/02/17 +// Copyright 2013-2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "Storage.h" + +#include +#include +#include + +Q_LOGGING_CATEGORY(storagelogging, "hifi.core.storage") + +using namespace storage; + +ViewStorage::ViewStorage(const storage::StoragePointer& owner, size_t size, const uint8_t* data) + : _owner(owner), _size(size), _data(data) {} + +StoragePointer Storage::createView(size_t viewSize, size_t offset) const { + auto selfSize = size(); + if (0 == viewSize) { + viewSize = selfSize; + } + if ((viewSize + offset) > selfSize) { + throw std::runtime_error("Invalid mapping range"); + } + return std::make_shared(shared_from_this(), viewSize, data() + offset); +} + +StoragePointer Storage::toMemoryStorage() const { + return std::make_shared(size(), data()); +} + +StoragePointer Storage::toFileStorage(const QString& filename) const { + return FileStorage::create(filename, size(), data()); +} + +MemoryStorage::MemoryStorage(size_t size, const uint8_t* data) { + _data.resize(size); + if (data) { + memcpy(_data.data(), data, size); + } +} + +StoragePointer FileStorage::create(const QString& filename, size_t size, const uint8_t* data) { + QFile file(filename); + if (!file.open(QFile::ReadWrite | QIODevice::Truncate)) { + throw std::runtime_error("Unable to open file for writing"); + } + if (!file.resize(size)) { + throw std::runtime_error("Unable to resize file"); + } + { + auto mapped = file.map(0, size); + if (!mapped) { + throw std::runtime_error("Unable to map file"); + } + memcpy(mapped, data, size); + if (!file.unmap(mapped)) { + throw std::runtime_error("Unable to unmap file"); + } + } + file.close(); + return std::make_shared(filename); +} + +FileStorage::FileStorage(const QString& filename) : _file(filename) { + if (_file.open(QFile::ReadOnly)) { + _mapped = _file.map(0, _file.size()); + if (_mapped) { + _valid = true; + } else { + qCWarning(storagelogging) << "Failed to map file " << filename; + } + } else { + qCWarning(storagelogging) << "Failed to open file " << filename; + } +} + +FileStorage::~FileStorage() { + if (_mapped) { + if (!_file.unmap(_mapped)) { + throw std::runtime_error("Unable to unmap file"); + } + } + if (_file.isOpen()) { + _file.close(); + } +} diff --git a/libraries/shared/src/shared/Storage.h b/libraries/shared/src/shared/Storage.h new file mode 100644 index 0000000000..306984040f --- /dev/null +++ b/libraries/shared/src/shared/Storage.h @@ -0,0 +1,82 @@ +// +// Created by Bradley Austin Davis on 2016/02/17 +// Copyright 2013-2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#pragma once +#ifndef hifi_Storage_h +#define hifi_Storage_h + +#include +#include +#include +#include +#include + +namespace storage { + class Storage; + using StoragePointer = std::shared_ptr; + + class Storage : public std::enable_shared_from_this { + public: + virtual ~Storage() {} + virtual const uint8_t* data() const = 0; + virtual size_t size() const = 0; + virtual operator bool() const { return true; } + + StoragePointer createView(size_t size = 0, size_t offset = 0) const; + StoragePointer toFileStorage(const QString& filename) const; + StoragePointer toMemoryStorage() const; + + // Aliases to prevent having to re-write a ton of code + inline size_t getSize() const { return size(); } + inline const uint8_t* readData() const { return data(); } + }; + + class MemoryStorage : public Storage { + public: + MemoryStorage(size_t size, const uint8_t* data = nullptr); + const uint8_t* data() const override { return _data.data(); } + uint8_t* data() { return _data.data(); } + size_t size() const override { return _data.size(); } + operator bool() const override { return true; } + private: + std::vector _data; + }; + + class FileStorage : public Storage { + public: + static StoragePointer create(const QString& filename, size_t size, const uint8_t* data); + FileStorage(const QString& filename); + ~FileStorage(); + // Prevent copying + FileStorage(const FileStorage& other) = delete; + FileStorage& operator=(const FileStorage& other) = delete; + + const uint8_t* data() const override { return _mapped; } + size_t size() const override { return _file.size(); } + operator bool() const override { return _valid; } + private: + bool _valid { false }; + QFile _file; + uint8_t* _mapped { nullptr }; + }; + + class ViewStorage : public Storage { + public: + ViewStorage(const storage::StoragePointer& owner, size_t size, const uint8_t* data); + const uint8_t* data() const override { return _data; } + size_t size() const override { return _size; } + operator bool() const override { return *_owner; } + private: + const storage::StoragePointer _owner; + const size_t _size; + const uint8_t* _data; + }; + +} + +#endif // hifi_Storage_h diff --git a/libraries/ui/src/ui/Menu.cpp b/libraries/ui/src/ui/Menu.cpp index f68fff0204..a793942056 100644 --- a/libraries/ui/src/ui/Menu.cpp +++ b/libraries/ui/src/ui/Menu.cpp @@ -470,8 +470,8 @@ void Menu::removeSeparator(const QString& menuName, const QString& separatorName if (menu) { int textAt = findPositionOfMenuItem(menu, separatorName); QList menuActions = menu->actions(); - QAction* separatorText = menuActions[textAt]; if (textAt > 0 && textAt < menuActions.size()) { + QAction* separatorText = menuActions[textAt]; QAction* separatorLine = menuActions[textAt - 1]; if (separatorLine) { if (separatorLine->isSeparator()) { diff --git a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp index 09f3e6dc8c..b759a06aee 100644 --- a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp +++ b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp @@ -255,7 +255,7 @@ void OculusLegacyDisplayPlugin::hmdPresent() { memset(eyePoses, 0, sizeof(ovrPosef) * 2); eyePoses[0].Orientation = eyePoses[1].Orientation = ovrRotation; - GLint texture = getGLBackend()->getTextureID(_compositeFramebuffer->getRenderBuffer(0), false); + GLint texture = getGLBackend()->getTextureID(_compositeFramebuffer->getRenderBuffer(0)); auto sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); glFlush(); if (_hmdWindow->makeCurrent()) { diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.cpp b/plugins/openvr/src/OpenVrDisplayPlugin.cpp index 6d503a208a..46c2cf3ff2 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.cpp +++ b/plugins/openvr/src/OpenVrDisplayPlugin.cpp @@ -494,9 +494,9 @@ void OpenVrDisplayPlugin::customizeContext() { _compositeInfos[0].texture = _compositeFramebuffer->getRenderBuffer(0); for (size_t i = 0; i < COMPOSITING_BUFFER_SIZE; ++i) { if (0 != i) { - _compositeInfos[i].texture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, _renderTargetSize.x, _renderTargetSize.y, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT))); + _compositeInfos[i].texture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32, _renderTargetSize.x, _renderTargetSize.y, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT))); } - _compositeInfos[i].textureID = getGLBackend()->getTextureID(_compositeInfos[i].texture, false); + _compositeInfos[i].textureID = getGLBackend()->getTextureID(_compositeInfos[i].texture); } _submitThread->_canvas = _submitCanvas; _submitThread->start(QThread::HighPriority); @@ -624,7 +624,7 @@ void OpenVrDisplayPlugin::compositeLayers() { glFlush(); if (!newComposite.textureID) { - newComposite.textureID = getGLBackend()->getTextureID(newComposite.texture, false); + newComposite.textureID = getGLBackend()->getTextureID(newComposite.texture); } withPresentThreadLock([&] { _submitThread->update(newComposite); @@ -638,7 +638,7 @@ void OpenVrDisplayPlugin::hmdPresent() { if (_threadedSubmit) { _submitThread->waitForPresent(); } else { - GLuint glTexId = getGLBackend()->getTextureID(_compositeFramebuffer->getRenderBuffer(0), false); + GLuint glTexId = getGLBackend()->getTextureID(_compositeFramebuffer->getRenderBuffer(0)); vr::Texture_t vrTexture { (void*)glTexId, vr::API_OpenGL, vr::ColorSpace_Auto }; vr::VRCompositor()->Submit(vr::Eye_Left, &vrTexture, &OPENVR_TEXTURE_BOUNDS_LEFT); vr::VRCompositor()->Submit(vr::Eye_Right, &vrTexture, &OPENVR_TEXTURE_BOUNDS_RIGHT); diff --git a/scripts/developer/libraries/jasmine/hifi-boot.js b/scripts/developer/libraries/jasmine/hifi-boot.js index f490a3618f..772dd8c17e 100644 --- a/scripts/developer/libraries/jasmine/hifi-boot.js +++ b/scripts/developer/libraries/jasmine/hifi-boot.js @@ -6,7 +6,7 @@ var lastSpecStartTime; function ConsoleReporter(options) { var startTime = new Date().getTime(); - var errorCount = 0; + var errorCount = 0, pending = []; this.jasmineStarted = function (obj) { print('Jasmine started with ' + obj.totalSpecsDefined + ' tests.'); }; @@ -15,11 +15,14 @@ var endTime = new Date().getTime(); print('
'); if (errorCount === 0) { - print ('All tests passed!'); + print ('All enabled tests passed!'); } else { print('Tests completed with ' + errorCount + ' ' + ERROR + '.'); } + if (pending.length) + print ('disabled:
   '+ + pending.join('
   ')+'
'); print('Tests completed in ' + (endTime - startTime) + 'ms.'); }; this.suiteStarted = function(obj) { @@ -32,6 +35,10 @@ lastSpecStartTime = new Date().getTime(); }; this.specDone = function(obj) { + if (obj.status === 'pending') { + pending.push(obj.fullName); + return print('...(pending ' + obj.fullName +')'); + } var specEndTime = new Date().getTime(); var symbol = obj.status === PASSED ? '' + CHECKMARK + '' : @@ -55,7 +62,7 @@ clearTimeout = Script.clearTimeout; clearInterval = Script.clearInterval; - var jasmine = jasmineRequire.core(jasmineRequire); + var jasmine = this.jasmine = jasmineRequire.core(jasmineRequire); var env = jasmine.getEnv(); diff --git a/scripts/developer/tests/.gitignore b/scripts/developer/tests/.gitignore new file mode 100644 index 0000000000..7cacbf042c --- /dev/null +++ b/scripts/developer/tests/.gitignore @@ -0,0 +1 @@ +cube_texture.ktx \ No newline at end of file diff --git a/scripts/developer/tests/ambientSoundTest.js b/scripts/developer/tests/ambientSoundTest.js index 5b373715c0..d048d5f73d 100644 --- a/scripts/developer/tests/ambientSoundTest.js +++ b/scripts/developer/tests/ambientSoundTest.js @@ -4,7 +4,7 @@ var uuid = Entities.addEntity({ shape: "Icosahedron", dimensions: Vec3.HALF, script: Script.resolvePath('../../tutorials/entity_scripts/ambientSound.js'), - position: Vec3.sum(Vec3.multiply(5, Quat.getFront(MyAvatar.orientation)), MyAvatar.position), + position: Vec3.sum(Vec3.multiply(5, Quat.getForward(MyAvatar.orientation)), MyAvatar.position), userData: JSON.stringify({ soundURL: WAVE, maxVolume: 0.1, diff --git a/scripts/developer/tests/basicEntityTest/entitySpawner.js b/scripts/developer/tests/basicEntityTest/entitySpawner.js index a2f38f59eb..538e9145f5 100644 --- a/scripts/developer/tests/basicEntityTest/entitySpawner.js +++ b/scripts/developer/tests/basicEntityTest/entitySpawner.js @@ -2,7 +2,7 @@ orientation = Quat.safeEulerAngles(orientation); orientation.x = 0; orientation = Quat.fromVec3Degrees(orientation); - var center = Vec3.sum(MyAvatar.position, Vec3.multiply(3, Quat.getFront(orientation))); + var center = Vec3.sum(MyAvatar.position, Vec3.multiply(3, Quat.getForward(orientation))); // Math.random ensures no caching of script var SCRIPT_URL = Script.resolvePath("myEntityScript.js") diff --git a/scripts/developer/tests/batonSoundEntityTest/batonSoundTestEntitySpawner.js b/scripts/developer/tests/batonSoundEntityTest/batonSoundTestEntitySpawner.js index fdcef8d32c..f5fc35a1de 100644 --- a/scripts/developer/tests/batonSoundEntityTest/batonSoundTestEntitySpawner.js +++ b/scripts/developer/tests/batonSoundEntityTest/batonSoundTestEntitySpawner.js @@ -2,7 +2,7 @@ orientation = Quat.safeEulerAngles(orientation); orientation.x = 0; orientation = Quat.fromVec3Degrees(orientation); - var center = Vec3.sum(MyAvatar.position, Vec3.multiply(3, Quat.getFront(orientation))); + var center = Vec3.sum(MyAvatar.position, Vec3.multiply(3, Quat.getForward(orientation))); // Math.random ensures no caching of script var SCRIPT_URL = Script.resolvePath("batonSoundTestEntityScript.js") diff --git a/scripts/developer/tests/entityServerStampedeTest.js b/scripts/developer/tests/entityServerStampedeTest.js index 3fcf01bb34..33aa53f9b1 100644 --- a/scripts/developer/tests/entityServerStampedeTest.js +++ b/scripts/developer/tests/entityServerStampedeTest.js @@ -4,7 +4,7 @@ var DIV = NUM_ENTITIES / Math.PI / 2; var PASS_SCRIPT_URL = Script.resolvePath('entityServerStampedeTest-entity.js'); var FAIL_SCRIPT_URL = Script.resolvePath('entityStampedeTest-entity-fail.js'); -var origin = Vec3.sum(MyAvatar.position, Vec3.multiply(5, Quat.getFront(MyAvatar.orientation))); +var origin = Vec3.sum(MyAvatar.position, Vec3.multiply(5, Quat.getForward(MyAvatar.orientation))); origin.y += HMD.eyeHeight; var uuids = []; diff --git a/scripts/developer/tests/entityStampedeTest.js b/scripts/developer/tests/entityStampedeTest.js index c5040a9796..644bf0a216 100644 --- a/scripts/developer/tests/entityStampedeTest.js +++ b/scripts/developer/tests/entityStampedeTest.js @@ -4,7 +4,7 @@ var DIV = NUM_ENTITIES / Math.PI / 2; var PASS_SCRIPT_URL = Script.resolvePath('').replace('.js', '-entity.js'); var FAIL_SCRIPT_URL = Script.resolvePath('').replace('.js', '-entity-fail.js'); -var origin = Vec3.sum(MyAvatar.position, Vec3.multiply(5, Quat.getFront(MyAvatar.orientation))); +var origin = Vec3.sum(MyAvatar.position, Vec3.multiply(5, Quat.getForward(MyAvatar.orientation))); origin.y += HMD.eyeHeight; var uuids = []; diff --git a/scripts/developer/tests/lodTest.js b/scripts/developer/tests/lodTest.js index 4b6706cd70..ce91b54d0f 100644 --- a/scripts/developer/tests/lodTest.js +++ b/scripts/developer/tests/lodTest.js @@ -19,7 +19,7 @@ var WIDTH = MAX_DIM * NUM_SPHERES; var entities = []; var right = Quat.getRight(Camera.orientation); // Starting position will be 30 meters in front of the camera -var position = Vec3.sum(Camera.position, Vec3.multiply(30, Quat.getFront(Camera.orientation))); +var position = Vec3.sum(Camera.position, Vec3.multiply(30, Quat.getForward(Camera.orientation))); position = Vec3.sum(position, Vec3.multiply(-WIDTH/2, right)); for (var i = 0; i < NUM_SPHERES; ++i) { diff --git a/scripts/developer/tests/mat4test.js b/scripts/developer/tests/mat4test.js index ebce420dcb..4e835ec82f 100644 --- a/scripts/developer/tests/mat4test.js +++ b/scripts/developer/tests/mat4test.js @@ -141,12 +141,12 @@ function testInverse() { assert(mat4FuzzyEqual(IDENTITY, Mat4.multiply(test2, Mat4.inverse(test2)))); } -function testFront() { +function testForward() { var test0 = IDENTITY; - assert(mat4FuzzyEqual({x: 0, y: 0, z: -1}, Mat4.getFront(test0))); + assert(mat4FuzzyEqual({x: 0, y: 0, z: -1}, Mat4.getForward(test0))); var test1 = Mat4.createFromScaleRotAndTrans(ONE_HALF, ROT_Y_180, ONE_TWO_THREE); - assert(mat4FuzzyEqual({x: 0, y: 0, z: 1}, Mat4.getFront(test1))); + assert(mat4FuzzyEqual({x: 0, y: 0, z: 1}, Mat4.getForward(test1))); } function testMat4() { @@ -157,7 +157,7 @@ function testMat4() { testTransformPoint(); testTransformVector(); testInverse(); - testFront(); + testForward(); print("MAT4 TEST complete! (" + (testCount - failureCount) + "/" + testCount + ") tests passed!"); } diff --git a/scripts/developer/tests/performance/tribbles.js b/scripts/developer/tests/performance/tribbles.js index 4c04f8b5b7..c5735b7359 100644 --- a/scripts/developer/tests/performance/tribbles.js +++ b/scripts/developer/tests/performance/tribbles.js @@ -43,7 +43,7 @@ var HOW_FAR_UP = RANGE / 1.5; // higher (for uneven ground) above range/2 (for var totalCreated = 0; var offset = Vec3.sum(Vec3.multiply(HOW_FAR_UP, Vec3.UNIT_Y), - Vec3.multiply(HOW_FAR_IN_FRONT_OF_ME, Quat.getFront(Camera.orientation))); + Vec3.multiply(HOW_FAR_IN_FRONT_OF_ME, Quat.getForward(Camera.orientation))); var center = Vec3.sum(MyAvatar.position, offset); function randomVector(range) { diff --git a/scripts/developer/tests/rapidProceduralChange/rapidProceduralChangeTest.js b/scripts/developer/tests/rapidProceduralChange/rapidProceduralChangeTest.js index 6897a1b70f..e28a7b01e2 100644 --- a/scripts/developer/tests/rapidProceduralChange/rapidProceduralChangeTest.js +++ b/scripts/developer/tests/rapidProceduralChange/rapidProceduralChangeTest.js @@ -20,9 +20,9 @@ orientation = Quat.safeEulerAngles(orientation); orientation.x = 0; orientation = Quat.fromVec3Degrees(orientation); -var centerUp = Vec3.sum(MyAvatar.position, Vec3.multiply(3, Quat.getFront(orientation))); +var centerUp = Vec3.sum(MyAvatar.position, Vec3.multiply(3, Quat.getForward(orientation))); centerUp.y += 0.5; -var centerDown = Vec3.sum(MyAvatar.position, Vec3.multiply(3, Quat.getFront(orientation))); +var centerDown = Vec3.sum(MyAvatar.position, Vec3.multiply(3, Quat.getForward(orientation))); centerDown.y -= 0.5; var ENTITY_SHADER_URL = "https://s3-us-west-1.amazonaws.com/hifi-content/eric/shaders/uniformTest.fs"; diff --git a/scripts/developer/tests/scaling.png b/scripts/developer/tests/scaling.png new file mode 100644 index 0000000000000000000000000000000000000000..1e6a7df45d8440cc52d3b45501b909419fed2f1b GIT binary patch literal 3172 zcmd5;c~p~E7XQ63A%rEG0zw6aOe>MPK~IBJisaL=qAd_A)CCaGwo@pwR8gZC_|%SD z7o4J$st8ugBC@Cz1PuvBYq45TM5xH3-~b|o9YRPlCwSUFX3m*2?O*fPeed4;yT5ne zyYHTRFu>o3XKrr}fVXnRvQ+>DfPl*ZaO3bV9|M+iS1wx;Bqh%uyiNeFQgFJcjPsAZ zQ+zK$=}JukxPSm)@JBWj{=Z@I&oh>M-7es>42Jw255u%;9b7_Ar+anY4|u#h;LQ2T zXWDU%Msp|eqeX1oR5;Ed z%2}=L#||eS>yaPipfV=$G*|spnb+iuN)}3+#M!v_fuM66=g`gr46y9i{f9JUb}sR=GCq% zR(P3YRJ{JO!mW$0%9Jz@eYk?q7U{t?rcyCMhi z6{Z`TtYfCa`{>n@WsR%lQ;+N=tEm?r;P=t9QTaV#{6N zh1Pmom+C}u<3gMv&9tVn=P0E-_pG{G3+9@Qtto#hLS%otuAK(%ys9nwp}|Zmc#fx{ z@4e;VgALMJ*0VC@D40W;6Y8yVt(VamH@OrP?uF{bEryGZ>yr$b7q$3XH8k5~RT|1?GJ0JWv*9}?nbf>SGfY8XT`;`MnhRsYzrz*~= z*A&XdI*PFZ)w71p)L&UCq1xqkU@kKXfq;M)EJ!w0C{PvwOE%?t%W$GO* zQv5VVq%*X-k;SH=>(8gm)6xC*3s9bQ+sx>5RT7 zd4@9yS6et(OIvWcdDMMG9_j>>&9K$pDm%-Ru&KU$#B5$#Qtn?aEa-mH*Htd;G0_(7mw3bheDNh9_>{ z^Qy-bHmNZfzG!*8rbotKzSn=mCMS0TmwUqEoo9E0*ZsPG0VuUUW9$J8a9P8m5Z~r{ znNSEYOmn9J|Bl24<@*GEGdIz|qp>9b*Z}=!1OfPyUG2Y#)@lD4`*&c4Ogob4Bu+Xq z_pKif4jc5(+ssD9x+{L8cDah|fwsBUq1F8w&M`yF?qNW+?YVekiy}GmSVryVJ^eS| zh;`M+4z^hYRd^Q_9@K?wTb$A{7Xri|9nTYI6X~FIpsJ+#E>F2LwJt$rXE|;LH{VE| z;Xo7yWI3rZm-dT5(ROTHRc9U&zROcKs$;mhfnn zo9B75R?*7_8w>h>xgX>%Gl`By(!f8X>z@^CVZb)o~%L;_f%|NAB(nv;`jxp6AniR}RWt zOD#%0&=$Nt`J&_#-1=(EQqWKi8Lkb8!WuZfDcvVI0&)GgVqLa^bHM_2#0)@K3 zdjq1dUpco+wDwFy&qUnnvkH^scz73k? z*0^wSGR<<^RCVlhggmC)f!R|PKFR46xws_6p1H3d{JG8pRmZZE`8aUG%TL(o%%i$p34%Ee%*y;?&!bVsIIHVCU`&x67@V=oz80bPkRU#~ z>gCI6fJ#&z?)_yH7DU1RsqTCaah zxRduvIgME=l>hJbq$|g`^ed--g)kOKgQTUv^iBC-opi?Po#V+ zm|^$~;%#`!C&53@XE@4QRL7AQRrS7~vVf;t!Q#)z7t-jQjVzM8iRz8TkG3?+?X(#m z<7SA&gQxS2ubgGVr}+3Khj9P?+aBJ>0aLF{GTn+ZpK29FWaXm}S4>*R@TAF$j? zP`PuvJC1>5odjZPH}|b$E>yXWsgLIvw%Rj3?!mY;P72a{OGk~S zzi8qDb@f4?vTADBHjg5jCSF2k=JgQ|XpW2w2Br6wKArRmOB~8C&>lM*hdIYVKp2Y| z6JeT!ai>eA(2mL#^M^8v9P}5>P@G3x7M0vnt4+sq z^pA=!`D%4M<-@eFMq`Z_C+h!Q7-w(pixK<}i+|%%4w63`O{v~IOI3Pm{_;5hu<~vH KWra&4_WTP0c9{JD literal 0 HcmV?d00001 diff --git a/scripts/developer/tests/sphereLODTest.js b/scripts/developer/tests/sphereLODTest.js index dc19094664..d0cb35eaa1 100644 --- a/scripts/developer/tests/sphereLODTest.js +++ b/scripts/developer/tests/sphereLODTest.js @@ -15,7 +15,7 @@ MyAvatar.orientation = Quat.fromPitchYawRollDegrees(0, 0, 0); orientation = Quat.safeEulerAngles(MyAvatar.orientation); orientation.x = 0; orientation = Quat.fromVec3Degrees(orientation); -var tablePosition = Vec3.sum(MyAvatar.position, Quat.getFront(orientation)); +var tablePosition = Vec3.sum(MyAvatar.position, Quat.getForward(orientation)); tablePosition.y += 0.5; diff --git a/scripts/developer/tests/testInterval.js b/scripts/developer/tests/testInterval.js index 94a5fe1fa5..7898610c6d 100644 --- a/scripts/developer/tests/testInterval.js +++ b/scripts/developer/tests/testInterval.js @@ -12,7 +12,7 @@ var UPDATE_HZ = 60; // standard script update rate var UPDATE_INTERVAL = 1000/UPDATE_HZ; // standard script update interval var UPDATE_WORK_EFFORT = 0; // 1000 is light work, 1000000 ~= 30ms -var basePosition = Vec3.sum(Camera.getPosition(), Quat.getFront(Camera.getOrientation())); +var basePosition = Vec3.sum(Camera.getPosition(), Quat.getForward(Camera.getOrientation())); var timerBox = Entities.addEntity( { type: "Box", diff --git a/scripts/developer/tests/unit_tests/entityUnitTests.js b/scripts/developer/tests/unit_tests/entityUnitTests.js index 033a484663..1372676901 100644 --- a/scripts/developer/tests/unit_tests/entityUnitTests.js +++ b/scripts/developer/tests/unit_tests/entityUnitTests.js @@ -1,7 +1,7 @@ describe('Entity', function() { var center = Vec3.sum( MyAvatar.position, - Vec3.multiply(3, Quat.getFront(Camera.getOrientation())) + Vec3.multiply(3, Quat.getForward(Camera.getOrientation())) ); var boxEntity; var boxProps = { diff --git a/scripts/developer/tests/unit_tests/moduleTests/cycles/a.js b/scripts/developer/tests/unit_tests/moduleTests/cycles/a.js new file mode 100644 index 0000000000..265cfaa2df --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/cycles/a.js @@ -0,0 +1,10 @@ +/* eslint-env node */ +var a = exports; +a.done = false; +var b = require('./b.js'); +a.done = true; +a.name = 'a'; +a['a.done?'] = a.done; +a['b.done?'] = b.done; + +print('from a.js a.done =', a.done, '/ b.done =', b.done); diff --git a/scripts/developer/tests/unit_tests/moduleTests/cycles/b.js b/scripts/developer/tests/unit_tests/moduleTests/cycles/b.js new file mode 100644 index 0000000000..c46c872828 --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/cycles/b.js @@ -0,0 +1,10 @@ +/* eslint-env node */ +var b = exports; +b.done = false; +var a = require('./a.js'); +b.done = true; +b.name = 'b'; +b['a.done?'] = a.done; +b['b.done?'] = b.done; + +print('from b.js a.done =', a.done, '/ b.done =', b.done); diff --git a/scripts/developer/tests/unit_tests/moduleTests/cycles/main.js b/scripts/developer/tests/unit_tests/moduleTests/cycles/main.js new file mode 100644 index 0000000000..0ec39cd656 --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/cycles/main.js @@ -0,0 +1,17 @@ +/* eslint-env node */ +/* global print */ +/* eslint-disable comma-dangle */ + +print('main.js'); +var a = require('./a.js'), + b = require('./b.js'); + +print('from main.js a.done =', a.done, 'and b.done =', b.done); + +module.exports = { + name: 'main', + a: a, + b: b, + 'a.done?': a.done, + 'b.done?': b.done, +}; diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorAPIException.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorAPIException.js new file mode 100644 index 0000000000..bbe694b578 --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorAPIException.js @@ -0,0 +1,13 @@ +/* eslint-disable comma-dangle */ +// test module method exception being thrown within main constructor +(function() { + var apiMethod = Script.require('../exceptions/exceptionInFunction.js'); + print(Script.resolvePath(''), "apiMethod", apiMethod); + // this next line throws from within apiMethod + print(apiMethod()); + return { + preload: function(uuid) { + print("entityConstructorAPIException::preload -- never seen --", uuid, Script.resolvePath('')); + }, + }; +}); diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorModule.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorModule.js new file mode 100644 index 0000000000..a4e8c17ab6 --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorModule.js @@ -0,0 +1,23 @@ +/* global module */ +/* eslint-disable comma-dangle */ +// test dual-purpose module and standalone Entity script +function MyEntity(filename) { + return { + preload: function(uuid) { + print("entityConstructorModule.js::preload"); + if (typeof module === 'object') { + print("module.filename", module.filename); + print("module.parent.filename", module.parent && module.parent.filename); + } + }, + clickDownOnEntity: function(uuid, evt) { + print("entityConstructorModule.js::clickDownOnEntity"); + }, + }; +} + +try { + module.exports = MyEntity; +} catch (e) {} // eslint-disable-line no-empty +print('entityConstructorModule::MyEntity', typeof MyEntity); +(MyEntity); diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested.js new file mode 100644 index 0000000000..a90d979877 --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested.js @@ -0,0 +1,14 @@ +/* global module */ +// test Entity constructor based on inherited constructor from a module +function constructor() { + print("entityConstructorNested::constructor"); + var MyEntity = Script.require('./entityConstructorModule.js'); + return new MyEntity("-- created from entityConstructorNested --"); +} + +try { + module.exports = constructor; +} catch (e) { + constructor; +} + diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested2.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested2.js new file mode 100644 index 0000000000..29e0ed65b1 --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested2.js @@ -0,0 +1,25 @@ +/* global module */ +// test Entity constructor based on nested, inherited module constructors +function constructor() { + print("entityConstructorNested2::constructor"); + + // inherit from entityConstructorNested + var MyEntity = Script.require('./entityConstructorNested.js'); + function SubEntity() {} + SubEntity.prototype = new MyEntity('-- created from entityConstructorNested2 --'); + + // create new instance + var entity = new SubEntity(); + // "override" clickDownOnEntity for just this new instance + entity.clickDownOnEntity = function(uuid, evt) { + print("entityConstructorNested2::clickDownOnEntity"); + SubEntity.prototype.clickDownOnEntity.apply(this, arguments); + }; + return entity; +} + +try { + module.exports = constructor; +} catch (e) { + constructor; +} diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorRequireException.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorRequireException.js new file mode 100644 index 0000000000..5872bce529 --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorRequireException.js @@ -0,0 +1,10 @@ +/* eslint-disable comma-dangle */ +// test module-related exception from within "require" evaluation itself +(function() { + var mod = Script.require('../exceptions/exception.js'); + return { + preload: function(uuid) { + print("entityConstructorRequireException::preload (never happens)", uuid, Script.resolvePath(''), mod); + }, + }; +}); diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadAPIError.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadAPIError.js new file mode 100644 index 0000000000..eaee178b0a --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadAPIError.js @@ -0,0 +1,13 @@ +/* eslint-disable comma-dangle */ +// test module method exception being thrown within preload +(function() { + var apiMethod = Script.require('../exceptions/exceptionInFunction.js'); + print(Script.resolvePath(''), "apiMethod", apiMethod); + return { + preload: function(uuid) { + // this next line throws from within apiMethod + print(apiMethod()); + print("entityPreloadAPIException::preload -- never seen --", uuid, Script.resolvePath('')); + }, + }; +}); diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadRequire.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadRequire.js new file mode 100644 index 0000000000..50dab9fa7c --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadRequire.js @@ -0,0 +1,11 @@ +/* eslint-disable comma-dangle */ +// test requiring a module from within preload +(function constructor() { + return { + preload: function(uuid) { + print("entityPreloadRequire::preload"); + var example = Script.require('../example.json'); + print("entityPreloadRequire::example::name", example.name); + }, + }; +}); diff --git a/scripts/developer/tests/unit_tests/moduleTests/example.json b/scripts/developer/tests/unit_tests/moduleTests/example.json new file mode 100644 index 0000000000..42d7fe07da --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/example.json @@ -0,0 +1,9 @@ +{ + "name": "Example JSON Module", + "last-modified": 1485789862, + "config": { + "title": "My Title", + "width": 800, + "height": 600 + } +} diff --git a/scripts/developer/tests/unit_tests/moduleTests/exceptions/exception.js b/scripts/developer/tests/unit_tests/moduleTests/exceptions/exception.js new file mode 100644 index 0000000000..8d25d6b7a4 --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/exceptions/exception.js @@ -0,0 +1,4 @@ +/* eslint-env node */ +module.exports = "n/a"; +throw new Error('exception on line 2'); + diff --git a/scripts/developer/tests/unit_tests/moduleTests/exceptions/exceptionInFunction.js b/scripts/developer/tests/unit_tests/moduleTests/exceptions/exceptionInFunction.js new file mode 100644 index 0000000000..69415a0741 --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/exceptions/exceptionInFunction.js @@ -0,0 +1,38 @@ +/* eslint-env node */ +// dummy lines to make sure exception line number is well below parent test script +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// + + +function myfunc() { + throw new Error('exception on line 32 in myfunc'); +} +module.exports = myfunc; +if (Script[module.filename] === 'throw') { + myfunc(); +} diff --git a/scripts/developer/tests/unit_tests/moduleUnitTests.js b/scripts/developer/tests/unit_tests/moduleUnitTests.js new file mode 100644 index 0000000000..6810dd8b6d --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleUnitTests.js @@ -0,0 +1,378 @@ +/* eslint-env jasmine, node */ +/* global print:true, Script:true, global:true, require:true */ +/* eslint-disable comma-dangle */ +var isNode = instrumentTestrunner(), + runInterfaceTests = !isNode, + runNetworkTests = true; + +// describe wrappers (note: `xdescribe` indicates a disabled or "pending" jasmine test) +var INTERFACE = { describe: runInterfaceTests ? describe : xdescribe }, + NETWORK = { describe: runNetworkTests ? describe : xdescribe }; + +describe('require', function() { + describe('resolve', function() { + it('should resolve relative filenames', function() { + var expected = Script.resolvePath('./moduleTests/example.json'); + expect(require.resolve('./moduleTests/example.json')).toEqual(expected); + }); + describe('exceptions', function() { + it('should reject blank "" module identifiers', function() { + expect(function() { + require.resolve(''); + }).toThrowError(/Cannot find/); + }); + it('should reject excessive identifier sizes', function() { + expect(function() { + require.resolve(new Array(8193).toString()); + }).toThrowError(/Cannot find/); + }); + it('should reject implicitly-relative filenames', function() { + expect(function() { + var mod = require.resolve('example.js'); + mod.exists; + }).toThrowError(/Cannot find/); + }); + it('should reject unanchored, existing filenames with advice', function() { + expect(function() { + var mod = require.resolve('moduleTests/example.json'); + mod.exists; + }).toThrowError(/use '.\/moduleTests\/example\.json'/); + }); + it('should reject unanchored, non-existing filenames', function() { + expect(function() { + var mod = require.resolve('asdfssdf/example.json'); + mod.exists; + }).toThrowError(/Cannot find.*system module not found/); + }); + it('should reject non-existent filenames', function() { + expect(function() { + require.resolve('./404error.js'); + }).toThrowError(/Cannot find/); + }); + it('should reject identifiers resolving to a directory', function() { + expect(function() { + var mod = require.resolve('.'); + mod.exists; + // console.warn('resolved(.)', mod); + }).toThrowError(/Cannot find/); + expect(function() { + var mod = require.resolve('..'); + mod.exists; + // console.warn('resolved(..)', mod); + }).toThrowError(/Cannot find/); + expect(function() { + var mod = require.resolve('../'); + mod.exists; + // console.warn('resolved(../)', mod); + }).toThrowError(/Cannot find/); + }); + (isNode ? xit : it)('should reject non-system, extensionless identifiers', function() { + expect(function() { + require.resolve('./example'); + }).toThrowError(/Cannot find/); + }); + }); + }); + + describe('JSON', function() { + it('should import .json modules', function() { + var example = require('./moduleTests/example.json'); + expect(example.name).toEqual('Example JSON Module'); + }); + // noet: support for loading JSON via content type workarounds reverted + // (leaving these tests intact in case ever revisited later) + // INTERFACE.describe('interface', function() { + // NETWORK.describe('network', function() { + // xit('should import #content-type=application/json modules', function() { + // var results = require('https://jsonip.com#content-type=application/json'); + // expect(results.ip).toMatch(/^[.0-9]+$/); + // }); + // xit('should import content-type: application/json modules', function() { + // var scope = { 'content-type': 'application/json' }; + // var results = require.call(scope, 'https://jsonip.com'); + // expect(results.ip).toMatch(/^[.0-9]+$/); + // }); + // }); + // }); + + }); + + INTERFACE.describe('system', function() { + it('require("vec3")', function() { + expect(require('vec3')).toEqual(jasmine.any(Function)); + }); + it('require("vec3").method', function() { + expect(require('vec3')().isValid).toEqual(jasmine.any(Function)); + }); + it('require("vec3") as constructor', function() { + var vec3 = require('vec3'); + var v = vec3(1.1, 2.2, 3.3); + expect(v).toEqual(jasmine.any(Object)); + expect(v.isValid).toEqual(jasmine.any(Function)); + expect(v.isValid()).toBe(true); + expect(v.toString()).toEqual('[Vec3 (1.100,2.200,3.300)]'); + }); + }); + + describe('cache', function() { + it('should cache modules by resolved module id', function() { + var value = new Date; + var example = require('./moduleTests/example.json'); + // earmark the module object with a unique value + example['.test'] = value; + var example2 = require('../../tests/unit_tests/moduleTests/example.json'); + expect(example2).toBe(example); + // verify earmark is still the same after a second require() + expect(example2['.test']).toBe(example['.test']); + }); + it('should reload cached modules set to null', function() { + var value = new Date; + var example = require('./moduleTests/example.json'); + example['.test'] = value; + require.cache[require.resolve('../../tests/unit_tests/moduleTests/example.json')] = null; + var example2 = require('../../tests/unit_tests/moduleTests/example.json'); + // verify the earmark is *not* the same as before + expect(example2).not.toBe(example); + expect(example2['.test']).not.toBe(example['.test']); + }); + it('should reload when module property is deleted', function() { + var value = new Date; + var example = require('./moduleTests/example.json'); + example['.test'] = value; + delete require.cache[require.resolve('../../tests/unit_tests/moduleTests/example.json')]; + var example2 = require('../../tests/unit_tests/moduleTests/example.json'); + // verify the earmark is *not* the same as before + expect(example2).not.toBe(example); + expect(example2['.test']).not.toBe(example['.test']); + }); + }); + + describe('cyclic dependencies', function() { + describe('should allow lazy-ref cyclic module resolution', function() { + var main; + beforeEach(function() { + // eslint-disable-next-line + try { this._print = print; } catch (e) {} + // during these tests print() is no-op'd so that it doesn't disrupt the reporter output + print = function() {}; + Script.resetModuleCache(); + }); + afterEach(function() { + print = this._print; + }); + it('main is requirable', function() { + main = require('./moduleTests/cycles/main.js'); + expect(main).toEqual(jasmine.any(Object)); + }); + it('transient a and b done values', function() { + expect(main.a['b.done?']).toBe(true); + expect(main.b['a.done?']).toBe(false); + }); + it('ultimate a.done?', function() { + expect(main['a.done?']).toBe(true); + }); + it('ultimate b.done?', function() { + expect(main['b.done?']).toBe(true); + }); + }); + }); + + describe('JS', function() { + it('should throw catchable local file errors', function() { + expect(function() { + require('file:///dev/null/non-existent-file.js'); + }).toThrowError(/path not found|Cannot find.*non-existent-file/); + }); + it('should throw catchable invalid id errors', function() { + expect(function() { + require(new Array(4096 * 2).toString()); + }).toThrowError(/invalid.*size|Cannot find.*,{30}/); + }); + it('should throw catchable unresolved id errors', function() { + expect(function() { + require('foobar:/baz.js'); + }).toThrowError(/could not resolve|Cannot find.*foobar:/); + }); + + NETWORK.describe('network', function() { + // note: depending on retries these tests can take up to 60 seconds each to timeout + var timeout = 75 * 1000; + it('should throw catchable host errors', function() { + expect(function() { + var mod = require('http://non.existent.highfidelity.io/moduleUnitTest.js'); + print("mod", Object.keys(mod)); + }).toThrowError(/error retrieving script .ServerUnavailable.|Cannot find.*non.existent/); + }, timeout); + it('should throw catchable network timeouts', function() { + expect(function() { + require('http://ping.highfidelity.io:1024'); + }).toThrowError(/error retrieving script .Timeout.|Cannot find.*ping.highfidelity/); + }, timeout); + }); + }); + + INTERFACE.describe('entity', function() { + var sampleScripts = [ + 'entityConstructorAPIException.js', + 'entityConstructorModule.js', + 'entityConstructorNested2.js', + 'entityConstructorNested.js', + 'entityConstructorRequireException.js', + 'entityPreloadAPIError.js', + 'entityPreloadRequire.js', + ].filter(Boolean).map(function(id) { + return Script.require.resolve('./moduleTests/entity/'+id); + }); + + var uuids = []; + function cleanup() { + uuids.splice(0,uuids.length).forEach(function(uuid) { + Entities.deleteEntity(uuid); + }); + } + afterAll(cleanup); + // extra sanity check to avoid lingering entities + Script.scriptEnding.connect(cleanup); + + for (var i=0; i < sampleScripts.length; i++) { + maketest(i); + } + + function maketest(i) { + var script = sampleScripts[ i % sampleScripts.length ]; + var shortname = '['+i+'] ' + script.split('/').pop(); + var position = MyAvatar.position; + position.y -= i/2; + // define a unique jasmine test for the current entity script + it(shortname, function(done) { + var uuid = Entities.addEntity({ + text: shortname, + description: Script.resolvePath('').split('/').pop(), + type: 'Text', + position: position, + rotation: MyAvatar.orientation, + script: script, + scriptTimestamp: +new Date, + lifetime: 20, + lineHeight: 1/8, + dimensions: { x: 2, y: 0.5, z: 0.01 }, + backgroundColor: { red: 0, green: 0, blue: 0 }, + color: { red: 0xff, green: 0xff, blue: 0xff }, + }, !Entities.serversExist() || !Entities.canRezTmp()); + uuids.push(uuid); + function stopChecking() { + if (ii) { + Script.clearInterval(ii); + ii = 0; + } + } + var ii = Script.setInterval(function() { + Entities.queryPropertyMetadata(uuid, "script", function(err, result) { + if (err) { + stopChecking(); + throw new Error(err); + } + if (result.success) { + stopChecking(); + if (/Exception/.test(script)) { + expect(result.status).toMatch(/^error_(loading|running)_script$/); + } else { + expect(result.status).toEqual("running"); + } + Entities.deleteEntity(uuid); + done(); + } else { + print('!result.success', JSON.stringify(result)); + } + }); + }, 100); + Script.setTimeout(stopChecking, 4900); + }, 5000 /* jasmine async timeout */); + } + }); +}); + +// support for isomorphic Node.js / Interface unit testing +// note: run `npm install` from unit_tests/ and then `node moduleUnitTests.js` +function run() {} +function instrumentTestrunner() { + var isNode = typeof process === 'object' && process.title === 'node'; + if (typeof describe === 'function') { + // already running within a test runner; assume jasmine is ready-to-go + return isNode; + } + if (isNode) { + /* eslint-disable no-console */ + // Node.js test mode + // to keep things consistent Node.js uses the local jasmine.js library (instead of an npm version) + var jasmineRequire = require('../../libraries/jasmine/jasmine.js'); + var jasmine = jasmineRequire.core(jasmineRequire); + var env = jasmine.getEnv(); + var jasmineInterface = jasmineRequire.interface(jasmine, env); + for (var p in jasmineInterface) { + global[p] = jasmineInterface[p]; + } + env.addReporter(new (require('jasmine-console-reporter'))); + // testing mocks + Script = { + resetModuleCache: function() { + module.require.cache = {}; + }, + setTimeout: setTimeout, + clearTimeout: clearTimeout, + resolvePath: function(id) { + // this attempts to accurately emulate how Script.resolvePath works + var trace = {}; Error.captureStackTrace(trace); + var base = trace.stack.split('\n')[2].replace(/^.*[(]|[)].*$/g,'').replace(/:[0-9]+:[0-9]+.*$/,''); + if (!id) { + return base; + } + var rel = base.replace(/[^\/]+$/, id); + console.info('rel', rel); + return require.resolve(rel); + }, + require: function(mod) { + return require(Script.require.resolve(mod)); + }, + }; + Script.require.cache = require.cache; + Script.require.resolve = function(mod) { + if (mod === '.' || /^\.\.($|\/)/.test(mod)) { + throw new Error("Cannot find module '"+mod+"' (is dir)"); + } + var path = require.resolve(mod); + // console.info('node-require-reoslved', mod, path); + try { + if (require('fs').lstatSync(path).isDirectory()) { + throw new Error("Cannot find module '"+path+"' (is directory)"); + } + // console.info('!path', path); + } catch (e) { + console.error(e); + } + return path; + }; + print = console.info.bind(console, '[print]'); + /* eslint-enable no-console */ + } else { + // Interface test mode + global = this; + Script.require('../../../system/libraries/utils.js'); + this.jasmineRequire = Script.require('../../libraries/jasmine/jasmine.js'); + Script.require('../../libraries/jasmine/hifi-boot.js'); + require = Script.require; + // polyfill console + /* global console:true */ + console = { + log: print, + info: print.bind(this, '[info]'), + warn: print.bind(this, '[warn]'), + error: print.bind(this, '[error]'), + debug: print.bind(this, '[debug]'), + }; + } + // eslint-disable-next-line + run = function() { global.jasmine.getEnv().execute(); }; + return isNode; +} +run(); diff --git a/scripts/developer/tests/unit_tests/package.json b/scripts/developer/tests/unit_tests/package.json new file mode 100644 index 0000000000..91d719b687 --- /dev/null +++ b/scripts/developer/tests/unit_tests/package.json @@ -0,0 +1,6 @@ +{ + "name": "unit_tests", + "devDependencies": { + "jasmine-console-reporter": "^1.2.7" + } +} diff --git a/scripts/developer/tests/unit_tests/scriptUnitTests.js b/scripts/developer/tests/unit_tests/scriptUnitTests.js index 63b451e97f..fa8cb44608 100644 --- a/scripts/developer/tests/unit_tests/scriptUnitTests.js +++ b/scripts/developer/tests/unit_tests/scriptUnitTests.js @@ -15,10 +15,20 @@ describe('Script', function () { // characterization tests // initially these are just to capture how the app works currently var testCases = { + // special relative resolves '': filename, '.': dirname, '..': parentdir, + + // local file "magic" tilde path expansion + '/~/defaultScripts.js': ScriptDiscoveryService.defaultScriptsPath + '/defaultScripts.js', + + // these schemes appear to always get resolved to empty URLs + 'qrc://test': '', 'about:Entities 1': '', + 'ftp://host:port/path': '', + 'data:text/html;text,foo': '', + 'Entities 1': dirname + 'Entities 1', './file.js': dirname + 'file.js', 'c:/temp/': 'file:///c:/temp/', @@ -31,6 +41,12 @@ describe('Script', function () { '/~/libraries/utils.js': 'file:///~/libraries/utils.js', '/temp/file.js': 'file:///temp/file.js', '/~/': 'file:///~/', + + // these schemes appear to always get resolved to the same URL again + 'http://highfidelity.com': 'http://highfidelity.com', + 'atp:/highfidelity': 'atp:/highfidelity', + 'atp:c2d7e3a48cadf9ba75e4f8d9f4d80e75276774880405a093fdee36543aa04f': + 'atp:c2d7e3a48cadf9ba75e4f8d9f4d80e75276774880405a093fdee36543aa04f', }; describe('resolvePath', function () { Object.keys(testCases).forEach(function(input) { @@ -42,7 +58,7 @@ describe('Script', function () { describe('include', function () { var old_cache_buster; - var cache_buster = '#' + +new Date; + var cache_buster = '#' + new Date().getTime().toString(36); beforeAll(function() { old_cache_buster = Settings.getValue('cache_buster'); Settings.setValue('cache_buster', cache_buster); diff --git a/scripts/developer/tests/viveTouchpadTest.js b/scripts/developer/tests/viveTouchpadTest.js index 913da5888d..b5d9575adf 100644 --- a/scripts/developer/tests/viveTouchpadTest.js +++ b/scripts/developer/tests/viveTouchpadTest.js @@ -24,10 +24,10 @@ var boxZAxis, boxYAxis; var prevThumbDown = false; function init() { - boxPosition = Vec3.sum(MyAvatar.position, Vec3.multiply(3, Quat.getFront(Camera.getOrientation()))); - var front = Quat.getFront(Camera.getOrientation()); - boxZAxis = Vec3.normalize(Vec3.cross(front, Y_AXIS)); - boxYAxis = Vec3.normalize(Vec3.cross(boxZAxis, front)); + boxPosition = Vec3.sum(MyAvatar.position, Vec3.multiply(3, Quat.getForward(Camera.getOrientation()))); + var forward = Quat.getForward(Camera.getOrientation()); + boxZAxis = Vec3.normalize(Vec3.cross(forward, Y_AXIS)); + boxYAxis = Vec3.normalize(Vec3.cross(boxZAxis, forward)); boxEntity = Entities.addEntity({ type: "Box", diff --git a/scripts/developer/utilities/record/recorder.js b/scripts/developer/utilities/record/recorder.js index 0e335116d5..ba1c8b0393 100644 --- a/scripts/developer/utilities/record/recorder.js +++ b/scripts/developer/utilities/record/recorder.js @@ -9,12 +9,14 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +/* globals HIFI_PUBLIC_BUCKET:true, Tool, ToolBar */ + HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/"; Script.include("/~/system/libraries/toolBars.js"); var recordingFile = "recording.hfr"; -function setPlayerOptions() { +function setDefaultPlayerOptions() { Recording.setPlayFromCurrentLocation(true); Recording.setPlayerUseDisplayName(false); Recording.setPlayerUseAttachments(false); @@ -38,16 +40,16 @@ var saveIcon; var loadIcon; var spacing; var timerOffset; -setupToolBar(); - var timer = null; var slider = null; + +setupToolBar(); setupTimer(); var watchStop = false; function setupToolBar() { - if (toolBar != null) { + if (toolBar !== null) { print("Multiple calls to Recorder.js:setupToolBar()"); return; } @@ -56,6 +58,8 @@ function setupToolBar() { toolBar = new ToolBar(0, 0, ToolBar.HORIZONTAL); + toolBar.onMove = onToolbarMove; + toolBar.setBack(COLOR_TOOL_BAR, ALPHA_OFF); recordIcon = toolBar.addTool({ @@ -86,7 +90,7 @@ function setupToolBar() { visible: true }, false); - timerOffset = toolBar.width; + timerOffset = toolBar.width + ToolBar.SPACING; spacing = toolBar.addSpacing(0); saveIcon = toolBar.addTool({ @@ -112,15 +116,15 @@ function setupTimer() { text: (0.00).toFixed(3), backgroundColor: COLOR_OFF, x: 0, y: 0, - width: 0, height: 0, - leftMargin: 10, topMargin: 10, + width: 200, height: 25, + leftMargin: 5, topMargin: 3, alpha: 1.0, backgroundAlpha: 1.0, visible: true }); slider = { x: 0, y: 0, w: 200, h: 20, - pos: 0.0, // 0.0 <= pos <= 1.0 + pos: 0.0 // 0.0 <= pos <= 1.0 }; slider.background = Overlays.addOverlay("text", { text: "", @@ -144,20 +148,40 @@ function setupTimer() { }); } +function onToolbarMove(newX, newY, deltaX, deltaY) { + Overlays.editOverlay(timer, { + x: newX + timerOffset - ToolBar.SPACING, + y: newY + }); + + slider.x = newX - ToolBar.SPACING; + slider.y = newY - slider.h - ToolBar.SPACING; + + Overlays.editOverlay(slider.background, { + x: slider.x, + y: slider.y + }); + Overlays.editOverlay(slider.foreground, { + x: slider.x, + y: slider.y + }); +} + function updateTimer() { var text = ""; if (Recording.isRecording()) { text = formatTime(Recording.recorderElapsed()); - } else { - text = formatTime(Recording.playerElapsed()) + " / " + - formatTime(Recording.playerLength()); + text = formatTime(Recording.playerElapsed()) + " / " + formatTime(Recording.playerLength()); } + var timerWidth = text.length * 8 + ((Recording.isRecording()) ? 15 : 0); + Overlays.editOverlay(timer, { - text: text - }) - toolBar.changeSpacing(text.length * 8 + ((Recording.isRecording()) ? 15 : 0), spacing); + text: text, + width: timerWidth + }); + toolBar.changeSpacing(timerWidth + ToolBar.SPACING, spacing); if (Recording.isRecording()) { slider.pos = 1.0; @@ -173,7 +197,7 @@ function updateTimer() { function formatTime(time) { var MIN_PER_HOUR = 60; var SEC_PER_MIN = 60; - var MSEC_PER_SEC = 1000; + var MSEC_DIGITS = 3; var hours = Math.floor(time / (SEC_PER_MIN * MIN_PER_HOUR)); time -= hours * (SEC_PER_MIN * MIN_PER_HOUR); @@ -184,37 +208,19 @@ function formatTime(time) { var seconds = time; var text = ""; - text += (hours > 0) ? hours + ":" : - ""; - text += (minutes > 0) ? ((minutes < 10 && text != "") ? "0" : "") + minutes + ":" : - ""; - text += ((seconds < 10 && text != "") ? "0" : "") + seconds.toFixed(3); + text += (hours > 0) ? hours + ":" : ""; + text += (minutes > 0) ? ((minutes < 10 && text !== "") ? "0" : "") + minutes + ":" : ""; + text += ((seconds < 10 && text !== "") ? "0" : "") + seconds.toFixed(MSEC_DIGITS); return text; } function moveUI() { var relative = { x: 70, y: 40 }; toolBar.move(relative.x, windowDimensions.y - relative.y); - Overlays.editOverlay(timer, { - x: relative.x + timerOffset - ToolBar.SPACING, - y: windowDimensions.y - relative.y - ToolBar.SPACING - }); - - slider.x = relative.x - ToolBar.SPACING; - slider.y = windowDimensions.y - relative.y - slider.h - ToolBar.SPACING; - - Overlays.editOverlay(slider.background, { - x: slider.x, - y: slider.y, - }); - Overlays.editOverlay(slider.foreground, { - x: slider.x, - y: slider.y, - }); } function mousePressEvent(event) { - clickedOverlay = Overlays.getOverlayAtPoint({ x: event.x, y: event.y }); + var clickedOverlay = Overlays.getOverlayAtPoint({ x: event.x, y: event.y }); if (recordIcon === toolBar.clicked(clickedOverlay, false) && !Recording.isPlaying()) { if (!Recording.isRecording()) { @@ -226,7 +232,11 @@ function mousePressEvent(event) { toolBar.setAlpha(ALPHA_OFF, loadIcon); } else { Recording.stopRecording(); - toolBar.selectTool(recordIcon, true ); + toolBar.selectTool(recordIcon, true); + setDefaultPlayerOptions(); + // Plays the recording at the same spot as you recorded it + Recording.setPlayFromCurrentLocation(false); + Recording.setPlayerTime(0); Recording.loadLastRecording(); toolBar.setAlpha(ALPHA_ON, playIcon); toolBar.setAlpha(ALPHA_ON, playLoopIcon); @@ -240,7 +250,6 @@ function mousePressEvent(event) { toolBar.setAlpha(ALPHA_ON, saveIcon); toolBar.setAlpha(ALPHA_ON, loadIcon); } else if (Recording.playerLength() > 0) { - setPlayerOptions(); Recording.setPlayerLoop(false); Recording.startPlaying(); toolBar.setAlpha(ALPHA_OFF, recordIcon); @@ -255,7 +264,6 @@ function mousePressEvent(event) { toolBar.setAlpha(ALPHA_ON, saveIcon); toolBar.setAlpha(ALPHA_ON, loadIcon); } else if (Recording.playerLength() > 0) { - setPlayerOptions(); Recording.setPlayerLoop(true); Recording.startPlaying(); toolBar.setAlpha(ALPHA_OFF, recordIcon); @@ -263,7 +271,7 @@ function mousePressEvent(event) { toolBar.setAlpha(ALPHA_OFF, loadIcon); } } else if (saveIcon === toolBar.clicked(clickedOverlay)) { - if (!Recording.isRecording() && !Recording.isPlaying() && Recording.playerLength() != 0) { + if (!Recording.isRecording() && !Recording.isPlaying() && Recording.playerLength() !== 0) { recordingFile = Window.save("Save recording to file", ".", "Recordings (*.hfr)"); if (!(recordingFile === "null" || recordingFile === null || recordingFile === "")) { Recording.saveRecording(recordingFile); @@ -274,6 +282,7 @@ function mousePressEvent(event) { recordingFile = Window.browse("Load recording from file", ".", "Recordings (*.hfr *.rec *.HFR *.REC)"); if (!(recordingFile === "null" || recordingFile === null || recordingFile === "")) { Recording.loadRecording(recordingFile); + setDefaultPlayerOptions(); } if (Recording.playerLength() > 0) { toolBar.setAlpha(ALPHA_ON, playIcon); @@ -282,8 +291,8 @@ function mousePressEvent(event) { } } } else if (Recording.playerLength() > 0 && - slider.x < event.x && event.x < slider.x + slider.w && - slider.y < event.y && event.y < slider.y + slider.h) { + slider.x < event.x && event.x < slider.x + slider.w && + slider.y < event.y && event.y < slider.y + slider.h) { isSliding = true; slider.pos = (event.x - slider.x) / slider.w; Recording.setPlayerTime(slider.pos * Recording.playerLength()); @@ -308,7 +317,7 @@ function mouseReleaseEvent(event) { function update() { var newDimensions = Controller.getViewportDimensions(); - if (windowDimensions.x != newDimensions.x || windowDimensions.y != newDimensions.y) { + if (windowDimensions.x !== newDimensions.x || windowDimensions.y !== newDimensions.y) { windowDimensions = newDimensions; moveUI(); } diff --git a/scripts/developer/utilities/render/deferredLighting.qml b/scripts/developer/utilities/render/deferredLighting.qml index 99a9f258e3..c7ec8e1153 100644 --- a/scripts/developer/utilities/render/deferredLighting.qml +++ b/scripts/developer/utilities/render/deferredLighting.qml @@ -25,7 +25,7 @@ Column { "Lightmap:LightingModel:enableLightmap", "Background:LightingModel:enableBackground", "ssao:AmbientOcclusion:enabled", - "Textures:LightingModel:enableMaterialTexturing", + "Textures:LightingModel:enableMaterialTexturing" ] CheckBox { text: modelData.split(":")[0] @@ -45,6 +45,7 @@ Column { "Diffuse:LightingModel:enableDiffuse", "Specular:LightingModel:enableSpecular", "Albedo:LightingModel:enableAlbedo", + "Wireframe:LightingModel:enableWireframe" ] CheckBox { text: modelData.split(":")[0] diff --git a/scripts/developer/utilities/render/photobooth/photobooth.js b/scripts/developer/utilities/render/photobooth/photobooth.js index 3e86d83a98..b78986be1a 100644 --- a/scripts/developer/utilities/render/photobooth/photobooth.js +++ b/scripts/developer/utilities/render/photobooth/photobooth.js @@ -8,12 +8,13 @@ var PhotoBooth = {}; PhotoBooth.init = function () { var success = Clipboard.importEntities(PHOTOBOOTH_SETUP_JSON_URL); - var frontFactor = 10; - var frontUnitVec = Vec3.normalize(Quat.getFront(MyAvatar.orientation)); - var frontOffset = Vec3.multiply(frontUnitVec,frontFactor); + var forwardFactor = 10; + var forwardUnitVector = Vec3.normalize(Quat.getForward(MyAvatar.orientation)); + var forwardOffset = Vec3.multiply(forwardUnitVector,forwardFactor); var rightFactor = 3; + // TODO: rightUnitVec is unused and spawnLocation declaration is incorrect var rightUnitVec = Vec3.normalize(Quat.getRight(MyAvatar.orientation)); - var spawnLocation = Vec3.sum(Vec3.sum(MyAvatar.position,frontOffset),rightFactor); + var spawnLocation = Vec3.sum(Vec3.sum(MyAvatar.position,forwardOffset),rightFactor); if (success) { this.pastedEntityIDs = Clipboard.pasteEntities(spawnLocation); this.processPastedEntities(); diff --git a/scripts/modules/vec3.js b/scripts/modules/vec3.js new file mode 100644 index 0000000000..f164f01374 --- /dev/null +++ b/scripts/modules/vec3.js @@ -0,0 +1,69 @@ +// Example of using a "system module" to decouple Vec3's implementation details. +// +// Users would bring Vec3 support in as a module: +// var vec3 = Script.require('vec3'); +// + +// (this example is compatible with using as a Script.include and as a Script.require module) +try { + // Script.require + module.exports = vec3; +} catch(e) { + // Script.include + Script.registerValue("vec3", vec3); +} + +vec3.fromObject = function(v) { + //return new vec3(v.x, v.y, v.z); + //... this is even faster and achieves the same effect + v.__proto__ = vec3.prototype; + return v; +}; + +vec3.prototype = { + multiply: function(v2) { + // later on could support overrides like so: + // if (v2 instanceof quat) { [...] } + // which of the below is faster (C++ or JS)? + // (dunno -- but could systematically find out and go with that version) + + // pure JS option + // return new vec3(this.x * v2.x, this.y * v2.y, this.z * v2.z); + + // hybrid C++ option + return vec3.fromObject(Vec3.multiply(this, v2)); + }, + // detects any NaN and Infinity values + isValid: function() { + return isFinite(this.x) && isFinite(this.y) && isFinite(this.z); + }, + // format Vec3's, eg: + // var v = vec3(); + // print(v); // outputs [Vec3 (0.000, 0.000, 0.000)] + toString: function() { + if (this === vec3.prototype) { + return "{Vec3 prototype}"; + } + function fixed(n) { return n.toFixed(3); } + return "[Vec3 (" + [this.x, this.y, this.z].map(fixed) + ")]"; + }, +}; + +vec3.DEBUG = true; + +function vec3(x, y, z) { + if (!(this instanceof vec3)) { + // if vec3 is called as a function then re-invoke as a constructor + // (so that `value instanceof vec3` holds true for created values) + return new vec3(x, y, z); + } + + // unfold default arguments (vec3(), vec3(.5), vec3(0,1), etc.) + this.x = x !== undefined ? x : 0; + this.y = y !== undefined ? y : this.x; + this.z = z !== undefined ? z : this.y; + + if (vec3.DEBUG && !this.isValid()) + throw new Error('vec3() -- invalid initial values ['+[].slice.call(arguments)+']'); +}; + diff --git a/scripts/system/assets/images/icon-particles.svg b/scripts/system/assets/images/icon-particles.svg new file mode 100644 index 0000000000..5e0105d7cd --- /dev/null +++ b/scripts/system/assets/images/icon-particles.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + diff --git a/scripts/system/assets/images/icon-point-light.svg b/scripts/system/assets/images/icon-point-light.svg new file mode 100644 index 0000000000..896c35b63b --- /dev/null +++ b/scripts/system/assets/images/icon-point-light.svg @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/scripts/system/assets/images/icon-spot-light.svg b/scripts/system/assets/images/icon-spot-light.svg new file mode 100644 index 0000000000..ac2f87bb27 --- /dev/null +++ b/scripts/system/assets/images/icon-spot-light.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/scripts/system/away.js b/scripts/system/away.js index 541fe6f679..4ca938d492 100644 --- a/scripts/system/away.js +++ b/scripts/system/away.js @@ -87,8 +87,8 @@ function moveCloserToCamera(positionAtHUD) { // we don't actually want to render at the slerped look at... instead, we want to render // slightly closer to the camera than that. var MOVE_CLOSER_TO_CAMERA_BY = -0.25; - var cameraFront = Quat.getFront(Camera.orientation); - var closerToCamera = Vec3.multiply(cameraFront, MOVE_CLOSER_TO_CAMERA_BY); // slightly closer to camera + var cameraForward = Quat.getForward(Camera.orientation); + var closerToCamera = Vec3.multiply(cameraForward, MOVE_CLOSER_TO_CAMERA_BY); // slightly closer to camera var slightlyCloserPosition = Vec3.sum(positionAtHUD, closerToCamera); return slightlyCloserPosition; diff --git a/scripts/system/controllers/grab.js b/scripts/system/controllers/grab.js index f0b6663bec..05b2eefeb5 100644 --- a/scripts/system/controllers/grab.js +++ b/scripts/system/controllers/grab.js @@ -463,7 +463,7 @@ Grabber.prototype.moveEvent = function(event) { var orientation = Camera.getOrientation(); var dragOffset = Vec3.multiply(drag.x, Quat.getRight(orientation)); dragOffset = Vec3.sum(dragOffset, Vec3.multiply(-drag.y, Quat.getUp(orientation))); - var axis = Vec3.cross(dragOffset, Quat.getFront(orientation)); + var axis = Vec3.cross(dragOffset, Quat.getForward(orientation)); axis = Vec3.normalize(axis); var ROTATE_STRENGTH = 0.4; // magic number tuned by hand var angle = ROTATE_STRENGTH * Math.sqrt((drag.x * drag.x) + (drag.y * drag.y)); @@ -487,7 +487,7 @@ Grabber.prototype.moveEvent = function(event) { if (this.mode === "verticalCylinder") { // for this mode we recompute the plane based on current Camera - var planeNormal = Quat.getFront(Camera.getOrientation()); + var planeNormal = Quat.getForward(Camera.getOrientation()); planeNormal.y = 0; planeNormal = Vec3.normalize(planeNormal); var pointOnCylinder = Vec3.multiply(planeNormal, this.xzDistanceToGrab); diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 51b01e60a2..e83e31aaa5 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -1481,7 +1481,7 @@ function MyController(hand) { var pickRay = { origin: PICK_WITH_HAND_RAY ? worldHandPosition : Camera.position, direction: PICK_WITH_HAND_RAY ? Quat.getUp(worldHandRotation) : Vec3.mix(Quat.getUp(worldHandRotation), - Quat.getFront(Camera.orientation), + Quat.getForward(Camera.orientation), HAND_HEAD_MIX_RATIO), length: PICK_MAX_DISTANCE }; diff --git a/scripts/system/controllers/handControllerPointer.js b/scripts/system/controllers/handControllerPointer.js index f8a336a017..eb94428100 100644 --- a/scripts/system/controllers/handControllerPointer.js +++ b/scripts/system/controllers/handControllerPointer.js @@ -174,7 +174,7 @@ function calculateRayUICollisionPoint(position, direction) { // interect HUD plane, 1m in front of camera, using formula: // scale = hudNormal dot (hudPoint - position) / hudNormal dot direction // intersection = postion + scale*direction - var hudNormal = Quat.getFront(Camera.getOrientation()); + var hudNormal = Quat.getForward(Camera.getOrientation()); var hudPoint = Vec3.sum(Camera.getPosition(), hudNormal); // must also scale if PLANAR_PERPENDICULAR_HUD_DISTANCE!=1 var denominator = Vec3.dot(hudNormal, direction); if (denominator === 0) { diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index c058f046db..1c6c9af272 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -85,6 +85,7 @@ function Trigger(hand) { } var coolInTimeout = null; +var ignoredEntities = []; var TELEPORTER_STATES = { IDLE: 'idle', @@ -239,11 +240,11 @@ function Teleporter() { // We might hit an invisible entity that is not a seat, so we need to do a second pass. // * In the second pass we pick against visible entities only. // - var intersection = Entities.findRayIntersection(pickRay, true, [], [this.targetEntity], false, true); + var intersection = Entities.findRayIntersection(pickRay, true, [], [this.targetEntity].concat(ignoredEntities), false, true); var teleportLocationType = getTeleportTargetType(intersection); if (teleportLocationType === TARGET.INVISIBLE) { - intersection = Entities.findRayIntersection(pickRay, true, [], [this.targetEntity], true, true); + intersection = Entities.findRayIntersection(pickRay, true, [], [this.targetEntity].concat(ignoredEntities), true, true); teleportLocationType = getTeleportTargetType(intersection); } @@ -513,7 +514,7 @@ function cleanup() { Script.scriptEnding.connect(cleanup); var isDisabled = false; -var handleHandMessages = function(channel, message, sender) { +var handleTeleportMessages = function(channel, message, sender) { var data; if (sender === MyAvatar.sessionUUID) { if (channel === 'Hifi-Teleport-Disabler') { @@ -529,12 +530,20 @@ var handleHandMessages = function(channel, message, sender) { if (message === 'none') { isDisabled = false; } - + } else if (channel === 'Hifi-Teleport-Ignore-Add' && !Uuid.isNull(message) && ignoredEntities.indexOf(message) === -1) { + ignoredEntities.push(message); + } else if (channel === 'Hifi-Teleport-Ignore-Remove' && !Uuid.isNull(message)) { + var removeIndex = ignoredEntities.indexOf(message); + if (removeIndex > -1) { + ignoredEntities.splice(removeIndex, 1); + } } } } Messages.subscribe('Hifi-Teleport-Disabler'); -Messages.messageReceived.connect(handleHandMessages); +Messages.subscribe('Hifi-Teleport-Ignore-Add'); +Messages.subscribe('Hifi-Teleport-Ignore-Remove'); +Messages.messageReceived.connect(handleTeleportMessages); }()); // END LOCAL_SCOPE diff --git a/scripts/system/controllers/toggleAdvancedMovementForHandControllers.js b/scripts/system/controllers/toggleAdvancedMovementForHandControllers.js index 46464dc2e1..e6c9b0aee0 100644 --- a/scripts/system/controllers/toggleAdvancedMovementForHandControllers.js +++ b/scripts/system/controllers/toggleAdvancedMovementForHandControllers.js @@ -17,15 +17,14 @@ var mappingName, basicMapping, isChecked; var TURN_RATE = 1000; var MENU_ITEM_NAME = "Advanced Movement For Hand Controllers"; -var SETTINGS_KEY = 'advancedMovementForHandControllersIsChecked'; var isDisabled = false; -var previousSetting = Settings.getValue(SETTINGS_KEY); -if (previousSetting === '' || previousSetting === false || previousSetting === 'false') { +var previousSetting = MyAvatar.useAdvancedMovementControls; +if (previousSetting === false) { previousSetting = false; isChecked = false; } -if (previousSetting === true || previousSetting === 'true') { +if (previousSetting === true) { previousSetting = true; isChecked = true; } @@ -37,7 +36,6 @@ function addAdvancedMovementItemToSettingsMenu() { isCheckable: true, isChecked: previousSetting }); - } function rotate180() { @@ -72,7 +70,6 @@ function registerBasicMapping() { } return; }); - basicMapping.from(Controller.Standard.LX).to(Controller.Standard.RX); basicMapping.from(Controller.Standard.RY).to(function(value) { if (isDisabled) { return; @@ -112,10 +109,10 @@ function menuItemEvent(menuItem) { if (menuItem == MENU_ITEM_NAME) { isChecked = Menu.isOptionChecked(MENU_ITEM_NAME); if (isChecked === true) { - Settings.setValue(SETTINGS_KEY, true); + MyAvatar.useAdvancedMovementControls = true; disableMappings(); } else if (isChecked === false) { - Settings.setValue(SETTINGS_KEY, false); + MyAvatar.useAdvancedMovementControls = false; enableMappings(); } } diff --git a/scripts/system/edit.js b/scripts/system/edit.js index a440fec1ac..8b02eb1550 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -33,13 +33,27 @@ Script.include([ "libraries/gridTool.js", "libraries/entityList.js", "particle_explorer/particleExplorerTool.js", - "libraries/lightOverlayManager.js" + "libraries/entityIconOverlayManager.js" ]); var selectionDisplay = SelectionDisplay; var selectionManager = SelectionManager; -var lightOverlayManager = new LightOverlayManager(); +const PARTICLE_SYSTEM_URL = Script.resolvePath("assets/images/icon-particles.svg"); +const POINT_LIGHT_URL = Script.resolvePath("assets/images/icon-point-light.svg"); +const SPOT_LIGHT_URL = Script.resolvePath("assets/images/icon-spot-light.svg"); +entityIconOverlayManager = new EntityIconOverlayManager(['Light', 'ParticleEffect'], function(entityID) { + var properties = Entities.getEntityProperties(entityID, ['type', 'isSpotlight']); + if (properties.type === 'Light') { + return { + url: properties.isSpotlight ? SPOT_LIGHT_URL : POINT_LIGHT_URL, + } + } else { + return { + url: PARTICLE_SYSTEM_URL, + } + } +}); var cameraManager = new CameraManager(); @@ -53,7 +67,45 @@ var entityListTool = new EntityListTool(); selectionManager.addEventListener(function () { selectionDisplay.updateHandles(); - lightOverlayManager.updatePositions(); + entityIconOverlayManager.updatePositions(); + + // Update particle explorer + var needToDestroyParticleExplorer = false; + if (selectionManager.selections.length === 1) { + var selectedEntityID = selectionManager.selections[0]; + if (selectedEntityID === selectedParticleEntityID) { + return; + } + var type = Entities.getEntityProperties(selectedEntityID, "type").type; + if (type === "ParticleEffect") { + // Destroy the old particles web view first + particleExplorerTool.destroyWebView(); + particleExplorerTool.createWebView(); + var properties = Entities.getEntityProperties(selectedEntityID); + var particleData = { + messageType: "particle_settings", + currentProperties: properties + }; + selectedParticleEntityID = selectedEntityID; + particleExplorerTool.setActiveParticleEntity(selectedParticleEntityID); + + particleExplorerTool.webView.webEventReceived.connect(function (data) { + data = JSON.parse(data); + if (data.messageType === "page_loaded") { + particleExplorerTool.webView.emitScriptEvent(JSON.stringify(particleData)); + } + }); + } else { + needToDestroyParticleExplorer = true; + } + } else { + needToDestroyParticleExplorer = true; + } + + if (needToDestroyParticleExplorer && selectedParticleEntityID !== null) { + selectedParticleEntityID = null; + particleExplorerTool.destroyWebView(); + } }); const KEY_P = 80; //Key code for letter p used for Parenting hotkey. @@ -82,13 +134,13 @@ var DEFAULT_LIGHT_DIMENSIONS = Vec3.multiply(20, DEFAULT_DIMENSIONS); var MENU_AUTO_FOCUS_ON_SELECT = "Auto Focus on Select"; var MENU_EASE_ON_FOCUS = "Ease Orientation on Focus"; -var MENU_SHOW_LIGHTS_IN_EDIT_MODE = "Show Lights in Edit Mode"; +var MENU_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE = "Show Lights and Particle Systems in Edit Mode"; var MENU_SHOW_ZONES_IN_EDIT_MODE = "Show Zones in Edit Mode"; var SETTING_INSPECT_TOOL_ENABLED = "inspectToolEnabled"; var SETTING_AUTO_FOCUS_ON_SELECT = "autoFocusOnSelect"; var SETTING_EASE_ON_FOCUS = "cameraEaseOnFocus"; -var SETTING_SHOW_LIGHTS_IN_EDIT_MODE = "showLightsInEditMode"; +var SETTING_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE = "showLightsAndParticlesInEditMode"; var SETTING_SHOW_ZONES_IN_EDIT_MODE = "showZonesInEditMode"; @@ -506,7 +558,7 @@ var toolBar = (function () { toolBar.writeProperty("shown", false); toolBar.writeProperty("shown", true); } - lightOverlayManager.setVisible(isActive && Menu.isOptionChecked(MENU_SHOW_LIGHTS_IN_EDIT_MODE)); + entityIconOverlayManager.setVisible(isActive && Menu.isOptionChecked(MENU_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE)); Entities.setDrawZoneBoundaries(isActive && Menu.isOptionChecked(MENU_SHOW_ZONES_IN_EDIT_MODE)); }; @@ -571,8 +623,8 @@ function findClickedEntity(event) { } var entityResult = Entities.findRayIntersection(pickRay, true); // want precision picking - var lightResult = lightOverlayManager.findRayIntersection(pickRay); - lightResult.accurate = true; + var iconResult = entityIconOverlayManager.findRayIntersection(pickRay); + iconResult.accurate = true; if (pickZones) { Entities.setZonesArePickable(false); @@ -580,18 +632,12 @@ function findClickedEntity(event) { var result; - if (!entityResult.intersects && !lightResult.intersects) { - return null; - } else if (entityResult.intersects && !lightResult.intersects) { + if (iconResult.intersects) { + result = iconResult; + } else if (entityResult.intersects) { result = entityResult; - } else if (!entityResult.intersects && lightResult.intersects) { - result = lightResult; } else { - if (entityResult.distance < lightResult.distance) { - result = entityResult; - } else { - result = lightResult; - } + return null; } if (!result.accurate) { @@ -770,7 +816,7 @@ function mouseClickEvent(event) { if (0 < x && sizeOK) { selectedEntityID = foundEntity; orientation = MyAvatar.orientation; - intersection = rayPlaneIntersection(pickRay, P, Quat.getFront(orientation)); + intersection = rayPlaneIntersection(pickRay, P, Quat.getForward(orientation)); if (!event.isShifted) { @@ -945,18 +991,18 @@ function setupModelMenus() { }); Menu.addMenuItem({ menuName: "Edit", - menuItemName: MENU_SHOW_LIGHTS_IN_EDIT_MODE, + menuItemName: MENU_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE, afterItem: MENU_EASE_ON_FOCUS, isCheckable: true, - isChecked: Settings.getValue(SETTING_SHOW_LIGHTS_IN_EDIT_MODE) === "true", + isChecked: Settings.getValue(SETTING_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE) !== "false", grouping: "Advanced" }); Menu.addMenuItem({ menuName: "Edit", menuItemName: MENU_SHOW_ZONES_IN_EDIT_MODE, - afterItem: MENU_SHOW_LIGHTS_IN_EDIT_MODE, + afterItem: MENU_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE, isCheckable: true, - isChecked: Settings.getValue(SETTING_SHOW_ZONES_IN_EDIT_MODE) === "true", + isChecked: Settings.getValue(SETTING_SHOW_ZONES_IN_EDIT_MODE) !== "false", grouping: "Advanced" }); @@ -987,7 +1033,7 @@ function cleanupModelMenus() { Menu.removeMenuItem("Edit", MENU_AUTO_FOCUS_ON_SELECT); Menu.removeMenuItem("Edit", MENU_EASE_ON_FOCUS); - Menu.removeMenuItem("Edit", MENU_SHOW_LIGHTS_IN_EDIT_MODE); + Menu.removeMenuItem("Edit", MENU_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE); Menu.removeMenuItem("Edit", MENU_SHOW_ZONES_IN_EDIT_MODE); } @@ -995,7 +1041,7 @@ Script.scriptEnding.connect(function () { toolBar.setActive(false); Settings.setValue(SETTING_AUTO_FOCUS_ON_SELECT, Menu.isOptionChecked(MENU_AUTO_FOCUS_ON_SELECT)); Settings.setValue(SETTING_EASE_ON_FOCUS, Menu.isOptionChecked(MENU_EASE_ON_FOCUS)); - Settings.setValue(SETTING_SHOW_LIGHTS_IN_EDIT_MODE, Menu.isOptionChecked(MENU_SHOW_LIGHTS_IN_EDIT_MODE)); + Settings.setValue(SETTING_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE, Menu.isOptionChecked(MENU_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE)); Settings.setValue(SETTING_SHOW_ZONES_IN_EDIT_MODE, Menu.isOptionChecked(MENU_SHOW_ZONES_IN_EDIT_MODE)); progressDialog.cleanup(); @@ -1184,7 +1230,7 @@ function parentSelectedEntities() { } function deleteSelectedEntities() { if (SelectionManager.hasSelection()) { - selectedParticleEntity = 0; + selectedParticleEntityID = null; particleExplorerTool.destroyWebView(); SelectionManager.saveProperties(); var savedProperties = []; @@ -1283,8 +1329,8 @@ function handeMenuEvent(menuItem) { selectAllEtitiesInCurrentSelectionBox(false); } else if (menuItem === "Select All Entities Touching Box") { selectAllEtitiesInCurrentSelectionBox(true); - } else if (menuItem === MENU_SHOW_LIGHTS_IN_EDIT_MODE) { - lightOverlayManager.setVisible(isActive && Menu.isOptionChecked(MENU_SHOW_LIGHTS_IN_EDIT_MODE)); + } else if (menuItem === MENU_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE) { + entityIconOverlayManager.setVisible(isActive && Menu.isOptionChecked(MENU_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE)); } else if (menuItem === MENU_SHOW_ZONES_IN_EDIT_MODE) { Entities.setDrawZoneBoundaries(isActive && Menu.isOptionChecked(MENU_SHOW_ZONES_IN_EDIT_MODE)); } @@ -1292,12 +1338,12 @@ function handeMenuEvent(menuItem) { } function getPositionToCreateEntity() { var HALF_TREE_SCALE = 16384; - var direction = Quat.getFront(MyAvatar.orientation); + var direction = Quat.getForward(MyAvatar.orientation); var distance = 1; var position = Vec3.sum(MyAvatar.position, Vec3.multiply(direction, distance)); if (Camera.mode === "entity" || Camera.mode === "independent") { - position = Vec3.sum(Camera.position, Vec3.multiply(Quat.getFront(Camera.orientation), distance)) + position = Vec3.sum(Camera.position, Vec3.multiply(Quat.getForward(Camera.orientation), distance)) } position.y += 0.5; if (position.x > HALF_TREE_SCALE || position.y > HALF_TREE_SCALE || position.z > HALF_TREE_SCALE) { @@ -1309,13 +1355,13 @@ function getPositionToCreateEntity() { function getPositionToImportEntity() { var dimensions = Clipboard.getContentsDimensions(); var HALF_TREE_SCALE = 16384; - var direction = Quat.getFront(MyAvatar.orientation); + var direction = Quat.getForward(MyAvatar.orientation); var longest = 1; longest = Math.sqrt(Math.pow(dimensions.x, 2) + Math.pow(dimensions.z, 2)); var position = Vec3.sum(MyAvatar.position, Vec3.multiply(direction, longest)); if (Camera.mode === "entity" || Camera.mode === "independent") { - position = Vec3.sum(Camera.position, Vec3.multiply(Quat.getFront(Camera.orientation), longest)) + position = Vec3.sum(Camera.position, Vec3.multiply(Quat.getForward(Camera.orientation), longest)) } if (position.x > HALF_TREE_SCALE || position.y > HALF_TREE_SCALE || position.z > HALF_TREE_SCALE) { @@ -1959,43 +2005,13 @@ var showMenuItem = propertyMenu.addMenuItem("Show in Marketplace"); var propertiesTool = new PropertiesTool(); var particleExplorerTool = new ParticleExplorerTool(); -var selectedParticleEntity = 0; +var selectedParticleEntityID = null; entityListTool.webView.webEventReceived.connect(function (data) { data = JSON.parse(data); - if(data.type === 'parent') { + if (data.type === 'parent') { parentSelectedEntities(); } else if(data.type === 'unparent') { unparentSelectedEntities(); - } else if (data.type === "selectionUpdate") { - var ids = data.entityIds; - if (ids.length === 1) { - if (Entities.getEntityProperties(ids[0], "type").type === "ParticleEffect") { - if (JSON.stringify(selectedParticleEntity) === JSON.stringify(ids[0])) { - // This particle entity is already selected, so return - return; - } - // Destroy the old particles web view first - particleExplorerTool.destroyWebView(); - particleExplorerTool.createWebView(); - var properties = Entities.getEntityProperties(ids[0]); - var particleData = { - messageType: "particle_settings", - currentProperties: properties - }; - selectedParticleEntity = ids[0]; - particleExplorerTool.setActiveParticleEntity(ids[0]); - - particleExplorerTool.webView.webEventReceived.connect(function (data) { - data = JSON.parse(data); - if (data.messageType === "page_loaded") { - particleExplorerTool.webView.emitScriptEvent(JSON.stringify(particleData)); - } - }); - } else { - selectedParticleEntity = 0; - particleExplorerTool.destroyWebView(); - } - } } }); diff --git a/scripts/system/libraries/WebTablet.js b/scripts/system/libraries/WebTablet.js index dd2aaf346b..32f45188dc 100644 --- a/scripts/system/libraries/WebTablet.js +++ b/scripts/system/libraries/WebTablet.js @@ -78,9 +78,9 @@ function calcSpawnInfo(hand, height) { rotation: lookAtRot }; } else { - var front = Quat.getFront(headRot); - finalPosition = Vec3.sum(headPos, Vec3.multiply(0.6, front)); - var orientation = Quat.lookAt({x: 0, y: 0, z: 0}, front, {x: 0, y: 1, z: 0}); + var forward = Quat.getForward(headRot); + finalPosition = Vec3.sum(headPos, Vec3.multiply(0.6, forward)); + var orientation = Quat.lookAt({x: 0, y: 0, z: 0}, forward, {x: 0, y: 1, z: 0}); return { position: finalPosition, rotation: Quat.multiply(orientation, {x: 0, y: 1, z: 0, w: 0}) diff --git a/scripts/system/libraries/entityCameraTool.js b/scripts/system/libraries/entityCameraTool.js index 301b60f550..6becc81d9b 100644 --- a/scripts/system/libraries/entityCameraTool.js +++ b/scripts/system/libraries/entityCameraTool.js @@ -158,7 +158,7 @@ CameraManager = function() { that.zoomDistance = INITIAL_ZOOM_DISTANCE; that.targetZoomDistance = that.zoomDistance + 3.0; var focalPoint = Vec3.sum(Camera.getPosition(), - Vec3.multiply(that.zoomDistance, Quat.getFront(Camera.getOrientation()))); + Vec3.multiply(that.zoomDistance, Quat.getForward(Camera.getOrientation()))); // Determine the correct yaw and pitch to keep the camera in the same location var dPos = Vec3.subtract(focalPoint, Camera.getPosition()); @@ -435,7 +435,7 @@ CameraManager = function() { }); var q = Quat.multiply(yRot, xRot); - var pos = Vec3.multiply(Quat.getFront(q), that.zoomDistance); + var pos = Vec3.multiply(Quat.getForward(q), that.zoomDistance); Camera.setPosition(Vec3.sum(that.focalPoint, pos)); yRot = Quat.angleAxis(that.yaw - 180, { diff --git a/scripts/system/libraries/lightOverlayManager.js b/scripts/system/libraries/entityIconOverlayManager.js similarity index 67% rename from scripts/system/libraries/lightOverlayManager.js rename to scripts/system/libraries/entityIconOverlayManager.js index 2d3618096b..7f7a293bc3 100644 --- a/scripts/system/libraries/lightOverlayManager.js +++ b/scripts/system/libraries/entityIconOverlayManager.js @@ -1,9 +1,6 @@ -var POINT_LIGHT_URL = "http://s3.amazonaws.com/hifi-public/images/tools/point-light.svg"; -var SPOT_LIGHT_URL = "http://s3.amazonaws.com/hifi-public/images/tools/spot-light.svg"; - -LightOverlayManager = function() { - var self = this; +/* globals EntityIconOverlayManager:true */ +EntityIconOverlayManager = function(entityTypes, getOverlayPropertiesFunc) { var visible = false; // List of all created overlays @@ -22,9 +19,16 @@ LightOverlayManager = function() { for (var id in entityIDs) { var entityID = entityIDs[id]; var properties = Entities.getEntityProperties(entityID); - Overlays.editOverlay(entityOverlays[entityID], { + var overlayProperties = { position: properties.position - }); + }; + if (getOverlayPropertiesFunc) { + var customProperties = getOverlayPropertiesFunc(entityID, properties); + for (var key in customProperties) { + overlayProperties[key] = customProperties[key]; + } + } + Overlays.editOverlay(entityOverlays[entityID], overlayProperties); } }; @@ -34,7 +38,7 @@ LightOverlayManager = function() { if (result.intersects) { for (var id in entityOverlays) { - if (result.overlayID == entityOverlays[id]) { + if (result.overlayID === entityOverlays[id]) { result.entityID = entityIDs[id]; found = true; break; @@ -50,7 +54,7 @@ LightOverlayManager = function() { }; this.setVisible = function(isVisible) { - if (visible != isVisible) { + if (visible !== isVisible) { visible = isVisible; for (var id in entityOverlays) { Overlays.editOverlay(entityOverlays[id], { @@ -62,12 +66,13 @@ LightOverlayManager = function() { // Allocate or get an unused overlay function getOverlay() { - if (unusedOverlays.length == 0) { - var overlay = Overlays.addOverlay("image3d", {}); + var overlay; + if (unusedOverlays.length === 0) { + overlay = Overlays.addOverlay("image3d", {}); allOverlays.push(overlay); } else { - var overlay = unusedOverlays.pop(); - }; + overlay = unusedOverlays.pop(); + } return overlay; } @@ -79,24 +84,32 @@ LightOverlayManager = function() { } function addEntity(entityID) { - var properties = Entities.getEntityProperties(entityID); - if (properties.type == "Light" && !(entityID in entityOverlays)) { + var properties = Entities.getEntityProperties(entityID, ['position', 'type']); + if (entityTypes.indexOf(properties.type) > -1 && !(entityID in entityOverlays)) { var overlay = getOverlay(); entityOverlays[entityID] = overlay; entityIDs[entityID] = entityID; - Overlays.editOverlay(overlay, { + var overlayProperties = { position: properties.position, - url: properties.isSpotlight ? SPOT_LIGHT_URL : POINT_LIGHT_URL, rotation: Quat.fromPitchYawRollDegrees(0, 0, 270), visible: visible, alpha: 0.9, scale: 0.5, + drawInFront: true, + isFacingAvatar: true, color: { red: 255, green: 255, blue: 255 } - }); + }; + if (getOverlayPropertiesFunc) { + var customProperties = getOverlayPropertiesFunc(entityID, properties); + for (var key in customProperties) { + overlayProperties[key] = customProperties[key]; + } + } + Overlays.editOverlay(overlay, overlayProperties); } } @@ -130,4 +143,4 @@ LightOverlayManager = function() { Overlays.deleteOverlay(allOverlays[i]); } }); -}; \ No newline at end of file +}; diff --git a/scripts/system/libraries/entitySelectionTool.js b/scripts/system/libraries/entitySelectionTool.js index d68a525458..9d4bf7d9a8 100644 --- a/scripts/system/libraries/entitySelectionTool.js +++ b/scripts/system/libraries/entitySelectionTool.js @@ -1032,10 +1032,12 @@ SelectionDisplay = (function() { var pickRay = controllerComputePickRay(); if (pickRay) { var entityIntersection = Entities.findRayIntersection(pickRay, true); - - + var iconIntersection = entityIconOverlayManager.findRayIntersection(pickRay); var overlayIntersection = Overlays.findRayIntersection(pickRay); - if (entityIntersection.intersects && + + if (iconIntersection.intersects) { + selectionManager.setSelections([iconIntersection.entityID]); + } else if (entityIntersection.intersects && (!overlayIntersection.intersects || (entityIntersection.distance < overlayIntersection.distance))) { if (HMD.tabletID === entityIntersection.entityID) { @@ -2515,7 +2517,7 @@ SelectionDisplay = (function() { onBegin: function(event) { pickRay = generalComputePickRay(event.x, event.y); - upDownPickNormal = Quat.getFront(lastCameraOrientation); + upDownPickNormal = Quat.getForward(lastCameraOrientation); // Remove y component so the y-axis lies along the plane we picking on - this will // give movements that follow the mouse. upDownPickNormal.y = 0; diff --git a/scripts/system/libraries/soundArray.js b/scripts/system/libraries/soundArray.js index f59c88a723..7e5da11948 100644 --- a/scripts/system/libraries/soundArray.js +++ b/scripts/system/libraries/soundArray.js @@ -36,7 +36,7 @@ SoundArray = function(audioOptions, autoUpdateAudioPosition) { }; this.updateAudioPosition = function() { var position = MyAvatar.position; - var forwardVector = Quat.getFront(MyAvatar.orientation); + var forwardVector = Quat.getForward(MyAvatar.orientation); this.audioOptions.position = Vec3.sum(position, forwardVector); }; }; diff --git a/scripts/system/libraries/toolBars.js b/scripts/system/libraries/toolBars.js index e49f8c4004..351f10e7bd 100644 --- a/scripts/system/libraries/toolBars.js +++ b/scripts/system/libraries/toolBars.js @@ -160,6 +160,7 @@ ToolBar = function(x, y, direction, optionalPersistenceKey, optionalInitialPosit visible: false }); this.spacing = []; + this.onMove = null; this.addTool = function(properties, selectable, selected) { if (direction == ToolBar.HORIZONTAL) { @@ -254,6 +255,9 @@ ToolBar = function(x, y, direction, optionalPersistenceKey, optionalInitialPosit y: y - ToolBar.SPACING }); } + if (this.onMove !== null) { + this.onMove(x, y, dx, dy); + }; } this.setAlpha = function(alpha, tool) { diff --git a/scripts/system/nameTag.js b/scripts/system/nameTag.js index e25db69064..17944bcf85 100644 --- a/scripts/system/nameTag.js +++ b/scripts/system/nameTag.js @@ -33,7 +33,7 @@ Script.setTimeout(function() { }, STARTUP_DELAY); function addNameTag() { - var nameTagPosition = Vec3.sum(MyAvatar.getHeadPosition(), Vec3.multiply(HEAD_OFFSET, Quat.getFront(MyAvatar.orientation))); + var nameTagPosition = Vec3.sum(MyAvatar.getHeadPosition(), Vec3.multiply(HEAD_OFFSET, Quat.getForward(MyAvatar.orientation))); nameTagPosition.y += HEIGHT_ABOVE_HEAD; var nameTagProperties = { name: MyAvatar.displayName + ' Name Tag', @@ -49,7 +49,7 @@ function addNameTag() { function updateNameTag() { var nameTagProps = Entities.getEntityProperties(nameTagEntityID); - var nameTagPosition = Vec3.sum(MyAvatar.getHeadPosition(), Vec3.multiply(HEAD_OFFSET, Quat.getFront(MyAvatar.orientation))); + var nameTagPosition = Vec3.sum(MyAvatar.getHeadPosition(), Vec3.multiply(HEAD_OFFSET, Quat.getForward(MyAvatar.orientation))); nameTagPosition.y += HEIGHT_ABOVE_HEAD; Entities.editEntity(nameTagEntityID, { diff --git a/scripts/system/pal.js b/scripts/system/pal.js index d9734850e5..a7c4f56ea6 100644 --- a/scripts/system/pal.js +++ b/scripts/system/pal.js @@ -444,7 +444,7 @@ function populateNearbyUserList(selectData, oldAudioData) { verticalHalfAngle = filter && (frustum.fieldOfView / 2), horizontalHalfAngle = filter && (verticalHalfAngle * frustum.aspectRatio), orientation = filter && Camera.orientation, - front = filter && Quat.getFront(orientation), + forward = filter && Quat.getForward(orientation), verticalAngleNormal = filter && Quat.getRight(orientation), horizontalAngleNormal = filter && Quat.getUp(orientation); avatarsOfInterest = {}; @@ -463,8 +463,8 @@ function populateNearbyUserList(selectData, oldAudioData) { return; } var normal = id && filter && Vec3.normalize(Vec3.subtract(avatar.position, myPosition)); - var horizontal = normal && angleBetweenVectorsInPlane(normal, front, horizontalAngleNormal); - var vertical = normal && angleBetweenVectorsInPlane(normal, front, verticalAngleNormal); + var horizontal = normal && angleBetweenVectorsInPlane(normal, forward, horizontalAngleNormal); + var vertical = normal && angleBetweenVectorsInPlane(normal, forward, verticalAngleNormal); if (id && filter && ((Math.abs(horizontal) > horizontalHalfAngle) || (Math.abs(vertical) > verticalHalfAngle))) { return; } diff --git a/scripts/system/voxels.js b/scripts/system/voxels.js index 3c219ebc7a..2f1d0eced9 100644 --- a/scripts/system/voxels.js +++ b/scripts/system/voxels.js @@ -253,7 +253,7 @@ function addTerrainBlock() { if (alreadyThere) { // there is already a terrain block under MyAvatar. // try in front of the avatar. - facingPosition = Vec3.sum(MyAvatar.position, Vec3.multiply(8.0, Quat.getFront(Camera.getOrientation()))); + facingPosition = Vec3.sum(MyAvatar.position, Vec3.multiply(8.0, Quat.getForward(Camera.getOrientation()))); facingPosition = Vec3.sum(facingPosition, { x: 8, y: 8, diff --git a/scripts/tutorials/NBody/makePlanets.js b/scripts/tutorials/NBody/makePlanets.js index 58a3c7cc2d..21415ccdc2 100644 --- a/scripts/tutorials/NBody/makePlanets.js +++ b/scripts/tutorials/NBody/makePlanets.js @@ -53,7 +53,7 @@ var deleteButton = toolBar.addOverlay("image", { }); function inFrontOfMe(distance) { - return Vec3.sum(Camera.getPosition(), Vec3.multiply(distance, Quat.getFront(Camera.getOrientation()))); + return Vec3.sum(Camera.getPosition(), Vec3.multiply(distance, Quat.getForward(Camera.getOrientation()))); } function onButtonClick() { diff --git a/scripts/tutorials/butterflies.js b/scripts/tutorials/butterflies.js index 55bafc0a27..9d8d1de52c 100644 --- a/scripts/tutorials/butterflies.js +++ b/scripts/tutorials/butterflies.js @@ -44,8 +44,8 @@ var FIXED_LOCATION = false; if (!FIXED_LOCATION) { var flockPosition = Vec3.sum(MyAvatar.position,Vec3.sum( - Vec3.multiply(Quat.getFront(MyAvatar.orientation), DISTANCE_ABOVE_ME), - Vec3.multiply(Quat.getFront(MyAvatar.orientation), DISTANCE_IN_FRONT_OF_ME))); + Vec3.multiply(Quat.getForward(MyAvatar.orientation), DISTANCE_ABOVE_ME), + Vec3.multiply(Quat.getForward(MyAvatar.orientation), DISTANCE_IN_FRONT_OF_ME))); } else { var flockPosition = { x: 4999.6, y: 4986.5, z: 5003.5 }; } @@ -119,7 +119,7 @@ function updateButterflies(deltaTime) { var HORIZ_SCALE = 0.50; var VERT_SCALE = 0.50; var newHeading = Math.random() * 360.0; - var newVelocity = Vec3.multiply(HORIZ_SCALE, Quat.getFront(Quat.fromPitchYawRollDegrees(0.0, newHeading, 0.0))); + var newVelocity = Vec3.multiply(HORIZ_SCALE, Quat.getForward(Quat.fromPitchYawRollDegrees(0.0, newHeading, 0.0))); newVelocity.y = (Math.random() + 0.5) * VERT_SCALE; Entities.editEntity(butterflies[i], { rotation: Quat.fromPitchYawRollDegrees(-80 + Math.random() * 20, newHeading, (Math.random() - 0.5) * 10), velocity: newVelocity } ); diff --git a/scripts/tutorials/createCow.js b/scripts/tutorials/createCow.js index 7446aa0fd0..16498e0e8c 100644 --- a/scripts/tutorials/createCow.js +++ b/scripts/tutorials/createCow.js @@ -18,7 +18,7 @@ var orientation = MyAvatar.orientation; orientation = Quat.safeEulerAngles(orientation); orientation.x = 0; orientation = Quat.fromVec3Degrees(orientation); -var center = Vec3.sum(MyAvatar.getHeadPosition(), Vec3.multiply(2, Quat.getFront(orientation))); +var center = Vec3.sum(MyAvatar.getHeadPosition(), Vec3.multiply(2, Quat.getForward(orientation))); // An entity is described and created by specifying a map of properties var cow = Entities.addEntity({ diff --git a/scripts/tutorials/createDice.js b/scripts/tutorials/createDice.js index 0d39d11d48..46ad0172aa 100644 --- a/scripts/tutorials/createDice.js +++ b/scripts/tutorials/createDice.js @@ -127,8 +127,8 @@ function mousePressEvent(event) { deleteDice(); } else if (clickedOverlay == diceButton) { var HOW_HARD = 2.0; - var position = Vec3.sum(Camera.getPosition(), Quat.getFront(Camera.getOrientation())); - var velocity = Vec3.multiply(HOW_HARD, Quat.getFront(Camera.getOrientation())); + var position = Vec3.sum(Camera.getPosition(), Quat.getForward(Camera.getOrientation())); + var velocity = Vec3.multiply(HOW_HARD, Quat.getForward(Camera.getOrientation())); shootDice(position, velocity); madeSound = false; } diff --git a/scripts/tutorials/createFlashlight.js b/scripts/tutorials/createFlashlight.js index 0e3581a435..f3e1e72182 100644 --- a/scripts/tutorials/createFlashlight.js +++ b/scripts/tutorials/createFlashlight.js @@ -16,7 +16,7 @@ var center = Vec3.sum(Vec3.sum(MyAvatar.position, { x: 0, y: 0.5, z: 0 -}), Vec3.multiply(0.5, Quat.getFront(Camera.getOrientation()))); +}), Vec3.multiply(0.5, Quat.getForward(Camera.getOrientation()))); var flashlight = Entities.addEntity({ type: "Model", diff --git a/scripts/tutorials/createGolfClub.js b/scripts/tutorials/createGolfClub.js index aa9834276a..21e60f26ef 100644 --- a/scripts/tutorials/createGolfClub.js +++ b/scripts/tutorials/createGolfClub.js @@ -15,7 +15,7 @@ var orientation = MyAvatar.orientation; orientation = Quat.safeEulerAngles(orientation); orientation.x = 0; orientation = Quat.fromVec3Degrees(orientation); -var center = Vec3.sum(MyAvatar.getHeadPosition(), Vec3.multiply(2, Quat.getFront(orientation))); +var center = Vec3.sum(MyAvatar.getHeadPosition(), Vec3.multiply(2, Quat.getForward(orientation))); var CLUB_MODEL = "http://hifi-production.s3.amazonaws.com/tutorials/golfClub/putter_VR.fbx"; var CLUB_COLLISION_HULL = "http://hifi-production.s3.amazonaws.com/tutorials/golfClub/club_collision_hull.obj"; diff --git a/scripts/tutorials/createPictureFrame.js b/scripts/tutorials/createPictureFrame.js index 4a1e5b16a7..873b604bfa 100644 --- a/scripts/tutorials/createPictureFrame.js +++ b/scripts/tutorials/createPictureFrame.js @@ -14,7 +14,7 @@ var center = Vec3.sum(Vec3.sum(MyAvatar.position, { x: 0, y: 0.5, z: 0 -}), Vec3.multiply(1, Quat.getFront(Camera.getOrientation()))); +}), Vec3.multiply(1, Quat.getForward(Camera.getOrientation()))); // this is just a model exported from blender with a texture named 'Picture' on one face. also made it emissive so it doesn't require lighting. var MODEL_URL = "http://hifi-production.s3.amazonaws.com/tutorials/pictureFrame/finalFrame.fbx"; diff --git a/scripts/tutorials/createPingPongGun.js b/scripts/tutorials/createPingPongGun.js index a077e5308d..c86a78e96d 100644 --- a/scripts/tutorials/createPingPongGun.js +++ b/scripts/tutorials/createPingPongGun.js @@ -14,7 +14,7 @@ var center = Vec3.sum(Vec3.sum(MyAvatar.position, { x: 0, y: 0.5, z: 0 -}), Vec3.multiply(0.5, Quat.getFront(Camera.getOrientation()))); +}), Vec3.multiply(0.5, Quat.getForward(Camera.getOrientation()))); var pingPongGunProperties = { diff --git a/scripts/tutorials/createPistol.js b/scripts/tutorials/createPistol.js index ae2f398840..8851f53d09 100644 --- a/scripts/tutorials/createPistol.js +++ b/scripts/tutorials/createPistol.js @@ -6,7 +6,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -var center = Vec3.sum(MyAvatar.position, Vec3.multiply(1.5, Quat.getFront(Camera.getOrientation()))); +var center = Vec3.sum(MyAvatar.position, Vec3.multiply(1.5, Quat.getForward(Camera.getOrientation()))); var SCRIPT_URL = "http://hifi-production.s3.amazonaws.com/tutorials/entity_scripts/pistol.js"; var MODEL_URL = "http://hifi-production.s3.amazonaws.com/tutorials/pistol/gun.fbx"; var COLLISION_SOUND_URL = 'http://hifi-production.s3.amazonaws.com/tutorials/pistol/drop.wav' diff --git a/scripts/tutorials/createSoundMaker.js b/scripts/tutorials/createSoundMaker.js index b79c650e27..2d86864982 100644 --- a/scripts/tutorials/createSoundMaker.js +++ b/scripts/tutorials/createSoundMaker.js @@ -13,7 +13,7 @@ var center = Vec3.sum(Vec3.sum(MyAvatar.position, { x: 0, y: 0.5, z: 0 -}), Vec3.multiply(1, Quat.getFront(Camera.getOrientation()))); +}), Vec3.multiply(1, Quat.getForward(Camera.getOrientation()))); function makeBell() { var soundMakerProperties = { diff --git a/scripts/tutorials/entity_scripts/golfClub.js b/scripts/tutorials/entity_scripts/golfClub.js index 2df3be8b60..6342838aa4 100644 --- a/scripts/tutorials/entity_scripts/golfClub.js +++ b/scripts/tutorials/entity_scripts/golfClub.js @@ -57,7 +57,7 @@ // Position yourself facing in the direction you were originally facing, but with a // point on the ground *away* meters from *position* and in front of you. - var offset = Quat.getFront(MyAvatar.orientation); + var offset = Quat.getForward(MyAvatar.orientation); offset.y = 0.0; offset = Vec3.multiply(-away, Vec3.normalize(offset)); var newAvatarPosition = Vec3.sum(position, offset); @@ -72,7 +72,7 @@ } function inFrontOfMe() { - return Vec3.sum(MyAvatar.position, Vec3.multiply(BALL_DROP_DISTANCE, Quat.getFront(MyAvatar.orientation))); + return Vec3.sum(MyAvatar.position, Vec3.multiply(BALL_DROP_DISTANCE, Quat.getForward(MyAvatar.orientation))); } function avatarHalfHeight() { diff --git a/scripts/tutorials/entity_scripts/pingPongGun.js b/scripts/tutorials/entity_scripts/pingPongGun.js index 4ec0254747..5ba4b15ea7 100644 --- a/scripts/tutorials/entity_scripts/pingPongGun.js +++ b/scripts/tutorials/entity_scripts/pingPongGun.js @@ -94,9 +94,9 @@ }, shootBall: function(gunProperties) { - var forwardVec = Quat.getFront(Quat.multiply(gunProperties.rotation, Quat.fromPitchYawRollDegrees(0, 180, 0))); - forwardVec = Vec3.normalize(forwardVec); - forwardVec = Vec3.multiply(forwardVec, GUN_FORCE); + var forwardVector = Quat.getForward(Quat.multiply(gunProperties.rotation, Quat.fromPitchYawRollDegrees(0, 180, 0))); + forwardVector = Vec3.normalize(forwardVector); + forwardVector = Vec3.multiply(forwardVector, GUN_FORCE); var properties = { name: 'Tutorial Ping Pong Ball', @@ -111,7 +111,7 @@ rotation: gunProperties.rotation, position: this.getGunTipPosition(gunProperties), gravity: PING_PONG_GUN_GRAVITY, - velocity: forwardVec, + velocity: forwardVector, lifetime: 10 }; @@ -131,12 +131,12 @@ getGunTipPosition: function(properties) { //the tip of the gun is going to be in a different place than the center, so we move in space relative to the model to find that position - var frontVector = Quat.getFront(properties.rotation); - var frontOffset = Vec3.multiply(frontVector, GUN_TIP_FWD_OFFSET); + var forwardVector = Quat.getForward(properties.rotation); + var forwardOffset = Vec3.multiply(forwardVector, GUN_TIP_FWD_OFFSET); var upVector = Quat.getUp(properties.rotation); var upOffset = Vec3.multiply(upVector, GUN_TIP_UP_OFFSET); - var gunTipPosition = Vec3.sum(properties.position, frontOffset); + var gunTipPosition = Vec3.sum(properties.position, forwardOffset); gunTipPosition = Vec3.sum(gunTipPosition, upOffset); return gunTipPosition; diff --git a/scripts/tutorials/entity_scripts/pistol.js b/scripts/tutorials/entity_scripts/pistol.js index 73a6daab93..38eb929177 100644 --- a/scripts/tutorials/entity_scripts/pistol.js +++ b/scripts/tutorials/entity_scripts/pistol.js @@ -69,7 +69,7 @@ var gunProps = Entities.getEntityProperties(this.entityID, ['position', 'rotation']); this.position = gunProps.position; this.rotation = gunProps.rotation; - this.firingDirection = Quat.getFront(this.rotation); + this.firingDirection = Quat.getForward(this.rotation); var upVec = Quat.getUp(this.rotation); this.barrelPoint = Vec3.sum(this.position, Vec3.multiply(upVec, this.laserOffsets.y)); this.laserTip = Vec3.sum(this.barrelPoint, Vec3.multiply(this.firingDirection, this.laserLength)); diff --git a/scripts/tutorials/entity_scripts/sit.js b/scripts/tutorials/entity_scripts/sit.js index 2ba19231e0..82afdc8974 100644 --- a/scripts/tutorials/entity_scripts/sit.js +++ b/scripts/tutorials/entity_scripts/sit.js @@ -2,31 +2,41 @@ Script.include("/~/system/libraries/utils.js"); var SETTING_KEY = "com.highfidelity.avatar.isSitting"; - var ROLE = "fly"; var ANIMATION_URL = "https://s3-us-west-1.amazonaws.com/hifi-content/clement/production/animations/sitting_idle.fbx"; var ANIMATION_FPS = 30; var ANIMATION_FIRST_FRAME = 1; var ANIMATION_LAST_FRAME = 10; - var RELEASE_KEYS = ['w', 'a', 's', 'd', 'UP', 'LEFT', 'DOWN', 'RIGHT']; var RELEASE_TIME = 500; // ms var RELEASE_DISTANCE = 0.2; // meters - var MAX_IK_ERROR = 20; - var DESKTOP_UI_CHECK_INTERVAL = 250; + var MAX_IK_ERROR = 30; + var IK_SETTLE_TIME = 250; // ms + var DESKTOP_UI_CHECK_INTERVAL = 100; var DESKTOP_MAX_DISTANCE = 5; - var SIT_DELAY = 25 + var SIT_DELAY = 25; + var MAX_RESET_DISTANCE = 0.5; // meters + var OVERRIDEN_DRIVE_KEYS = [ + DriveKeys.TRANSLATE_X, + DriveKeys.TRANSLATE_Y, + DriveKeys.TRANSLATE_Z, + DriveKeys.STEP_TRANSLATE_X, + DriveKeys.STEP_TRANSLATE_Y, + DriveKeys.STEP_TRANSLATE_Z, + ]; this.entityID = null; - this.timers = {}; this.animStateHandlerID = null; + this.interval = null; + this.sitDownSettlePeriod = null; + this.lastTimeNoDriveKeys = null; this.preload = function(entityID) { this.entityID = entityID; } this.unload = function() { - if (MyAvatar.sessionUUID === this.getSeatUser()) { - this.sitUp(this.entityID); + if (Settings.getValue(SETTING_KEY) === this.entityID) { + this.standUp(); } - if (this.interval) { + if (this.interval !== null) { Script.clearInterval(this.interval); this.interval = null; } @@ -34,42 +44,60 @@ } this.setSeatUser = function(user) { - var userData = Entities.getEntityProperties(this.entityID, ["userData"]).userData; - userData = JSON.parse(userData); + try { + var userData = Entities.getEntityProperties(this.entityID, ["userData"]).userData; + userData = JSON.parse(userData); - if (user) { - userData.seat.user = user; - } else { - delete userData.seat.user; + if (user !== null) { + userData.seat.user = user; + } else { + delete userData.seat.user; + } + + Entities.editEntity(this.entityID, { + userData: JSON.stringify(userData) + }); + } catch (e) { + // Do Nothing } - - Entities.editEntity(this.entityID, { - userData: JSON.stringify(userData) - }); } this.getSeatUser = function() { - var properties = Entities.getEntityProperties(this.entityID, ["userData", "position"]); - var userData = JSON.parse(properties.userData); + try { + var properties = Entities.getEntityProperties(this.entityID, ["userData", "position"]); + var userData = JSON.parse(properties.userData); - if (userData.seat.user && userData.seat.user !== MyAvatar.sessionUUID) { - var avatar = AvatarList.getAvatar(userData.seat.user); - if (avatar && Vec3.distance(avatar.position, properties.position) > RELEASE_DISTANCE) { - return null; + // If MyAvatar return my uuid + if (userData.seat.user === MyAvatar.sessionUUID) { + return userData.seat.user; } + + + // If Avatar appears to be sitting + if (userData.seat.user) { + var avatar = AvatarList.getAvatar(userData.seat.user); + if (avatar && (Vec3.distance(avatar.position, properties.position) < RELEASE_DISTANCE)) { + return userData.seat.user; + } + } + } catch (e) { + // Do nothing } - return userData.seat.user; + + // Nobody on the seat + return null; } + // Is the seat used this.checkSeatForAvatar = function() { var seatUser = this.getSeatUser(); - var avatarIdentifiers = AvatarList.getAvatarIdentifiers(); - for (var i in avatarIdentifiers) { - var avatar = AvatarList.getAvatar(avatarIdentifiers[i]); - if (avatar && avatar.sessionUUID === seatUser) { - return true; - } + + // If MyAvatar appears to be sitting + if (seatUser === MyAvatar.sessionUUID) { + var properties = Entities.getEntityProperties(this.entityID, ["position"]); + return Vec3.distance(MyAvatar.position, properties.position) < RELEASE_DISTANCE; } - return false; + + return seatUser !== null; } this.sitDown = function() { @@ -77,41 +105,53 @@ print("Someone is already sitting in that chair."); return; } + print("Sitting down (" + this.entityID + ")"); - this.setSeatUser(MyAvatar.sessionUUID); + var now = Date.now(); + this.sitDownSettlePeriod = now + IK_SETTLE_TIME; + this.lastTimeNoDriveKeys = now; var previousValue = Settings.getValue(SETTING_KEY); Settings.setValue(SETTING_KEY, this.entityID); + this.setSeatUser(MyAvatar.sessionUUID); if (previousValue === "") { MyAvatar.characterControllerEnabled = false; MyAvatar.hmdLeanRecenterEnabled = false; - MyAvatar.overrideRoleAnimation(ROLE, ANIMATION_URL, ANIMATION_FPS, true, ANIMATION_FIRST_FRAME, ANIMATION_LAST_FRAME); + var ROLES = MyAvatar.getAnimationRoles(); + for (i in ROLES) { + MyAvatar.overrideRoleAnimation(ROLES[i], ANIMATION_URL, ANIMATION_FPS, true, ANIMATION_FIRST_FRAME, ANIMATION_LAST_FRAME); + } MyAvatar.resetSensorsAndBody(); } - var that = this; - Script.setTimeout(function() { - var properties = Entities.getEntityProperties(that.entityID, ["position", "rotation"]); - var index = MyAvatar.getJointIndex("Hips"); - MyAvatar.pinJoint(index, properties.position, properties.rotation); + var properties = Entities.getEntityProperties(this.entityID, ["position", "rotation"]); + var index = MyAvatar.getJointIndex("Hips"); + MyAvatar.pinJoint(index, properties.position, properties.rotation); - that.animStateHandlerID = MyAvatar.addAnimationStateHandler(function(properties) { - return { headType: 0 }; - }, ["headType"]); - Script.update.connect(that, that.update); - Controller.keyPressEvent.connect(that, that.keyPressed); - Controller.keyReleaseEvent.connect(that, that.keyReleased); - for (var i in RELEASE_KEYS) { - Controller.captureKeyEvents({ text: RELEASE_KEYS[i] }); - } - }, SIT_DELAY); + this.animStateHandlerID = MyAvatar.addAnimationStateHandler(function(properties) { + return { headType: 0 }; + }, ["headType"]); + Script.update.connect(this, this.update); + for (var i in OVERRIDEN_DRIVE_KEYS) { + MyAvatar.disableDriveKey(OVERRIDEN_DRIVE_KEYS[i]); + } } - this.sitUp = function() { - this.setSeatUser(null); + this.standUp = function() { + print("Standing up (" + this.entityID + ")"); + MyAvatar.removeAnimationStateHandler(this.animStateHandlerID); + Script.update.disconnect(this, this.update); + for (var i in OVERRIDEN_DRIVE_KEYS) { + MyAvatar.enableDriveKey(OVERRIDEN_DRIVE_KEYS[i]); + } + this.setSeatUser(null); if (Settings.getValue(SETTING_KEY) === this.entityID) { - MyAvatar.restoreRoleAnimation(ROLE); + Settings.setValue(SETTING_KEY, ""); + var ROLES = MyAvatar.getAnimationRoles(); + for (i in ROLES) { + MyAvatar.restoreRoleAnimation(ROLES[i]); + } MyAvatar.characterControllerEnabled = true; MyAvatar.hmdLeanRecenterEnabled = true; @@ -124,19 +164,10 @@ MyAvatar.bodyPitch = 0.0; MyAvatar.bodyRoll = 0.0; }, SIT_DELAY); - - Settings.setValue(SETTING_KEY, ""); - } - - MyAvatar.removeAnimationStateHandler(this.animStateHandlerID); - Script.update.disconnect(this, this.update); - Controller.keyPressEvent.disconnect(this, this.keyPressed); - Controller.keyReleaseEvent.disconnect(this, this.keyReleased); - for (var i in RELEASE_KEYS) { - Controller.releaseKeyEvents({ text: RELEASE_KEYS[i] }); } } + // function called by teleport.js if it detects the appropriate userData this.sit = function () { this.sitDown(); } @@ -183,39 +214,52 @@ } } - this.update = function(dt) { if (MyAvatar.sessionUUID === this.getSeatUser()) { - var properties = Entities.getEntityProperties(this.entityID, ["position"]); + var properties = Entities.getEntityProperties(this.entityID); var avatarDistance = Vec3.distance(MyAvatar.position, properties.position); var ikError = MyAvatar.getIKErrorOnLastSolve(); - if (avatarDistance > RELEASE_DISTANCE || ikError > MAX_IK_ERROR) { + var now = Date.now(); + var shouldStandUp = false; + + // Check if a drive key is pressed + var hasActiveDriveKey = false; + for (var i in OVERRIDEN_DRIVE_KEYS) { + if (MyAvatar.getRawDriveKey(OVERRIDEN_DRIVE_KEYS[i]) != 0.0) { + hasActiveDriveKey = true; + break; + } + } + + // Only standup if user has been pushing a drive key for RELEASE_TIME + if (hasActiveDriveKey) { + var elapsed = now - this.lastTimeNoDriveKeys; + shouldStandUp = elapsed > RELEASE_TIME; + } else { + this.lastTimeNoDriveKeys = Date.now(); + } + + // Allow some time for the IK to settle + if (ikError > MAX_IK_ERROR && now > this.sitDownSettlePeriod) { + shouldStandUp = true; + } + + + if (shouldStandUp || avatarDistance > RELEASE_DISTANCE) { print("IK error: " + ikError + ", distance from chair: " + avatarDistance); - this.sitUp(this.entityID); + + // Move avatar in front of the chair to avoid getting stuck in collision hulls + if (avatarDistance < MAX_RESET_DISTANCE) { + var offset = { x: 0, y: 1.0, z: -0.5 - properties.dimensions.z * properties.registrationPoint.z }; + var position = Vec3.sum(properties.position, Vec3.multiplyQbyV(properties.rotation, offset)); + MyAvatar.position = position; + print("Moving Avatar in front of the chair.") + } + + this.standUp(); } } } - this.keyPressed = function(event) { - if (isInEditMode()) { - return; - } - - if (RELEASE_KEYS.indexOf(event.text) !== -1) { - var that = this; - this.timers[event.text] = Script.setTimeout(function() { - that.sitUp(); - }, RELEASE_TIME); - } - } - this.keyReleased = function(event) { - if (RELEASE_KEYS.indexOf(event.text) !== -1) { - if (this.timers[event.text]) { - Script.clearTimeout(this.timers[event.text]); - delete this.timers[event.text]; - } - } - } - this.canSitDesktop = function() { var properties = Entities.getEntityProperties(this.entityID, ["position"]); var distanceFromSeat = Vec3.distance(MyAvatar.position, properties.position); @@ -223,7 +267,7 @@ } this.hoverEnterEntity = function(event) { - if (isInEditMode() || (MyAvatar.sessionUUID === this.getSeatUser())) { + if (isInEditMode() || this.interval !== null) { return; } @@ -239,18 +283,18 @@ }, DESKTOP_UI_CHECK_INTERVAL); } this.hoverLeaveEntity = function(event) { - if (this.interval) { + if (this.interval !== null) { Script.clearInterval(this.interval); this.interval = null; } this.cleanupOverlay(); } - this.clickDownOnEntity = function () { - if (isInEditMode() || (MyAvatar.sessionUUID === this.getSeatUser())) { + this.clickDownOnEntity = function (id, event) { + if (isInEditMode()) { return; } - if (this.canSitDesktop()) { + if (event.isPrimaryButton && this.canSitDesktop()) { this.sitDown(); } } diff --git a/scripts/tutorials/makeBlocks.js b/scripts/tutorials/makeBlocks.js index 54bdead792..432f7444c4 100644 --- a/scripts/tutorials/makeBlocks.js +++ b/scripts/tutorials/makeBlocks.js @@ -34,12 +34,12 @@ var SCRIPT_URL = Script.resolvePath("./entity_scripts/magneticBlock.js"); - var frontVector = Quat.getFront(MyAvatar.orientation); - frontVector.y += VERTICAL_OFFSET; + var forwardVector = Quat.getForward(MyAvatar.orientation); + forwardVector.y += VERTICAL_OFFSET; for (var x = 0; x < COLUMNS; x++) { for (var y = 0; y < ROWS; y++) { - var frontOffset = { + var forwardOffset = { x: 0, y: SIZE * y + SIZE, z: SIZE * x + SIZE @@ -61,7 +61,7 @@ cloneLimit: 9999 } }), - position: Vec3.sum(MyAvatar.position, Vec3.sum(frontOffset, frontVector)), + position: Vec3.sum(MyAvatar.position, Vec3.sum(forwardOffset, forwardVector)), color: newColor(), script: SCRIPT_URL }); diff --git a/tests/ktx/CMakeLists.txt b/tests/ktx/CMakeLists.txt new file mode 100644 index 0000000000..d72379efd6 --- /dev/null +++ b/tests/ktx/CMakeLists.txt @@ -0,0 +1,15 @@ + +set(TARGET_NAME ktx-test) + +if (WIN32) + SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /ignore:4049 /ignore:4217") +endif() + +# This is not a testcase -- just set it up as a regular hifi project +setup_hifi_project(Quick Gui OpenGL) +set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/") + +# link in the shared libraries +link_hifi_libraries(shared octree ktx gl gpu gpu-gl render model model-networking networking render-utils fbx entities entities-renderer animation audio avatars script-engine physics) + +package_libraries_for_deployment() diff --git a/tests/ktx/src/main.cpp b/tests/ktx/src/main.cpp new file mode 100644 index 0000000000..aa6795e17b --- /dev/null +++ b/tests/ktx/src/main.cpp @@ -0,0 +1,150 @@ +// +// Created by Bradley Austin Davis on 2016/07/01 +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + + +#include +#include +#include +#include + + +QSharedPointer logger; + +gpu::Texture* cacheTexture(const std::string& name, gpu::Texture* srcTexture, bool write = true, bool read = true); + + +void messageHandler(QtMsgType type, const QMessageLogContext& context, const QString& message) { + QString logMessage = LogHandler::getInstance().printMessage((LogMsgType)type, context, message); + + if (!logMessage.isEmpty()) { +#ifdef Q_OS_WIN + OutputDebugStringA(logMessage.toLocal8Bit().constData()); + OutputDebugStringA("\n"); +#endif + logger->addMessage(qPrintable(logMessage + "\n")); + } +} + +const char * LOG_FILTER_RULES = R"V0G0N( +hifi.gpu=true +)V0G0N"; + +QString getRootPath() { + static std::once_flag once; + static QString result; + std::call_once(once, [&] { + QFileInfo file(__FILE__); + QDir parent = file.absolutePath(); + result = QDir::cleanPath(parent.currentPath() + "/../../.."); + }); + return result; +} + +const QString TEST_IMAGE = getRootPath() + "/scripts/developer/tests/cube_texture.png"; +const QString TEST_IMAGE_KTX = getRootPath() + "/scripts/developer/tests/cube_texture.ktx"; + +int main(int argc, char** argv) { + QApplication app(argc, argv); + QCoreApplication::setApplicationName("KTX"); + QCoreApplication::setOrganizationName("High Fidelity"); + QCoreApplication::setOrganizationDomain("highfidelity.com"); + logger.reset(new FileLogger()); + + Q_ASSERT(sizeof(ktx::Header) == 12 + (sizeof(uint32_t) * 13)); + + DependencyManager::set(); + qInstallMessageHandler(messageHandler); + QLoggingCategory::setFilterRules(LOG_FILTER_RULES); + + QImage image(TEST_IMAGE); + gpu::Texture* testTexture = model::TextureUsage::process2DTextureColorFromImage(image, TEST_IMAGE.toStdString(), true, false, true); + + auto ktxMemory = gpu::Texture::serialize(*testTexture); + { + const auto& ktxStorage = ktxMemory->getStorage(); + QFile outFile(TEST_IMAGE_KTX); + if (!outFile.open(QFile::Truncate | QFile::ReadWrite)) { + throw std::runtime_error("Unable to open file"); + } + auto ktxSize = ktxStorage->size(); + outFile.resize(ktxSize); + auto dest = outFile.map(0, ktxSize); + memcpy(dest, ktxStorage->data(), ktxSize); + outFile.unmap(dest); + outFile.close(); + } + + auto ktxFile = ktx::KTX::create(std::shared_ptr(new storage::FileStorage(TEST_IMAGE_KTX))); + { + const auto& memStorage = ktxMemory->getStorage(); + const auto& fileStorage = ktxFile->getStorage(); + Q_ASSERT(memStorage->size() == fileStorage->size()); + Q_ASSERT(memStorage->data() != fileStorage->data()); + Q_ASSERT(0 == memcmp(memStorage->data(), fileStorage->data(), memStorage->size())); + Q_ASSERT(ktxFile->_images.size() == ktxMemory->_images.size()); + auto imageCount = ktxFile->_images.size(); + auto startMemory = ktxMemory->_storage->data(); + auto startFile = ktxFile->_storage->data(); + for (size_t i = 0; i < imageCount; ++i) { + auto memImages = ktxMemory->_images[i]; + auto fileImages = ktxFile->_images[i]; + Q_ASSERT(memImages._padding == fileImages._padding); + Q_ASSERT(memImages._numFaces == fileImages._numFaces); + Q_ASSERT(memImages._imageSize == fileImages._imageSize); + Q_ASSERT(memImages._faceSize == fileImages._faceSize); + Q_ASSERT(memImages._faceBytes.size() == memImages._numFaces); + Q_ASSERT(fileImages._faceBytes.size() == fileImages._numFaces); + auto faceCount = fileImages._numFaces; + for (uint32_t face = 0; face < faceCount; ++face) { + auto memFace = memImages._faceBytes[face]; + auto memOffset = memFace - startMemory; + auto fileFace = fileImages._faceBytes[face]; + auto fileOffset = fileFace - startFile; + Q_ASSERT(memOffset % 4 == 0); + Q_ASSERT(memOffset == fileOffset); + } + } + } + testTexture->setKtxBacking(ktxFile); + return 0; +} + +#include "main.moc" + diff --git a/tests/render-perf/CMakeLists.txt b/tests/render-perf/CMakeLists.txt index d4f90fdace..96cede9c43 100644 --- a/tests/render-perf/CMakeLists.txt +++ b/tests/render-perf/CMakeLists.txt @@ -10,7 +10,7 @@ setup_hifi_project(Quick Gui OpenGL) set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/") # link in the shared libraries -link_hifi_libraries(shared octree gl gpu gpu-gl render model model-networking networking render-utils fbx entities entities-renderer animation audio avatars script-engine physics) +link_hifi_libraries(shared octree ktx gl gpu gpu-gl render model model-networking networking render-utils fbx entities entities-renderer animation audio avatars script-engine physics) package_libraries_for_deployment() diff --git a/tests/render-perf/src/Camera.hpp b/tests/render-perf/src/Camera.hpp index a3b33ceb14..ada1277c47 100644 --- a/tests/render-perf/src/Camera.hpp +++ b/tests/render-perf/src/Camera.hpp @@ -123,16 +123,16 @@ public: void update(float deltaTime) { if (moving()) { - glm::vec3 camFront = getOrientation() * Vectors::FRONT; + glm::vec3 camForward = getOrientation() * Vectors::FRONT; glm::vec3 camRight = getOrientation() * Vectors::RIGHT; glm::vec3 camUp = getOrientation() * Vectors::UP; float moveSpeed = deltaTime * movementSpeed; if (keys[FORWARD]) { - position += camFront * moveSpeed; + position += camForward * moveSpeed; } if (keys[BACK]) { - position -= camFront * moveSpeed; + position -= camForward * moveSpeed; } if (keys[LEFT]) { position -= camRight * moveSpeed; diff --git a/tests/render-perf/src/main.cpp b/tests/render-perf/src/main.cpp index 7e9d2c426f..522fe79b10 100644 --- a/tests/render-perf/src/main.cpp +++ b/tests/render-perf/src/main.cpp @@ -642,7 +642,6 @@ protected: gpu::Texture::setAllowedGPUMemoryUsage(MB_TO_BYTES(64)); return; - default: break; } diff --git a/tests/render-texture-load/src/main.cpp b/tests/render-texture-load/src/main.cpp index 09a420f018..d924f76232 100644 --- a/tests/render-texture-load/src/main.cpp +++ b/tests/render-texture-load/src/main.cpp @@ -48,6 +48,7 @@ #include #include +#include #include #include #include diff --git a/tests/shared/src/StorageTests.cpp b/tests/shared/src/StorageTests.cpp new file mode 100644 index 0000000000..fa538f6911 --- /dev/null +++ b/tests/shared/src/StorageTests.cpp @@ -0,0 +1,75 @@ +// +// Created by Bradley Austin Davis on 2016/02/17 +// Copyright 2013-2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "StorageTests.h" + +QTEST_MAIN(StorageTests) + +using namespace storage; + +StorageTests::StorageTests() { + for (size_t i = 0; i < _testData.size(); ++i) { + _testData[i] = (uint8_t)rand(); + } + _testFile = QDir::tempPath() + "/" + QUuid::createUuid().toString(); +} + +StorageTests::~StorageTests() { + QFileInfo fileInfo(_testFile); + if (fileInfo.exists()) { + QFile(_testFile).remove(); + } +} + + +void StorageTests::testConversion() { + { + QFileInfo fileInfo(_testFile); + QCOMPARE(fileInfo.exists(), false); + } + StoragePointer storagePointer = std::make_unique(_testData.size(), _testData.data()); + QCOMPARE(storagePointer->size(), (quint64)_testData.size()); + QCOMPARE(memcmp(_testData.data(), storagePointer->data(), _testData.size()), 0); + // Convert to a file + storagePointer = storagePointer->toFileStorage(_testFile); + { + QFileInfo fileInfo(_testFile); + QCOMPARE(fileInfo.exists(), true); + QCOMPARE(fileInfo.size(), (qint64)_testData.size()); + } + QCOMPARE(storagePointer->size(), (quint64)_testData.size()); + QCOMPARE(memcmp(_testData.data(), storagePointer->data(), _testData.size()), 0); + + // Convert to memory + storagePointer = storagePointer->toMemoryStorage(); + QCOMPARE(storagePointer->size(), (quint64)_testData.size()); + QCOMPARE(memcmp(_testData.data(), storagePointer->data(), _testData.size()), 0); + { + // ensure the file is unaffected + QFileInfo fileInfo(_testFile); + QCOMPARE(fileInfo.exists(), true); + QCOMPARE(fileInfo.size(), (qint64)_testData.size()); + } + + // truncate the data as a new memory object + auto newSize = _testData.size() / 2; + storagePointer = std::make_unique(newSize, storagePointer->data()); + QCOMPARE(storagePointer->size(), (quint64)newSize); + QCOMPARE(memcmp(_testData.data(), storagePointer->data(), newSize), 0); + + // Convert back to file + storagePointer = storagePointer->toFileStorage(_testFile); + QCOMPARE(storagePointer->size(), (quint64)newSize); + QCOMPARE(memcmp(_testData.data(), storagePointer->data(), newSize), 0); + { + // ensure the file is truncated + QFileInfo fileInfo(_testFile); + QCOMPARE(fileInfo.exists(), true); + QCOMPARE(fileInfo.size(), (qint64)newSize); + } +} diff --git a/tests/shared/src/StorageTests.h b/tests/shared/src/StorageTests.h new file mode 100644 index 0000000000..6a2c153223 --- /dev/null +++ b/tests/shared/src/StorageTests.h @@ -0,0 +1,32 @@ +// +// Created by Bradley Austin Davis on 2016/02/17 +// Copyright 2013-2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_StorageTests_h +#define hifi_StorageTests_h + +#include + +#include +#include + +class StorageTests : public QObject { + Q_OBJECT + +public: + StorageTests(); + ~StorageTests(); + +private slots: + void testConversion(); + +private: + std::array _testData; + QString _testFile; +}; + +#endif // hifi_StorageTests_h diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index a85a112bf5..8dc993e6fe 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -17,3 +17,5 @@ set_target_properties(ac-client PROPERTIES FOLDER "Tools") add_subdirectory(skeleton-dump) set_target_properties(skeleton-dump PROPERTIES FOLDER "Tools") +add_subdirectory(atp-get) +set_target_properties(atp-get PROPERTIES FOLDER "Tools") diff --git a/tools/atp-get/CMakeLists.txt b/tools/atp-get/CMakeLists.txt new file mode 100644 index 0000000000..b1646dc023 --- /dev/null +++ b/tools/atp-get/CMakeLists.txt @@ -0,0 +1,3 @@ +set(TARGET_NAME atp-get) +setup_hifi_project(Core Widgets) +link_hifi_libraries(shared networking) diff --git a/tools/atp-get/src/ATPGetApp.cpp b/tools/atp-get/src/ATPGetApp.cpp new file mode 100644 index 0000000000..30054fffea --- /dev/null +++ b/tools/atp-get/src/ATPGetApp.cpp @@ -0,0 +1,269 @@ +// +// ATPGetApp.cpp +// tools/atp-get/src +// +// Created by Seth Alves on 2017-3-15 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ATPGetApp.h" + +ATPGetApp::ATPGetApp(int argc, char* argv[]) : + QCoreApplication(argc, argv) +{ + // parse command-line + QCommandLineParser parser; + parser.setApplicationDescription("High Fidelity ATP-Get"); + + const QCommandLineOption helpOption = parser.addHelpOption(); + + const QCommandLineOption verboseOutput("v", "verbose output"); + parser.addOption(verboseOutput); + + const QCommandLineOption domainAddressOption("d", "domain-server address", "127.0.0.1"); + parser.addOption(domainAddressOption); + + const QCommandLineOption cacheSTUNOption("s", "cache stun-server response"); + parser.addOption(cacheSTUNOption); + + const QCommandLineOption listenPortOption("listenPort", "listen port", QString::number(INVALID_PORT)); + parser.addOption(listenPortOption); + + + if (!parser.parse(QCoreApplication::arguments())) { + qCritical() << parser.errorText() << endl; + parser.showHelp(); + Q_UNREACHABLE(); + } + + if (parser.isSet(helpOption)) { + parser.showHelp(); + Q_UNREACHABLE(); + } + + _verbose = parser.isSet(verboseOutput); + if (!_verbose) { + QLoggingCategory::setFilterRules("qt.network.ssl.warning=false"); + + const_cast(&networking())->setEnabled(QtDebugMsg, false); + const_cast(&networking())->setEnabled(QtInfoMsg, false); + const_cast(&networking())->setEnabled(QtWarningMsg, false); + + const_cast(&shared())->setEnabled(QtDebugMsg, false); + const_cast(&shared())->setEnabled(QtInfoMsg, false); + const_cast(&shared())->setEnabled(QtWarningMsg, false); + } + + + QStringList filenames = parser.positionalArguments(); + if (filenames.empty() || filenames.size() > 2) { + qDebug() << "give remote url and optional local filename as arguments"; + parser.showHelp(); + Q_UNREACHABLE(); + } + + _url = QUrl(filenames[0]); + if (_url.scheme() != "atp") { + qDebug() << "url should start with atp:"; + parser.showHelp(); + Q_UNREACHABLE(); + } + + if (filenames.size() == 2) { + _localOutputFile = filenames[1]; + } + + QString domainServerAddress = "127.0.0.1:40103"; + if (parser.isSet(domainAddressOption)) { + domainServerAddress = parser.value(domainAddressOption); + } + + if (_verbose) { + qDebug() << "domain-server address is" << domainServerAddress; + } + + int listenPort = INVALID_PORT; + if (parser.isSet(listenPortOption)) { + listenPort = parser.value(listenPortOption).toInt(); + } + + Setting::init(); + DependencyManager::registerInheritance(); + + DependencyManager::set([&]{ return QString("Mozilla/5.0 (HighFidelityATPGet)"); }); + DependencyManager::set(); + DependencyManager::set(NodeType::Agent, listenPort); + + + auto nodeList = DependencyManager::get(); + + // start the nodeThread so its event loop is running + QThread* nodeThread = new QThread(this); + nodeThread->setObjectName("NodeList Thread"); + nodeThread->start(); + + // make sure the node thread is given highest priority + nodeThread->setPriority(QThread::TimeCriticalPriority); + + // setup a timer for domain-server check ins + QTimer* domainCheckInTimer = new QTimer(nodeList.data()); + connect(domainCheckInTimer, &QTimer::timeout, nodeList.data(), &NodeList::sendDomainServerCheckIn); + domainCheckInTimer->start(DOMAIN_SERVER_CHECK_IN_MSECS); + + // put the NodeList and datagram processing on the node thread + nodeList->moveToThread(nodeThread); + + const DomainHandler& domainHandler = nodeList->getDomainHandler(); + + connect(&domainHandler, SIGNAL(hostnameChanged(const QString&)), SLOT(domainChanged(const QString&))); + // connect(&domainHandler, SIGNAL(resetting()), SLOT(resettingDomain())); + // connect(&domainHandler, SIGNAL(disconnectedFromDomain()), SLOT(clearDomainOctreeDetails())); + connect(&domainHandler, &DomainHandler::domainConnectionRefused, this, &ATPGetApp::domainConnectionRefused); + + connect(nodeList.data(), &NodeList::nodeAdded, this, &ATPGetApp::nodeAdded); + connect(nodeList.data(), &NodeList::nodeKilled, this, &ATPGetApp::nodeKilled); + connect(nodeList.data(), &NodeList::nodeActivated, this, &ATPGetApp::nodeActivated); + // connect(nodeList.data(), &NodeList::uuidChanged, getMyAvatar(), &MyAvatar::setSessionUUID); + // connect(nodeList.data(), &NodeList::uuidChanged, this, &ATPGetApp::setSessionUUID); + connect(nodeList.data(), &NodeList::packetVersionMismatch, this, &ATPGetApp::notifyPacketVersionMismatch); + + nodeList->addSetOfNodeTypesToNodeInterestSet(NodeSet() << NodeType::AudioMixer << NodeType::AvatarMixer + << NodeType::EntityServer << NodeType::AssetServer << NodeType::MessagesMixer); + + DependencyManager::get()->handleLookupString(domainServerAddress, false); + + auto assetClient = DependencyManager::set(); + assetClient->init(); + + QTimer* doTimer = new QTimer(this); + doTimer->setSingleShot(true); + connect(doTimer, &QTimer::timeout, this, &ATPGetApp::timedOut); + doTimer->start(4000); +} + +ATPGetApp::~ATPGetApp() { +} + + +void ATPGetApp::domainConnectionRefused(const QString& reasonMessage, int reasonCodeInt, const QString& extraInfo) { + qDebug() << "domainConnectionRefused"; +} + +void ATPGetApp::domainChanged(const QString& domainHostname) { + if (_verbose) { + qDebug() << "domainChanged"; + } +} + +void ATPGetApp::nodeAdded(SharedNodePointer node) { + if (_verbose) { + qDebug() << "node added: " << node->getType(); + } +} + +void ATPGetApp::nodeActivated(SharedNodePointer node) { + if (node->getType() == NodeType::AssetServer) { + lookup(); + } +} + +void ATPGetApp::nodeKilled(SharedNodePointer node) { + qDebug() << "nodeKilled"; +} + +void ATPGetApp::timedOut() { + finish(1); +} + +void ATPGetApp::notifyPacketVersionMismatch() { + if (_verbose) { + qDebug() << "packet version mismatch"; + } + finish(1); +} + +void ATPGetApp::lookup() { + + auto path = _url.path(); + qDebug() << "path is " << path; + + auto request = DependencyManager::get()->createGetMappingRequest(path); + QObject::connect(request, &GetMappingRequest::finished, this, [=](GetMappingRequest* request) mutable { + auto result = request->getError(); + if (result == GetMappingRequest::NotFound) { + qDebug() << "not found"; + } else if (result == GetMappingRequest::NoError) { + qDebug() << "found, hash is " << request->getHash(); + download(request->getHash()); + } else { + qDebug() << "error -- " << request->getError() << " -- " << request->getErrorString(); + } + request->deleteLater(); + }); + request->start(); +} + +void ATPGetApp::download(AssetHash hash) { + auto assetClient = DependencyManager::get(); + auto assetRequest = new AssetRequest(hash); + + connect(assetRequest, &AssetRequest::finished, this, [this](AssetRequest* request) mutable { + Q_ASSERT(request->getState() == AssetRequest::Finished); + + if (request->getError() == AssetRequest::Error::NoError) { + QString data = QString::fromUtf8(request->getData()); + if (_localOutputFile == "") { + QTextStream cout(stdout); + cout << data; + } else { + QFile outputHandle(_localOutputFile); + if (outputHandle.open(QIODevice::ReadWrite)) { + QTextStream stream( &outputHandle ); + stream << data; + } else { + qDebug() << "couldn't open output file:" << _localOutputFile; + } + } + } + + request->deleteLater(); + finish(0); + }); + + assetRequest->start(); +} + +void ATPGetApp::finish(int exitCode) { + auto nodeList = DependencyManager::get(); + + // send the domain a disconnect packet, force stoppage of domain-server check-ins + nodeList->getDomainHandler().disconnect(); + nodeList->setIsShuttingDown(true); + + // tell the packet receiver we're shutting down, so it can drop packets + nodeList->getPacketReceiver().setShouldDropPackets(true); + + QThread* nodeThread = DependencyManager::get()->thread(); + // remove the NodeList from the DependencyManager + DependencyManager::destroy(); + // ask the node thread to quit and wait until it is done + nodeThread->quit(); + nodeThread->wait(); + + QCoreApplication::exit(exitCode); +} diff --git a/tools/atp-get/src/ATPGetApp.h b/tools/atp-get/src/ATPGetApp.h new file mode 100644 index 0000000000..5507d2aa62 --- /dev/null +++ b/tools/atp-get/src/ATPGetApp.h @@ -0,0 +1,52 @@ +// +// ATPGetApp.h +// tools/atp-get/src +// +// Created by Seth Alves on 2017-3-15 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + + +#ifndef hifi_ATPGetApp_h +#define hifi_ATPGetApp_h + +#include +#include +#include +#include +#include +#include +#include +#include + + +class ATPGetApp : public QCoreApplication { + Q_OBJECT +public: + ATPGetApp(int argc, char* argv[]); + ~ATPGetApp(); + +private slots: + void domainConnectionRefused(const QString& reasonMessage, int reasonCodeInt, const QString& extraInfo); + void domainChanged(const QString& domainHostname); + void nodeAdded(SharedNodePointer node); + void nodeActivated(SharedNodePointer node); + void nodeKilled(SharedNodePointer node); + void notifyPacketVersionMismatch(); + +private: + NodeList* _nodeList; + void timedOut(); + void lookup(); + void download(AssetHash hash); + void finish(int exitCode); + bool _verbose; + + QUrl _url; + QString _localOutputFile; +}; + +#endif // hifi_ATPGetApp_h diff --git a/tools/atp-get/src/main.cpp b/tools/atp-get/src/main.cpp new file mode 100644 index 0000000000..bddf30c666 --- /dev/null +++ b/tools/atp-get/src/main.cpp @@ -0,0 +1,31 @@ +// +// main.cpp +// tools/atp-get/src +// +// Created by Seth Alves on 2017-3-15 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +#include +#include +#include +#include + +#include + +#include "ATPGetApp.h" + +using namespace std; + +int main(int argc, char * argv[]) { + QCoreApplication::setApplicationName(BuildInfo::AC_CLIENT_SERVER_NAME); + QCoreApplication::setOrganizationName(BuildInfo::MODIFIED_ORGANIZATION); + QCoreApplication::setOrganizationDomain(BuildInfo::ORGANIZATION_DOMAIN); + QCoreApplication::setApplicationVersion(BuildInfo::VERSION); + + ATPGetApp app(argc, argv); + + return app.exec(); +} diff --git a/unpublishedScripts/marketplace/boppo/boppoClownEntity.js b/unpublishedScripts/marketplace/boppo/boppoClownEntity.js new file mode 100644 index 0000000000..36f2bf5ab0 --- /dev/null +++ b/unpublishedScripts/marketplace/boppo/boppoClownEntity.js @@ -0,0 +1,80 @@ +// +// boppoClownEntity.js +// +// Created by Thijs Wenker on 3/15/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +/* globals LookAtTarget */ + +(function() { + var SFX_PREFIX = 'https://hifi-content.s3-us-west-1.amazonaws.com/caitlyn/production/elBoppo/sfx/'; + var CHANNEL_PREFIX = 'io.highfidelity.boppo_server_'; + var PUNCH_SOUNDS = [ + 'punch_1.wav', + 'punch_2.wav' + ]; + var PUNCH_COOLDOWN = 300; + + Script.include('lookAtEntity.js'); + + var createBoppoClownEntity = function() { + var _this, + _entityID, + _boppoUserData, + _lookAtTarget, + _punchSounds = [], + _lastPlayedPunch = {}; + + var getOwnBoppoUserData = function() { + try { + return JSON.parse(Entities.getEntityProperties(_entityID, ['userData']).userData).Boppo; + } catch (e) { + // e + } + return {}; + }; + + var BoppoClownEntity = function () { + _this = this; + PUNCH_SOUNDS.forEach(function(punch) { + _punchSounds.push(SoundCache.getSound(SFX_PREFIX + punch)); + }); + }; + + BoppoClownEntity.prototype = { + preload: function(entityID) { + _entityID = entityID; + _boppoUserData = getOwnBoppoUserData(); + _lookAtTarget = new LookAtTarget(_entityID); + }, + collisionWithEntity: function(boppoEntity, collidingEntity, collisionInfo) { + if (collisionInfo.type === 0 && + Entities.getEntityProperties(collidingEntity, ['name']).name.indexOf('Boxing Glove ') === 0) { + + if (_lastPlayedPunch[collidingEntity] === undefined || + Date.now() - _lastPlayedPunch[collidingEntity] > PUNCH_COOLDOWN) { + + // If boxing glove detected here: + Messages.sendMessage(CHANNEL_PREFIX + _boppoUserData.gameParentID, 'hit'); + + _lookAtTarget.lookAtByAction(); + var randomPunchIndex = Math.floor(Math.random() * _punchSounds.length); + Audio.playSound(_punchSounds[randomPunchIndex], { + position: collisionInfo.contactPoint + }); + _lastPlayedPunch[collidingEntity] = Date.now(); + } + } + } + + }; + + return new BoppoClownEntity(); + }; + + return createBoppoClownEntity(); +}); diff --git a/unpublishedScripts/marketplace/boppo/boppoServer.js b/unpublishedScripts/marketplace/boppo/boppoServer.js new file mode 100644 index 0000000000..f03154573c --- /dev/null +++ b/unpublishedScripts/marketplace/boppo/boppoServer.js @@ -0,0 +1,303 @@ +// +// boppoServer.js +// +// Created by Thijs Wenker on 3/15/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +(function() { + var SFX_PREFIX = 'https://hifi-content.s3-us-west-1.amazonaws.com/caitlyn/production/elBoppo/sfx/'; + var CLOWN_LAUGHS = [ + 'clown_laugh_1.wav', + 'clown_laugh_2.wav', + 'clown_laugh_3.wav', + 'clown_laugh_4.wav' + ]; + var TICK_TOCK_SOUND = 'ticktock%20-%20tock.wav'; + var BOXING_RING_BELL_START = 'boxingRingBell.wav'; + var BOXING_RING_BELL_END = 'boxingRingBell-end.wav'; + var BOPPO_MUSIC = 'boppoMusic.wav'; + var CHANNEL_PREFIX = 'io.highfidelity.boppo_server_'; + var MESSAGE_HIT = 'hit'; + var MESSAGE_ENTER_ZONE = 'enter-zone'; + var MESSAGE_UNLOAD_FIX = 'unload-fix'; + + var DEFAULT_SOUND_VOLUME = 0.6; + + // don't set the search radius too high, it might remove boppo's from other nearby instances + var BOPPO_SEARCH_RADIUS = 4.0; + + var MILLISECONDS_PER_SECOND = 1000; + // Make sure the entities are loaded at startup (TODO: more solid fix) + var LOAD_TIMEOUT = 5000; + var SECONDS_PER_MINUTE = 60; + var DEFAULT_PLAYTIME = 30; // seconds + var BASE_TEN = 10; + var TICK_TOCK_FROM = 3; // seconds + var COOLDOWN_TIME_MS = MILLISECONDS_PER_SECOND * 3; + + var createBoppoServer = function() { + var _this, + _isInitialized = false, + _clownLaughs = [], + _musicInjector, + _music, + _laughingInjector, + _tickTockSound, + _boxingBellRingStart, + _boxingBellRingEnd, + _entityID, + _boppoClownID, + _channel, + _boppoEntities, + _isGameRunning, + _updateInterval, + _timeLeft, + _hits, + _coolDown; + + var getOwnBoppoUserData = function() { + try { + return JSON.parse(Entities.getEntityProperties(_entityID, ['userData']).userData).Boppo; + } catch (e) { + // e + } + return {}; + }; + + var updateBoppoEntities = function() { + Entities.getChildrenIDs(_entityID).forEach(function(entityID) { + try { + var userData = JSON.parse(Entities.getEntityProperties(entityID, ['userData']).userData); + if (userData.Boppo.type !== undefined) { + _boppoEntities[userData.Boppo.type] = entityID; + } + } catch (e) { + // e + } + }); + }; + + var clearUntrackedBoppos = function() { + var position = Entities.getEntityProperties(_entityID, ['position']).position; + Entities.findEntities(position, BOPPO_SEARCH_RADIUS).forEach(function(entityID) { + try { + if (JSON.parse(Entities.getEntityProperties(entityID, ['userData']).userData).Boppo.type === 'boppo') { + Entities.deleteEntity(entityID); + } + } catch (e) { + // e + } + }); + }; + + var updateTimerDisplay = function() { + if (_boppoEntities['timer']) { + var secondsString = _timeLeft % SECONDS_PER_MINUTE; + if (secondsString < BASE_TEN) { + secondsString = '0' + secondsString; + } + var minutesString = Math.floor(_timeLeft / SECONDS_PER_MINUTE); + Entities.editEntity(_boppoEntities['timer'], { + text: minutesString + ':' + secondsString + }); + } + }; + + var updateScoreDisplay = function() { + if (_boppoEntities['score']) { + Entities.editEntity(_boppoEntities['score'], { + text: 'SCORE: ' + _hits + }); + } + }; + + var playSoundAtBoxingRing = function(sound, properties) { + var _properties = properties ? properties : {}; + if (_properties['volume'] === undefined) { + _properties['volume'] = DEFAULT_SOUND_VOLUME; + } + _properties['position'] = Entities.getEntityProperties(_entityID, ['position']).position; + // play beep + return Audio.playSound(sound, _properties); + }; + + var onUpdate = function() { + _timeLeft--; + + if (_timeLeft > 0 && _timeLeft <= TICK_TOCK_FROM) { + // play beep + playSoundAtBoxingRing(_tickTockSound); + } + if (_timeLeft === 0) { + if (_musicInjector !== undefined && _musicInjector.isPlaying()) { + _musicInjector.stop(); + _musicInjector = undefined; + } + playSoundAtBoxingRing(_boxingBellRingEnd); + _isGameRunning = false; + Script.clearInterval(_updateInterval); + _updateInterval = null; + _coolDown = true; + Script.setTimeout(function() { + _coolDown = false; + _this.resetBoppo(); + }, COOLDOWN_TIME_MS); + } + updateTimerDisplay(); + }; + + var onMessage = function(channel, message, sender) { + if (channel === _channel) { + if (message === MESSAGE_HIT) { + _this.hit(); + } else if (message === MESSAGE_ENTER_ZONE && !_isGameRunning) { + _this.resetBoppo(); + } else if (message === MESSAGE_UNLOAD_FIX && _isInitialized) { + _this.unload(); + } + } + }; + + var BoppoServer = function () { + _this = this; + _hits = 0; + _boppoClownID = null; + _coolDown = false; + CLOWN_LAUGHS.forEach(function(clownLaugh) { + _clownLaughs.push(SoundCache.getSound(SFX_PREFIX + clownLaugh)); + }); + _tickTockSound = SoundCache.getSound(SFX_PREFIX + TICK_TOCK_SOUND); + _boxingBellRingStart = SoundCache.getSound(SFX_PREFIX + BOXING_RING_BELL_START); + _boxingBellRingEnd = SoundCache.getSound(SFX_PREFIX + BOXING_RING_BELL_END); + _music = SoundCache.getSound(SFX_PREFIX + BOPPO_MUSIC); + _boppoEntities = {}; + }; + + BoppoServer.prototype = { + preload: function(entityID) { + _entityID = entityID; + _channel = CHANNEL_PREFIX + entityID; + + Messages.sendLocalMessage(_channel, MESSAGE_UNLOAD_FIX); + Script.setTimeout(function() { + clearUntrackedBoppos(); + updateBoppoEntities(); + Messages.subscribe(_channel); + Messages.messageReceived.connect(onMessage); + _this.resetBoppo(); + _isInitialized = true; + }, LOAD_TIMEOUT); + }, + resetBoppo: function() { + if (_boppoClownID !== null) { + print('deleting boppo: ' + _boppoClownID); + Entities.deleteEntity(_boppoClownID); + } + var boppoBaseProperties = Entities.getEntityProperties(_entityID, ['position', 'rotation']); + _boppoClownID = Entities.addEntity({ + angularDamping: 0.0, + collisionSoundURL: 'https://hifi-content.s3.amazonaws.com/caitlyn/production/elBoppo/51460__andre-rocha-nascimento__basket-ball-01-bounce.wav', + collisionsWillMove: true, + compoundShapeURL: 'https://hifi-content.s3.amazonaws.com/caitlyn/production/elBoppo/bopo_phys.obj', + damping: 1.0, + density: 10000, + dimensions: { + x: 1.2668079137802124, + y: 2.0568051338195801, + z: 0.88563752174377441 + }, + dynamic: 1.0, + friction: 1.0, + gravity: { + x: 0, + y: -25, + z: 0 + }, + modelURL: 'https://hifi-content.s3.amazonaws.com/caitlyn/production/elBoppo/elBoppo3_VR.fbx', + name: 'El Boppo the Punching Bag Clown', + registrationPoint: { + x: 0.5, + y: 0, + z: 0.3 + }, + restitution: 0.99, + rotation: boppoBaseProperties.rotation, + position: Vec3.sum(boppoBaseProperties.position, + Vec3.multiplyQbyV(boppoBaseProperties.rotation, { + x: 0.08666179329156876, + y: -1.5698202848434448, + z: 0.1847127377986908 + })), + script: Script.resolvePath('boppoClownEntity.js'), + shapeType: 'compound', + type: 'Model', + userData: JSON.stringify({ + lookAt: { + targetID: _boppoEntities['lookAtThis'], + disablePitch: true, + disableYaw: false, + disableRoll: true, + clearDisabledAxis: true, + rotationOffset: { x: 0.0, y: 180.0, z: 0.0} + }, + Boppo: { + type: 'boppo', + gameParentID: _entityID + }, + grabbableKey: { + grabbable: false + } + }) + }); + updateBoppoEntities(); + _boppoEntities['boppo'] = _boppoClownID; + }, + laugh: function() { + if (_laughingInjector !== undefined && _laughingInjector.isPlaying()) { + return; + } + var randomLaughIndex = Math.floor(Math.random() * _clownLaughs.length); + _laughingInjector = Audio.playSound(_clownLaughs[randomLaughIndex], { + position: Entities.getEntityProperties(_boppoClownID, ['position']).position + }); + }, + hit: function() { + if (_coolDown) { + return; + } + if (!_isGameRunning) { + var boxingRingBoppoData = getOwnBoppoUserData(); + _updateInterval = Script.setInterval(onUpdate, MILLISECONDS_PER_SECOND); + _timeLeft = boxingRingBoppoData.playTimeSeconds ? parseInt(boxingRingBoppoData.playTimeSeconds) : + DEFAULT_PLAYTIME; + _isGameRunning = true; + _hits = 0; + playSoundAtBoxingRing(_boxingBellRingStart); + _musicInjector = playSoundAtBoxingRing(_music, {loop: true, volume: 0.6}); + } + _hits++; + updateTimerDisplay(); + updateScoreDisplay(); + _this.laugh(); + }, + unload: function() { + print('unload called'); + if (_updateInterval) { + Script.clearInterval(_updateInterval); + } + Messages.messageReceived.disconnect(onMessage); + Messages.unsubscribe(_channel); + Entities.deleteEntity(_boppoClownID); + print('endOfUnload'); + } + }; + + return new BoppoServer(); + }; + + return createBoppoServer(); +}); diff --git a/unpublishedScripts/marketplace/boppo/clownGloveDispenser.js b/unpublishedScripts/marketplace/boppo/clownGloveDispenser.js new file mode 100644 index 0000000000..cd0a0c0614 --- /dev/null +++ b/unpublishedScripts/marketplace/boppo/clownGloveDispenser.js @@ -0,0 +1,154 @@ +// +// clownGloveDispenser.js +// +// Created by Thijs Wenker on 8/2/16. +// Copyright 2016 High Fidelity, Inc. +// +// Based on examples/winterSmashUp/targetPractice/shooterPlatform.js +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +(function() { + var _this = this; + + var CHANNEL_PREFIX = 'io.highfidelity.boppo_server_'; + + var leftBoxingGlove = undefined; + var rightBoxingGlove = undefined; + + var inZone = false; + + var wearGloves = function() { + leftBoxingGlove = Entities.addEntity({ + position: MyAvatar.position, + collisionsWillMove: true, + dimensions: { + x: 0.24890634417533875, + y: 0.28214839100837708, + z: 0.21127720177173615 + }, + dynamic: true, + gravity: { + x: 0, + y: -9.8, + z: 0 + }, + modelURL: "https://hifi-content.s3.amazonaws.com/caitlyn/production/elBoppo/LFT_glove_VR3.fbx", + name: "Boxing Glove - Left", + registrationPoint: { + x: 0.5, + y: 0, + z: 0.5 + }, + shapeType: "simple-hull", + type: "Model", + userData: JSON.stringify({ + grabbableKey: { + invertSolidWhileHeld: true + }, + wearable: { + joints: { + LeftHand: [ + {x: 0, y: 0.0, z: 0.02 }, + Quat.fromVec3Degrees({x: 0, y: 0, z: 0}) + ] + } + } + }) + }); + Messages.sendLocalMessage('Hifi-Hand-Grab', JSON.stringify({hand: 'left', entityID: leftBoxingGlove})); + // Allows teleporting while glove is wielded + Messages.sendLocalMessage('Hifi-Teleport-Ignore-Add', leftBoxingGlove); + + rightBoxingGlove = Entities.addEntity({ + position: MyAvatar.position, + collisionsWillMove: true, + dimensions: { + x: 0.24890634417533875, + y: 0.28214839100837708, + z: 0.21127720177173615 + }, + dynamic: true, + gravity: { + x: 0, + y: -9.8, + z: 0 + }, + modelURL: "https://hifi-content.s3.amazonaws.com/caitlyn/production/elBoppo/RT_glove_VR2.fbx", + name: "Boxing Glove - Right", + registrationPoint: { + x: 0.5, + y: 0, + z: 0.5 + }, + shapeType: "simple-hull", + type: "Model", + userData: JSON.stringify({ + grabbableKey: { + invertSolidWhileHeld: true + }, + wearable: { + joints: { + RightHand: [ + {x: 0, y: 0.0, z: 0.02 }, + Quat.fromVec3Degrees({x: 0, y: 0, z: 0}) + ] + } + } + }) + }); + Messages.sendLocalMessage('Hifi-Hand-Grab', JSON.stringify({hand: 'right', entityID: rightBoxingGlove})); + // Allows teleporting while glove is wielded + Messages.sendLocalMessage('Hifi-Teleport-Ignore-Add', rightBoxingGlove); + }; + + var cleanUpGloves = function() { + if (leftBoxingGlove !== undefined) { + Entities.deleteEntity(leftBoxingGlove); + leftBoxingGlove = undefined; + } + if (rightBoxingGlove !== undefined) { + Entities.deleteEntity(rightBoxingGlove); + rightBoxingGlove = undefined; + } + }; + + var wearGlovesIfHMD = function() { + // cleanup your old gloves if they're still there (unlikely) + cleanUpGloves(); + if (HMD.active) { + wearGloves(); + } + }; + + _this.preload = function(entityID) { + HMD.displayModeChanged.connect(function() { + if (inZone) { + wearGlovesIfHMD(); + } + }); + }; + + _this.unload = function() { + cleanUpGloves(); + }; + + _this.enterEntity = function(entityID) { + inZone = true; + print('entered boxing glove dispenser entity'); + wearGlovesIfHMD(); + + // Reset boppo if game is not running: + var parentID = Entities.getEntityProperties(entityID, ['parentID']).parentID; + Messages.sendMessage(CHANNEL_PREFIX + parentID, 'enter-zone'); + }; + + _this.leaveEntity = function(entityID) { + inZone = false; + cleanUpGloves(); + }; + + _this.unload = _this.leaveEntity; +}); diff --git a/unpublishedScripts/marketplace/boppo/createElBoppo.js b/unpublishedScripts/marketplace/boppo/createElBoppo.js new file mode 100644 index 0000000000..4df6a2acda --- /dev/null +++ b/unpublishedScripts/marketplace/boppo/createElBoppo.js @@ -0,0 +1,430 @@ +// +// createElBoppo.js +// +// Created by Thijs Wenker on 3/17/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +/* globals SCRIPT_IMPORT_PROPERTIES */ + +var MODELS_PATH = 'https://hifi-content.s3.amazonaws.com/DomainContent/Welcome%20Area/production/models/boxingRing/'; +var WANT_CLEANUP_ON_SCRIPT_ENDING = false; + +var getScriptPath = function(localPath) { + if (this.isCleanupAndSpawnScript) { + return 'https://hifi-content.s3.amazonaws.com/DomainContent/Welcome%20Area/Scripts/boppo/' + localPath; + } + return Script.resolvePath(localPath); +}; + +var getCreatePosition = function() { + // can either return position defined by resetScript or avatar position + if (this.isCleanupAndSpawnScript) { + return SCRIPT_IMPORT_PROPERTIES.rootPosition; + } + return Vec3.sum(MyAvatar.position, {x: 1, z: -2}); +}; + +var boxingRing = Entities.addEntity({ + dimensions: { + x: 4.0584001541137695, + y: 4.0418000221252441, + z: 3.0490000247955322 + }, + modelURL: MODELS_PATH + 'assembled/boppoBoxingRingAssembly.fbx', + name: 'Boxing Ring Assembly', + rotation: { + w: 0.9996337890625, + x: -1.52587890625e-05, + y: -0.026230275630950928, + z: -4.57763671875e-05 + }, + position: getCreatePosition(), + scriptTimestamp: 1489612158459, + serverScripts: getScriptPath('boppoServer.js'), + shapeType: 'static-mesh', + type: 'Model', + userData: JSON.stringify({ + Boppo: { + type: 'boxingring', + playTimeSeconds: 15 + } + }) +}); + +var boppoEntities = [ + { + dimensions: { + x: 0.36947935819625854, + y: 0.25536194443702698, + z: 0.059455446898937225 + }, + modelURL: MODELS_PATH + 'boxingGameSign/boppoSignFrame.fbx', + parentID: boxingRing, + localPosition: { + x: -1.0251024961471558, + y: 0.51661628484725952, + z: -1.1176263093948364 + }, + rotation: { + w: 0.996856689453125, + x: 0.013321161270141602, + y: 0.0024566650390625, + z: 0.078049898147583008 + }, + shapeType: 'box', + type: 'Model' + }, + { + dimensions: { + x: 0.33255371451377869, + y: 0.1812121719121933, + z: 0.0099999997764825821 + }, + lineHeight: 0.125, + name: 'Boxing Ring - High Score Board', + parentID: boxingRing, + localPosition: { + x: -1.0239436626434326, + y: 0.52212876081466675, + z: -1.0971509218215942 + }, + rotation: { + w: 0.9876401424407959, + x: 0.013046503067016602, + y: 0.0012359619140625, + z: 0.15605401992797852 + }, + text: '0:00', + textColor: { + blue: 0, + green: 0, + red: 255 + }, + type: 'Text', + userData: JSON.stringify({ + Boppo: { + type: 'timer' + } + }) + }, + { + dimensions: { + x: 0.50491130352020264, + y: 0.13274604082107544, + z: 0.0099999997764825821 + }, + lineHeight: 0.090000003576278687, + name: 'Boxing Ring - Score Board', + parentID: boxingRing, + localPosition: { + x: -0.77596306800842285, + y: 0.37797555327415466, + z: -1.0910623073577881 + }, + rotation: { + w: 0.9518122673034668, + x: 0.004237703513354063, + y: -0.0010041374480351806, + z: 0.30455198884010315 + }, + text: 'SCORE: 0', + textColor: { + blue: 0, + green: 0, + red: 255 + }, + type: 'Text', + userData: JSON.stringify({ + Boppo: { + type: 'score' + } + }) + }, + { + dimensions: { + x: 0.58153259754180908, + y: 0.1884911060333252, + z: 0.059455446898937225 + }, + modelURL: MODELS_PATH + 'boxingGameSign/boppoSignFrame.fbx', + parentID: boxingRing, + localPosition: { + x: -0.78200173377990723, + y: 0.35684797167778015, + z: -1.108180046081543 + }, + rotation: { + w: 0.97814905643463135, + x: 0.0040436983108520508, + y: -0.0005645751953125, + z: 0.20778214931488037 + }, + shapeType: 'box', + type: 'Model' + }, + { + dimensions: { + x: 4.1867804527282715, + y: 3.5065803527832031, + z: 5.6845207214355469 + }, + name: 'El Boppo the Clown boxing area & glove maker', + parentID: boxingRing, + localPosition: { + x: -0.012308252975344658, + y: 0.054641719907522202, + z: 0.98782551288604736 + }, + rotation: { + w: 1, + x: -1.52587890625e-05, + y: -1.52587890625e-05, + z: -1.52587890625e-05 + }, + script: getScriptPath('clownGloveDispenser.js'), + shapeType: 'box', + type: 'Zone', + visible: false + }, + { + color: { + blue: 255, + green: 5, + red: 255 + }, + dimensions: { + x: 0.20000000298023224, + y: 0.20000000298023224, + z: 0.20000000298023224 + }, + name: 'LookAtBox', + parentID: boxingRing, + localPosition: { + x: -0.1772226095199585, + y: -1.7072629928588867, + z: 1.3122396469116211 + }, + rotation: { + w: 0.999969482421875, + x: 1.52587890625e-05, + y: 0.0043793916702270508, + z: 1.52587890625e-05 + }, + shape: 'Cube', + type: 'Box', + userData: JSON.stringify({ + Boppo: { + type: 'lookAtThis' + } + }) + }, + { + color: { + blue: 209, + green: 157, + red: 209 + }, + dimensions: { + x: 1.6913000345230103, + y: 1.2124500274658203, + z: 0.2572999894618988 + }, + name: 'boppoBackBoard', + parentID: boxingRing, + localPosition: { + x: -0.19500596821308136, + y: -1.1044719219207764, + z: -0.55993378162384033 + }, + rotation: { + w: 0.9807126522064209, + x: -0.19511711597442627, + y: 0.0085297822952270508, + z: 0.0016937255859375 + }, + shape: 'Cube', + type: 'Box', + visible: false + }, + { + color: { + blue: 0, + green: 0, + red: 255 + }, + dimensions: { + x: 1.8155574798583984, + y: 0.92306196689605713, + z: 0.51203572750091553 + }, + name: 'boppoBackBoard', + parentID: boxingRing, + localPosition: { + x: -0.11036647111177444, + y: -0.051978692412376404, + z: -0.79054081439971924 + }, + rotation: { + w: 0.9807431697845459, + x: 0.19505608081817627, + y: 0.0085602998733520508, + z: -0.0017547607421875 + }, + shape: 'Cube', + type: 'Box', + visible: false + }, + { + color: { + blue: 209, + green: 157, + red: 209 + }, + dimensions: { + x: 1.9941408634185791, + y: 1.2124500274658203, + z: 0.2572999894618988 + }, + name: 'boppoBackBoard', + localPosition: { + x: 0.69560068845748901, + y: -1.3840068578720093, + z: 0.059689953923225403 + }, + rotation: { + w: 0.73458456993103027, + x: -0.24113833904266357, + y: -0.56545358896255493, + z: -0.28734266757965088 + }, + shape: 'Cube', + type: 'Box', + visible: false + }, + { + color: { + blue: 82, + green: 82, + red: 82 + }, + dimensions: { + x: 8.3777303695678711, + y: 0.87573593854904175, + z: 7.9759469032287598 + }, + parentID: boxingRing, + localPosition: { + x: -0.38302639126777649, + y: -2.121284008026123, + z: 0.3699878454208374 + }, + rotation: { + w: 0.70711839199066162, + x: -7.62939453125e-05, + y: 0.70705735683441162, + z: -1.52587890625e-05 + }, + shape: 'Triangle', + type: 'Shape' + }, + { + color: { + blue: 209, + green: 157, + red: 209 + }, + dimensions: { + x: 1.889795184135437, + y: 0.86068248748779297, + z: 0.2572999894618988 + }, + name: 'boppoBackBoard', + parentID: boxingRing, + localPosition: { + x: -0.95167744159698486, + y: -1.4756947755813599, + z: -0.042313352227210999 + }, + rotation: { + w: 0.74004733562469482, + x: -0.24461740255355835, + y: 0.56044864654541016, + z: 0.27998781204223633 + }, + shape: 'Cube', + type: 'Box', + visible: false + }, + { + color: { + blue: 0, + green: 0, + red: 255 + }, + dimensions: { + x: 4.0720257759094238, + y: 0.50657749176025391, + z: 1.4769613742828369 + }, + name: 'boppo-stepsRamp', + parentID: boxingRing, + localPosition: { + x: -0.002939039608463645, + y: -1.9770187139511108, + z: 2.2165381908416748 + }, + rotation: { + w: 0.99252307415008545, + x: 0.12184333801269531, + y: -1.52587890625e-05, + z: -1.52587890625e-05 + }, + shape: 'Cube', + type: 'Box', + visible: false + }, + { + color: { + blue: 150, + green: 150, + red: 150 + }, + cutoff: 90, + dimensions: { + x: 5.2220535278320312, + y: 5.2220535278320312, + z: 5.2220535278320312 + }, + falloffRadius: 2, + intensity: 15, + name: 'boxing ring light', + parentID: boxingRing, + localPosition: { + x: -1.4094564914703369, + y: -0.36021926999092102, + z: 0.81797939538955688 + }, + rotation: { + w: 0.9807431697845459, + x: 1.52587890625e-05, + y: -0.19520866870880127, + z: -1.52587890625e-05 + }, + type: 'Light' + } +]; + +boppoEntities.forEach(function(entityProperties) { + entityProperties['parentID'] = boxingRing; + Entities.addEntity(entityProperties); +}); + +if (WANT_CLEANUP_ON_SCRIPT_ENDING) { + Script.scriptEnding.connect(function() { + Entities.deleteEntity(boxingRing); + }); +} diff --git a/unpublishedScripts/marketplace/boppo/lookAtEntity.js b/unpublishedScripts/marketplace/boppo/lookAtEntity.js new file mode 100644 index 0000000000..ba072814f2 --- /dev/null +++ b/unpublishedScripts/marketplace/boppo/lookAtEntity.js @@ -0,0 +1,98 @@ +// +// lookAtTarget.js +// +// Created by Thijs Wenker on 3/15/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +/* globals LookAtTarget:true */ + +LookAtTarget = function(sourceEntityID) { + /* private variables */ + var _this, + _options, + _sourceEntityID, + _sourceEntityProperties, + REQUIRED_PROPERTIES = ['position', 'rotation', 'userData'], + LOOK_AT_TAG = 'lookAtTarget'; + + LookAtTarget = function(sourceEntityID) { + _this = this; + _sourceEntityID = sourceEntityID; + _this.updateOptions(); + }; + + /* private functions */ + var updateEntitySourceProperties = function() { + _sourceEntityProperties = Entities.getEntityProperties(_sourceEntityID, REQUIRED_PROPERTIES); + }; + + var getUpdatedActionProperties = function() { + return { + targetRotation: _this.getLookAtRotation(), + angularTimeScale: 0.1, + ttl: 10 + }; + }; + + var getNewActionProperties = function() { + var newActionProperties = getUpdatedActionProperties(); + newActionProperties.tag = LOOK_AT_TAG; + return newActionProperties; + }; + + LookAtTarget.prototype = { + /* public functions */ + updateOptions: function() { + updateEntitySourceProperties(); + _options = JSON.parse(_sourceEntityProperties.userData).lookAt; + }, + getTargetPosition: function() { + return Entities.getEntityProperties(_options.targetID).position; + }, + getLookAtRotation: function() { + _this.updateOptions(); + + var newRotation = Quat.lookAt(_sourceEntityProperties.position, _this.getTargetPosition(), Vec3.UP); + if (_options.rotationOffset !== undefined) { + newRotation = Quat.multiply(newRotation, Quat.fromVec3Degrees(_options.rotationOffset)); + } + if (_options.disablePitch || _options.disableYaw || _options.disablePitch) { + var disabledAxis = _options.clearDisabledAxis ? Vec3.ZERO : + Quat.safeEulerAngles(_sourceEntityProperties.rotation); + var newEulers = Quat.safeEulerAngles(newRotation); + newRotation = Quat.fromVec3Degrees({ + x: _options.disablePitch ? disabledAxis.x : newEulers.x, + y: _options.disableYaw ? disabledAxis.y : newEulers.y, + z: _options.disableRoll ? disabledAxis.z : newEulers.z + }); + } + return newRotation; + }, + lookAtDirectly: function() { + Entities.editEntity(_sourceEntityID, {rotation: _this.getLookAtRotation()}); + }, + lookAtByAction: function() { + var actionIDs = Entities.getActionIDs(_sourceEntityID); + var actionFound = false; + actionIDs.forEach(function(actionID) { + if (actionFound) { + return; + } + var actionArguments = Entities.getActionArguments(_sourceEntityID, actionID); + if (actionArguments.tag === LOOK_AT_TAG) { + actionFound = true; + Entities.updateAction(_sourceEntityID, actionID, getUpdatedActionProperties()); + } + }); + if (!actionFound) { + Entities.addAction('spring', _sourceEntityID, getNewActionProperties()); + } + } + }; + + return new LookAtTarget(sourceEntityID); +}; From e5741869e545e3f0a55b0c355136bf57e64580df Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Fri, 24 Mar 2017 12:22:11 -0700 Subject: [PATCH 050/118] Checkpoint --- scripts/system/selectAudioDevice.js | 152 ++++++++++++++++------------ 1 file changed, 90 insertions(+), 62 deletions(-) diff --git a/scripts/system/selectAudioDevice.js b/scripts/system/selectAudioDevice.js index 04215c966d..5d1f44c1f6 100644 --- a/scripts/system/selectAudioDevice.js +++ b/scripts/system/selectAudioDevice.js @@ -61,28 +61,29 @@ var switchedAudioInputToHMD = false; var switchedAudioOutputToHMD = false; var previousSelectedInputAudioDevice = ""; var previousSelectedOutputAudioDevice = ""; +var menuConnected = false; /**************************************** BEGIN FUNCTION DEFINITIONS ****************************************/ function setupAudioMenus() { + if (menuConnected) { + Menu.menuItemEvent.disconnect(menuItemEvent); + menuConnected = false; + } removeAudioMenus(); /* Setup audio input devices */ Menu.addSeparator("Audio", "Input Audio Device"); - var inputDeviceSetting = Settings.getValue(INPUT_DEVICE_SETTING); var inputDevices = AudioDevice.getInputDevices(); + print("selectAudioDevice: Audio input devices: " + inputDevices); var selectedInputDevice = AudioDevice.getInputDevice(); + var inputDeviceSetting = Settings.getValue(INPUT_DEVICE_SETTING); if (inputDevices.indexOf(inputDeviceSetting) != -1 && selectedInputDevice != inputDeviceSetting) { - print ("Audio input device SETTING does not match Input AudioDevice. Attempting to change Input AudioDevice...") - if (AudioDevice.setInputDevice(inputDeviceSetting)) { - selectedInputDevice = inputDeviceSetting; - } else { - print("Error setting audio input device!") - } + print("selectAudioDevice: Input Setting & Device mismatch! Input SETTING:", inputDeviceSetting, "Input DEVICE IN USE:", selectedInputDevice); + switchAudioDevice(true, inputDeviceSetting); } - print("Audio input devices: " + inputDevices); - for(var i = 0; i < inputDevices.length; i++) { + for (var i = 0; i < inputDevices.length; i++) { var thisDeviceSelected = (inputDevices[i] == selectedInputDevice); var menuItem = "Use " + inputDevices[i] + " for Input"; Menu.addMenuItem({ @@ -99,18 +100,14 @@ function setupAudioMenus() { /* Setup audio output devices */ Menu.addSeparator("Audio", "Output Audio Device"); - var outputDeviceSetting = Settings.getValue(OUTPUT_DEVICE_SETTING); var outputDevices = AudioDevice.getOutputDevices(); + print("selectAudioDevice: Audio output devices: " + outputDevices); var selectedOutputDevice = AudioDevice.getOutputDevice(); + var outputDeviceSetting = Settings.getValue(OUTPUT_DEVICE_SETTING); if (outputDevices.indexOf(outputDeviceSetting) != -1 && selectedOutputDevice != outputDeviceSetting) { - print("Audio output device SETTING does not match Output AudioDevice. Attempting to change Output AudioDevice...") - if (AudioDevice.setOutputDevice(outputDeviceSetting)) { - selectedOutputDevice = outputDeviceSetting; - } else { - print("Error setting audio output device!") - } + print("selectAudioDevice: Output Setting & Device mismatch! Output SETTING:", outputDeviceSetting, "Output DEVICE IN USE:", selectedOutputDevice); + switchAudioDevice(false, outputDeviceSetting); } - print("Audio output devices: " + outputDevices); for (var i = 0; i < outputDevices.length; i++) { var thisDeviceSelected = (outputDevices[i] == selectedOutputDevice); var menuItem = "Use " + outputDevices[i] + " for Output"; @@ -125,6 +122,10 @@ function setupAudioMenus() { selectedOutputMenu = menuItem; } } + if (!menuConnected) { + Menu.menuItemEvent.connect(menuItemEvent); + menuConnected = true; + } } function removeAudioMenus() { @@ -141,51 +142,77 @@ function removeAudioMenus() { } function onDevicechanged() { - print("System audio device changed. Removing and replacing Audio Menus..."); + print("selectAudioDevice: System audio device changed. Removing and replacing Audio Menus..."); setupAudioMenus(); } function menuItemEvent(menuItem) { + if (menuConnected) { + Menu.menuItemEvent.disconnect(menuItemEvent); + menuConnected = false; + } if (menuItem.startsWith("Use ")) { if (menuItem.endsWith(" for Input")) { var selectedDevice = menuItem.trimStartsWith("Use ").trimEndsWith(" for Input"); - print("User selected a new Audio Input Device: " + selectedDevice); - Menu.menuItemEvent.disconnect(menuItemEvent); - Menu.setIsOptionChecked(selectedInputMenu, false); - selectedInputMenu = menuItem; - Menu.setIsOptionChecked(selectedInputMenu, true); - if (AudioDevice.setInputDevice(selectedDevice)) { - Settings.setValue(INPUT_DEVICE_SETTING, selectedDevice); - } else { - print("Error setting audio input device!") - } - Menu.menuItemEvent.connect(menuItemEvent); + print("selectAudioDevice: User selected a new Audio Input Device: " + selectedDevice); + switchAudioDevice(true, selectedDevice); } else if (menuItem.endsWith(" for Output")) { var selectedDevice = menuItem.trimStartsWith("Use ").trimEndsWith(" for Output"); - print("User selected a new Audio Output Device: " + selectedDevice); - Menu.menuItemEvent.disconnect(menuItemEvent); - Menu.setIsOptionChecked(selectedOutputMenu, false); - selectedOutputMenu = menuItem; - Menu.setIsOptionChecked(selectedOutputMenu, true); - if (AudioDevice.setOutputDevice(selectedDevice)) { - Settings.setValue(OUTPUT_DEVICE_SETTING, selectedDevice); - } else { - print("Error setting audio output device!") - } - Menu.menuItemEvent.connect(menuItemEvent); + print("selectAudioDevice: User selected a new Audio Output Device: " + selectedDevice); + switchAudioDevice(false, selectedDevice); + } else { + print("selectAudioDevice: Invalid Audio menuItem! Doesn't end with 'for Input' or 'for Output'") } + } else { + print("selectAudioDevice: Invalid Audio menuItem! Doesn't start with 'Use '") + } + if (!menuConnected) { + Menu.menuItemEvent.connect(menuItemEvent); + menuConnected = true; + } +} + +function switchAudioDevice(isInput, device) { + if (menuConnected) { + Menu.menuItemEvent.disconnect(menuItemEvent); + menuConnected = false; + } + if (isInput) { + print("selectAudioDevice: Switching audio INPUT device to:", device); + if (AudioDevice.setInputDevice(device)) { + Menu.setIsOptionChecked(selectedInputMenu, false); + selectedInputMenu = "Use " + device + " for Input"; + Menu.setIsOptionChecked(selectedInputMenu, true); + Settings.setValue(INPUT_DEVICE_SETTING, device); + } else { + print("selectAudioDevice: Error setting audio input device!") + } + } else { + print("selectAudioDevice: Switching audio OUTPUT device to:", device); + if (AudioDevice.setOutputDevice(device)) { + Menu.setIsOptionChecked(selectedOutputMenu, false); + selectedOutputMenu = "Use " + device + " for Output"; + Menu.setIsOptionChecked(selectedOutputMenu, true); + Settings.setValue(OUTPUT_DEVICE_SETTING, device); + } else { + print("selectAudioDevice: Error setting audio output device!") + } + } + if (!menuConnected) { + Menu.menuItemEvent.connect(menuItemEvent); + menuConnected = true; } } function restoreAudio() { if (switchedAudioInputToHMD) { - print("Switching back from HMD preferred audio input to: " + previousSelectedInputAudioDevice); - menuItemEvent("Use " + previousSelectedInputAudioDevice + " for Input"); + print("selectAudioDevice: Switching back from HMD preferred audio input to: " + previousSelectedInputAudioDevice); + switchAudioDevice(true, previousSelectedInputAudioDevice); switchedAudioInputToHMD = false; } if (switchedAudioOutputToHMD) { - print("Switching back from HMD preferred audio output to: " + previousSelectedOutputAudioDevice); - menuItemEvent("Use " + previousSelectedOutputAudioDevice + " for Output"); + print("selectAudioDevice: Switching back from HMD preferred audio output to: " + previousSelectedOutputAudioDevice); + switchAudioDevice(false, previousSelectedOutputAudioDevice); switchedAudioOutputToHMD = false; } } @@ -193,39 +220,38 @@ function restoreAudio() { function checkHMDAudio() { // HMD Active state is changing; handle switching if (HMD.active != wasHmdActive) { - print("HMD Active state changed!"); + print("selectAudioDevice: HMD Active state changed!"); // We're putting the HMD on; switch to those devices if (HMD.active) { - print("HMD is now Active."); + print("selectAudioDevice: HMD is now Active."); var hmdPreferredAudioInput = HMD.preferredAudioInput(); var hmdPreferredAudioOutput = HMD.preferredAudioOutput(); - print("hmdPreferredAudioInput: " + hmdPreferredAudioInput); - print("hmdPreferredAudioOutput: " + hmdPreferredAudioOutput); - + print("selectAudioDevice: hmdPreferredAudioInput: " + hmdPreferredAudioInput); + print("selectAudioDevice: hmdPreferredAudioOutput: " + hmdPreferredAudioOutput); if (hmdPreferredAudioInput !== "") { - print("HMD has preferred audio input device."); + print("selectAudioDevice: HMD has preferred audio input device."); previousSelectedInputAudioDevice = Settings.getValue(INPUT_DEVICE_SETTING); - print("previousSelectedInputAudioDevice: " + previousSelectedInputAudioDevice); + print("selectAudioDevice: previousSelectedInputAudioDevice: " + previousSelectedInputAudioDevice); if (hmdPreferredAudioInput != previousSelectedInputAudioDevice) { - print("Switching Audio Input device to HMD preferred device: " + hmdPreferredAudioInput); + print("selectAudioDevice: Switching Audio Input device to HMD preferred device: " + hmdPreferredAudioInput); switchedAudioInputToHMD = true; - menuItemEvent("Use " + hmdPreferredAudioInput + " for Input"); + switchAudioDevice(true, hmdPreferredAudioInput); } } if (hmdPreferredAudioOutput !== "") { - print("HMD has preferred audio output device."); + print("selectAudioDevice: HMD has preferred audio output device."); previousSelectedOutputAudioDevice = Settings.getValue(OUTPUT_DEVICE_SETTING); - print("previousSelectedOutputAudioDevice: " + previousSelectedOutputAudioDevice); + print("selectAudioDevice: previousSelectedOutputAudioDevice: " + previousSelectedOutputAudioDevice); if (hmdPreferredAudioOutput != previousSelectedOutputAudioDevice) { - print("Switching Audio Output device to HMD preferred device: " + hmdPreferredAudioOutput); + print("selectAudioDevice: Switching Audio Output device to HMD preferred device: " + hmdPreferredAudioOutput); switchedAudioOutputToHMD = true; - menuItemEvent("Use " + hmdPreferredAudioOutput + " for Output"); + switchAudioDevice(false, hmdPreferredAudioOutput); } } } else { - print("HMD no longer active. Restoring audio I/O devices..."); + print("selectAudioDevice: HMD no longer active. Restoring audio I/O devices..."); restoreAudio(); } } @@ -240,15 +266,17 @@ function checkHMDAudio() { ****************************************/ // Have a small delay before the menus get setup so the audio devices can switch to the last selected ones Script.setTimeout(function () { - print("Connecting deviceChanged() and displayModeChanged()"); + print("selectAudioDevice: Connecting deviceChanged() and displayModeChanged()"); AudioDevice.deviceChanged.connect(onDevicechanged); HMD.displayModeChanged.connect(checkHMDAudio); - print("Setting up Audio > Devices menu for the first time"); + print("selectAudioDevice: Setting up Audio > Devices menu for the first time"); setupAudioMenus(); -}, 2000); + checkHMDAudio(); +}, 5000); -print("Connecting menuItemEvent() and scriptEnding()"); +print("selectAudioDevice: Connecting menuItemEvent() and scriptEnding()"); Menu.menuItemEvent.connect(menuItemEvent); +menuConnected = true; Script.scriptEnding.connect(function () { restoreAudio(); removeAudioMenus(); From 31ae326880b18ff9754989557c4b1990c7ff9914 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Fri, 24 Mar 2017 13:05:07 -0700 Subject: [PATCH 051/118] Pretty sure about this now --- scripts/system/selectAudioDevice.js | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/scripts/system/selectAudioDevice.js b/scripts/system/selectAudioDevice.js index 5d1f44c1f6..cc0f25b005 100644 --- a/scripts/system/selectAudioDevice.js +++ b/scripts/system/selectAudioDevice.js @@ -95,6 +95,7 @@ function setupAudioMenus() { audioDevicesList.push(menuItem); if (thisDeviceSelected) { selectedInputMenu = menuItem; + print("selectAudioDevice: selectedInputMenu: " + selectedInputMenu); } } @@ -120,6 +121,7 @@ function setupAudioMenus() { audioDevicesList.push(menuItem); if (thisDeviceSelected) { selectedOutputMenu = menuItem; + print("selectAudioDevice: selectedOutputMenu: " + selectedOutputMenu); } } if (!menuConnected) { @@ -180,9 +182,6 @@ function switchAudioDevice(isInput, device) { if (isInput) { print("selectAudioDevice: Switching audio INPUT device to:", device); if (AudioDevice.setInputDevice(device)) { - Menu.setIsOptionChecked(selectedInputMenu, false); - selectedInputMenu = "Use " + device + " for Input"; - Menu.setIsOptionChecked(selectedInputMenu, true); Settings.setValue(INPUT_DEVICE_SETTING, device); } else { print("selectAudioDevice: Error setting audio input device!") @@ -190,14 +189,12 @@ function switchAudioDevice(isInput, device) { } else { print("selectAudioDevice: Switching audio OUTPUT device to:", device); if (AudioDevice.setOutputDevice(device)) { - Menu.setIsOptionChecked(selectedOutputMenu, false); - selectedOutputMenu = "Use " + device + " for Output"; - Menu.setIsOptionChecked(selectedOutputMenu, true); Settings.setValue(OUTPUT_DEVICE_SETTING, device); } else { print("selectAudioDevice: Error setting audio output device!") } } + setupAudioMenus(); if (!menuConnected) { Menu.menuItemEvent.connect(menuItemEvent); menuConnected = true; @@ -218,6 +215,10 @@ function restoreAudio() { } function checkHMDAudio() { + if (menuConnected) { + Menu.menuItemEvent.disconnect(menuItemEvent); + menuConnected = false; + } // HMD Active state is changing; handle switching if (HMD.active != wasHmdActive) { print("selectAudioDevice: HMD Active state changed!"); @@ -256,6 +257,10 @@ function checkHMDAudio() { } } wasHmdActive = HMD.active; + if (!menuConnected) { + Menu.menuItemEvent.connect(menuItemEvent); + menuConnected = true; + } } /**************************************** END FUNCTION DEFINITIONS @@ -269,10 +274,11 @@ Script.setTimeout(function () { print("selectAudioDevice: Connecting deviceChanged() and displayModeChanged()"); AudioDevice.deviceChanged.connect(onDevicechanged); HMD.displayModeChanged.connect(checkHMDAudio); + print ("selectAudioDevice: Checking HMD audio status...") + checkHMDAudio(); print("selectAudioDevice: Setting up Audio > Devices menu for the first time"); setupAudioMenus(); - checkHMDAudio(); -}, 5000); +}, 3000); print("selectAudioDevice: Connecting menuItemEvent() and scriptEnding()"); Menu.menuItemEvent.connect(menuItemEvent); From e88895935bc2c4aeeaa10ffd7263173ef90e4e89 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Fri, 24 Mar 2017 17:56:48 -0700 Subject: [PATCH 052/118] Revert "Actually merge from master" This reverts commit 056d6fbe4f384386f3c35d0f142a1cf9a6dd532a. --- BUILD_WIN.md | 153 +-- assignment-client/src/Agent.cpp | 33 +- assignment-client/src/Agent.h | 8 - assignment-client/src/assets/AssetServer.cpp | 4 +- assignment-client/src/octree/OctreeServer.cpp | 9 +- .../src/scripts/EntityScriptServer.cpp | 30 +- .../PackageLibrariesForDeployment.cmake | 28 +- .../SymlinkOrCopyDirectoryBesideTarget.cmake | 6 +- domain-server/src/DomainServer.cpp | 4 +- .../src/DomainServerSettingsManager.cpp | 37 +- interface/CMakeLists.txt | 10 +- interface/resources/controllers/standard.json | 44 +- interface/resources/html/img/devices.png | Bin 0 -> 7492 bytes interface/resources/html/img/models.png | Bin 0 -> 8664 bytes interface/resources/html/img/move.png | Bin 0 -> 6121 bytes interface/resources/html/img/run-script.png | Bin 0 -> 4873 bytes interface/resources/html/img/talk.png | Bin 0 -> 2611 bytes interface/resources/html/img/write-script.png | Bin 0 -> 2006 bytes .../resources/html/interface-welcome.html | 187 +++ interface/resources/icons/load-script.svg | 125 ++ interface/resources/icons/new-script.svg | 129 ++ interface/resources/icons/save-script.svg | 674 +++++++++++ interface/resources/icons/start-script.svg | 550 +++++++++ interface/resources/icons/stop-script.svg | 163 +++ interface/resources/qml/AssetServer.qml | 2 +- interface/resources/qml/AvatarInputs.qml | 57 +- interface/resources/qml/Stats.qml | 12 +- interface/resources/styles/log_dialog.qss | 4 +- interface/src/Application.cpp | 218 ++-- interface/src/Application.h | 7 +- interface/src/Menu.cpp | 21 +- interface/src/Menu.h | 6 +- interface/src/avatar/Avatar.h | 1 + interface/src/avatar/AvatarManager.cpp | 2 +- interface/src/avatar/AvatarManager.h | 2 +- .../src/avatar/CauterizedMeshPartPayload.cpp | 53 +- .../src/avatar/CauterizedMeshPartPayload.h | 7 +- interface/src/avatar/CauterizedModel.cpp | 44 +- interface/src/avatar/Head.cpp | 4 +- interface/src/avatar/Head.h | 6 +- interface/src/avatar/MyAvatar.cpp | 115 +- interface/src/avatar/MyAvatar.h | 56 +- interface/src/ui/ApplicationOverlay.cpp | 49 +- interface/src/ui/ApplicationOverlay.h | 3 + interface/src/ui/AvatarInputs.cpp | 20 + interface/src/ui/AvatarInputs.h | 6 + interface/src/ui/BaseLogDialog.cpp | 48 +- interface/src/ui/BaseLogDialog.h | 4 +- interface/src/ui/CachesSizeDialog.cpp | 84 ++ interface/src/ui/CachesSizeDialog.h | 45 + interface/src/ui/DialogsManager.cpp | 24 + interface/src/ui/DialogsManager.h | 6 + interface/src/ui/DiskCacheEditor.cpp | 146 +++ interface/src/ui/DiskCacheEditor.h | 49 + interface/src/ui/ScriptEditBox.cpp | 111 ++ interface/src/ui/ScriptEditBox.h | 38 + interface/src/ui/ScriptEditorWidget.cpp | 256 ++++ interface/src/ui/ScriptEditorWidget.h | 64 + interface/src/ui/ScriptEditorWindow.cpp | 259 +++++ interface/src/ui/ScriptEditorWindow.h | 64 + interface/src/ui/ScriptLineNumberArea.cpp | 28 + interface/src/ui/ScriptLineNumberArea.h | 32 + interface/src/ui/ScriptsTableWidget.cpp | 49 + interface/src/ui/ScriptsTableWidget.h | 28 + interface/src/ui/Stats.cpp | 10 +- interface/src/ui/Stats.h | 8 +- interface/src/ui/overlays/Overlays.cpp | 4 +- interface/src/ui/overlays/Web3DOverlay.cpp | 2 +- interface/ui/scriptEditorWidget.ui | 142 +++ interface/ui/scriptEditorWindow.ui | 324 ++++++ libraries/animation/src/Rig.cpp | 8 +- libraries/animation/src/Rig.h | 2 +- libraries/audio-client/src/AudioClient.cpp | 244 ++-- libraries/audio-client/src/AudioClient.h | 16 +- libraries/avatars/src/HeadData.cpp | 4 +- .../display-plugins/OpenGLDisplayPlugin.cpp | 6 +- .../display-plugins/hmd/HmdDisplayPlugin.cpp | 35 +- .../src/EntityTreeRenderer.cpp | 5 +- .../src/RenderablePolyVoxEntityItem.cpp | 78 +- .../src/RenderablePolyVoxEntityItem.h | 9 +- .../src/RenderableShapeEntityItem.cpp | 13 +- .../src/RenderableWebEntityItem.cpp | 2 +- .../src/EntitiesScriptEngineProvider.h | 4 +- libraries/entities/src/EntityItem.cpp | 8 +- .../entities/src/EntityItemProperties.cpp | 21 + libraries/entities/src/EntityItemProperties.h | 4 + .../entities/src/EntityScriptingInterface.cpp | 176 +-- .../entities/src/EntityScriptingInterface.h | 54 +- libraries/entities/src/PolyVoxEntityItem.cpp | 4 - libraries/entities/src/PolyVoxEntityItem.h | 6 +- libraries/entities/src/PropertyGroup.h | 29 +- libraries/fbx/src/FBXReader.cpp | 19 +- libraries/fbx/src/FBXReader.h | 20 + libraries/fbx/src/FBXReader_Node.cpp | 3 +- libraries/fbx/src/OBJReader.cpp | 10 +- libraries/fbx/src/OBJReader.h | 2 +- libraries/fbx/src/OBJWriter.cpp | 148 --- libraries/fbx/src/OBJWriter.h | 26 - libraries/gpu-gl/src/gpu/gl/GLBackend.cpp | 11 +- libraries/gpu-gl/src/gpu/gl/GLBackend.h | 13 +- .../gpu-gl/src/gpu/gl/GLBackendTexture.cpp | 54 +- libraries/gpu-gl/src/gpu/gl/GLFramebuffer.cpp | 9 +- libraries/gpu-gl/src/gpu/gl/GLFramebuffer.h | 2 +- libraries/gpu-gl/src/gpu/gl/GLTexelFormat.cpp | 5 - libraries/gpu-gl/src/gpu/gl/GLTexture.cpp | 233 +++- libraries/gpu-gl/src/gpu/gl/GLTexture.h | 203 +++- .../gpu-gl/src/gpu/gl/GLTextureTransfer.cpp | 208 ++++ .../gpu-gl/src/gpu/gl/GLTextureTransfer.h | 78 ++ libraries/gpu-gl/src/gpu/gl41/GL41Backend.h | 31 +- .../gpu-gl/src/gpu/gl41/GL41BackendOutput.cpp | 10 +- .../src/gpu/gl41/GL41BackendTexture.cpp | 192 ++- libraries/gpu-gl/src/gpu/gl45/GL45Backend.cpp | 11 +- libraries/gpu-gl/src/gpu/gl45/GL45Backend.h | 254 +--- .../gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp | 10 +- .../src/gpu/gl45/GL45BackendTexture.cpp | 542 +++++++-- .../gpu/gl45/GL45BackendVariableTexture.cpp | 1033 ----------------- libraries/gpu/CMakeLists.txt | 2 +- libraries/gpu/src/gpu/Batch.cpp | 7 + libraries/gpu/src/gpu/Buffer.h | 2 +- libraries/gpu/src/gpu/Context.cpp | 17 - libraries/gpu/src/gpu/Context.h | 4 - libraries/gpu/src/gpu/Format.cpp | 7 - libraries/gpu/src/gpu/Format.h | 6 - libraries/gpu/src/gpu/Framebuffer.cpp | 12 +- libraries/gpu/src/gpu/Texture.cpp | 261 +++-- libraries/gpu/src/gpu/Texture.h | 178 +-- libraries/gpu/src/gpu/Texture_ktx.cpp | 289 ----- libraries/ktx/CMakeLists.txt | 3 - libraries/ktx/src/ktx/KTX.cpp | 165 --- libraries/ktx/src/ktx/KTX.h | 494 -------- libraries/ktx/src/ktx/Reader.cpp | 195 ---- libraries/ktx/src/ktx/Writer.cpp | 171 --- libraries/model-networking/CMakeLists.txt | 2 +- .../src/model-networking/KTXCache.cpp | 47 - .../src/model-networking/KTXCache.h | 51 - .../src/model-networking/TextureCache.cpp | 492 +++----- .../src/model-networking/TextureCache.h | 26 +- libraries/model/CMakeLists.txt | 2 +- libraries/model/src/model/Geometry.cpp | 112 +- libraries/model/src/model/Geometry.h | 14 +- libraries/model/src/model/TextureMap.cpp | 149 +-- libraries/model/src/model/TextureMap.h | 3 +- libraries/networking/src/Assignment.cpp | 1 + libraries/networking/src/FileCache.cpp | 243 ---- libraries/networking/src/FileCache.h | 158 --- libraries/networking/src/NodePermissions.h | 48 +- libraries/networking/src/udt/PacketQueue.cpp | 23 +- libraries/networking/src/udt/PacketQueue.h | 5 +- libraries/octree/src/OctreeQuery.cpp | 2 +- libraries/physics/src/EntityMotionState.cpp | 24 +- libraries/physics/src/EntityMotionState.h | 1 - .../physics/src/PhysicalEntitySimulation.cpp | 18 +- .../physics/src/PhysicalEntitySimulation.h | 5 +- libraries/physics/src/PhysicsEngine.cpp | 2 +- libraries/physics/src/PhysicsEngine.h | 3 +- .../physics/src/ThreadSafeDynamicsWorld.cpp | 27 +- .../physics/src/ThreadSafeDynamicsWorld.h | 4 - libraries/recording/src/recording/Deck.cpp | 5 +- libraries/render-utils/CMakeLists.txt | 2 +- .../render-utils/src/AntialiasingEffect.cpp | 2 +- .../render-utils/src/DeferredFramebuffer.cpp | 10 +- .../src/DeferredLightingEffect.cpp | 4 +- .../render-utils/src/FramebufferCache.cpp | 16 + libraries/render-utils/src/FramebufferCache.h | 5 + libraries/render-utils/src/LightAmbient.slh | 13 +- libraries/render-utils/src/LightStage.cpp | 4 +- libraries/render-utils/src/LightingModel.cpp | 10 - libraries/render-utils/src/LightingModel.h | 12 +- libraries/render-utils/src/LightingModel.slh | 9 +- .../render-utils/src/MaterialTextures.slh | 2 +- .../render-utils/src/MeshPartPayload.cpp | 26 +- libraries/render-utils/src/MeshPartPayload.h | 6 +- libraries/render-utils/src/Model.cpp | 98 +- libraries/render-utils/src/Model.h | 10 +- .../render-utils/src/RenderDeferredTask.cpp | 27 +- .../render-utils/src/RenderPipelines.cpp | 2 +- .../render-utils/src/SubsurfaceScattering.cpp | 6 +- .../render-utils/src/SurfaceGeometryPass.cpp | 12 +- libraries/render-utils/src/text/Font.cpp | 3 +- libraries/render/CMakeLists.txt | 2 +- libraries/render/src/render/DrawTask.cpp | 12 +- libraries/render/src/render/DrawTask.h | 4 +- libraries/render/src/render/ShapePipeline.h | 8 +- .../render/src/render/drawItemStatus.slv | 4 +- .../src/AudioScriptingInterface.cpp | 5 + .../src/AudioScriptingInterface.h | 9 +- .../src/BaseScriptEngine.cpp | 130 +-- .../script-engine/src/BaseScriptEngine.h | 67 ++ libraries/script-engine/src/Mat4.cpp | 2 +- libraries/script-engine/src/Mat4.h | 4 +- libraries/script-engine/src/MeshProxy.h | 41 - .../src/ModelScriptingInterface.cpp | 159 --- .../src/ModelScriptingInterface.h | 45 - libraries/script-engine/src/Quat.cpp | 2 +- libraries/script-engine/src/Quat.h | 4 +- libraries/script-engine/src/ScriptEngine.cpp | 561 +-------- libraries/script-engine/src/ScriptEngine.h | 37 +- .../script-engine/src/ScriptEngineLogging.cpp | 1 - .../script-engine/src/ScriptEngineLogging.h | 1 - libraries/script-engine/src/ScriptEngines.cpp | 20 +- libraries/script-engine/src/ScriptEngines.h | 10 +- libraries/shared/src/BaseScriptEngine.h | 90 -- libraries/shared/src/GLMHelpers.h | 2 +- libraries/shared/src/HifiConfigVariantMap.cpp | 6 +- libraries/shared/src/PathUtils.cpp | 22 +- libraries/shared/src/PathUtils.h | 7 +- libraries/shared/src/RenderArgs.h | 1 - libraries/shared/src/ServerPathUtils.cpp | 31 + libraries/shared/src/ServerPathUtils.h | 22 + libraries/shared/src/ViewFrustum.cpp | 2 +- libraries/shared/src/ViewFrustum.h | 2 +- libraries/shared/src/shared/Storage.cpp | 92 -- libraries/shared/src/shared/Storage.h | 82 -- libraries/ui/src/ui/Menu.cpp | 2 +- .../src/OculusLegacyDisplayPlugin.cpp | 2 +- plugins/openvr/src/OpenVrDisplayPlugin.cpp | 8 +- .../developer/libraries/jasmine/hifi-boot.js | 13 +- scripts/developer/tests/.gitignore | 1 - scripts/developer/tests/ambientSoundTest.js | 2 +- .../tests/basicEntityTest/entitySpawner.js | 2 +- .../batonSoundTestEntitySpawner.js | 2 +- .../tests/entityServerStampedeTest.js | 2 +- scripts/developer/tests/entityStampedeTest.js | 2 +- scripts/developer/tests/lodTest.js | 2 +- scripts/developer/tests/mat4test.js | 8 +- .../developer/tests/performance/tribbles.js | 2 +- .../rapidProceduralChangeTest.js | 4 +- scripts/developer/tests/scaling.png | Bin 3172 -> 0 bytes scripts/developer/tests/sphereLODTest.js | 2 +- scripts/developer/tests/testInterval.js | 2 +- .../tests/unit_tests/entityUnitTests.js | 2 +- .../tests/unit_tests/moduleTests/cycles/a.js | 10 - .../tests/unit_tests/moduleTests/cycles/b.js | 10 - .../unit_tests/moduleTests/cycles/main.js | 17 - .../entity/entityConstructorAPIException.js | 13 - .../entity/entityConstructorModule.js | 23 - .../entity/entityConstructorNested.js | 14 - .../entity/entityConstructorNested2.js | 25 - .../entityConstructorRequireException.js | 10 - .../entity/entityPreloadAPIError.js | 13 - .../entity/entityPreloadRequire.js | 11 - .../tests/unit_tests/moduleTests/example.json | 9 - .../moduleTests/exceptions/exception.js | 4 - .../exceptions/exceptionInFunction.js | 38 - .../tests/unit_tests/moduleUnitTests.js | 378 ------ .../developer/tests/unit_tests/package.json | 6 - .../tests/unit_tests/scriptUnitTests.js | 18 +- scripts/developer/tests/viveTouchpadTest.js | 8 +- .../developer/utilities/record/recorder.js | 97 +- .../utilities/render/deferredLighting.qml | 3 +- .../utilities/render/photobooth/photobooth.js | 9 +- scripts/modules/vec3.js | 69 -- .../system/assets/images/icon-particles.svg | 29 - .../system/assets/images/icon-point-light.svg | 57 - .../system/assets/images/icon-spot-light.svg | 37 - scripts/system/away.js | 4 +- scripts/system/controllers/grab.js | 4 +- .../system/controllers/handControllerGrab.js | 2 +- .../controllers/handControllerPointer.js | 2 +- scripts/system/controllers/teleport.js | 19 +- ...oggleAdvancedMovementForHandControllers.js | 13 +- scripts/system/edit.js | 146 ++- scripts/system/libraries/WebTablet.js | 6 +- scripts/system/libraries/entityCameraTool.js | 4 +- .../system/libraries/entitySelectionTool.js | 10 +- ...erlayManager.js => lightOverlayManager.js} | 51 +- scripts/system/libraries/soundArray.js | 2 +- scripts/system/libraries/toolBars.js | 4 - scripts/system/nameTag.js | 4 +- scripts/system/pal.js | 6 +- scripts/system/voxels.js | 2 +- scripts/tutorials/NBody/makePlanets.js | 2 +- scripts/tutorials/butterflies.js | 6 +- scripts/tutorials/createCow.js | 2 +- scripts/tutorials/createDice.js | 4 +- scripts/tutorials/createFlashlight.js | 2 +- scripts/tutorials/createGolfClub.js | 2 +- scripts/tutorials/createPictureFrame.js | 2 +- scripts/tutorials/createPingPongGun.js | 2 +- scripts/tutorials/createPistol.js | 2 +- scripts/tutorials/createSoundMaker.js | 2 +- scripts/tutorials/entity_scripts/golfClub.js | 4 +- .../tutorials/entity_scripts/pingPongGun.js | 14 +- scripts/tutorials/entity_scripts/pistol.js | 2 +- scripts/tutorials/entity_scripts/sit.js | 230 ++-- scripts/tutorials/makeBlocks.js | 8 +- tests/ktx/CMakeLists.txt | 15 - tests/ktx/src/main.cpp | 150 --- tests/render-perf/CMakeLists.txt | 2 +- tests/render-perf/src/Camera.hpp | 6 +- tests/render-perf/src/main.cpp | 1 + tests/render-texture-load/src/main.cpp | 1 - tests/shared/src/StorageTests.cpp | 75 -- tests/shared/src/StorageTests.h | 32 - tools/CMakeLists.txt | 2 - tools/atp-get/CMakeLists.txt | 3 - tools/atp-get/src/ATPGetApp.cpp | 269 ----- tools/atp-get/src/ATPGetApp.h | 52 - tools/atp-get/src/main.cpp | 31 - .../marketplace/boppo/boppoClownEntity.js | 80 -- .../marketplace/boppo/boppoServer.js | 303 ----- .../marketplace/boppo/clownGloveDispenser.js | 154 --- .../marketplace/boppo/createElBoppo.js | 430 ------- .../marketplace/boppo/lookAtEntity.js | 98 -- 304 files changed, 6966 insertions(+), 9882 deletions(-) create mode 100644 interface/resources/html/img/devices.png create mode 100644 interface/resources/html/img/models.png create mode 100644 interface/resources/html/img/move.png create mode 100644 interface/resources/html/img/run-script.png create mode 100644 interface/resources/html/img/talk.png create mode 100644 interface/resources/html/img/write-script.png create mode 100644 interface/resources/html/interface-welcome.html create mode 100644 interface/resources/icons/load-script.svg create mode 100644 interface/resources/icons/new-script.svg create mode 100644 interface/resources/icons/save-script.svg create mode 100644 interface/resources/icons/start-script.svg create mode 100644 interface/resources/icons/stop-script.svg create mode 100644 interface/src/ui/CachesSizeDialog.cpp create mode 100644 interface/src/ui/CachesSizeDialog.h create mode 100644 interface/src/ui/DiskCacheEditor.cpp create mode 100644 interface/src/ui/DiskCacheEditor.h create mode 100644 interface/src/ui/ScriptEditBox.cpp create mode 100644 interface/src/ui/ScriptEditBox.h create mode 100644 interface/src/ui/ScriptEditorWidget.cpp create mode 100644 interface/src/ui/ScriptEditorWidget.h create mode 100644 interface/src/ui/ScriptEditorWindow.cpp create mode 100644 interface/src/ui/ScriptEditorWindow.h create mode 100644 interface/src/ui/ScriptLineNumberArea.cpp create mode 100644 interface/src/ui/ScriptLineNumberArea.h create mode 100644 interface/src/ui/ScriptsTableWidget.cpp create mode 100644 interface/src/ui/ScriptsTableWidget.h create mode 100644 interface/ui/scriptEditorWidget.ui create mode 100644 interface/ui/scriptEditorWindow.ui delete mode 100644 libraries/fbx/src/OBJWriter.cpp delete mode 100644 libraries/fbx/src/OBJWriter.h create mode 100644 libraries/gpu-gl/src/gpu/gl/GLTextureTransfer.cpp create mode 100644 libraries/gpu-gl/src/gpu/gl/GLTextureTransfer.h delete mode 100644 libraries/gpu-gl/src/gpu/gl45/GL45BackendVariableTexture.cpp delete mode 100644 libraries/gpu/src/gpu/Texture_ktx.cpp delete mode 100644 libraries/ktx/CMakeLists.txt delete mode 100644 libraries/ktx/src/ktx/KTX.cpp delete mode 100644 libraries/ktx/src/ktx/KTX.h delete mode 100644 libraries/ktx/src/ktx/Reader.cpp delete mode 100644 libraries/ktx/src/ktx/Writer.cpp delete mode 100644 libraries/model-networking/src/model-networking/KTXCache.cpp delete mode 100644 libraries/model-networking/src/model-networking/KTXCache.h delete mode 100644 libraries/networking/src/FileCache.cpp delete mode 100644 libraries/networking/src/FileCache.h rename libraries/{shared => script-engine}/src/BaseScriptEngine.cpp (68%) create mode 100644 libraries/script-engine/src/BaseScriptEngine.h delete mode 100644 libraries/script-engine/src/MeshProxy.h delete mode 100644 libraries/script-engine/src/ModelScriptingInterface.cpp delete mode 100644 libraries/script-engine/src/ModelScriptingInterface.h delete mode 100644 libraries/shared/src/BaseScriptEngine.h create mode 100644 libraries/shared/src/ServerPathUtils.cpp create mode 100644 libraries/shared/src/ServerPathUtils.h delete mode 100644 libraries/shared/src/shared/Storage.cpp delete mode 100644 libraries/shared/src/shared/Storage.h delete mode 100644 scripts/developer/tests/.gitignore delete mode 100644 scripts/developer/tests/scaling.png delete mode 100644 scripts/developer/tests/unit_tests/moduleTests/cycles/a.js delete mode 100644 scripts/developer/tests/unit_tests/moduleTests/cycles/b.js delete mode 100644 scripts/developer/tests/unit_tests/moduleTests/cycles/main.js delete mode 100644 scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorAPIException.js delete mode 100644 scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorModule.js delete mode 100644 scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested.js delete mode 100644 scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested2.js delete mode 100644 scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorRequireException.js delete mode 100644 scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadAPIError.js delete mode 100644 scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadRequire.js delete mode 100644 scripts/developer/tests/unit_tests/moduleTests/example.json delete mode 100644 scripts/developer/tests/unit_tests/moduleTests/exceptions/exception.js delete mode 100644 scripts/developer/tests/unit_tests/moduleTests/exceptions/exceptionInFunction.js delete mode 100644 scripts/developer/tests/unit_tests/moduleUnitTests.js delete mode 100644 scripts/developer/tests/unit_tests/package.json delete mode 100644 scripts/modules/vec3.js delete mode 100644 scripts/system/assets/images/icon-particles.svg delete mode 100644 scripts/system/assets/images/icon-point-light.svg delete mode 100644 scripts/system/assets/images/icon-spot-light.svg rename scripts/system/libraries/{entityIconOverlayManager.js => lightOverlayManager.js} (67%) delete mode 100644 tests/ktx/CMakeLists.txt delete mode 100644 tests/ktx/src/main.cpp delete mode 100644 tests/shared/src/StorageTests.cpp delete mode 100644 tests/shared/src/StorageTests.h delete mode 100644 tools/atp-get/CMakeLists.txt delete mode 100644 tools/atp-get/src/ATPGetApp.cpp delete mode 100644 tools/atp-get/src/ATPGetApp.h delete mode 100644 tools/atp-get/src/main.cpp delete mode 100644 unpublishedScripts/marketplace/boppo/boppoClownEntity.js delete mode 100644 unpublishedScripts/marketplace/boppo/boppoServer.js delete mode 100644 unpublishedScripts/marketplace/boppo/clownGloveDispenser.js delete mode 100644 unpublishedScripts/marketplace/boppo/createElBoppo.js delete mode 100644 unpublishedScripts/marketplace/boppo/lookAtEntity.js diff --git a/BUILD_WIN.md b/BUILD_WIN.md index e37bf27503..45373d3093 100644 --- a/BUILD_WIN.md +++ b/BUILD_WIN.md @@ -1,81 +1,104 @@ -This is a stand-alone guide for creating your first High Fidelity build for Windows 64-bit. +Please read the [general build guide](BUILD.md) for information on dependencies required for all platforms. Only Windows specific instructions are found in this file. -###Step 1. Installing Visual Studio 2013 +Interface can be built as 32 or 64 bit. -If you don't already have the Community or Professional edition of Visual Studio 2013, download and install [Visual Studio Community 2013](https://www.visualstudio.com/en-us/news/releasenotes/vs2013-community-vs). You do not need to install any of the optional components when going through the installer. +###Visual Studio 2013 -Note: Newer versions of Visual Studio are not yet compatible. +You can use the Community or Professional editions of Visual Studio 2013. -###Step 2. Installing CMake +You can start a Visual Studio 2013 command prompt using the shortcut provided in the Visual Studio Tools folder installed as part of Visual Studio 2013. -Download and install the CMake 3.8.0-rc2 "win64-x64 Installer" from the [CMake Website](https://cmake.org/download/). Make sure "Add CMake to system PATH for all users" is checked when going through the installer. +Or you can start a regular command prompt and then run: -###Step 3. Installing Qt + "%VS120COMNTOOLS%\vsvars32.bat" -Download and install the [Qt 5.6.1 Installer](https://download.qt.io/official_releases/qt/5.6/5.6.1-1/qt-opensource-windows-x86-msvc2013_64-5.6.1-1.exe). Please note that the download file is large (850MB) and may take some time. +####Windows SDK 8.1 -Make sure to select all components when going through the installer. +If using Visual Studio 2013 and building as a Visual Studio 2013 project you need the Windows 8 SDK which you should already have as part of installing Visual Studio 2013. You should be able to see it at `C:\Program Files (x86)\Windows Kits\8.1\Lib\winv6.3\um\x86`. -###Step 4. Setting Qt Environment Variable +####nmake -Go to "Control Panel > System > Advanced System Settings > Environment Variables > New..." (or search “Environment Variables” in Start Search). -* Set "Variable name": QT_CMAKE_PREFIX_PATH -* Set "Variable value": `C:\Qt\Qt5.6.1\5.6\msvc2013_64\lib\cmake` +Some of the external projects may require nmake to compile and install. If it is not installed at the location listed below, please ensure that it is in your PATH so CMake can find it when required. -###Step 5. Installing OpenSSL +We expect nmake.exe to be located at the following path. -Download and install the "Win64 OpenSSL v1.0.2k" Installer from [this website](https://slproweb.com/products/Win32OpenSSL.html). - -###Step 6. Running CMake to Generate Build Files - -Run Command Prompt from Start and run the following commands: - cd "%HIFI_DIR%" - mkdir build - cd build - cmake .. -G "Visual Studio 12 Win64" - -Where %HIFI_DIR% is the directory for the highfidelity repository. - -###Step 7. Making a Build - -Open '%HIFI_DIR%\build\hifi.sln' using Visual Studio. - -Change the Solution Configuration (next to the green play button) from "Debug" to "Release" for best performance. - -Run Build > Build Solution. - -###Step 8. Testing Interface - -Create another environment variable (see Step #4) -* Set "Variable name": _NO_DEBUG_HEAP -* Set "Variable value": 1 - -In Visual Studio, right+click "interface" under the Apps folder in Solution Explorer and select "Set as Startup Project". Run Debug > Start Debugging. - -Now, you should have a full build of High Fidelity and be able to run the Interface using Visual Studio. Please check our [Docs](https://wiki.highfidelity.com/wiki/Main_Page) for more information regarding the programming workflow. - -Note: You can also run Interface by launching it from command line or File Explorer from %HIFI_DIR%\build\interface\Release\interface.exe - -###Troubleshooting - -For any problems after Step #6, first try this: -* Delete your locally cloned copy of the highfidelity repository -* Restart your computer -* Redownload the [repository](https://github.com/highfidelity/hifi) -* Restart directions from Step #6 - -####CMake gives you the same error message repeatedly after the build fails - -Remove `CMakeCache.txt` found in the '%HIFI_DIR%\build' directory - -####nmake cannot be found - -Make sure nmake.exe is located at the following path: C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\bin - -If not, add the directory where nmake is located to the PATH environment variable. -####Qt is throwing an error +###Qt +You can use the online installer or the offline installer. If you use the offline installer, be sure to select the "OpenGL" version. -Make sure you have the correct version (5.6.1-1) installed and 'QT_CMAKE_PREFIX_PATH' environment variable is set correctly. +* [Download the online installer](http://www.qt.io/download-open-source/#section-2) + * When it asks you to select components, ONLY select one of the following, 32- or 64-bit to match your build preference: + * Qt > Qt 5.6.1 > **msvc2013 32-bit** + * Qt > Qt 5.6.1 > **msvc2013 64-bit** +* Download the offline installer, 32- or 64-bit to match your build preference: + * [32-bit](https://download.qt.io/official_releases/qt/5.6/5.6.1-1/qt-opensource-windows-x86-msvc2013-5.6.1-1.exe) + * [64-bit](https://download.qt.io/official_releases/qt/5.6/5.6.1-1/qt-opensource-windows-x86-msvc2013_64-5.6.1-1.exe) + +Once Qt is installed, you need to manually configure the following: +* Set the QT_CMAKE_PREFIX_PATH environment variable to your `Qt\5.6.1\msvc2013\lib\cmake` or `Qt\5.6.1\msvc2013_64\lib\cmake` directory. + * You can set an environment variable from Control Panel > System > Advanced System Settings > Environment Variables > New + +###External Libraries + +All libraries should be 32- or 64-bit to match your build preference. + +CMake will need to know where the headers and libraries for required external dependencies are. + +We use CMake's `fixup_bundle` to find the DLLs all of our executable targets require, and then copy them beside the executable in a post-build step. If `fixup_bundle` is having problems finding a DLL, you can fix it manually on your end by adding the folder containing that DLL to your path. Let us know which DLL CMake had trouble finding, as it is possible a tweak to our CMake files is required. + +The recommended route for CMake to find the external dependencies is to place all of the dependencies in one folder and set one ENV variable - HIFI_LIB_DIR. That ENV variable should point to a directory with the following structure: + + root_lib_dir + -> openssl + -> bin + -> include + -> lib + +For many of the external libraries where precompiled binaries are readily available you should be able to simply copy the extracted folder that you get from the download links provided at the top of the guide. Otherwise you may need to build from source and install the built product to this directory. The `root_lib_dir` in the above example can be wherever you choose on your system - as long as the environment variable HIFI_LIB_DIR is set to it. From here on, whenever you see %HIFI_LIB_DIR% you should substitute the directory that you chose. + +####OpenSSL + +Qt will use OpenSSL if it's available, but it doesn't install it, so you must install it separately. + +Your system may already have several versions of the OpenSSL DLL's (ssleay32.dll, libeay32.dll) lying around, but they may be the wrong version. If these DLL's are in the PATH then QT will try to use them, and if they're the wrong version then you will see the following errors in the console: + + QSslSocket: cannot resolve TLSv1_1_client_method + QSslSocket: cannot resolve TLSv1_2_client_method + QSslSocket: cannot resolve TLSv1_1_server_method + QSslSocket: cannot resolve TLSv1_2_server_method + QSslSocket: cannot resolve SSL_select_next_proto + QSslSocket: cannot resolve SSL_CTX_set_next_proto_select_cb + QSslSocket: cannot resolve SSL_get0_next_proto_negotiated + +To prevent these problems, install OpenSSL yourself. Download one of the following binary packages [from this website](https://slproweb.com/products/Win32OpenSSL.html): +* Win32 OpenSSL v1.0.1q +* Win64 OpenSSL v1.0.1q + +Install OpenSSL into the Windows system directory, to make sure that Qt uses the version that you've just installed, and not some other version. + +###Build High Fidelity using Visual Studio +Follow the same build steps from the CMake section of [BUILD.md](BUILD.md), but pass a different generator to CMake. + +For 32-bit builds: + + cmake .. -G "Visual Studio 12" + +For 64-bit builds: + + cmake .. -G "Visual Studio 12 Win64" + +Open %HIFI_DIR%\build\hifi.sln and compile. + +###Running Interface +If you need to debug Interface, you can run interface from within Visual Studio (see the section below). You can also run Interface by launching it from command line or File Explorer from %HIFI_DIR%\build\interface\Debug\interface.exe + +###Debugging Interface +* In the Solution Explorer, right click interface and click Set as StartUp Project +* Set the "Working Directory" for the Interface debugging sessions to the Debug output directory so that your application can load resources. Do this: right click interface and click Properties, choose Debugging from Configuration Properties, set Working Directory to .\Debug +* Now you can run and debug interface through Visual Studio + +For better performance when running debug builds, set the environment variable ```_NO_DEBUG_HEAP``` to ```1``` + +http://preshing.com/20110717/the-windows-heap-is-slow-when-launched-from-the-debugger/ diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index a5063b09b6..be23dcfa25 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -62,7 +62,6 @@ Agent::Agent(ReceivedMessage& message) : DependencyManager::set(); DependencyManager::set(); - DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); @@ -372,39 +371,25 @@ void Agent::executeScript() { using namespace recording; static const FrameType AUDIO_FRAME_TYPE = Frame::registerFrameType(AudioConstants::getAudioFrameName()); Frame::registerFrameHandler(AUDIO_FRAME_TYPE, [this, &scriptedAvatar](Frame::ConstPointer frame) { + const QByteArray& audio = frame->data; static quint16 audioSequenceNumber{ 0 }; - - QByteArray audio(frame->data); - - if (_isNoiseGateEnabled) { - static int numSamples = AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL; - _noiseGate.gateSamples(reinterpret_cast(audio.data()), numSamples); - } - - computeLoudness(&audio, scriptedAvatar); - - // the codec needs a flush frame before sending silent packets, so - // do not send one if the gate closed in this block (eventually this can be crossfaded). - auto packetType = PacketType::MicrophoneAudioNoEcho; - if (scriptedAvatar->getAudioLoudness() == 0.0f && !_noiseGate.closedInLastBlock()) { - packetType = PacketType::SilentAudioFrame; - } - Transform audioTransform; + auto headOrientation = scriptedAvatar->getHeadOrientation(); audioTransform.setTranslation(scriptedAvatar->getPosition()); audioTransform.setRotation(headOrientation); + computeLoudness(&audio, scriptedAvatar); + QByteArray encodedBuffer; if (_encoder) { _encoder->encode(audio, encodedBuffer); } else { encodedBuffer = audio; } - AbstractAudioInterface::emitAudioPacket(encodedBuffer.data(), encodedBuffer.size(), audioSequenceNumber, audioTransform, scriptedAvatar->getPosition(), glm::vec3(0), - packetType, _selectedCodecName); + PacketType::MicrophoneAudioNoEcho, _selectedCodecName); }); auto avatarHashMap = DependencyManager::set(); @@ -498,14 +483,6 @@ void Agent::setIsListeningToAudioStream(bool isListeningToAudioStream) { _isListeningToAudioStream = isListeningToAudioStream; } -void Agent::setIsNoiseGateEnabled(bool isNoiseGateEnabled) { - if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "setIsNoiseGateEnabled", Q_ARG(bool, isNoiseGateEnabled)); - return; - } - _isNoiseGateEnabled = isNoiseGateEnabled; -} - void Agent::setIsAvatar(bool isAvatar) { // this must happen on Agent's main thread if (QThread::currentThread() != thread()) { diff --git a/assignment-client/src/Agent.h b/assignment-client/src/Agent.h index 620ac8e047..0ce7b71d5d 100644 --- a/assignment-client/src/Agent.h +++ b/assignment-client/src/Agent.h @@ -29,7 +29,6 @@ #include -#include "AudioNoiseGate.h" #include "MixedAudioStream.h" #include "avatars/ScriptableAvatar.h" @@ -39,7 +38,6 @@ class Agent : public ThreadedAssignment { Q_PROPERTY(bool isAvatar READ isAvatar WRITE setIsAvatar) Q_PROPERTY(bool isPlayingAvatarSound READ isPlayingAvatarSound) Q_PROPERTY(bool isListeningToAudioStream READ isListeningToAudioStream WRITE setIsListeningToAudioStream) - Q_PROPERTY(bool isNoiseGateEnabled READ isNoiseGateEnabled WRITE setIsNoiseGateEnabled) Q_PROPERTY(float lastReceivedAudioLoudness READ getLastReceivedAudioLoudness) Q_PROPERTY(QUuid sessionUUID READ getSessionUUID) @@ -54,9 +52,6 @@ public: bool isListeningToAudioStream() const { return _isListeningToAudioStream; } void setIsListeningToAudioStream(bool isListeningToAudioStream); - bool isNoiseGateEnabled() const { return _isNoiseGateEnabled; } - void setIsNoiseGateEnabled(bool isNoiseGateEnabled); - float getLastReceivedAudioLoudness() const { return _lastReceivedAudioLoudness; } QUuid getSessionUUID() const; @@ -111,9 +106,6 @@ private: QTimer* _avatarIdentityTimer = nullptr; QHash _outgoingScriptAudioSequenceNumbers; - AudioNoiseGate _noiseGate; - bool _isNoiseGateEnabled { false }; - CodecPluginPointer _codec; QString _selectedCodecName; Encoder* _encoder { nullptr }; diff --git a/assignment-client/src/assets/AssetServer.cpp b/assignment-client/src/assets/AssetServer.cpp index 3886ff8d92..82dd23a9de 100644 --- a/assignment-client/src/assets/AssetServer.cpp +++ b/assignment-client/src/assets/AssetServer.cpp @@ -24,7 +24,7 @@ #include #include -#include +#include #include "NetworkLogging.h" #include "NodeType.h" @@ -162,7 +162,7 @@ void AssetServer::completeSetup() { if (assetsPath.isRelative()) { // if the domain settings passed us a relative path, make an absolute path that is relative to the // default data directory - absoluteFilePath = PathUtils::getAppDataFilePath("assets/" + assetsPathString); + absoluteFilePath = ServerPathUtils::getDataFilePath("assets/" + assetsPathString); } _resourcesDirectory = QDir(absoluteFilePath); diff --git a/assignment-client/src/octree/OctreeServer.cpp b/assignment-client/src/octree/OctreeServer.cpp index f2dbe5d1d2..2eee2ee229 100644 --- a/assignment-client/src/octree/OctreeServer.cpp +++ b/assignment-client/src/octree/OctreeServer.cpp @@ -29,7 +29,7 @@ #include "OctreeQueryNode.h" #include "OctreeServerConsts.h" #include -#include +#include #include int OctreeServer::_clientCount = 0; @@ -279,7 +279,8 @@ OctreeServer::~OctreeServer() { void OctreeServer::initHTTPManager(int port) { // setup the embedded web server - QString documentRoot = QString("%1/web").arg(PathUtils::getAppDataPath()); + + QString documentRoot = QString("%1/web").arg(ServerPathUtils::getDataDirectory()); // setup an httpManager with us as the request handler and the parent _httpManager = new HTTPManager(QHostAddress::AnyIPv4, port, documentRoot, this, this); @@ -1178,7 +1179,7 @@ void OctreeServer::domainSettingsRequestComplete() { if (persistPath.isRelative()) { // if the domain settings passed us a relative path, make an absolute path that is relative to the // default data directory - persistAbsoluteFilePath = QDir(PathUtils::getAppDataFilePath("entities/")).absoluteFilePath(_persistFilePath); + persistAbsoluteFilePath = QDir(ServerPathUtils::getDataFilePath("entities/")).absoluteFilePath(_persistFilePath); } static const QString ENTITY_PERSIST_EXTENSION = ".json.gz"; @@ -1244,7 +1245,7 @@ void OctreeServer::domainSettingsRequestComplete() { QDir backupDirectory { _backupDirectoryPath }; QString absoluteBackupDirectory; if (backupDirectory.isRelative()) { - absoluteBackupDirectory = QDir(PathUtils::getAppDataFilePath("entities/")).absoluteFilePath(_backupDirectoryPath); + absoluteBackupDirectory = QDir(ServerPathUtils::getDataFilePath("entities/")).absoluteFilePath(_backupDirectoryPath); absoluteBackupDirectory = QDir(absoluteBackupDirectory).absolutePath(); } else { absoluteBackupDirectory = backupDirectory.absolutePath(); diff --git a/assignment-client/src/scripts/EntityScriptServer.cpp b/assignment-client/src/scripts/EntityScriptServer.cpp index 954c25a342..47071b10b7 100644 --- a/assignment-client/src/scripts/EntityScriptServer.cpp +++ b/assignment-client/src/scripts/EntityScriptServer.cpp @@ -58,8 +58,6 @@ EntityScriptServer::EntityScriptServer(ReceivedMessage& message) : ThreadedAssig DependencyManager::registerInheritance(); - DependencyManager::set(); - DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); @@ -326,26 +324,7 @@ void EntityScriptServer::nodeActivated(SharedNodePointer activatedNode) { void EntityScriptServer::nodeKilled(SharedNodePointer killedNode) { switch (killedNode->getType()) { case NodeType::EntityServer: { - // Before we clear, make sure this was our only entity server. - // Otherwise we're assuming that we have "trading" entity servers - // (an old one going away and a new one coming onboard) - // and that we shouldn't clear here because we're still doing work. - bool hasAnotherEntityServer = false; - auto nodeList = DependencyManager::get(); - - nodeList->eachNodeBreakable([&hasAnotherEntityServer, &killedNode](const SharedNodePointer& node){ - if (node->getType() == NodeType::EntityServer && node->getUUID() != killedNode->getUUID()) { - // we're talking to > 1 entity servers, we know we won't clear - hasAnotherEntityServer = true; - return false; - } - - return true; - }); - - if (!hasAnotherEntityServer) { - clear(); - } + clear(); break; } @@ -416,8 +395,7 @@ void EntityScriptServer::selectAudioFormat(const QString& selectedCodecName) { void EntityScriptServer::resetEntitiesScriptEngine() { auto engineName = QString("about:Entities %1").arg(++_entitiesScriptEngineCount); - auto newEngine = QSharedPointer(new ScriptEngine(ScriptEngine::ENTITY_SERVER_SCRIPT, NO_SCRIPT, engineName), - &ScriptEngine::deleteLater); + auto newEngine = QSharedPointer(new ScriptEngine(ScriptEngine::ENTITY_SERVER_SCRIPT, NO_SCRIPT, engineName)); auto webSocketServerConstructorValue = newEngine->newFunction(WebSocketServerClass::constructor); newEngine->globalObject().setProperty("WebSocketServer", webSocketServerConstructorValue); @@ -477,13 +455,13 @@ void EntityScriptServer::addingEntity(const EntityItemID& entityID) { void EntityScriptServer::deletingEntity(const EntityItemID& entityID) { if (_entityViewer.getTree() && !_shuttingDown && _entitiesScriptEngine) { - _entitiesScriptEngine->unloadEntityScript(entityID, true); + _entitiesScriptEngine->unloadEntityScript(entityID); } } void EntityScriptServer::entityServerScriptChanging(const EntityItemID& entityID, const bool reload) { if (_entityViewer.getTree() && !_shuttingDown) { - _entitiesScriptEngine->unloadEntityScript(entityID, true); + _entitiesScriptEngine->unloadEntityScript(entityID); checkAndCallPreload(entityID, reload); } } diff --git a/cmake/macros/PackageLibrariesForDeployment.cmake b/cmake/macros/PackageLibrariesForDeployment.cmake index d324776572..795e3642a5 100644 --- a/cmake/macros/PackageLibrariesForDeployment.cmake +++ b/cmake/macros/PackageLibrariesForDeployment.cmake @@ -24,9 +24,9 @@ macro(PACKAGE_LIBRARIES_FOR_DEPLOYMENT) TARGET ${TARGET_NAME} POST_BUILD COMMAND ${CMAKE_COMMAND} - -DBUNDLE_EXECUTABLE="$" - -DBUNDLE_PLUGIN_DIR="$/${PLUGIN_PATH}" - -P "${CMAKE_CURRENT_BINARY_DIR}/FixupBundlePostBuild.cmake" + -DBUNDLE_EXECUTABLE=$ + -DBUNDLE_PLUGIN_DIR=$/${PLUGIN_PATH} + -P ${CMAKE_CURRENT_BINARY_DIR}/FixupBundlePostBuild.cmake ) find_program(WINDEPLOYQT_COMMAND windeployqt PATHS ${QT_DIR}/bin NO_DEFAULT_PATH) @@ -39,27 +39,27 @@ macro(PACKAGE_LIBRARIES_FOR_DEPLOYMENT) add_custom_command( TARGET ${TARGET_NAME} POST_BUILD - COMMAND CMD /C "SET PATH=%PATH%;${QT_DIR}/bin && ${WINDEPLOYQT_COMMAND} ${EXTRA_DEPLOY_OPTIONS} $<$,$,$>:--release> \"$\"" + COMMAND CMD /C "SET PATH=%PATH%;${QT_DIR}/bin && ${WINDEPLOYQT_COMMAND} ${EXTRA_DEPLOY_OPTIONS} $<$,$,$>:--release> $" ) - set(QTAUDIO_PATH "$/audio") - set(QTAUDIO_WIN7_PATH "$/audioWin7/audio") - set(QTAUDIO_WIN8_PATH "$/audioWin8/audio") + set(QTAUDIO_PATH $/audio) + set(QTAUDIO_WIN7_PATH $/audioWin7/audio) + set(QTAUDIO_WIN8_PATH $/audioWin8/audio) # copy qtaudio_wasapi.dll and qtaudio_windows.dll in the correct directories for runtime selection add_custom_command( TARGET ${TARGET_NAME} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E make_directory "${QTAUDIO_WIN7_PATH}" - COMMAND ${CMAKE_COMMAND} -E make_directory "${QTAUDIO_WIN8_PATH}" + COMMAND ${CMAKE_COMMAND} -E make_directory ${QTAUDIO_WIN7_PATH} + COMMAND ${CMAKE_COMMAND} -E make_directory ${QTAUDIO_WIN8_PATH} # copy release DLLs - COMMAND if exist "${QTAUDIO_PATH}/qtaudio_windows.dll" ( ${CMAKE_COMMAND} -E copy "${QTAUDIO_PATH}/qtaudio_windows.dll" "${QTAUDIO_WIN7_PATH}" ) - COMMAND if exist "${QTAUDIO_PATH}/qtaudio_windows.dll" ( ${CMAKE_COMMAND} -E copy "${WASAPI_DLL_PATH}/qtaudio_wasapi.dll" "${QTAUDIO_WIN8_PATH}" ) + COMMAND if exist ${QTAUDIO_PATH}/qtaudio_windows.dll ( ${CMAKE_COMMAND} -E copy ${QTAUDIO_PATH}/qtaudio_windows.dll ${QTAUDIO_WIN7_PATH} ) + COMMAND if exist ${QTAUDIO_PATH}/qtaudio_windows.dll ( ${CMAKE_COMMAND} -E copy ${WASAPI_DLL_PATH}/qtaudio_wasapi.dll ${QTAUDIO_WIN8_PATH} ) # copy debug DLLs - COMMAND if exist "${QTAUDIO_PATH}/qtaudio_windowsd.dll" ( ${CMAKE_COMMAND} -E copy "${QTAUDIO_PATH}/qtaudio_windowsd.dll" "${QTAUDIO_WIN7_PATH}" ) - COMMAND if exist "${QTAUDIO_PATH}/qtaudio_windowsd.dll" ( ${CMAKE_COMMAND} -E copy "${WASAPI_DLL_PATH}/qtaudio_wasapid.dll" "${QTAUDIO_WIN8_PATH}" ) + COMMAND if exist ${QTAUDIO_PATH}/qtaudio_windowsd.dll ( ${CMAKE_COMMAND} -E copy ${QTAUDIO_PATH}/qtaudio_windowsd.dll ${QTAUDIO_WIN7_PATH} ) + COMMAND if exist ${QTAUDIO_PATH}/qtaudio_windowsd.dll ( ${CMAKE_COMMAND} -E copy ${WASAPI_DLL_PATH}/qtaudio_wasapid.dll ${QTAUDIO_WIN8_PATH} ) # remove directory - COMMAND ${CMAKE_COMMAND} -E remove_directory "${QTAUDIO_PATH}" + COMMAND ${CMAKE_COMMAND} -E remove_directory ${QTAUDIO_PATH} ) endif () diff --git a/cmake/macros/SymlinkOrCopyDirectoryBesideTarget.cmake b/cmake/macros/SymlinkOrCopyDirectoryBesideTarget.cmake index 9ae47aad82..37a7a9caa0 100644 --- a/cmake/macros/SymlinkOrCopyDirectoryBesideTarget.cmake +++ b/cmake/macros/SymlinkOrCopyDirectoryBesideTarget.cmake @@ -14,7 +14,7 @@ macro(SYMLINK_OR_COPY_DIRECTORY_BESIDE_TARGET _SHOULD_SYMLINK _DIRECTORY _DESTIN # remove the current directory add_custom_command( TARGET ${TARGET_NAME} POST_BUILD - COMMAND "${CMAKE_COMMAND}" -E remove_directory "$/${_DESTINATION}" + COMMAND "${CMAKE_COMMAND}" -E remove_directory $/${_DESTINATION} ) if (${_SHOULD_SYMLINK}) @@ -48,8 +48,8 @@ macro(SYMLINK_OR_COPY_DIRECTORY_BESIDE_TARGET _SHOULD_SYMLINK _DIRECTORY _DESTIN # copy the directory add_custom_command( TARGET ${TARGET_NAME} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_directory "${_DIRECTORY}" - "$/${_DESTINATION}" + COMMAND ${CMAKE_COMMAND} -E copy_directory ${_DIRECTORY} + $/${_DESTINATION} ) endif () # glob everything in this directory - add a custom command to copy any files diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 620b11d8ad..c741c22b83 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -38,7 +38,7 @@ #include #include #include -#include +#include #include #include "DomainServerNodeData.h" @@ -1618,7 +1618,7 @@ QJsonObject DomainServer::jsonObjectForNode(const SharedNodePointer& node) { QDir pathForAssignmentScriptsDirectory() { static const QString SCRIPTS_DIRECTORY_NAME = "/scripts/"; - QDir directory(PathUtils::getAppDataPath() + SCRIPTS_DIRECTORY_NAME); + QDir directory(ServerPathUtils::getDataDirectory() + SCRIPTS_DIRECTORY_NAME); if (!directory.exists()) { directory.mkpath("."); qInfo() << "Created path to " << directory.path(); diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index d6b57b450a..661a6213b8 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -246,13 +246,10 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList _agentPermissions[editorKey]->set(NodePermissions::Permission::canAdjustLocks); } - std::list> permissionsSets{ - _standardAgentPermissions.get(), - _agentPermissions.get() - }; + QList> permissionsSets; + permissionsSets << _standardAgentPermissions.get() << _agentPermissions.get(); foreach (auto permissionsSet, permissionsSets) { - for (auto entry : permissionsSet) { - const auto& userKey = entry.first; + foreach (NodePermissionsKey userKey, permissionsSet.keys()) { if (onlyEditorsAreRezzers) { if (permissionsSet[userKey]->can(NodePermissions::Permission::canAdjustLocks)) { permissionsSet[userKey]->set(NodePermissions::Permission::canRezPermanentEntities); @@ -303,6 +300,7 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList } QVariantMap& DomainServerSettingsManager::getDescriptorsMap() { + static const QString DESCRIPTORS{ "descriptors" }; auto& settingsMap = getSettingsMap(); @@ -1357,12 +1355,18 @@ QStringList DomainServerSettingsManager::getAllKnownGroupNames() { // extract all the group names from the group-permissions and group-forbiddens settings QSet result; - for (const auto& entry : _groupPermissions.get()) { - result += entry.first.first; + QHashIterator i(_groupPermissions.get()); + while (i.hasNext()) { + i.next(); + NodePermissionsKey key = i.key(); + result += key.first; } - for (const auto& entry : _groupForbiddens.get()) { - result += entry.first.first; + QHashIterator j(_groupForbiddens.get()); + while (j.hasNext()) { + j.next(); + NodePermissionsKey key = j.key(); + result += key.first; } return result.toList(); @@ -1373,17 +1377,20 @@ bool DomainServerSettingsManager::setGroupID(const QString& groupName, const QUu _groupIDs[groupName.toLower()] = groupID; _groupNames[groupID] = groupName; - - for (const auto& entry : _groupPermissions.get()) { - auto& perms = entry.second; + QHashIterator i(_groupPermissions.get()); + while (i.hasNext()) { + i.next(); + NodePermissionsPointer perms = i.value(); if (perms->getID().toLower() == groupName.toLower() && !perms->isGroup()) { changed = true; perms->setGroupID(groupID); } } - for (const auto& entry : _groupForbiddens.get()) { - auto& perms = entry.second; + QHashIterator j(_groupForbiddens.get()); + while (j.hasNext()) { + j.next(); + NodePermissionsPointer perms = j.value(); if (perms->getID().toLower() == groupName.toLower() && !perms->isGroup()) { changed = true; perms->setGroupID(groupID); diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index 726aa7ef84..dbc484d0b9 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -189,7 +189,7 @@ endif() # link required hifi libraries link_hifi_libraries( - shared octree ktx gpu gl gpu-gl procedural model render + shared octree gpu gl gpu-gl procedural model render recording fbx networking model-networking entities avatars audio audio-client animation script-engine physics render-utils entities-renderer ui auto-updater @@ -288,7 +288,7 @@ if (APPLE) add_custom_command(TARGET ${TARGET_NAME} POST_BUILD COMMAND "${CMAKE_COMMAND}" -E copy_directory "${CMAKE_SOURCE_DIR}/scripts" - "$/../Resources/scripts" + $/../Resources/scripts ) # call the fixup_interface macro to add required bundling commands for installation @@ -299,10 +299,10 @@ else (APPLE) add_custom_command(TARGET ${TARGET_NAME} POST_BUILD COMMAND "${CMAKE_COMMAND}" -E copy_directory "${PROJECT_SOURCE_DIR}/resources" - "$/resources" + $/resources COMMAND "${CMAKE_COMMAND}" -E copy_directory "${CMAKE_SOURCE_DIR}/scripts" - "$/scripts" + $/scripts ) # link target to external libraries @@ -337,7 +337,7 @@ endif() add_bugsplat() if (WIN32) - set(EXTRA_DEPLOY_OPTIONS "--qmldir \"${PROJECT_SOURCE_DIR}/resources/qml\"") + set(EXTRA_DEPLOY_OPTIONS "--qmldir ${PROJECT_SOURCE_DIR}/resources/qml") set(TARGET_INSTALL_DIR ${INTERFACE_INSTALL_DIR}) set(TARGET_INSTALL_COMPONENT ${CLIENT_COMPONENT}) diff --git a/interface/resources/controllers/standard.json b/interface/resources/controllers/standard.json index 9e3b2f4d13..04a3f560b6 100644 --- a/interface/resources/controllers/standard.json +++ b/interface/resources/controllers/standard.json @@ -2,27 +2,7 @@ "name": "Standard to Action", "channels": [ { "from": "Standard.LY", "to": "Actions.TranslateZ" }, - - { "from": "Standard.LX", - "when": [ - "Application.InHMD", "!Application.AdvancedMovement", - "Application.SnapTurn", "!Standard.RX" - ], - "to": "Actions.StepYaw", - "filters": - [ - { "type": "deadZone", "min": 0.15 }, - "constrainToInteger", - { "type": "pulse", "interval": 0.25 }, - { "type": "scale", "scale": 22.5 } - ] - }, - { "from": "Standard.LX", "to": "Actions.TranslateX", - "when": [ "Application.AdvancedMovement" ] - }, - { "from": "Standard.LX", "to": "Actions.Yaw", - "when": [ "!Application.AdvancedMovement", "!Application.SnapTurn" ] - }, + { "from": "Standard.LX", "to": "Actions.TranslateX" }, { "from": "Standard.RX", "when": [ "Application.InHMD", "Application.SnapTurn" ], @@ -35,29 +15,29 @@ { "type": "scale", "scale": 22.5 } ] }, - { "from": "Standard.RX", "to": "Actions.Yaw", - "when": [ "!Application.SnapTurn" ] - }, - { "from": "Standard.RY", - "when": "Application.Grounded", - "to": "Actions.Up", - "filters": + { "from": "Standard.RX", "to": "Actions.Yaw" }, + { "from": "Standard.RY", + "when": "Application.Grounded", + "to": "Actions.Up", + "filters": [ { "type": "deadZone", "min": 0.6 }, "invert" ] - }, + }, - { "from": "Standard.RY", "to": "Actions.Up", "filters": "invert"}, + { "from": "Standard.RY", "to": "Actions.Up", "filters": "invert"}, { "from": "Standard.Back", "to": "Actions.CycleCamera" }, { "from": "Standard.Start", "to": "Actions.ContextMenu" }, - { "from": "Standard.LT", "to": "Actions.LeftHandClick" }, + { "from": "Standard.LT", "to": "Actions.LeftHandClick" }, { "from": "Standard.RT", "to": "Actions.RightHandClick" }, - { "from": "Standard.LeftHand", "to": "Actions.LeftHand" }, + { "from": "Standard.LeftHand", "to": "Actions.LeftHand" }, { "from": "Standard.RightHand", "to": "Actions.RightHand" } ] } + + diff --git a/interface/resources/html/img/devices.png b/interface/resources/html/img/devices.png new file mode 100644 index 0000000000000000000000000000000000000000..fc4231e96e25732a0659c911e7c15ded5b54911b GIT binary patch literal 7492 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 literal 0 HcmV?d00001 diff --git a/interface/resources/html/img/models.png b/interface/resources/html/img/models.png new file mode 100644 index 0000000000000000000000000000000000000000..b09c36011d540759da5326ed70bda97592e9636b GIT binary patch literal 8664 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 literal 0 HcmV?d00001 diff --git a/interface/resources/html/img/run-script.png b/interface/resources/html/img/run-script.png new file mode 100644 index 0000000000000000000000000000000000000000..941b8ee9f13664fc2b2ec51a75273ba3e553d3a5 GIT binary patch literal 4873 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 literal 0 HcmV?d00001 diff --git a/interface/resources/html/img/write-script.png b/interface/resources/html/img/write-script.png new file mode 100644 index 0000000000000000000000000000000000000000..dae97e59b14bae50013b583cd9cb3b5bdf6ad464 GIT binary patch literal 2006 zcmb`HX*Ao38pi)h(Ww^WC~;|$Yo=Oy6}h!H%2>+{#aNnJBh9EKC6-vKrnL4lqlR8f z+CxLd)*4blsO1QWsHU;S(vqMgmZY&S_c%TCW#-;9=gheuo-gltK0NP--!H=jVRz~) zwXXmGIOSk(?Fs;Z5CDKBS!n`!Um!t4q*Wpv@1Fw zC@!KOZ4Ll3cn51scl-o@sspQp0s1~_21*qu6P+5Qx8)(@64A${yN;nQG-&a!vK46|YxiCC+UCW# z?78c1pDn_!(s`qA{2QK}Osh^O6w)&nq|Y#8K%!6GZ2QSKTb`%$ z3fZ>{+WbzhbKE8?t5!s}?(f#PZ^l`Cy89ua>7VGH@y;Zlu%JU%-FpS2``0zP)S?M~ zYMcE+3MG7Uj=OMBeYTuBMD$bP8*mLO*t0C(S41!!jdI!=H4CS}wHew%@uOnH0Z!6q zos_8@nng~FU`KAV#e8;03_6sDa8IZg`}c(Z%?KG9_pr}Qda$=!#5pMW2uHg4cCb2Rtjhd*rqmR@Ph0qaA_#**$5H#z#5Q?MJ=)Ym$FZY9?% zS|;<*8MQ}Nr2o58>dbfJE24Op%Qi~X%CMlTv`)|WljBXHaM;DiJZ4^&B916Q8tH`B z-RQPdyR4W#97+6;nfFACb8g?Ma&$S41a8QYlLaXlK!T2Pm6n`mNlVU?fJ$0PA0<^z z7IYK^1HETh;ov5s#Q3_pMbh3JFLrfW89PH2b8Kmi@!LWkWApCXhc|bgSl5s0FiXum zOH6P?>2-|Y#)3HGd^xH%B$d%Dc>UAX!Tv6x z))ph)9&g0hbj*MckS!%+&ZVZ86Ki7DK>eB|4uq?38_etv!)W7hRZ-DI{NAdSd0U7JdT(r6szh|#Qp-YgZ1M|+?autgHT9)N;V z*=+WZE+#tuUAJQfygZURO})$U1FYaQ#g_IR^PIiC=Z=LB^;7cX?RSkMaK*8%=h7iu z$&u;eD}s+&3PSZCR;TJP(Ld&?*l!q;$yn^<>-AoQo`O)_u}9EtJ04EOEg&L9^MlNm z5eV1hasGwKu@{1_2Pb>;wVWSXVm4G{EX(7_nALToi@caH&RYn`95W@l$;-i zzxg}V%;`a%N@yzc?8*kM#E35mz2wCZcn2UTS7|4!+U+>gA*tq?>zzglk-I){VpB1! zv@IRN)o`N7;Ek}O`C$`Ii$&tzr`v{YKBS2?pR^Js(v*Jz`me?trK~fp%{Z8;)?)BvYwS>KaG-Vg!;)==52-4{=z4A@{ucqO zr$jL4JyU49Nq10&i%vjed#)3d+T8q2FnBT)9piD=py#!GLdYRlBx)gCVJAACE-2@9 z_|1WtF~PR8^M)a_FACc|?wxgSmv$j-rr)dWLl4yybP%YO0g=3!ZBa!thC}mtHPA*# zi(Yc}ks^)!S2T~f{lcyUH02Assd8V~RWSGh_g^XeM*g3w;SU8j + + + + + Welcome to Interface + + + + + +
+
+

Move around

+ Move around +

+ Move around with WASD & fly
+ up or down with E & C.
+ Cmnd/Ctrl+G will send you
+ home. Hitting Enter will let you
+ teleport to a user or location. +

+
+
+

Listen & talk

+ Talk +

+ Use your best headphones
+ and microphone for high
+ fidelity audio. +

+
+
+

Connect devices

+ Connect devices +

+ Have an Oculus Rift, a Razer
+ Hydra, or a PrimeSense 3D
+ camera? We support them all. +

+
+
+

Run a script

+ Run a script +

+ Cmnd/Cntrl+J will launch a
+ Running Scripts dialog to help
+ manage your scripts and search
+ for new ones to run. +

+
+
+

Script something

+ Write a script +

+ Write a script; we're always
+ adding new features.
+ Cmnd/Cntrl+J will launch a
+ Running Scripts dialog to help
+ manage your scripts. +

+
+
+

Import models

+ Import models +

+ Use the edit.js script to
+ add FBX models in-world. You
+ can use grids and fine tune
+ placement-related parameters
+ with ease. +

+
+
+
+

Read the docs

+

+ We are writing documentation on
+ just about everything. Please,
+ devour all we've written and make
+ suggestions where necessary.
+ Documentation is always at
+ docs.highfidelity.com +

+
+
+
+ + + + + diff --git a/interface/resources/icons/load-script.svg b/interface/resources/icons/load-script.svg new file mode 100644 index 0000000000..21be61c321 --- /dev/null +++ b/interface/resources/icons/load-script.svg @@ -0,0 +1,125 @@ + + + + + + + + + + image/svg+xml + + + + + T.Hofmeister + + + + + + + + + + + + + + + + + + + + + + diff --git a/interface/resources/icons/new-script.svg b/interface/resources/icons/new-script.svg new file mode 100644 index 0000000000..f68fcfa967 --- /dev/null +++ b/interface/resources/icons/new-script.svg @@ -0,0 +1,129 @@ + + + + + + + + + + image/svg+xml + + + + + T.Hofmeister + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/interface/resources/icons/save-script.svg b/interface/resources/icons/save-script.svg new file mode 100644 index 0000000000..04d41b8302 --- /dev/null +++ b/interface/resources/icons/save-script.svg @@ -0,0 +1,674 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + T.Hofmeister + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/interface/resources/icons/start-script.svg b/interface/resources/icons/start-script.svg new file mode 100644 index 0000000000..994eb61efe --- /dev/null +++ b/interface/resources/icons/start-script.svg @@ -0,0 +1,550 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + Maximillian Merlin + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/interface/resources/icons/stop-script.svg b/interface/resources/icons/stop-script.svg new file mode 100644 index 0000000000..31cdcee749 --- /dev/null +++ b/interface/resources/icons/stop-script.svg @@ -0,0 +1,163 @@ + + + + + + + + + + image/svg+xml + + + + + Maximillian Merlin + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/interface/resources/qml/AssetServer.qml b/interface/resources/qml/AssetServer.qml index 6f3076b408..cf61a2ae4a 100644 --- a/interface/resources/qml/AssetServer.qml +++ b/interface/resources/qml/AssetServer.qml @@ -206,7 +206,7 @@ ScrollingWindow { print("Error: model cannot be both static mesh and dynamic. This should never happen."); } else if (url) { var name = assetProxyModel.data(treeView.selection.currentIndex); - var addPosition = Vec3.sum(MyAvatar.position, Vec3.multiply(2, Quat.getForward(MyAvatar.orientation))); + var addPosition = Vec3.sum(MyAvatar.position, Vec3.multiply(2, Quat.getFront(MyAvatar.orientation))); var gravity; if (dynamic) { // Create a vector <0, -10, 0>. { x: 0, y: -10, z: 0 } won't work because Qt is dumb and this is a diff --git a/interface/resources/qml/AvatarInputs.qml b/interface/resources/qml/AvatarInputs.qml index 28f3c0c7b9..384504aaa0 100644 --- a/interface/resources/qml/AvatarInputs.qml +++ b/interface/resources/qml/AvatarInputs.qml @@ -15,11 +15,12 @@ import Qt.labs.settings 1.0 Hifi.AvatarInputs { id: root objectName: "AvatarInputs" - width: rootWidth - height: controls.height + width: mirrorWidth + height: controls.height + mirror.height x: 10; y: 5 - readonly property int rootWidth: 265 + readonly property int mirrorHeight: 215 + readonly property int mirrorWidth: 265 readonly property int iconSize: 24 readonly property int iconPadding: 5 @@ -38,15 +39,61 @@ Hifi.AvatarInputs { anchors.fill: parent } + Item { + id: mirror + width: root.mirrorWidth + height: root.mirrorVisible ? root.mirrorHeight : 0 + visible: root.mirrorVisible + anchors.left: parent.left + clip: true + + Image { + id: closeMirror + visible: hover.containsMouse + width: root.iconSize + height: root.iconSize + anchors.top: parent.top + anchors.topMargin: root.iconPadding + anchors.left: parent.left + anchors.leftMargin: root.iconPadding + source: "../images/close.svg" + MouseArea { + anchors.fill: parent + onClicked: { + root.closeMirror(); + } + } + } + + Image { + id: zoomIn + visible: hover.containsMouse + width: root.iconSize + height: root.iconSize + anchors.bottom: parent.bottom + anchors.bottomMargin: root.iconPadding + anchors.left: parent.left + anchors.leftMargin: root.iconPadding + source: root.mirrorZoomed ? "../images/minus.svg" : "../images/plus.svg" + MouseArea { + anchors.fill: parent + onClicked: { + root.toggleZoom(); + } + } + } + } + Item { id: controls - width: root.rootWidth + width: root.mirrorWidth height: 44 visible: root.showAudioTools + anchors.top: mirror.bottom Rectangle { anchors.fill: parent - color: "#00000000" + color: root.mirrorVisible ? (root.audioClipping ? "red" : "#696969") : "#00000000" Item { id: audioMeter diff --git a/interface/resources/qml/Stats.qml b/interface/resources/qml/Stats.qml index 17e6578e4d..564c74b526 100644 --- a/interface/resources/qml/Stats.qml +++ b/interface/resources/qml/Stats.qml @@ -198,7 +198,7 @@ Item { } StatText { visible: root.expanded; - text: "Audio Out Mic: " + root.audioOutboundPPS + " pps, " + + text: "Audio Out Mic: " + root.audioMicOutboundPPS + " pps, " + "Silent: " + root.audioSilentOutboundPPS + " pps"; } StatText { @@ -266,7 +266,7 @@ Item { text: "GPU Textures: "; } StatText { - text: " Pressure State: " + root.gpuTextureMemoryPressureState; + text: " Sparse Enabled: " + (0 == root.gpuSparseTextureEnabled ? "false" : "true"); } StatText { text: " Count: " + root.gpuTextures; @@ -278,10 +278,14 @@ Item { text: " Decimated: " + root.decimatedTextureCount; } StatText { - text: " Pending Transfer: " + root.texturePendingTransfers + " MB"; + text: " Sparse Count: " + root.gpuTexturesSparse; + visible: 0 != root.gpuSparseTextureEnabled; } StatText { - text: " Resource Memory: " + root.gpuTextureMemory + " MB"; + text: " Virtual Memory: " + root.gpuTextureVirtualMemory + " MB"; + } + StatText { + text: " Commited Memory: " + root.gpuTextureMemory + " MB"; } StatText { text: " Framebuffer Memory: " + root.gpuTextureFramebufferMemory + " MB"; diff --git a/interface/resources/styles/log_dialog.qss b/interface/resources/styles/log_dialog.qss index d3ae4e0a00..1fc4df0717 100644 --- a/interface/resources/styles/log_dialog.qss +++ b/interface/resources/styles/log_dialog.qss @@ -1,6 +1,6 @@ QPlainTextEdit { - font-family: Inconsolata, Consolas, Courier New, monospace; + font-family: Inconsolata, Lucida Console, Andale Mono, Monaco; font-size: 16px; padding-left: 28px; padding-top: 7px; @@ -11,7 +11,7 @@ QPlainTextEdit { } QLineEdit { - font-family: Inconsolata, Consolas, Courier New, monospace; + font-family: Inconsolata, Lucida Console, Andale Mono, Monaco; padding-left: 7px; background-color: #CCCCCC; border-width: 0; diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 78707ee635..1bb4c64884 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -177,8 +177,6 @@ #include "FrameTimingsScriptingInterface.h" #include #include -#include -#include // On Windows PC, NVidia Optimus laptop, we want to enable NVIDIA GPU // FIXME seems to be broken. @@ -215,10 +213,18 @@ static const QString FBX_EXTENSION = ".fbx"; static const QString OBJ_EXTENSION = ".obj"; static const QString AVA_JSON_EXTENSION = ".ava.json"; +static const int MIRROR_VIEW_TOP_PADDING = 5; +static const int MIRROR_VIEW_LEFT_PADDING = 10; +static const int MIRROR_VIEW_WIDTH = 265; +static const int MIRROR_VIEW_HEIGHT = 215; static const float MIRROR_FULLSCREEN_DISTANCE = 0.389f; +static const float MIRROR_REARVIEW_DISTANCE = 0.722f; +static const float MIRROR_REARVIEW_BODY_DISTANCE = 2.56f; +static const float MIRROR_FIELD_OF_VIEW = 30.0f; static const quint64 TOO_LONG_SINCE_LAST_SEND_DOWNSTREAM_AUDIO_STATS = 1 * USECS_PER_SECOND; +static const QString INFO_WELCOME_PATH = "html/interface-welcome.html"; static const QString INFO_EDIT_ENTITIES_PATH = "html/edit-commands.html"; static const QString INFO_HELP_PATH = "html/help.html"; @@ -417,7 +423,6 @@ static const QString STATE_CAMERA_THIRD_PERSON = "CameraThirdPerson"; static const QString STATE_CAMERA_ENTITY = "CameraEntity"; static const QString STATE_CAMERA_INDEPENDENT = "CameraIndependent"; static const QString STATE_SNAP_TURN = "SnapTurn"; -static const QString STATE_ADVANCED_MOVEMENT_CONTROLS = "AdvancedMovement"; static const QString STATE_GROUNDED = "Grounded"; static const QString STATE_NAV_FOCUSED = "NavigationFocused"; @@ -508,7 +513,7 @@ bool setupEssentials(int& argc, char** argv) { DependencyManager::set(); controller::StateController::setStateVariables({ { STATE_IN_HMD, STATE_CAMERA_FULL_SCREEN_MIRROR, STATE_CAMERA_FIRST_PERSON, STATE_CAMERA_THIRD_PERSON, STATE_CAMERA_ENTITY, STATE_CAMERA_INDEPENDENT, - STATE_SNAP_TURN, STATE_ADVANCED_MOVEMENT_CONTROLS, STATE_GROUNDED, STATE_NAV_FOCUSED } }); + STATE_SNAP_TURN, STATE_GROUNDED, STATE_NAV_FOCUSED } }); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); @@ -560,6 +565,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo _entityClipboardRenderer(false, this, this), _entityClipboard(new EntityTree()), _lastQueriedTime(usecTimestampNow()), + _mirrorViewRect(QRect(MIRROR_VIEW_LEFT_PADDING, MIRROR_VIEW_TOP_PADDING, MIRROR_VIEW_WIDTH, MIRROR_VIEW_HEIGHT)), _previousScriptLocation("LastScriptLocation", DESKTOP_LOCATION), _fieldOfView("fieldOfView", DEFAULT_FIELD_OF_VIEW_DEGREES), _hmdTabletScale("hmdTabletScale", DEFAULT_HMD_TABLET_SCALE_PERCENT), @@ -740,24 +746,23 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo } }); - auto audioScriptingInterface = DependencyManager::set(); + auto& audioScriptingInterface = AudioScriptingInterface::getInstance(); connect(audioThread, &QThread::started, audioIO.data(), &AudioClient::start); connect(audioIO.data(), &AudioClient::destroyed, audioThread, &QThread::quit); connect(audioThread, &QThread::finished, audioThread, &QThread::deleteLater); connect(audioIO.data(), &AudioClient::muteToggled, this, &Application::audioMuteToggled); - connect(audioIO.data(), &AudioClient::mutedByMixer, audioScriptingInterface.data(), &AudioScriptingInterface::mutedByMixer); - connect(audioIO.data(), &AudioClient::receivedFirstPacket, audioScriptingInterface.data(), &AudioScriptingInterface::receivedFirstPacket); - connect(audioIO.data(), &AudioClient::disconnected, audioScriptingInterface.data(), &AudioScriptingInterface::disconnected); + connect(audioIO.data(), &AudioClient::mutedByMixer, &audioScriptingInterface, &AudioScriptingInterface::mutedByMixer); + connect(audioIO.data(), &AudioClient::receivedFirstPacket, &audioScriptingInterface, &AudioScriptingInterface::receivedFirstPacket); + connect(audioIO.data(), &AudioClient::disconnected, &audioScriptingInterface, &AudioScriptingInterface::disconnected); connect(audioIO.data(), &AudioClient::muteEnvironmentRequested, [](glm::vec3 position, float radius) { auto audioClient = DependencyManager::get(); - auto audioScriptingInterface = DependencyManager::get(); auto myAvatarPosition = DependencyManager::get()->getMyAvatar()->getPosition(); float distance = glm::distance(myAvatarPosition, position); bool shouldMute = !audioClient->isMuted() && (distance < radius); if (shouldMute) { audioClient->toggleMute(); - audioScriptingInterface->environmentMuted(); + AudioScriptingInterface::getInstance().environmentMuted(); } }); @@ -1124,10 +1129,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo _applicationStateDevice->setInputVariant(STATE_SNAP_TURN, []() -> float { return qApp->getMyAvatar()->getSnapTurn() ? 1 : 0; }); - _applicationStateDevice->setInputVariant(STATE_ADVANCED_MOVEMENT_CONTROLS, []() -> float { - return qApp->getMyAvatar()->useAdvancedMovementControls() ? 1 : 0; - }); - _applicationStateDevice->setInputVariant(STATE_GROUNDED, []() -> float { return qApp->getMyAvatar()->getCharacterController()->onGround() ? 1 : 0; }); @@ -1182,10 +1183,10 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo // set the local loopback interface for local sounds AudioInjector::setLocalAudioInterface(audioIO.data()); - audioScriptingInterface->setLocalAudioInterface(audioIO.data()); - connect(audioIO.data(), &AudioClient::noiseGateOpened, audioScriptingInterface.data(), &AudioScriptingInterface::noiseGateOpened); - connect(audioIO.data(), &AudioClient::noiseGateClosed, audioScriptingInterface.data(), &AudioScriptingInterface::noiseGateClosed); - connect(audioIO.data(), &AudioClient::inputReceived, audioScriptingInterface.data(), &AudioScriptingInterface::inputReceived); + AudioScriptingInterface::getInstance().setLocalAudioInterface(audioIO.data()); + connect(audioIO.data(), &AudioClient::noiseGateOpened, &AudioScriptingInterface::getInstance(), &AudioScriptingInterface::noiseGateOpened); + connect(audioIO.data(), &AudioClient::noiseGateClosed, &AudioScriptingInterface::getInstance(), &AudioScriptingInterface::noiseGateClosed); + connect(audioIO.data(), &AudioClient::inputReceived, &AudioScriptingInterface::getInstance(), &AudioScriptingInterface::inputReceived); this->installEventFilter(this); @@ -1444,7 +1445,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo scriptEngines->loadScript(testScript, false); } else { // Get sandbox content set version, if available - auto acDirPath = PathUtils::getAppDataPath() + "../../" + BuildInfo::MODIFIED_ORGANIZATION + "/assignment-client/"; + auto acDirPath = PathUtils::getRootDataDirectory() + BuildInfo::MODIFIED_ORGANIZATION + "/assignment-client/"; auto contentVersionPath = acDirPath + "content-version.txt"; qCDebug(interfaceapp) << "Checking " << contentVersionPath << " for content version"; auto contentVersion = 0; @@ -1950,7 +1951,7 @@ void Application::initializeUi() { // For some reason there is already an "Application" object in the QML context, // though I can't find it. Hence, "ApplicationInterface" rootContext->setContextProperty("ApplicationInterface", this); - rootContext->setContextProperty("Audio", DependencyManager::get().data()); + rootContext->setContextProperty("Audio", &AudioScriptingInterface::getInstance()); rootContext->setContextProperty("AudioStats", DependencyManager::get()->getStats().data()); rootContext->setContextProperty("AudioScope", DependencyManager::get().data()); @@ -2118,6 +2119,21 @@ void Application::paintGL() { batch.resetStages(); }); + auto inputs = AvatarInputs::getInstance(); + if (inputs->mirrorVisible()) { + PerformanceTimer perfTimer("Mirror"); + + renderArgs._renderMode = RenderArgs::MIRROR_RENDER_MODE; + renderArgs._blitFramebuffer = DependencyManager::get()->getSelfieFramebuffer(); + + _mirrorViewRect.moveTo(inputs->x(), inputs->y()); + + renderRearViewMirror(&renderArgs, _mirrorViewRect, inputs->mirrorZoomed()); + + renderArgs._blitFramebuffer.reset(); + renderArgs._renderMode = RenderArgs::DEFAULT_RENDER_MODE; + } + { PerformanceTimer perfTimer("renderOverlay"); // NOTE: There is no batch associated with this renderArgs @@ -2132,7 +2148,7 @@ void Application::paintGL() { PerformanceTimer perfTimer("CameraUpdates"); auto myAvatar = getMyAvatar(); - boomOffset = myAvatar->getScale() * myAvatar->getBoomLength() * -IDENTITY_FORWARD; + boomOffset = myAvatar->getScale() * myAvatar->getBoomLength() * -IDENTITY_FRONT; if (_myCamera.getMode() == CAMERA_MODE_FIRST_PERSON || _myCamera.getMode() == CAMERA_MODE_THIRD_PERSON) { Menu::getInstance()->setIsOptionChecked(MenuOption::FirstPerson, myAvatar->getBoomLength() <= MyAvatar::ZOOM_MIN); @@ -2365,6 +2381,10 @@ void Application::setSettingConstrainToolbarPosition(bool setting) { DependencyManager::get()->setConstrainToolbarToCenterX(setting); } +void Application::aboutApp() { + InfoView::show(INFO_WELCOME_PATH); +} + void Application::showHelp() { static const QString HAND_CONTROLLER_NAME_VIVE = "vive"; static const QString HAND_CONTROLLER_NAME_OCULUS_TOUCH = "oculus"; @@ -2746,6 +2766,8 @@ void Application::keyPressEvent(QKeyEvent* event) { case Qt::Key_S: if (isShifted && isMeta && !isOption) { Menu::getInstance()->triggerOption(MenuOption::SuppressShortTimings); + } else if (isOption && !isShifted && !isMeta) { + Menu::getInstance()->triggerOption(MenuOption::ScriptEditor); } else if (!isOption && !isShifted && isMeta) { takeSnapshot(true); } @@ -2864,49 +2886,51 @@ void Application::keyPressEvent(QKeyEvent* event) { break; #endif - case Qt::Key_H: { - // whenever switching to/from full screen mirror from the keyboard, remember - // the state you were in before full screen mirror, and return to that. - auto previousMode = _myCamera.getMode(); - if (previousMode != CAMERA_MODE_MIRROR) { - switch (previousMode) { - case CAMERA_MODE_FIRST_PERSON: - _returnFromFullScreenMirrorTo = MenuOption::FirstPerson; - break; - case CAMERA_MODE_THIRD_PERSON: - _returnFromFullScreenMirrorTo = MenuOption::ThirdPerson; - break; + case Qt::Key_H: + if (isShifted) { + Menu::getInstance()->triggerOption(MenuOption::MiniMirror); + } else { + // whenever switching to/from full screen mirror from the keyboard, remember + // the state you were in before full screen mirror, and return to that. + auto previousMode = _myCamera.getMode(); + if (previousMode != CAMERA_MODE_MIRROR) { + switch (previousMode) { + case CAMERA_MODE_FIRST_PERSON: + _returnFromFullScreenMirrorTo = MenuOption::FirstPerson; + break; + case CAMERA_MODE_THIRD_PERSON: + _returnFromFullScreenMirrorTo = MenuOption::ThirdPerson; + break; - // FIXME - it's not clear that these modes make sense to return to... - case CAMERA_MODE_INDEPENDENT: - _returnFromFullScreenMirrorTo = MenuOption::IndependentMode; - break; - case CAMERA_MODE_ENTITY: - _returnFromFullScreenMirrorTo = MenuOption::CameraEntityMode; - break; + // FIXME - it's not clear that these modes make sense to return to... + case CAMERA_MODE_INDEPENDENT: + _returnFromFullScreenMirrorTo = MenuOption::IndependentMode; + break; + case CAMERA_MODE_ENTITY: + _returnFromFullScreenMirrorTo = MenuOption::CameraEntityMode; + break; - default: - _returnFromFullScreenMirrorTo = MenuOption::ThirdPerson; - break; + default: + _returnFromFullScreenMirrorTo = MenuOption::ThirdPerson; + break; + } } - } - bool isMirrorChecked = Menu::getInstance()->isOptionChecked(MenuOption::FullscreenMirror); - Menu::getInstance()->setIsOptionChecked(MenuOption::FullscreenMirror, !isMirrorChecked); - if (isMirrorChecked) { + bool isMirrorChecked = Menu::getInstance()->isOptionChecked(MenuOption::FullscreenMirror); + Menu::getInstance()->setIsOptionChecked(MenuOption::FullscreenMirror, !isMirrorChecked); + if (isMirrorChecked) { - // if we got here without coming in from a non-Full Screen mirror case, then our - // _returnFromFullScreenMirrorTo is unknown. In that case we'll go to the old - // behavior of returning to ThirdPerson - if (_returnFromFullScreenMirrorTo.isEmpty()) { - _returnFromFullScreenMirrorTo = MenuOption::ThirdPerson; + // if we got here without coming in from a non-Full Screen mirror case, then our + // _returnFromFullScreenMirrorTo is unknown. In that case we'll go to the old + // behavior of returning to ThirdPerson + if (_returnFromFullScreenMirrorTo.isEmpty()) { + _returnFromFullScreenMirrorTo = MenuOption::ThirdPerson; + } + Menu::getInstance()->setIsOptionChecked(_returnFromFullScreenMirrorTo, true); } - Menu::getInstance()->setIsOptionChecked(_returnFromFullScreenMirrorTo, true); + cameraMenuChanged(); } - cameraMenuChanged(); break; - } - case Qt::Key_P: { if (!(isShifted || isMeta || isOption)) { bool isFirstPersonChecked = Menu::getInstance()->isOptionChecked(MenuOption::FirstPerson); @@ -3821,6 +3845,8 @@ void Application::init() { DependencyManager::get()->init(); _myCamera.setMode(CAMERA_MODE_FIRST_PERSON); + _mirrorCamera.setMode(CAMERA_MODE_MIRROR); + _timerStart.start(); _lastTimeUpdated.start(); @@ -3955,7 +3981,7 @@ void Application::updateMyAvatarLookAtPosition() { auto lookingAtHead = static_pointer_cast(lookingAt)->getHead(); const float MAXIMUM_FACE_ANGLE = 65.0f * RADIANS_PER_DEGREE; - glm::vec3 lookingAtFaceOrientation = lookingAtHead->getFinalOrientationInWorldFrame() * IDENTITY_FORWARD; + glm::vec3 lookingAtFaceOrientation = lookingAtHead->getFinalOrientationInWorldFrame() * IDENTITY_FRONT; glm::vec3 fromLookingAtToMe = glm::normalize(myAvatar->getHead()->getEyePosition() - lookingAtHead->getEyePosition()); float faceAngle = glm::angle(lookingAtFaceOrientation, fromLookingAtToMe); @@ -4357,16 +4383,16 @@ void Application::update(float deltaTime) { myAvatar->clearDriveKeys(); if (_myCamera.getMode() != CAMERA_MODE_INDEPENDENT) { if (!_controllerScriptingInterface->areActionsCaptured()) { - myAvatar->setDriveKey(MyAvatar::TRANSLATE_Z, -1.0f * userInputMapper->getActionState(controller::Action::TRANSLATE_Z)); - myAvatar->setDriveKey(MyAvatar::TRANSLATE_Y, userInputMapper->getActionState(controller::Action::TRANSLATE_Y)); - myAvatar->setDriveKey(MyAvatar::TRANSLATE_X, userInputMapper->getActionState(controller::Action::TRANSLATE_X)); + myAvatar->setDriveKeys(TRANSLATE_Z, -1.0f * userInputMapper->getActionState(controller::Action::TRANSLATE_Z)); + myAvatar->setDriveKeys(TRANSLATE_Y, userInputMapper->getActionState(controller::Action::TRANSLATE_Y)); + myAvatar->setDriveKeys(TRANSLATE_X, userInputMapper->getActionState(controller::Action::TRANSLATE_X)); if (deltaTime > FLT_EPSILON) { - myAvatar->setDriveKey(MyAvatar::PITCH, -1.0f * userInputMapper->getActionState(controller::Action::PITCH)); - myAvatar->setDriveKey(MyAvatar::YAW, -1.0f * userInputMapper->getActionState(controller::Action::YAW)); - myAvatar->setDriveKey(MyAvatar::STEP_YAW, -1.0f * userInputMapper->getActionState(controller::Action::STEP_YAW)); + myAvatar->setDriveKeys(PITCH, -1.0f * userInputMapper->getActionState(controller::Action::PITCH)); + myAvatar->setDriveKeys(YAW, -1.0f * userInputMapper->getActionState(controller::Action::YAW)); + myAvatar->setDriveKeys(STEP_YAW, -1.0f * userInputMapper->getActionState(controller::Action::STEP_YAW)); } } - myAvatar->setDriveKey(MyAvatar::ZOOM, userInputMapper->getActionState(controller::Action::TRANSLATE_CAMERA_Z)); + myAvatar->setDriveKeys(ZOOM, userInputMapper->getActionState(controller::Action::TRANSLATE_CAMERA_Z)); } controller::Pose leftHandPose = userInputMapper->getPoseState(controller::Action::LEFT_HAND); @@ -4437,12 +4463,9 @@ void Application::update(float deltaTime) { getEntities()->getTree()->withWriteLock([&] { PerformanceTimer perfTimer("handleOutgoingChanges"); - const VectorOfMotionStates& deactivations = _physicsEngine->getDeactivatedMotionStates(); - _entitySimulation->handleDeactivatedMotionStates(deactivations); - - const VectorOfMotionStates& outgoingChanges = _physicsEngine->getChangedMotionStates(); - _entitySimulation->handleChangedMotionStates(outgoingChanges); - avatarManager->handleChangedMotionStates(outgoingChanges); + const VectorOfMotionStates& outgoingChanges = _physicsEngine->getOutgoingChanges(); + _entitySimulation->handleOutgoingChanges(outgoingChanges); + avatarManager->handleOutgoingChanges(outgoingChanges); }); if (!_aboutToQuit) { @@ -5099,6 +5122,58 @@ void Application::displaySide(RenderArgs* renderArgs, Camera& theCamera, bool se activeRenderingThread = nullptr; } +void Application::renderRearViewMirror(RenderArgs* renderArgs, const QRect& region, bool isZoomed) { + auto originalViewport = renderArgs->_viewport; + // Grab current viewport to reset it at the end + + float aspect = (float)region.width() / region.height(); + float fov = MIRROR_FIELD_OF_VIEW; + + auto myAvatar = getMyAvatar(); + + // bool eyeRelativeCamera = false; + if (!isZoomed) { + _mirrorCamera.setPosition(myAvatar->getChestPosition() + + myAvatar->getOrientation() * glm::vec3(0.0f, 0.0f, -1.0f) * MIRROR_REARVIEW_BODY_DISTANCE * myAvatar->getScale()); + + } else { // HEAD zoom level + // FIXME note that the positioning of the camera relative to the avatar can suffer limited + // precision as the user's position moves further away from the origin. Thus at + // /1e7,1e7,1e7 (well outside the buildable volume) the mirror camera veers and sways + // wildly as you rotate your avatar because the floating point values are becoming + // larger, squeezing out the available digits of precision you have available at the + // human scale for camera positioning. + + // Previously there was a hack to correct this using the mechanism of repositioning + // the avatar at the origin of the world for the purposes of rendering the mirror, + // but it resulted in failing to render the avatar's head model in the mirror view + // when in first person mode. Presumably this was because of some missed culling logic + // that was not accounted for in the hack. + + // This was removed in commit 71e59cfa88c6563749594e25494102fe01db38e9 but could be further + // investigated in order to adapt the technique while fixing the head rendering issue, + // but the complexity of the hack suggests that a better approach + _mirrorCamera.setPosition(myAvatar->getDefaultEyePosition() + + myAvatar->getOrientation() * glm::vec3(0.0f, 0.0f, -1.0f) * MIRROR_REARVIEW_DISTANCE * myAvatar->getScale()); + } + _mirrorCamera.setProjection(glm::perspective(glm::radians(fov), aspect, DEFAULT_NEAR_CLIP, DEFAULT_FAR_CLIP)); + _mirrorCamera.setOrientation(myAvatar->getWorldAlignedOrientation() * glm::quat(glm::vec3(0.0f, PI, 0.0f))); + + + // set the bounds of rear mirror view + // the region is in device independent coordinates; must convert to device + float ratio = (float)QApplication::desktop()->windowHandle()->devicePixelRatio() * getRenderResolutionScale(); + int width = region.width() * ratio; + int height = region.height() * ratio; + gpu::Vec4i viewport = gpu::Vec4i(0, 0, width, height); + renderArgs->_viewport = viewport; + + // render rear mirror view + displaySide(renderArgs, _mirrorCamera, true); + + renderArgs->_viewport = originalViewport; +} + void Application::resetSensors(bool andReload) { DependencyManager::get()->reset(); DependencyManager::get()->reset(); @@ -5428,7 +5503,8 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri scriptEngine->registerGlobalObject("Rates", new RatesScriptingInterface(this)); // hook our avatar and avatar hash map object into this script engine - getMyAvatar()->registerMetaTypes(scriptEngine); + scriptEngine->registerGlobalObject("MyAvatar", getMyAvatar().get()); + qScriptRegisterMetaType(scriptEngine, audioListenModeToScriptValue, audioListenModeFromScriptValue); scriptEngine->registerGlobalObject("AvatarList", DependencyManager::get().data()); diff --git a/interface/src/Application.h b/interface/src/Application.h index 7ae4160f8b..98080783a6 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -72,8 +72,6 @@ #include #include -#include - class OffscreenGLCanvas; class GLCanvas; @@ -278,6 +276,8 @@ public: virtual void pushPostUpdateLambda(void* key, std::function func) override; + const QRect& getMirrorViewRect() const { return _mirrorViewRect; } + void updateMyAvatarLookAtPosition(); float getAvatarSimrate() const { return _avatarSimCounter.rate(); } @@ -368,6 +368,7 @@ public slots: void calibrateEyeTracker5Points(); #endif + void aboutApp(); static void showHelp(); void cycleCamera(); @@ -556,6 +557,8 @@ private: int _avatarSimsPerSecondReport {0}; quint64 _lastAvatarSimsPerSecondUpdate {0}; Camera _myCamera; // My view onto the world + Camera _mirrorCamera; // Camera for mirror view + QRect _mirrorViewRect; Setting::Handle _previousScriptLocation; Setting::Handle _fieldOfView; diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index a48ee4e7db..beacbaccab 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -74,6 +74,9 @@ Menu::Menu() { // File > Help addActionToQMenuAndActionHash(fileMenu, MenuOption::Help, 0, qApp, SLOT(showHelp())); + // File > About + addActionToQMenuAndActionHash(fileMenu, MenuOption::AboutApp, 0, qApp, SLOT(aboutApp()), QAction::AboutRole); + // File > Quit addActionToQMenuAndActionHash(fileMenu, MenuOption::Quit, Qt::CTRL | Qt::Key_Q, qApp, SLOT(quit()), QAction::QuitRole); @@ -117,6 +120,11 @@ Menu::Menu() { scriptEngines.data(), SLOT(reloadAllScripts()), QAction::NoRole, UNSPECIFIED_POSITION, "Advanced"); + // Edit > Scripts Editor... [advanced] + addActionToQMenuAndActionHash(editMenu, MenuOption::ScriptEditor, Qt::ALT | Qt::Key_S, + dialogsManager.data(), SLOT(showScriptEditor()), + QAction::NoRole, UNSPECIFIED_POSITION, "Advanced"); + // Edit > Console... [advanced] addActionToQMenuAndActionHash(editMenu, MenuOption::Console, Qt::CTRL | Qt::ALT | Qt::Key_J, DependencyManager::get().data(), @@ -241,6 +249,9 @@ Menu::Menu() { viewMenu->addSeparator(); + // View > Mini Mirror + addCheckableActionToQMenuAndActionHash(viewMenu, MenuOption::MiniMirror, 0, false); + // View > Center Player In View addCheckableActionToQMenuAndActionHash(viewMenu, MenuOption::CenterPlayerInView, 0, true, qApp, SLOT(rotationModeChanged()), @@ -406,9 +417,6 @@ Menu::Menu() { } // Developer > Assets >>> - // Menu item is not currently needed but code should be kept in case it proves useful again at some stage. -//#define WANT_ASSET_MIGRATION -#ifdef WANT_ASSET_MIGRATION MenuWrapper* assetDeveloperMenu = developerMenu->addMenu("Assets"); auto& atpMigrator = ATPAssetMigrator::getInstance(); atpMigrator.setDialogParent(this); @@ -416,7 +424,6 @@ Menu::Menu() { addActionToQMenuAndActionHash(assetDeveloperMenu, MenuOption::AssetMigration, 0, &atpMigrator, SLOT(loadEntityServerFile())); -#endif // Developer > Avatar >>> MenuWrapper* avatarDebugMenu = developerMenu->addMenu("Avatar"); @@ -547,14 +554,16 @@ Menu::Menu() { "NetworkingPreferencesDialog"); }); addActionToQMenuAndActionHash(networkMenu, MenuOption::ReloadContent, 0, qApp, SLOT(reloadResourceCaches())); - addActionToQMenuAndActionHash(networkMenu, MenuOption::ClearDiskCache, 0, - DependencyManager::get().data(), SLOT(clearCache())); addCheckableActionToQMenuAndActionHash(networkMenu, MenuOption::DisableActivityLogger, 0, false, &UserActivityLogger::getInstance(), SLOT(disable(bool))); + addActionToQMenuAndActionHash(networkMenu, MenuOption::CachesSize, 0, + dialogsManager.data(), SLOT(cachesSizeDialog())); + addActionToQMenuAndActionHash(networkMenu, MenuOption::DiskCacheEditor, 0, + dialogsManager.data(), SLOT(toggleDiskCacheEditor())); addActionToQMenuAndActionHash(networkMenu, MenuOption::ShowDSConnectTable, 0, dialogsManager.data(), SLOT(showDomainConnectionDialog())); diff --git a/interface/src/Menu.h b/interface/src/Menu.h index b4eaf56758..c806ffa9ee 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -26,6 +26,7 @@ public: }; namespace MenuOption { + const QString AboutApp = "About Interface"; const QString AddRemoveFriends = "Add/Remove Friends..."; const QString AddressBar = "Show Address Bar"; const QString Animations = "Animations..."; @@ -51,11 +52,11 @@ namespace MenuOption { const QString BinaryEyelidControl = "Binary Eyelid Control"; const QString BookmarkLocation = "Bookmark Location"; const QString Bookmarks = "Bookmarks"; + const QString CachesSize = "RAM Caches Size"; const QString CalibrateCamera = "Calibrate Camera"; const QString CameraEntityMode = "Entity Mode"; const QString CenterPlayerInView = "Center Player In View"; const QString Chat = "Chat..."; - const QString ClearDiskCache = "Clear Disk Cache"; const QString Collisions = "Collisions"; const QString Connexion = "Activate 3D Connexion Devices"; const QString Console = "Console..."; @@ -82,6 +83,7 @@ namespace MenuOption { const QString DisableActivityLogger = "Disable Activity Logger"; const QString DisableEyelidAdjustment = "Disable Eyelid Adjustment"; const QString DisableLightEntities = "Disable Light Entities"; + const QString DiskCacheEditor = "Disk Cache Editor"; const QString DisplayCrashOptions = "Display Crash Options"; const QString DisplayHandTargets = "Show Hand Targets"; const QString DisplayModelBounds = "Display Model Bounds"; @@ -122,6 +124,7 @@ namespace MenuOption { const QString LogExtraTimings = "Log Extra Timing Details"; const QString LowVelocityFilter = "Low Velocity Filter"; const QString MeshVisible = "Draw Mesh"; + const QString MiniMirror = "Mini Mirror"; const QString MuteAudio = "Mute Microphone"; const QString MuteEnvironment = "Mute Environment"; const QString MuteFaceTracking = "Mute Face Tracking"; @@ -166,6 +169,7 @@ namespace MenuOption { const QString RunningScripts = "Running Scripts..."; const QString RunClientScriptTests = "Run Client Script Tests"; const QString RunTimingTests = "Run Timing Tests"; + const QString ScriptEditor = "Script Editor..."; const QString ScriptedMotorControl = "Enable Scripted Motor Control"; const QString SendWrongDSConnectVersion = "Send wrong DS connect version"; const QString SendWrongProtocolVersion = "Send wrong protocol version"; diff --git a/interface/src/avatar/Avatar.h b/interface/src/avatar/Avatar.h index d4bd03367e..ca4dbd2af8 100644 --- a/interface/src/avatar/Avatar.h +++ b/interface/src/avatar/Avatar.h @@ -236,6 +236,7 @@ protected: glm::vec3 getBodyRightDirection() const { return getOrientation() * IDENTITY_RIGHT; } glm::vec3 getBodyUpDirection() const { return getOrientation() * IDENTITY_UP; } + glm::vec3 getBodyFrontDirection() const { return getOrientation() * IDENTITY_FRONT; } glm::quat computeRotationFromBodyToWorldUp(float proportion = 1.0f) const; void measureMotionDerivatives(float deltaTime); diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 6152148887..94ce444416 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -424,7 +424,7 @@ void AvatarManager::getObjectsToChange(VectorOfMotionStates& result) { } } -void AvatarManager::handleChangedMotionStates(const VectorOfMotionStates& motionStates) { +void AvatarManager::handleOutgoingChanges(const VectorOfMotionStates& motionStates) { // TODO: extract the MyAvatar results once we use a MotionState for it. } diff --git a/interface/src/avatar/AvatarManager.h b/interface/src/avatar/AvatarManager.h index b94f9e6a96..e1f5a3b411 100644 --- a/interface/src/avatar/AvatarManager.h +++ b/interface/src/avatar/AvatarManager.h @@ -70,7 +70,7 @@ public: void getObjectsToRemoveFromPhysics(VectorOfMotionStates& motionStates); void getObjectsToAddToPhysics(VectorOfMotionStates& motionStates); void getObjectsToChange(VectorOfMotionStates& motionStates); - void handleChangedMotionStates(const VectorOfMotionStates& motionStates); + void handleOutgoingChanges(const VectorOfMotionStates& motionStates); void handleCollisionEvents(const CollisionEvents& collisionEvents); Q_INVOKABLE float getAvatarDataRate(const QUuid& sessionID, const QString& rateName = QString("")) const; diff --git a/interface/src/avatar/CauterizedMeshPartPayload.cpp b/interface/src/avatar/CauterizedMeshPartPayload.cpp index c11f92083b..c8ec90dcee 100644 --- a/interface/src/avatar/CauterizedMeshPartPayload.cpp +++ b/interface/src/avatar/CauterizedMeshPartPayload.cpp @@ -20,28 +20,55 @@ using namespace render; CauterizedMeshPartPayload::CauterizedMeshPartPayload(Model* model, int meshIndex, int partIndex, int shapeIndex, const Transform& transform, const Transform& offsetTransform) : ModelMeshPartPayload(model, meshIndex, partIndex, shapeIndex, transform, offsetTransform) {} -void CauterizedMeshPartPayload::updateTransformForCauterizedMesh( - const Transform& renderTransform, - const gpu::BufferPointer& buffer) { - _cauterizedTransform = renderTransform; - _cauterizedClusterBuffer = buffer; +void CauterizedMeshPartPayload::updateTransformForSkinnedCauterizedMesh(const Transform& transform, + const QVector& clusterMatrices, + const QVector& cauterizedClusterMatrices) { + _transform = transform; + _cauterizedTransform = transform; + + if (clusterMatrices.size() > 0) { + _worldBound = AABox(); + for (auto& clusterMatrix : clusterMatrices) { + AABox clusterBound = _localBound; + clusterBound.transform(clusterMatrix); + _worldBound += clusterBound; + } + + _worldBound.transform(transform); + if (clusterMatrices.size() == 1) { + _transform = _transform.worldTransform(Transform(clusterMatrices[0])); + if (cauterizedClusterMatrices.size() != 0) { + _cauterizedTransform = _cauterizedTransform.worldTransform(Transform(cauterizedClusterMatrices[0])); + } else { + _cauterizedTransform = _transform; + } + } + } else { + _worldBound = _localBound; + _worldBound.transform(_drawTransform); + } } void CauterizedMeshPartPayload::bindTransform(gpu::Batch& batch, const render::ShapePipeline::LocationsPointer locations, RenderArgs::RenderMode renderMode) const { // Still relying on the raw data from the model + const Model::MeshState& state = _model->getMeshState(_meshIndex); SkeletonModel* skeleton = static_cast(_model); bool useCauterizedMesh = (renderMode != RenderArgs::RenderMode::SHADOW_RENDER_MODE) && skeleton->getEnableCauterization(); - if (useCauterizedMesh) { - if (_cauterizedClusterBuffer) { - batch.setUniformBuffer(ShapePipeline::Slot::BUFFER::SKINNING, _cauterizedClusterBuffer); - } - batch.setModelTransform(_cauterizedTransform); - } else { - if (_clusterBuffer) { - batch.setUniformBuffer(ShapePipeline::Slot::BUFFER::SKINNING, _clusterBuffer); + if (state.clusterBuffer) { + if (useCauterizedMesh) { + const Model::MeshState& cState = skeleton->getCauterizeMeshState(_meshIndex); + batch.setUniformBuffer(ShapePipeline::Slot::BUFFER::SKINNING, cState.clusterBuffer); + } else { + batch.setUniformBuffer(ShapePipeline::Slot::BUFFER::SKINNING, state.clusterBuffer); } batch.setModelTransform(_transform); + } else { + if (useCauterizedMesh) { + batch.setModelTransform(_cauterizedTransform); + } else { + batch.setModelTransform(_transform); + } } } diff --git a/interface/src/avatar/CauterizedMeshPartPayload.h b/interface/src/avatar/CauterizedMeshPartPayload.h index dc88e950c1..f4319ead6f 100644 --- a/interface/src/avatar/CauterizedMeshPartPayload.h +++ b/interface/src/avatar/CauterizedMeshPartPayload.h @@ -17,13 +17,12 @@ class CauterizedMeshPartPayload : public ModelMeshPartPayload { public: CauterizedMeshPartPayload(Model* model, int meshIndex, int partIndex, int shapeIndex, const Transform& transform, const Transform& offsetTransform); - - void updateTransformForCauterizedMesh(const Transform& renderTransform, const gpu::BufferPointer& buffer); + void updateTransformForSkinnedCauterizedMesh(const Transform& transform, + const QVector& clusterMatrices, + const QVector& cauterizedClusterMatrices); void bindTransform(gpu::Batch& batch, const render::ShapePipeline::LocationsPointer locations, RenderArgs::RenderMode renderMode) const override; - private: - gpu::BufferPointer _cauterizedClusterBuffer; Transform _cauterizedTransform; }; diff --git a/interface/src/avatar/CauterizedModel.cpp b/interface/src/avatar/CauterizedModel.cpp index f479ed9a35..1ca87a498a 100644 --- a/interface/src/avatar/CauterizedModel.cpp +++ b/interface/src/avatar/CauterizedModel.cpp @@ -26,8 +26,8 @@ CauterizedModel::~CauterizedModel() { } void CauterizedModel::deleteGeometry() { - Model::deleteGeometry(); - _cauterizeMeshStates.clear(); + Model::deleteGeometry(); + _cauterizeMeshStates.clear(); } bool CauterizedModel::updateGeometry() { @@ -41,7 +41,7 @@ bool CauterizedModel::updateGeometry() { _cauterizeMeshStates.append(state); } } - return needsFullUpdate; + return needsFullUpdate; } void CauterizedModel::createVisibleRenderItemSet() { @@ -56,9 +56,9 @@ void CauterizedModel::createVisibleRenderItemSet() { } // We should not have any existing renderItems if we enter this section of code - Q_ASSERT(_modelMeshRenderItems.isEmpty()); + Q_ASSERT(_modelMeshRenderItemsSet.isEmpty()); - _modelMeshRenderItems.clear(); + _modelMeshRenderItemsSet.clear(); Transform transform; transform.setTranslation(_translation); @@ -81,18 +81,18 @@ void CauterizedModel::createVisibleRenderItemSet() { int numParts = (int)mesh->getNumParts(); for (int partIndex = 0; partIndex < numParts; partIndex++) { auto ptr = std::make_shared(this, i, partIndex, shapeID, transform, offset); - _modelMeshRenderItems << std::static_pointer_cast(ptr); + _modelMeshRenderItemsSet << std::static_pointer_cast(ptr); shapeID++; } } } else { - Model::createVisibleRenderItemSet(); + Model::createVisibleRenderItemSet(); } } void CauterizedModel::createCollisionRenderItemSet() { // Temporary HACK: use base class method for now - Model::createCollisionRenderItemSet(); + Model::createCollisionRenderItemSet(); } void CauterizedModel::updateClusterMatrices() { @@ -122,8 +122,8 @@ void CauterizedModel::updateClusterMatrices() { state.clusterBuffer->setSubData(0, state.clusterMatrices.size() * sizeof(glm::mat4), (const gpu::Byte*) state.clusterMatrices.constData()); } - } - } + } + } // as an optimization, don't build cautrizedClusterMatrices if the boneSet is empty. if (!_cauterizeBoneSet.empty()) { @@ -191,9 +191,6 @@ void CauterizedModel::updateRenderItems() { return; } - // lazy update of cluster matrices used for rendering. We need to update them here, so we can correctly update the bounding box. - self->updateClusterMatrices(); - render::ScenePointer scene = AbstractViewStateInterface::instance()->getMain3DScene(); Transform modelTransform; @@ -212,22 +209,15 @@ void CauterizedModel::updateRenderItems() { if (data._model && data._model->isLoaded()) { // Ensure the model geometry was not reset between frames if (deleteGeometryCounter == data._model->getGeometryCounter()) { - // this stuff identical to what happens in regular Model - const Model::MeshState& state = data._model->getMeshState(data._meshIndex); - Transform renderTransform = modelTransform; - if (state.clusterMatrices.size() == 1) { - renderTransform = modelTransform.worldTransform(Transform(state.clusterMatrices[0])); - } - data.updateTransformForSkinnedMesh(renderTransform, modelTransform, state.clusterBuffer); + // lazy update of cluster matrices used for rendering. We need to update them here, so we can correctly update the bounding box. + data._model->updateClusterMatrices(); - // this stuff for cauterized mesh + // update the model transform and bounding box for this render item. + const Model::MeshState& state = data._model->getMeshState(data._meshIndex); CauterizedModel* cModel = static_cast(data._model); - const Model::MeshState& cState = cModel->getCauterizeMeshState(data._meshIndex); - renderTransform = modelTransform; - if (cState.clusterMatrices.size() == 1) { - renderTransform = modelTransform.worldTransform(Transform(cState.clusterMatrices[0])); - } - data.updateTransformForCauterizedMesh(renderTransform, cState.clusterBuffer); + assert(data._meshIndex < cModel->_cauterizeMeshStates.size()); + const Model::MeshState& cState = cModel->_cauterizeMeshStates.at(data._meshIndex); + data.updateTransformForSkinnedCauterizedMesh(modelTransform, state.clusterMatrices, cState.clusterMatrices); } } }); diff --git a/interface/src/avatar/Head.cpp b/interface/src/avatar/Head.cpp index f4fb844d9b..d7bf2b79bf 100644 --- a/interface/src/avatar/Head.cpp +++ b/interface/src/avatar/Head.cpp @@ -268,7 +268,7 @@ void Head::applyEyelidOffset(glm::quat headOrientation) { return; } - glm::quat eyeRotation = rotationBetween(headOrientation * IDENTITY_FORWARD, getLookAtPosition() - _eyePosition); + glm::quat eyeRotation = rotationBetween(headOrientation * IDENTITY_FRONT, getLookAtPosition() - _eyePosition); eyeRotation = eyeRotation * glm::angleAxis(safeEulerAngles(headOrientation).y, IDENTITY_UP); // Rotation w.r.t. head float eyePitch = safeEulerAngles(eyeRotation).x; @@ -375,7 +375,7 @@ glm::quat Head::getCameraOrientation() const { glm::quat Head::getEyeRotation(const glm::vec3& eyePosition) const { glm::quat orientation = getOrientation(); glm::vec3 lookAtDelta = _lookAtPosition - eyePosition; - return rotationBetween(orientation * IDENTITY_FORWARD, lookAtDelta + glm::length(lookAtDelta) * _saccade) * orientation; + return rotationBetween(orientation * IDENTITY_FRONT, lookAtDelta + glm::length(lookAtDelta) * _saccade) * orientation; } void Head::setFinalPitch(float finalPitch) { diff --git a/interface/src/avatar/Head.h b/interface/src/avatar/Head.h index aa801e5eb5..3d25c79087 100644 --- a/interface/src/avatar/Head.h +++ b/interface/src/avatar/Head.h @@ -58,14 +58,14 @@ public: const glm::vec3& getSaccade() const { return _saccade; } glm::vec3 getRightDirection() const { return getOrientation() * IDENTITY_RIGHT; } glm::vec3 getUpDirection() const { return getOrientation() * IDENTITY_UP; } - glm::vec3 getForwardDirection() const { return getOrientation() * IDENTITY_FORWARD; } + glm::vec3 getFrontDirection() const { return getOrientation() * IDENTITY_FRONT; } glm::quat getEyeRotation(const glm::vec3& eyePosition) const; const glm::vec3& getRightEyePosition() const { return _rightEyePosition; } const glm::vec3& getLeftEyePosition() const { return _leftEyePosition; } - glm::vec3 getRightEarPosition() const { return _rightEyePosition + (getRightDirection() * EYE_EAR_GAP) + (getForwardDirection() * -EYE_EAR_GAP); } - glm::vec3 getLeftEarPosition() const { return _leftEyePosition + (getRightDirection() * -EYE_EAR_GAP) + (getForwardDirection() * -EYE_EAR_GAP); } + glm::vec3 getRightEarPosition() const { return _rightEyePosition + (getRightDirection() * EYE_EAR_GAP) + (getFrontDirection() * -EYE_EAR_GAP); } + glm::vec3 getLeftEarPosition() const { return _leftEyePosition + (getRightDirection() * -EYE_EAR_GAP) + (getFrontDirection() * -EYE_EAR_GAP); } glm::vec3 getMouthPosition() const { return _eyePosition - getUpDirection() * glm::length(_rightEyePosition - _leftEyePosition); } bool getReturnToCenter() const { return _returnHeadToCenter; } // Do you want head to try to return to center (depends on interface detected) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index e0f4b55393..969268c549 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -104,7 +104,6 @@ MyAvatar::MyAvatar(RigPointer rig) : _eyeContactTarget(LEFT_EYE), _realWorldFieldOfView("realWorldFieldOfView", DEFAULT_REAL_WORLD_FIELD_OF_VIEW_DEGREES), - _useAdvancedMovementControls("advancedMovementForHandControllersIsChecked", false), _hmdSensorMatrix(), _hmdSensorOrientation(), _hmdSensorPosition(), @@ -120,7 +119,9 @@ MyAvatar::MyAvatar(RigPointer rig) : using namespace recording; _skeletonModel->flagAsCauterized(); - clearDriveKeys(); + for (int i = 0; i < MAX_DRIVE_KEYS; i++) { + _driveKeys[i] = 0.0f; + } // Necessary to select the correct slot using SlotType = void(MyAvatar::*)(const glm::vec3&, bool, const glm::quat&, bool); @@ -153,12 +154,9 @@ MyAvatar::MyAvatar(RigPointer rig) : if (recordingInterface->getPlayFromCurrentLocation()) { setRecordingBasis(); } - _wasCharacterControllerEnabled = _characterController.isEnabled(); - _characterController.setEnabled(false); } else { clearRecordingBasis(); useFullAvatarURL(_fullAvatarURLFromPreferences, _fullAvatarModelName); - _characterController.setEnabled(_wasCharacterControllerEnabled); } auto audioIO = DependencyManager::get(); @@ -229,21 +227,6 @@ MyAvatar::~MyAvatar() { _lookAtTargetAvatar.reset(); } -void MyAvatar::registerMetaTypes(QScriptEngine* engine) { - QScriptValue value = engine->newQObject(this, QScriptEngine::QtOwnership, QScriptEngine::ExcludeDeleteLater | QScriptEngine::ExcludeChildObjects); - engine->globalObject().setProperty("MyAvatar", value); - - QScriptValue driveKeys = engine->newObject(); - auto metaEnum = QMetaEnum::fromType(); - for (int i = 0; i < MAX_DRIVE_KEYS; ++i) { - driveKeys.setProperty(metaEnum.key(i), metaEnum.value(i)); - } - engine->globalObject().setProperty("DriveKeys", driveKeys); - - qScriptRegisterMetaType(engine, audioListenModeToScriptValue, audioListenModeFromScriptValue); - qScriptRegisterMetaType(engine, driveKeysToScriptValue, driveKeysFromScriptValue); -} - void MyAvatar::setOrientationVar(const QVariant& newOrientationVar) { Avatar::setOrientation(quatFromVariant(newOrientationVar)); } @@ -476,7 +459,7 @@ void MyAvatar::simulate(float deltaTime) { // When there are no step values, we zero out the last step pulse. // This allows a user to do faster snapping by tapping a control for (int i = STEP_TRANSLATE_X; !stepAction && i <= STEP_YAW; ++i) { - if (getDriveKey((DriveKeys)i) != 0.0f) { + if (_driveKeys[i] != 0.0f) { stepAction = true; } } @@ -1068,7 +1051,7 @@ void MyAvatar::updateLookAtTargetAvatar() { _lookAtTargetAvatar.reset(); _targetAvatarPosition = glm::vec3(0.0f); - glm::vec3 lookForward = getHead()->getFinalOrientationInWorldFrame() * IDENTITY_FORWARD; + glm::vec3 lookForward = getHead()->getFinalOrientationInWorldFrame() * IDENTITY_FRONT; glm::vec3 cameraPosition = qApp->getCamera()->getPosition(); float smallestAngleTo = glm::radians(DEFAULT_FIELD_OF_VIEW_DEGREES) / 2.0f; @@ -1669,7 +1652,7 @@ bool MyAvatar::shouldRenderHead(const RenderArgs* renderArgs) const { void MyAvatar::updateOrientation(float deltaTime) { // Smoothly rotate body with arrow keys - float targetSpeed = getDriveKey(YAW) * _yawSpeed; + float targetSpeed = _driveKeys[YAW] * _yawSpeed; if (targetSpeed != 0.0f) { const float ROTATION_RAMP_TIMESCALE = 0.1f; float blend = deltaTime / ROTATION_RAMP_TIMESCALE; @@ -1698,8 +1681,8 @@ void MyAvatar::updateOrientation(float deltaTime) { // Comfort Mode: If you press any of the left/right rotation drive keys or input, you'll // get an instantaneous 15 degree turn. If you keep holding the key down you'll get another // snap turn every half second. - if (getDriveKey(STEP_YAW) != 0.0f) { - totalBodyYaw += getDriveKey(STEP_YAW); + if (_driveKeys[STEP_YAW] != 0.0f) { + totalBodyYaw += _driveKeys[STEP_YAW]; } // use head/HMD orientation to turn while flying @@ -1736,7 +1719,7 @@ void MyAvatar::updateOrientation(float deltaTime) { // update body orientation by movement inputs setOrientation(getOrientation() * glm::quat(glm::radians(glm::vec3(0.0f, totalBodyYaw, 0.0f)))); - getHead()->setBasePitch(getHead()->getBasePitch() + getDriveKey(PITCH) * _pitchSpeed * deltaTime); + getHead()->setBasePitch(getHead()->getBasePitch() + _driveKeys[PITCH] * _pitchSpeed * deltaTime); if (qApp->isHMDMode()) { glm::quat orientation = glm::quat_cast(getSensorToWorldMatrix()) * getHMDSensorOrientation(); @@ -1770,14 +1753,14 @@ void MyAvatar::updateActionMotor(float deltaTime) { } // compute action input - glm::vec3 forward = (getDriveKey(TRANSLATE_Z)) * IDENTITY_FORWARD; - glm::vec3 right = (getDriveKey(TRANSLATE_X)) * IDENTITY_RIGHT; + glm::vec3 front = (_driveKeys[TRANSLATE_Z]) * IDENTITY_FRONT; + glm::vec3 right = (_driveKeys[TRANSLATE_X]) * IDENTITY_RIGHT; - glm::vec3 direction = forward + right; + glm::vec3 direction = front + right; CharacterController::State state = _characterController.getState(); if (state == CharacterController::State::Hover) { // we're flying --> support vertical motion - glm::vec3 up = (getDriveKey(TRANSLATE_Y)) * IDENTITY_UP; + glm::vec3 up = (_driveKeys[TRANSLATE_Y]) * IDENTITY_UP; direction += up; } @@ -1816,7 +1799,7 @@ void MyAvatar::updateActionMotor(float deltaTime) { _actionMotorVelocity = MAX_WALKING_SPEED * direction; } - float boomChange = getDriveKey(ZOOM); + float boomChange = _driveKeys[ZOOM]; _boomLength += 2.0f * _boomLength * boomChange + boomChange * boomChange; _boomLength = glm::clamp(_boomLength, ZOOM_MIN, ZOOM_MAX); } @@ -1847,11 +1830,11 @@ void MyAvatar::updatePosition(float deltaTime) { } // capture the head rotation, in sensor space, when the user first indicates they would like to move/fly. - if (!_hoverReferenceCameraFacingIsCaptured && (fabs(getDriveKey(TRANSLATE_Z)) > 0.1f || fabs(getDriveKey(TRANSLATE_X)) > 0.1f)) { + if (!_hoverReferenceCameraFacingIsCaptured && (fabs(_driveKeys[TRANSLATE_Z]) > 0.1f || fabs(_driveKeys[TRANSLATE_X]) > 0.1f)) { _hoverReferenceCameraFacingIsCaptured = true; // transform the camera facing vector into sensor space. _hoverReferenceCameraFacing = transformVectorFast(glm::inverse(_sensorToWorldMatrix), getHead()->getCameraOrientation() * Vectors::UNIT_Z); - } else if (_hoverReferenceCameraFacingIsCaptured && (fabs(getDriveKey(TRANSLATE_Z)) <= 0.1f && fabs(getDriveKey(TRANSLATE_X)) <= 0.1f)) { + } else if (_hoverReferenceCameraFacingIsCaptured && (fabs(_driveKeys[TRANSLATE_Z]) <= 0.1f && fabs(_driveKeys[TRANSLATE_X]) <= 0.1f)) { _hoverReferenceCameraFacingIsCaptured = false; } } @@ -2053,7 +2036,7 @@ void MyAvatar::goToLocation(const glm::vec3& newPosition, // move the user a couple units away const float DISTANCE_TO_USER = 2.0f; - _goToPosition = newPosition - quatOrientation * IDENTITY_FORWARD * DISTANCE_TO_USER; + _goToPosition = newPosition - quatOrientation * IDENTITY_FRONT * DISTANCE_TO_USER; } _goToOrientation = quatOrientation; @@ -2107,61 +2090,17 @@ bool MyAvatar::getCharacterControllerEnabled() { } void MyAvatar::clearDriveKeys() { - _driveKeys.fill(0.0f); -} - -void MyAvatar::setDriveKey(DriveKeys key, float val) { - try { - _driveKeys.at(key) = val; - } catch (const std::exception&) { - qCCritical(interfaceapp) << Q_FUNC_INFO << ": Index out of bounds"; - } -} - -float MyAvatar::getDriveKey(DriveKeys key) const { - return isDriveKeyDisabled(key) ? 0.0f : getRawDriveKey(key); -} - -float MyAvatar::getRawDriveKey(DriveKeys key) const { - try { - return _driveKeys.at(key); - } catch (const std::exception&) { - qCCritical(interfaceapp) << Q_FUNC_INFO << ": Index out of bounds"; - return 0.0f; + for (int i = 0; i < MAX_DRIVE_KEYS; ++i) { + _driveKeys[i] = 0.0f; } } void MyAvatar::relayDriveKeysToCharacterController() { - if (getDriveKey(TRANSLATE_Y) > 0.0f) { + if (_driveKeys[TRANSLATE_Y] > 0.0f) { _characterController.jump(); } } -void MyAvatar::disableDriveKey(DriveKeys key) { - try { - _disabledDriveKeys.set(key); - } catch (const std::exception&) { - qCCritical(interfaceapp) << Q_FUNC_INFO << ": Index out of bounds"; - } -} - -void MyAvatar::enableDriveKey(DriveKeys key) { - try { - _disabledDriveKeys.reset(key); - } catch (const std::exception&) { - qCCritical(interfaceapp) << Q_FUNC_INFO << ": Index out of bounds"; - } -} - -bool MyAvatar::isDriveKeyDisabled(DriveKeys key) const { - try { - return _disabledDriveKeys.test(key); - } catch (const std::exception&) { - qCCritical(interfaceapp) << Q_FUNC_INFO << ": Index out of bounds"; - return true; - } -} - glm::vec3 MyAvatar::getWorldBodyPosition() const { return transformPoint(_sensorToWorldMatrix, extractTranslation(_bodySensorMatrix)); } @@ -2247,15 +2186,7 @@ QScriptValue audioListenModeToScriptValue(QScriptEngine* engine, const AudioList } void audioListenModeFromScriptValue(const QScriptValue& object, AudioListenerMode& audioListenerMode) { - audioListenerMode = static_cast(object.toUInt16()); -} - -QScriptValue driveKeysToScriptValue(QScriptEngine* engine, const MyAvatar::DriveKeys& driveKeys) { - return driveKeys; -} - -void driveKeysFromScriptValue(const QScriptValue& object, MyAvatar::DriveKeys& driveKeys) { - driveKeys = static_cast(object.toUInt16()); + audioListenerMode = (AudioListenerMode)object.toUInt16(); } @@ -2448,7 +2379,7 @@ bool MyAvatar::didTeleport() { } bool MyAvatar::hasDriveInput() const { - return fabsf(getDriveKey(TRANSLATE_X)) > 0.0f || fabsf(getDriveKey(TRANSLATE_Y)) > 0.0f || fabsf(getDriveKey(TRANSLATE_Z)) > 0.0f; + return fabsf(_driveKeys[TRANSLATE_X]) > 0.0f || fabsf(_driveKeys[TRANSLATE_Y]) > 0.0f || fabsf(_driveKeys[TRANSLATE_Z]) > 0.0f; } void MyAvatar::setAway(bool value) { @@ -2564,7 +2495,7 @@ bool MyAvatar::pinJoint(int index, const glm::vec3& position, const glm::quat& o return false; } - slamPosition(position); + setPosition(position); setOrientation(orientation); _rig->setMaxHipsOffsetLength(0.05f); diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 5f812f1f99..3cc665b533 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -12,8 +12,6 @@ #ifndef hifi_MyAvatar_h #define hifi_MyAvatar_h -#include - #include #include @@ -31,6 +29,20 @@ class AvatarActionHold; class ModelItemID; +enum DriveKeys { + TRANSLATE_X = 0, + TRANSLATE_Y, + TRANSLATE_Z, + YAW, + STEP_TRANSLATE_X, + STEP_TRANSLATE_Y, + STEP_TRANSLATE_Z, + STEP_YAW, + PITCH, + ZOOM, + MAX_DRIVE_KEYS +}; + enum eyeContactTarget { LEFT_EYE, RIGHT_EYE, @@ -74,29 +86,11 @@ class MyAvatar : public Avatar { Q_PROPERTY(bool hmdLeanRecenterEnabled READ getHMDLeanRecenterEnabled WRITE setHMDLeanRecenterEnabled) Q_PROPERTY(bool characterControllerEnabled READ getCharacterControllerEnabled WRITE setCharacterControllerEnabled) - Q_PROPERTY(bool useAdvancedMovementControls READ useAdvancedMovementControls WRITE setUseAdvancedMovementControls) public: - enum DriveKeys { - TRANSLATE_X = 0, - TRANSLATE_Y, - TRANSLATE_Z, - YAW, - STEP_TRANSLATE_X, - STEP_TRANSLATE_Y, - STEP_TRANSLATE_Z, - STEP_YAW, - PITCH, - ZOOM, - MAX_DRIVE_KEYS - }; - Q_ENUM(DriveKeys) - explicit MyAvatar(RigPointer rig); ~MyAvatar(); - void registerMetaTypes(QScriptEngine* engine); - virtual void simulateAttachments(float deltaTime) override; AudioListenerMode getAudioListenerModeHead() const { return FROM_HEAD; } @@ -177,10 +171,6 @@ public: Q_INVOKABLE void setHMDLeanRecenterEnabled(bool value) { _hmdLeanRecenterEnabled = value; } Q_INVOKABLE bool getHMDLeanRecenterEnabled() const { return _hmdLeanRecenterEnabled; } - bool useAdvancedMovementControls() const { return _useAdvancedMovementControls.get(); } - void setUseAdvancedMovementControls(bool useAdvancedMovementControls) - { _useAdvancedMovementControls.set(useAdvancedMovementControls); } - // get/set avatar data void saveData(); void loadData(); @@ -190,15 +180,9 @@ public: // Set what driving keys are being pressed to control thrust levels void clearDriveKeys(); - void setDriveKey(DriveKeys key, float val); - float getDriveKey(DriveKeys key) const; - Q_INVOKABLE float getRawDriveKey(DriveKeys key) const; + void setDriveKeys(int key, float val) { _driveKeys[key] = val; }; void relayDriveKeysToCharacterController(); - Q_INVOKABLE void disableDriveKey(DriveKeys key); - Q_INVOKABLE void enableDriveKey(DriveKeys key); - Q_INVOKABLE bool isDriveKeyDisabled(DriveKeys key) const; - eyeContactTarget getEyeContactTarget(); Q_INVOKABLE glm::vec3 getTrackedHeadPosition() const { return _trackedHeadPosition; } @@ -368,6 +352,7 @@ private: virtual bool shouldRenderHead(const RenderArgs* renderArgs) const override; void setShouldRenderLocally(bool shouldRender) { _shouldRender = shouldRender; setEnableMeshVisible(shouldRender); } bool getShouldRenderLocally() const { return _shouldRender; } + bool getDriveKeys(int key) { return _driveKeys[key] != 0.0f; }; bool isMyAvatar() const override { return true; } virtual int parseDataFromBuffer(const QByteArray& buffer) override; virtual glm::vec3 getSkeletonPosition() const override; @@ -403,9 +388,7 @@ private: void clampScaleChangeToDomainLimits(float desiredScale); glm::mat4 computeCameraRelativeHandControllerMatrix(const glm::mat4& controllerSensorMatrix) const; - std::array _driveKeys; - std::bitset _disabledDriveKeys; - + float _driveKeys[MAX_DRIVE_KEYS]; bool _wasPushing; bool _isPushing; bool _isBeingPushed; @@ -428,7 +411,6 @@ private: SharedSoundPointer _collisionSound; MyCharacterController _characterController; - bool _wasCharacterControllerEnabled { true }; AvatarWeakPointer _lookAtTargetAvatar; glm::vec3 _targetAvatarPosition; @@ -441,7 +423,6 @@ private: glm::vec3 _trackedHeadPosition; Setting::Handle _realWorldFieldOfView; - Setting::Handle _useAdvancedMovementControls; // private methods void updateOrientation(float deltaTime); @@ -559,7 +540,4 @@ private: QScriptValue audioListenModeToScriptValue(QScriptEngine* engine, const AudioListenerMode& audioListenerMode); void audioListenModeFromScriptValue(const QScriptValue& object, AudioListenerMode& audioListenerMode); -QScriptValue driveKeysToScriptValue(QScriptEngine* engine, const MyAvatar::DriveKeys& driveKeys); -void driveKeysFromScriptValue(const QScriptValue& object, MyAvatar::DriveKeys& driveKeys); - #endif // hifi_MyAvatar_h diff --git a/interface/src/ui/ApplicationOverlay.cpp b/interface/src/ui/ApplicationOverlay.cpp index f2d97a0137..364dff52a3 100644 --- a/interface/src/ui/ApplicationOverlay.cpp +++ b/interface/src/ui/ApplicationOverlay.cpp @@ -13,6 +13,7 @@ #include #include +#include #include #include #include @@ -41,6 +42,7 @@ ApplicationOverlay::ApplicationOverlay() _domainStatusBorder = geometryCache->allocateID(); _magnifierBorder = geometryCache->allocateID(); _qmlGeometryId = geometryCache->allocateID(); + _rearViewGeometryId = geometryCache->allocateID(); } ApplicationOverlay::~ApplicationOverlay() { @@ -49,6 +51,7 @@ ApplicationOverlay::~ApplicationOverlay() { geometryCache->releaseID(_domainStatusBorder); geometryCache->releaseID(_magnifierBorder); geometryCache->releaseID(_qmlGeometryId); + geometryCache->releaseID(_rearViewGeometryId); } } @@ -83,6 +86,7 @@ void ApplicationOverlay::renderOverlay(RenderArgs* renderArgs) { // Now render the overlay components together into a single texture renderDomainConnectionStatusBorder(renderArgs); // renders the connected domain line renderAudioScope(renderArgs); // audio scope in the very back - NOTE: this is the debug audio scope, not the VU meter + renderRearView(renderArgs); // renders the mirror view selfie renderOverlays(renderArgs); // renders Scripts Overlay and AudioScope renderQmlUi(renderArgs); // renders a unit quad with the QML UI texture, and the text overlays from scripts renderStatsAndLogs(renderArgs); // currently renders nothing @@ -95,7 +99,7 @@ void ApplicationOverlay::renderQmlUi(RenderArgs* renderArgs) { PROFILE_RANGE(app, __FUNCTION__); if (!_uiTexture) { - _uiTexture = gpu::TexturePointer(gpu::Texture::createExternal(OffscreenQmlSurface::getDiscardLambda())); + _uiTexture = gpu::TexturePointer(gpu::Texture::createExternal2D(OffscreenQmlSurface::getDiscardLambda())); _uiTexture->setSource(__FUNCTION__); } // Once we move UI rendering and screen rendering to different @@ -159,6 +163,45 @@ void ApplicationOverlay::renderOverlays(RenderArgs* renderArgs) { qApp->getOverlays().renderHUD(renderArgs); } +void ApplicationOverlay::renderRearViewToFbo(RenderArgs* renderArgs) { +} + +void ApplicationOverlay::renderRearView(RenderArgs* renderArgs) { + if (!qApp->isHMDMode() && Menu::getInstance()->isOptionChecked(MenuOption::MiniMirror) && + !Menu::getInstance()->isOptionChecked(MenuOption::FullscreenMirror)) { + gpu::Batch& batch = *renderArgs->_batch; + + auto geometryCache = DependencyManager::get(); + + auto framebuffer = DependencyManager::get(); + auto selfieTexture = framebuffer->getSelfieFramebuffer()->getRenderBuffer(0); + + int width = renderArgs->_viewport.z; + int height = renderArgs->_viewport.w; + mat4 legacyProjection = glm::ortho(0, width, height, 0, ORTHO_NEAR_CLIP, ORTHO_FAR_CLIP); + batch.setProjectionTransform(legacyProjection); + batch.setModelTransform(Transform()); + batch.resetViewTransform(); + + float screenRatio = ((float)qApp->getDevicePixelRatio()); + float renderRatio = ((float)qApp->getRenderResolutionScale()); + + auto viewport = qApp->getMirrorViewRect(); + glm::vec2 bottomLeft(viewport.left(), viewport.top() + viewport.height()); + glm::vec2 topRight(viewport.left() + viewport.width(), viewport.top()); + bottomLeft *= screenRatio; + topRight *= screenRatio; + glm::vec2 texCoordMinCorner(0.0f, 0.0f); + glm::vec2 texCoordMaxCorner(viewport.width() * renderRatio / float(selfieTexture->getWidth()), viewport.height() * renderRatio / float(selfieTexture->getHeight())); + + batch.setResourceTexture(0, selfieTexture); + float alpha = DependencyManager::get()->getDesktop()->property("unpinnedAlpha").toFloat(); + geometryCache->renderQuad(batch, bottomLeft, topRight, texCoordMinCorner, texCoordMaxCorner, glm::vec4(1.0f, 1.0f, 1.0f, alpha), _rearViewGeometryId); + + batch.setResourceTexture(0, renderArgs->_whiteTexture); + } +} + void ApplicationOverlay::renderStatsAndLogs(RenderArgs* renderArgs) { // Display stats and log text onscreen @@ -229,13 +272,13 @@ void ApplicationOverlay::buildFramebufferObject() { auto width = uiSize.x; auto height = uiSize.y; if (!_overlayFramebuffer->getDepthStencilBuffer()) { - auto overlayDepthTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(DEPTH_FORMAT, width, height, DEFAULT_SAMPLER)); + auto overlayDepthTexture = gpu::TexturePointer(gpu::Texture::create2D(DEPTH_FORMAT, width, height, DEFAULT_SAMPLER)); _overlayFramebuffer->setDepthStencilBuffer(overlayDepthTexture, DEPTH_FORMAT); } if (!_overlayFramebuffer->getRenderBuffer(0)) { const gpu::Sampler OVERLAY_SAMPLER(gpu::Sampler::FILTER_MIN_MAG_LINEAR, gpu::Sampler::WRAP_CLAMP); - auto colorBuffer = gpu::TexturePointer(gpu::Texture::createRenderBuffer(COLOR_FORMAT, width, height, OVERLAY_SAMPLER)); + auto colorBuffer = gpu::TexturePointer(gpu::Texture::create2D(COLOR_FORMAT, width, height, OVERLAY_SAMPLER)); _overlayFramebuffer->setRenderBuffer(0, colorBuffer); } } diff --git a/interface/src/ui/ApplicationOverlay.h b/interface/src/ui/ApplicationOverlay.h index af4d8779d4..7ace5ee885 100644 --- a/interface/src/ui/ApplicationOverlay.h +++ b/interface/src/ui/ApplicationOverlay.h @@ -31,6 +31,8 @@ public: private: void renderStatsAndLogs(RenderArgs* renderArgs); void renderDomainConnectionStatusBorder(RenderArgs* renderArgs); + void renderRearViewToFbo(RenderArgs* renderArgs); + void renderRearView(RenderArgs* renderArgs); void renderQmlUi(RenderArgs* renderArgs); void renderAudioScope(RenderArgs* renderArgs); void renderOverlays(RenderArgs* renderArgs); @@ -49,6 +51,7 @@ private: gpu::TexturePointer _overlayColorTexture; gpu::FramebufferPointer _overlayFramebuffer; int _qmlGeometryId { 0 }; + int _rearViewGeometryId { 0 }; }; #endif // hifi_ApplicationOverlay_h diff --git a/interface/src/ui/AvatarInputs.cpp b/interface/src/ui/AvatarInputs.cpp index 944be4bf9e..b09289c78a 100644 --- a/interface/src/ui/AvatarInputs.cpp +++ b/interface/src/ui/AvatarInputs.cpp @@ -20,6 +20,10 @@ HIFI_QML_DEF(AvatarInputs) static AvatarInputs* INSTANCE{ nullptr }; +static const char SETTINGS_GROUP_NAME[] = "Rear View Tools"; +static const char ZOOM_LEVEL_SETTINGS[] = "ZoomLevel"; + +static Setting::Handle rearViewZoomLevel(QStringList() << SETTINGS_GROUP_NAME << ZOOM_LEVEL_SETTINGS, 0); AvatarInputs* AvatarInputs::getInstance() { if (!INSTANCE) { @@ -32,6 +36,8 @@ AvatarInputs* AvatarInputs::getInstance() { AvatarInputs::AvatarInputs(QQuickItem* parent) : QQuickItem(parent) { INSTANCE = this; + int zoomSetting = rearViewZoomLevel.get(); + _mirrorZoomed = zoomSetting == 0; } #define AI_UPDATE(name, src) \ @@ -56,6 +62,8 @@ void AvatarInputs::update() { if (!Menu::getInstance()) { return; } + AI_UPDATE(mirrorVisible, Menu::getInstance()->isOptionChecked(MenuOption::MiniMirror) && !qApp->isHMDMode() + && !Menu::getInstance()->isOptionChecked(MenuOption::FullscreenMirror)); AI_UPDATE(cameraEnabled, !Menu::getInstance()->isOptionChecked(MenuOption::NoFaceTracking)); AI_UPDATE(cameraMuted, Menu::getInstance()->isOptionChecked(MenuOption::MuteFaceTracking)); AI_UPDATE(isHMD, qApp->isHMDMode()); @@ -114,3 +122,15 @@ void AvatarInputs::toggleAudioMute() { void AvatarInputs::resetSensors() { qApp->resetSensors(); } + +void AvatarInputs::toggleZoom() { + _mirrorZoomed = !_mirrorZoomed; + rearViewZoomLevel.set(_mirrorZoomed ? 0 : 1); + emit mirrorZoomedChanged(); +} + +void AvatarInputs::closeMirror() { + if (Menu::getInstance()->isOptionChecked(MenuOption::MiniMirror)) { + Menu::getInstance()->triggerOption(MenuOption::MiniMirror); + } +} diff --git a/interface/src/ui/AvatarInputs.h b/interface/src/ui/AvatarInputs.h index 5535469445..85570ecd3c 100644 --- a/interface/src/ui/AvatarInputs.h +++ b/interface/src/ui/AvatarInputs.h @@ -28,6 +28,8 @@ class AvatarInputs : public QQuickItem { AI_PROPERTY(bool, audioMuted, false) AI_PROPERTY(bool, audioClipping, false) AI_PROPERTY(float, audioLevel, 0) + AI_PROPERTY(bool, mirrorVisible, false) + AI_PROPERTY(bool, mirrorZoomed, true) AI_PROPERTY(bool, isHMD, false) AI_PROPERTY(bool, showAudioTools, true) @@ -42,6 +44,8 @@ signals: void audioMutedChanged(); void audioClippingChanged(); void audioLevelChanged(); + void mirrorVisibleChanged(); + void mirrorZoomedChanged(); void isHMDChanged(); void showAudioToolsChanged(); @@ -49,6 +53,8 @@ protected: Q_INVOKABLE void resetSensors(); Q_INVOKABLE void toggleCameraMute(); Q_INVOKABLE void toggleAudioMute(); + Q_INVOKABLE void toggleZoom(); + Q_INVOKABLE void closeMirror(); private: float _trailingAudioLoudness{ 0 }; diff --git a/interface/src/ui/BaseLogDialog.cpp b/interface/src/ui/BaseLogDialog.cpp index 571d3ac403..7e0027e0a8 100644 --- a/interface/src/ui/BaseLogDialog.cpp +++ b/interface/src/ui/BaseLogDialog.cpp @@ -28,23 +28,17 @@ const int SEARCH_BUTTON_LEFT = 25; const int SEARCH_BUTTON_WIDTH = 20; const int SEARCH_TOGGLE_BUTTON_WIDTH = 50; const int SEARCH_TEXT_WIDTH = 240; -const int TIME_STAMP_LENGTH = 16; -const int FONT_WEIGHT = 75; const QColor HIGHLIGHT_COLOR = QColor("#3366CC"); -const QColor BOLD_COLOR = QColor("#445c8c"); -const QString BOLD_PATTERN = "\\[\\d*\\/.*:\\d*:\\d*\\]"; -class Highlighter : public QSyntaxHighlighter { +class KeywordHighlighter : public QSyntaxHighlighter { public: - Highlighter(QTextDocument* parent = nullptr); - void setBold(int indexToBold); + KeywordHighlighter(QTextDocument* parent = nullptr); QString keyword; protected: void highlightBlock(const QString& text) override; private: - QTextCharFormat boldFormat; QTextCharFormat keywordFormat; }; @@ -95,7 +89,7 @@ void BaseLogDialog::initControls() { _leftPad += SEARCH_TOGGLE_BUTTON_WIDTH + BUTTON_MARGIN; _searchPrevButton->show(); connect(_searchPrevButton, SIGNAL(clicked()), SLOT(toggleSearchPrev())); - + _searchNextButton = new QPushButton(this); _searchNextButton->setObjectName("searchNextButton"); _searchNextButton->setGeometry(_leftPad, ELEMENT_MARGIN, SEARCH_TOGGLE_BUTTON_WIDTH, ELEMENT_HEIGHT); @@ -107,8 +101,9 @@ void BaseLogDialog::initControls() { _logTextBox = new QPlainTextEdit(this); _logTextBox->setReadOnly(true); _logTextBox->show(); - _highlighter = new Highlighter(_logTextBox->document()); + _highlighter = new KeywordHighlighter(_logTextBox->document()); connect(_logTextBox, SIGNAL(selectionChanged()), SLOT(updateSelection())); + } void BaseLogDialog::showEvent(QShowEvent* event) { @@ -121,9 +116,7 @@ void BaseLogDialog::resizeEvent(QResizeEvent* event) { void BaseLogDialog::appendLogLine(QString logLine) { if (logLine.contains(_searchTerm, Qt::CaseInsensitive)) { - int indexToBold = _logTextBox->document()->characterCount(); _logTextBox->appendPlainText(logLine.trimmed()); - _highlighter->setBold(indexToBold); } } @@ -135,7 +128,7 @@ void BaseLogDialog::handleSearchTextChanged(QString searchText) { if (searchText.isEmpty()) { return; } - + QTextCursor cursor = _logTextBox->textCursor(); if (cursor.hasSelection()) { QString selectedTerm = cursor.selectedText(); @@ -143,16 +136,16 @@ void BaseLogDialog::handleSearchTextChanged(QString searchText) { return; } } - + cursor.setPosition(0, QTextCursor::MoveAnchor); _logTextBox->setTextCursor(cursor); bool foundTerm = _logTextBox->find(searchText); - + if (!foundTerm) { cursor.movePosition(QTextCursor::End, QTextCursor::MoveAnchor); _logTextBox->setTextCursor(cursor); } - + _searchTerm = searchText; _highlighter->keyword = searchText; _highlighter->rehighlight(); @@ -182,7 +175,6 @@ void BaseLogDialog::showLogData() { _logTextBox->clear(); _logTextBox->appendPlainText(getCurrentLog()); _logTextBox->ensureCursorVisible(); - _highlighter->rehighlight(); } void BaseLogDialog::updateSelection() { @@ -195,28 +187,16 @@ void BaseLogDialog::updateSelection() { } } -Highlighter::Highlighter(QTextDocument* parent) : QSyntaxHighlighter(parent) { - boldFormat.setFontWeight(FONT_WEIGHT); - boldFormat.setForeground(BOLD_COLOR); +KeywordHighlighter::KeywordHighlighter(QTextDocument* parent) : QSyntaxHighlighter(parent) { keywordFormat.setForeground(HIGHLIGHT_COLOR); } -void Highlighter::highlightBlock(const QString& text) { - QRegExp expression(BOLD_PATTERN); - - int index = text.indexOf(expression, 0); - - while (index >= 0) { - int length = expression.matchedLength(); - setFormat(index, length, boldFormat); - index = text.indexOf(expression, index + length); - } - +void KeywordHighlighter::highlightBlock(const QString& text) { if (keyword.isNull() || keyword.isEmpty()) { return; } - index = text.indexOf(keyword, 0, Qt::CaseInsensitive); + int index = text.indexOf(keyword, 0, Qt::CaseInsensitive); int length = keyword.length(); while (index >= 0) { @@ -224,7 +204,3 @@ void Highlighter::highlightBlock(const QString& text) { index = text.indexOf(keyword, index + length, Qt::CaseInsensitive); } } - -void Highlighter::setBold(int indexToBold) { - setFormat(indexToBold, TIME_STAMP_LENGTH, boldFormat); -} diff --git a/interface/src/ui/BaseLogDialog.h b/interface/src/ui/BaseLogDialog.h index e18d23937f..d097010bae 100644 --- a/interface/src/ui/BaseLogDialog.h +++ b/interface/src/ui/BaseLogDialog.h @@ -23,7 +23,7 @@ const int BUTTON_MARGIN = 8; class QPushButton; class QLineEdit; class QPlainTextEdit; -class Highlighter; +class KeywordHighlighter; class BaseLogDialog : public QDialog { Q_OBJECT @@ -56,7 +56,7 @@ private: QPushButton* _searchPrevButton { nullptr }; QPushButton* _searchNextButton { nullptr }; QString _searchTerm; - Highlighter* _highlighter { nullptr }; + KeywordHighlighter* _highlighter { nullptr }; void initControls(); void showLogData(); diff --git a/interface/src/ui/CachesSizeDialog.cpp b/interface/src/ui/CachesSizeDialog.cpp new file mode 100644 index 0000000000..935a6d126e --- /dev/null +++ b/interface/src/ui/CachesSizeDialog.cpp @@ -0,0 +1,84 @@ +// +// CachesSizeDialog.cpp +// +// +// Created by Clement on 1/12/15. +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "CachesSizeDialog.h" + + +QDoubleSpinBox* createDoubleSpinBox(QWidget* parent) { + QDoubleSpinBox* box = new QDoubleSpinBox(parent); + box->setDecimals(0); + box->setRange(MIN_UNUSED_MAX_SIZE / BYTES_PER_MEGABYTES, MAX_UNUSED_MAX_SIZE / BYTES_PER_MEGABYTES); + + return box; +} + +CachesSizeDialog::CachesSizeDialog(QWidget* parent) : + QDialog(parent, Qt::Window | Qt::WindowCloseButtonHint) +{ + setWindowTitle("Caches Size"); + + // Create layouter + QFormLayout* form = new QFormLayout(this); + setLayout(form); + + form->addRow("Animations cache size (MB):", _animations = createDoubleSpinBox(this)); + form->addRow("Geometries cache size (MB):", _geometries = createDoubleSpinBox(this)); + form->addRow("Sounds cache size (MB):", _sounds = createDoubleSpinBox(this)); + form->addRow("Textures cache size (MB):", _textures = createDoubleSpinBox(this)); + + resetClicked(true); + + // Add a button to reset + QPushButton* confirmButton = new QPushButton("Confirm", this); + QPushButton* resetButton = new QPushButton("Reset", this); + form->addRow(confirmButton, resetButton); + connect(confirmButton, SIGNAL(clicked(bool)), this, SLOT(confirmClicked(bool))); + connect(resetButton, SIGNAL(clicked(bool)), this, SLOT(resetClicked(bool))); +} + +void CachesSizeDialog::confirmClicked(bool checked) { + DependencyManager::get()->setUnusedResourceCacheSize(_animations->value() * BYTES_PER_MEGABYTES); + DependencyManager::get()->setUnusedResourceCacheSize(_geometries->value() * BYTES_PER_MEGABYTES); + DependencyManager::get()->setUnusedResourceCacheSize(_sounds->value() * BYTES_PER_MEGABYTES); + // Disabling the texture cache because it's a liability in cases where we're overcommiting GPU memory +#if 0 + DependencyManager::get()->setUnusedResourceCacheSize(_textures->value() * BYTES_PER_MEGABYTES); +#endif + + QDialog::close(); +} + +void CachesSizeDialog::resetClicked(bool checked) { + _animations->setValue(DependencyManager::get()->getUnusedResourceCacheSize() / BYTES_PER_MEGABYTES); + _geometries->setValue(DependencyManager::get()->getUnusedResourceCacheSize() / BYTES_PER_MEGABYTES); + _sounds->setValue(DependencyManager::get()->getUnusedResourceCacheSize() / BYTES_PER_MEGABYTES); + _textures->setValue(DependencyManager::get()->getUnusedResourceCacheSize() / BYTES_PER_MEGABYTES); +} + +void CachesSizeDialog::reject() { + // Just regularly close upon ESC + QDialog::close(); +} + +void CachesSizeDialog::closeEvent(QCloseEvent* event) { + QDialog::closeEvent(event); + emit closed(); +} diff --git a/interface/src/ui/CachesSizeDialog.h b/interface/src/ui/CachesSizeDialog.h new file mode 100644 index 0000000000..025d0f2bac --- /dev/null +++ b/interface/src/ui/CachesSizeDialog.h @@ -0,0 +1,45 @@ +// +// CachesSizeDialog.h +// +// +// Created by Clement on 1/12/15. +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_CachesSizeDialog_h +#define hifi_CachesSizeDialog_h + +#include + +class QDoubleSpinBox; + +class CachesSizeDialog : public QDialog { + Q_OBJECT +public: + // Sets up the UI + CachesSizeDialog(QWidget* parent); + +signals: + void closed(); + +public slots: + void reject() override; + void confirmClicked(bool checked); + void resetClicked(bool checked); + +protected: + // Emits a 'closed' signal when this dialog is closed. + void closeEvent(QCloseEvent* event) override; + +private: + QDoubleSpinBox* _animations = nullptr; + QDoubleSpinBox* _geometries = nullptr; + QDoubleSpinBox* _scripts = nullptr; + QDoubleSpinBox* _sounds = nullptr; + QDoubleSpinBox* _textures = nullptr; +}; + +#endif // hifi_CachesSizeDialog_h diff --git a/interface/src/ui/DialogsManager.cpp b/interface/src/ui/DialogsManager.cpp index f1d6f585d7..3252fef4f0 100644 --- a/interface/src/ui/DialogsManager.cpp +++ b/interface/src/ui/DialogsManager.cpp @@ -19,13 +19,16 @@ #include #include "AddressBarDialog.h" +#include "CachesSizeDialog.h" #include "ConnectionFailureDialog.h" +#include "DiskCacheEditor.h" #include "DomainConnectionDialog.h" #include "HMDToolsDialog.h" #include "LodToolsDialog.h" #include "LoginDialog.h" #include "OctreeStatsDialog.h" #include "PreferencesDialog.h" +#include "ScriptEditorWindow.h" #include "UpdateDialog.h" template @@ -64,6 +67,11 @@ void DialogsManager::setDomainConnectionFailureVisibility(bool visible) { } } +void DialogsManager::toggleDiskCacheEditor() { + maybeCreateDialog(_diskCacheEditor); + _diskCacheEditor->toggle(); +} + void DialogsManager::toggleLoginDialog() { LoginDialog::toggleAction(); } @@ -89,6 +97,16 @@ void DialogsManager::octreeStatsDetails() { _octreeStatsDialog->raise(); } +void DialogsManager::cachesSizeDialog() { + if (!_cachesSizeDialog) { + maybeCreateDialog(_cachesSizeDialog); + + connect(_cachesSizeDialog, SIGNAL(closed()), _cachesSizeDialog, SLOT(deleteLater())); + _cachesSizeDialog->show(); + } + _cachesSizeDialog->raise(); +} + void DialogsManager::lodTools() { if (!_lodToolsDialog) { maybeCreateDialog(_lodToolsDialog); @@ -119,6 +137,12 @@ void DialogsManager::hmdToolsClosed() { } } +void DialogsManager::showScriptEditor() { + maybeCreateDialog(_scriptEditor); + _scriptEditor->show(); + _scriptEditor->raise(); +} + void DialogsManager::showTestingResults() { if (!_testingDialog) { _testingDialog = new TestingDialog(qApp->getWindow()); diff --git a/interface/src/ui/DialogsManager.h b/interface/src/ui/DialogsManager.h index 608195aca7..54aef38984 100644 --- a/interface/src/ui/DialogsManager.h +++ b/interface/src/ui/DialogsManager.h @@ -22,6 +22,7 @@ class AnimationsDialog; class AttachmentsDialog; class CachesSizeDialog; +class DiskCacheEditor; class LodToolsDialog; class OctreeStatsDialog; class ScriptEditorWindow; @@ -45,11 +46,14 @@ public slots: void showAddressBar(); void showFeed(); void setDomainConnectionFailureVisibility(bool visible); + void toggleDiskCacheEditor(); void toggleLoginDialog(); void showLoginDialog(); void octreeStatsDetails(); + void cachesSizeDialog(); void lodTools(); void hmdTools(bool showTools); + void showScriptEditor(); void showDomainConnectionDialog(); void showTestingResults(); @@ -73,10 +77,12 @@ private: QPointer _animationsDialog; QPointer _attachmentsDialog; QPointer _cachesSizeDialog; + QPointer _diskCacheEditor; QPointer _ircInfoBox; QPointer _hmdToolsDialog; QPointer _lodToolsDialog; QPointer _octreeStatsDialog; + QPointer _scriptEditor; QPointer _testingDialog; QPointer _domainConnectionDialog; }; diff --git a/interface/src/ui/DiskCacheEditor.cpp b/interface/src/ui/DiskCacheEditor.cpp new file mode 100644 index 0000000000..1a7be8642b --- /dev/null +++ b/interface/src/ui/DiskCacheEditor.cpp @@ -0,0 +1,146 @@ +// +// DiskCacheEditor.cpp +// +// +// Created by Clement on 3/4/15. +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "DiskCacheEditor.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "OffscreenUi.h" + +DiskCacheEditor::DiskCacheEditor(QWidget* parent) : QObject(parent) { +} + +QWindow* DiskCacheEditor::windowHandle() { + return (_dialog) ? _dialog->windowHandle() : nullptr; +} + +void DiskCacheEditor::toggle() { + if (!_dialog) { + makeDialog(); + } + + if (!_dialog->isActiveWindow()) { + _dialog->show(); + _dialog->raise(); + _dialog->activateWindow(); + } else { + _dialog->close(); + } +} + +void DiskCacheEditor::makeDialog() { + _dialog = new QDialog(static_cast(parent())); + Q_CHECK_PTR(_dialog); + _dialog->setAttribute(Qt::WA_DeleteOnClose); + _dialog->setWindowTitle("Disk Cache Editor"); + + QGridLayout* layout = new QGridLayout(_dialog); + Q_CHECK_PTR(layout); + _dialog->setLayout(layout); + + + QLabel* path = new QLabel("Path : ", _dialog); + Q_CHECK_PTR(path); + path->setAlignment(Qt::AlignRight); + layout->addWidget(path, 0, 0); + + QLabel* size = new QLabel("Current Size : ", _dialog); + Q_CHECK_PTR(size); + size->setAlignment(Qt::AlignRight); + layout->addWidget(size, 1, 0); + + QLabel* maxSize = new QLabel("Max Size : ", _dialog); + Q_CHECK_PTR(maxSize); + maxSize->setAlignment(Qt::AlignRight); + layout->addWidget(maxSize, 2, 0); + + + _path = new QLabel(_dialog); + Q_CHECK_PTR(_path); + _path->setAlignment(Qt::AlignLeft); + layout->addWidget(_path, 0, 1, 1, 3); + + _size = new QLabel(_dialog); + Q_CHECK_PTR(_size); + _size->setAlignment(Qt::AlignLeft); + layout->addWidget(_size, 1, 1, 1, 3); + + _maxSize = new QLabel(_dialog); + Q_CHECK_PTR(_maxSize); + _maxSize->setAlignment(Qt::AlignLeft); + layout->addWidget(_maxSize, 2, 1, 1, 3); + + refresh(); + + + static const int REFRESH_INTERVAL = 100; // msec + _refreshTimer = new QTimer(_dialog); + _refreshTimer->setInterval(REFRESH_INTERVAL); // Qt::CoarseTimer acceptable, no need for real time accuracy + _refreshTimer->setSingleShot(false); + QObject::connect(_refreshTimer.data(), &QTimer::timeout, this, &DiskCacheEditor::refresh); + _refreshTimer->start(); + + QPushButton* clearCacheButton = new QPushButton(_dialog); + Q_CHECK_PTR(clearCacheButton); + clearCacheButton->setText("Clear"); + clearCacheButton->setToolTip("Erases the entire content of the disk cache."); + connect(clearCacheButton, SIGNAL(clicked()), SLOT(clear())); + layout->addWidget(clearCacheButton, 3, 3); +} + +void DiskCacheEditor::refresh() { + DependencyManager::get()->cacheInfoRequest(this, "cacheInfoCallback"); +} + +void DiskCacheEditor::cacheInfoCallback(QString cacheDirectory, qint64 cacheSize, qint64 maximumCacheSize) { + static const auto stringify = [](qint64 number) { + static const QStringList UNITS = QStringList() << "B" << "KB" << "MB" << "GB"; + static const qint64 CHUNK = 1024; + QString unit; + int i = 0; + for (i = 0; i < 4; ++i) { + if (number / CHUNK > 0) { + number /= CHUNK; + } else { + break; + } + } + return QString("%0 %1").arg(number).arg(UNITS[i]); + }; + + if (_path) { + _path->setText(cacheDirectory); + } + if (_size) { + _size->setText(stringify(cacheSize)); + } + if (_maxSize) { + _maxSize->setText(stringify(maximumCacheSize)); + } +} + +void DiskCacheEditor::clear() { + auto buttonClicked = OffscreenUi::question(_dialog, "Clearing disk cache", + "You are about to erase all the content of the disk cache, " + "are you sure you want to do that?", + QMessageBox::Ok | QMessageBox::Cancel); + if (buttonClicked == QMessageBox::Ok) { + DependencyManager::get()->clearCache(); + } +} diff --git a/interface/src/ui/DiskCacheEditor.h b/interface/src/ui/DiskCacheEditor.h new file mode 100644 index 0000000000..3f8fa1a883 --- /dev/null +++ b/interface/src/ui/DiskCacheEditor.h @@ -0,0 +1,49 @@ +// +// DiskCacheEditor.h +// +// +// Created by Clement on 3/4/15. +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_DiskCacheEditor_h +#define hifi_DiskCacheEditor_h + +#include +#include + +class QDialog; +class QLabel; +class QWindow; +class QTimer; + +class DiskCacheEditor : public QObject { + Q_OBJECT + +public: + DiskCacheEditor(QWidget* parent = nullptr); + + QWindow* windowHandle(); + +public slots: + void toggle(); + +private slots: + void refresh(); + void cacheInfoCallback(QString cacheDirectory, qint64 cacheSize, qint64 maximumCacheSize); + void clear(); + +private: + void makeDialog(); + + QPointer _dialog; + QPointer _path; + QPointer _size; + QPointer _maxSize; + QPointer _refreshTimer; +}; + +#endif // hifi_DiskCacheEditor_h \ No newline at end of file diff --git a/interface/src/ui/ScriptEditBox.cpp b/interface/src/ui/ScriptEditBox.cpp new file mode 100644 index 0000000000..2aea225b17 --- /dev/null +++ b/interface/src/ui/ScriptEditBox.cpp @@ -0,0 +1,111 @@ +// +// ScriptEditBox.cpp +// interface/src/ui +// +// Created by Thijs Wenker on 4/30/14. +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "ScriptEditBox.h" + +#include +#include + +#include "ScriptLineNumberArea.h" + +ScriptEditBox::ScriptEditBox(QWidget* parent) : + QPlainTextEdit(parent) +{ + _scriptLineNumberArea = new ScriptLineNumberArea(this); + + connect(this, &ScriptEditBox::blockCountChanged, this, &ScriptEditBox::updateLineNumberAreaWidth); + connect(this, &ScriptEditBox::updateRequest, this, &ScriptEditBox::updateLineNumberArea); + connect(this, &ScriptEditBox::cursorPositionChanged, this, &ScriptEditBox::highlightCurrentLine); + + updateLineNumberAreaWidth(0); + highlightCurrentLine(); +} + +int ScriptEditBox::lineNumberAreaWidth() { + int digits = 1; + const int SPACER_PIXELS = 3; + const int BASE_TEN = 10; + int max = qMax(1, blockCount()); + while (max >= BASE_TEN) { + max /= BASE_TEN; + digits++; + } + return SPACER_PIXELS + fontMetrics().width(QLatin1Char('H')) * digits; +} + +void ScriptEditBox::updateLineNumberAreaWidth(int blockCount) { + setViewportMargins(lineNumberAreaWidth(), 0, 0, 0); +} + +void ScriptEditBox::updateLineNumberArea(const QRect& rect, int deltaY) { + if (deltaY) { + _scriptLineNumberArea->scroll(0, deltaY); + } else { + _scriptLineNumberArea->update(0, rect.y(), _scriptLineNumberArea->width(), rect.height()); + } + + if (rect.contains(viewport()->rect())) { + updateLineNumberAreaWidth(0); + } +} + +void ScriptEditBox::resizeEvent(QResizeEvent* event) { + QPlainTextEdit::resizeEvent(event); + + QRect localContentsRect = contentsRect(); + _scriptLineNumberArea->setGeometry(QRect(localContentsRect.left(), localContentsRect.top(), lineNumberAreaWidth(), + localContentsRect.height())); +} + +void ScriptEditBox::highlightCurrentLine() { + QList extraSelections; + + if (!isReadOnly()) { + QTextEdit::ExtraSelection selection; + + QColor lineColor = QColor(Qt::gray).lighter(); + + selection.format.setBackground(lineColor); + selection.format.setProperty(QTextFormat::FullWidthSelection, true); + selection.cursor = textCursor(); + selection.cursor.clearSelection(); + extraSelections.append(selection); + } + + setExtraSelections(extraSelections); +} + +void ScriptEditBox::lineNumberAreaPaintEvent(QPaintEvent* event) +{ + QPainter painter(_scriptLineNumberArea); + painter.fillRect(event->rect(), Qt::lightGray); + QTextBlock block = firstVisibleBlock(); + int blockNumber = block.blockNumber(); + int top = (int) blockBoundingGeometry(block).translated(contentOffset()).top(); + int bottom = top + (int) blockBoundingRect(block).height(); + + while (block.isValid() && top <= event->rect().bottom()) { + if (block.isVisible() && bottom >= event->rect().top()) { + QFont font = painter.font(); + font.setBold(this->textCursor().blockNumber() == block.blockNumber()); + painter.setFont(font); + QString number = QString::number(blockNumber + 1); + painter.setPen(Qt::black); + painter.drawText(0, top, _scriptLineNumberArea->width(), fontMetrics().height(), + Qt::AlignRight, number); + } + + block = block.next(); + top = bottom; + bottom = top + (int) blockBoundingRect(block).height(); + blockNumber++; + } +} diff --git a/interface/src/ui/ScriptEditBox.h b/interface/src/ui/ScriptEditBox.h new file mode 100644 index 0000000000..0b037db16a --- /dev/null +++ b/interface/src/ui/ScriptEditBox.h @@ -0,0 +1,38 @@ +// +// ScriptEditBox.h +// interface/src/ui +// +// Created by Thijs Wenker on 4/30/14. +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_ScriptEditBox_h +#define hifi_ScriptEditBox_h + +#include + +class ScriptEditBox : public QPlainTextEdit { + Q_OBJECT + +public: + ScriptEditBox(QWidget* parent = NULL); + + void lineNumberAreaPaintEvent(QPaintEvent* event); + int lineNumberAreaWidth(); + +protected: + void resizeEvent(QResizeEvent* event) override; + +private slots: + void updateLineNumberAreaWidth(int blockCount); + void highlightCurrentLine(); + void updateLineNumberArea(const QRect& rect, int deltaY); + +private: + QWidget* _scriptLineNumberArea; +}; + +#endif // hifi_ScriptEditBox_h diff --git a/interface/src/ui/ScriptEditorWidget.cpp b/interface/src/ui/ScriptEditorWidget.cpp new file mode 100644 index 0000000000..ada6b11355 --- /dev/null +++ b/interface/src/ui/ScriptEditorWidget.cpp @@ -0,0 +1,256 @@ +// +// ScriptEditorWidget.cpp +// interface/src/ui +// +// Created by Thijs Wenker on 4/14/14. +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "ui_scriptEditorWidget.h" +#include "ScriptEditorWidget.h" +#include "ScriptEditorWindow.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "Application.h" +#include "ScriptHighlighting.h" + +ScriptEditorWidget::ScriptEditorWidget() : + _scriptEditorWidgetUI(new Ui::ScriptEditorWidget), + _scriptEngine(NULL), + _isRestarting(false), + _isReloading(false) +{ + setAttribute(Qt::WA_DeleteOnClose); + + _scriptEditorWidgetUI->setupUi(this); + + connect(_scriptEditorWidgetUI->scriptEdit->document(), &QTextDocument::modificationChanged, this, + &ScriptEditorWidget::scriptModified); + connect(_scriptEditorWidgetUI->scriptEdit->document(), &QTextDocument::contentsChanged, this, + &ScriptEditorWidget::onScriptModified); + + // remove the title bar (see the Qt docs on setTitleBarWidget) + setTitleBarWidget(new QWidget()); + QFontMetrics fm(_scriptEditorWidgetUI->scriptEdit->font()); + _scriptEditorWidgetUI->scriptEdit->setTabStopWidth(fm.width('0') * 4); + // We create a new ScriptHighligting QObject and provide it with a parent so this is NOT a memory leak. + new ScriptHighlighting(_scriptEditorWidgetUI->scriptEdit->document()); + QTimer::singleShot(0, _scriptEditorWidgetUI->scriptEdit, SLOT(setFocus())); + + _console = new JSConsole(this); + _console->setFixedHeight(CONSOLE_HEIGHT); + _scriptEditorWidgetUI->verticalLayout->addWidget(_console); + connect(_scriptEditorWidgetUI->clearButton, &QPushButton::clicked, _console, &JSConsole::clear); +} + +ScriptEditorWidget::~ScriptEditorWidget() { + delete _scriptEditorWidgetUI; + delete _console; +} + +void ScriptEditorWidget::onScriptModified() { + if(_scriptEditorWidgetUI->onTheFlyCheckBox->isChecked() && isModified() && isRunning() && !_isReloading) { + _isRestarting = true; + setRunning(false); + // Script is restarted once current script instance finishes. + } +} + +void ScriptEditorWidget::onScriptFinished(const QString& scriptPath) { + _scriptEngine = NULL; + _console->setScriptEngine(NULL); + if (_isRestarting) { + _isRestarting = false; + setRunning(true); + } +} + +bool ScriptEditorWidget::isModified() { + return _scriptEditorWidgetUI->scriptEdit->document()->isModified(); +} + +bool ScriptEditorWidget::isRunning() { + return (_scriptEngine != NULL) ? _scriptEngine->isRunning() : false; +} + +bool ScriptEditorWidget::setRunning(bool run) { + if (run && isModified() && !save()) { + return false; + } + + if (_scriptEngine != NULL) { + disconnect(_scriptEngine, &ScriptEngine::runningStateChanged, this, &ScriptEditorWidget::runningStateChanged); + disconnect(_scriptEngine, &ScriptEngine::update, this, &ScriptEditorWidget::onScriptModified); + disconnect(_scriptEngine, &ScriptEngine::finished, this, &ScriptEditorWidget::onScriptFinished); + } + + auto scriptEngines = DependencyManager::get(); + if (run) { + const QString& scriptURLString = QUrl(_currentScript).toString(); + // Reload script so that an out of date copy is not retrieved from the cache + _scriptEngine = scriptEngines->loadScript(scriptURLString, true, true, false, true); + connect(_scriptEngine, &ScriptEngine::runningStateChanged, this, &ScriptEditorWidget::runningStateChanged); + connect(_scriptEngine, &ScriptEngine::update, this, &ScriptEditorWidget::onScriptModified); + connect(_scriptEngine, &ScriptEngine::finished, this, &ScriptEditorWidget::onScriptFinished); + } else { + connect(_scriptEngine, &ScriptEngine::finished, this, &ScriptEditorWidget::onScriptFinished); + const QString& scriptURLString = QUrl(_currentScript).toString(); + scriptEngines->stopScript(scriptURLString); + _scriptEngine = NULL; + } + _console->setScriptEngine(_scriptEngine); + return true; +} + +bool ScriptEditorWidget::saveFile(const QString &scriptPath) { + QFile file(scriptPath); + if (!file.open(QFile::WriteOnly | QFile::Text)) { + OffscreenUi::warning(this, tr("Interface"), tr("Cannot write script %1:\n%2.").arg(scriptPath) + .arg(file.errorString())); + return false; + } + + QTextStream out(&file); + out << _scriptEditorWidgetUI->scriptEdit->toPlainText(); + file.close(); + + setScriptFile(scriptPath); + return true; +} + +void ScriptEditorWidget::loadFile(const QString& scriptPath) { + QUrl url(scriptPath); + + // if the scheme length is one or lower, maybe they typed in a file, let's try + const int WINDOWS_DRIVE_LETTER_SIZE = 1; + if (url.scheme().size() <= WINDOWS_DRIVE_LETTER_SIZE) { + QFile file(scriptPath); + if (!file.open(QFile::ReadOnly | QFile::Text)) { + OffscreenUi::warning(this, tr("Interface"), tr("Cannot read script %1:\n%2.").arg(scriptPath) + .arg(file.errorString())); + return; + } + QTextStream in(&file); + _scriptEditorWidgetUI->scriptEdit->setPlainText(in.readAll()); + file.close(); + setScriptFile(scriptPath); + + if (_scriptEngine != NULL) { + disconnect(_scriptEngine, &ScriptEngine::runningStateChanged, this, &ScriptEditorWidget::runningStateChanged); + disconnect(_scriptEngine, &ScriptEngine::update, this, &ScriptEditorWidget::onScriptModified); + disconnect(_scriptEngine, &ScriptEngine::finished, this, &ScriptEditorWidget::onScriptFinished); + } + } else { + QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); + QNetworkRequest networkRequest = QNetworkRequest(url); + networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); + networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); + QNetworkReply* reply = networkAccessManager.get(networkRequest); + qDebug() << "Downloading included script at" << scriptPath; + QEventLoop loop; + QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); + loop.exec(); + _scriptEditorWidgetUI->scriptEdit->setPlainText(reply->readAll()); + delete reply; + + if (!saveAs()) { + static_cast(this->parent()->parent()->parent())->terminateCurrentTab(); + } + } + const QString& scriptURLString = QUrl(_currentScript).toString(); + _scriptEngine = DependencyManager::get()->getScriptEngine(scriptURLString); + if (_scriptEngine != NULL) { + connect(_scriptEngine, &ScriptEngine::runningStateChanged, this, &ScriptEditorWidget::runningStateChanged); + connect(_scriptEngine, &ScriptEngine::update, this, &ScriptEditorWidget::onScriptModified); + connect(_scriptEngine, &ScriptEngine::finished, this, &ScriptEditorWidget::onScriptFinished); + } + _console->setScriptEngine(_scriptEngine); +} + +bool ScriptEditorWidget::save() { + return _currentScript.isEmpty() ? saveAs() : saveFile(_currentScript); +} + +bool ScriptEditorWidget::saveAs() { + auto scriptEngines = DependencyManager::get(); + QString fileName = QFileDialog::getSaveFileName(this, tr("Save script"), + qApp->getPreviousScriptLocation(), + tr("JavaScript Files (*.js)")); + if (!fileName.isEmpty()) { + qApp->setPreviousScriptLocation(fileName); + return saveFile(fileName); + } else { + return false; + } +} + +void ScriptEditorWidget::setScriptFile(const QString& scriptPath) { + _currentScript = scriptPath; + _currentScriptModified = QFileInfo(_currentScript).lastModified(); + _scriptEditorWidgetUI->scriptEdit->document()->setModified(false); + setWindowModified(false); + + emit scriptnameChanged(); +} + +bool ScriptEditorWidget::questionSave() { + if (_scriptEditorWidgetUI->scriptEdit->document()->isModified()) { + QMessageBox::StandardButton button = OffscreenUi::warning(this, tr("Interface"), + tr("The script has been modified.\nDo you want to save your changes?"), QMessageBox::Save | QMessageBox::Discard | + QMessageBox::Cancel, QMessageBox::Save); + return button == QMessageBox::Save ? save() : (button == QMessageBox::Discard); + } + return true; +} + +void ScriptEditorWidget::onWindowActivated() { + if (!_isReloading) { + _isReloading = true; + + QDateTime fileStamp = QFileInfo(_currentScript).lastModified(); + if (fileStamp > _currentScriptModified) { + bool doReload = false; + auto window = static_cast(this->parent()->parent()->parent()); + window->inModalDialog = true; + if (window->autoReloadScripts() + || OffscreenUi::question(this, tr("Reload Script"), + tr("The following file has been modified outside of the Interface editor:") + "\n" + _currentScript + "\n" + + (isModified() + ? tr("Do you want to reload it and lose the changes you've made in the Interface editor?") + : tr("Do you want to reload it?")), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + doReload = true; + } + window->inModalDialog = false; + if (doReload) { + loadFile(_currentScript); + if (_scriptEditorWidgetUI->onTheFlyCheckBox->isChecked() && isRunning()) { + _isRestarting = true; + setRunning(false); + // Script is restarted once current script instance finishes. + } + } else { + _currentScriptModified = fileStamp; // Asked and answered. Don't ask again until the external file is changed again. + } + } + _isReloading = false; + } +} diff --git a/interface/src/ui/ScriptEditorWidget.h b/interface/src/ui/ScriptEditorWidget.h new file mode 100644 index 0000000000..f53fd7b718 --- /dev/null +++ b/interface/src/ui/ScriptEditorWidget.h @@ -0,0 +1,64 @@ +// +// ScriptEditorWidget.h +// interface/src/ui +// +// Created by Thijs Wenker on 4/14/14. +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_ScriptEditorWidget_h +#define hifi_ScriptEditorWidget_h + +#include + +#include "JSConsole.h" +#include "ScriptEngine.h" + +namespace Ui { + class ScriptEditorWidget; +} + +class ScriptEditorWidget : public QDockWidget { + Q_OBJECT + +public: + ScriptEditorWidget(); + ~ScriptEditorWidget(); + + bool isModified(); + bool isRunning(); + bool setRunning(bool run); + bool saveFile(const QString& scriptPath); + void loadFile(const QString& scriptPath); + void setScriptFile(const QString& scriptPath); + bool save(); + bool saveAs(); + bool questionSave(); + const QString getScriptName() const { return _currentScript; }; + +signals: + void runningStateChanged(); + void scriptnameChanged(); + void scriptModified(); + +public slots: + void onWindowActivated(); + +private slots: + void onScriptModified(); + void onScriptFinished(const QString& scriptName); + +private: + JSConsole* _console; + Ui::ScriptEditorWidget* _scriptEditorWidgetUI; + ScriptEngine* _scriptEngine; + QString _currentScript; + QDateTime _currentScriptModified; + bool _isRestarting; + bool _isReloading; +}; + +#endif // hifi_ScriptEditorWidget_h diff --git a/interface/src/ui/ScriptEditorWindow.cpp b/interface/src/ui/ScriptEditorWindow.cpp new file mode 100644 index 0000000000..58abd23979 --- /dev/null +++ b/interface/src/ui/ScriptEditorWindow.cpp @@ -0,0 +1,259 @@ +// +// ScriptEditorWindow.cpp +// interface/src/ui +// +// Created by Thijs Wenker on 4/14/14. +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include + +#include "ui_scriptEditorWindow.h" +#include "ScriptEditorWindow.h" +#include "ScriptEditorWidget.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include "Application.h" +#include "PathUtils.h" + +ScriptEditorWindow::ScriptEditorWindow(QWidget* parent) : + QWidget(parent), + _ScriptEditorWindowUI(new Ui::ScriptEditorWindow), + _loadMenu(new QMenu), + _saveMenu(new QMenu) +{ + setAttribute(Qt::WA_DeleteOnClose); + + _ScriptEditorWindowUI->setupUi(this); + + this->setWindowFlags(Qt::Tool); + addScriptEditorWidget("New script"); + connect(_loadMenu, &QMenu::aboutToShow, this, &ScriptEditorWindow::loadMenuAboutToShow); + _ScriptEditorWindowUI->loadButton->setMenu(_loadMenu); + + _saveMenu->addAction("Save as..", this, SLOT(saveScriptAsClicked()), Qt::CTRL | Qt::SHIFT | Qt::Key_S); + + _ScriptEditorWindowUI->saveButton->setMenu(_saveMenu); + + connect(new QShortcut(QKeySequence("Ctrl+N"), this), &QShortcut::activated, this, &ScriptEditorWindow::newScriptClicked); + connect(new QShortcut(QKeySequence("Ctrl+S"), this), &QShortcut::activated, this,&ScriptEditorWindow::saveScriptClicked); + connect(new QShortcut(QKeySequence("Ctrl+O"), this), &QShortcut::activated, this, &ScriptEditorWindow::loadScriptClicked); + connect(new QShortcut(QKeySequence("F5"), this), &QShortcut::activated, this, &ScriptEditorWindow::toggleRunScriptClicked); + + _ScriptEditorWindowUI->loadButton->setIcon(QIcon(QPixmap(PathUtils::resourcesPath() + "icons/load-script.svg"))); + _ScriptEditorWindowUI->newButton->setIcon(QIcon(QPixmap(PathUtils::resourcesPath() + "icons/new-script.svg"))); + _ScriptEditorWindowUI->saveButton->setIcon(QIcon(QPixmap(PathUtils::resourcesPath() + "icons/save-script.svg"))); + _ScriptEditorWindowUI->toggleRunButton->setIcon(QIcon(QPixmap(PathUtils::resourcesPath() + "icons/start-script.svg"))); +} + +ScriptEditorWindow::~ScriptEditorWindow() { + delete _ScriptEditorWindowUI; +} + +void ScriptEditorWindow::setRunningState(bool run) { + if (_ScriptEditorWindowUI->tabWidget->currentIndex() != -1) { + static_cast(_ScriptEditorWindowUI->tabWidget->currentWidget())->setRunning(run); + } + this->updateButtons(); +} + +void ScriptEditorWindow::updateButtons() { + bool isRunning = _ScriptEditorWindowUI->tabWidget->currentIndex() != -1 && + static_cast(_ScriptEditorWindowUI->tabWidget->currentWidget())->isRunning(); + _ScriptEditorWindowUI->toggleRunButton->setEnabled(_ScriptEditorWindowUI->tabWidget->currentIndex() != -1); + _ScriptEditorWindowUI->toggleRunButton->setIcon(QIcon(QPixmap(PathUtils::resourcesPath() + ((isRunning ? + "icons/stop-script.svg" : "icons/start-script.svg"))))); +} + +void ScriptEditorWindow::loadScriptMenu(const QString& scriptName) { + addScriptEditorWidget("loading...")->loadFile(scriptName); + updateButtons(); +} + +void ScriptEditorWindow::loadScriptClicked() { + QString scriptName = QFileDialog::getOpenFileName(this, tr("Interface"), + qApp->getPreviousScriptLocation(), + tr("JavaScript Files (*.js)")); + if (!scriptName.isEmpty()) { + qApp->setPreviousScriptLocation(scriptName); + addScriptEditorWidget("loading...")->loadFile(scriptName); + updateButtons(); + } +} + +void ScriptEditorWindow::loadMenuAboutToShow() { + _loadMenu->clear(); + QStringList runningScripts = DependencyManager::get()->getRunningScripts(); + if (runningScripts.count() > 0) { + QSignalMapper* signalMapper = new QSignalMapper(this); + foreach (const QString& runningScript, runningScripts) { + QAction* runningScriptAction = new QAction(runningScript, _loadMenu); + connect(runningScriptAction, SIGNAL(triggered()), signalMapper, SLOT(map())); + signalMapper->setMapping(runningScriptAction, runningScript); + _loadMenu->addAction(runningScriptAction); + } + connect(signalMapper, SIGNAL(mapped(const QString &)), this, SLOT(loadScriptMenu(const QString&))); + } else { + QAction* naAction = new QAction("(no running scripts)", _loadMenu); + naAction->setDisabled(true); + _loadMenu->addAction(naAction); + } +} + +void ScriptEditorWindow::newScriptClicked() { + addScriptEditorWidget(QString("New script")); +} + +void ScriptEditorWindow::toggleRunScriptClicked() { + this->setRunningState(!(_ScriptEditorWindowUI->tabWidget->currentIndex() !=-1 + && static_cast(_ScriptEditorWindowUI->tabWidget->currentWidget())->isRunning())); +} + +void ScriptEditorWindow::saveScriptClicked() { + if (_ScriptEditorWindowUI->tabWidget->currentIndex() != -1) { + ScriptEditorWidget* currentScriptWidget = static_cast(_ScriptEditorWindowUI->tabWidget + ->currentWidget()); + currentScriptWidget->save(); + } +} + +void ScriptEditorWindow::saveScriptAsClicked() { + if (_ScriptEditorWindowUI->tabWidget->currentIndex() != -1) { + ScriptEditorWidget* currentScriptWidget = static_cast(_ScriptEditorWindowUI->tabWidget + ->currentWidget()); + currentScriptWidget->saveAs(); + } +} + +ScriptEditorWidget* ScriptEditorWindow::addScriptEditorWidget(QString title) { + ScriptEditorWidget* newScriptEditorWidget = new ScriptEditorWidget(); + connect(newScriptEditorWidget, &ScriptEditorWidget::scriptnameChanged, this, &ScriptEditorWindow::updateScriptNameOrStatus); + connect(newScriptEditorWidget, &ScriptEditorWidget::scriptModified, this, &ScriptEditorWindow::updateScriptNameOrStatus); + connect(newScriptEditorWidget, &ScriptEditorWidget::runningStateChanged, this, &ScriptEditorWindow::updateButtons); + connect(this, &ScriptEditorWindow::windowActivated, newScriptEditorWidget, &ScriptEditorWidget::onWindowActivated); + _ScriptEditorWindowUI->tabWidget->addTab(newScriptEditorWidget, title); + _ScriptEditorWindowUI->tabWidget->setCurrentWidget(newScriptEditorWidget); + newScriptEditorWidget->setUpdatesEnabled(true); + newScriptEditorWidget->adjustSize(); + return newScriptEditorWidget; +} + +void ScriptEditorWindow::tabSwitched(int tabIndex) { + this->updateButtons(); + if (_ScriptEditorWindowUI->tabWidget->currentIndex() != -1) { + ScriptEditorWidget* currentScriptWidget = static_cast(_ScriptEditorWindowUI->tabWidget + ->currentWidget()); + QString modifiedStar = (currentScriptWidget->isModified() ? "*" : ""); + if (currentScriptWidget->getScriptName().length() > 0) { + this->setWindowTitle("Script Editor [" + currentScriptWidget->getScriptName() + modifiedStar + "]"); + } else { + this->setWindowTitle("Script Editor [New script" + modifiedStar + "]"); + } + } else { + this->setWindowTitle("Script Editor"); + } +} + +void ScriptEditorWindow::tabCloseRequested(int tabIndex) { + if (ignoreCloseForModal(nullptr)) { + return; + } + ScriptEditorWidget* closingScriptWidget = static_cast(_ScriptEditorWindowUI->tabWidget + ->widget(tabIndex)); + if(closingScriptWidget->questionSave()) { + _ScriptEditorWindowUI->tabWidget->removeTab(tabIndex); + } +} + +// If this operating system window causes a qml overlay modal dialog (which might not even be seen by the user), closing this window +// will crash the code that was waiting on the dialog result. So that code whousl set inModalDialog to true while the question is up. +// This code will not be necessary when switch out all operating system windows for qml overlays. +bool ScriptEditorWindow::ignoreCloseForModal(QCloseEvent* event) { + if (!inModalDialog) { + return false; + } + // Deliberately not using OffscreenUi, so that the dialog is seen. + QMessageBox::information(this, tr("Interface"), tr("There is a modal dialog that must be answered before closing."), + QMessageBox::Discard, QMessageBox::Discard); + if (event) { + event->ignore(); // don't close + } + return true; +} + +void ScriptEditorWindow::closeEvent(QCloseEvent *event) { + if (ignoreCloseForModal(event)) { + return; + } + bool unsaved_docs_warning = false; + for (int i = 0; i < _ScriptEditorWindowUI->tabWidget->count(); i++){ + if(static_cast(_ScriptEditorWindowUI->tabWidget->widget(i))->isModified()){ + unsaved_docs_warning = true; + break; + } + } + + if (!unsaved_docs_warning || QMessageBox::warning(this, tr("Interface"), + tr("There are some unsaved scripts, are you sure you want to close the editor? Changes will be lost!"), + QMessageBox::Discard | QMessageBox::Cancel, QMessageBox::Cancel) == QMessageBox::Discard) { + event->accept(); + } else { + event->ignore(); + } +} + +void ScriptEditorWindow::updateScriptNameOrStatus() { + ScriptEditorWidget* source = static_cast(QObject::sender()); + QString modifiedStar = (source->isModified()? "*" : ""); + if (source->getScriptName().length() > 0) { + for (int i = 0; i < _ScriptEditorWindowUI->tabWidget->count(); i++){ + if (_ScriptEditorWindowUI->tabWidget->widget(i) == source) { + _ScriptEditorWindowUI->tabWidget->setTabText(i, modifiedStar + QFileInfo(source->getScriptName()).fileName()); + _ScriptEditorWindowUI->tabWidget->setTabToolTip(i, source->getScriptName()); + } + } + } + + if (_ScriptEditorWindowUI->tabWidget->currentWidget() == source) { + if (source->getScriptName().length() > 0) { + this->setWindowTitle("Script Editor [" + source->getScriptName() + modifiedStar + "]"); + } else { + this->setWindowTitle("Script Editor [New script" + modifiedStar + "]"); + } + } +} + +void ScriptEditorWindow::terminateCurrentTab() { + if (_ScriptEditorWindowUI->tabWidget->currentIndex() != -1) { + _ScriptEditorWindowUI->tabWidget->removeTab(_ScriptEditorWindowUI->tabWidget->currentIndex()); + this->raise(); + } +} + +bool ScriptEditorWindow::autoReloadScripts() { + return _ScriptEditorWindowUI->autoReloadCheckBox->isChecked(); +} + +bool ScriptEditorWindow::event(QEvent* event) { + if (event->type() == QEvent::WindowActivate) { + emit windowActivated(); + } + return QWidget::event(event); +} + diff --git a/interface/src/ui/ScriptEditorWindow.h b/interface/src/ui/ScriptEditorWindow.h new file mode 100644 index 0000000000..af9863d136 --- /dev/null +++ b/interface/src/ui/ScriptEditorWindow.h @@ -0,0 +1,64 @@ +// +// ScriptEditorWindow.h +// interface/src/ui +// +// Created by Thijs Wenker on 4/14/14. +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_ScriptEditorWindow_h +#define hifi_ScriptEditorWindow_h + +#include "ScriptEditorWidget.h" + +namespace Ui { + class ScriptEditorWindow; +} + +class ScriptEditorWindow : public QWidget { + Q_OBJECT + +public: + ScriptEditorWindow(QWidget* parent = nullptr); + ~ScriptEditorWindow(); + + void terminateCurrentTab(); + bool autoReloadScripts(); + + bool inModalDialog { false }; + bool ignoreCloseForModal(QCloseEvent* event); + +signals: + void windowActivated(); + +protected: + void closeEvent(QCloseEvent* event) override; + virtual bool event(QEvent* event) override; + +private: + Ui::ScriptEditorWindow* _ScriptEditorWindowUI; + QMenu* _loadMenu; + QMenu* _saveMenu; + + ScriptEditorWidget* addScriptEditorWidget(QString title); + void setRunningState(bool run); + void setScriptName(const QString& scriptName); + +private slots: + void loadScriptMenu(const QString& scriptName); + void loadScriptClicked(); + void newScriptClicked(); + void toggleRunScriptClicked(); + void saveScriptClicked(); + void saveScriptAsClicked(); + void loadMenuAboutToShow(); + void tabSwitched(int tabIndex); + void tabCloseRequested(int tabIndex); + void updateScriptNameOrStatus(); + void updateButtons(); +}; + +#endif // hifi_ScriptEditorWindow_h diff --git a/interface/src/ui/ScriptLineNumberArea.cpp b/interface/src/ui/ScriptLineNumberArea.cpp new file mode 100644 index 0000000000..6d7e9185ea --- /dev/null +++ b/interface/src/ui/ScriptLineNumberArea.cpp @@ -0,0 +1,28 @@ +// +// ScriptLineNumberArea.cpp +// interface/src/ui +// +// Created by Thijs Wenker on 4/30/14. +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "ScriptLineNumberArea.h" + +#include "ScriptEditBox.h" + +ScriptLineNumberArea::ScriptLineNumberArea(ScriptEditBox* scriptEditBox) : + QWidget(scriptEditBox) +{ + _scriptEditBox = scriptEditBox; +} + +QSize ScriptLineNumberArea::sizeHint() const { + return QSize(_scriptEditBox->lineNumberAreaWidth(), 0); +} + +void ScriptLineNumberArea::paintEvent(QPaintEvent* event) { + _scriptEditBox->lineNumberAreaPaintEvent(event); +} diff --git a/interface/src/ui/ScriptLineNumberArea.h b/interface/src/ui/ScriptLineNumberArea.h new file mode 100644 index 0000000000..77de8244ce --- /dev/null +++ b/interface/src/ui/ScriptLineNumberArea.h @@ -0,0 +1,32 @@ +// +// ScriptLineNumberArea.h +// interface/src/ui +// +// Created by Thijs Wenker on 4/30/14. +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_ScriptLineNumberArea_h +#define hifi_ScriptLineNumberArea_h + +#include + +class ScriptEditBox; + +class ScriptLineNumberArea : public QWidget { + +public: + ScriptLineNumberArea(ScriptEditBox* scriptEditBox); + QSize sizeHint() const override; + +protected: + void paintEvent(QPaintEvent* event) override; + +private: + ScriptEditBox* _scriptEditBox; +}; + +#endif // hifi_ScriptLineNumberArea_h diff --git a/interface/src/ui/ScriptsTableWidget.cpp b/interface/src/ui/ScriptsTableWidget.cpp new file mode 100644 index 0000000000..7b4f9e6b1f --- /dev/null +++ b/interface/src/ui/ScriptsTableWidget.cpp @@ -0,0 +1,49 @@ +// +// ScriptsTableWidget.cpp +// interface +// +// Created by Mohammed Nafees on 04/03/2014. +// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include +#include +#include +#include + +#include "ScriptsTableWidget.h" + +ScriptsTableWidget::ScriptsTableWidget(QWidget* parent) : + QTableWidget(parent) { + verticalHeader()->setVisible(false); + horizontalHeader()->setVisible(false); + setShowGrid(false); + setSelectionMode(QAbstractItemView::NoSelection); + setEditTriggers(QAbstractItemView::NoEditTriggers); + setStyleSheet("QTableWidget { border: none; background: transparent; color: #333333; } QToolTip { color: #000000; background: #f9f6e4; padding: 2px; }"); + setToolTipDuration(200); + setWordWrap(true); + setGeometry(0, 0, parent->width(), parent->height()); +} + +void ScriptsTableWidget::paintEvent(QPaintEvent* event) { + QPainter painter(viewport()); + painter.setPen(QColor::fromRgb(225, 225, 225)); // #e1e1e1 + + int y = 0; + for (int i = 0; i < rowCount(); i++) { + painter.drawLine(5, rowHeight(i) + y, width(), rowHeight(i) + y); + y += rowHeight(i); + } + painter.end(); + + QTableWidget::paintEvent(event); +} + +void ScriptsTableWidget::keyPressEvent(QKeyEvent* event) { + // Ignore keys so they will propagate correctly + event->ignore(); +} diff --git a/interface/src/ui/ScriptsTableWidget.h b/interface/src/ui/ScriptsTableWidget.h new file mode 100644 index 0000000000..f5e3407e97 --- /dev/null +++ b/interface/src/ui/ScriptsTableWidget.h @@ -0,0 +1,28 @@ +// +// ScriptsTableWidget.h +// interface +// +// Created by Mohammed Nafees on 04/03/2014. +// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi__ScriptsTableWidget_h +#define hifi__ScriptsTableWidget_h + +#include +#include + +class ScriptsTableWidget : public QTableWidget { + Q_OBJECT +public: + explicit ScriptsTableWidget(QWidget* parent); + +protected: + virtual void paintEvent(QPaintEvent* event) override; + virtual void keyPressEvent(QKeyEvent* event) override; +}; + +#endif // hifi__ScriptsTableWidget_h diff --git a/interface/src/ui/Stats.cpp b/interface/src/ui/Stats.cpp index cedcb923d9..923d9f642d 100644 --- a/interface/src/ui/Stats.cpp +++ b/interface/src/ui/Stats.cpp @@ -38,8 +38,6 @@ using namespace std; static Stats* INSTANCE{ nullptr }; -QString getTextureMemoryPressureModeString(); - Stats* Stats::getInstance() { if (!INSTANCE) { Stats::registerType(); @@ -222,10 +220,10 @@ void Stats::updateStats(bool force) { STAT_UPDATE(audioMixerInPps, roundf(bandwidthRecorder->getAverageInputPacketsPerSecond(NodeType::AudioMixer))); STAT_UPDATE(audioMixerOutKbps, roundf(bandwidthRecorder->getAverageOutputKilobitsPerSecond(NodeType::AudioMixer))); STAT_UPDATE(audioMixerOutPps, roundf(bandwidthRecorder->getAverageOutputPacketsPerSecond(NodeType::AudioMixer))); + STAT_UPDATE(audioMicOutboundPPS, audioClient->getMicAudioOutboundPPS()); + STAT_UPDATE(audioSilentOutboundPPS, audioClient->getSilentOutboundPPS()); STAT_UPDATE(audioAudioInboundPPS, audioClient->getAudioInboundPPS()); STAT_UPDATE(audioSilentInboundPPS, audioClient->getSilentInboundPPS()); - STAT_UPDATE(audioOutboundPPS, audioClient->getAudioOutboundPPS()); - STAT_UPDATE(audioSilentOutboundPPS, audioClient->getSilentOutboundPPS()); } else { STAT_UPDATE(audioMixerKbps, -1); STAT_UPDATE(audioMixerPps, -1); @@ -233,7 +231,7 @@ void Stats::updateStats(bool force) { STAT_UPDATE(audioMixerInPps, -1); STAT_UPDATE(audioMixerOutKbps, -1); STAT_UPDATE(audioMixerOutPps, -1); - STAT_UPDATE(audioOutboundPPS, -1); + STAT_UPDATE(audioMicOutboundPPS, -1); STAT_UPDATE(audioSilentOutboundPPS, -1); STAT_UPDATE(audioAudioInboundPPS, -1); STAT_UPDATE(audioSilentInboundPPS, -1); @@ -342,12 +340,10 @@ void Stats::updateStats(bool force) { STAT_UPDATE(glContextSwapchainMemory, (int)BYTES_TO_MB(gl::Context::getSwapchainMemoryUsage())); STAT_UPDATE(qmlTextureMemory, (int)BYTES_TO_MB(OffscreenQmlSurface::getUsedTextureMemory())); - STAT_UPDATE(texturePendingTransfers, (int)BYTES_TO_MB(gpu::Texture::getTextureTransferPendingSize())); STAT_UPDATE(gpuTextureMemory, (int)BYTES_TO_MB(gpu::Texture::getTextureGPUMemoryUsage())); STAT_UPDATE(gpuTextureVirtualMemory, (int)BYTES_TO_MB(gpu::Texture::getTextureGPUVirtualMemoryUsage())); STAT_UPDATE(gpuTextureFramebufferMemory, (int)BYTES_TO_MB(gpu::Texture::getTextureGPUFramebufferMemoryUsage())); STAT_UPDATE(gpuTextureSparseMemory, (int)BYTES_TO_MB(gpu::Texture::getTextureGPUSparseMemoryUsage())); - STAT_UPDATE(gpuTextureMemoryPressureState, getTextureMemoryPressureModeString()); STAT_UPDATE(gpuSparseTextureEnabled, gpuContext->getBackend()->isTextureManagementSparseEnabled() ? 1 : 0); STAT_UPDATE(gpuFreeMemory, (int)BYTES_TO_MB(gpu::Context::getFreeGPUMemory())); STAT_UPDATE(rectifiedTextureCount, (int)RECTIFIED_TEXTURE_COUNT.load()); diff --git a/interface/src/ui/Stats.h b/interface/src/ui/Stats.h index a93a255a06..0ce113e0a0 100644 --- a/interface/src/ui/Stats.h +++ b/interface/src/ui/Stats.h @@ -77,7 +77,7 @@ class Stats : public QQuickItem { STATS_PROPERTY(int, audioMixerOutPps, 0) STATS_PROPERTY(int, audioMixerKbps, 0) STATS_PROPERTY(int, audioMixerPps, 0) - STATS_PROPERTY(int, audioOutboundPPS, 0) + STATS_PROPERTY(int, audioMicOutboundPPS, 0) STATS_PROPERTY(int, audioSilentOutboundPPS, 0) STATS_PROPERTY(int, audioAudioInboundPPS, 0) STATS_PROPERTY(int, audioSilentInboundPPS, 0) @@ -117,13 +117,11 @@ class Stats : public QQuickItem { STATS_PROPERTY(int, gpuTexturesSparse, 0) STATS_PROPERTY(int, glContextSwapchainMemory, 0) STATS_PROPERTY(int, qmlTextureMemory, 0) - STATS_PROPERTY(int, texturePendingTransfers, 0) STATS_PROPERTY(int, gpuTextureMemory, 0) STATS_PROPERTY(int, gpuTextureVirtualMemory, 0) STATS_PROPERTY(int, gpuTextureFramebufferMemory, 0) STATS_PROPERTY(int, gpuTextureSparseMemory, 0) STATS_PROPERTY(int, gpuSparseTextureEnabled, 0) - STATS_PROPERTY(QString, gpuTextureMemoryPressureState, QString()) STATS_PROPERTY(int, gpuFreeMemory, 0) STATS_PROPERTY(float, gpuFrameTime, 0) STATS_PROPERTY(float, batchFrameTime, 0) @@ -200,7 +198,7 @@ signals: void audioMixerOutPpsChanged(); void audioMixerKbpsChanged(); void audioMixerPpsChanged(); - void audioOutboundPPSChanged(); + void audioMicOutboundPPSChanged(); void audioSilentOutboundPPSChanged(); void audioAudioInboundPPSChanged(); void audioSilentInboundPPSChanged(); @@ -234,7 +232,6 @@ signals: void timingStatsChanged(); void glContextSwapchainMemoryChanged(); void qmlTextureMemoryChanged(); - void texturePendingTransfersChanged(); void gpuBuffersChanged(); void gpuBufferMemoryChanged(); void gpuTexturesChanged(); @@ -243,7 +240,6 @@ signals: void gpuTextureVirtualMemoryChanged(); void gpuTextureFramebufferMemoryChanged(); void gpuTextureSparseMemoryChanged(); - void gpuTextureMemoryPressureStateChanged(); void gpuSparseTextureEnabledChanged(); void gpuFreeMemoryChanged(); void gpuFrameTimeChanged(); diff --git a/interface/src/ui/overlays/Overlays.cpp b/interface/src/ui/overlays/Overlays.cpp index 6514052d26..f40dd522c4 100644 --- a/interface/src/ui/overlays/Overlays.cpp +++ b/interface/src/ui/overlays/Overlays.cpp @@ -431,9 +431,7 @@ RayToOverlayIntersectionResult Overlays::findRayIntersectionInternal(const PickR if (thisOverlay->findRayIntersectionExtraInfo(ray.origin, ray.direction, thisDistance, thisFace, thisSurfaceNormal, thisExtraInfo)) { bool isDrawInFront = thisOverlay->getDrawInFront(); - if ((bestIsFront && isDrawInFront && thisDistance < bestDistance) - || (!bestIsFront && (isDrawInFront || thisDistance < bestDistance))) { - + if (thisDistance < bestDistance && (!bestIsFront || isDrawInFront)) { bestIsFront = isDrawInFront; bestDistance = thisDistance; result.intersects = true; diff --git a/interface/src/ui/overlays/Web3DOverlay.cpp b/interface/src/ui/overlays/Web3DOverlay.cpp index 97e5344062..ba864d2c5c 100644 --- a/interface/src/ui/overlays/Web3DOverlay.cpp +++ b/interface/src/ui/overlays/Web3DOverlay.cpp @@ -270,7 +270,7 @@ void Web3DOverlay::render(RenderArgs* args) { if (!_texture) { auto webSurface = _webSurface; - _texture = gpu::TexturePointer(gpu::Texture::createExternal(OffscreenQmlSurface::getDiscardLambda())); + _texture = gpu::TexturePointer(gpu::Texture::createExternal2D(OffscreenQmlSurface::getDiscardLambda())); _texture->setSource(__FUNCTION__); } OffscreenQmlSurface::TextureAndFence newTextureAndFence; diff --git a/interface/ui/scriptEditorWidget.ui b/interface/ui/scriptEditorWidget.ui new file mode 100644 index 0000000000..e2e538a595 --- /dev/null +++ b/interface/ui/scriptEditorWidget.ui @@ -0,0 +1,142 @@ + + + ScriptEditorWidget + + + + 0 + 0 + 691 + 549 + + + + + 0 + 0 + + + + + 690 + 328 + + + + font-family: Helvetica, Arial, sans-serif; + + + QDockWidget::DockWidgetFloatable|QDockWidget::DockWidgetMovable + + + Qt::NoDockWidgetArea + + + Edit Script + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + Courier + -1 + 50 + false + false + + + + font: 16px "Courier"; + + + + + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + font: 13px "Helvetica","Arial","sans-serif"; + + + Debug Log: + + + + + + + + Helvetica,Arial,sans-serif + -1 + 50 + false + false + + + + font: 13px "Helvetica","Arial","sans-serif"; + + + Run on the fly (Careful: Any valid change made to the code will run immediately) + + + + + + + Clear + + + + 16 + 16 + + + + + + + + + + + + ScriptEditBox + QTextEdit +
ui/ScriptEditBox.h
+
+
+ +
diff --git a/interface/ui/scriptEditorWindow.ui b/interface/ui/scriptEditorWindow.ui new file mode 100644 index 0000000000..1e50aaef0b --- /dev/null +++ b/interface/ui/scriptEditorWindow.ui @@ -0,0 +1,324 @@ + + + ScriptEditorWindow + + + Qt::NonModal + + + + 0 + 0 + 780 + 717 + + + + + 400 + 250 + + + + Script Editor + + + font-family: Helvetica, Arial, sans-serif; + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 3 + + + QLayout::SetNoConstraint + + + 0 + + + 0 + + + + + New Script (Ctrl+N) + + + New + + + + 32 + 32 + + + + + + + + + 30 + 0 + + + + + 25 + 0 + + + + Load Script (Ctrl+O) + + + Load + + + + 32 + 32 + + + + false + + + QToolButton::MenuButtonPopup + + + Qt::ToolButtonIconOnly + + + + + + + + 30 + 0 + + + + + 32 + 0 + + + + Qt::NoFocus + + + Qt::NoContextMenu + + + Save Script (Ctrl+S) + + + Save + + + + 32 + 32 + + + + 316 + + + QToolButton::MenuButtonPopup + + + + + + + Toggle Run Script (F5) + + + Run/Stop + + + + 32 + 32 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + font: 13px "Helvetica","Arial","sans-serif"; + + + Automatically reload externally changed files + + + + + + + + + true + + + + 250 + 80 + + + + QTabWidget::West + + + QTabWidget::Triangular + + + -1 + + + Qt::ElideNone + + + true + + + true + + + + + + + + + saveButton + clicked() + ScriptEditorWindow + saveScriptClicked() + + + 236 + 10 + + + 199 + 264 + + + + + toggleRunButton + clicked() + ScriptEditorWindow + toggleRunScriptClicked() + + + 330 + 10 + + + 199 + 264 + + + + + newButton + clicked() + ScriptEditorWindow + newScriptClicked() + + + 58 + 10 + + + 199 + 264 + + + + + loadButton + clicked() + ScriptEditorWindow + loadScriptClicked() + + + 85 + 10 + + + 199 + 264 + + + + + tabWidget + currentChanged(int) + ScriptEditorWindow + tabSwitched(int) + + + 352 + 360 + + + 352 + 340 + + + + + tabWidget + tabCloseRequested(int) + ScriptEditorWindow + tabCloseRequested(int) + + + 352 + 360 + + + 352 + 340 + + + + + diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 0520e5c5a1..9ecd0f6352 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -558,15 +558,15 @@ static const std::vector LATERAL_SPEEDS = { 0.2f, 0.65f }; // m/s void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPosition, const glm::vec3& worldVelocity, const glm::quat& worldRotation, CharacterControllerState ccState) { - glm::vec3 forward = worldRotation * IDENTITY_FORWARD; + glm::vec3 front = worldRotation * IDENTITY_FRONT; glm::vec3 workingVelocity = worldVelocity; { glm::vec3 localVel = glm::inverse(worldRotation) * workingVelocity; - float forwardSpeed = glm::dot(localVel, IDENTITY_FORWARD); + float forwardSpeed = glm::dot(localVel, IDENTITY_FRONT); float lateralSpeed = glm::dot(localVel, IDENTITY_RIGHT); - float turningSpeed = glm::orientedAngle(forward, _lastForward, IDENTITY_UP) / deltaTime; + float turningSpeed = glm::orientedAngle(front, _lastFront, IDENTITY_UP) / deltaTime; // filter speeds using a simple moving average. _averageForwardSpeed.updateAverage(forwardSpeed); @@ -852,7 +852,7 @@ void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPos _lastEnableInverseKinematics = _enableInverseKinematics; } - _lastForward = forward; + _lastFront = front; _lastPosition = worldPosition; _lastVelocity = workingVelocity; } diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index 41cc5cabc6..b2cc877460 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -267,7 +267,7 @@ protected: int _rightElbowJointIndex { -1 }; int _rightShoulderJointIndex { -1 }; - glm::vec3 _lastForward; + glm::vec3 _lastFront; glm::vec3 _lastPosition; glm::vec3 _lastVelocity; diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 4a2de0a64b..c32b5600d9 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -160,7 +160,7 @@ AudioClient::AudioClient() : _loopbackAudioOutput(NULL), _loopbackOutputDevice(NULL), _inputRingBuffer(0), - _localInjectorsStream(0, 1), + _localInjectorsStream(0), _receivedAudioStream(RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES), _isStereoInput(false), _outputStarveDetectionStartTimeMsec(0), @@ -184,6 +184,7 @@ AudioClient::AudioClient() : _outgoingAvatarAudioSequenceNumber(0), _audioOutputIODevice(_localInjectorsStream, _receivedAudioStream, this), _stats(&_receivedAudioStream), + _inputGate(), _positionGetter(DEFAULT_POSITION_GETTER), _orientationGetter(DEFAULT_ORIENTATION_GETTER) { // avoid putting a lock in the device callback @@ -970,87 +971,14 @@ void AudioClient::handleLocalEchoAndReverb(QByteArray& inputByteArray) { } } -void AudioClient::handleAudioInput(QByteArray& audioBuffer) { - if (_muted) { - _lastInputLoudness = 0.0f; - _timeSinceLastClip = 0.0f; - } else { - int16_t* samples = reinterpret_cast(audioBuffer.data()); - int numSamples = audioBuffer.size() / sizeof(AudioConstants::SAMPLE_SIZE); - bool didClip = false; - - bool shouldRemoveDCOffset = !_isPlayingBackRecording && !_isStereoInput; - if (shouldRemoveDCOffset) { - _noiseGate.removeDCOffset(samples, numSamples); - } - - bool shouldNoiseGate = (_isPlayingBackRecording || !_isStereoInput) && _isNoiseGateEnabled; - if (shouldNoiseGate) { - _noiseGate.gateSamples(samples, numSamples); - _lastInputLoudness = _noiseGate.getLastLoudness(); - didClip = _noiseGate.clippedInLastBlock(); - } else { - float loudness = 0.0f; - for (int i = 0; i < numSamples; ++i) { - int16_t sample = std::abs(samples[i]); - loudness += (float)sample; - didClip = didClip || - (sample > (AudioConstants::MAX_SAMPLE_VALUE * AudioNoiseGate::CLIPPING_THRESHOLD)); - } - _lastInputLoudness = fabs(loudness / numSamples); - } - - if (didClip) { - _timeSinceLastClip = 0.0f; - } else if (_timeSinceLastClip >= 0.0f) { - _timeSinceLastClip += (float)numSamples / (float)AudioConstants::SAMPLE_RATE; - } - - emit inputReceived({ audioBuffer.data(), numSamples }); - - if (_noiseGate.openedInLastBlock()) { - emit noiseGateOpened(); - } else if (_noiseGate.closedInLastBlock()) { - emit noiseGateClosed(); - } - } - - // the codec needs a flush frame before sending silent packets, so - // do not send one if the gate closed in this block (eventually this can be crossfaded). - auto packetType = _shouldEchoToServer ? - PacketType::MicrophoneAudioWithEcho : PacketType::MicrophoneAudioNoEcho; - if (_lastInputLoudness == 0.0f && !_noiseGate.closedInLastBlock()) { - packetType = PacketType::SilentAudioFrame; - _silentOutbound.increment(); - } else { - _audioOutbound.increment(); - } - - Transform audioTransform; - audioTransform.setTranslation(_positionGetter()); - audioTransform.setRotation(_orientationGetter()); - - QByteArray encodedBuffer; - if (_encoder) { - _encoder->encode(audioBuffer, encodedBuffer); - } else { - encodedBuffer = audioBuffer; - } - - emitAudioPacket(encodedBuffer.data(), encodedBuffer.size(), _outgoingAvatarAudioSequenceNumber, - audioTransform, avatarBoundingBoxCorner, avatarBoundingBoxScale, - packetType, _selectedCodecName); - _stats.sentPacket(); -} - -void AudioClient::handleMicAudioInput() { +void AudioClient::handleAudioInput() { if (!_inputDevice || _isPlayingBackRecording) { return; } // input samples required to produce exactly NETWORK_FRAME_SAMPLES of output - const int inputSamplesRequired = (_inputToNetworkResampler ? - _inputToNetworkResampler->getMinInput(AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL) : + const int inputSamplesRequired = (_inputToNetworkResampler ? + _inputToNetworkResampler->getMinInput(AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL) : AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL) * _inputFormat.channelCount(); const auto inputAudioSamples = std::unique_ptr(new int16_t[inputSamplesRequired]); @@ -1073,27 +1001,126 @@ void AudioClient::handleMicAudioInput() { static int16_t networkAudioSamples[AudioConstants::NETWORK_FRAME_SAMPLES_STEREO]; while (_inputRingBuffer.samplesAvailable() >= inputSamplesRequired) { - if (_muted) { - _inputRingBuffer.shiftReadPosition(inputSamplesRequired); - } else { + + if (!_muted) { + + + // Increment the time since the last clip + if (_timeSinceLastClip >= 0.0f) { + _timeSinceLastClip += (float)numNetworkSamples / (float)AudioConstants::SAMPLE_RATE; + } + _inputRingBuffer.readSamples(inputAudioSamples.get(), inputSamplesRequired); possibleResampling(_inputToNetworkResampler, inputAudioSamples.get(), networkAudioSamples, inputSamplesRequired, numNetworkSamples, _inputFormat.channelCount(), _desiredInputFormat.channelCount()); + + // Remove DC offset + if (!_isStereoInput) { + _inputGate.removeDCOffset(networkAudioSamples, numNetworkSamples); + } + + // only impose the noise gate and perform tone injection if we are sending mono audio + if (!_isStereoInput && _isNoiseGateEnabled) { + _inputGate.gateSamples(networkAudioSamples, numNetworkSamples); + + // if we performed the noise gate we can get values from it instead of enumerating the samples again + _lastInputLoudness = _inputGate.getLastLoudness(); + + if (_inputGate.clippedInLastBlock()) { + _timeSinceLastClip = 0.0f; + } + + } else { + float loudness = 0.0f; + + for (int i = 0; i < numNetworkSamples; i++) { + int thisSample = std::abs(networkAudioSamples[i]); + loudness += (float)thisSample; + + if (thisSample > (AudioConstants::MAX_SAMPLE_VALUE * AudioNoiseGate::CLIPPING_THRESHOLD)) { + _timeSinceLastClip = 0.0f; + } + } + + _lastInputLoudness = fabs(loudness / numNetworkSamples); + } + + emit inputReceived({ reinterpret_cast(networkAudioSamples), numNetworkBytes }); + + if (_inputGate.openedInLastBlock()) { + emit noiseGateOpened(); + } else if (_inputGate.closedInLastBlock()) { + emit noiseGateClosed(); + } + + } else { + // our input loudness is 0, since we're muted + _lastInputLoudness = 0; + _timeSinceLastClip = 0.0f; + + _inputRingBuffer.shiftReadPosition(inputSamplesRequired); } + + auto packetType = _shouldEchoToServer ? + PacketType::MicrophoneAudioWithEcho : PacketType::MicrophoneAudioNoEcho; + + // if the _inputGate closed in this last frame, then we don't actually want + // to send a silent packet, instead, we want to go ahead and encode and send + // the output from the input gate (eventually, this could be crossfaded) + // and allow the codec to properly encode down to silent/zero. If we still + // have _lastInputLoudness of 0 in our NEXT frame, we will send a silent packet + if (_lastInputLoudness == 0 && !_inputGate.closedInLastBlock()) { + packetType = PacketType::SilentAudioFrame; + _silentOutbound.increment(); + } else { + _micAudioOutbound.increment(); + } + + Transform audioTransform; + audioTransform.setTranslation(_positionGetter()); + audioTransform.setRotation(_orientationGetter()); + // FIXME find a way to properly handle both playback audio and user audio concurrently + + QByteArray decodedBuffer(reinterpret_cast(networkAudioSamples), numNetworkBytes); + QByteArray encodedBuffer; + if (_encoder) { + _encoder->encode(decodedBuffer, encodedBuffer); + } else { + encodedBuffer = decodedBuffer; + } + + emitAudioPacket(encodedBuffer.constData(), encodedBuffer.size(), _outgoingAvatarAudioSequenceNumber, + audioTransform, avatarBoundingBoxCorner, avatarBoundingBoxScale, + packetType, _selectedCodecName); + _stats.sentPacket(); + int bytesInInputRingBuffer = _inputRingBuffer.samplesAvailable() * AudioConstants::SAMPLE_SIZE; float msecsInInputRingBuffer = bytesInInputRingBuffer / (float)(_inputFormat.bytesForDuration(USECS_PER_MSEC)); _stats.updateInputMsUnplayed(msecsInInputRingBuffer); - - QByteArray audioBuffer(reinterpret_cast(networkAudioSamples), numNetworkBytes); - handleAudioInput(audioBuffer); } } +// FIXME - should this go through the noise gate and honor mute and echo? void AudioClient::handleRecordedAudioInput(const QByteArray& audio) { - QByteArray audioBuffer(audio); - handleAudioInput(audioBuffer); + Transform audioTransform; + audioTransform.setTranslation(_positionGetter()); + audioTransform.setRotation(_orientationGetter()); + + QByteArray encodedBuffer; + if (_encoder) { + _encoder->encode(audio, encodedBuffer); + } else { + encodedBuffer = audio; + } + + _micAudioOutbound.increment(); + + // FIXME check a flag to see if we should echo audio? + emitAudioPacket(encodedBuffer.data(), encodedBuffer.size(), _outgoingAvatarAudioSequenceNumber, + audioTransform, avatarBoundingBoxCorner, avatarBoundingBoxScale, + PacketType::MicrophoneAudioWithEcho, _selectedCodecName); } void AudioClient::prepareLocalAudioInjectors() { @@ -1407,7 +1434,7 @@ bool AudioClient::switchInputToAudioDevice(const QAudioDeviceInfo& inputDeviceIn lock.unlock(); if (_inputDevice) { - connect(_inputDevice, SIGNAL(readyRead()), this, SLOT(handleMicAudioInput())); + connect(_inputDevice, SIGNAL(readyRead()), this, SLOT(handleAudioInput())); supportedFormat = true; } else { qCDebug(audioclient) << "Error starting audio input -" << _audioInput->error(); @@ -1513,39 +1540,12 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDevice // setup our general output device for audio-mixer audio _audioOutput = new QAudioOutput(outputDeviceInfo, _outputFormat, this); + int osDefaultBufferSize = _audioOutput->bufferSize(); int deviceChannelCount = _outputFormat.channelCount(); - int frameSize = (AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * deviceChannelCount * _outputFormat.sampleRate()) / _desiredOutputFormat.sampleRate(); - int requestedSize = _sessionOutputBufferSizeFrames * frameSize * AudioConstants::SAMPLE_SIZE; + int deviceFrameSize = (AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * deviceChannelCount * _outputFormat.sampleRate()) / _desiredOutputFormat.sampleRate(); + int requestedSize = _sessionOutputBufferSizeFrames * deviceFrameSize * AudioConstants::SAMPLE_SIZE; _audioOutput->setBufferSize(requestedSize); - // initialize mix buffers on the _audioOutput thread to avoid races - connect(_audioOutput, &QAudioOutput::stateChanged, [&, frameSize, requestedSize](QAudio::State state) { - if (state == QAudio::ActiveState) { - // restrict device callback to _outputPeriod samples - _outputPeriod = (_audioOutput->periodSize() / AudioConstants::SAMPLE_SIZE) * 2; - _outputMixBuffer = new float[_outputPeriod]; - _outputScratchBuffer = new int16_t[_outputPeriod]; - - // size local output mix buffer based on resampled network frame size - _networkPeriod = _localToOutputResampler->getMaxOutput(AudioConstants::NETWORK_FRAME_SAMPLES_STEREO); - _localOutputMixBuffer = new float[_networkPeriod]; - int localPeriod = _outputPeriod * 2; - _localInjectorsStream.resizeForFrameSize(localPeriod); - - int bufferSize = _audioOutput->bufferSize(); - int bufferSamples = bufferSize / AudioConstants::SAMPLE_SIZE; - int bufferFrames = bufferSamples / (float)frameSize; - qCDebug(audioclient) << "frame (samples):" << frameSize; - qCDebug(audioclient) << "buffer (frames):" << bufferFrames; - qCDebug(audioclient) << "buffer (samples):" << bufferSamples; - qCDebug(audioclient) << "buffer (bytes):" << bufferSize; - qCDebug(audioclient) << "requested (bytes):" << requestedSize; - qCDebug(audioclient) << "period (samples):" << _outputPeriod; - qCDebug(audioclient) << "local buffer (samples):" << localPeriod; - - disconnect(_audioOutput, &QAudioOutput::stateChanged, 0, 0); - } - }); connect(_audioOutput, &QAudioOutput::notify, this, &AudioClient::outputNotify); _audioOutputIODevice.start(); @@ -1555,6 +1555,18 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDevice _audioOutput->start(&_audioOutputIODevice); lock.unlock(); + int periodSampleSize = _audioOutput->periodSize() / AudioConstants::SAMPLE_SIZE; + // device callback is not restricted to periodSampleSize, so double the mix/scratch buffer sizes + _outputPeriod = periodSampleSize * 2; + _outputMixBuffer = new float[_outputPeriod]; + _outputScratchBuffer = new int16_t[_outputPeriod]; + _localOutputMixBuffer = new float[_outputPeriod]; + _localInjectorsStream.resizeForFrameSize(_outputPeriod * 2); + + qCDebug(audioclient) << "Output Buffer capacity in frames: " << _audioOutput->bufferSize() / AudioConstants::SAMPLE_SIZE / (float)deviceFrameSize << + "requested bytes:" << requestedSize << "actual bytes:" << _audioOutput->bufferSize() << + "os default:" << osDefaultBufferSize << "period size:" << _audioOutput->periodSize(); + // setup a loopback audio output device _loopbackAudioOutput = new QAudioOutput(outputDeviceInfo, _outputFormat, this); diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index 139749e8e8..7e9acc0586 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -124,16 +124,16 @@ public: void selectAudioFormat(const QString& selectedCodecName); Q_INVOKABLE QString getSelectedAudioFormat() const { return _selectedCodecName; } - Q_INVOKABLE bool getNoiseGateOpen() const { return _noiseGate.isOpen(); } + Q_INVOKABLE bool getNoiseGateOpen() const { return _inputGate.isOpen(); } + Q_INVOKABLE float getSilentOutboundPPS() const { return _silentOutbound.rate(); } + Q_INVOKABLE float getMicAudioOutboundPPS() const { return _micAudioOutbound.rate(); } Q_INVOKABLE float getSilentInboundPPS() const { return _silentInbound.rate(); } Q_INVOKABLE float getAudioInboundPPS() const { return _audioInbound.rate(); } - Q_INVOKABLE float getSilentOutboundPPS() const { return _silentOutbound.rate(); } - Q_INVOKABLE float getAudioOutboundPPS() const { return _audioOutbound.rate(); } const MixedProcessedAudioStream& getReceivedAudioStream() const { return _receivedAudioStream; } MixedProcessedAudioStream& getReceivedAudioStream() { return _receivedAudioStream; } - float getLastInputLoudness() const { return glm::max(_lastInputLoudness - _noiseGate.getMeasuredFloor(), 0.0f); } + float getLastInputLoudness() const { return glm::max(_lastInputLoudness - _inputGate.getMeasuredFloor(), 0.0f); } float getTimeSinceLastClip() const { return _timeSinceLastClip; } float getAudioAverageInputLoudness() const { return _lastInputLoudness; } @@ -180,7 +180,7 @@ public slots: void handleMismatchAudioFormat(SharedNodePointer node, const QString& currentCodec, const QString& recievedCodec); void sendDownstreamAudioStatsPacket() { _stats.publish(); } - void handleMicAudioInput(); + void handleAudioInput(); void handleRecordedAudioInput(const QByteArray& audio); void reset(); void audioMixerKilled(); @@ -250,7 +250,6 @@ protected: private: void outputFormatChanged(); - void handleAudioInput(QByteArray& audioBuffer); bool mixLocalAudioInjectors(float* mixBuffer); float azimuthForSource(const glm::vec3& relativePosition); float gainForSource(float distance, float volume); @@ -340,7 +339,6 @@ private: int16_t _networkScratchBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_AMBISONIC]; // for local audio (used by audio injectors thread) - int _networkPeriod { 0 }; float _localMixBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_STEREO]; int16_t _localScratchBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_AMBISONIC]; float* _localOutputMixBuffer { NULL }; @@ -373,7 +371,7 @@ private: AudioIOStats _stats; - AudioNoiseGate _noiseGate; + AudioNoiseGate _inputGate; AudioPositionGetter _positionGetter; AudioOrientationGetter _orientationGetter; @@ -397,7 +395,7 @@ private: QThread* _checkDevicesThread { nullptr }; RateCounter<> _silentOutbound; - RateCounter<> _audioOutbound; + RateCounter<> _micAudioOutbound; RateCounter<> _silentInbound; RateCounter<> _audioInbound; }; diff --git a/libraries/avatars/src/HeadData.cpp b/libraries/avatars/src/HeadData.cpp index bf8593f1d9..72516d9740 100644 --- a/libraries/avatars/src/HeadData.cpp +++ b/libraries/avatars/src/HeadData.cpp @@ -65,8 +65,8 @@ glm::quat HeadData::getOrientation() const { void HeadData::setOrientation(const glm::quat& orientation) { // rotate body about vertical axis glm::quat bodyOrientation = _owningAvatar->getOrientation(); - glm::vec3 newForward = glm::inverse(bodyOrientation) * (orientation * IDENTITY_FORWARD); - bodyOrientation = bodyOrientation * glm::angleAxis(atan2f(-newForward.x, -newForward.z), glm::vec3(0.0f, 1.0f, 0.0f)); + glm::vec3 newFront = glm::inverse(bodyOrientation) * (orientation * IDENTITY_FRONT); + bodyOrientation = bodyOrientation * glm::angleAxis(atan2f(-newFront.x, -newFront.z), glm::vec3(0.0f, 1.0f, 0.0f)); _owningAvatar->setOrientation(bodyOrientation); // the rest goes to the head diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index 5a317f64bc..b23b59d3f0 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -355,16 +355,14 @@ void OpenGLDisplayPlugin::customizeContext() { if ((image.width() > 0) && (image.height() > 0)) { cursorData.texture.reset( - gpu::Texture::createStrict( + gpu::Texture::create2D( gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA), image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); cursorData.texture->setSource("cursor texture"); auto usage = gpu::Texture::Usage::Builder().withColor().withAlpha(); cursorData.texture->setUsage(usage.build()); - cursorData.texture->setStoredMipFormat(gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA)); - cursorData.texture->assignStoredMip(0, image.byteCount(), image.constBits()); - cursorData.texture->autoGenerateMips(-1); + cursorData.texture->assignStoredMip(0, gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA), image.byteCount(), image.constBits()); } } } diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index c55d985a62..a8b8ba3618 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -296,32 +296,33 @@ void HmdDisplayPlugin::internalPresent() { image = image.convertToFormat(QImage::Format_RGBA8888); if (!_previewTexture) { _previewTexture.reset( - gpu::Texture::createStrict( + gpu::Texture::create2D( gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA), image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); _previewTexture->setSource("HMD Preview Texture"); _previewTexture->setUsage(gpu::Texture::Usage::Builder().withColor().build()); - _previewTexture->setStoredMipFormat(gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA)); - _previewTexture->assignStoredMip(0, image.byteCount(), image.constBits()); + _previewTexture->assignStoredMip(0, gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA), image.byteCount(), image.constBits()); _previewTexture->autoGenerateMips(-1); } - auto viewport = getViewportForSourceSize(uvec2(_previewTexture->getDimensions())); + if (getGLBackend()->isTextureReady(_previewTexture)) { + auto viewport = getViewportForSourceSize(uvec2(_previewTexture->getDimensions())); - render([&](gpu::Batch& batch) { - batch.enableStereo(false); - batch.resetViewTransform(); - batch.setFramebuffer(gpu::FramebufferPointer()); - batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, vec4(0)); - batch.setStateScissorRect(viewport); - batch.setViewportTransform(viewport); - batch.setResourceTexture(0, _previewTexture); - batch.setPipeline(_presentPipeline); - batch.draw(gpu::TRIANGLE_STRIP, 4); - }); - _clearPreviewFlag = false; - swapBuffers(); + render([&](gpu::Batch& batch) { + batch.enableStereo(false); + batch.resetViewTransform(); + batch.setFramebuffer(gpu::FramebufferPointer()); + batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, vec4(0)); + batch.setStateScissorRect(viewport); + batch.setViewportTransform(viewport); + batch.setResourceTexture(0, _previewTexture); + batch.setPipeline(_presentPipeline); + batch.draw(gpu::TRIANGLE_STRIP, 4); + }); + _clearPreviewFlag = false; + swapBuffers(); + } } postPreview(); diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index fb6054a514..27e00b47c6 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -146,7 +146,6 @@ void EntityTreeRenderer::clear() { void EntityTreeRenderer::reloadEntityScripts() { _entitiesScriptEngine->unloadAllEntityScripts(); - _entitiesScriptEngine->resetModuleCache(); foreach(auto entity, _entitiesInScene) { if (!entity->getScript().isEmpty()) { _entitiesScriptEngine->loadEntityScript(entity->getEntityItemID(), entity->getScript(), true); @@ -941,7 +940,7 @@ void EntityTreeRenderer::mouseMoveEvent(QMouseEvent* event) { void EntityTreeRenderer::deletingEntity(const EntityItemID& entityID) { if (_tree && !_shuttingDown && _entitiesScriptEngine) { - _entitiesScriptEngine->unloadEntityScript(entityID, true); + _entitiesScriptEngine->unloadEntityScript(entityID); } forceRecheckEntities(); // reset our state to force checking our inside/outsideness of entities @@ -996,7 +995,7 @@ void EntityTreeRenderer::checkAndCallPreload(const EntityItemID& entityID, const } bool shouldLoad = entity->shouldPreloadScript() && _entitiesScriptEngine; QString scriptUrl = entity->getScript(); - if (shouldLoad && (unloadFirst || scriptUrl.isEmpty())) { + if ((unloadFirst && shouldLoad) || scriptUrl.isEmpty()) { _entitiesScriptEngine->unloadEntityScript(entityID); entity->scriptHasUnloaded(); } diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp index 1d58527427..7359a548fc 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp @@ -14,7 +14,6 @@ #include #include #include -#include "ModelScriptingInterface.h" #if defined(__GNUC__) && !defined(__clang__) #pragma GCC diagnostic push @@ -54,8 +53,6 @@ #include "PhysicalEntitySimulation.h" gpu::PipelinePointer RenderablePolyVoxEntityItem::_pipeline = nullptr; -gpu::PipelinePointer RenderablePolyVoxEntityItem::_wireframePipeline = nullptr; - const float MARCHING_CUBE_COLLISION_HULL_OFFSET = 0.5; @@ -76,7 +73,7 @@ const float MARCHING_CUBE_COLLISION_HULL_OFFSET = 0.5; _meshDirty In RenderablePolyVoxEntityItem::render, these flags are checked and changes are propagated along the chain. - decompressVolumeData() is called to decompress _voxelData into _volData. recomputeMesh() is called to invoke the + decompressVolumeData() is called to decompress _voxelData into _volData. getMesh() is called to invoke the polyVox surface extractor to create _mesh (as well as set Simulation _dirtyFlags). Because Simulation::DIRTY_SHAPE is set, isReadyToComputeShape() gets called and _shape is created either from _volData or _shape, depending on the surface style. @@ -84,7 +81,7 @@ const float MARCHING_CUBE_COLLISION_HULL_OFFSET = 0.5; When a script changes _volData, compressVolumeDataAndSendEditPacket is called to update _voxelData and to send a packet to the entity-server. - decompressVolumeData, recomputeMesh, computeShapeInfoWorker, and compressVolumeDataAndSendEditPacket are too expensive + decompressVolumeData, getMesh, computeShapeInfoWorker, and compressVolumeDataAndSendEditPacket are too expensive to run on a thread that has other things to do. These use QtConcurrent::run to spawn a thread. As each thread finishes, it adjusts the dirty flags so that the next call to render() will kick off the next step. @@ -666,8 +663,11 @@ void RenderablePolyVoxEntityItem::setZTextureURL(QString zTextureURL) { } } +void RenderablePolyVoxEntityItem::render(RenderArgs* args) { + PerformanceTimer perfTimer("RenderablePolyVoxEntityItem::render"); + assert(getType() == EntityTypes::PolyVox); + Q_ASSERT(args->_batch); -bool RenderablePolyVoxEntityItem::updateDependents() { bool voxelDataDirty; bool volDataDirty; withWriteLock([&] { @@ -682,20 +682,9 @@ bool RenderablePolyVoxEntityItem::updateDependents() { if (voxelDataDirty) { decompressVolumeData(); } else if (volDataDirty) { - recomputeMesh(); + getMesh(); } - return !volDataDirty; -} - - -void RenderablePolyVoxEntityItem::render(RenderArgs* args) { - PerformanceTimer perfTimer("RenderablePolyVoxEntityItem::render"); - assert(getType() == EntityTypes::PolyVox); - Q_ASSERT(args->_batch); - - updateDependents(); - model::MeshPointer mesh; glm::vec3 voxelVolumeSize; withReadLock([&] { @@ -707,7 +696,7 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) { !mesh->getIndexBuffer()._buffer) { return; } - + if (!_pipeline) { gpu::ShaderPointer vertexShader = gpu::Shader::createVertex(std::string(polyvox_vert)); gpu::ShaderPointer pixelShader = gpu::Shader::createPixel(std::string(polyvox_frag)); @@ -726,13 +715,6 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) { state->setDepthTest(true, true, gpu::LESS_EQUAL); _pipeline = gpu::Pipeline::create(program, state); - - auto wireframeState = std::make_shared(); - wireframeState->setCullMode(gpu::State::CULL_BACK); - wireframeState->setDepthTest(true, true, gpu::LESS_EQUAL); - wireframeState->setFillMode(gpu::State::FILL_LINE); - - _wireframePipeline = gpu::Pipeline::create(program, wireframeState); } if (!_vertexFormat) { @@ -743,11 +725,7 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) { } gpu::Batch& batch = *args->_batch; - - // Pick correct Pipeline - bool wireframe = (render::ShapeKey(args->_globalShapeKey).isWireframe()); - auto pipeline = (wireframe ? _wireframePipeline : _pipeline); - batch.setPipeline(pipeline); + batch.setPipeline(_pipeline); Transform transform(voxelToWorldMatrix()); batch.setModelTransform(transform); @@ -784,7 +762,7 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) { batch.setResourceTexture(2, DependencyManager::get()->getWhiteTexture()); } - int voxelVolumeSizeLocation = pipeline->getProgram()->getUniforms().findLocation("voxelVolumeSize"); + int voxelVolumeSizeLocation = _pipeline->getProgram()->getUniforms().findLocation("voxelVolumeSize"); batch._glUniform3f(voxelVolumeSizeLocation, voxelVolumeSize.x, voxelVolumeSize.y, voxelVolumeSize.z); batch.drawIndexed(gpu::TRIANGLES, (gpu::uint32)mesh->getNumIndices(), 0); @@ -1221,7 +1199,7 @@ void RenderablePolyVoxEntityItem::copyUpperEdgesFromNeighbors() { } } -void RenderablePolyVoxEntityItem::recomputeMesh() { +void RenderablePolyVoxEntityItem::getMesh() { // use _volData to make a renderable mesh PolyVoxSurfaceStyle voxelSurfaceStyle; withReadLock([&] { @@ -1291,20 +1269,12 @@ void RenderablePolyVoxEntityItem::recomputeMesh() { vertexBufferPtr->getSize() , sizeof(PolyVox::PositionMaterialNormal), gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::RAW))); - - std::vector parts; - parts.emplace_back(model::Mesh::Part((model::Index)0, // startIndex - (model::Index)vecIndices.size(), // numIndices - (model::Index)0, // baseVertex - model::Mesh::TRIANGLES)); // topology - mesh->setPartBuffer(gpu::BufferView(new gpu::Buffer(parts.size() * sizeof(model::Mesh::Part), - (gpu::Byte*) parts.data()), gpu::Element::PART_DRAWCALL)); entity->setMesh(mesh); }); } void RenderablePolyVoxEntityItem::setMesh(model::MeshPointer mesh) { - // this catches the payload from recomputeMesh + // this catches the payload from getMesh bool neighborsNeedUpdate; withWriteLock([&] { if (!_collisionless) { @@ -1561,6 +1531,7 @@ std::shared_ptr RenderablePolyVoxEntityItem::getZPN return std::dynamic_pointer_cast(_zPNeighbor.lock()); } + void RenderablePolyVoxEntityItem::bonkNeighbors() { // flag neighbors to the negative of this entity as needing to rebake their meshes. cacheNeighbors(); @@ -1580,6 +1551,7 @@ void RenderablePolyVoxEntityItem::bonkNeighbors() { } } + void RenderablePolyVoxEntityItem::locationChanged(bool tellPhysics) { EntityItem::locationChanged(tellPhysics); if (!_pipeline || !render::Item::isValidID(_myItem)) { @@ -1591,25 +1563,3 @@ void RenderablePolyVoxEntityItem::locationChanged(bool tellPhysics) { scene->enqueuePendingChanges(pendingChanges); } - -bool RenderablePolyVoxEntityItem::getMeshAsScriptValue(QScriptEngine *engine, QScriptValue& result) { - if (!updateDependents()) { - return false; - } - - bool success = false; - MeshProxy* meshProxy = nullptr; - glm::mat4 transform = voxelToLocalMatrix(); - withReadLock([&] { - if (_meshInitialized) { - success = true; - // the mesh will be in voxel-space. transform it into object-space - meshProxy = new MeshProxy( - _mesh->map([=](glm::vec3 position){ return glm::vec3(transform * glm::vec4(position, 1.0f)); }, - [=](glm::vec3 normal){ return glm::vec3(transform * glm::vec4(normal, 0.0f)); }, - [](uint32_t index){ return index; })); - } - }); - result = meshToScriptValue(engine, meshProxy); - return success; -} diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h index cf4672f068..45842c2fb9 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h @@ -61,8 +61,6 @@ public: virtual uint8_t getVoxel(int x, int y, int z) override; virtual bool setVoxel(int x, int y, int z, uint8_t toValue) override; - int getOnCount() const override { return _onCount; } - void render(RenderArgs* args) override; virtual bool supportsDetailedRayIntersection() const override { return true; } virtual bool findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, @@ -135,7 +133,6 @@ public: QByteArray volDataToArray(quint16 voxelXSize, quint16 voxelYSize, quint16 voxelZSize) const; void setMesh(model::MeshPointer mesh); - bool getMeshAsScriptValue(QScriptEngine *engine, QScriptValue& result) override; void setCollisionPoints(ShapeInfo::PointCollection points, AABox box); PolyVox::SimpleVolume* getVolData() { return _volData; } @@ -166,12 +163,11 @@ private: const int MATERIAL_GPU_SLOT = 3; render::ItemID _myItem{ render::Item::INVALID_ITEM_ID }; static gpu::PipelinePointer _pipeline; - static gpu::PipelinePointer _wireframePipeline; ShapeInfo _shapeInfo; PolyVox::SimpleVolume* _volData = nullptr; - bool _volDataDirty = false; // does recomputeMesh need to be called? + bool _volDataDirty = false; // does getMesh need to be called? int _onCount; // how many non-zero voxels are in _volData bool _neighborsNeedUpdate { false }; @@ -182,7 +178,7 @@ private: // these are run off the main thread void decompressVolumeData(); void compressVolumeDataAndSendEditPacket(); - virtual void recomputeMesh() override; // recompute mesh + virtual void getMesh() override; // recompute mesh void computeShapeInfoWorker(); // these are cached lookups of _xNNeighborID, _yNNeighborID, _zNNeighborID, _xPNeighborID, _yPNeighborID, _zPNeighborID @@ -195,7 +191,6 @@ private: void cacheNeighbors(); void copyUpperEdgesFromNeighbors(); void bonkNeighbors(); - bool updateDependents(); }; bool inUserBounds(const PolyVox::SimpleVolume* vol, PolyVoxEntityItem::PolyVoxSurfaceStyle surfaceStyle, diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp index 1ad60bf7c6..c3e097382c 100644 --- a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp @@ -114,22 +114,13 @@ void RenderableShapeEntityItem::render(RenderArgs* args) { auto outColor = _procedural->getColor(color); outColor.a *= _procedural->isFading() ? Interpolate::calculateFadeRatio(_procedural->getFadeStartTime()) : 1.0f; batch._glColor4f(outColor.r, outColor.g, outColor.b, outColor.a); - if (render::ShapeKey(args->_globalShapeKey).isWireframe()) { - DependencyManager::get()->renderWireShape(batch, MAPPING[_shape]); - } else { - DependencyManager::get()->renderShape(batch, MAPPING[_shape]); - } + DependencyManager::get()->renderShape(batch, MAPPING[_shape]); } else { // FIXME, support instanced multi-shape rendering using multidraw indirect color.a *= _isFading ? Interpolate::calculateFadeRatio(_fadeStartTime) : 1.0f; auto geometryCache = DependencyManager::get(); auto pipeline = color.a < 1.0f ? geometryCache->getTransparentShapePipeline() : geometryCache->getOpaqueShapePipeline(); - - if (render::ShapeKey(args->_globalShapeKey).isWireframe()) { - geometryCache->renderWireShapeInstance(batch, MAPPING[_shape], color, pipeline); - } else { - geometryCache->renderSolidShapeInstance(batch, MAPPING[_shape], color, pipeline); - } + geometryCache->renderSolidShapeInstance(batch, MAPPING[_shape], color, pipeline); } static const auto triCount = DependencyManager::get()->getShapeTriangleCount(MAPPING[_shape]); diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp index c4ae0db1aa..d7d7013f59 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp @@ -216,7 +216,7 @@ void RenderableWebEntityItem::render(RenderArgs* args) { if (!_texture) { auto webSurface = _webSurface; - _texture = gpu::TexturePointer(gpu::Texture::createExternal(OffscreenQmlSurface::getDiscardLambda())); + _texture = gpu::TexturePointer(gpu::Texture::createExternal2D(OffscreenQmlSurface::getDiscardLambda())); _texture->setSource(__FUNCTION__); } OffscreenQmlSurface::TextureAndFence newTextureAndFence; diff --git a/libraries/entities/src/EntitiesScriptEngineProvider.h b/libraries/entities/src/EntitiesScriptEngineProvider.h index d87dd105c2..69bf73e688 100644 --- a/libraries/entities/src/EntitiesScriptEngineProvider.h +++ b/libraries/entities/src/EntitiesScriptEngineProvider.h @@ -15,13 +15,11 @@ #define hifi_EntitiesScriptEngineProvider_h #include -#include #include "EntityItemID.h" class EntitiesScriptEngineProvider { public: virtual void callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const QStringList& params = QStringList()) = 0; - virtual QFuture getLocalEntityScriptDetails(const EntityItemID& entityID) = 0; }; -#endif // hifi_EntitiesScriptEngineProvider_h +#endif // hifi_EntitiesScriptEngineProvider_h \ No newline at end of file diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index 0bb085459e..3ef1648fae 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -655,11 +655,13 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef // pack SimulationOwner and terse update properties near each other + // NOTE: the server is authoritative for changes to simOwnerID so we always unpack ownership data // even when we would otherwise ignore the rest of the packet. bool filterRejection = false; if (propertyFlags.getHasProperty(PROP_SIMULATION_OWNER)) { + QByteArray simOwnerData; int bytes = OctreePacketData::unpackDataFromBytes(dataAt, simOwnerData); SimulationOwner newSimOwner; @@ -1877,7 +1879,6 @@ void EntityItem::setSimulationOwner(const SimulationOwner& owner) { } void EntityItem::updateSimulationOwner(const SimulationOwner& owner) { - // NOTE: this method only used by EntityServer. The Interface uses special code in readEntityDataFromBuffer(). if (wantTerseEditLogging() && _simulationOwner != owner) { qCDebug(entities) << "sim ownership for" << getDebugName() << "is now" << owner; } @@ -1893,9 +1894,8 @@ void EntityItem::clearSimulationOwnership() { } _simulationOwner.clear(); - // don't bother setting the DIRTY_SIMULATOR_ID flag because: - // (a) when entity-server calls clearSimulationOwnership() the dirty-flags are meaningless (only used by interface) - // (b) the interface only calls clearSimulationOwnership() in a context that already knows best about dirty flags + // don't bother setting the DIRTY_SIMULATOR_ID flag because clearSimulationOwnership() + // is only ever called on the entity-server and the flags are only used client-side //_dirtyFlags |= Simulation::DIRTY_SIMULATOR_ID; } diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index 1ed020e592..ea81df3801 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -49,6 +49,13 @@ EntityItemProperties::EntityItemProperties(EntityPropertyFlags desiredProperties } +void EntityItemProperties::setSittingPoints(const QVector& sittingPoints) { + _sittingPoints.clear(); + foreach (SittingPoint sitPoint, sittingPoints) { + _sittingPoints.append(sitPoint); + } +} + void EntityItemProperties::calculateNaturalPosition(const glm::vec3& min, const glm::vec3& max) { glm::vec3 halfDimension = (max - min) / 2.0f; _naturalPosition = max - halfDimension; @@ -539,6 +546,20 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_TEXTURES, textures); } + // Sitting properties support + if (!skipDefaults && !strictSemantics) { + QScriptValue sittingPoints = engine->newObject(); + for (int i = 0; i < _sittingPoints.size(); ++i) { + QScriptValue sittingPoint = engine->newObject(); + sittingPoint.setProperty("name", _sittingPoints.at(i).name); + sittingPoint.setProperty("position", vec3toScriptValue(engine, _sittingPoints.at(i).position)); + sittingPoint.setProperty("rotation", quatToScriptValue(engine, _sittingPoints.at(i).rotation)); + sittingPoints.setProperty(i, sittingPoint); + } + sittingPoints.setProperty("length", _sittingPoints.size()); + COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER_ALWAYS(sittingPoints, sittingPoints); // gettable, but not settable + } + if (!skipDefaults && !strictSemantics) { AABox aaBox = getAABox(); QScriptValue boundingBox = engine->newObject(); diff --git a/libraries/entities/src/EntityItemProperties.h b/libraries/entities/src/EntityItemProperties.h index 590298e102..419740e4ea 100644 --- a/libraries/entities/src/EntityItemProperties.h +++ b/libraries/entities/src/EntityItemProperties.h @@ -22,6 +22,7 @@ #include #include +#include // for SittingPoint #include #include #include @@ -254,6 +255,8 @@ public: void clearID() { _id = UNKNOWN_ENTITY_ID; _idSet = false; } void markAllChanged(); + void setSittingPoints(const QVector& sittingPoints); + const glm::vec3& getNaturalDimensions() const { return _naturalDimensions; } void setNaturalDimensions(const glm::vec3& value) { _naturalDimensions = value; } @@ -322,6 +325,7 @@ private: // NOTE: The following are pseudo client only properties. They are only used in clients which can access // properties of model geometry. But these properties are not serialized like other properties. + QVector _sittingPoints; QVariantMap _textureNames; glm::vec3 _naturalDimensions; glm::vec3 _naturalPosition; diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index 1ab5438e53..540eba4511 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -8,15 +8,8 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // - -#include -#include - #include "EntityScriptingInterface.h" -#include -#include - #include "EntityItemID.h" #include #include @@ -296,11 +289,13 @@ EntityItemProperties EntityScriptingInterface::getEntityProperties(QUuid identit results = entity->getProperties(desiredProperties); - // TODO: improve naturalDimensions in the future, - // for now we've added this hack for setting natural dimensions of models + // TODO: improve sitting points and naturalDimensions in the future, + // for now we've included the old sitting points model behavior for entity types that are models + // we've also added this hack for setting natural dimensions of models if (entity->getType() == EntityTypes::Model) { const FBXGeometry* geometry = _entityTree->getGeometryForEntity(entity); if (geometry) { + results.setSittingPoints(geometry->sittingPoints); Extents meshExtents = geometry->getUnscaledMeshExtents(); results.setNaturalDimensions(meshExtents.maximum - meshExtents.minimum); results.calculateNaturalPosition(meshExtents.minimum, meshExtents.maximum); @@ -685,118 +680,6 @@ bool EntityScriptingInterface::reloadServerScripts(QUuid entityID) { return client->reloadServerScript(entityID); } -bool EntityPropertyMetadataRequest::script(EntityItemID entityID, QScriptValue handler) { - using LocalScriptStatusRequest = QFutureWatcher; - - LocalScriptStatusRequest* request = new LocalScriptStatusRequest; - QObject::connect(request, &LocalScriptStatusRequest::finished, _engine, [=]() mutable { - auto details = request->result().toMap(); - QScriptValue err, result; - if (details.contains("isError")) { - if (!details.contains("message")) { - details["message"] = details["errorInfo"]; - } - err = _engine->makeError(_engine->toScriptValue(details)); - } else { - details["success"] = true; - result = _engine->toScriptValue(details); - } - callScopedHandlerObject(handler, err, result); - request->deleteLater(); - }); - auto entityScriptingInterface = DependencyManager::get(); - entityScriptingInterface->withEntitiesScriptEngine([&](EntitiesScriptEngineProvider* entitiesScriptEngine) { - if (entitiesScriptEngine) { - request->setFuture(entitiesScriptEngine->getLocalEntityScriptDetails(entityID)); - } - }); - if (!request->isStarted()) { - request->deleteLater(); - callScopedHandlerObject(handler, _engine->makeError("Entities Scripting Provider unavailable", "InternalError"), QScriptValue()); - return false; - } - return true; -} - -bool EntityPropertyMetadataRequest::serverScripts(EntityItemID entityID, QScriptValue handler) { - auto client = DependencyManager::get(); - auto request = client->createScriptStatusRequest(entityID); - QPointer engine = _engine; - QObject::connect(request, &GetScriptStatusRequest::finished, _engine, [=](GetScriptStatusRequest* request) mutable { - auto engine = _engine; - if (!engine) { - qCDebug(entities) << __FUNCTION__ << " -- engine destroyed while inflight" << entityID; - return; - } - QVariantMap details; - details["success"] = request->getResponseReceived(); - details["isRunning"] = request->getIsRunning(); - details["status"] = EntityScriptStatus_::valueToKey(request->getStatus()).toLower(); - details["errorInfo"] = request->getErrorInfo(); - - QScriptValue err, result; - if (!details["success"].toBool()) { - if (!details.contains("message") && details.contains("errorInfo")) { - details["message"] = details["errorInfo"]; - } - if (details["message"].toString().isEmpty()) { - details["message"] = "entity server script details not found"; - } - err = engine->makeError(engine->toScriptValue(details)); - } else { - result = engine->toScriptValue(details); - } - callScopedHandlerObject(handler, err, result); - request->deleteLater(); - }); - request->start(); - return true; -} - -bool EntityScriptingInterface::queryPropertyMetadata(QUuid entityID, QScriptValue property, QScriptValue scopeOrCallback, QScriptValue methodOrName) { - auto name = property.toString(); - auto handler = makeScopedHandlerObject(scopeOrCallback, methodOrName); - QPointer engine = dynamic_cast(handler.engine()); - if (!engine) { - qCDebug(entities) << "queryPropertyMetadata without detectable engine" << entityID << name; - return false; - } -#ifdef DEBUG_ENGINE_STATE - connect(engine, &QObject::destroyed, this, [=]() { - qDebug() << "queryPropertyMetadata -- engine destroyed!" << (!engine ? "nullptr" : "engine"); - }); -#endif - if (!handler.property("callback").isFunction()) { - qDebug() << "!handler.callback.isFunction" << engine; - engine->raiseException(engine->makeError("callback is not a function", "TypeError")); - return false; - } - - // NOTE: this approach is a work-in-progress and for now just meant to work 100% correctly and provide - // some initial structure for organizing metadata adapters around. - - // The extra layer of indirection is *essential* because in real world conditions errors are often introduced - // by accident and sometimes without exact memory of "what just changed." - - // Here the scripter only needs to know an entityID and a property name -- which means all scripters can - // level this method when stuck in dead-end scenarios or to learn more about "magic" Entity properties - // like .script that work in terms of side-effects. - - // This is an async callback pattern -- so if needed C++ can easily throttle or restrict queries later. - - EntityPropertyMetadataRequest request(engine); - - if (name == "script") { - return request.script(entityID, handler); - } else if (name == "serverScripts") { - return request.serverScripts(entityID, handler); - } else { - engine->raiseException(engine->makeError("metadata for property " + name + " is not yet queryable")); - engine->maybeEmitUncaughtException(__FUNCTION__); - return false; - } -} - bool EntityScriptingInterface::getServerScriptStatus(QUuid entityID, QScriptValue callback) { auto client = DependencyManager::get(); auto request = client->createScriptStatusRequest(entityID); @@ -932,7 +815,8 @@ void RayToEntityIntersectionResultFromScriptValue(const QScriptValue& object, Ra } } -bool EntityScriptingInterface::polyVoxWorker(QUuid entityID, std::function actor) { +bool EntityScriptingInterface::setVoxels(QUuid entityID, + std::function actor) { PROFILE_RANGE(script_entities, __FUNCTION__); if (!_entityTree) { @@ -998,9 +882,11 @@ bool EntityScriptingInterface::setPoints(QUuid entityID, std::function& points) { PROFILE_RANGE(script_entities, __FUNCTION__); @@ -1674,20 +1541,3 @@ bool EntityScriptingInterface::AABoxIntersectsCapsule(const glm::vec3& low, cons AABox aaBox(low, dimensions); return aaBox.findCapsulePenetration(start, end, radius, penetration); } - -glm::mat4 EntityScriptingInterface::getEntityTransform(const QUuid& entityID) { - glm::mat4 result; - if (_entityTree) { - _entityTree->withReadLock([&] { - EntityItemPointer entity = _entityTree->findEntityByEntityItemID(EntityItemID(entityID)); - if (entity) { - glm::mat4 translation = glm::translate(entity->getPosition()); - glm::mat4 rotation = glm::mat4_cast(entity->getRotation()); - glm::mat4 registration = glm::translate(ENTITY_ITEM_DEFAULT_REGISTRATION_POINT - - entity->getRegistrationPoint()); - result = translation * rotation * registration; - } - }); - } - return result; -} diff --git a/libraries/entities/src/EntityScriptingInterface.h b/libraries/entities/src/EntityScriptingInterface.h index 63b5771e60..e9f0637830 100644 --- a/libraries/entities/src/EntityScriptingInterface.h +++ b/libraries/entities/src/EntityScriptingInterface.h @@ -34,23 +34,7 @@ #include "EntitiesScriptEngineProvider.h" #include "EntityItemProperties.h" -#include "BaseScriptEngine.h" - class EntityTree; -class MeshProxy; - -// helper factory to compose standardized, async metadata queries for "magic" Entity properties -// like .script and .serverScripts. This is used for automated testing of core scripting features -// as well as to provide early adopters a self-discoverable, consistent way to diagnose common -// problems with their own Entity scripts. -class EntityPropertyMetadataRequest { -public: - EntityPropertyMetadataRequest(BaseScriptEngine* engine) : _engine(engine) {}; - bool script(EntityItemID entityID, QScriptValue handler); - bool serverScripts(EntityItemID entityID, QScriptValue handler); -private: - QPointer _engine; -}; class RayToEntityIntersectionResult { public: @@ -83,7 +67,6 @@ class EntityScriptingInterface : public OctreeScriptingInterface, public Depende Q_PROPERTY(float costMultiplier READ getCostMultiplier WRITE setCostMultiplier) Q_PROPERTY(QUuid keyboardFocusEntity READ getKeyboardFocusEntity WRITE setKeyboardFocusEntity) - friend EntityPropertyMetadataRequest; public: EntityScriptingInterface(bool bidOnSimulationOwnership); @@ -228,26 +211,6 @@ public slots: Q_INVOKABLE RayToEntityIntersectionResult findRayIntersectionBlocking(const PickRay& ray, bool precisionPicking = false, const QScriptValue& entityIdsToInclude = QScriptValue(), const QScriptValue& entityIdsToDiscard = QScriptValue()); Q_INVOKABLE bool reloadServerScripts(QUuid entityID); - - /**jsdoc - * Query additional metadata for "magic" Entity properties like `script` and `serverScripts`. - * - * @function Entities.queryPropertyMetadata - * @param {EntityID} entityID The ID of the entity. - * @param {string} property The name of the property extended metadata is wanted for. - * @param {ResultCallback} callback Executes callback(err, result) with the query results. - */ - /**jsdoc - * Query additional metadata for "magic" Entity properties like `script` and `serverScripts`. - * - * @function Entities.queryPropertyMetadata - * @param {EntityID} entityID The ID of the entity. - * @param {string} property The name of the property extended metadata is wanted for. - * @param {Object} thisObject The scoping "this" context that callback will be executed within. - * @param {ResultCallback} callbackOrMethodName Executes thisObject[callbackOrMethodName](err, result) with the query results. - */ - Q_INVOKABLE bool queryPropertyMetadata(QUuid entityID, QScriptValue property, QScriptValue scopeOrCallback, QScriptValue methodOrName = QScriptValue()); - Q_INVOKABLE bool getServerScriptStatus(QUuid entityID, QScriptValue callback); Q_INVOKABLE void setLightsArePickable(bool value); @@ -266,7 +229,6 @@ public slots: Q_INVOKABLE bool setAllVoxels(QUuid entityID, int value); Q_INVOKABLE bool setVoxelsInCuboid(QUuid entityID, const glm::vec3& lowPosition, const glm::vec3& cuboidSize, int value); - Q_INVOKABLE void voxelsToMesh(QUuid entityID, QScriptValue callback); Q_INVOKABLE bool setAllPoints(QUuid entityID, const QVector& points); Q_INVOKABLE bool appendPoint(QUuid entityID, const glm::vec3& point); @@ -331,15 +293,6 @@ public slots: const glm::vec3& start, const glm::vec3& end, float radius); - /**jsdoc - * Returns object to world transform, excluding scale - * - * @function Entities.getEntityTransform - * @param {EntityID} entityID The ID of the entity whose transform is to be returned - * @return {Mat4} Entity's object to world transform, excluding scale - */ - Q_INVOKABLE glm::mat4 getEntityTransform(const QUuid& entityID); - signals: void collisionWithEntity(const EntityItemID& idA, const EntityItemID& idB, const Collision& collision); @@ -370,14 +323,9 @@ signals: void webEventReceived(const EntityItemID& entityItemID, const QVariant& message); -protected: - void withEntitiesScriptEngine(std::function function) { - std::lock_guard lock(_entitiesScriptEngineLock); - function(_entitiesScriptEngine); - }; private: bool actionWorker(const QUuid& entityID, std::function actor); - bool polyVoxWorker(QUuid entityID, std::function actor); + bool setVoxels(QUuid entityID, std::function actor); bool setPoints(QUuid entityID, std::function actor); void queueEntityMessage(PacketType packetType, EntityItemID entityID, const EntityItemProperties& properties); diff --git a/libraries/entities/src/PolyVoxEntityItem.cpp b/libraries/entities/src/PolyVoxEntityItem.cpp index 90344d6c4b..2a374c1d17 100644 --- a/libraries/entities/src/PolyVoxEntityItem.cpp +++ b/libraries/entities/src/PolyVoxEntityItem.cpp @@ -242,7 +242,3 @@ const QByteArray PolyVoxEntityItem::getVoxelData() const { }); return voxelDataCopy; } - -bool PolyVoxEntityItem::getMeshAsScriptValue(QScriptEngine *engine, QScriptValue& result) { - return false; -} diff --git a/libraries/entities/src/PolyVoxEntityItem.h b/libraries/entities/src/PolyVoxEntityItem.h index 311a002a4a..910d8eff88 100644 --- a/libraries/entities/src/PolyVoxEntityItem.h +++ b/libraries/entities/src/PolyVoxEntityItem.h @@ -57,8 +57,6 @@ class PolyVoxEntityItem : public EntityItem { virtual void setVoxelData(QByteArray voxelData); virtual const QByteArray getVoxelData() const; - virtual int getOnCount() const { return 0; } - enum PolyVoxSurfaceStyle { SURFACE_MARCHING_CUBES, SURFACE_CUBIC, @@ -133,9 +131,7 @@ class PolyVoxEntityItem : public EntityItem { virtual void rebakeMesh() {}; void setVoxelDataDirty(bool value) { withWriteLock([&] { _voxelDataDirty = value; }); } - virtual void recomputeMesh() {}; - - virtual bool getMeshAsScriptValue(QScriptEngine *engine, QScriptValue& result); + virtual void getMesh() {}; // recompute mesh protected: glm::vec3 _voxelVolumeSize; // this is always 3 bytes diff --git a/libraries/entities/src/PropertyGroup.h b/libraries/entities/src/PropertyGroup.h index f45d19f5eb..38b1e5f599 100644 --- a/libraries/entities/src/PropertyGroup.h +++ b/libraries/entities/src/PropertyGroup.h @@ -14,11 +14,9 @@ #include -#include - +//#include "EntityItemProperties.h" #include "EntityPropertyFlags.h" - class EntityItemProperties; class EncodeBitstreamParams; class OctreePacketData; @@ -26,6 +24,31 @@ class EntityTreeElementExtraEncodeData; class ReadBitstreamToTreeParams; using EntityTreeElementExtraEncodeDataPointer = std::shared_ptr; +#include + +/* +#include + +#include +#include + +#include +#include +#include + +#include +#include // for SittingPoint +#include +#include +#include + +#include "EntityItemID.h" +#include "PropertyGroupMacros.h" +#include "EntityTypes.h" +*/ + +//typedef PropertyFlags EntityPropertyFlags; + class PropertyGroup { public: diff --git a/libraries/fbx/src/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp index 718793fefa..fcaef90527 100644 --- a/libraries/fbx/src/FBXReader.cpp +++ b/libraries/fbx/src/FBXReader.cpp @@ -1468,9 +1468,6 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS // Create the Material Library consolidateFBXMaterials(mapping); - // We can't allow the scaling of a given image to different sizes, because the hash used for the KTX cache is based on the original image - // Allowing scaling of the same image to different sizes would cause different KTX files to target the same cache key -#if 0 // HACK: until we get proper LOD management we're going to cap model textures // according to how many unique textures the model uses: // 1 - 8 textures --> 2048 @@ -1484,7 +1481,6 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS int numTextures = uniqueTextures.size(); const int MAX_NUM_TEXTURES_AT_MAX_RESOLUTION = 8; int maxWidth = sqrt(MAX_NUM_PIXELS_FOR_FBX_TEXTURE); - if (numTextures > MAX_NUM_TEXTURES_AT_MAX_RESOLUTION) { int numTextureThreshold = MAX_NUM_TEXTURES_AT_MAX_RESOLUTION; const int MIN_MIP_TEXTURE_WIDTH = 64; @@ -1498,7 +1494,7 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS material.setMaxNumPixelsPerTexture(maxWidth * maxWidth); } } -#endif + geometry.materials = _fbxMaterials; // see if any materials have texture children @@ -1799,6 +1795,19 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS } geometry.palmDirection = parseVec3(mapping.value("palmDirection", "0, -1, 0").toString()); + // Add sitting points + QVariantHash sittingPoints = mapping.value("sit").toHash(); + for (QVariantHash::const_iterator it = sittingPoints.constBegin(); it != sittingPoints.constEnd(); it++) { + SittingPoint sittingPoint; + sittingPoint.name = it.key(); + + QVariantList properties = it->toList(); + sittingPoint.position = parseVec3(properties.at(0).toString()); + sittingPoint.rotation = glm::quat(glm::radians(parseVec3(properties.at(1).toString()))); + + geometry.sittingPoints.append(sittingPoint); + } + // attempt to map any meshes to a named model for (QHash::const_iterator m = meshIDsToMeshIndices.constBegin(); m != meshIDsToMeshIndices.constEnd(); m++) { diff --git a/libraries/fbx/src/FBXReader.h b/libraries/fbx/src/FBXReader.h index fa047e512f..6e51c413dc 100644 --- a/libraries/fbx/src/FBXReader.h +++ b/libraries/fbx/src/FBXReader.h @@ -265,6 +265,24 @@ public: Q_DECLARE_METATYPE(FBXAnimationFrame) Q_DECLARE_METATYPE(QVector) +/// A point where an avatar can sit +class SittingPoint { +public: + QString name; + glm::vec3 position; // relative postion + glm::quat rotation; // relative orientation +}; + +inline bool operator==(const SittingPoint& lhs, const SittingPoint& rhs) +{ + return (lhs.name == rhs.name) && (lhs.position == rhs.position) && (lhs.rotation == rhs.rotation); +} + +inline bool operator!=(const SittingPoint& lhs, const SittingPoint& rhs) +{ + return (lhs.name != rhs.name) || (lhs.position != rhs.position) || (lhs.rotation != rhs.rotation); +} + /// A set of meshes extracted from an FBX document. class FBXGeometry { public: @@ -302,6 +320,8 @@ public: glm::vec3 palmDirection; + QVector sittingPoints; + glm::vec3 neckPivot; Extents bindExtents; diff --git a/libraries/fbx/src/FBXReader_Node.cpp b/libraries/fbx/src/FBXReader_Node.cpp index d987f885eb..d814f58dab 100644 --- a/libraries/fbx/src/FBXReader_Node.cpp +++ b/libraries/fbx/src/FBXReader_Node.cpp @@ -54,8 +54,7 @@ template QVariant readBinaryArray(QDataStream& in, int& position) { in.readRawData(compressed.data() + sizeof(quint32), compressedLength); position += compressedLength; arrayData = qUncompress(compressed); - if (arrayData.isEmpty() || - (unsigned int)arrayData.size() != (sizeof(T) * arrayLength)) { // answers empty byte array if corrupt + if (arrayData.isEmpty() || arrayData.size() != (sizeof(T) * arrayLength)) { // answers empty byte array if corrupt throw QString("corrupt fbx file"); } } else { diff --git a/libraries/fbx/src/OBJReader.cpp b/libraries/fbx/src/OBJReader.cpp index c1bb72dff8..73cf7a520e 100644 --- a/libraries/fbx/src/OBJReader.cpp +++ b/libraries/fbx/src/OBJReader.cpp @@ -267,7 +267,7 @@ void OBJReader::parseMaterialLibrary(QIODevice* device) { } if (token == "map_Kd") { currentMaterial.diffuseTextureFilename = filename; - } else if( token == "map_Ks" ) { + } else { currentMaterial.specularTextureFilename = filename; } } @@ -546,7 +546,6 @@ FBXGeometry* OBJReader::readOBJ(QByteArray& model, const QVariantHash& mapping, QString queryPart = _url.query(); bool suppressMaterialsHack = queryPart.contains("hifiusemat"); // If this appears in query string, don't fetch mtl even if used. OBJMaterial& preDefinedMaterial = materials[SMART_DEFAULT_MATERIAL_NAME]; - preDefinedMaterial.used = true; if (suppressMaterialsHack) { needsMaterialLibrary = preDefinedMaterial.userSpecifiesUV = false; // I said it was a hack... } @@ -595,8 +594,8 @@ FBXGeometry* OBJReader::readOBJ(QByteArray& model, const QVariantHash& mapping, } foreach (QString materialID, materials.keys()) { - OBJMaterial& objMaterial = materials[materialID]; - if (!objMaterial.used) { + OBJMaterial& objMaterial = materials[materialID]; + if (!objMaterial.used) { continue; } geometry.materials[materialID] = FBXMaterial(objMaterial.diffuseColor, @@ -612,9 +611,6 @@ FBXGeometry* OBJReader::readOBJ(QByteArray& model, const QVariantHash& mapping, if (!objMaterial.diffuseTextureFilename.isEmpty()) { fbxMaterial.albedoTexture.filename = objMaterial.diffuseTextureFilename; } - if (!objMaterial.specularTextureFilename.isEmpty()) { - fbxMaterial.specularTexture.filename = objMaterial.specularTextureFilename; - } modelMaterial->setEmissive(fbxMaterial.emissiveColor); modelMaterial->setAlbedo(fbxMaterial.diffuseColor); diff --git a/libraries/fbx/src/OBJReader.h b/libraries/fbx/src/OBJReader.h index b4a48c570e..200f11548d 100644 --- a/libraries/fbx/src/OBJReader.h +++ b/libraries/fbx/src/OBJReader.h @@ -58,7 +58,7 @@ public: QByteArray specularTextureFilename; bool used { false }; bool userSpecifiesUV { false }; - OBJMaterial() : shininess(0.0f), opacity(1.0f), diffuseColor(0.9f), specularColor(0.9f) {} + OBJMaterial() : shininess(96.0f), opacity(1.0f), diffuseColor(1.0f), specularColor(1.0f) {} }; class OBJReader: public QObject { // QObject so we can make network requests. diff --git a/libraries/fbx/src/OBJWriter.cpp b/libraries/fbx/src/OBJWriter.cpp deleted file mode 100644 index 5ee04c5718..0000000000 --- a/libraries/fbx/src/OBJWriter.cpp +++ /dev/null @@ -1,148 +0,0 @@ -// -// OBJWriter.cpp -// libraries/fbx/src/ -// -// Created by Seth Alves on 2017-1-27. -// Copyright 2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include -#include -#include "model/Geometry.h" -#include "OBJWriter.h" -#include "ModelFormatLogging.h" - -static QString formatFloat(double n) { - // limit precision to 6, but don't output trailing zeros. - QString s = QString::number(n, 'f', 6); - while (s.endsWith("0")) { - s.remove(s.size() - 1, 1); - } - if (s.endsWith(".")) { - s.remove(s.size() - 1, 1); - } - - // check for non-numbers. if we get NaN or inf or scientific notation, just return 0 - for (int i = 0; i < s.length(); i++) { - auto c = s.at(i).toLatin1(); - if (c != '-' && - c != '.' && - (c < '0' || c > '9')) { - qCDebug(modelformat) << "OBJWriter zeroing bad vertex coordinate:" << s << "because of" << c; - return QString("0"); - } - } - - return s; -} - -bool writeOBJToTextStream(QTextStream& out, QList meshes) { - // each mesh's vertices are numbered from zero. We're combining all their vertices into one list here, - // so keep track of the start index for each mesh. - QList meshVertexStartOffset; - int currentVertexStartOffset = 0; - - // write out all vertices - foreach (const MeshPointer& mesh, meshes) { - meshVertexStartOffset.append(currentVertexStartOffset); - const gpu::BufferView& vertexBuffer = mesh->getVertexBuffer(); - int vertexCount = 0; - gpu::BufferView::Iterator vertexItr = vertexBuffer.cbegin(); - while (vertexItr != vertexBuffer.cend()) { - glm::vec3 v = *vertexItr; - out << "v "; - out << formatFloat(v[0]) << " "; - out << formatFloat(v[1]) << " "; - out << formatFloat(v[2]) << "\n"; - vertexItr++; - vertexCount++; - } - currentVertexStartOffset += vertexCount; - } - out << "\n"; - - // write out faces - int nth = 0; - foreach (const MeshPointer& mesh, meshes) { - currentVertexStartOffset = meshVertexStartOffset.takeFirst(); - - const gpu::BufferView& partBuffer = mesh->getPartBuffer(); - const gpu::BufferView& indexBuffer = mesh->getIndexBuffer(); - - model::Index partCount = (model::Index)mesh->getNumParts(); - for (int partIndex = 0; partIndex < partCount; partIndex++) { - const model::Mesh::Part& part = partBuffer.get(partIndex); - - out << "g part-" << nth++ << "\n"; - - // model::Mesh::TRIANGLES - // TODO -- handle other formats - gpu::BufferView::Iterator indexItr = indexBuffer.cbegin(); - indexItr += part._startIndex; - - int indexCount = 0; - while (indexItr != indexBuffer.cend() && indexCount < part._numIndices) { - uint32_t index0 = *indexItr; - indexItr++; - indexCount++; - if (indexItr == indexBuffer.cend() || indexCount >= part._numIndices) { - qCDebug(modelformat) << "OBJWriter -- index buffer length isn't multiple of 3"; - break; - } - uint32_t index1 = *indexItr; - indexItr++; - indexCount++; - if (indexItr == indexBuffer.cend() || indexCount >= part._numIndices) { - qCDebug(modelformat) << "OBJWriter -- index buffer length isn't multiple of 3"; - break; - } - uint32_t index2 = *indexItr; - indexItr++; - indexCount++; - - out << "f "; - out << currentVertexStartOffset + index0 + 1 << " "; - out << currentVertexStartOffset + index1 + 1 << " "; - out << currentVertexStartOffset + index2 + 1 << "\n"; - } - out << "\n"; - } - } - - return true; -} - -bool writeOBJToFile(QString path, QList meshes) { - if (QFileInfo(path).exists() && !QFile::remove(path)) { - qCDebug(modelformat) << "OBJ writer failed, file exists:" << path; - return false; - } - - QFile file(path); - if (!file.open(QIODevice::WriteOnly)) { - qCDebug(modelformat) << "OBJ writer failed to open output file:" << path; - return false; - } - - QTextStream outStream(&file); - - bool success; - success = writeOBJToTextStream(outStream, meshes); - - file.close(); - return success; -} - -QString writeOBJToString(QList meshes) { - QString result; - QTextStream outStream(&result, QIODevice::ReadWrite); - bool success; - success = writeOBJToTextStream(outStream, meshes); - if (success) { - return result; - } - return QString(""); -} diff --git a/libraries/fbx/src/OBJWriter.h b/libraries/fbx/src/OBJWriter.h deleted file mode 100644 index b6e20e1ae6..0000000000 --- a/libraries/fbx/src/OBJWriter.h +++ /dev/null @@ -1,26 +0,0 @@ -// -// OBJWriter.h -// libraries/fbx/src/ -// -// Created by Seth Alves on 2017-1-27. -// Copyright 2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#ifndef hifi_objwriter_h -#define hifi_objwriter_h - - -#include -#include -#include - -using MeshPointer = std::shared_ptr; - -bool writeOBJToTextStream(QTextStream& out, QList meshes); -bool writeOBJToFile(QString path, QList meshes); -QString writeOBJToString(QList meshes); - -#endif // hifi_objwriter_h diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp index 0800c27839..c51f468908 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp @@ -62,6 +62,8 @@ BackendPointer GLBackend::createBackend() { INSTANCE = result.get(); void* voidInstance = &(*result); qApp->setProperty(hifi::properties::gl::BACKEND, QVariant::fromValue(voidInstance)); + + gl::GLTexture::initTextureTransferHelper(); return result; } @@ -207,7 +209,7 @@ void GLBackend::renderPassTransfer(const Batch& batch) { } } - { // Sync all the transform states + { // Sync all the buffers PROFILE_RANGE(render_gpu_gl_detail, "syncCPUTransform"); _transform._cameras.clear(); _transform._cameraOffsets.clear(); @@ -275,7 +277,7 @@ void GLBackend::renderPassDraw(const Batch& batch) { updateInput(); updateTransform(batch); updatePipeline(); - + CommandCall call = _commandCalls[(*command)]; (this->*(call))(batch, *offset); break; @@ -621,7 +623,6 @@ void GLBackend::queueLambda(const std::function lambda) const { } void GLBackend::recycle() const { - PROFILE_RANGE(render_gpu_gl, __FUNCTION__) { std::list> lamdbasTrash; { @@ -744,6 +745,10 @@ void GLBackend::recycle() const { glDeleteQueries((GLsizei)ids.size(), ids.data()); } } + +#ifndef THREADED_TEXTURE_TRANSFER + gl::GLTexture::_textureTransferHelper->process(); +#endif } void GLBackend::setCameraCorrection(const Mat4& correction) { diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackend.h b/libraries/gpu-gl/src/gpu/gl/GLBackend.h index 76c950ec2b..950ac65a3f 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackend.h +++ b/libraries/gpu-gl/src/gpu/gl/GLBackend.h @@ -187,15 +187,10 @@ public: virtual void do_setStateScissorRect(const Batch& batch, size_t paramOffset) final; virtual GLuint getFramebufferID(const FramebufferPointer& framebuffer) = 0; - virtual GLuint getTextureID(const TexturePointer& texture) final; + virtual GLuint getTextureID(const TexturePointer& texture, bool needTransfer = true) = 0; virtual GLuint getBufferID(const Buffer& buffer) = 0; virtual GLuint getQueryID(const QueryPointer& query) = 0; - - virtual GLFramebuffer* syncGPUObject(const Framebuffer& framebuffer) = 0; - virtual GLBuffer* syncGPUObject(const Buffer& buffer) = 0; - virtual GLTexture* syncGPUObject(const TexturePointer& texture); - virtual GLQuery* syncGPUObject(const Query& query) = 0; - //virtual bool isTextureReady(const TexturePointer& texture); + virtual bool isTextureReady(const TexturePointer& texture); virtual void releaseBuffer(GLuint id, Size size) const; virtual void releaseExternalTexture(GLuint id, const Texture::ExternalRecycler& recycler) const; @@ -211,6 +206,10 @@ public: protected: void recycle() const override; + virtual GLFramebuffer* syncGPUObject(const Framebuffer& framebuffer) = 0; + virtual GLBuffer* syncGPUObject(const Buffer& buffer) = 0; + virtual GLTexture* syncGPUObject(const TexturePointer& texture, bool sync = true) = 0; + virtual GLQuery* syncGPUObject(const Query& query) = 0; static const size_t INVALID_OFFSET = (size_t)-1; bool _inRenderTransferPass { false }; diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackendTexture.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackendTexture.cpp index ca4e328612..f51eac0e33 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackendTexture.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackendTexture.cpp @@ -14,56 +14,12 @@ using namespace gpu; using namespace gpu::gl; - -GLuint GLBackend::getTextureID(const TexturePointer& texture) { - GLTexture* object = syncGPUObject(texture); - - if (!object) { - return 0; - } - - return object->_id; +bool GLBackend::isTextureReady(const TexturePointer& texture) { + // DO not transfer the texture, this call is expected for rendering texture + GLTexture* object = syncGPUObject(texture, true); + return object && object->isReady(); } -GLTexture* GLBackend::syncGPUObject(const TexturePointer& texturePointer) { - const Texture& texture = *texturePointer; - // Special case external textures - if (TextureUsageType::EXTERNAL == texture.getUsageType()) { - Texture::ExternalUpdates updates = texture.getUpdates(); - if (!updates.empty()) { - Texture::ExternalRecycler recycler = texture.getExternalRecycler(); - Q_ASSERT(recycler); - // Discard any superfluous updates - while (updates.size() > 1) { - const auto& update = updates.front(); - // Superfluous updates will never have been read, but we want to ensure the previous - // writes to them are complete before they're written again, so return them with the - // same fences they arrived with. This can happen on any thread because no GL context - // work is involved - recycler(update.first, update.second); - updates.pop_front(); - } - - // The last texture remaining is the one we'll use to create the GLTexture - const auto& update = updates.front(); - // Check for a fence, and if it exists, inject a wait into the command stream, then destroy the fence - if (update.second) { - GLsync fence = static_cast(update.second); - glWaitSync(fence, 0, GL_TIMEOUT_IGNORED); - glDeleteSync(fence); - } - - // Create the new texture object (replaces any previous texture object) - new GLExternalTexture(shared_from_this(), texture, update.first); - } - - // Return the texture object (if any) associated with the texture, without extensive logic - // (external textures are - return Backend::getGPUObject(texture); - } - - return nullptr; -} void GLBackend::do_generateTextureMips(const Batch& batch, size_t paramOffset) { TexturePointer resourceTexture = batch._textures.get(batch._params[paramOffset + 0]._uint); @@ -72,7 +28,7 @@ void GLBackend::do_generateTextureMips(const Batch& batch, size_t paramOffset) { } // DO not transfer the texture, this call is expected for rendering texture - GLTexture* object = syncGPUObject(resourceTexture); + GLTexture* object = syncGPUObject(resourceTexture, false); if (!object) { return; } diff --git a/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.cpp b/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.cpp index 2ac7e9d060..85cf069062 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.cpp @@ -21,12 +21,13 @@ GLFramebuffer::~GLFramebuffer() { } } -bool GLFramebuffer::checkStatus() const { +bool GLFramebuffer::checkStatus(GLenum target) const { + bool result = false; switch (_status) { case GL_FRAMEBUFFER_COMPLETE: // Success ! - return true; - + result = true; + break; case GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT: qCWarning(gpugllogging) << "GLFramebuffer::syncGPUObject : Framebuffer not valid, GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT."; break; @@ -43,5 +44,5 @@ bool GLFramebuffer::checkStatus() const { qCWarning(gpugllogging) << "GLFramebuffer::syncGPUObject : Framebuffer not valid, GL_FRAMEBUFFER_UNSUPPORTED."; break; } - return false; + return result; } diff --git a/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.h b/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.h index c0633cfdef..9b4f9703fc 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.h +++ b/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.h @@ -64,7 +64,7 @@ public: protected: GLenum _status { GL_FRAMEBUFFER_COMPLETE }; virtual void update() = 0; - bool checkStatus() const; + bool checkStatus(GLenum target) const; GLFramebuffer(const std::weak_ptr& backend, const Framebuffer& framebuffer, GLuint id) : GLObject(backend, framebuffer, id) {} ~GLFramebuffer(); diff --git a/libraries/gpu-gl/src/gpu/gl/GLTexelFormat.cpp b/libraries/gpu-gl/src/gpu/gl/GLTexelFormat.cpp index 7e26e65e02..bd945cbaaa 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLTexelFormat.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLTexelFormat.cpp @@ -17,7 +17,6 @@ GLenum GLTexelFormat::evalGLTexelFormatInternal(const gpu::Element& dstFormat) { switch (dstFormat.getDimension()) { case gpu::SCALAR: { switch (dstFormat.getSemantic()) { - case gpu::RED: case gpu::RGB: case gpu::RGBA: case gpu::SRGB: @@ -263,7 +262,6 @@ GLTexelFormat GLTexelFormat::evalGLTexelFormat(const Element& dstFormat, const E texel.type = ELEMENT_TYPE_TO_GL[dstFormat.getType()]; switch (dstFormat.getSemantic()) { - case gpu::RED: case gpu::RGB: case gpu::RGBA: texel.internalFormat = GL_R8; @@ -274,10 +272,8 @@ GLTexelFormat GLTexelFormat::evalGLTexelFormat(const Element& dstFormat, const E break; case gpu::DEPTH: - texel.format = GL_DEPTH_COMPONENT; texel.internalFormat = GL_DEPTH_COMPONENT32; break; - case gpu::DEPTH_STENCIL: texel.type = GL_UNSIGNED_INT_24_8; texel.format = GL_DEPTH_STENCIL; @@ -407,7 +403,6 @@ GLTexelFormat GLTexelFormat::evalGLTexelFormat(const Element& dstFormat, const E texel.internalFormat = GL_COMPRESSED_RED_RGTC1; break; } - case gpu::RED: case gpu::RGB: case gpu::RGBA: case gpu::SRGB: diff --git a/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp b/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp index 1de820e1df..1e0dd08ae1 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp @@ -10,13 +10,15 @@ #include +#include "GLTextureTransfer.h" #include "GLBackend.h" using namespace gpu; using namespace gpu::gl; +std::shared_ptr GLTexture::_textureTransferHelper; -const GLenum GLTexture::CUBE_FACE_LAYOUT[GLTexture::TEXTURE_CUBE_NUM_FACES] = { +const GLenum GLTexture::CUBE_FACE_LAYOUT[6] = { GL_TEXTURE_CUBE_MAP_POSITIVE_X, GL_TEXTURE_CUBE_MAP_NEGATIVE_X, GL_TEXTURE_CUBE_MAP_POSITIVE_Y, GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, GL_TEXTURE_CUBE_MAP_POSITIVE_Z, GL_TEXTURE_CUBE_MAP_NEGATIVE_Z @@ -65,17 +67,6 @@ GLenum GLTexture::getGLTextureType(const Texture& texture) { } -uint8_t GLTexture::getFaceCount(GLenum target) { - switch (target) { - case GL_TEXTURE_2D: - return TEXTURE_2D_NUM_FACES; - case GL_TEXTURE_CUBE_MAP: - return TEXTURE_CUBE_NUM_FACES; - default: - Q_UNREACHABLE(); - break; - } -} const std::vector& GLTexture::getFaceTargets(GLenum target) { static std::vector cubeFaceTargets { GL_TEXTURE_CUBE_MAP_POSITIVE_X, GL_TEXTURE_CUBE_MAP_NEGATIVE_X, @@ -98,34 +89,216 @@ const std::vector& GLTexture::getFaceTargets(GLenum target) { return faceTargets; } +// Default texture memory = GPU total memory - 2GB +#define GPU_MEMORY_RESERVE_BYTES MB_TO_BYTES(2048) +// Minimum texture memory = 1GB +#define TEXTURE_MEMORY_MIN_BYTES MB_TO_BYTES(1024) + + +float GLTexture::getMemoryPressure() { + // Check for an explicit memory limit + auto availableTextureMemory = Texture::getAllowedGPUMemoryUsage(); + + + // If no memory limit has been set, use a percentage of the total dedicated memory + if (!availableTextureMemory) { +#if 0 + auto totalMemory = getDedicatedMemory(); + if ((GPU_MEMORY_RESERVE_BYTES + TEXTURE_MEMORY_MIN_BYTES) > totalMemory) { + availableTextureMemory = TEXTURE_MEMORY_MIN_BYTES; + } else { + availableTextureMemory = totalMemory - GPU_MEMORY_RESERVE_BYTES; + } +#else + // Hardcode texture limit for sparse textures at 1 GB for now + availableTextureMemory = TEXTURE_MEMORY_MIN_BYTES; +#endif + } + + // Return the consumed texture memory divided by the available texture memory. + auto consumedGpuMemory = Context::getTextureGPUMemoryUsage() - Context::getTextureGPUFramebufferMemoryUsage(); + float memoryPressure = (float)consumedGpuMemory / (float)availableTextureMemory; + static Context::Size lastConsumedGpuMemory = 0; + if (memoryPressure > 1.0f && lastConsumedGpuMemory != consumedGpuMemory) { + lastConsumedGpuMemory = consumedGpuMemory; + qCDebug(gpugllogging) << "Exceeded max allowed texture memory: " << consumedGpuMemory << " / " << availableTextureMemory; + } + return memoryPressure; +} + + +// Create the texture and allocate storage +GLTexture::GLTexture(const std::weak_ptr& backend, const Texture& texture, GLuint id, bool transferrable) : + GLObject(backend, texture, id), + _external(false), + _source(texture.source()), + _storageStamp(texture.getStamp()), + _target(getGLTextureType(texture)), + _internalFormat(gl::GLTexelFormat::evalGLTexelFormatInternal(texture.getTexelFormat())), + _maxMip(texture.maxMip()), + _minMip(texture.minMip()), + _virtualSize(texture.evalTotalSize()), + _transferrable(transferrable) +{ + auto strongBackend = _backend.lock(); + strongBackend->recycle(); + Backend::incrementTextureGPUCount(); + Backend::updateTextureGPUVirtualMemoryUsage(0, _virtualSize); + Backend::setGPUObject(texture, this); +} + GLTexture::GLTexture(const std::weak_ptr& backend, const Texture& texture, GLuint id) : GLObject(backend, texture, id), + _external(true), _source(texture.source()), - _target(getGLTextureType(texture)) + _storageStamp(0), + _target(getGLTextureType(texture)), + _internalFormat(GL_RGBA8), + // FIXME force mips to 0? + _maxMip(texture.maxMip()), + _minMip(texture.minMip()), + _virtualSize(0), + _transferrable(false) { Backend::setGPUObject(texture, this); + + // FIXME Is this necessary? + //withPreservedTexture([this] { + // syncSampler(); + // if (_gpuObject.isAutogenerateMips()) { + // generateMips(); + // } + //}); } GLTexture::~GLTexture() { - auto backend = _backend.lock(); - if (backend && _id) { - backend->releaseTexture(_id, 0); - } -} - - -GLExternalTexture::GLExternalTexture(const std::weak_ptr& backend, const Texture& texture, GLuint id) - : Parent(backend, texture, id) { } - -GLExternalTexture::~GLExternalTexture() { auto backend = _backend.lock(); if (backend) { - auto recycler = _gpuObject.getExternalRecycler(); - if (recycler) { - backend->releaseExternalTexture(_id, recycler); - } else { - qWarning() << "No recycler available for texture " << _id << " possible leak"; + if (_external) { + auto recycler = _gpuObject.getExternalRecycler(); + if (recycler) { + backend->releaseExternalTexture(_id, recycler); + } else { + qWarning() << "No recycler available for texture " << _id << " possible leak"; + } + } else if (_id) { + // WARNING! Sparse textures do not use this code path. See GL45BackendTexture for + // the GL45Texture destructor for doing any required work tracking GPU stats + backend->releaseTexture(_id, _size); } - const_cast(_id) = 0; + + if (!_external && !_transferrable) { + Backend::updateTextureGPUFramebufferMemoryUsage(_size, 0); + } + } + Backend::updateTextureGPUVirtualMemoryUsage(_virtualSize, 0); +} + +void GLTexture::createTexture() { + withPreservedTexture([&] { + allocateStorage(); + (void)CHECK_GL_ERROR(); + syncSampler(); + (void)CHECK_GL_ERROR(); + }); +} + +void GLTexture::withPreservedTexture(std::function f) const { + GLint boundTex = -1; + switch (_target) { + case GL_TEXTURE_2D: + glGetIntegerv(GL_TEXTURE_BINDING_2D, &boundTex); + break; + + case GL_TEXTURE_CUBE_MAP: + glGetIntegerv(GL_TEXTURE_BINDING_CUBE_MAP, &boundTex); + break; + + default: + qFatal("Unsupported texture type"); + } + (void)CHECK_GL_ERROR(); + + glBindTexture(_target, _texture); + f(); + glBindTexture(_target, boundTex); + (void)CHECK_GL_ERROR(); +} + +void GLTexture::setSize(GLuint size) const { + if (!_external && !_transferrable) { + Backend::updateTextureGPUFramebufferMemoryUsage(_size, size); + } + Backend::updateTextureGPUMemoryUsage(_size, size); + const_cast(_size) = size; +} + +bool GLTexture::isInvalid() const { + return _storageStamp < _gpuObject.getStamp(); +} + +bool GLTexture::isOutdated() const { + return GLSyncState::Idle == _syncState && _contentStamp < _gpuObject.getDataStamp(); +} + +bool GLTexture::isReady() const { + // If we have an invalid texture, we're never ready + if (isInvalid()) { + return false; + } + + auto syncState = _syncState.load(); + if (isOutdated() || Idle != syncState) { + return false; + } + + return true; +} + + +// Do any post-transfer operations that might be required on the main context / rendering thread +void GLTexture::postTransfer() { + setSyncState(GLSyncState::Idle); + ++_transferCount; + + // At this point the mip pixels have been loaded, we can notify the gpu texture to abandon it's memory + switch (_gpuObject.getType()) { + case Texture::TEX_2D: + for (uint16_t i = 0; i < Sampler::MAX_MIP_LEVEL; ++i) { + if (_gpuObject.isStoredMipFaceAvailable(i)) { + _gpuObject.notifyMipFaceGPULoaded(i); + } + } + break; + + case Texture::TEX_CUBE: + // transfer pixels from each faces + for (uint8_t f = 0; f < CUBE_NUM_FACES; f++) { + for (uint16_t i = 0; i < Sampler::MAX_MIP_LEVEL; ++i) { + if (_gpuObject.isStoredMipFaceAvailable(i, f)) { + _gpuObject.notifyMipFaceGPULoaded(i, f); + } + } + } + break; + + default: + qCWarning(gpugllogging) << __FUNCTION__ << " case for Texture Type " << _gpuObject.getType() << " not supported"; + break; } } + +void GLTexture::initTextureTransferHelper() { + _textureTransferHelper = std::make_shared(); +} + +void GLTexture::startTransfer() { + createTexture(); +} + +void GLTexture::finishTransfer() { + if (_gpuObject.isAutogenerateMips()) { + generateMips(); + } +} + diff --git a/libraries/gpu-gl/src/gpu/gl/GLTexture.h b/libraries/gpu-gl/src/gpu/gl/GLTexture.h index 1f91e17157..0f75a6fe51 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLTexture.h +++ b/libraries/gpu-gl/src/gpu/gl/GLTexture.h @@ -9,6 +9,7 @@ #define hifi_gpu_gl_GLTexture_h #include "GLShared.h" +#include "GLTextureTransfer.h" #include "GLBackend.h" #include "GLTexelFormat.h" @@ -19,47 +20,209 @@ struct GLFilterMode { GLint magFilter; }; + class GLTexture : public GLObject { - using Parent = GLObject; - friend class GLBackend; public: static const uint16_t INVALID_MIP { (uint16_t)-1 }; static const uint8_t INVALID_FACE { (uint8_t)-1 }; + static void initTextureTransferHelper(); + static std::shared_ptr _textureTransferHelper; + + template + static GLTexture* sync(GLBackend& backend, const TexturePointer& texturePointer, bool needTransfer) { + const Texture& texture = *texturePointer; + + // Special case external textures + if (texture.getUsage().isExternal()) { + Texture::ExternalUpdates updates = texture.getUpdates(); + if (!updates.empty()) { + Texture::ExternalRecycler recycler = texture.getExternalRecycler(); + Q_ASSERT(recycler); + // Discard any superfluous updates + while (updates.size() > 1) { + const auto& update = updates.front(); + // Superfluous updates will never have been read, but we want to ensure the previous + // writes to them are complete before they're written again, so return them with the + // same fences they arrived with. This can happen on any thread because no GL context + // work is involved + recycler(update.first, update.second); + updates.pop_front(); + } + + // The last texture remaining is the one we'll use to create the GLTexture + const auto& update = updates.front(); + // Check for a fence, and if it exists, inject a wait into the command stream, then destroy the fence + if (update.second) { + GLsync fence = static_cast(update.second); + glWaitSync(fence, 0, GL_TIMEOUT_IGNORED); + glDeleteSync(fence); + } + + // Create the new texture object (replaces any previous texture object) + new GLTextureType(backend.shared_from_this(), texture, update.first); + } + + // Return the texture object (if any) associated with the texture, without extensive logic + // (external textures are + return Backend::getGPUObject(texture); + } + + if (!texture.isDefined()) { + // NO texture definition yet so let's avoid thinking + return nullptr; + } + + // If the object hasn't been created, or the object definition is out of date, drop and re-create + GLTexture* object = Backend::getGPUObject(texture); + + // Create the texture if need be (force re-creation if the storage stamp changes + // for easier use of immutable storage) + if (!object || object->isInvalid()) { + // This automatically any previous texture + object = new GLTextureType(backend.shared_from_this(), texture, needTransfer); + if (!object->_transferrable) { + object->createTexture(); + object->_contentStamp = texture.getDataStamp(); + object->updateSize(); + object->postTransfer(); + } + } + + // Object maybe doens't neet to be tranasferred after creation + if (!object->_transferrable) { + return object; + } + + // If we just did a transfer, return the object after doing post-transfer work + if (GLSyncState::Transferred == object->getSyncState()) { + object->postTransfer(); + } + + if (object->isOutdated()) { + // Object might be outdated, if so, start the transfer + // (outdated objects that are already in transfer will have reported 'true' for ready() + _textureTransferHelper->transferTexture(texturePointer); + return nullptr; + } + + if (!object->isReady()) { + return nullptr; + } + + ((GLTexture*)object)->updateMips(); + + return object; + } + + template + static GLuint getId(GLBackend& backend, const TexturePointer& texture, bool shouldSync) { + if (!texture) { + return 0; + } + GLTexture* object { nullptr }; + if (shouldSync) { + object = sync(backend, texture, shouldSync); + } else { + object = Backend::getGPUObject(*texture); + } + + if (!object) { + return 0; + } + + if (!shouldSync) { + return object->_id; + } + + // Don't return textures that are in transfer state + if ((object->getSyncState() != GLSyncState::Idle) || + // Don't return transferrable textures that have never completed transfer + (!object->_transferrable || 0 != object->_transferCount)) { + return 0; + } + + return object->_id; + } + ~GLTexture(); + // Is this texture generated outside the GPU library? + const bool _external; const GLuint& _texture { _id }; const std::string _source; + const Stamp _storageStamp; const GLenum _target; + const GLenum _internalFormat; + const uint16 _maxMip; + uint16 _minMip; + const GLuint _virtualSize; // theoretical size as expected + Stamp _contentStamp { 0 }; + const bool _transferrable; + Size _transferCount { 0 }; + GLuint size() const { return _size; } + GLSyncState getSyncState() const { return _syncState; } - static const std::vector& getFaceTargets(GLenum textureType); - static uint8_t getFaceCount(GLenum textureType); - static GLenum getGLTextureType(const Texture& texture); + // Is the storage out of date relative to the gpu texture? + bool isInvalid() const; - static const uint8_t TEXTURE_2D_NUM_FACES = 1; - static const uint8_t TEXTURE_CUBE_NUM_FACES = 6; - static const GLenum CUBE_FACE_LAYOUT[TEXTURE_CUBE_NUM_FACES]; + // Is the content out of date relative to the gpu texture? + bool isOutdated() const; + + // Is the texture in a state where it can be rendered with no work? + bool isReady() const; + + // Execute any post-move operations that must occur only on the main thread + virtual void postTransfer(); + + uint16 usedMipLevels() const { return (_maxMip - _minMip) + 1; } + + static const size_t CUBE_NUM_FACES = 6; + static const GLenum CUBE_FACE_LAYOUT[6]; static const GLFilterMode FILTER_MODES[Sampler::NUM_FILTERS]; static const GLenum WRAP_MODES[Sampler::NUM_WRAP_MODES]; + // Return a floating point value indicating how much of the allowed + // texture memory we are currently consuming. A value of 0 indicates + // no texture memory usage, while a value of 1 indicates all available / allowed memory + // is consumed. A value above 1 indicates that there is a problem. + static float getMemoryPressure(); protected: - virtual uint32 size() const = 0; - virtual void generateMips() const = 0; + static const std::vector& getFaceTargets(GLenum textureType); + + static GLenum getGLTextureType(const Texture& texture); + + + const GLuint _size { 0 }; // true size as reported by the gl api + std::atomic _syncState { GLSyncState::Idle }; + + GLTexture(const std::weak_ptr& backend, const Texture& texture, GLuint id, bool transferrable); GLTexture(const std::weak_ptr& backend, const Texture& texture, GLuint id); -}; -class GLExternalTexture : public GLTexture { - using Parent = GLTexture; - friend class GLBackend; -public: - ~GLExternalTexture(); + void setSyncState(GLSyncState syncState) { _syncState = syncState; } + + void createTexture(); + + virtual void updateMips() {} + virtual void allocateStorage() const = 0; + virtual void updateSize() const = 0; + virtual void syncSampler() const = 0; + virtual void generateMips() const = 0; + virtual void withPreservedTexture(std::function f) const; + protected: - GLExternalTexture(const std::weak_ptr& backend, const Texture& texture, GLuint id); - void generateMips() const override {} - uint32 size() const override { return 0; } -}; + void setSize(GLuint size) const; + virtual void startTransfer(); + // Returns true if this is the last block required to complete transfer + virtual bool continueTransfer() { return false; } + virtual void finishTransfer(); + +private: + friend class GLTextureTransferHelper; + friend class GLBackend; +}; } } diff --git a/libraries/gpu-gl/src/gpu/gl/GLTextureTransfer.cpp b/libraries/gpu-gl/src/gpu/gl/GLTextureTransfer.cpp new file mode 100644 index 0000000000..9dac2986e3 --- /dev/null +++ b/libraries/gpu-gl/src/gpu/gl/GLTextureTransfer.cpp @@ -0,0 +1,208 @@ +// +// Created by Bradley Austin Davis on 2016/04/03 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#include "GLTextureTransfer.h" + +#include +#include + +#include + +#include "GLShared.h" +#include "GLTexture.h" + +#ifdef HAVE_NSIGHT +#include "nvToolsExt.h" +std::unordered_map _map; +#endif + + +#ifdef TEXTURE_TRANSFER_PBOS +#define TEXTURE_TRANSFER_BLOCK_SIZE (64 * 1024) +#define TEXTURE_TRANSFER_PBO_COUNT 128 +#endif + +using namespace gpu; +using namespace gpu::gl; + +GLTextureTransferHelper::GLTextureTransferHelper() { +#ifdef THREADED_TEXTURE_TRANSFER + setObjectName("TextureTransferThread"); + _context.create(); + initialize(true, QThread::LowPriority); + // Clean shutdown on UNIX, otherwise _canvas is freed early + connect(qApp, &QCoreApplication::aboutToQuit, [&] { terminate(); }); +#else + initialize(false, QThread::LowPriority); +#endif +} + +GLTextureTransferHelper::~GLTextureTransferHelper() { +#ifdef THREADED_TEXTURE_TRANSFER + if (isStillRunning()) { + terminate(); + } +#else + terminate(); +#endif +} + +void GLTextureTransferHelper::transferTexture(const gpu::TexturePointer& texturePointer) { + GLTexture* object = Backend::getGPUObject(*texturePointer); + + Backend::incrementTextureGPUTransferCount(); + object->setSyncState(GLSyncState::Pending); + Lock lock(_mutex); + _pendingTextures.push_back(texturePointer); +} + +void GLTextureTransferHelper::setup() { +#ifdef THREADED_TEXTURE_TRANSFER + _context.makeCurrent(); + +#ifdef TEXTURE_TRANSFER_FORCE_DRAW + // FIXME don't use opengl 4.5 DSA functionality without verifying it's present + glCreateRenderbuffers(1, &_drawRenderbuffer); + glNamedRenderbufferStorage(_drawRenderbuffer, GL_RGBA8, 128, 128); + glCreateFramebuffers(1, &_drawFramebuffer); + glNamedFramebufferRenderbuffer(_drawFramebuffer, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, _drawRenderbuffer); + glCreateFramebuffers(1, &_readFramebuffer); +#endif + +#ifdef TEXTURE_TRANSFER_PBOS + std::array pbos; + glCreateBuffers(TEXTURE_TRANSFER_PBO_COUNT, &pbos[0]); + for (uint32_t i = 0; i < TEXTURE_TRANSFER_PBO_COUNT; ++i) { + TextureTransferBlock newBlock; + newBlock._pbo = pbos[i]; + glNamedBufferStorage(newBlock._pbo, TEXTURE_TRANSFER_BLOCK_SIZE, 0, GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT); + newBlock._mapped = glMapNamedBufferRange(newBlock._pbo, 0, TEXTURE_TRANSFER_BLOCK_SIZE, GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT); + _readyQueue.push(newBlock); + } +#endif +#endif +} + +void GLTextureTransferHelper::shutdown() { +#ifdef THREADED_TEXTURE_TRANSFER + _context.makeCurrent(); +#endif + +#ifdef TEXTURE_TRANSFER_FORCE_DRAW + glNamedFramebufferRenderbuffer(_drawFramebuffer, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, 0); + glDeleteFramebuffers(1, &_drawFramebuffer); + _drawFramebuffer = 0; + glDeleteFramebuffers(1, &_readFramebuffer); + _readFramebuffer = 0; + + glNamedFramebufferTexture(_readFramebuffer, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0); + glDeleteRenderbuffers(1, &_drawRenderbuffer); + _drawRenderbuffer = 0; +#endif +} + +void GLTextureTransferHelper::queueExecution(VoidLambda lambda) { + Lock lock(_mutex); + _pendingCommands.push_back(lambda); +} + +#define MAX_TRANSFERS_PER_PASS 2 + +bool GLTextureTransferHelper::process() { + // Take any new textures or commands off the queue + VoidLambdaList pendingCommands; + TextureList newTransferTextures; + { + Lock lock(_mutex); + newTransferTextures.swap(_pendingTextures); + pendingCommands.swap(_pendingCommands); + } + + if (!pendingCommands.empty()) { + for (auto command : pendingCommands) { + command(); + } + glFlush(); + } + + if (!newTransferTextures.empty()) { + for (auto& texturePointer : newTransferTextures) { +#ifdef HAVE_NSIGHT + _map[texturePointer] = nvtxRangeStart("TextureTansfer"); +#endif + GLTexture* object = Backend::getGPUObject(*texturePointer); + object->startTransfer(); + _transferringTextures.push_back(texturePointer); + _textureIterator = _transferringTextures.begin(); + } + _transferringTextures.sort([](const gpu::TexturePointer& a, const gpu::TexturePointer& b)->bool { + return a->getSize() < b->getSize(); + }); + } + + // No transfers in progress, sleep + if (_transferringTextures.empty()) { +#ifdef THREADED_TEXTURE_TRANSFER + QThread::usleep(1); +#endif + return true; + } + PROFILE_COUNTER_IF_CHANGED(render_gpu_gl, "transferringTextures", int, (int) _transferringTextures.size()) + + static auto lastReport = usecTimestampNow(); + auto now = usecTimestampNow(); + auto lastReportInterval = now - lastReport; + if (lastReportInterval > USECS_PER_SECOND * 4) { + lastReport = now; + qCDebug(gpulogging) << "Texture list " << _transferringTextures.size(); + } + + size_t transferCount = 0; + for (_textureIterator = _transferringTextures.begin(); _textureIterator != _transferringTextures.end();) { + if (++transferCount > MAX_TRANSFERS_PER_PASS) { + break; + } + auto texture = *_textureIterator; + GLTexture* gltexture = Backend::getGPUObject(*texture); + if (gltexture->continueTransfer()) { + ++_textureIterator; + continue; + } + + gltexture->finishTransfer(); + +#ifdef TEXTURE_TRANSFER_FORCE_DRAW + // FIXME force a draw on the texture transfer thread before passing the texture to the main thread for use +#endif + +#ifdef THREADED_TEXTURE_TRANSFER + clientWait(); +#endif + gltexture->_contentStamp = gltexture->_gpuObject.getDataStamp(); + gltexture->updateSize(); + gltexture->setSyncState(gpu::gl::GLSyncState::Transferred); + Backend::decrementTextureGPUTransferCount(); +#ifdef HAVE_NSIGHT + // Mark the texture as transferred + nvtxRangeEnd(_map[texture]); + _map.erase(texture); +#endif + _textureIterator = _transferringTextures.erase(_textureIterator); + } + +#ifdef THREADED_TEXTURE_TRANSFER + if (!_transferringTextures.empty()) { + // Don't saturate the GPU + clientWait(); + } else { + // Don't saturate the CPU + QThread::msleep(1); + } +#endif + + return true; +} diff --git a/libraries/gpu-gl/src/gpu/gl/GLTextureTransfer.h b/libraries/gpu-gl/src/gpu/gl/GLTextureTransfer.h new file mode 100644 index 0000000000..a23c282fd4 --- /dev/null +++ b/libraries/gpu-gl/src/gpu/gl/GLTextureTransfer.h @@ -0,0 +1,78 @@ +// +// Created by Bradley Austin Davis on 2016/04/03 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#ifndef hifi_gpu_gl_GLTextureTransfer_h +#define hifi_gpu_gl_GLTextureTransfer_h + +#include +#include + +#include + +#include + +#include "GLShared.h" + +#ifdef Q_OS_WIN +#define THREADED_TEXTURE_TRANSFER +#endif + +#ifdef THREADED_TEXTURE_TRANSFER +// FIXME when sparse textures are enabled, it's harder to force a draw on the transfer thread +// also, the current draw code is implicitly using OpenGL 4.5 functionality +//#define TEXTURE_TRANSFER_FORCE_DRAW +// FIXME PBO's increase the complexity and don't seem to work reliably +//#define TEXTURE_TRANSFER_PBOS +#endif + +namespace gpu { namespace gl { + +using TextureList = std::list; +using TextureListIterator = TextureList::iterator; + +class GLTextureTransferHelper : public GenericThread { +public: + using VoidLambda = std::function; + using VoidLambdaList = std::list; + using Pointer = std::shared_ptr; + GLTextureTransferHelper(); + ~GLTextureTransferHelper(); + void transferTexture(const gpu::TexturePointer& texturePointer); + void queueExecution(VoidLambda lambda); + + void setup() override; + void shutdown() override; + bool process() override; + +private: +#ifdef THREADED_TEXTURE_TRANSFER + ::gl::OffscreenContext _context; +#endif + +#ifdef TEXTURE_TRANSFER_FORCE_DRAW + // Framebuffers / renderbuffers for forcing access to the texture on the transfer thread + GLuint _drawRenderbuffer { 0 }; + GLuint _drawFramebuffer { 0 }; + GLuint _readFramebuffer { 0 }; +#endif + + // A mutex for protecting items access on the render and transfer threads + Mutex _mutex; + // Commands that have been submitted for execution on the texture transfer thread + VoidLambdaList _pendingCommands; + // Textures that have been submitted for transfer + TextureList _pendingTextures; + // Textures currently in the transfer process + // Only used on the transfer thread + TextureList _transferringTextures; + TextureListIterator _textureIterator; + +}; + +} } + +#endif \ No newline at end of file diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h b/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h index 6d2f91c436..72e2f5a804 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h +++ b/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h @@ -40,28 +40,18 @@ public: class GL41Texture : public GLTexture { using Parent = GLTexture; - static GLuint allocate(); - + GLuint allocate(); public: - ~GL41Texture(); - - private: - GL41Texture(const std::weak_ptr& backend, const Texture& buffer); + GL41Texture(const std::weak_ptr& backend, const Texture& buffer, GLuint externalId); + GL41Texture(const std::weak_ptr& backend, const Texture& buffer, bool transferrable); + protected: + void transferMip(uint16_t mipLevel, uint8_t face) const; + void startTransfer() override; + void allocateStorage() const override; + void updateSize() const override; + void syncSampler() const override; void generateMips() const override; - uint32 size() const override; - - friend class GL41Backend; - const Stamp _storageStamp; - mutable Stamp _contentStamp { 0 }; - mutable Stamp _samplerStamp { 0 }; - const uint32 _size; - - - bool isOutdated() const; - void withPreservedTexture(std::function f) const; - void syncContent() const; - void syncSampler() const; }; @@ -72,7 +62,8 @@ protected: GLuint getBufferID(const Buffer& buffer) override; GLBuffer* syncGPUObject(const Buffer& buffer) override; - GLTexture* syncGPUObject(const TexturePointer& texture) override; + GLuint getTextureID(const TexturePointer& texture, bool needTransfer = true) override; + GLTexture* syncGPUObject(const TexturePointer& texture, bool sync = true) override; GLuint getQueryID(const QueryPointer& query) override; GLQuery* syncGPUObject(const Query& query) override; diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41BackendOutput.cpp b/libraries/gpu-gl/src/gpu/gl41/GL41BackendOutput.cpp index 195b155bf3..6d11a52035 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41BackendOutput.cpp +++ b/libraries/gpu-gl/src/gpu/gl41/GL41BackendOutput.cpp @@ -53,12 +53,10 @@ public: GL_COLOR_ATTACHMENT15 }; int unit = 0; - auto backend = _backend.lock(); for (auto& b : _gpuObject.getRenderBuffers()) { surface = b._texture; if (surface) { - Q_ASSERT(TextureUsageType::RENDERBUFFER == surface->getUsageType()); - gltexture = backend->syncGPUObject(surface); + gltexture = gl::GLTexture::sync(*_backend.lock().get(), surface, false); // Grab the gltexture and don't transfer } else { gltexture = nullptr; } @@ -83,11 +81,9 @@ public: } if (_gpuObject.getDepthStamp() != _depthStamp) { - auto backend = _backend.lock(); auto surface = _gpuObject.getDepthStencilBuffer(); if (_gpuObject.hasDepthStencil() && surface) { - Q_ASSERT(TextureUsageType::RENDERBUFFER == surface->getUsageType()); - gltexture = backend->syncGPUObject(surface); + gltexture = gl::GLTexture::sync(*_backend.lock().get(), surface, false); // Grab the gltexture and don't transfer } if (gltexture) { @@ -114,7 +110,7 @@ public: glBindFramebuffer(GL_DRAW_FRAMEBUFFER, currentFBO); } - checkStatus(); + checkStatus(GL_DRAW_FRAMEBUFFER); } diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp b/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp index 8dbef09f06..65c45111db 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp +++ b/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp @@ -29,102 +29,20 @@ GLuint GL41Texture::allocate() { return result; } -GLTexture* GL41Backend::syncGPUObject(const TexturePointer& texturePointer) { - if (!texturePointer) { - return nullptr; - } - const Texture& texture = *texturePointer; - if (TextureUsageType::EXTERNAL == texture.getUsageType()) { - return Parent::syncGPUObject(texturePointer); - } - - if (!texture.isDefined()) { - // NO texture definition yet so let's avoid thinking - return nullptr; - } - - // If the object hasn't been created, or the object definition is out of date, drop and re-create - GL41Texture* object = Backend::getGPUObject(texture); - if (!object || object->_storageStamp < texture.getStamp()) { - // This automatically any previous texture - object = new GL41Texture(shared_from_this(), texture); - } - - // FIXME internalize to GL41Texture 'sync' function - if (object->isOutdated()) { - object->withPreservedTexture([&] { - if (object->_contentStamp <= texture.getDataStamp()) { - // FIXME implement synchronous texture transfer here - object->syncContent(); - } - - if (object->_samplerStamp <= texture.getSamplerStamp()) { - object->syncSampler(); - } - }); - } - - return object; +GLuint GL41Backend::getTextureID(const TexturePointer& texture, bool transfer) { + return GL41Texture::getId(*this, texture, transfer); } -GL41Texture::GL41Texture(const std::weak_ptr& backend, const Texture& texture) - : GLTexture(backend, texture, allocate()), _storageStamp { texture.getStamp() }, _size(texture.evalTotalSize()) { - incrementTextureGPUCount(); - withPreservedTexture([&] { - GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat(), _gpuObject.getStoredMipFormat()); - auto numMips = _gpuObject.evalNumMips(); - for (uint16_t mipLevel = 0; mipLevel < numMips; ++mipLevel) { - // Get the mip level dimensions, accounting for the downgrade level - Vec3u dimensions = _gpuObject.evalMipDimensions(mipLevel); - uint8_t face = 0; - for (GLenum target : getFaceTargets(_target)) { - const Byte* mipData = nullptr; - if (_gpuObject.isStoredMipFaceAvailable(mipLevel, face)) { - auto mip = _gpuObject.accessStoredMipFace(mipLevel, face); - mipData = mip->readData(); - } - glTexImage2D(target, mipLevel, texelFormat.internalFormat, dimensions.x, dimensions.y, 0, texelFormat.format, texelFormat.type, mipData); - (void)CHECK_GL_ERROR(); - ++face; - } - } - }); +GLTexture* GL41Backend::syncGPUObject(const TexturePointer& texture, bool transfer) { + return GL41Texture::sync(*this, texture, transfer); } -GL41Texture::~GL41Texture() { - +GL41Texture::GL41Texture(const std::weak_ptr& backend, const Texture& texture, GLuint externalId) + : GLTexture(backend, texture, externalId) { } -bool GL41Texture::isOutdated() const { - if (_samplerStamp <= _gpuObject.getSamplerStamp()) { - return true; - } - if (TextureUsageType::RESOURCE == _gpuObject.getUsageType() && _contentStamp <= _gpuObject.getDataStamp()) { - return true; - } - return false; -} - -void GL41Texture::withPreservedTexture(std::function f) const { - GLint boundTex = -1; - switch (_target) { - case GL_TEXTURE_2D: - glGetIntegerv(GL_TEXTURE_BINDING_2D, &boundTex); - break; - - case GL_TEXTURE_CUBE_MAP: - glGetIntegerv(GL_TEXTURE_BINDING_CUBE_MAP, &boundTex); - break; - - default: - qFatal("Unsupported texture type"); - } - (void)CHECK_GL_ERROR(); - - glBindTexture(_target, _texture); - f(); - glBindTexture(_target, boundTex); - (void)CHECK_GL_ERROR(); +GL41Texture::GL41Texture(const std::weak_ptr& backend, const Texture& texture, bool transferrable) + : GLTexture(backend, texture, allocate(), transferrable) { } void GL41Texture::generateMips() const { @@ -134,12 +52,94 @@ void GL41Texture::generateMips() const { (void)CHECK_GL_ERROR(); } -void GL41Texture::syncContent() const { - // FIXME actually copy the texture data - _contentStamp = _gpuObject.getDataStamp() + 1; +void GL41Texture::allocateStorage() const { + GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat()); + glTexParameteri(_target, GL_TEXTURE_BASE_LEVEL, 0); + (void)CHECK_GL_ERROR(); + glTexParameteri(_target, GL_TEXTURE_MAX_LEVEL, _maxMip - _minMip); + (void)CHECK_GL_ERROR(); + if (GLEW_VERSION_4_2 && !_gpuObject.getTexelFormat().isCompressed()) { + // Get the dimensions, accounting for the downgrade level + Vec3u dimensions = _gpuObject.evalMipDimensions(_minMip); + glTexStorage2D(_target, usedMipLevels(), texelFormat.internalFormat, dimensions.x, dimensions.y); + (void)CHECK_GL_ERROR(); + } else { + for (uint16_t l = _minMip; l <= _maxMip; l++) { + // Get the mip level dimensions, accounting for the downgrade level + Vec3u dimensions = _gpuObject.evalMipDimensions(l); + for (GLenum target : getFaceTargets(_target)) { + glTexImage2D(target, l - _minMip, texelFormat.internalFormat, dimensions.x, dimensions.y, 0, texelFormat.format, texelFormat.type, NULL); + (void)CHECK_GL_ERROR(); + } + } + } } -void GL41Texture::syncSampler() const { +void GL41Texture::updateSize() const { + setSize(_virtualSize); + if (!_id) { + return; + } + + if (_gpuObject.getTexelFormat().isCompressed()) { + GLenum proxyType = GL_TEXTURE_2D; + GLuint numFaces = 1; + if (_gpuObject.getType() == gpu::Texture::TEX_CUBE) { + proxyType = CUBE_FACE_LAYOUT[0]; + numFaces = (GLuint)CUBE_NUM_FACES; + } + GLint gpuSize{ 0 }; + glGetTexLevelParameteriv(proxyType, 0, GL_TEXTURE_COMPRESSED, &gpuSize); + (void)CHECK_GL_ERROR(); + + if (gpuSize) { + for (GLuint level = _minMip; level < _maxMip; level++) { + GLint levelSize{ 0 }; + glGetTexLevelParameteriv(proxyType, level, GL_TEXTURE_COMPRESSED_IMAGE_SIZE, &levelSize); + levelSize *= numFaces; + + if (levelSize <= 0) { + break; + } + gpuSize += levelSize; + } + (void)CHECK_GL_ERROR(); + setSize(gpuSize); + return; + } + } +} + +// Move content bits from the CPU to the GPU for a given mip / face +void GL41Texture::transferMip(uint16_t mipLevel, uint8_t face) const { + auto mip = _gpuObject.accessStoredMipFace(mipLevel, face); + GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat(), mip->getFormat()); + //GLenum target = getFaceTargets()[face]; + GLenum target = _target == GL_TEXTURE_2D ? GL_TEXTURE_2D : CUBE_FACE_LAYOUT[face]; + auto size = _gpuObject.evalMipDimensions(mipLevel); + glTexSubImage2D(target, mipLevel, 0, 0, size.x, size.y, texelFormat.format, texelFormat.type, mip->readData()); + (void)CHECK_GL_ERROR(); +} + +void GL41Texture::startTransfer() { + PROFILE_RANGE(render_gpu_gl, __FUNCTION__); + Parent::startTransfer(); + + glBindTexture(_target, _id); + (void)CHECK_GL_ERROR(); + + // transfer pixels from each faces + uint8_t numFaces = (Texture::TEX_CUBE == _gpuObject.getType()) ? CUBE_NUM_FACES : 1; + for (uint8_t f = 0; f < numFaces; f++) { + for (uint16_t i = 0; i < Sampler::MAX_MIP_LEVEL; ++i) { + if (_gpuObject.isStoredMipFaceAvailable(i, f)) { + transferMip(i, f); + } + } + } +} + +void GL41Backend::GL41Texture::syncSampler() const { const Sampler& sampler = _gpuObject.getSampler(); const auto& fm = FILTER_MODES[sampler.getFilter()]; glTexParameteri(_target, GL_TEXTURE_MIN_FILTER, fm.minFilter); @@ -161,9 +161,5 @@ void GL41Texture::syncSampler() const { glTexParameterf(_target, GL_TEXTURE_MIN_LOD, (float)sampler.getMinMip()); glTexParameterf(_target, GL_TEXTURE_MAX_LOD, (sampler.getMaxMip() == Sampler::MAX_MIP_LEVEL ? 1000.f : sampler.getMaxMip())); glTexParameterf(_target, GL_TEXTURE_MAX_ANISOTROPY_EXT, sampler.getMaxAnisotropy()); - _samplerStamp = _gpuObject.getSamplerStamp() + 1; } -uint32 GL41Texture::size() const { - return _size; -} diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45Backend.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45Backend.cpp index 12c4b818f7..d7dde8b7d6 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45Backend.cpp +++ b/libraries/gpu-gl/src/gpu/gl45/GL45Backend.cpp @@ -18,12 +18,6 @@ Q_LOGGING_CATEGORY(gpugl45logging, "hifi.gpu.gl45") using namespace gpu; using namespace gpu::gl45; -void GL45Backend::recycle() const { - Parent::recycle(); - GL45VariableAllocationTexture::manageMemory(); - GL45VariableAllocationTexture::_frameTexturesCreated = 0; -} - void GL45Backend::do_draw(const Batch& batch, size_t paramOffset) { Primitive primitiveType = (Primitive)batch._params[paramOffset + 2]._uint; GLenum mode = gl::PRIMITIVE_TO_GL[primitiveType]; @@ -169,3 +163,8 @@ void GL45Backend::do_multiDrawIndexedIndirect(const Batch& batch, size_t paramOf _stats._DSNumAPIDrawcalls++; (void)CHECK_GL_ERROR(); } + +void GL45Backend::recycle() const { + Parent::recycle(); + derezTextures(); +} diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h b/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h index 6a9811b055..2242bba5d9 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h +++ b/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h @@ -8,21 +8,17 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#pragma once #ifndef hifi_gpu_45_GL45Backend_h #define hifi_gpu_45_GL45Backend_h #include "../gl/GLBackend.h" #include "../gl/GLTexture.h" -#include #define INCREMENTAL_TRANSFER 0 -#define THREADED_TEXTURE_BUFFERING 1 namespace gpu { namespace gl45 { using namespace gpu::gl; -using TextureWeakPointer = std::weak_ptr; class GL45Backend : public GLBackend { using Parent = GLBackend; @@ -35,219 +31,60 @@ public: class GL45Texture : public GLTexture { using Parent = GLTexture; - friend class GL45Backend; static GLuint allocate(const Texture& texture); - protected: - GL45Texture(const std::weak_ptr& backend, const Texture& texture); - void generateMips() const override; - void copyMipFaceFromTexture(uint16_t sourceMip, uint16_t targetMip, uint8_t face) const; - void copyMipFaceLinesFromTexture(uint16_t mip, uint8_t face, const uvec3& size, uint32_t yOffset, GLenum format, GLenum type, const void* sourcePointer) const; - virtual void syncSampler() const; - }; - - // - // Textures that have fixed allocation sizes and cannot be managed at runtime - // - - class GL45FixedAllocationTexture : public GL45Texture { - using Parent = GL45Texture; - friend class GL45Backend; - - public: - GL45FixedAllocationTexture(const std::weak_ptr& backend, const Texture& texture); - ~GL45FixedAllocationTexture(); - - protected: - uint32 size() const override { return _size; } - void allocateStorage() const; - void syncSampler() const override; - const uint32 _size { 0 }; - }; - - class GL45AttachmentTexture : public GL45FixedAllocationTexture { - using Parent = GL45FixedAllocationTexture; - friend class GL45Backend; - protected: - GL45AttachmentTexture(const std::weak_ptr& backend, const Texture& texture); - ~GL45AttachmentTexture(); - }; - - class GL45StrictResourceTexture : public GL45FixedAllocationTexture { - using Parent = GL45FixedAllocationTexture; - friend class GL45Backend; - protected: - GL45StrictResourceTexture(const std::weak_ptr& backend, const Texture& texture); - }; - - // - // Textures that can be managed at runtime to increase or decrease their memory load - // - - class GL45VariableAllocationTexture : public GL45Texture { - using Parent = GL45Texture; - friend class GL45Backend; - using PromoteLambda = std::function; - - public: - enum class MemoryPressureState { - Idle, - Transfer, - Oversubscribed, - Undersubscribed, - }; - - using QueuePair = std::pair; - struct QueuePairLess { - bool operator()(const QueuePair& a, const QueuePair& b) { - return a.second < b.second; - } - }; - using WorkQueue = std::priority_queue, QueuePairLess>; - - class TransferJob { - using VoidLambda = std::function; - using VoidLambdaQueue = std::queue; - using ThreadPointer = std::shared_ptr; - const GL45VariableAllocationTexture& _parent; - // Holds the contents to transfer to the GPU in CPU memory - std::vector _buffer; - // Indicates if a transfer from backing storage to interal storage has started - bool _bufferingStarted { false }; - bool _bufferingCompleted { false }; - VoidLambda _transferLambda; - VoidLambda _bufferingLambda; -#if THREADED_TEXTURE_BUFFERING - static Mutex _mutex; - static VoidLambdaQueue _bufferLambdaQueue; - static ThreadPointer _bufferThread; - static std::atomic _shutdownBufferingThread; - static void bufferLoop(); -#endif - - public: - TransferJob(const TransferJob& other) = delete; - TransferJob(const GL45VariableAllocationTexture& parent, std::function transferLambda); - TransferJob(const GL45VariableAllocationTexture& parent, uint16_t sourceMip, uint16_t targetMip, uint8_t face, uint32_t lines = 0, uint32_t lineOffset = 0); - ~TransferJob(); - bool tryTransfer(); - -#if THREADED_TEXTURE_BUFFERING - static void startTransferLoop(); - static void stopTransferLoop(); -#endif - - private: - size_t _transferSize { 0 }; -#if THREADED_TEXTURE_BUFFERING - void startBuffering(); -#endif - void transfer(); - }; - - using TransferQueue = std::queue>; - static MemoryPressureState _memoryPressureState; - protected: - static size_t _frameTexturesCreated; - static std::atomic _memoryPressureStateStale; - static std::list _memoryManagedTextures; - static WorkQueue _transferQueue; - static WorkQueue _promoteQueue; - static WorkQueue _demoteQueue; - static TexturePointer _currentTransferTexture; - static const uvec3 INITIAL_MIP_TRANSFER_DIMENSIONS; - - - static void updateMemoryPressure(); - static void processWorkQueues(); - static void addMemoryManagedTexture(const TexturePointer& texturePointer); - static void addToWorkQueue(const TexturePointer& texture); - static WorkQueue& getActiveWorkQueue(); - - static void manageMemory(); - - protected: - GL45VariableAllocationTexture(const std::weak_ptr& backend, const Texture& texture); - ~GL45VariableAllocationTexture(); - //bool canPromoteNoAllocate() const { return _allocatedMip < _populatedMip; } - bool canPromote() const { return _allocatedMip > 0; } - bool canDemote() const { return _allocatedMip < _maxAllocatedMip; } - bool hasPendingTransfers() const { return _populatedMip > _allocatedMip; } - void executeNextTransfer(const TexturePointer& currentTexture); - uint32 size() const override { return _size; } - virtual void populateTransferQueue() = 0; - virtual void promote() = 0; - virtual void demote() = 0; - - // The allocated mip level, relative to the number of mips in the gpu::Texture object - // The relationship between a given glMip to the original gpu::Texture mip is always - // glMip + _allocatedMip - uint16 _allocatedMip { 0 }; - // The populated mip level, relative to the number of mips in the gpu::Texture object - // This must always be >= the allocated mip - uint16 _populatedMip { 0 }; - // The highest (lowest resolution) mip that we will support, relative to the number - // of mips in the gpu::Texture object - uint16 _maxAllocatedMip { 0 }; - uint32 _size { 0 }; - // Contains a series of lambdas that when executed will transfer data to the GPU, modify - // the _populatedMip and update the sampler in order to fully populate the allocated texture - // until _populatedMip == _allocatedMip - TransferQueue _pendingTransfers; - }; - - class GL45ResourceTexture : public GL45VariableAllocationTexture { - using Parent = GL45VariableAllocationTexture; - friend class GL45Backend; - protected: - GL45ResourceTexture(const std::weak_ptr& backend, const Texture& texture); - - void syncSampler() const override; - void promote() override; - void demote() override; - void populateTransferQueue() override; - - void allocateStorage(uint16 mip); - void copyMipsFromTexture(); - }; - -#if 0 - class GL45SparseResourceTexture : public GL45VariableAllocationTexture { - using Parent = GL45VariableAllocationTexture; - friend class GL45Backend; - using TextureTypeFormat = std::pair; - using PageDimensions = std::vector; - using PageDimensionsMap = std::map; - static PageDimensionsMap pageDimensionsByFormat; - static Mutex pageDimensionsMutex; - - static bool isSparseEligible(const Texture& texture); - static PageDimensions getPageDimensionsForFormat(const TextureTypeFormat& typeFormat); - static PageDimensions getPageDimensionsForFormat(GLenum type, GLenum format); static const uint32_t DEFAULT_PAGE_DIMENSION = 128; static const uint32_t DEFAULT_MAX_SPARSE_LEVEL = 0xFFFF; + public: + GL45Texture(const std::weak_ptr& backend, const Texture& texture, GLuint externalId); + GL45Texture(const std::weak_ptr& backend, const Texture& texture, bool transferrable); + ~GL45Texture(); + + void postTransfer() override; + + struct SparseInfo { + SparseInfo(GL45Texture& texture); + void maybeMakeSparse(); + void update(); + uvec3 getPageCounts(const uvec3& dimensions) const; + uint32_t getPageCount(const uvec3& dimensions) const; + uint32_t getSize() const; + + GL45Texture& texture; + bool sparse { false }; + uvec3 pageDimensions { DEFAULT_PAGE_DIMENSION }; + GLuint maxSparseLevel { DEFAULT_MAX_SPARSE_LEVEL }; + uint32_t allocatedPages { 0 }; + uint32_t maxPages { 0 }; + uint32_t pageBytes { 0 }; + GLint pageDimensionsIndex { 0 }; + }; + protected: - GL45SparseResourceTexture(const std::weak_ptr& backend, const Texture& texture); - ~GL45SparseResourceTexture(); - uint32 size() const override { return _allocatedPages * _pageBytes; } - void promote() override; - void demote() override; + void updateMips() override; + void stripToMip(uint16_t newMinMip); + void startTransfer() override; + bool continueTransfer() override; + void finishTransfer() override; + void incrementalTransfer(const uvec3& size, const gpu::Texture::PixelsPointer& mip, std::function f) const; + void transferMip(uint16_t mipLevel, uint8_t face = 0) const; + void allocateMip(uint16_t mipLevel, uint8_t face = 0) const; + void allocateStorage() const override; + void updateSize() const override; + void syncSampler() const override; + void generateMips() const override; + void withPreservedTexture(std::function f) const override; + void derez(); - private: - uvec3 getPageCounts(const uvec3& dimensions) const; - uint32_t getPageCount(const uvec3& dimensions) const; - - uint32_t _allocatedPages { 0 }; - uint32_t _pageBytes { 0 }; - uvec3 _pageDimensions { DEFAULT_PAGE_DIMENSION }; - GLuint _maxSparseLevel { DEFAULT_MAX_SPARSE_LEVEL }; + SparseInfo _sparseInfo; + uint16_t _mipOffset { 0 }; + friend class GL45Backend; }; -#endif protected: - void recycle() const override; + void derezTextures() const; GLuint getFramebufferID(const FramebufferPointer& framebuffer) override; GLFramebuffer* syncGPUObject(const Framebuffer& framebuffer) override; @@ -255,7 +92,8 @@ protected: GLuint getBufferID(const Buffer& buffer) override; GLBuffer* syncGPUObject(const Buffer& buffer) override; - GLTexture* syncGPUObject(const TexturePointer& texture) override; + GLuint getTextureID(const TexturePointer& texture, bool needTransfer = true) override; + GLTexture* syncGPUObject(const TexturePointer& texture, bool sync = true) override; GLuint getQueryID(const QueryPointer& query) override; GLQuery* syncGPUObject(const Query& query) override; @@ -288,5 +126,5 @@ protected: Q_DECLARE_LOGGING_CATEGORY(gpugl45logging) -#endif +#endif diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp index 9648af9b21..c5b84b7deb 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp @@ -49,12 +49,10 @@ public: GL_COLOR_ATTACHMENT15 }; int unit = 0; - auto backend = _backend.lock(); for (auto& b : _gpuObject.getRenderBuffers()) { surface = b._texture; if (surface) { - Q_ASSERT(TextureUsageType::RENDERBUFFER == surface->getUsageType()); - gltexture = backend->syncGPUObject(surface); + gltexture = gl::GLTexture::sync(*_backend.lock().get(), surface, false); // Grab the gltexture and don't transfer } else { gltexture = nullptr; } @@ -80,10 +78,8 @@ public: if (_gpuObject.getDepthStamp() != _depthStamp) { auto surface = _gpuObject.getDepthStencilBuffer(); - auto backend = _backend.lock(); if (_gpuObject.hasDepthStencil() && surface) { - Q_ASSERT(TextureUsageType::RENDERBUFFER == surface->getUsageType()); - gltexture = backend->syncGPUObject(surface); + gltexture = gl::GLTexture::sync(*_backend.lock().get(), surface, false); // Grab the gltexture and don't transfer } if (gltexture) { @@ -106,7 +102,7 @@ public: _status = glCheckNamedFramebufferStatus(_id, GL_DRAW_FRAMEBUFFER); // restore the current framebuffer - checkStatus(); + checkStatus(GL_DRAW_FRAMEBUFFER); } diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp index 36aaf75e81..6948a045a2 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp @@ -8,10 +8,9 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // - #include "GL45Backend.h" + #include -#include #include #include #include @@ -20,70 +19,142 @@ #include #include -#include #include "../gl/GLTexelFormat.h" using namespace gpu; using namespace gpu::gl; using namespace gpu::gl45; -#define SPARSE_PAGE_SIZE_OVERHEAD_ESTIMATE 1.3f -#define MAX_RESOURCE_TEXTURES_PER_FRAME 2 +// Allocate 1 MB of buffer space for paged transfers +#define DEFAULT_PAGE_BUFFER_SIZE (1024*1024) +#define DEFAULT_GL_PIXEL_ALIGNMENT 4 -GLTexture* GL45Backend::syncGPUObject(const TexturePointer& texturePointer) { - if (!texturePointer) { - return nullptr; +using GL45Texture = GL45Backend::GL45Texture; + +static std::map> texturesByMipCounts; +static Mutex texturesByMipCountsMutex; +using TextureTypeFormat = std::pair; +std::map> sparsePageDimensionsByFormat; +Mutex sparsePageDimensionsByFormatMutex; + +static std::vector getPageDimensionsForFormat(const TextureTypeFormat& typeFormat) { + { + Lock lock(sparsePageDimensionsByFormatMutex); + if (sparsePageDimensionsByFormat.count(typeFormat)) { + return sparsePageDimensionsByFormat[typeFormat]; + } } + GLint count = 0; + glGetInternalformativ(typeFormat.first, typeFormat.second, GL_NUM_VIRTUAL_PAGE_SIZES_ARB, 1, &count); - const Texture& texture = *texturePointer; - if (TextureUsageType::EXTERNAL == texture.getUsageType()) { - return Parent::syncGPUObject(texturePointer); - } + std::vector result; + if (count > 0) { + std::vector x, y, z; + x.resize(count); + glGetInternalformativ(typeFormat.first, typeFormat.second, GL_VIRTUAL_PAGE_SIZE_X_ARB, 1, &x[0]); + y.resize(count); + glGetInternalformativ(typeFormat.first, typeFormat.second, GL_VIRTUAL_PAGE_SIZE_Y_ARB, 1, &y[0]); + z.resize(count); + glGetInternalformativ(typeFormat.first, typeFormat.second, GL_VIRTUAL_PAGE_SIZE_Z_ARB, 1, &z[0]); - if (!texture.isDefined()) { - // NO texture definition yet so let's avoid thinking - return nullptr; - } - - GL45Texture* object = Backend::getGPUObject(texture); - if (!object) { - switch (texture.getUsageType()) { - case TextureUsageType::RENDERBUFFER: - object = new GL45AttachmentTexture(shared_from_this(), texture); - break; - - case TextureUsageType::STRICT_RESOURCE: - qCDebug(gpugllogging) << "Strict texture " << texture.source().c_str(); - object = new GL45StrictResourceTexture(shared_from_this(), texture); - break; - - case TextureUsageType::RESOURCE: { - if (GL45VariableAllocationTexture::_frameTexturesCreated < MAX_RESOURCE_TEXTURES_PER_FRAME) { -#if 0 - if (isTextureManagementSparseEnabled() && GL45Texture::isSparseEligible(texture)) { - object = new GL45SparseResourceTexture(shared_from_this(), texture); - } else { - object = new GL45ResourceTexture(shared_from_this(), texture); - } -#else - object = new GL45ResourceTexture(shared_from_this(), texture); -#endif - GL45VariableAllocationTexture::addMemoryManagedTexture(texturePointer); - } else { - auto fallback = texturePointer->getFallbackTexture(); - if (fallback) { - object = static_cast(syncGPUObject(fallback)); - } - } - break; - } - - default: - Q_UNREACHABLE(); + result.resize(count); + for (GLint i = 0; i < count; ++i) { + result[i] = uvec3(x[i], y[i], z[i]); } } - return object; + { + Lock lock(sparsePageDimensionsByFormatMutex); + if (0 == sparsePageDimensionsByFormat.count(typeFormat)) { + sparsePageDimensionsByFormat[typeFormat] = result; + } + } + + return result; +} + +static std::vector getPageDimensionsForFormat(GLenum target, GLenum format) { + return getPageDimensionsForFormat({ target, format }); +} + +GLTexture* GL45Backend::syncGPUObject(const TexturePointer& texture, bool transfer) { + return GL45Texture::sync(*this, texture, transfer); +} + +using SparseInfo = GL45Backend::GL45Texture::SparseInfo; + +SparseInfo::SparseInfo(GL45Texture& texture) + : texture(texture) { +} + +void SparseInfo::maybeMakeSparse() { + // Don't enable sparse for objects with explicitly managed mip levels + if (!texture._gpuObject.isAutogenerateMips()) { + return; + } + return; + + const uvec3 dimensions = texture._gpuObject.getDimensions(); + auto allowedPageDimensions = getPageDimensionsForFormat(texture._target, texture._internalFormat); + // In order to enable sparse the texture size must be an integer multiple of the page size + for (size_t i = 0; i < allowedPageDimensions.size(); ++i) { + pageDimensionsIndex = (uint32_t) i; + pageDimensions = allowedPageDimensions[i]; + // Is this texture an integer multiple of page dimensions? + if (uvec3(0) == (dimensions % pageDimensions)) { + qCDebug(gpugl45logging) << "Enabling sparse for texture " << texture._source.c_str(); + sparse = true; + break; + } + } + + if (sparse) { + glTextureParameteri(texture._id, GL_TEXTURE_SPARSE_ARB, GL_TRUE); + glTextureParameteri(texture._id, GL_VIRTUAL_PAGE_SIZE_INDEX_ARB, pageDimensionsIndex); + } else { + qCDebug(gpugl45logging) << "Size " << dimensions.x << " x " << dimensions.y << + " is not supported by any sparse page size for texture" << texture._source.c_str(); + } +} + +#define SPARSE_PAGE_SIZE_OVERHEAD_ESTIMATE 1.3f + +// This can only be called after we've established our storage size +void SparseInfo::update() { + if (!sparse) { + return; + } + glGetTextureParameterIuiv(texture._id, GL_NUM_SPARSE_LEVELS_ARB, &maxSparseLevel); + pageBytes = texture._gpuObject.getTexelFormat().getSize(); + pageBytes *= pageDimensions.x * pageDimensions.y * pageDimensions.z; + // Testing with a simple texture allocating app shows an estimated 20% GPU memory overhead for + // sparse textures as compared to non-sparse, so we acount for that here. + pageBytes = (uint32_t)(pageBytes * SPARSE_PAGE_SIZE_OVERHEAD_ESTIMATE); + + for (uint16_t mipLevel = 0; mipLevel <= maxSparseLevel; ++mipLevel) { + auto mipDimensions = texture._gpuObject.evalMipDimensions(mipLevel); + auto mipPageCount = getPageCount(mipDimensions); + maxPages += mipPageCount; + } + if (texture._target == GL_TEXTURE_CUBE_MAP) { + maxPages *= GLTexture::CUBE_NUM_FACES; + } +} + +uvec3 SparseInfo::getPageCounts(const uvec3& dimensions) const { + auto result = (dimensions / pageDimensions) + + glm::clamp(dimensions % pageDimensions, glm::uvec3(0), glm::uvec3(1)); + return result; +} + +uint32_t SparseInfo::getPageCount(const uvec3& dimensions) const { + auto pageCounts = getPageCounts(dimensions); + return pageCounts.x * pageCounts.y * pageCounts.z; +} + + +uint32_t SparseInfo::getSize() const { + return allocatedPages * pageBytes; } void GL45Backend::initTextureManagementStage() { @@ -100,12 +171,6 @@ void GL45Backend::initTextureManagementStage() { } } -using GL45Texture = GL45Backend::GL45Texture; - -GL45Texture::GL45Texture(const std::weak_ptr& backend, const Texture& texture) - : GLTexture(backend, texture, allocate(texture)) { - incrementTextureGPUCount(); -} GLuint GL45Texture::allocate(const Texture& texture) { GLuint result; @@ -113,43 +178,164 @@ GLuint GL45Texture::allocate(const Texture& texture) { return result; } +GLuint GL45Backend::getTextureID(const TexturePointer& texture, bool transfer) { + return GL45Texture::getId(*this, texture, transfer); +} + +GL45Texture::GL45Texture(const std::weak_ptr& backend, const Texture& texture, GLuint externalId) + : GLTexture(backend, texture, externalId), _sparseInfo(*this) +{ +} + +GL45Texture::GL45Texture(const std::weak_ptr& backend, const Texture& texture, bool transferrable) + : GLTexture(backend, texture, allocate(texture), transferrable), _sparseInfo(*this) + { + + auto theBackend = _backend.lock(); + if (_transferrable && theBackend && theBackend->isTextureManagementSparseEnabled()) { + _sparseInfo.maybeMakeSparse(); + if (_sparseInfo.sparse) { + Backend::incrementTextureGPUSparseCount(); + } + } +} + +GL45Texture::~GL45Texture() { + // Remove this texture from the candidate list of derezzable textures + if (_transferrable) { + auto mipLevels = usedMipLevels(); + Lock lock(texturesByMipCountsMutex); + if (texturesByMipCounts.count(mipLevels)) { + auto& textures = texturesByMipCounts[mipLevels]; + textures.erase(this); + if (textures.empty()) { + texturesByMipCounts.erase(mipLevels); + } + } + } + + if (_sparseInfo.sparse) { + Backend::decrementTextureGPUSparseCount(); + + // Experimenation suggests that allocating sparse textures on one context/thread and deallocating + // them on another is buggy. So for sparse textures we need to queue a lambda with the deallocation + // callls to the transfer thread + auto id = _id; + // Set the class _id to 0 so we don't try to double delete + const_cast(_id) = 0; + std::list> destructionFunctions; + + uint8_t maxFace = (uint8_t)((_target == GL_TEXTURE_CUBE_MAP) ? GLTexture::CUBE_NUM_FACES : 1); + auto maxSparseMip = std::min(_maxMip, _sparseInfo.maxSparseLevel); + for (uint16_t mipLevel = _minMip; mipLevel <= maxSparseMip; ++mipLevel) { + auto mipDimensions = _gpuObject.evalMipDimensions(mipLevel); + destructionFunctions.push_back([id, maxFace, mipLevel, mipDimensions] { + glTexturePageCommitmentEXT(id, mipLevel, 0, 0, 0, mipDimensions.x, mipDimensions.y, maxFace, GL_FALSE); + }); + + auto deallocatedPages = _sparseInfo.getPageCount(mipDimensions) * maxFace; + assert(deallocatedPages <= _sparseInfo.allocatedPages); + _sparseInfo.allocatedPages -= deallocatedPages; + } + + if (0 != _sparseInfo.allocatedPages) { + qCWarning(gpugl45logging) << "Allocated pages remaining " << _id << " " << _sparseInfo.allocatedPages; + } + + auto size = _size; + const_cast(_size) = 0; + _textureTransferHelper->queueExecution([id, size, destructionFunctions] { + for (auto function : destructionFunctions) { + function(); + } + glDeleteTextures(1, &id); + Backend::decrementTextureGPUCount(); + Backend::updateTextureGPUMemoryUsage(size, 0); + Backend::updateTextureGPUSparseMemoryUsage(size, 0); + }); + } +} + +void GL45Texture::withPreservedTexture(std::function f) const { + f(); +} + void GL45Texture::generateMips() const { glGenerateTextureMipmap(_id); (void)CHECK_GL_ERROR(); } -void GL45Texture::copyMipFaceLinesFromTexture(uint16_t mip, uint8_t face, const uvec3& size, uint32_t yOffset, GLenum format, GLenum type, const void* sourcePointer) const { - if (GL_TEXTURE_2D == _target) { - glTextureSubImage2D(_id, mip, 0, yOffset, size.x, size.y, format, type, sourcePointer); - } else if (GL_TEXTURE_CUBE_MAP == _target) { - // DSA ARB does not work on AMD, so use EXT - // unless EXT is not available on the driver - if (glTextureSubImage2DEXT) { - auto target = GLTexture::CUBE_FACE_LAYOUT[face]; - glTextureSubImage2DEXT(_id, target, mip, 0, yOffset, size.x, size.y, format, type, sourcePointer); - } else { - glTextureSubImage3D(_id, mip, 0, yOffset, face, size.x, size.y, 1, format, type, sourcePointer); - } - } else { - Q_ASSERT(false); +void GL45Texture::allocateStorage() const { + if (_gpuObject.getTexelFormat().isCompressed()) { + qFatal("Compressed textures not yet supported"); } + glTextureParameteri(_id, GL_TEXTURE_BASE_LEVEL, 0); + glTextureParameteri(_id, GL_TEXTURE_MAX_LEVEL, _maxMip - _minMip); + // Get the dimensions, accounting for the downgrade level + Vec3u dimensions = _gpuObject.evalMipDimensions(_minMip + _mipOffset); + glTextureStorage2D(_id, usedMipLevels(), _internalFormat, dimensions.x, dimensions.y); (void)CHECK_GL_ERROR(); } -void GL45Texture::copyMipFaceFromTexture(uint16_t sourceMip, uint16_t targetMip, uint8_t face) const { - if (!_gpuObject.isStoredMipFaceAvailable(sourceMip)) { - return; +void GL45Texture::updateSize() const { + if (_gpuObject.getTexelFormat().isCompressed()) { + qFatal("Compressed textures not yet supported"); } - auto size = _gpuObject.evalMipDimensions(sourceMip); - auto mipData = _gpuObject.accessStoredMipFace(sourceMip, face); - if (mipData) { - GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat(), _gpuObject.getStoredMipFormat()); - copyMipFaceLinesFromTexture(targetMip, face, size, 0, texelFormat.format, texelFormat.type, mipData->readData()); + + if (_transferrable && _sparseInfo.sparse) { + auto size = _sparseInfo.getSize(); + Backend::updateTextureGPUSparseMemoryUsage(_size, size); + setSize(size); } else { - qCDebug(gpugllogging) << "Missing mipData level=" << sourceMip << " face=" << (int)face << " for texture " << _gpuObject.source().c_str(); + setSize(_gpuObject.evalTotalSize(_mipOffset)); } } +void GL45Texture::startTransfer() { + Parent::startTransfer(); + _sparseInfo.update(); +} + +bool GL45Texture::continueTransfer() { + PROFILE_RANGE(render_gpu_gl, "continueTransfer") + size_t maxFace = GL_TEXTURE_CUBE_MAP == _target ? CUBE_NUM_FACES : 1; + for (uint8_t face = 0; face < maxFace; ++face) { + for (uint16_t mipLevel = _minMip; mipLevel <= _maxMip; ++mipLevel) { + auto size = _gpuObject.evalMipDimensions(mipLevel); + if (_sparseInfo.sparse && mipLevel <= _sparseInfo.maxSparseLevel) { + glTexturePageCommitmentEXT(_id, mipLevel, 0, 0, face, size.x, size.y, 1, GL_TRUE); + _sparseInfo.allocatedPages += _sparseInfo.getPageCount(size); + } + if (_gpuObject.isStoredMipFaceAvailable(mipLevel, face)) { + PROFILE_RANGE_EX(render_gpu_gl, "texSubImage", 0x0000ffff, (size.x * size.y * maxFace / 1024)); + + auto mip = _gpuObject.accessStoredMipFace(mipLevel, face); + GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat(), mip->getFormat()); + if (GL_TEXTURE_2D == _target) { + glTextureSubImage2D(_id, mipLevel, 0, 0, size.x, size.y, texelFormat.format, texelFormat.type, mip->readData()); + } else if (GL_TEXTURE_CUBE_MAP == _target) { + // DSA ARB does not work on AMD, so use EXT + // unless EXT is not available on the driver + if (glTextureSubImage2DEXT) { + auto target = CUBE_FACE_LAYOUT[face]; + glTextureSubImage2DEXT(_id, target, mipLevel, 0, 0, size.x, size.y, texelFormat.format, texelFormat.type, mip->readData()); + } else { + glTextureSubImage3D(_id, mipLevel, 0, 0, face, size.x, size.y, 1, texelFormat.format, texelFormat.type, mip->readData()); + } + } else { + Q_ASSERT(false); + } + (void)CHECK_GL_ERROR(); + } + } + } + return false; +} + +void GL45Texture::finishTransfer() { + Parent::finishTransfer(); +} + void GL45Texture::syncSampler() const { const Sampler& sampler = _gpuObject.getSampler(); @@ -167,63 +353,163 @@ void GL45Texture::syncSampler() const { glTextureParameteri(_id, GL_TEXTURE_WRAP_S, WRAP_MODES[sampler.getWrapModeU()]); glTextureParameteri(_id, GL_TEXTURE_WRAP_T, WRAP_MODES[sampler.getWrapModeV()]); glTextureParameteri(_id, GL_TEXTURE_WRAP_R, WRAP_MODES[sampler.getWrapModeW()]); - glTextureParameterf(_id, GL_TEXTURE_MAX_ANISOTROPY_EXT, sampler.getMaxAnisotropy()); glTextureParameterfv(_id, GL_TEXTURE_BORDER_COLOR, (const float*)&sampler.getBorderColor()); - glTextureParameterf(_id, GL_TEXTURE_MIN_LOD, sampler.getMinMip()); - glTextureParameterf(_id, GL_TEXTURE_MAX_LOD, (sampler.getMaxMip() == Sampler::MAX_MIP_LEVEL ? 1000.f : sampler.getMaxMip())); -} - -using GL45FixedAllocationTexture = GL45Backend::GL45FixedAllocationTexture; - -GL45FixedAllocationTexture::GL45FixedAllocationTexture(const std::weak_ptr& backend, const Texture& texture) : GL45Texture(backend, texture), _size(texture.evalTotalSize()) { - allocateStorage(); - syncSampler(); -} - -GL45FixedAllocationTexture::~GL45FixedAllocationTexture() { -} - -void GL45FixedAllocationTexture::allocateStorage() const { - const GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat()); - const auto dimensions = _gpuObject.getDimensions(); - const auto mips = _gpuObject.evalNumMips(); - glTextureStorage2D(_id, mips, texelFormat.internalFormat, dimensions.x, dimensions.y); -} - -void GL45FixedAllocationTexture::syncSampler() const { - Parent::syncSampler(); - const Sampler& sampler = _gpuObject.getSampler(); - auto baseMip = std::max(sampler.getMipOffset(), sampler.getMinMip()); + // FIXME account for mip offsets here + auto baseMip = std::max(sampler.getMipOffset(), _minMip); glTextureParameteri(_id, GL_TEXTURE_BASE_LEVEL, baseMip); glTextureParameterf(_id, GL_TEXTURE_MIN_LOD, (float)sampler.getMinMip()); - glTextureParameterf(_id, GL_TEXTURE_MAX_LOD, (sampler.getMaxMip() == Sampler::MAX_MIP_LEVEL ? 1000.f : sampler.getMaxMip())); + glTextureParameterf(_id, GL_TEXTURE_MAX_LOD, (sampler.getMaxMip() == Sampler::MAX_MIP_LEVEL ? 1000.f : sampler.getMaxMip() - _mipOffset)); + glTextureParameterf(_id, GL_TEXTURE_MAX_ANISOTROPY_EXT, sampler.getMaxAnisotropy()); } -// Renderbuffer attachment textures -using GL45AttachmentTexture = GL45Backend::GL45AttachmentTexture; - -GL45AttachmentTexture::GL45AttachmentTexture(const std::weak_ptr& backend, const Texture& texture) : GL45FixedAllocationTexture(backend, texture) { - Backend::updateTextureGPUFramebufferMemoryUsage(0, size()); +void GL45Texture::postTransfer() { + Parent::postTransfer(); + auto mipLevels = usedMipLevels(); + if (_transferrable && mipLevels > 1 && _minMip < _sparseInfo.maxSparseLevel) { + Lock lock(texturesByMipCountsMutex); + texturesByMipCounts[mipLevels].insert(this); + } } -GL45AttachmentTexture::~GL45AttachmentTexture() { - Backend::updateTextureGPUFramebufferMemoryUsage(size(), 0); -} +void GL45Texture::stripToMip(uint16_t newMinMip) { + if (newMinMip < _minMip) { + qCWarning(gpugl45logging) << "Cannot decrease the min mip"; + return; + } -// Strict resource textures -using GL45StrictResourceTexture = GL45Backend::GL45StrictResourceTexture; + if (_sparseInfo.sparse && newMinMip > _sparseInfo.maxSparseLevel) { + qCWarning(gpugl45logging) << "Cannot increase the min mip into the mip tail"; + return; + } -GL45StrictResourceTexture::GL45StrictResourceTexture(const std::weak_ptr& backend, const Texture& texture) : GL45FixedAllocationTexture(backend, texture) { - auto mipLevels = _gpuObject.evalNumMips(); - for (uint16_t sourceMip = 0; sourceMip < mipLevels; ++sourceMip) { - uint16_t targetMip = sourceMip; - size_t maxFace = GLTexture::getFaceCount(_target); - for (uint8_t face = 0; face < maxFace; ++face) { - copyMipFaceFromTexture(sourceMip, targetMip, face); + PROFILE_RANGE(render_gpu_gl, "GL45Texture::stripToMip"); + + auto mipLevels = usedMipLevels(); + { + Lock lock(texturesByMipCountsMutex); + assert(0 != texturesByMipCounts.count(mipLevels)); + assert(0 != texturesByMipCounts[mipLevels].count(this)); + texturesByMipCounts[mipLevels].erase(this); + if (texturesByMipCounts[mipLevels].empty()) { + texturesByMipCounts.erase(mipLevels); } } - if (texture.isAutogenerateMips()) { - generateMips(); + + // If we weren't generating mips before, we need to now that we're stripping down mip levels. + if (!_gpuObject.isAutogenerateMips()) { + qCDebug(gpugl45logging) << "Force mip generation for texture"; + glGenerateTextureMipmap(_id); + } + + + uint8_t maxFace = (uint8_t)((_target == GL_TEXTURE_CUBE_MAP) ? GLTexture::CUBE_NUM_FACES : 1); + if (_sparseInfo.sparse) { + for (uint16_t mip = _minMip; mip < newMinMip; ++mip) { + auto id = _id; + auto mipDimensions = _gpuObject.evalMipDimensions(mip); + _textureTransferHelper->queueExecution([id, mip, mipDimensions, maxFace] { + glTexturePageCommitmentEXT(id, mip, 0, 0, 0, mipDimensions.x, mipDimensions.y, maxFace, GL_FALSE); + }); + + auto deallocatedPages = _sparseInfo.getPageCount(mipDimensions) * maxFace; + assert(deallocatedPages < _sparseInfo.allocatedPages); + _sparseInfo.allocatedPages -= deallocatedPages; + } + _minMip = newMinMip; + } else { + GLuint oldId = _id; + // Find the distance between the old min mip and the new one + uint16 mipDelta = newMinMip - _minMip; + _mipOffset += mipDelta; + const_cast(_maxMip) -= mipDelta; + auto newLevels = usedMipLevels(); + + // Create and setup the new texture (allocate) + { + Vec3u newDimensions = _gpuObject.evalMipDimensions(_mipOffset); + PROFILE_RANGE_EX(render_gpu_gl, "Re-Allocate", 0xff0000ff, (newDimensions.x * newDimensions.y)); + + glCreateTextures(_target, 1, &const_cast(_id)); + glTextureParameteri(_id, GL_TEXTURE_BASE_LEVEL, 0); + glTextureParameteri(_id, GL_TEXTURE_MAX_LEVEL, _maxMip - _minMip); + glTextureStorage2D(_id, newLevels, _internalFormat, newDimensions.x, newDimensions.y); + } + + // Copy the contents of the old texture to the new + { + PROFILE_RANGE(render_gpu_gl, "Blit"); + // Preferred path only available in 4.3 + for (uint16 targetMip = _minMip; targetMip <= _maxMip; ++targetMip) { + uint16 sourceMip = targetMip + mipDelta; + Vec3u mipDimensions = _gpuObject.evalMipDimensions(targetMip + _mipOffset); + for (GLenum target : getFaceTargets(_target)) { + glCopyImageSubData( + oldId, target, sourceMip, 0, 0, 0, + _id, target, targetMip, 0, 0, 0, + mipDimensions.x, mipDimensions.y, 1 + ); + (void)CHECK_GL_ERROR(); + } + } + + glDeleteTextures(1, &oldId); + } + } + + // Re-sync the sampler to force access to the new mip level + syncSampler(); + updateSize(); + + // Re-insert into the texture-by-mips map if appropriate + mipLevels = usedMipLevels(); + if (mipLevels > 1 && (!_sparseInfo.sparse || _minMip < _sparseInfo.maxSparseLevel)) { + Lock lock(texturesByMipCountsMutex); + texturesByMipCounts[mipLevels].insert(this); } } +void GL45Texture::updateMips() { + if (!_sparseInfo.sparse) { + return; + } + auto newMinMip = std::min(_gpuObject.minMip(), _sparseInfo.maxSparseLevel); + if (_minMip < newMinMip) { + stripToMip(newMinMip); + } +} + +void GL45Texture::derez() { + if (_sparseInfo.sparse) { + assert(_minMip < _sparseInfo.maxSparseLevel); + } + assert(_minMip < _maxMip); + assert(_transferrable); + stripToMip(_minMip + 1); +} + +void GL45Backend::derezTextures() const { + if (GLTexture::getMemoryPressure() < 1.0f) { + return; + } + + Lock lock(texturesByMipCountsMutex); + if (texturesByMipCounts.empty()) { + // No available textures to derez + return; + } + + auto mipLevel = texturesByMipCounts.rbegin()->first; + if (mipLevel <= 1) { + // No mips available to remove + return; + } + + GL45Texture* targetTexture = nullptr; + { + auto& textures = texturesByMipCounts[mipLevel]; + assert(!textures.empty()); + targetTexture = *textures.begin(); + } + lock.unlock(); + targetTexture->derez(); +} diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendVariableTexture.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendVariableTexture.cpp deleted file mode 100644 index d54ad1ea4b..0000000000 --- a/libraries/gpu-gl/src/gpu/gl45/GL45BackendVariableTexture.cpp +++ /dev/null @@ -1,1033 +0,0 @@ -// -// GL45BackendTexture.cpp -// libraries/gpu/src/gpu -// -// Created by Sam Gateau on 1/19/2015. -// Copyright 2014 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include "GL45Backend.h" -#include -#include -#include -#include -#include -#include - -#include -#include - -#include -#include "../gl/GLTexelFormat.h" - -using namespace gpu; -using namespace gpu::gl; -using namespace gpu::gl45; - -// Variable sized textures -using GL45VariableAllocationTexture = GL45Backend::GL45VariableAllocationTexture; -using MemoryPressureState = GL45VariableAllocationTexture::MemoryPressureState; -using WorkQueue = GL45VariableAllocationTexture::WorkQueue; - -std::list GL45VariableAllocationTexture::_memoryManagedTextures; -MemoryPressureState GL45VariableAllocationTexture::_memoryPressureState = MemoryPressureState::Idle; -std::atomic GL45VariableAllocationTexture::_memoryPressureStateStale { false }; -const uvec3 GL45VariableAllocationTexture::INITIAL_MIP_TRANSFER_DIMENSIONS { 64, 64, 1 }; -WorkQueue GL45VariableAllocationTexture::_transferQueue; -WorkQueue GL45VariableAllocationTexture::_promoteQueue; -WorkQueue GL45VariableAllocationTexture::_demoteQueue; -TexturePointer GL45VariableAllocationTexture::_currentTransferTexture; - -#define OVERSUBSCRIBED_PRESSURE_VALUE 0.95f -#define UNDERSUBSCRIBED_PRESSURE_VALUE 0.85f -#define DEFAULT_ALLOWED_TEXTURE_MEMORY_MB ((size_t)1024) - -static const size_t DEFAULT_ALLOWED_TEXTURE_MEMORY = MB_TO_BYTES(DEFAULT_ALLOWED_TEXTURE_MEMORY_MB); - -using TransferJob = GL45VariableAllocationTexture::TransferJob; - -static const uvec3 MAX_TRANSFER_DIMENSIONS { 1024, 1024, 1 }; -static const size_t MAX_TRANSFER_SIZE = MAX_TRANSFER_DIMENSIONS.x * MAX_TRANSFER_DIMENSIONS.y * 4; - -#if THREADED_TEXTURE_BUFFERING -std::shared_ptr TransferJob::_bufferThread { nullptr }; -std::atomic TransferJob::_shutdownBufferingThread { false }; -Mutex TransferJob::_mutex; -TransferJob::VoidLambdaQueue TransferJob::_bufferLambdaQueue; - -void TransferJob::startTransferLoop() { - if (_bufferThread) { - return; - } - _shutdownBufferingThread = false; - _bufferThread = std::make_shared([] { - TransferJob::bufferLoop(); - }); -} - -void TransferJob::stopTransferLoop() { - if (!_bufferThread) { - return; - } - _shutdownBufferingThread = true; - _bufferThread->join(); - _bufferThread.reset(); - _shutdownBufferingThread = false; -} -#endif - -TransferJob::TransferJob(const GL45VariableAllocationTexture& parent, uint16_t sourceMip, uint16_t targetMip, uint8_t face, uint32_t lines, uint32_t lineOffset) - : _parent(parent) { - - auto transferDimensions = _parent._gpuObject.evalMipDimensions(sourceMip); - GLenum format; - GLenum type; - auto mipData = _parent._gpuObject.accessStoredMipFace(sourceMip, face); - GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_parent._gpuObject.getTexelFormat(), _parent._gpuObject.getStoredMipFormat()); - format = texelFormat.format; - type = texelFormat.type; - - if (0 == lines) { - _transferSize = mipData->getSize(); - _bufferingLambda = [=] { - _buffer.resize(_transferSize); - memcpy(&_buffer[0], mipData->readData(), _transferSize); - _bufferingCompleted = true; - }; - - } else { - transferDimensions.y = lines; - auto dimensions = _parent._gpuObject.evalMipDimensions(sourceMip); - auto mipSize = mipData->getSize(); - auto bytesPerLine = (uint32_t)mipSize / dimensions.y; - _transferSize = bytesPerLine * lines; - auto sourceOffset = bytesPerLine * lineOffset; - _bufferingLambda = [=] { - _buffer.resize(_transferSize); - memcpy(&_buffer[0], mipData->readData() + sourceOffset, _transferSize); - _bufferingCompleted = true; - }; - } - - Backend::updateTextureTransferPendingSize(0, _transferSize); - - _transferLambda = [=] { - _parent.copyMipFaceLinesFromTexture(targetMip, face, transferDimensions, lineOffset, format, type, _buffer.data()); - std::vector emptyVector; - _buffer.swap(emptyVector); - }; -} - -TransferJob::TransferJob(const GL45VariableAllocationTexture& parent, std::function transferLambda) - : _parent(parent), _bufferingCompleted(true), _transferLambda(transferLambda) { -} - -TransferJob::~TransferJob() { - Backend::updateTextureTransferPendingSize(_transferSize, 0); -} - - -bool TransferJob::tryTransfer() { - // Disable threaded texture transfer for now -#if THREADED_TEXTURE_BUFFERING - // Are we ready to transfer - if (_bufferingCompleted) { - _transferLambda(); - return true; - } - - startBuffering(); - return false; -#else - if (!_bufferingCompleted) { - _bufferingLambda(); - _bufferingCompleted = true; - } - _transferLambda(); - return true; -#endif -} - -#if THREADED_TEXTURE_BUFFERING - -void TransferJob::startBuffering() { - if (_bufferingStarted) { - return; - } - _bufferingStarted = true; - { - Lock lock(_mutex); - _bufferLambdaQueue.push(_bufferingLambda); - } -} - -void TransferJob::bufferLoop() { - while (!_shutdownBufferingThread) { - VoidLambdaQueue workingQueue; - { - Lock lock(_mutex); - _bufferLambdaQueue.swap(workingQueue); - } - - if (workingQueue.empty()) { - QThread::msleep(5); - continue; - } - - while (!workingQueue.empty()) { - workingQueue.front()(); - workingQueue.pop(); - } - } -} -#endif - - -void GL45VariableAllocationTexture::addMemoryManagedTexture(const TexturePointer& texturePointer) { - _memoryManagedTextures.push_back(texturePointer); - addToWorkQueue(texturePointer); -} - -void GL45VariableAllocationTexture::addToWorkQueue(const TexturePointer& texturePointer) { - GL45VariableAllocationTexture* object = Backend::getGPUObject(*texturePointer); - switch (_memoryPressureState) { - case MemoryPressureState::Oversubscribed: - if (object->canDemote()) { - // Demote largest first - _demoteQueue.push({ texturePointer, (float)object->size() }); - } - break; - - case MemoryPressureState::Undersubscribed: - if (object->canPromote()) { - // Promote smallest first - _promoteQueue.push({ texturePointer, 1.0f / (float)object->size() }); - } - break; - - case MemoryPressureState::Transfer: - if (object->hasPendingTransfers()) { - // Transfer priority given to smaller mips first - _transferQueue.push({ texturePointer, 1.0f / (float)object->_gpuObject.evalMipSize(object->_populatedMip) }); - } - break; - - case MemoryPressureState::Idle: - break; - - default: - Q_UNREACHABLE(); - } -} - -WorkQueue& GL45VariableAllocationTexture::getActiveWorkQueue() { - static WorkQueue empty; - switch (_memoryPressureState) { - case MemoryPressureState::Oversubscribed: - return _demoteQueue; - - case MemoryPressureState::Undersubscribed: - return _promoteQueue; - - case MemoryPressureState::Transfer: - return _transferQueue; - - default: - break; - } - Q_UNREACHABLE(); - return empty; -} - -// FIXME hack for stats display -QString getTextureMemoryPressureModeString() { - switch (GL45VariableAllocationTexture::_memoryPressureState) { - case MemoryPressureState::Oversubscribed: - return "Oversubscribed"; - - case MemoryPressureState::Undersubscribed: - return "Undersubscribed"; - - case MemoryPressureState::Transfer: - return "Transfer"; - - case MemoryPressureState::Idle: - return "Idle"; - } - Q_UNREACHABLE(); - return "Unknown"; -} - -void GL45VariableAllocationTexture::updateMemoryPressure() { - static size_t lastAllowedMemoryAllocation = gpu::Texture::getAllowedGPUMemoryUsage(); - - size_t allowedMemoryAllocation = gpu::Texture::getAllowedGPUMemoryUsage(); - if (0 == allowedMemoryAllocation) { - allowedMemoryAllocation = DEFAULT_ALLOWED_TEXTURE_MEMORY; - } - - // If the user explicitly changed the allowed memory usage, we need to mark ourselves stale - // so that we react - if (allowedMemoryAllocation != lastAllowedMemoryAllocation) { - _memoryPressureStateStale = true; - lastAllowedMemoryAllocation = allowedMemoryAllocation; - } - - if (!_memoryPressureStateStale.exchange(false)) { - return; - } - - PROFILE_RANGE(render_gpu_gl, __FUNCTION__); - - // Clear any defunct textures (weak pointers that no longer have a valid texture) - _memoryManagedTextures.remove_if([&](const TextureWeakPointer& weakPointer) { - return weakPointer.expired(); - }); - - // Convert weak pointers to strong. This new list may still contain nulls if a texture was - // deleted on another thread between the previous line and this one - std::vector strongTextures; { - strongTextures.reserve(_memoryManagedTextures.size()); - std::transform( - _memoryManagedTextures.begin(), _memoryManagedTextures.end(), - std::back_inserter(strongTextures), - [](const TextureWeakPointer& p) { return p.lock(); }); - } - - size_t totalVariableMemoryAllocation = 0; - size_t idealMemoryAllocation = 0; - bool canDemote = false; - bool canPromote = false; - bool hasTransfers = false; - for (const auto& texture : strongTextures) { - // Race conditions can still leave nulls in the list, so we need to check - if (!texture) { - continue; - } - GL45VariableAllocationTexture* object = Backend::getGPUObject(*texture); - // Track how much the texture thinks it should be using - idealMemoryAllocation += texture->evalTotalSize(); - // Track how much we're actually using - totalVariableMemoryAllocation += object->size(); - canDemote |= object->canDemote(); - canPromote |= object->canPromote(); - hasTransfers |= object->hasPendingTransfers(); - } - - size_t unallocated = idealMemoryAllocation - totalVariableMemoryAllocation; - float pressure = (float)totalVariableMemoryAllocation / (float)allowedMemoryAllocation; - - auto newState = MemoryPressureState::Idle; - if (pressure > OVERSUBSCRIBED_PRESSURE_VALUE && canDemote) { - newState = MemoryPressureState::Oversubscribed; - } else if (pressure < UNDERSUBSCRIBED_PRESSURE_VALUE && unallocated != 0 && canPromote) { - newState = MemoryPressureState::Undersubscribed; - } else if (hasTransfers) { - newState = MemoryPressureState::Transfer; - } - - if (newState != _memoryPressureState) { -#if THREADED_TEXTURE_BUFFERING - if (MemoryPressureState::Transfer == _memoryPressureState) { - TransferJob::stopTransferLoop(); - } - _memoryPressureState = newState; - if (MemoryPressureState::Transfer == _memoryPressureState) { - TransferJob::startTransferLoop(); - } -#else - _memoryPressureState = newState; -#endif - // Clear the existing queue - _transferQueue = WorkQueue(); - _promoteQueue = WorkQueue(); - _demoteQueue = WorkQueue(); - - // Populate the existing textures into the queue - for (const auto& texture : strongTextures) { - addToWorkQueue(texture); - } - } -} - -void GL45VariableAllocationTexture::processWorkQueues() { - if (MemoryPressureState::Idle == _memoryPressureState) { - return; - } - - auto& workQueue = getActiveWorkQueue(); - PROFILE_RANGE(render_gpu_gl, __FUNCTION__); - while (!workQueue.empty()) { - auto workTarget = workQueue.top(); - workQueue.pop(); - auto texture = workTarget.first.lock(); - if (!texture) { - continue; - } - - // Grab the first item off the demote queue - GL45VariableAllocationTexture* object = Backend::getGPUObject(*texture); - if (MemoryPressureState::Oversubscribed == _memoryPressureState) { - if (!object->canDemote()) { - continue; - } - object->demote(); - } else if (MemoryPressureState::Undersubscribed == _memoryPressureState) { - if (!object->canPromote()) { - continue; - } - object->promote(); - } else if (MemoryPressureState::Transfer == _memoryPressureState) { - if (!object->hasPendingTransfers()) { - continue; - } - object->executeNextTransfer(texture); - } else { - Q_UNREACHABLE(); - } - - // Reinject into the queue if more work to be done - addToWorkQueue(texture); - break; - } - - if (workQueue.empty()) { - _memoryPressureStateStale = true; - } -} - -void GL45VariableAllocationTexture::manageMemory() { - PROFILE_RANGE(render_gpu_gl, __FUNCTION__); - updateMemoryPressure(); - processWorkQueues(); -} - -size_t GL45VariableAllocationTexture::_frameTexturesCreated { 0 }; - -GL45VariableAllocationTexture::GL45VariableAllocationTexture(const std::weak_ptr& backend, const Texture& texture) : GL45Texture(backend, texture) { - ++_frameTexturesCreated; -} - -GL45VariableAllocationTexture::~GL45VariableAllocationTexture() { - _memoryPressureStateStale = true; - Backend::updateTextureGPUMemoryUsage(_size, 0); -} - -void GL45VariableAllocationTexture::executeNextTransfer(const TexturePointer& currentTexture) { - if (_populatedMip <= _allocatedMip) { - return; - } - - if (_pendingTransfers.empty()) { - populateTransferQueue(); - } - - if (!_pendingTransfers.empty()) { - // Keeping hold of a strong pointer during the transfer ensures that the transfer thread cannot try to access a destroyed texture - _currentTransferTexture = currentTexture; - if (_pendingTransfers.front()->tryTransfer()) { - _pendingTransfers.pop(); - _currentTransferTexture.reset(); - } - } -} - -// Managed size resource textures -using GL45ResourceTexture = GL45Backend::GL45ResourceTexture; - -GL45ResourceTexture::GL45ResourceTexture(const std::weak_ptr& backend, const Texture& texture) : GL45VariableAllocationTexture(backend, texture) { - auto mipLevels = texture.evalNumMips(); - _allocatedMip = mipLevels; - uvec3 mipDimensions; - for (uint16_t mip = 0; mip < mipLevels; ++mip) { - if (glm::all(glm::lessThanEqual(texture.evalMipDimensions(mip), INITIAL_MIP_TRANSFER_DIMENSIONS))) { - _maxAllocatedMip = _populatedMip = mip; - break; - } - } - - uint16_t allocatedMip = _populatedMip - std::min(_populatedMip, 2); - allocateStorage(allocatedMip); - _memoryPressureStateStale = true; - copyMipsFromTexture(); - syncSampler(); - -} - -void GL45ResourceTexture::allocateStorage(uint16 allocatedMip) { - _allocatedMip = allocatedMip; - const GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat()); - const auto dimensions = _gpuObject.evalMipDimensions(_allocatedMip); - const auto totalMips = _gpuObject.evalNumMips(); - const auto mips = totalMips - _allocatedMip; - glTextureStorage2D(_id, mips, texelFormat.internalFormat, dimensions.x, dimensions.y); - auto mipLevels = _gpuObject.evalNumMips(); - _size = 0; - for (uint16_t mip = _allocatedMip; mip < mipLevels; ++mip) { - _size += _gpuObject.evalMipSize(mip); - } - Backend::updateTextureGPUMemoryUsage(0, _size); - -} - -void GL45ResourceTexture::copyMipsFromTexture() { - auto mipLevels = _gpuObject.evalNumMips(); - size_t maxFace = GLTexture::getFaceCount(_target); - for (uint16_t sourceMip = _populatedMip; sourceMip < mipLevels; ++sourceMip) { - uint16_t targetMip = sourceMip - _allocatedMip; - for (uint8_t face = 0; face < maxFace; ++face) { - copyMipFaceFromTexture(sourceMip, targetMip, face); - } - } -} - -void GL45ResourceTexture::syncSampler() const { - Parent::syncSampler(); - glTextureParameteri(_id, GL_TEXTURE_BASE_LEVEL, _populatedMip - _allocatedMip); -} - -void GL45ResourceTexture::promote() { - PROFILE_RANGE(render_gpu_gl, __FUNCTION__); - Q_ASSERT(_allocatedMip > 0); - GLuint oldId = _id; - uint32_t oldSize = _size; - // create new texture - const_cast(_id) = allocate(_gpuObject); - uint16_t oldAllocatedMip = _allocatedMip; - // allocate storage for new level - allocateStorage(_allocatedMip - std::min(_allocatedMip, 2)); - uint16_t mips = _gpuObject.evalNumMips(); - // copy pre-existing mips - for (uint16_t mip = _populatedMip; mip < mips; ++mip) { - auto mipDimensions = _gpuObject.evalMipDimensions(mip); - uint16_t targetMip = mip - _allocatedMip; - uint16_t sourceMip = mip - oldAllocatedMip; - auto faces = getFaceCount(_target); - for (uint8_t face = 0; face < faces; ++face) { - glCopyImageSubData( - oldId, _target, sourceMip, 0, 0, face, - _id, _target, targetMip, 0, 0, face, - mipDimensions.x, mipDimensions.y, 1 - ); - (void)CHECK_GL_ERROR(); - } - } - // destroy the old texture - glDeleteTextures(1, &oldId); - // update the memory usage - Backend::updateTextureGPUMemoryUsage(oldSize, 0); - _memoryPressureStateStale = true; - syncSampler(); - populateTransferQueue(); -} - -void GL45ResourceTexture::demote() { - PROFILE_RANGE(render_gpu_gl, __FUNCTION__); - Q_ASSERT(_allocatedMip < _maxAllocatedMip); - auto oldId = _id; - auto oldSize = _size; - const_cast(_id) = allocate(_gpuObject); - allocateStorage(_allocatedMip + 1); - _populatedMip = std::max(_populatedMip, _allocatedMip); - uint16_t mips = _gpuObject.evalNumMips(); - // copy pre-existing mips - for (uint16_t mip = _populatedMip; mip < mips; ++mip) { - auto mipDimensions = _gpuObject.evalMipDimensions(mip); - uint16_t targetMip = mip - _allocatedMip; - uint16_t sourceMip = targetMip + 1; - auto faces = getFaceCount(_target); - for (uint8_t face = 0; face < faces; ++face) { - glCopyImageSubData( - oldId, _target, sourceMip, 0, 0, face, - _id, _target, targetMip, 0, 0, face, - mipDimensions.x, mipDimensions.y, 1 - ); - (void)CHECK_GL_ERROR(); - } - } - // destroy the old texture - glDeleteTextures(1, &oldId); - // update the memory usage - Backend::updateTextureGPUMemoryUsage(oldSize, 0); - _memoryPressureStateStale = true; - syncSampler(); - populateTransferQueue(); -} - - -void GL45ResourceTexture::populateTransferQueue() { - PROFILE_RANGE(render_gpu_gl, __FUNCTION__); - if (_populatedMip <= _allocatedMip) { - return; - } - _pendingTransfers = TransferQueue(); - - const uint8_t maxFace = GLTexture::getFaceCount(_target); - uint16_t sourceMip = _populatedMip; - do { - --sourceMip; - auto targetMip = sourceMip - _allocatedMip; - auto mipDimensions = _gpuObject.evalMipDimensions(sourceMip); - for (uint8_t face = 0; face < maxFace; ++face) { - if (!_gpuObject.isStoredMipFaceAvailable(sourceMip, face)) { - continue; - } - - // If the mip is less than the max transfer size, then just do it in one transfer - if (glm::all(glm::lessThanEqual(mipDimensions, MAX_TRANSFER_DIMENSIONS))) { - // Can the mip be transferred in one go - _pendingTransfers.emplace(new TransferJob(*this, sourceMip, targetMip, face)); - continue; - } - - // break down the transfers into chunks so that no single transfer is - // consuming more than X bandwidth - auto mipData = _gpuObject.accessStoredMipFace(sourceMip, face); - const auto lines = mipDimensions.y; - auto bytesPerLine = (uint32_t)mipData->getSize() / lines; - Q_ASSERT(0 == (mipData->getSize() % lines)); - uint32_t linesPerTransfer = (uint32_t)(MAX_TRANSFER_SIZE / bytesPerLine); - uint32_t lineOffset = 0; - while (lineOffset < lines) { - uint32_t linesToCopy = std::min(lines - lineOffset, linesPerTransfer); - _pendingTransfers.emplace(new TransferJob(*this, sourceMip, targetMip, face, linesToCopy, lineOffset)); - lineOffset += linesToCopy; - } - } - - // queue up the sampler and populated mip change for after the transfer has completed - _pendingTransfers.emplace(new TransferJob(*this, [=] { - _populatedMip = sourceMip; - syncSampler(); - })); - } while (sourceMip != _allocatedMip); -} - -// Sparsely allocated, managed size resource textures -#if 0 -#define SPARSE_PAGE_SIZE_OVERHEAD_ESTIMATE 1.3f - -using GL45SparseResourceTexture = GL45Backend::GL45SparseResourceTexture; - -GL45Texture::PageDimensionsMap GL45Texture::pageDimensionsByFormat; -Mutex GL45Texture::pageDimensionsMutex; - -GL45Texture::PageDimensions GL45Texture::getPageDimensionsForFormat(const TextureTypeFormat& typeFormat) { - { - Lock lock(pageDimensionsMutex); - if (pageDimensionsByFormat.count(typeFormat)) { - return pageDimensionsByFormat[typeFormat]; - } - } - - GLint count = 0; - glGetInternalformativ(typeFormat.first, typeFormat.second, GL_NUM_VIRTUAL_PAGE_SIZES_ARB, 1, &count); - - std::vector result; - if (count > 0) { - std::vector x, y, z; - x.resize(count); - glGetInternalformativ(typeFormat.first, typeFormat.second, GL_VIRTUAL_PAGE_SIZE_X_ARB, 1, &x[0]); - y.resize(count); - glGetInternalformativ(typeFormat.first, typeFormat.second, GL_VIRTUAL_PAGE_SIZE_Y_ARB, 1, &y[0]); - z.resize(count); - glGetInternalformativ(typeFormat.first, typeFormat.second, GL_VIRTUAL_PAGE_SIZE_Z_ARB, 1, &z[0]); - - result.resize(count); - for (GLint i = 0; i < count; ++i) { - result[i] = uvec3(x[i], y[i], z[i]); - } - } - - { - Lock lock(pageDimensionsMutex); - if (0 == pageDimensionsByFormat.count(typeFormat)) { - pageDimensionsByFormat[typeFormat] = result; - } - } - - return result; -} - -GL45Texture::PageDimensions GL45Texture::getPageDimensionsForFormat(GLenum target, GLenum format) { - return getPageDimensionsForFormat({ target, format }); -} -bool GL45Texture::isSparseEligible(const Texture& texture) { - Q_ASSERT(TextureUsageType::RESOURCE == texture.getUsageType()); - - // Disabling sparse for the momemnt - return false; - - const auto allowedPageDimensions = getPageDimensionsForFormat(getGLTextureType(texture), - gl::GLTexelFormat::evalGLTexelFormatInternal(texture.getTexelFormat())); - const auto textureDimensions = texture.getDimensions(); - for (const auto& pageDimensions : allowedPageDimensions) { - if (uvec3(0) == (textureDimensions % pageDimensions)) { - return true; - } - } - - return false; -} - - -GL45SparseResourceTexture::GL45SparseResourceTexture(const std::weak_ptr& backend, const Texture& texture) : GL45VariableAllocationTexture(backend, texture) { - const GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat()); - const uvec3 dimensions = _gpuObject.getDimensions(); - auto allowedPageDimensions = getPageDimensionsForFormat(_target, texelFormat.internalFormat); - uint32_t pageDimensionsIndex = 0; - // In order to enable sparse the texture size must be an integer multiple of the page size - for (size_t i = 0; i < allowedPageDimensions.size(); ++i) { - pageDimensionsIndex = (uint32_t)i; - _pageDimensions = allowedPageDimensions[i]; - // Is this texture an integer multiple of page dimensions? - if (uvec3(0) == (dimensions % _pageDimensions)) { - qCDebug(gpugl45logging) << "Enabling sparse for texture " << _gpuObject.source().c_str(); - break; - } - } - glTextureParameteri(_id, GL_TEXTURE_SPARSE_ARB, GL_TRUE); - glTextureParameteri(_id, GL_VIRTUAL_PAGE_SIZE_INDEX_ARB, pageDimensionsIndex); - glGetTextureParameterIuiv(_id, GL_NUM_SPARSE_LEVELS_ARB, &_maxSparseLevel); - - _pageBytes = _gpuObject.getTexelFormat().getSize(); - _pageBytes *= _pageDimensions.x * _pageDimensions.y * _pageDimensions.z; - // Testing with a simple texture allocating app shows an estimated 20% GPU memory overhead for - // sparse textures as compared to non-sparse, so we acount for that here. - _pageBytes = (uint32_t)(_pageBytes * SPARSE_PAGE_SIZE_OVERHEAD_ESTIMATE); - - //allocateStorage(); - syncSampler(); -} - -GL45SparseResourceTexture::~GL45SparseResourceTexture() { - Backend::updateTextureGPUVirtualMemoryUsage(size(), 0); -} - -uvec3 GL45SparseResourceTexture::getPageCounts(const uvec3& dimensions) const { - auto result = (dimensions / _pageDimensions) + - glm::clamp(dimensions % _pageDimensions, glm::uvec3(0), glm::uvec3(1)); - return result; -} - -uint32_t GL45SparseResourceTexture::getPageCount(const uvec3& dimensions) const { - auto pageCounts = getPageCounts(dimensions); - return pageCounts.x * pageCounts.y * pageCounts.z; -} - -void GL45SparseResourceTexture::promote() { -} - -void GL45SparseResourceTexture::demote() { -} - -SparseInfo::SparseInfo(GL45Texture& texture) - : texture(texture) { -} - -void SparseInfo::maybeMakeSparse() { - // Don't enable sparse for objects with explicitly managed mip levels - if (!texture._gpuObject.isAutogenerateMips()) { - return; - } - - const uvec3 dimensions = texture._gpuObject.getDimensions(); - auto allowedPageDimensions = getPageDimensionsForFormat(texture._target, texture._internalFormat); - // In order to enable sparse the texture size must be an integer multiple of the page size - for (size_t i = 0; i < allowedPageDimensions.size(); ++i) { - pageDimensionsIndex = (uint32_t)i; - pageDimensions = allowedPageDimensions[i]; - // Is this texture an integer multiple of page dimensions? - if (uvec3(0) == (dimensions % pageDimensions)) { - qCDebug(gpugl45logging) << "Enabling sparse for texture " << texture._source.c_str(); - sparse = true; - break; - } - } - - if (sparse) { - glTextureParameteri(texture._id, GL_TEXTURE_SPARSE_ARB, GL_TRUE); - glTextureParameteri(texture._id, GL_VIRTUAL_PAGE_SIZE_INDEX_ARB, pageDimensionsIndex); - } else { - qCDebug(gpugl45logging) << "Size " << dimensions.x << " x " << dimensions.y << - " is not supported by any sparse page size for texture" << texture._source.c_str(); - } -} - - -// This can only be called after we've established our storage size -void SparseInfo::update() { - if (!sparse) { - return; - } - glGetTextureParameterIuiv(texture._id, GL_NUM_SPARSE_LEVELS_ARB, &maxSparseLevel); - - for (uint16_t mipLevel = 0; mipLevel <= maxSparseLevel; ++mipLevel) { - auto mipDimensions = texture._gpuObject.evalMipDimensions(mipLevel); - auto mipPageCount = getPageCount(mipDimensions); - maxPages += mipPageCount; - } - if (texture._target == GL_TEXTURE_CUBE_MAP) { - maxPages *= GLTexture::CUBE_NUM_FACES; - } -} - - -void SparseInfo::allocateToMip(uint16_t targetMip) { - // Not sparse, do nothing - if (!sparse) { - return; - } - - if (allocatedMip == INVALID_MIP) { - allocatedMip = maxSparseLevel + 1; - } - - // Don't try to allocate below the maximum sparse level - if (targetMip > maxSparseLevel) { - targetMip = maxSparseLevel; - } - - // Already allocated this level - if (allocatedMip <= targetMip) { - return; - } - - uint32_t maxFace = (uint32_t)(GL_TEXTURE_CUBE_MAP == texture._target ? CUBE_NUM_FACES : 1); - for (uint16_t mip = targetMip; mip < allocatedMip; ++mip) { - auto size = texture._gpuObject.evalMipDimensions(mip); - glTexturePageCommitmentEXT(texture._id, mip, 0, 0, 0, size.x, size.y, maxFace, GL_TRUE); - allocatedPages += getPageCount(size); - } - allocatedMip = targetMip; -} - -uint32_t SparseInfo::getSize() const { - return allocatedPages * pageBytes; -} -using SparseInfo = GL45Backend::GL45Texture::SparseInfo; - -void GL45Texture::updateSize() const { - if (_gpuObject.getTexelFormat().isCompressed()) { - qFatal("Compressed textures not yet supported"); - } - - if (_transferrable && _sparseInfo.sparse) { - auto size = _sparseInfo.getSize(); - Backend::updateTextureGPUSparseMemoryUsage(_size, size); - setSize(size); - } else { - setSize(_gpuObject.evalTotalSize(_mipOffset)); - } -} - -void GL45Texture::startTransfer() { - Parent::startTransfer(); - _sparseInfo.update(); - _populatedMip = _maxMip + 1; -} - -bool GL45Texture::continueTransfer() { - size_t maxFace = GL_TEXTURE_CUBE_MAP == _target ? CUBE_NUM_FACES : 1; - if (_populatedMip == _minMip) { - return false; - } - - uint16_t targetMip = _populatedMip - 1; - while (targetMip > 0 && !_gpuObject.isStoredMipFaceAvailable(targetMip)) { - --targetMip; - } - - _sparseInfo.allocateToMip(targetMip); - for (uint8_t face = 0; face < maxFace; ++face) { - auto size = _gpuObject.evalMipDimensions(targetMip); - if (_gpuObject.isStoredMipFaceAvailable(targetMip, face)) { - auto mip = _gpuObject.accessStoredMipFace(targetMip, face); - GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat(), mip->getFormat()); - if (GL_TEXTURE_2D == _target) { - glTextureSubImage2D(_id, targetMip, 0, 0, size.x, size.y, texelFormat.format, texelFormat.type, mip->readData()); - } else if (GL_TEXTURE_CUBE_MAP == _target) { - // DSA ARB does not work on AMD, so use EXT - // unless EXT is not available on the driver - if (glTextureSubImage2DEXT) { - auto target = CUBE_FACE_LAYOUT[face]; - glTextureSubImage2DEXT(_id, target, targetMip, 0, 0, size.x, size.y, texelFormat.format, texelFormat.type, mip->readData()); - } else { - glTextureSubImage3D(_id, targetMip, 0, 0, face, size.x, size.y, 1, texelFormat.format, texelFormat.type, mip->readData()); - } - } else { - Q_ASSERT(false); - } - (void)CHECK_GL_ERROR(); - break; - } - } - _populatedMip = targetMip; - return _populatedMip != _minMip; -} - -void GL45Texture::finishTransfer() { - Parent::finishTransfer(); -} - -void GL45Texture::postTransfer() { - Parent::postTransfer(); -} - -void GL45Texture::stripToMip(uint16_t newMinMip) { - if (newMinMip < _minMip) { - qCWarning(gpugl45logging) << "Cannot decrease the min mip"; - return; - } - - if (_sparseInfo.sparse && newMinMip > _sparseInfo.maxSparseLevel) { - qCWarning(gpugl45logging) << "Cannot increase the min mip into the mip tail"; - return; - } - - // If we weren't generating mips before, we need to now that we're stripping down mip levels. - if (!_gpuObject.isAutogenerateMips()) { - qCDebug(gpugl45logging) << "Force mip generation for texture"; - glGenerateTextureMipmap(_id); - } - - - uint8_t maxFace = (uint8_t)((_target == GL_TEXTURE_CUBE_MAP) ? GLTexture::CUBE_NUM_FACES : 1); - if (_sparseInfo.sparse) { - for (uint16_t mip = _minMip; mip < newMinMip; ++mip) { - auto id = _id; - auto mipDimensions = _gpuObject.evalMipDimensions(mip); - glTexturePageCommitmentEXT(id, mip, 0, 0, 0, mipDimensions.x, mipDimensions.y, maxFace, GL_FALSE); - auto deallocatedPages = _sparseInfo.getPageCount(mipDimensions) * maxFace; - assert(deallocatedPages < _sparseInfo.allocatedPages); - _sparseInfo.allocatedPages -= deallocatedPages; - } - _minMip = newMinMip; - } else { - GLuint oldId = _id; - // Find the distance between the old min mip and the new one - uint16 mipDelta = newMinMip - _minMip; - _mipOffset += mipDelta; - const_cast(_maxMip) -= mipDelta; - auto newLevels = usedMipLevels(); - - // Create and setup the new texture (allocate) - glCreateTextures(_target, 1, &const_cast(_id)); - glTextureParameteri(_id, GL_TEXTURE_BASE_LEVEL, 0); - glTextureParameteri(_id, GL_TEXTURE_MAX_LEVEL, _maxMip - _minMip); - Vec3u newDimensions = _gpuObject.evalMipDimensions(_mipOffset); - glTextureStorage2D(_id, newLevels, _internalFormat, newDimensions.x, newDimensions.y); - - // Copy the contents of the old texture to the new - GLuint fbo { 0 }; - glCreateFramebuffers(1, &fbo); - glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo); - for (uint16 targetMip = _minMip; targetMip <= _maxMip; ++targetMip) { - uint16 sourceMip = targetMip + mipDelta; - Vec3u mipDimensions = _gpuObject.evalMipDimensions(targetMip + _mipOffset); - for (GLenum target : getFaceTargets(_target)) { - glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, target, oldId, sourceMip); - (void)CHECK_GL_ERROR(); - glCopyTextureSubImage2D(_id, targetMip, 0, 0, 0, 0, mipDimensions.x, mipDimensions.y); - (void)CHECK_GL_ERROR(); - } - } - glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); - glDeleteFramebuffers(1, &fbo); - glDeleteTextures(1, &oldId); - } - - // Re-sync the sampler to force access to the new mip level - syncSampler(); - updateSize(); -} - -bool GL45Texture::derezable() const { - if (_external) { - return false; - } - auto maxMinMip = _sparseInfo.sparse ? _sparseInfo.maxSparseLevel : _maxMip; - return _transferrable && (_targetMinMip < maxMinMip); -} - -size_t GL45Texture::getMipByteCount(uint16_t mip) const { - if (!_sparseInfo.sparse) { - return Parent::getMipByteCount(mip); - } - - auto dimensions = _gpuObject.evalMipDimensions(_targetMinMip); - return _sparseInfo.getPageCount(dimensions) * _sparseInfo.pageBytes; -} - -std::pair GL45Texture::preDerez() { - assert(!_sparseInfo.sparse || _targetMinMip < _sparseInfo.maxSparseLevel); - size_t freedMemory = getMipByteCount(_targetMinMip); - bool liveMip = _populatedMip != INVALID_MIP && _populatedMip <= _targetMinMip; - ++_targetMinMip; - return { freedMemory, liveMip }; -} - -void GL45Texture::derez() { - if (_sparseInfo.sparse) { - assert(_minMip < _sparseInfo.maxSparseLevel); - } - assert(_minMip < _maxMip); - assert(_transferrable); - stripToMip(_minMip + 1); -} - -size_t GL45Texture::getCurrentGpuSize() const { - if (!_sparseInfo.sparse) { - return Parent::getCurrentGpuSize(); - } - - return _sparseInfo.getSize(); -} - -size_t GL45Texture::getTargetGpuSize() const { - if (!_sparseInfo.sparse) { - return Parent::getTargetGpuSize(); - } - - size_t result = 0; - for (auto mip = _targetMinMip; mip <= _sparseInfo.maxSparseLevel; ++mip) { - result += (_sparseInfo.pageBytes * _sparseInfo.getPageCount(_gpuObject.evalMipDimensions(mip))); - } - - return result; -} - -GL45Texture::~GL45Texture() { - if (_sparseInfo.sparse) { - uint8_t maxFace = (uint8_t)((_target == GL_TEXTURE_CUBE_MAP) ? GLTexture::CUBE_NUM_FACES : 1); - auto maxSparseMip = std::min(_maxMip, _sparseInfo.maxSparseLevel); - for (uint16_t mipLevel = _minMip; mipLevel <= maxSparseMip; ++mipLevel) { - auto mipDimensions = _gpuObject.evalMipDimensions(mipLevel); - glTexturePageCommitmentEXT(_texture, mipLevel, 0, 0, 0, mipDimensions.x, mipDimensions.y, maxFace, GL_FALSE); - auto deallocatedPages = _sparseInfo.getPageCount(mipDimensions) * maxFace; - assert(deallocatedPages <= _sparseInfo.allocatedPages); - _sparseInfo.allocatedPages -= deallocatedPages; - } - - if (0 != _sparseInfo.allocatedPages) { - qCWarning(gpugl45logging) << "Allocated pages remaining " << _id << " " << _sparseInfo.allocatedPages; - } - Backend::decrementTextureGPUSparseCount(); - } -} -GL45Texture::GL45Texture(const std::weak_ptr& backend, const Texture& texture) - : GLTexture(backend, texture, allocate(texture)), _sparseInfo(*this), _targetMinMip(_minMip) -{ - - auto theBackend = _backend.lock(); - if (_transferrable && theBackend && theBackend->isTextureManagementSparseEnabled()) { - _sparseInfo.maybeMakeSparse(); - if (_sparseInfo.sparse) { - Backend::incrementTextureGPUSparseCount(); - } - } -} -#endif diff --git a/libraries/gpu/CMakeLists.txt b/libraries/gpu/CMakeLists.txt index 207431d8c7..384c5709ee 100644 --- a/libraries/gpu/CMakeLists.txt +++ b/libraries/gpu/CMakeLists.txt @@ -1,6 +1,6 @@ set(TARGET_NAME gpu) autoscribe_shader_lib(gpu) setup_hifi_library() -link_hifi_libraries(shared ktx) +link_hifi_libraries(shared) target_nsight() diff --git a/libraries/gpu/src/gpu/Batch.cpp b/libraries/gpu/src/gpu/Batch.cpp index f822da129b..c15da61800 100644 --- a/libraries/gpu/src/gpu/Batch.cpp +++ b/libraries/gpu/src/gpu/Batch.cpp @@ -292,8 +292,15 @@ void Batch::setUniformBuffer(uint32 slot, const BufferView& view) { setUniformBuffer(slot, view._buffer, view._offset, view._size); } + void Batch::setResourceTexture(uint32 slot, const TexturePointer& texture) { + if (texture && texture->getUsage().isExternal()) { + auto recycler = texture->getExternalRecycler(); + Q_ASSERT(recycler); + } + ADD_COMMAND(setResourceTexture); + _params.emplace_back(_textures.cache(texture)); _params.emplace_back(slot); } diff --git a/libraries/gpu/src/gpu/Buffer.h b/libraries/gpu/src/gpu/Buffer.h index 290b84bef0..2507e8e0a6 100644 --- a/libraries/gpu/src/gpu/Buffer.h +++ b/libraries/gpu/src/gpu/Buffer.h @@ -198,7 +198,7 @@ public: BufferView(const BufferPointer& buffer, Size offset, Size size, const Element& element = DEFAULT_ELEMENT); BufferView(const BufferPointer& buffer, Size offset, Size size, uint16 stride, const Element& element = DEFAULT_ELEMENT); - Size getNumElements() const { return (_size - _offset) / _stride; } + Size getNumElements() const { return _size / _element.getSize(); } //Template iterator with random access on the buffer sysmem template diff --git a/libraries/gpu/src/gpu/Context.cpp b/libraries/gpu/src/gpu/Context.cpp index cc570f696f..78b472bdae 100644 --- a/libraries/gpu/src/gpu/Context.cpp +++ b/libraries/gpu/src/gpu/Context.cpp @@ -241,7 +241,6 @@ std::atomic Context::_bufferGPUMemoryUsage { 0 }; std::atomic Context::_textureGPUCount{ 0 }; std::atomic Context::_textureGPUSparseCount { 0 }; -std::atomic Context::_textureTransferPendingSize { 0 }; std::atomic Context::_textureGPUMemoryUsage { 0 }; std::atomic Context::_textureGPUVirtualMemoryUsage { 0 }; std::atomic Context::_textureGPUFramebufferMemoryUsage { 0 }; @@ -318,17 +317,6 @@ void Context::decrementTextureGPUSparseCount() { --_textureGPUSparseCount; } -void Context::updateTextureTransferPendingSize(Size prevObjectSize, Size newObjectSize) { - if (prevObjectSize == newObjectSize) { - return; - } - if (newObjectSize > prevObjectSize) { - _textureTransferPendingSize.fetch_add(newObjectSize - prevObjectSize); - } else { - _textureTransferPendingSize.fetch_sub(prevObjectSize - newObjectSize); - } -} - void Context::updateTextureGPUMemoryUsage(Size prevObjectSize, Size newObjectSize) { if (prevObjectSize == newObjectSize) { return; @@ -402,10 +390,6 @@ uint32_t Context::getTextureGPUSparseCount() { return _textureGPUSparseCount.load(); } -Context::Size Context::getTextureTransferPendingSize() { - return _textureTransferPendingSize.load(); -} - Context::Size Context::getTextureGPUMemoryUsage() { return _textureGPUMemoryUsage.load(); } @@ -435,7 +419,6 @@ void Backend::incrementTextureGPUCount() { Context::incrementTextureGPUCount(); void Backend::decrementTextureGPUCount() { Context::decrementTextureGPUCount(); } void Backend::incrementTextureGPUSparseCount() { Context::incrementTextureGPUSparseCount(); } void Backend::decrementTextureGPUSparseCount() { Context::decrementTextureGPUSparseCount(); } -void Backend::updateTextureTransferPendingSize(Resource::Size prevObjectSize, Resource::Size newObjectSize) { Context::updateTextureTransferPendingSize(prevObjectSize, newObjectSize); } void Backend::updateTextureGPUMemoryUsage(Resource::Size prevObjectSize, Resource::Size newObjectSize) { Context::updateTextureGPUMemoryUsage(prevObjectSize, newObjectSize); } void Backend::updateTextureGPUVirtualMemoryUsage(Resource::Size prevObjectSize, Resource::Size newObjectSize) { Context::updateTextureGPUVirtualMemoryUsage(prevObjectSize, newObjectSize); } void Backend::updateTextureGPUFramebufferMemoryUsage(Resource::Size prevObjectSize, Resource::Size newObjectSize) { Context::updateTextureGPUFramebufferMemoryUsage(prevObjectSize, newObjectSize); } diff --git a/libraries/gpu/src/gpu/Context.h b/libraries/gpu/src/gpu/Context.h index 102c754cd7..01c841992d 100644 --- a/libraries/gpu/src/gpu/Context.h +++ b/libraries/gpu/src/gpu/Context.h @@ -101,7 +101,6 @@ public: static void decrementTextureGPUCount(); static void incrementTextureGPUSparseCount(); static void decrementTextureGPUSparseCount(); - static void updateTextureTransferPendingSize(Resource::Size prevObjectSize, Resource::Size newObjectSize); static void updateTextureGPUMemoryUsage(Resource::Size prevObjectSize, Resource::Size newObjectSize); static void updateTextureGPUSparseMemoryUsage(Resource::Size prevObjectSize, Resource::Size newObjectSize); static void updateTextureGPUVirtualMemoryUsage(Resource::Size prevObjectSize, Resource::Size newObjectSize); @@ -221,7 +220,6 @@ public: static uint32_t getTextureGPUSparseCount(); static Size getFreeGPUMemory(); static Size getUsedGPUMemory(); - static Size getTextureTransferPendingSize(); static Size getTextureGPUMemoryUsage(); static Size getTextureGPUVirtualMemoryUsage(); static Size getTextureGPUFramebufferMemoryUsage(); @@ -265,7 +263,6 @@ protected: static void decrementTextureGPUCount(); static void incrementTextureGPUSparseCount(); static void decrementTextureGPUSparseCount(); - static void updateTextureTransferPendingSize(Size prevObjectSize, Size newObjectSize); static void updateTextureGPUMemoryUsage(Size prevObjectSize, Size newObjectSize); static void updateTextureGPUSparseMemoryUsage(Size prevObjectSize, Size newObjectSize); static void updateTextureGPUVirtualMemoryUsage(Size prevObjectSize, Size newObjectSize); @@ -282,7 +279,6 @@ protected: static std::atomic _textureGPUCount; static std::atomic _textureGPUSparseCount; - static std::atomic _textureTransferPendingSize; static std::atomic _textureGPUMemoryUsage; static std::atomic _textureGPUSparseMemoryUsage; static std::atomic _textureGPUVirtualMemoryUsage; diff --git a/libraries/gpu/src/gpu/Format.cpp b/libraries/gpu/src/gpu/Format.cpp index de202911e3..2a8185bf94 100644 --- a/libraries/gpu/src/gpu/Format.cpp +++ b/libraries/gpu/src/gpu/Format.cpp @@ -10,15 +10,8 @@ using namespace gpu; -const Element Element::COLOR_R_8 { SCALAR, NUINT8, RED }; -const Element Element::COLOR_SR_8 { SCALAR, NUINT8, SRED }; - const Element Element::COLOR_RGBA_32{ VEC4, NUINT8, RGBA }; const Element Element::COLOR_SRGBA_32{ VEC4, NUINT8, SRGBA }; - -const Element Element::COLOR_BGRA_32{ VEC4, NUINT8, BGRA }; -const Element Element::COLOR_SBGRA_32{ VEC4, NUINT8, SBGRA }; - const Element Element::COLOR_R11G11B10{ SCALAR, FLOAT, R11G11B10 }; const Element Element::VEC4F_COLOR_RGBA{ VEC4, FLOAT, RGBA }; const Element Element::VEC2F_UV{ VEC2, FLOAT, UV }; diff --git a/libraries/gpu/src/gpu/Format.h b/libraries/gpu/src/gpu/Format.h index 493a2de3c2..13809a41e6 100644 --- a/libraries/gpu/src/gpu/Format.h +++ b/libraries/gpu/src/gpu/Format.h @@ -133,7 +133,6 @@ static const int SCALAR_COUNT[NUM_DIMENSIONS] = { enum Semantic { RAW = 0, // used as RAW memory - RED, RGB, RGBA, BGRA, @@ -150,7 +149,6 @@ enum Semantic { STENCIL, // Stencil only buffer DEPTH_STENCIL, // Depth Stencil buffer - SRED, SRGB, SRGBA, SBGRA, @@ -229,12 +227,8 @@ public: return getRaw() != right.getRaw(); } - static const Element COLOR_R_8; - static const Element COLOR_SR_8; static const Element COLOR_RGBA_32; static const Element COLOR_SRGBA_32; - static const Element COLOR_BGRA_32; - static const Element COLOR_SBGRA_32; static const Element COLOR_R11G11B10; static const Element VEC4F_COLOR_RGBA; static const Element VEC2F_UV; diff --git a/libraries/gpu/src/gpu/Framebuffer.cpp b/libraries/gpu/src/gpu/Framebuffer.cpp index 0d3291a74d..e8ccfce3b2 100755 --- a/libraries/gpu/src/gpu/Framebuffer.cpp +++ b/libraries/gpu/src/gpu/Framebuffer.cpp @@ -32,7 +32,7 @@ Framebuffer* Framebuffer::create(const std::string& name) { Framebuffer* Framebuffer::create(const std::string& name, const Format& colorBufferFormat, uint16 width, uint16 height) { auto framebuffer = Framebuffer::create(name); - auto colorTexture = TexturePointer(Texture::createRenderBuffer(colorBufferFormat, width, height, Sampler(Sampler::FILTER_MIN_MAG_POINT))); + auto colorTexture = TexturePointer(Texture::create2D(colorBufferFormat, width, height, Sampler(Sampler::FILTER_MIN_MAG_POINT))); colorTexture->setSource("Framebuffer::colorTexture"); framebuffer->setRenderBuffer(0, colorTexture); @@ -43,8 +43,8 @@ Framebuffer* Framebuffer::create(const std::string& name, const Format& colorBuf Framebuffer* Framebuffer::create(const std::string& name, const Format& colorBufferFormat, const Format& depthStencilBufferFormat, uint16 width, uint16 height) { auto framebuffer = Framebuffer::create(name); - auto colorTexture = TexturePointer(Texture::createRenderBuffer(colorBufferFormat, width, height, Sampler(Sampler::FILTER_MIN_MAG_POINT))); - auto depthTexture = TexturePointer(Texture::createRenderBuffer(depthStencilBufferFormat, width, height, Sampler(Sampler::FILTER_MIN_MAG_POINT))); + auto colorTexture = TexturePointer(Texture::create2D(colorBufferFormat, width, height, Sampler(Sampler::FILTER_MIN_MAG_POINT))); + auto depthTexture = TexturePointer(Texture::create2D(depthStencilBufferFormat, width, height, Sampler(Sampler::FILTER_MIN_MAG_POINT))); framebuffer->setRenderBuffer(0, colorTexture); framebuffer->setDepthStencilBuffer(depthTexture, depthStencilBufferFormat); @@ -55,7 +55,7 @@ Framebuffer* Framebuffer::createShadowmap(uint16 width) { auto framebuffer = Framebuffer::create("Shadowmap"); auto depthFormat = Element(gpu::SCALAR, gpu::FLOAT, gpu::DEPTH); // Depth32 texel format - auto depthTexture = TexturePointer(Texture::createRenderBuffer(depthFormat, width, width)); + auto depthTexture = TexturePointer(Texture::create2D(depthFormat, width, width)); Sampler::Desc samplerDesc; samplerDesc._borderColor = glm::vec4(1.0f); samplerDesc._wrapModeU = Sampler::WRAP_BORDER; @@ -143,8 +143,6 @@ int Framebuffer::setRenderBuffer(uint32 slot, const TexturePointer& texture, uin return -1; } - Q_ASSERT(!texture || TextureUsageType::RENDERBUFFER == texture->getUsageType()); - // Check for the slot if (slot >= getMaxNumRenderBuffers()) { return -1; @@ -224,8 +222,6 @@ bool Framebuffer::setDepthStencilBuffer(const TexturePointer& texture, const For return false; } - Q_ASSERT(!texture || TextureUsageType::RENDERBUFFER == texture->getUsageType()); - // Check for the compatibility of size if (texture) { if (!validateTargetCompatibility(*texture)) { diff --git a/libraries/gpu/src/gpu/Texture.cpp b/libraries/gpu/src/gpu/Texture.cpp index 1f66b2900e..5b0c4c876a 100755 --- a/libraries/gpu/src/gpu/Texture.cpp +++ b/libraries/gpu/src/gpu/Texture.cpp @@ -19,7 +19,6 @@ #include #include -#include #include #include "GPULogging.h" @@ -89,10 +88,6 @@ uint32_t Texture::getTextureGPUSparseCount() { return Context::getTextureGPUSparseCount(); } -Texture::Size Texture::getTextureTransferPendingSize() { - return Context::getTextureTransferPendingSize(); -} - Texture::Size Texture::getTextureGPUMemoryUsage() { return Context::getTextureGPUMemoryUsage(); } @@ -125,23 +120,62 @@ void Texture::setAllowedGPUMemoryUsage(Size size) { uint8 Texture::NUM_FACES_PER_TYPE[NUM_TYPES] = { 1, 1, 1, 6 }; -using Storage = Texture::Storage; -using PixelsPointer = Texture::PixelsPointer; -using MemoryStorage = Texture::MemoryStorage; +Texture::Pixels::Pixels(const Element& format, Size size, const Byte* bytes) : + _format(format), + _sysmem(size, bytes), + _isGPULoaded(false) { + Texture::updateTextureCPUMemoryUsage(0, _sysmem.getSize()); +} -void Storage::assignTexture(Texture* texture) { +Texture::Pixels::~Pixels() { + Texture::updateTextureCPUMemoryUsage(_sysmem.getSize(), 0); +} + +Texture::Size Texture::Pixels::resize(Size pSize) { + auto prevSize = _sysmem.getSize(); + auto newSize = _sysmem.resize(pSize); + Texture::updateTextureCPUMemoryUsage(prevSize, newSize); + return newSize; +} + +Texture::Size Texture::Pixels::setData(const Element& format, Size size, const Byte* bytes ) { + _format = format; + auto prevSize = _sysmem.getSize(); + auto newSize = _sysmem.setData(size, bytes); + Texture::updateTextureCPUMemoryUsage(prevSize, newSize); + _isGPULoaded = false; + return newSize; +} + +void Texture::Pixels::notifyGPULoaded() { + _isGPULoaded = true; + auto prevSize = _sysmem.getSize(); + auto newSize = _sysmem.resize(0); + Texture::updateTextureCPUMemoryUsage(prevSize, newSize); +} + +void Texture::Storage::assignTexture(Texture* texture) { _texture = texture; if (_texture) { _type = _texture->getType(); } } -void MemoryStorage::reset() { +void Texture::Storage::reset() { _mips.clear(); bumpStamp(); } -PixelsPointer MemoryStorage::getMipFace(uint16 level, uint8 face) const { +Texture::PixelsPointer Texture::Storage::editMipFace(uint16 level, uint8 face) { + if (level < _mips.size()) { + assert(face < _mips[level].size()); + bumpStamp(); + return _mips[level][face]; + } + return PixelsPointer(); +} + +const Texture::PixelsPointer Texture::Storage::getMipFace(uint16 level, uint8 face) const { if (level < _mips.size()) { assert(face < _mips[level].size()); return _mips[level][face]; @@ -149,12 +183,20 @@ PixelsPointer MemoryStorage::getMipFace(uint16 level, uint8 face) const { return PixelsPointer(); } -bool MemoryStorage::isMipAvailable(uint16 level, uint8 face) const { +void Texture::Storage::notifyMipFaceGPULoaded(uint16 level, uint8 face) const { + PixelsPointer mipFace = getMipFace(level, face); + // Free the mips + if (mipFace) { + mipFace->notifyGPULoaded(); + } +} + +bool Texture::Storage::isMipAvailable(uint16 level, uint8 face) const { PixelsPointer mipFace = getMipFace(level, face); return (mipFace && mipFace->getSize()); } -bool MemoryStorage::allocateMip(uint16 level) { +bool Texture::Storage::allocateMip(uint16 level) { bool changed = false; if (level >= _mips.size()) { _mips.resize(level+1, std::vector(Texture::NUM_FACES_PER_TYPE[getType()])); @@ -164,6 +206,7 @@ bool MemoryStorage::allocateMip(uint16 level) { auto& mip = _mips[level]; for (auto& face : mip) { if (!face) { + face = std::make_shared(); changed = true; } } @@ -173,7 +216,7 @@ bool MemoryStorage::allocateMip(uint16 level) { return changed; } -void MemoryStorage::assignMipData(uint16 level, const storage::StoragePointer& storagePointer) { +bool Texture::Storage::assignMipData(uint16 level, const Element& format, Size size, const Byte* bytes) { allocateMip(level); auto& mip = _mips[level]; @@ -182,63 +225,64 @@ void MemoryStorage::assignMipData(uint16 level, const storage::StoragePointer& s // The bytes assigned here are supposed to contain all the faces bytes of the mip. // For tex1D, 2D, 3D there is only one face // For Cube, we expect the 6 faces in the order X+, X-, Y+, Y-, Z+, Z- - auto sizePerFace = storagePointer->size() / mip.size(); - size_t offset = 0; + auto sizePerFace = size / mip.size(); + auto faceBytes = bytes; + Size allocated = 0; for (auto& face : mip) { - face = storagePointer->createView(sizePerFace, offset); - offset += sizePerFace; + allocated += face->setData(format, sizePerFace, faceBytes); + faceBytes += sizePerFace; } bumpStamp(); + + return allocated == size; } -void Texture::MemoryStorage::assignMipFaceData(uint16 level, uint8 face, const storage::StoragePointer& storagePointer) { +bool Texture::Storage::assignMipFaceData(uint16 level, const Element& format, Size size, const Byte* bytes, uint8 face) { + allocateMip(level); - auto& mip = _mips[level]; + auto mip = _mips[level]; + Size allocated = 0; if (face < mip.size()) { - mip[face] = storagePointer; + auto mipFace = mip[face]; + allocated += mipFace->setData(format, size, bytes); bumpStamp(); } + + return allocated == size; } -Texture* Texture::createExternal(const ExternalRecycler& recycler, const Sampler& sampler) { - Texture* tex = new Texture(TextureUsageType::EXTERNAL); +Texture* Texture::createExternal2D(const ExternalRecycler& recycler, const Sampler& sampler) { + Texture* tex = new Texture(); tex->_type = TEX_2D; tex->_maxMip = 0; tex->_sampler = sampler; + tex->setUsage(Usage::Builder().withExternal().withColor()); tex->setExternalRecycler(recycler); return tex; } -Texture* Texture::createRenderBuffer(const Element& texelFormat, uint16 width, uint16 height, const Sampler& sampler) { - return create(TextureUsageType::RENDERBUFFER, TEX_2D, texelFormat, width, height, 1, 1, 0, sampler); -} - Texture* Texture::create1D(const Element& texelFormat, uint16 width, const Sampler& sampler) { - return create(TextureUsageType::RESOURCE, TEX_1D, texelFormat, width, 1, 1, 1, 0, sampler); + return create(TEX_1D, texelFormat, width, 1, 1, 1, 1, sampler); } Texture* Texture::create2D(const Element& texelFormat, uint16 width, uint16 height, const Sampler& sampler) { - return create(TextureUsageType::RESOURCE, TEX_2D, texelFormat, width, height, 1, 1, 0, sampler); -} - -Texture* Texture::createStrict(const Element& texelFormat, uint16 width, uint16 height, const Sampler& sampler) { - return create(TextureUsageType::STRICT_RESOURCE, TEX_2D, texelFormat, width, height, 1, 1, 0, sampler); + return create(TEX_2D, texelFormat, width, height, 1, 1, 1, sampler); } Texture* Texture::create3D(const Element& texelFormat, uint16 width, uint16 height, uint16 depth, const Sampler& sampler) { - return create(TextureUsageType::RESOURCE, TEX_3D, texelFormat, width, height, depth, 1, 0, sampler); + return create(TEX_3D, texelFormat, width, height, depth, 1, 1, sampler); } Texture* Texture::createCube(const Element& texelFormat, uint16 width, const Sampler& sampler) { - return create(TextureUsageType::RESOURCE, TEX_CUBE, texelFormat, width, width, 1, 1, 0, sampler); + return create(TEX_CUBE, texelFormat, width, width, 1, 1, 1, sampler); } -Texture* Texture::create(TextureUsageType usageType, Type type, const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numSamples, uint16 numSlices, const Sampler& sampler) +Texture* Texture::create(Type type, const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numSamples, uint16 numSlices, const Sampler& sampler) { - Texture* tex = new Texture(usageType); - tex->_storage.reset(new MemoryStorage()); + Texture* tex = new Texture(); + tex->_storage.reset(new Storage()); tex->_type = type; tex->_storage->assignTexture(tex); tex->_maxMip = 0; @@ -249,14 +293,16 @@ Texture* Texture::create(TextureUsageType usageType, Type type, const Element& t return tex; } -Texture::Texture(TextureUsageType usageType) : - Resource(), _usageType(usageType) { +Texture::Texture(): + Resource() +{ _textureCPUCount++; } -Texture::~Texture() { +Texture::~Texture() +{ _textureCPUCount--; - if (_usageType == TextureUsageType::EXTERNAL) { + if (getUsage().isExternal()) { Texture::ExternalUpdates externalUpdates; { Lock lock(_externalMutex); @@ -275,7 +321,7 @@ Texture::~Texture() { } Texture::Size Texture::resize(Type type, const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numSamples, uint16 numSlices) { - if (width && height && depth && numSamples) { + if (width && height && depth && numSamples && numSlices) { bool changed = false; if ( _type != type) { @@ -336,20 +382,20 @@ Texture::Size Texture::resize(Type type, const Element& texelFormat, uint16 widt } Texture::Size Texture::resize1D(uint16 width, uint16 numSamples) { - return resize(TEX_1D, getTexelFormat(), width, 1, 1, numSamples, 0); + return resize(TEX_1D, getTexelFormat(), width, 1, 1, numSamples, 1); } Texture::Size Texture::resize2D(uint16 width, uint16 height, uint16 numSamples) { - return resize(TEX_2D, getTexelFormat(), width, height, 1, numSamples, 0); + return resize(TEX_2D, getTexelFormat(), width, height, 1, numSamples, 1); } Texture::Size Texture::resize3D(uint16 width, uint16 height, uint16 depth, uint16 numSamples) { - return resize(TEX_3D, getTexelFormat(), width, height, depth, numSamples, 0); + return resize(TEX_3D, getTexelFormat(), width, height, depth, numSamples, 1); } Texture::Size Texture::resizeCube(uint16 width, uint16 numSamples) { - return resize(TEX_CUBE, getTexelFormat(), width, 1, 1, numSamples, 0); + return resize(TEX_CUBE, getTexelFormat(), width, 1, 1, numSamples, 1); } Texture::Size Texture::reformat(const Element& texelFormat) { - return resize(_type, texelFormat, getWidth(), getHeight(), getDepth(), getNumSamples(), _numSlices); + return resize(_type, texelFormat, getWidth(), getHeight(), getDepth(), getNumSamples(), getNumSlices()); } bool Texture::isColorRenderTarget() const { @@ -380,83 +426,69 @@ uint16 Texture::evalNumMips() const { return evalNumMips({ _width, _height, _depth }); } -void Texture::setStoredMipFormat(const Element& format) { - _storage->setFormat(format); -} - -const Element& Texture::getStoredMipFormat() const { - return _storage->getFormat(); -} - -void Texture::assignStoredMip(uint16 level, Size size, const Byte* bytes) { - storage::StoragePointer storage = std::make_shared(size, bytes); - assignStoredMip(level, storage); -} - -void Texture::assignStoredMipFace(uint16 level, uint8 face, Size size, const Byte* bytes) { - storage::StoragePointer storage = std::make_shared(size, bytes); - assignStoredMipFace(level, face, storage); -} - -void Texture::assignStoredMip(uint16 level, storage::StoragePointer& storage) { +bool Texture::assignStoredMip(uint16 level, const Element& format, Size size, const Byte* bytes) { // Check that level accessed make sense if (level != 0) { if (_autoGenerateMips) { - return; + return false; } if (level >= evalNumMips()) { - return; + return false; } } // THen check that the mem texture passed make sense with its format - Size expectedSize = evalStoredMipSize(level, getStoredMipFormat()); - auto size = storage->size(); - if (storage->size() == expectedSize) { - _storage->assignMipData(level, storage); - _maxMip = std::max(_maxMip, level); - _stamp++; - } else if (size > expectedSize) { - // NOTE: We are facing this case sometime because apparently QImage (from where we get the bits) is generating images - // and alligning the line of pixels to 32 bits. - // We should probably consider something a bit more smart to get the correct result but for now (UI elements) - // it seems to work... - _storage->assignMipData(level, storage); - _maxMip = std::max(_maxMip, level); - _stamp++; - } -} - -void Texture::assignStoredMipFace(uint16 level, uint8 face, storage::StoragePointer& storage) { - // Check that level accessed make sense - if (level != 0) { - if (_autoGenerateMips) { - return; - } - if (level >= evalNumMips()) { - return; - } - } - - // THen check that the mem texture passed make sense with its format - Size expectedSize = evalStoredMipFaceSize(level, getStoredMipFormat()); - auto size = storage->size(); + Size expectedSize = evalStoredMipSize(level, format); if (size == expectedSize) { - _storage->assignMipFaceData(level, face, storage); + _storage->assignMipData(level, format, size, bytes); _maxMip = std::max(_maxMip, level); _stamp++; + return true; } else if (size > expectedSize) { // NOTE: We are facing this case sometime because apparently QImage (from where we get the bits) is generating images // and alligning the line of pixels to 32 bits. // We should probably consider something a bit more smart to get the correct result but for now (UI elements) // it seems to work... - _storage->assignMipFaceData(level, face, storage); + _storage->assignMipData(level, format, size, bytes); _maxMip = std::max(_maxMip, level); _stamp++; + return true; } + + return false; } +bool Texture::assignStoredMipFace(uint16 level, const Element& format, Size size, const Byte* bytes, uint8 face) { + // Check that level accessed make sense + if (level != 0) { + if (_autoGenerateMips) { + return false; + } + if (level >= evalNumMips()) { + return false; + } + } + + // THen check that the mem texture passed make sense with its format + Size expectedSize = evalStoredMipFaceSize(level, format); + if (size == expectedSize) { + _storage->assignMipFaceData(level, format, size, bytes, face); + _stamp++; + return true; + } else if (size > expectedSize) { + // NOTE: We are facing this case sometime because apparently QImage (from where we get the bits) is generating images + // and alligning the line of pixels to 32 bits. + // We should probably consider something a bit more smart to get the correct result but for now (UI elements) + // it seems to work... + _storage->assignMipFaceData(level, format, size, bytes, face); + _stamp++; + return true; + } + + return false; +} + uint16 Texture::autoGenerateMips(uint16 maxMip) { bool changed = false; if (!_autoGenerateMips) { @@ -490,7 +522,7 @@ uint16 Texture::getStoredMipHeight(uint16 level) const { if (mip && mip->getSize()) { return evalMipHeight(level); } - return 0; + return 0; } uint16 Texture::getStoredMipDepth(uint16 level) const { @@ -762,16 +794,7 @@ bool sphericalHarmonicsFromTexture(const gpu::Texture& cubeTexture, std::vector< for(int face=0; face < gpu::Texture::NUM_CUBE_FACES; face++) { PROFILE_RANGE(render_gpu, "ProcessFace"); - auto mipFormat = cubeTexture.getStoredMipFormat(); - auto numComponents = mipFormat.getScalarCount(); - int roffset { 0 }; - int goffset { 1 }; - int boffset { 2 }; - if ((mipFormat.getSemantic() == gpu::BGRA) || (mipFormat.getSemantic() == gpu::SBGRA)) { - roffset = 2; - boffset = 0; - } - + auto numComponents = cubeTexture.accessStoredMipFace(0,face)->getFormat().getScalarCount(); auto data = cubeTexture.accessStoredMipFace(0,face)->readData(); if (data == nullptr) { continue; @@ -859,9 +882,9 @@ bool sphericalHarmonicsFromTexture(const gpu::Texture& cubeTexture, std::vector< for (int i = 0; i < stride; ++i) { for (int j = 0; j < stride; ++j) { int k = (int)(x + i - halfStride + (y + j - halfStride) * width) * numComponents; - red += ColorUtils::sRGB8ToLinearFloat(data[k + roffset]); - green += ColorUtils::sRGB8ToLinearFloat(data[k + goffset]); - blue += ColorUtils::sRGB8ToLinearFloat(data[k + boffset]); + red += ColorUtils::sRGB8ToLinearFloat(data[k]); + green += ColorUtils::sRGB8ToLinearFloat(data[k + 1]); + blue += ColorUtils::sRGB8ToLinearFloat(data[k + 2]); } } glm::vec3 clr(red, green, blue); @@ -888,6 +911,8 @@ bool sphericalHarmonicsFromTexture(const gpu::Texture& cubeTexture, std::vector< // save result for(uint i=0; i < sqOrder; i++) { + // gamma Correct + // output[i] = linearTosRGB(glm::vec3(resultR[i], resultG[i], resultB[i])); output[i] = glm::vec3(resultR[i], resultG[i], resultB[i]); } @@ -976,7 +1001,3 @@ Texture::ExternalUpdates Texture::getUpdates() const { } return result; } - -void Texture::setStorage(std::unique_ptr& newStorage) { - _storage.swap(newStorage); -} diff --git a/libraries/gpu/src/gpu/Texture.h b/libraries/gpu/src/gpu/Texture.h index f7297b3280..856bd4983d 100755 --- a/libraries/gpu/src/gpu/Texture.h +++ b/libraries/gpu/src/gpu/Texture.h @@ -17,17 +17,9 @@ #include #include -#include - #include "Forward.h" #include "Resource.h" -namespace ktx { - class KTX; - using KTXUniquePointer = std::unique_ptr; - struct Header; -} - namespace gpu { // THe spherical harmonics is a nice tool for cubemap, so if required, the irradiance SH can be automatically generated @@ -143,18 +135,10 @@ public: uint8 getMinMip() const { return _desc._minMip; } uint8 getMaxMip() const { return _desc._maxMip; } - const Desc& getDesc() const { return _desc; } protected: Desc _desc; }; -enum class TextureUsageType { - RENDERBUFFER, // Used as attachments to a framebuffer - RESOURCE, // Resource textures, like materials... subject to memory manipulation - STRICT_RESOURCE, // Resource textures not subject to manipulation, like the normal fitting texture - EXTERNAL, -}; - class Texture : public Resource { static std::atomic _textureCPUCount; static std::atomic _textureCPUMemoryUsage; @@ -163,12 +147,10 @@ class Texture : public Resource { static void updateTextureCPUMemoryUsage(Size prevObjectSize, Size newObjectSize); public: - static const uint32_t CUBE_FACE_COUNT { 6 }; static uint32_t getTextureCPUCount(); static Size getTextureCPUMemoryUsage(); static uint32_t getTextureGPUCount(); static uint32_t getTextureGPUSparseCount(); - static Size getTextureTransferPendingSize(); static Size getTextureGPUMemoryUsage(); static Size getTextureGPUVirtualMemoryUsage(); static Size getTextureGPUFramebufferMemoryUsage(); @@ -191,9 +173,9 @@ public: NORMAL, // Texture is a normal map ALPHA, // Texture has an alpha channel ALPHA_MASK, // Texture alpha channel is a Mask 0/1 + EXTERNAL, NUM_FLAGS, }; - typedef std::bitset Flags; // The key is the Flags @@ -217,6 +199,7 @@ public: Builder& withNormal() { _flags.set(NORMAL); return (*this); } Builder& withAlpha() { _flags.set(ALPHA); return (*this); } Builder& withAlphaMask() { _flags.set(ALPHA_MASK); return (*this); } + Builder& withExternal() { _flags.set(EXTERNAL); return (*this); } }; Usage(const Builder& builder) : Usage(builder._flags) {} @@ -225,12 +208,37 @@ public: bool isAlpha() const { return _flags[ALPHA]; } bool isAlphaMask() const { return _flags[ALPHA_MASK]; } + bool isExternal() const { return _flags[EXTERNAL]; } + bool operator==(const Usage& usage) { return (_flags == usage._flags); } bool operator!=(const Usage& usage) { return (_flags != usage._flags); } }; - using PixelsPointer = storage::StoragePointer; + class Pixels { + public: + Pixels() {} + Pixels(const Pixels& pixels) = default; + Pixels(const Element& format, Size size, const Byte* bytes); + ~Pixels(); + + const Byte* readData() const { return _sysmem.readData(); } + Size getSize() const { return _sysmem.getSize(); } + Size resize(Size pSize); + Size setData(const Element& format, Size size, const Byte* bytes ); + + const Element& getFormat() const { return _format; } + + void notifyGPULoaded(); + + protected: + Element _format; + Sysmem _sysmem; + bool _isGPULoaded; + + friend class Texture; + }; + typedef std::shared_ptr< Pixels > PixelsPointer; enum Type { TEX_1D = 0, @@ -253,78 +261,46 @@ public: NUM_CUBE_FACES, // Not a valid vace index }; - class Storage { public: Storage() {} virtual ~Storage() {} + virtual void reset(); + virtual PixelsPointer editMipFace(uint16 level, uint8 face = 0); + virtual const PixelsPointer getMipFace(uint16 level, uint8 face = 0) const; + virtual bool allocateMip(uint16 level); + virtual bool assignMipData(uint16 level, const Element& format, Size size, const Byte* bytes); + virtual bool assignMipFaceData(uint16 level, const Element& format, Size size, const Byte* bytes, uint8 face); + virtual bool isMipAvailable(uint16 level, uint8 face = 0) const; - virtual void reset() = 0; - virtual PixelsPointer getMipFace(uint16 level, uint8 face = 0) const = 0; - virtual void assignMipData(uint16 level, const storage::StoragePointer& storage) = 0; - virtual void assignMipFaceData(uint16 level, uint8 face, const storage::StoragePointer& storage) = 0; - virtual bool isMipAvailable(uint16 level, uint8 face = 0) const = 0; Texture::Type getType() const { return _type; } - + Stamp getStamp() const { return _stamp; } Stamp bumpStamp() { return ++_stamp; } + protected: + Stamp _stamp = 0; + Texture* _texture = nullptr; // Points to the parent texture (not owned) + Texture::Type _type = Texture::TEX_2D; // The type of texture is needed to know the number of faces to expect + std::vector> _mips; // an array of mips, each mip is an array of faces - void setFormat(const Element& format) { _format = format; } - const Element& getFormat() const { return _format; } - - private: - Stamp _stamp { 0 }; - Element _format; - Texture::Type _type { Texture::TEX_2D }; // The type of texture is needed to know the number of faces to expect - Texture* _texture { nullptr }; // Points to the parent texture (not owned) virtual void assignTexture(Texture* tex); // Texture storage is pointing to ONE corrresponding Texture. const Texture* getTexture() const { return _texture; } + friend class Texture; + + // THis should be only called by the Texture from the Backend to notify the storage that the specified mip face pixels + // have been uploaded to the GPU memory. IT is possible for the storage to free the system memory then + virtual void notifyMipFaceGPULoaded(uint16 level, uint8 face) const; }; - class MemoryStorage : public Storage { - public: - void reset() override; - PixelsPointer getMipFace(uint16 level, uint8 face = 0) const override; - void assignMipData(uint16 level, const storage::StoragePointer& storage) override; - void assignMipFaceData(uint16 level, uint8 face, const storage::StoragePointer& storage) override; - bool isMipAvailable(uint16 level, uint8 face = 0) const override; - - protected: - bool allocateMip(uint16 level); - std::vector> _mips; // an array of mips, each mip is an array of faces - }; - - class KtxStorage : public Storage { - public: - KtxStorage(ktx::KTXUniquePointer& ktxData); - PixelsPointer getMipFace(uint16 level, uint8 face = 0) const override; - // By convention, all mip levels and faces MUST be populated when using KTX backing - bool isMipAvailable(uint16 level, uint8 face = 0) const override { return true; } - - void assignMipData(uint16 level, const storage::StoragePointer& storage) override { - throw std::runtime_error("Invalid call"); - } - - void assignMipFaceData(uint16 level, uint8 face, const storage::StoragePointer& storage) override { - throw std::runtime_error("Invalid call"); - } - void reset() override { } - - protected: - ktx::KTXUniquePointer _ktxData; - friend class Texture; - }; - + static Texture* create1D(const Element& texelFormat, uint16 width, const Sampler& sampler = Sampler()); static Texture* create2D(const Element& texelFormat, uint16 width, uint16 height, const Sampler& sampler = Sampler()); static Texture* create3D(const Element& texelFormat, uint16 width, uint16 height, uint16 depth, const Sampler& sampler = Sampler()); static Texture* createCube(const Element& texelFormat, uint16 width, const Sampler& sampler = Sampler()); - static Texture* createRenderBuffer(const Element& texelFormat, uint16 width, uint16 height, const Sampler& sampler = Sampler()); - static Texture* createStrict(const Element& texelFormat, uint16 width, uint16 height, const Sampler& sampler = Sampler()); - static Texture* createExternal(const ExternalRecycler& recycler, const Sampler& sampler = Sampler()); + static Texture* createExternal2D(const ExternalRecycler& recycler, const Sampler& sampler = Sampler()); - Texture(TextureUsageType usageType); + Texture(); Texture(const Texture& buf); // deep copy of the sysmem texture Texture& operator=(const Texture& buf); // deep copy of the sysmem texture ~Texture(); @@ -349,7 +325,6 @@ public: // Size and format Type getType() const { return _type; } - TextureUsageType getUsageType() const { return _usageType; } bool isColorRenderTarget() const; bool isDepthStencilRenderTarget() const; @@ -372,12 +347,7 @@ public: uint32 getNumTexels() const { return _width * _height * _depth * getNumFaces(); } - // The texture is an array if the _numSlices is not 0. - // otherwise, if _numSLices is 0, then the texture is NOT an array - // The number of slices returned is 1 at the minimum (if not an array) or the actual _numSlices. - bool isArray() const { return _numSlices > 0; } - uint16 getNumSlices() const { return (isArray() ? _numSlices : 1); } - + uint16 getNumSlices() const { return _numSlices; } uint16 getNumSamples() const { return _numSamples; } @@ -459,29 +429,18 @@ public: // Managing Storage and mips - // Mip storage format is constant across all mips - void setStoredMipFormat(const Element& format); - const Element& getStoredMipFormat() const; - // Manually allocate the mips down until the specified maxMip // this is just allocating the sysmem version of it // in case autoGen is on, this doesn't allocate // Explicitely assign mip data for a certain level // If Bytes is NULL then simply allocate the space so mip sysmem can be accessed - - void assignStoredMip(uint16 level, Size size, const Byte* bytes); - void assignStoredMipFace(uint16 level, uint8 face, Size size, const Byte* bytes); - - void assignStoredMip(uint16 level, storage::StoragePointer& storage); - void assignStoredMipFace(uint16 level, uint8 face, storage::StoragePointer& storage); + bool assignStoredMip(uint16 level, const Element& format, Size size, const Byte* bytes); + bool assignStoredMipFace(uint16 level, const Element& format, Size size, const Byte* bytes, uint8 face); // Access the the sub mips bool isStoredMipFaceAvailable(uint16 level, uint8 face = 0) const { return _storage->isMipAvailable(level, face); } const PixelsPointer accessStoredMipFace(uint16 level, uint8 face = 0) const { return _storage->getMipFace(level, face); } - void setStorage(std::unique_ptr& newStorage); - void setKtxBacking(ktx::KTXUniquePointer& newBacking); - // access sizes for the stored mips uint16 getStoredMipWidth(uint16 level) const; uint16 getStoredMipHeight(uint16 level) const; @@ -505,8 +464,8 @@ public: const Sampler& getSampler() const { return _sampler; } Stamp getSamplerStamp() const { return _samplerStamp; } - void setFallbackTexture(const TexturePointer& fallback) { _fallback = fallback; } - TexturePointer getFallbackTexture() const { return _fallback.lock(); } + // Only callable by the Backend + void notifyMipFaceGPULoaded(uint16 level, uint8 face = 0) const { return _storage->notifyMipFaceGPULoaded(level, face); } void setExternalTexture(uint32 externalId, void* externalFence); void setExternalRecycler(const ExternalRecycler& recycler); @@ -516,45 +475,36 @@ public: ExternalUpdates getUpdates() const; - // Textures can be serialized directly to ktx data file, here is how - static ktx::KTXUniquePointer serialize(const Texture& texture); - static Texture* unserialize(const ktx::KTXUniquePointer& srcData, TextureUsageType usageType = TextureUsageType::RESOURCE, Usage usage = Usage(), const Sampler::Desc& sampler = Sampler::Desc()); - static bool evalKTXFormat(const Element& mipFormat, const Element& texelFormat, ktx::Header& header); - static bool evalTextureFormat(const ktx::Header& header, Element& mipFormat, Element& texelFormat); - protected: - const TextureUsageType _usageType; - // Should only be accessed internally or by the backend sync function mutable Mutex _externalMutex; mutable std::list _externalUpdates; ExternalRecycler _externalRecycler; - std::weak_ptr _fallback; // Not strictly necessary, but incredibly useful for debugging std::string _source; std::unique_ptr< Storage > _storage; - Stamp _stamp { 0 }; + Stamp _stamp = 0; Sampler _sampler; - Stamp _samplerStamp { 0 }; + Stamp _samplerStamp; - uint32 _size { 0 }; + uint32 _size = 0; Element _texelFormat; - uint16 _width { 1 }; - uint16 _height { 1 }; - uint16 _depth { 1 }; + uint16 _width = 1; + uint16 _height = 1; + uint16 _depth = 1; - uint16 _numSamples { 1 }; - uint16 _numSlices { 0 }; // if _numSlices is 0, the texture is not an "Array", the getNumSlices reported is 1 + uint16 _numSamples = 1; + uint16 _numSlices = 1; uint16 _maxMip { 0 }; uint16 _minMip { 0 }; - Type _type { TEX_1D }; + Type _type = TEX_1D; Usage _usage; @@ -563,7 +513,7 @@ protected: bool _isIrradianceValid = false; bool _defined = false; - static Texture* create(TextureUsageType usageType, Type type, const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numSamples, uint16 numSlices, const Sampler& sampler); + static Texture* create(Type type, const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numSamples, uint16 numSlices, const Sampler& sampler); Size resize(Type type, const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numSamples, uint16 numSlices); }; diff --git a/libraries/gpu/src/gpu/Texture_ktx.cpp b/libraries/gpu/src/gpu/Texture_ktx.cpp deleted file mode 100644 index 5f0ededee7..0000000000 --- a/libraries/gpu/src/gpu/Texture_ktx.cpp +++ /dev/null @@ -1,289 +0,0 @@ -// -// Texture_ktx.cpp -// libraries/gpu/src/gpu -// -// Created by Sam Gateau on 2/16/2017. -// Copyright 2014 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - - -#include "Texture.h" - -#include -using namespace gpu; - -using PixelsPointer = Texture::PixelsPointer; -using KtxStorage = Texture::KtxStorage; - -struct GPUKTXPayload { - Sampler::Desc _samplerDesc; - Texture::Usage _usage; - TextureUsageType _usageType; - - static std::string KEY; - static bool isGPUKTX(const ktx::KeyValue& val) { - return (val._key.compare(KEY) == 0); - } - - static bool findInKeyValues(const ktx::KeyValues& keyValues, GPUKTXPayload& payload) { - auto found = std::find_if(keyValues.begin(), keyValues.end(), isGPUKTX); - if (found != keyValues.end()) { - if ((*found)._value.size() == sizeof(GPUKTXPayload)) { - memcpy(&payload, (*found)._value.data(), sizeof(GPUKTXPayload)); - return true; - } - } - return false; - } -}; - -std::string GPUKTXPayload::KEY { "hifi.gpu" }; - -KtxStorage::KtxStorage(ktx::KTXUniquePointer& ktxData) { - - // if the source ktx is valid let's config this KtxStorage correctly - if (ktxData && ktxData->getHeader()) { - - // now that we know the ktx, let's get the header info to configure this Texture::Storage: - Format mipFormat = Format::COLOR_BGRA_32; - Format texelFormat = Format::COLOR_SRGBA_32; - if (Texture::evalTextureFormat(*ktxData->getHeader(), mipFormat, texelFormat)) { - _format = mipFormat; - } - - - } - - _ktxData.reset(ktxData.release()); -} - -PixelsPointer KtxStorage::getMipFace(uint16 level, uint8 face) const { - return _ktxData->getMipFaceTexelsData(level, face); -} - -void Texture::setKtxBacking(ktx::KTXUniquePointer& ktxBacking) { - auto newBacking = std::unique_ptr(new KtxStorage(ktxBacking)); - setStorage(newBacking); -} - -ktx::KTXUniquePointer Texture::serialize(const Texture& texture) { - ktx::Header header; - - // From texture format to ktx format description - auto texelFormat = texture.getTexelFormat(); - auto mipFormat = texture.getStoredMipFormat(); - - if (!Texture::evalKTXFormat(mipFormat, texelFormat, header)) { - return nullptr; - } - - // Set Dimensions - uint32_t numFaces = 1; - switch (texture.getType()) { - case TEX_1D: { - if (texture.isArray()) { - header.set1DArray(texture.getWidth(), texture.getNumSlices()); - } else { - header.set1D(texture.getWidth()); - } - break; - } - case TEX_2D: { - if (texture.isArray()) { - header.set2DArray(texture.getWidth(), texture.getHeight(), texture.getNumSlices()); - } else { - header.set2D(texture.getWidth(), texture.getHeight()); - } - break; - } - case TEX_3D: { - if (texture.isArray()) { - header.set3DArray(texture.getWidth(), texture.getHeight(), texture.getDepth(), texture.getNumSlices()); - } else { - header.set3D(texture.getWidth(), texture.getHeight(), texture.getDepth()); - } - break; - } - case TEX_CUBE: { - if (texture.isArray()) { - header.setCubeArray(texture.getWidth(), texture.getHeight(), texture.getNumSlices()); - } else { - header.setCube(texture.getWidth(), texture.getHeight()); - } - numFaces = Texture::CUBE_FACE_COUNT; - break; - } - default: - return nullptr; - } - - // Number level of mips coming - header.numberOfMipmapLevels = texture.maxMip() + 1; - - ktx::Images images; - for (uint32_t level = 0; level < header.numberOfMipmapLevels; level++) { - auto mip = texture.accessStoredMipFace(level); - if (mip) { - if (numFaces == 1) { - images.emplace_back(ktx::Image((uint32_t)mip->getSize(), 0, mip->readData())); - } else { - ktx::Image::FaceBytes cubeFaces(Texture::CUBE_FACE_COUNT); - cubeFaces[0] = mip->readData(); - for (uint32_t face = 1; face < Texture::CUBE_FACE_COUNT; face++) { - cubeFaces[face] = texture.accessStoredMipFace(level, face)->readData(); - } - images.emplace_back(ktx::Image((uint32_t)mip->getSize(), 0, cubeFaces)); - } - } - } - - GPUKTXPayload keyval; - keyval._samplerDesc = texture.getSampler().getDesc(); - keyval._usage = texture.getUsage(); - keyval._usageType = texture.getUsageType(); - ktx::KeyValues keyValues; - keyValues.emplace_back(ktx::KeyValue(GPUKTXPayload::KEY, sizeof(GPUKTXPayload), (ktx::Byte*) &keyval)); - - auto ktxBuffer = ktx::KTX::create(header, images, keyValues); -#if 0 - auto expectedMipCount = texture.evalNumMips(); - assert(expectedMipCount == ktxBuffer->_images.size()); - assert(expectedMipCount == header.numberOfMipmapLevels); - - assert(0 == memcmp(&header, ktxBuffer->getHeader(), sizeof(ktx::Header))); - assert(ktxBuffer->_images.size() == images.size()); - auto start = ktxBuffer->_storage->data(); - for (size_t i = 0; i < images.size(); ++i) { - auto expected = images[i]; - auto actual = ktxBuffer->_images[i]; - assert(expected._padding == actual._padding); - assert(expected._numFaces == actual._numFaces); - assert(expected._imageSize == actual._imageSize); - assert(expected._faceSize == actual._faceSize); - assert(actual._faceBytes.size() == actual._numFaces); - for (uint32_t face = 0; face < expected._numFaces; ++face) { - auto expectedFace = expected._faceBytes[face]; - auto actualFace = actual._faceBytes[face]; - auto offset = actualFace - start; - assert(offset % 4 == 0); - assert(expectedFace != actualFace); - assert(0 == memcmp(expectedFace, actualFace, expected._faceSize)); - } - } -#endif - return ktxBuffer; -} - -Texture* Texture::unserialize(const ktx::KTXUniquePointer& srcData, TextureUsageType usageType, Usage usage, const Sampler::Desc& sampler) { - if (!srcData) { - return nullptr; - } - const auto& header = *srcData->getHeader(); - - Format mipFormat = Format::COLOR_BGRA_32; - Format texelFormat = Format::COLOR_SRGBA_32; - - if (!Texture::evalTextureFormat(header, mipFormat, texelFormat)) { - return nullptr; - } - - // Find Texture Type based on dimensions - Type type = TEX_1D; - if (header.pixelWidth == 0) { - return nullptr; - } else if (header.pixelHeight == 0) { - type = TEX_1D; - } else if (header.pixelDepth == 0) { - if (header.numberOfFaces == ktx::NUM_CUBEMAPFACES) { - type = TEX_CUBE; - } else { - type = TEX_2D; - } - } else { - type = TEX_3D; - } - - - // If found, use the - GPUKTXPayload gpuktxKeyValue; - bool isGPUKTXPayload = GPUKTXPayload::findInKeyValues(srcData->_keyValues, gpuktxKeyValue); - - auto tex = Texture::create( (isGPUKTXPayload ? gpuktxKeyValue._usageType : usageType), - type, - texelFormat, - header.getPixelWidth(), - header.getPixelHeight(), - header.getPixelDepth(), - 1, // num Samples - header.getNumberOfSlices(), - (isGPUKTXPayload ? gpuktxKeyValue._samplerDesc : sampler)); - - tex->setUsage((isGPUKTXPayload ? gpuktxKeyValue._usage : usage)); - - // Assing the mips availables - tex->setStoredMipFormat(mipFormat); - uint16_t level = 0; - for (auto& image : srcData->_images) { - for (uint32_t face = 0; face < image._numFaces; face++) { - tex->assignStoredMipFace(level, face, image._faceSize, image._faceBytes[face]); - } - level++; - } - - return tex; -} - -bool Texture::evalKTXFormat(const Element& mipFormat, const Element& texelFormat, ktx::Header& header) { - if (texelFormat == Format::COLOR_RGBA_32 && mipFormat == Format::COLOR_BGRA_32) { - header.setUncompressed(ktx::GLType::UNSIGNED_BYTE, 1, ktx::GLFormat::BGRA, ktx::GLInternalFormat_Uncompressed::RGBA8, ktx::GLBaseInternalFormat::RGBA); - } else if (texelFormat == Format::COLOR_RGBA_32 && mipFormat == Format::COLOR_RGBA_32) { - header.setUncompressed(ktx::GLType::UNSIGNED_BYTE, 1, ktx::GLFormat::RGBA, ktx::GLInternalFormat_Uncompressed::RGBA8, ktx::GLBaseInternalFormat::RGBA); - } else if (texelFormat == Format::COLOR_SRGBA_32 && mipFormat == Format::COLOR_SBGRA_32) { - header.setUncompressed(ktx::GLType::UNSIGNED_BYTE, 1, ktx::GLFormat::BGRA, ktx::GLInternalFormat_Uncompressed::SRGB8_ALPHA8, ktx::GLBaseInternalFormat::RGBA); - } else if (texelFormat == Format::COLOR_SRGBA_32 && mipFormat == Format::COLOR_SRGBA_32) { - header.setUncompressed(ktx::GLType::UNSIGNED_BYTE, 1, ktx::GLFormat::RGBA, ktx::GLInternalFormat_Uncompressed::SRGB8_ALPHA8, ktx::GLBaseInternalFormat::RGBA); - } else if (texelFormat == Format::COLOR_R_8 && mipFormat == Format::COLOR_R_8) { - header.setUncompressed(ktx::GLType::UNSIGNED_BYTE, 1, ktx::GLFormat::RED, ktx::GLInternalFormat_Uncompressed::R8, ktx::GLBaseInternalFormat::RED); - } else { - return false; - } - - return true; -} - -bool Texture::evalTextureFormat(const ktx::Header& header, Element& mipFormat, Element& texelFormat) { - if (header.getGLFormat() == ktx::GLFormat::BGRA && header.getGLType() == ktx::GLType::UNSIGNED_BYTE && header.getTypeSize() == 1) { - if (header.getGLInternaFormat_Uncompressed() == ktx::GLInternalFormat_Uncompressed::RGBA8) { - mipFormat = Format::COLOR_BGRA_32; - texelFormat = Format::COLOR_RGBA_32; - } else if (header.getGLInternaFormat_Uncompressed() == ktx::GLInternalFormat_Uncompressed::SRGB8_ALPHA8) { - mipFormat = Format::COLOR_SBGRA_32; - texelFormat = Format::COLOR_SRGBA_32; - } else { - return false; - } - } else if (header.getGLFormat() == ktx::GLFormat::RGBA && header.getGLType() == ktx::GLType::UNSIGNED_BYTE && header.getTypeSize() == 1) { - if (header.getGLInternaFormat_Uncompressed() == ktx::GLInternalFormat_Uncompressed::RGBA8) { - mipFormat = Format::COLOR_RGBA_32; - texelFormat = Format::COLOR_RGBA_32; - } else if (header.getGLInternaFormat_Uncompressed() == ktx::GLInternalFormat_Uncompressed::SRGB8_ALPHA8) { - mipFormat = Format::COLOR_SRGBA_32; - texelFormat = Format::COLOR_SRGBA_32; - } else { - return false; - } - } else if (header.getGLFormat() == ktx::GLFormat::RED && header.getGLType() == ktx::GLType::UNSIGNED_BYTE && header.getTypeSize() == 1) { - mipFormat = Format::COLOR_R_8; - if (header.getGLInternaFormat_Uncompressed() == ktx::GLInternalFormat_Uncompressed::R8) { - texelFormat = Format::COLOR_R_8; - } else { - return false; - } - } else { - return false; - } - return true; -} diff --git a/libraries/ktx/CMakeLists.txt b/libraries/ktx/CMakeLists.txt deleted file mode 100644 index 404660b247..0000000000 --- a/libraries/ktx/CMakeLists.txt +++ /dev/null @@ -1,3 +0,0 @@ -set(TARGET_NAME ktx) -setup_hifi_library() -link_hifi_libraries() \ No newline at end of file diff --git a/libraries/ktx/src/ktx/KTX.cpp b/libraries/ktx/src/ktx/KTX.cpp deleted file mode 100644 index bbd4e1bc86..0000000000 --- a/libraries/ktx/src/ktx/KTX.cpp +++ /dev/null @@ -1,165 +0,0 @@ -// -// KTX.cpp -// ktx/src/ktx -// -// Created by Zach Pomerantz on 2/08/2017. -// Copyright 2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include "KTX.h" - -#include //min max and more - -using namespace ktx; - -uint32_t Header::evalPadding(size_t byteSize) { - //auto padding = byteSize % PACKING_SIZE; - // return (uint32_t) (padding ? PACKING_SIZE - padding : 0); - return (uint32_t) (3 - (byteSize + 3) % PACKING_SIZE);// padding ? PACKING_SIZE - padding : 0); -} - - -const Header::Identifier ktx::Header::IDENTIFIER {{ - 0xAB, 0x4B, 0x54, 0x58, 0x20, 0x31, 0x31, 0xBB, 0x0D, 0x0A, 0x1A, 0x0A -}}; - -Header::Header() { - memcpy(identifier, IDENTIFIER.data(), IDENTIFIER_LENGTH); -} - -uint32_t Header::evalMaxDimension() const { - return std::max(getPixelWidth(), std::max(getPixelHeight(), getPixelDepth())); -} - -uint32_t Header::evalPixelWidth(uint32_t level) const { - return std::max(getPixelWidth() >> level, 1U); -} -uint32_t Header::evalPixelHeight(uint32_t level) const { - return std::max(getPixelHeight() >> level, 1U); -} -uint32_t Header::evalPixelDepth(uint32_t level) const { - return std::max(getPixelDepth() >> level, 1U); -} - -size_t Header::evalPixelSize() const { - return glTypeSize; // Really we should generate the size from the FOrmat etc -} - -size_t Header::evalRowSize(uint32_t level) const { - auto pixWidth = evalPixelWidth(level); - auto pixSize = evalPixelSize(); - auto netSize = pixWidth * pixSize; - auto padding = evalPadding(netSize); - return netSize + padding; -} -size_t Header::evalFaceSize(uint32_t level) const { - auto pixHeight = evalPixelHeight(level); - auto pixDepth = evalPixelDepth(level); - auto rowSize = evalRowSize(level); - return pixDepth * pixHeight * rowSize; -} -size_t Header::evalImageSize(uint32_t level) const { - auto faceSize = evalFaceSize(level); - if (numberOfFaces == NUM_CUBEMAPFACES && numberOfArrayElements == 0) { - return faceSize; - } else { - return (getNumberOfSlices() * numberOfFaces * faceSize); - } -} - - -KeyValue::KeyValue(const std::string& key, uint32_t valueByteSize, const Byte* value) : - _byteSize((uint32_t) key.size() + 1 + valueByteSize), // keyString size + '\0' ending char + the value size - _key(key), - _value(valueByteSize) -{ - if (_value.size() && value) { - memcpy(_value.data(), value, valueByteSize); - } -} - -KeyValue::KeyValue(const std::string& key, const std::string& value) : - KeyValue(key, (uint32_t) value.size(), (const Byte*) value.data()) -{ - -} - -uint32_t KeyValue::serializedByteSize() const { - return (uint32_t) (sizeof(uint32_t) + _byteSize + Header::evalPadding(_byteSize)); -} - -uint32_t KeyValue::serializedKeyValuesByteSize(const KeyValues& keyValues) { - uint32_t keyValuesSize = 0; - for (auto& keyval : keyValues) { - keyValuesSize += keyval.serializedByteSize(); - } - return (keyValuesSize + Header::evalPadding(keyValuesSize)); -} - - -KTX::KTX() { -} - -KTX::~KTX() { -} - -void KTX::resetStorage(const StoragePointer& storage) { - _storage = storage; -} - -const Header* KTX::getHeader() const { - if (!_storage) { - return nullptr; - } - return reinterpret_cast(_storage->data()); -} - - -size_t KTX::getKeyValueDataSize() const { - if (_storage) { - return getHeader()->bytesOfKeyValueData; - } else { - return 0; - } -} - -size_t KTX::getTexelsDataSize() const { - if (_storage) { - //return _storage->size() - (sizeof(Header) + getKeyValueDataSize()); - return (_storage->data() + _storage->size()) - getTexelsData(); - } else { - return 0; - } -} - -const Byte* KTX::getKeyValueData() const { - if (_storage) { - return (_storage->data() + sizeof(Header)); - } else { - return nullptr; - } -} - -const Byte* KTX::getTexelsData() const { - if (_storage) { - return (_storage->data() + sizeof(Header) + getKeyValueDataSize()); - } else { - return nullptr; - } -} - -storage::StoragePointer KTX::getMipFaceTexelsData(uint16_t mip, uint8_t face) const { - storage::StoragePointer result; - if (mip < _images.size()) { - const auto& faces = _images[mip]; - if (face < faces._numFaces) { - auto faceOffset = faces._faceBytes[face] - _storage->data(); - auto faceSize = faces._faceSize; - result = _storage->createView(faceSize, faceOffset); - } - } - return result; -} diff --git a/libraries/ktx/src/ktx/KTX.h b/libraries/ktx/src/ktx/KTX.h deleted file mode 100644 index 8e901b1105..0000000000 --- a/libraries/ktx/src/ktx/KTX.h +++ /dev/null @@ -1,494 +0,0 @@ -// -// KTX.h -// ktx/src/ktx -// -// Created by Zach Pomerantz on 2/08/2017. -// Copyright 2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// -#pragma once -#ifndef hifi_ktx_KTX_h -#define hifi_ktx_KTX_h - -#include -#include -#include -#include -#include -#include -#include - -#include - -/* KTX Spec: - -Byte[12] identifier -UInt32 endianness -UInt32 glType -UInt32 glTypeSize -UInt32 glFormat -Uint32 glInternalFormat -Uint32 glBaseInternalFormat -UInt32 pixelWidth -UInt32 pixelHeight -UInt32 pixelDepth -UInt32 numberOfArrayElements -UInt32 numberOfFaces -UInt32 numberOfMipmapLevels -UInt32 bytesOfKeyValueData - -for each keyValuePair that fits in bytesOfKeyValueData - UInt32 keyAndValueByteSize - Byte keyAndValue[keyAndValueByteSize] - Byte valuePadding[3 - ((keyAndValueByteSize + 3) % 4)] -end - -for each mipmap_level in numberOfMipmapLevels* - UInt32 imageSize; - for each array_element in numberOfArrayElements* - for each face in numberOfFaces - for each z_slice in pixelDepth* - for each row or row_of_blocks in pixelHeight* - for each pixel or block_of_pixels in pixelWidth - Byte data[format-specific-number-of-bytes]** - end - end - end - Byte cubePadding[0-3] - end - end - Byte mipPadding[3 - ((imageSize + 3) % 4)] -end - -* Replace with 1 if this field is 0. - -** Uncompressed texture data matches a GL_UNPACK_ALIGNMENT of 4. -*/ - - - -namespace ktx { - const uint32_t PACKING_SIZE { sizeof(uint32_t) }; - using Byte = uint8_t; - - enum class GLType : uint32_t { - COMPRESSED_TYPE = 0, - - // GL 4.4 Table 8.2 - UNSIGNED_BYTE = 0x1401, - BYTE = 0x1400, - UNSIGNED_SHORT = 0x1403, - SHORT = 0x1402, - UNSIGNED_INT = 0x1405, - INT = 0x1404, - HALF_FLOAT = 0x140B, - FLOAT = 0x1406, - UNSIGNED_BYTE_3_3_2 = 0x8032, - UNSIGNED_BYTE_2_3_3_REV = 0x8362, - UNSIGNED_SHORT_5_6_5 = 0x8363, - UNSIGNED_SHORT_5_6_5_REV = 0x8364, - UNSIGNED_SHORT_4_4_4_4 = 0x8033, - UNSIGNED_SHORT_4_4_4_4_REV = 0x8365, - UNSIGNED_SHORT_5_5_5_1 = 0x8034, - UNSIGNED_SHORT_1_5_5_5_REV = 0x8366, - UNSIGNED_INT_8_8_8_8 = 0x8035, - UNSIGNED_INT_8_8_8_8_REV = 0x8367, - UNSIGNED_INT_10_10_10_2 = 0x8036, - UNSIGNED_INT_2_10_10_10_REV = 0x8368, - UNSIGNED_INT_24_8 = 0x84FA, - UNSIGNED_INT_10F_11F_11F_REV = 0x8C3B, - UNSIGNED_INT_5_9_9_9_REV = 0x8C3E, - FLOAT_32_UNSIGNED_INT_24_8_REV = 0x8DAD, - - NUM_GLTYPES = 25, - }; - - enum class GLFormat : uint32_t { - COMPRESSED_FORMAT = 0, - - // GL 4.4 Table 8.3 - STENCIL_INDEX = 0x1901, - DEPTH_COMPONENT = 0x1902, - DEPTH_STENCIL = 0x84F9, - - RED = 0x1903, - GREEN = 0x1904, - BLUE = 0x1905, - RG = 0x8227, - RGB = 0x1907, - RGBA = 0x1908, - BGR = 0x80E0, - BGRA = 0x80E1, - - RG_INTEGER = 0x8228, - RED_INTEGER = 0x8D94, - GREEN_INTEGER = 0x8D95, - BLUE_INTEGER = 0x8D96, - RGB_INTEGER = 0x8D98, - RGBA_INTEGER = 0x8D99, - BGR_INTEGER = 0x8D9A, - BGRA_INTEGER = 0x8D9B, - - NUM_GLFORMATS = 20, - }; - - enum class GLInternalFormat_Uncompressed : uint32_t { - // GL 4.4 Table 8.12 - R8 = 0x8229, - R8_SNORM = 0x8F94, - - R16 = 0x822A, - R16_SNORM = 0x8F98, - - RG8 = 0x822B, - RG8_SNORM = 0x8F95, - - RG16 = 0x822C, - RG16_SNORM = 0x8F99, - - R3_G3_B2 = 0x2A10, - RGB4 = 0x804F, - RGB5 = 0x8050, - RGB565 = 0x8D62, - - RGB8 = 0x8051, - RGB8_SNORM = 0x8F96, - RGB10 = 0x8052, - RGB12 = 0x8053, - - RGB16 = 0x8054, - RGB16_SNORM = 0x8F9A, - - RGBA2 = 0x8055, - RGBA4 = 0x8056, - RGB5_A1 = 0x8057, - RGBA8 = 0x8058, - RGBA8_SNORM = 0x8F97, - - RGB10_A2 = 0x8059, - RGB10_A2UI = 0x906F, - - RGBA12 = 0x805A, - RGBA16 = 0x805B, - RGBA16_SNORM = 0x8F9B, - - SRGB8 = 0x8C41, - SRGB8_ALPHA8 = 0x8C43, - - R16F = 0x822D, - RG16F = 0x822F, - RGB16F = 0x881B, - RGBA16F = 0x881A, - - R32F = 0x822E, - RG32F = 0x8230, - RGB32F = 0x8815, - RGBA32F = 0x8814, - - R11F_G11F_B10F = 0x8C3A, - RGB9_E5 = 0x8C3D, - - - R8I = 0x8231, - R8UI = 0x8232, - R16I = 0x8233, - R16UI = 0x8234, - R32I = 0x8235, - R32UI = 0x8236, - RG8I = 0x8237, - RG8UI = 0x8238, - RG16I = 0x8239, - RG16UI = 0x823A, - RG32I = 0x823B, - RG32UI = 0x823C, - - RGB8I = 0x8D8F, - RGB8UI = 0x8D7D, - RGB16I = 0x8D89, - RGB16UI = 0x8D77, - - RGB32I = 0x8D83, - RGB32UI = 0x8D71, - RGBA8I = 0x8D8E, - RGBA8UI = 0x8D7C, - RGBA16I = 0x8D88, - RGBA16UI = 0x8D76, - RGBA32I = 0x8D82, - - RGBA32UI = 0x8D70, - - // GL 4.4 Table 8.13 - DEPTH_COMPONENT16 = 0x81A5, - DEPTH_COMPONENT24 = 0x81A6, - DEPTH_COMPONENT32 = 0x81A7, - - DEPTH_COMPONENT32F = 0x8CAC, - DEPTH24_STENCIL8 = 0x88F0, - DEPTH32F_STENCIL8 = 0x8CAD, - - STENCIL_INDEX1 = 0x8D46, - STENCIL_INDEX4 = 0x8D47, - STENCIL_INDEX8 = 0x8D48, - STENCIL_INDEX16 = 0x8D49, - - NUM_UNCOMPRESSED_GLINTERNALFORMATS = 74, - }; - - enum class GLInternalFormat_Compressed : uint32_t { - // GL 4.4 Table 8.14 - COMPRESSED_RED = 0x8225, - COMPRESSED_RG = 0x8226, - COMPRESSED_RGB = 0x84ED, - COMPRESSED_RGBA = 0x84EE, - - COMPRESSED_SRGB = 0x8C48, - COMPRESSED_SRGB_ALPHA = 0x8C49, - - COMPRESSED_RED_RGTC1 = 0x8DBB, - COMPRESSED_SIGNED_RED_RGTC1 = 0x8DBC, - COMPRESSED_RG_RGTC2 = 0x8DBD, - COMPRESSED_SIGNED_RG_RGTC2 = 0x8DBE, - - COMPRESSED_RGBA_BPTC_UNORM = 0x8E8C, - COMPRESSED_SRGB_ALPHA_BPTC_UNORM = 0x8E8D, - COMPRESSED_RGB_BPTC_SIGNED_FLOAT = 0x8E8E, - COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT = 0x8E8F, - - COMPRESSED_RGB8_ETC2 = 0x9274, - COMPRESSED_SRGB8_ETC2 = 0x9275, - COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2 = 0x9276, - COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2 = 0x9277, - COMPRESSED_RGBA8_ETC2_EAC = 0x9278, - COMPRESSED_SRGB8_ALPHA8_ETC2_EAC = 0x9279, - - COMPRESSED_R11_EAC = 0x9270, - COMPRESSED_SIGNED_R11_EAC = 0x9271, - COMPRESSED_RG11_EAC = 0x9272, - COMPRESSED_SIGNED_RG11_EAC = 0x9273, - - NUM_COMPRESSED_GLINTERNALFORMATS = 24, - }; - - enum class GLBaseInternalFormat : uint32_t { - // GL 4.4 Table 8.11 - DEPTH_COMPONENT = 0x1902, - DEPTH_STENCIL = 0x84F9, - RED = 0x1903, - RG = 0x8227, - RGB = 0x1907, - RGBA = 0x1908, - STENCIL_INDEX = 0x1901, - - NUM_GLBASEINTERNALFORMATS = 7, - }; - - enum CubeMapFace { - POS_X = 0, - NEG_X = 1, - POS_Y = 2, - NEG_Y = 3, - POS_Z = 4, - NEG_Z = 5, - NUM_CUBEMAPFACES = 6, - }; - - using Storage = storage::Storage; - using StoragePointer = std::shared_ptr; - - // Header - struct Header { - static const size_t IDENTIFIER_LENGTH = 12; - using Identifier = std::array; - static const Identifier IDENTIFIER; - - static const uint32_t ENDIAN_TEST = 0x04030201; - static const uint32_t REVERSE_ENDIAN_TEST = 0x01020304; - - static uint32_t evalPadding(size_t byteSize); - - Header(); - - Byte identifier[IDENTIFIER_LENGTH]; - uint32_t endianness { ENDIAN_TEST }; - - uint32_t glType; - uint32_t glTypeSize { 0 }; - uint32_t glFormat; - uint32_t glInternalFormat; - uint32_t glBaseInternalFormat; - - uint32_t pixelWidth { 1 }; - uint32_t pixelHeight { 0 }; - uint32_t pixelDepth { 0 }; - uint32_t numberOfArrayElements { 0 }; - uint32_t numberOfFaces { 1 }; - uint32_t numberOfMipmapLevels { 1 }; - - uint32_t bytesOfKeyValueData { 0 }; - - uint32_t getPixelWidth() const { return (pixelWidth ? pixelWidth : 1); } - uint32_t getPixelHeight() const { return (pixelHeight ? pixelHeight : 1); } - uint32_t getPixelDepth() const { return (pixelDepth ? pixelDepth : 1); } - uint32_t getNumberOfSlices() const { return (numberOfArrayElements ? numberOfArrayElements : 1); } - uint32_t getNumberOfLevels() const { return (numberOfMipmapLevels ? numberOfMipmapLevels : 1); } - - uint32_t evalMaxDimension() const; - uint32_t evalPixelWidth(uint32_t level) const; - uint32_t evalPixelHeight(uint32_t level) const; - uint32_t evalPixelDepth(uint32_t level) const; - - size_t evalPixelSize() const; - size_t evalRowSize(uint32_t level) const; - size_t evalFaceSize(uint32_t level) const; - size_t evalImageSize(uint32_t level) const; - - void setUncompressed(GLType type, uint32_t typeSize, GLFormat format, GLInternalFormat_Uncompressed internalFormat, GLBaseInternalFormat baseInternalFormat) { - glType = (uint32_t) type; - glTypeSize = typeSize; - glFormat = (uint32_t) format; - glInternalFormat = (uint32_t) internalFormat; - glBaseInternalFormat = (uint32_t) baseInternalFormat; - } - void setCompressed(GLInternalFormat_Compressed internalFormat, GLBaseInternalFormat baseInternalFormat) { - glType = (uint32_t) GLType::COMPRESSED_TYPE; - glTypeSize = 1; - glFormat = (uint32_t) GLFormat::COMPRESSED_FORMAT; - glInternalFormat = (uint32_t) internalFormat; - glBaseInternalFormat = (uint32_t) baseInternalFormat; - } - - GLType getGLType() const { return (GLType)glType; } - uint32_t getTypeSize() const { return glTypeSize; } - GLFormat getGLFormat() const { return (GLFormat)glFormat; } - GLInternalFormat_Uncompressed getGLInternaFormat_Uncompressed() const { return (GLInternalFormat_Uncompressed)glInternalFormat; } - GLInternalFormat_Compressed getGLInternaFormat_Compressed() const { return (GLInternalFormat_Compressed)glInternalFormat; } - GLBaseInternalFormat getGLBaseInternalFormat() const { return (GLBaseInternalFormat)glBaseInternalFormat; } - - - void setDimensions(uint32_t width, uint32_t height = 0, uint32_t depth = 0, uint32_t numSlices = 0, uint32_t numFaces = 1) { - pixelWidth = (width > 0 ? width : 1); - pixelHeight = height; - pixelDepth = depth; - numberOfArrayElements = numSlices; - numberOfFaces = ((numFaces == 1) || (numFaces == NUM_CUBEMAPFACES) ? numFaces : 1); - } - void set1D(uint32_t width) { setDimensions(width); } - void set1DArray(uint32_t width, uint32_t numSlices) { setDimensions(width, 0, 0, (numSlices > 0 ? numSlices : 1)); } - void set2D(uint32_t width, uint32_t height) { setDimensions(width, height); } - void set2DArray(uint32_t width, uint32_t height, uint32_t numSlices) { setDimensions(width, height, 0, (numSlices > 0 ? numSlices : 1)); } - void set3D(uint32_t width, uint32_t height, uint32_t depth) { setDimensions(width, height, depth); } - void set3DArray(uint32_t width, uint32_t height, uint32_t depth, uint32_t numSlices) { setDimensions(width, height, depth, (numSlices > 0 ? numSlices : 1)); } - void setCube(uint32_t width, uint32_t height) { setDimensions(width, height, 0, 0, NUM_CUBEMAPFACES); } - void setCubeArray(uint32_t width, uint32_t height, uint32_t numSlices) { setDimensions(width, height, 0, (numSlices > 0 ? numSlices : 1), NUM_CUBEMAPFACES); } - - }; - - // Key Values - struct KeyValue { - uint32_t _byteSize { 0 }; - std::string _key; - std::vector _value; - - - KeyValue(const std::string& key, uint32_t valueByteSize, const Byte* value); - - KeyValue(const std::string& key, const std::string& value); - - uint32_t serializedByteSize() const; - - static KeyValue parseSerializedKeyAndValue(uint32_t srcSize, const Byte* srcBytes); - static uint32_t writeSerializedKeyAndValue(Byte* destBytes, uint32_t destByteSize, const KeyValue& keyval); - - using KeyValues = std::list; - static uint32_t serializedKeyValuesByteSize(const KeyValues& keyValues); - - }; - using KeyValues = KeyValue::KeyValues; - - - struct Image { - using FaceBytes = std::vector; - - uint32_t _numFaces{ 1 }; - uint32_t _imageSize; - uint32_t _faceSize; - uint32_t _padding; - FaceBytes _faceBytes; - - - Image(uint32_t imageSize, uint32_t padding, const Byte* bytes) : - _numFaces(1), - _imageSize(imageSize), - _faceSize(imageSize), - _padding(padding), - _faceBytes(1, bytes) {} - - Image(uint32_t pageSize, uint32_t padding, const FaceBytes& cubeFaceBytes) : - _numFaces(NUM_CUBEMAPFACES), - _imageSize(pageSize * NUM_CUBEMAPFACES), - _faceSize(pageSize), - _padding(padding) - { - if (cubeFaceBytes.size() == NUM_CUBEMAPFACES) { - _faceBytes = cubeFaceBytes; - } - } - }; - using Images = std::vector; - - class KTX { - void resetStorage(const StoragePointer& src); - - KTX(); - public: - - ~KTX(); - - // Define a KTX object manually to write it somewhere (in a file on disk?) - // This path allocate the Storage where to store header, keyvalues and copy mips - // Then COPY all the data - static std::unique_ptr create(const Header& header, const Images& images, const KeyValues& keyValues = KeyValues()); - - // Instead of creating a full Copy of the src data in a KTX object, the write serialization can be performed with the - // following two functions - // size_t sizeNeeded = KTX::evalStorageSize(header, images); - // - // //allocate a buffer of size "sizeNeeded" or map a file with enough capacity - // Byte* destBytes = new Byte[sizeNeeded]; - // - // // THen perform the writing of the src data to the destinnation buffer - // write(destBytes, sizeNeeded, header, images); - // - // This is exactly what is done in the create function - static size_t evalStorageSize(const Header& header, const Images& images, const KeyValues& keyValues = KeyValues()); - static size_t write(Byte* destBytes, size_t destByteSize, const Header& header, const Images& images, const KeyValues& keyValues = KeyValues()); - static size_t writeKeyValues(Byte* destBytes, size_t destByteSize, const KeyValues& keyValues); - static Images writeImages(Byte* destBytes, size_t destByteSize, const Images& images); - - // Parse a block of memory and create a KTX object from it - static std::unique_ptr create(const StoragePointer& src); - - static bool checkHeaderFromStorage(size_t srcSize, const Byte* srcBytes); - static KeyValues parseKeyValues(size_t srcSize, const Byte* srcBytes); - static Images parseImages(const Header& header, size_t srcSize, const Byte* srcBytes); - - // Access raw pointers to the main sections of the KTX - const Header* getHeader() const; - const Byte* getKeyValueData() const; - const Byte* getTexelsData() const; - storage::StoragePointer getMipFaceTexelsData(uint16_t mip = 0, uint8_t face = 0) const; - const StoragePointer& getStorage() const { return _storage; } - - size_t getKeyValueDataSize() const; - size_t getTexelsDataSize() const; - - StoragePointer _storage; - KeyValues _keyValues; - Images _images; - }; - -} - -#endif // hifi_ktx_KTX_h diff --git a/libraries/ktx/src/ktx/Reader.cpp b/libraries/ktx/src/ktx/Reader.cpp deleted file mode 100644 index 277ce42e69..0000000000 --- a/libraries/ktx/src/ktx/Reader.cpp +++ /dev/null @@ -1,195 +0,0 @@ -// -// Reader.cpp -// ktx/src/ktx -// -// Created by Zach Pomerantz on 2/08/2017. -// Copyright 2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// -#include "KTX.h" - -#include -#include -#include - -#ifndef _MSC_VER -#define NOEXCEPT noexcept -#else -#define NOEXCEPT -#endif - -namespace ktx { - class ReaderException: public std::exception { - public: - ReaderException(const std::string& explanation) : _explanation("KTX deserialization error: " + explanation) {} - const char* what() const NOEXCEPT override { return _explanation.c_str(); } - private: - const std::string _explanation; - }; - - bool checkEndianness(uint32_t endianness, bool& matching) { - switch (endianness) { - case Header::ENDIAN_TEST: { - matching = true; - return true; - } - break; - case Header::REVERSE_ENDIAN_TEST: - { - matching = false; - return true; - } - break; - default: - throw ReaderException("endianness field has invalid value"); - return false; - } - } - - bool checkIdentifier(const Byte* identifier) { - if (!(0 == memcmp(identifier, Header::IDENTIFIER.data(), Header::IDENTIFIER_LENGTH))) { - throw ReaderException("identifier field invalid"); - return false; - } - return true; - } - - bool KTX::checkHeaderFromStorage(size_t srcSize, const Byte* srcBytes) { - try { - // validation - if (srcSize < sizeof(Header)) { - throw ReaderException("length is too short for header"); - } - const Header* header = reinterpret_cast(srcBytes); - - checkIdentifier(header->identifier); - - bool endianMatch { true }; - checkEndianness(header->endianness, endianMatch); - - // TODO: endian conversion if !endianMatch - for now, this is for local use and is unnecessary - - - // TODO: calculated bytesOfTexData - if (srcSize < (sizeof(Header) + header->bytesOfKeyValueData)) { - throw ReaderException("length is too short for metadata"); - } - - size_t bytesOfTexData = 0; - if (srcSize < (sizeof(Header) + header->bytesOfKeyValueData + bytesOfTexData)) { - - throw ReaderException("length is too short for data"); - } - - return true; - } - catch (const ReaderException& e) { - qWarning() << e.what(); - return false; - } - } - - KeyValue KeyValue::parseSerializedKeyAndValue(uint32_t srcSize, const Byte* srcBytes) { - uint32_t keyAndValueByteSize; - memcpy(&keyAndValueByteSize, srcBytes, sizeof(uint32_t)); - if (keyAndValueByteSize + sizeof(uint32_t) > srcSize) { - throw ReaderException("invalid key-value size"); - } - auto keyValueBytes = srcBytes + sizeof(uint32_t); - - // find the first null character \0 and extract the key - uint32_t keyLength = 0; - while (reinterpret_cast(keyValueBytes)[++keyLength] != '\0') { - if (keyLength == keyAndValueByteSize) { - // key must be null-terminated, and there must be space for the value - throw ReaderException("invalid key-value " + std::string(reinterpret_cast(keyValueBytes), keyLength)); - } - } - uint32_t valueStartOffset = keyLength + 1; - - // parse the key-value - return KeyValue(std::string(reinterpret_cast(keyValueBytes), keyLength), - keyAndValueByteSize - valueStartOffset, keyValueBytes + valueStartOffset); - } - - KeyValues KTX::parseKeyValues(size_t srcSize, const Byte* srcBytes) { - KeyValues keyValues; - try { - auto src = srcBytes; - uint32_t length = (uint32_t) srcSize; - uint32_t offset = 0; - while (offset < length) { - auto keyValue = KeyValue::parseSerializedKeyAndValue(length - offset, src); - keyValues.emplace_back(keyValue); - - // advance offset/src - offset += keyValue.serializedByteSize(); - src += keyValue.serializedByteSize(); - } - } - catch (const ReaderException& e) { - qWarning() << e.what(); - } - return keyValues; - } - - Images KTX::parseImages(const Header& header, size_t srcSize, const Byte* srcBytes) { - Images images; - auto currentPtr = srcBytes; - auto numFaces = header.numberOfFaces; - - // Keep identifying new mip as long as we can at list query the next imageSize - while ((currentPtr - srcBytes) + sizeof(uint32_t) <= (srcSize)) { - - // Grab the imageSize coming up - size_t imageSize = *reinterpret_cast(currentPtr); - currentPtr += sizeof(uint32_t); - - // If enough data ahead then capture the pointer - if ((currentPtr - srcBytes) + imageSize <= (srcSize)) { - auto padding = Header::evalPadding(imageSize); - - if (numFaces == NUM_CUBEMAPFACES) { - size_t faceSize = imageSize / NUM_CUBEMAPFACES; - Image::FaceBytes faces(NUM_CUBEMAPFACES); - for (uint32_t face = 0; face < NUM_CUBEMAPFACES; face++) { - faces[face] = currentPtr; - currentPtr += faceSize; - } - images.emplace_back(Image((uint32_t) faceSize, padding, faces)); - currentPtr += padding; - } else { - images.emplace_back(Image((uint32_t) imageSize, padding, currentPtr)); - currentPtr += imageSize + padding; - } - } else { - break; - } - } - - return images; - } - - std::unique_ptr KTX::create(const StoragePointer& src) { - if (!src) { - return nullptr; - } - - if (!checkHeaderFromStorage(src->size(), src->data())) { - return nullptr; - } - - std::unique_ptr result(new KTX()); - result->resetStorage(src); - - // read metadata - result->_keyValues = parseKeyValues(result->getHeader()->bytesOfKeyValueData, result->getKeyValueData()); - - // populate image table - result->_images = parseImages(*result->getHeader(), result->getTexelsDataSize(), result->getTexelsData()); - - return result; - } -} diff --git a/libraries/ktx/src/ktx/Writer.cpp b/libraries/ktx/src/ktx/Writer.cpp deleted file mode 100644 index 25b363d31b..0000000000 --- a/libraries/ktx/src/ktx/Writer.cpp +++ /dev/null @@ -1,171 +0,0 @@ -// -// Writer.cpp -// ktx/src/ktx -// -// Created by Zach Pomerantz on 2/08/2017. -// Copyright 2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// -#include "KTX.h" - - -#include -#include -#ifndef _MSC_VER -#define NOEXCEPT noexcept -#else -#define NOEXCEPT -#endif - -namespace ktx { - - class WriterException : public std::exception { - public: - WriterException(const std::string& explanation) : _explanation("KTX serialization error: " + explanation) {} - const char* what() const NOEXCEPT override { return _explanation.c_str(); } - private: - const std::string _explanation; - }; - - std::unique_ptr KTX::create(const Header& header, const Images& images, const KeyValues& keyValues) { - StoragePointer storagePointer; - { - auto storageSize = ktx::KTX::evalStorageSize(header, images, keyValues); - auto memoryStorage = new storage::MemoryStorage(storageSize); - ktx::KTX::write(memoryStorage->data(), memoryStorage->size(), header, images, keyValues); - storagePointer.reset(memoryStorage); - } - return create(storagePointer); - } - - size_t KTX::evalStorageSize(const Header& header, const Images& images, const KeyValues& keyValues) { - size_t storageSize = sizeof(Header); - - if (!keyValues.empty()) { - size_t keyValuesSize = KeyValue::serializedKeyValuesByteSize(keyValues); - storageSize += keyValuesSize; - } - - auto numMips = header.getNumberOfLevels(); - for (uint32_t l = 0; l < numMips; l++) { - if (images.size() > l) { - storageSize += sizeof(uint32_t); - storageSize += images[l]._imageSize; - storageSize += Header::evalPadding(images[l]._imageSize); - } - } - return storageSize; - } - - size_t KTX::write(Byte* destBytes, size_t destByteSize, const Header& header, const Images& srcImages, const KeyValues& keyValues) { - // Check again that we have enough destination capacity - if (!destBytes || (destByteSize < evalStorageSize(header, srcImages, keyValues))) { - return 0; - } - - auto currentDestPtr = destBytes; - // Header - auto destHeader = reinterpret_cast(currentDestPtr); - memcpy(currentDestPtr, &header, sizeof(Header)); - currentDestPtr += sizeof(Header); - - // KeyValues - if (!keyValues.empty()) { - destHeader->bytesOfKeyValueData = (uint32_t) writeKeyValues(currentDestPtr, destByteSize - sizeof(Header), keyValues); - } else { - // Make sure the header contains the right bytesOfKeyValueData size - destHeader->bytesOfKeyValueData = 0; - } - currentDestPtr += destHeader->bytesOfKeyValueData; - - // Images - auto destImages = writeImages(currentDestPtr, destByteSize - sizeof(Header) - destHeader->bytesOfKeyValueData, srcImages); - // We chould check here that the amoutn of dest IMages generated is the same as the source - - return destByteSize; - } - - uint32_t KeyValue::writeSerializedKeyAndValue(Byte* destBytes, uint32_t destByteSize, const KeyValue& keyval) { - uint32_t keyvalSize = keyval.serializedByteSize(); - if (keyvalSize > destByteSize) { - throw WriterException("invalid key-value size"); - } - - *((uint32_t*) destBytes) = keyval._byteSize; - - auto dest = destBytes + sizeof(uint32_t); - - auto keySize = keyval._key.size() + 1; // Add 1 for the '\0' character at the end of the string - memcpy(dest, keyval._key.data(), keySize); - dest += keySize; - - memcpy(dest, keyval._value.data(), keyval._value.size()); - - return keyvalSize; - } - - size_t KTX::writeKeyValues(Byte* destBytes, size_t destByteSize, const KeyValues& keyValues) { - size_t writtenByteSize = 0; - try { - auto dest = destBytes; - for (auto& keyval : keyValues) { - size_t keyvalSize = KeyValue::writeSerializedKeyAndValue(dest, (uint32_t) (destByteSize - writtenByteSize), keyval); - writtenByteSize += keyvalSize; - dest += keyvalSize; - } - } - catch (const WriterException& e) { - qWarning() << e.what(); - } - return writtenByteSize; - } - - Images KTX::writeImages(Byte* destBytes, size_t destByteSize, const Images& srcImages) { - Images destImages; - auto imagesDataPtr = destBytes; - if (!imagesDataPtr) { - return destImages; - } - auto allocatedImagesDataSize = destByteSize; - size_t currentDataSize = 0; - auto currentPtr = imagesDataPtr; - - for (uint32_t l = 0; l < srcImages.size(); l++) { - if (currentDataSize + sizeof(uint32_t) < allocatedImagesDataSize) { - size_t imageSize = srcImages[l]._imageSize; - *(reinterpret_cast (currentPtr)) = (uint32_t) imageSize; - currentPtr += sizeof(uint32_t); - currentDataSize += sizeof(uint32_t); - - // If enough data ahead then capture the copy source pointer - if (currentDataSize + imageSize <= (allocatedImagesDataSize)) { - auto padding = Header::evalPadding(imageSize); - - // Single face vs cubes - if (srcImages[l]._numFaces == 1) { - memcpy(currentPtr, srcImages[l]._faceBytes[0], imageSize); - destImages.emplace_back(Image((uint32_t) imageSize, padding, currentPtr)); - currentPtr += imageSize; - } else { - Image::FaceBytes faceBytes(NUM_CUBEMAPFACES); - auto faceSize = srcImages[l]._faceSize; - for (int face = 0; face < NUM_CUBEMAPFACES; face++) { - memcpy(currentPtr, srcImages[l]._faceBytes[face], faceSize); - faceBytes[face] = currentPtr; - currentPtr += faceSize; - } - destImages.emplace_back(Image(faceSize, padding, faceBytes)); - } - - currentPtr += padding; - currentDataSize += imageSize + padding; - } - } - } - - return destImages; - } - -} diff --git a/libraries/model-networking/CMakeLists.txt b/libraries/model-networking/CMakeLists.txt index 00aa17ff57..ed8cd7b5f9 100644 --- a/libraries/model-networking/CMakeLists.txt +++ b/libraries/model-networking/CMakeLists.txt @@ -1,4 +1,4 @@ set(TARGET_NAME model-networking) setup_hifi_library() -link_hifi_libraries(shared networking model fbx ktx) +link_hifi_libraries(shared networking model fbx) diff --git a/libraries/model-networking/src/model-networking/KTXCache.cpp b/libraries/model-networking/src/model-networking/KTXCache.cpp deleted file mode 100644 index 63d35fe4a4..0000000000 --- a/libraries/model-networking/src/model-networking/KTXCache.cpp +++ /dev/null @@ -1,47 +0,0 @@ -// -// KTXCache.cpp -// libraries/model-networking/src -// -// Created by Zach Pomerantz on 2/22/2017. -// Copyright 2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include "KTXCache.h" - -#include - -using File = cache::File; -using FilePointer = cache::FilePointer; - -KTXCache::KTXCache(const std::string& dir, const std::string& ext) : - FileCache(dir, ext) { - initialize(); -} - -KTXFilePointer KTXCache::writeFile(const char* data, Metadata&& metadata) { - FilePointer file = FileCache::writeFile(data, std::move(metadata)); - return std::static_pointer_cast(file); -} - -KTXFilePointer KTXCache::getFile(const Key& key) { - return std::static_pointer_cast(FileCache::getFile(key)); -} - -std::unique_ptr KTXCache::createFile(Metadata&& metadata, const std::string& filepath) { - qCInfo(file_cache) << "Wrote KTX" << metadata.key.c_str(); - return std::unique_ptr(new KTXFile(std::move(metadata), filepath)); -} - -KTXFile::KTXFile(Metadata&& metadata, const std::string& filepath) : - cache::File(std::move(metadata), filepath) {} - -std::unique_ptr KTXFile::getKTX() const { - ktx::StoragePointer storage = std::make_shared(getFilepath().c_str()); - if (*storage) { - return ktx::KTX::create(storage); - } - return {}; -} diff --git a/libraries/model-networking/src/model-networking/KTXCache.h b/libraries/model-networking/src/model-networking/KTXCache.h deleted file mode 100644 index 4ef5e52721..0000000000 --- a/libraries/model-networking/src/model-networking/KTXCache.h +++ /dev/null @@ -1,51 +0,0 @@ -// -// KTXCache.h -// libraries/model-networking/src -// -// Created by Zach Pomerantz 2/22/2017. -// Copyright 2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#ifndef hifi_KTXCache_h -#define hifi_KTXCache_h - -#include - -#include - -namespace ktx { - class KTX; -} - -class KTXFile; -using KTXFilePointer = std::shared_ptr; - -class KTXCache : public cache::FileCache { - Q_OBJECT - -public: - KTXCache(const std::string& dir, const std::string& ext); - - KTXFilePointer writeFile(const char* data, Metadata&& metadata); - KTXFilePointer getFile(const Key& key); - -protected: - std::unique_ptr createFile(Metadata&& metadata, const std::string& filepath) override final; -}; - -class KTXFile : public cache::File { - Q_OBJECT - -public: - std::unique_ptr getKTX() const; - -protected: - friend class KTXCache; - - KTXFile(Metadata&& metadata, const std::string& filepath); -}; - -#endif // hifi_KTXCache_h diff --git a/libraries/model-networking/src/model-networking/TextureCache.cpp b/libraries/model-networking/src/model-networking/TextureCache.cpp index 5dfaddd471..8a4e85cfe6 100644 --- a/libraries/model-networking/src/model-networking/TextureCache.cpp +++ b/libraries/model-networking/src/model-networking/TextureCache.cpp @@ -18,37 +18,27 @@ #include #include #include - -#if DEBUG_DUMP_TEXTURE_LOADS #include #include -#endif #include #include #include -#include - #include #include #include +#include #include "ModelNetworkingLogging.h" #include #include Q_LOGGING_CATEGORY(trace_resource_parse_image, "trace.resource.parse.image") -Q_LOGGING_CATEGORY(trace_resource_parse_image_raw, "trace.resource.parse.image.raw") -Q_LOGGING_CATEGORY(trace_resource_parse_image_ktx, "trace.resource.parse.image.ktx") -const std::string TextureCache::KTX_DIRNAME { "ktx_cache" }; -const std::string TextureCache::KTX_EXT { "ktx" }; - -TextureCache::TextureCache() : - _ktxCache(KTX_DIRNAME, KTX_EXT) { +TextureCache::TextureCache() { setUnusedResourceCacheSize(0); setObjectName("TextureCache"); @@ -71,7 +61,7 @@ TextureCache::~TextureCache() { // this list taken from Ken Perlin's Improved Noise reference implementation (orig. in Java) at // http://mrl.nyu.edu/~perlin/noise/ -const int permutation[256] = +const int permutation[256] = { 151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23, 190, 6, 148, @@ -118,8 +108,7 @@ const gpu::TexturePointer& TextureCache::getPermutationNormalTexture() { } _permutationNormalTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element(gpu::VEC3, gpu::NUINT8, gpu::RGB), 256, 2)); - _permutationNormalTexture->setStoredMipFormat(_permutationNormalTexture->getTexelFormat()); - _permutationNormalTexture->assignStoredMip(0, sizeof(data), data); + _permutationNormalTexture->assignStoredMip(0, _blueTexture->getTexelFormat(), sizeof(data), data); } return _permutationNormalTexture; } @@ -131,40 +120,36 @@ const unsigned char OPAQUE_BLACK[] = { 0x00, 0x00, 0x00, 0xFF }; const gpu::TexturePointer& TextureCache::getWhiteTexture() { if (!_whiteTexture) { - _whiteTexture = gpu::TexturePointer(gpu::Texture::createStrict(gpu::Element::COLOR_RGBA_32, 1, 1)); + _whiteTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, 1, 1)); _whiteTexture->setSource("TextureCache::_whiteTexture"); - _whiteTexture->setStoredMipFormat(_whiteTexture->getTexelFormat()); - _whiteTexture->assignStoredMip(0, sizeof(OPAQUE_WHITE), OPAQUE_WHITE); + _whiteTexture->assignStoredMip(0, _whiteTexture->getTexelFormat(), sizeof(OPAQUE_WHITE), OPAQUE_WHITE); } return _whiteTexture; } const gpu::TexturePointer& TextureCache::getGrayTexture() { if (!_grayTexture) { - _grayTexture = gpu::TexturePointer(gpu::Texture::createStrict(gpu::Element::COLOR_RGBA_32, 1, 1)); + _grayTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, 1, 1)); _grayTexture->setSource("TextureCache::_grayTexture"); - _grayTexture->setStoredMipFormat(_grayTexture->getTexelFormat()); - _grayTexture->assignStoredMip(0, sizeof(OPAQUE_GRAY), OPAQUE_GRAY); + _grayTexture->assignStoredMip(0, _grayTexture->getTexelFormat(), sizeof(OPAQUE_GRAY), OPAQUE_GRAY); } return _grayTexture; } const gpu::TexturePointer& TextureCache::getBlueTexture() { if (!_blueTexture) { - _blueTexture = gpu::TexturePointer(gpu::Texture::createStrict(gpu::Element::COLOR_RGBA_32, 1, 1)); + _blueTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, 1, 1)); _blueTexture->setSource("TextureCache::_blueTexture"); - _blueTexture->setStoredMipFormat(_blueTexture->getTexelFormat()); - _blueTexture->assignStoredMip(0, sizeof(OPAQUE_BLUE), OPAQUE_BLUE); + _blueTexture->assignStoredMip(0, _blueTexture->getTexelFormat(), sizeof(OPAQUE_BLUE), OPAQUE_BLUE); } return _blueTexture; } const gpu::TexturePointer& TextureCache::getBlackTexture() { if (!_blackTexture) { - _blackTexture = gpu::TexturePointer(gpu::Texture::createStrict(gpu::Element::COLOR_RGBA_32, 1, 1)); + _blackTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, 1, 1)); _blackTexture->setSource("TextureCache::_blackTexture"); - _blackTexture->setStoredMipFormat(_blackTexture->getTexelFormat()); - _blackTexture->assignStoredMip(0, sizeof(OPAQUE_BLACK), OPAQUE_BLACK); + _blackTexture->assignStoredMip(0, _blackTexture->getTexelFormat(), sizeof(OPAQUE_BLACK), OPAQUE_BLACK); } return _blackTexture; } @@ -188,72 +173,6 @@ NetworkTexturePointer TextureCache::getTexture(const QUrl& url, Type type, const return ResourceCache::getResource(url, QUrl(), &extra).staticCast(); } -gpu::TexturePointer TextureCache::getTextureByHash(const std::string& hash) { - std::weak_ptr weakPointer; - { - std::unique_lock lock(_texturesByHashesMutex); - weakPointer = _texturesByHashes[hash]; - } - auto result = weakPointer.lock(); - if (result) { - qCWarning(modelnetworking) << "QQQ Returning live texture for hash " << hash.c_str(); - } - return result; -} - -gpu::TexturePointer TextureCache::cacheTextureByHash(const std::string& hash, const gpu::TexturePointer& texture) { - gpu::TexturePointer result; - { - std::unique_lock lock(_texturesByHashesMutex); - result = _texturesByHashes[hash].lock(); - if (!result) { - _texturesByHashes[hash] = texture; - result = texture; - } else { - qCWarning(modelnetworking) << "QQQ Swapping out texture with previous live texture in hash " << hash.c_str(); - } - } - return result; -} - - -gpu::TexturePointer getFallbackTextureForType(NetworkTexture::Type type) { - gpu::TexturePointer result; - auto textureCache = DependencyManager::get(); - // Since this can be called on a background thread, there's a chance that the cache - // will be destroyed by the time we request it - if (!textureCache) { - return result; - } - switch (type) { - case NetworkTexture::DEFAULT_TEXTURE: - case NetworkTexture::ALBEDO_TEXTURE: - case NetworkTexture::ROUGHNESS_TEXTURE: - case NetworkTexture::OCCLUSION_TEXTURE: - result = textureCache->getWhiteTexture(); - break; - - case NetworkTexture::NORMAL_TEXTURE: - result = textureCache->getBlueTexture(); - break; - - case NetworkTexture::EMISSIVE_TEXTURE: - case NetworkTexture::LIGHTMAP_TEXTURE: - result = textureCache->getBlackTexture(); - break; - - case NetworkTexture::BUMP_TEXTURE: - case NetworkTexture::SPECULAR_TEXTURE: - case NetworkTexture::GLOSS_TEXTURE: - case NetworkTexture::CUBE_TEXTURE: - case NetworkTexture::CUSTOM_TEXTURE: - case NetworkTexture::STRICT_TEXTURE: - default: - break; - } - return result; -} - NetworkTexture::TextureLoaderFunc getTextureLoaderForType(NetworkTexture::Type type, const QVariantMap& options = QVariantMap()) { @@ -300,16 +219,11 @@ NetworkTexture::TextureLoaderFunc getTextureLoaderForType(NetworkTexture::Type t return model::TextureUsage::createMetallicTextureFromImage; break; } - case Type::STRICT_TEXTURE: { - return model::TextureUsage::createStrict2DTextureFromImage; - break; - } case Type::CUSTOM_TEXTURE: { Q_ASSERT(false); return NetworkTexture::TextureLoaderFunc(); break; } - case Type::DEFAULT_TEXTURE: default: { return model::TextureUsage::create2DTextureFromImage; @@ -331,8 +245,8 @@ QSharedPointer TextureCache::createResource(const QUrl& url, const QSh auto type = textureExtra ? textureExtra->type : Type::DEFAULT_TEXTURE; auto content = textureExtra ? textureExtra->content : QByteArray(); auto maxNumPixels = textureExtra ? textureExtra->maxNumPixels : ABSOLUTE_MAX_TEXTURE_NUM_PIXELS; - NetworkTexture* texture = new NetworkTexture(url, type, content, maxNumPixels); - return QSharedPointer(texture, &Resource::deleter); + return QSharedPointer(new NetworkTexture(url, type, content, maxNumPixels), + &Resource::deleter); } NetworkTexture::NetworkTexture(const QUrl& url, Type type, const QByteArray& content, int maxNumPixels) : @@ -346,6 +260,7 @@ NetworkTexture::NetworkTexture(const QUrl& url, Type type, const QByteArray& con _loaded = true; } + std::string theName = url.toString().toStdString(); // if we have content, load it after we have our self pointer if (!content.isEmpty()) { _startedLoading = true; @@ -353,6 +268,12 @@ NetworkTexture::NetworkTexture(const QUrl& url, Type type, const QByteArray& con } } +NetworkTexture::NetworkTexture(const QUrl& url, const TextureLoaderFunc& textureLoader, const QByteArray& content) : + NetworkTexture(url, CUSTOM_TEXTURE, content, ABSOLUTE_MAX_TEXTURE_NUM_PIXELS) +{ + _textureLoader = textureLoader; +} + NetworkTexture::TextureLoaderFunc NetworkTexture::getTextureLoader() const { if (_type == CUSTOM_TEXTURE) { return _textureLoader; @@ -360,6 +281,149 @@ NetworkTexture::TextureLoaderFunc NetworkTexture::getTextureLoader() const { return getTextureLoaderForType(_type); } + +class ImageReader : public QRunnable { +public: + + ImageReader(const QWeakPointer& resource, const QByteArray& data, + const QUrl& url = QUrl(), int maxNumPixels = ABSOLUTE_MAX_TEXTURE_NUM_PIXELS); + + virtual void run() override; + +private: + static void listSupportedImageFormats(); + + QWeakPointer _resource; + QUrl _url; + QByteArray _content; + int _maxNumPixels; +}; + +void NetworkTexture::downloadFinished(const QByteArray& data) { + // send the reader off to the thread pool + QThreadPool::globalInstance()->start(new ImageReader(_self, data, _url)); +} + +void NetworkTexture::loadContent(const QByteArray& content) { + QThreadPool::globalInstance()->start(new ImageReader(_self, content, _url, _maxNumPixels)); +} + +ImageReader::ImageReader(const QWeakPointer& resource, const QByteArray& data, + const QUrl& url, int maxNumPixels) : + _resource(resource), + _url(url), + _content(data), + _maxNumPixels(maxNumPixels) +{ +#if DEBUG_DUMP_TEXTURE_LOADS + static auto start = usecTimestampNow() / USECS_PER_MSEC; + auto now = usecTimestampNow() / USECS_PER_MSEC - start; + QString urlStr = _url.toString(); + auto dot = urlStr.lastIndexOf("."); + QString outFileName = QString(QCryptographicHash::hash(urlStr.toLocal8Bit(), QCryptographicHash::Md5).toHex()) + urlStr.right(urlStr.length() - dot); + QFile loadRecord("h:/textures/loads.txt"); + loadRecord.open(QFile::Text | QFile::Append | QFile::ReadWrite); + loadRecord.write(QString("%1 %2\n").arg(now).arg(outFileName).toLocal8Bit()); + outFileName = "h:/textures/" + outFileName; + QFileInfo outInfo(outFileName); + if (!outInfo.exists()) { + QFile outFile(outFileName); + outFile.open(QFile::WriteOnly | QFile::Truncate); + outFile.write(data); + outFile.close(); + } +#endif + DependencyManager::get()->incrementStat("PendingProcessing"); +} + +void ImageReader::listSupportedImageFormats() { + static std::once_flag once; + std::call_once(once, []{ + auto supportedFormats = QImageReader::supportedImageFormats(); + qCDebug(modelnetworking) << "List of supported Image formats:" << supportedFormats.join(", "); + }); +} + +void ImageReader::run() { + DependencyManager::get()->decrementStat("PendingProcessing"); + + CounterStat counter("Processing"); + + PROFILE_RANGE_EX(resource_parse_image, __FUNCTION__, 0xffff0000, 0, { { "url", _url.toString() } }); + auto originalPriority = QThread::currentThread()->priority(); + if (originalPriority == QThread::InheritPriority) { + originalPriority = QThread::NormalPriority; + } + QThread::currentThread()->setPriority(QThread::LowPriority); + Finally restorePriority([originalPriority]{ + QThread::currentThread()->setPriority(originalPriority); + }); + + if (!_resource.data()) { + qCWarning(modelnetworking) << "Abandoning load of" << _url << "; could not get strong ref"; + return; + } + listSupportedImageFormats(); + + // Help the QImage loader by extracting the image file format from the url filename ext. + // Some tga are not created properly without it. + auto filename = _url.fileName().toStdString(); + auto filenameExtension = filename.substr(filename.find_last_of('.') + 1); + QImage image = QImage::fromData(_content, filenameExtension.c_str()); + + // Note that QImage.format is the pixel format which is different from the "format" of the image file... + auto imageFormat = image.format(); + int imageWidth = image.width(); + int imageHeight = image.height(); + + if (imageWidth == 0 || imageHeight == 0 || imageFormat == QImage::Format_Invalid) { + if (filenameExtension.empty()) { + qCDebug(modelnetworking) << "QImage failed to create from content, no file extension:" << _url; + } else { + qCDebug(modelnetworking) << "QImage failed to create from content" << _url; + } + return; + } + + if (imageWidth * imageHeight > _maxNumPixels) { + float scaleFactor = sqrtf(_maxNumPixels / (float)(imageWidth * imageHeight)); + int originalWidth = imageWidth; + int originalHeight = imageHeight; + imageWidth = (int)(scaleFactor * (float)imageWidth + 0.5f); + imageHeight = (int)(scaleFactor * (float)imageHeight + 0.5f); + QImage newImage = image.scaled(QSize(imageWidth, imageHeight), Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + image.swap(newImage); + qCDebug(modelnetworking) << "Downscale image" << _url + << "from" << originalWidth << "x" << originalHeight + << "to" << imageWidth << "x" << imageHeight; + } + + gpu::TexturePointer texture = nullptr; + { + // Double-check the resource still exists between long operations. + auto resource = _resource.toStrongRef(); + if (!resource) { + qCWarning(modelnetworking) << "Abandoning load of" << _url << "; could not get strong ref"; + return; + } + + auto url = _url.toString().toStdString(); + + PROFILE_RANGE_EX(resource_parse_image, __FUNCTION__, 0xffffff00, 0); + texture.reset(resource.dynamicCast()->getTextureLoader()(image, url)); + } + + // Ensure the resource has not been deleted + auto resource = _resource.toStrongRef(); + if (!resource) { + qCWarning(modelnetworking) << "Abandoning load of" << _url << "; could not get strong ref"; + } else { + QMetaObject::invokeMethod(resource.data(), "setImage", + Q_ARG(gpu::TexturePointer, texture), + Q_ARG(int, imageWidth), Q_ARG(int, imageHeight)); + } +} + void NetworkTexture::setImage(gpu::TexturePointer texture, int originalWidth, int originalHeight) { _originalWidth = originalWidth; @@ -382,231 +446,3 @@ void NetworkTexture::setImage(gpu::TexturePointer texture, int originalWidth, emit networkTextureCreated(qWeakPointerCast (_self)); } - -gpu::TexturePointer NetworkTexture::getFallbackTexture() const { - if (_type == CUSTOM_TEXTURE) { - return gpu::TexturePointer(); - } - return getFallbackTextureForType(_type); -} - -class Reader : public QRunnable { -public: - Reader(const QWeakPointer& resource, const QUrl& url); - void run() override final; - virtual void read() = 0; - -protected: - QWeakPointer _resource; - QUrl _url; -}; - -class ImageReader : public Reader { -public: - ImageReader(const QWeakPointer& resource, const QUrl& url, - const QByteArray& data, const std::string& hash, int maxNumPixels); - void read() override final; - -private: - static void listSupportedImageFormats(); - - QByteArray _content; - std::string _hash; - int _maxNumPixels; -}; - -void NetworkTexture::downloadFinished(const QByteArray& data) { - loadContent(data); -} - -void NetworkTexture::loadContent(const QByteArray& content) { - // Hash the source image to for KTX caching - std::string hash; - { - QCryptographicHash hasher(QCryptographicHash::Md5); - hasher.addData(content); - hash = hasher.result().toHex().toStdString(); - } - - auto textureCache = static_cast(_cache.data()); - - if (textureCache != nullptr) { - // If we already have a live texture with the same hash, use it - auto texture = textureCache->getTextureByHash(hash); - - // If there is no live texture, check if there's an existing KTX file - if (!texture) { - KTXFilePointer ktxFile = textureCache->_ktxCache.getFile(hash); - if (ktxFile) { - // Ensure that the KTX deserialization worked - auto ktx = ktxFile->getKTX(); - if (ktx) { - texture.reset(gpu::Texture::unserialize(ktx)); - // Ensure that the texture population worked - if (texture) { - texture->setKtxBacking(ktx); - texture = textureCache->cacheTextureByHash(hash, texture); - } - } - } - } - - // If we found the texture either because it's in use or via KTX deserialization, - // set the image and return immediately. - if (texture) { - setImage(texture, texture->getWidth(), texture->getHeight()); - return; - } - } - - // We failed to find an existing live or KTX texture, so trigger an image reader - QThreadPool::globalInstance()->start(new ImageReader(_self, _url, content, hash, _maxNumPixels)); -} - -Reader::Reader(const QWeakPointer& resource, const QUrl& url) : - _resource(resource), _url(url) { - DependencyManager::get()->incrementStat("PendingProcessing"); -} - -void Reader::run() { - PROFILE_RANGE_EX(resource_parse_image, __FUNCTION__, 0xffff0000, 0, { { "url", _url.toString() } }); - DependencyManager::get()->decrementStat("PendingProcessing"); - CounterStat counter("Processing"); - - auto originalPriority = QThread::currentThread()->priority(); - if (originalPriority == QThread::InheritPriority) { - originalPriority = QThread::NormalPriority; - } - QThread::currentThread()->setPriority(QThread::LowPriority); - Finally restorePriority([originalPriority]{ QThread::currentThread()->setPriority(originalPriority); }); - - if (!_resource.data()) { - qCWarning(modelnetworking) << "Abandoning load of" << _url << "; could not get strong ref"; - return; - } - - read(); -} - -ImageReader::ImageReader(const QWeakPointer& resource, const QUrl& url, - const QByteArray& data, const std::string& hash, int maxNumPixels) : - Reader(resource, url), _content(data), _hash(hash), _maxNumPixels(maxNumPixels) { - listSupportedImageFormats(); - -#if DEBUG_DUMP_TEXTURE_LOADS - static auto start = usecTimestampNow() / USECS_PER_MSEC; - auto now = usecTimestampNow() / USECS_PER_MSEC - start; - QString urlStr = _url.toString(); - auto dot = urlStr.lastIndexOf("."); - QString outFileName = QString(QCryptographicHash::hash(urlStr.toLocal8Bit(), QCryptographicHash::Md5).toHex()) + urlStr.right(urlStr.length() - dot); - QFile loadRecord("h:/textures/loads.txt"); - loadRecord.open(QFile::Text | QFile::Append | QFile::ReadWrite); - loadRecord.write(QString("%1 %2\n").arg(now).arg(outFileName).toLocal8Bit()); - outFileName = "h:/textures/" + outFileName; - QFileInfo outInfo(outFileName); - if (!outInfo.exists()) { - QFile outFile(outFileName); - outFile.open(QFile::WriteOnly | QFile::Truncate); - outFile.write(data); - outFile.close(); - } -#endif -} - -void ImageReader::listSupportedImageFormats() { - static std::once_flag once; - std::call_once(once, []{ - auto supportedFormats = QImageReader::supportedImageFormats(); - qCDebug(modelnetworking) << "List of supported Image formats:" << supportedFormats.join(", "); - }); -} - -void ImageReader::read() { - // Help the QImage loader by extracting the image file format from the url filename ext. - // Some tga are not created properly without it. - auto filename = _url.fileName().toStdString(); - auto filenameExtension = filename.substr(filename.find_last_of('.') + 1); - QImage image = QImage::fromData(_content, filenameExtension.c_str()); - int imageWidth = image.width(); - int imageHeight = image.height(); - - // Validate that the image loaded - if (imageWidth == 0 || imageHeight == 0 || image.format() == QImage::Format_Invalid) { - QString reason(filenameExtension.empty() ? "" : "(no file extension)"); - qCWarning(modelnetworking) << "Failed to load" << _url << reason; - return; - } - - // Validate the image is less than _maxNumPixels, and downscale if necessary - if (imageWidth * imageHeight > _maxNumPixels) { - float scaleFactor = sqrtf(_maxNumPixels / (float)(imageWidth * imageHeight)); - int originalWidth = imageWidth; - int originalHeight = imageHeight; - imageWidth = (int)(scaleFactor * (float)imageWidth + 0.5f); - imageHeight = (int)(scaleFactor * (float)imageHeight + 0.5f); - QImage newImage = image.scaled(QSize(imageWidth, imageHeight), Qt::IgnoreAspectRatio, Qt::SmoothTransformation); - image.swap(newImage); - qCDebug(modelnetworking).nospace() << "Downscaled " << _url << " (" << - QSize(originalWidth, originalHeight) << " to " << - QSize(imageWidth, imageHeight) << ")"; - } - - gpu::TexturePointer texture = nullptr; - { - auto resource = _resource.lock(); // to ensure the resource is still needed - if (!resource) { - qCDebug(modelnetworking) << _url << "loading stopped; resource out of scope"; - return; - } - - auto url = _url.toString().toStdString(); - - PROFILE_RANGE_EX(resource_parse_image_raw, __FUNCTION__, 0xffff0000, 0); - // Load the image into a gpu::Texture - auto networkTexture = resource.staticCast(); - texture.reset(networkTexture->getTextureLoader()(image, url)); - texture->setSource(url); - if (texture) { - texture->setFallbackTexture(networkTexture->getFallbackTexture()); - } - - auto textureCache = DependencyManager::get(); - // Save the image into a KTXFile - auto memKtx = gpu::Texture::serialize(*texture); - if (!memKtx) { - qCWarning(modelnetworking) << "Unable to serialize texture to KTX " << _url; - } - - if (memKtx && textureCache) { - const char* data = reinterpret_cast(memKtx->_storage->data()); - size_t length = memKtx->_storage->size(); - KTXFilePointer file; - auto& ktxCache = textureCache->_ktxCache; - if (!memKtx || !(file = ktxCache.writeFile(data, KTXCache::Metadata(_hash, length)))) { - qCWarning(modelnetworking) << _url << "file cache failed"; - } else { - resource.staticCast()->_file = file; - auto fileKtx = file->getKTX(); - if (fileKtx) { - texture->setKtxBacking(fileKtx); - } - } - } - - // We replace the texture with the one stored in the cache. This deals with the possible race condition of two different - // images with the same hash being loaded concurrently. Only one of them will make it into the cache by hash first and will - // be the winner - if (textureCache) { - texture = textureCache->cacheTextureByHash(_hash, texture); - } - } - - auto resource = _resource.lock(); // to ensure the resource is still needed - if (resource) { - QMetaObject::invokeMethod(resource.data(), "setImage", - Q_ARG(gpu::TexturePointer, texture), - Q_ARG(int, imageWidth), Q_ARG(int, imageHeight)); - } else { - qCDebug(modelnetworking) << _url << "loading stopped; resource out of scope"; - } -} diff --git a/libraries/model-networking/src/model-networking/TextureCache.h b/libraries/model-networking/src/model-networking/TextureCache.h index 6005cc1226..77311afae6 100644 --- a/libraries/model-networking/src/model-networking/TextureCache.h +++ b/libraries/model-networking/src/model-networking/TextureCache.h @@ -23,8 +23,6 @@ #include #include -#include "KTXCache.h" - const int ABSOLUTE_MAX_TEXTURE_NUM_PIXELS = 8192 * 8192; namespace gpu { @@ -45,7 +43,6 @@ class NetworkTexture : public Resource, public Texture { public: enum Type { DEFAULT_TEXTURE, - STRICT_TEXTURE, ALBEDO_TEXTURE, NORMAL_TEXTURE, BUMP_TEXTURE, @@ -66,6 +63,7 @@ public: using TextureLoaderFunc = std::function; NetworkTexture(const QUrl& url, Type type, const QByteArray& content, int maxNumPixels); + NetworkTexture(const QUrl& url, const TextureLoaderFunc& textureLoader, const QByteArray& content); QString getType() const override { return "NetworkTexture"; } @@ -76,12 +74,12 @@ public: Type getTextureType() const { return _type; } TextureLoaderFunc getTextureLoader() const; - gpu::TexturePointer getFallbackTexture() const; signals: void networkTextureCreated(const QWeakPointer& self); protected: + virtual bool isCacheable() const override { return _loaded; } virtual void downloadFinished(const QByteArray& data) override; @@ -90,12 +88,8 @@ protected: Q_INVOKABLE void setImage(gpu::TexturePointer texture, int originalWidth, int originalHeight); private: - friend class KTXReader; - friend class ImageReader; - Type _type; TextureLoaderFunc _textureLoader { [](const QImage&, const std::string&){ return nullptr; } }; - KTXFilePointer _file; int _originalWidth { 0 }; int _originalHeight { 0 }; int _width { 0 }; @@ -137,10 +131,6 @@ public: NetworkTexturePointer getTexture(const QUrl& url, Type type = Type::DEFAULT_TEXTURE, const QByteArray& content = QByteArray(), int maxNumPixels = ABSOLUTE_MAX_TEXTURE_NUM_PIXELS); - - gpu::TexturePointer getTextureByHash(const std::string& hash); - gpu::TexturePointer cacheTextureByHash(const std::string& hash, const gpu::TexturePointer& texture); - protected: // Overload ResourceCache::prefetch to allow specifying texture type for loads Q_INVOKABLE ScriptableResource* prefetch(const QUrl& url, int type, int maxNumPixels = ABSOLUTE_MAX_TEXTURE_NUM_PIXELS); @@ -149,19 +139,9 @@ protected: const void* extra) override; private: - friend class ImageReader; - friend class NetworkTexture; - friend class DilatableNetworkTexture; - TextureCache(); virtual ~TextureCache(); - - static const std::string KTX_DIRNAME; - static const std::string KTX_EXT; - KTXCache _ktxCache; - // Map from image hashes to texture weak pointers - std::unordered_map> _texturesByHashes; - std::mutex _texturesByHashesMutex; + friend class DilatableNetworkTexture; gpu::TexturePointer _permutationNormalTexture; gpu::TexturePointer _whiteTexture; diff --git a/libraries/model/CMakeLists.txt b/libraries/model/CMakeLists.txt index 021aa3d027..63f632e484 100755 --- a/libraries/model/CMakeLists.txt +++ b/libraries/model/CMakeLists.txt @@ -1,5 +1,5 @@ set(TARGET_NAME model) AUTOSCRIBE_SHADER_LIB(gpu model) setup_hifi_library() -link_hifi_libraries(shared ktx gpu) +link_hifi_libraries(shared gpu) diff --git a/libraries/model/src/model/Geometry.cpp b/libraries/model/src/model/Geometry.cpp index 04b0db92d3..2bb6cfa436 100755 --- a/libraries/model/src/model/Geometry.cpp +++ b/libraries/model/src/model/Geometry.cpp @@ -117,7 +117,7 @@ Box Mesh::evalPartsBound(int partStart, int partEnd) const { auto partItEnd = _partBuffer.cbegin() + partEnd; for (;part != partItEnd; part++) { - + Box partBound; auto index = _indexBuffer.cbegin() + (*part)._startIndex; auto endIndex = index + (*part)._numIndices; @@ -134,115 +134,6 @@ Box Mesh::evalPartsBound(int partStart, int partEnd) const { return totalBound; } - -model::MeshPointer Mesh::map(std::function vertexFunc, - std::function normalFunc, - std::function indexFunc) { - // vertex data - const gpu::BufferView& vertexBufferView = getVertexBuffer(); - gpu::BufferView::Index numVertices = (gpu::BufferView::Index)getNumVertices(); - gpu::Resource::Size vertexSize = numVertices * sizeof(glm::vec3); - unsigned char* resultVertexData = new unsigned char[vertexSize]; - unsigned char* vertexDataCursor = resultVertexData; - - for (gpu::BufferView::Index i = 0; i < numVertices; i ++) { - glm::vec3 pos = vertexFunc(vertexBufferView.get(i)); - memcpy(vertexDataCursor, &pos, sizeof(pos)); - vertexDataCursor += sizeof(pos); - } - - // normal data - int attributeTypeNormal = gpu::Stream::InputSlot::NORMAL; // libraries/gpu/src/gpu/Stream.h - const gpu::BufferView& normalsBufferView = getAttributeBuffer(attributeTypeNormal); - gpu::BufferView::Index numNormals = (gpu::BufferView::Index)normalsBufferView.getNumElements(); - gpu::Resource::Size normalSize = numNormals * sizeof(glm::vec3); - unsigned char* resultNormalData = new unsigned char[normalSize]; - unsigned char* normalDataCursor = resultNormalData; - - for (gpu::BufferView::Index i = 0; i < numNormals; i ++) { - glm::vec3 normal = normalFunc(normalsBufferView.get(i)); - memcpy(normalDataCursor, &normal, sizeof(normal)); - normalDataCursor += sizeof(normal); - } - // TODO -- other attributes - - // face data - const gpu::BufferView& indexBufferView = getIndexBuffer(); - gpu::BufferView::Index numIndexes = (gpu::BufferView::Index)getNumIndices(); - gpu::Resource::Size indexSize = numIndexes * sizeof(uint32_t); - unsigned char* resultIndexData = new unsigned char[indexSize]; - unsigned char* indexDataCursor = resultIndexData; - - for (gpu::BufferView::Index i = 0; i < numIndexes; i ++) { - uint32_t index = indexFunc(indexBufferView.get(i)); - memcpy(indexDataCursor, &index, sizeof(index)); - indexDataCursor += sizeof(index); - } - - model::MeshPointer result(new model::Mesh()); - - gpu::Element vertexElement = gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ); - gpu::Buffer* resultVertexBuffer = new gpu::Buffer(vertexSize, resultVertexData); - gpu::BufferPointer resultVertexBufferPointer(resultVertexBuffer); - gpu::BufferView resultVertexBufferView(resultVertexBufferPointer, vertexElement); - result->setVertexBuffer(resultVertexBufferView); - - gpu::Element normalElement = gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ); - gpu::Buffer* resultNormalsBuffer = new gpu::Buffer(normalSize, resultNormalData); - gpu::BufferPointer resultNormalsBufferPointer(resultNormalsBuffer); - gpu::BufferView resultNormalsBufferView(resultNormalsBufferPointer, normalElement); - result->addAttribute(attributeTypeNormal, resultNormalsBufferView); - - gpu::Element indexElement = gpu::Element(gpu::SCALAR, gpu::UINT32, gpu::RAW); - gpu::Buffer* resultIndexesBuffer = new gpu::Buffer(indexSize, resultIndexData); - gpu::BufferPointer resultIndexesBufferPointer(resultIndexesBuffer); - gpu::BufferView resultIndexesBufferView(resultIndexesBufferPointer, indexElement); - result->setIndexBuffer(resultIndexesBufferView); - - - // TODO -- shouldn't assume just one part - - std::vector parts; - parts.emplace_back(model::Mesh::Part((model::Index)0, // startIndex - (model::Index)result->getNumIndices(), // numIndices - (model::Index)0, // baseVertex - model::Mesh::TRIANGLES)); // topology - result->setPartBuffer(gpu::BufferView(new gpu::Buffer(parts.size() * sizeof(model::Mesh::Part), - (gpu::Byte*) parts.data()), gpu::Element::PART_DRAWCALL)); - - return result; -} - - -void Mesh::forEach(std::function vertexFunc, - std::function normalFunc, - std::function indexFunc) { - int attributeTypeNormal = gpu::Stream::InputSlot::NORMAL; // libraries/gpu/src/gpu/Stream.h - - // vertex data - const gpu::BufferView& vertexBufferView = getVertexBuffer(); - gpu::BufferView::Index numVertices = (gpu::BufferView::Index)getNumVertices(); - for (gpu::BufferView::Index i = 0; i < numVertices; i ++) { - vertexFunc(vertexBufferView.get(i)); - } - - // normal data - const gpu::BufferView& normalsBufferView = getAttributeBuffer(attributeTypeNormal); - gpu::BufferView::Index numNormals = (gpu::BufferView::Index) normalsBufferView.getNumElements(); - for (gpu::BufferView::Index i = 0; i < numNormals; i ++) { - normalFunc(normalsBufferView.get(i)); - } - // TODO -- other attributes - - // face data - const gpu::BufferView& indexBufferView = getIndexBuffer(); - gpu::BufferView::Index numIndexes = (gpu::BufferView::Index)getNumIndices(); - for (gpu::BufferView::Index i = 0; i < numIndexes; i ++) { - indexFunc(indexBufferView.get(i)); - } -} - - Geometry::Geometry() { } @@ -257,3 +148,4 @@ Geometry::~Geometry() { void Geometry::setMesh(const MeshPointer& mesh) { _mesh = mesh; } + diff --git a/libraries/model/src/model/Geometry.h b/libraries/model/src/model/Geometry.h index 7ba3e83407..4256f0be03 100755 --- a/libraries/model/src/model/Geometry.h +++ b/libraries/model/src/model/Geometry.h @@ -25,10 +25,6 @@ typedef AABox Box; typedef std::vector< Box > Boxes; typedef glm::vec3 Vec3; -class Mesh; -using MeshPointer = std::shared_ptr< Mesh >; - - class Mesh { public: const static Index PRIMITIVE_RESTART_INDEX = -1; @@ -118,15 +114,6 @@ public: static gpu::Primitive topologyToPrimitive(Topology topo) { return static_cast(topo); } - // create a copy of this mesh after passing its vertices, normals, and indexes though the provided functions - MeshPointer map(std::function vertexFunc, - std::function normalFunc, - std::function indexFunc); - - void forEach(std::function vertexFunc, - std::function normalFunc, - std::function indexFunc); - protected: gpu::Stream::FormatPointer _vertexFormat; @@ -143,6 +130,7 @@ protected: void evalVertexStream(); }; +using MeshPointer = std::shared_ptr< Mesh >; class Geometry { diff --git a/libraries/model/src/model/TextureMap.cpp b/libraries/model/src/model/TextureMap.cpp index d07eae2166..7ac8083d9c 100755 --- a/libraries/model/src/model/TextureMap.cpp +++ b/libraries/model/src/model/TextureMap.cpp @@ -10,15 +10,10 @@ // #include "TextureMap.h" -#include - #include #include #include -#include -#include -#include -#include + #include #include "ModelLogging.h" @@ -154,7 +149,7 @@ const QImage TextureUsage::process2DImageColor(const QImage& srcImage, bool& val return image; } -void TextureUsage::defineColorTexelFormats(gpu::Element& formatGPU, gpu::Element& formatMip, +void TextureUsage::defineColorTexelFormats(gpu::Element& formatGPU, gpu::Element& formatMip, const QImage& image, bool isLinear, bool doCompress) { #ifdef COMPRESS_TEXTURES @@ -207,7 +202,7 @@ const QImage& image, bool isLinear, bool doCompress) { #define CPU_MIPMAPS 1 -void generateMips(gpu::Texture* texture, QImage& image, bool fastResize) { +void generateMips(gpu::Texture* texture, QImage& image, gpu::Element formatMip, bool fastResize) { #if CPU_MIPMAPS PROFILE_RANGE(resource_parse, "generateMips"); auto numMips = texture->evalNumMips(); @@ -215,33 +210,32 @@ void generateMips(gpu::Texture* texture, QImage& image, bool fastResize) { QSize mipSize(texture->evalMipWidth(level), texture->evalMipHeight(level)); if (fastResize) { image = image.scaled(mipSize); - texture->assignStoredMip(level, image.byteCount(), image.constBits()); + texture->assignStoredMip(level, formatMip, image.byteCount(), image.constBits()); } else { QImage mipImage = image.scaled(mipSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); - texture->assignStoredMip(level, mipImage.byteCount(), mipImage.constBits()); + texture->assignStoredMip(level, formatMip, mipImage.byteCount(), mipImage.constBits()); } } - #else texture->autoGenerateMips(-1); #endif } -void generateFaceMips(gpu::Texture* texture, QImage& image, uint8 face) { +void generateFaceMips(gpu::Texture* texture, QImage& image, gpu::Element formatMip, uint8 face) { #if CPU_MIPMAPS PROFILE_RANGE(resource_parse, "generateFaceMips"); auto numMips = texture->evalNumMips(); for (uint16 level = 1; level < numMips; ++level) { QSize mipSize(texture->evalMipWidth(level), texture->evalMipHeight(level)); QImage mipImage = image.scaled(mipSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); - texture->assignStoredMipFace(level, face, mipImage.byteCount(), mipImage.constBits()); + texture->assignStoredMipFace(level, formatMip, mipImage.byteCount(), mipImage.constBits(), face); } #else texture->autoGenerateMips(-1); #endif } -gpu::Texture* TextureUsage::process2DTextureColorFromImage(const QImage& srcImage, const std::string& srcImageName, bool isLinear, bool doCompress, bool generateMips, bool isStrict) { +gpu::Texture* TextureUsage::process2DTextureColorFromImage(const QImage& srcImage, const std::string& srcImageName, bool isLinear, bool doCompress, bool generateMips) { PROFILE_RANGE(resource_parse, "process2DTextureColorFromImage"); bool validAlpha = false; bool alphaAsMask = true; @@ -254,11 +248,7 @@ gpu::Texture* TextureUsage::process2DTextureColorFromImage(const QImage& srcImag gpu::Element formatMip; defineColorTexelFormats(formatGPU, formatMip, image, isLinear, doCompress); - if (isStrict) { - theTexture = (gpu::Texture::createStrict(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); - } else { - theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); - } + theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); theTexture->setSource(srcImageName); auto usage = gpu::Texture::Usage::Builder().withColor(); if (validAlpha) { @@ -268,26 +258,22 @@ gpu::Texture* TextureUsage::process2DTextureColorFromImage(const QImage& srcImag } } theTexture->setUsage(usage.build()); - theTexture->setStoredMipFormat(formatMip); - theTexture->assignStoredMip(0, image.byteCount(), image.constBits()); + + theTexture->assignStoredMip(0, formatMip, image.byteCount(), image.constBits()); if (generateMips) { - ::generateMips(theTexture, image, false); + ::generateMips(theTexture, image, formatMip, false); } - theTexture->setSource(srcImageName); } return theTexture; } -gpu::Texture* TextureUsage::createStrict2DTextureFromImage(const QImage& srcImage, const std::string& srcImageName) { - return process2DTextureColorFromImage(srcImage, srcImageName, false, false, true, true); -} - gpu::Texture* TextureUsage::create2DTextureFromImage(const QImage& srcImage, const std::string& srcImageName) { return process2DTextureColorFromImage(srcImage, srcImageName, false, false, true); } + gpu::Texture* TextureUsage::createAlbedoTextureFromImage(const QImage& srcImage, const std::string& srcImageName) { return process2DTextureColorFromImage(srcImage, srcImageName, false, true, true); } @@ -305,25 +291,21 @@ gpu::Texture* TextureUsage::createNormalTextureFromNormalImage(const QImage& src PROFILE_RANGE(resource_parse, "createNormalTextureFromNormalImage"); QImage image = processSourceImage(srcImage, false); - // Make sure the normal map source image is ARGB32 - if (image.format() != QImage::Format_ARGB32) { - image = image.convertToFormat(QImage::Format_ARGB32); + // Make sure the normal map source image is RGBA32 + if (image.format() != QImage::Format_RGBA8888) { + image = image.convertToFormat(QImage::Format_RGBA8888); } - gpu::Texture* theTexture = nullptr; if ((image.width() > 0) && (image.height() > 0)) { - gpu::Element formatMip = gpu::Element::COLOR_BGRA_32; - gpu::Element formatGPU = gpu::Element::COLOR_RGBA_32; + gpu::Element formatGPU = gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA); + gpu::Element formatMip = gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA); theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); theTexture->setSource(srcImageName); - theTexture->setStoredMipFormat(formatMip); - theTexture->assignStoredMip(0, image.byteCount(), image.constBits()); - generateMips(theTexture, image, true); - - theTexture->setSource(srcImageName); + theTexture->assignStoredMip(0, formatMip, image.byteCount(), image.constBits()); + generateMips(theTexture, image, formatMip, true); } return theTexture; @@ -354,17 +336,16 @@ gpu::Texture* TextureUsage::createNormalTextureFromBumpImage(const QImage& srcIm const double pStrength = 2.0; int width = image.width(); int height = image.height(); - - QImage result(width, height, QImage::Format_ARGB32); - + QImage result(width, height, QImage::Format_RGB888); + for (int i = 0; i < width; i++) { const int iNextClamped = clampPixelCoordinate(i + 1, width - 1); const int iPrevClamped = clampPixelCoordinate(i - 1, width - 1); - + for (int j = 0; j < height; j++) { const int jNextClamped = clampPixelCoordinate(j + 1, height - 1); const int jPrevClamped = clampPixelCoordinate(j - 1, height - 1); - + // surrounding pixels const QRgb topLeft = image.pixel(iPrevClamped, jPrevClamped); const QRgb top = image.pixel(iPrevClamped, j); @@ -374,7 +355,7 @@ gpu::Texture* TextureUsage::createNormalTextureFromBumpImage(const QImage& srcIm const QRgb bottom = image.pixel(iNextClamped, j); const QRgb bottomLeft = image.pixel(iNextClamped, jPrevClamped); const QRgb left = image.pixel(i, jPrevClamped); - + // take their gray intensities // since it's a grayscale image, the value of each component RGB is the same const double tl = qRed(topLeft); @@ -385,15 +366,15 @@ gpu::Texture* TextureUsage::createNormalTextureFromBumpImage(const QImage& srcIm const double b = qRed(bottom); const double bl = qRed(bottomLeft); const double l = qRed(left); - + // apply the sobel filter const double dX = (tr + pStrength * r + br) - (tl + pStrength * l + bl); const double dY = (bl + pStrength * b + br) - (tl + pStrength * t + tr); const double dZ = RGBA_MAX / pStrength; - + glm::vec3 v(dX, dY, dZ); glm::normalize(v); - + // convert to rgb from the value obtained computing the filter QRgb qRgbValue = qRgba(mapComponent(v.x), mapComponent(v.y), mapComponent(v.z), 1.0); result.setPixel(i, j, qRgbValue); @@ -401,19 +382,13 @@ gpu::Texture* TextureUsage::createNormalTextureFromBumpImage(const QImage& srcIm } gpu::Texture* theTexture = nullptr; - if ((result.width() > 0) && (result.height() > 0)) { - - gpu::Element formatMip = gpu::Element::COLOR_BGRA_32; - gpu::Element formatGPU = gpu::Element::COLOR_RGBA_32; - - - theTexture = (gpu::Texture::create2D(formatGPU, result.width(), result.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); - theTexture->setSource(srcImageName); - theTexture->setStoredMipFormat(formatMip); - theTexture->assignStoredMip(0, result.byteCount(), result.constBits()); - generateMips(theTexture, result, true); + if ((image.width() > 0) && (image.height() > 0)) { + gpu::Element formatGPU = gpu::Element(gpu::VEC3, gpu::NUINT8, gpu::RGB); + gpu::Element formatMip = gpu::Element(gpu::VEC3, gpu::NUINT8, gpu::RGB); + theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); theTexture->setSource(srcImageName); + theTexture->assignStoredMip(0, formatMip, image.byteCount(), image.constBits()); } return theTexture; @@ -439,17 +414,16 @@ gpu::Texture* TextureUsage::createRoughnessTextureFromImage(const QImage& srcIma #ifdef COMPRESS_TEXTURES gpu::Element formatGPU = gpu::Element(gpu::SCALAR, gpu::NUINT8, gpu::COMPRESSED_R); #else - gpu::Element formatGPU = gpu::Element::COLOR_R_8; + gpu::Element formatGPU = gpu::Element(gpu::SCALAR, gpu::NUINT8, gpu::RGB); #endif - gpu::Element formatMip = gpu::Element::COLOR_R_8; + gpu::Element formatMip = gpu::Element(gpu::SCALAR, gpu::NUINT8, gpu::RGB); theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); theTexture->setSource(srcImageName); - theTexture->setStoredMipFormat(formatMip); - theTexture->assignStoredMip(0, image.byteCount(), image.constBits()); - generateMips(theTexture, image, true); + theTexture->assignStoredMip(0, formatMip, image.byteCount(), image.constBits()); + generateMips(theTexture, image, formatMip, true); - theTexture->setSource(srcImageName); + // FIXME queue for transfer to GPU and block on completion } return theTexture; @@ -470,28 +444,27 @@ gpu::Texture* TextureUsage::createRoughnessTextureFromGlossImage(const QImage& s // Gloss turned into Rough image.invertPixels(QImage::InvertRgba); - + image = image.convertToFormat(QImage::Format_Grayscale8); - + gpu::Texture* theTexture = nullptr; if ((image.width() > 0) && (image.height() > 0)) { - + #ifdef COMPRESS_TEXTURES gpu::Element formatGPU = gpu::Element(gpu::SCALAR, gpu::NUINT8, gpu::COMPRESSED_R); #else - gpu::Element formatGPU = gpu::Element::COLOR_R_8; + gpu::Element formatGPU = gpu::Element(gpu::SCALAR, gpu::NUINT8, gpu::RGB); #endif - gpu::Element formatMip = gpu::Element::COLOR_R_8; + gpu::Element formatMip = gpu::Element(gpu::SCALAR, gpu::NUINT8, gpu::RGB); theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); theTexture->setSource(srcImageName); - theTexture->setStoredMipFormat(formatMip); - theTexture->assignStoredMip(0, image.byteCount(), image.constBits()); - generateMips(theTexture, image, true); - - theTexture->setSource(srcImageName); + theTexture->assignStoredMip(0, formatMip, image.byteCount(), image.constBits()); + generateMips(theTexture, image, formatMip, true); + + // FIXME queue for transfer to GPU and block on completion } - + return theTexture; } @@ -516,17 +489,16 @@ gpu::Texture* TextureUsage::createMetallicTextureFromImage(const QImage& srcImag #ifdef COMPRESS_TEXTURES gpu::Element formatGPU = gpu::Element(gpu::SCALAR, gpu::NUINT8, gpu::COMPRESSED_R); #else - gpu::Element formatGPU = gpu::Element::COLOR_R_8; + gpu::Element formatGPU = gpu::Element(gpu::SCALAR, gpu::NUINT8, gpu::RGB); #endif - gpu::Element formatMip = gpu::Element::COLOR_R_8; + gpu::Element formatMip = gpu::Element(gpu::SCALAR, gpu::NUINT8, gpu::RGB); theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); theTexture->setSource(srcImageName); - theTexture->setStoredMipFormat(formatMip); - theTexture->assignStoredMip(0, image.byteCount(), image.constBits()); - generateMips(theTexture, image, true); + theTexture->assignStoredMip(0, formatMip, image.byteCount(), image.constBits()); + generateMips(theTexture, image, formatMip, true); - theTexture->setSource(srcImageName); + // FIXME queue for transfer to GPU and block on completion } return theTexture; @@ -549,18 +521,18 @@ public: int _y = 0; bool _horizontalMirror = false; bool _verticalMirror = false; - + Face() {} Face(int x, int y, bool horizontalMirror, bool verticalMirror) : _x(x), _y(y), _horizontalMirror(horizontalMirror), _verticalMirror(verticalMirror) {} }; - + Face _faceXPos; Face _faceXNeg; Face _faceYPos; Face _faceYNeg; Face _faceZPos; Face _faceZNeg; - + CubeLayout(int wr, int hr, Face fXP, Face fXN, Face fYP, Face fYN, Face fZP, Face fZN) : _type(FLAT), _widthRatio(wr), @@ -803,7 +775,7 @@ gpu::Texture* TextureUsage::processCubeTextureColorFromImage(const QImage& srcIm defineColorTexelFormats(formatGPU, formatMip, image, isLinear, doCompress); // Find the layout of the cubemap in the 2D image - // Use the original image size since processSourceImage may have altered the size / aspect ratio + // Use the original image size since processSourceImage may have altered the size / aspect ratio int foundLayout = CubeLayout::findLayout(srcImage.width(), srcImage.height()); std::vector faces; @@ -838,12 +810,11 @@ gpu::Texture* TextureUsage::processCubeTextureColorFromImage(const QImage& srcIm if (faces.size() == gpu::Texture::NUM_FACES_PER_TYPE[gpu::Texture::TEX_CUBE]) { theTexture = gpu::Texture::createCube(formatGPU, faces[0].width(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP)); theTexture->setSource(srcImageName); - theTexture->setStoredMipFormat(formatMip); int f = 0; for (auto& face : faces) { - theTexture->assignStoredMipFace(0, f, face.byteCount(), face.constBits()); + theTexture->assignStoredMipFace(0, formatMip, face.byteCount(), face.constBits(), f); if (generateMips) { - generateFaceMips(theTexture, face, f); + generateFaceMips(theTexture, face, formatMip, f); } f++; } @@ -858,8 +829,6 @@ gpu::Texture* TextureUsage::processCubeTextureColorFromImage(const QImage& srcIm PROFILE_RANGE(resource_parse, "generateIrradiance"); theTexture->generateIrradiance(); } - - theTexture->setSource(srcImageName); } } diff --git a/libraries/model/src/model/TextureMap.h b/libraries/model/src/model/TextureMap.h index a4bb861502..220ee57a97 100755 --- a/libraries/model/src/model/TextureMap.h +++ b/libraries/model/src/model/TextureMap.h @@ -32,7 +32,6 @@ public: int _environmentUsage = 0; static gpu::Texture* create2DTextureFromImage(const QImage& image, const std::string& srcImageName); - static gpu::Texture* createStrict2DTextureFromImage(const QImage& image, const std::string& srcImageName); static gpu::Texture* createAlbedoTextureFromImage(const QImage& image, const std::string& srcImageName); static gpu::Texture* createEmissiveTextureFromImage(const QImage& image, const std::string& srcImageName); static gpu::Texture* createNormalTextureFromNormalImage(const QImage& image, const std::string& srcImageName); @@ -48,7 +47,7 @@ public: static const QImage process2DImageColor(const QImage& srcImage, bool& validAlpha, bool& alphaAsMask); static void defineColorTexelFormats(gpu::Element& formatGPU, gpu::Element& formatMip, const QImage& srcImage, bool isLinear, bool doCompress); - static gpu::Texture* process2DTextureColorFromImage(const QImage& srcImage, const std::string& srcImageName, bool isLinear, bool doCompress, bool generateMips, bool isStrict = false); + static gpu::Texture* process2DTextureColorFromImage(const QImage& srcImage, const std::string& srcImageName, bool isLinear, bool doCompress, bool generateMips); static gpu::Texture* processCubeTextureColorFromImage(const QImage& srcImage, const std::string& srcImageName, bool isLinear, bool doCompress, bool generateMips, bool generateIrradiance); }; diff --git a/libraries/networking/src/Assignment.cpp b/libraries/networking/src/Assignment.cpp index 27d4a31ccf..9efad15398 100644 --- a/libraries/networking/src/Assignment.cpp +++ b/libraries/networking/src/Assignment.cpp @@ -12,6 +12,7 @@ #include "udt/PacketHeaders.h" #include "SharedUtil.h" #include "UUID.h" +#include "ServerPathUtils.h" #include diff --git a/libraries/networking/src/FileCache.cpp b/libraries/networking/src/FileCache.cpp deleted file mode 100644 index f8a86903cb..0000000000 --- a/libraries/networking/src/FileCache.cpp +++ /dev/null @@ -1,243 +0,0 @@ -// -// FileCache.cpp -// libraries/model-networking/src -// -// Created by Zach Pomerantz on 2/21/2017. -// Copyright 2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include "FileCache.h" - -#include -#include -#include -#include - -#include - -#include - -Q_LOGGING_CATEGORY(file_cache, "hifi.file_cache", QtWarningMsg) - -using namespace cache; - -static const std::string MANIFEST_NAME = "manifest"; - -static const size_t BYTES_PER_MEGABYTES = 1024 * 1024; -static const size_t BYTES_PER_GIGABYTES = 1024 * BYTES_PER_MEGABYTES; -const size_t FileCache::DEFAULT_UNUSED_MAX_SIZE = 5 * BYTES_PER_GIGABYTES; // 5GB -const size_t FileCache::MAX_UNUSED_MAX_SIZE = 100 * BYTES_PER_GIGABYTES; // 100GB -const size_t FileCache::DEFAULT_OFFLINE_MAX_SIZE = 2 * BYTES_PER_GIGABYTES; // 2GB - -void FileCache::setUnusedFileCacheSize(size_t unusedFilesMaxSize) { - _unusedFilesMaxSize = std::min(unusedFilesMaxSize, MAX_UNUSED_MAX_SIZE); - reserve(0); - emit dirty(); -} - -void FileCache::setOfflineFileCacheSize(size_t offlineFilesMaxSize) { - _offlineFilesMaxSize = std::min(offlineFilesMaxSize, MAX_UNUSED_MAX_SIZE); -} - -FileCache::FileCache(const std::string& dirname, const std::string& ext, QObject* parent) : - QObject(parent), - _ext(ext), - _dirname(dirname), - _dirpath(PathUtils::getAppLocalDataFilePath(dirname.c_str()).toStdString()) {} - -FileCache::~FileCache() { - clear(); -} - -void fileDeleter(File* file) { - file->deleter(); -} - -void FileCache::initialize() { - QDir dir(_dirpath.c_str()); - - if (dir.exists()) { - auto nameFilters = QStringList(("*." + _ext).c_str()); - auto filters = QDir::Filters(QDir::NoDotAndDotDot | QDir::Files); - auto sort = QDir::SortFlags(QDir::Time); - auto files = dir.entryList(nameFilters, filters, sort); - - // load persisted files - foreach(QString filename, files) { - const Key key = filename.section('.', 0, 1).toStdString(); - const std::string filepath = dir.filePath(filename).toStdString(); - const size_t length = std::ifstream(filepath, std::ios::binary | std::ios::ate).tellg(); - addFile(Metadata(key, length), filepath); - } - - qCDebug(file_cache, "[%s] Initialized %s", _dirname.c_str(), _dirpath.c_str()); - } else { - dir.mkpath(_dirpath.c_str()); - qCDebug(file_cache, "[%s] Created %s", _dirname.c_str(), _dirpath.c_str()); - } - - _initialized = true; -} - -FilePointer FileCache::addFile(Metadata&& metadata, const std::string& filepath) { - FilePointer file(createFile(std::move(metadata), filepath).release(), &fileDeleter); - if (file) { - _numTotalFiles += 1; - _totalFilesSize += file->getLength(); - file->_cache = this; - emit dirty(); - - Lock lock(_filesMutex); - _files[file->getKey()] = file; - } - return file; -} - -FilePointer FileCache::writeFile(const char* data, File::Metadata&& metadata) { - assert(_initialized); - - std::string filepath = getFilepath(metadata.key); - - Lock lock(_filesMutex); - - // if file already exists, return it - FilePointer file = getFile(metadata.key); - if (file) { - qCWarning(file_cache, "[%s] Attempted to overwrite %s", _dirname.c_str(), metadata.key.c_str()); - return file; - } - - // write the new file - FILE* saveFile = fopen(filepath.c_str(), "wb"); - if (saveFile != nullptr && fwrite(data, metadata.length, 1, saveFile) && fclose(saveFile) == 0) { - file = addFile(std::move(metadata), filepath); - } else { - qCWarning(file_cache, "[%s] Failed to write %s (%s)", _dirname.c_str(), metadata.key.c_str(), strerror(errno)); - errno = 0; - } - - return file; -} - -FilePointer FileCache::getFile(const Key& key) { - assert(_initialized); - - FilePointer file; - - Lock lock(_filesMutex); - - // check if file exists - const auto it = _files.find(key); - if (it != _files.cend()) { - file = it->second.lock(); - if (file) { - // if it exists, it is active - remove it from the cache - removeUnusedFile(file); - qCDebug(file_cache, "[%s] Found %s", _dirname.c_str(), key.c_str()); - emit dirty(); - } else { - // if not, remove the weak_ptr - _files.erase(it); - } - } - - return file; -} - -std::string FileCache::getFilepath(const Key& key) { - return _dirpath + '/' + key + '.' + _ext; -} - -void FileCache::addUnusedFile(const FilePointer file) { - { - Lock lock(_filesMutex); - _files[file->getKey()] = file; - } - - reserve(file->getLength()); - file->_LRUKey = ++_lastLRUKey; - - { - Lock lock(_unusedFilesMutex); - _unusedFiles.insert({ file->_LRUKey, file }); - _numUnusedFiles += 1; - _unusedFilesSize += file->getLength(); - } - - emit dirty(); -} - -void FileCache::removeUnusedFile(const FilePointer file) { - Lock lock(_unusedFilesMutex); - const auto it = _unusedFiles.find(file->_LRUKey); - if (it != _unusedFiles.cend()) { - _unusedFiles.erase(it); - _numUnusedFiles -= 1; - _unusedFilesSize -= file->getLength(); - } -} - -void FileCache::reserve(size_t length) { - Lock unusedLock(_unusedFilesMutex); - while (!_unusedFiles.empty() && - _unusedFilesSize + length > _unusedFilesMaxSize) { - auto it = _unusedFiles.begin(); - auto file = it->second; - auto length = file->getLength(); - - unusedLock.unlock(); - { - file->_cache = nullptr; - Lock lock(_filesMutex); - _files.erase(file->getKey()); - } - unusedLock.lock(); - - _unusedFiles.erase(it); - _numTotalFiles -= 1; - _numUnusedFiles -= 1; - _totalFilesSize -= length; - _unusedFilesSize -= length; - } -} - -void FileCache::clear() { - Lock unusedFilesLock(_unusedFilesMutex); - for (const auto& pair : _unusedFiles) { - auto& file = pair.second; - file->_cache = nullptr; - - if (_totalFilesSize > _offlineFilesMaxSize) { - _totalFilesSize -= file->getLength(); - } else { - file->_shouldPersist = true; - qCDebug(file_cache, "[%s] Persisting %s", _dirname.c_str(), file->getKey().c_str()); - } - } - _unusedFiles.clear(); -} - -void File::deleter() { - if (_cache) { - FilePointer self(this, &fileDeleter); - _cache->addUnusedFile(self); - } else { - deleteLater(); - } -} - -File::File(Metadata&& metadata, const std::string& filepath) : - _key(std::move(metadata.key)), - _length(metadata.length), - _filepath(filepath) {} - -File::~File() { - QFile file(getFilepath().c_str()); - if (file.exists() && !_shouldPersist) { - qCInfo(file_cache, "Unlinked %s", getFilepath().c_str()); - file.remove(); - } -} diff --git a/libraries/networking/src/FileCache.h b/libraries/networking/src/FileCache.h deleted file mode 100644 index f77db555bc..0000000000 --- a/libraries/networking/src/FileCache.h +++ /dev/null @@ -1,158 +0,0 @@ -// -// FileCache.h -// libraries/networking/src -// -// Created by Zach Pomerantz on 2/21/2017. -// Copyright 2017 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#ifndef hifi_FileCache_h -#define hifi_FileCache_h - -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -Q_DECLARE_LOGGING_CATEGORY(file_cache) - -namespace cache { - -class File; -using FilePointer = std::shared_ptr; - -class FileCache : public QObject { - Q_OBJECT - Q_PROPERTY(size_t numTotal READ getNumTotalFiles NOTIFY dirty) - Q_PROPERTY(size_t numCached READ getNumCachedFiles NOTIFY dirty) - Q_PROPERTY(size_t sizeTotal READ getSizeTotalFiles NOTIFY dirty) - Q_PROPERTY(size_t sizeCached READ getSizeCachedFiles NOTIFY dirty) - - static const size_t DEFAULT_UNUSED_MAX_SIZE; - static const size_t MAX_UNUSED_MAX_SIZE; - static const size_t DEFAULT_OFFLINE_MAX_SIZE; - -public: - size_t getNumTotalFiles() const { return _numTotalFiles; } - size_t getNumCachedFiles() const { return _numUnusedFiles; } - size_t getSizeTotalFiles() const { return _totalFilesSize; } - size_t getSizeCachedFiles() const { return _unusedFilesSize; } - - void setUnusedFileCacheSize(size_t unusedFilesMaxSize); - size_t getUnusedFileCacheSize() const { return _unusedFilesSize; } - - void setOfflineFileCacheSize(size_t offlineFilesMaxSize); - - // initialize FileCache with a directory name (not a path, ex.: "temp_jpgs") and an ext (ex.: "jpg") - FileCache(const std::string& dirname, const std::string& ext, QObject* parent = nullptr); - virtual ~FileCache(); - - using Key = std::string; - struct Metadata { - Metadata(const Key& key, size_t length) : - key(key), length(length) {} - Key key; - size_t length; - }; - - // derived classes should implement a setter/getter, for example, for a FileCache backing a network cache: - // - // DerivedFilePointer writeFile(const char* data, DerivedMetadata&& metadata) { - // return writeFile(data, std::forward(metadata)); - // } - // - // DerivedFilePointer getFile(const QUrl& url) { - // auto key = lookup_hash_for(url); // assuming hashing url in create/evictedFile overrides - // return getFile(key); - // } - -signals: - void dirty(); - -protected: - /// must be called after construction to create the cache on the fs and restore persisted files - void initialize(); - - FilePointer writeFile(const char* data, Metadata&& metadata); - FilePointer getFile(const Key& key); - - /// create a file - virtual std::unique_ptr createFile(Metadata&& metadata, const std::string& filepath) = 0; - -private: - using Mutex = std::recursive_mutex; - using Lock = std::unique_lock; - - friend class File; - - std::string getFilepath(const Key& key); - - FilePointer addFile(Metadata&& metadata, const std::string& filepath); - void addUnusedFile(const FilePointer file); - void removeUnusedFile(const FilePointer file); - void reserve(size_t length); - void clear(); - - std::atomic _numTotalFiles { 0 }; - std::atomic _numUnusedFiles { 0 }; - std::atomic _totalFilesSize { 0 }; - std::atomic _unusedFilesSize { 0 }; - - std::string _ext; - std::string _dirname; - std::string _dirpath; - bool _initialized { false }; - - std::unordered_map> _files; - Mutex _filesMutex; - - std::map _unusedFiles; - Mutex _unusedFilesMutex; - size_t _unusedFilesMaxSize { DEFAULT_UNUSED_MAX_SIZE }; - int _lastLRUKey { 0 }; - - size_t _offlineFilesMaxSize { DEFAULT_OFFLINE_MAX_SIZE }; -}; - -class File : public QObject { - Q_OBJECT - -public: - using Key = FileCache::Key; - using Metadata = FileCache::Metadata; - - Key getKey() const { return _key; } - size_t getLength() const { return _length; } - std::string getFilepath() const { return _filepath; } - - virtual ~File(); - /// overrides should call File::deleter to maintain caching behavior - virtual void deleter(); - -protected: - /// when constructed, the file has already been created/written - File(Metadata&& metadata, const std::string& filepath); - -private: - friend class FileCache; - - const Key _key; - const size_t _length; - const std::string _filepath; - - FileCache* _cache; - int _LRUKey { 0 }; - - bool _shouldPersist { false }; -}; - -} - -#endif // hifi_FileCache_h diff --git a/libraries/networking/src/NodePermissions.h b/libraries/networking/src/NodePermissions.h index 6fa005e360..5d2755f9b5 100644 --- a/libraries/networking/src/NodePermissions.h +++ b/libraries/networking/src/NodePermissions.h @@ -13,31 +13,18 @@ #define hifi_NodePermissions_h #include -#include #include #include #include #include -#include -#include + #include "GroupRank.h" class NodePermissions; using NodePermissionsPointer = std::shared_ptr; -using NodePermissionsKey = std::pair; // name, rankID +using NodePermissionsKey = QPair; // name, rankID using NodePermissionsKeyList = QList>; -namespace std { - template<> - struct hash { - size_t operator()(const NodePermissionsKey& key) const { - size_t result = qHash(key.first); - result <<= 32; - result |= qHash(key.second); - return result; - } - }; -} class NodePermissions { public: @@ -113,40 +100,27 @@ public: NodePermissionsMap() { } NodePermissionsPointer& operator[](const NodePermissionsKey& key) { NodePermissionsKey dataKey(key.first.toLower(), key.second); - if (0 == _data.count(dataKey)) { + if (!_data.contains(dataKey)) { _data[dataKey] = NodePermissionsPointer(new NodePermissions(key)); } return _data[dataKey]; } NodePermissionsPointer operator[](const NodePermissionsKey& key) const { - NodePermissionsPointer result; - auto itr = _data.find(NodePermissionsKey(key.first.toLower(), key.second)); - if (_data.end() != itr) { - result = itr->second; - } - return result; + return _data.value(NodePermissionsKey(key.first.toLower(), key.second)); } bool contains(const NodePermissionsKey& key) const { - return 0 != _data.count(NodePermissionsKey(key.first.toLower(), key.second)); + return _data.contains(NodePermissionsKey(key.first.toLower(), key.second)); } - bool contains(const QString& keyFirst, const QUuid& keySecond) const { - return 0 != _data.count(NodePermissionsKey(keyFirst.toLower(), keySecond)); + bool contains(const QString& keyFirst, QUuid keySecond) const { + return _data.contains(NodePermissionsKey(keyFirst.toLower(), keySecond)); } - - QList keys() const { - QList result; - for (const auto& entry : _data) { - result.push_back(entry.first); - } - return result; - } - - const std::unordered_map& get() { return _data; } + QList keys() const { return _data.keys(); } + QHash get() { return _data; } void clear() { _data.clear(); } - void remove(const NodePermissionsKey& key) { _data.erase(key); } + void remove(const NodePermissionsKey& key) { _data.remove(key); } private: - std::unordered_map _data; + QHash _data; }; diff --git a/libraries/networking/src/udt/PacketQueue.cpp b/libraries/networking/src/udt/PacketQueue.cpp index 9560f2f187..bb20982ca4 100644 --- a/libraries/networking/src/udt/PacketQueue.cpp +++ b/libraries/networking/src/udt/PacketQueue.cpp @@ -15,10 +15,6 @@ using namespace udt; -PacketQueue::PacketQueue() { - _channels.emplace_back(new std::list()); -} - MessageNumber PacketQueue::getNextMessageNumber() { static const MessageNumber MAX_MESSAGE_NUMBER = MessageNumber(1) << MESSAGE_NUMBER_SIZE; _currentMessageNumber = (_currentMessageNumber + 1) % MAX_MESSAGE_NUMBER; @@ -28,7 +24,7 @@ MessageNumber PacketQueue::getNextMessageNumber() { bool PacketQueue::isEmpty() const { LockGuard locker(_packetsLock); // Only the main channel and it is empty - return (_channels.size() == 1) && _channels.front()->empty(); + return (_channels.size() == 1) && _channels.front().empty(); } PacketQueue::PacketPointer PacketQueue::takePacket() { @@ -38,19 +34,19 @@ PacketQueue::PacketPointer PacketQueue::takePacket() { } // Find next non empty channel - if (_channels[nextIndex()]->empty()) { + if (_channels[nextIndex()].empty()) { nextIndex(); } auto& channel = _channels[_currentIndex]; - Q_ASSERT(!channel->empty()); + Q_ASSERT(!channel.empty()); // Take front packet - auto packet = std::move(channel->front()); - channel->pop_front(); + auto packet = std::move(channel.front()); + channel.pop_front(); // Remove now empty channel (Don't remove the main channel) - if (channel->empty() && _currentIndex != 0) { - channel->swap(*_channels.back()); + if (channel.empty() && _currentIndex != 0) { + channel.swap(_channels.back()); _channels.pop_back(); --_currentIndex; } @@ -65,7 +61,7 @@ unsigned int PacketQueue::nextIndex() { void PacketQueue::queuePacket(PacketPointer packet) { LockGuard locker(_packetsLock); - _channels.front()->push_back(std::move(packet)); + _channels.front().push_back(std::move(packet)); } void PacketQueue::queuePacketList(PacketListPointer packetList) { @@ -74,6 +70,5 @@ void PacketQueue::queuePacketList(PacketListPointer packetList) { } LockGuard locker(_packetsLock); - _channels.emplace_back(new std::list()); - _channels.back()->swap(packetList->_packets); + _channels.push_back(std::move(packetList->_packets)); } diff --git a/libraries/networking/src/udt/PacketQueue.h b/libraries/networking/src/udt/PacketQueue.h index 2b3d3a4b5b..69784fd8db 100644 --- a/libraries/networking/src/udt/PacketQueue.h +++ b/libraries/networking/src/udt/PacketQueue.h @@ -30,11 +30,10 @@ class PacketQueue { using LockGuard = std::lock_guard; using PacketPointer = std::unique_ptr; using PacketListPointer = std::unique_ptr; - using Channel = std::unique_ptr>; + using Channel = std::list; using Channels = std::vector; public: - PacketQueue(); void queuePacket(PacketPointer packet); void queuePacketList(PacketListPointer packetList); @@ -50,7 +49,7 @@ private: MessageNumber _currentMessageNumber { 0 }; mutable Mutex _packetsLock; // Protects the packets to be sent. - Channels _channels; // One channel per packet list + Main channel + Channels _channels = Channels(1); // One channel per packet list + Main channel unsigned int _currentIndex { 0 }; }; diff --git a/libraries/octree/src/OctreeQuery.cpp b/libraries/octree/src/OctreeQuery.cpp index 7d9fc7d08c..a639eccaba 100644 --- a/libraries/octree/src/OctreeQuery.cpp +++ b/libraries/octree/src/OctreeQuery.cpp @@ -142,6 +142,6 @@ int OctreeQuery::parseData(ReceivedMessage& message) { } glm::vec3 OctreeQuery::calculateCameraDirection() const { - glm::vec3 direction = glm::vec3(_cameraOrientation * glm::vec4(IDENTITY_FORWARD, 0.0f)); + glm::vec3 direction = glm::vec3(_cameraOrientation * glm::vec4(IDENTITY_FRONT, 0.0f)); return direction; } diff --git a/libraries/physics/src/EntityMotionState.cpp b/libraries/physics/src/EntityMotionState.cpp index d383f4c199..c175a836cc 100644 --- a/libraries/physics/src/EntityMotionState.cpp +++ b/libraries/physics/src/EntityMotionState.cpp @@ -97,21 +97,6 @@ void EntityMotionState::updateServerPhysicsVariables() { _serverActionData = _entity->getActionData(); } -void EntityMotionState::handleDeactivation() { - // copy _server data to entity - bool success; - _entity->setPosition(_serverPosition, success, false); - _entity->setOrientation(_serverRotation, success, false); - _entity->setVelocity(ENTITY_ITEM_ZERO_VEC3); - _entity->setAngularVelocity(ENTITY_ITEM_ZERO_VEC3); - // and also to RigidBody - btTransform worldTrans; - worldTrans.setOrigin(glmToBullet(_serverPosition)); - worldTrans.setRotation(glmToBullet(_serverRotation)); - _body->setWorldTransform(worldTrans); - // no need to update velocities... should already be zero -} - // virtual void EntityMotionState::handleEasyChanges(uint32_t& flags) { assert(entityTreeIsLocked()); @@ -126,8 +111,6 @@ void EntityMotionState::handleEasyChanges(uint32_t& flags) { flags &= ~Simulation::DIRTY_PHYSICS_ACTIVATION; _body->setActivationState(WANTS_DEACTIVATION); _outgoingPriority = 0; - const float ACTIVATION_EXPIRY = 3.0f; // something larger than the 2.0 hard coded in Bullet - _body->setDeactivationTime(ACTIVATION_EXPIRY); } else { // disowned object is still moving --> start timer for ownership bid // TODO? put a delay in here proportional to distance from object? @@ -238,9 +221,12 @@ void EntityMotionState::getWorldTransform(btTransform& worldTrans) const { } // This callback is invoked by the physics simulation at the end of each simulation step... -// iff the corresponding RigidBody is DYNAMIC and ACTIVE. +// iff the corresponding RigidBody is DYNAMIC and has moved. void EntityMotionState::setWorldTransform(const btTransform& worldTrans) { - assert(_entity); + if (!_entity) { + return; + } + assert(entityTreeIsLocked()); measureBodyAcceleration(); bool positionSuccess; diff --git a/libraries/physics/src/EntityMotionState.h b/libraries/physics/src/EntityMotionState.h index 380edf3927..feac47d8ec 100644 --- a/libraries/physics/src/EntityMotionState.h +++ b/libraries/physics/src/EntityMotionState.h @@ -29,7 +29,6 @@ public: virtual ~EntityMotionState(); void updateServerPhysicsVariables(); - void handleDeactivation(); virtual void handleEasyChanges(uint32_t& flags) override; virtual bool handleHardAndEasyChanges(uint32_t& flags, PhysicsEngine* engine) override; diff --git a/libraries/physics/src/PhysicalEntitySimulation.cpp b/libraries/physics/src/PhysicalEntitySimulation.cpp index bd76b2d70f..903b160a5e 100644 --- a/libraries/physics/src/PhysicalEntitySimulation.cpp +++ b/libraries/physics/src/PhysicalEntitySimulation.cpp @@ -259,27 +259,13 @@ void PhysicalEntitySimulation::getObjectsToChange(VectorOfMotionStates& result) _pendingChanges.clear(); } -void PhysicalEntitySimulation::handleDeactivatedMotionStates(const VectorOfMotionStates& motionStates) { - for (auto stateItr : motionStates) { - ObjectMotionState* state = &(*stateItr); - assert(state); - if (state->getType() == MOTIONSTATE_TYPE_ENTITY) { - EntityMotionState* entityState = static_cast(state); - entityState->handleDeactivation(); - EntityItemPointer entity = entityState->getEntity(); - _entitiesToSort.insert(entity); - } - } -} - -void PhysicalEntitySimulation::handleChangedMotionStates(const VectorOfMotionStates& motionStates) { +void PhysicalEntitySimulation::handleOutgoingChanges(const VectorOfMotionStates& motionStates) { QMutexLocker lock(&_mutex); // walk the motionStates looking for those that correspond to entities for (auto stateItr : motionStates) { ObjectMotionState* state = &(*stateItr); - assert(state); - if (state->getType() == MOTIONSTATE_TYPE_ENTITY) { + if (state && state->getType() == MOTIONSTATE_TYPE_ENTITY) { EntityMotionState* entityState = static_cast(state); EntityItemPointer entity = entityState->getEntity(); assert(entity.get()); diff --git a/libraries/physics/src/PhysicalEntitySimulation.h b/libraries/physics/src/PhysicalEntitySimulation.h index 5f6185add3..af5def9775 100644 --- a/libraries/physics/src/PhysicalEntitySimulation.h +++ b/libraries/physics/src/PhysicalEntitySimulation.h @@ -56,8 +56,7 @@ public: void setObjectsToChange(const VectorOfMotionStates& objectsToChange); void getObjectsToChange(VectorOfMotionStates& result); - void handleDeactivatedMotionStates(const VectorOfMotionStates& motionStates); - void handleChangedMotionStates(const VectorOfMotionStates& motionStates); + void handleOutgoingChanges(const VectorOfMotionStates& motionStates); void handleCollisionEvents(const CollisionEvents& collisionEvents); EntityEditPacketSender* getPacketSender() { return _entityPacketSender; } @@ -68,7 +67,7 @@ private: SetOfEntities _entitiesToAddToPhysics; SetOfEntityMotionStates _pendingChanges; // EntityMotionStates already in PhysicsEngine that need their physics changed - SetOfEntityMotionStates _outgoingChanges; // EntityMotionStates for which we may need to send updates to entity-server + SetOfEntityMotionStates _outgoingChanges; // EntityMotionStates for which we need to send updates to entity-server SetOfMotionStates _physicalObjects; // MotionStates of entities in PhysicsEngine diff --git a/libraries/physics/src/PhysicsEngine.cpp b/libraries/physics/src/PhysicsEngine.cpp index a8a8e6acfd..363887de25 100644 --- a/libraries/physics/src/PhysicsEngine.cpp +++ b/libraries/physics/src/PhysicsEngine.cpp @@ -472,7 +472,7 @@ const CollisionEvents& PhysicsEngine::getCollisionEvents() { return _collisionEvents; } -const VectorOfMotionStates& PhysicsEngine::getChangedMotionStates() { +const VectorOfMotionStates& PhysicsEngine::getOutgoingChanges() { BT_PROFILE("copyOutgoingChanges"); // Bullet will not deactivate static objects (it doesn't expect them to be active) // so we must deactivate them ourselves diff --git a/libraries/physics/src/PhysicsEngine.h b/libraries/physics/src/PhysicsEngine.h index b2ebe58f08..bbafbb06b6 100644 --- a/libraries/physics/src/PhysicsEngine.h +++ b/libraries/physics/src/PhysicsEngine.h @@ -65,8 +65,7 @@ public: bool hasOutgoingChanges() const { return _hasOutgoingChanges; } /// \return reference to list of changed MotionStates. The list is only valid until beginning of next simulation loop. - const VectorOfMotionStates& getChangedMotionStates(); - const VectorOfMotionStates& getDeactivatedMotionStates() const { return _dynamicsWorld->getDeactivatedMotionStates(); } + const VectorOfMotionStates& getOutgoingChanges(); /// \return reference to list of Collision events. The list is only valid until beginning of next simulation loop. const CollisionEvents& getCollisionEvents(); diff --git a/libraries/physics/src/ThreadSafeDynamicsWorld.cpp b/libraries/physics/src/ThreadSafeDynamicsWorld.cpp index 24cfbc2609..5fe99f137c 100644 --- a/libraries/physics/src/ThreadSafeDynamicsWorld.cpp +++ b/libraries/physics/src/ThreadSafeDynamicsWorld.cpp @@ -120,41 +120,30 @@ void ThreadSafeDynamicsWorld::synchronizeMotionState(btRigidBody* body) { void ThreadSafeDynamicsWorld::synchronizeMotionStates() { BT_PROFILE("synchronizeMotionStates"); _changedMotionStates.clear(); - - // NOTE: m_synchronizeAllMotionStates is 'false' by default for optimization. - // See PhysicsEngine::init() where we call _dynamicsWorld->setForceUpdateAllAabbs(false) if (m_synchronizeAllMotionStates) { //iterate over all collision objects for (int i=0;igetMotionState()) { - synchronizeMotionState(body); - _changedMotionStates.push_back(static_cast(body->getMotionState())); + if (body) { + if (body->getMotionState()) { + synchronizeMotionState(body); + _changedMotionStates.push_back(static_cast(body->getMotionState())); + } } } } else { //iterate over all active rigid bodies - // TODO? if this becomes a performance bottleneck we could derive our own SimulationIslandManager - // that remembers a list of objects deactivated last step - _activeStates.clear(); - _deactivatedStates.clear(); for (int i=0;i(body->getMotionState()); - if (motionState) { - if (body->isActive()) { + if (body->isActive()) { + if (body->getMotionState()) { synchronizeMotionState(body); - _changedMotionStates.push_back(motionState); - _activeStates.insert(motionState); - } else if (_lastActiveStates.find(motionState) != _lastActiveStates.end()) { - // this object was active last frame but is no longer - _deactivatedStates.push_back(motionState); + _changedMotionStates.push_back(static_cast(body->getMotionState())); } } } } - _activeStates.swap(_lastActiveStates); } void ThreadSafeDynamicsWorld::saveKinematicState(btScalar timeStep) { diff --git a/libraries/physics/src/ThreadSafeDynamicsWorld.h b/libraries/physics/src/ThreadSafeDynamicsWorld.h index b4fcca8cdb..68062d8d29 100644 --- a/libraries/physics/src/ThreadSafeDynamicsWorld.h +++ b/libraries/physics/src/ThreadSafeDynamicsWorld.h @@ -49,16 +49,12 @@ public: float getLocalTimeAccumulation() const { return m_localTime; } const VectorOfMotionStates& getChangedMotionStates() const { return _changedMotionStates; } - const VectorOfMotionStates& getDeactivatedMotionStates() const { return _deactivatedStates; } private: // call this instead of non-virtual btDiscreteDynamicsWorld::synchronizeSingleMotionState() void synchronizeMotionState(btRigidBody* body); VectorOfMotionStates _changedMotionStates; - VectorOfMotionStates _deactivatedStates; - SetOfMotionStates _activeStates; - SetOfMotionStates _lastActiveStates; }; #endif // hifi_ThreadSafeDynamicsWorld_h diff --git a/libraries/recording/src/recording/Deck.cpp b/libraries/recording/src/recording/Deck.cpp index 186516e01c..61eb86c91f 100644 --- a/libraries/recording/src/recording/Deck.cpp +++ b/libraries/recording/src/recording/Deck.cpp @@ -33,7 +33,6 @@ void Deck::queueClip(ClipPointer clip, float timeOffset) { // FIXME disabling multiple clips for now _clips.clear(); - _length = 0.0f; // if the time offset is not zero, wrap in an OffsetClip if (timeOffset != 0.0f) { @@ -154,8 +153,8 @@ void Deck::processFrames() { // if doing relative movement emit looped(); } else { - // otherwise stop playback - stop(); + // otherwise pause playback + pause(); } return; } diff --git a/libraries/render-utils/CMakeLists.txt b/libraries/render-utils/CMakeLists.txt index 3bf389973a..ecafb8f565 100644 --- a/libraries/render-utils/CMakeLists.txt +++ b/libraries/render-utils/CMakeLists.txt @@ -3,7 +3,7 @@ AUTOSCRIBE_SHADER_LIB(gpu model render) # pull in the resources.qrc file qt5_add_resources(QT_RESOURCES_FILE "${CMAKE_CURRENT_SOURCE_DIR}/res/fonts/fonts.qrc") setup_hifi_library(Widgets OpenGL Network Qml Quick Script) -link_hifi_libraries(shared ktx gpu model model-networking render animation fbx entities) +link_hifi_libraries(shared gpu model model-networking render animation fbx entities) if (NOT ANDROID) target_nsight() diff --git a/libraries/render-utils/src/AntialiasingEffect.cpp b/libraries/render-utils/src/AntialiasingEffect.cpp index f95d45de04..2941197e6d 100644 --- a/libraries/render-utils/src/AntialiasingEffect.cpp +++ b/libraries/render-utils/src/AntialiasingEffect.cpp @@ -52,7 +52,7 @@ const gpu::PipelinePointer& Antialiasing::getAntialiasingPipeline() { _antialiasingBuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("antialiasing")); auto format = gpu::Element::COLOR_SRGBA_32; // DependencyManager::get()->getLightingTexture()->getTexelFormat(); auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT); - _antialiasingTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(format, width, height, defaultSampler)); + _antialiasingTexture = gpu::TexturePointer(gpu::Texture::create2D(format, width, height, defaultSampler)); _antialiasingBuffer->setRenderBuffer(0, _antialiasingTexture); } diff --git a/libraries/render-utils/src/DeferredFramebuffer.cpp b/libraries/render-utils/src/DeferredFramebuffer.cpp index 40c22beba4..e8783e0e0d 100644 --- a/libraries/render-utils/src/DeferredFramebuffer.cpp +++ b/libraries/render-utils/src/DeferredFramebuffer.cpp @@ -53,9 +53,9 @@ void DeferredFramebuffer::allocate() { auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT); - _deferredColorTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(colorFormat, width, height, defaultSampler)); - _deferredNormalTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(linearFormat, width, height, defaultSampler)); - _deferredSpecularTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(colorFormat, width, height, defaultSampler)); + _deferredColorTexture = gpu::TexturePointer(gpu::Texture::create2D(colorFormat, width, height, defaultSampler)); + _deferredNormalTexture = gpu::TexturePointer(gpu::Texture::create2D(linearFormat, width, height, defaultSampler)); + _deferredSpecularTexture = gpu::TexturePointer(gpu::Texture::create2D(colorFormat, width, height, defaultSampler)); _deferredFramebuffer->setRenderBuffer(0, _deferredColorTexture); _deferredFramebuffer->setRenderBuffer(1, _deferredNormalTexture); @@ -65,7 +65,7 @@ void DeferredFramebuffer::allocate() { auto depthFormat = gpu::Element(gpu::SCALAR, gpu::UINT32, gpu::DEPTH_STENCIL); // Depth24_Stencil8 texel format if (!_primaryDepthTexture) { - _primaryDepthTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(depthFormat, width, height, defaultSampler)); + _primaryDepthTexture = gpu::TexturePointer(gpu::Texture::create2D(depthFormat, width, height, defaultSampler)); } _deferredFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, depthFormat); @@ -75,7 +75,7 @@ void DeferredFramebuffer::allocate() { auto smoothSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR); - _lightingTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::R11G11B10), width, height, defaultSampler)); + _lightingTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::R11G11B10), width, height, defaultSampler)); _lightingFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("lighting")); _lightingFramebuffer->setRenderBuffer(0, _lightingTexture); _lightingFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, depthFormat); diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp index ce340583ee..6f1152ac16 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.cpp +++ b/libraries/render-utils/src/DeferredLightingEffect.cpp @@ -496,14 +496,14 @@ void PreparePrimaryFramebuffer::run(const SceneContextPointer& sceneContext, con auto colorFormat = gpu::Element::COLOR_SRGBA_32; auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT); - auto primaryColorTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(colorFormat, frameSize.x, frameSize.y, defaultSampler)); + auto primaryColorTexture = gpu::TexturePointer(gpu::Texture::create2D(colorFormat, frameSize.x, frameSize.y, defaultSampler)); _primaryFramebuffer->setRenderBuffer(0, primaryColorTexture); auto depthFormat = gpu::Element(gpu::SCALAR, gpu::UINT32, gpu::DEPTH_STENCIL); // Depth24_Stencil8 texel format - auto primaryDepthTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(depthFormat, frameSize.x, frameSize.y, defaultSampler)); + auto primaryDepthTexture = gpu::TexturePointer(gpu::Texture::create2D(depthFormat, frameSize.x, frameSize.y, defaultSampler)); _primaryFramebuffer->setDepthStencilBuffer(primaryDepthTexture, depthFormat); } diff --git a/libraries/render-utils/src/FramebufferCache.cpp b/libraries/render-utils/src/FramebufferCache.cpp index 72b3c2ceb4..27429595b4 100644 --- a/libraries/render-utils/src/FramebufferCache.cpp +++ b/libraries/render-utils/src/FramebufferCache.cpp @@ -21,6 +21,7 @@ void FramebufferCache::setFrameBufferSize(QSize frameBufferSize) { //If the size changed, we need to delete our FBOs if (_frameBufferSize != frameBufferSize) { _frameBufferSize = frameBufferSize; + _selfieFramebuffer.reset(); { std::unique_lock lock(_mutex); _cachedFramebuffers.clear(); @@ -29,8 +30,16 @@ void FramebufferCache::setFrameBufferSize(QSize frameBufferSize) { } void FramebufferCache::createPrimaryFramebuffer() { + auto colorFormat = gpu::Element::COLOR_SRGBA_32; + auto width = _frameBufferSize.width(); + auto height = _frameBufferSize.height(); + auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT); + _selfieFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("selfie")); + auto tex = gpu::TexturePointer(gpu::Texture::create2D(colorFormat, width * 0.5, height * 0.5, defaultSampler)); + _selfieFramebuffer->setRenderBuffer(0, tex); + auto smoothSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR); } @@ -51,3 +60,10 @@ void FramebufferCache::releaseFramebuffer(const gpu::FramebufferPointer& framebu _cachedFramebuffers.push_back(framebuffer); } } + +gpu::FramebufferPointer FramebufferCache::getSelfieFramebuffer() { + if (!_selfieFramebuffer) { + createPrimaryFramebuffer(); + } + return _selfieFramebuffer; +} diff --git a/libraries/render-utils/src/FramebufferCache.h b/libraries/render-utils/src/FramebufferCache.h index 8065357615..f74d224a61 100644 --- a/libraries/render-utils/src/FramebufferCache.h +++ b/libraries/render-utils/src/FramebufferCache.h @@ -27,6 +27,9 @@ public: void setFrameBufferSize(QSize frameBufferSize); const QSize& getFrameBufferSize() const { return _frameBufferSize; } + /// Returns the framebuffer object used to render selfie maps; + gpu::FramebufferPointer getSelfieFramebuffer(); + /// Returns a free framebuffer with a single color attachment for temp or intra-frame operations gpu::FramebufferPointer getFramebuffer(); @@ -39,6 +42,8 @@ private: gpu::FramebufferPointer _shadowFramebuffer; + gpu::FramebufferPointer _selfieFramebuffer; + QSize _frameBufferSize{ 100, 100 }; std::mutex _mutex; diff --git a/libraries/render-utils/src/LightAmbient.slh b/libraries/render-utils/src/LightAmbient.slh index e343d8c239..15e23015cb 100644 --- a/libraries/render-utils/src/LightAmbient.slh +++ b/libraries/render-utils/src/LightAmbient.slh @@ -30,8 +30,9 @@ vec3 fresnelSchlickAmbient(vec3 fresnelColor, vec3 lightDir, vec3 halfDir, float <$declareSkyboxMap()$> <@endif@> -vec3 evalAmbientSpecularIrradiance(LightAmbient ambient, vec3 fragEyeDir, vec3 fragNormal, float roughness) { +vec3 evalAmbientSpecularIrradiance(LightAmbient ambient, vec3 fragEyeDir, vec3 fragNormal, float roughness, vec3 fresnel) { vec3 direction = -reflect(fragEyeDir, fragNormal); + vec3 ambientFresnel = fresnelSchlickAmbient(fresnel, fragEyeDir, fragNormal, 1.0 - roughness); vec3 specularLight; <@if supportIfAmbientMapElseAmbientSphere@> if (getLightHasAmbientMap(ambient)) @@ -52,7 +53,7 @@ vec3 evalAmbientSpecularIrradiance(LightAmbient ambient, vec3 fragEyeDir, vec3 f } <@endif@> - return specularLight; + return specularLight * ambientFresnel; } <@endfunc@> @@ -73,14 +74,12 @@ void evalLightingAmbient(out vec3 diffuse, out vec3 specular, LightAmbient ambie <@endif@> ) { - // Fresnel - vec3 ambientFresnel = fresnelSchlickAmbient(fresnel, eyeDir, normal, 1.0 - roughness); - // Diffuse from ambient - diffuse = (1.0 - metallic) * (vec3(1.0) - ambientFresnel) * sphericalHarmonics_evalSphericalLight(getLightAmbientSphere(ambient), normal).xyz; + diffuse = (1.0 - metallic) * sphericalHarmonics_evalSphericalLight(getLightAmbientSphere(ambient), normal).xyz; // Specular highlight from ambient - specular = evalAmbientSpecularIrradiance(ambient, eyeDir, normal, roughness) * ambientFresnel; + specular = evalAmbientSpecularIrradiance(ambient, eyeDir, normal, roughness, fresnel) * obscurance * getLightAmbientIntensity(ambient); + <@if supportScattering@> float ambientOcclusion = curvatureAO(lowNormalCurvature.w * 20.0f) * 0.5f; diff --git a/libraries/render-utils/src/LightStage.cpp b/libraries/render-utils/src/LightStage.cpp index dd6a046dea..66a9797d3c 100644 --- a/libraries/render-utils/src/LightStage.cpp +++ b/libraries/render-utils/src/LightStage.cpp @@ -27,9 +27,9 @@ void LightStage::Shadow::setKeylightFrustum(const ViewFrustum& viewFrustum, floa const auto& direction = glm::normalize(_light->getDirection()); glm::quat orientation; if (direction == IDENTITY_UP) { - orientation = glm::quat(glm::mat3(-IDENTITY_RIGHT, IDENTITY_FORWARD, -IDENTITY_UP)); + orientation = glm::quat(glm::mat3(-IDENTITY_RIGHT, IDENTITY_FRONT, -IDENTITY_UP)); } else if (direction == -IDENTITY_UP) { - orientation = glm::quat(glm::mat3(IDENTITY_RIGHT, IDENTITY_FORWARD, IDENTITY_UP)); + orientation = glm::quat(glm::mat3(IDENTITY_RIGHT, IDENTITY_FRONT, IDENTITY_UP)); } else { auto side = glm::normalize(glm::cross(direction, IDENTITY_UP)); auto up = glm::normalize(glm::cross(side, direction)); diff --git a/libraries/render-utils/src/LightingModel.cpp b/libraries/render-utils/src/LightingModel.cpp index bd321bad95..47af83da36 100644 --- a/libraries/render-utils/src/LightingModel.cpp +++ b/libraries/render-utils/src/LightingModel.cpp @@ -133,7 +133,6 @@ void LightingModel::setSpotLight(bool enable) { bool LightingModel::isSpotLightEnabled() const { return (bool)_parametersBuffer.get().enableSpotLight; } - void LightingModel::setShowLightContour(bool enable) { if (enable != isShowLightContourEnabled()) { _parametersBuffer.edit().showLightContour = (float)enable; @@ -143,14 +142,6 @@ bool LightingModel::isShowLightContourEnabled() const { return (bool)_parametersBuffer.get().showLightContour; } -void LightingModel::setWireframe(bool enable) { - if (enable != isWireframeEnabled()) { - _parametersBuffer.edit().enableWireframe = (float)enable; - } -} -bool LightingModel::isWireframeEnabled() const { - return (bool)_parametersBuffer.get().enableWireframe; -} MakeLightingModel::MakeLightingModel() { _lightingModel = std::make_shared(); } @@ -176,7 +167,6 @@ void MakeLightingModel::configure(const Config& config) { _lightingModel->setSpotLight(config.enableSpotLight); _lightingModel->setShowLightContour(config.showLightContour); - _lightingModel->setWireframe(config.enableWireframe); } void MakeLightingModel::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, LightingModelPointer& lightingModel) { diff --git a/libraries/render-utils/src/LightingModel.h b/libraries/render-utils/src/LightingModel.h index c1189d5160..45514654f2 100644 --- a/libraries/render-utils/src/LightingModel.h +++ b/libraries/render-utils/src/LightingModel.h @@ -64,9 +64,6 @@ public: void setShowLightContour(bool enable); bool isShowLightContourEnabled() const; - void setWireframe(bool enable); - bool isWireframeEnabled() const; - UniformBufferView getParametersBuffer() const { return _parametersBuffer; } protected: @@ -92,12 +89,13 @@ protected: float enablePointLight{ 1.0f }; float enableSpotLight{ 1.0f }; - float showLightContour { 0.0f }; // false by default + float showLightContour{ 0.0f }; // false by default float enableObscurance{ 1.0f }; float enableMaterialTexturing { 1.0f }; - float enableWireframe { 0.0f }; // false by default + + float spares{ 0.0f }; Parameters() {} }; @@ -131,7 +129,6 @@ class MakeLightingModelConfig : public render::Job::Config { Q_PROPERTY(bool enablePointLight MEMBER enablePointLight NOTIFY dirty) Q_PROPERTY(bool enableSpotLight MEMBER enableSpotLight NOTIFY dirty) - Q_PROPERTY(bool enableWireframe MEMBER enableWireframe NOTIFY dirty) Q_PROPERTY(bool showLightContour MEMBER showLightContour NOTIFY dirty) public: @@ -155,9 +152,8 @@ public: bool enablePointLight{ true }; bool enableSpotLight{ true }; - bool showLightContour { false }; // false by default - bool enableWireframe { false }; // false by default + bool showLightContour { false }; // false by default signals: void dirty(); diff --git a/libraries/render-utils/src/LightingModel.slh b/libraries/render-utils/src/LightingModel.slh index 209a1f38d6..74285aa6a9 100644 --- a/libraries/render-utils/src/LightingModel.slh +++ b/libraries/render-utils/src/LightingModel.slh @@ -17,7 +17,7 @@ struct LightingModel { vec4 _UnlitEmissiveLightmapBackground; vec4 _ScatteringDiffuseSpecularAlbedo; vec4 _AmbientDirectionalPointSpot; - vec4 _ShowContourObscuranceWireframe; + vec4 _ShowContourObscuranceSpare2; }; uniform lightingModelBuffer{ @@ -37,7 +37,7 @@ float isBackgroundEnabled() { return lightingModel._UnlitEmissiveLightmapBackground.w; } float isObscuranceEnabled() { - return lightingModel._ShowContourObscuranceWireframe.y; + return lightingModel._ShowContourObscuranceSpare2.y; } float isScatteringEnabled() { @@ -67,12 +67,9 @@ float isSpotEnabled() { } float isShowLightContour() { - return lightingModel._ShowContourObscuranceWireframe.x; + return lightingModel._ShowContourObscuranceSpare2.x; } -float isWireframeEnabled() { - return lightingModel._ShowContourObscuranceWireframe.z; -} <@endfunc@> <$declareLightingModel()$> diff --git a/libraries/render-utils/src/MaterialTextures.slh b/libraries/render-utils/src/MaterialTextures.slh index 7b73896cc5..6d2ad23c21 100644 --- a/libraries/render-utils/src/MaterialTextures.slh +++ b/libraries/render-utils/src/MaterialTextures.slh @@ -64,7 +64,7 @@ float fetchRoughnessMap(vec2 uv) { uniform sampler2D normalMap; vec3 fetchNormalMap(vec2 uv) { // unpack normal, swizzle to get into hifi tangent space with Y axis pointing out - return normalize(texture(normalMap, uv).rbg -vec3(0.5, 0.5, 0.5)); + return normalize(texture(normalMap, uv).xzy -vec3(0.5, 0.5, 0.5)); } <@endif@> diff --git a/libraries/render-utils/src/MeshPartPayload.cpp b/libraries/render-utils/src/MeshPartPayload.cpp index 41a1bb4c74..5b3d285b47 100644 --- a/libraries/render-utils/src/MeshPartPayload.cpp +++ b/libraries/render-utils/src/MeshPartPayload.cpp @@ -372,12 +372,19 @@ void ModelMeshPartPayload::notifyLocationChanged() { } -void ModelMeshPartPayload::updateTransformForSkinnedMesh(const Transform& renderTransform, const Transform& boundTransform, - const gpu::BufferPointer& buffer) { - _transform = renderTransform; - _worldBound = _adjustedLocalBound; - _worldBound.transform(boundTransform); - _clusterBuffer = buffer; +void ModelMeshPartPayload::updateTransformForSkinnedMesh(const Transform& transform, const QVector& clusterMatrices) { + _transform = transform; + + if (clusterMatrices.size() > 0) { + _worldBound = _adjustedLocalBound; + _worldBound.transform(_transform); + if (clusterMatrices.size() == 1) { + _transform = _transform.worldTransform(Transform(clusterMatrices[0])); + } + } else { + _worldBound = _localBound; + _worldBound.transform(_transform); + } } ItemKey ModelMeshPartPayload::getKey() const { @@ -525,8 +532,9 @@ void ModelMeshPartPayload::bindMesh(gpu::Batch& batch) const { void ModelMeshPartPayload::bindTransform(gpu::Batch& batch, const ShapePipeline::LocationsPointer locations, RenderArgs::RenderMode renderMode) const { // Still relying on the raw data from the model - if (_clusterBuffer) { - batch.setUniformBuffer(ShapePipeline::Slot::BUFFER::SKINNING, _clusterBuffer); + const Model::MeshState& state = _model->getMeshState(_meshIndex); + if (state.clusterBuffer) { + batch.setUniformBuffer(ShapePipeline::Slot::BUFFER::SKINNING, state.clusterBuffer); } batch.setModelTransform(_transform); } @@ -582,6 +590,8 @@ void ModelMeshPartPayload::render(RenderArgs* args) const { auto locations = args->_pipeline->locations; assert(locations); + // Bind the model transform and the skinCLusterMatrices if needed + _model->updateClusterMatrices(); bindTransform(batch, locations, args->_renderMode); //Bind the index buffer and vertex buffer and Blend shapes if needed diff --git a/libraries/render-utils/src/MeshPartPayload.h b/libraries/render-utils/src/MeshPartPayload.h index ef74011c40..c585c95025 100644 --- a/libraries/render-utils/src/MeshPartPayload.h +++ b/libraries/render-utils/src/MeshPartPayload.h @@ -89,9 +89,8 @@ public: typedef Payload::DataPointer Pointer; void notifyLocationChanged() override; - void updateTransformForSkinnedMesh(const Transform& renderTransform, - const Transform& boundTransform, - const gpu::BufferPointer& buffer); + void updateTransformForSkinnedMesh(const Transform& transform, + const QVector& clusterMatrices); float computeFadeAlpha() const; @@ -109,7 +108,6 @@ public: void computeAdjustedLocalBound(const QVector& clusterMatrices); - gpu::BufferPointer _clusterBuffer; Model* _model; int _meshIndex; diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 3448c9e8da..48c1d29b68 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -176,11 +176,11 @@ void Model::setOffset(const glm::vec3& offset) { } void Model::calculateTextureInfo() { - if (!_hasCalculatedTextureInfo && isLoaded() && getGeometry()->areTexturesLoaded() && !_modelMeshRenderItemsMap.isEmpty()) { + if (!_hasCalculatedTextureInfo && isLoaded() && getGeometry()->areTexturesLoaded() && !_modelMeshRenderItems.isEmpty()) { size_t textureSize = 0; int textureCount = 0; bool allTexturesLoaded = true; - foreach(auto renderItem, _modelMeshRenderItems) { + foreach(auto renderItem, _modelMeshRenderItemsSet) { auto meshPart = renderItem.get(); textureSize += meshPart->getMaterialTextureSize(); textureCount += meshPart->getMaterialTextureCount(); @@ -227,16 +227,12 @@ void Model::updateRenderItems() { return; } - // lazy update of cluster matrices used for rendering. - // We need to update them here so we can correctly update the bounding box. - self->updateClusterMatrices(); - render::ScenePointer scene = AbstractViewStateInterface::instance()->getMain3DScene(); uint32_t deleteGeometryCounter = self->_deleteGeometryCounter; render::PendingChanges pendingChanges; - foreach (auto itemID, self->_modelMeshRenderItemsMap.keys()) { + foreach (auto itemID, self->_modelMeshRenderItems.keys()) { pendingChanges.updateItem(itemID, [deleteGeometryCounter](ModelMeshPartPayload& data) { if (data._model && data._model->isLoaded()) { // Ensure the model geometry was not reset between frames @@ -244,12 +240,12 @@ void Model::updateRenderItems() { Transform modelTransform = data._model->getTransform(); modelTransform.setScale(glm::vec3(1.0f)); - const Model::MeshState& state = data._model->getMeshState(data._meshIndex); - Transform renderTransform = modelTransform; - if (state.clusterMatrices.size() == 1) { - renderTransform = modelTransform.worldTransform(Transform(state.clusterMatrices[0])); - } - data.updateTransformForSkinnedMesh(renderTransform, modelTransform, state.clusterBuffer); + // lazy update of cluster matrices used for rendering. We need to update them here, so we can correctly update the bounding box. + data._model->updateClusterMatrices(); + + // update the model transform and bounding box for this render item. + const Model::MeshState& state = data._model->_meshStates.at(data._meshIndex); + data.updateTransformForSkinnedMesh(modelTransform, state.clusterMatrices); } } }); @@ -259,7 +255,7 @@ void Model::updateRenderItems() { Transform collisionMeshOffset; collisionMeshOffset.setIdentity(); Transform modelTransform = self->getTransform(); - foreach(auto itemID, self->_collisionRenderItemsMap.keys()) { + foreach (auto itemID, self->_collisionRenderItems.keys()) { pendingChanges.updateItem(itemID, [modelTransform, collisionMeshOffset](MeshPartPayload& data) { // update the model transform for this render item. data.updateTransform(modelTransform, collisionMeshOffset); @@ -539,11 +535,11 @@ void Model::setVisibleInScene(bool newValue, std::shared_ptr scen _isVisible = newValue; render::PendingChanges pendingChanges; - foreach (auto item, _modelMeshRenderItemsMap.keys()) { - pendingChanges.resetItem(item, _modelMeshRenderItemsMap[item]); + foreach (auto item, _modelMeshRenderItems.keys()) { + pendingChanges.resetItem(item, _modelMeshRenderItems[item]); } - foreach(auto item, _collisionRenderItemsMap.keys()) { - pendingChanges.resetItem(item, _collisionRenderItemsMap[item]); + foreach (auto item, _collisionRenderItems.keys()) { + pendingChanges.resetItem(item, _collisionRenderItems[item]); } scene->enqueuePendingChanges(pendingChanges); } @@ -555,11 +551,11 @@ void Model::setLayeredInFront(bool layered, std::shared_ptr scene _isLayeredInFront = layered; render::PendingChanges pendingChanges; - foreach(auto item, _modelMeshRenderItemsMap.keys()) { - pendingChanges.resetItem(item, _modelMeshRenderItemsMap[item]); + foreach(auto item, _modelMeshRenderItems.keys()) { + pendingChanges.resetItem(item, _modelMeshRenderItems[item]); } - foreach(auto item, _collisionRenderItemsMap.keys()) { - pendingChanges.resetItem(item, _collisionRenderItemsMap[item]); + foreach(auto item, _collisionRenderItems.keys()) { + pendingChanges.resetItem(item, _collisionRenderItems[item]); } scene->enqueuePendingChanges(pendingChanges); } @@ -576,39 +572,39 @@ bool Model::addToScene(std::shared_ptr scene, bool somethingAdded = false; if (_collisionGeometry) { if (_collisionRenderItems.empty()) { - foreach (auto renderItem, _collisionRenderItems) { + foreach (auto renderItem, _collisionRenderItemsSet) { auto item = scene->allocateID(); auto renderPayload = std::make_shared(renderItem); - if (_collisionRenderItems.empty() && statusGetters.size()) { + if (statusGetters.size()) { renderPayload->addStatusGetters(statusGetters); } pendingChanges.resetItem(item, renderPayload); - _collisionRenderItemsMap.insert(item, renderPayload); + _collisionRenderItems.insert(item, renderPayload); } somethingAdded = !_collisionRenderItems.empty(); } } else { - if (_modelMeshRenderItemsMap.empty()) { + if (_modelMeshRenderItems.empty()) { bool hasTransparent = false; size_t verticesCount = 0; - foreach(auto renderItem, _modelMeshRenderItems) { + foreach(auto renderItem, _modelMeshRenderItemsSet) { auto item = scene->allocateID(); auto renderPayload = std::make_shared(renderItem); - if (_modelMeshRenderItemsMap.empty() && statusGetters.size()) { + if (statusGetters.size()) { renderPayload->addStatusGetters(statusGetters); } pendingChanges.resetItem(item, renderPayload); hasTransparent = hasTransparent || renderItem.get()->getShapeKey().isTranslucent(); verticesCount += renderItem.get()->getVerticesCount(); - _modelMeshRenderItemsMap.insert(item, renderPayload); + _modelMeshRenderItems.insert(item, renderPayload); _modelMeshRenderItemIDs.emplace_back(item); } - somethingAdded = !_modelMeshRenderItemsMap.empty(); + somethingAdded = !_modelMeshRenderItems.empty(); _renderInfoVertexCount = verticesCount; - _renderInfoDrawCalls = _modelMeshRenderItemsMap.count(); + _renderInfoDrawCalls = _modelMeshRenderItems.count(); _renderInfoHasTransparent = hasTransparent; } } @@ -623,18 +619,18 @@ bool Model::addToScene(std::shared_ptr scene, } void Model::removeFromScene(std::shared_ptr scene, render::PendingChanges& pendingChanges) { - foreach (auto item, _modelMeshRenderItemsMap.keys()) { + foreach (auto item, _modelMeshRenderItems.keys()) { pendingChanges.removeItem(item); } _modelMeshRenderItemIDs.clear(); - _modelMeshRenderItemsMap.clear(); _modelMeshRenderItems.clear(); + _modelMeshRenderItemsSet.clear(); - foreach(auto item, _collisionRenderItemsMap.keys()) { + foreach (auto item, _collisionRenderItems.keys()) { pendingChanges.removeItem(item); } _collisionRenderItems.clear(); - _collisionRenderItems.clear(); + _collisionRenderItemsSet.clear(); _addedToScene = false; _renderInfoVertexCount = 0; @@ -1052,8 +1048,8 @@ void Model::updateRig(float deltaTime, glm::mat4 parentTransform) { } void Model::computeMeshPartLocalBounds() { - for (auto& part : _modelMeshRenderItems) { - assert(part->_meshIndex < _modelMeshRenderItems.size()); + for (auto& part : _modelMeshRenderItemsSet) { + assert(part->_meshIndex < _modelMeshRenderItemsSet.size()); const Model::MeshState& state = _meshStates.at(part->_meshIndex); part->computeAdjustedLocalBound(state.clusterMatrices); } @@ -1167,7 +1163,7 @@ AABox Model::getRenderableMeshBound() const { } else { // Build a bound using the last known bound from all the renderItems. AABox totalBound; - for (auto& renderItem : _modelMeshRenderItems) { + for (auto& renderItem : _modelMeshRenderItemsSet) { totalBound += renderItem->getBound(); } return totalBound; @@ -1180,11 +1176,11 @@ const render::ItemIDs& Model::fetchRenderItemIDs() const { void Model::createRenderItemSet() { if (_collisionGeometry) { - if (_collisionRenderItems.empty()) { + if (_collisionRenderItemsSet.empty()) { createCollisionRenderItemSet(); } } else { - if (_modelMeshRenderItems.empty()) { + if (_modelMeshRenderItemsSet.empty()) { createVisibleRenderItemSet(); } } @@ -1201,9 +1197,9 @@ void Model::createVisibleRenderItemSet() { } // We should not have any existing renderItems if we enter this section of code - Q_ASSERT(_modelMeshRenderItems.isEmpty()); + Q_ASSERT(_modelMeshRenderItemsSet.isEmpty()); - _modelMeshRenderItems.clear(); + _modelMeshRenderItemsSet.clear(); Transform transform; transform.setTranslation(_translation); @@ -1225,7 +1221,7 @@ void Model::createVisibleRenderItemSet() { // Create the render payloads int numParts = (int)mesh->getNumParts(); for (int partIndex = 0; partIndex < numParts; partIndex++) { - _modelMeshRenderItems << std::make_shared(this, i, partIndex, shapeID, transform, offset); + _modelMeshRenderItemsSet << std::make_shared(this, i, partIndex, shapeID, transform, offset); shapeID++; } } @@ -1241,7 +1237,7 @@ void Model::createCollisionRenderItemSet() { const auto& meshes = _collisionGeometry->getMeshes(); // We should not have any existing renderItems if we enter this section of code - Q_ASSERT(_collisionRenderItems.isEmpty()); + Q_ASSERT(_collisionRenderItemsSet.isEmpty()); Transform identity; identity.setIdentity(); @@ -1262,7 +1258,7 @@ void Model::createCollisionRenderItemSet() { model::MaterialPointer& material = _collisionMaterials[partIndex % NUM_COLLISION_HULL_COLORS]; auto payload = std::make_shared(mesh, partIndex, material); payload->updateTransform(identity, offset); - _collisionRenderItems << payload; + _collisionRenderItemsSet << payload; } } } @@ -1283,28 +1279,28 @@ bool Model::initWhenReady(render::ScenePointer scene) { bool addedPendingChanges = false; if (_collisionGeometry) { - foreach (auto renderItem, _collisionRenderItems) { + foreach (auto renderItem, _collisionRenderItemsSet) { auto item = scene->allocateID(); auto renderPayload = std::make_shared(renderItem); - _collisionRenderItemsMap.insert(item, renderPayload); + _collisionRenderItems.insert(item, renderPayload); pendingChanges.resetItem(item, renderPayload); } addedPendingChanges = !_collisionRenderItems.empty(); } else { bool hasTransparent = false; size_t verticesCount = 0; - foreach (auto renderItem, _modelMeshRenderItems) { + foreach (auto renderItem, _modelMeshRenderItemsSet) { auto item = scene->allocateID(); auto renderPayload = std::make_shared(renderItem); hasTransparent = hasTransparent || renderItem.get()->getShapeKey().isTranslucent(); verticesCount += renderItem.get()->getVerticesCount(); - _modelMeshRenderItemsMap.insert(item, renderPayload); + _modelMeshRenderItems.insert(item, renderPayload); pendingChanges.resetItem(item, renderPayload); } - addedPendingChanges = !_modelMeshRenderItemsMap.empty(); + addedPendingChanges = !_modelMeshRenderItems.empty(); _renderInfoVertexCount = verticesCount; - _renderInfoDrawCalls = _modelMeshRenderItemsMap.count(); + _renderInfoDrawCalls = _modelMeshRenderItems.count(); _renderInfoHasTransparent = hasTransparent; } _addedToScene = addedPendingChanges; diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index bb283cce1f..41821736f7 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -248,7 +248,7 @@ public: const MeshState& getMeshState(int index) { return _meshStates.at(index); } uint32_t getGeometryCounter() const { return _deleteGeometryCounter; } - const QMap& getRenderItems() const { return _modelMeshRenderItemsMap; } + const QMap& getRenderItems() const { return _modelMeshRenderItems; } void renderDebugMeshBoxes(gpu::Batch& batch); @@ -373,11 +373,11 @@ protected: static AbstractViewStateInterface* _viewState; - QVector> _collisionRenderItems; - QMap _collisionRenderItemsMap; + QSet> _collisionRenderItemsSet; + QMap _collisionRenderItems; - QVector> _modelMeshRenderItems; - QMap _modelMeshRenderItemsMap; + QSet> _modelMeshRenderItemsSet; + QMap _modelMeshRenderItems; render::ItemIDs _modelMeshRenderItemIDs; diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index 22aa95090c..676d176cca 100644 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -194,7 +194,7 @@ RenderDeferredTask::RenderDeferredTask(RenderFetchCullSortTask::Output items) { { // Grab a texture map representing the different status icons and assign that to the drawStatsuJob auto iconMapPath = PathUtils::resourcesPath() + "icons/statusIconAtlas.svg"; - auto statusIconMap = DependencyManager::get()->getImageTexture(iconMapPath, NetworkTexture::STRICT_TEXTURE); + auto statusIconMap = DependencyManager::get()->getImageTexture(iconMapPath); addJob("DrawStatus", opaques, DrawStatus(statusIconMap)); } } @@ -259,18 +259,8 @@ void DrawDeferred::run(const SceneContextPointer& sceneContext, const RenderCont // Setup lighting model for all items; batch.setUniformBuffer(render::ShapePipeline::Slot::LIGHTING_MODEL, lightingModel->getParametersBuffer()); - // From the lighting model define a global shapKey ORED with individiual keys - ShapeKey::Builder keyBuilder; - if (lightingModel->isWireframeEnabled()) { - keyBuilder.withWireframe(); - } - ShapeKey globalKey = keyBuilder.build(); - args->_globalShapeKey = globalKey._flags.to_ulong(); - - renderShapes(sceneContext, renderContext, _shapePlumber, inItems, _maxDrawn, globalKey); - + renderShapes(sceneContext, renderContext, _shapePlumber, inItems, _maxDrawn); args->_batch = nullptr; - args->_globalShapeKey = 0; }); config->setNumDrawn((int)inItems.size()); @@ -305,21 +295,12 @@ void DrawStateSortDeferred::run(const SceneContextPointer& sceneContext, const R // Setup lighting model for all items; batch.setUniformBuffer(render::ShapePipeline::Slot::LIGHTING_MODEL, lightingModel->getParametersBuffer()); - // From the lighting model define a global shapKey ORED with individiual keys - ShapeKey::Builder keyBuilder; - if (lightingModel->isWireframeEnabled()) { - keyBuilder.withWireframe(); - } - ShapeKey globalKey = keyBuilder.build(); - args->_globalShapeKey = globalKey._flags.to_ulong(); - if (_stateSort) { - renderStateSortShapes(sceneContext, renderContext, _shapePlumber, inItems, _maxDrawn, globalKey); + renderStateSortShapes(sceneContext, renderContext, _shapePlumber, inItems, _maxDrawn); } else { - renderShapes(sceneContext, renderContext, _shapePlumber, inItems, _maxDrawn, globalKey); + renderShapes(sceneContext, renderContext, _shapePlumber, inItems, _maxDrawn); } args->_batch = nullptr; - args->_globalShapeKey = 0; }); config->setNumDrawn((int)inItems.size()); diff --git a/libraries/render-utils/src/RenderPipelines.cpp b/libraries/render-utils/src/RenderPipelines.cpp index 414bcf0d63..4fbac4170e 100644 --- a/libraries/render-utils/src/RenderPipelines.cpp +++ b/libraries/render-utils/src/RenderPipelines.cpp @@ -307,7 +307,7 @@ void initForwardPipelines(render::ShapePlumber& plumber) { void addPlumberPipeline(ShapePlumber& plumber, const ShapeKey& key, const gpu::ShaderPointer& vertex, const gpu::ShaderPointer& pixel) { // These key-values' pipelines are added by this functor in addition to the key passed - assert(!key.isWireframe()); + assert(!key.isWireFrame()); assert(!key.isDepthBiased()); assert(key.isCullFace()); diff --git a/libraries/render-utils/src/SubsurfaceScattering.cpp b/libraries/render-utils/src/SubsurfaceScattering.cpp index 25a01bff1b..188381b822 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.cpp +++ b/libraries/render-utils/src/SubsurfaceScattering.cpp @@ -414,7 +414,7 @@ gpu::TexturePointer SubsurfaceScatteringResource::generateScatteringProfile(Rend const int PROFILE_RESOLUTION = 512; // const auto pixelFormat = gpu::Element::COLOR_SRGBA_32; const auto pixelFormat = gpu::Element::COLOR_R11G11B10; - auto profileMap = gpu::TexturePointer(gpu::Texture::createRenderBuffer(pixelFormat, PROFILE_RESOLUTION, 1, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP))); + auto profileMap = gpu::TexturePointer(gpu::Texture::create2D(pixelFormat, PROFILE_RESOLUTION, 1, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP))); profileMap->setSource("Generated Scattering Profile"); diffuseProfileGPU(profileMap, args); return profileMap; @@ -425,7 +425,7 @@ gpu::TexturePointer SubsurfaceScatteringResource::generatePreIntegratedScatterin const int TABLE_RESOLUTION = 512; // const auto pixelFormat = gpu::Element::COLOR_SRGBA_32; const auto pixelFormat = gpu::Element::COLOR_R11G11B10; - auto scatteringLUT = gpu::TexturePointer(gpu::Texture::createRenderBuffer(pixelFormat, TABLE_RESOLUTION, TABLE_RESOLUTION, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP))); + auto scatteringLUT = gpu::TexturePointer(gpu::Texture::create2D(pixelFormat, TABLE_RESOLUTION, TABLE_RESOLUTION, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP))); //diffuseScatter(scatteringLUT); scatteringLUT->setSource("Generated pre-integrated scattering"); diffuseScatterGPU(profile, scatteringLUT, args); @@ -434,7 +434,7 @@ gpu::TexturePointer SubsurfaceScatteringResource::generatePreIntegratedScatterin gpu::TexturePointer SubsurfaceScatteringResource::generateScatteringSpecularBeckmann(RenderArgs* args) { const int SPECULAR_RESOLUTION = 256; - auto beckmannMap = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32 /*gpu::Element(gpu::SCALAR, gpu::HALF, gpu::RGB)*/, SPECULAR_RESOLUTION, SPECULAR_RESOLUTION, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP))); + auto beckmannMap = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32 /*gpu::Element(gpu::SCALAR, gpu::HALF, gpu::RGB)*/, SPECULAR_RESOLUTION, SPECULAR_RESOLUTION, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP))); beckmannMap->setSource("Generated beckmannMap"); computeSpecularBeckmannGPU(beckmannMap, args); return beckmannMap; diff --git a/libraries/render-utils/src/SurfaceGeometryPass.cpp b/libraries/render-utils/src/SurfaceGeometryPass.cpp index 3a23e70664..f0ac56ac26 100644 --- a/libraries/render-utils/src/SurfaceGeometryPass.cpp +++ b/libraries/render-utils/src/SurfaceGeometryPass.cpp @@ -72,18 +72,18 @@ void LinearDepthFramebuffer::allocate() { auto height = _frameSize.y; // For Linear Depth: - _linearDepthTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::RGB), width, height, + _linearDepthTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::RGB), width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); _linearDepthFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("linearDepth")); _linearDepthFramebuffer->setRenderBuffer(0, _linearDepthTexture); _linearDepthFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, _primaryDepthTexture->getTexelFormat()); // For Downsampling: - _halfLinearDepthTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::RGB), _halfFrameSize.x, _halfFrameSize.y, + _halfLinearDepthTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::RGB), _halfFrameSize.x, _halfFrameSize.y, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); _halfLinearDepthTexture->autoGenerateMips(5); - _halfNormalTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element(gpu::VEC3, gpu::NUINT8, gpu::RGB), _halfFrameSize.x, _halfFrameSize.y, + _halfNormalTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element(gpu::VEC3, gpu::NUINT8, gpu::RGB), _halfFrameSize.x, _halfFrameSize.y, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); _downsampleFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("halfLinearDepth")); @@ -304,15 +304,15 @@ void SurfaceGeometryFramebuffer::allocate() { auto width = _frameSize.x; auto height = _frameSize.y; - _curvatureTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32, width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); + _curvatureTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); _curvatureFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("surfaceGeometry::curvature")); _curvatureFramebuffer->setRenderBuffer(0, _curvatureTexture); - _lowCurvatureTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32, width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); + _lowCurvatureTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); _lowCurvatureFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("surfaceGeometry::lowCurvature")); _lowCurvatureFramebuffer->setRenderBuffer(0, _lowCurvatureTexture); - _blurringTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32, width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); + _blurringTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); _blurringFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("surfaceGeometry::blurring")); _blurringFramebuffer->setRenderBuffer(0, _blurringTexture); } diff --git a/libraries/render-utils/src/text/Font.cpp b/libraries/render-utils/src/text/Font.cpp index c405f6d6ae..4f4ee12622 100644 --- a/libraries/render-utils/src/text/Font.cpp +++ b/libraries/render-utils/src/text/Font.cpp @@ -209,8 +209,7 @@ void Font::read(QIODevice& in) { } _texture = gpu::TexturePointer(gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_POINT_MAG_LINEAR))); - _texture->setStoredMipFormat(formatMip); - _texture->assignStoredMip(0, image.byteCount(), image.constBits()); + _texture->assignStoredMip(0, formatMip, image.byteCount(), image.constBits()); } void Font::setupGPU() { diff --git a/libraries/render/CMakeLists.txt b/libraries/render/CMakeLists.txt index 8fd05bd320..735bb7f086 100644 --- a/libraries/render/CMakeLists.txt +++ b/libraries/render/CMakeLists.txt @@ -3,6 +3,6 @@ AUTOSCRIBE_SHADER_LIB(gpu model) setup_hifi_library() # render needs octree only for getAccuracyAngle(float, int) -link_hifi_libraries(shared ktx gpu model octree) +link_hifi_libraries(shared gpu model octree) target_nsight() diff --git a/libraries/render/src/render/DrawTask.cpp b/libraries/render/src/render/DrawTask.cpp index e8537e3452..2829c6f8e7 100755 --- a/libraries/render/src/render/DrawTask.cpp +++ b/libraries/render/src/render/DrawTask.cpp @@ -39,9 +39,9 @@ void render::renderItems(const SceneContextPointer& sceneContext, const RenderCo } } -void renderShape(RenderArgs* args, const ShapePlumberPointer& shapeContext, const Item& item, const ShapeKey& globalKey) { +void renderShape(RenderArgs* args, const ShapePlumberPointer& shapeContext, const Item& item) { assert(item.getKey().isShape()); - auto key = item.getShapeKey() | globalKey; + const auto& key = item.getShapeKey(); if (key.isValid() && !key.hasOwnPipeline()) { args->_pipeline = shapeContext->pickPipeline(args, key); if (args->_pipeline) { @@ -56,7 +56,7 @@ void renderShape(RenderArgs* args, const ShapePlumberPointer& shapeContext, cons } void render::renderShapes(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, - const ShapePlumberPointer& shapeContext, const ItemBounds& inItems, int maxDrawnItems, const ShapeKey& globalKey) { + const ShapePlumberPointer& shapeContext, const ItemBounds& inItems, int maxDrawnItems) { auto& scene = sceneContext->_scene; RenderArgs* args = renderContext->args; @@ -66,12 +66,12 @@ void render::renderShapes(const SceneContextPointer& sceneContext, const RenderC } for (auto i = 0; i < numItemsToDraw; ++i) { auto& item = scene->getItem(inItems[i].id); - renderShape(args, shapeContext, item, globalKey); + renderShape(args, shapeContext, item); } } void render::renderStateSortShapes(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, - const ShapePlumberPointer& shapeContext, const ItemBounds& inItems, int maxDrawnItems, const ShapeKey& globalKey) { + const ShapePlumberPointer& shapeContext, const ItemBounds& inItems, int maxDrawnItems) { auto& scene = sceneContext->_scene; RenderArgs* args = renderContext->args; @@ -91,7 +91,7 @@ void render::renderStateSortShapes(const SceneContextPointer& sceneContext, cons { assert(item.getKey().isShape()); - auto key = item.getShapeKey() | globalKey; + const auto key = item.getShapeKey(); if (key.isValid() && !key.hasOwnPipeline()) { auto& bucket = sortedShapes[key]; if (bucket.empty()) { diff --git a/libraries/render/src/render/DrawTask.h b/libraries/render/src/render/DrawTask.h index a9c5f3a4d8..6e0e5ba10b 100755 --- a/libraries/render/src/render/DrawTask.h +++ b/libraries/render/src/render/DrawTask.h @@ -17,8 +17,8 @@ namespace render { void renderItems(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const ItemBounds& inItems, int maxDrawnItems = -1); -void renderShapes(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const ShapePlumberPointer& shapeContext, const ItemBounds& inItems, int maxDrawnItems = -1, const ShapeKey& globalKey = ShapeKey()); -void renderStateSortShapes(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const ShapePlumberPointer& shapeContext, const ItemBounds& inItems, int maxDrawnItems = -1, const ShapeKey& globalKey = ShapeKey()); +void renderShapes(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const ShapePlumberPointer& shapeContext, const ItemBounds& inItems, int maxDrawnItems = -1); +void renderStateSortShapes(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const ShapePlumberPointer& shapeContext, const ItemBounds& inItems, int maxDrawnItems = -1); class DrawLightConfig : public Job::Config { Q_OBJECT diff --git a/libraries/render/src/render/ShapePipeline.h b/libraries/render/src/render/ShapePipeline.h index 73e8f82f24..0c77a15184 100644 --- a/libraries/render/src/render/ShapePipeline.h +++ b/libraries/render/src/render/ShapePipeline.h @@ -46,10 +46,6 @@ public: ShapeKey() : _flags{ 0 } {} ShapeKey(const Flags& flags) : _flags{flags} {} - friend ShapeKey operator&(const ShapeKey& _Left, const ShapeKey& _Right) { return ShapeKey(_Left._flags & _Right._flags); } - friend ShapeKey operator|(const ShapeKey& _Left, const ShapeKey& _Right) { return ShapeKey(_Left._flags | _Right._flags); } - friend ShapeKey operator^(const ShapeKey& _Left, const ShapeKey& _Right) { return ShapeKey(_Left._flags ^ _Right._flags); } - class Builder { public: Builder() {} @@ -148,7 +144,7 @@ public: bool isSkinned() const { return _flags[SKINNED]; } bool isDepthOnly() const { return _flags[DEPTH_ONLY]; } bool isDepthBiased() const { return _flags[DEPTH_BIAS]; } - bool isWireframe() const { return _flags[WIREFRAME]; } + bool isWireFrame() const { return _flags[WIREFRAME]; } bool isCullFace() const { return !_flags[NO_CULL_FACE]; } bool hasOwnPipeline() const { return _flags[OWN_PIPELINE]; } @@ -184,7 +180,7 @@ inline QDebug operator<<(QDebug debug, const ShapeKey& key) { << "isSkinned:" << key.isSkinned() << "isDepthOnly:" << key.isDepthOnly() << "isDepthBiased:" << key.isDepthBiased() - << "isWireframe:" << key.isWireframe() + << "isWireFrame:" << key.isWireFrame() << "isCullFace:" << key.isCullFace() << "]"; } diff --git a/libraries/render/src/render/drawItemStatus.slv b/libraries/render/src/render/drawItemStatus.slv index 792f2733c5..cb4ae7ebd2 100644 --- a/libraries/render/src/render/drawItemStatus.slv +++ b/libraries/render/src/render/drawItemStatus.slv @@ -75,7 +75,7 @@ void main(void) { vec4(1.0, 1.0, 0.0, 1.0) ); - const vec2 ICON_PIXEL_SIZE = vec2(36, 36); + const vec2 ICON_PIXEL_SIZE = vec2(20, 20); const vec2 MARGIN_PIXEL_SIZE = vec2(2, 2); const vec2 ICON_GRID_SLOTS[MAX_NUM_ICONS] = vec2[MAX_NUM_ICONS](vec2(-1.5, 0.5), vec2(-0.5, 0.5), @@ -114,7 +114,7 @@ void main(void) { varColor = vec4(paintRainbow(abs(iconStatus.y)), 1.0); // Pass the texcoord and the z texcoord is representing the texture icon - varTexcoord = vec3( (quadPos.x + 1.0) * 0.5, (quadPos.y + 1.0) * -0.5, iconStatus.z); + varTexcoord = vec3((quadPos.xy + 1.0) * 0.5, iconStatus.z); // Also changes the size of the notification vec2 iconScale = ICON_PIXEL_SIZE; diff --git a/libraries/script-engine/src/AudioScriptingInterface.cpp b/libraries/script-engine/src/AudioScriptingInterface.cpp index 8452494d95..fcc1f201f9 100644 --- a/libraries/script-engine/src/AudioScriptingInterface.cpp +++ b/libraries/script-engine/src/AudioScriptingInterface.cpp @@ -19,6 +19,11 @@ void registerAudioMetaTypes(QScriptEngine* engine) { qScriptRegisterMetaType(engine, soundSharedPointerToScriptValue, soundSharedPointerFromScriptValue); } +AudioScriptingInterface& AudioScriptingInterface::getInstance() { + static AudioScriptingInterface staticInstance; + return staticInstance; +} + AudioScriptingInterface::AudioScriptingInterface() : _localAudioInterface(NULL) { diff --git a/libraries/script-engine/src/AudioScriptingInterface.h b/libraries/script-engine/src/AudioScriptingInterface.h index e97bc329c6..6cce78d48f 100644 --- a/libraries/script-engine/src/AudioScriptingInterface.h +++ b/libraries/script-engine/src/AudioScriptingInterface.h @@ -14,20 +14,18 @@ #include #include -#include #include class ScriptAudioInjector; -class AudioScriptingInterface : public QObject, public Dependency { +class AudioScriptingInterface : public QObject { Q_OBJECT - SINGLETON_DEPENDENCY - public: + static AudioScriptingInterface& getInstance(); + void setLocalAudioInterface(AbstractAudioInterface* audioInterface) { _localAudioInterface = audioInterface; } protected: - // this method is protected to stop C++ callers from calling, but invokable from script Q_INVOKABLE ScriptAudioInjector* playSound(SharedSoundPointer sound, const AudioInjectorOptions& injectorOptions = AudioInjectorOptions()); @@ -44,7 +42,6 @@ signals: private: AudioScriptingInterface(); - AbstractAudioInterface* _localAudioInterface; }; diff --git a/libraries/shared/src/BaseScriptEngine.cpp b/libraries/script-engine/src/BaseScriptEngine.cpp similarity index 68% rename from libraries/shared/src/BaseScriptEngine.cpp rename to libraries/script-engine/src/BaseScriptEngine.cpp index c92d629b75..16308c0650 100644 --- a/libraries/shared/src/BaseScriptEngine.cpp +++ b/libraries/script-engine/src/BaseScriptEngine.cpp @@ -10,7 +10,6 @@ // #include "BaseScriptEngine.h" -#include "SharedLogging.h" #include #include @@ -19,27 +18,18 @@ #include #include +#include "ScriptEngineLogging.h" #include "Profile.h" +const QString BaseScriptEngine::_SETTINGS_ENABLE_EXTENDED_EXCEPTIONS { + "com.highfidelity.experimental.enableExtendedJSExceptions" +}; + const QString BaseScriptEngine::SCRIPT_EXCEPTION_FORMAT { "[%0] %1 in %2:%3" }; const QString BaseScriptEngine::SCRIPT_BACKTRACE_SEP { "\n " }; -bool BaseScriptEngine::IS_THREADSAFE_INVOCATION(const QThread *thread, const QString& method) { - if (QThread::currentThread() == thread) { - return true; - } - qCCritical(shared) << QString("Scripting::%1 @ %2 -- ignoring thread-unsafe call from %3") - .arg(method).arg(thread ? thread->objectName() : "(!thread)").arg(QThread::currentThread()->objectName()); - qCDebug(shared) << "(please resolve on the calling side by using invokeMethod, executeOnScriptThread, etc.)"; - Q_ASSERT(false); - return false; -} - // engine-aware JS Error copier and factory QScriptValue BaseScriptEngine::makeError(const QScriptValue& _other, const QString& type) { - if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { - return unboundNullValue(); - } auto other = _other; if (other.isString()) { other = newObject(); @@ -51,7 +41,7 @@ QScriptValue BaseScriptEngine::makeError(const QScriptValue& _other, const QStri } if (!proto.isFunction()) { #ifdef DEBUG_JS_EXCEPTIONS - qCDebug(shared) << "BaseScriptEngine::makeError -- couldn't find constructor for" << type << " -- using Error instead"; + qCDebug(scriptengine) << "BaseScriptEngine::makeError -- couldn't find constructor for" << type << " -- using Error instead"; #endif proto = globalObject().property("Error"); } @@ -74,9 +64,6 @@ QScriptValue BaseScriptEngine::makeError(const QScriptValue& _other, const QStri // check syntax and when there are issues returns an actual "SyntaxError" with the details QScriptValue BaseScriptEngine::lintScript(const QString& sourceCode, const QString& fileName, const int lineNumber) { - if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { - return unboundNullValue(); - } const auto syntaxCheck = checkSyntax(sourceCode); if (syntaxCheck.state() != QScriptSyntaxCheckResult::Valid) { auto err = globalObject().property("SyntaxError") @@ -95,16 +82,13 @@ QScriptValue BaseScriptEngine::lintScript(const QString& sourceCode, const QStri } return err; } - return QScriptValue(); + return undefinedValue(); } // this pulls from the best available information to create a detailed snapshot of the current exception QScriptValue BaseScriptEngine::cloneUncaughtException(const QString& extraDetail) { - if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { - return unboundNullValue(); - } if (!hasUncaughtException()) { - return unboundNullValue(); + return QScriptValue(); } auto exception = uncaughtException(); // ensure the error object is engine-local @@ -160,10 +144,7 @@ QScriptValue BaseScriptEngine::cloneUncaughtException(const QString& extraDetail return err; } -QString BaseScriptEngine::formatException(const QScriptValue& exception, bool includeExtendedDetails) { - if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { - return QString(); - } +QString BaseScriptEngine::formatException(const QScriptValue& exception) { QString note { "UncaughtException" }; QString result; @@ -175,8 +156,8 @@ QString BaseScriptEngine::formatException(const QScriptValue& exception, bool in const auto lineNumber = exception.property("lineNumber").toString(); const auto stacktrace = exception.property("stack").toString(); - if (includeExtendedDetails) { - // Display additional exception / troubleshooting hints that can be added via the custom Error .detail property + if (_enableExtendedJSExceptions.get()) { + // This setting toggles display of the hints now being added during the loading process. // Example difference: // [UncaughtExceptions] Error: Can't find variable: foobar in atp:/myentity.js\n... // [UncaughtException (construct {1eb5d3fa-23b1-411c-af83-163af7220e3f})] Error: Can't find variable: foobar in atp:/myentity.js\n... @@ -192,39 +173,14 @@ QString BaseScriptEngine::formatException(const QScriptValue& exception, bool in return result; } -bool BaseScriptEngine::raiseException(const QScriptValue& exception) { - if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { - return false; - } - if (currentContext()) { - // we have an active context / JS stack frame so throw the exception per usual - currentContext()->throwValue(makeError(exception)); - return true; - } else { - // we are within a pure C++ stack frame (ie: being called directly by other C++ code) - // in this case no context information is available so just emit the exception for reporting - emit unhandledException(makeError(exception)); - } - return false; -} - -bool BaseScriptEngine::maybeEmitUncaughtException(const QString& debugHint) { - if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { - return false; - } - if (!isEvaluating() && hasUncaughtException()) { - emit unhandledException(cloneUncaughtException(debugHint)); - clearExceptions(); - return true; - } - return false; -} - QScriptValue BaseScriptEngine::evaluateInClosure(const QScriptValue& closure, const QScriptProgram& program) { PROFILE_RANGE(script, "evaluateInClosure"); - if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { - return unboundNullValue(); + if (QThread::currentThread() != thread()) { + qCCritical(scriptengine) << "*** CRITICAL *** ScriptEngine::evaluateInClosure() is meant to be called from engine thread only."; + // note: a recursive mutex might be needed around below code if this method ever becomes Q_INVOKABLE + return QScriptValue(); } + const auto fileName = program.fileName(); const auto shortName = QUrl(fileName).fileName(); @@ -233,7 +189,7 @@ QScriptValue BaseScriptEngine::evaluateInClosure(const QScriptValue& closure, co auto global = closure.property("global"); if (global.isObject()) { #ifdef DEBUG_JS - qCDebug(shared) << " setting global = closure.global" << shortName; + qCDebug(scriptengine) << " setting global = closure.global" << shortName; #endif oldGlobal = globalObject(); setGlobalObject(global); @@ -244,34 +200,34 @@ QScriptValue BaseScriptEngine::evaluateInClosure(const QScriptValue& closure, co auto thiz = closure.property("this"); if (thiz.isObject()) { #ifdef DEBUG_JS - qCDebug(shared) << " setting this = closure.this" << shortName; + qCDebug(scriptengine) << " setting this = closure.this" << shortName; #endif context->setThisObject(thiz); } context->pushScope(closure); #ifdef DEBUG_JS - qCDebug(shared) << QString("[%1] evaluateInClosure %2").arg(isEvaluating()).arg(shortName); + qCDebug(scriptengine) << QString("[%1] evaluateInClosure %2").arg(isEvaluating()).arg(shortName); #endif { result = BaseScriptEngine::evaluate(program); if (hasUncaughtException()) { auto err = cloneUncaughtException(__FUNCTION__); #ifdef DEBUG_JS_EXCEPTIONS - qCWarning(shared) << __FUNCTION__ << "---------- hasCaught:" << err.toString() << result.toString(); + qCWarning(scriptengine) << __FUNCTION__ << "---------- hasCaught:" << err.toString() << result.toString(); err.setProperty("_result", result); #endif result = err; } } #ifdef DEBUG_JS - qCDebug(shared) << QString("[%1] //evaluateInClosure %2").arg(isEvaluating()).arg(shortName); + qCDebug(scriptengine) << QString("[%1] //evaluateInClosure %2").arg(isEvaluating()).arg(shortName); #endif popContext(); if (oldGlobal.isValid()) { #ifdef DEBUG_JS - qCDebug(shared) << " restoring global" << shortName; + qCDebug(scriptengine) << " restoring global" << shortName; #endif setGlobalObject(oldGlobal); } @@ -280,6 +236,7 @@ QScriptValue BaseScriptEngine::evaluateInClosure(const QScriptValue& closure, co } // Lambda + QScriptValue BaseScriptEngine::newLambdaFunction(std::function operation, const QScriptValue& data, const QScriptEngine::ValueOwnership& ownership) { auto lambda = new Lambda(this, operation, data); auto object = newQObject(lambda, ownership); @@ -305,57 +262,26 @@ Lambda::Lambda(QScriptEngine *engine, std::functionthread(), __FUNCTION__)) { - return BaseScriptEngine::unboundNullValue(); - } return operation(engine->currentContext(), engine); } -QScriptValue makeScopedHandlerObject(QScriptValue scopeOrCallback, QScriptValue methodOrName) { - auto engine = scopeOrCallback.engine(); - if (!engine) { - return scopeOrCallback; - } - auto scope = QScriptValue(); - auto callback = scopeOrCallback; - if (scopeOrCallback.isObject()) { - if (methodOrName.isString()) { - scope = scopeOrCallback; - callback = scope.property(methodOrName.toString()); - } else if (methodOrName.isFunction()) { - scope = scopeOrCallback; - callback = methodOrName; - } - } - auto handler = engine->newObject(); - handler.setProperty("scope", scope); - handler.setProperty("callback", callback); - return handler; -} - -QScriptValue callScopedHandlerObject(QScriptValue handler, QScriptValue err, QScriptValue result) { - return handler.property("callback").call(handler.property("scope"), QScriptValueList({ err, result })); -} - #ifdef DEBUG_JS void BaseScriptEngine::_debugDump(const QString& header, const QScriptValue& object, const QString& footer) { - if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { - return; - } if (!header.isEmpty()) { - qCDebug(shared) << header; + qCDebug(scriptengine) << header; } if (!object.isObject()) { - qCDebug(shared) << "(!isObject)" << object.toVariant().toString() << object.toString(); + qCDebug(scriptengine) << "(!isObject)" << object.toVariant().toString() << object.toString(); return; } QScriptValueIterator it(object); while (it.hasNext()) { it.next(); - qCDebug(shared) << it.name() << ":" << it.value().toString(); + qCDebug(scriptengine) << it.name() << ":" << it.value().toString(); } if (!footer.isEmpty()) { - qCDebug(shared) << footer; + qCDebug(scriptengine) << footer; } } #endif + diff --git a/libraries/script-engine/src/BaseScriptEngine.h b/libraries/script-engine/src/BaseScriptEngine.h new file mode 100644 index 0000000000..27a6eff33d --- /dev/null +++ b/libraries/script-engine/src/BaseScriptEngine.h @@ -0,0 +1,67 @@ +// +// BaseScriptEngine.h +// libraries/script-engine/src +// +// Created by Timothy Dedischew on 02/01/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_BaseScriptEngine_h +#define hifi_BaseScriptEngine_h + +#include +#include +#include + +#include "SettingHandle.h" + +// common base class for extending QScriptEngine itself +class BaseScriptEngine : public QScriptEngine { + Q_OBJECT +public: + static const QString SCRIPT_EXCEPTION_FORMAT; + static const QString SCRIPT_BACKTRACE_SEP; + + BaseScriptEngine() {} + + Q_INVOKABLE QScriptValue evaluateInClosure(const QScriptValue& locals, const QScriptProgram& program); + + Q_INVOKABLE QScriptValue lintScript(const QString& sourceCode, const QString& fileName, const int lineNumber = 1); + Q_INVOKABLE QScriptValue makeError(const QScriptValue& other = QScriptValue(), const QString& type = "Error"); + Q_INVOKABLE QString formatException(const QScriptValue& exception); + QScriptValue cloneUncaughtException(const QString& detail = QString()); + +signals: + void unhandledException(const QScriptValue& exception); + +protected: + void _emitUnhandledException(const QScriptValue& exception); + QScriptValue newLambdaFunction(std::function operation, const QScriptValue& data = QScriptValue(), const QScriptEngine::ValueOwnership& ownership = QScriptEngine::AutoOwnership); + + static const QString _SETTINGS_ENABLE_EXTENDED_EXCEPTIONS; + Setting::Handle _enableExtendedJSExceptions { _SETTINGS_ENABLE_EXTENDED_EXCEPTIONS, true }; +#ifdef DEBUG_JS + static void _debugDump(const QString& header, const QScriptValue& object, const QString& footer = QString()); +#endif +}; + +// Lambda helps create callable QScriptValues out of std::functions: +// (just meant for use from within the script engine itself) +class Lambda : public QObject { + Q_OBJECT +public: + Lambda(QScriptEngine *engine, std::function operation, QScriptValue data); + ~Lambda(); + public slots: + QScriptValue call(); + QString toString() const; +private: + QScriptEngine* engine; + std::function operation; + QScriptValue data; +}; + +#endif // hifi_BaseScriptEngine_h diff --git a/libraries/script-engine/src/Mat4.cpp b/libraries/script-engine/src/Mat4.cpp index 6676d0cde1..52b9690321 100644 --- a/libraries/script-engine/src/Mat4.cpp +++ b/libraries/script-engine/src/Mat4.cpp @@ -54,7 +54,7 @@ glm::mat4 Mat4::inverse(const glm::mat4& m) const { return glm::inverse(m); } -glm::vec3 Mat4::getForward(const glm::mat4& m) const { +glm::vec3 Mat4::getFront(const glm::mat4& m) const { return glm::vec3(-m[0][2], -m[1][2], -m[2][2]); } diff --git a/libraries/script-engine/src/Mat4.h b/libraries/script-engine/src/Mat4.h index 19bbbe178a..8b2a8aa8c1 100644 --- a/libraries/script-engine/src/Mat4.h +++ b/libraries/script-engine/src/Mat4.h @@ -37,9 +37,7 @@ public slots: glm::mat4 inverse(const glm::mat4& m) const; - // redundant, calls getForward which better describes the returned vector as a direction - glm::vec3 getFront(const glm::mat4& m) const { return getForward(m); } - glm::vec3 getForward(const glm::mat4& m) const; + glm::vec3 getFront(const glm::mat4& m) const; glm::vec3 getRight(const glm::mat4& m) const; glm::vec3 getUp(const glm::mat4& m) const; diff --git a/libraries/script-engine/src/MeshProxy.h b/libraries/script-engine/src/MeshProxy.h deleted file mode 100644 index 82f5038348..0000000000 --- a/libraries/script-engine/src/MeshProxy.h +++ /dev/null @@ -1,41 +0,0 @@ -// -// MeshProxy.h -// libraries/script-engine/src -// -// Created by Seth Alves on 2017-1-27. -// Copyright 2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#ifndef hifi_MeshProxy_h -#define hifi_MeshProxy_h - -#include - -using MeshPointer = std::shared_ptr; - -class MeshProxy : public QObject { - Q_OBJECT - -public: - MeshProxy(MeshPointer mesh) : _mesh(mesh) {} - ~MeshProxy() {} - - MeshPointer getMeshPointer() const { return _mesh; } - - Q_INVOKABLE int getNumVertices() const { return (int)_mesh->getNumVertices(); } - Q_INVOKABLE glm::vec3 getPos3(int index) const { return _mesh->getPos3(index); } - - -protected: - MeshPointer _mesh; -}; - -Q_DECLARE_METATYPE(MeshProxy*); - -class MeshProxyList : public QList {}; // typedef and using fight with the Qt macros/templates, do this instead -Q_DECLARE_METATYPE(MeshProxyList); - -#endif // hifi_MeshProxy_h diff --git a/libraries/script-engine/src/ModelScriptingInterface.cpp b/libraries/script-engine/src/ModelScriptingInterface.cpp deleted file mode 100644 index 833ac5b64d..0000000000 --- a/libraries/script-engine/src/ModelScriptingInterface.cpp +++ /dev/null @@ -1,159 +0,0 @@ -// -// ModelScriptingInterface.cpp -// libraries/script-engine/src -// -// Created by Seth Alves on 2017-1-27. -// Copyright 2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include -#include -#include -#include "ScriptEngine.h" -#include "ModelScriptingInterface.h" -#include "OBJWriter.h" - -ModelScriptingInterface::ModelScriptingInterface(QObject* parent) : QObject(parent) { - _modelScriptEngine = qobject_cast(parent); -} - -QScriptValue meshToScriptValue(QScriptEngine* engine, MeshProxy* const &in) { - return engine->newQObject(in, QScriptEngine::QtOwnership, - QScriptEngine::ExcludeDeleteLater | QScriptEngine::ExcludeChildObjects); -} - -void meshFromScriptValue(const QScriptValue& value, MeshProxy* &out) { - out = qobject_cast(value.toQObject()); -} - -QScriptValue meshesToScriptValue(QScriptEngine* engine, const MeshProxyList &in) { - return engine->toScriptValue(in); -} - -void meshesFromScriptValue(const QScriptValue& value, MeshProxyList &out) { - QScriptValueIterator itr(value); - while(itr.hasNext()) { - itr.next(); - MeshProxy* meshProxy = qscriptvalue_cast(itr.value()); - if (meshProxy) { - out.append(meshProxy); - } - } -} - -QString ModelScriptingInterface::meshToOBJ(MeshProxyList in) { - QList meshes; - foreach (const MeshProxy* meshProxy, in) { - meshes.append(meshProxy->getMeshPointer()); - } - - return writeOBJToString(meshes); -} - -QScriptValue ModelScriptingInterface::appendMeshes(MeshProxyList in) { - // figure out the size of the resulting mesh - size_t totalVertexCount { 0 }; - size_t totalAttributeCount { 0 }; - size_t totalIndexCount { 0 }; - foreach (const MeshProxy* meshProxy, in) { - MeshPointer mesh = meshProxy->getMeshPointer(); - totalVertexCount += mesh->getNumVertices(); - - int attributeTypeNormal = gpu::Stream::InputSlot::NORMAL; // libraries/gpu/src/gpu/Stream.h - const gpu::BufferView& normalsBufferView = mesh->getAttributeBuffer(attributeTypeNormal); - gpu::BufferView::Index numNormals = (gpu::BufferView::Index)normalsBufferView.getNumElements(); - totalAttributeCount += numNormals; - - totalIndexCount += mesh->getNumIndices(); - } - - // alloc the resulting mesh - gpu::Resource::Size combinedVertexSize = totalVertexCount * sizeof(glm::vec3); - unsigned char* combinedVertexData = new unsigned char[combinedVertexSize]; - unsigned char* combinedVertexDataCursor = combinedVertexData; - - gpu::Resource::Size combinedNormalSize = totalAttributeCount * sizeof(glm::vec3); - unsigned char* combinedNormalData = new unsigned char[combinedNormalSize]; - unsigned char* combinedNormalDataCursor = combinedNormalData; - - gpu::Resource::Size combinedIndexSize = totalIndexCount * sizeof(uint32_t); - unsigned char* combinedIndexData = new unsigned char[combinedIndexSize]; - unsigned char* combinedIndexDataCursor = combinedIndexData; - - uint32_t indexStartOffset { 0 }; - - foreach (const MeshProxy* meshProxy, in) { - MeshPointer mesh = meshProxy->getMeshPointer(); - mesh->forEach( - [&](glm::vec3 position){ - memcpy(combinedVertexDataCursor, &position, sizeof(position)); - combinedVertexDataCursor += sizeof(position); - }, - [&](glm::vec3 normal){ - memcpy(combinedNormalDataCursor, &normal, sizeof(normal)); - combinedNormalDataCursor += sizeof(normal); - }, - [&](uint32_t index){ - index += indexStartOffset; - memcpy(combinedIndexDataCursor, &index, sizeof(index)); - combinedIndexDataCursor += sizeof(index); - }); - - gpu::BufferView::Index numVertices = (gpu::BufferView::Index)mesh->getNumVertices(); - indexStartOffset += numVertices; - } - - model::MeshPointer result(new model::Mesh()); - - gpu::Element vertexElement = gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ); - gpu::Buffer* combinedVertexBuffer = new gpu::Buffer(combinedVertexSize, combinedVertexData); - gpu::BufferPointer combinedVertexBufferPointer(combinedVertexBuffer); - gpu::BufferView combinedVertexBufferView(combinedVertexBufferPointer, vertexElement); - result->setVertexBuffer(combinedVertexBufferView); - - int attributeTypeNormal = gpu::Stream::InputSlot::NORMAL; // libraries/gpu/src/gpu/Stream.h - gpu::Element normalElement = gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ); - gpu::Buffer* combinedNormalsBuffer = new gpu::Buffer(combinedNormalSize, combinedNormalData); - gpu::BufferPointer combinedNormalsBufferPointer(combinedNormalsBuffer); - gpu::BufferView combinedNormalsBufferView(combinedNormalsBufferPointer, normalElement); - result->addAttribute(attributeTypeNormal, combinedNormalsBufferView); - - gpu::Element indexElement = gpu::Element(gpu::SCALAR, gpu::UINT32, gpu::RAW); - gpu::Buffer* combinedIndexesBuffer = new gpu::Buffer(combinedIndexSize, combinedIndexData); - gpu::BufferPointer combinedIndexesBufferPointer(combinedIndexesBuffer); - gpu::BufferView combinedIndexesBufferView(combinedIndexesBufferPointer, indexElement); - result->setIndexBuffer(combinedIndexesBufferView); - - std::vector parts; - parts.emplace_back(model::Mesh::Part((model::Index)0, // startIndex - (model::Index)result->getNumIndices(), // numIndices - (model::Index)0, // baseVertex - model::Mesh::TRIANGLES)); // topology - result->setPartBuffer(gpu::BufferView(new gpu::Buffer(parts.size() * sizeof(model::Mesh::Part), - (gpu::Byte*) parts.data()), gpu::Element::PART_DRAWCALL)); - - - MeshProxy* resultProxy = new MeshProxy(result); - return meshToScriptValue(_modelScriptEngine, resultProxy); -} - - - -QScriptValue ModelScriptingInterface::transformMesh(glm::mat4 transform, MeshProxy* meshProxy) { - if (!meshProxy) { - return QScriptValue(false); - } - MeshPointer mesh = meshProxy->getMeshPointer(); - if (!mesh) { - return QScriptValue(false); - } - - model::MeshPointer result = mesh->map([&](glm::vec3 position){ return glm::vec3(transform * glm::vec4(position, 1.0f)); }, - [&](glm::vec3 normal){ return glm::vec3(transform * glm::vec4(normal, 0.0f)); }, - [&](uint32_t index){ return index; }); - MeshProxy* resultProxy = new MeshProxy(result); - return meshToScriptValue(_modelScriptEngine, resultProxy); -} diff --git a/libraries/script-engine/src/ModelScriptingInterface.h b/libraries/script-engine/src/ModelScriptingInterface.h deleted file mode 100644 index 14789943e3..0000000000 --- a/libraries/script-engine/src/ModelScriptingInterface.h +++ /dev/null @@ -1,45 +0,0 @@ -// -// ModelScriptingInterface.h -// libraries/script-engine/src -// -// Created by Seth Alves on 2017-1-27. -// Copyright 2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - - -#ifndef hifi_ModelScriptingInterface_h -#define hifi_ModelScriptingInterface_h - -#include -#include -#include -#include -#include "MeshProxy.h" - -using MeshPointer = std::shared_ptr; -class ScriptEngine; - -class ModelScriptingInterface : public QObject { - Q_OBJECT - -public: - ModelScriptingInterface(QObject* parent); - - Q_INVOKABLE QString meshToOBJ(MeshProxyList in); - Q_INVOKABLE QScriptValue appendMeshes(MeshProxyList in); - Q_INVOKABLE QScriptValue transformMesh(glm::mat4 transform, MeshProxy* meshProxy); - -private: - ScriptEngine* _modelScriptEngine { nullptr }; -}; - -QScriptValue meshToScriptValue(QScriptEngine* engine, MeshProxy* const &in); -void meshFromScriptValue(const QScriptValue& value, MeshProxy* &out); - -QScriptValue meshesToScriptValue(QScriptEngine* engine, const MeshProxyList &in); -void meshesFromScriptValue(const QScriptValue& value, MeshProxyList &out); - -#endif // hifi_ModelScriptingInterface_h diff --git a/libraries/script-engine/src/Quat.cpp b/libraries/script-engine/src/Quat.cpp index 6d49ed27c1..6c2e7a349e 100644 --- a/libraries/script-engine/src/Quat.cpp +++ b/libraries/script-engine/src/Quat.cpp @@ -68,7 +68,7 @@ glm::quat Quat::inverse(const glm::quat& q) { return glm::inverse(q); } -glm::vec3 Quat::getForward(const glm::quat& orientation) { +glm::vec3 Quat::getFront(const glm::quat& orientation) { return orientation * Vectors::FRONT; } diff --git a/libraries/script-engine/src/Quat.h b/libraries/script-engine/src/Quat.h index 8a88767a41..b51f1cb47e 100644 --- a/libraries/script-engine/src/Quat.h +++ b/libraries/script-engine/src/Quat.h @@ -45,9 +45,7 @@ public slots: glm::quat fromPitchYawRollDegrees(float pitch, float yaw, float roll); // degrees glm::quat fromPitchYawRollRadians(float pitch, float yaw, float roll); // radians glm::quat inverse(const glm::quat& q); - // redundant, calls getForward which better describes the returned vector as a direction - glm::vec3 getFront(const glm::quat& orientation) { return getForward(orientation); } - glm::vec3 getForward(const glm::quat& orientation); + glm::vec3 getFront(const glm::quat& orientation); glm::vec3 getRight(const glm::quat& orientation); glm::vec3 getUp(const glm::quat& orientation); glm::vec3 safeEulerAngles(const glm::quat& orientation); // degrees diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index a5c94c1bb4..d721d1c86f 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -19,9 +19,6 @@ #include #include -#include -#include - #include #include @@ -68,25 +65,18 @@ #include "RecordingScriptingInterface.h" #include "ScriptEngines.h" #include "TabletScriptingInterface.h" -#include "ModelScriptingInterface.h" - #include #include "MIDIEvent.h" -const QString ScriptEngine::_SETTINGS_ENABLE_EXTENDED_EXCEPTIONS { - "com.highfidelity.experimental.enableExtendedJSExceptions" -}; - -static const int MAX_MODULE_ID_LENGTH { 4096 }; -static const int MAX_DEBUG_VALUE_LENGTH { 80 }; - static const QScriptEngine::QObjectWrapOptions DEFAULT_QOBJECT_WRAP_OPTIONS = QScriptEngine::ExcludeDeleteLater | QScriptEngine::ExcludeChildObjects; static const QScriptValue::PropertyFlags READONLY_PROP_FLAGS { QScriptValue::ReadOnly | QScriptValue::Undeletable }; static const QScriptValue::PropertyFlags READONLY_HIDDEN_PROP_FLAGS { READONLY_PROP_FLAGS | QScriptValue::SkipInEnumeration }; + + static const bool HIFI_AUTOREFRESH_FILE_SCRIPTS { true }; Q_DECLARE_METATYPE(QScriptEngine::FunctionSignature) @@ -94,7 +84,7 @@ int functionSignatureMetaID = qRegisterMetaTypeargumentCount(); i++) { if (i > 0) { @@ -151,7 +141,7 @@ QString encodeEntityIdIntoEntityUrl(const QString& url, const QString& entityID) } QString ScriptEngine::logException(const QScriptValue& exception) { - auto message = formatException(exception, _enableExtendedJSExceptions.get()); + auto message = formatException(exception); scriptErrorMessage(message); return message; } @@ -343,7 +333,7 @@ void ScriptEngine::runInThread() { // The thread interface cannot live on itself, and we want to move this into the thread, so // the thread cannot have this as a parent. QThread* workerThread = new QThread(); - workerThread->setObjectName(QString("js:") + getFilename().replace("about:","")); + workerThread->setObjectName(QString("Script Thread:") + getFilename()); moveToThread(workerThread); // NOTE: If you connect any essential signals for proper shutdown or cleanup of @@ -464,17 +454,17 @@ void ScriptEngine::loadURL(const QUrl& scriptURL, bool reload) { void ScriptEngine::scriptErrorMessage(const QString& message) { qCCritical(scriptengine) << qPrintable(message); - emit errorMessage(message, getFilename()); + emit errorMessage(message); } void ScriptEngine::scriptWarningMessage(const QString& message) { qCWarning(scriptengine) << message; - emit warningMessage(message, getFilename()); + emit warningMessage(message); } void ScriptEngine::scriptInfoMessage(const QString& message) { qCInfo(scriptengine) << message; - emit infoMessage(message, getFilename()); + emit infoMessage(message); } // Even though we never pass AnimVariantMap directly to and from javascript, the queued invokeMethod of @@ -542,40 +532,6 @@ static QScriptValue createScriptableResourcePrototype(QScriptEngine* engine) { return prototype; } -void ScriptEngine::resetModuleCache(bool deleteScriptCache) { - if (QThread::currentThread() != thread()) { - executeOnScriptThread([=]() { resetModuleCache(deleteScriptCache); }); - return; - } - auto jsRequire = globalObject().property("Script").property("require"); - auto cache = jsRequire.property("cache"); - auto cacheMeta = jsRequire.data(); - - if (deleteScriptCache) { - QScriptValueIterator it(cache); - while (it.hasNext()) { - it.next(); - if (it.flags() & QScriptValue::SkipInEnumeration) { - continue; - } - qCDebug(scriptengine) << "resetModuleCache(true) -- staging " << it.name() << " for cache reset at next require"; - cacheMeta.setProperty(it.name(), true); - } - } - cache = newObject(); - if (!cacheMeta.isObject()) { - cacheMeta = newObject(); - cacheMeta.setProperty("id", "Script.require.cacheMeta"); - cacheMeta.setProperty("type", "cacheMeta"); - jsRequire.setData(cacheMeta); - } - cache.setProperty("__created__", (double)QDateTime::currentMSecsSinceEpoch(), QScriptValue::SkipInEnumeration); -#if DEBUG_JS_MODULES - cache.setProperty("__meta__", cacheMeta, READONLY_HIDDEN_PROP_FLAGS); -#endif - jsRequire.setProperty("cache", cache, READONLY_PROP_FLAGS); -} - void ScriptEngine::init() { if (_isInitialized) { return; // only initialize once @@ -585,6 +541,16 @@ void ScriptEngine::init() { auto entityScriptingInterface = DependencyManager::get(); entityScriptingInterface->init(); + connect(entityScriptingInterface.data(), &EntityScriptingInterface::deletingEntity, this, [this](const EntityItemID& entityID) { + if (_entityScripts.contains(entityID)) { + if (isEntityScriptRunning(entityID)) { + qCWarning(scriptengine) << "deletingEntity while entity script is still running!" << entityID; + } + _entityScripts.remove(entityID); + emit entityScriptDetailsUpdated(); + } + }); + // register various meta-types registerMetaTypes(this); @@ -627,22 +593,9 @@ void ScriptEngine::init() { qScriptRegisterMetaType(this, qWSCloseCodeToScriptValue, qWSCloseCodeFromScriptValue); qScriptRegisterMetaType(this, wscReadyStateToScriptValue, wscReadyStateFromScriptValue); - // NOTE: You do not want to end up creating new instances of singletons here. They will be on the ScriptEngine thread - // and are likely to be unusable if we "reset" the ScriptEngine by creating a new one (on a whole new thread). - registerGlobalObject("Script", this); - { - // set up Script.require.resolve and Script.require.cache - auto Script = globalObject().property("Script"); - auto require = Script.property("require"); - auto resolve = Script.property("_requireResolve"); - require.setProperty("resolve", resolve, READONLY_PROP_FLAGS); - resetModuleCache(); - } - - registerGlobalObject("Audio", DependencyManager::get().data()); - + registerGlobalObject("Audio", &AudioScriptingInterface::getInstance()); registerGlobalObject("Entities", entityScriptingInterface.data()); registerGlobalObject("Quat", &_quatLibrary); registerGlobalObject("Vec3", &_vec3Library); @@ -651,7 +604,7 @@ void ScriptEngine::init() { registerGlobalObject("Messages", DependencyManager::get().data()); registerGlobalObject("File", new FileScriptingInterface(this)); - + qScriptRegisterMetaType(this, animVarMapToScriptValue, animVarMapFromScriptValue); qScriptRegisterMetaType(this, resultHandlerToScriptValue, resultHandlerFromScriptValue); @@ -669,10 +622,6 @@ void ScriptEngine::init() { registerGlobalObject("Resources", DependencyManager::get().data()); registerGlobalObject("DebugDraw", &DebugDraw::getInstance()); - - registerGlobalObject("Model", new ModelScriptingInterface(this)); - qScriptRegisterMetaType(this, meshToScriptValue, meshFromScriptValue); - qScriptRegisterMetaType(this, meshesToScriptValue, meshesFromScriptValue); } void ScriptEngine::registerValue(const QString& valueName, QScriptValue value) { @@ -914,11 +863,6 @@ void ScriptEngine::addEventHandler(const EntityItemID& entityID, const QString& handlersForEvent << handlerData; // Note that the same handler can be added many times. See removeEntityEventHandler(). } -// this is not redundant -- the version in BaseScriptEngine is specifically not Q_INVOKABLE -QScriptValue ScriptEngine::evaluateInClosure(const QScriptValue& closure, const QScriptProgram& program) { - return BaseScriptEngine::evaluateInClosure(closure, program); -} - QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fileName, int lineNumber) { if (DependencyManager::get()->isStopped()) { return QScriptValue(); // bail early @@ -941,26 +885,29 @@ QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fi // Check syntax auto syntaxError = lintScript(sourceCode, fileName); if (syntaxError.isError()) { - if (!isEvaluating()) { + if (isEvaluating()) { + currentContext()->throwValue(syntaxError); + } else { syntaxError.setProperty("detail", "evaluate"); + emit unhandledException(syntaxError); } - raiseException(syntaxError); - maybeEmitUncaughtException("lint"); return syntaxError; } QScriptProgram program { sourceCode, fileName, lineNumber }; if (program.isNull()) { // can this happen? auto err = makeError("could not create QScriptProgram for " + fileName); - raiseException(err); - maybeEmitUncaughtException("compile"); + emit unhandledException(err); return err; } QScriptValue result; { result = BaseScriptEngine::evaluate(program); - maybeEmitUncaughtException("evaluate"); + if (!isEvaluating() && hasUncaughtException()) { + emit unhandledException(cloneUncaughtException(__FUNCTION__)); + clearExceptions(); + } } return result; } @@ -983,7 +930,10 @@ void ScriptEngine::run() { { evaluate(_scriptContents, _fileNameString); - maybeEmitUncaughtException(__FUNCTION__); + if (!isEvaluating() && hasUncaughtException()) { + emit unhandledException(cloneUncaughtException(__FUNCTION__)); + clearExceptions(); + } } #ifdef _WIN32 // VS13 does not sleep_until unless it uses the system_clock, see: @@ -1351,354 +1301,7 @@ QUrl ScriptEngine::resourcesPath() const { } void ScriptEngine::print(const QString& message) { - emit printedMessage(message, getFilename()); -} - -// Script.require.resolve -- like resolvePath, but performs more validation and throws exceptions on invalid module identifiers (for consistency with Node.js) -QString ScriptEngine::_requireResolve(const QString& moduleId, const QString& relativeTo) { - if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { - return QString(); - } - QUrl defaultScriptsLoc = defaultScriptsLocation(); - QUrl url(moduleId); - - auto displayId = moduleId; - if (displayId.length() > MAX_DEBUG_VALUE_LENGTH) { - displayId = displayId.mid(0, MAX_DEBUG_VALUE_LENGTH) + "..."; - } - auto message = QString("Cannot find module '%1' (%2)").arg(displayId); - - auto throwResolveError = [&](const QScriptValue& error) -> QString { - raiseException(error); - maybeEmitUncaughtException("require.resolve"); - return QString(); - }; - - // de-fuzz the input a little by restricting to rational sizes - auto idLength = url.toString().length(); - if (idLength < 1 || idLength > MAX_MODULE_ID_LENGTH) { - auto details = QString("rejecting invalid module id size (%1 chars [1,%2])") - .arg(idLength).arg(MAX_MODULE_ID_LENGTH); - return throwResolveError(makeError(message.arg(details), "RangeError")); - } - - // this regex matches: absolute, dotted or path-like URLs - // (ie: the kind of stuff ScriptEngine::resolvePath already handles) - QRegularExpression qualified ("^\\w+:|^/|^[.]{1,2}(/|$)"); - - // this is for module.require (which is a bound version of require that's always relative to the module path) - if (!relativeTo.isEmpty()) { - url = QUrl(relativeTo).resolved(moduleId); - url = resolvePath(url.toString()); - } else if (qualified.match(moduleId).hasMatch()) { - url = resolvePath(moduleId); - } else { - // check if the moduleId refers to a "system" module - QString systemPath = defaultScriptsLoc.path(); - QString systemModulePath = QString("%1/modules/%2.js").arg(systemPath).arg(moduleId); - url = defaultScriptsLoc; - url.setPath(systemModulePath); - if (!QFileInfo(url.toLocalFile()).isFile()) { - if (!moduleId.contains("./")) { - // the user might be trying to refer to a relative file without anchoring it - // let's do them a favor and test for that case -- offering specific advice if detected - auto unanchoredUrl = resolvePath("./" + moduleId); - if (QFileInfo(unanchoredUrl.toLocalFile()).isFile()) { - auto msg = QString("relative module ids must be anchored; use './%1' instead") - .arg(moduleId); - return throwResolveError(makeError(message.arg(msg))); - } - } - return throwResolveError(makeError(message.arg("system module not found"))); - } - } - - if (url.isRelative()) { - return throwResolveError(makeError(message.arg("could not resolve module id"))); - } - - // if it looks like a local file, verify that it's an allowed path and really a file - if (url.isLocalFile()) { - QFileInfo file(url.toLocalFile()); - QUrl canonical = url; - if (file.exists()) { - canonical.setPath(file.canonicalFilePath()); - } - - bool disallowOutsideFiles = !defaultScriptsLocation().isParentOf(canonical) && !currentSandboxURL.isLocalFile(); - if (disallowOutsideFiles && !PathUtils::isDescendantOf(canonical, currentSandboxURL)) { - return throwResolveError(makeError(message.arg( - QString("path '%1' outside of origin script '%2' '%3'") - .arg(PathUtils::stripFilename(url)) - .arg(PathUtils::stripFilename(currentSandboxURL)) - .arg(canonical.toString()) - ))); - } - if (!file.exists()) { - return throwResolveError(makeError(message.arg("path does not exist: " + url.toLocalFile()))); - } - if (!file.isFile()) { - return throwResolveError(makeError(message.arg("path is not a file: " + url.toLocalFile()))); - } - } - - maybeEmitUncaughtException(__FUNCTION__); - return url.toString(); -} - -// retrieves the current parent module from the JS scope chain -QScriptValue ScriptEngine::currentModule() { - if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { - return unboundNullValue(); - } - auto jsRequire = globalObject().property("Script").property("require"); - auto cache = jsRequire.property("cache"); - auto candidate = QScriptValue(); - for (auto c = currentContext(); c && !candidate.isObject(); c = c->parentContext()) { - QScriptContextInfo contextInfo { c }; - candidate = cache.property(contextInfo.fileName()); - } - if (!candidate.isObject()) { - return QScriptValue(); - } - return candidate; -} - -// replaces or adds "module" to "parent.children[]" array -// (for consistency with Node.js and userscript cache invalidation without "cache busters") -bool ScriptEngine::registerModuleWithParent(const QScriptValue& module, const QScriptValue& parent) { - auto children = parent.property("children"); - if (children.isArray()) { - auto key = module.property("id"); - auto length = children.property("length").toInt32(); - for (int i = 0; i < length; i++) { - if (children.property(i).property("id").strictlyEquals(key)) { - qCDebug(scriptengine_module) << key.toString() << " updating parent.children[" << i << "] = module"; - children.setProperty(i, module); - return true; - } - } - qCDebug(scriptengine_module) << key.toString() << " appending parent.children[" << length << "] = module"; - children.setProperty(length, module); - return true; - } else if (parent.isValid()) { - qCDebug(scriptengine_module) << "registerModuleWithParent -- unrecognized parent" << parent.toVariant().toString(); - } - return false; -} - -// creates a new JS "module" Object with default metadata properties -QScriptValue ScriptEngine::newModule(const QString& modulePath, const QScriptValue& parent) { - auto closure = newObject(); - auto exports = newObject(); - auto module = newObject(); - qCDebug(scriptengine_module) << "newModule" << modulePath << parent.property("filename").toString(); - - closure.setProperty("module", module, READONLY_PROP_FLAGS); - - // note: this becomes the "exports" free variable, so should not be set read only - closure.setProperty("exports", exports); - - // make the closure available to module instantiation - module.setProperty("__closure__", closure, READONLY_HIDDEN_PROP_FLAGS); - - // for consistency with Node.js Module - module.setProperty("id", modulePath, READONLY_PROP_FLAGS); - module.setProperty("filename", modulePath, READONLY_PROP_FLAGS); - module.setProperty("exports", exports); // not readonly - module.setProperty("loaded", false, READONLY_PROP_FLAGS); - module.setProperty("parent", parent, READONLY_PROP_FLAGS); - module.setProperty("children", newArray(), READONLY_PROP_FLAGS); - - // module.require is a bound version of require that always resolves relative to that module's path - auto boundRequire = QScriptEngine::evaluate("(function(id) { return Script.require(Script.require.resolve(id, this.filename)); })", "(boundRequire)"); - module.setProperty("require", boundRequire, READONLY_PROP_FLAGS); - - return module; -} - -// synchronously fetch a module's source code using BatchLoader -QVariantMap ScriptEngine::fetchModuleSource(const QString& modulePath, const bool forceDownload) { - using UrlMap = QMap; - auto scriptCache = DependencyManager::get(); - QVariantMap req; - qCDebug(scriptengine_module) << "require.fetchModuleSource: " << QUrl(modulePath).fileName() << QThread::currentThread(); - - auto onload = [=, &req](const UrlMap& data, const UrlMap& _status) { - auto url = modulePath; - auto status = _status[url]; - auto contents = data[url]; - qCDebug(scriptengine_module) << "require.fetchModuleSource.onload: " << QUrl(url).fileName() << status << QThread::currentThread(); - if (isStopping()) { - req["status"] = "Stopped"; - req["success"] = false; - } else { - req["url"] = url; - req["status"] = status; - req["success"] = ScriptCache::isSuccessStatus(status); - req["contents"] = contents; - } - }; - - if (forceDownload) { - qCDebug(scriptengine_module) << "require.requestScript -- clearing cache for" << modulePath; - scriptCache->deleteScript(modulePath); - } - BatchLoader* loader = new BatchLoader(QList({ modulePath })); - connect(loader, &BatchLoader::finished, this, onload); - connect(this, &QObject::destroyed, loader, &QObject::deleteLater); - // fail faster? (since require() blocks the engine thread while resolving dependencies) - const int MAX_RETRIES = 1; - - loader->start(MAX_RETRIES); - - if (!loader->isFinished()) { - QTimer monitor; - QEventLoop loop; - QObject::connect(loader, &BatchLoader::finished, this, [this, &monitor, &loop]{ - monitor.stop(); - loop.quit(); - }); - - // this helps detect the case where stop() is invoked during the download - // but not seen in time to abort processing in onload()... - connect(&monitor, &QTimer::timeout, this, [this, &loop, &loader]{ - if (isStopping()) { - loop.exit(-1); - } - }); - monitor.start(500); - loop.exec(); - } - loader->deleteLater(); - return req; -} - -// evaluate a pending module object using the fetched source code -QScriptValue ScriptEngine::instantiateModule(const QScriptValue& module, const QString& sourceCode) { - QScriptValue result; - auto modulePath = module.property("filename").toString(); - auto closure = module.property("__closure__"); - - qCDebug(scriptengine_module) << QString("require.instantiateModule: %1 / %2 bytes") - .arg(QUrl(modulePath).fileName()).arg(sourceCode.length()); - - if (module.property("content-type").toString() == "application/json") { - qCDebug(scriptengine_module) << "... parsing as JSON"; - closure.setProperty("__json", sourceCode); - result = evaluateInClosure(closure, { "module.exports = JSON.parse(__json)", modulePath }); - } else { - // scoped vars for consistency with Node.js - closure.setProperty("require", module.property("require")); - closure.setProperty("__filename", modulePath, READONLY_HIDDEN_PROP_FLAGS); - closure.setProperty("__dirname", QString(modulePath).replace(QRegExp("/[^/]*$"), ""), READONLY_HIDDEN_PROP_FLAGS); - result = evaluateInClosure(closure, { sourceCode, modulePath }); - } - maybeEmitUncaughtException(__FUNCTION__); - return result; -} - -// CommonJS/Node.js like require/module support -QScriptValue ScriptEngine::require(const QString& moduleId) { - qCDebug(scriptengine_module) << "ScriptEngine::require(" << moduleId.left(MAX_DEBUG_VALUE_LENGTH) << ")"; - if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { - return unboundNullValue(); - } - - auto jsRequire = globalObject().property("Script").property("require"); - auto cacheMeta = jsRequire.data(); - auto cache = jsRequire.property("cache"); - auto parent = currentModule(); - - auto throwModuleError = [&](const QString& modulePath, const QScriptValue& error) { - cache.setProperty(modulePath, nullValue()); - if (!error.isNull()) { -#ifdef DEBUG_JS_MODULES - qCWarning(scriptengine_module) << "throwing module error:" << error.toString() << modulePath << error.property("stack").toString(); -#endif - raiseException(error); - } - maybeEmitUncaughtException("module"); - return unboundNullValue(); - }; - - // start by resolving the moduleId into a fully-qualified path/URL - QString modulePath = _requireResolve(moduleId); - if (modulePath.isNull() || hasUncaughtException()) { - // the resolver already threw an exception -- bail early - maybeEmitUncaughtException(__FUNCTION__); - return unboundNullValue(); - } - - // check the resolved path against the cache - auto module = cache.property(modulePath); - - // modules get cached in `Script.require.cache` and (similar to Node.js) users can access it - // to inspect particular entries and invalidate them by deleting the key: - // `delete Script.require.cache[Script.require.resolve(moduleId)];` - - // cacheMeta is just used right now to tell deleted keys apart from undefined ones - bool invalidateCache = module.isUndefined() && cacheMeta.property(moduleId).isValid(); - - // reset the cacheMeta record so invalidation won't apply next time, even if the module fails to load - cacheMeta.setProperty(modulePath, QScriptValue()); - - auto exports = module.property("exports"); - if (!invalidateCache && exports.isObject()) { - // we have found a cached module -- just need to possibly register it with current parent - qCDebug(scriptengine_module) << QString("require - using cached module '%1' for '%2' (loaded: %3)") - .arg(modulePath).arg(moduleId).arg(module.property("loaded").toString()); - registerModuleWithParent(module, parent); - maybeEmitUncaughtException("cached module"); - return exports; - } - - // bootstrap / register new empty module - module = newModule(modulePath, parent); - registerModuleWithParent(module, parent); - - // add it to the cache (this is done early so any cyclic dependencies pick up) - cache.setProperty(modulePath, module); - - // download the module source - auto req = fetchModuleSource(modulePath, invalidateCache); - - if (!req.contains("success") || !req["success"].toBool()) { - auto error = QString("error retrieving script (%1)").arg(req["status"].toString()); - return throwModuleError(modulePath, error); - } - -#if DEBUG_JS_MODULES - qCDebug(scriptengine_module) << "require.loaded: " << - QUrl(req["url"].toString()).fileName() << req["status"].toString(); -#endif - - auto sourceCode = req["contents"].toString(); - - if (QUrl(modulePath).fileName().endsWith(".json", Qt::CaseInsensitive)) { - module.setProperty("content-type", "application/json"); - } else { - module.setProperty("content-type", "application/javascript"); - } - - // evaluate the module - auto result = instantiateModule(module, sourceCode); - - if (result.isError() && !result.strictlyEquals(module.property("exports"))) { - qCWarning(scriptengine_module) << "-- result.isError --" << result.toString(); - return throwModuleError(modulePath, result); - } - - // mark as fully-loaded - module.setProperty("loaded", true, READONLY_PROP_FLAGS); - - // set up a new reference point for detecting cache key deletion - cacheMeta.setProperty(modulePath, module); - - qCDebug(scriptengine_module) << "//ScriptEngine::require(" << moduleId << ")"; - - maybeEmitUncaughtException(__FUNCTION__); - return module.property("exports"); + emit printedMessage(message); } // If a callback is specified, the included files will be loaded asynchronously and the callback will be called @@ -1706,9 +1309,6 @@ QScriptValue ScriptEngine::require(const QString& moduleId) { // If no callback is specified, the included files will be loaded synchronously and will block execution until // all of the files have finished loading. void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callback) { - if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { - return; - } if (DependencyManager::get()->isStopped()) { scriptWarningMessage("Script.include() while shutting down is ignored... includeFiles:" + includeFiles.join(",") + "parent script:" + getFilename()); @@ -1771,7 +1371,7 @@ void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callbac doWithEnvironment(capturedEntityIdentifier, capturedSandboxURL, operation); if (hasUncaughtException()) { - emit unhandledException(cloneUncaughtException("evaluateInclude")); + emit unhandledException(cloneUncaughtException(__FUNCTION__)); clearExceptions(); } } else { @@ -1818,9 +1418,6 @@ void ScriptEngine::include(const QString& includeFile, QScriptValue callback) { // as a stand-alone script. To accomplish this, the ScriptEngine class just emits a signal which // the Application or other context will connect to in order to know to actually load the script void ScriptEngine::load(const QString& loadFile) { - if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { - return; - } if (DependencyManager::get()->isStopped()) { scriptWarningMessage("Script.load() while shutting down is ignored... loadFile:" + loadFile + "parent script:" + getFilename()); @@ -1890,52 +1487,6 @@ void ScriptEngine::updateEntityScriptStatus(const EntityItemID& entityID, const emit entityScriptDetailsUpdated(); } -QVariant ScriptEngine::cloneEntityScriptDetails(const EntityItemID& entityID) { - static const QVariant NULL_VARIANT { qVariantFromValue((QObject*)nullptr) }; - QVariantMap map; - if (entityID.isNull()) { - // TODO: find better way to report JS Error across thread/process boundaries - map["isError"] = true; - map["errorInfo"] = "Error: getEntityScriptDetails -- invalid entityID"; - } else { -#ifdef DEBUG_ENTITY_STATES - qDebug() << "cloneEntityScriptDetails" << entityID << QThread::currentThread(); -#endif - EntityScriptDetails scriptDetails; - if (getEntityScriptDetails(entityID, scriptDetails)) { -#ifdef DEBUG_ENTITY_STATES - qDebug() << "gotEntityScriptDetails" << scriptDetails.status << QThread::currentThread(); -#endif - map["isRunning"] = isEntityScriptRunning(entityID); - map["status"] = EntityScriptStatus_::valueToKey(scriptDetails.status).toLower(); - map["errorInfo"] = scriptDetails.errorInfo; - map["entityID"] = entityID.toString(); -#ifdef DEBUG_ENTITY_STATES - { - auto debug = QVariantMap(); - debug["script"] = scriptDetails.scriptText; - debug["scriptObject"] = scriptDetails.scriptObject.toVariant(); - debug["lastModified"] = (qlonglong)scriptDetails.lastModified; - debug["sandboxURL"] = scriptDetails.definingSandboxURL; - map["debug"] = debug; - } -#endif - } else { -#ifdef DEBUG_ENTITY_STATES - qDebug() << "!gotEntityScriptDetails" << QThread::currentThread(); -#endif - map["isError"] = true; - map["errorInfo"] = "Entity script details unavailable"; - map["entityID"] = entityID.toString(); - } - } - return map; -} - -QFuture ScriptEngine::getLocalEntityScriptDetails(const EntityItemID& entityID) { - return QtConcurrent::run(this, &ScriptEngine::cloneEntityScriptDetails, entityID); -} - bool ScriptEngine::getEntityScriptDetails(const EntityItemID& entityID, EntityScriptDetails &details) const { auto it = _entityScripts.constFind(entityID); if (it == _entityScripts.constEnd()) { @@ -2074,10 +1625,10 @@ void ScriptEngine::loadEntityScript(const EntityItemID& entityID, const QString& auto scriptCache = DependencyManager::get(); // note: see EntityTreeRenderer.cpp for shared pointer lifecycle management - QWeakPointer weakRef(sharedFromThis()); + QWeakPointer weakRef(sharedFromThis()); scriptCache->getScriptContents(entityScript, [this, weakRef, entityScript, entityID](const QString& url, const QString& contents, bool isURL, bool success, const QString& status) { - QSharedPointer strongRef(weakRef); + QSharedPointer strongRef(weakRef); if (!strongRef) { qCWarning(scriptengine) << "loadEntityScript.contentAvailable -- ScriptEngine was deleted during getScriptContents!!"; return; @@ -2196,12 +1747,13 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co timeout.setSingleShot(true); timeout.start(SANDBOX_TIMEOUT); connect(&timeout, &QTimer::timeout, [&sandbox, SANDBOX_TIMEOUT, scriptOrURL]{ + auto context = sandbox.currentContext(); + if (context) { qCDebug(scriptengine) << "ScriptEngine::entityScriptContentAvailable timeout(" << scriptOrURL << ")"; // Guard against infinite loops and non-performant code - sandbox.raiseException( - sandbox.makeError(QString("Timed out (entity constructors are limited to %1ms)").arg(SANDBOX_TIMEOUT)) - ); + context->throwError(QString("Timed out (entity constructors are limited to %1ms)").arg(SANDBOX_TIMEOUT)); + } }); testConstructor = sandbox.evaluate(program); @@ -2217,7 +1769,7 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co if (exception.isError()) { // create a local copy using makeError to decouple from the sandbox engine exception = makeError(exception); - setError(formatException(exception, _enableExtendedJSExceptions.get()), EntityScriptStatus::ERROR_RUNNING_SCRIPT); + setError(formatException(exception), EntityScriptStatus::ERROR_RUNNING_SCRIPT); emit unhandledException(exception); return; } @@ -2229,8 +1781,9 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co testConstructorType = "empty"; } QString testConstructorValue = testConstructor.toString(); - if (testConstructorValue.size() > MAX_DEBUG_VALUE_LENGTH) { - testConstructorValue = testConstructorValue.mid(0, MAX_DEBUG_VALUE_LENGTH) + "..."; + const int maxTestConstructorValueSize = 80; + if (testConstructorValue.size() > maxTestConstructorValueSize) { + testConstructorValue = testConstructorValue.mid(0, maxTestConstructorValueSize) + "..."; } auto message = QString("failed to load entity script -- expected a function, got %1, %2") .arg(testConstructorType).arg(testConstructorValue); @@ -2268,7 +1821,7 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co if (entityScriptObject.isError()) { auto exception = entityScriptObject; - setError(formatException(exception, _enableExtendedJSExceptions.get()), EntityScriptStatus::ERROR_RUNNING_SCRIPT); + setError(formatException(exception), EntityScriptStatus::ERROR_RUNNING_SCRIPT); emit unhandledException(exception); return; } @@ -2291,7 +1844,7 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co processDeferredEntityLoads(entityScript, entityID); } -void ScriptEngine::unloadEntityScript(const EntityItemID& entityID, bool shouldRemoveFromMap) { +void ScriptEngine::unloadEntityScript(const EntityItemID& entityID) { if (QThread::currentThread() != thread()) { #ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "*** WARNING *** ScriptEngine::unloadEntityScript() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] " @@ -2299,8 +1852,7 @@ void ScriptEngine::unloadEntityScript(const EntityItemID& entityID, bool shouldR #endif QMetaObject::invokeMethod(this, "unloadEntityScript", - Q_ARG(const EntityItemID&, entityID), - Q_ARG(bool, shouldRemoveFromMap)); + Q_ARG(const EntityItemID&, entityID)); return; } #ifdef THREAD_DEBUGGING @@ -2312,17 +1864,10 @@ void ScriptEngine::unloadEntityScript(const EntityItemID& entityID, bool shouldR const EntityScriptDetails &oldDetails = _entityScripts[entityID]; if (isEntityScriptRunning(entityID)) { callEntityScriptMethod(entityID, "unload"); - } -#ifdef DEBUG_ENTITY_STATES - else { + } else { qCDebug(scriptengine) << "unload called while !running" << entityID << oldDetails.status; } -#endif - if (shouldRemoveFromMap) { - // this was a deleted entity, we've been asked to remove it from the map - _entityScripts.remove(entityID); - emit entityScriptDetailsUpdated(); - } else if (oldDetails.status != EntityScriptStatus::UNLOADED) { + if (oldDetails.status != EntityScriptStatus::UNLOADED) { EntityScriptDetails newDetails; newDetails.status = EntityScriptStatus::UNLOADED; newDetails.lastModified = QDateTime::currentMSecsSinceEpoch(); @@ -2330,7 +1875,6 @@ void ScriptEngine::unloadEntityScript(const EntityItemID& entityID, bool shouldR newDetails.scriptText = oldDetails.scriptText; setEntityScriptDetails(entityID, newDetails); } - stopAllTimersForEntityScript(entityID); { // FIXME: shouldn't have to do this here, but currently something seems to be firing unloads moments after firing initial load requests @@ -2409,7 +1953,10 @@ void ScriptEngine::doWithEnvironment(const EntityItemID& entityID, const QUrl& s #else operation(); #endif - maybeEmitUncaughtException(!entityID.isNull() ? entityID.toString() : __FUNCTION__); + if (!isEvaluating() && hasUncaughtException()) { + emit unhandledException(cloneUncaughtException(!entityID.isNull() ? entityID.toString() : __FUNCTION__)); + clearExceptions(); + } currentEntityIdentifier = oldIdentifier; currentSandboxURL = oldSandboxURL; } diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index 5ea8d052e9..b988ccfe90 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -41,7 +41,6 @@ #include "ScriptCache.h" #include "ScriptUUID.h" #include "Vec3.h" -#include "SettingHandle.h" class QScriptEngineDebugger; @@ -79,7 +78,7 @@ public: QUrl definingSandboxURL { QUrl("about:EntityScript") }; }; -class ScriptEngine : public BaseScriptEngine, public EntitiesScriptEngineProvider { +class ScriptEngine : public BaseScriptEngine, public EntitiesScriptEngineProvider, public QEnableSharedFromThis { Q_OBJECT Q_PROPERTY(QString context READ getContext) public: @@ -138,8 +137,6 @@ public: /// evaluate some code in the context of the ScriptEngine and return the result Q_INVOKABLE QScriptValue evaluate(const QString& program, const QString& fileName, int lineNumber = 1); // this is also used by the script tool widget - Q_INVOKABLE QScriptValue evaluateInClosure(const QScriptValue& locals, const QScriptProgram& program); - /// if the script engine is not already running, this will download the URL and start the process of seting it up /// to run... NOTE - this is used by Application currently to load the url. We don't really want it to be exposed /// to scripts. we may not need this to be invokable @@ -160,16 +157,6 @@ public: Q_INVOKABLE void include(const QStringList& includeFiles, QScriptValue callback = QScriptValue()); Q_INVOKABLE void include(const QString& includeFile, QScriptValue callback = QScriptValue()); - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // MODULE related methods - Q_INVOKABLE QScriptValue require(const QString& moduleId); - Q_INVOKABLE void resetModuleCache(bool deleteScriptCache = false); - QScriptValue currentModule(); - bool registerModuleWithParent(const QScriptValue& module, const QScriptValue& parent); - QScriptValue newModule(const QString& modulePath, const QScriptValue& parent = QScriptValue()); - QVariantMap fetchModuleSource(const QString& modulePath, const bool forceDownload = false); - QScriptValue instantiateModule(const QScriptValue& module, const QString& sourceCode); - Q_INVOKABLE QObject* setInterval(const QScriptValue& function, int intervalMS); Q_INVOKABLE QObject* setTimeout(const QScriptValue& function, int timeoutMS); Q_INVOKABLE void clearInterval(QObject* timer) { stopTimer(reinterpret_cast(timer)); } @@ -183,10 +170,8 @@ public: Q_INVOKABLE bool isEntityScriptRunning(const EntityItemID& entityID) { return _entityScripts.contains(entityID) && _entityScripts[entityID].status == EntityScriptStatus::RUNNING; } - QVariant cloneEntityScriptDetails(const EntityItemID& entityID); - QFuture getLocalEntityScriptDetails(const EntityItemID& entityID) override; Q_INVOKABLE void loadEntityScript(const EntityItemID& entityID, const QString& entityScript, bool forceRedownload); - Q_INVOKABLE void unloadEntityScript(const EntityItemID& entityID, bool shouldRemoveFromMap = false); // will call unload method + Q_INVOKABLE void unloadEntityScript(const EntityItemID& entityID); // will call unload method Q_INVOKABLE void unloadAllEntityScripts(); Q_INVOKABLE void callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const QStringList& params = QStringList()) override; @@ -236,10 +221,10 @@ signals: void scriptEnding(); void finished(const QString& fileNameString, ScriptEngine* engine); void cleanupMenuItem(const QString& menuItemString); - void printedMessage(const QString& message, const QString& scriptName); - void errorMessage(const QString& message, const QString& scriptName); - void warningMessage(const QString& message, const QString& scriptName); - void infoMessage(const QString& message, const QString& scriptName); + void printedMessage(const QString& message); + void errorMessage(const QString& message); + void warningMessage(const QString& message); + void infoMessage(const QString& message); void runningStateChanged(); void loadScript(const QString& scriptName, bool isUserLoaded); void reloadScript(const QString& scriptName, bool isUserLoaded); @@ -252,9 +237,6 @@ signals: protected: void init(); Q_INVOKABLE void executeOnScriptThread(std::function function, const Qt::ConnectionType& type = Qt::QueuedConnection ); - // note: this is not meant to be called directly, but just to have QMetaObject take care of wiring it up in general; - // then inside of init() we just have to do "Script.require.resolve = Script._requireResolve;" - Q_INVOKABLE QString _requireResolve(const QString& moduleId, const QString& relativeTo = QString()); QString logException(const QScriptValue& exception); void timerFired(); @@ -308,16 +290,11 @@ protected: AssetScriptingInterface _assetScriptingInterface{ this }; - std::function _emitScriptUpdates{ []() { return true; } }; + std::function _emitScriptUpdates{ [](){ return true; } }; std::recursive_mutex _lock; std::chrono::microseconds _totalTimerExecution { 0 }; - - static const QString _SETTINGS_ENABLE_EXTENDED_MODULE_COMPAT; - static const QString _SETTINGS_ENABLE_EXTENDED_EXCEPTIONS; - - Setting::Handle _enableExtendedJSExceptions { _SETTINGS_ENABLE_EXTENDED_EXCEPTIONS, true }; }; #endif // hifi_ScriptEngine_h diff --git a/libraries/script-engine/src/ScriptEngineLogging.cpp b/libraries/script-engine/src/ScriptEngineLogging.cpp index 392bc05129..2e5d293728 100644 --- a/libraries/script-engine/src/ScriptEngineLogging.cpp +++ b/libraries/script-engine/src/ScriptEngineLogging.cpp @@ -12,4 +12,3 @@ #include "ScriptEngineLogging.h" Q_LOGGING_CATEGORY(scriptengine, "hifi.scriptengine") -Q_LOGGING_CATEGORY(scriptengine_module, "hifi.scriptengine.module") diff --git a/libraries/script-engine/src/ScriptEngineLogging.h b/libraries/script-engine/src/ScriptEngineLogging.h index 62e46632a6..0e614dd5bf 100644 --- a/libraries/script-engine/src/ScriptEngineLogging.h +++ b/libraries/script-engine/src/ScriptEngineLogging.h @@ -15,7 +15,6 @@ #include Q_DECLARE_LOGGING_CATEGORY(scriptengine) -Q_DECLARE_LOGGING_CATEGORY(scriptengine_module) #endif // hifi_ScriptEngineLogging_h diff --git a/libraries/script-engine/src/ScriptEngines.cpp b/libraries/script-engine/src/ScriptEngines.cpp index 88b0e0b7b5..57887d2d96 100644 --- a/libraries/script-engine/src/ScriptEngines.cpp +++ b/libraries/script-engine/src/ScriptEngines.cpp @@ -34,24 +34,34 @@ ScriptsModel& getScriptsModel() { return scriptsModel; } -void ScriptEngines::onPrintedMessage(const QString& message, const QString& scriptName) { +void ScriptEngines::onPrintedMessage(const QString& message) { + auto scriptEngine = qobject_cast(sender()); + auto scriptName = scriptEngine ? scriptEngine->getFilename() : ""; emit printedMessage(message, scriptName); } -void ScriptEngines::onErrorMessage(const QString& message, const QString& scriptName) { +void ScriptEngines::onErrorMessage(const QString& message) { + auto scriptEngine = qobject_cast(sender()); + auto scriptName = scriptEngine ? scriptEngine->getFilename() : ""; emit errorMessage(message, scriptName); } -void ScriptEngines::onWarningMessage(const QString& message, const QString& scriptName) { +void ScriptEngines::onWarningMessage(const QString& message) { + auto scriptEngine = qobject_cast(sender()); + auto scriptName = scriptEngine ? scriptEngine->getFilename() : ""; emit warningMessage(message, scriptName); } -void ScriptEngines::onInfoMessage(const QString& message, const QString& scriptName) { +void ScriptEngines::onInfoMessage(const QString& message) { + auto scriptEngine = qobject_cast(sender()); + auto scriptName = scriptEngine ? scriptEngine->getFilename() : ""; emit infoMessage(message, scriptName); } void ScriptEngines::onErrorLoadingScript(const QString& url) { - emit errorLoadingScript(url); + auto scriptEngine = qobject_cast(sender()); + auto scriptName = scriptEngine ? scriptEngine->getFilename() : ""; + emit errorLoadingScript(url, scriptName); } ScriptEngines::ScriptEngines(ScriptEngine::Context context) diff --git a/libraries/script-engine/src/ScriptEngines.h b/libraries/script-engine/src/ScriptEngines.h index 63b7e8f11c..2fadfc81f8 100644 --- a/libraries/script-engine/src/ScriptEngines.h +++ b/libraries/script-engine/src/ScriptEngines.h @@ -79,13 +79,13 @@ signals: void errorMessage(const QString& message, const QString& engineName); void warningMessage(const QString& message, const QString& engineName); void infoMessage(const QString& message, const QString& engineName); - void errorLoadingScript(const QString& url); + void errorLoadingScript(const QString& url, const QString& engineName); public slots: - void onPrintedMessage(const QString& message, const QString& scriptName); - void onErrorMessage(const QString& message, const QString& scriptName); - void onWarningMessage(const QString& message, const QString& scriptName); - void onInfoMessage(const QString& message, const QString& scriptName); + void onPrintedMessage(const QString& message); + void onErrorMessage(const QString& message); + void onWarningMessage(const QString& message); + void onInfoMessage(const QString& message); void onErrorLoadingScript(const QString& url); protected slots: diff --git a/libraries/shared/src/BaseScriptEngine.h b/libraries/shared/src/BaseScriptEngine.h deleted file mode 100644 index 138e46fafa..0000000000 --- a/libraries/shared/src/BaseScriptEngine.h +++ /dev/null @@ -1,90 +0,0 @@ -// -// BaseScriptEngine.h -// libraries/script-engine/src -// -// Created by Timothy Dedischew on 02/01/17. -// Copyright 2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#ifndef hifi_BaseScriptEngine_h -#define hifi_BaseScriptEngine_h - -#include -#include -#include - -// common base class for extending QScriptEngine itself -class BaseScriptEngine : public QScriptEngine, public QEnableSharedFromThis { - Q_OBJECT -public: - static const QString SCRIPT_EXCEPTION_FORMAT; - static const QString SCRIPT_BACKTRACE_SEP; - - // threadsafe "unbound" version of QScriptEngine::nullValue() - static const QScriptValue unboundNullValue() { return QScriptValue(0, QScriptValue::NullValue); } - - BaseScriptEngine() {} - - Q_INVOKABLE QScriptValue lintScript(const QString& sourceCode, const QString& fileName, const int lineNumber = 1); - Q_INVOKABLE QScriptValue makeError(const QScriptValue& other = QScriptValue(), const QString& type = "Error"); - Q_INVOKABLE QString formatException(const QScriptValue& exception, bool includeExtendedDetails); - - QScriptValue cloneUncaughtException(const QString& detail = QString()); - QScriptValue evaluateInClosure(const QScriptValue& locals, const QScriptProgram& program); - - // if there is a pending exception and we are at the top level (non-recursive) stack frame, this emits and resets it - bool maybeEmitUncaughtException(const QString& debugHint = QString()); - - // if the currentContext() is valid then throw the passed exception; otherwise, immediately emit it. - // note: this is used in cases where C++ code might call into JS API methods directly - bool raiseException(const QScriptValue& exception); - - // helper to detect and log warnings when other code invokes QScriptEngine/BaseScriptEngine in thread-unsafe ways - static bool IS_THREADSAFE_INVOCATION(const QThread *thread, const QString& method); -signals: - void unhandledException(const QScriptValue& exception); - -protected: - // like `newFunction`, but allows mapping inline C++ lambdas with captures as callable QScriptValues - // even though the context/engine parameters are redundant in most cases, the function signature matches `newFunction` - // anyway so that newLambdaFunction can be used to rapidly prototype / test utility APIs and then if becoming - // permanent more easily promoted into regular static newFunction scenarios. - QScriptValue newLambdaFunction(std::function operation, const QScriptValue& data = QScriptValue(), const QScriptEngine::ValueOwnership& ownership = QScriptEngine::AutoOwnership); - -#ifdef DEBUG_JS - static void _debugDump(const QString& header, const QScriptValue& object, const QString& footer = QString()); -#endif -}; - -// Standardized CPS callback helpers (see: http://fredkschott.com/post/2014/03/understanding-error-first-callbacks-in-node-js/) -// These two helpers allow async JS APIs that use a callback parameter to be more friendly to scripters by accepting thisObject -// context and adopting a consistent and intuitable callback signature: -// function callback(err, result) { if (err) { ... } else { /* do stuff with result */ } } -// -// To use, first pass the user-specified callback args in the same order used with optionally-scoped Qt signal connections: -// auto handler = makeScopedHandlerObject(scopeOrCallback, optionalMethodOrName); -// And then invoke the scoped handler later per CPS conventions: -// auto result = callScopedHandlerObject(handler, err, result); -QScriptValue makeScopedHandlerObject(QScriptValue scopeOrCallback, QScriptValue methodOrName); -QScriptValue callScopedHandlerObject(QScriptValue handler, QScriptValue err, QScriptValue result); - -// Lambda helps create callable QScriptValues out of std::functions: -// (just meant for use from within the script engine itself) -class Lambda : public QObject { - Q_OBJECT -public: - Lambda(QScriptEngine *engine, std::function operation, QScriptValue data); - ~Lambda(); - public slots: - QScriptValue call(); - QString toString() const; -private: - QScriptEngine* engine; - std::function operation; - QScriptValue data; -}; - -#endif // hifi_BaseScriptEngine_h diff --git a/libraries/shared/src/GLMHelpers.h b/libraries/shared/src/GLMHelpers.h index deb87930fc..609c3ab08b 100644 --- a/libraries/shared/src/GLMHelpers.h +++ b/libraries/shared/src/GLMHelpers.h @@ -50,7 +50,7 @@ using glm::quat; // this is where the coordinate system is represented const glm::vec3 IDENTITY_RIGHT = glm::vec3( 1.0f, 0.0f, 0.0f); const glm::vec3 IDENTITY_UP = glm::vec3( 0.0f, 1.0f, 0.0f); -const glm::vec3 IDENTITY_FORWARD = glm::vec3( 0.0f, 0.0f,-1.0f); +const glm::vec3 IDENTITY_FRONT = glm::vec3( 0.0f, 0.0f,-1.0f); glm::quat safeMix(const glm::quat& q1, const glm::quat& q2, float alpha); diff --git a/libraries/shared/src/HifiConfigVariantMap.cpp b/libraries/shared/src/HifiConfigVariantMap.cpp index d0fb14e104..5be6b2cd74 100644 --- a/libraries/shared/src/HifiConfigVariantMap.cpp +++ b/libraries/shared/src/HifiConfigVariantMap.cpp @@ -21,7 +21,7 @@ #include #include -#include "PathUtils.h" +#include "ServerPathUtils.h" #include "SharedLogging.h" QVariantMap HifiConfigVariantMap::mergeCLParametersWithJSONConfig(const QStringList& argumentList) { @@ -127,7 +127,7 @@ void HifiConfigVariantMap::loadConfig(const QStringList& argumentList) { _userConfigFilename = argumentList[userConfigIndex + 1]; } else { // we weren't passed a user config path - _userConfigFilename = PathUtils::getAppDataFilePath(USER_CONFIG_FILE_NAME); + _userConfigFilename = ServerPathUtils::getDataFilePath(USER_CONFIG_FILE_NAME); // as of 1/19/2016 this path was moved so we attempt a migration for first run post migration here @@ -153,7 +153,7 @@ void HifiConfigVariantMap::loadConfig(const QStringList& argumentList) { // we have the old file and not the new file - time to copy the file // make the destination directory if it doesn't exist - auto dataDirectory = PathUtils::getAppDataPath(); + auto dataDirectory = ServerPathUtils::getDataDirectory(); if (QDir().mkpath(dataDirectory)) { if (oldConfigFile.copy(_userConfigFilename)) { qCDebug(shared) << "Migrated config file from" << oldConfigFilename << "to" << _userConfigFilename; diff --git a/libraries/shared/src/PathUtils.cpp b/libraries/shared/src/PathUtils.cpp index 6e3acc5e99..265eaaa5b6 100644 --- a/libraries/shared/src/PathUtils.cpp +++ b/libraries/shared/src/PathUtils.cpp @@ -30,20 +30,18 @@ const QString& PathUtils::resourcesPath() { return staticResourcePath; } -QString PathUtils::getAppDataPath() { - return QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/"; -} +QString PathUtils::getRootDataDirectory() { + auto dataPath = QStandardPaths::writableLocation(QStandardPaths::HomeLocation); -QString PathUtils::getAppLocalDataPath() { - return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/"; -} +#ifdef Q_OS_WIN + dataPath += "/AppData/Roaming/"; +#elif defined(Q_OS_OSX) + dataPath += "/Library/Application Support/"; +#else + dataPath += "/.local/share/"; +#endif -QString PathUtils::getAppDataFilePath(const QString& filename) { - return QDir(getAppDataPath()).absoluteFilePath(filename); -} - -QString PathUtils::getAppLocalDataFilePath(const QString& filename) { - return QDir(getAppLocalDataPath()).absoluteFilePath(filename); + return dataPath; } QString fileNameWithoutExtension(const QString& fileName, const QVector possibleExtensions) { diff --git a/libraries/shared/src/PathUtils.h b/libraries/shared/src/PathUtils.h index a7af44221c..1f7dcbe466 100644 --- a/libraries/shared/src/PathUtils.h +++ b/libraries/shared/src/PathUtils.h @@ -27,12 +27,7 @@ class PathUtils : public QObject, public Dependency { Q_PROPERTY(QString resources READ resourcesPath) public: static const QString& resourcesPath(); - - static QString getAppDataPath(); - static QString getAppLocalDataPath(); - - static QString getAppDataFilePath(const QString& filename); - static QString getAppLocalDataFilePath(const QString& filename); + static QString getRootDataDirectory(); static Qt::CaseSensitivity getFSCaseSensitivity(); static QString stripFilename(const QUrl& url); diff --git a/libraries/shared/src/RenderArgs.h b/libraries/shared/src/RenderArgs.h index 50722c0deb..b2c05b0548 100644 --- a/libraries/shared/src/RenderArgs.h +++ b/libraries/shared/src/RenderArgs.h @@ -122,7 +122,6 @@ public: gpu::Batch* _batch = nullptr; std::shared_ptr _whiteTexture; - uint32_t _globalShapeKey { 0 }; bool _enableTexturing { true }; RenderDetails _details; diff --git a/libraries/shared/src/ServerPathUtils.cpp b/libraries/shared/src/ServerPathUtils.cpp new file mode 100644 index 0000000000..cf52875c5f --- /dev/null +++ b/libraries/shared/src/ServerPathUtils.cpp @@ -0,0 +1,31 @@ +// +// ServerPathUtils.cpp +// libraries/shared/src +// +// Created by Ryan Huffman on 01/12/16. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#include "ServerPathUtils.h" + +#include +#include +#include +#include + +#include "PathUtils.h" + +QString ServerPathUtils::getDataDirectory() { + auto dataPath = PathUtils::getRootDataDirectory(); + + dataPath += qApp->organizationName() + "/" + qApp->applicationName(); + + return QDir::cleanPath(dataPath); +} + +QString ServerPathUtils::getDataFilePath(QString filename) { + return QDir(getDataDirectory()).absoluteFilePath(filename); +} + diff --git a/libraries/shared/src/ServerPathUtils.h b/libraries/shared/src/ServerPathUtils.h new file mode 100644 index 0000000000..28a9a71f0d --- /dev/null +++ b/libraries/shared/src/ServerPathUtils.h @@ -0,0 +1,22 @@ +// +// ServerPathUtils.h +// libraries/shared/src +// +// Created by Ryan Huffman on 01/12/16. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_ServerPathUtils_h +#define hifi_ServerPathUtils_h + +#include + +namespace ServerPathUtils { + QString getDataDirectory(); + QString getDataFilePath(QString filename); +} + +#endif // hifi_ServerPathUtils_h \ No newline at end of file diff --git a/libraries/shared/src/ViewFrustum.cpp b/libraries/shared/src/ViewFrustum.cpp index 7e4f64686b..a0b7d17e46 100644 --- a/libraries/shared/src/ViewFrustum.cpp +++ b/libraries/shared/src/ViewFrustum.cpp @@ -31,7 +31,7 @@ void ViewFrustum::setOrientation(const glm::quat& orientationAsQuaternion) { _orientation = orientationAsQuaternion; _right = glm::vec3(orientationAsQuaternion * glm::vec4(IDENTITY_RIGHT, 0.0f)); _up = glm::vec3(orientationAsQuaternion * glm::vec4(IDENTITY_UP, 0.0f)); - _direction = glm::vec3(orientationAsQuaternion * glm::vec4(IDENTITY_FORWARD, 0.0f)); + _direction = glm::vec3(orientationAsQuaternion * glm::vec4(IDENTITY_FRONT, 0.0f)); _view = glm::translate(mat4(), _position) * glm::mat4_cast(_orientation); } diff --git a/libraries/shared/src/ViewFrustum.h b/libraries/shared/src/ViewFrustum.h index 221b0b5a07..9a6cb9ab68 100644 --- a/libraries/shared/src/ViewFrustum.h +++ b/libraries/shared/src/ViewFrustum.h @@ -153,7 +153,7 @@ private: glm::quat _orientation; // orientation in world-frame // calculated from orientation - glm::vec3 _direction = IDENTITY_FORWARD; + glm::vec3 _direction = IDENTITY_FRONT; glm::vec3 _up = IDENTITY_UP; glm::vec3 _right = IDENTITY_RIGHT; diff --git a/libraries/shared/src/shared/Storage.cpp b/libraries/shared/src/shared/Storage.cpp deleted file mode 100644 index 3c46347a49..0000000000 --- a/libraries/shared/src/shared/Storage.cpp +++ /dev/null @@ -1,92 +0,0 @@ -// -// Created by Bradley Austin Davis on 2016/02/17 -// Copyright 2013-2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include "Storage.h" - -#include -#include -#include - -Q_LOGGING_CATEGORY(storagelogging, "hifi.core.storage") - -using namespace storage; - -ViewStorage::ViewStorage(const storage::StoragePointer& owner, size_t size, const uint8_t* data) - : _owner(owner), _size(size), _data(data) {} - -StoragePointer Storage::createView(size_t viewSize, size_t offset) const { - auto selfSize = size(); - if (0 == viewSize) { - viewSize = selfSize; - } - if ((viewSize + offset) > selfSize) { - throw std::runtime_error("Invalid mapping range"); - } - return std::make_shared(shared_from_this(), viewSize, data() + offset); -} - -StoragePointer Storage::toMemoryStorage() const { - return std::make_shared(size(), data()); -} - -StoragePointer Storage::toFileStorage(const QString& filename) const { - return FileStorage::create(filename, size(), data()); -} - -MemoryStorage::MemoryStorage(size_t size, const uint8_t* data) { - _data.resize(size); - if (data) { - memcpy(_data.data(), data, size); - } -} - -StoragePointer FileStorage::create(const QString& filename, size_t size, const uint8_t* data) { - QFile file(filename); - if (!file.open(QFile::ReadWrite | QIODevice::Truncate)) { - throw std::runtime_error("Unable to open file for writing"); - } - if (!file.resize(size)) { - throw std::runtime_error("Unable to resize file"); - } - { - auto mapped = file.map(0, size); - if (!mapped) { - throw std::runtime_error("Unable to map file"); - } - memcpy(mapped, data, size); - if (!file.unmap(mapped)) { - throw std::runtime_error("Unable to unmap file"); - } - } - file.close(); - return std::make_shared(filename); -} - -FileStorage::FileStorage(const QString& filename) : _file(filename) { - if (_file.open(QFile::ReadOnly)) { - _mapped = _file.map(0, _file.size()); - if (_mapped) { - _valid = true; - } else { - qCWarning(storagelogging) << "Failed to map file " << filename; - } - } else { - qCWarning(storagelogging) << "Failed to open file " << filename; - } -} - -FileStorage::~FileStorage() { - if (_mapped) { - if (!_file.unmap(_mapped)) { - throw std::runtime_error("Unable to unmap file"); - } - } - if (_file.isOpen()) { - _file.close(); - } -} diff --git a/libraries/shared/src/shared/Storage.h b/libraries/shared/src/shared/Storage.h deleted file mode 100644 index 306984040f..0000000000 --- a/libraries/shared/src/shared/Storage.h +++ /dev/null @@ -1,82 +0,0 @@ -// -// Created by Bradley Austin Davis on 2016/02/17 -// Copyright 2013-2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#pragma once -#ifndef hifi_Storage_h -#define hifi_Storage_h - -#include -#include -#include -#include -#include - -namespace storage { - class Storage; - using StoragePointer = std::shared_ptr; - - class Storage : public std::enable_shared_from_this { - public: - virtual ~Storage() {} - virtual const uint8_t* data() const = 0; - virtual size_t size() const = 0; - virtual operator bool() const { return true; } - - StoragePointer createView(size_t size = 0, size_t offset = 0) const; - StoragePointer toFileStorage(const QString& filename) const; - StoragePointer toMemoryStorage() const; - - // Aliases to prevent having to re-write a ton of code - inline size_t getSize() const { return size(); } - inline const uint8_t* readData() const { return data(); } - }; - - class MemoryStorage : public Storage { - public: - MemoryStorage(size_t size, const uint8_t* data = nullptr); - const uint8_t* data() const override { return _data.data(); } - uint8_t* data() { return _data.data(); } - size_t size() const override { return _data.size(); } - operator bool() const override { return true; } - private: - std::vector _data; - }; - - class FileStorage : public Storage { - public: - static StoragePointer create(const QString& filename, size_t size, const uint8_t* data); - FileStorage(const QString& filename); - ~FileStorage(); - // Prevent copying - FileStorage(const FileStorage& other) = delete; - FileStorage& operator=(const FileStorage& other) = delete; - - const uint8_t* data() const override { return _mapped; } - size_t size() const override { return _file.size(); } - operator bool() const override { return _valid; } - private: - bool _valid { false }; - QFile _file; - uint8_t* _mapped { nullptr }; - }; - - class ViewStorage : public Storage { - public: - ViewStorage(const storage::StoragePointer& owner, size_t size, const uint8_t* data); - const uint8_t* data() const override { return _data; } - size_t size() const override { return _size; } - operator bool() const override { return *_owner; } - private: - const storage::StoragePointer _owner; - const size_t _size; - const uint8_t* _data; - }; - -} - -#endif // hifi_Storage_h diff --git a/libraries/ui/src/ui/Menu.cpp b/libraries/ui/src/ui/Menu.cpp index a793942056..f68fff0204 100644 --- a/libraries/ui/src/ui/Menu.cpp +++ b/libraries/ui/src/ui/Menu.cpp @@ -470,8 +470,8 @@ void Menu::removeSeparator(const QString& menuName, const QString& separatorName if (menu) { int textAt = findPositionOfMenuItem(menu, separatorName); QList menuActions = menu->actions(); + QAction* separatorText = menuActions[textAt]; if (textAt > 0 && textAt < menuActions.size()) { - QAction* separatorText = menuActions[textAt]; QAction* separatorLine = menuActions[textAt - 1]; if (separatorLine) { if (separatorLine->isSeparator()) { diff --git a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp index b759a06aee..09f3e6dc8c 100644 --- a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp +++ b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp @@ -255,7 +255,7 @@ void OculusLegacyDisplayPlugin::hmdPresent() { memset(eyePoses, 0, sizeof(ovrPosef) * 2); eyePoses[0].Orientation = eyePoses[1].Orientation = ovrRotation; - GLint texture = getGLBackend()->getTextureID(_compositeFramebuffer->getRenderBuffer(0)); + GLint texture = getGLBackend()->getTextureID(_compositeFramebuffer->getRenderBuffer(0), false); auto sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); glFlush(); if (_hmdWindow->makeCurrent()) { diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.cpp b/plugins/openvr/src/OpenVrDisplayPlugin.cpp index 46c2cf3ff2..6d503a208a 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.cpp +++ b/plugins/openvr/src/OpenVrDisplayPlugin.cpp @@ -494,9 +494,9 @@ void OpenVrDisplayPlugin::customizeContext() { _compositeInfos[0].texture = _compositeFramebuffer->getRenderBuffer(0); for (size_t i = 0; i < COMPOSITING_BUFFER_SIZE; ++i) { if (0 != i) { - _compositeInfos[i].texture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32, _renderTargetSize.x, _renderTargetSize.y, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT))); + _compositeInfos[i].texture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, _renderTargetSize.x, _renderTargetSize.y, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT))); } - _compositeInfos[i].textureID = getGLBackend()->getTextureID(_compositeInfos[i].texture); + _compositeInfos[i].textureID = getGLBackend()->getTextureID(_compositeInfos[i].texture, false); } _submitThread->_canvas = _submitCanvas; _submitThread->start(QThread::HighPriority); @@ -624,7 +624,7 @@ void OpenVrDisplayPlugin::compositeLayers() { glFlush(); if (!newComposite.textureID) { - newComposite.textureID = getGLBackend()->getTextureID(newComposite.texture); + newComposite.textureID = getGLBackend()->getTextureID(newComposite.texture, false); } withPresentThreadLock([&] { _submitThread->update(newComposite); @@ -638,7 +638,7 @@ void OpenVrDisplayPlugin::hmdPresent() { if (_threadedSubmit) { _submitThread->waitForPresent(); } else { - GLuint glTexId = getGLBackend()->getTextureID(_compositeFramebuffer->getRenderBuffer(0)); + GLuint glTexId = getGLBackend()->getTextureID(_compositeFramebuffer->getRenderBuffer(0), false); vr::Texture_t vrTexture { (void*)glTexId, vr::API_OpenGL, vr::ColorSpace_Auto }; vr::VRCompositor()->Submit(vr::Eye_Left, &vrTexture, &OPENVR_TEXTURE_BOUNDS_LEFT); vr::VRCompositor()->Submit(vr::Eye_Right, &vrTexture, &OPENVR_TEXTURE_BOUNDS_RIGHT); diff --git a/scripts/developer/libraries/jasmine/hifi-boot.js b/scripts/developer/libraries/jasmine/hifi-boot.js index 772dd8c17e..f490a3618f 100644 --- a/scripts/developer/libraries/jasmine/hifi-boot.js +++ b/scripts/developer/libraries/jasmine/hifi-boot.js @@ -6,7 +6,7 @@ var lastSpecStartTime; function ConsoleReporter(options) { var startTime = new Date().getTime(); - var errorCount = 0, pending = []; + var errorCount = 0; this.jasmineStarted = function (obj) { print('Jasmine started with ' + obj.totalSpecsDefined + ' tests.'); }; @@ -15,14 +15,11 @@ var endTime = new Date().getTime(); print('
'); if (errorCount === 0) { - print ('All enabled tests passed!'); + print ('All tests passed!'); } else { print('Tests completed with ' + errorCount + ' ' + ERROR + '.'); } - if (pending.length) - print ('disabled:
   '+ - pending.join('
   ')+'
'); print('Tests completed in ' + (endTime - startTime) + 'ms.'); }; this.suiteStarted = function(obj) { @@ -35,10 +32,6 @@ lastSpecStartTime = new Date().getTime(); }; this.specDone = function(obj) { - if (obj.status === 'pending') { - pending.push(obj.fullName); - return print('...(pending ' + obj.fullName +')'); - } var specEndTime = new Date().getTime(); var symbol = obj.status === PASSED ? '' + CHECKMARK + '' : @@ -62,7 +55,7 @@ clearTimeout = Script.clearTimeout; clearInterval = Script.clearInterval; - var jasmine = this.jasmine = jasmineRequire.core(jasmineRequire); + var jasmine = jasmineRequire.core(jasmineRequire); var env = jasmine.getEnv(); diff --git a/scripts/developer/tests/.gitignore b/scripts/developer/tests/.gitignore deleted file mode 100644 index 7cacbf042c..0000000000 --- a/scripts/developer/tests/.gitignore +++ /dev/null @@ -1 +0,0 @@ -cube_texture.ktx \ No newline at end of file diff --git a/scripts/developer/tests/ambientSoundTest.js b/scripts/developer/tests/ambientSoundTest.js index d048d5f73d..5b373715c0 100644 --- a/scripts/developer/tests/ambientSoundTest.js +++ b/scripts/developer/tests/ambientSoundTest.js @@ -4,7 +4,7 @@ var uuid = Entities.addEntity({ shape: "Icosahedron", dimensions: Vec3.HALF, script: Script.resolvePath('../../tutorials/entity_scripts/ambientSound.js'), - position: Vec3.sum(Vec3.multiply(5, Quat.getForward(MyAvatar.orientation)), MyAvatar.position), + position: Vec3.sum(Vec3.multiply(5, Quat.getFront(MyAvatar.orientation)), MyAvatar.position), userData: JSON.stringify({ soundURL: WAVE, maxVolume: 0.1, diff --git a/scripts/developer/tests/basicEntityTest/entitySpawner.js b/scripts/developer/tests/basicEntityTest/entitySpawner.js index 538e9145f5..a2f38f59eb 100644 --- a/scripts/developer/tests/basicEntityTest/entitySpawner.js +++ b/scripts/developer/tests/basicEntityTest/entitySpawner.js @@ -2,7 +2,7 @@ orientation = Quat.safeEulerAngles(orientation); orientation.x = 0; orientation = Quat.fromVec3Degrees(orientation); - var center = Vec3.sum(MyAvatar.position, Vec3.multiply(3, Quat.getForward(orientation))); + var center = Vec3.sum(MyAvatar.position, Vec3.multiply(3, Quat.getFront(orientation))); // Math.random ensures no caching of script var SCRIPT_URL = Script.resolvePath("myEntityScript.js") diff --git a/scripts/developer/tests/batonSoundEntityTest/batonSoundTestEntitySpawner.js b/scripts/developer/tests/batonSoundEntityTest/batonSoundTestEntitySpawner.js index f5fc35a1de..fdcef8d32c 100644 --- a/scripts/developer/tests/batonSoundEntityTest/batonSoundTestEntitySpawner.js +++ b/scripts/developer/tests/batonSoundEntityTest/batonSoundTestEntitySpawner.js @@ -2,7 +2,7 @@ orientation = Quat.safeEulerAngles(orientation); orientation.x = 0; orientation = Quat.fromVec3Degrees(orientation); - var center = Vec3.sum(MyAvatar.position, Vec3.multiply(3, Quat.getForward(orientation))); + var center = Vec3.sum(MyAvatar.position, Vec3.multiply(3, Quat.getFront(orientation))); // Math.random ensures no caching of script var SCRIPT_URL = Script.resolvePath("batonSoundTestEntityScript.js") diff --git a/scripts/developer/tests/entityServerStampedeTest.js b/scripts/developer/tests/entityServerStampedeTest.js index 33aa53f9b1..3fcf01bb34 100644 --- a/scripts/developer/tests/entityServerStampedeTest.js +++ b/scripts/developer/tests/entityServerStampedeTest.js @@ -4,7 +4,7 @@ var DIV = NUM_ENTITIES / Math.PI / 2; var PASS_SCRIPT_URL = Script.resolvePath('entityServerStampedeTest-entity.js'); var FAIL_SCRIPT_URL = Script.resolvePath('entityStampedeTest-entity-fail.js'); -var origin = Vec3.sum(MyAvatar.position, Vec3.multiply(5, Quat.getForward(MyAvatar.orientation))); +var origin = Vec3.sum(MyAvatar.position, Vec3.multiply(5, Quat.getFront(MyAvatar.orientation))); origin.y += HMD.eyeHeight; var uuids = []; diff --git a/scripts/developer/tests/entityStampedeTest.js b/scripts/developer/tests/entityStampedeTest.js index 644bf0a216..c5040a9796 100644 --- a/scripts/developer/tests/entityStampedeTest.js +++ b/scripts/developer/tests/entityStampedeTest.js @@ -4,7 +4,7 @@ var DIV = NUM_ENTITIES / Math.PI / 2; var PASS_SCRIPT_URL = Script.resolvePath('').replace('.js', '-entity.js'); var FAIL_SCRIPT_URL = Script.resolvePath('').replace('.js', '-entity-fail.js'); -var origin = Vec3.sum(MyAvatar.position, Vec3.multiply(5, Quat.getForward(MyAvatar.orientation))); +var origin = Vec3.sum(MyAvatar.position, Vec3.multiply(5, Quat.getFront(MyAvatar.orientation))); origin.y += HMD.eyeHeight; var uuids = []; diff --git a/scripts/developer/tests/lodTest.js b/scripts/developer/tests/lodTest.js index ce91b54d0f..4b6706cd70 100644 --- a/scripts/developer/tests/lodTest.js +++ b/scripts/developer/tests/lodTest.js @@ -19,7 +19,7 @@ var WIDTH = MAX_DIM * NUM_SPHERES; var entities = []; var right = Quat.getRight(Camera.orientation); // Starting position will be 30 meters in front of the camera -var position = Vec3.sum(Camera.position, Vec3.multiply(30, Quat.getForward(Camera.orientation))); +var position = Vec3.sum(Camera.position, Vec3.multiply(30, Quat.getFront(Camera.orientation))); position = Vec3.sum(position, Vec3.multiply(-WIDTH/2, right)); for (var i = 0; i < NUM_SPHERES; ++i) { diff --git a/scripts/developer/tests/mat4test.js b/scripts/developer/tests/mat4test.js index 4e835ec82f..ebce420dcb 100644 --- a/scripts/developer/tests/mat4test.js +++ b/scripts/developer/tests/mat4test.js @@ -141,12 +141,12 @@ function testInverse() { assert(mat4FuzzyEqual(IDENTITY, Mat4.multiply(test2, Mat4.inverse(test2)))); } -function testForward() { +function testFront() { var test0 = IDENTITY; - assert(mat4FuzzyEqual({x: 0, y: 0, z: -1}, Mat4.getForward(test0))); + assert(mat4FuzzyEqual({x: 0, y: 0, z: -1}, Mat4.getFront(test0))); var test1 = Mat4.createFromScaleRotAndTrans(ONE_HALF, ROT_Y_180, ONE_TWO_THREE); - assert(mat4FuzzyEqual({x: 0, y: 0, z: 1}, Mat4.getForward(test1))); + assert(mat4FuzzyEqual({x: 0, y: 0, z: 1}, Mat4.getFront(test1))); } function testMat4() { @@ -157,7 +157,7 @@ function testMat4() { testTransformPoint(); testTransformVector(); testInverse(); - testForward(); + testFront(); print("MAT4 TEST complete! (" + (testCount - failureCount) + "/" + testCount + ") tests passed!"); } diff --git a/scripts/developer/tests/performance/tribbles.js b/scripts/developer/tests/performance/tribbles.js index c5735b7359..4c04f8b5b7 100644 --- a/scripts/developer/tests/performance/tribbles.js +++ b/scripts/developer/tests/performance/tribbles.js @@ -43,7 +43,7 @@ var HOW_FAR_UP = RANGE / 1.5; // higher (for uneven ground) above range/2 (for var totalCreated = 0; var offset = Vec3.sum(Vec3.multiply(HOW_FAR_UP, Vec3.UNIT_Y), - Vec3.multiply(HOW_FAR_IN_FRONT_OF_ME, Quat.getForward(Camera.orientation))); + Vec3.multiply(HOW_FAR_IN_FRONT_OF_ME, Quat.getFront(Camera.orientation))); var center = Vec3.sum(MyAvatar.position, offset); function randomVector(range) { diff --git a/scripts/developer/tests/rapidProceduralChange/rapidProceduralChangeTest.js b/scripts/developer/tests/rapidProceduralChange/rapidProceduralChangeTest.js index e28a7b01e2..6897a1b70f 100644 --- a/scripts/developer/tests/rapidProceduralChange/rapidProceduralChangeTest.js +++ b/scripts/developer/tests/rapidProceduralChange/rapidProceduralChangeTest.js @@ -20,9 +20,9 @@ orientation = Quat.safeEulerAngles(orientation); orientation.x = 0; orientation = Quat.fromVec3Degrees(orientation); -var centerUp = Vec3.sum(MyAvatar.position, Vec3.multiply(3, Quat.getForward(orientation))); +var centerUp = Vec3.sum(MyAvatar.position, Vec3.multiply(3, Quat.getFront(orientation))); centerUp.y += 0.5; -var centerDown = Vec3.sum(MyAvatar.position, Vec3.multiply(3, Quat.getForward(orientation))); +var centerDown = Vec3.sum(MyAvatar.position, Vec3.multiply(3, Quat.getFront(orientation))); centerDown.y -= 0.5; var ENTITY_SHADER_URL = "https://s3-us-west-1.amazonaws.com/hifi-content/eric/shaders/uniformTest.fs"; diff --git a/scripts/developer/tests/scaling.png b/scripts/developer/tests/scaling.png deleted file mode 100644 index 1e6a7df45d8440cc52d3b45501b909419fed2f1b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3172 zcmd5;c~p~E7XQ63A%rEG0zw6aOe>MPK~IBJisaL=qAd_A)CCaGwo@pwR8gZC_|%SD z7o4J$st8ugBC@Cz1PuvBYq45TM5xH3-~b|o9YRPlCwSUFX3m*2?O*fPeed4;yT5ne zyYHTRFu>o3XKrr}fVXnRvQ+>DfPl*ZaO3bV9|M+iS1wx;Bqh%uyiNeFQgFJcjPsAZ zQ+zK$=}JukxPSm)@JBWj{=Z@I&oh>M-7es>42Jw255u%;9b7_Ar+anY4|u#h;LQ2T zXWDU%Msp|eqeX1oR5;Ed z%2}=L#||eS>yaPipfV=$G*|spnb+iuN)}3+#M!v_fuM66=g`gr46y9i{f9JUb}sR=GCq% zR(P3YRJ{JO!mW$0%9Jz@eYk?q7U{t?rcyCMhi z6{Z`TtYfCa`{>n@WsR%lQ;+N=tEm?r;P=t9QTaV#{6N zh1Pmom+C}u<3gMv&9tVn=P0E-_pG{G3+9@Qtto#hLS%otuAK(%ys9nwp}|Zmc#fx{ z@4e;VgALMJ*0VC@D40W;6Y8yVt(VamH@OrP?uF{bEryGZ>yr$b7q$3XH8k5~RT|1?GJ0JWv*9}?nbf>SGfY8XT`;`MnhRsYzrz*~= z*A&XdI*PFZ)w71p)L&UCq1xqkU@kKXfq;M)EJ!w0C{PvwOE%?t%W$GO* zQv5VVq%*X-k;SH=>(8gm)6xC*3s9bQ+sx>5RT7 zd4@9yS6et(OIvWcdDMMG9_j>>&9K$pDm%-Ru&KU$#B5$#Qtn?aEa-mH*Htd;G0_(7mw3bheDNh9_>{ z^Qy-bHmNZfzG!*8rbotKzSn=mCMS0TmwUqEoo9E0*ZsPG0VuUUW9$J8a9P8m5Z~r{ znNSEYOmn9J|Bl24<@*GEGdIz|qp>9b*Z}=!1OfPyUG2Y#)@lD4`*&c4Ogob4Bu+Xq z_pKif4jc5(+ssD9x+{L8cDah|fwsBUq1F8w&M`yF?qNW+?YVekiy}GmSVryVJ^eS| zh;`M+4z^hYRd^Q_9@K?wTb$A{7Xri|9nTYI6X~FIpsJ+#E>F2LwJt$rXE|;LH{VE| z;Xo7yWI3rZm-dT5(ROTHRc9U&zROcKs$;mhfnn zo9B75R?*7_8w>h>xgX>%Gl`By(!f8X>z@^CVZb)o~%L;_f%|NAB(nv;`jxp6AniR}RWt zOD#%0&=$Nt`J&_#-1=(EQqWKi8Lkb8!WuZfDcvVI0&)GgVqLa^bHM_2#0)@K3 zdjq1dUpco+wDwFy&qUnnvkH^scz73k? z*0^wSGR<<^RCVlhggmC)f!R|PKFR46xws_6p1H3d{JG8pRmZZE`8aUG%TL(o%%i$p34%Ee%*y;?&!bVsIIHVCU`&x67@V=oz80bPkRU#~ z>gCI6fJ#&z?)_yH7DU1RsqTCaah zxRduvIgME=l>hJbq$|g`^ed--g)kOKgQTUv^iBC-opi?Po#V+ zm|^$~;%#`!C&53@XE@4QRL7AQRrS7~vVf;t!Q#)z7t-jQjVzM8iRz8TkG3?+?X(#m z<7SA&gQxS2ubgGVr}+3Khj9P?+aBJ>0aLF{GTn+ZpK29FWaXm}S4>*R@TAF$j? zP`PuvJC1>5odjZPH}|b$E>yXWsgLIvw%Rj3?!mY;P72a{OGk~S zzi8qDb@f4?vTADBHjg5jCSF2k=JgQ|XpW2w2Br6wKArRmOB~8C&>lM*hdIYVKp2Y| z6JeT!ai>eA(2mL#^M^8v9P}5>P@G3x7M0vnt4+sq z^pA=!`D%4M<-@eFMq`Z_C+h!Q7-w(pixK<}i+|%%4w63`O{v~IOI3Pm{_;5hu<~vH KWra&4_WTP0c9{JD diff --git a/scripts/developer/tests/sphereLODTest.js b/scripts/developer/tests/sphereLODTest.js index d0cb35eaa1..dc19094664 100644 --- a/scripts/developer/tests/sphereLODTest.js +++ b/scripts/developer/tests/sphereLODTest.js @@ -15,7 +15,7 @@ MyAvatar.orientation = Quat.fromPitchYawRollDegrees(0, 0, 0); orientation = Quat.safeEulerAngles(MyAvatar.orientation); orientation.x = 0; orientation = Quat.fromVec3Degrees(orientation); -var tablePosition = Vec3.sum(MyAvatar.position, Quat.getForward(orientation)); +var tablePosition = Vec3.sum(MyAvatar.position, Quat.getFront(orientation)); tablePosition.y += 0.5; diff --git a/scripts/developer/tests/testInterval.js b/scripts/developer/tests/testInterval.js index 7898610c6d..94a5fe1fa5 100644 --- a/scripts/developer/tests/testInterval.js +++ b/scripts/developer/tests/testInterval.js @@ -12,7 +12,7 @@ var UPDATE_HZ = 60; // standard script update rate var UPDATE_INTERVAL = 1000/UPDATE_HZ; // standard script update interval var UPDATE_WORK_EFFORT = 0; // 1000 is light work, 1000000 ~= 30ms -var basePosition = Vec3.sum(Camera.getPosition(), Quat.getForward(Camera.getOrientation())); +var basePosition = Vec3.sum(Camera.getPosition(), Quat.getFront(Camera.getOrientation())); var timerBox = Entities.addEntity( { type: "Box", diff --git a/scripts/developer/tests/unit_tests/entityUnitTests.js b/scripts/developer/tests/unit_tests/entityUnitTests.js index 1372676901..033a484663 100644 --- a/scripts/developer/tests/unit_tests/entityUnitTests.js +++ b/scripts/developer/tests/unit_tests/entityUnitTests.js @@ -1,7 +1,7 @@ describe('Entity', function() { var center = Vec3.sum( MyAvatar.position, - Vec3.multiply(3, Quat.getForward(Camera.getOrientation())) + Vec3.multiply(3, Quat.getFront(Camera.getOrientation())) ); var boxEntity; var boxProps = { diff --git a/scripts/developer/tests/unit_tests/moduleTests/cycles/a.js b/scripts/developer/tests/unit_tests/moduleTests/cycles/a.js deleted file mode 100644 index 265cfaa2df..0000000000 --- a/scripts/developer/tests/unit_tests/moduleTests/cycles/a.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-env node */ -var a = exports; -a.done = false; -var b = require('./b.js'); -a.done = true; -a.name = 'a'; -a['a.done?'] = a.done; -a['b.done?'] = b.done; - -print('from a.js a.done =', a.done, '/ b.done =', b.done); diff --git a/scripts/developer/tests/unit_tests/moduleTests/cycles/b.js b/scripts/developer/tests/unit_tests/moduleTests/cycles/b.js deleted file mode 100644 index c46c872828..0000000000 --- a/scripts/developer/tests/unit_tests/moduleTests/cycles/b.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-env node */ -var b = exports; -b.done = false; -var a = require('./a.js'); -b.done = true; -b.name = 'b'; -b['a.done?'] = a.done; -b['b.done?'] = b.done; - -print('from b.js a.done =', a.done, '/ b.done =', b.done); diff --git a/scripts/developer/tests/unit_tests/moduleTests/cycles/main.js b/scripts/developer/tests/unit_tests/moduleTests/cycles/main.js deleted file mode 100644 index 0ec39cd656..0000000000 --- a/scripts/developer/tests/unit_tests/moduleTests/cycles/main.js +++ /dev/null @@ -1,17 +0,0 @@ -/* eslint-env node */ -/* global print */ -/* eslint-disable comma-dangle */ - -print('main.js'); -var a = require('./a.js'), - b = require('./b.js'); - -print('from main.js a.done =', a.done, 'and b.done =', b.done); - -module.exports = { - name: 'main', - a: a, - b: b, - 'a.done?': a.done, - 'b.done?': b.done, -}; diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorAPIException.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorAPIException.js deleted file mode 100644 index bbe694b578..0000000000 --- a/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorAPIException.js +++ /dev/null @@ -1,13 +0,0 @@ -/* eslint-disable comma-dangle */ -// test module method exception being thrown within main constructor -(function() { - var apiMethod = Script.require('../exceptions/exceptionInFunction.js'); - print(Script.resolvePath(''), "apiMethod", apiMethod); - // this next line throws from within apiMethod - print(apiMethod()); - return { - preload: function(uuid) { - print("entityConstructorAPIException::preload -- never seen --", uuid, Script.resolvePath('')); - }, - }; -}); diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorModule.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorModule.js deleted file mode 100644 index a4e8c17ab6..0000000000 --- a/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorModule.js +++ /dev/null @@ -1,23 +0,0 @@ -/* global module */ -/* eslint-disable comma-dangle */ -// test dual-purpose module and standalone Entity script -function MyEntity(filename) { - return { - preload: function(uuid) { - print("entityConstructorModule.js::preload"); - if (typeof module === 'object') { - print("module.filename", module.filename); - print("module.parent.filename", module.parent && module.parent.filename); - } - }, - clickDownOnEntity: function(uuid, evt) { - print("entityConstructorModule.js::clickDownOnEntity"); - }, - }; -} - -try { - module.exports = MyEntity; -} catch (e) {} // eslint-disable-line no-empty -print('entityConstructorModule::MyEntity', typeof MyEntity); -(MyEntity); diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested.js deleted file mode 100644 index a90d979877..0000000000 --- a/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested.js +++ /dev/null @@ -1,14 +0,0 @@ -/* global module */ -// test Entity constructor based on inherited constructor from a module -function constructor() { - print("entityConstructorNested::constructor"); - var MyEntity = Script.require('./entityConstructorModule.js'); - return new MyEntity("-- created from entityConstructorNested --"); -} - -try { - module.exports = constructor; -} catch (e) { - constructor; -} - diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested2.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested2.js deleted file mode 100644 index 29e0ed65b1..0000000000 --- a/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested2.js +++ /dev/null @@ -1,25 +0,0 @@ -/* global module */ -// test Entity constructor based on nested, inherited module constructors -function constructor() { - print("entityConstructorNested2::constructor"); - - // inherit from entityConstructorNested - var MyEntity = Script.require('./entityConstructorNested.js'); - function SubEntity() {} - SubEntity.prototype = new MyEntity('-- created from entityConstructorNested2 --'); - - // create new instance - var entity = new SubEntity(); - // "override" clickDownOnEntity for just this new instance - entity.clickDownOnEntity = function(uuid, evt) { - print("entityConstructorNested2::clickDownOnEntity"); - SubEntity.prototype.clickDownOnEntity.apply(this, arguments); - }; - return entity; -} - -try { - module.exports = constructor; -} catch (e) { - constructor; -} diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorRequireException.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorRequireException.js deleted file mode 100644 index 5872bce529..0000000000 --- a/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorRequireException.js +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable comma-dangle */ -// test module-related exception from within "require" evaluation itself -(function() { - var mod = Script.require('../exceptions/exception.js'); - return { - preload: function(uuid) { - print("entityConstructorRequireException::preload (never happens)", uuid, Script.resolvePath(''), mod); - }, - }; -}); diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadAPIError.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadAPIError.js deleted file mode 100644 index eaee178b0a..0000000000 --- a/scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadAPIError.js +++ /dev/null @@ -1,13 +0,0 @@ -/* eslint-disable comma-dangle */ -// test module method exception being thrown within preload -(function() { - var apiMethod = Script.require('../exceptions/exceptionInFunction.js'); - print(Script.resolvePath(''), "apiMethod", apiMethod); - return { - preload: function(uuid) { - // this next line throws from within apiMethod - print(apiMethod()); - print("entityPreloadAPIException::preload -- never seen --", uuid, Script.resolvePath('')); - }, - }; -}); diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadRequire.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadRequire.js deleted file mode 100644 index 50dab9fa7c..0000000000 --- a/scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadRequire.js +++ /dev/null @@ -1,11 +0,0 @@ -/* eslint-disable comma-dangle */ -// test requiring a module from within preload -(function constructor() { - return { - preload: function(uuid) { - print("entityPreloadRequire::preload"); - var example = Script.require('../example.json'); - print("entityPreloadRequire::example::name", example.name); - }, - }; -}); diff --git a/scripts/developer/tests/unit_tests/moduleTests/example.json b/scripts/developer/tests/unit_tests/moduleTests/example.json deleted file mode 100644 index 42d7fe07da..0000000000 --- a/scripts/developer/tests/unit_tests/moduleTests/example.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "Example JSON Module", - "last-modified": 1485789862, - "config": { - "title": "My Title", - "width": 800, - "height": 600 - } -} diff --git a/scripts/developer/tests/unit_tests/moduleTests/exceptions/exception.js b/scripts/developer/tests/unit_tests/moduleTests/exceptions/exception.js deleted file mode 100644 index 8d25d6b7a4..0000000000 --- a/scripts/developer/tests/unit_tests/moduleTests/exceptions/exception.js +++ /dev/null @@ -1,4 +0,0 @@ -/* eslint-env node */ -module.exports = "n/a"; -throw new Error('exception on line 2'); - diff --git a/scripts/developer/tests/unit_tests/moduleTests/exceptions/exceptionInFunction.js b/scripts/developer/tests/unit_tests/moduleTests/exceptions/exceptionInFunction.js deleted file mode 100644 index 69415a0741..0000000000 --- a/scripts/developer/tests/unit_tests/moduleTests/exceptions/exceptionInFunction.js +++ /dev/null @@ -1,38 +0,0 @@ -/* eslint-env node */ -// dummy lines to make sure exception line number is well below parent test script -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// - - -function myfunc() { - throw new Error('exception on line 32 in myfunc'); -} -module.exports = myfunc; -if (Script[module.filename] === 'throw') { - myfunc(); -} diff --git a/scripts/developer/tests/unit_tests/moduleUnitTests.js b/scripts/developer/tests/unit_tests/moduleUnitTests.js deleted file mode 100644 index 6810dd8b6d..0000000000 --- a/scripts/developer/tests/unit_tests/moduleUnitTests.js +++ /dev/null @@ -1,378 +0,0 @@ -/* eslint-env jasmine, node */ -/* global print:true, Script:true, global:true, require:true */ -/* eslint-disable comma-dangle */ -var isNode = instrumentTestrunner(), - runInterfaceTests = !isNode, - runNetworkTests = true; - -// describe wrappers (note: `xdescribe` indicates a disabled or "pending" jasmine test) -var INTERFACE = { describe: runInterfaceTests ? describe : xdescribe }, - NETWORK = { describe: runNetworkTests ? describe : xdescribe }; - -describe('require', function() { - describe('resolve', function() { - it('should resolve relative filenames', function() { - var expected = Script.resolvePath('./moduleTests/example.json'); - expect(require.resolve('./moduleTests/example.json')).toEqual(expected); - }); - describe('exceptions', function() { - it('should reject blank "" module identifiers', function() { - expect(function() { - require.resolve(''); - }).toThrowError(/Cannot find/); - }); - it('should reject excessive identifier sizes', function() { - expect(function() { - require.resolve(new Array(8193).toString()); - }).toThrowError(/Cannot find/); - }); - it('should reject implicitly-relative filenames', function() { - expect(function() { - var mod = require.resolve('example.js'); - mod.exists; - }).toThrowError(/Cannot find/); - }); - it('should reject unanchored, existing filenames with advice', function() { - expect(function() { - var mod = require.resolve('moduleTests/example.json'); - mod.exists; - }).toThrowError(/use '.\/moduleTests\/example\.json'/); - }); - it('should reject unanchored, non-existing filenames', function() { - expect(function() { - var mod = require.resolve('asdfssdf/example.json'); - mod.exists; - }).toThrowError(/Cannot find.*system module not found/); - }); - it('should reject non-existent filenames', function() { - expect(function() { - require.resolve('./404error.js'); - }).toThrowError(/Cannot find/); - }); - it('should reject identifiers resolving to a directory', function() { - expect(function() { - var mod = require.resolve('.'); - mod.exists; - // console.warn('resolved(.)', mod); - }).toThrowError(/Cannot find/); - expect(function() { - var mod = require.resolve('..'); - mod.exists; - // console.warn('resolved(..)', mod); - }).toThrowError(/Cannot find/); - expect(function() { - var mod = require.resolve('../'); - mod.exists; - // console.warn('resolved(../)', mod); - }).toThrowError(/Cannot find/); - }); - (isNode ? xit : it)('should reject non-system, extensionless identifiers', function() { - expect(function() { - require.resolve('./example'); - }).toThrowError(/Cannot find/); - }); - }); - }); - - describe('JSON', function() { - it('should import .json modules', function() { - var example = require('./moduleTests/example.json'); - expect(example.name).toEqual('Example JSON Module'); - }); - // noet: support for loading JSON via content type workarounds reverted - // (leaving these tests intact in case ever revisited later) - // INTERFACE.describe('interface', function() { - // NETWORK.describe('network', function() { - // xit('should import #content-type=application/json modules', function() { - // var results = require('https://jsonip.com#content-type=application/json'); - // expect(results.ip).toMatch(/^[.0-9]+$/); - // }); - // xit('should import content-type: application/json modules', function() { - // var scope = { 'content-type': 'application/json' }; - // var results = require.call(scope, 'https://jsonip.com'); - // expect(results.ip).toMatch(/^[.0-9]+$/); - // }); - // }); - // }); - - }); - - INTERFACE.describe('system', function() { - it('require("vec3")', function() { - expect(require('vec3')).toEqual(jasmine.any(Function)); - }); - it('require("vec3").method', function() { - expect(require('vec3')().isValid).toEqual(jasmine.any(Function)); - }); - it('require("vec3") as constructor', function() { - var vec3 = require('vec3'); - var v = vec3(1.1, 2.2, 3.3); - expect(v).toEqual(jasmine.any(Object)); - expect(v.isValid).toEqual(jasmine.any(Function)); - expect(v.isValid()).toBe(true); - expect(v.toString()).toEqual('[Vec3 (1.100,2.200,3.300)]'); - }); - }); - - describe('cache', function() { - it('should cache modules by resolved module id', function() { - var value = new Date; - var example = require('./moduleTests/example.json'); - // earmark the module object with a unique value - example['.test'] = value; - var example2 = require('../../tests/unit_tests/moduleTests/example.json'); - expect(example2).toBe(example); - // verify earmark is still the same after a second require() - expect(example2['.test']).toBe(example['.test']); - }); - it('should reload cached modules set to null', function() { - var value = new Date; - var example = require('./moduleTests/example.json'); - example['.test'] = value; - require.cache[require.resolve('../../tests/unit_tests/moduleTests/example.json')] = null; - var example2 = require('../../tests/unit_tests/moduleTests/example.json'); - // verify the earmark is *not* the same as before - expect(example2).not.toBe(example); - expect(example2['.test']).not.toBe(example['.test']); - }); - it('should reload when module property is deleted', function() { - var value = new Date; - var example = require('./moduleTests/example.json'); - example['.test'] = value; - delete require.cache[require.resolve('../../tests/unit_tests/moduleTests/example.json')]; - var example2 = require('../../tests/unit_tests/moduleTests/example.json'); - // verify the earmark is *not* the same as before - expect(example2).not.toBe(example); - expect(example2['.test']).not.toBe(example['.test']); - }); - }); - - describe('cyclic dependencies', function() { - describe('should allow lazy-ref cyclic module resolution', function() { - var main; - beforeEach(function() { - // eslint-disable-next-line - try { this._print = print; } catch (e) {} - // during these tests print() is no-op'd so that it doesn't disrupt the reporter output - print = function() {}; - Script.resetModuleCache(); - }); - afterEach(function() { - print = this._print; - }); - it('main is requirable', function() { - main = require('./moduleTests/cycles/main.js'); - expect(main).toEqual(jasmine.any(Object)); - }); - it('transient a and b done values', function() { - expect(main.a['b.done?']).toBe(true); - expect(main.b['a.done?']).toBe(false); - }); - it('ultimate a.done?', function() { - expect(main['a.done?']).toBe(true); - }); - it('ultimate b.done?', function() { - expect(main['b.done?']).toBe(true); - }); - }); - }); - - describe('JS', function() { - it('should throw catchable local file errors', function() { - expect(function() { - require('file:///dev/null/non-existent-file.js'); - }).toThrowError(/path not found|Cannot find.*non-existent-file/); - }); - it('should throw catchable invalid id errors', function() { - expect(function() { - require(new Array(4096 * 2).toString()); - }).toThrowError(/invalid.*size|Cannot find.*,{30}/); - }); - it('should throw catchable unresolved id errors', function() { - expect(function() { - require('foobar:/baz.js'); - }).toThrowError(/could not resolve|Cannot find.*foobar:/); - }); - - NETWORK.describe('network', function() { - // note: depending on retries these tests can take up to 60 seconds each to timeout - var timeout = 75 * 1000; - it('should throw catchable host errors', function() { - expect(function() { - var mod = require('http://non.existent.highfidelity.io/moduleUnitTest.js'); - print("mod", Object.keys(mod)); - }).toThrowError(/error retrieving script .ServerUnavailable.|Cannot find.*non.existent/); - }, timeout); - it('should throw catchable network timeouts', function() { - expect(function() { - require('http://ping.highfidelity.io:1024'); - }).toThrowError(/error retrieving script .Timeout.|Cannot find.*ping.highfidelity/); - }, timeout); - }); - }); - - INTERFACE.describe('entity', function() { - var sampleScripts = [ - 'entityConstructorAPIException.js', - 'entityConstructorModule.js', - 'entityConstructorNested2.js', - 'entityConstructorNested.js', - 'entityConstructorRequireException.js', - 'entityPreloadAPIError.js', - 'entityPreloadRequire.js', - ].filter(Boolean).map(function(id) { - return Script.require.resolve('./moduleTests/entity/'+id); - }); - - var uuids = []; - function cleanup() { - uuids.splice(0,uuids.length).forEach(function(uuid) { - Entities.deleteEntity(uuid); - }); - } - afterAll(cleanup); - // extra sanity check to avoid lingering entities - Script.scriptEnding.connect(cleanup); - - for (var i=0; i < sampleScripts.length; i++) { - maketest(i); - } - - function maketest(i) { - var script = sampleScripts[ i % sampleScripts.length ]; - var shortname = '['+i+'] ' + script.split('/').pop(); - var position = MyAvatar.position; - position.y -= i/2; - // define a unique jasmine test for the current entity script - it(shortname, function(done) { - var uuid = Entities.addEntity({ - text: shortname, - description: Script.resolvePath('').split('/').pop(), - type: 'Text', - position: position, - rotation: MyAvatar.orientation, - script: script, - scriptTimestamp: +new Date, - lifetime: 20, - lineHeight: 1/8, - dimensions: { x: 2, y: 0.5, z: 0.01 }, - backgroundColor: { red: 0, green: 0, blue: 0 }, - color: { red: 0xff, green: 0xff, blue: 0xff }, - }, !Entities.serversExist() || !Entities.canRezTmp()); - uuids.push(uuid); - function stopChecking() { - if (ii) { - Script.clearInterval(ii); - ii = 0; - } - } - var ii = Script.setInterval(function() { - Entities.queryPropertyMetadata(uuid, "script", function(err, result) { - if (err) { - stopChecking(); - throw new Error(err); - } - if (result.success) { - stopChecking(); - if (/Exception/.test(script)) { - expect(result.status).toMatch(/^error_(loading|running)_script$/); - } else { - expect(result.status).toEqual("running"); - } - Entities.deleteEntity(uuid); - done(); - } else { - print('!result.success', JSON.stringify(result)); - } - }); - }, 100); - Script.setTimeout(stopChecking, 4900); - }, 5000 /* jasmine async timeout */); - } - }); -}); - -// support for isomorphic Node.js / Interface unit testing -// note: run `npm install` from unit_tests/ and then `node moduleUnitTests.js` -function run() {} -function instrumentTestrunner() { - var isNode = typeof process === 'object' && process.title === 'node'; - if (typeof describe === 'function') { - // already running within a test runner; assume jasmine is ready-to-go - return isNode; - } - if (isNode) { - /* eslint-disable no-console */ - // Node.js test mode - // to keep things consistent Node.js uses the local jasmine.js library (instead of an npm version) - var jasmineRequire = require('../../libraries/jasmine/jasmine.js'); - var jasmine = jasmineRequire.core(jasmineRequire); - var env = jasmine.getEnv(); - var jasmineInterface = jasmineRequire.interface(jasmine, env); - for (var p in jasmineInterface) { - global[p] = jasmineInterface[p]; - } - env.addReporter(new (require('jasmine-console-reporter'))); - // testing mocks - Script = { - resetModuleCache: function() { - module.require.cache = {}; - }, - setTimeout: setTimeout, - clearTimeout: clearTimeout, - resolvePath: function(id) { - // this attempts to accurately emulate how Script.resolvePath works - var trace = {}; Error.captureStackTrace(trace); - var base = trace.stack.split('\n')[2].replace(/^.*[(]|[)].*$/g,'').replace(/:[0-9]+:[0-9]+.*$/,''); - if (!id) { - return base; - } - var rel = base.replace(/[^\/]+$/, id); - console.info('rel', rel); - return require.resolve(rel); - }, - require: function(mod) { - return require(Script.require.resolve(mod)); - }, - }; - Script.require.cache = require.cache; - Script.require.resolve = function(mod) { - if (mod === '.' || /^\.\.($|\/)/.test(mod)) { - throw new Error("Cannot find module '"+mod+"' (is dir)"); - } - var path = require.resolve(mod); - // console.info('node-require-reoslved', mod, path); - try { - if (require('fs').lstatSync(path).isDirectory()) { - throw new Error("Cannot find module '"+path+"' (is directory)"); - } - // console.info('!path', path); - } catch (e) { - console.error(e); - } - return path; - }; - print = console.info.bind(console, '[print]'); - /* eslint-enable no-console */ - } else { - // Interface test mode - global = this; - Script.require('../../../system/libraries/utils.js'); - this.jasmineRequire = Script.require('../../libraries/jasmine/jasmine.js'); - Script.require('../../libraries/jasmine/hifi-boot.js'); - require = Script.require; - // polyfill console - /* global console:true */ - console = { - log: print, - info: print.bind(this, '[info]'), - warn: print.bind(this, '[warn]'), - error: print.bind(this, '[error]'), - debug: print.bind(this, '[debug]'), - }; - } - // eslint-disable-next-line - run = function() { global.jasmine.getEnv().execute(); }; - return isNode; -} -run(); diff --git a/scripts/developer/tests/unit_tests/package.json b/scripts/developer/tests/unit_tests/package.json deleted file mode 100644 index 91d719b687..0000000000 --- a/scripts/developer/tests/unit_tests/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "unit_tests", - "devDependencies": { - "jasmine-console-reporter": "^1.2.7" - } -} diff --git a/scripts/developer/tests/unit_tests/scriptUnitTests.js b/scripts/developer/tests/unit_tests/scriptUnitTests.js index fa8cb44608..63b451e97f 100644 --- a/scripts/developer/tests/unit_tests/scriptUnitTests.js +++ b/scripts/developer/tests/unit_tests/scriptUnitTests.js @@ -15,20 +15,10 @@ describe('Script', function () { // characterization tests // initially these are just to capture how the app works currently var testCases = { - // special relative resolves '': filename, '.': dirname, '..': parentdir, - - // local file "magic" tilde path expansion - '/~/defaultScripts.js': ScriptDiscoveryService.defaultScriptsPath + '/defaultScripts.js', - - // these schemes appear to always get resolved to empty URLs - 'qrc://test': '', 'about:Entities 1': '', - 'ftp://host:port/path': '', - 'data:text/html;text,foo': '', - 'Entities 1': dirname + 'Entities 1', './file.js': dirname + 'file.js', 'c:/temp/': 'file:///c:/temp/', @@ -41,12 +31,6 @@ describe('Script', function () { '/~/libraries/utils.js': 'file:///~/libraries/utils.js', '/temp/file.js': 'file:///temp/file.js', '/~/': 'file:///~/', - - // these schemes appear to always get resolved to the same URL again - 'http://highfidelity.com': 'http://highfidelity.com', - 'atp:/highfidelity': 'atp:/highfidelity', - 'atp:c2d7e3a48cadf9ba75e4f8d9f4d80e75276774880405a093fdee36543aa04f': - 'atp:c2d7e3a48cadf9ba75e4f8d9f4d80e75276774880405a093fdee36543aa04f', }; describe('resolvePath', function () { Object.keys(testCases).forEach(function(input) { @@ -58,7 +42,7 @@ describe('Script', function () { describe('include', function () { var old_cache_buster; - var cache_buster = '#' + new Date().getTime().toString(36); + var cache_buster = '#' + +new Date; beforeAll(function() { old_cache_buster = Settings.getValue('cache_buster'); Settings.setValue('cache_buster', cache_buster); diff --git a/scripts/developer/tests/viveTouchpadTest.js b/scripts/developer/tests/viveTouchpadTest.js index b5d9575adf..913da5888d 100644 --- a/scripts/developer/tests/viveTouchpadTest.js +++ b/scripts/developer/tests/viveTouchpadTest.js @@ -24,10 +24,10 @@ var boxZAxis, boxYAxis; var prevThumbDown = false; function init() { - boxPosition = Vec3.sum(MyAvatar.position, Vec3.multiply(3, Quat.getForward(Camera.getOrientation()))); - var forward = Quat.getForward(Camera.getOrientation()); - boxZAxis = Vec3.normalize(Vec3.cross(forward, Y_AXIS)); - boxYAxis = Vec3.normalize(Vec3.cross(boxZAxis, forward)); + boxPosition = Vec3.sum(MyAvatar.position, Vec3.multiply(3, Quat.getFront(Camera.getOrientation()))); + var front = Quat.getFront(Camera.getOrientation()); + boxZAxis = Vec3.normalize(Vec3.cross(front, Y_AXIS)); + boxYAxis = Vec3.normalize(Vec3.cross(boxZAxis, front)); boxEntity = Entities.addEntity({ type: "Box", diff --git a/scripts/developer/utilities/record/recorder.js b/scripts/developer/utilities/record/recorder.js index ba1c8b0393..0e335116d5 100644 --- a/scripts/developer/utilities/record/recorder.js +++ b/scripts/developer/utilities/record/recorder.js @@ -9,14 +9,12 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -/* globals HIFI_PUBLIC_BUCKET:true, Tool, ToolBar */ - HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/"; Script.include("/~/system/libraries/toolBars.js"); var recordingFile = "recording.hfr"; -function setDefaultPlayerOptions() { +function setPlayerOptions() { Recording.setPlayFromCurrentLocation(true); Recording.setPlayerUseDisplayName(false); Recording.setPlayerUseAttachments(false); @@ -40,16 +38,16 @@ var saveIcon; var loadIcon; var spacing; var timerOffset; +setupToolBar(); + var timer = null; var slider = null; - -setupToolBar(); setupTimer(); var watchStop = false; function setupToolBar() { - if (toolBar !== null) { + if (toolBar != null) { print("Multiple calls to Recorder.js:setupToolBar()"); return; } @@ -58,8 +56,6 @@ function setupToolBar() { toolBar = new ToolBar(0, 0, ToolBar.HORIZONTAL); - toolBar.onMove = onToolbarMove; - toolBar.setBack(COLOR_TOOL_BAR, ALPHA_OFF); recordIcon = toolBar.addTool({ @@ -90,7 +86,7 @@ function setupToolBar() { visible: true }, false); - timerOffset = toolBar.width + ToolBar.SPACING; + timerOffset = toolBar.width; spacing = toolBar.addSpacing(0); saveIcon = toolBar.addTool({ @@ -116,15 +112,15 @@ function setupTimer() { text: (0.00).toFixed(3), backgroundColor: COLOR_OFF, x: 0, y: 0, - width: 200, height: 25, - leftMargin: 5, topMargin: 3, + width: 0, height: 0, + leftMargin: 10, topMargin: 10, alpha: 1.0, backgroundAlpha: 1.0, visible: true }); slider = { x: 0, y: 0, w: 200, h: 20, - pos: 0.0 // 0.0 <= pos <= 1.0 + pos: 0.0, // 0.0 <= pos <= 1.0 }; slider.background = Overlays.addOverlay("text", { text: "", @@ -148,40 +144,20 @@ function setupTimer() { }); } -function onToolbarMove(newX, newY, deltaX, deltaY) { - Overlays.editOverlay(timer, { - x: newX + timerOffset - ToolBar.SPACING, - y: newY - }); - - slider.x = newX - ToolBar.SPACING; - slider.y = newY - slider.h - ToolBar.SPACING; - - Overlays.editOverlay(slider.background, { - x: slider.x, - y: slider.y - }); - Overlays.editOverlay(slider.foreground, { - x: slider.x, - y: slider.y - }); -} - function updateTimer() { var text = ""; if (Recording.isRecording()) { text = formatTime(Recording.recorderElapsed()); + } else { - text = formatTime(Recording.playerElapsed()) + " / " + formatTime(Recording.playerLength()); + text = formatTime(Recording.playerElapsed()) + " / " + + formatTime(Recording.playerLength()); } - var timerWidth = text.length * 8 + ((Recording.isRecording()) ? 15 : 0); - Overlays.editOverlay(timer, { - text: text, - width: timerWidth - }); - toolBar.changeSpacing(timerWidth + ToolBar.SPACING, spacing); + text: text + }) + toolBar.changeSpacing(text.length * 8 + ((Recording.isRecording()) ? 15 : 0), spacing); if (Recording.isRecording()) { slider.pos = 1.0; @@ -197,7 +173,7 @@ function updateTimer() { function formatTime(time) { var MIN_PER_HOUR = 60; var SEC_PER_MIN = 60; - var MSEC_DIGITS = 3; + var MSEC_PER_SEC = 1000; var hours = Math.floor(time / (SEC_PER_MIN * MIN_PER_HOUR)); time -= hours * (SEC_PER_MIN * MIN_PER_HOUR); @@ -208,19 +184,37 @@ function formatTime(time) { var seconds = time; var text = ""; - text += (hours > 0) ? hours + ":" : ""; - text += (minutes > 0) ? ((minutes < 10 && text !== "") ? "0" : "") + minutes + ":" : ""; - text += ((seconds < 10 && text !== "") ? "0" : "") + seconds.toFixed(MSEC_DIGITS); + text += (hours > 0) ? hours + ":" : + ""; + text += (minutes > 0) ? ((minutes < 10 && text != "") ? "0" : "") + minutes + ":" : + ""; + text += ((seconds < 10 && text != "") ? "0" : "") + seconds.toFixed(3); return text; } function moveUI() { var relative = { x: 70, y: 40 }; toolBar.move(relative.x, windowDimensions.y - relative.y); + Overlays.editOverlay(timer, { + x: relative.x + timerOffset - ToolBar.SPACING, + y: windowDimensions.y - relative.y - ToolBar.SPACING + }); + + slider.x = relative.x - ToolBar.SPACING; + slider.y = windowDimensions.y - relative.y - slider.h - ToolBar.SPACING; + + Overlays.editOverlay(slider.background, { + x: slider.x, + y: slider.y, + }); + Overlays.editOverlay(slider.foreground, { + x: slider.x, + y: slider.y, + }); } function mousePressEvent(event) { - var clickedOverlay = Overlays.getOverlayAtPoint({ x: event.x, y: event.y }); + clickedOverlay = Overlays.getOverlayAtPoint({ x: event.x, y: event.y }); if (recordIcon === toolBar.clicked(clickedOverlay, false) && !Recording.isPlaying()) { if (!Recording.isRecording()) { @@ -232,11 +226,7 @@ function mousePressEvent(event) { toolBar.setAlpha(ALPHA_OFF, loadIcon); } else { Recording.stopRecording(); - toolBar.selectTool(recordIcon, true); - setDefaultPlayerOptions(); - // Plays the recording at the same spot as you recorded it - Recording.setPlayFromCurrentLocation(false); - Recording.setPlayerTime(0); + toolBar.selectTool(recordIcon, true ); Recording.loadLastRecording(); toolBar.setAlpha(ALPHA_ON, playIcon); toolBar.setAlpha(ALPHA_ON, playLoopIcon); @@ -250,6 +240,7 @@ function mousePressEvent(event) { toolBar.setAlpha(ALPHA_ON, saveIcon); toolBar.setAlpha(ALPHA_ON, loadIcon); } else if (Recording.playerLength() > 0) { + setPlayerOptions(); Recording.setPlayerLoop(false); Recording.startPlaying(); toolBar.setAlpha(ALPHA_OFF, recordIcon); @@ -264,6 +255,7 @@ function mousePressEvent(event) { toolBar.setAlpha(ALPHA_ON, saveIcon); toolBar.setAlpha(ALPHA_ON, loadIcon); } else if (Recording.playerLength() > 0) { + setPlayerOptions(); Recording.setPlayerLoop(true); Recording.startPlaying(); toolBar.setAlpha(ALPHA_OFF, recordIcon); @@ -271,7 +263,7 @@ function mousePressEvent(event) { toolBar.setAlpha(ALPHA_OFF, loadIcon); } } else if (saveIcon === toolBar.clicked(clickedOverlay)) { - if (!Recording.isRecording() && !Recording.isPlaying() && Recording.playerLength() !== 0) { + if (!Recording.isRecording() && !Recording.isPlaying() && Recording.playerLength() != 0) { recordingFile = Window.save("Save recording to file", ".", "Recordings (*.hfr)"); if (!(recordingFile === "null" || recordingFile === null || recordingFile === "")) { Recording.saveRecording(recordingFile); @@ -282,7 +274,6 @@ function mousePressEvent(event) { recordingFile = Window.browse("Load recording from file", ".", "Recordings (*.hfr *.rec *.HFR *.REC)"); if (!(recordingFile === "null" || recordingFile === null || recordingFile === "")) { Recording.loadRecording(recordingFile); - setDefaultPlayerOptions(); } if (Recording.playerLength() > 0) { toolBar.setAlpha(ALPHA_ON, playIcon); @@ -291,8 +282,8 @@ function mousePressEvent(event) { } } } else if (Recording.playerLength() > 0 && - slider.x < event.x && event.x < slider.x + slider.w && - slider.y < event.y && event.y < slider.y + slider.h) { + slider.x < event.x && event.x < slider.x + slider.w && + slider.y < event.y && event.y < slider.y + slider.h) { isSliding = true; slider.pos = (event.x - slider.x) / slider.w; Recording.setPlayerTime(slider.pos * Recording.playerLength()); @@ -317,7 +308,7 @@ function mouseReleaseEvent(event) { function update() { var newDimensions = Controller.getViewportDimensions(); - if (windowDimensions.x !== newDimensions.x || windowDimensions.y !== newDimensions.y) { + if (windowDimensions.x != newDimensions.x || windowDimensions.y != newDimensions.y) { windowDimensions = newDimensions; moveUI(); } diff --git a/scripts/developer/utilities/render/deferredLighting.qml b/scripts/developer/utilities/render/deferredLighting.qml index c7ec8e1153..99a9f258e3 100644 --- a/scripts/developer/utilities/render/deferredLighting.qml +++ b/scripts/developer/utilities/render/deferredLighting.qml @@ -25,7 +25,7 @@ Column { "Lightmap:LightingModel:enableLightmap", "Background:LightingModel:enableBackground", "ssao:AmbientOcclusion:enabled", - "Textures:LightingModel:enableMaterialTexturing" + "Textures:LightingModel:enableMaterialTexturing", ] CheckBox { text: modelData.split(":")[0] @@ -45,7 +45,6 @@ Column { "Diffuse:LightingModel:enableDiffuse", "Specular:LightingModel:enableSpecular", "Albedo:LightingModel:enableAlbedo", - "Wireframe:LightingModel:enableWireframe" ] CheckBox { text: modelData.split(":")[0] diff --git a/scripts/developer/utilities/render/photobooth/photobooth.js b/scripts/developer/utilities/render/photobooth/photobooth.js index b78986be1a..3e86d83a98 100644 --- a/scripts/developer/utilities/render/photobooth/photobooth.js +++ b/scripts/developer/utilities/render/photobooth/photobooth.js @@ -8,13 +8,12 @@ var PhotoBooth = {}; PhotoBooth.init = function () { var success = Clipboard.importEntities(PHOTOBOOTH_SETUP_JSON_URL); - var forwardFactor = 10; - var forwardUnitVector = Vec3.normalize(Quat.getForward(MyAvatar.orientation)); - var forwardOffset = Vec3.multiply(forwardUnitVector,forwardFactor); + var frontFactor = 10; + var frontUnitVec = Vec3.normalize(Quat.getFront(MyAvatar.orientation)); + var frontOffset = Vec3.multiply(frontUnitVec,frontFactor); var rightFactor = 3; - // TODO: rightUnitVec is unused and spawnLocation declaration is incorrect var rightUnitVec = Vec3.normalize(Quat.getRight(MyAvatar.orientation)); - var spawnLocation = Vec3.sum(Vec3.sum(MyAvatar.position,forwardOffset),rightFactor); + var spawnLocation = Vec3.sum(Vec3.sum(MyAvatar.position,frontOffset),rightFactor); if (success) { this.pastedEntityIDs = Clipboard.pasteEntities(spawnLocation); this.processPastedEntities(); diff --git a/scripts/modules/vec3.js b/scripts/modules/vec3.js deleted file mode 100644 index f164f01374..0000000000 --- a/scripts/modules/vec3.js +++ /dev/null @@ -1,69 +0,0 @@ -// Example of using a "system module" to decouple Vec3's implementation details. -// -// Users would bring Vec3 support in as a module: -// var vec3 = Script.require('vec3'); -// - -// (this example is compatible with using as a Script.include and as a Script.require module) -try { - // Script.require - module.exports = vec3; -} catch(e) { - // Script.include - Script.registerValue("vec3", vec3); -} - -vec3.fromObject = function(v) { - //return new vec3(v.x, v.y, v.z); - //... this is even faster and achieves the same effect - v.__proto__ = vec3.prototype; - return v; -}; - -vec3.prototype = { - multiply: function(v2) { - // later on could support overrides like so: - // if (v2 instanceof quat) { [...] } - // which of the below is faster (C++ or JS)? - // (dunno -- but could systematically find out and go with that version) - - // pure JS option - // return new vec3(this.x * v2.x, this.y * v2.y, this.z * v2.z); - - // hybrid C++ option - return vec3.fromObject(Vec3.multiply(this, v2)); - }, - // detects any NaN and Infinity values - isValid: function() { - return isFinite(this.x) && isFinite(this.y) && isFinite(this.z); - }, - // format Vec3's, eg: - // var v = vec3(); - // print(v); // outputs [Vec3 (0.000, 0.000, 0.000)] - toString: function() { - if (this === vec3.prototype) { - return "{Vec3 prototype}"; - } - function fixed(n) { return n.toFixed(3); } - return "[Vec3 (" + [this.x, this.y, this.z].map(fixed) + ")]"; - }, -}; - -vec3.DEBUG = true; - -function vec3(x, y, z) { - if (!(this instanceof vec3)) { - // if vec3 is called as a function then re-invoke as a constructor - // (so that `value instanceof vec3` holds true for created values) - return new vec3(x, y, z); - } - - // unfold default arguments (vec3(), vec3(.5), vec3(0,1), etc.) - this.x = x !== undefined ? x : 0; - this.y = y !== undefined ? y : this.x; - this.z = z !== undefined ? z : this.y; - - if (vec3.DEBUG && !this.isValid()) - throw new Error('vec3() -- invalid initial values ['+[].slice.call(arguments)+']'); -}; - diff --git a/scripts/system/assets/images/icon-particles.svg b/scripts/system/assets/images/icon-particles.svg deleted file mode 100644 index 5e0105d7cd..0000000000 --- a/scripts/system/assets/images/icon-particles.svg +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/scripts/system/assets/images/icon-point-light.svg b/scripts/system/assets/images/icon-point-light.svg deleted file mode 100644 index 896c35b63b..0000000000 --- a/scripts/system/assets/images/icon-point-light.svg +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/scripts/system/assets/images/icon-spot-light.svg b/scripts/system/assets/images/icon-spot-light.svg deleted file mode 100644 index ac2f87bb27..0000000000 --- a/scripts/system/assets/images/icon-spot-light.svg +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/scripts/system/away.js b/scripts/system/away.js index 4ca938d492..541fe6f679 100644 --- a/scripts/system/away.js +++ b/scripts/system/away.js @@ -87,8 +87,8 @@ function moveCloserToCamera(positionAtHUD) { // we don't actually want to render at the slerped look at... instead, we want to render // slightly closer to the camera than that. var MOVE_CLOSER_TO_CAMERA_BY = -0.25; - var cameraForward = Quat.getForward(Camera.orientation); - var closerToCamera = Vec3.multiply(cameraForward, MOVE_CLOSER_TO_CAMERA_BY); // slightly closer to camera + var cameraFront = Quat.getFront(Camera.orientation); + var closerToCamera = Vec3.multiply(cameraFront, MOVE_CLOSER_TO_CAMERA_BY); // slightly closer to camera var slightlyCloserPosition = Vec3.sum(positionAtHUD, closerToCamera); return slightlyCloserPosition; diff --git a/scripts/system/controllers/grab.js b/scripts/system/controllers/grab.js index 05b2eefeb5..f0b6663bec 100644 --- a/scripts/system/controllers/grab.js +++ b/scripts/system/controllers/grab.js @@ -463,7 +463,7 @@ Grabber.prototype.moveEvent = function(event) { var orientation = Camera.getOrientation(); var dragOffset = Vec3.multiply(drag.x, Quat.getRight(orientation)); dragOffset = Vec3.sum(dragOffset, Vec3.multiply(-drag.y, Quat.getUp(orientation))); - var axis = Vec3.cross(dragOffset, Quat.getForward(orientation)); + var axis = Vec3.cross(dragOffset, Quat.getFront(orientation)); axis = Vec3.normalize(axis); var ROTATE_STRENGTH = 0.4; // magic number tuned by hand var angle = ROTATE_STRENGTH * Math.sqrt((drag.x * drag.x) + (drag.y * drag.y)); @@ -487,7 +487,7 @@ Grabber.prototype.moveEvent = function(event) { if (this.mode === "verticalCylinder") { // for this mode we recompute the plane based on current Camera - var planeNormal = Quat.getForward(Camera.getOrientation()); + var planeNormal = Quat.getFront(Camera.getOrientation()); planeNormal.y = 0; planeNormal = Vec3.normalize(planeNormal); var pointOnCylinder = Vec3.multiply(planeNormal, this.xzDistanceToGrab); diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index e83e31aaa5..51b01e60a2 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -1481,7 +1481,7 @@ function MyController(hand) { var pickRay = { origin: PICK_WITH_HAND_RAY ? worldHandPosition : Camera.position, direction: PICK_WITH_HAND_RAY ? Quat.getUp(worldHandRotation) : Vec3.mix(Quat.getUp(worldHandRotation), - Quat.getForward(Camera.orientation), + Quat.getFront(Camera.orientation), HAND_HEAD_MIX_RATIO), length: PICK_MAX_DISTANCE }; diff --git a/scripts/system/controllers/handControllerPointer.js b/scripts/system/controllers/handControllerPointer.js index eb94428100..f8a336a017 100644 --- a/scripts/system/controllers/handControllerPointer.js +++ b/scripts/system/controllers/handControllerPointer.js @@ -174,7 +174,7 @@ function calculateRayUICollisionPoint(position, direction) { // interect HUD plane, 1m in front of camera, using formula: // scale = hudNormal dot (hudPoint - position) / hudNormal dot direction // intersection = postion + scale*direction - var hudNormal = Quat.getForward(Camera.getOrientation()); + var hudNormal = Quat.getFront(Camera.getOrientation()); var hudPoint = Vec3.sum(Camera.getPosition(), hudNormal); // must also scale if PLANAR_PERPENDICULAR_HUD_DISTANCE!=1 var denominator = Vec3.dot(hudNormal, direction); if (denominator === 0) { diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index 1c6c9af272..c058f046db 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -85,7 +85,6 @@ function Trigger(hand) { } var coolInTimeout = null; -var ignoredEntities = []; var TELEPORTER_STATES = { IDLE: 'idle', @@ -240,11 +239,11 @@ function Teleporter() { // We might hit an invisible entity that is not a seat, so we need to do a second pass. // * In the second pass we pick against visible entities only. // - var intersection = Entities.findRayIntersection(pickRay, true, [], [this.targetEntity].concat(ignoredEntities), false, true); + var intersection = Entities.findRayIntersection(pickRay, true, [], [this.targetEntity], false, true); var teleportLocationType = getTeleportTargetType(intersection); if (teleportLocationType === TARGET.INVISIBLE) { - intersection = Entities.findRayIntersection(pickRay, true, [], [this.targetEntity].concat(ignoredEntities), true, true); + intersection = Entities.findRayIntersection(pickRay, true, [], [this.targetEntity], true, true); teleportLocationType = getTeleportTargetType(intersection); } @@ -514,7 +513,7 @@ function cleanup() { Script.scriptEnding.connect(cleanup); var isDisabled = false; -var handleTeleportMessages = function(channel, message, sender) { +var handleHandMessages = function(channel, message, sender) { var data; if (sender === MyAvatar.sessionUUID) { if (channel === 'Hifi-Teleport-Disabler') { @@ -530,20 +529,12 @@ var handleTeleportMessages = function(channel, message, sender) { if (message === 'none') { isDisabled = false; } - } else if (channel === 'Hifi-Teleport-Ignore-Add' && !Uuid.isNull(message) && ignoredEntities.indexOf(message) === -1) { - ignoredEntities.push(message); - } else if (channel === 'Hifi-Teleport-Ignore-Remove' && !Uuid.isNull(message)) { - var removeIndex = ignoredEntities.indexOf(message); - if (removeIndex > -1) { - ignoredEntities.splice(removeIndex, 1); - } + } } } Messages.subscribe('Hifi-Teleport-Disabler'); -Messages.subscribe('Hifi-Teleport-Ignore-Add'); -Messages.subscribe('Hifi-Teleport-Ignore-Remove'); -Messages.messageReceived.connect(handleTeleportMessages); +Messages.messageReceived.connect(handleHandMessages); }()); // END LOCAL_SCOPE diff --git a/scripts/system/controllers/toggleAdvancedMovementForHandControllers.js b/scripts/system/controllers/toggleAdvancedMovementForHandControllers.js index e6c9b0aee0..46464dc2e1 100644 --- a/scripts/system/controllers/toggleAdvancedMovementForHandControllers.js +++ b/scripts/system/controllers/toggleAdvancedMovementForHandControllers.js @@ -17,14 +17,15 @@ var mappingName, basicMapping, isChecked; var TURN_RATE = 1000; var MENU_ITEM_NAME = "Advanced Movement For Hand Controllers"; +var SETTINGS_KEY = 'advancedMovementForHandControllersIsChecked'; var isDisabled = false; -var previousSetting = MyAvatar.useAdvancedMovementControls; -if (previousSetting === false) { +var previousSetting = Settings.getValue(SETTINGS_KEY); +if (previousSetting === '' || previousSetting === false || previousSetting === 'false') { previousSetting = false; isChecked = false; } -if (previousSetting === true) { +if (previousSetting === true || previousSetting === 'true') { previousSetting = true; isChecked = true; } @@ -36,6 +37,7 @@ function addAdvancedMovementItemToSettingsMenu() { isCheckable: true, isChecked: previousSetting }); + } function rotate180() { @@ -70,6 +72,7 @@ function registerBasicMapping() { } return; }); + basicMapping.from(Controller.Standard.LX).to(Controller.Standard.RX); basicMapping.from(Controller.Standard.RY).to(function(value) { if (isDisabled) { return; @@ -109,10 +112,10 @@ function menuItemEvent(menuItem) { if (menuItem == MENU_ITEM_NAME) { isChecked = Menu.isOptionChecked(MENU_ITEM_NAME); if (isChecked === true) { - MyAvatar.useAdvancedMovementControls = true; + Settings.setValue(SETTINGS_KEY, true); disableMappings(); } else if (isChecked === false) { - MyAvatar.useAdvancedMovementControls = false; + Settings.setValue(SETTINGS_KEY, false); enableMappings(); } } diff --git a/scripts/system/edit.js b/scripts/system/edit.js index 8b02eb1550..a440fec1ac 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -33,27 +33,13 @@ Script.include([ "libraries/gridTool.js", "libraries/entityList.js", "particle_explorer/particleExplorerTool.js", - "libraries/entityIconOverlayManager.js" + "libraries/lightOverlayManager.js" ]); var selectionDisplay = SelectionDisplay; var selectionManager = SelectionManager; -const PARTICLE_SYSTEM_URL = Script.resolvePath("assets/images/icon-particles.svg"); -const POINT_LIGHT_URL = Script.resolvePath("assets/images/icon-point-light.svg"); -const SPOT_LIGHT_URL = Script.resolvePath("assets/images/icon-spot-light.svg"); -entityIconOverlayManager = new EntityIconOverlayManager(['Light', 'ParticleEffect'], function(entityID) { - var properties = Entities.getEntityProperties(entityID, ['type', 'isSpotlight']); - if (properties.type === 'Light') { - return { - url: properties.isSpotlight ? SPOT_LIGHT_URL : POINT_LIGHT_URL, - } - } else { - return { - url: PARTICLE_SYSTEM_URL, - } - } -}); +var lightOverlayManager = new LightOverlayManager(); var cameraManager = new CameraManager(); @@ -67,45 +53,7 @@ var entityListTool = new EntityListTool(); selectionManager.addEventListener(function () { selectionDisplay.updateHandles(); - entityIconOverlayManager.updatePositions(); - - // Update particle explorer - var needToDestroyParticleExplorer = false; - if (selectionManager.selections.length === 1) { - var selectedEntityID = selectionManager.selections[0]; - if (selectedEntityID === selectedParticleEntityID) { - return; - } - var type = Entities.getEntityProperties(selectedEntityID, "type").type; - if (type === "ParticleEffect") { - // Destroy the old particles web view first - particleExplorerTool.destroyWebView(); - particleExplorerTool.createWebView(); - var properties = Entities.getEntityProperties(selectedEntityID); - var particleData = { - messageType: "particle_settings", - currentProperties: properties - }; - selectedParticleEntityID = selectedEntityID; - particleExplorerTool.setActiveParticleEntity(selectedParticleEntityID); - - particleExplorerTool.webView.webEventReceived.connect(function (data) { - data = JSON.parse(data); - if (data.messageType === "page_loaded") { - particleExplorerTool.webView.emitScriptEvent(JSON.stringify(particleData)); - } - }); - } else { - needToDestroyParticleExplorer = true; - } - } else { - needToDestroyParticleExplorer = true; - } - - if (needToDestroyParticleExplorer && selectedParticleEntityID !== null) { - selectedParticleEntityID = null; - particleExplorerTool.destroyWebView(); - } + lightOverlayManager.updatePositions(); }); const KEY_P = 80; //Key code for letter p used for Parenting hotkey. @@ -134,13 +82,13 @@ var DEFAULT_LIGHT_DIMENSIONS = Vec3.multiply(20, DEFAULT_DIMENSIONS); var MENU_AUTO_FOCUS_ON_SELECT = "Auto Focus on Select"; var MENU_EASE_ON_FOCUS = "Ease Orientation on Focus"; -var MENU_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE = "Show Lights and Particle Systems in Edit Mode"; +var MENU_SHOW_LIGHTS_IN_EDIT_MODE = "Show Lights in Edit Mode"; var MENU_SHOW_ZONES_IN_EDIT_MODE = "Show Zones in Edit Mode"; var SETTING_INSPECT_TOOL_ENABLED = "inspectToolEnabled"; var SETTING_AUTO_FOCUS_ON_SELECT = "autoFocusOnSelect"; var SETTING_EASE_ON_FOCUS = "cameraEaseOnFocus"; -var SETTING_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE = "showLightsAndParticlesInEditMode"; +var SETTING_SHOW_LIGHTS_IN_EDIT_MODE = "showLightsInEditMode"; var SETTING_SHOW_ZONES_IN_EDIT_MODE = "showZonesInEditMode"; @@ -558,7 +506,7 @@ var toolBar = (function () { toolBar.writeProperty("shown", false); toolBar.writeProperty("shown", true); } - entityIconOverlayManager.setVisible(isActive && Menu.isOptionChecked(MENU_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE)); + lightOverlayManager.setVisible(isActive && Menu.isOptionChecked(MENU_SHOW_LIGHTS_IN_EDIT_MODE)); Entities.setDrawZoneBoundaries(isActive && Menu.isOptionChecked(MENU_SHOW_ZONES_IN_EDIT_MODE)); }; @@ -623,8 +571,8 @@ function findClickedEntity(event) { } var entityResult = Entities.findRayIntersection(pickRay, true); // want precision picking - var iconResult = entityIconOverlayManager.findRayIntersection(pickRay); - iconResult.accurate = true; + var lightResult = lightOverlayManager.findRayIntersection(pickRay); + lightResult.accurate = true; if (pickZones) { Entities.setZonesArePickable(false); @@ -632,12 +580,18 @@ function findClickedEntity(event) { var result; - if (iconResult.intersects) { - result = iconResult; - } else if (entityResult.intersects) { - result = entityResult; - } else { + if (!entityResult.intersects && !lightResult.intersects) { return null; + } else if (entityResult.intersects && !lightResult.intersects) { + result = entityResult; + } else if (!entityResult.intersects && lightResult.intersects) { + result = lightResult; + } else { + if (entityResult.distance < lightResult.distance) { + result = entityResult; + } else { + result = lightResult; + } } if (!result.accurate) { @@ -816,7 +770,7 @@ function mouseClickEvent(event) { if (0 < x && sizeOK) { selectedEntityID = foundEntity; orientation = MyAvatar.orientation; - intersection = rayPlaneIntersection(pickRay, P, Quat.getForward(orientation)); + intersection = rayPlaneIntersection(pickRay, P, Quat.getFront(orientation)); if (!event.isShifted) { @@ -991,18 +945,18 @@ function setupModelMenus() { }); Menu.addMenuItem({ menuName: "Edit", - menuItemName: MENU_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE, + menuItemName: MENU_SHOW_LIGHTS_IN_EDIT_MODE, afterItem: MENU_EASE_ON_FOCUS, isCheckable: true, - isChecked: Settings.getValue(SETTING_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE) !== "false", + isChecked: Settings.getValue(SETTING_SHOW_LIGHTS_IN_EDIT_MODE) === "true", grouping: "Advanced" }); Menu.addMenuItem({ menuName: "Edit", menuItemName: MENU_SHOW_ZONES_IN_EDIT_MODE, - afterItem: MENU_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE, + afterItem: MENU_SHOW_LIGHTS_IN_EDIT_MODE, isCheckable: true, - isChecked: Settings.getValue(SETTING_SHOW_ZONES_IN_EDIT_MODE) !== "false", + isChecked: Settings.getValue(SETTING_SHOW_ZONES_IN_EDIT_MODE) === "true", grouping: "Advanced" }); @@ -1033,7 +987,7 @@ function cleanupModelMenus() { Menu.removeMenuItem("Edit", MENU_AUTO_FOCUS_ON_SELECT); Menu.removeMenuItem("Edit", MENU_EASE_ON_FOCUS); - Menu.removeMenuItem("Edit", MENU_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE); + Menu.removeMenuItem("Edit", MENU_SHOW_LIGHTS_IN_EDIT_MODE); Menu.removeMenuItem("Edit", MENU_SHOW_ZONES_IN_EDIT_MODE); } @@ -1041,7 +995,7 @@ Script.scriptEnding.connect(function () { toolBar.setActive(false); Settings.setValue(SETTING_AUTO_FOCUS_ON_SELECT, Menu.isOptionChecked(MENU_AUTO_FOCUS_ON_SELECT)); Settings.setValue(SETTING_EASE_ON_FOCUS, Menu.isOptionChecked(MENU_EASE_ON_FOCUS)); - Settings.setValue(SETTING_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE, Menu.isOptionChecked(MENU_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE)); + Settings.setValue(SETTING_SHOW_LIGHTS_IN_EDIT_MODE, Menu.isOptionChecked(MENU_SHOW_LIGHTS_IN_EDIT_MODE)); Settings.setValue(SETTING_SHOW_ZONES_IN_EDIT_MODE, Menu.isOptionChecked(MENU_SHOW_ZONES_IN_EDIT_MODE)); progressDialog.cleanup(); @@ -1230,7 +1184,7 @@ function parentSelectedEntities() { } function deleteSelectedEntities() { if (SelectionManager.hasSelection()) { - selectedParticleEntityID = null; + selectedParticleEntity = 0; particleExplorerTool.destroyWebView(); SelectionManager.saveProperties(); var savedProperties = []; @@ -1329,8 +1283,8 @@ function handeMenuEvent(menuItem) { selectAllEtitiesInCurrentSelectionBox(false); } else if (menuItem === "Select All Entities Touching Box") { selectAllEtitiesInCurrentSelectionBox(true); - } else if (menuItem === MENU_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE) { - entityIconOverlayManager.setVisible(isActive && Menu.isOptionChecked(MENU_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE)); + } else if (menuItem === MENU_SHOW_LIGHTS_IN_EDIT_MODE) { + lightOverlayManager.setVisible(isActive && Menu.isOptionChecked(MENU_SHOW_LIGHTS_IN_EDIT_MODE)); } else if (menuItem === MENU_SHOW_ZONES_IN_EDIT_MODE) { Entities.setDrawZoneBoundaries(isActive && Menu.isOptionChecked(MENU_SHOW_ZONES_IN_EDIT_MODE)); } @@ -1338,12 +1292,12 @@ function handeMenuEvent(menuItem) { } function getPositionToCreateEntity() { var HALF_TREE_SCALE = 16384; - var direction = Quat.getForward(MyAvatar.orientation); + var direction = Quat.getFront(MyAvatar.orientation); var distance = 1; var position = Vec3.sum(MyAvatar.position, Vec3.multiply(direction, distance)); if (Camera.mode === "entity" || Camera.mode === "independent") { - position = Vec3.sum(Camera.position, Vec3.multiply(Quat.getForward(Camera.orientation), distance)) + position = Vec3.sum(Camera.position, Vec3.multiply(Quat.getFront(Camera.orientation), distance)) } position.y += 0.5; if (position.x > HALF_TREE_SCALE || position.y > HALF_TREE_SCALE || position.z > HALF_TREE_SCALE) { @@ -1355,13 +1309,13 @@ function getPositionToCreateEntity() { function getPositionToImportEntity() { var dimensions = Clipboard.getContentsDimensions(); var HALF_TREE_SCALE = 16384; - var direction = Quat.getForward(MyAvatar.orientation); + var direction = Quat.getFront(MyAvatar.orientation); var longest = 1; longest = Math.sqrt(Math.pow(dimensions.x, 2) + Math.pow(dimensions.z, 2)); var position = Vec3.sum(MyAvatar.position, Vec3.multiply(direction, longest)); if (Camera.mode === "entity" || Camera.mode === "independent") { - position = Vec3.sum(Camera.position, Vec3.multiply(Quat.getForward(Camera.orientation), longest)) + position = Vec3.sum(Camera.position, Vec3.multiply(Quat.getFront(Camera.orientation), longest)) } if (position.x > HALF_TREE_SCALE || position.y > HALF_TREE_SCALE || position.z > HALF_TREE_SCALE) { @@ -2005,13 +1959,43 @@ var showMenuItem = propertyMenu.addMenuItem("Show in Marketplace"); var propertiesTool = new PropertiesTool(); var particleExplorerTool = new ParticleExplorerTool(); -var selectedParticleEntityID = null; +var selectedParticleEntity = 0; entityListTool.webView.webEventReceived.connect(function (data) { data = JSON.parse(data); - if (data.type === 'parent') { + if(data.type === 'parent') { parentSelectedEntities(); } else if(data.type === 'unparent') { unparentSelectedEntities(); + } else if (data.type === "selectionUpdate") { + var ids = data.entityIds; + if (ids.length === 1) { + if (Entities.getEntityProperties(ids[0], "type").type === "ParticleEffect") { + if (JSON.stringify(selectedParticleEntity) === JSON.stringify(ids[0])) { + // This particle entity is already selected, so return + return; + } + // Destroy the old particles web view first + particleExplorerTool.destroyWebView(); + particleExplorerTool.createWebView(); + var properties = Entities.getEntityProperties(ids[0]); + var particleData = { + messageType: "particle_settings", + currentProperties: properties + }; + selectedParticleEntity = ids[0]; + particleExplorerTool.setActiveParticleEntity(ids[0]); + + particleExplorerTool.webView.webEventReceived.connect(function (data) { + data = JSON.parse(data); + if (data.messageType === "page_loaded") { + particleExplorerTool.webView.emitScriptEvent(JSON.stringify(particleData)); + } + }); + } else { + selectedParticleEntity = 0; + particleExplorerTool.destroyWebView(); + } + } } }); diff --git a/scripts/system/libraries/WebTablet.js b/scripts/system/libraries/WebTablet.js index 32f45188dc..dd2aaf346b 100644 --- a/scripts/system/libraries/WebTablet.js +++ b/scripts/system/libraries/WebTablet.js @@ -78,9 +78,9 @@ function calcSpawnInfo(hand, height) { rotation: lookAtRot }; } else { - var forward = Quat.getForward(headRot); - finalPosition = Vec3.sum(headPos, Vec3.multiply(0.6, forward)); - var orientation = Quat.lookAt({x: 0, y: 0, z: 0}, forward, {x: 0, y: 1, z: 0}); + var front = Quat.getFront(headRot); + finalPosition = Vec3.sum(headPos, Vec3.multiply(0.6, front)); + var orientation = Quat.lookAt({x: 0, y: 0, z: 0}, front, {x: 0, y: 1, z: 0}); return { position: finalPosition, rotation: Quat.multiply(orientation, {x: 0, y: 1, z: 0, w: 0}) diff --git a/scripts/system/libraries/entityCameraTool.js b/scripts/system/libraries/entityCameraTool.js index 6becc81d9b..301b60f550 100644 --- a/scripts/system/libraries/entityCameraTool.js +++ b/scripts/system/libraries/entityCameraTool.js @@ -158,7 +158,7 @@ CameraManager = function() { that.zoomDistance = INITIAL_ZOOM_DISTANCE; that.targetZoomDistance = that.zoomDistance + 3.0; var focalPoint = Vec3.sum(Camera.getPosition(), - Vec3.multiply(that.zoomDistance, Quat.getForward(Camera.getOrientation()))); + Vec3.multiply(that.zoomDistance, Quat.getFront(Camera.getOrientation()))); // Determine the correct yaw and pitch to keep the camera in the same location var dPos = Vec3.subtract(focalPoint, Camera.getPosition()); @@ -435,7 +435,7 @@ CameraManager = function() { }); var q = Quat.multiply(yRot, xRot); - var pos = Vec3.multiply(Quat.getForward(q), that.zoomDistance); + var pos = Vec3.multiply(Quat.getFront(q), that.zoomDistance); Camera.setPosition(Vec3.sum(that.focalPoint, pos)); yRot = Quat.angleAxis(that.yaw - 180, { diff --git a/scripts/system/libraries/entitySelectionTool.js b/scripts/system/libraries/entitySelectionTool.js index 9d4bf7d9a8..d68a525458 100644 --- a/scripts/system/libraries/entitySelectionTool.js +++ b/scripts/system/libraries/entitySelectionTool.js @@ -1032,12 +1032,10 @@ SelectionDisplay = (function() { var pickRay = controllerComputePickRay(); if (pickRay) { var entityIntersection = Entities.findRayIntersection(pickRay, true); - var iconIntersection = entityIconOverlayManager.findRayIntersection(pickRay); - var overlayIntersection = Overlays.findRayIntersection(pickRay); - if (iconIntersection.intersects) { - selectionManager.setSelections([iconIntersection.entityID]); - } else if (entityIntersection.intersects && + + var overlayIntersection = Overlays.findRayIntersection(pickRay); + if (entityIntersection.intersects && (!overlayIntersection.intersects || (entityIntersection.distance < overlayIntersection.distance))) { if (HMD.tabletID === entityIntersection.entityID) { @@ -2517,7 +2515,7 @@ SelectionDisplay = (function() { onBegin: function(event) { pickRay = generalComputePickRay(event.x, event.y); - upDownPickNormal = Quat.getForward(lastCameraOrientation); + upDownPickNormal = Quat.getFront(lastCameraOrientation); // Remove y component so the y-axis lies along the plane we picking on - this will // give movements that follow the mouse. upDownPickNormal.y = 0; diff --git a/scripts/system/libraries/entityIconOverlayManager.js b/scripts/system/libraries/lightOverlayManager.js similarity index 67% rename from scripts/system/libraries/entityIconOverlayManager.js rename to scripts/system/libraries/lightOverlayManager.js index 7f7a293bc3..2d3618096b 100644 --- a/scripts/system/libraries/entityIconOverlayManager.js +++ b/scripts/system/libraries/lightOverlayManager.js @@ -1,6 +1,9 @@ -/* globals EntityIconOverlayManager:true */ +var POINT_LIGHT_URL = "http://s3.amazonaws.com/hifi-public/images/tools/point-light.svg"; +var SPOT_LIGHT_URL = "http://s3.amazonaws.com/hifi-public/images/tools/spot-light.svg"; + +LightOverlayManager = function() { + var self = this; -EntityIconOverlayManager = function(entityTypes, getOverlayPropertiesFunc) { var visible = false; // List of all created overlays @@ -19,16 +22,9 @@ EntityIconOverlayManager = function(entityTypes, getOverlayPropertiesFunc) { for (var id in entityIDs) { var entityID = entityIDs[id]; var properties = Entities.getEntityProperties(entityID); - var overlayProperties = { + Overlays.editOverlay(entityOverlays[entityID], { position: properties.position - }; - if (getOverlayPropertiesFunc) { - var customProperties = getOverlayPropertiesFunc(entityID, properties); - for (var key in customProperties) { - overlayProperties[key] = customProperties[key]; - } - } - Overlays.editOverlay(entityOverlays[entityID], overlayProperties); + }); } }; @@ -38,7 +34,7 @@ EntityIconOverlayManager = function(entityTypes, getOverlayPropertiesFunc) { if (result.intersects) { for (var id in entityOverlays) { - if (result.overlayID === entityOverlays[id]) { + if (result.overlayID == entityOverlays[id]) { result.entityID = entityIDs[id]; found = true; break; @@ -54,7 +50,7 @@ EntityIconOverlayManager = function(entityTypes, getOverlayPropertiesFunc) { }; this.setVisible = function(isVisible) { - if (visible !== isVisible) { + if (visible != isVisible) { visible = isVisible; for (var id in entityOverlays) { Overlays.editOverlay(entityOverlays[id], { @@ -66,13 +62,12 @@ EntityIconOverlayManager = function(entityTypes, getOverlayPropertiesFunc) { // Allocate or get an unused overlay function getOverlay() { - var overlay; - if (unusedOverlays.length === 0) { - overlay = Overlays.addOverlay("image3d", {}); + if (unusedOverlays.length == 0) { + var overlay = Overlays.addOverlay("image3d", {}); allOverlays.push(overlay); } else { - overlay = unusedOverlays.pop(); - } + var overlay = unusedOverlays.pop(); + }; return overlay; } @@ -84,32 +79,24 @@ EntityIconOverlayManager = function(entityTypes, getOverlayPropertiesFunc) { } function addEntity(entityID) { - var properties = Entities.getEntityProperties(entityID, ['position', 'type']); - if (entityTypes.indexOf(properties.type) > -1 && !(entityID in entityOverlays)) { + var properties = Entities.getEntityProperties(entityID); + if (properties.type == "Light" && !(entityID in entityOverlays)) { var overlay = getOverlay(); entityOverlays[entityID] = overlay; entityIDs[entityID] = entityID; - var overlayProperties = { + Overlays.editOverlay(overlay, { position: properties.position, + url: properties.isSpotlight ? SPOT_LIGHT_URL : POINT_LIGHT_URL, rotation: Quat.fromPitchYawRollDegrees(0, 0, 270), visible: visible, alpha: 0.9, scale: 0.5, - drawInFront: true, - isFacingAvatar: true, color: { red: 255, green: 255, blue: 255 } - }; - if (getOverlayPropertiesFunc) { - var customProperties = getOverlayPropertiesFunc(entityID, properties); - for (var key in customProperties) { - overlayProperties[key] = customProperties[key]; - } - } - Overlays.editOverlay(overlay, overlayProperties); + }); } } @@ -143,4 +130,4 @@ EntityIconOverlayManager = function(entityTypes, getOverlayPropertiesFunc) { Overlays.deleteOverlay(allOverlays[i]); } }); -}; +}; \ No newline at end of file diff --git a/scripts/system/libraries/soundArray.js b/scripts/system/libraries/soundArray.js index 7e5da11948..f59c88a723 100644 --- a/scripts/system/libraries/soundArray.js +++ b/scripts/system/libraries/soundArray.js @@ -36,7 +36,7 @@ SoundArray = function(audioOptions, autoUpdateAudioPosition) { }; this.updateAudioPosition = function() { var position = MyAvatar.position; - var forwardVector = Quat.getForward(MyAvatar.orientation); + var forwardVector = Quat.getFront(MyAvatar.orientation); this.audioOptions.position = Vec3.sum(position, forwardVector); }; }; diff --git a/scripts/system/libraries/toolBars.js b/scripts/system/libraries/toolBars.js index 351f10e7bd..e49f8c4004 100644 --- a/scripts/system/libraries/toolBars.js +++ b/scripts/system/libraries/toolBars.js @@ -160,7 +160,6 @@ ToolBar = function(x, y, direction, optionalPersistenceKey, optionalInitialPosit visible: false }); this.spacing = []; - this.onMove = null; this.addTool = function(properties, selectable, selected) { if (direction == ToolBar.HORIZONTAL) { @@ -255,9 +254,6 @@ ToolBar = function(x, y, direction, optionalPersistenceKey, optionalInitialPosit y: y - ToolBar.SPACING }); } - if (this.onMove !== null) { - this.onMove(x, y, dx, dy); - }; } this.setAlpha = function(alpha, tool) { diff --git a/scripts/system/nameTag.js b/scripts/system/nameTag.js index 17944bcf85..e25db69064 100644 --- a/scripts/system/nameTag.js +++ b/scripts/system/nameTag.js @@ -33,7 +33,7 @@ Script.setTimeout(function() { }, STARTUP_DELAY); function addNameTag() { - var nameTagPosition = Vec3.sum(MyAvatar.getHeadPosition(), Vec3.multiply(HEAD_OFFSET, Quat.getForward(MyAvatar.orientation))); + var nameTagPosition = Vec3.sum(MyAvatar.getHeadPosition(), Vec3.multiply(HEAD_OFFSET, Quat.getFront(MyAvatar.orientation))); nameTagPosition.y += HEIGHT_ABOVE_HEAD; var nameTagProperties = { name: MyAvatar.displayName + ' Name Tag', @@ -49,7 +49,7 @@ function addNameTag() { function updateNameTag() { var nameTagProps = Entities.getEntityProperties(nameTagEntityID); - var nameTagPosition = Vec3.sum(MyAvatar.getHeadPosition(), Vec3.multiply(HEAD_OFFSET, Quat.getForward(MyAvatar.orientation))); + var nameTagPosition = Vec3.sum(MyAvatar.getHeadPosition(), Vec3.multiply(HEAD_OFFSET, Quat.getFront(MyAvatar.orientation))); nameTagPosition.y += HEIGHT_ABOVE_HEAD; Entities.editEntity(nameTagEntityID, { diff --git a/scripts/system/pal.js b/scripts/system/pal.js index a7c4f56ea6..d9734850e5 100644 --- a/scripts/system/pal.js +++ b/scripts/system/pal.js @@ -444,7 +444,7 @@ function populateNearbyUserList(selectData, oldAudioData) { verticalHalfAngle = filter && (frustum.fieldOfView / 2), horizontalHalfAngle = filter && (verticalHalfAngle * frustum.aspectRatio), orientation = filter && Camera.orientation, - forward = filter && Quat.getForward(orientation), + front = filter && Quat.getFront(orientation), verticalAngleNormal = filter && Quat.getRight(orientation), horizontalAngleNormal = filter && Quat.getUp(orientation); avatarsOfInterest = {}; @@ -463,8 +463,8 @@ function populateNearbyUserList(selectData, oldAudioData) { return; } var normal = id && filter && Vec3.normalize(Vec3.subtract(avatar.position, myPosition)); - var horizontal = normal && angleBetweenVectorsInPlane(normal, forward, horizontalAngleNormal); - var vertical = normal && angleBetweenVectorsInPlane(normal, forward, verticalAngleNormal); + var horizontal = normal && angleBetweenVectorsInPlane(normal, front, horizontalAngleNormal); + var vertical = normal && angleBetweenVectorsInPlane(normal, front, verticalAngleNormal); if (id && filter && ((Math.abs(horizontal) > horizontalHalfAngle) || (Math.abs(vertical) > verticalHalfAngle))) { return; } diff --git a/scripts/system/voxels.js b/scripts/system/voxels.js index 2f1d0eced9..3c219ebc7a 100644 --- a/scripts/system/voxels.js +++ b/scripts/system/voxels.js @@ -253,7 +253,7 @@ function addTerrainBlock() { if (alreadyThere) { // there is already a terrain block under MyAvatar. // try in front of the avatar. - facingPosition = Vec3.sum(MyAvatar.position, Vec3.multiply(8.0, Quat.getForward(Camera.getOrientation()))); + facingPosition = Vec3.sum(MyAvatar.position, Vec3.multiply(8.0, Quat.getFront(Camera.getOrientation()))); facingPosition = Vec3.sum(facingPosition, { x: 8, y: 8, diff --git a/scripts/tutorials/NBody/makePlanets.js b/scripts/tutorials/NBody/makePlanets.js index 21415ccdc2..58a3c7cc2d 100644 --- a/scripts/tutorials/NBody/makePlanets.js +++ b/scripts/tutorials/NBody/makePlanets.js @@ -53,7 +53,7 @@ var deleteButton = toolBar.addOverlay("image", { }); function inFrontOfMe(distance) { - return Vec3.sum(Camera.getPosition(), Vec3.multiply(distance, Quat.getForward(Camera.getOrientation()))); + return Vec3.sum(Camera.getPosition(), Vec3.multiply(distance, Quat.getFront(Camera.getOrientation()))); } function onButtonClick() { diff --git a/scripts/tutorials/butterflies.js b/scripts/tutorials/butterflies.js index 9d8d1de52c..55bafc0a27 100644 --- a/scripts/tutorials/butterflies.js +++ b/scripts/tutorials/butterflies.js @@ -44,8 +44,8 @@ var FIXED_LOCATION = false; if (!FIXED_LOCATION) { var flockPosition = Vec3.sum(MyAvatar.position,Vec3.sum( - Vec3.multiply(Quat.getForward(MyAvatar.orientation), DISTANCE_ABOVE_ME), - Vec3.multiply(Quat.getForward(MyAvatar.orientation), DISTANCE_IN_FRONT_OF_ME))); + Vec3.multiply(Quat.getFront(MyAvatar.orientation), DISTANCE_ABOVE_ME), + Vec3.multiply(Quat.getFront(MyAvatar.orientation), DISTANCE_IN_FRONT_OF_ME))); } else { var flockPosition = { x: 4999.6, y: 4986.5, z: 5003.5 }; } @@ -119,7 +119,7 @@ function updateButterflies(deltaTime) { var HORIZ_SCALE = 0.50; var VERT_SCALE = 0.50; var newHeading = Math.random() * 360.0; - var newVelocity = Vec3.multiply(HORIZ_SCALE, Quat.getForward(Quat.fromPitchYawRollDegrees(0.0, newHeading, 0.0))); + var newVelocity = Vec3.multiply(HORIZ_SCALE, Quat.getFront(Quat.fromPitchYawRollDegrees(0.0, newHeading, 0.0))); newVelocity.y = (Math.random() + 0.5) * VERT_SCALE; Entities.editEntity(butterflies[i], { rotation: Quat.fromPitchYawRollDegrees(-80 + Math.random() * 20, newHeading, (Math.random() - 0.5) * 10), velocity: newVelocity } ); diff --git a/scripts/tutorials/createCow.js b/scripts/tutorials/createCow.js index 16498e0e8c..7446aa0fd0 100644 --- a/scripts/tutorials/createCow.js +++ b/scripts/tutorials/createCow.js @@ -18,7 +18,7 @@ var orientation = MyAvatar.orientation; orientation = Quat.safeEulerAngles(orientation); orientation.x = 0; orientation = Quat.fromVec3Degrees(orientation); -var center = Vec3.sum(MyAvatar.getHeadPosition(), Vec3.multiply(2, Quat.getForward(orientation))); +var center = Vec3.sum(MyAvatar.getHeadPosition(), Vec3.multiply(2, Quat.getFront(orientation))); // An entity is described and created by specifying a map of properties var cow = Entities.addEntity({ diff --git a/scripts/tutorials/createDice.js b/scripts/tutorials/createDice.js index 46ad0172aa..0d39d11d48 100644 --- a/scripts/tutorials/createDice.js +++ b/scripts/tutorials/createDice.js @@ -127,8 +127,8 @@ function mousePressEvent(event) { deleteDice(); } else if (clickedOverlay == diceButton) { var HOW_HARD = 2.0; - var position = Vec3.sum(Camera.getPosition(), Quat.getForward(Camera.getOrientation())); - var velocity = Vec3.multiply(HOW_HARD, Quat.getForward(Camera.getOrientation())); + var position = Vec3.sum(Camera.getPosition(), Quat.getFront(Camera.getOrientation())); + var velocity = Vec3.multiply(HOW_HARD, Quat.getFront(Camera.getOrientation())); shootDice(position, velocity); madeSound = false; } diff --git a/scripts/tutorials/createFlashlight.js b/scripts/tutorials/createFlashlight.js index f3e1e72182..0e3581a435 100644 --- a/scripts/tutorials/createFlashlight.js +++ b/scripts/tutorials/createFlashlight.js @@ -16,7 +16,7 @@ var center = Vec3.sum(Vec3.sum(MyAvatar.position, { x: 0, y: 0.5, z: 0 -}), Vec3.multiply(0.5, Quat.getForward(Camera.getOrientation()))); +}), Vec3.multiply(0.5, Quat.getFront(Camera.getOrientation()))); var flashlight = Entities.addEntity({ type: "Model", diff --git a/scripts/tutorials/createGolfClub.js b/scripts/tutorials/createGolfClub.js index 21e60f26ef..aa9834276a 100644 --- a/scripts/tutorials/createGolfClub.js +++ b/scripts/tutorials/createGolfClub.js @@ -15,7 +15,7 @@ var orientation = MyAvatar.orientation; orientation = Quat.safeEulerAngles(orientation); orientation.x = 0; orientation = Quat.fromVec3Degrees(orientation); -var center = Vec3.sum(MyAvatar.getHeadPosition(), Vec3.multiply(2, Quat.getForward(orientation))); +var center = Vec3.sum(MyAvatar.getHeadPosition(), Vec3.multiply(2, Quat.getFront(orientation))); var CLUB_MODEL = "http://hifi-production.s3.amazonaws.com/tutorials/golfClub/putter_VR.fbx"; var CLUB_COLLISION_HULL = "http://hifi-production.s3.amazonaws.com/tutorials/golfClub/club_collision_hull.obj"; diff --git a/scripts/tutorials/createPictureFrame.js b/scripts/tutorials/createPictureFrame.js index 873b604bfa..4a1e5b16a7 100644 --- a/scripts/tutorials/createPictureFrame.js +++ b/scripts/tutorials/createPictureFrame.js @@ -14,7 +14,7 @@ var center = Vec3.sum(Vec3.sum(MyAvatar.position, { x: 0, y: 0.5, z: 0 -}), Vec3.multiply(1, Quat.getForward(Camera.getOrientation()))); +}), Vec3.multiply(1, Quat.getFront(Camera.getOrientation()))); // this is just a model exported from blender with a texture named 'Picture' on one face. also made it emissive so it doesn't require lighting. var MODEL_URL = "http://hifi-production.s3.amazonaws.com/tutorials/pictureFrame/finalFrame.fbx"; diff --git a/scripts/tutorials/createPingPongGun.js b/scripts/tutorials/createPingPongGun.js index c86a78e96d..a077e5308d 100644 --- a/scripts/tutorials/createPingPongGun.js +++ b/scripts/tutorials/createPingPongGun.js @@ -14,7 +14,7 @@ var center = Vec3.sum(Vec3.sum(MyAvatar.position, { x: 0, y: 0.5, z: 0 -}), Vec3.multiply(0.5, Quat.getForward(Camera.getOrientation()))); +}), Vec3.multiply(0.5, Quat.getFront(Camera.getOrientation()))); var pingPongGunProperties = { diff --git a/scripts/tutorials/createPistol.js b/scripts/tutorials/createPistol.js index 8851f53d09..ae2f398840 100644 --- a/scripts/tutorials/createPistol.js +++ b/scripts/tutorials/createPistol.js @@ -6,7 +6,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -var center = Vec3.sum(MyAvatar.position, Vec3.multiply(1.5, Quat.getForward(Camera.getOrientation()))); +var center = Vec3.sum(MyAvatar.position, Vec3.multiply(1.5, Quat.getFront(Camera.getOrientation()))); var SCRIPT_URL = "http://hifi-production.s3.amazonaws.com/tutorials/entity_scripts/pistol.js"; var MODEL_URL = "http://hifi-production.s3.amazonaws.com/tutorials/pistol/gun.fbx"; var COLLISION_SOUND_URL = 'http://hifi-production.s3.amazonaws.com/tutorials/pistol/drop.wav' diff --git a/scripts/tutorials/createSoundMaker.js b/scripts/tutorials/createSoundMaker.js index 2d86864982..b79c650e27 100644 --- a/scripts/tutorials/createSoundMaker.js +++ b/scripts/tutorials/createSoundMaker.js @@ -13,7 +13,7 @@ var center = Vec3.sum(Vec3.sum(MyAvatar.position, { x: 0, y: 0.5, z: 0 -}), Vec3.multiply(1, Quat.getForward(Camera.getOrientation()))); +}), Vec3.multiply(1, Quat.getFront(Camera.getOrientation()))); function makeBell() { var soundMakerProperties = { diff --git a/scripts/tutorials/entity_scripts/golfClub.js b/scripts/tutorials/entity_scripts/golfClub.js index 6342838aa4..2df3be8b60 100644 --- a/scripts/tutorials/entity_scripts/golfClub.js +++ b/scripts/tutorials/entity_scripts/golfClub.js @@ -57,7 +57,7 @@ // Position yourself facing in the direction you were originally facing, but with a // point on the ground *away* meters from *position* and in front of you. - var offset = Quat.getForward(MyAvatar.orientation); + var offset = Quat.getFront(MyAvatar.orientation); offset.y = 0.0; offset = Vec3.multiply(-away, Vec3.normalize(offset)); var newAvatarPosition = Vec3.sum(position, offset); @@ -72,7 +72,7 @@ } function inFrontOfMe() { - return Vec3.sum(MyAvatar.position, Vec3.multiply(BALL_DROP_DISTANCE, Quat.getForward(MyAvatar.orientation))); + return Vec3.sum(MyAvatar.position, Vec3.multiply(BALL_DROP_DISTANCE, Quat.getFront(MyAvatar.orientation))); } function avatarHalfHeight() { diff --git a/scripts/tutorials/entity_scripts/pingPongGun.js b/scripts/tutorials/entity_scripts/pingPongGun.js index 5ba4b15ea7..4ec0254747 100644 --- a/scripts/tutorials/entity_scripts/pingPongGun.js +++ b/scripts/tutorials/entity_scripts/pingPongGun.js @@ -94,9 +94,9 @@ }, shootBall: function(gunProperties) { - var forwardVector = Quat.getForward(Quat.multiply(gunProperties.rotation, Quat.fromPitchYawRollDegrees(0, 180, 0))); - forwardVector = Vec3.normalize(forwardVector); - forwardVector = Vec3.multiply(forwardVector, GUN_FORCE); + var forwardVec = Quat.getFront(Quat.multiply(gunProperties.rotation, Quat.fromPitchYawRollDegrees(0, 180, 0))); + forwardVec = Vec3.normalize(forwardVec); + forwardVec = Vec3.multiply(forwardVec, GUN_FORCE); var properties = { name: 'Tutorial Ping Pong Ball', @@ -111,7 +111,7 @@ rotation: gunProperties.rotation, position: this.getGunTipPosition(gunProperties), gravity: PING_PONG_GUN_GRAVITY, - velocity: forwardVector, + velocity: forwardVec, lifetime: 10 }; @@ -131,12 +131,12 @@ getGunTipPosition: function(properties) { //the tip of the gun is going to be in a different place than the center, so we move in space relative to the model to find that position - var forwardVector = Quat.getForward(properties.rotation); - var forwardOffset = Vec3.multiply(forwardVector, GUN_TIP_FWD_OFFSET); + var frontVector = Quat.getFront(properties.rotation); + var frontOffset = Vec3.multiply(frontVector, GUN_TIP_FWD_OFFSET); var upVector = Quat.getUp(properties.rotation); var upOffset = Vec3.multiply(upVector, GUN_TIP_UP_OFFSET); - var gunTipPosition = Vec3.sum(properties.position, forwardOffset); + var gunTipPosition = Vec3.sum(properties.position, frontOffset); gunTipPosition = Vec3.sum(gunTipPosition, upOffset); return gunTipPosition; diff --git a/scripts/tutorials/entity_scripts/pistol.js b/scripts/tutorials/entity_scripts/pistol.js index 38eb929177..73a6daab93 100644 --- a/scripts/tutorials/entity_scripts/pistol.js +++ b/scripts/tutorials/entity_scripts/pistol.js @@ -69,7 +69,7 @@ var gunProps = Entities.getEntityProperties(this.entityID, ['position', 'rotation']); this.position = gunProps.position; this.rotation = gunProps.rotation; - this.firingDirection = Quat.getForward(this.rotation); + this.firingDirection = Quat.getFront(this.rotation); var upVec = Quat.getUp(this.rotation); this.barrelPoint = Vec3.sum(this.position, Vec3.multiply(upVec, this.laserOffsets.y)); this.laserTip = Vec3.sum(this.barrelPoint, Vec3.multiply(this.firingDirection, this.laserLength)); diff --git a/scripts/tutorials/entity_scripts/sit.js b/scripts/tutorials/entity_scripts/sit.js index 82afdc8974..2ba19231e0 100644 --- a/scripts/tutorials/entity_scripts/sit.js +++ b/scripts/tutorials/entity_scripts/sit.js @@ -2,41 +2,31 @@ Script.include("/~/system/libraries/utils.js"); var SETTING_KEY = "com.highfidelity.avatar.isSitting"; + var ROLE = "fly"; var ANIMATION_URL = "https://s3-us-west-1.amazonaws.com/hifi-content/clement/production/animations/sitting_idle.fbx"; var ANIMATION_FPS = 30; var ANIMATION_FIRST_FRAME = 1; var ANIMATION_LAST_FRAME = 10; + var RELEASE_KEYS = ['w', 'a', 's', 'd', 'UP', 'LEFT', 'DOWN', 'RIGHT']; var RELEASE_TIME = 500; // ms var RELEASE_DISTANCE = 0.2; // meters - var MAX_IK_ERROR = 30; - var IK_SETTLE_TIME = 250; // ms - var DESKTOP_UI_CHECK_INTERVAL = 100; + var MAX_IK_ERROR = 20; + var DESKTOP_UI_CHECK_INTERVAL = 250; var DESKTOP_MAX_DISTANCE = 5; - var SIT_DELAY = 25; - var MAX_RESET_DISTANCE = 0.5; // meters - var OVERRIDEN_DRIVE_KEYS = [ - DriveKeys.TRANSLATE_X, - DriveKeys.TRANSLATE_Y, - DriveKeys.TRANSLATE_Z, - DriveKeys.STEP_TRANSLATE_X, - DriveKeys.STEP_TRANSLATE_Y, - DriveKeys.STEP_TRANSLATE_Z, - ]; + var SIT_DELAY = 25 this.entityID = null; + this.timers = {}; this.animStateHandlerID = null; - this.interval = null; - this.sitDownSettlePeriod = null; - this.lastTimeNoDriveKeys = null; this.preload = function(entityID) { this.entityID = entityID; } this.unload = function() { - if (Settings.getValue(SETTING_KEY) === this.entityID) { - this.standUp(); + if (MyAvatar.sessionUUID === this.getSeatUser()) { + this.sitUp(this.entityID); } - if (this.interval !== null) { + if (this.interval) { Script.clearInterval(this.interval); this.interval = null; } @@ -44,60 +34,42 @@ } this.setSeatUser = function(user) { - try { - var userData = Entities.getEntityProperties(this.entityID, ["userData"]).userData; - userData = JSON.parse(userData); + var userData = Entities.getEntityProperties(this.entityID, ["userData"]).userData; + userData = JSON.parse(userData); - if (user !== null) { - userData.seat.user = user; - } else { - delete userData.seat.user; - } - - Entities.editEntity(this.entityID, { - userData: JSON.stringify(userData) - }); - } catch (e) { - // Do Nothing + if (user) { + userData.seat.user = user; + } else { + delete userData.seat.user; } + + Entities.editEntity(this.entityID, { + userData: JSON.stringify(userData) + }); } this.getSeatUser = function() { - try { - var properties = Entities.getEntityProperties(this.entityID, ["userData", "position"]); - var userData = JSON.parse(properties.userData); + var properties = Entities.getEntityProperties(this.entityID, ["userData", "position"]); + var userData = JSON.parse(properties.userData); - // If MyAvatar return my uuid - if (userData.seat.user === MyAvatar.sessionUUID) { - return userData.seat.user; + if (userData.seat.user && userData.seat.user !== MyAvatar.sessionUUID) { + var avatar = AvatarList.getAvatar(userData.seat.user); + if (avatar && Vec3.distance(avatar.position, properties.position) > RELEASE_DISTANCE) { + return null; } - - - // If Avatar appears to be sitting - if (userData.seat.user) { - var avatar = AvatarList.getAvatar(userData.seat.user); - if (avatar && (Vec3.distance(avatar.position, properties.position) < RELEASE_DISTANCE)) { - return userData.seat.user; - } - } - } catch (e) { - // Do nothing } - - // Nobody on the seat - return null; + return userData.seat.user; } - // Is the seat used this.checkSeatForAvatar = function() { var seatUser = this.getSeatUser(); - - // If MyAvatar appears to be sitting - if (seatUser === MyAvatar.sessionUUID) { - var properties = Entities.getEntityProperties(this.entityID, ["position"]); - return Vec3.distance(MyAvatar.position, properties.position) < RELEASE_DISTANCE; + var avatarIdentifiers = AvatarList.getAvatarIdentifiers(); + for (var i in avatarIdentifiers) { + var avatar = AvatarList.getAvatar(avatarIdentifiers[i]); + if (avatar && avatar.sessionUUID === seatUser) { + return true; + } } - - return seatUser !== null; + return false; } this.sitDown = function() { @@ -105,53 +77,41 @@ print("Someone is already sitting in that chair."); return; } - print("Sitting down (" + this.entityID + ")"); - var now = Date.now(); - this.sitDownSettlePeriod = now + IK_SETTLE_TIME; - this.lastTimeNoDriveKeys = now; + this.setSeatUser(MyAvatar.sessionUUID); var previousValue = Settings.getValue(SETTING_KEY); Settings.setValue(SETTING_KEY, this.entityID); - this.setSeatUser(MyAvatar.sessionUUID); if (previousValue === "") { MyAvatar.characterControllerEnabled = false; MyAvatar.hmdLeanRecenterEnabled = false; - var ROLES = MyAvatar.getAnimationRoles(); - for (i in ROLES) { - MyAvatar.overrideRoleAnimation(ROLES[i], ANIMATION_URL, ANIMATION_FPS, true, ANIMATION_FIRST_FRAME, ANIMATION_LAST_FRAME); - } + MyAvatar.overrideRoleAnimation(ROLE, ANIMATION_URL, ANIMATION_FPS, true, ANIMATION_FIRST_FRAME, ANIMATION_LAST_FRAME); MyAvatar.resetSensorsAndBody(); } - var properties = Entities.getEntityProperties(this.entityID, ["position", "rotation"]); - var index = MyAvatar.getJointIndex("Hips"); - MyAvatar.pinJoint(index, properties.position, properties.rotation); + var that = this; + Script.setTimeout(function() { + var properties = Entities.getEntityProperties(that.entityID, ["position", "rotation"]); + var index = MyAvatar.getJointIndex("Hips"); + MyAvatar.pinJoint(index, properties.position, properties.rotation); - this.animStateHandlerID = MyAvatar.addAnimationStateHandler(function(properties) { - return { headType: 0 }; - }, ["headType"]); - Script.update.connect(this, this.update); - for (var i in OVERRIDEN_DRIVE_KEYS) { - MyAvatar.disableDriveKey(OVERRIDEN_DRIVE_KEYS[i]); - } + that.animStateHandlerID = MyAvatar.addAnimationStateHandler(function(properties) { + return { headType: 0 }; + }, ["headType"]); + Script.update.connect(that, that.update); + Controller.keyPressEvent.connect(that, that.keyPressed); + Controller.keyReleaseEvent.connect(that, that.keyReleased); + for (var i in RELEASE_KEYS) { + Controller.captureKeyEvents({ text: RELEASE_KEYS[i] }); + } + }, SIT_DELAY); } - this.standUp = function() { - print("Standing up (" + this.entityID + ")"); - MyAvatar.removeAnimationStateHandler(this.animStateHandlerID); - Script.update.disconnect(this, this.update); - for (var i in OVERRIDEN_DRIVE_KEYS) { - MyAvatar.enableDriveKey(OVERRIDEN_DRIVE_KEYS[i]); - } - + this.sitUp = function() { this.setSeatUser(null); + if (Settings.getValue(SETTING_KEY) === this.entityID) { - Settings.setValue(SETTING_KEY, ""); - var ROLES = MyAvatar.getAnimationRoles(); - for (i in ROLES) { - MyAvatar.restoreRoleAnimation(ROLES[i]); - } + MyAvatar.restoreRoleAnimation(ROLE); MyAvatar.characterControllerEnabled = true; MyAvatar.hmdLeanRecenterEnabled = true; @@ -164,10 +124,19 @@ MyAvatar.bodyPitch = 0.0; MyAvatar.bodyRoll = 0.0; }, SIT_DELAY); + + Settings.setValue(SETTING_KEY, ""); + } + + MyAvatar.removeAnimationStateHandler(this.animStateHandlerID); + Script.update.disconnect(this, this.update); + Controller.keyPressEvent.disconnect(this, this.keyPressed); + Controller.keyReleaseEvent.disconnect(this, this.keyReleased); + for (var i in RELEASE_KEYS) { + Controller.releaseKeyEvents({ text: RELEASE_KEYS[i] }); } } - // function called by teleport.js if it detects the appropriate userData this.sit = function () { this.sitDown(); } @@ -214,52 +183,39 @@ } } + this.update = function(dt) { if (MyAvatar.sessionUUID === this.getSeatUser()) { - var properties = Entities.getEntityProperties(this.entityID); + var properties = Entities.getEntityProperties(this.entityID, ["position"]); var avatarDistance = Vec3.distance(MyAvatar.position, properties.position); var ikError = MyAvatar.getIKErrorOnLastSolve(); - var now = Date.now(); - var shouldStandUp = false; - - // Check if a drive key is pressed - var hasActiveDriveKey = false; - for (var i in OVERRIDEN_DRIVE_KEYS) { - if (MyAvatar.getRawDriveKey(OVERRIDEN_DRIVE_KEYS[i]) != 0.0) { - hasActiveDriveKey = true; - break; - } - } - - // Only standup if user has been pushing a drive key for RELEASE_TIME - if (hasActiveDriveKey) { - var elapsed = now - this.lastTimeNoDriveKeys; - shouldStandUp = elapsed > RELEASE_TIME; - } else { - this.lastTimeNoDriveKeys = Date.now(); - } - - // Allow some time for the IK to settle - if (ikError > MAX_IK_ERROR && now > this.sitDownSettlePeriod) { - shouldStandUp = true; - } - - - if (shouldStandUp || avatarDistance > RELEASE_DISTANCE) { + if (avatarDistance > RELEASE_DISTANCE || ikError > MAX_IK_ERROR) { print("IK error: " + ikError + ", distance from chair: " + avatarDistance); - - // Move avatar in front of the chair to avoid getting stuck in collision hulls - if (avatarDistance < MAX_RESET_DISTANCE) { - var offset = { x: 0, y: 1.0, z: -0.5 - properties.dimensions.z * properties.registrationPoint.z }; - var position = Vec3.sum(properties.position, Vec3.multiplyQbyV(properties.rotation, offset)); - MyAvatar.position = position; - print("Moving Avatar in front of the chair.") - } - - this.standUp(); + this.sitUp(this.entityID); } } } + this.keyPressed = function(event) { + if (isInEditMode()) { + return; + } + + if (RELEASE_KEYS.indexOf(event.text) !== -1) { + var that = this; + this.timers[event.text] = Script.setTimeout(function() { + that.sitUp(); + }, RELEASE_TIME); + } + } + this.keyReleased = function(event) { + if (RELEASE_KEYS.indexOf(event.text) !== -1) { + if (this.timers[event.text]) { + Script.clearTimeout(this.timers[event.text]); + delete this.timers[event.text]; + } + } + } + this.canSitDesktop = function() { var properties = Entities.getEntityProperties(this.entityID, ["position"]); var distanceFromSeat = Vec3.distance(MyAvatar.position, properties.position); @@ -267,7 +223,7 @@ } this.hoverEnterEntity = function(event) { - if (isInEditMode() || this.interval !== null) { + if (isInEditMode() || (MyAvatar.sessionUUID === this.getSeatUser())) { return; } @@ -283,18 +239,18 @@ }, DESKTOP_UI_CHECK_INTERVAL); } this.hoverLeaveEntity = function(event) { - if (this.interval !== null) { + if (this.interval) { Script.clearInterval(this.interval); this.interval = null; } this.cleanupOverlay(); } - this.clickDownOnEntity = function (id, event) { - if (isInEditMode()) { + this.clickDownOnEntity = function () { + if (isInEditMode() || (MyAvatar.sessionUUID === this.getSeatUser())) { return; } - if (event.isPrimaryButton && this.canSitDesktop()) { + if (this.canSitDesktop()) { this.sitDown(); } } diff --git a/scripts/tutorials/makeBlocks.js b/scripts/tutorials/makeBlocks.js index 432f7444c4..54bdead792 100644 --- a/scripts/tutorials/makeBlocks.js +++ b/scripts/tutorials/makeBlocks.js @@ -34,12 +34,12 @@ var SCRIPT_URL = Script.resolvePath("./entity_scripts/magneticBlock.js"); - var forwardVector = Quat.getForward(MyAvatar.orientation); - forwardVector.y += VERTICAL_OFFSET; + var frontVector = Quat.getFront(MyAvatar.orientation); + frontVector.y += VERTICAL_OFFSET; for (var x = 0; x < COLUMNS; x++) { for (var y = 0; y < ROWS; y++) { - var forwardOffset = { + var frontOffset = { x: 0, y: SIZE * y + SIZE, z: SIZE * x + SIZE @@ -61,7 +61,7 @@ cloneLimit: 9999 } }), - position: Vec3.sum(MyAvatar.position, Vec3.sum(forwardOffset, forwardVector)), + position: Vec3.sum(MyAvatar.position, Vec3.sum(frontOffset, frontVector)), color: newColor(), script: SCRIPT_URL }); diff --git a/tests/ktx/CMakeLists.txt b/tests/ktx/CMakeLists.txt deleted file mode 100644 index d72379efd6..0000000000 --- a/tests/ktx/CMakeLists.txt +++ /dev/null @@ -1,15 +0,0 @@ - -set(TARGET_NAME ktx-test) - -if (WIN32) - SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /ignore:4049 /ignore:4217") -endif() - -# This is not a testcase -- just set it up as a regular hifi project -setup_hifi_project(Quick Gui OpenGL) -set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/") - -# link in the shared libraries -link_hifi_libraries(shared octree ktx gl gpu gpu-gl render model model-networking networking render-utils fbx entities entities-renderer animation audio avatars script-engine physics) - -package_libraries_for_deployment() diff --git a/tests/ktx/src/main.cpp b/tests/ktx/src/main.cpp deleted file mode 100644 index aa6795e17b..0000000000 --- a/tests/ktx/src/main.cpp +++ /dev/null @@ -1,150 +0,0 @@ -// -// Created by Bradley Austin Davis on 2016/07/01 -// Copyright 2014 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include -#include -#include -#include - -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include - - -#include -#include -#include -#include - - -QSharedPointer logger; - -gpu::Texture* cacheTexture(const std::string& name, gpu::Texture* srcTexture, bool write = true, bool read = true); - - -void messageHandler(QtMsgType type, const QMessageLogContext& context, const QString& message) { - QString logMessage = LogHandler::getInstance().printMessage((LogMsgType)type, context, message); - - if (!logMessage.isEmpty()) { -#ifdef Q_OS_WIN - OutputDebugStringA(logMessage.toLocal8Bit().constData()); - OutputDebugStringA("\n"); -#endif - logger->addMessage(qPrintable(logMessage + "\n")); - } -} - -const char * LOG_FILTER_RULES = R"V0G0N( -hifi.gpu=true -)V0G0N"; - -QString getRootPath() { - static std::once_flag once; - static QString result; - std::call_once(once, [&] { - QFileInfo file(__FILE__); - QDir parent = file.absolutePath(); - result = QDir::cleanPath(parent.currentPath() + "/../../.."); - }); - return result; -} - -const QString TEST_IMAGE = getRootPath() + "/scripts/developer/tests/cube_texture.png"; -const QString TEST_IMAGE_KTX = getRootPath() + "/scripts/developer/tests/cube_texture.ktx"; - -int main(int argc, char** argv) { - QApplication app(argc, argv); - QCoreApplication::setApplicationName("KTX"); - QCoreApplication::setOrganizationName("High Fidelity"); - QCoreApplication::setOrganizationDomain("highfidelity.com"); - logger.reset(new FileLogger()); - - Q_ASSERT(sizeof(ktx::Header) == 12 + (sizeof(uint32_t) * 13)); - - DependencyManager::set(); - qInstallMessageHandler(messageHandler); - QLoggingCategory::setFilterRules(LOG_FILTER_RULES); - - QImage image(TEST_IMAGE); - gpu::Texture* testTexture = model::TextureUsage::process2DTextureColorFromImage(image, TEST_IMAGE.toStdString(), true, false, true); - - auto ktxMemory = gpu::Texture::serialize(*testTexture); - { - const auto& ktxStorage = ktxMemory->getStorage(); - QFile outFile(TEST_IMAGE_KTX); - if (!outFile.open(QFile::Truncate | QFile::ReadWrite)) { - throw std::runtime_error("Unable to open file"); - } - auto ktxSize = ktxStorage->size(); - outFile.resize(ktxSize); - auto dest = outFile.map(0, ktxSize); - memcpy(dest, ktxStorage->data(), ktxSize); - outFile.unmap(dest); - outFile.close(); - } - - auto ktxFile = ktx::KTX::create(std::shared_ptr(new storage::FileStorage(TEST_IMAGE_KTX))); - { - const auto& memStorage = ktxMemory->getStorage(); - const auto& fileStorage = ktxFile->getStorage(); - Q_ASSERT(memStorage->size() == fileStorage->size()); - Q_ASSERT(memStorage->data() != fileStorage->data()); - Q_ASSERT(0 == memcmp(memStorage->data(), fileStorage->data(), memStorage->size())); - Q_ASSERT(ktxFile->_images.size() == ktxMemory->_images.size()); - auto imageCount = ktxFile->_images.size(); - auto startMemory = ktxMemory->_storage->data(); - auto startFile = ktxFile->_storage->data(); - for (size_t i = 0; i < imageCount; ++i) { - auto memImages = ktxMemory->_images[i]; - auto fileImages = ktxFile->_images[i]; - Q_ASSERT(memImages._padding == fileImages._padding); - Q_ASSERT(memImages._numFaces == fileImages._numFaces); - Q_ASSERT(memImages._imageSize == fileImages._imageSize); - Q_ASSERT(memImages._faceSize == fileImages._faceSize); - Q_ASSERT(memImages._faceBytes.size() == memImages._numFaces); - Q_ASSERT(fileImages._faceBytes.size() == fileImages._numFaces); - auto faceCount = fileImages._numFaces; - for (uint32_t face = 0; face < faceCount; ++face) { - auto memFace = memImages._faceBytes[face]; - auto memOffset = memFace - startMemory; - auto fileFace = fileImages._faceBytes[face]; - auto fileOffset = fileFace - startFile; - Q_ASSERT(memOffset % 4 == 0); - Q_ASSERT(memOffset == fileOffset); - } - } - } - testTexture->setKtxBacking(ktxFile); - return 0; -} - -#include "main.moc" - diff --git a/tests/render-perf/CMakeLists.txt b/tests/render-perf/CMakeLists.txt index 96cede9c43..d4f90fdace 100644 --- a/tests/render-perf/CMakeLists.txt +++ b/tests/render-perf/CMakeLists.txt @@ -10,7 +10,7 @@ setup_hifi_project(Quick Gui OpenGL) set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/") # link in the shared libraries -link_hifi_libraries(shared octree ktx gl gpu gpu-gl render model model-networking networking render-utils fbx entities entities-renderer animation audio avatars script-engine physics) +link_hifi_libraries(shared octree gl gpu gpu-gl render model model-networking networking render-utils fbx entities entities-renderer animation audio avatars script-engine physics) package_libraries_for_deployment() diff --git a/tests/render-perf/src/Camera.hpp b/tests/render-perf/src/Camera.hpp index ada1277c47..a3b33ceb14 100644 --- a/tests/render-perf/src/Camera.hpp +++ b/tests/render-perf/src/Camera.hpp @@ -123,16 +123,16 @@ public: void update(float deltaTime) { if (moving()) { - glm::vec3 camForward = getOrientation() * Vectors::FRONT; + glm::vec3 camFront = getOrientation() * Vectors::FRONT; glm::vec3 camRight = getOrientation() * Vectors::RIGHT; glm::vec3 camUp = getOrientation() * Vectors::UP; float moveSpeed = deltaTime * movementSpeed; if (keys[FORWARD]) { - position += camForward * moveSpeed; + position += camFront * moveSpeed; } if (keys[BACK]) { - position -= camForward * moveSpeed; + position -= camFront * moveSpeed; } if (keys[LEFT]) { position -= camRight * moveSpeed; diff --git a/tests/render-perf/src/main.cpp b/tests/render-perf/src/main.cpp index 522fe79b10..7e9d2c426f 100644 --- a/tests/render-perf/src/main.cpp +++ b/tests/render-perf/src/main.cpp @@ -642,6 +642,7 @@ protected: gpu::Texture::setAllowedGPUMemoryUsage(MB_TO_BYTES(64)); return; + default: break; } diff --git a/tests/render-texture-load/src/main.cpp b/tests/render-texture-load/src/main.cpp index d924f76232..09a420f018 100644 --- a/tests/render-texture-load/src/main.cpp +++ b/tests/render-texture-load/src/main.cpp @@ -48,7 +48,6 @@ #include #include -#include #include #include #include diff --git a/tests/shared/src/StorageTests.cpp b/tests/shared/src/StorageTests.cpp deleted file mode 100644 index fa538f6911..0000000000 --- a/tests/shared/src/StorageTests.cpp +++ /dev/null @@ -1,75 +0,0 @@ -// -// Created by Bradley Austin Davis on 2016/02/17 -// Copyright 2013-2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include "StorageTests.h" - -QTEST_MAIN(StorageTests) - -using namespace storage; - -StorageTests::StorageTests() { - for (size_t i = 0; i < _testData.size(); ++i) { - _testData[i] = (uint8_t)rand(); - } - _testFile = QDir::tempPath() + "/" + QUuid::createUuid().toString(); -} - -StorageTests::~StorageTests() { - QFileInfo fileInfo(_testFile); - if (fileInfo.exists()) { - QFile(_testFile).remove(); - } -} - - -void StorageTests::testConversion() { - { - QFileInfo fileInfo(_testFile); - QCOMPARE(fileInfo.exists(), false); - } - StoragePointer storagePointer = std::make_unique(_testData.size(), _testData.data()); - QCOMPARE(storagePointer->size(), (quint64)_testData.size()); - QCOMPARE(memcmp(_testData.data(), storagePointer->data(), _testData.size()), 0); - // Convert to a file - storagePointer = storagePointer->toFileStorage(_testFile); - { - QFileInfo fileInfo(_testFile); - QCOMPARE(fileInfo.exists(), true); - QCOMPARE(fileInfo.size(), (qint64)_testData.size()); - } - QCOMPARE(storagePointer->size(), (quint64)_testData.size()); - QCOMPARE(memcmp(_testData.data(), storagePointer->data(), _testData.size()), 0); - - // Convert to memory - storagePointer = storagePointer->toMemoryStorage(); - QCOMPARE(storagePointer->size(), (quint64)_testData.size()); - QCOMPARE(memcmp(_testData.data(), storagePointer->data(), _testData.size()), 0); - { - // ensure the file is unaffected - QFileInfo fileInfo(_testFile); - QCOMPARE(fileInfo.exists(), true); - QCOMPARE(fileInfo.size(), (qint64)_testData.size()); - } - - // truncate the data as a new memory object - auto newSize = _testData.size() / 2; - storagePointer = std::make_unique(newSize, storagePointer->data()); - QCOMPARE(storagePointer->size(), (quint64)newSize); - QCOMPARE(memcmp(_testData.data(), storagePointer->data(), newSize), 0); - - // Convert back to file - storagePointer = storagePointer->toFileStorage(_testFile); - QCOMPARE(storagePointer->size(), (quint64)newSize); - QCOMPARE(memcmp(_testData.data(), storagePointer->data(), newSize), 0); - { - // ensure the file is truncated - QFileInfo fileInfo(_testFile); - QCOMPARE(fileInfo.exists(), true); - QCOMPARE(fileInfo.size(), (qint64)newSize); - } -} diff --git a/tests/shared/src/StorageTests.h b/tests/shared/src/StorageTests.h deleted file mode 100644 index 6a2c153223..0000000000 --- a/tests/shared/src/StorageTests.h +++ /dev/null @@ -1,32 +0,0 @@ -// -// Created by Bradley Austin Davis on 2016/02/17 -// Copyright 2013-2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#ifndef hifi_StorageTests_h -#define hifi_StorageTests_h - -#include - -#include -#include - -class StorageTests : public QObject { - Q_OBJECT - -public: - StorageTests(); - ~StorageTests(); - -private slots: - void testConversion(); - -private: - std::array _testData; - QString _testFile; -}; - -#endif // hifi_StorageTests_h diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index 8dc993e6fe..a85a112bf5 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -17,5 +17,3 @@ set_target_properties(ac-client PROPERTIES FOLDER "Tools") add_subdirectory(skeleton-dump) set_target_properties(skeleton-dump PROPERTIES FOLDER "Tools") -add_subdirectory(atp-get) -set_target_properties(atp-get PROPERTIES FOLDER "Tools") diff --git a/tools/atp-get/CMakeLists.txt b/tools/atp-get/CMakeLists.txt deleted file mode 100644 index b1646dc023..0000000000 --- a/tools/atp-get/CMakeLists.txt +++ /dev/null @@ -1,3 +0,0 @@ -set(TARGET_NAME atp-get) -setup_hifi_project(Core Widgets) -link_hifi_libraries(shared networking) diff --git a/tools/atp-get/src/ATPGetApp.cpp b/tools/atp-get/src/ATPGetApp.cpp deleted file mode 100644 index 30054fffea..0000000000 --- a/tools/atp-get/src/ATPGetApp.cpp +++ /dev/null @@ -1,269 +0,0 @@ -// -// ATPGetApp.cpp -// tools/atp-get/src -// -// Created by Seth Alves on 2017-3-15 -// Copyright 2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "ATPGetApp.h" - -ATPGetApp::ATPGetApp(int argc, char* argv[]) : - QCoreApplication(argc, argv) -{ - // parse command-line - QCommandLineParser parser; - parser.setApplicationDescription("High Fidelity ATP-Get"); - - const QCommandLineOption helpOption = parser.addHelpOption(); - - const QCommandLineOption verboseOutput("v", "verbose output"); - parser.addOption(verboseOutput); - - const QCommandLineOption domainAddressOption("d", "domain-server address", "127.0.0.1"); - parser.addOption(domainAddressOption); - - const QCommandLineOption cacheSTUNOption("s", "cache stun-server response"); - parser.addOption(cacheSTUNOption); - - const QCommandLineOption listenPortOption("listenPort", "listen port", QString::number(INVALID_PORT)); - parser.addOption(listenPortOption); - - - if (!parser.parse(QCoreApplication::arguments())) { - qCritical() << parser.errorText() << endl; - parser.showHelp(); - Q_UNREACHABLE(); - } - - if (parser.isSet(helpOption)) { - parser.showHelp(); - Q_UNREACHABLE(); - } - - _verbose = parser.isSet(verboseOutput); - if (!_verbose) { - QLoggingCategory::setFilterRules("qt.network.ssl.warning=false"); - - const_cast(&networking())->setEnabled(QtDebugMsg, false); - const_cast(&networking())->setEnabled(QtInfoMsg, false); - const_cast(&networking())->setEnabled(QtWarningMsg, false); - - const_cast(&shared())->setEnabled(QtDebugMsg, false); - const_cast(&shared())->setEnabled(QtInfoMsg, false); - const_cast(&shared())->setEnabled(QtWarningMsg, false); - } - - - QStringList filenames = parser.positionalArguments(); - if (filenames.empty() || filenames.size() > 2) { - qDebug() << "give remote url and optional local filename as arguments"; - parser.showHelp(); - Q_UNREACHABLE(); - } - - _url = QUrl(filenames[0]); - if (_url.scheme() != "atp") { - qDebug() << "url should start with atp:"; - parser.showHelp(); - Q_UNREACHABLE(); - } - - if (filenames.size() == 2) { - _localOutputFile = filenames[1]; - } - - QString domainServerAddress = "127.0.0.1:40103"; - if (parser.isSet(domainAddressOption)) { - domainServerAddress = parser.value(domainAddressOption); - } - - if (_verbose) { - qDebug() << "domain-server address is" << domainServerAddress; - } - - int listenPort = INVALID_PORT; - if (parser.isSet(listenPortOption)) { - listenPort = parser.value(listenPortOption).toInt(); - } - - Setting::init(); - DependencyManager::registerInheritance(); - - DependencyManager::set([&]{ return QString("Mozilla/5.0 (HighFidelityATPGet)"); }); - DependencyManager::set(); - DependencyManager::set(NodeType::Agent, listenPort); - - - auto nodeList = DependencyManager::get(); - - // start the nodeThread so its event loop is running - QThread* nodeThread = new QThread(this); - nodeThread->setObjectName("NodeList Thread"); - nodeThread->start(); - - // make sure the node thread is given highest priority - nodeThread->setPriority(QThread::TimeCriticalPriority); - - // setup a timer for domain-server check ins - QTimer* domainCheckInTimer = new QTimer(nodeList.data()); - connect(domainCheckInTimer, &QTimer::timeout, nodeList.data(), &NodeList::sendDomainServerCheckIn); - domainCheckInTimer->start(DOMAIN_SERVER_CHECK_IN_MSECS); - - // put the NodeList and datagram processing on the node thread - nodeList->moveToThread(nodeThread); - - const DomainHandler& domainHandler = nodeList->getDomainHandler(); - - connect(&domainHandler, SIGNAL(hostnameChanged(const QString&)), SLOT(domainChanged(const QString&))); - // connect(&domainHandler, SIGNAL(resetting()), SLOT(resettingDomain())); - // connect(&domainHandler, SIGNAL(disconnectedFromDomain()), SLOT(clearDomainOctreeDetails())); - connect(&domainHandler, &DomainHandler::domainConnectionRefused, this, &ATPGetApp::domainConnectionRefused); - - connect(nodeList.data(), &NodeList::nodeAdded, this, &ATPGetApp::nodeAdded); - connect(nodeList.data(), &NodeList::nodeKilled, this, &ATPGetApp::nodeKilled); - connect(nodeList.data(), &NodeList::nodeActivated, this, &ATPGetApp::nodeActivated); - // connect(nodeList.data(), &NodeList::uuidChanged, getMyAvatar(), &MyAvatar::setSessionUUID); - // connect(nodeList.data(), &NodeList::uuidChanged, this, &ATPGetApp::setSessionUUID); - connect(nodeList.data(), &NodeList::packetVersionMismatch, this, &ATPGetApp::notifyPacketVersionMismatch); - - nodeList->addSetOfNodeTypesToNodeInterestSet(NodeSet() << NodeType::AudioMixer << NodeType::AvatarMixer - << NodeType::EntityServer << NodeType::AssetServer << NodeType::MessagesMixer); - - DependencyManager::get()->handleLookupString(domainServerAddress, false); - - auto assetClient = DependencyManager::set(); - assetClient->init(); - - QTimer* doTimer = new QTimer(this); - doTimer->setSingleShot(true); - connect(doTimer, &QTimer::timeout, this, &ATPGetApp::timedOut); - doTimer->start(4000); -} - -ATPGetApp::~ATPGetApp() { -} - - -void ATPGetApp::domainConnectionRefused(const QString& reasonMessage, int reasonCodeInt, const QString& extraInfo) { - qDebug() << "domainConnectionRefused"; -} - -void ATPGetApp::domainChanged(const QString& domainHostname) { - if (_verbose) { - qDebug() << "domainChanged"; - } -} - -void ATPGetApp::nodeAdded(SharedNodePointer node) { - if (_verbose) { - qDebug() << "node added: " << node->getType(); - } -} - -void ATPGetApp::nodeActivated(SharedNodePointer node) { - if (node->getType() == NodeType::AssetServer) { - lookup(); - } -} - -void ATPGetApp::nodeKilled(SharedNodePointer node) { - qDebug() << "nodeKilled"; -} - -void ATPGetApp::timedOut() { - finish(1); -} - -void ATPGetApp::notifyPacketVersionMismatch() { - if (_verbose) { - qDebug() << "packet version mismatch"; - } - finish(1); -} - -void ATPGetApp::lookup() { - - auto path = _url.path(); - qDebug() << "path is " << path; - - auto request = DependencyManager::get()->createGetMappingRequest(path); - QObject::connect(request, &GetMappingRequest::finished, this, [=](GetMappingRequest* request) mutable { - auto result = request->getError(); - if (result == GetMappingRequest::NotFound) { - qDebug() << "not found"; - } else if (result == GetMappingRequest::NoError) { - qDebug() << "found, hash is " << request->getHash(); - download(request->getHash()); - } else { - qDebug() << "error -- " << request->getError() << " -- " << request->getErrorString(); - } - request->deleteLater(); - }); - request->start(); -} - -void ATPGetApp::download(AssetHash hash) { - auto assetClient = DependencyManager::get(); - auto assetRequest = new AssetRequest(hash); - - connect(assetRequest, &AssetRequest::finished, this, [this](AssetRequest* request) mutable { - Q_ASSERT(request->getState() == AssetRequest::Finished); - - if (request->getError() == AssetRequest::Error::NoError) { - QString data = QString::fromUtf8(request->getData()); - if (_localOutputFile == "") { - QTextStream cout(stdout); - cout << data; - } else { - QFile outputHandle(_localOutputFile); - if (outputHandle.open(QIODevice::ReadWrite)) { - QTextStream stream( &outputHandle ); - stream << data; - } else { - qDebug() << "couldn't open output file:" << _localOutputFile; - } - } - } - - request->deleteLater(); - finish(0); - }); - - assetRequest->start(); -} - -void ATPGetApp::finish(int exitCode) { - auto nodeList = DependencyManager::get(); - - // send the domain a disconnect packet, force stoppage of domain-server check-ins - nodeList->getDomainHandler().disconnect(); - nodeList->setIsShuttingDown(true); - - // tell the packet receiver we're shutting down, so it can drop packets - nodeList->getPacketReceiver().setShouldDropPackets(true); - - QThread* nodeThread = DependencyManager::get()->thread(); - // remove the NodeList from the DependencyManager - DependencyManager::destroy(); - // ask the node thread to quit and wait until it is done - nodeThread->quit(); - nodeThread->wait(); - - QCoreApplication::exit(exitCode); -} diff --git a/tools/atp-get/src/ATPGetApp.h b/tools/atp-get/src/ATPGetApp.h deleted file mode 100644 index 5507d2aa62..0000000000 --- a/tools/atp-get/src/ATPGetApp.h +++ /dev/null @@ -1,52 +0,0 @@ -// -// ATPGetApp.h -// tools/atp-get/src -// -// Created by Seth Alves on 2017-3-15 -// Copyright 2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - - -#ifndef hifi_ATPGetApp_h -#define hifi_ATPGetApp_h - -#include -#include -#include -#include -#include -#include -#include -#include - - -class ATPGetApp : public QCoreApplication { - Q_OBJECT -public: - ATPGetApp(int argc, char* argv[]); - ~ATPGetApp(); - -private slots: - void domainConnectionRefused(const QString& reasonMessage, int reasonCodeInt, const QString& extraInfo); - void domainChanged(const QString& domainHostname); - void nodeAdded(SharedNodePointer node); - void nodeActivated(SharedNodePointer node); - void nodeKilled(SharedNodePointer node); - void notifyPacketVersionMismatch(); - -private: - NodeList* _nodeList; - void timedOut(); - void lookup(); - void download(AssetHash hash); - void finish(int exitCode); - bool _verbose; - - QUrl _url; - QString _localOutputFile; -}; - -#endif // hifi_ATPGetApp_h diff --git a/tools/atp-get/src/main.cpp b/tools/atp-get/src/main.cpp deleted file mode 100644 index bddf30c666..0000000000 --- a/tools/atp-get/src/main.cpp +++ /dev/null @@ -1,31 +0,0 @@ -// -// main.cpp -// tools/atp-get/src -// -// Created by Seth Alves on 2017-3-15 -// Copyright 2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html - -#include -#include -#include -#include - -#include - -#include "ATPGetApp.h" - -using namespace std; - -int main(int argc, char * argv[]) { - QCoreApplication::setApplicationName(BuildInfo::AC_CLIENT_SERVER_NAME); - QCoreApplication::setOrganizationName(BuildInfo::MODIFIED_ORGANIZATION); - QCoreApplication::setOrganizationDomain(BuildInfo::ORGANIZATION_DOMAIN); - QCoreApplication::setApplicationVersion(BuildInfo::VERSION); - - ATPGetApp app(argc, argv); - - return app.exec(); -} diff --git a/unpublishedScripts/marketplace/boppo/boppoClownEntity.js b/unpublishedScripts/marketplace/boppo/boppoClownEntity.js deleted file mode 100644 index 36f2bf5ab0..0000000000 --- a/unpublishedScripts/marketplace/boppo/boppoClownEntity.js +++ /dev/null @@ -1,80 +0,0 @@ -// -// boppoClownEntity.js -// -// Created by Thijs Wenker on 3/15/17. -// Copyright 2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -/* globals LookAtTarget */ - -(function() { - var SFX_PREFIX = 'https://hifi-content.s3-us-west-1.amazonaws.com/caitlyn/production/elBoppo/sfx/'; - var CHANNEL_PREFIX = 'io.highfidelity.boppo_server_'; - var PUNCH_SOUNDS = [ - 'punch_1.wav', - 'punch_2.wav' - ]; - var PUNCH_COOLDOWN = 300; - - Script.include('lookAtEntity.js'); - - var createBoppoClownEntity = function() { - var _this, - _entityID, - _boppoUserData, - _lookAtTarget, - _punchSounds = [], - _lastPlayedPunch = {}; - - var getOwnBoppoUserData = function() { - try { - return JSON.parse(Entities.getEntityProperties(_entityID, ['userData']).userData).Boppo; - } catch (e) { - // e - } - return {}; - }; - - var BoppoClownEntity = function () { - _this = this; - PUNCH_SOUNDS.forEach(function(punch) { - _punchSounds.push(SoundCache.getSound(SFX_PREFIX + punch)); - }); - }; - - BoppoClownEntity.prototype = { - preload: function(entityID) { - _entityID = entityID; - _boppoUserData = getOwnBoppoUserData(); - _lookAtTarget = new LookAtTarget(_entityID); - }, - collisionWithEntity: function(boppoEntity, collidingEntity, collisionInfo) { - if (collisionInfo.type === 0 && - Entities.getEntityProperties(collidingEntity, ['name']).name.indexOf('Boxing Glove ') === 0) { - - if (_lastPlayedPunch[collidingEntity] === undefined || - Date.now() - _lastPlayedPunch[collidingEntity] > PUNCH_COOLDOWN) { - - // If boxing glove detected here: - Messages.sendMessage(CHANNEL_PREFIX + _boppoUserData.gameParentID, 'hit'); - - _lookAtTarget.lookAtByAction(); - var randomPunchIndex = Math.floor(Math.random() * _punchSounds.length); - Audio.playSound(_punchSounds[randomPunchIndex], { - position: collisionInfo.contactPoint - }); - _lastPlayedPunch[collidingEntity] = Date.now(); - } - } - } - - }; - - return new BoppoClownEntity(); - }; - - return createBoppoClownEntity(); -}); diff --git a/unpublishedScripts/marketplace/boppo/boppoServer.js b/unpublishedScripts/marketplace/boppo/boppoServer.js deleted file mode 100644 index f03154573c..0000000000 --- a/unpublishedScripts/marketplace/boppo/boppoServer.js +++ /dev/null @@ -1,303 +0,0 @@ -// -// boppoServer.js -// -// Created by Thijs Wenker on 3/15/17. -// Copyright 2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -(function() { - var SFX_PREFIX = 'https://hifi-content.s3-us-west-1.amazonaws.com/caitlyn/production/elBoppo/sfx/'; - var CLOWN_LAUGHS = [ - 'clown_laugh_1.wav', - 'clown_laugh_2.wav', - 'clown_laugh_3.wav', - 'clown_laugh_4.wav' - ]; - var TICK_TOCK_SOUND = 'ticktock%20-%20tock.wav'; - var BOXING_RING_BELL_START = 'boxingRingBell.wav'; - var BOXING_RING_BELL_END = 'boxingRingBell-end.wav'; - var BOPPO_MUSIC = 'boppoMusic.wav'; - var CHANNEL_PREFIX = 'io.highfidelity.boppo_server_'; - var MESSAGE_HIT = 'hit'; - var MESSAGE_ENTER_ZONE = 'enter-zone'; - var MESSAGE_UNLOAD_FIX = 'unload-fix'; - - var DEFAULT_SOUND_VOLUME = 0.6; - - // don't set the search radius too high, it might remove boppo's from other nearby instances - var BOPPO_SEARCH_RADIUS = 4.0; - - var MILLISECONDS_PER_SECOND = 1000; - // Make sure the entities are loaded at startup (TODO: more solid fix) - var LOAD_TIMEOUT = 5000; - var SECONDS_PER_MINUTE = 60; - var DEFAULT_PLAYTIME = 30; // seconds - var BASE_TEN = 10; - var TICK_TOCK_FROM = 3; // seconds - var COOLDOWN_TIME_MS = MILLISECONDS_PER_SECOND * 3; - - var createBoppoServer = function() { - var _this, - _isInitialized = false, - _clownLaughs = [], - _musicInjector, - _music, - _laughingInjector, - _tickTockSound, - _boxingBellRingStart, - _boxingBellRingEnd, - _entityID, - _boppoClownID, - _channel, - _boppoEntities, - _isGameRunning, - _updateInterval, - _timeLeft, - _hits, - _coolDown; - - var getOwnBoppoUserData = function() { - try { - return JSON.parse(Entities.getEntityProperties(_entityID, ['userData']).userData).Boppo; - } catch (e) { - // e - } - return {}; - }; - - var updateBoppoEntities = function() { - Entities.getChildrenIDs(_entityID).forEach(function(entityID) { - try { - var userData = JSON.parse(Entities.getEntityProperties(entityID, ['userData']).userData); - if (userData.Boppo.type !== undefined) { - _boppoEntities[userData.Boppo.type] = entityID; - } - } catch (e) { - // e - } - }); - }; - - var clearUntrackedBoppos = function() { - var position = Entities.getEntityProperties(_entityID, ['position']).position; - Entities.findEntities(position, BOPPO_SEARCH_RADIUS).forEach(function(entityID) { - try { - if (JSON.parse(Entities.getEntityProperties(entityID, ['userData']).userData).Boppo.type === 'boppo') { - Entities.deleteEntity(entityID); - } - } catch (e) { - // e - } - }); - }; - - var updateTimerDisplay = function() { - if (_boppoEntities['timer']) { - var secondsString = _timeLeft % SECONDS_PER_MINUTE; - if (secondsString < BASE_TEN) { - secondsString = '0' + secondsString; - } - var minutesString = Math.floor(_timeLeft / SECONDS_PER_MINUTE); - Entities.editEntity(_boppoEntities['timer'], { - text: minutesString + ':' + secondsString - }); - } - }; - - var updateScoreDisplay = function() { - if (_boppoEntities['score']) { - Entities.editEntity(_boppoEntities['score'], { - text: 'SCORE: ' + _hits - }); - } - }; - - var playSoundAtBoxingRing = function(sound, properties) { - var _properties = properties ? properties : {}; - if (_properties['volume'] === undefined) { - _properties['volume'] = DEFAULT_SOUND_VOLUME; - } - _properties['position'] = Entities.getEntityProperties(_entityID, ['position']).position; - // play beep - return Audio.playSound(sound, _properties); - }; - - var onUpdate = function() { - _timeLeft--; - - if (_timeLeft > 0 && _timeLeft <= TICK_TOCK_FROM) { - // play beep - playSoundAtBoxingRing(_tickTockSound); - } - if (_timeLeft === 0) { - if (_musicInjector !== undefined && _musicInjector.isPlaying()) { - _musicInjector.stop(); - _musicInjector = undefined; - } - playSoundAtBoxingRing(_boxingBellRingEnd); - _isGameRunning = false; - Script.clearInterval(_updateInterval); - _updateInterval = null; - _coolDown = true; - Script.setTimeout(function() { - _coolDown = false; - _this.resetBoppo(); - }, COOLDOWN_TIME_MS); - } - updateTimerDisplay(); - }; - - var onMessage = function(channel, message, sender) { - if (channel === _channel) { - if (message === MESSAGE_HIT) { - _this.hit(); - } else if (message === MESSAGE_ENTER_ZONE && !_isGameRunning) { - _this.resetBoppo(); - } else if (message === MESSAGE_UNLOAD_FIX && _isInitialized) { - _this.unload(); - } - } - }; - - var BoppoServer = function () { - _this = this; - _hits = 0; - _boppoClownID = null; - _coolDown = false; - CLOWN_LAUGHS.forEach(function(clownLaugh) { - _clownLaughs.push(SoundCache.getSound(SFX_PREFIX + clownLaugh)); - }); - _tickTockSound = SoundCache.getSound(SFX_PREFIX + TICK_TOCK_SOUND); - _boxingBellRingStart = SoundCache.getSound(SFX_PREFIX + BOXING_RING_BELL_START); - _boxingBellRingEnd = SoundCache.getSound(SFX_PREFIX + BOXING_RING_BELL_END); - _music = SoundCache.getSound(SFX_PREFIX + BOPPO_MUSIC); - _boppoEntities = {}; - }; - - BoppoServer.prototype = { - preload: function(entityID) { - _entityID = entityID; - _channel = CHANNEL_PREFIX + entityID; - - Messages.sendLocalMessage(_channel, MESSAGE_UNLOAD_FIX); - Script.setTimeout(function() { - clearUntrackedBoppos(); - updateBoppoEntities(); - Messages.subscribe(_channel); - Messages.messageReceived.connect(onMessage); - _this.resetBoppo(); - _isInitialized = true; - }, LOAD_TIMEOUT); - }, - resetBoppo: function() { - if (_boppoClownID !== null) { - print('deleting boppo: ' + _boppoClownID); - Entities.deleteEntity(_boppoClownID); - } - var boppoBaseProperties = Entities.getEntityProperties(_entityID, ['position', 'rotation']); - _boppoClownID = Entities.addEntity({ - angularDamping: 0.0, - collisionSoundURL: 'https://hifi-content.s3.amazonaws.com/caitlyn/production/elBoppo/51460__andre-rocha-nascimento__basket-ball-01-bounce.wav', - collisionsWillMove: true, - compoundShapeURL: 'https://hifi-content.s3.amazonaws.com/caitlyn/production/elBoppo/bopo_phys.obj', - damping: 1.0, - density: 10000, - dimensions: { - x: 1.2668079137802124, - y: 2.0568051338195801, - z: 0.88563752174377441 - }, - dynamic: 1.0, - friction: 1.0, - gravity: { - x: 0, - y: -25, - z: 0 - }, - modelURL: 'https://hifi-content.s3.amazonaws.com/caitlyn/production/elBoppo/elBoppo3_VR.fbx', - name: 'El Boppo the Punching Bag Clown', - registrationPoint: { - x: 0.5, - y: 0, - z: 0.3 - }, - restitution: 0.99, - rotation: boppoBaseProperties.rotation, - position: Vec3.sum(boppoBaseProperties.position, - Vec3.multiplyQbyV(boppoBaseProperties.rotation, { - x: 0.08666179329156876, - y: -1.5698202848434448, - z: 0.1847127377986908 - })), - script: Script.resolvePath('boppoClownEntity.js'), - shapeType: 'compound', - type: 'Model', - userData: JSON.stringify({ - lookAt: { - targetID: _boppoEntities['lookAtThis'], - disablePitch: true, - disableYaw: false, - disableRoll: true, - clearDisabledAxis: true, - rotationOffset: { x: 0.0, y: 180.0, z: 0.0} - }, - Boppo: { - type: 'boppo', - gameParentID: _entityID - }, - grabbableKey: { - grabbable: false - } - }) - }); - updateBoppoEntities(); - _boppoEntities['boppo'] = _boppoClownID; - }, - laugh: function() { - if (_laughingInjector !== undefined && _laughingInjector.isPlaying()) { - return; - } - var randomLaughIndex = Math.floor(Math.random() * _clownLaughs.length); - _laughingInjector = Audio.playSound(_clownLaughs[randomLaughIndex], { - position: Entities.getEntityProperties(_boppoClownID, ['position']).position - }); - }, - hit: function() { - if (_coolDown) { - return; - } - if (!_isGameRunning) { - var boxingRingBoppoData = getOwnBoppoUserData(); - _updateInterval = Script.setInterval(onUpdate, MILLISECONDS_PER_SECOND); - _timeLeft = boxingRingBoppoData.playTimeSeconds ? parseInt(boxingRingBoppoData.playTimeSeconds) : - DEFAULT_PLAYTIME; - _isGameRunning = true; - _hits = 0; - playSoundAtBoxingRing(_boxingBellRingStart); - _musicInjector = playSoundAtBoxingRing(_music, {loop: true, volume: 0.6}); - } - _hits++; - updateTimerDisplay(); - updateScoreDisplay(); - _this.laugh(); - }, - unload: function() { - print('unload called'); - if (_updateInterval) { - Script.clearInterval(_updateInterval); - } - Messages.messageReceived.disconnect(onMessage); - Messages.unsubscribe(_channel); - Entities.deleteEntity(_boppoClownID); - print('endOfUnload'); - } - }; - - return new BoppoServer(); - }; - - return createBoppoServer(); -}); diff --git a/unpublishedScripts/marketplace/boppo/clownGloveDispenser.js b/unpublishedScripts/marketplace/boppo/clownGloveDispenser.js deleted file mode 100644 index cd0a0c0614..0000000000 --- a/unpublishedScripts/marketplace/boppo/clownGloveDispenser.js +++ /dev/null @@ -1,154 +0,0 @@ -// -// clownGloveDispenser.js -// -// Created by Thijs Wenker on 8/2/16. -// Copyright 2016 High Fidelity, Inc. -// -// Based on examples/winterSmashUp/targetPractice/shooterPlatform.js -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -(function() { - var _this = this; - - var CHANNEL_PREFIX = 'io.highfidelity.boppo_server_'; - - var leftBoxingGlove = undefined; - var rightBoxingGlove = undefined; - - var inZone = false; - - var wearGloves = function() { - leftBoxingGlove = Entities.addEntity({ - position: MyAvatar.position, - collisionsWillMove: true, - dimensions: { - x: 0.24890634417533875, - y: 0.28214839100837708, - z: 0.21127720177173615 - }, - dynamic: true, - gravity: { - x: 0, - y: -9.8, - z: 0 - }, - modelURL: "https://hifi-content.s3.amazonaws.com/caitlyn/production/elBoppo/LFT_glove_VR3.fbx", - name: "Boxing Glove - Left", - registrationPoint: { - x: 0.5, - y: 0, - z: 0.5 - }, - shapeType: "simple-hull", - type: "Model", - userData: JSON.stringify({ - grabbableKey: { - invertSolidWhileHeld: true - }, - wearable: { - joints: { - LeftHand: [ - {x: 0, y: 0.0, z: 0.02 }, - Quat.fromVec3Degrees({x: 0, y: 0, z: 0}) - ] - } - } - }) - }); - Messages.sendLocalMessage('Hifi-Hand-Grab', JSON.stringify({hand: 'left', entityID: leftBoxingGlove})); - // Allows teleporting while glove is wielded - Messages.sendLocalMessage('Hifi-Teleport-Ignore-Add', leftBoxingGlove); - - rightBoxingGlove = Entities.addEntity({ - position: MyAvatar.position, - collisionsWillMove: true, - dimensions: { - x: 0.24890634417533875, - y: 0.28214839100837708, - z: 0.21127720177173615 - }, - dynamic: true, - gravity: { - x: 0, - y: -9.8, - z: 0 - }, - modelURL: "https://hifi-content.s3.amazonaws.com/caitlyn/production/elBoppo/RT_glove_VR2.fbx", - name: "Boxing Glove - Right", - registrationPoint: { - x: 0.5, - y: 0, - z: 0.5 - }, - shapeType: "simple-hull", - type: "Model", - userData: JSON.stringify({ - grabbableKey: { - invertSolidWhileHeld: true - }, - wearable: { - joints: { - RightHand: [ - {x: 0, y: 0.0, z: 0.02 }, - Quat.fromVec3Degrees({x: 0, y: 0, z: 0}) - ] - } - } - }) - }); - Messages.sendLocalMessage('Hifi-Hand-Grab', JSON.stringify({hand: 'right', entityID: rightBoxingGlove})); - // Allows teleporting while glove is wielded - Messages.sendLocalMessage('Hifi-Teleport-Ignore-Add', rightBoxingGlove); - }; - - var cleanUpGloves = function() { - if (leftBoxingGlove !== undefined) { - Entities.deleteEntity(leftBoxingGlove); - leftBoxingGlove = undefined; - } - if (rightBoxingGlove !== undefined) { - Entities.deleteEntity(rightBoxingGlove); - rightBoxingGlove = undefined; - } - }; - - var wearGlovesIfHMD = function() { - // cleanup your old gloves if they're still there (unlikely) - cleanUpGloves(); - if (HMD.active) { - wearGloves(); - } - }; - - _this.preload = function(entityID) { - HMD.displayModeChanged.connect(function() { - if (inZone) { - wearGlovesIfHMD(); - } - }); - }; - - _this.unload = function() { - cleanUpGloves(); - }; - - _this.enterEntity = function(entityID) { - inZone = true; - print('entered boxing glove dispenser entity'); - wearGlovesIfHMD(); - - // Reset boppo if game is not running: - var parentID = Entities.getEntityProperties(entityID, ['parentID']).parentID; - Messages.sendMessage(CHANNEL_PREFIX + parentID, 'enter-zone'); - }; - - _this.leaveEntity = function(entityID) { - inZone = false; - cleanUpGloves(); - }; - - _this.unload = _this.leaveEntity; -}); diff --git a/unpublishedScripts/marketplace/boppo/createElBoppo.js b/unpublishedScripts/marketplace/boppo/createElBoppo.js deleted file mode 100644 index 4df6a2acda..0000000000 --- a/unpublishedScripts/marketplace/boppo/createElBoppo.js +++ /dev/null @@ -1,430 +0,0 @@ -// -// createElBoppo.js -// -// Created by Thijs Wenker on 3/17/17. -// Copyright 2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -/* globals SCRIPT_IMPORT_PROPERTIES */ - -var MODELS_PATH = 'https://hifi-content.s3.amazonaws.com/DomainContent/Welcome%20Area/production/models/boxingRing/'; -var WANT_CLEANUP_ON_SCRIPT_ENDING = false; - -var getScriptPath = function(localPath) { - if (this.isCleanupAndSpawnScript) { - return 'https://hifi-content.s3.amazonaws.com/DomainContent/Welcome%20Area/Scripts/boppo/' + localPath; - } - return Script.resolvePath(localPath); -}; - -var getCreatePosition = function() { - // can either return position defined by resetScript or avatar position - if (this.isCleanupAndSpawnScript) { - return SCRIPT_IMPORT_PROPERTIES.rootPosition; - } - return Vec3.sum(MyAvatar.position, {x: 1, z: -2}); -}; - -var boxingRing = Entities.addEntity({ - dimensions: { - x: 4.0584001541137695, - y: 4.0418000221252441, - z: 3.0490000247955322 - }, - modelURL: MODELS_PATH + 'assembled/boppoBoxingRingAssembly.fbx', - name: 'Boxing Ring Assembly', - rotation: { - w: 0.9996337890625, - x: -1.52587890625e-05, - y: -0.026230275630950928, - z: -4.57763671875e-05 - }, - position: getCreatePosition(), - scriptTimestamp: 1489612158459, - serverScripts: getScriptPath('boppoServer.js'), - shapeType: 'static-mesh', - type: 'Model', - userData: JSON.stringify({ - Boppo: { - type: 'boxingring', - playTimeSeconds: 15 - } - }) -}); - -var boppoEntities = [ - { - dimensions: { - x: 0.36947935819625854, - y: 0.25536194443702698, - z: 0.059455446898937225 - }, - modelURL: MODELS_PATH + 'boxingGameSign/boppoSignFrame.fbx', - parentID: boxingRing, - localPosition: { - x: -1.0251024961471558, - y: 0.51661628484725952, - z: -1.1176263093948364 - }, - rotation: { - w: 0.996856689453125, - x: 0.013321161270141602, - y: 0.0024566650390625, - z: 0.078049898147583008 - }, - shapeType: 'box', - type: 'Model' - }, - { - dimensions: { - x: 0.33255371451377869, - y: 0.1812121719121933, - z: 0.0099999997764825821 - }, - lineHeight: 0.125, - name: 'Boxing Ring - High Score Board', - parentID: boxingRing, - localPosition: { - x: -1.0239436626434326, - y: 0.52212876081466675, - z: -1.0971509218215942 - }, - rotation: { - w: 0.9876401424407959, - x: 0.013046503067016602, - y: 0.0012359619140625, - z: 0.15605401992797852 - }, - text: '0:00', - textColor: { - blue: 0, - green: 0, - red: 255 - }, - type: 'Text', - userData: JSON.stringify({ - Boppo: { - type: 'timer' - } - }) - }, - { - dimensions: { - x: 0.50491130352020264, - y: 0.13274604082107544, - z: 0.0099999997764825821 - }, - lineHeight: 0.090000003576278687, - name: 'Boxing Ring - Score Board', - parentID: boxingRing, - localPosition: { - x: -0.77596306800842285, - y: 0.37797555327415466, - z: -1.0910623073577881 - }, - rotation: { - w: 0.9518122673034668, - x: 0.004237703513354063, - y: -0.0010041374480351806, - z: 0.30455198884010315 - }, - text: 'SCORE: 0', - textColor: { - blue: 0, - green: 0, - red: 255 - }, - type: 'Text', - userData: JSON.stringify({ - Boppo: { - type: 'score' - } - }) - }, - { - dimensions: { - x: 0.58153259754180908, - y: 0.1884911060333252, - z: 0.059455446898937225 - }, - modelURL: MODELS_PATH + 'boxingGameSign/boppoSignFrame.fbx', - parentID: boxingRing, - localPosition: { - x: -0.78200173377990723, - y: 0.35684797167778015, - z: -1.108180046081543 - }, - rotation: { - w: 0.97814905643463135, - x: 0.0040436983108520508, - y: -0.0005645751953125, - z: 0.20778214931488037 - }, - shapeType: 'box', - type: 'Model' - }, - { - dimensions: { - x: 4.1867804527282715, - y: 3.5065803527832031, - z: 5.6845207214355469 - }, - name: 'El Boppo the Clown boxing area & glove maker', - parentID: boxingRing, - localPosition: { - x: -0.012308252975344658, - y: 0.054641719907522202, - z: 0.98782551288604736 - }, - rotation: { - w: 1, - x: -1.52587890625e-05, - y: -1.52587890625e-05, - z: -1.52587890625e-05 - }, - script: getScriptPath('clownGloveDispenser.js'), - shapeType: 'box', - type: 'Zone', - visible: false - }, - { - color: { - blue: 255, - green: 5, - red: 255 - }, - dimensions: { - x: 0.20000000298023224, - y: 0.20000000298023224, - z: 0.20000000298023224 - }, - name: 'LookAtBox', - parentID: boxingRing, - localPosition: { - x: -0.1772226095199585, - y: -1.7072629928588867, - z: 1.3122396469116211 - }, - rotation: { - w: 0.999969482421875, - x: 1.52587890625e-05, - y: 0.0043793916702270508, - z: 1.52587890625e-05 - }, - shape: 'Cube', - type: 'Box', - userData: JSON.stringify({ - Boppo: { - type: 'lookAtThis' - } - }) - }, - { - color: { - blue: 209, - green: 157, - red: 209 - }, - dimensions: { - x: 1.6913000345230103, - y: 1.2124500274658203, - z: 0.2572999894618988 - }, - name: 'boppoBackBoard', - parentID: boxingRing, - localPosition: { - x: -0.19500596821308136, - y: -1.1044719219207764, - z: -0.55993378162384033 - }, - rotation: { - w: 0.9807126522064209, - x: -0.19511711597442627, - y: 0.0085297822952270508, - z: 0.0016937255859375 - }, - shape: 'Cube', - type: 'Box', - visible: false - }, - { - color: { - blue: 0, - green: 0, - red: 255 - }, - dimensions: { - x: 1.8155574798583984, - y: 0.92306196689605713, - z: 0.51203572750091553 - }, - name: 'boppoBackBoard', - parentID: boxingRing, - localPosition: { - x: -0.11036647111177444, - y: -0.051978692412376404, - z: -0.79054081439971924 - }, - rotation: { - w: 0.9807431697845459, - x: 0.19505608081817627, - y: 0.0085602998733520508, - z: -0.0017547607421875 - }, - shape: 'Cube', - type: 'Box', - visible: false - }, - { - color: { - blue: 209, - green: 157, - red: 209 - }, - dimensions: { - x: 1.9941408634185791, - y: 1.2124500274658203, - z: 0.2572999894618988 - }, - name: 'boppoBackBoard', - localPosition: { - x: 0.69560068845748901, - y: -1.3840068578720093, - z: 0.059689953923225403 - }, - rotation: { - w: 0.73458456993103027, - x: -0.24113833904266357, - y: -0.56545358896255493, - z: -0.28734266757965088 - }, - shape: 'Cube', - type: 'Box', - visible: false - }, - { - color: { - blue: 82, - green: 82, - red: 82 - }, - dimensions: { - x: 8.3777303695678711, - y: 0.87573593854904175, - z: 7.9759469032287598 - }, - parentID: boxingRing, - localPosition: { - x: -0.38302639126777649, - y: -2.121284008026123, - z: 0.3699878454208374 - }, - rotation: { - w: 0.70711839199066162, - x: -7.62939453125e-05, - y: 0.70705735683441162, - z: -1.52587890625e-05 - }, - shape: 'Triangle', - type: 'Shape' - }, - { - color: { - blue: 209, - green: 157, - red: 209 - }, - dimensions: { - x: 1.889795184135437, - y: 0.86068248748779297, - z: 0.2572999894618988 - }, - name: 'boppoBackBoard', - parentID: boxingRing, - localPosition: { - x: -0.95167744159698486, - y: -1.4756947755813599, - z: -0.042313352227210999 - }, - rotation: { - w: 0.74004733562469482, - x: -0.24461740255355835, - y: 0.56044864654541016, - z: 0.27998781204223633 - }, - shape: 'Cube', - type: 'Box', - visible: false - }, - { - color: { - blue: 0, - green: 0, - red: 255 - }, - dimensions: { - x: 4.0720257759094238, - y: 0.50657749176025391, - z: 1.4769613742828369 - }, - name: 'boppo-stepsRamp', - parentID: boxingRing, - localPosition: { - x: -0.002939039608463645, - y: -1.9770187139511108, - z: 2.2165381908416748 - }, - rotation: { - w: 0.99252307415008545, - x: 0.12184333801269531, - y: -1.52587890625e-05, - z: -1.52587890625e-05 - }, - shape: 'Cube', - type: 'Box', - visible: false - }, - { - color: { - blue: 150, - green: 150, - red: 150 - }, - cutoff: 90, - dimensions: { - x: 5.2220535278320312, - y: 5.2220535278320312, - z: 5.2220535278320312 - }, - falloffRadius: 2, - intensity: 15, - name: 'boxing ring light', - parentID: boxingRing, - localPosition: { - x: -1.4094564914703369, - y: -0.36021926999092102, - z: 0.81797939538955688 - }, - rotation: { - w: 0.9807431697845459, - x: 1.52587890625e-05, - y: -0.19520866870880127, - z: -1.52587890625e-05 - }, - type: 'Light' - } -]; - -boppoEntities.forEach(function(entityProperties) { - entityProperties['parentID'] = boxingRing; - Entities.addEntity(entityProperties); -}); - -if (WANT_CLEANUP_ON_SCRIPT_ENDING) { - Script.scriptEnding.connect(function() { - Entities.deleteEntity(boxingRing); - }); -} diff --git a/unpublishedScripts/marketplace/boppo/lookAtEntity.js b/unpublishedScripts/marketplace/boppo/lookAtEntity.js deleted file mode 100644 index ba072814f2..0000000000 --- a/unpublishedScripts/marketplace/boppo/lookAtEntity.js +++ /dev/null @@ -1,98 +0,0 @@ -// -// lookAtTarget.js -// -// Created by Thijs Wenker on 3/15/17. -// Copyright 2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -/* globals LookAtTarget:true */ - -LookAtTarget = function(sourceEntityID) { - /* private variables */ - var _this, - _options, - _sourceEntityID, - _sourceEntityProperties, - REQUIRED_PROPERTIES = ['position', 'rotation', 'userData'], - LOOK_AT_TAG = 'lookAtTarget'; - - LookAtTarget = function(sourceEntityID) { - _this = this; - _sourceEntityID = sourceEntityID; - _this.updateOptions(); - }; - - /* private functions */ - var updateEntitySourceProperties = function() { - _sourceEntityProperties = Entities.getEntityProperties(_sourceEntityID, REQUIRED_PROPERTIES); - }; - - var getUpdatedActionProperties = function() { - return { - targetRotation: _this.getLookAtRotation(), - angularTimeScale: 0.1, - ttl: 10 - }; - }; - - var getNewActionProperties = function() { - var newActionProperties = getUpdatedActionProperties(); - newActionProperties.tag = LOOK_AT_TAG; - return newActionProperties; - }; - - LookAtTarget.prototype = { - /* public functions */ - updateOptions: function() { - updateEntitySourceProperties(); - _options = JSON.parse(_sourceEntityProperties.userData).lookAt; - }, - getTargetPosition: function() { - return Entities.getEntityProperties(_options.targetID).position; - }, - getLookAtRotation: function() { - _this.updateOptions(); - - var newRotation = Quat.lookAt(_sourceEntityProperties.position, _this.getTargetPosition(), Vec3.UP); - if (_options.rotationOffset !== undefined) { - newRotation = Quat.multiply(newRotation, Quat.fromVec3Degrees(_options.rotationOffset)); - } - if (_options.disablePitch || _options.disableYaw || _options.disablePitch) { - var disabledAxis = _options.clearDisabledAxis ? Vec3.ZERO : - Quat.safeEulerAngles(_sourceEntityProperties.rotation); - var newEulers = Quat.safeEulerAngles(newRotation); - newRotation = Quat.fromVec3Degrees({ - x: _options.disablePitch ? disabledAxis.x : newEulers.x, - y: _options.disableYaw ? disabledAxis.y : newEulers.y, - z: _options.disableRoll ? disabledAxis.z : newEulers.z - }); - } - return newRotation; - }, - lookAtDirectly: function() { - Entities.editEntity(_sourceEntityID, {rotation: _this.getLookAtRotation()}); - }, - lookAtByAction: function() { - var actionIDs = Entities.getActionIDs(_sourceEntityID); - var actionFound = false; - actionIDs.forEach(function(actionID) { - if (actionFound) { - return; - } - var actionArguments = Entities.getActionArguments(_sourceEntityID, actionID); - if (actionArguments.tag === LOOK_AT_TAG) { - actionFound = true; - Entities.updateAction(_sourceEntityID, actionID, getUpdatedActionProperties()); - } - }); - if (!actionFound) { - Entities.addAction('spring', _sourceEntityID, getNewActionProperties()); - } - } - }; - - return new LookAtTarget(sourceEntityID); -}; From 73a77f7f2563c37f8831c751e2d3466cd5011d4d Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Fri, 24 Mar 2017 18:19:34 -0700 Subject: [PATCH 053/118] OK, how about now --- scripts/system/selectAudioDevice.js | 102 ++++++++++------------------ 1 file changed, 34 insertions(+), 68 deletions(-) diff --git a/scripts/system/selectAudioDevice.js b/scripts/system/selectAudioDevice.js index cc0f25b005..a2246bf1d5 100644 --- a/scripts/system/selectAudioDevice.js +++ b/scripts/system/selectAudioDevice.js @@ -50,8 +50,6 @@ if (typeof String.prototype.trimEndsWith != 'function') { ****************************************/ const INPUT_DEVICE_SETTING = "audio_input_device"; const OUTPUT_DEVICE_SETTING = "audio_output_device"; -var selectedInputMenu = ""; -var selectedOutputMenu = ""; var audioDevicesList = []; // Some HMDs (like Oculus CV1) have a built in audio device. If they // do, then this function will handle switching to that device automatically @@ -61,72 +59,59 @@ var switchedAudioInputToHMD = false; var switchedAudioOutputToHMD = false; var previousSelectedInputAudioDevice = ""; var previousSelectedOutputAudioDevice = ""; -var menuConnected = false; /**************************************** BEGIN FUNCTION DEFINITIONS ****************************************/ function setupAudioMenus() { - if (menuConnected) { - Menu.menuItemEvent.disconnect(menuItemEvent); - menuConnected = false; - } - removeAudioMenus(); + Menu.menuItemEvent.disconnect(menuItemEvent); /* Setup audio input devices */ Menu.addSeparator("Audio", "Input Audio Device"); var inputDevices = AudioDevice.getInputDevices(); print("selectAudioDevice: Audio input devices: " + inputDevices); - var selectedInputDevice = AudioDevice.getInputDevice(); - var inputDeviceSetting = Settings.getValue(INPUT_DEVICE_SETTING); - if (inputDevices.indexOf(inputDeviceSetting) != -1 && selectedInputDevice != inputDeviceSetting) { - print("selectAudioDevice: Input Setting & Device mismatch! Input SETTING:", inputDeviceSetting, "Input DEVICE IN USE:", selectedInputDevice); - switchAudioDevice(true, inputDeviceSetting); - } for (var i = 0; i < inputDevices.length; i++) { - var thisDeviceSelected = (inputDevices[i] == selectedInputDevice); var menuItem = "Use " + inputDevices[i] + " for Input"; Menu.addMenuItem({ menuName: "Audio", menuItemName: menuItem, isCheckable: true, - isChecked: thisDeviceSelected + isChecked: inputDevices[i] == AudioDevice.getInputDevice() }); audioDevicesList.push(menuItem); - if (thisDeviceSelected) { - selectedInputMenu = menuItem; - print("selectAudioDevice: selectedInputMenu: " + selectedInputMenu); - } } /* Setup audio output devices */ Menu.addSeparator("Audio", "Output Audio Device"); var outputDevices = AudioDevice.getOutputDevices(); print("selectAudioDevice: Audio output devices: " + outputDevices); - var selectedOutputDevice = AudioDevice.getOutputDevice(); - var outputDeviceSetting = Settings.getValue(OUTPUT_DEVICE_SETTING); - if (outputDevices.indexOf(outputDeviceSetting) != -1 && selectedOutputDevice != outputDeviceSetting) { - print("selectAudioDevice: Output Setting & Device mismatch! Output SETTING:", outputDeviceSetting, "Output DEVICE IN USE:", selectedOutputDevice); - switchAudioDevice(false, outputDeviceSetting); - } for (var i = 0; i < outputDevices.length; i++) { - var thisDeviceSelected = (outputDevices[i] == selectedOutputDevice); var menuItem = "Use " + outputDevices[i] + " for Output"; Menu.addMenuItem({ menuName: "Audio", menuItemName: menuItem, isCheckable: true, - isChecked: thisDeviceSelected + isChecked: outputDevices[i] == AudioDevice.getOutputDevice() }); audioDevicesList.push(menuItem); - if (thisDeviceSelected) { - selectedOutputMenu = menuItem; - print("selectAudioDevice: selectedOutputMenu: " + selectedOutputMenu); - } } - if (!menuConnected) { - Menu.menuItemEvent.connect(menuItemEvent); - menuConnected = true; + + Menu.menuItemEvent.connect(menuItemEvent); +} + +function checkDeviceMismatch() { + var inputDeviceSetting = Settings.getValue(INPUT_DEVICE_SETTING); + var interfaceInputDevice = AudioDevice.getInputDevice(); + if (interfaceInputDevice != inputDeviceSetting) { + print("selectAudioDevice: Input Setting & Device mismatch! Input SETTING:", inputDeviceSetting, "Input DEVICE IN USE:", AudioDevice.getInputDevice()); + switchAudioDevice(true, inputDeviceSetting); + } + + var outputDeviceSetting = Settings.getValue(OUTPUT_DEVICE_SETTING); + var interfaceOutputDevice = AudioDevice.getOutputDevice(); + if (interfaceOutputDevice != outputDeviceSetting) { + print("selectAudioDevice: Output Setting & Device mismatch! Output SETTING:", outputDeviceSetting, "Output DEVICE IN USE:", interfaceOutputDevice); + switchAudioDevice(false, outputDeviceSetting); } } @@ -135,7 +120,9 @@ function removeAudioMenus() { Menu.removeSeparator("Audio", "Output Audio Device"); for (var index = 0; index < audioDevicesList.length; index++) { - Menu.removeMenuItem("Audio", audioDevicesList[index]); + if (Menu.menuItemExists("Audio", audioDevicesList[index])) { + Menu.removeMenuItem("Audio", audioDevicesList[index]); + } } Menu.removeMenu("Audio > Devices"); @@ -145,14 +132,12 @@ function removeAudioMenus() { function onDevicechanged() { print("selectAudioDevice: System audio device changed. Removing and replacing Audio Menus..."); + removeAudioMenus(); setupAudioMenus(); + checkDeviceMismatch(); } function menuItemEvent(menuItem) { - if (menuConnected) { - Menu.menuItemEvent.disconnect(menuItemEvent); - menuConnected = false; - } if (menuItem.startsWith("Use ")) { if (menuItem.endsWith(" for Input")) { var selectedDevice = menuItem.trimStartsWith("Use ").trimEndsWith(" for Input"); @@ -165,20 +150,10 @@ function menuItemEvent(menuItem) { } else { print("selectAudioDevice: Invalid Audio menuItem! Doesn't end with 'for Input' or 'for Output'") } - } else { - print("selectAudioDevice: Invalid Audio menuItem! Doesn't start with 'Use '") - } - if (!menuConnected) { - Menu.menuItemEvent.connect(menuItemEvent); - menuConnected = true; } } function switchAudioDevice(isInput, device) { - if (menuConnected) { - Menu.menuItemEvent.disconnect(menuItemEvent); - menuConnected = false; - } if (isInput) { print("selectAudioDevice: Switching audio INPUT device to:", device); if (AudioDevice.setInputDevice(device)) { @@ -194,11 +169,9 @@ function switchAudioDevice(isInput, device) { print("selectAudioDevice: Error setting audio output device!") } } + + removeAudioMenus(); setupAudioMenus(); - if (!menuConnected) { - Menu.menuItemEvent.connect(menuItemEvent); - menuConnected = true; - } } function restoreAudio() { @@ -215,10 +188,6 @@ function restoreAudio() { } function checkHMDAudio() { - if (menuConnected) { - Menu.menuItemEvent.disconnect(menuItemEvent); - menuConnected = false; - } // HMD Active state is changing; handle switching if (HMD.active != wasHmdActive) { print("selectAudioDevice: HMD Active state changed!"); @@ -257,10 +226,6 @@ function checkHMDAudio() { } } wasHmdActive = HMD.active; - if (!menuConnected) { - Menu.menuItemEvent.connect(menuItemEvent); - menuConnected = true; - } } /**************************************** END FUNCTION DEFINITIONS @@ -271,23 +236,24 @@ function checkHMDAudio() { ****************************************/ // Have a small delay before the menus get setup so the audio devices can switch to the last selected ones Script.setTimeout(function () { - print("selectAudioDevice: Connecting deviceChanged() and displayModeChanged()"); + print("selectAudioDevice: Connecting deviceChanged(), displayModeChanged(), and menuItemEvent()"); AudioDevice.deviceChanged.connect(onDevicechanged); HMD.displayModeChanged.connect(checkHMDAudio); - print ("selectAudioDevice: Checking HMD audio status...") - checkHMDAudio(); + Menu.menuItemEvent.connect(menuItemEvent); print("selectAudioDevice: Setting up Audio > Devices menu for the first time"); setupAudioMenus(); + checkDeviceMismatch(); + print("selectAudioDevice: Checking HMD audio status...") + checkHMDAudio(); }, 3000); -print("selectAudioDevice: Connecting menuItemEvent() and scriptEnding()"); -Menu.menuItemEvent.connect(menuItemEvent); -menuConnected = true; +print("selectAudioDevice: Connecting scriptEnding()"); Script.scriptEnding.connect(function () { restoreAudio(); removeAudioMenus(); Menu.menuItemEvent.disconnect(menuItemEvent); HMD.displayModeChanged.disconnect(checkHMDAudio); + AudioDevice.deviceChanged.disconnect(onDevicechanged); }); }()); // END LOCAL_SCOPE From 59008e4a40015069049cfb776d9b5efc3e38f9de Mon Sep 17 00:00:00 2001 From: humbletim Date: Sat, 25 Mar 2017 18:37:36 -0400 Subject: [PATCH 054/118] remove old ../../qml/menus import --- interface/resources/QtWebEngine/UIDelegates/Menu.qml | 1 - 1 file changed, 1 deletion(-) diff --git a/interface/resources/QtWebEngine/UIDelegates/Menu.qml b/interface/resources/QtWebEngine/UIDelegates/Menu.qml index 5176d9d11e..1bbbbd6cbe 100644 --- a/interface/resources/QtWebEngine/UIDelegates/Menu.qml +++ b/interface/resources/QtWebEngine/UIDelegates/Menu.qml @@ -1,7 +1,6 @@ import QtQuick 2.5 import QtQuick.Controls 1.4 as Controls -import "../../qml/menus" import "../../qml/controls-uit" import "../../qml/styles-uit" From 76657a670c5d19ca3255b9e69c39c80df95567a1 Mon Sep 17 00:00:00 2001 From: David Kelly Date: Mon, 27 Mar 2017 11:28:57 -0700 Subject: [PATCH 055/118] added notifications (faked user name for now), along with switch to avatar entities --- .../src/scripting/WindowScriptingInterface.cpp | 10 +++++++++- interface/src/scripting/WindowScriptingInterface.h | 4 ++++ scripts/system/makeUserConnection.js | 9 ++++++--- scripts/system/notifications.js | 14 +++++++++++++- 4 files changed, 32 insertions(+), 5 deletions(-) diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index 9c1aedf7a0..39c2f2e402 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -235,6 +235,14 @@ void WindowScriptingInterface::shareSnapshot(const QString& path, const QUrl& hr qApp->shareSnapshot(path, href); } +void WindowScriptingInterface::makeConnection(bool success, const QString& userNameOrError) { + if (success) { + emit connectionAdded(userNameOrError); + } else { + emit connectionError(userNameOrError); + } +} + bool WindowScriptingInterface::isPhysicsEnabled() { return qApp->isPhysicsEnabled(); } @@ -255,7 +263,7 @@ int WindowScriptingInterface::openMessageBox(QString title, QString text, int bu } int WindowScriptingInterface::createMessageBox(QString title, QString text, int buttons, int defaultButton) { - auto messageBox = DependencyManager::get()->createMessageBox(OffscreenUi::ICON_INFORMATION, title, text, + auto messageBox = DependencyManager::get()->createMessageBox(OffscreenUi::ICON_INFORMATION, title, text, static_cast>(buttons), static_cast(defaultButton)); connect(messageBox, SIGNAL(selected(int)), this, SLOT(onMessageBoxSelected(int))); diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h index 60d24d50df..b7bed7d85f 100644 --- a/interface/src/scripting/WindowScriptingInterface.h +++ b/interface/src/scripting/WindowScriptingInterface.h @@ -56,6 +56,7 @@ public slots: void showAssetServer(const QString& upload = ""); void copyToClipboard(const QString& text); void takeSnapshot(bool notify = true, bool includeAnimated = false, float aspectRatio = 0.0f); + void makeConnection(bool success, const QString& userNameOrError); void shareSnapshot(const QString& path, const QUrl& href = QUrl("")); bool isPhysicsEnabled(); @@ -74,6 +75,9 @@ signals: void snapshotShared(const QString& error); void processingGif(); + void connectionAdded(const QString& connectionName); + void connectionError(const QString& errorString); + void messageBoxClosed(int id, int button); // triggered when window size or position changes diff --git a/scripts/system/makeUserConnection.js b/scripts/system/makeUserConnection.js index 0b73beaca5..bb3d99ddee 100644 --- a/scripts/system/makeUserConnection.js +++ b/scripts/system/makeUserConnection.js @@ -271,7 +271,8 @@ function updateVisualization() { particleProps = PARTICLE_EFFECT_PROPS; particleProps.isEmitting = 0; particleProps.position = calcParticlePos(myHandPosition, otherHand, otherOrientation); - particleEffect = Entities.addEntity(particleProps); + particleProps.parentID = MyAvatar.sessionUUID; + particleEffect = Entities.addEntity(particleProps, true); } else { particleProps.position = calcParticlePos(myHandPosition, otherHand, otherOrientation); particleProps.isEmitting = 1; @@ -279,10 +280,11 @@ function updateVisualization() { } if (!makingConnectionParticleEffect) { var props = MAKING_CONNECTION_PARTICLE_PROPS; + props.parentID = MyAvatar.sessionUUID; makingConnectionEmitRate = 2000; props.emitRate = makingConnectionEmitRate; props.position = myHandPosition; - makingConnectionParticleEffect = Entities.addEntity(props); + makingConnectionParticleEffect = Entities.addEntity(props, true); } else { makingConnectionEmitRate *= 0.5; Entities.editEntity(makingConnectionParticleEffect, {emitRate: makingConnectionEmitRate, position: myHandPosition, isEmitting: 1}); @@ -485,7 +487,6 @@ function makeConnection(id) { // probably, in which we do this. Controller.triggerHapticPulse(HAPTIC_DATA.background.strength, MAKING_CONNECTION_TIMEOUT, handToHaptic(currentHand)); - // now that we made connection, reset everything makingFriendsTimeout = Script.setTimeout(function () { if (!successfulHandshakeInjector) { successfulHandshakeInjector = Audio.playSound(successfulHandshakeSound, {position: getHandPosition(MyAvatar, currentHand), volume: 0.5, localOnly: true}); @@ -494,6 +495,8 @@ function makeConnection(id) { } Controller.triggerHapticPulse(HAPTIC_DATA.success.strength, HAPTIC_DATA.success.duration, handToHaptic(currentHand)); // don't change state (so animation continues while gripped) + // but do send a notification, by calling the slot that emits the signal for it + Window.makeConnection(true, "otherGuy"); // this is successful, unsucessful would be false, errorString }, MAKING_CONNECTION_TIMEOUT); } diff --git a/scripts/system/notifications.js b/scripts/system/notifications.js index b2ebb1fd46..59384114e0 100644 --- a/scripts/system/notifications.js +++ b/scripts/system/notifications.js @@ -94,11 +94,13 @@ var NotificationType = { LOD_WARNING: 2, CONNECTION_REFUSED: 3, EDIT_ERROR: 4, + CONNECTION: 5, properties: [ { text: "Snapshot" }, { text: "Level of Detail" }, { text: "Connection Refused" }, - { text: "Edit error" } + { text: "Edit error" }, + { text: "Connection" } ], getTypeFromMenuItem: function(menuItemName) { if (menuItemName.substr(menuItemName.length - NOTIFICATION_MENU_ITEM_POST.length) !== NOTIFICATION_MENU_ITEM_POST) { @@ -539,6 +541,14 @@ function processingGif() { createNotification("Processing GIF snapshot...", NotificationType.SNAPSHOT); } +function connectionAdded(connectionName) { + createNotification(wordWrap("Successfully connected to " + connectionName), NotificationType.CONNECTION); +} + +function connectionError(error) { + createNotification(wordWrap("Error trying to make connection: " + error), NotificationType.CONNECTION); +} + // handles mouse clicks on buttons function mousePressEvent(event) { var pickRay, @@ -639,6 +649,8 @@ Menu.menuItemEvent.connect(menuItemEvent); Window.domainConnectionRefused.connect(onDomainConnectionRefused); Window.snapshotTaken.connect(onSnapshotTaken); Window.processingGif.connect(processingGif); +Window.connectionAdded.connect(connectionAdded); +Window.connectionError.connect(connectionError); Window.notifyEditError = onEditError; Window.notify = onNotify; From 85347e32699cf2ed41ed047081b91c0932f0fe75 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Mon, 27 Mar 2017 15:55:05 -0700 Subject: [PATCH 056/118] new location protocol --- interface/src/DiscoverabilityManager.cpp | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/interface/src/DiscoverabilityManager.cpp b/interface/src/DiscoverabilityManager.cpp index 8fcc1e5477..ff30bd1585 100644 --- a/interface/src/DiscoverabilityManager.cpp +++ b/interface/src/DiscoverabilityManager.cpp @@ -40,9 +40,10 @@ void DiscoverabilityManager::updateLocation() { auto accountManager = DependencyManager::get(); auto addressManager = DependencyManager::get(); auto& domainHandler = DependencyManager::get()->getDomainHandler(); + bool discoverable = (_mode.get() != Discoverability::None); - if (_mode.get() != Discoverability::None && accountManager->isLoggedIn()) { + if (accountManager->isLoggedIn()) { // construct a QJsonObject given the user's current address information QJsonObject rootObject; @@ -54,7 +55,7 @@ void DiscoverabilityManager::updateLocation() { locationObject.insert(PATH_KEY_IN_LOCATION, pathString); const QString CONNECTED_KEY_IN_LOCATION = "connected"; - locationObject.insert(CONNECTED_KEY_IN_LOCATION, domainHandler.isConnected()); + locationObject.insert(CONNECTED_KEY_IN_LOCATION, discoverable && domainHandler.isConnected()); if (!addressManager->getRootPlaceID().isNull()) { const QString PLACE_ID_KEY_IN_LOCATION = "place_id"; @@ -140,13 +141,7 @@ void DiscoverabilityManager::setDiscoverabilityMode(Discoverability::Mode discov // update the setting to the new value _mode.set(static_cast(discoverabilityMode)); - if (static_cast(_mode.get()) == Discoverability::None) { - // if we just got set to no discoverability, make sure that we delete our location in DB - removeLocation(); - } else { - // we have a discoverability mode that says we should send a location, do that right away - updateLocation(); - } + updateLocation(); // update right away emit discoverabilityModeChanged(discoverabilityMode); } From 0ae4ce7e985c86f7a600034c29ef176b84493c3a Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Mon, 27 Mar 2017 16:15:11 -0700 Subject: [PATCH 057/118] Have I finally tamed the beast? --- scripts/system/selectAudioDevice.js | 198 +++++++++++++++------------- 1 file changed, 110 insertions(+), 88 deletions(-) diff --git a/scripts/system/selectAudioDevice.js b/scripts/system/selectAudioDevice.js index a2246bf1d5..f3d40820cb 100644 --- a/scripts/system/selectAudioDevice.js +++ b/scripts/system/selectAudioDevice.js @@ -45,73 +45,78 @@ if (typeof String.prototype.trimEndsWith != 'function') { }; } -/**************************************** - VAR DEFINITIONS -****************************************/ +// +// VAR DEFINITIONS +// +var debugPrintStatements = true; const INPUT_DEVICE_SETTING = "audio_input_device"; const OUTPUT_DEVICE_SETTING = "audio_output_device"; var audioDevicesList = []; -// Some HMDs (like Oculus CV1) have a built in audio device. If they -// do, then this function will handle switching to that device automatically -// when you goActive with the HMD active. var wasHmdActive = false; // assume it's not active to start var switchedAudioInputToHMD = false; var switchedAudioOutputToHMD = false; var previousSelectedInputAudioDevice = ""; var previousSelectedOutputAudioDevice = ""; -/**************************************** - BEGIN FUNCTION DEFINITIONS -****************************************/ -function setupAudioMenus() { - Menu.menuItemEvent.disconnect(menuItemEvent); +// +// BEGIN FUNCTION DEFINITIONS +// +function debug() { + if (debugPrintStatements) { + print.apply(null, [].concat.apply(["selectAudioDevice.js:"], [].map.call(arguments, JSON.stringify))); + } +} - /* Setup audio input devices */ + +function setupAudioMenus() { + Menu.menuItemEvent.disconnect(switchAudioDevice); + + removeAudioMenus(); + + // Setup audio input devices Menu.addSeparator("Audio", "Input Audio Device"); var inputDevices = AudioDevice.getInputDevices(); - print("selectAudioDevice: Audio input devices: " + inputDevices); for (var i = 0; i < inputDevices.length; i++) { - var menuItem = "Use " + inputDevices[i] + " for Input"; + var audioDeviceMenuString = "Use " + inputDevices[i] + " for Input"; Menu.addMenuItem({ menuName: "Audio", - menuItemName: menuItem, + menuItemName: audioDeviceMenuString, isCheckable: true, isChecked: inputDevices[i] == AudioDevice.getInputDevice() }); - audioDevicesList.push(menuItem); + audioDevicesList.push(audioDeviceMenuString); } - /* Setup audio output devices */ + // Setup audio output devices Menu.addSeparator("Audio", "Output Audio Device"); var outputDevices = AudioDevice.getOutputDevices(); - print("selectAudioDevice: Audio output devices: " + outputDevices); for (var i = 0; i < outputDevices.length; i++) { - var menuItem = "Use " + outputDevices[i] + " for Output"; + var audioDeviceMenuString = "Use " + outputDevices[i] + " for Output"; Menu.addMenuItem({ menuName: "Audio", - menuItemName: menuItem, + menuItemName: audioDeviceMenuString, isCheckable: true, isChecked: outputDevices[i] == AudioDevice.getOutputDevice() }); - audioDevicesList.push(menuItem); + audioDevicesList.push(audioDeviceMenuString); } - Menu.menuItemEvent.connect(menuItemEvent); + Menu.menuItemEvent.connect(switchAudioDevice); } function checkDeviceMismatch() { var inputDeviceSetting = Settings.getValue(INPUT_DEVICE_SETTING); var interfaceInputDevice = AudioDevice.getInputDevice(); if (interfaceInputDevice != inputDeviceSetting) { - print("selectAudioDevice: Input Setting & Device mismatch! Input SETTING:", inputDeviceSetting, "Input DEVICE IN USE:", AudioDevice.getInputDevice()); - switchAudioDevice(true, inputDeviceSetting); + debug("Input Setting & Device mismatch! Input SETTING: " + inputDeviceSetting + "Input DEVICE IN USE: " + interfaceInputDevice); + switchAudioDevice("Use " + inputDeviceSetting + " for Input"); } var outputDeviceSetting = Settings.getValue(OUTPUT_DEVICE_SETTING); var interfaceOutputDevice = AudioDevice.getOutputDevice(); if (interfaceOutputDevice != outputDeviceSetting) { - print("selectAudioDevice: Output Setting & Device mismatch! Output SETTING:", outputDeviceSetting, "Output DEVICE IN USE:", interfaceOutputDevice); - switchAudioDevice(false, outputDeviceSetting); + debug("Output Setting & Device mismatch! Output SETTING: " + outputDeviceSetting + "Output DEVICE IN USE: " + interfaceOutputDevice); + switchAudioDevice("Use " + outputDeviceSetting + " for Output"); } } @@ -131,129 +136,146 @@ function removeAudioMenus() { } function onDevicechanged() { - print("selectAudioDevice: System audio device changed. Removing and replacing Audio Menus..."); - removeAudioMenus(); + debug("System audio devices changed. Removing and replacing Audio Menus..."); setupAudioMenus(); + AudioDevice.setOutputDevice(AudioDevice.getOutputDevice()); + AudioDevice.setInputDevice(AudioDevice.getInputDevice()); checkDeviceMismatch(); } -function menuItemEvent(menuItem) { - if (menuItem.startsWith("Use ")) { - if (menuItem.endsWith(" for Input")) { - var selectedDevice = menuItem.trimStartsWith("Use ").trimEndsWith(" for Input"); - print("selectAudioDevice: User selected a new Audio Input Device: " + selectedDevice); - switchAudioDevice(true, selectedDevice); - } else if (menuItem.endsWith(" for Output")) { - var selectedDevice = menuItem.trimStartsWith("Use ").trimEndsWith(" for Output"); - print("selectAudioDevice: User selected a new Audio Output Device: " + selectedDevice); - switchAudioDevice(false, selectedDevice); - } else { - print("selectAudioDevice: Invalid Audio menuItem! Doesn't end with 'for Input' or 'for Output'") - } - } -} +function switchAudioDevice(audioDeviceMenuString) { + Menu.menuItemEvent.disconnect(switchAudioDevice); -function switchAudioDevice(isInput, device) { - if (isInput) { - print("selectAudioDevice: Switching audio INPUT device to:", device); - if (AudioDevice.setInputDevice(device)) { - Settings.setValue(INPUT_DEVICE_SETTING, device); + if (audioDeviceMenuString.startsWith("Use ")) { + if (audioDeviceMenuString.endsWith(" for Input")) { + var selectedDevice = audioDeviceMenuString.trimStartsWith("Use ").trimEndsWith(" for Input"); + var currentInputDevice = AudioDevice.getInputDevice(); + if (selectedDevice != currentInputDevice) { + debug("Switching audio INPUT device from " + currentInputDevice + " to " + selectedDevice); + Menu.setIsOptionChecked("Use " + currentInputDevice + " for Input", false); + if (AudioDevice.setInputDevice(selectedDevice)) { + Settings.setValue(INPUT_DEVICE_SETTING, selectedDevice); + Menu.setIsOptionChecked(audioDeviceMenuString, true); + } else { + debug("Error setting audio input device!") + Menu.setIsOptionChecked(audioDeviceMenuString, false); + } + } else { + debug("Selected input device is the same as the current input device!") + Menu.setIsOptionChecked(audioDeviceMenuString, true); + AudioDevice.setInputDevice(selectedDevice); // Still try to force-set the device (in case the user's trying to forcefully debug an issue) + } + } else if (audioDeviceMenuString.endsWith(" for Output")) { + var selectedDevice = audioDeviceMenuString.trimStartsWith("Use ").trimEndsWith(" for Output"); + var currentOutputDevice = AudioDevice.getOutputDevice(); + if (selectedDevice != currentOutputDevice) { + debug("Switching audio OUTPUT device from " + currentOutputDevice + " to " + selectedDevice); + Menu.setIsOptionChecked("Use " + currentOutputDevice + " for Output", false); + if (AudioDevice.setOutputDevice(selectedDevice)) { + Settings.setValue(OUTPUT_DEVICE_SETTING, selectedDevice); + Menu.setIsOptionChecked(audioDeviceMenuString, true); + } else { + debug("Error setting audio output device!") + Menu.setIsOptionChecked(audioDeviceMenuString, false); + } + } else { + debug("Selected output device is the same as the current output device!") + Menu.setIsOptionChecked(audioDeviceMenuString, true); + AudioDevice.setOutputDevice(selectedDevice); // Still try to force-set the device (in case the user's trying to forcefully debug an issue) + } } else { - print("selectAudioDevice: Error setting audio input device!") - } - } else { - print("selectAudioDevice: Switching audio OUTPUT device to:", device); - if (AudioDevice.setOutputDevice(device)) { - Settings.setValue(OUTPUT_DEVICE_SETTING, device); - } else { - print("selectAudioDevice: Error setting audio output device!") + debug("Invalid Audio audioDeviceMenuString! Doesn't end with 'for Input' or 'for Output'") } } - removeAudioMenus(); - setupAudioMenus(); + Menu.menuItemEvent.connect(switchAudioDevice); } function restoreAudio() { if (switchedAudioInputToHMD) { - print("selectAudioDevice: Switching back from HMD preferred audio input to: " + previousSelectedInputAudioDevice); - switchAudioDevice(true, previousSelectedInputAudioDevice); + debug("Switching back from HMD preferred audio input to: " + previousSelectedInputAudioDevice); + switchAudioDevice("Use " + previousSelectedInputAudioDevice + " for Input"); switchedAudioInputToHMD = false; } if (switchedAudioOutputToHMD) { - print("selectAudioDevice: Switching back from HMD preferred audio output to: " + previousSelectedOutputAudioDevice); - switchAudioDevice(false, previousSelectedOutputAudioDevice); + debug("Switching back from HMD preferred audio output to: " + previousSelectedOutputAudioDevice); + switchAudioDevice("Use " + previousSelectedOutputAudioDevice + " for Output"); switchedAudioOutputToHMD = false; } } +// Some HMDs (like Oculus CV1) have a built in audio device. If they +// do, then this function will handle switching to that device automatically +// when you goActive with the HMD active. function checkHMDAudio() { // HMD Active state is changing; handle switching if (HMD.active != wasHmdActive) { - print("selectAudioDevice: HMD Active state changed!"); + debug("HMD Active state changed!"); // We're putting the HMD on; switch to those devices if (HMD.active) { - print("selectAudioDevice: HMD is now Active."); + debug("HMD is now Active."); var hmdPreferredAudioInput = HMD.preferredAudioInput(); var hmdPreferredAudioOutput = HMD.preferredAudioOutput(); - print("selectAudioDevice: hmdPreferredAudioInput: " + hmdPreferredAudioInput); - print("selectAudioDevice: hmdPreferredAudioOutput: " + hmdPreferredAudioOutput); + debug("hmdPreferredAudioInput: " + hmdPreferredAudioInput); + debug("hmdPreferredAudioOutput: " + hmdPreferredAudioOutput); if (hmdPreferredAudioInput !== "") { - print("selectAudioDevice: HMD has preferred audio input device."); + debug("HMD has preferred audio input device."); previousSelectedInputAudioDevice = Settings.getValue(INPUT_DEVICE_SETTING); - print("selectAudioDevice: previousSelectedInputAudioDevice: " + previousSelectedInputAudioDevice); + debug("previousSelectedInputAudioDevice: " + previousSelectedInputAudioDevice); if (hmdPreferredAudioInput != previousSelectedInputAudioDevice) { - print("selectAudioDevice: Switching Audio Input device to HMD preferred device: " + hmdPreferredAudioInput); switchedAudioInputToHMD = true; - switchAudioDevice(true, hmdPreferredAudioInput); + switchAudioDevice("Use " + hmdPreferredAudioInput + " for Input"); } } if (hmdPreferredAudioOutput !== "") { - print("selectAudioDevice: HMD has preferred audio output device."); + debug("HMD has preferred audio output device."); previousSelectedOutputAudioDevice = Settings.getValue(OUTPUT_DEVICE_SETTING); - print("selectAudioDevice: previousSelectedOutputAudioDevice: " + previousSelectedOutputAudioDevice); + debug("previousSelectedOutputAudioDevice: " + previousSelectedOutputAudioDevice); if (hmdPreferredAudioOutput != previousSelectedOutputAudioDevice) { - print("selectAudioDevice: Switching Audio Output device to HMD preferred device: " + hmdPreferredAudioOutput); switchedAudioOutputToHMD = true; - switchAudioDevice(false, hmdPreferredAudioOutput); + switchAudioDevice("Use " + hmdPreferredAudioOutput + " for Output"); } } } else { - print("selectAudioDevice: HMD no longer active. Restoring audio I/O devices..."); + debug("HMD no longer active. Restoring audio I/O devices..."); restoreAudio(); } } wasHmdActive = HMD.active; } -/**************************************** - END FUNCTION DEFINITIONS -****************************************/ +// +// END FUNCTION DEFINITIONS +// -/**************************************** - BEGIN SCRIPT BODY -****************************************/ -// Have a small delay before the menus get setup so the audio devices can switch to the last selected ones +// +// BEGIN SCRIPT BODY +// +// Wait for the C++ systems to fire up before trying to do anything with audio devices Script.setTimeout(function () { - print("selectAudioDevice: Connecting deviceChanged(), displayModeChanged(), and menuItemEvent()"); + debug("Connecting deviceChanged(), displayModeChanged(), and switchAudioDevice()..."); AudioDevice.deviceChanged.connect(onDevicechanged); HMD.displayModeChanged.connect(checkHMDAudio); - Menu.menuItemEvent.connect(menuItemEvent); - print("selectAudioDevice: Setting up Audio > Devices menu for the first time"); + Menu.menuItemEvent.connect(switchAudioDevice); + debug("Setting up Audio I/O menu for the first time..."); setupAudioMenus(); checkDeviceMismatch(); - print("selectAudioDevice: Checking HMD audio status...") + debug("Checking HMD audio status...") checkHMDAudio(); }, 3000); -print("selectAudioDevice: Connecting scriptEnding()"); +debug("Connecting scriptEnding()"); Script.scriptEnding.connect(function () { restoreAudio(); removeAudioMenus(); - Menu.menuItemEvent.disconnect(menuItemEvent); + Menu.menuItemEvent.disconnect(switchAudioDevice); HMD.displayModeChanged.disconnect(checkHMDAudio); AudioDevice.deviceChanged.disconnect(onDevicechanged); }); +// +// END SCRIPT BODY +// + }()); // END LOCAL_SCOPE From fce2049630b4ca8d3961adf39fe385e0aca324c7 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Mon, 27 Mar 2017 16:44:40 -0700 Subject: [PATCH 058/118] Trivial removal of commented code --- interface/resources/qml/controls-uit/Table.qml | 1 - 1 file changed, 1 deletion(-) diff --git a/interface/resources/qml/controls-uit/Table.qml b/interface/resources/qml/controls-uit/Table.qml index 21bd8b60dc..11d1920f95 100644 --- a/interface/resources/qml/controls-uit/Table.qml +++ b/interface/resources/qml/controls-uit/Table.qml @@ -90,7 +90,6 @@ TableView { Rectangle { color: "#00000000" anchors { fill: parent; margins: -2 } - //radius: hifi.dimensions.borderRadius border.color: isLightColorScheme ? hifi.colors.lightGrayText : hifi.colors.baseGrayHighlight border.width: 2 } From 92718b60515aaac31d95a08d25a2d610798be844 Mon Sep 17 00:00:00 2001 From: David Kelly Date: Tue, 28 Mar 2017 08:57:27 -0700 Subject: [PATCH 059/118] cr feedback --- scripts/system/makeUserConnection.js | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/scripts/system/makeUserConnection.js b/scripts/system/makeUserConnection.js index bb3d99ddee..fe51345233 100644 --- a/scripts/system/makeUserConnection.js +++ b/scripts/system/makeUserConnection.js @@ -11,10 +11,9 @@ // (function() { // BEGIN LOCAL_SCOPE -const version = 0.1; const label = "makeUserConnection"; -const MAX_AVATAR_DISTANCE = 0.2; -const GRIP_MIN = 0.05; +const MAX_AVATAR_DISTANCE = 0.2; // m +const GRIP_MIN = 0.05; // goes from 0-1, so 5% pressed is pressed const MESSAGE_CHANNEL = "io.highfidelity.makeUserConnection"; const STATES = { inactive : 0, @@ -27,14 +26,14 @@ const WAITING_INTERVAL = 100; // ms const CONNECTING_INTERVAL = 100; // ms const MAKING_CONNECTION_TIMEOUT = 800; // ms const CONNECTING_TIME = 1600; // ms -const PARTICLE_RADIUS = 0.15; +const PARTICLE_RADIUS = 0.15; // m const PARTICLE_ANGLE_INCREMENT = 360/45; // 1hz const HANDSHAKE_SOUND_URL = "https://s3-us-west-1.amazonaws.com/hifi-content/davidkelly/production/audio/4beat_sweep.wav"; const SUCCESSFUL_HANDSHAKE_SOUND_URL = "https://s3-us-west-1.amazonaws.com/hifi-content/davidkelly/production/audio/3rdbeat_success_bell.wav"; const HAPTIC_DATA = { - initial: { duration: 20, strength: 0.6}, - background: { duration: 100, strength: 0.3 }, - success: { duration: 60, strength: 1.0} + initial: { duration: 20, strength: 0.6}, // duration is in ms + background: { duration: 100, strength: 0.3 }, // duration is in ms + success: { duration: 60, strength: 1.0} // duration is in ms }; const PARTICLE_EFFECT_PROPS = { "alpha": 0.8, @@ -115,9 +114,8 @@ var successfulHandshakeSound; function debug() { var stateString = "<" + STATE_STRINGS[state] + ">"; - var versionString = "v" + version; var connecting = "[" + connectingId + "/" + connectingHand + "]"; - print.apply(null, [].concat.apply([label, versionString, stateString, JSON.stringify(waitingList), connecting], [].map.call(arguments, JSON.stringify))); + print.apply(null, [].concat.apply([label, stateString, JSON.stringify(waitingList), connecting], [].map.call(arguments, JSON.stringify))); } function handToString(hand) { @@ -126,6 +124,7 @@ function handToString(hand) { } else if (hand === Controller.Standard.LeftHand) { return "LeftHand"; } + debug("handToString called without valid hand!"); return ""; } @@ -145,6 +144,7 @@ function handToHaptic(hand) { } else if (hand === Controller.Standard.LeftHand) { return 0; } + debug("handToHaptic called without a valid hand!"); return -1; } @@ -261,7 +261,7 @@ function updateVisualization() { break; case STATES.connecting: var particleProps = {}; - // put the position between the 2 hands, if we have a ingId. This + // put the position between the 2 hands, if we have a connectingId. This // helps define the plane in which the particles move. positionFractionallyTowards(myHandPosition, otherHand, 0.5); // now manage the rest of the entity @@ -451,8 +451,6 @@ function lookForWaitingAvatar() { waitingInterval = Script.setInterval(function () { if (state == STATES.waiting && !connectingId) { // find the closest in-range avatar, and send connection request - // TODO: this is same code as in startHandshake - get this - // cleaned up. var nearestAvatar = findNearestWaitingAvatar(); if (nearestAvatar.avatar) { connectingId = nearestAvatar.avatar; @@ -519,7 +517,7 @@ function startConnecting(id, hand) { handshakeInjector.restart(); } - // send message that we are ing them + // send message that we are connecting with them messageSend({ key: "connecting", id: id, From 1bdef141c3626ea1bd9ab7d8c4140be8c56414b5 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Tue, 28 Mar 2017 15:33:54 -0400 Subject: [PATCH 060/118] simplify audio menu parsing --- scripts/system/selectAudioDevice.js | 110 ++++++++++++---------------- 1 file changed, 47 insertions(+), 63 deletions(-) diff --git a/scripts/system/selectAudioDevice.js b/scripts/system/selectAudioDevice.js index f3d40820cb..bc7906712a 100644 --- a/scripts/system/selectAudioDevice.js +++ b/scripts/system/selectAudioDevice.js @@ -15,34 +15,19 @@ (function() { // BEGIN LOCAL_SCOPE -if (typeof String.prototype.startsWith != 'function') { - String.prototype.startsWith = function (str){ - return this.slice(0, str.length) == str; - }; -} - -if (typeof String.prototype.endsWith != 'function') { - String.prototype.endsWith = function (str){ - return this.slice(-str.length) == str; - }; -} - -if (typeof String.prototype.trimStartsWith != 'function') { - String.prototype.trimStartsWith = function (str){ - if (this.startsWith(str)) { - return this.substr(str.length); - } - return this; - }; -} - -if (typeof String.prototype.trimEndsWith != 'function') { - String.prototype.trimEndsWith = function (str){ - if (this.endsWith(str)) { - return this.substr(0,this.length - str.length); - } - return this; - }; +const INPUT = "Input"; +const OUTPUT = "Output"; +function parseMenuItem(item) { + const USE = "Use "; + const FOR_INPUT = " for " + INPUT; + const FOR_OUTPUT = " for " + OUTPUT; + if (item.slice(0, USE.length) == USE) { + if (item.slice(-FOR_INPUT.length) == FOR_INPUT) { + return { device: item.slice(USE.length, -FOR_INPUT.length), mode: INPUT }; + } else if (item.slice(-FOR_OUTPUT.length) == FOR_OUTPUT) { + return { device: item.slice(USE.length, -FOR_OUTPUT.length), mode: OUTPUT }; + } + } } // @@ -146,45 +131,44 @@ function onDevicechanged() { function switchAudioDevice(audioDeviceMenuString) { Menu.menuItemEvent.disconnect(switchAudioDevice); - if (audioDeviceMenuString.startsWith("Use ")) { - if (audioDeviceMenuString.endsWith(" for Input")) { - var selectedDevice = audioDeviceMenuString.trimStartsWith("Use ").trimEndsWith(" for Input"); - var currentInputDevice = AudioDevice.getInputDevice(); - if (selectedDevice != currentInputDevice) { - debug("Switching audio INPUT device from " + currentInputDevice + " to " + selectedDevice); - Menu.setIsOptionChecked("Use " + currentInputDevice + " for Input", false); - if (AudioDevice.setInputDevice(selectedDevice)) { - Settings.setValue(INPUT_DEVICE_SETTING, selectedDevice); - Menu.setIsOptionChecked(audioDeviceMenuString, true); - } else { - debug("Error setting audio input device!") - Menu.setIsOptionChecked(audioDeviceMenuString, false); - } - } else { - debug("Selected input device is the same as the current input device!") + var selection = parseMenuItem(audioDeviceMenuString); + if (!selection) { + debug("Invalid Audio audioDeviceMenuString! Doesn't end with 'for Input' or 'for Output'") + } else if (selection.mode == INPUT) { + var selectedDevice = selection.device; + var currentInputDevice = AudioDevice.getInputDevice(); + if (selectedDevice != currentInputDevice) { + debug("Switching audio INPUT device from " + currentInputDevice + " to " + selectedDevice); + Menu.setIsOptionChecked("Use " + currentInputDevice + " for Input", false); + if (AudioDevice.setInputDevice(selectedDevice)) { + Settings.setValue(INPUT_DEVICE_SETTING, selectedDevice); Menu.setIsOptionChecked(audioDeviceMenuString, true); - AudioDevice.setInputDevice(selectedDevice); // Still try to force-set the device (in case the user's trying to forcefully debug an issue) - } - } else if (audioDeviceMenuString.endsWith(" for Output")) { - var selectedDevice = audioDeviceMenuString.trimStartsWith("Use ").trimEndsWith(" for Output"); - var currentOutputDevice = AudioDevice.getOutputDevice(); - if (selectedDevice != currentOutputDevice) { - debug("Switching audio OUTPUT device from " + currentOutputDevice + " to " + selectedDevice); - Menu.setIsOptionChecked("Use " + currentOutputDevice + " for Output", false); - if (AudioDevice.setOutputDevice(selectedDevice)) { - Settings.setValue(OUTPUT_DEVICE_SETTING, selectedDevice); - Menu.setIsOptionChecked(audioDeviceMenuString, true); - } else { - debug("Error setting audio output device!") - Menu.setIsOptionChecked(audioDeviceMenuString, false); - } } else { - debug("Selected output device is the same as the current output device!") - Menu.setIsOptionChecked(audioDeviceMenuString, true); - AudioDevice.setOutputDevice(selectedDevice); // Still try to force-set the device (in case the user's trying to forcefully debug an issue) + debug("Error setting audio input device!") + Menu.setIsOptionChecked(audioDeviceMenuString, false); } } else { - debug("Invalid Audio audioDeviceMenuString! Doesn't end with 'for Input' or 'for Output'") + debug("Selected input device is the same as the current input device!") + Menu.setIsOptionChecked(audioDeviceMenuString, true); + AudioDevice.setInputDevice(selectedDevice); // Still try to force-set the device (in case the user's trying to forcefully debug an issue) + } + } else if (selection.mode == OUTPUT) { + var selectedDevice = selection.device; + var currentOutputDevice = AudioDevice.getOutputDevice(); + if (selectedDevice != currentOutputDevice) { + debug("Switching audio OUTPUT device from " + currentOutputDevice + " to " + selectedDevice); + Menu.setIsOptionChecked("Use " + currentOutputDevice + " for Output", false); + if (AudioDevice.setOutputDevice(selectedDevice)) { + Settings.setValue(OUTPUT_DEVICE_SETTING, selectedDevice); + Menu.setIsOptionChecked(audioDeviceMenuString, true); + } else { + debug("Error setting audio output device!") + Menu.setIsOptionChecked(audioDeviceMenuString, false); + } + } else { + debug("Selected output device is the same as the current output device!") + Menu.setIsOptionChecked(audioDeviceMenuString, true); + AudioDevice.setOutputDevice(selectedDevice); // Still try to force-set the device (in case the user's trying to forcefully debug an issue) } } From 5e0cfb61376a3a0a7a5876fd0afecef0d5d697f0 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Tue, 28 Mar 2017 15:34:13 -0400 Subject: [PATCH 061/118] short-circuit audio switching on missing device --- scripts/system/selectAudioDevice.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/scripts/system/selectAudioDevice.js b/scripts/system/selectAudioDevice.js index bc7906712a..48ab7fea7f 100644 --- a/scripts/system/selectAudioDevice.js +++ b/scripts/system/selectAudioDevice.js @@ -129,6 +129,11 @@ function onDevicechanged() { } function switchAudioDevice(audioDeviceMenuString) { + // if the device is not plugged in, short-circuit + if (!~audioDevicesList.indexOf(audioDeviceMenuString)) { + return; + } + Menu.menuItemEvent.disconnect(switchAudioDevice); var selection = parseMenuItem(audioDeviceMenuString); From 06bf5807ae3a0017832db495616d85a518da35e0 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Tue, 28 Mar 2017 15:34:42 -0400 Subject: [PATCH 062/118] fix audio setting persistence for already selected device --- scripts/system/selectAudioDevice.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/system/selectAudioDevice.js b/scripts/system/selectAudioDevice.js index 48ab7fea7f..5d1d61b7d3 100644 --- a/scripts/system/selectAudioDevice.js +++ b/scripts/system/selectAudioDevice.js @@ -154,6 +154,7 @@ function switchAudioDevice(audioDeviceMenuString) { } } else { debug("Selected input device is the same as the current input device!") + Settings.setValue(INPUT_DEVICE_SETTING, selectedDevice); Menu.setIsOptionChecked(audioDeviceMenuString, true); AudioDevice.setInputDevice(selectedDevice); // Still try to force-set the device (in case the user's trying to forcefully debug an issue) } @@ -172,6 +173,7 @@ function switchAudioDevice(audioDeviceMenuString) { } } else { debug("Selected output device is the same as the current output device!") + Settings.setValue(OUTPUT_DEVICE_SETTING, selectedDevice); Menu.setIsOptionChecked(audioDeviceMenuString, true); AudioDevice.setOutputDevice(selectedDevice); // Still try to force-set the device (in case the user's trying to forcefully debug an issue) } From c2a49a582e6088c11ebb39b1183f4030e4dd4e28 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Tue, 28 Mar 2017 16:44:16 -0700 Subject: [PATCH 063/118] send credentials to local metaverse servers, and initial users data integration. --- .../HFWebEngineRequestInterceptor.cpp | 6 +- scripts/system/pal.js | 84 +++++-------------- 2 files changed, 27 insertions(+), 63 deletions(-) diff --git a/interface/src/networking/HFWebEngineRequestInterceptor.cpp b/interface/src/networking/HFWebEngineRequestInterceptor.cpp index 9c3f0b232e..f6b0914f08 100644 --- a/interface/src/networking/HFWebEngineRequestInterceptor.cpp +++ b/interface/src/networking/HFWebEngineRequestInterceptor.cpp @@ -10,6 +10,7 @@ // #include "HFWebEngineRequestInterceptor.h" +#include "NetworkingConstants.h" #include @@ -20,8 +21,11 @@ bool isAuthableHighFidelityURL(const QUrl& url) { "highfidelity.com", "highfidelity.io", "metaverse.highfidelity.com", "metaverse.highfidelity.io" }; + const auto& scheme = url.scheme(); + const auto& host = url.host(); - return url.scheme() == "https" && HF_HOSTS.contains(url.host()); + return (scheme == "https" && HF_HOSTS.contains(host)) || + ((scheme == NetworkingConstants::METAVERSE_SERVER_URL.scheme()) && (host == NetworkingConstants::METAVERSE_SERVER_URL.host())); } void HFWebEngineRequestInterceptor::interceptRequest(QWebEngineUrlRequestInfo& info) { diff --git a/scripts/system/pal.js b/scripts/system/pal.js index a7c4f56ea6..80df8413c3 100644 --- a/scripts/system/pal.js +++ b/scripts/system/pal.js @@ -334,75 +334,35 @@ function getProfilePicture(username, callback) { // callback(url) if successfull function getAvailableConnections(domain, callback) { // callback([{usename, location}...]) if successfull. (Logs otherwise) // The back end doesn't do user connections yet. Fake it by getting all users that have made themselves accessible to us, // and pretending that they are all connections. - function getData(cb) { - requestJSON(METAVERSE_BASE + '/api/v1/users?status=online', function (connectionsData) { - - // The above does not include friend status. Fetch that separately. - requestJSON(METAVERSE_BASE + '/api/v1/user/friends', function (friendsData) { - var users = connectionsData.users || [], friends = friendsData.friends || []; - users.forEach(function (user) { - user.connection = (friends.indexOf(user.username) < 0) ? 'connection' : 'friend'; - }); - - // The back end doesn't include the profile picture data, but we can add that here. - // For our current purposes, there's no need to be fancy and try to reduce latency by doing some number of requests in parallel, - // so these requests are all sequential. - function addPicture(index) { - if (index >= users.length) { - return cb(users); - } - var user = users[index]; - getProfilePicture(user.username, function (url) { - user.profileUrl = url; - addPicture(index + 1); - }); - } - addPicture(0); - }); - }); - } - + url = METAVERSE_BASE + '/api/v1/users?' if (domain) { - // The back end doesn't keep sessionUUID in the location data yet. Fake it by finding the avatar closest to the path. - var positions = {}; - AvatarList.getAvatarIdentifiers().forEach(function (id) { - positions[id || ''] = AvatarList.getAvatar(id).position; // Don't use null id as a key. Properties must be a string, and we don't want 'null'. - }); - getData(function (users) { - // The endpoint in getData doesn't take a domain filter. So filter out the unwanted stuff now. - domain = domain.slice(1, -1); // without curly braces - users = users.filter(function (user) { return (user.location.domain || (user.location.root && user.location.root.domain) || {}).id === domain; }); - - // Now fill in the sessionUUID as if it were in the data all along. - users.forEach(function (user) { - var coordinates = user.location.path.match(/\/([^,]+)\,([^,]+),([^\/]+)\//); - if (coordinates) { - var position = {x: Number(coordinates[1]), y: Number(coordinates[2]), z: Number(coordinates[3])}; - var none = 'not found', closestId = none, bestDistance = Infinity, distance, id; - for (id in positions) { - distance = Vec3.distance(position, positions[id]); - if (distance < bestDistance) { - closestId = id; - bestDistance = distance; - } - } - if (closestId !== none) { - user.location.sessionUUID = closestId; - } - } - }); - - callback(users); - }); - } else { // We don't need to filter, nor add any sessionUUID data - getData(callback); + url += 'status=' + domain.slice(1, -1); // without curly braces + } else { + url += 'filter=connections'; // regardless of whether online } + requestJSON(url, function (connectionsData) { + // The back end doesn't include the profile picture data, but we can add that here. + // For our current purposes, there's no need to be fancy and try to reduce latency by doing some number of requests in parallel, + // so these requests are all sequential. + var users = connectionsData.users; + function addPicture(index) { + if (index >= users.length) { + return callback(users); + } + var user = users[index]; + getProfilePicture(user.username, function (url) { + user.profileUrl = url; + addPicture(index + 1); + }); + } + addPicture(0); + }); } function getConnectionData(domain) { // Update all the usernames that I am entitled to see, using my login but not dependent on canKick. function frob(user) { // get into the right format return { - sessionId: user.location.sessionUUID || '', + sessionId: user.location.node_id || '', userName: user.username, connection: user.connection, profileUrl: user.profileUrl, From b5ba4a109d56b698450b41a8e19cbcdb4367e03f Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Tue, 28 Mar 2017 17:37:34 -0700 Subject: [PATCH 064/118] Merge from upstream/master --- cmake/externals/bullet/CMakeLists.txt | 6 +- cmake/externals/faceshift/CMakeLists.txt | 4 + cmake/externals/polyvox/CMakeLists.txt | 12 +- cmake/externals/vhacd/CMakeLists.txt | 10 +- cmake/macros/LinkHifiLibraries.cmake | 2 +- cmake/modules/FindKinect.cmake | 4 +- .../html/img/controls-help-oculus.png | Bin 125680 -> 127440 bytes .../html/img/tablet-help-gamepad.jpg | Bin 0 -> 259716 bytes .../html/img/tablet-help-keyboard.jpg | Bin 0 -> 190462 bytes .../resources/html/img/tablet-help-oculus.jpg | Bin 0 -> 258949 bytes .../resources/html/img/tablet-help-vive.jpg | Bin 0 -> 216386 bytes interface/resources/html/tabletHelp.html | 157 +++ .../icons/create-icons/20-text-01.svg | 26 + .../icons/create-icons/21-cube-01.svg | 17 + .../icons/create-icons/22-sphere-01.svg | 21 + .../icons/create-icons/23-zone-01.svg | 38 + .../icons/create-icons/24-light-01.svg | 34 + .../icons/create-icons/25-web-1-01.svg | 30 + .../icons/create-icons/90-particles-01.svg | 29 + .../icons/create-icons/94-model-01.svg | 20 + interface/resources/images/sphere-01.svg | 108 ++ interface/resources/qml/AssetServer.qml | 2 +- .../TabletLoginDialog/CompleteProfileBody.qml | 124 ++ .../qml/TabletLoginDialog/LinkAccountBody.qml | 296 +++++ .../qml/TabletLoginDialog/SignInBody.qml | 109 ++ .../qml/TabletLoginDialog/SignUpBody.qml | 276 +++++ .../UsernameCollisionBody.qml | 157 +++ .../qml/TabletLoginDialog/WelcomeBody.qml | 79 ++ .../qml/controls-uit/AttachmentsTable.qml | 2 +- .../qml/controls-uit/BaseWebView.qml | 2 +- .../resources/qml/controls-uit/CheckBox.qml | 2 + .../resources/qml/controls-uit/ComboBox.qml | 32 +- .../resources/qml/controls-uit/Slider.qml | 2 +- .../qml/controls-uit/TabletHeader.qml | 35 + .../qml/dialogs/CustomQueryDialog.qml | 9 +- .../qml/dialogs/TabletCustomQueryDialog.qml | 355 ++++++ .../qml/dialogs/TabletFileDialog.qml | 782 +++++++++++++ .../qml/dialogs/TabletLoginDialog.qml | 113 ++ .../qml/dialogs/TabletMessageBox.qml | 249 ++++ .../qml/dialogs/TabletQueryDialog.qml | 206 ++++ .../dialogs/preferences/AvatarPreference.qml | 26 +- interface/resources/qml/hifi/Audio.qml | 253 ++++ interface/resources/qml/hifi/Card.qml | 15 +- .../resources/qml/hifi/TabletTextButton.qml | 58 + .../qml/hifi/components/AudioCheckbox.qml | 29 + .../qml/hifi/dialogs/AttachmentsDialog.qml | 213 +--- .../hifi/dialogs/GeneralPreferencesDialog.qml | 2 +- .../qml/hifi/dialogs/ModelBrowserDialog.qml | 94 +- .../qml/hifi/dialogs/TabletAssetServer.qml | 614 ++++++++++ .../qml/hifi/dialogs/TabletDCDialog.qml | 160 +++ .../qml/hifi/dialogs/TabletDebugWindow.qml | 78 ++ .../hifi/dialogs/TabletEntityStatistics.qml | 244 ++++ .../dialogs/TabletEntityStatisticsItem.qml | 49 + .../qml/hifi/dialogs/TabletLODTools.qml | 119 ++ .../qml/hifi/dialogs/TabletRunningScripts.qml | 376 ++++++ .../hifi/dialogs/attachments/Attachment.qml | 50 +- .../qml/hifi/dialogs/attachments/Vector3.qml | 32 +- .../dialogs/content/AttachmentsContent.qml | 260 +++++ .../dialogs/content/ModelBrowserContent.qml | 64 ++ interface/resources/qml/hifi/tablet/Edit.qml | 299 +++++ .../qml/hifi/tablet/NewEntityButton.qml | 160 +++ .../qml/hifi/tablet/NewModelDialog.qml | 158 +++ .../qml/hifi/tablet/TabletAddressDialog.qml | 294 ++--- .../hifi/tablet/TabletAttachmentsDialog.qml | 105 ++ .../hifi/tablet/TabletAudioPreferences.qml | 38 + .../hifi/tablet/TabletAvatarPreferences.qml | 38 + ...tings.qml => TabletGeneralPreferences.qml} | 30 +- .../hifi/tablet/TabletGraphicsPreferences.qml | 38 + .../qml/hifi/tablet/TabletLodPreferences.qml | 38 + .../resources/qml/hifi/tablet/TabletMenu.qml | 21 +- .../qml/hifi/tablet/TabletMenuItem.qml | 6 +- ...etMouseHandler.qml => TabletMenuStack.qml} | 64 +- .../hifi/tablet/TabletModelBrowserDialog.qml | 87 ++ .../tablet/TabletNetworkingPreferences.qml | 38 + .../resources/qml/hifi/tablet/TabletRoot.qml | 44 +- .../qml/hifi/tablet/TabletStoryCard.qml | 132 +++ .../tablet/tabletWindows/TabletFileDialog.qml | 2 +- .../tabletWindows/TabletPreferencesDialog.qml | 105 +- .../preferences/TabletAvatarBrowser.qml | 116 ++ .../resources/qml/hifi/toolbars/Toolbar.qml | 4 +- .../qml/styles-uit/FiraSansSemiBold.qml | 2 +- .../resources/qml/styles-uit/HiFiGlyphs.qml | 2 +- .../qml/styles-uit/HifiConstants.qml | 1 + .../resources/qml/styles-uit/RalewayBold.qml | 2 +- .../qml/styles-uit/RalewayRegular.qml | 2 +- .../qml/styles-uit/RalewaySemiBold.qml | 2 +- .../qml/windows/TabletModalFrame.qml | 89 ++ .../qml/windows/TabletModalWindow.qml | 22 + interface/resources/qml/windows/Window.qml | 4 +- interface/src/Application.cpp | 147 ++- interface/src/Application.h | 6 + interface/src/Menu.cpp | 44 +- interface/src/Menu.h | 1 + interface/src/avatar/MyAvatar.cpp | 9 + interface/src/avatar/MyAvatar.h | 2 + .../AssetMappingsScriptingInterface.h | 8 +- .../AudioDeviceScriptingInterface.cpp | 29 +- .../scripting/AudioDeviceScriptingInterface.h | 14 + .../src/scripting/HMDScriptingInterface.cpp | 2 +- interface/src/ui/AvatarInputs.cpp | 25 +- interface/src/ui/AvatarInputs.h | 10 +- interface/src/ui/DialogsManager.cpp | 5 +- interface/src/ui/DomainConnectionModel.cpp | 101 ++ interface/src/ui/DomainConnectionModel.h | 47 + interface/src/ui/LoginDialog.cpp | 43 +- interface/src/ui/LoginDialog.h | 1 + interface/src/ui/OctreeStatsProvider.cpp | 377 ++++++ interface/src/ui/OctreeStatsProvider.h | 154 +++ interface/src/ui/PreferencesDialog.cpp | 6 +- interface/src/ui/overlays/Overlay.cpp | 3 + interface/src/ui/overlays/Overlays.cpp | 7 +- interface/src/ui/overlays/Web3DOverlay.cpp | 164 ++- interface/src/ui/overlays/Web3DOverlay.h | 12 + libraries/animation/src/AnimBlendLinear.cpp | 14 +- libraries/animation/src/AnimBlendLinear.h | 4 +- .../animation/src/AnimBlendLinearMove.cpp | 14 +- libraries/animation/src/AnimBlendLinearMove.h | 4 +- libraries/animation/src/AnimClip.cpp | 2 +- libraries/animation/src/AnimClip.h | 2 +- libraries/animation/src/AnimContext.cpp | 16 + libraries/animation/src/AnimContext.h | 30 + .../animation/src/AnimInverseKinematics.cpp | 28 +- .../animation/src/AnimInverseKinematics.h | 5 +- libraries/animation/src/AnimManipulator.cpp | 6 +- libraries/animation/src/AnimManipulator.h | 4 +- libraries/animation/src/AnimNode.h | 8 +- libraries/animation/src/AnimOverlay.cpp | 6 +- libraries/animation/src/AnimOverlay.h | 2 +- libraries/animation/src/AnimStateMachine.cpp | 14 +- libraries/animation/src/AnimStateMachine.h | 4 +- libraries/animation/src/Rig.cpp | 9 +- libraries/animation/src/Rig.h | 5 +- .../src/EntityTreeRenderer.cpp | 36 +- .../src/RenderableModelEntityItem.cpp | 2 +- .../src/RenderablePolyVoxEntityItem.cpp | 80 +- .../src/RenderablePolyVoxEntityItem.h | 9 +- libraries/entities/CMakeLists.txt | 2 +- libraries/entities/src/EntityItem.h | 6 +- .../entities/src/EntityScriptingInterface.cpp | 70 +- .../entities/src/EntityScriptingInterface.h | 14 +- libraries/entities/src/PolyVoxEntityItem.cpp | 4 - libraries/entities/src/PolyVoxEntityItem.h | 2 - libraries/fbx/src/FBXReader.cpp | 76 +- libraries/fbx/src/FBXReader.h | 10 +- libraries/fbx/src/FBXReader_Mesh.cpp | 38 +- libraries/fbx/src/OBJWriter.cpp | 36 +- libraries/gl/src/gl/OffscreenQmlSurface.cpp | 27 +- libraries/gpu-gl/CMakeLists.txt | 3 + libraries/gpu-gl/src/gpu/gl/GLTexture.cpp | 2 - .../gpu-gl/src/gpu/gl41/GL41BackendInput.cpp | 9 +- .../src/gpu/gl41/GL41BackendTexture.cpp | 2 +- .../gpu-gl/src/gpu/gl45/GL45BackendInput.cpp | 7 +- .../src/gpu/gl45/GL45BackendTexture.cpp | 6 +- .../gpu/gl45/GL45BackendVariableTexture.cpp | 10 +- libraries/gpu/src/gpu/Format.h | 12 +- libraries/gpu/src/gpu/Inputs.slh | 2 +- libraries/gpu/src/gpu/Texture.h | 13 +- libraries/gpu/src/gpu/Texture_ktx.cpp | 2 +- .../src/input-plugins/KeyboardMouseDevice.cpp | 4 +- .../src/model-networking/MeshFace.cpp | 44 + .../src/model-networking/MeshFace.h | 43 + .../src/model-networking/MeshProxy.cpp | 48 + .../src/model-networking}/MeshProxy.h | 13 +- libraries/model/src/model/Geometry.cpp | 17 +- .../src/AmbientOcclusionEffect.cpp | 8 +- .../render-utils/src/DebugDeferredBuffer.cpp | 1 + libraries/render-utils/src/LightAmbient.slh | 2 +- libraries/render-utils/src/Skinning.slh | 12 +- .../render-utils/src/SurfaceGeometryPass.cpp | 6 +- .../src/debug_deferred_buffer.slf | 2 + .../surfaceGeometry_downsampleDepthNormal.slf | 4 +- .../src/ModelScriptingInterface.cpp | 84 +- .../src/ModelScriptingInterface.h | 12 +- .../src/TabletScriptingInterface.cpp | 80 +- .../src/TabletScriptingInterface.h | 28 +- libraries/shared/src/AABox.cpp | 32 + libraries/shared/src/AABox.h | 1 + libraries/shared/src/PointerEvent.cpp | 41 +- libraries/shared/src/PointerEvent.h | 11 +- libraries/shared/src/RegisteredMetaTypes.cpp | 19 + libraries/shared/src/RegisteredMetaTypes.h | 4 + .../shared/src/shared/GlobalAppProperties.cpp | 1 + .../shared/src/shared/GlobalAppProperties.h | 1 + libraries/ui/CMakeLists.txt | 2 +- libraries/ui/src/OffscreenUi.cpp | 73 +- libraries/ui/src/ui/Menu.cpp | 2 +- script-archive/example/games/grabHockey.js | 3 +- .../example/games/hydraGrabHockey.js | 5 +- .../example/scripts/rayPickExample.js | 1 - .../whiteboard/whiteboardEntityScript.js | 4 +- script-archive/vrShop/item/shopItemGrab.js | 7 +- scripts/defaultScripts.js | 35 +- scripts/developer/tests/sliderTest.html | 157 +++ scripts/developer/tests/sliderTestMain.js | 35 + .../utilities/audio/{stats.qml => Stats.qml} | 17 +- .../developer/utilities/audio/TabletStats.qml | 89 ++ scripts/developer/utilities/audio/stats.js | 28 +- scripts/system/audio.js | 3 +- .../system/controllers/controllerScripts.js | 41 + scripts/system/controllers/grab.js | 3 +- .../system/controllers/handControllerGrab.js | 1016 ++++++++++------- scripts/system/controllers/squeezeHands.js | 84 +- scripts/system/controllers/teleport.js | 7 +- scripts/system/edit.js | 317 ++--- scripts/system/fingerPaint.js | 14 +- scripts/system/generalSettings.js | 2 +- scripts/system/help.js | 26 +- scripts/system/html/SnapshotReview.html | 56 +- scripts/system/html/css/SnapshotReview.css | 120 +- scripts/system/html/css/edit-style.css | 1 + scripts/system/html/entityList.html | 2 +- scripts/system/html/entityProperties.html | 2 +- scripts/system/html/js/SnapshotReview.js | 61 +- scripts/system/html/js/entityList.js | 10 +- scripts/system/html/js/entityProperties.js | 14 +- scripts/system/html/js/marketplacesInject.js | 9 +- .../html/js/marketplacesInjectNoScrollbar.js | 349 ++++++ scripts/system/libraries/WebTablet.js | 123 +- scripts/system/libraries/entityList.js | 53 +- .../system/libraries/entitySelectionTool.js | 152 ++- scripts/system/libraries/gridTool.js | 7 +- scripts/system/marketplaces/marketplaces.js | 9 +- scripts/system/notifications.js | 10 +- .../particle_explorer/particleExplorer.html | 2 +- .../particle_explorer/particleExplorer.js | 24 +- .../particle_explorer/particleExplorerTool.js | 24 +- scripts/system/snapshot.js | 73 +- scripts/system/tablet-ui/tabletUI.js | 194 +++- scripts/system/tablet-users.js | 7 +- scripts/tutorials/entity_scripts/pistol.js | 7 +- scripts/tutorials/entity_scripts/sit.js | 70 +- tests/render-texture-load/src/main.cpp | 2 +- 232 files changed, 11842 insertions(+), 1931 deletions(-) create mode 100644 interface/resources/html/img/tablet-help-gamepad.jpg create mode 100644 interface/resources/html/img/tablet-help-keyboard.jpg create mode 100644 interface/resources/html/img/tablet-help-oculus.jpg create mode 100644 interface/resources/html/img/tablet-help-vive.jpg create mode 100644 interface/resources/html/tabletHelp.html create mode 100644 interface/resources/icons/create-icons/20-text-01.svg create mode 100644 interface/resources/icons/create-icons/21-cube-01.svg create mode 100644 interface/resources/icons/create-icons/22-sphere-01.svg create mode 100644 interface/resources/icons/create-icons/23-zone-01.svg create mode 100644 interface/resources/icons/create-icons/24-light-01.svg create mode 100644 interface/resources/icons/create-icons/25-web-1-01.svg create mode 100644 interface/resources/icons/create-icons/90-particles-01.svg create mode 100644 interface/resources/icons/create-icons/94-model-01.svg create mode 100644 interface/resources/images/sphere-01.svg create mode 100644 interface/resources/qml/TabletLoginDialog/CompleteProfileBody.qml create mode 100644 interface/resources/qml/TabletLoginDialog/LinkAccountBody.qml create mode 100644 interface/resources/qml/TabletLoginDialog/SignInBody.qml create mode 100644 interface/resources/qml/TabletLoginDialog/SignUpBody.qml create mode 100644 interface/resources/qml/TabletLoginDialog/UsernameCollisionBody.qml create mode 100644 interface/resources/qml/TabletLoginDialog/WelcomeBody.qml create mode 100644 interface/resources/qml/controls-uit/TabletHeader.qml create mode 100644 interface/resources/qml/dialogs/TabletCustomQueryDialog.qml create mode 100644 interface/resources/qml/dialogs/TabletFileDialog.qml create mode 100644 interface/resources/qml/dialogs/TabletLoginDialog.qml create mode 100644 interface/resources/qml/dialogs/TabletMessageBox.qml create mode 100644 interface/resources/qml/dialogs/TabletQueryDialog.qml create mode 100644 interface/resources/qml/hifi/Audio.qml create mode 100644 interface/resources/qml/hifi/TabletTextButton.qml create mode 100644 interface/resources/qml/hifi/components/AudioCheckbox.qml create mode 100644 interface/resources/qml/hifi/dialogs/TabletAssetServer.qml create mode 100644 interface/resources/qml/hifi/dialogs/TabletDCDialog.qml create mode 100644 interface/resources/qml/hifi/dialogs/TabletDebugWindow.qml create mode 100644 interface/resources/qml/hifi/dialogs/TabletEntityStatistics.qml create mode 100644 interface/resources/qml/hifi/dialogs/TabletEntityStatisticsItem.qml create mode 100644 interface/resources/qml/hifi/dialogs/TabletLODTools.qml create mode 100644 interface/resources/qml/hifi/dialogs/TabletRunningScripts.qml create mode 100644 interface/resources/qml/hifi/dialogs/content/AttachmentsContent.qml create mode 100644 interface/resources/qml/hifi/dialogs/content/ModelBrowserContent.qml create mode 100644 interface/resources/qml/hifi/tablet/Edit.qml create mode 100644 interface/resources/qml/hifi/tablet/NewEntityButton.qml create mode 100644 interface/resources/qml/hifi/tablet/NewModelDialog.qml create mode 100644 interface/resources/qml/hifi/tablet/TabletAttachmentsDialog.qml create mode 100644 interface/resources/qml/hifi/tablet/TabletAudioPreferences.qml create mode 100644 interface/resources/qml/hifi/tablet/TabletAvatarPreferences.qml rename interface/resources/qml/hifi/tablet/{TabletGeneralSettings.qml => TabletGeneralPreferences.qml} (68%) create mode 100644 interface/resources/qml/hifi/tablet/TabletGraphicsPreferences.qml create mode 100644 interface/resources/qml/hifi/tablet/TabletLodPreferences.qml rename interface/resources/qml/hifi/tablet/{TabletMouseHandler.qml => TabletMenuStack.qml} (79%) create mode 100644 interface/resources/qml/hifi/tablet/TabletModelBrowserDialog.qml create mode 100644 interface/resources/qml/hifi/tablet/TabletNetworkingPreferences.qml create mode 100644 interface/resources/qml/hifi/tablet/TabletStoryCard.qml create mode 100644 interface/resources/qml/hifi/tablet/tabletWindows/preferences/TabletAvatarBrowser.qml create mode 100644 interface/resources/qml/windows/TabletModalFrame.qml create mode 100644 interface/resources/qml/windows/TabletModalWindow.qml create mode 100644 interface/src/ui/DomainConnectionModel.cpp create mode 100644 interface/src/ui/DomainConnectionModel.h create mode 100644 interface/src/ui/OctreeStatsProvider.cpp create mode 100644 interface/src/ui/OctreeStatsProvider.h create mode 100644 libraries/animation/src/AnimContext.cpp create mode 100644 libraries/animation/src/AnimContext.h create mode 100644 libraries/model-networking/src/model-networking/MeshFace.cpp create mode 100644 libraries/model-networking/src/model-networking/MeshFace.h create mode 100644 libraries/model-networking/src/model-networking/MeshProxy.cpp rename libraries/{script-engine/src => model-networking/src/model-networking}/MeshProxy.h (69%) create mode 100644 scripts/developer/tests/sliderTest.html create mode 100644 scripts/developer/tests/sliderTestMain.js rename scripts/developer/utilities/audio/{stats.qml => Stats.qml} (93%) create mode 100644 scripts/developer/utilities/audio/TabletStats.qml create mode 100644 scripts/system/controllers/controllerScripts.js create mode 100644 scripts/system/html/js/marketplacesInjectNoScrollbar.js diff --git a/cmake/externals/bullet/CMakeLists.txt b/cmake/externals/bullet/CMakeLists.txt index 125432002c..317e3302d9 100644 --- a/cmake/externals/bullet/CMakeLists.txt +++ b/cmake/externals/bullet/CMakeLists.txt @@ -66,11 +66,15 @@ if (DEFINED BULLET_LIB_EXT) list(GET _LIB_PAIR 0 _LIB_VAR_NAME) list(GET _LIB_PAIR 1 _LIB_NAME) - set(${EXTERNAL_NAME_UPPER}_${_LIB_VAR_NAME}_RELEASE ${BULLET_LIB_DIR}/${LIB_PREFIX}${_LIB_NAME}.${BULLET_LIB_EXT} CACHE FILEPATH "${_LIB_NAME} release library location") + if (WIN32) + # on windows, we might end up with a library that ends with RelWithDebInfo if Visual Studio is building for that configuration + set(${EXTERNAL_NAME_UPPER}_${_LIB_VAR_NAME}_RELEASE "${BULLET_LIB_DIR}/${LIB_PREFIX}${_LIB_NAME}$<$:_RelWithDebugInfo>$<$:_MinsizeRel>.${BULLET_LIB_EXT}" CACHE FILEPATH "${_LIB_NAME} release library location") + set(${EXTERNAL_NAME_UPPER}_${_LIB_VAR_NAME}_DEBUG ${BULLET_LIB_DIR}/${LIB_PREFIX}${_LIB_NAME}_Debug.${BULLET_LIB_EXT} CACHE FILEPATH "${_LIB_NAME} debug library location") else () + set(${EXTERNAL_NAME_UPPER}_${_LIB_VAR_NAME}_RELEASE ${BULLET_LIB_DIR}/${LIB_PREFIX}${_LIB_NAME}.${BULLET_LIB_EXT} CACHE FILEPATH "${_LIB_NAME} release library location") set(${EXTERNAL_NAME_UPPER}_${_LIB_VAR_NAME}_DEBUG "" CACHE FILEPATH "${_LIB_NAME} debug library location") endif () endforeach() diff --git a/cmake/externals/faceshift/CMakeLists.txt b/cmake/externals/faceshift/CMakeLists.txt index 28fbffec34..c4f2055435 100644 --- a/cmake/externals/faceshift/CMakeLists.txt +++ b/cmake/externals/faceshift/CMakeLists.txt @@ -27,6 +27,10 @@ set(LIBRARY_RELEASE_PATH "lib/Release") if (WIN32) set(LIBRARY_PREFIX "") set(LIBRARY_EXT "lib") + # use selected configuration in release path when building on Windows + set(LIBRARY_RELEASE_PATH "$<$:build/RelWithDebInfo>") + set(LIBRARY_RELEASE_PATH "${LIBRARY_RELEASE_PATH}$<$:build/MinSizeRel>") + set(LIBRARY_RELEASE_PATH "${LIBRARY_RELEASE_PATH}$<$,$>:lib/Release>") elseif (APPLE) set(LIBRARY_EXT "a") set(LIBRARY_PREFIX "lib") diff --git a/cmake/externals/polyvox/CMakeLists.txt b/cmake/externals/polyvox/CMakeLists.txt index 3740e26762..c799b45e78 100644 --- a/cmake/externals/polyvox/CMakeLists.txt +++ b/cmake/externals/polyvox/CMakeLists.txt @@ -19,7 +19,7 @@ ExternalProject_Get_Property(${EXTERNAL_NAME} INSTALL_DIR) if (APPLE) set(INSTALL_NAME_LIBRARY_DIR ${INSTALL_DIR}/lib) - + ExternalProject_Add_Step( ${EXTERNAL_NAME} change-install-name-debug @@ -29,7 +29,7 @@ if (APPLE) WORKING_DIRECTORY LOG 1 ) - + ExternalProject_Add_Step( ${EXTERNAL_NAME} change-install-name-release @@ -59,7 +59,13 @@ endif () if (WIN32) set(${EXTERNAL_NAME_UPPER}_CORE_LIBRARY_DEBUG ${INSTALL_DIR}/PolyVoxCore/lib/Debug/PolyVoxCore.lib CACHE FILEPATH "polyvox core library") - set(${EXTERNAL_NAME_UPPER}_CORE_LIBRARY_RELEASE ${INSTALL_DIR}/PolyVoxCore/lib/Release/PolyVoxCore.lib CACHE FILEPATH "polyvox core library") + + # use generator expression to ensure the correct library is found when building different configurations in VS + set(_LIB_FOLDER "$<$:PolyVoxCore/lib/RelWithDebInfo>") + set(_LIB_FOLDER "${_LIB_FOLDER}$<$:build/library/PolyVoxCore/MinSizeRel>") + set(_LIB_FOLDER "${_LIB_FOLDER}$<$,$>:PolyVoxCore/lib/Release>") + + set(${EXTERNAL_NAME_UPPER}_CORE_LIBRARY_RELEASE "${INSTALL_DIR}/${_LIB_FOLDER}/PolyVoxCore.lib" CACHE FILEPATH "polyvox core library") # set(${EXTERNAL_NAME_UPPER}_UTIL_LIBRARY ${INSTALL_DIR}/PolyVoxUtil/lib/PolyVoxUtil.lib CACHE FILEPATH "polyvox util library") elseif (APPLE) set(${EXTERNAL_NAME_UPPER}_CORE_LIBRARY_DEBUG ${INSTALL_DIR}/lib/Debug/libPolyVoxCore.dylib CACHE FILEPATH "polyvox core library") diff --git a/cmake/externals/vhacd/CMakeLists.txt b/cmake/externals/vhacd/CMakeLists.txt index efe6ed0381..11afa255f1 100644 --- a/cmake/externals/vhacd/CMakeLists.txt +++ b/cmake/externals/vhacd/CMakeLists.txt @@ -8,7 +8,7 @@ include(ExternalProject) ExternalProject_Add( ${EXTERNAL_NAME} URL http://hifi-public.s3.amazonaws.com/dependencies/v-hacd-master.zip - URL_MD5 3bfc94f8dd3dfbfe8f4dc088f4820b3e + URL_MD5 3bfc94f8dd3dfbfe8f4dc088f4820b3e CMAKE_ARGS ${ANDROID_CMAKE_ARGS} -DCMAKE_INSTALL_PREFIX:PATH= BINARY_DIR ${EXTERNAL_PROJECT_PREFIX}/build LOG_DOWNLOAD 1 @@ -25,7 +25,13 @@ string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER) if (WIN32) set(${EXTERNAL_NAME_UPPER}_LIBRARY_DEBUG ${INSTALL_DIR}/lib/Debug/VHACD_LIB.lib CACHE FILEPATH "Path to V-HACD debug library") - set(${EXTERNAL_NAME_UPPER}_LIBRARY_RELEASE ${INSTALL_DIR}/lib/Release/VHACD_LIB.lib CACHE FILEPATH "Path to V-HACD release library") + + # use generator expression to ensure the correct library is found when building different configurations in VS + set(_LIB_FOLDER "$<$:build/src/VHACD_Lib/RelWithDebInfo>") + set(_LIB_FOLDER "${_LIB_FOLDER}$<$:build/src/VHACD_Lib/MinSizeRel>") + set(_LIB_FOLDER "${_LIB_FOLDER}$<$,$>:lib/Release>") + + set(${EXTERNAL_NAME_UPPER}_LIBRARY_RELEASE ${INSTALL_DIR}/${_LIB_FOLDER}/VHACD_LIB.lib CACHE FILEPATH "Path to V-HACD release library") else () set(${EXTERNAL_NAME_UPPER}_LIBRARY_DEBUG "" CACHE FILEPATH "Path to V-HACD debug library") set(${EXTERNAL_NAME_UPPER}_LIBRARY_RELEASE ${INSTALL_DIR}/lib/libVHACD.a CACHE FILEPATH "Path to V-HACD release library") diff --git a/cmake/macros/LinkHifiLibraries.cmake b/cmake/macros/LinkHifiLibraries.cmake index 3767dc7131..de4ff23863 100644 --- a/cmake/macros/LinkHifiLibraries.cmake +++ b/cmake/macros/LinkHifiLibraries.cmake @@ -21,7 +21,7 @@ macro(LINK_HIFI_LIBRARIES) include_directories("${HIFI_LIBRARY_DIR}/${HIFI_LIBRARY}/src") include_directories("${CMAKE_BINARY_DIR}/libraries/${HIFI_LIBRARY}/shaders") - add_dependencies(${TARGET_NAME} ${HIFI_LIBRARY}) + #add_dependencies(${TARGET_NAME} ${HIFI_LIBRARY}) # link the actual library - it is static so don't bubble it up target_link_libraries(${TARGET_NAME} ${HIFI_LIBRARY}) diff --git a/cmake/modules/FindKinect.cmake b/cmake/modules/FindKinect.cmake index 7607de1f44..895c6ebe8a 100644 --- a/cmake/modules/FindKinect.cmake +++ b/cmake/modules/FindKinect.cmake @@ -21,7 +21,7 @@ include("${MACRO_DIR}/HifiLibrarySearchHints.cmake") hifi_library_search_hints("kinect") -find_path(KINECT_INCLUDE_DIRS Kinect.h PATH_SUFFIXES inc HINTS $ENV{KINECT_ROOT_DIR}) +find_path(KINECT_INCLUDE_DIRS Kinect.h PATH_SUFFIXES inc HINTS $ENV{KINECT_ROOT_DIR} $ENV{KINECTSDK20_DIR}) if (WIN32) @@ -35,7 +35,7 @@ if (WIN32) KINECT_LIBRARY_RELEASE Kinect20 PATH_SUFFIXES "Lib/${ARCH_DIR}" "lib" HINTS ${KINECT_SEARCH_DIRS} - PATH $ENV{KINECT_ROOT_DIR}) + PATHS $ENV{KINECT_ROOT_DIR} $ENV{KINECTSDK20_DIR}) set(KINECT_LIBRARIES ${KINECT_LIBRARY}) diff --git a/interface/resources/html/img/controls-help-oculus.png b/interface/resources/html/img/controls-help-oculus.png index 0bd0a656debf2203bce976b1fe956ca308704b23..8887bc9ab151bc8ace3b7cb21a6dbef04a632333 100644 GIT binary patch literal 127440 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ɰ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%0Ae;gBBT$dl2U+s1jZ>oFFE3=qtY7Rrl3KX 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 z z4e~+K)XsiD9#@lOO$=F7;>Stj9FaaG`}p(5b`Fs$@$^cX*Bvl4tb;3q5A|W^Xr^N6 zzU7D>DO&0Chl|P=nlrRTk`J8Fj!N$~Rsha#T7*mcOeG%W&T<2%hRJt=YU;F~ep9rD zbZ!W|HV2oHNHr-TKyD529VbD`(G_}pYS#z`dagOQax|C|jvgYpR;Q2ksiqBS_MXWa zkROWKpp%Jtc}4OpG`|g(K#isiry9zBtqhWe-=Hqb%s*{8NN|gwUiJ&PZz4RdBGol6 zJ=DZnGG}RhsN-VIvtY{zNURzzDb7Lc_~kf_Luh@IX>d?en{*46&lc)0vK{jQT6K&N-8=e8Tz908W!#8@ZF4x9;78%ZizPBr4OKH1A` zE<}GGdcTnip;%X&DCY{dq@+xkr)Bkmwu!F#}zIu53#bS&Hn1>iU@xmh);MWoN4vXaf)T1wS z^})q4(!j`j>#w|SPH{Y4p1M3taN?8?&P(W#(3=pn9z(e3z9(jS&swlZ&Mrx}=JRR4 z#BF*p(+|G*63H1hd+H-3oVXy8zMd8fN>GHK=4(dt@<;j^-CJZDPLl9gZi7f^jFq+-f3|12JBW}Or8zq zgp0)&Ub&rx%5DB&og_8KAyCVfvESf2g1R2OO`JV9a!TrwExx+*0ZGSW^b2;$1^sK2 z)x5sCCAEGm2s^yxbx*^Y*qPnOxT`ojzNhxVvNZu0!(I1? z)NVwO>Of0yR9g8^cqg-IZ~#D_Eh5ena4RV>H-!_$!vQ!^_YNJ)se3@Zd5mT{;gd;X zE5@iE(!}@%3~^^DIIkqFto2IH8KSBpT@7yFqzPSlHNMl}Eq{dBD&oIX2rrmKXxtu3KeVMT1AF$J5>> zk4e8(j?K#Liip9*vG`87_omrY5fY>0oGrHAF8D;htaLyo_*(!14X%Iv2107&%a3Ar z>hkKiDtJVk6Vf)RF#N9dQ@=5Ng`^YUX;$?C*6T@!^W4VYF|KTT^v~)u-Vc{`M~pMW!{srb?r%hr2y*Q@iNI)D}xy zLk8;yCT0HKN~#pj8@b*vfz7k9iJo>pQ)qw@GfK%l5qjE<5#1{pEZM%j+B|;QM@2QT zG9lHu+7$~U+6c`hi9l9L+@jo)8N4d|2c{ZH?P_5(&3W%ODl;`#J#qjp$@G>G!<4b9 zT_bS}m8cGLK#|kjpE*bt;_t&D`Kh`&7lQ{kFx|~7mABBN!G!r*`SCJE=8<1S6Mc4E zdMi!$Qm9MK)?#`)Y1R!!d)eSXo#GFkCNGiEMjZ8336C9=548(Y!KnUCh~#l9WoNac zINsUZ1h*aSy94P^u=yLe zD7VRl1H09joY?vpc8QI2z9XnBy+_mqmc#B7dWoQWoYvJngE@Ibow_5;|RENvJzSS~DCfzKp_`0o$DRlhw}o<@|; z*06NIt83-F^px>4(5Xhb4;!|Z#KXtidB^0F3^`acH@Lyx?OaA+@l*{EqPo>UO=!Lgpdt_C3zs9dy9HSK%8{LZ3>~jllevOEj|-t@A zAmfqZnh{r?lxt8VY-nO0%r1JP^>=8YJl25vrJD0VMl8DPFoPfWcuoobHsT($Xq1DXJ*;~hWi9DDtq4d59D_kfSHeCAIG& zPwrhHk23Q%5ed+qjC&skTkCN6q439JmTr`4rMd4b+NQp;Nw{BH8^nOxx7xk>k^cy@ z_JCVciZ#$Atao;0?gzW3lHh!#SrCL( zGZ(X3QR!~)XNyXVml`EuYYIuYttg~6P=h#uwv!+lol#PySfv7AH~Ykp82E}^WnQnD z8y&yun^l(K)s2Vx=|F$m5S8>y^=y~|0nYB5Y4a>LLCpT%rG*U&h}D;#99XFmTv*dO{fT3$8f*VQBIxiB7CLjNny40Q6#(1*U%TKE4B^U*~ z{Rf)e6nOx=&q$uE(ZgO5DIBQ}YKdZ$kA7L^DzeGdk$@#7O#;OMe;j<8gU!dE&rW0+ zfF79Z&&lsK8cgbh8_~Oa0asq0FLAn;JihiUz1i-QMG@ewFdU47wMW2wwhL9P%P|HM z(#*LkO)YZD#=Rx{CD$>W7#jk%rbknSnG6uOOgp+=(k%-Ixv1D5+r2OiU7(K8NeuA1`m zbyUhx`E>Psz>mSNgEimHiCz(;C*Y6{{+*E=fuub?w0oeAB~7t#x|g2&ET^0EiQjNV z@R?skT*3hXU?lsC7Y^uEjnxHaU4#>%4>VYKaqby6&*E5-KF{K0)l~AA-L(@Ax#}@! zainvIOKAb8q`&=q161VUUIb2EgBOxqQ{Q(OtbQxHKpsxpJaTIxWaj&DHP`qP)_9t< zclI;TTJycRf9^QhJ*A4UR?uuJxWx43`hK2;VN}$gB7vXJj5ry!>Z;`-%j3BV>%l%> z`V`2P_hx>hCb8RV@eDcd7AHFZ?{c+Buq^Vdt)l4H^AGfqAzZ}|{+DskI%Kpaw*XvP zhp*rXJ*EFaLRqyoa7cZW??$t1E5s zsCh9I$aXV2J2Eh-WXA$>ZuD$ySi5;o1D{sl_4%|$;4SzYUCR)A)Cu&SYq?$`2QSW` zjDJMr-+y6xNiX*4Yk@OQ!Z#k8DlSHM|S&$fbsY`)TB4gr$+ZXkNPbw$|olK(4@0$HR z_DGr=i6Vo9vQReDd*~QeM#_*4sS_Y+pDpjdjt*l{Q|qkNP*z*@l7!5H)>6>;g*02bIp{eDMT``SC2t_RoBP$m43Y#hSE7AHO5Ug9;asb zR9#{LRO5@Hpn~QEkB+uH=L_2fhh_lw;g|J|>*iI)pc5+E0DnZD$Mbo^){Ek!L7#MD zC$Bi<^5CtBc8LEO{ZGqWVp4iez6wK$=ImQxK)({~3hEGW*fSTSKg$J*D!Uybp<@^} zSMC0R(b*+rz=jHEu{CX-@CR%#4RJeZc@{A^o02WB*{rXW;At1s&L}}ss4DrSlw2`F z_-=`JPD=@N_26n>`@6w*!8`_fhQp&^-#kMnt8ETyF1N;f6*Y8lsH2IktMd6LB z_A)0|!cUAdXF6tgnR~!Tw=u!T5pYvZEq+=W=(A+W&ZW7g-HqpXoGR&tY8yqY^YP$L z9mu+H5O1v9%39qy1T~Fl=c;((IdK#Fja_#UErDG4J6EUwalZZAE%8%5WHY_$G%$|> zA-@ev*ch)g;uWuY7#PZB%=f%MmJwl_RsJlg&}kpC<)PIMQUTU*3LMp+*RnOCub`%$ zRN>yi+tzFbeGCkh9&rG$*BW{Ud*&*2hJp@Y@%LMOZ+O&~g(`=T6V1B9mr6OTtic>Q znyG-R%DobL_v||Mx+~1+LddfpY(=gPhG+cxQO7#d)eR&*kS>w?ja6f(@UjQ9X*W2R zy6EGb`pn;yY(F{wsS}r7yCqDV?$;Rdqz-{Q8%z1ymGN#vjgpsqJ}b}Ume~N0*^xnR zJdm?HvP*+~yNMhl&y|SZ&E_DDAqkfjc;c{h;m&;%SP&K|v-ax-VLem%y+IO?i_@(K z-Te`pO!!#dx9B{M2g_HsaE80+nD~b9YwG!p%{L+YrTx6e(BfYZT&8LL(Elv{gQF@05?7g@NTO9XZ?EkfII{3(+F_X`!*!; zd0)K#bhUlUKIPf4j%&m;frXLXkberRWA zj=VR=Wu)Vhgz1jaJ_bC&H<&Xr_h($3F#&ANAFd2rbTErJW_i^*o2vG$%Oo%Tdcno} zJ5?pQT881>wIb|$it2-2pSkPC?esg9vB^$J1HIaDNnckxXT~6gXVemm$UVkFb8^nd z!}*z>RM5EkpOuMn#VKro+zzPwzWNwn4y$GifpuHnvAwJF^_Xf?Idqnx;zyh`Ya*nH zz1|@tuTk6>B7T1ov-5Y~mUj3%BL#Q4P^>B<&jV8dQrLdcYWpGdUH zD}~z`kR!nR1ESvg<2~LrcbeGY3s|&r;neM@HelM#MfD<1KWqNur8ST=Eya*6wqUw= zxp|eoD}s@Q_(N5rp*mI4`i=nkr@dL9Rd2E;NqdZt&@N@eZ?~is9fz#-&Moo~J?d+O zHP_U-FK_hV-DzE1!{JM@`Yom;8l3I}&B{rc9Kfl|CG~FCmK$rMqd4jNL&3VnOq$0) zr&(-2IfWnG;>W$i^mhiBGnug&`|G&?yJ+zQt{6Tq(JH5w{j?aj2ZjJsKu>CG_NElicsa2jPr|@Iti>)n1 z`k+M6*2qwIPmCiOEl&5rIiBk=&5eS@QAhT6apX=Cm30(-CDtaQOqBUOqv0(^bU+3k zDUZ~B!FzU{^U7S2hvTL++y&G2aW6Z4vzxK&7W~S|pb0Aqh9Ik^F4mjFufdZnI)N(@ zhcLr}Ep><@uqI}Q$E?R+>2Hgm^J2)iC&v11kx}u99Zgh4i@EB+n!4_nV)d3T7rP~j zJJN;72)3kZDcZ7)`HW9I+kkWZS!_R18B~IXoQxCom+nKZUTi$v-k1X*RXQry=T>eL&(tvW1A>Jr>J1`k$_38ax(GwcL2r zcmwi}D%^_Gx_+(CTt1}%l`Gk&h*@gQW(9fvUO2Nualbo_Igq}o`pnCw>Fv2C$n2~c z%iy5}D^ghtB5Nm21~!QnGLe>NKdgV;2HT|{w^siQWxG5NI_938OWXpacd^)*Jaz~d zX41l4Gxr$o@hVEr$D8Tp`7h2Y=Z7(K%YR*L-=@p#(n~zUz+STKm5m%2QYDJ)a{=zD zKN?leg+-;hw9<%x)S7txzukGo7|n+J*})x0CvwQ1MpzXlMAHlL-i*$|=G_KKBnr?4 zPKPJgwTJOIDotwE+P)`#U-R7^m&a`P&0NQCvT~7}iz{nqCFacLL21l9z6+OG`aGmB zu3uf7R3RhS6oo8G;LJp?3e4@CM3twU#-E?fNZxmt1BrnxwHGn4pPi`1(|!^f1)?9k zRLuAoFRB7>p-7W>>6z0icJvNuVCBJgOGxEONW!ZtwYfG;6t+pvI?jBohH-sI2JX@a+_Yz*Iiv+hJDX;q*LY1*~fer#vLC9 zA(`N6VL|?86efm*Vj#`=+abBg5D^2tbd@F>X_I+@$E~AcpCY2Cp_qtuDFki&YcB2wo4q|Np;N94UTC!l?R)jTiBRNDYW^t;+Xx z^5CeBIcn1X(x<;MW>0oVfYMPS3c|c zc}e}vd1&1me9zk~Y^O3>v;v)B_NT0DV)1INyn%CT?p42(!?lO3FyU`jB+x{MlMVIoWYGg3VGj@FLF8E{=mdaHuKGIeY}%P z>wVOc$n{rVc)^iC3%Xoi;p&}@O7)w?y3MUYLkMBfENaScGqH}aIzIm8e3ONf*JTz& zXtEu8G}=;zMiJ``4U7+}uw?+{uVfmII=FVCR@&->X=xsPjXhzpHrncVGQ0aBe1LwX zHWkh&-))&w#m-uL6)6qFfO*y5qUM!%$VR;zYO$ijClJT15QVYiE4KJYD;O2T?A>-d zCk8L%Amc!~P{jOkygLnJX;}h?oTW}=k@zP@`(7Ji6GvEkaPiSsE^wl=A>%B1CtQ_u z!`9c!)R+Z-Y**jN+`nLJ62cs{N+EOf?IISCXxRScl33eC!S7~{VMspCr|HyqP!&(- z5xw!}wlNcULA)reAAA>i(Dj7+tupV;AvzMW;VAd%i<{+)C>h)5vi9A(z)UNs0$~Y-7NNJr%KY4N88023B98cioS5MS| z`d2T%w-K#$3-NHh+`bZ5V7D3hGw9Q(#oei#l8>?j70VdN@rGi&?9}gLDA`Zq=~3hs z_}hJbt<43wB6Qf%a^q&U4a_;@*wY#rfAsn(?N*qQeOY^((ZW813a>}BL*Wy8 zy?|w;7OF_HNXCR>!=*Zje6p(L==L^!ksv+GOO<`Q}SI`Er8CwQ*n{C z7HM6e6mMAlTlIEmVDQx_p(YRW<&FS^?&Sp=f;k$fJlRsZ#V6`E zOHK36GH4;)=)&qg%~g5%Mjvk*pNm(^^iX}HnTOld5%$Dw_IuYO$<55}=tQfI)#hLj zkmtU)>7if6iwojqecRdWqLP*y6j0Mq_HdHRLM1)hv@@ozXBMI|qLiFm&8zV|b|13~ z0dDe4W!O`%Hr;+T_Edg*mnuXCG&1*a5tQEORMAW+0}9@SmB9h#5A)?tdL4U5g8A?I zNhEq{>RaG=^T?De$AH5ylUb2ysKBGlZXf9nhJd6PUZdQg4n&1Y5g>86#KU(LpCP)S zGGXtrBpW(bnh2?CZl>+o2bdM=4>&GFr z?c~|fY??$vl$Ikk(7gPVHC}?Hbw@5*a9aV`!-laDh!T5Q(QyHqz9)@4s!h-*X4vp) zjyZjhT-7js%!Hfty}=Ld1aKKQD?RENIino=LP*W~60z-a%Ltc*Crq5E={u*E*n%FBm=kP_?~2k=Rj$ ze?kb7Hp)SaM?YU~Uyb+<4!u$UAQREM{aXBp^PI|cn|3OpR(n8IzQ~(5t^jKMPrJsp z`v85C!v)aZ?`~FUSF5J3CS)V7>8$YY1FCn|iqSv45#^(f_in(s+oM7oqvnQz@-!9M zEU}Y6JPHTb!)AAO3soq|Z={mkb3hCD*QjJZ844N1!DiaBRPc6=$zkQd4b7Jt{Vsiz zhf51~9pEZyJu-77CA*MdROo)YdATj)T9fUGBx?dj+bg@!trEnKY8EvyC#P*4vt>F+ zQlOm@`YG3~a_0@FLzirkUI*p+=`;L5IPg&>41KR(`!lK}4Z^7TEPB?j>~1|uDetdm ziuegE?x-@XmcY26u^qpe>0@@@>Q%`+*Z=Q#r^ykp?pWixK(X!QK;GY_YYor}Aj z#+Fw(nnN!^v;C6%)cg}dD(cz_rYJx3HZAoiz+}Rs$vm6-ShgaDR7N>f)WoZITNS)` z#=*&5Z_-!M{YMwE)jp=u00dPLGcyR1wBGT?f6RXJNy9OT8huGN)HKDF^SV7y9Tr?O~8Ua{D{+aR*Zpl>PvTL)KBlNN11Pd@8@6L)n#0nhoP0Wq~I92B0+ zq#ORi+%ZyNO3v_zTTOj2%4)ul#uM(FK!4UcM@wiLo>h0rOPZEu?9*53F++5N)7{jP zjnM+a?YR1P?9vqhLhR7K86vYq2S4V_M=IqjV9c*o^CtVscS=r?njHzX(hR))7k zJA7zbS;F+f^4@Y~@kV-*1M*W4)hg6|o@zmmcO;c#BdmIl0E z$#60gcI%r6zV&dt;i{D#G^Q#|kk*U&FdOa%Qfp%t*}LBPYqxv9QwN&!N@^>v*o#$I zVlO||F(-vuPhjyxtT~BTTr2Vhyy^5b{Cmx}XOtm8EA42=&n7YxLRQg#tzBui@o77n zywa(^j<|99o+SFjD-pil8cojXav!}&DTH1Qp>gLh@`1F8NV{5%>_f5wgK94?o(*Ji z({yT#X_GbuaaE5cj`HT#wmmbsBEhnFp;~>reZ@Q)7ccpTAgsyck#fVQ`D|0FB1+=R zN-Lw9gx10lN4j?sJlC)EXiwdEZ{6H;G+>wNB(<{KA?Mw@1K)vm;oUDHv}K2GZk@Ns zWJ?0ySMgz28++6g>p26N3uAAn?~h*M3(PW9G$h@Jr(W>+4r^lKhmIc^)H52_zd^NH ze|)^D4-oVnQ8FjlIemKkH4l#5$F@^fdIkJ4MwsBz=_>L<`LmcYGHIkb=Q($^<2&bi zLwo>@9xoy3%NNvdwGQ1^h2(fH_jP_qN&>EB>aBSd2!qq=@H4%j)LVU4~|oqouuT{DEY zjriGFG}g~JQa}G~5gg;Y{V)KQ`BI#9k__;ZgO8~fZy06-&wD-N6uEGBXf&q$^Skxr zYZFjLQk@f6{kz6fmH2|M6)~^?%K2k%yD1%O*0eVpoKVpCaFUCwqCCcTqxt$1L*r@v z-hJ0m;P2QBhI`uIVL49sidfzs)OYh`K1GrwF!%-s|M4Ib``s>97P}={#-SlSlq3+9|k^+-)5CraW4g zDbhcrmI(AbQ%)f(J7BZ4yCko#^NEjg{8Ga7sZhl+;2>GoR8(paW1+$PUg;rU=M`-m(Pzx$h}o!AJ+

;L>03w0E3`gA80riXJc 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 diff --git a/interface/resources/html/img/tablet-help-gamepad.jpg b/interface/resources/html/img/tablet-help-gamepad.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5abb38d66b37f600e1ab8bc5061bc671df6792de GIT binary patch literal 259716 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 FA^8Z_9e8 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�A%d4{5J<=-$sW% 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

a@w7!bUJy;zCF4Cp9ZxM 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 z z*Lm39`Ug1QXy>E)QnoC_HWY))SGV;g)DzzLLki*)WJFNlq9w@VK@4$F`|L>gB?F6=e-vEYDwab)vc@4g;mHvyf`!Q zW>HTFZ=Xp;tpA@mXJTEfihaXxZK4FG5H7ib1Mc`fTaolWvf?bBQo@U~v_aeFu;QL(Y7n_G@g%ew z@EPNL2TAT(i~6eU)1L7EN7q|NMb&oy!`BQT2!aR*k|G@f(j|=`-6)N8&Cp$=g3=1o zA>AR}sdP%ikOBkJ2vUOhF7Wn_@AG^AU@>d2>zuRq{=}Yh)|zvD;)2DQ_vzVXckHLK z;OciN1#m`>xg~VYW|xr;-p)Rape%ITk`){du1gu;6d7?Q^JkEHD_Sl#M_>T==1QO^ zK!$faVSToUIq|7NaylvVD)v6G(X`3A9cz5HjV

vuP{38-i^t;;rh;T=)a`MGap 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 IKk=pY|G>DivH$=8 literal 0 HcmV?d00001 diff --git a/interface/resources/html/img/tablet-help-keyboard.jpg b/interface/resources/html/img/tablet-help-keyboard.jpg new file mode 100644 index 0000000000000000000000000000000000000000..40c6017561116f90106539cc6bab8176986ffb7e GIT binary patch literal 190462 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 z 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