From b23d168ed74f6278ada7fbdc57150c35bb7b98393c6b6f91d016a3256c56266d Mon Sep 17 00:00:00 2001 From: copecone Date: Tue, 16 Jun 2026 11:44:34 +0900 Subject: [PATCH] Add Compute Shader Color Conversion --- ADOFAIRenderer.csproj | 4 + Bundle/Linux/shaders | Bin 0 -> 10607 bytes Bundle/Win64/shaders | Bin 0 -> 2915 bytes RenderMod.cs | 12 ++ Util/FFmpeg/FFmpegNativeEncoder.cs | 188 ++++++++++++++++++++++++++++- Util/FrameReader.cs | 169 ++++++++++++++++++++++---- Util/GPUUtil.cs | 28 ++++- 7 files changed, 369 insertions(+), 32 deletions(-) create mode 100644 Bundle/Linux/shaders create mode 100644 Bundle/Win64/shaders diff --git a/ADOFAIRenderer.csproj b/ADOFAIRenderer.csproj index ba45748..ffd87e3 100644 --- a/ADOFAIRenderer.csproj +++ b/ADOFAIRenderer.csproj @@ -78,6 +78,8 @@ + + @@ -85,6 +87,8 @@ + + diff --git a/Bundle/Linux/shaders b/Bundle/Linux/shaders new file mode 100644 index 0000000000000000000000000000000000000000000000000000000000000000..899ccbacb28a7b6c782b32372277a1d83c436a7315a84c7b1739c8569a1bb244 GIT binary patch literal 10607 zcmV-#DUjAxZfSIRMpFO)000OzE_g0@05&i%FfKDLF)(H^000000000fZvX%QK>z>% zTL1t8LjV8(00000000000000U009880RRCF0000e-vB`l00|rb5Df+Z@D~I_K|(Dt zWMepFGB#pmI5#pfVK+85IW;q5WjSOxVKg)}WjJF100000000000000000018000O8 z003+OBl`3c2^SI3&FcQ-wSNG1WV2GU$>)S8i#D=?g3XWx62BQ=#r7@0_&52LzrCzBDpHYJ@)(p zmWXBcH-zhNb~5NlOVcjYMPZYi^1vPE^&*!tmx6F&;ZZF@TnYCQfL@BQ4xSQx8gS01 zlszr@G7_n$0Lbj&*M&OKznfBB?||a+1vBB0r0WY2e|S6q3}DTGId*JT<)`=*)Hk6g zpP;*e?rQv%?nVY5L7r2*y}qx4ct~Xq548^tYtXZqFDfP=;91ho&glz0Sld;O+jvLr zJI1V*@K9H?e@-pWG5nK~V3B_A`d`)QR3a2cDo?^#S;8qCCZ@W0d$tgwiav%L2lVQ` z->_SA38{dIu;W4LX6s6B&-=6sIFZ>AR@q$H&5yp5P323a$;w7livW5P0}|;kK~n^@ zT`|L%ldoHi7WphMNYEkv+gSUvC07@p5^fa%R9fXnkTDf=FMVZCZdgbohDCxX&|Wa6 z%OwYg{+SNCm;khx(hZz{=wBWPbwA}w3E$?>gpz(O##9WQPn)B%@5BurRr7DSSfWTa z5x2Lxt?1RhHg03b^skqz1hrTS{IFD}k=D;HE;n_P#dKiFN+ofbmx`7w1~|I0gzpmU zF}%T3F`ay1`-w=8sbg_zMsorbL^nF)0ya-7v;#CA5vhD@!S$C=Hh-a%CE2JuC0;-_ zB6r!7)^}+^Ng47I#QhH&fZ4-X?Bq#>Opj^wEGMle&v2%`wt!eL>aEOf*w08# zITlC1b=6Sk3?kB4jBaffjSgfesq=Z$gSpl}X`~{UH~TWQK?RLR5W?<{jkB3LDaoAD z%5a{ZWNvYNOXs;e#MF{ga`0y{Pp5=iy86qs|M;`u>NswMBCvU|_cLn`qF9#b=BF>x zi0*i0{O9#-{|Gn2_slHE450XSHRh%0xlyj(e(wQONksi7QH!?8&dqJL&Rmzzq3xU# z`)Ez^trD^{BVV_@F$wNTQ>)tW$rRg@S>`;Kg}{wBqsZmYi<6YHbDi364BnyK@!zi| zaxG-ZsE7&UHLpZZqWX1VhoG{M6pr*T>{L9%>7w+B5c4>C!GcKBgFFQTDJD1P_!aIk zmpjgDMC1cX+rwrXv(G6Jul72Cfx@h7nTh|YejG`p9!hVW8bjN|rpKrV7~>RhM>R`wzVI-vTQi^NI`L^<+Q^(7xk1g` zAErX0mK}QS>xn8<#+U+C*oJ5|TN{HK&azU3v!@wboDY<1fjTS+xZQZAndBUMUq0bC z^xconqCzpU>1v8#W9Ad>tLDAkf%iHW&MlvKUx8dFZ}WbvPATsvI~6IO?Z+1SNA%q8 zR+CGvYvfsEzet!%@7f<4bxlhxE|znf(mK)sbBM|7s;cTw#zjk6M#8ge!pisw0%E2T zJ8T`0+ImaC-ij&kj=Rqi{%ZI7e|K6d=(xu=73WIR?G6g(We>hesWLN$P>aX2;s@l@ zXg>MH-KMnICb^LVld8tC_EPT+AmIC%rWk zuJPM3El3%9lf9AQ>hV_X4qRfTEBNYda4^T-3+TuwhuLZFCN=kTgooIlxT@&VQ`VrC zNuyDg^E8%Vc%?lieQj}w83`6@18rObj$@(x(SMm=-*PFjgn(q@CXj zGLA7cvLy#$);&D-0?}LPt&w$*)V)hDZdnZ4Qi%yD0UQ*gb}{6LmL`=;3B$=q1ktI( z<*_#b#t??w7TuFWeRPC)o>R6eV{x3KG}9a=>t5Yn%e}V^=Rz*6e5Za@vj?;sm<1(8 zx(f=u3PjW*^GONoT=J{y1h-OOFEN1a$$l!LOK=A44=457%V~K-mFhLt+Vjtp*D2ev%m_tu=8exPG)G)1ob2L?H(8C3}1= zp9u=^vaCje}6;!R>IWe%QQm7%3s!9d{xX@537 zRG{Fr#|Jw6C;~EzQMbPGZMUm)1_gRzEv1vYJ?gR2V=In8gH){_j`>xqT1Nn#?zR?` zM*Dnh0r4!qXI}S%#uKT6f7xyzY~U^fm!NXy6c^L7ZswYD{V2{xjk8sYEsz36cn^$c z$9uWHs1nt}(!7bDbv<-)u*0rIcOW9l;R21>QqaU7M5(t1o3qd-)vm|EALpQ{yMf&6 zT&H;seJTc+y~V*%t(Y`tVOy|h5C9YEC;O4=;aZMg_xL*YT;&aj8;b>vtQHa#j>fWe zu1+Vdl?7!X#ycw*8GBJ}?a8bCR3mO;C5E1xB>l7pxz^R9tY+BlQd6=kwB z(GI-;D$fe$g^2|*Dl%1N(@#Jw&4rS)VF_MkT$fRjH8hxPTS9}7n*jwrdHMcrv*V?qhnC+x-yL0$${G-zhJ*8vLg|Nzx z2kFe~m{hw+whipwf2^pX#&i=TpDBaFz!0#!HV*8FGTG>`MxivKSGWiZWbAUFVO$CW~CS`>2;FsDit&+(az#o zgOFl~Oep>ubJChDa$Ki@yP8VZI=ghVRsAhb#a;GG|5nX69saICLLbIO|JZ@Imka+) zl4;^2w9M}KY^H~Pq42jzb3_^8S7`?bqlHm<84n$5T-FqJr*E>>RKu~aXd0DO*F|!2 zq5{nM#H#tyOENobLj=P;Laf~{2Sv9#a)U07exOMw91=4+;eLqghvPb|WjSedVUPNO zgg^kG_0I3z&ZwQ^{#tg#d0dk0n05eW(2bm*wpii~qO1Usw6@dRJpeQWo~zF^RVuLv zJ#xt`!M{rg?)PP#QkKtT_bv88KrzF_62KxP+fl&=79-Vye6+1H5=HEcY!U~D{S1>g zEKN%3j2@gWVgjHY!3I@4<(_C0#NweS14I+_Yaud;VOPA8ysZ#FesY{n z>(_K{g?8s>POk?XJvE|de@VDwgGFo4P22<5zOT7+n@802Q~nTj69=gYVPlfJKPdZk zBC(*_4-d=rP$W8CV69j=b$Uec6)9;-g=tVcpwvIT} zK=7)*T+f0QNBrzD?uU5{k)e*G6qj;-V+~_bD8$jN{ou}*OF-C9q=?N3BRfr$dAUgL z!85^zeUt`!o0k>X&|eOk0=nAb$Np1wO5sF+dw)5ZE34fnF&VAm3Gt_r1`;#*SEm#l zhF=rF2*t=T{PsUMCVxD4SLCm|&Ff-7e*c+(p6|H_5LmIouy>(Axm@{f8vug6ql;I5 z4van5TA$%=qi2q$2;PpJB>SEWzPmzCBs^0n9f&>=!!m=Pb~Nu2HZ~mr2!P#i^yes7 zpnj43pItHV{i&TP5pj*RH#e?+0Aqf9j^3Eu4V!h=p~Y;ij?V9{()wtFiqGOS&rRF0 z2rf16Xir#yuz0Et`RZe9alnRlcKM9+f?j*=+zII(PW%Sxr2ox7jYUT%$}^-epD4r# zyPnEg`7$zTokx`!0SC0~=4Tc(+HA1=3@7dylfQ>l>C@j9od`UBXV$~rbsFTq9;o84 zI@AYP|Z?tVWln1{(q$9&m&)Z4A7;@2c)}9B$KmT#F}2e#!TN$G~XO9-K&J9FKa8W zDkV{@jk8c3!U)320IUS#`$}q74=JlE8O0PZg+s1$HvyAfE z`>^lwX14*%(mf9mGRAvp9TVF3B-5UPS`dQwb2O#DE*g>}+h@MjqZh~9-(Hmf7;|`p;^E6~maxR;3^!LHwy9xm{;Ltfy@(4ER&#K_ii~DmW1W7k%<++BByb%7 ze@yl*pV8R7`2_M)N`Ds%&#uKuCGqs!SkL=uS4@GUFP#K^i&@%QzUrAX6AoMs_ME#C4b$L3-6$_h3^S=Fzyw9$9UH82}= zRVX|jc5myDzODPXcs8g{$aR{S7`(bf--rfI-7EQd8m3=CJkTnLw7Be9`pfsyL)acr z2&v6SqjJsj4J{aY3TBcaZ9l|Rt$vqd=bTJ<&yGZcYRbGzSgDiOSjM#%Pz`?la#Eir zF5_jQbzo0ZV{z*G4mvQU1}vFYp^xGgJT=;y+LfYt?IiYpAX@{7kMvI*3$b)`xTKJu zTcWAZ6MV7%-5&Rul_8%`9sbRsnk%x;pFryLXZEt}v?Vt4Ruzmp-Wx{yq@1i_Hx!ND zGQ92`k#_1&DWa=@v}rdu$Sdy2D zFjvKU%s09(Cj9V;7@@|!FV?$bVTk3FZa+UR>Bw$3HQyFWX3GSUfD9;*5Ps7qZy67Y zMx!2u@Lo*)iJgWosy78oG%8BO+7z#5X&t+^!z*DQ20~MlgB~%wM+}Gkw&xxAg}t>M zbUj^50BSG5MpV)6K>Q<-g8H6pYYx;Wf6#cQ1ky^$1WMuHBV08okd)bu67jHIPGuhb!cF#Q1s zOCw*TM)u|MG5%2wuY1Du_CyKsW=+1JOtd_jI12lO0~?0HGi?0P3xN<9F6mDd^70aU zUq0eP>|A7>5~DS}iQbH-!6n>Hc45PYimHsuIMgPwN+5Z9c{62$h z_+@#Jb^!R4%L?ZrJvopP`NksduTUxM?$B5%&6xmAgsRLxZYC2_#u7%gnTuc0!SYnb zf5Oq5JuPMhpP3K2km#(Z(ieofb*I5t#iOU|E#jwz64Rv$*a6be5PS?B&2n7%tdUnV z4KR`8n!W0sWC7ud?UMbvpoyw{3ETwl%Tzad_I!WOW2$w|>I?C0j>)^9Yt!AaY)tG~ zZ`r3J!XEqpjzj_mXm?dmxDh!Fo%WUi@aW92WgKQmhQLvUb$_lD75MV|y7&?g&SHoM zw>j*@!w_%N09R4X|A+^#SJN)s|8O=|&2TPYv_mW=czFG8FB+uhlU4iFQ>l3CGD%w2 zEn%>_p+zeuP?Y1y>MGE3rm=Wxxv_Wx) z^f$W^Q-d;^oKOn00tII**1)x)mr@ZaJSX2R^KZ`h;ub45Kcu@THsmirwx4uZ>L0#C z?!VierX6$UGy|?=+ue7rnMy|vxh^5n@;I%I*bInr8FVn0SO^?y>h<#{re99KW$~)$ z6OMiz;?<_%L9Nj+an{@lnEu_Wx#b`N0uy?VK`zdi&DJ4?zUmG{q*rx+_B#|92(#+r zvX3QK#BKVOzp%BRLQ!+dtlYJ!exV!IQ<57^QgaO0dNHDW-#^oBB}Q6!?a`o5_20|g z=!&B)1=aniIx+}ww1N9jZ8$?;;|Qh!F{>%7ru}pK)r)^|=ux-$;K5BFP>dq3?3xw8 zk-=bhjr&42p$kmE#)%YM!u}Y8aGl=43U|Ql5r8T%2GReo+MRWX3TBA4k8i9Ih)&St zxS+Iopt{gV@>Q^3;(opynq%uT?~88sSbt)@;t5AdQL|vL8&V zP*)$aS}!3^gx8=#V#R;;ThKN4ZVcSLmen&5Ilxe!dBN_yUQx$BE5qqg1?#U`F(+to zovo+k(2js%TLkNd&$x}QyuiCq`*|-IF0D8Pd>QCmeIn$G;zrxdAV7E1^PoyH6C&a( zC{Rx8w`O+HqAaOu*%if>m~1KYu<*Qbnd00 zM-kmu9r4%phN{LOQLI*u$))r57iJ3lDA~pLw!33@MLOUel;1E{JXVWNk8W!UPH_Lu`>76Vu-fC`=@oa6};t~!otK?HCcxwa+2vh1 zIwfUkn!Fts3?i;!z^sGMkE^QP9$rNZaF<)|$+8(LyyfEK5@j@aj>IOnRy%YEw$b|j z1zKT8daF5TsMNj)8Xavuzh^79LX$71SN6&e%tSl29vr%Eu%4&Vn7D*5L#JIWm%n& z5Jxn)OE{{&`rJ_Yj4hPYQeDI!853=`s0@ur)u!lTff`vfk26R(d;oQ;MX@iqYD6%k z5#mfq?pd3xUd)9!cSHRqPYvu5o?6ZI-mJa_7`pejvTe?m$wQ24Wb7%W>jgJ53bQ=E zN{QX|>JE%J;>1v zM5OaF$VKR6jzcy6B-!U;z|~;{$sm0EX%}cZ~nkT%F&*Vvkw_pf{DKzuH0dKd)Ax-`=6`R7_(Y15epFPH=?Xj zVPZRPayVV7m#o=sA!!vwL1RV<*G;@(Zy=fx&%_(fMVbWCzIFD00fYf6;dseOT1^%= zuL98&%aT|36J)X#uWq#S_IdQI-yS)Zijvwuw8i#L=UYDzvUKclwJ5U(Us?L1QLKZ~ z*qWjzXz)Tg0thd*Ctve6TO$!d@b>clN@3m;)-vUZ(7Rkx_pof1hMv(X%o9_E0+;A_ zk$3b|x-W^655?cg;aTrPZ=p>C#x*smT3KFYQWxxg=3@#ZR_lb3MKL+HMH{JjYn%BT zo)3gN3>{4)Mtmz|K}#gI@PC-tjC!9oDLOIbM3QDWjXUfQ)tR)Dr+6*bPrymF3|g^D(jROr7EXD9~QET@BN``OdU z*vMRW4qVeNtOlj4Gk8ENK)j2Ar}xT-7z#m2=ye^HBRire6qKM`(R!r!f+yIdM#q8>WC4Y7y@S4U62ESAZYFz=a(zgAS@dWg+ zMwu^*U*XO0P)LhUQ*q+w2wRC|*vvo*Y&?6n_$rrQ??aLLHfq)c10^mY(8-yM6 zyOkUln$&2|xAvW0R5y5Bm4+ldMk37+hWb~1{2W%@;?p4F;&0=kLqKApr z38Lz>tLqHhzLO?8>HhQ@ZXk7KX#ahhr-j>@Wz6RZtG5PT0qc`Lj0latC)Mq8zN`X0 z=^;5=E=oomaK}gUCY9|VR3Pnbg2d8)*j9IF5gKVexVl`3R=i8;J}!zv{}3wD&s7_Q zh4+O_pO5^EF!A^g=qeXaiKRJ|JvS@mx~W_k?0u~{aoPwk$O)#7c5;8Ee0HSfNVv@Y z!F~w1r$XKOIK2)fTI z1|AsPf%D}mpn8eFdaK&|uh~Cd3HAp8qF`XQv_twXomC3iu2dSPAF9)DEO&~S&qF>p z-*S#yU~kcCsb&m#4~p+1(sC$Pop;w{E|nmN%Bmp)2z94yTKM)bRqtZ3K4hfnM@~N_ zToJ7*8MULaMXbhB$zb!b$xq~BR#x@D{P=o1_?(}Ppb*9|mGT~HD1SRss6Kl+-N}hN znUBzyv~`a0S0ay!_7@-?!1%SqjR-i8Mg?!et#b`W1|+dml8m`4AuBNOXs%j>ANh8+ zJ;oNJpE>f%1@r@!lMS-=n6UpnEzI?&z~KI0=0Wv@Zx)OLosD_qNH=5Gwf*Cdtmk&f zJ0ruSzLXEp*kdnprF7|{-ExF9_QCZS)O~`6iP&e_r7AHBCCr`sEB6nSF)gfttrUC& zIv<@7K0J!K#gJ1%y}{-Sp-bQve;HX)t)lE#zrH}a#=Zs1`W~t4gfwXh@!~7>+}$e- zxO@UdNuo_DvnzhczMJiWl&)41TOaQv^P4k-%`%zD-uhgG*2T|&3J;DI2zsvErrlfa zOA?@XL^EI>+SDFh-dY*#2EPP2Q$nEFkDb=ye?Rdpm~LS8X0MgkdpHpVYRB|;!rYjN z^G1dbZ_65&&U1$M{6NZwu%gj%#-C10}_+qc2N^2C`%>=%Yvni4+qmozwwU&)b;q-fqKXld7VzQ z-bAut^7+pzKQyOHnn!KW$`}4XkJ5I=WMWu*AoR5oS-8dr}FBc|qr! z!ZxCe@`3PjI&)@J74h zcLEgDWd6`V!S?;Gadp9w;foY~fr%nxcIf>{y)f4hYdNHdw9r?E{%vY>@viy=v)u|_ z;n|5%qpr&;Y83p`q@0BYIN$|8+CfJ2k}LW&;G3<3okEth`m&&*)@hWBPMkIwHUM6E zmh<$hzET{~3ZZ{`oX!E~6TfmnZR#8B*?oS&BhSF%`8ADw7jelU%gMs#DqGYrt^lya zZmUzj^3Hb=W>WZ3`6Bqg8>U4xwhAhSLJNkpQZxCqknq}j4d`oP2Z8FEgM5*_5O@mJ zkNR5lbkQkib>5%U8k}=HadKq4LDYouoHe+e9Vl% z>Fj6roJ^3`VhDmQ9umB8z3iC{bAXjMKXs1>O^gAaR1_0Y`HN=;do(8Cb_=h)7f{6&3cpHosU#WJ+P>s0H=%$fCuGwoEJ ze*Pq=jdWkT&Z}FUv$;p8pYA((Oz=P^`w|tj?28kNiQcqe!Qs6g&GoOPNXYG z9h58-aMK)RDI?0xo>Pf#u~x1FEgJ=C@T7;$)Qyl}?Fc%qcV_u(E?5(z>V`f@P6gnM zb6ztjv1`Ss9*vp0otA}K`po%JU^Be6=;PYg(3$fxC~HxLe8X^dYK zOLNED$u!oES#0}c(sW7Sv?R;3HOOz_L!7#6=7MSt9Of5|Rf+X%7zI-f5C%ywdIWqj zLl}S6X@3L~NBZz;&A1ksn(H`j4W65A!pyJm;*P`_JPyfZ@EF&^Z{lPshae(Z4k&F@ zR!5cfR??PQ->;m8g6Q|R>m*O3=56yx-(~pn#k}xG9U_ie;(as}1;q}?E0)Mb%X{f) zO;L|k%oRszJ((P5jzcNim_=5D-dwnDSGDbJwE&OaLE2Ed^!2G*t~Bm}+`g2VMn^Kf z0RB|lEeE1T46183yby~#j=Tc{nQd`M=hM01Na7Pb#`+{RX582eCbq8pv_>zM0K7nD zLoQ`qhMIKBRsZPNAk-@8YIevTBDXWT8 z;1zk7s`S9QYFQsdwRi=m)>d>$2|~3|bI8ztdzf|LN{H4<1)w9B%+XsP{SIzSK>mFS zPbxV6J0{=8s9$SU+h@Z}5S;F%&lun21AhFu_BmBi>?L)?r;R)`&jer~KGnQKX(oN< zaQj~92wf8~&>57wvfHodkM7QDG(}dT8xi|!YS+8Uvm@`qlrJ5S^2la#iw!W&6r&?X zLzKU*i>S&zu0c#bCctXmZr#6Lzh|`r+{k0?`pg;JImK2i6w9j~$Aum6Cbppi9d+30 z);E&$ow(H8x_Vt!L7df+d4g>*vIh$5aX^%$hki4(Y$w$tN6dp=H?<=MV77G-yIT&m J|No47NYBAzRJ#BG literal 0 HcmV?d00001 diff --git a/Bundle/Win64/shaders b/Bundle/Win64/shaders new file mode 100644 index 0000000000000000000000000000000000000000000000000000000000000000..26679639736ed728edd46d3a919df33db3bf1eb3e8e3f09a6ded28d367a3338f GIT binary patch literal 2915 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 literal 0 HcmV?d00001 diff --git a/RenderMod.cs b/RenderMod.cs index 15aa485..c26f577 100644 --- a/RenderMod.cs +++ b/RenderMod.cs @@ -17,6 +17,8 @@ namespace ADOFAIRenderer public static AssetBundle FontBundle = null!; // ReSharper disable once MemberCanBePrivate.Global public static AssetBundle SceneBundle = null!; + public static AssetBundle ShaderBundle = null!; + public static ComputeShader ColorConversionShader = null!; public static string[] ModScenes = {}; public override void OnInitializeMelon() @@ -38,6 +40,16 @@ namespace ADOFAIRenderer FontBundle = BundleLoader.GetBundle("fonts"); SceneBundle = BundleLoader.GetBundle("scenes"); ModScenes = SceneBundle.GetAllScenePaths(); + try + { + ShaderBundle = BundleLoader.GetBundle("shaders"); + ColorConversionShader = ShaderBundle.LoadAsset("ColorConversion"); + Melon.Logger.Msg("컬러 변환 Compute Shader 로드 완료."); + } + catch (System.Exception ex) + { + Melon.Logger.Warning($"Shader 에셋번들을 로드할 수 없습니다. GPU 컬러 변환이 비활성화되며 CPU로 폴백합니다. 에러: {ex.Message}"); + } } public static void FixEventSystem() diff --git a/Util/FFmpeg/FFmpegNativeEncoder.cs b/Util/FFmpeg/FFmpegNativeEncoder.cs index 2d4e53c..d846ae6 100644 --- a/Util/FFmpeg/FFmpegNativeEncoder.cs +++ b/Util/FFmpeg/FFmpegNativeEncoder.cs @@ -166,6 +166,7 @@ namespace ADOFAIRenderer.Util.FFmpeg /// 매 프레임마다 할당/해제하는 오버헤드를 방지하기 위해 멤버 변수로 캐싱합니다. /// private AVFrame* _pHwFrame = null; + private bool _loggedConversionMode; /// 생성 시 전달받은 인코딩 설정값 (불변). private readonly EncoderSettings _settings; @@ -303,6 +304,7 @@ namespace ADOFAIRenderer.Util.FFmpeg // ── 기본 비디오 파라미터 설정 ── _pCodecContext->width = _settings.Width; _pCodecContext->height = _settings.Height; + _pCodecContext->color_range = AVColorRange.AVCOL_RANGE_JPEG; // thread_count = 0: FFmpeg이 시스템 CPU 코어 수에 맞춰 자동으로 스레드를 할당합니다. // libvvenc의 경우에도 FFmpeg 래퍼가 thread_count를 VVenC 내부 스레드 수로 전달하므로, // 0(자동)을 설정해야 모든 코어를 활용한 병렬 인코딩이 가능합니다. @@ -665,6 +667,7 @@ namespace ADOFAIRenderer.Util.FFmpeg _pDstFrame->format = (int)_dstPixFmt; _pDstFrame->width = _settings.Width; _pDstFrame->height = _settings.Height; + _pDstFrame->color_range = AVColorRange.AVCOL_RANGE_JPEG; if (ffmpeg.av_frame_get_buffer(_pDstFrame, 0) < 0) throw new Exception($"{_dstPixFmt} 프레임 버퍼를 할당하는 데 실패했습니다."); @@ -689,6 +692,7 @@ namespace ADOFAIRenderer.Util.FFmpeg throw new Exception("소프트웨어 스케일러를 초기화하는 데 실패했습니다."); _isInitialized = true; + _loggedConversionMode = false; } catch (Exception) { @@ -873,7 +877,24 @@ namespace ADOFAIRenderer.Util.FFmpeg lock (_lock) { if (_disposed || !_isInitialized) return; - WriteFrameFromBuffer((byte*)pointer.ToPointer()); + var preConvertedSize = _settings.Width * _settings.Height * 3 / 2; + + if (!_loggedConversionMode) + { + _loggedConversionMode = true; + Melon.Logger.Msg(length == preConvertedSize + ? "[인코더] 고속 GPU 컬러 변환 활성화 (NV12/YUV420P Direct Write, sws_scale 우회)" + : "[인코더] CPU 컬러 변환 폴백 활성화 (RGBA Input, sws_scale 사용)"); + } + + if (length == preConvertedSize) + { + WritePreConvertedFrame((byte*)pointer.ToPointer()); + } + else + { + WriteFrameFromBuffer((byte*)pointer.ToPointer()); + } } } @@ -888,11 +909,169 @@ namespace ADOFAIRenderer.Util.FFmpeg if (_disposed || !_isInitialized) return; fixed (byte* pBuffer = buffer) { - WriteFrameFromBuffer(pBuffer); + var preConvertedSize = _settings.Width * _settings.Height * 3 / 2; + + if (!_loggedConversionMode) + { + _loggedConversionMode = true; + Melon.Logger.Msg(length == preConvertedSize + ? "[인코더] 고속 GPU 컬러 변환 활성화 (NV12/YUV420P Direct Write, sws_scale 우회)" + : "[인코더] CPU 컬러 변환 폴백 활성화 (RGBA Input, sws_scale 사용)"); + } + + if (length == preConvertedSize) + { + WritePreConvertedFrame(pBuffer); + } + else + { + WriteFrameFromBuffer(pBuffer); + } } } } + /// + /// 이미 NV12 혹은 YUV420P로 변환된 프레임 데이터를 받아서 다이렉트로 복사하여 인코딩합니다. + /// + private void WritePreConvertedFrame(byte* buffer) + { + ffmpeg.av_frame_make_writable(_pDstFrame); + + var width = _settings.Width; + var height = _settings.Height; + + switch (_dstPixFmt) + { + case AVPixelFormat.AV_PIX_FMT_NV12 or AVPixelFormat.AV_PIX_FMT_QSV: + { + // Y Plane 복사 + var pSrcY = buffer; + var pDstY = _pDstFrame->data[0]; + var dstStrideY = _pDstFrame->linesize[0]; + + if (width == dstStrideY) + { + Buffer.MemoryCopy(pSrcY, pDstY, dstStrideY * height, width * height); + } + else + { + for (var y = 0; y < height; y++) + { + Buffer.MemoryCopy(pSrcY + y * width, pDstY + y * dstStrideY, width, width); + } + } + + // UV Plane 복사 (Y 평면 크기 이후 위치) + var pSrcUV = buffer + width * height; + var pDstUV = _pDstFrame->data[1]; + var dstStrideUV = _pDstFrame->linesize[1]; + var uvHeight = height / 2; + + if (width == dstStrideUV) + { + Buffer.MemoryCopy(pSrcUV, pDstUV, dstStrideUV * uvHeight, width * uvHeight); + } + else + { + for (var y = 0; y < uvHeight; y++) + { + Buffer.MemoryCopy(pSrcUV + y * width, pDstUV + y * dstStrideUV, width, width); + } + } + + break; + } + case AVPixelFormat.AV_PIX_FMT_YUV420P: + { + // Y Plane 복사 + var pSrcY = buffer; + var pDstY = _pDstFrame->data[0]; + var dstStrideY = _pDstFrame->linesize[0]; + + if (width == dstStrideY) + { + Buffer.MemoryCopy(pSrcY, pDstY, dstStrideY * height, width * height); + } + else + { + for (var y = 0; y < height; y++) + { + Buffer.MemoryCopy(pSrcY + y * width, pDstY + y * dstStrideY, width, width); + } + } + + // U Plane 복사 + var pSrcU = buffer + width * height; + var pDstU = _pDstFrame->data[1]; + var dstStrideU = _pDstFrame->linesize[1]; + var chromaWidth = width / 2; + var chromaHeight = height / 2; + + if (chromaWidth == dstStrideU) + { + Buffer.MemoryCopy(pSrcU, pDstU, dstStrideU * chromaHeight, chromaWidth * chromaHeight); + } + else + { + for (var y = 0; y < chromaHeight; y++) + { + Buffer.MemoryCopy(pSrcU + y * chromaWidth, pDstU + y * dstStrideU, chromaWidth, chromaWidth); + } + } + + // V Plane 복사 + var pSrcV = pSrcU + chromaWidth * chromaHeight; + var pDstV = _pDstFrame->data[2]; + var dstStrideV = _pDstFrame->linesize[2]; + + if (chromaWidth == dstStrideV) + { + Buffer.MemoryCopy(pSrcV, pDstV, dstStrideV * chromaHeight, chromaWidth * chromaHeight); + } + else + { + for (var y = 0; y < chromaHeight; y++) + { + Buffer.MemoryCopy(pSrcV + y * chromaWidth, pDstV + y * dstStrideV, chromaWidth, chromaWidth); + } + } + + break; + } + default: + throw new NotSupportedException($"이 픽셀 포맷({_dstPixFmt})은 사전 변환된 버퍼 다이렉트 복사를 지원하지 않습니다."); + } + + // QSV 가속 처리 또는 인코더 전송 + if (_isQsvEncoder) + { + ffmpeg.av_frame_unref(_pHwFrame); + try + { + int ret = ffmpeg.av_hwframe_get_buffer(_pCodecContext->hw_frames_ctx, _pHwFrame, 0); + if (ret < 0) + throw new Exception($"QSV 하드웨어 프레임 버퍼 할당 실패 — {GetFFmpegErrorString(ret)} (Code: {ret})"); + + ret = ffmpeg.av_hwframe_transfer_data(_pHwFrame, _pDstFrame, 0); + if (ret < 0) + throw new Exception($"QSV 프레임 업로드 실패 — {GetFFmpegErrorString(ret)} (Code: {ret})"); + + _pHwFrame->pts = _frameIndex++; + SendFrameAndWritePackets(_pHwFrame); + } + finally + { + ffmpeg.av_frame_unref(_pHwFrame); + } + } + else + { + _pDstFrame->pts = _frameIndex++; + SendFrameAndWritePackets(_pDstFrame); + } + } + /// /// 실제 프레임 인코딩의 핵심 로직. /// @@ -1008,7 +1187,7 @@ namespace ADOFAIRenderer.Util.FFmpeg private void SendFrameAndWritePackets(AVFrame* pFrame) { // 인코더 내부 큐에 프레임 전달 - int response = ffmpeg.avcodec_send_frame(_pCodecContext, pFrame); + var response = ffmpeg.avcodec_send_frame(_pCodecContext, pFrame); if (response < 0) { throw new Exception($"인코더에 프레임을 전송하는 도중 에러가 발생했습니다. (Code: {response})"); @@ -1027,7 +1206,8 @@ namespace ADOFAIRenderer.Util.FFmpeg { break; } - else if (response < 0) + + if (response < 0) { throw new Exception($"인코더에서 패킷을 수신하는 도중 에러가 발생했습니다. (Code: {response})"); } diff --git a/Util/FrameReader.cs b/Util/FrameReader.cs index 513cbe5..9ffbb10 100644 --- a/Util/FrameReader.cs +++ b/Util/FrameReader.cs @@ -3,6 +3,8 @@ using System.Buffers; using System.Collections.Generic; using System.Linq; using ADOFAIRenderer.Core; +using ADOFAIRenderer.Util.FFmpeg; +using FFmpeg.AutoGen; using MelonLoader; using Unity.Collections; using Unity.Collections.LowLevel.Unsafe; @@ -13,6 +15,10 @@ namespace ADOFAIRenderer.Util { public class FrameReader : IDisposable { + private static readonly int Height = Shader.PropertyToID("Height"); + private static readonly int Width = Shader.PropertyToID("Width"); + private static readonly int DestinationBuffer = Shader.PropertyToID("DestinationBuffer"); + private static readonly int SourceTexture = Shader.PropertyToID("SourceTexture"); private const int InitialBufferSize = 16; private readonly Stack _statePool = new(); @@ -42,6 +48,24 @@ namespace ADOFAIRenderer.Util } } + private static unsafe AVPixelFormat PredictPixelFormat(Codec codec) + { + var candidates = GPUUtil.GetPrioritizedEncoderCandidates(codec); + // ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator + foreach (var encoderName in candidates) + { + var pCodec = ffmpeg.avcodec_find_encoder_by_name(encoderName); + if (pCodec == null) continue; + + if (encoderName.Contains("qsv") || encoderName.Contains("nvenc") || encoderName.Contains("amf")) + { + return AVPixelFormat.AV_PIX_FMT_NV12; + } + return encoderName == "libvvenc" ? AVPixelFormat.AV_PIX_FMT_YUV420P10LE : AVPixelFormat.AV_PIX_FMT_YUV420P; + } + return AVPixelFormat.AV_PIX_FMT_YUV420P; + } + public void RequestFrameCapture(Texture texture, long currentFrame) { if (!RenderManager.IsCapturingActive) return; @@ -62,15 +86,74 @@ namespace ADOFAIRenderer.Util state.IsInFlight = true; } - var bufferSize = texture.width * texture.height * 4; // RGBA = 4 channel - state.Setup(currentFrame, bufferSize); + // GPU 컬러 변환 활성화 여부 검사 + bool useGPUConversion = RenderMod.ColorConversionShader; + AVPixelFormat expectedFmt = AVPixelFormat.AV_PIX_FMT_RGBA; + + if (useGPUConversion && RenderManager.Instance) + { + expectedFmt = PredictPixelFormat(RenderManager.Instance.Configuration.Codec); + // Compute Shader는 NV12와 YUV420P만 지원하므로 그 외 포맷은 CPU 폴백 처리 + if (expectedFmt != AVPixelFormat.AV_PIX_FMT_NV12 && expectedFmt != AVPixelFormat.AV_PIX_FMT_YUV420P) + { + useGPUConversion = false; + } + } - AsyncGPUReadback.RequestIntoNativeArray( - ref state.NativeBuffer, - texture, - 0, - state.CachedCallback - ); + var bufferSize = texture.width * texture.height * 4; // RGBA = 4 channel + state.Setup(currentFrame, bufferSize, useGPUConversion, expectedFmt, texture.width, texture.height); + + if (useGPUConversion && state.GPUBuffer != null && RenderMod.ColorConversionShader) + { + int kernelIndex = -1; + if (expectedFmt == AVPixelFormat.AV_PIX_FMT_NV12) + { + kernelIndex = RenderMod.ColorConversionShader.FindKernel("CSMain_NV12"); + } + else if (expectedFmt == AVPixelFormat.AV_PIX_FMT_YUV420P) + { + kernelIndex = RenderMod.ColorConversionShader.FindKernel("CSMain_YUV420P"); + } + + if (kernelIndex >= 0) + { + RenderMod.ColorConversionShader.SetTexture(kernelIndex, SourceTexture, texture); + RenderMod.ColorConversionShader.SetBuffer(kernelIndex, DestinationBuffer, state.GPUBuffer); + RenderMod.ColorConversionShader.SetInt(Width, texture.width); + RenderMod.ColorConversionShader.SetInt(Height, texture.height); + + var threadGroupsX = expectedFmt == AVPixelFormat.AV_PIX_FMT_NV12 + ? (texture.width + 31) / 32 + : (texture.width + 63) / 64; + var threadGroupsY = (texture.height + 15) / 16; + + RenderMod.ColorConversionShader.Dispatch(kernelIndex, threadGroupsX, threadGroupsY, 1); + + AsyncGPUReadback.Request( + state.GPUBuffer, + state.CachedCallback + ); + } + else + { + // 커널을 못 찾은 경우 기존 RGBA 텍스처 백업 기법으로 폴백 + AsyncGPUReadback.RequestIntoNativeArray( + ref state.NativeBuffer, + texture, + 0, + state.CachedCallback + ); + } + } + else + { + AsyncGPUReadback.RequestIntoNativeArray( + ref state.NativeBuffer, + texture, + 0, + state.CachedCallback + ); + } } internal void OnReadbackComplete(ReadbackRequestState state, AsyncGPUReadbackRequest request) @@ -101,7 +184,9 @@ namespace ADOFAIRenderer.Util } Melon.Logger.Warning($"[OOM Prevent] GPU Readback failed for frame {frame}. Queue deadlock prevented by sending a black frame."); - var byteSize = state.NativeBuffer.Length; + var byteSize = state.IsGPUConversion + ? state.Width * state.Height * 3 / 2 + : state.Width * state.Height * 4; var blackFrame = ArrayPool.Shared.Rent(byteSize); Array.Clear(blackFrame, 0, byteSize); @@ -117,14 +202,32 @@ namespace ADOFAIRenderer.Util { unsafe { - var ptr = (IntPtr)state.NativeBuffer.GetUnsafeReadOnlyPtr(); - var capFrame = new RenderManager.CapturedFrame + if (state.IsGPUConversion) { - NativePointer = ptr, - Length = state.NativeBuffer.Length, - ReadbackState = state - }; - PushToQueue(frame, capFrame); + var data = request.GetData(); + // 비동기 요청 버퍼 데이터를 지속성(NativeBuffer) 메모리로 복사 + NativeArray.Copy(data, state.NativeBuffer, data.Length); + + var ptr = (IntPtr)state.NativeBuffer.GetUnsafeReadOnlyPtr(); + var capFrame = new RenderManager.CapturedFrame + { + NativePointer = ptr, + Length = state.NativeBuffer.Length, + ReadbackState = state + }; + PushToQueue(frame, capFrame); + } + else + { + var ptr = (IntPtr)state.NativeBuffer.GetUnsafeReadOnlyPtr(); + var capFrame = new RenderManager.CapturedFrame + { + NativePointer = ptr, + Length = state.NativeBuffer.Length, + ReadbackState = state + }; + PushToQueue(frame, capFrame); + } } } } @@ -178,6 +281,11 @@ namespace ADOFAIRenderer.Util public NativeArray NativeBuffer; public bool IsInFlight { get; internal set; } + public ComputeBuffer? GPUBuffer; + public bool IsGPUConversion { get; private set; } + public int Width { get; private set; } + public int Height { get; private set; } + public readonly Action CachedCallback; public ReadbackRequestState(FrameReader owner) @@ -187,19 +295,34 @@ namespace ADOFAIRenderer.Util FrameIndex = -1; } - public void Setup(long frameIndex, int bufferSize) + public void Setup(long frameIndex, int bufferSize, bool isGPUConversion, AVPixelFormat pixelFormat, int width, int height) { FrameIndex = frameIndex; - - // 버퍼가 생성되지 않았거나 크기가 다른 경우 재생성 - if (!NativeBuffer.IsCreated || NativeBuffer.Length != bufferSize) + IsGPUConversion = isGPUConversion; + Width = width; Height = height; + + var readbackSize = isGPUConversion ? (width * height * 3 / 2) : bufferSize; + if (!NativeBuffer.IsCreated || NativeBuffer.Length != readbackSize) { - // 이전 메모리 해제 if (NativeBuffer.IsCreated) NativeBuffer.Dispose(); - NativeBuffer = new NativeArray(bufferSize, Allocator.Persistent, + NativeBuffer = new NativeArray(readbackSize, Allocator.Persistent, NativeArrayOptions.UninitializedMemory); } + + if (isGPUConversion) + { + var computeBufferSize = readbackSize / 4; + if (GPUBuffer != null && GPUBuffer.count == computeBufferSize) return; + + GPUBuffer?.Dispose(); + GPUBuffer = new ComputeBuffer(computeBufferSize, 4, ComputeBufferType.Raw); + } + else + { + GPUBuffer?.Dispose(); + GPUBuffer = null; + } } private void OnComplete(AsyncGPUReadbackRequest request) @@ -215,6 +338,8 @@ namespace ADOFAIRenderer.Util public void Dispose() { if (NativeBuffer.IsCreated) NativeBuffer.Dispose(); + GPUBuffer?.Dispose(); + GPUBuffer = null; } } } \ No newline at end of file diff --git a/Util/GPUUtil.cs b/Util/GPUUtil.cs index b6a9293..5beebea 100644 --- a/Util/GPUUtil.cs +++ b/Util/GPUUtil.cs @@ -33,27 +33,43 @@ namespace ADOFAIRenderer.Util [Codec.VVC] = new List<(GPUVendor, string)> { (GPUVendor.Unknown, "libvvenc") } }; + private static GPUVendor _cachedVendor = GPUVendor.Unknown; + private static bool _hasCachedVendor = false; + private static GPUVendor GetActiveGPUVendor() { + if (_hasCachedVendor) return _cachedVendor; + var gpuName = UnityEngine.SystemInfo.graphicsDeviceName; Melon.Logger.Msg($"Active GPU: {gpuName}"); + _cachedVendor = GPUVendor.Unknown; foreach (Match unused in Regex.Matches(gpuName, NvidiaPattern)) { - return GPUVendor.NVIDIA; + _cachedVendor = GPUVendor.NVIDIA; + break; } - foreach (Match unused in Regex.Matches(gpuName, IntelPattern)) + if (_cachedVendor == GPUVendor.Unknown) { - return GPUVendor.Intel; + foreach (Match unused in Regex.Matches(gpuName, IntelPattern)) + { + _cachedVendor = GPUVendor.Intel; + break; + } } - foreach (Match unused in Regex.Matches(gpuName, AMDPattern)) + if (_cachedVendor == GPUVendor.Unknown) { - return GPUVendor.AMD; + foreach (Match unused in Regex.Matches(gpuName, AMDPattern)) + { + _cachedVendor = GPUVendor.AMD; + break; + } } - return GPUVendor.Unknown; + _hasCachedVendor = true; + return _cachedVendor; } public static List GetPrioritizedEncoderCandidates(Codec codec)