From 6200b74e77c5b90def007dd43c454d8147cd24a5 Mon Sep 17 00:00:00 2001 From: Diego Lopes Date: Sat, 21 Mar 2026 14:00:59 -0400 Subject: [PATCH] Add OpenGL 4.6 backend for NanoVG This commit introduces a new header file `nanovg_gl46.h` that implements the NanoVG rendering context using OpenGL 4.6. The new backend utilizes Direct State Access (DSA) functions, immutable texture storage, and explicit layout qualifiers. It includes definitions for creating and deleting the rendering context, as well as functions for handling images with OpenGL 4.6. --- .gitignore | 1 + .gitmodules | 3 + .vscode/c_cpp_properties.json | 16 + CMakeLists.txt | 3 +- external/nanovg | 1 + samples/font.otf | Bin 0 -> 67004 bytes samples/test.lua | 43 ++ src/lua.cpp | 225 +++++- src/nanovg_cpp.cpp | 171 +++++ src/nanovg_cpp.h | 149 ++++ src/nanovg_gl46.cpp | 1208 +++++++++++++++++++++++++++++++++ src/nanovg_gl46.h | 31 + 12 files changed, 1849 insertions(+), 2 deletions(-) create mode 100644 .gitmodules create mode 100644 .vscode/c_cpp_properties.json create mode 160000 external/nanovg create mode 100644 samples/font.otf create mode 100644 src/nanovg_cpp.cpp create mode 100644 src/nanovg_cpp.h create mode 100644 src/nanovg_gl46.cpp create mode 100644 src/nanovg_gl46.h diff --git a/.gitignore b/.gitignore index c3f6db6..c8a615f 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ cmake/ *.flac *.aac *.opus +external/nanovg \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..4845363 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "external/nanovg"] + path = external/nanovg + url = https://github.com/memononen/nanovg.git diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json new file mode 100644 index 0000000..a7e045e --- /dev/null +++ b/.vscode/c_cpp_properties.json @@ -0,0 +1,16 @@ +{ + "configurations": [ + { + "name": "Linux", + "includePath": [ + "${workspaceFolder}/**" + ], + "defines": [], + "compilerPath": "/usr/bin/gcc-14", + "cStandard": "c11", + "cppStandard": "c++20", + "intelliSenseMode": "linux-gcc-x64" + } + ], + "version": 4 +} \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index b586e5c..2413b2d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -57,9 +57,9 @@ if(stb_ADDED) add_library(stb INTERFACE) target_include_directories(stb INTERFACE ${stb_SOURCE_DIR}) endif() - # ── Local dependencies ─────────────────────────────────────────────────────── add_subdirectory(external/glad) +add_subdirectory(external/nanovg) find_package(OpenGL REQUIRED) find_package(X11 REQUIRED) @@ -90,6 +90,7 @@ target_link_libraries(${PROJECT_NAME} Lua::Library sol2 stb + nanovg glad PkgConfig::GST ) diff --git a/external/nanovg b/external/nanovg new file mode 160000 index 0000000..ce3bf74 --- /dev/null +++ b/external/nanovg @@ -0,0 +1 @@ +Subproject commit ce3bf745eb2d2dbc14a50bf2446783f691ac4353 diff --git a/samples/font.otf b/samples/font.otf new file mode 100644 index 0000000000000000000000000000000000000000..35daee439270ef63daa106a296471da9a7171e22 GIT binary patch literal 67004 zcmdSBXIvCXw>VtgJ<~l*!+?wfGGosG3SuS;W|D}aNK#ak93@FMA*OZBVb?Wh#hi1_ zIj%Y9b#=|LhAq_p>27tyeV*UFAKou7ZuP0EuC8!WojP@9BVuAAkQJC)rtp@XC5 z_naOG>1q&K_Axj*EEFL`BTBg$pei&tq(d!#?LLGK)Q56GXt#*yem9y3Q2rUAfb`Jl z*uaT%MmQphhVSX^5z);(v!{8zLx^e*?GsaSlL}O|{#=HTCA7Cl&rC~7Jy1J!0ld2f z$u|=!*gnb*(7ra5>u2T`mlp)-ETIf-)bDeX$_wZ{-QX4F2M;zcDL2hO{~Ua~=RHD< zO+kK9@s|n&-yTrFdq+gUQ|U1CRIjlI60BQ)Kn(K)YS3U8kJlo6*#7e9&%s~p6%ou# zC^E28`47Hhj(xF5NYMk{>lG1FPhVe(fZl^pH|dEKv;{R8DFKe9?S40RZu z-~Qe}Mob@2bIJ+XQy-88^$2O`xkyV@BU>~D>N=nxNGjTZ9BCfeqg_x}2PiDUCSM5k z2dYK6AqDj-q`Sz9x&`nLq^EvI*3=!yzXAw(vXbA;L?t3m>M^pSEn&|0Q7xF84Ta%d1NhEX%Ind7B%niI zbkswnM>~+7ehY0eKm&}+1Eio`0MuOr{2c-K9ujZ|eG>5f8q!B7?}2w+k&%i*#Z)K2 zLu2Id7584^xYPsa#}4Tr)e_s#_Q-)+iahCsFqe+V1@LP{cY(U2s1e}RlPZ(xL9x)@ z3hBgk&_T!`(gmFZUfBrssmN9OE(2-ld`JU;uFsIIgl`e&|4K{=)QL0z{Qo_nU0)7D z`V46$q`g2R^d0{q4gQ{Z)KV&oH28ZGX#ljMJ_9ZQ-~TBwO`%@Ig^2&ZCz~(NDI4TQ z*?hSK=?TqA?`8apH28ZG`3uSt4MaM8OF#pWRw6CLbxSn=H-xoIJn(NQ^5jI|RUU9C z@+Qpfe}y9N0{=)n{C5ca4BsQb<3?D^ZGZ~_o&vZB;O_uWq5WTuOLPRjqlZEJQD3e~ zybt^be5FJ?zI+npK;%Ucf;N<)Ql<;^uNUd_AE8Jq5num;49Fpq0`(wAA}{|Fet99v zmdM}#1A@GXJTJ*zuneU#&|bv7$OHcuD9I|wizurSjlRh&$PM6HAgQ4VzUZqSTBK>7e{!+`(OAXld#iSkti zGIR@|CnSGJyC6+~I`lakQWc~qs!%F-gnTNb6Ohu-JE?9Spnvs)(CXBmEPY+wI$xD4`Yk%fUiI{ zSxHvl9o-&r!0Q6YLTx%2zPXDysjqXO=UziUQ=#AeP~Y@RwNx+4n>ZHKfo@bu^931V z&LR~P3w=YK$iLE@+d*06FFQyl5YLpuSl-a5xE?VTLtC-UL`W@>9pjDkpqKPC^hN&) zQ2Z_pDVbi-Um$!duE`8@?Td_bIO6Fwu#U^1YsED~0vY7#LOCzh!CGiPD1QX{KSKiJ z0&SK;-2s3U)IR~;d=uacktcvRpkJt~4dba92I@r_6#E7}B0-=Hv=Qhgz_;UIpWg<3 zJ%jbYn#6gC^rcO(hT1UyAK;z1E*M|bvy!fv3;PUwOAiJbyThDX!uxpW`ysRed`M$p z&^zDc6>uZThlnRw8_bQ9_DtBfzC&5ZNIC(%_;Tof=z?!D{!OO8%J4TC{5SkAcfDb5 z|JIrRUzh){`t`rG`=&R)!S8tchQI$K{H9O-H~2sF+c*95zrnxhIf<7xgB)ie8*~6< z@&KxXPQ!jT9OQpEszc2{ZnO^7gX9W&sVTD@bVN_E{GNavCZJ>i|SIf zfG4dWuLCIN;hkg;e6tn)OFQ}!@O}Z%xGwNsCh%?y@NpyHul7hO=~FM@*(ac9>>zcN z>S{w?O^=8AY(z7^0O8Ck#!&Qta$AtE2B4R&!QOBKQUK`ZZz&LL zp-M;&kcLu@NVLVJM7tue#o@0m)Ibi(EUgwRpdh~y3;<@kH>?KL&PY7I8?0ERY)U z$Py@{LDoRiT3}0S;T!?C*+Lkq58-LxuOQh4K+lKP-D~t zHAT%(bL5UZVDIok-pB{}q87*x`J5#zpth(T*!%$~5Cx%N6oNXSP}C8Hp-!kX z3P)W~R}=vUi%1lOqEQTrMcq*k6o-1EUZ^*UM}1IVlz$njqm^g{Sn#XSTC@hN`1Qa|o6#n;1#LrHfl52kZnTHaLxm^}rK4=% zp)#}p6`&k?CMre!=;dfMnh9FAh+aXjM45p11@t0%AvK1sLb+%VjJ*tOnM#z3^64d% z7Y!#ml!pe>{ZR@UhDM@aD3;>rbUK6XN9WL)ASc`D)o4G}h%SLsQW0HDZ>G0^g4YD;ybdQ%nDBx)-4Gc}i5NUf)CQ@>LmsA}3k zx1a;*E_6>ig)XFz(I@EJjDoRbw2XnN$G9?XOjBkoGoD${lFC99NuETvt3*yi#hFlZ;kIt#TP+IyZLi z=$z=B=6uBElznO>tKzKsrQtb zt^;cfpu@ke@hGhE8iN@2A8Q;5Yn;d|Rv^V>#V?8lij|7Bij9h`ik+~=LyBXt#%qcv zikC`TSfi!U)>z-z+8Au?Wb9>3Hs%;BjZpD70YxI}aSnOEoxWaJ* ztZ}>JZdjuotTEcDx3tDue_La!v_?cISR?i21wvmAK{_BV_sdT432^h5jqqIfWx*Hm zT#P_Qzy5u(5HnwX{`}zcC9uybZq?ziJ+Ieh>(3=kb6==>8|M{{BtrIRbKJV(#9&dsE>#_TK1wgYG5V1AFuC z<695JH(zzu9mv2ss3mYZMATX8JZMDmPhti?UqcXP^nZJ zl}=?)nN&8_kIE7EB`TN7gMFzjYC`41zSNv55O*c2Kk|Z8kT+EXyHXpf7`3KKs8TeU zYDAS$<LPWCx=dZ6u2R>i>(mYECUpyR=^g4WNaTHx$A^Hk3Xsa* zKrVj=$qc8Sf;2t@`She-P%o)h)NASu^_F@EJIqH4Q)a4~`jaA54fTooOnspdP0=*X z&4;`ADNExnFj zPj8?%(woHn7c>bRWe`PC+MxXWoYJ)7tdyjtQE3?^IZ1_Lb&KkksQMLE{n`>$zoDz& zGUhr8QqPi^Z>gyJIk@f@`?`vVy39;v-TI@f>$)0hZ39Q}tl_rQHRu$KDm9o>VXR0CP4nor!1SW(E2XTm_5`gh@s6t@g3Sflsz>G71<>movtp z^nUs%eV)ES->09@ujr37flXA&s2Ll^juDvpOk>8A@dwo#%5-I7LG7l1f-PbOGb3PY z{fU_cCd6`P3$vR!#GGV)Wv(%gm}kserkeSz;1xQBQPD`@sc5ALRYWWLDDo6Vih+uu zicyNuiZP0biYbbp!Sq~8iDdy@T?y~f^UAG0sm_blOP&VsYy>^URn!ZqQ%xRzW1*O80hx^sQFRIVRa z$PM5sxzXH2ZW=d-TgN zRc%zks?Mq?RZmr-Dnpg0Dp3tqjZlqIO;Sx$%~mZ@tx;`O?NS|4omO2`-B8_EJyE?< zeN=t2P+0I5wiXT+jus6pnpyZ*w6+Mc=wuOT5oeKLk#3P|QEV~DVz|W^i^&$#E#_G) zwODPj$zrF)0gK}n=Pa&R+_rdR@yz0_MYS5KIkiTuQ`c43Q#Vq3sQuLK)E(4a)G_Mb z>ST46xUanrJ-m2cCKCC{azM#IYzNh|O{ZjowUBffH1#ivU z@d96;Z_Io0{(O5rl<&&N^6`8MpUwB@%lV=F5BvoFXMQ%nh_B*T@*DW={679De}=!r z-{c?gPx;q8=0961EiEnWEsd59ESp*STDG+evy8OtWtnc7YgueL$a1*l7|Y3)(=F#& zF11`|xx@0HwJr7$1vStJmU zT$omxmLxp`@-y=D()uOIASfxNq&O`_dIqKD7bm5pq~#T-e9r}?BtbVR(lc0krha`E z<|h@0$la&OAVlgvO?rlW?;-7bE<_$OO$H%SKQJ`(A1Zy5Aw5ICx6k;V6Bp&-=_v!R zj>$=dnUKPYvvN|?vZQC2JcTS7gh@SRNzX8OxGWijeeW}@Q&zUrp_i`=T7*kj$dR6M zRiF%l!lhZ}NYAd)`@FwedHYDMynVY$@AIT*NM1%-UPgpGoqQQYNb|{;o)MWPc^OHC zCAm3CCB^xFFGNW_7yi}1k32u07Ew~qh0-%xdS4_xqf^pSvvP8hioQZ=I=(Wte7$0% z_QldO2C!BPDOSQ+iS&$>Cs!haSZQ`8(lfR&D=(u&e8m2Jf+c@1#L8$bkwK4?tiqI% z-1MBZ^0L3OJ^#uM_$%8>n)^WM+3P!127b>;tMChvK?e!=d&?j=A~U}*FJFAbijNZU z5h|@PLwZWhI>@a%$mHqZCs+E5z|%{T6HjjsspKu;%F{bYE(J@#*GmE|o|K!MniQASEh{=JBR8pAW>&YNEU7_&f718)r2VaT_(&R2>|4r|$S?mfj`V3V zG|dnPOijxvPWsx%K&TYE5C<2#=$BMbkd!0MLK-$Os64A6t1vTPDg}8KiPI`hD)~A} zL1tD;W)_T?FD~-$>-*O<^8bdMudk%pO5e%z2I_meOWfh@E|JaKU823WyToJO?h=8$ z-6aBh%LMjzmzc}jUE*tRcZsjPJtQ{t_K-jKkSOgft8#A-iQwKIa-SY@XP)wSGEaDW zN=)W0vxc{)+|pAX-Sg`Q5_@@j%Ap$TRbi zXXYc%%t!9UNAAT(o`jG5xsUw0kNmm6yT}XP{_-;Y<<9)&&iv&G`pXmamnY~ickM5C z?JrL-K<*_#?j=C(B|z>aKyDczw+xiu13G=h(;EPVXLN{+y6k)QzN@g$-_Nggjy9#0}6RLbK8%Hsvf<4FXC zck+0F@_2#rc!BbGf%14k@_0e=ctP@b5+itoNQ?kL9xq5qCAk}kzn~=I1AvGK0P^P&V|jW=!VN&0g@;Fr0x<5fl5$eB($fnH z^HWPwii=8M`z+4N&ntv|JS8b7D=$4OFROS!R$g%$;423<&%*rjwEiVYIikrR!ko0C zA}Nzmn3hxw=7m@Z)(G^InUg;djM_AqN(dY|WFTS*O5zd#h*$z3VhMnVB>=vXH7V%? zDJM}%%86Eyl#y+y@5ZBSVtp@)&7_b1vL3sq6~ZQ-=jhYS0}emmy&#TRMUF|1#jspB zlN9!I%ujcW8Bma)3aTb6#jyieJ4KFRc`41}-BRKkI(qo~`#1G;_w@eHjsDi{SLe!K zHR!46D^bG5ljtL92NywwaP=|)t~}<$WzKT&ifjR&$R%(u-a`+;srV=Q48D&ZR6aP~ zHh@d(6u7ExQxCxn^$xBy^x(}11usTAIFAbG3i=0d3GJevGd$B4oW4VtY0P3~GqZy^ z!klMrFb}{B@tz?HH-)<*MUktR4i_7z6c-iMN>-^+>XnU@EtKuSgAk)kP^K$$l_km{ z${&xA-{S0m?w~E`w z9pcV$54n#j3zb1tU)4<2R@Ft-U6r89gcJM_)hN{jIKR(TJ+|x_g z183;P>XT{$C*)3i5BI;4E8eHOXqZ z)qbl(RyVBPXjB@b##8mNy4AxB6EYhsd?ADyq+|<0#e73f>cC>C}-OReRb+~oB zb*god^>FJc)(foHS#PsGYJJ)Iq4gVUvkhxg+s4_(-KMQgxJ|50vQ56tV4E>Ezt}9c zS#Go0X1~p8n`<`rY@XSCtVPwbtYu%zxmMGrLK#1P@Wg?WhN^gc4Mz+YupJqt!>2fG z6Z-}}ON2?d4F=_1GkTfA3ZhK%qBcGPFtE(Y7v$#{K>3q6!{q%WLe=_h3 zO3jsJeAYOu#_g~LcE&2cRYfm>ub3$Cx3Twaz7%)i3o0;$_hFj9wxARXm^FKp@#ZYM z3j(>u5p+KA#ksvp53MmmXVp#0D4rx^8~z7DqcbO0$0$SDnxs!LO5S!Uw!`6A$Lqoc zUbpk;_(S^j`HTCM=ZqLwW}F+NoIYp9#9s_^rj8soX!xihBTQvOe;81#j~J%gxQ)=5 zQ@X1+=+#y?TlnE#}^2i&SVHyVAXqb-7-F{v?SZelQufM2~g*b4SX3j3vl*1NZ_%+ zBnUvp{KOH1#!nn#JVkV5Jr|bQr(^6p;cVdB;|G@<1}vJ7{Ffy>`f2H*i~0kE^FPIn zO)V)-{!yjL?!PGBkW(TmeDou4Z%x}I>tcS_aRb^$H zmgVoARA$R#8)8RnIGCJBmCo3X86ZGm5q83pu`^@##{G5P2}z#q6H0~G?XV9He0c}^ z7|jvHnd5=$NW+Pk4a9y1+#J)VvFp=5nA#%!(v}UAAuBOj?}`5@J|U0SAKi&g}g&1o-t?Pa-Cm!&v<_WL9q{T1MG6(5$4Vu z?c093Km<}k%CQ6CaUgE??iFsl%*30c@iX0rk)00_X9H;z74DZXJ8egvX>G}lA$Q7D zTZXN#%+#kR59wp%S0wkC-PMpI;HJ;tV)vcHw+>uq;(>ksBbTfiv9e;8vBtONZINK& zzn18lXkF`+J+ENYy#)e4Z0V9878wp~Sb5dN_Zyy*nvwZaRX-#Dq}5G8T|k3Z{JHM? z|Jc{#EN?5(9Sb~3BpmQ*34e6yz}{0$Jbv)3NWdPa?_i5^!9;k)H+dU<@1dVtt`W%0V z8NqZM^yTr~wE-bnnO!@t?RWn2iZv&UJ?wG@4#>l&doiiZYsL^5P>pQW| zgl`iYa)RsBzo0{C|EkkQe1d4nZI0-!#p8OQogQGVxsn(-(v4hF;!C7}lKS11^`sK8KCJ~VrnY5f4@+$obMPciWLH}ekN`DN)n z;_}l2Oe7G8u)ME;@7Td!@uDH9<1W~@(R16Mv7V@4i!yKim!cE5BeNwpbXnD?#fAeL zS6w#o_47h?6>8 zwrKhkH^B_f#=z0y1T+#S_abAJcr35)u_627fmOfEnrGyADtW_UK-+?t~Hd#Y^~k zCTwD{zQXwfX=xx)gy(gj157|34K{#AHZjtZ%Bba7%deJHS_mr&7q43~u;mZsMHm$yF z(yptVFO=w^eV>5?GlrH;m@>e4N!)dI2xrO{2z#?Oc8W|)?Vdj^r)r2%`}S8sV|K>R zb*5<3`%TbnP0*r9Cq3Z`dUx+$1c)*6WrFt8C_($#T&HG-4tL|Om#pqK*>PXOW? z@C@w6YCq2v_~ycGQGZ=8v&HF$wJ*)C83IAow{+T%X}qpq`O*#R7A#r5Y+g}X#-M^+ z(>t86+dnd6MR$W=atv`G^v0;`r?>4rcRqP@_w4eD0fS5zCe2y1&ahy5W!VIi254h? z-U!pbipZDDr!LrDyVsm!ho5pEI~;D>x@T@ou2FlZwKx%MS&F@YdN2iEcVK=+YG!F! zLBD}>HkiWMj+>Is|GIPY*)0RAa%LEp&YH1&h5kUF^- zv7l4CNjp&oGKzE|la#nAi>-0El7wSR9L~X4;Y$8oK}=Ow1F>*-BP~h8OZBk*-7Q=8 zfi24+q2gfqz_eK#jifvOq|95uw)l$<-|Nn$ zjxH~cGW0!125`I^7{eo8u`$Q`KE}>^T+lRrzC8!>n8+&G9CqvoDy%+HcU+wB`~%>rZ$$My$rgY@xu7M`Uf9a%D! zEK=e{L}YmZmvWac;htn4Yz)S($C<_E`nkeCRV%-3%Hrim4SQFW_sbek(P_Ae*bitj zh&bz8?Tf(*eu?l*_l~Qfa6L05C1m@Ncx6+LKel7xXQcu@-SNtEwQ*ZLvtg?A9rTd}9w z-EqkNyBlyz?+3Pgn~GNraKORKTXBHvYg>LluC@2_K5F6V=cgEKgb@jaqp=Z35(v)X zv6TS4V7{y0oYyjbE( zA^=03ilGyJCp>WsX0b1}q_W*o}GdP|o2XNUq{m{f&y! zk()2QxEH5^Ro?*S+JKji-HU)$)%m)5C0VgVsdwS{10T0P-$zv)etw4e$VXT9P7T&K zz88TFU;vJRtpzL{Vgcsv>vemtUR>YZ(XV%87a+r2pqK|(527K=3lu-60;48>;G>V- z-|=8iz>(G(k`_#b$}$`;&eooX`O62#x3_t~JDwvRdfZBJ8>}{OEd%Sj4SNsIQH}>^ z1!r!3wj=JERt9RY#)0%SI{>1H{cU{UKHh)Z7LKVi;4B@2IWc>$jJkK4!Hsx|8cl%@ zjDla^&`L4Z*rEj*j8LFJLF{27jWQ5~8AHq~L=8clFa$P3tT4((5c3Sd&ZrVW*fYck zL)bIKcA+5%;`YFY27yKJgB^$^hA1Nhv5B)0xYNYI<+TW{q9E=ILTDi_8iJ?M1_VAf zw2_8@X0(+C&l`jxp}p{XFAD90AC@BU!hu5$9Y*K?jgBCQv=&2?Pa$-if|x)E4upUu z2y}z!K!{sH=V%BDL>FlYkcQYm@Y|tl6ht#YgdlkIAf6fBN9Ya>LCX;G1RgzVTtf&d zq_$W<925jXQTwbQ0vdv!sWTSf_k)N+>Z%SxqNo=PI)>0A1kq&>vQ53Sh1e;G{ia!j zhEWi34NgNe6hS;V&3$1w9m+=#i3efk5QYUoj3^61fH%Z4q5=w{8X)9eNIDf^ZS=6>VesCucAROzaIs-IPl zR4-H?Et*0QPD_hCi&6-@*{W^|K{P$o84x}*Sv^x-rQW380l_jaAwb5158yjPNX!=g zB!7#40l_W~mQE1jl4n_JInwf)Yl6nu9o}8xYJW-N9Q)q zJ)H+Qk8`eaKI;6mUekKz^+wg3Snq6o`})o5x33>rKehhg`ZMZptAEQy;S%W5)g{^` z&85O+tjm0t`>uj(Lsx&-9qH#pniv74n^YqvPJL2mQi z4!d1-6ST?EC#I;GsCaas6n?^UC*Ysf1-OGx2TYE-%#(NI*T;#dg^P`uASD;shSDDv*Z_2x#cPH;u@2TD& zecJnE`;7KE?epH((KpIB-S>J6ix#6>Z1iLN^nRs&Klsh?yXnvRkMQ5tQqeNL<))T* zTh(bbw$*{wwOc#4?%jHD>oKjDwcgjp(57XZ(l%GxI<@WDHnZ)Vwny4NYiHLkx80g{ z58GR|@6djD`-$y0w|^N>J0KunV8E1sc>zxX+Xapf{1lWDR33CZ=vlBsaP#2U;PJr+ zgI|Tz3TYM+7g7{5JLFsk%MRT-jO}nO)HO6ebb09Ej@phz9d~wo)bVpzy|A#bAa%z-SE2MG2w^0Xu5cGDerQ!3+dXb>%s_&h`5OL-I#81 z-DY*$6sd?zi#!rlC(1i2CMq$iJZfCj?5Jf?hoi1Wy^E%zwb8E8LD7AqE2DpjUKhPP z`atyA=!-FSF)=ayV&=rMv2|j7V>`qq$7aQjh#ecdBzAM`jqXf$&+c8imv$f4eQo#c z-S78cdieB6>#?ZE={P3NF3vYDBrYv(Xx#F+t#QA`-HrRRr@E(m�cmd(Q27yyw-P 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 URWsB~^*fb8dB5%o9Y4tb1q4L^Bme*a literal 0 HcmV?d00001 diff --git a/samples/test.lua b/samples/test.lua index 682669d..16702d1 100644 --- a/samples/test.lua +++ b/samples/test.lua @@ -1,5 +1,11 @@ effect = nil video = nil +ctx = nil + +timeDisplayText = "" +dateDisplayText = "" + +timer = 0.0 function _create() video = Texture.FromGStreamer("filesrc location=" .. Resolve("video.mkv")) @@ -21,10 +27,28 @@ function _create() } ]] effect = Effect.new(fxSrc) + + ctx = GraphicsContext.new() + + local fontPath = Resolve("font.otf") + ctx:CreateFont("font", fontPath) + + UpdateTimeAndDate() +end + +function UpdateTimeAndDate() + local now = os.date("*t") + timeDisplayText = string.format("%02d:%02d", now.hour, now.min) + dateDisplayText = os.date("%A, %B %d, %Y") end function _update(dt) + timer = timer + dt + if timer >= 0.5 then + timer = 0.0 + UpdateTimeAndDate() + end end function _render() @@ -32,4 +56,23 @@ function _render() effect:Use() effect:SetTexture("uTexture", video, 0) effect:Render() + + ctx:BeginFrame(DesktopSize.x, DesktopSize.y, DesktopSize.x / DesktopSize.y) + + local xPos = 80.0 + local yPos = 120.0 + local timeFontSize = 80 + local dateFontSize = 28 + + ctx:FontSize(timeFontSize) + ctx:FontFace("font") + ctx:FillColor(RGBAf(1, 1, 1, 0.75)) + ctx:TextAlign(Align.RIGHT | Align.BOTTOM) + ctx:Text(Displays[1].x + Displays[1].z - xPos, Displays[1].y + Displays[1].w - yPos, timeDisplayText) + + ctx:FontSize(dateFontSize) + ctx:FillColor(RGBAf(1, 1, 1, 0.45)) + ctx:Text(Displays[1].x + Displays[1].z - xPos, Displays[1].y + Displays[1].w - (yPos + timeFontSize + 8), dateDisplayText) + + ctx:EndFrame() end \ No newline at end of file diff --git a/src/lua.cpp b/src/lua.cpp index c600671..649a4ea 100644 --- a/src/lua.cpp +++ b/src/lua.cpp @@ -8,6 +8,7 @@ #include "framebuffer.h" #include "effect.h" #include "opengl.h" +#include "nanovg_cpp.h" #include "globals.h" static void RegisterTMath(sol::state& lua) { @@ -403,13 +404,234 @@ static void RegisterOpenGL(sol::state& lua) { gl["ONE_MINUS_DST_ALPHA"] = GL_ONE_MINUS_DST_ALPHA; } +#pragma region NanoVG bindings + +static void RegisterNanoVG(sol::state& lua) { + // ── NVGcolor ───────────────────────────────────────────────────────── + lua.new_usertype("Color", + sol::constructors<>(), + "r", &NVGcolor::r, + "g", &NVGcolor::g, + "b", &NVGcolor::b, + "a", &NVGcolor::a, + sol::meta_function::to_string, [](const NVGcolor& c) { + return "Color(" + std::to_string(c.r) + ", " + std::to_string(c.g) + ", " + std::to_string(c.b) + ", " + std::to_string(c.a) + ")"; + } + ); + + // ── NVGpaint ──────────────────────────────────────────────────────── + lua.new_usertype("Paint", + sol::constructors<>(), + "innerColor", &NVGpaint::innerColor, + "outerColor", &NVGpaint::outerColor, + "image", &NVGpaint::image, + "radius", &NVGpaint::radius, + "feather", &NVGpaint::feather + ); + + // ── NVG Enums ─────────────────────────────────────────────────────── + lua.new_enum("Winding", { + { "CCW", NVG_CCW }, + { "CW", NVG_CW } + }); + + lua.new_enum("Solidity", { + { "SOLID", NVG_SOLID }, + { "HOLE", NVG_HOLE } + }); + + lua.new_enum("LineCap", { + { "BUTT", NVG_BUTT }, + { "ROUND", NVG_ROUND }, + { "SQUARE", NVG_SQUARE }, + { "BEVEL", NVG_BEVEL }, + { "MITER", NVG_MITER } + }); + + lua.new_enum("Align", { + { "LEFT", NVG_ALIGN_LEFT }, + { "CENTER", NVG_ALIGN_CENTER }, + { "RIGHT", NVG_ALIGN_RIGHT }, + { "TOP", NVG_ALIGN_TOP }, + { "MIDDLE", NVG_ALIGN_MIDDLE }, + { "BOTTOM", NVG_ALIGN_BOTTOM }, + { "BASELINE", NVG_ALIGN_BASELINE } + }); + + lua.new_enum("CompositeOperation", { + { "SOURCE_OVER", NVG_SOURCE_OVER }, + { "SOURCE_IN", NVG_SOURCE_IN }, + { "SOURCE_OUT", NVG_SOURCE_OUT }, + { "ATOP", NVG_ATOP }, + { "DESTINATION_OVER", NVG_DESTINATION_OVER }, + { "DESTINATION_IN", NVG_DESTINATION_IN }, + { "DESTINATION_OUT", NVG_DESTINATION_OUT }, + { "DESTINATION_ATOP", NVG_DESTINATION_ATOP }, + { "LIGHTER", NVG_LIGHTER }, + { "COPY", NVG_COPY }, + { "XOR", NVG_XOR } + }); + + lua.create_named_table("ImageFlags", + "GENERATE_MIPMAPS", NVG_IMAGE_GENERATE_MIPMAPS, + "REPEATX", NVG_IMAGE_REPEATX, + "REPEATY", NVG_IMAGE_REPEATY, + "FLIPY", NVG_IMAGE_FLIPY, + "PREMULTIPLIED", NVG_IMAGE_PREMULTIPLIED, + "NEAREST", NVG_IMAGE_NEAREST + ); + + // ── GraphicsContext ───────────────────────────────────────────────── + lua.new_usertype("GraphicsContext", + sol::constructors(), + + // Frame management + "BeginFrame", &GraphicsContext::BeginFrame, + "CancelFrame", &GraphicsContext::CancelFrame, + "EndFrame", &GraphicsContext::EndFrame, + + // Composite operations + "GlobalCompositeOperation", &GraphicsContext::GlobalCompositeOperation, + "GlobalCompositeBlendFunc", &GraphicsContext::GlobalCompositeBlendFunc, + "GlobalCompositeBlendFuncSeparate", &GraphicsContext::GlobalCompositeBlendFuncSeparate, + + // State handling + "Save", &GraphicsContext::Save, + "Restore", &GraphicsContext::Restore, + "Reset", &GraphicsContext::Reset, + + // Render styles + "ShapeAntiAlias", &GraphicsContext::ShapeAntiAlias, + "StrokeColor", &GraphicsContext::StrokeColor, + "StrokePaint", &GraphicsContext::StrokePaint, + "FillColor", &GraphicsContext::FillColor, + "FillPaint", &GraphicsContext::FillPaint, + "MiterLimit", &GraphicsContext::MiterLimit, + "StrokeWidth", &GraphicsContext::StrokeWidth, + "LineCap", &GraphicsContext::LineCap, + "LineJoin", &GraphicsContext::LineJoin, + "GlobalAlpha", &GraphicsContext::GlobalAlpha, + + // Transforms + "ResetTransform", &GraphicsContext::ResetTransform, + "Transform", &GraphicsContext::Transform, + "Translate", &GraphicsContext::Translate, + "Rotate", &GraphicsContext::Rotate, + "SkewX", &GraphicsContext::SkewX, + "SkewY", &GraphicsContext::SkewY, + "Scale", &GraphicsContext::Scale, + "DegToRad", &GraphicsContext::DegToRad, + "RadToDeg", &GraphicsContext::RadToDeg, + + // Images + "CreateImage", &GraphicsContext::CreateImage, + "CreateImageRGBA", &GraphicsContext::CreateImageRGBA, + "ImageSize", [](GraphicsContext& g, int image) -> std::pair { + return g.ImageSize(image); + }, + "DeleteImage", &GraphicsContext::DeleteImage, + + // Paints (gradients & patterns) + "LinearGradient", &GraphicsContext::LinearGradient, + "BoxGradient", &GraphicsContext::BoxGradient, + "RadialGradient", &GraphicsContext::RadialGradient, + "ImagePattern", &GraphicsContext::ImagePattern, + + // Scissoring + "Scissor", &GraphicsContext::Scissor, + "IntersectScissor", &GraphicsContext::IntersectScissor, + "ResetScissor", &GraphicsContext::ResetScissor, + + // Paths + "BeginPath", &GraphicsContext::BeginPath, + "MoveTo", &GraphicsContext::MoveTo, + "LineTo", &GraphicsContext::LineTo, + "BezierTo", &GraphicsContext::BezierTo, + "QuadTo", &GraphicsContext::QuadTo, + "ArcTo", &GraphicsContext::ArcTo, + "ClosePath", &GraphicsContext::ClosePath, + "PathWinding", &GraphicsContext::PathWinding, + "Arc", &GraphicsContext::Arc, + "Rect", &GraphicsContext::Rect, + "RoundedRect", &GraphicsContext::RoundedRect, + "RoundedRectVarying", &GraphicsContext::RoundedRectVarying, + "Ellipse", &GraphicsContext::Ellipse, + "Circle", &GraphicsContext::Circle, + "Fill", &GraphicsContext::Fill, + "Stroke", &GraphicsContext::Stroke, + + // Fonts + "CreateFont", &GraphicsContext::CreateFont, + "CreateFontAtIndex", &GraphicsContext::CreateFontAtIndex, + "FindFont", &GraphicsContext::FindFont, + "AddFallbackFontId", &GraphicsContext::AddFallbackFontId, + "AddFallbackFont", &GraphicsContext::AddFallbackFont, + "ResetFallbackFontsId", &GraphicsContext::ResetFallbackFontsId, + "ResetFallbackFonts", &GraphicsContext::ResetFallbackFonts, + + // Text styling + "FontSize", &GraphicsContext::FontSize, + "FontBlur", &GraphicsContext::FontBlur, + "TextLetterSpacing", &GraphicsContext::TextLetterSpacing, + "TextLineHeight", &GraphicsContext::TextLineHeight, + "TextAlign", &GraphicsContext::TextAlign, + "FontFaceId", &GraphicsContext::FontFaceId, + "FontFace", &GraphicsContext::FontFace, + + // Text rendering & measurement + "Text", [](GraphicsContext& g, float x, float y, const std::string& str) -> float { + return g.Text(x, y, str.c_str()); + }, + "TextBox", [](GraphicsContext& g, float x, float y, float breakRowWidth, const std::string& str) { + g.TextBox(x, y, breakRowWidth, str.c_str()); + }, + "TextBounds", [](GraphicsContext& g, float x, float y, const std::string& str) -> std::tuple { + float bounds[4]; + float advance = g.TextBounds(x, y, str.c_str(), nullptr, bounds); + return { advance, bounds[0], bounds[1], bounds[2], bounds[3] }; + }, + "TextBoxBounds", [](GraphicsContext& g, float x, float y, float breakRowWidth, const std::string& str) -> std::tuple { + float bounds[4]; + g.TextBoxBounds(x, y, breakRowWidth, str.c_str(), nullptr, bounds); + return { bounds[0], bounds[1], bounds[2], bounds[3] }; + }, + "TextMetrics", [](GraphicsContext& g) -> std::tuple { + return g.TextMetrics(); + } + ); + + // Loose functions + lua.set_function("RGB", &GraphicsContext::RGB); + lua.set_function("RGBf", &GraphicsContext::RGBf); + lua.set_function("RGBA", &GraphicsContext::RGBA); + lua.set_function("RGBAf", &GraphicsContext::RGBAf); + lua.set_function("LerpRGBA", &GraphicsContext::LerpRGBA); + lua.set_function("TransRGBA", &GraphicsContext::TransRGBA); + lua.set_function("TransRGBAf", &GraphicsContext::TransRGBAf); + lua.set_function("HSL", &GraphicsContext::HSL); + lua.set_function("HSLA", &GraphicsContext::HSLA); + + // Matrices/utils + lua.set_function("DegToRad", &GraphicsContext::DegToRad); + lua.set_function("RadToDeg", &GraphicsContext::RadToDeg); + + lua.set_function("Rotate", &GraphicsContext::Rotate); + lua.set_function("Translate", &GraphicsContext::Translate); + lua.set_function("Scale", &GraphicsContext::Scale); +} + +#pragma endregion + void RegisterLuaBindings(sol::state& lua) { lua.open_libraries( sol::lib::base, sol::lib::math, sol::lib::string, sol::lib::table, - sol::lib::package + sol::lib::package, + sol::lib::os, + sol::lib::io, + sol::lib::coroutine ); RegisterTMath(lua); @@ -417,6 +639,7 @@ void RegisterLuaBindings(sol::state& lua) { RegisterFrameBuffer(lua); RegisterEffect(lua); RegisterOpenGL(lua); + RegisterNanoVG(lua); // Globals lua["DesktopSize"] = g_DesktopSize; diff --git a/src/nanovg_cpp.cpp b/src/nanovg_cpp.cpp new file mode 100644 index 0000000..b7de398 --- /dev/null +++ b/src/nanovg_cpp.cpp @@ -0,0 +1,171 @@ +#include "nanovg_cpp.h" + +GraphicsContext::GraphicsContext() +{ + ctx = nvgCreateGL46(NVG_ANTIALIAS | NVG_STENCIL_STROKES); +} + +GraphicsContext::~GraphicsContext() +{ + if (ctx) { + nvgDeleteGL46(ctx); + ctx = nullptr; + } +} + +// Frame management + +void GraphicsContext::BeginFrame(float width, float height, float devicePixelRatio) +{ + nvgBeginFrame(ctx, width, height, devicePixelRatio); +} + +void GraphicsContext::CancelFrame() { nvgCancelFrame(ctx); } + +void GraphicsContext::EndFrame() +{ + nvgEndFrame(ctx); + glBindVertexArray(0); + glDisable(GL_STENCIL_TEST); + glDisable(GL_CULL_FACE); + glUseProgram(0); +} + +// Composite operations + +void GraphicsContext::GlobalCompositeOperation(int op) { nvgGlobalCompositeOperation(ctx, op); } +void GraphicsContext::GlobalCompositeBlendFunc(int sfactor, int dfactor) { nvgGlobalCompositeBlendFunc(ctx, sfactor, dfactor); } +void GraphicsContext::GlobalCompositeBlendFuncSeparate(int srcRGB, int dstRGB, int srcAlpha, int dstAlpha) { nvgGlobalCompositeBlendFuncSeparate(ctx, srcRGB, dstRGB, srcAlpha, dstAlpha); } + +// Color utilities + +NVGcolor GraphicsContext::RGB(unsigned char r, unsigned char g, unsigned char b) { return nvgRGB(r, g, b); } +NVGcolor GraphicsContext::RGBf(float r, float g, float b) { return nvgRGBf(r, g, b); } +NVGcolor GraphicsContext::RGBA(unsigned char r, unsigned char g, unsigned char b, unsigned char a) { return nvgRGBA(r, g, b, a); } +NVGcolor GraphicsContext::RGBAf(float r, float g, float b, float a) { return nvgRGBAf(r, g, b, a); } +NVGcolor GraphicsContext::LerpRGBA(NVGcolor c0, NVGcolor c1, float u) { return nvgLerpRGBA(c0, c1, u); } +NVGcolor GraphicsContext::TransRGBA(NVGcolor c0, unsigned char a) { return nvgTransRGBA(c0, a); } +NVGcolor GraphicsContext::TransRGBAf(NVGcolor c0, float a) { return nvgTransRGBAf(c0, a); } +NVGcolor GraphicsContext::HSL(float h, float s, float l) { return nvgHSL(h, s, l); } +NVGcolor GraphicsContext::HSLA(float h, float s, float l, unsigned char a) { return nvgHSLA(h, s, l, a); } + +// State handling + +void GraphicsContext::Save() { nvgSave(ctx); } +void GraphicsContext::Restore() { nvgRestore(ctx); } +void GraphicsContext::Reset() { nvgReset(ctx); } + +// Render styles + +void GraphicsContext::ShapeAntiAlias(int enabled) { nvgShapeAntiAlias(ctx, enabled); } +void GraphicsContext::StrokeColor(NVGcolor color) { nvgStrokeColor(ctx, color); } +void GraphicsContext::StrokePaint(NVGpaint paint) { nvgStrokePaint(ctx, paint); } +void GraphicsContext::FillColor(NVGcolor color) { nvgFillColor(ctx, color); } +void GraphicsContext::FillPaint(NVGpaint paint) { nvgFillPaint(ctx, paint); } +void GraphicsContext::MiterLimit(float limit) { nvgMiterLimit(ctx, limit); } +void GraphicsContext::StrokeWidth(float size) { nvgStrokeWidth(ctx, size); } +void GraphicsContext::LineCap(int cap) { nvgLineCap(ctx, cap); } +void GraphicsContext::LineJoin(int join) { nvgLineJoin(ctx, join); } +void GraphicsContext::GlobalAlpha(float alpha) { nvgGlobalAlpha(ctx, alpha); } + +// Transforms + +void GraphicsContext::ResetTransform() { nvgResetTransform(ctx); } +void GraphicsContext::Transform(float a, float b, float c, float d, float e, float f) { nvgTransform(ctx, a, b, c, d, e, f); } +void GraphicsContext::Translate(float x, float y) { nvgTranslate(ctx, x, y); } +void GraphicsContext::Rotate(float angle) { nvgRotate(ctx, angle); } +void GraphicsContext::SkewX(float angle) { nvgSkewX(ctx, angle); } +void GraphicsContext::SkewY(float angle) { nvgSkewY(ctx, angle); } +void GraphicsContext::Scale(float x, float y) { nvgScale(ctx, x, y); } +void GraphicsContext::CurrentTransform(float* xform) { nvgCurrentTransform(ctx, xform); } + +// Transform matrix utilities + +void GraphicsContext::TransformIdentity(float* dst) { nvgTransformIdentity(dst); } +void GraphicsContext::TransformTranslate(float* dst, float tx, float ty) { nvgTransformTranslate(dst, tx, ty); } +void GraphicsContext::TransformScale(float* dst, float sx, float sy) { nvgTransformScale(dst, sx, sy); } +void GraphicsContext::TransformRotate(float* dst, float a) { nvgTransformRotate(dst, a); } +void GraphicsContext::TransformSkewX(float* dst, float a) { nvgTransformSkewX(dst, a); } +void GraphicsContext::TransformSkewY(float* dst, float a) { nvgTransformSkewY(dst, a); } +void GraphicsContext::TransformMultiply(float* dst, const float* src) { nvgTransformMultiply(dst, src); } +void GraphicsContext::TransformPremultiply(float* dst, const float* src) { nvgTransformPremultiply(dst, src); } +int GraphicsContext::TransformInverse(float* dst, const float* src) { return nvgTransformInverse(dst, src); } +void GraphicsContext::TransformPoint(float* dstx, float* dsty, const float* xform, float srcx, float srcy) { nvgTransformPoint(dstx, dsty, xform, srcx, srcy); } + +float GraphicsContext::DegToRad(float deg) { return nvgDegToRad(deg); } +float GraphicsContext::RadToDeg(float rad) { return nvgRadToDeg(rad); } + +// Images + +int GraphicsContext::CreateImage(const char* filename, int imageFlags) { return nvgCreateImage(ctx, filename, imageFlags); } +int GraphicsContext::CreateImageMem(int imageFlags, unsigned char* data, int ndata) { return nvgCreateImageMem(ctx, imageFlags, data, ndata); } +int GraphicsContext::CreateImageRGBA(int w, int h, int imageFlags, const unsigned char* data) { return nvgCreateImageRGBA(ctx, w, h, imageFlags, data); } +void GraphicsContext::UpdateImage(int image, const unsigned char* data) { nvgUpdateImage(ctx, image, data); } +void GraphicsContext::ImageSize(int image, int* w, int* h) { nvgImageSize(ctx, image, w, h); } +std::pair GraphicsContext::ImageSize(int image) { int w, h; nvgImageSize(ctx, image, &w, &h); return {w, h}; } +void GraphicsContext::DeleteImage(int image) { nvgDeleteImage(ctx, image); } + +// Paints (gradients & patterns) + +NVGpaint GraphicsContext::LinearGradient(float sx, float sy, float ex, float ey, NVGcolor icol, NVGcolor ocol) { return nvgLinearGradient(ctx, sx, sy, ex, ey, icol, ocol); } +NVGpaint GraphicsContext::BoxGradient(float x, float y, float w, float h, float r, float f, NVGcolor icol, NVGcolor ocol) { return nvgBoxGradient(ctx, x, y, w, h, r, f, icol, ocol); } +NVGpaint GraphicsContext::RadialGradient(float cx, float cy, float inr, float outr, NVGcolor icol, NVGcolor ocol) { return nvgRadialGradient(ctx, cx, cy, inr, outr, icol, ocol); } +NVGpaint GraphicsContext::ImagePattern(float ox, float oy, float ex, float ey, float angle, int image, float alpha) { return nvgImagePattern(ctx, ox, oy, ex, ey, angle, image, alpha); } + +// Scissoring + +void GraphicsContext::Scissor(float x, float y, float w, float h) { nvgScissor(ctx, x, y, w, h); } +void GraphicsContext::IntersectScissor(float x, float y, float w, float h) { nvgIntersectScissor(ctx, x, y, w, h); } +void GraphicsContext::ResetScissor() { nvgResetScissor(ctx); } + +// Paths + +void GraphicsContext::BeginPath() { nvgBeginPath(ctx); } +void GraphicsContext::MoveTo(float x, float y) { nvgMoveTo(ctx, x, y); } +void GraphicsContext::LineTo(float x, float y) { nvgLineTo(ctx, x, y); } +void GraphicsContext::BezierTo(float c1x, float c1y, float c2x, float c2y, float x, float y) { nvgBezierTo(ctx, c1x, c1y, c2x, c2y, x, y); } +void GraphicsContext::QuadTo(float cx, float cy, float x, float y) { nvgQuadTo(ctx, cx, cy, x, y); } +void GraphicsContext::ArcTo(float x1, float y1, float x2, float y2, float radius) { nvgArcTo(ctx, x1, y1, x2, y2, radius); } +void GraphicsContext::ClosePath() { nvgClosePath(ctx); } +void GraphicsContext::PathWinding(int dir) { nvgPathWinding(ctx, dir); } +void GraphicsContext::Arc(float cx, float cy, float r, float a0, float a1, int dir) { nvgArc(ctx, cx, cy, r, a0, a1, dir); } +void GraphicsContext::Rect(float x, float y, float w, float h) { nvgRect(ctx, x, y, w, h); } +void GraphicsContext::RoundedRect(float x, float y, float w, float h, float r) { nvgRoundedRect(ctx, x, y, w, h, r); } +void GraphicsContext::RoundedRectVarying(float x, float y, float w, float h, float radTopLeft, float radTopRight, float radBottomRight, float radBottomLeft) { nvgRoundedRectVarying(ctx, x, y, w, h, radTopLeft, radTopRight, radBottomRight, radBottomLeft); } +void GraphicsContext::Ellipse(float cx, float cy, float rx, float ry) { nvgEllipse(ctx, cx, cy, rx, ry); } +void GraphicsContext::Circle(float cx, float cy, float r) { nvgCircle(ctx, cx, cy, r); } +void GraphicsContext::Fill() { nvgFill(ctx); } +void GraphicsContext::Stroke() { nvgStroke(ctx); } + +// Fonts + +int GraphicsContext::CreateFont(const char* name, const char* filename) { return nvgCreateFont(ctx, name, filename); } +int GraphicsContext::CreateFontAtIndex(const char* name, const char* filename, int fontIndex) { return nvgCreateFontAtIndex(ctx, name, filename, fontIndex); } +int GraphicsContext::CreateFontMem(const char* name, unsigned char* data, int ndata, int freeData) { return nvgCreateFontMem(ctx, name, data, ndata, freeData); } +int GraphicsContext::CreateFontMemAtIndex(const char* name, unsigned char* data, int ndata, int freeData, int fontIndex) { return nvgCreateFontMemAtIndex(ctx, name, data, ndata, freeData, fontIndex); } +int GraphicsContext::FindFont(const char* name) { return nvgFindFont(ctx, name); } +int GraphicsContext::AddFallbackFontId(int baseFont, int fallbackFont) { return nvgAddFallbackFontId(ctx, baseFont, fallbackFont); } +int GraphicsContext::AddFallbackFont(const char* baseFont, const char* fallbackFont) { return nvgAddFallbackFont(ctx, baseFont, fallbackFont); } +void GraphicsContext::ResetFallbackFontsId(int baseFont) { nvgResetFallbackFontsId(ctx, baseFont); } +void GraphicsContext::ResetFallbackFonts(const char* baseFont) { nvgResetFallbackFonts(ctx, baseFont); } + +// Text styling + +void GraphicsContext::FontSize(float size) { nvgFontSize(ctx, size); } +void GraphicsContext::FontBlur(float blur) { nvgFontBlur(ctx, blur); } +void GraphicsContext::TextLetterSpacing(float spacing) { nvgTextLetterSpacing(ctx, spacing); } +void GraphicsContext::TextLineHeight(float lineHeight) { nvgTextLineHeight(ctx, lineHeight); } +void GraphicsContext::TextAlign(int align) { nvgTextAlign(ctx, align); } +void GraphicsContext::FontFaceId(int font) { nvgFontFaceId(ctx, font); } +void GraphicsContext::FontFace(const char* font) { nvgFontFace(ctx, font); } + +// Text rendering & measurement + +float GraphicsContext::Text(float x, float y, const char* string, const char* end) { return nvgText(ctx, x, y, string, end); } +void GraphicsContext::TextBox(float x, float y, float breakRowWidth, const char* string, const char* end) { nvgTextBox(ctx, x, y, breakRowWidth, string, end); } +float GraphicsContext::TextBounds(float x, float y, const char* string, const char* end, float* bounds) { return nvgTextBounds(ctx, x, y, string, end, bounds); } +void GraphicsContext::TextBoxBounds(float x, float y, float breakRowWidth, const char* string, const char* end, float* bounds) { nvgTextBoxBounds(ctx, x, y, breakRowWidth, string, end, bounds); } +int GraphicsContext::TextGlyphPositions(float x, float y, const char* string, const char* end, NVGglyphPosition* positions, int maxPositions) { return nvgTextGlyphPositions(ctx, x, y, string, end, positions, maxPositions); } +void GraphicsContext::TextMetrics(float* ascender, float* descender, float* lineh) { nvgTextMetrics(ctx, ascender, descender, lineh); } +std::tuple GraphicsContext::TextMetrics() { float a, d, l; nvgTextMetrics(ctx, &a, &d, &l); return {a, d, l}; } +int GraphicsContext::TextBreakLines(const char* string, const char* end, float breakRowWidth, NVGtextRow* rows, int maxRows) { return nvgTextBreakLines(ctx, string, end, breakRowWidth, rows, maxRows); } diff --git a/src/nanovg_cpp.h b/src/nanovg_cpp.h new file mode 100644 index 0000000..67af1ad --- /dev/null +++ b/src/nanovg_cpp.h @@ -0,0 +1,149 @@ +#pragma once + +#include "nanovg.h" +#include "nanovg_gl46.h" + +#include +#include + +class GraphicsContext { +public: + GraphicsContext(); + ~GraphicsContext(); + + NVGcontext* GetNVG() const { return ctx; } + + // Frame management + void BeginFrame(float width, float height, float devicePixelRatio); + void CancelFrame(); + void EndFrame(); + + // Composite operations + void GlobalCompositeOperation(int op); + void GlobalCompositeBlendFunc(int sfactor, int dfactor); + void GlobalCompositeBlendFuncSeparate(int srcRGB, int dstRGB, int srcAlpha, int dstAlpha); + + // Color utilities (static) + static NVGcolor RGB(unsigned char r, unsigned char g, unsigned char b); + static NVGcolor RGBf(float r, float g, float b); + static NVGcolor RGBA(unsigned char r, unsigned char g, unsigned char b, unsigned char a); + static NVGcolor RGBAf(float r, float g, float b, float a); + static NVGcolor LerpRGBA(NVGcolor c0, NVGcolor c1, float u); + static NVGcolor TransRGBA(NVGcolor c0, unsigned char a); + static NVGcolor TransRGBAf(NVGcolor c0, float a); + static NVGcolor HSL(float h, float s, float l); + static NVGcolor HSLA(float h, float s, float l, unsigned char a); + + // State handling + void Save(); + void Restore(); + void Reset(); + + // Render styles + void ShapeAntiAlias(int enabled); + void StrokeColor(NVGcolor color); + void StrokePaint(NVGpaint paint); + void FillColor(NVGcolor color); + void FillPaint(NVGpaint paint); + void MiterLimit(float limit); + void StrokeWidth(float size); + void LineCap(int cap); + void LineJoin(int join); + void GlobalAlpha(float alpha); + + // Transforms + void ResetTransform(); + void Transform(float a, float b, float c, float d, float e, float f); + void Translate(float x, float y); + void Rotate(float angle); + void SkewX(float angle); + void SkewY(float angle); + void Scale(float x, float y); + void CurrentTransform(float* xform); + + // Transform matrix utilities (static) + static void TransformIdentity(float* dst); + static void TransformTranslate(float* dst, float tx, float ty); + static void TransformScale(float* dst, float sx, float sy); + static void TransformRotate(float* dst, float a); + static void TransformSkewX(float* dst, float a); + static void TransformSkewY(float* dst, float a); + static void TransformMultiply(float* dst, const float* src); + static void TransformPremultiply(float* dst, const float* src); + static int TransformInverse(float* dst, const float* src); + static void TransformPoint(float* dstx, float* dsty, const float* xform, float srcx, float srcy); + + static float DegToRad(float deg); + static float RadToDeg(float rad); + + // Images + int CreateImage(const char* filename, int imageFlags); + int CreateImageMem(int imageFlags, unsigned char* data, int ndata); + int CreateImageRGBA(int w, int h, int imageFlags, const unsigned char* data); + void UpdateImage(int image, const unsigned char* data); + void ImageSize(int image, int* w, int* h); + std::pair ImageSize(int image); + void DeleteImage(int image); + + // Paints (gradients & patterns) + NVGpaint LinearGradient(float sx, float sy, float ex, float ey, NVGcolor icol, NVGcolor ocol); + NVGpaint BoxGradient(float x, float y, float w, float h, float r, float f, NVGcolor icol, NVGcolor ocol); + NVGpaint RadialGradient(float cx, float cy, float inr, float outr, NVGcolor icol, NVGcolor ocol); + NVGpaint ImagePattern(float ox, float oy, float ex, float ey, float angle, int image, float alpha); + + // Scissoring + void Scissor(float x, float y, float w, float h); + void IntersectScissor(float x, float y, float w, float h); + void ResetScissor(); + + // Paths + void BeginPath(); + void MoveTo(float x, float y); + void LineTo(float x, float y); + void BezierTo(float c1x, float c1y, float c2x, float c2y, float x, float y); + void QuadTo(float cx, float cy, float x, float y); + void ArcTo(float x1, float y1, float x2, float y2, float radius); + void ClosePath(); + void PathWinding(int dir); + void Arc(float cx, float cy, float r, float a0, float a1, int dir); + void Rect(float x, float y, float w, float h); + void RoundedRect(float x, float y, float w, float h, float r); + void RoundedRectVarying(float x, float y, float w, float h, float radTopLeft, float radTopRight, float radBottomRight, float radBottomLeft); + void Ellipse(float cx, float cy, float rx, float ry); + void Circle(float cx, float cy, float r); + void Fill(); + void Stroke(); + + // Fonts + int CreateFont(const char* name, const char* filename); + int CreateFontAtIndex(const char* name, const char* filename, int fontIndex); + int CreateFontMem(const char* name, unsigned char* data, int ndata, int freeData); + int CreateFontMemAtIndex(const char* name, unsigned char* data, int ndata, int freeData, int fontIndex); + int FindFont(const char* name); + int AddFallbackFontId(int baseFont, int fallbackFont); + int AddFallbackFont(const char* baseFont, const char* fallbackFont); + void ResetFallbackFontsId(int baseFont); + void ResetFallbackFonts(const char* baseFont); + + // Text styling + void FontSize(float size); + void FontBlur(float blur); + void TextLetterSpacing(float spacing); + void TextLineHeight(float lineHeight); + void TextAlign(int align); + void FontFaceId(int font); + void FontFace(const char* font); + + // Text rendering & measurement + float Text(float x, float y, const char* string, const char* end = nullptr); + void TextBox(float x, float y, float breakRowWidth, const char* string, const char* end = nullptr); + float TextBounds(float x, float y, const char* string, const char* end, float* bounds); + void TextBoxBounds(float x, float y, float breakRowWidth, const char* string, const char* end, float* bounds); + int TextGlyphPositions(float x, float y, const char* string, const char* end, NVGglyphPosition* positions, int maxPositions); + void TextMetrics(float* ascender, float* descender, float* lineh); + std::tuple TextMetrics(); + int TextBreakLines(const char* string, const char* end, float breakRowWidth, NVGtextRow* rows, int maxRows); + +private: + NVGcontext* ctx = nullptr; +}; diff --git a/src/nanovg_gl46.cpp b/src/nanovg_gl46.cpp new file mode 100644 index 0000000..5d03df6 --- /dev/null +++ b/src/nanovg_gl46.cpp @@ -0,0 +1,1208 @@ +// +// NanoVG OpenGL 4.6 Core Profile backend — implementation +// +// Based on nanovg_gl.h by Mikko Mononen (Copyright (c) 2009-2013) +// Adapted to use OpenGL 4.6 Direct State Access (DSA) functions, +// immutable texture storage, explicit layout qualifiers in GLSL 460, +// and named buffer/vertex-array operations. +// +#include "nanovg_gl46.h" +#include "nanovg.h" + +#include +#include +#include +#include + +// ── Constants ─────────────────────────────────────────────────────────────── + +enum GLNVGshaderType { + NSVG_SHADER_FILLGRAD, + NSVG_SHADER_FILLIMG, + NSVG_SHADER_SIMPLE, + NSVG_SHADER_IMG +}; + +enum GLNVGuniformBindings { + GLNVG_FRAG_BINDING = 0, +}; + +enum GLNVGcallType { + GLNVG_NONE = 0, + GLNVG_FILL, + GLNVG_CONVEXFILL, + GLNVG_STROKE, + GLNVG_TRIANGLES, +}; + +// viewSize uniform is at explicit location 0 in the shader +static constexpr GLint LOC_VIEWSIZE = 0; + +// ── Internal types ────────────────────────────────────────────────────────── + +struct GLNVGshader { + GLuint prog; + GLuint frag; + GLuint vert; +}; + +struct GLNVGtexture { + int id; + GLuint tex; + int width, height; + int type; + int flags; +}; + +struct GLNVGblend { + GLenum srcRGB; + GLenum dstRGB; + GLenum srcAlpha; + GLenum dstAlpha; +}; + +struct GLNVGcall { + int type; + int image; + int pathOffset; + int pathCount; + int triangleOffset; + int triangleCount; + int uniformOffset; + GLNVGblend blendFunc; +}; + +struct GLNVGpath { + int fillOffset; + int fillCount; + int strokeOffset; + int strokeCount; +}; + +struct GLNVGfragUniforms { + float scissorMat[12]; // mat3 stored as 3 vec4s (std140) + float paintMat[12]; + NVGcolor innerCol; + NVGcolor outerCol; + float scissorExt[2]; + float scissorScale[2]; + float extent[2]; + float radius; + float feather; + float strokeMult; + float strokeThr; + int texType; + int type; +}; + +struct GLNVGcontext { + GLNVGshader shader; + GLNVGtexture* textures; + float view[2]; + int ntextures; + int ctextures; + int textureId; + GLuint vertBuf; + GLuint vertArr; + GLuint fragBuf; + int fragSize; + int flags; + + // Per-frame buffers + GLNVGcall* calls; + int ccalls; + int ncalls; + GLNVGpath* paths; + int cpaths; + int npaths; + NVGvertex* verts; + int cverts; + int nverts; + unsigned char* uniforms; + int cuniforms; + int nuniforms; + + // Cached state + GLuint boundTexture; + GLuint stencilMask; + GLenum stencilFunc; + GLint stencilFuncRef; + GLuint stencilFuncMask; + GLNVGblend blendFunc; + + int dummyTex; +}; + +// ── Helpers ───────────────────────────────────────────────────────────────── + +static int glnvg__maxi(int a, int b) { return a > b ? a : b; } + +static void glnvg__bindTexture(GLNVGcontext* gl, GLuint tex) +{ + if (gl->boundTexture != tex) { + gl->boundTexture = tex; + glBindTextureUnit(0, tex); + } +} + +static void glnvg__stencilMask(GLNVGcontext* gl, GLuint mask) +{ + if (gl->stencilMask != mask) { + gl->stencilMask = mask; + glStencilMask(mask); + } +} + +static void glnvg__stencilFunc(GLNVGcontext* gl, GLenum func, GLint ref, GLuint mask) +{ + if ((gl->stencilFunc != func) || + (gl->stencilFuncRef != ref) || + (gl->stencilFuncMask != mask)) { + gl->stencilFunc = func; + gl->stencilFuncRef = ref; + gl->stencilFuncMask = mask; + glStencilFunc(func, ref, mask); + } +} + +static void glnvg__blendFuncSeparate(GLNVGcontext* gl, const GLNVGblend* blend) +{ + if ((gl->blendFunc.srcRGB != blend->srcRGB) || + (gl->blendFunc.dstRGB != blend->dstRGB) || + (gl->blendFunc.srcAlpha != blend->srcAlpha) || + (gl->blendFunc.dstAlpha != blend->dstAlpha)) { + gl->blendFunc = *blend; + glBlendFuncSeparate(blend->srcRGB, blend->dstRGB, blend->srcAlpha, blend->dstAlpha); + } +} + +// ── Texture management ────────────────────────────────────────────────────── + +static GLNVGtexture* glnvg__allocTexture(GLNVGcontext* gl) +{ + GLNVGtexture* tex = nullptr; + for (int i = 0; i < gl->ntextures; i++) { + if (gl->textures[i].id == 0) { + tex = &gl->textures[i]; + break; + } + } + if (tex == nullptr) { + if (gl->ntextures + 1 > gl->ctextures) { + int ctextures = glnvg__maxi(gl->ntextures + 1, 4) + gl->ctextures / 2; + GLNVGtexture* textures = (GLNVGtexture*)realloc(gl->textures, sizeof(GLNVGtexture) * ctextures); + if (textures == nullptr) return nullptr; + gl->textures = textures; + gl->ctextures = ctextures; + } + tex = &gl->textures[gl->ntextures++]; + } + memset(tex, 0, sizeof(*tex)); + tex->id = ++gl->textureId; + return tex; +} + +static GLNVGtexture* glnvg__findTexture(GLNVGcontext* gl, int id) +{ + for (int i = 0; i < gl->ntextures; i++) + if (gl->textures[i].id == id) + return &gl->textures[i]; + return nullptr; +} + +static int glnvg__deleteTexture(GLNVGcontext* gl, int id) +{ + for (int i = 0; i < gl->ntextures; i++) { + if (gl->textures[i].id == id) { + if (gl->textures[i].tex != 0 && (gl->textures[i].flags & NVG_IMAGE_NODELETE) == 0) + glDeleteTextures(1, &gl->textures[i].tex); + memset(&gl->textures[i], 0, sizeof(gl->textures[i])); + return 1; + } + } + return 0; +} + +// ── Shader ────────────────────────────────────────────────────────────────── + +static void glnvg__dumpShaderError(GLuint shader, const char* name, const char* type) +{ + GLchar str[512 + 1]; + GLsizei len = 0; + glGetShaderInfoLog(shader, 512, &len, str); + if (len > 512) len = 512; + str[len] = '\0'; + printf("Shader %s/%s error:\n%s\n", name, type, str); +} + +static void glnvg__dumpProgramError(GLuint prog, const char* name) +{ + GLchar str[512 + 1]; + GLsizei len = 0; + glGetProgramInfoLog(prog, 512, &len, str); + if (len > 512) len = 512; + str[len] = '\0'; + printf("Program %s error:\n%s\n", name, str); +} + +static void glnvg__checkError(GLNVGcontext* gl, const char* str) +{ + if ((gl->flags & NVG_DEBUG) == 0) return; + GLenum err = glGetError(); + if (err != GL_NO_ERROR) { + printf("Error %08x after %s\n", err, str); + } +} + +static int glnvg__createShader(GLNVGshader* shader, const char* name, + const char* header, const char* opts, + const char* vshader, const char* fshader) +{ + GLint status; + const char* str[3]; + str[0] = header; + str[1] = opts != nullptr ? opts : ""; + + memset(shader, 0, sizeof(*shader)); + + GLuint prog = glCreateProgram(); + GLuint vert = glCreateShader(GL_VERTEX_SHADER); + GLuint frag = glCreateShader(GL_FRAGMENT_SHADER); + + str[2] = vshader; + glShaderSource(vert, 3, str, 0); + str[2] = fshader; + glShaderSource(frag, 3, str, 0); + + glCompileShader(vert); + glGetShaderiv(vert, GL_COMPILE_STATUS, &status); + if (status != GL_TRUE) { + glnvg__dumpShaderError(vert, name, "vert"); + return 0; + } + + glCompileShader(frag); + glGetShaderiv(frag, GL_COMPILE_STATUS, &status); + if (status != GL_TRUE) { + glnvg__dumpShaderError(frag, name, "frag"); + return 0; + } + + glAttachShader(prog, vert); + glAttachShader(prog, frag); + + glLinkProgram(prog); + glGetProgramiv(prog, GL_LINK_STATUS, &status); + if (status != GL_TRUE) { + glnvg__dumpProgramError(prog, name); + return 0; + } + + shader->prog = prog; + shader->vert = vert; + shader->frag = frag; + return 1; +} + +static void glnvg__deleteShader(GLNVGshader* shader) +{ + if (shader->prog != 0) glDeleteProgram(shader->prog); + if (shader->vert != 0) glDeleteShader(shader->vert); + if (shader->frag != 0) glDeleteShader(shader->frag); +} + +// ── Render callbacks ──────────────────────────────────────────────────────── + +static int glnvg__renderCreateTexture(void* uptr, int type, int w, int h, int imageFlags, const unsigned char* data); + +static int glnvg__renderCreate(void* uptr) +{ + GLNVGcontext* gl = (GLNVGcontext*)uptr; + int align = 4; + + static const char* shaderHeader = + "#version 460 core\n" + "\n"; + + static const char* fillVertShader = + "layout(location = 0) in vec2 vertex;\n" + "layout(location = 1) in vec2 tcoord;\n" + "layout(location = 0) uniform vec2 viewSize;\n" + "out vec2 ftcoord;\n" + "out vec2 fpos;\n" + "void main(void) {\n" + " ftcoord = tcoord;\n" + " fpos = vertex;\n" + " gl_Position = vec4(2.0*vertex.x/viewSize.x - 1.0, 1.0 - 2.0*vertex.y/viewSize.y, 0, 1);\n" + "}\n"; + + static const char* fillFragShader = + "layout(std140, binding = 0) uniform frag {\n" + " mat3 scissorMat;\n" + " mat3 paintMat;\n" + " vec4 innerCol;\n" + " vec4 outerCol;\n" + " vec2 scissorExt;\n" + " vec2 scissorScale;\n" + " vec2 extent;\n" + " float radius;\n" + " float feather;\n" + " float strokeMult;\n" + " float strokeThr;\n" + " int texType;\n" + " int type;\n" + "};\n" + "layout(binding = 0) uniform sampler2D tex;\n" + "in vec2 ftcoord;\n" + "in vec2 fpos;\n" + "out vec4 outColor;\n" + "\n" + "float sdroundrect(vec2 pt, vec2 ext, float rad) {\n" + " vec2 ext2 = ext - vec2(rad,rad);\n" + " vec2 d = abs(pt) - ext2;\n" + " return min(max(d.x,d.y),0.0) + length(max(d,0.0)) - rad;\n" + "}\n" + "\n" + "float scissorMask(vec2 p) {\n" + " vec2 sc = (abs((scissorMat * vec3(p,1.0)).xy) - scissorExt);\n" + " sc = vec2(0.5,0.5) - sc * scissorScale;\n" + " return clamp(sc.x,0.0,1.0) * clamp(sc.y,0.0,1.0);\n" + "}\n" + "#ifdef EDGE_AA\n" + "float strokeMask() {\n" + " return min(1.0, (1.0-abs(ftcoord.x*2.0-1.0))*strokeMult) * min(1.0, ftcoord.y);\n" + "}\n" + "#endif\n" + "\n" + "void main(void) {\n" + " vec4 result;\n" + " float scissor = scissorMask(fpos);\n" + "#ifdef EDGE_AA\n" + " float strokeAlpha = strokeMask();\n" + " if (strokeAlpha < strokeThr) discard;\n" + "#else\n" + " float strokeAlpha = 1.0;\n" + "#endif\n" + " if (type == 0) {\n" + " vec2 pt = (paintMat * vec3(fpos,1.0)).xy;\n" + " float d = clamp((sdroundrect(pt, extent, radius) + feather*0.5) / feather, 0.0, 1.0);\n" + " vec4 color = mix(innerCol,outerCol,d);\n" + " color *= strokeAlpha * scissor;\n" + " result = color;\n" + " } else if (type == 1) {\n" + " vec2 pt = (paintMat * vec3(fpos,1.0)).xy / extent;\n" + " vec4 color = texture(tex, pt);\n" + " if (texType == 1) color = vec4(color.xyz*color.w,color.w);\n" + " if (texType == 2) color = vec4(color.x);\n" + " color *= innerCol;\n" + " color *= strokeAlpha * scissor;\n" + " result = color;\n" + " } else if (type == 2) {\n" + " result = vec4(1,1,1,1);\n" + " } else if (type == 3) {\n" + " vec4 color = texture(tex, ftcoord);\n" + " if (texType == 1) color = vec4(color.xyz*color.w,color.w);\n" + " if (texType == 2) color = vec4(color.x);\n" + " color *= scissor;\n" + " result = color * innerCol;\n" + " }\n" + " outColor = result;\n" + "}\n"; + + glnvg__checkError(gl, "init"); + + if (gl->flags & NVG_ANTIALIAS) { + if (glnvg__createShader(&gl->shader, "shader", shaderHeader, "#define EDGE_AA 1\n", fillVertShader, fillFragShader) == 0) + return 0; + } else { + if (glnvg__createShader(&gl->shader, "shader", shaderHeader, nullptr, fillVertShader, fillFragShader) == 0) + return 0; + } + + glnvg__checkError(gl, "uniform locations"); + + // Create VAO with DSA + glCreateVertexArrays(1, &gl->vertArr); + glCreateBuffers(1, &gl->vertBuf); + + // Set up vertex format (persistent — does not change per frame) + glEnableVertexArrayAttrib(gl->vertArr, 0); + glEnableVertexArrayAttrib(gl->vertArr, 1); + glVertexArrayAttribFormat(gl->vertArr, 0, 2, GL_FLOAT, GL_FALSE, 0); + glVertexArrayAttribFormat(gl->vertArr, 1, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float)); + glVertexArrayAttribBinding(gl->vertArr, 0, 0); + glVertexArrayAttribBinding(gl->vertArr, 1, 0); + + // Create UBO + glCreateBuffers(1, &gl->fragBuf); + glGetIntegerv(GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT, &align); + gl->fragSize = sizeof(GLNVGfragUniforms) + align - sizeof(GLNVGfragUniforms) % align; + + // Dummy texture so sampling never hits an unbound unit + gl->dummyTex = glnvg__renderCreateTexture(gl, NVG_TEXTURE_ALPHA, 1, 1, 0, nullptr); + + glnvg__checkError(gl, "create done"); + + glFinish(); + return 1; +} + +static int glnvg__renderCreateTexture(void* uptr, int type, int w, int h, int imageFlags, const unsigned char* data) +{ + GLNVGcontext* gl = (GLNVGcontext*)uptr; + GLNVGtexture* tex = glnvg__allocTexture(gl); + if (tex == nullptr) return 0; + + // Calculate mip levels + int levels = 1; + if (imageFlags & NVG_IMAGE_GENERATE_MIPMAPS) + levels = (int)floor(log2(fmax(w, h))) + 1; + + GLenum internalFormat = (type == NVG_TEXTURE_RGBA) ? GL_RGBA8 : GL_R8; + + // DSA texture creation with immutable storage + glCreateTextures(GL_TEXTURE_2D, 1, &tex->tex); + glTextureStorage2D(tex->tex, levels, internalFormat, w, h); + + tex->width = w; + tex->height = h; + tex->type = type; + tex->flags = imageFlags; + + // Upload initial data if provided + if (data != nullptr) { + GLenum format = (type == NVG_TEXTURE_RGBA) ? GL_RGBA : GL_RED; + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + glPixelStorei(GL_UNPACK_ROW_LENGTH, tex->width); + glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0); + glPixelStorei(GL_UNPACK_SKIP_ROWS, 0); + glTextureSubImage2D(tex->tex, 0, 0, 0, w, h, format, GL_UNSIGNED_BYTE, data); + glPixelStorei(GL_UNPACK_ALIGNMENT, 4); + glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); + } + + // Filter + if (imageFlags & NVG_IMAGE_GENERATE_MIPMAPS) { + if (imageFlags & NVG_IMAGE_NEAREST) + glTextureParameteri(tex->tex, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST); + else + glTextureParameteri(tex->tex, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); + } else { + if (imageFlags & NVG_IMAGE_NEAREST) + glTextureParameteri(tex->tex, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + else + glTextureParameteri(tex->tex, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + } + + if (imageFlags & NVG_IMAGE_NEAREST) + glTextureParameteri(tex->tex, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + else + glTextureParameteri(tex->tex, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + // Wrap + glTextureParameteri(tex->tex, GL_TEXTURE_WRAP_S, + (imageFlags & NVG_IMAGE_REPEATX) ? GL_REPEAT : GL_CLAMP_TO_EDGE); + glTextureParameteri(tex->tex, GL_TEXTURE_WRAP_T, + (imageFlags & NVG_IMAGE_REPEATY) ? GL_REPEAT : GL_CLAMP_TO_EDGE); + + // Generate mipmaps + if (imageFlags & NVG_IMAGE_GENERATE_MIPMAPS) + glGenerateTextureMipmap(tex->tex); + + glnvg__checkError(gl, "create tex"); + return tex->id; +} + +static int glnvg__renderDeleteTexture(void* uptr, int image) +{ + GLNVGcontext* gl = (GLNVGcontext*)uptr; + return glnvg__deleteTexture(gl, image); +} + +static int glnvg__renderUpdateTexture(void* uptr, int image, int x, int y, int w, int h, const unsigned char* data) +{ + GLNVGcontext* gl = (GLNVGcontext*)uptr; + GLNVGtexture* tex = glnvg__findTexture(gl, image); + if (tex == nullptr) return 0; + + GLenum format = (tex->type == NVG_TEXTURE_RGBA) ? GL_RGBA : GL_RED; + + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + glPixelStorei(GL_UNPACK_ROW_LENGTH, tex->width); + glPixelStorei(GL_UNPACK_SKIP_PIXELS, x); + glPixelStorei(GL_UNPACK_SKIP_ROWS, y); + + // DSA sub-image update — no need to bind the texture + glTextureSubImage2D(tex->tex, 0, x, y, w, h, format, GL_UNSIGNED_BYTE, data); + + glPixelStorei(GL_UNPACK_ALIGNMENT, 4); + glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); + glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0); + glPixelStorei(GL_UNPACK_SKIP_ROWS, 0); + + return 1; +} + +static int glnvg__renderGetTextureSize(void* uptr, int image, int* w, int* h) +{ + GLNVGcontext* gl = (GLNVGcontext*)uptr; + GLNVGtexture* tex = glnvg__findTexture(gl, image); + if (tex == nullptr) return 0; + *w = tex->width; + *h = tex->height; + return 1; +} + +// ── Paint / uniform conversion ────────────────────────────────────────────── + +static void glnvg__xformToMat3x4(float* m3, float* t) +{ + m3[ 0] = t[0]; m3[ 1] = t[1]; m3[ 2] = 0.0f; m3[ 3] = 0.0f; + m3[ 4] = t[2]; m3[ 5] = t[3]; m3[ 6] = 0.0f; m3[ 7] = 0.0f; + m3[ 8] = t[4]; m3[ 9] = t[5]; m3[10] = 1.0f; m3[11] = 0.0f; +} + +static NVGcolor glnvg__premulColor(NVGcolor c) +{ + c.r *= c.a; + c.g *= c.a; + c.b *= c.a; + return c; +} + +static int glnvg__convertPaint(GLNVGcontext* gl, GLNVGfragUniforms* frag, NVGpaint* paint, + NVGscissor* scissor, float width, float fringe, float strokeThr) +{ + GLNVGtexture* tex = nullptr; + float invxform[6]; + + memset(frag, 0, sizeof(*frag)); + + frag->innerCol = glnvg__premulColor(paint->innerColor); + frag->outerCol = glnvg__premulColor(paint->outerColor); + + if (scissor->extent[0] < -0.5f || scissor->extent[1] < -0.5f) { + memset(frag->scissorMat, 0, sizeof(frag->scissorMat)); + frag->scissorExt[0] = 1.0f; + frag->scissorExt[1] = 1.0f; + frag->scissorScale[0] = 1.0f; + frag->scissorScale[1] = 1.0f; + } else { + nvgTransformInverse(invxform, scissor->xform); + glnvg__xformToMat3x4(frag->scissorMat, invxform); + frag->scissorExt[0] = scissor->extent[0]; + frag->scissorExt[1] = scissor->extent[1]; + frag->scissorScale[0] = sqrtf(scissor->xform[0] * scissor->xform[0] + scissor->xform[2] * scissor->xform[2]) / fringe; + frag->scissorScale[1] = sqrtf(scissor->xform[1] * scissor->xform[1] + scissor->xform[3] * scissor->xform[3]) / fringe; + } + + memcpy(frag->extent, paint->extent, sizeof(frag->extent)); + frag->strokeMult = (width * 0.5f + fringe * 0.5f) / fringe; + frag->strokeThr = strokeThr; + + if (paint->image != 0) { + tex = glnvg__findTexture(gl, paint->image); + if (tex == nullptr) return 0; + if ((tex->flags & NVG_IMAGE_FLIPY) != 0) { + float m1[6], m2[6]; + nvgTransformTranslate(m1, 0.0f, frag->extent[1] * 0.5f); + nvgTransformMultiply(m1, paint->xform); + nvgTransformScale(m2, 1.0f, -1.0f); + nvgTransformMultiply(m2, m1); + nvgTransformTranslate(m1, 0.0f, -frag->extent[1] * 0.5f); + nvgTransformMultiply(m1, m2); + nvgTransformInverse(invxform, m1); + } else { + nvgTransformInverse(invxform, paint->xform); + } + frag->type = NSVG_SHADER_FILLIMG; + + if (tex->type == NVG_TEXTURE_RGBA) + frag->texType = (tex->flags & NVG_IMAGE_PREMULTIPLIED) ? 0 : 1; + else + frag->texType = 2; + } else { + frag->type = NSVG_SHADER_FILLGRAD; + frag->radius = paint->radius; + frag->feather = paint->feather; + nvgTransformInverse(invxform, paint->xform); + } + + glnvg__xformToMat3x4(frag->paintMat, invxform); + return 1; +} + +static GLNVGfragUniforms* nvg__fragUniformPtr(GLNVGcontext* gl, int i) +{ + return (GLNVGfragUniforms*)&gl->uniforms[i]; +} + +static void glnvg__setUniforms(GLNVGcontext* gl, int uniformOffset, int image) +{ + GLNVGtexture* tex = nullptr; + glBindBufferRange(GL_UNIFORM_BUFFER, GLNVG_FRAG_BINDING, gl->fragBuf, uniformOffset, sizeof(GLNVGfragUniforms)); + + if (image != 0) + tex = glnvg__findTexture(gl, image); + if (tex == nullptr) + tex = glnvg__findTexture(gl, gl->dummyTex); + + glnvg__bindTexture(gl, tex != nullptr ? tex->tex : 0); + glnvg__checkError(gl, "tex paint tex"); +} + +// ── Viewport ──────────────────────────────────────────────────────────────── + +static void glnvg__renderViewport(void* uptr, float width, float height, float devicePixelRatio) +{ + NVG_NOTUSED(devicePixelRatio); + GLNVGcontext* gl = (GLNVGcontext*)uptr; + gl->view[0] = width; + gl->view[1] = height; +} + +// ── Draw commands ─────────────────────────────────────────────────────────── + +static void glnvg__fill(GLNVGcontext* gl, GLNVGcall* call) +{ + GLNVGpath* paths = &gl->paths[call->pathOffset]; + int npaths = call->pathCount; + + glEnable(GL_STENCIL_TEST); + glnvg__stencilMask(gl, 0xff); + glnvg__stencilFunc(gl, GL_ALWAYS, 0, 0xff); + glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); + + glnvg__setUniforms(gl, call->uniformOffset, 0); + glnvg__checkError(gl, "fill simple"); + + glStencilOpSeparate(GL_FRONT, GL_KEEP, GL_KEEP, GL_INCR_WRAP); + glStencilOpSeparate(GL_BACK, GL_KEEP, GL_KEEP, GL_DECR_WRAP); + glDisable(GL_CULL_FACE); + for (int i = 0; i < npaths; i++) + glDrawArrays(GL_TRIANGLE_FAN, paths[i].fillOffset, paths[i].fillCount); + glEnable(GL_CULL_FACE); + + glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + + glnvg__setUniforms(gl, call->uniformOffset + gl->fragSize, call->image); + glnvg__checkError(gl, "fill fill"); + + if (gl->flags & NVG_ANTIALIAS) { + glnvg__stencilFunc(gl, GL_EQUAL, 0x00, 0xff); + glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP); + for (int i = 0; i < npaths; i++) + glDrawArrays(GL_TRIANGLE_STRIP, paths[i].strokeOffset, paths[i].strokeCount); + } + + glnvg__stencilFunc(gl, GL_NOTEQUAL, 0x0, 0xff); + glStencilOp(GL_ZERO, GL_ZERO, GL_ZERO); + glDrawArrays(GL_TRIANGLE_STRIP, call->triangleOffset, call->triangleCount); + + glDisable(GL_STENCIL_TEST); +} + +static void glnvg__convexFill(GLNVGcontext* gl, GLNVGcall* call) +{ + GLNVGpath* paths = &gl->paths[call->pathOffset]; + int npaths = call->pathCount; + + glnvg__setUniforms(gl, call->uniformOffset, call->image); + glnvg__checkError(gl, "convex fill"); + + for (int i = 0; i < npaths; i++) { + glDrawArrays(GL_TRIANGLE_FAN, paths[i].fillOffset, paths[i].fillCount); + if (paths[i].strokeCount > 0) + glDrawArrays(GL_TRIANGLE_STRIP, paths[i].strokeOffset, paths[i].strokeCount); + } +} + +static void glnvg__stroke(GLNVGcontext* gl, GLNVGcall* call) +{ + GLNVGpath* paths = &gl->paths[call->pathOffset]; + int npaths = call->pathCount; + + if (gl->flags & NVG_STENCIL_STROKES) { + glEnable(GL_STENCIL_TEST); + glnvg__stencilMask(gl, 0xff); + + glnvg__stencilFunc(gl, GL_EQUAL, 0x0, 0xff); + glStencilOp(GL_KEEP, GL_KEEP, GL_INCR); + glnvg__setUniforms(gl, call->uniformOffset + gl->fragSize, call->image); + glnvg__checkError(gl, "stroke fill 0"); + for (int i = 0; i < npaths; i++) + glDrawArrays(GL_TRIANGLE_STRIP, paths[i].strokeOffset, paths[i].strokeCount); + + glnvg__setUniforms(gl, call->uniformOffset, call->image); + glnvg__stencilFunc(gl, GL_EQUAL, 0x00, 0xff); + glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP); + for (int i = 0; i < npaths; i++) + glDrawArrays(GL_TRIANGLE_STRIP, paths[i].strokeOffset, paths[i].strokeCount); + + glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); + glnvg__stencilFunc(gl, GL_ALWAYS, 0x0, 0xff); + glStencilOp(GL_ZERO, GL_ZERO, GL_ZERO); + glnvg__checkError(gl, "stroke fill 1"); + for (int i = 0; i < npaths; i++) + glDrawArrays(GL_TRIANGLE_STRIP, paths[i].strokeOffset, paths[i].strokeCount); + glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + + glDisable(GL_STENCIL_TEST); + } else { + glnvg__setUniforms(gl, call->uniformOffset, call->image); + glnvg__checkError(gl, "stroke fill"); + for (int i = 0; i < npaths; i++) + glDrawArrays(GL_TRIANGLE_STRIP, paths[i].strokeOffset, paths[i].strokeCount); + } +} + +static void glnvg__triangles(GLNVGcontext* gl, GLNVGcall* call) +{ + glnvg__setUniforms(gl, call->uniformOffset, call->image); + glnvg__checkError(gl, "triangles fill"); + glDrawArrays(GL_TRIANGLES, call->triangleOffset, call->triangleCount); +} + +// ── Frame management ──────────────────────────────────────────────────────── + +static void glnvg__renderCancel(void* uptr) +{ + GLNVGcontext* gl = (GLNVGcontext*)uptr; + gl->nverts = 0; + gl->npaths = 0; + gl->ncalls = 0; + gl->nuniforms = 0; +} + +static GLenum glnvg_convertBlendFuncFactor(int factor) +{ + if (factor == NVG_ZERO) return GL_ZERO; + if (factor == NVG_ONE) return GL_ONE; + if (factor == NVG_SRC_COLOR) return GL_SRC_COLOR; + if (factor == NVG_ONE_MINUS_SRC_COLOR) return GL_ONE_MINUS_SRC_COLOR; + if (factor == NVG_DST_COLOR) return GL_DST_COLOR; + if (factor == NVG_ONE_MINUS_DST_COLOR) return GL_ONE_MINUS_DST_COLOR; + if (factor == NVG_SRC_ALPHA) return GL_SRC_ALPHA; + if (factor == NVG_ONE_MINUS_SRC_ALPHA) return GL_ONE_MINUS_SRC_ALPHA; + if (factor == NVG_DST_ALPHA) return GL_DST_ALPHA; + if (factor == NVG_ONE_MINUS_DST_ALPHA) return GL_ONE_MINUS_DST_ALPHA; + if (factor == NVG_SRC_ALPHA_SATURATE) return GL_SRC_ALPHA_SATURATE; + return GL_INVALID_ENUM; +} + +static GLNVGblend glnvg__blendCompositeOperation(NVGcompositeOperationState op) +{ + GLNVGblend blend; + blend.srcRGB = glnvg_convertBlendFuncFactor(op.srcRGB); + blend.dstRGB = glnvg_convertBlendFuncFactor(op.dstRGB); + blend.srcAlpha = glnvg_convertBlendFuncFactor(op.srcAlpha); + blend.dstAlpha = glnvg_convertBlendFuncFactor(op.dstAlpha); + if (blend.srcRGB == GL_INVALID_ENUM || blend.dstRGB == GL_INVALID_ENUM || + blend.srcAlpha == GL_INVALID_ENUM || blend.dstAlpha == GL_INVALID_ENUM) { + blend.srcRGB = GL_ONE; + blend.dstRGB = GL_ONE_MINUS_SRC_ALPHA; + blend.srcAlpha = GL_ONE; + blend.dstAlpha = GL_ONE_MINUS_SRC_ALPHA; + } + return blend; +} + +static void glnvg__renderFlush(void* uptr) +{ + GLNVGcontext* gl = (GLNVGcontext*)uptr; + + if (gl->ncalls > 0) { + glUseProgram(gl->shader.prog); + + glEnable(GL_CULL_FACE); + glCullFace(GL_BACK); + glFrontFace(GL_CCW); + glEnable(GL_BLEND); + glDisable(GL_DEPTH_TEST); + glDisable(GL_SCISSOR_TEST); + glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + glStencilMask(0xffffffff); + glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP); + glStencilFunc(GL_ALWAYS, 0, 0xffffffff); + glBindTextureUnit(0, 0); + + // Reset cached state + gl->boundTexture = 0; + gl->stencilMask = 0xffffffff; + gl->stencilFunc = GL_ALWAYS; + gl->stencilFuncRef = 0; + gl->stencilFuncMask = 0xffffffff; + gl->blendFunc.srcRGB = GL_INVALID_ENUM; + gl->blendFunc.srcAlpha = GL_INVALID_ENUM; + gl->blendFunc.dstRGB = GL_INVALID_ENUM; + gl->blendFunc.dstAlpha = GL_INVALID_ENUM; + + // Upload UBO data + glNamedBufferData(gl->fragBuf, gl->nuniforms * gl->fragSize, gl->uniforms, GL_STREAM_DRAW); + + // Upload vertex data + glNamedBufferData(gl->vertBuf, gl->nverts * sizeof(NVGvertex), gl->verts, GL_STREAM_DRAW); + + // Bind VAO and associate the VBO + glBindVertexArray(gl->vertArr); + glVertexArrayVertexBuffer(gl->vertArr, 0, gl->vertBuf, 0, sizeof(NVGvertex)); + + // Set viewSize uniform (layout location = 0) + glUniform2fv(LOC_VIEWSIZE, 1, gl->view); + + // Dispatch draw calls + for (int i = 0; i < gl->ncalls; i++) { + GLNVGcall* call = &gl->calls[i]; + glnvg__blendFuncSeparate(gl, &call->blendFunc); + if (call->type == GLNVG_FILL) + glnvg__fill(gl, call); + else if (call->type == GLNVG_CONVEXFILL) + glnvg__convexFill(gl, call); + else if (call->type == GLNVG_STROKE) + glnvg__stroke(gl, call); + else if (call->type == GLNVG_TRIANGLES) + glnvg__triangles(gl, call); + } + + glBindVertexArray(0); + glDisable(GL_CULL_FACE); + glBindBuffer(GL_ARRAY_BUFFER, 0); + glUseProgram(0); + glnvg__bindTexture(gl, 0); + } + + // Reset per-frame state + gl->nverts = 0; + gl->npaths = 0; + gl->ncalls = 0; + gl->nuniforms = 0; +} + +// ── Allocators ────────────────────────────────────────────────────────────── + +static int glnvg__maxVertCount(const NVGpath* paths, int npaths) +{ + int count = 0; + for (int i = 0; i < npaths; i++) { + count += paths[i].nfill; + count += paths[i].nstroke; + } + return count; +} + +static GLNVGcall* glnvg__allocCall(GLNVGcontext* gl) +{ + if (gl->ncalls + 1 > gl->ccalls) { + int ccalls = glnvg__maxi(gl->ncalls + 1, 128) + gl->ccalls / 2; + GLNVGcall* calls = (GLNVGcall*)realloc(gl->calls, sizeof(GLNVGcall) * ccalls); + if (calls == nullptr) return nullptr; + gl->calls = calls; + gl->ccalls = ccalls; + } + GLNVGcall* ret = &gl->calls[gl->ncalls++]; + memset(ret, 0, sizeof(GLNVGcall)); + return ret; +} + +static int glnvg__allocPaths(GLNVGcontext* gl, int n) +{ + if (gl->npaths + n > gl->cpaths) { + int cpaths = glnvg__maxi(gl->npaths + n, 128) + gl->cpaths / 2; + GLNVGpath* paths = (GLNVGpath*)realloc(gl->paths, sizeof(GLNVGpath) * cpaths); + if (paths == nullptr) return -1; + gl->paths = paths; + gl->cpaths = cpaths; + } + int ret = gl->npaths; + gl->npaths += n; + return ret; +} + +static int glnvg__allocVerts(GLNVGcontext* gl, int n) +{ + if (gl->nverts + n > gl->cverts) { + int cverts = glnvg__maxi(gl->nverts + n, 4096) + gl->cverts / 2; + NVGvertex* verts = (NVGvertex*)realloc(gl->verts, sizeof(NVGvertex) * cverts); + if (verts == nullptr) return -1; + gl->verts = verts; + gl->cverts = cverts; + } + int ret = gl->nverts; + gl->nverts += n; + return ret; +} + +static int glnvg__allocFragUniforms(GLNVGcontext* gl, int n) +{ + int structSize = gl->fragSize; + if (gl->nuniforms + n > gl->cuniforms) { + int cuniforms = glnvg__maxi(gl->nuniforms + n, 128) + gl->cuniforms / 2; + unsigned char* uniforms = (unsigned char*)realloc(gl->uniforms, structSize * cuniforms); + if (uniforms == nullptr) return -1; + gl->uniforms = uniforms; + gl->cuniforms = cuniforms; + } + int ret = gl->nuniforms * structSize; + gl->nuniforms += n; + return ret; +} + +static void glnvg__vset(NVGvertex* vtx, float x, float y, float u, float v) +{ + vtx->x = x; + vtx->y = y; + vtx->u = u; + vtx->v = v; +} + +// ── Render path recording ─────────────────────────────────────────────────── + +static void glnvg__renderFill(void* uptr, NVGpaint* paint, NVGcompositeOperationState compositeOperation, + NVGscissor* scissor, float fringe, const float* bounds, + const NVGpath* paths, int npaths) +{ + GLNVGcontext* gl = (GLNVGcontext*)uptr; + GLNVGcall* call = glnvg__allocCall(gl); + NVGvertex* quad; + GLNVGfragUniforms* frag; + int maxverts, offset; + + if (call == nullptr) return; + + call->type = GLNVG_FILL; + call->triangleCount = 4; + call->pathOffset = glnvg__allocPaths(gl, npaths); + if (call->pathOffset == -1) goto error; + call->pathCount = npaths; + call->image = paint->image; + call->blendFunc = glnvg__blendCompositeOperation(compositeOperation); + + if (npaths == 1 && paths[0].convex) { + call->type = GLNVG_CONVEXFILL; + call->triangleCount = 0; + } + + maxverts = glnvg__maxVertCount(paths, npaths) + call->triangleCount; + offset = glnvg__allocVerts(gl, maxverts); + if (offset == -1) goto error; + + for (int i = 0; i < npaths; i++) { + GLNVGpath* copy = &gl->paths[call->pathOffset + i]; + const NVGpath* path = &paths[i]; + memset(copy, 0, sizeof(GLNVGpath)); + if (path->nfill > 0) { + copy->fillOffset = offset; + copy->fillCount = path->nfill; + memcpy(&gl->verts[offset], path->fill, sizeof(NVGvertex) * path->nfill); + offset += path->nfill; + } + if (path->nstroke > 0) { + copy->strokeOffset = offset; + copy->strokeCount = path->nstroke; + memcpy(&gl->verts[offset], path->stroke, sizeof(NVGvertex) * path->nstroke); + offset += path->nstroke; + } + } + + if (call->type == GLNVG_FILL) { + call->triangleOffset = offset; + quad = &gl->verts[call->triangleOffset]; + glnvg__vset(&quad[0], bounds[2], bounds[3], 0.5f, 1.0f); + glnvg__vset(&quad[1], bounds[2], bounds[1], 0.5f, 1.0f); + glnvg__vset(&quad[2], bounds[0], bounds[3], 0.5f, 1.0f); + glnvg__vset(&quad[3], bounds[0], bounds[1], 0.5f, 1.0f); + + call->uniformOffset = glnvg__allocFragUniforms(gl, 2); + if (call->uniformOffset == -1) goto error; + frag = nvg__fragUniformPtr(gl, call->uniformOffset); + memset(frag, 0, sizeof(*frag)); + frag->strokeThr = -1.0f; + frag->type = NSVG_SHADER_SIMPLE; + glnvg__convertPaint(gl, nvg__fragUniformPtr(gl, call->uniformOffset + gl->fragSize), paint, scissor, fringe, fringe, -1.0f); + } else { + call->uniformOffset = glnvg__allocFragUniforms(gl, 1); + if (call->uniformOffset == -1) goto error; + glnvg__convertPaint(gl, nvg__fragUniformPtr(gl, call->uniformOffset), paint, scissor, fringe, fringe, -1.0f); + } + + return; + +error: + if (gl->ncalls > 0) gl->ncalls--; +} + +static void glnvg__renderStroke(void* uptr, NVGpaint* paint, NVGcompositeOperationState compositeOperation, + NVGscissor* scissor, float fringe, float strokeWidth, + const NVGpath* paths, int npaths) +{ + GLNVGcontext* gl = (GLNVGcontext*)uptr; + GLNVGcall* call = glnvg__allocCall(gl); + int maxverts, offset; + + if (call == nullptr) return; + + call->type = GLNVG_STROKE; + call->pathOffset = glnvg__allocPaths(gl, npaths); + if (call->pathOffset == -1) goto error; + call->pathCount = npaths; + call->image = paint->image; + call->blendFunc = glnvg__blendCompositeOperation(compositeOperation); + + maxverts = glnvg__maxVertCount(paths, npaths); + offset = glnvg__allocVerts(gl, maxverts); + if (offset == -1) goto error; + + for (int i = 0; i < npaths; i++) { + GLNVGpath* copy = &gl->paths[call->pathOffset + i]; + const NVGpath* path = &paths[i]; + memset(copy, 0, sizeof(GLNVGpath)); + if (path->nstroke) { + copy->strokeOffset = offset; + copy->strokeCount = path->nstroke; + memcpy(&gl->verts[offset], path->stroke, sizeof(NVGvertex) * path->nstroke); + offset += path->nstroke; + } + } + + if (gl->flags & NVG_STENCIL_STROKES) { + call->uniformOffset = glnvg__allocFragUniforms(gl, 2); + if (call->uniformOffset == -1) goto error; + glnvg__convertPaint(gl, nvg__fragUniformPtr(gl, call->uniformOffset), paint, scissor, strokeWidth, fringe, -1.0f); + glnvg__convertPaint(gl, nvg__fragUniformPtr(gl, call->uniformOffset + gl->fragSize), paint, scissor, strokeWidth, fringe, 1.0f - 0.5f / 255.0f); + } else { + call->uniformOffset = glnvg__allocFragUniforms(gl, 1); + if (call->uniformOffset == -1) goto error; + glnvg__convertPaint(gl, nvg__fragUniformPtr(gl, call->uniformOffset), paint, scissor, strokeWidth, fringe, -1.0f); + } + + return; + +error: + if (gl->ncalls > 0) gl->ncalls--; +} + +static void glnvg__renderTriangles(void* uptr, NVGpaint* paint, NVGcompositeOperationState compositeOperation, + NVGscissor* scissor, const NVGvertex* verts, int nverts, float fringe) +{ + GLNVGcontext* gl = (GLNVGcontext*)uptr; + GLNVGcall* call = glnvg__allocCall(gl); + GLNVGfragUniforms* frag; + + if (call == nullptr) return; + + call->type = GLNVG_TRIANGLES; + call->image = paint->image; + call->blendFunc = glnvg__blendCompositeOperation(compositeOperation); + + call->triangleOffset = glnvg__allocVerts(gl, nverts); + if (call->triangleOffset == -1) goto error; + call->triangleCount = nverts; + + memcpy(&gl->verts[call->triangleOffset], verts, sizeof(NVGvertex) * nverts); + + call->uniformOffset = glnvg__allocFragUniforms(gl, 1); + if (call->uniformOffset == -1) goto error; + frag = nvg__fragUniformPtr(gl, call->uniformOffset); + glnvg__convertPaint(gl, frag, paint, scissor, 1.0f, fringe, -1.0f); + frag->type = NSVG_SHADER_IMG; + + return; + +error: + if (gl->ncalls > 0) gl->ncalls--; +} + +// ── Cleanup ───────────────────────────────────────────────────────────────── + +static void glnvg__renderDelete(void* uptr) +{ + GLNVGcontext* gl = (GLNVGcontext*)uptr; + if (gl == nullptr) return; + + glnvg__deleteShader(&gl->shader); + + if (gl->fragBuf != 0) + glDeleteBuffers(1, &gl->fragBuf); + if (gl->vertArr != 0) + glDeleteVertexArrays(1, &gl->vertArr); + if (gl->vertBuf != 0) + glDeleteBuffers(1, &gl->vertBuf); + + for (int i = 0; i < gl->ntextures; i++) { + if (gl->textures[i].tex != 0 && (gl->textures[i].flags & NVG_IMAGE_NODELETE) == 0) + glDeleteTextures(1, &gl->textures[i].tex); + } + free(gl->textures); + + free(gl->paths); + free(gl->verts); + free(gl->uniforms); + free(gl->calls); + + free(gl); +} + +// ── Public API ────────────────────────────────────────────────────────────── + +NVGcontext* nvgCreateGL46(int flags) +{ + NVGparams params; + NVGcontext* ctx = nullptr; + GLNVGcontext* gl = (GLNVGcontext*)malloc(sizeof(GLNVGcontext)); + if (gl == nullptr) goto error; + memset(gl, 0, sizeof(GLNVGcontext)); + + memset(¶ms, 0, sizeof(params)); + params.renderCreate = glnvg__renderCreate; + params.renderCreateTexture = glnvg__renderCreateTexture; + params.renderDeleteTexture = glnvg__renderDeleteTexture; + params.renderUpdateTexture = glnvg__renderUpdateTexture; + params.renderGetTextureSize = glnvg__renderGetTextureSize; + params.renderViewport = glnvg__renderViewport; + params.renderCancel = glnvg__renderCancel; + params.renderFlush = glnvg__renderFlush; + params.renderFill = glnvg__renderFill; + params.renderStroke = glnvg__renderStroke; + params.renderTriangles = glnvg__renderTriangles; + params.renderDelete = glnvg__renderDelete; + params.userPtr = gl; + params.edgeAntiAlias = flags & NVG_ANTIALIAS ? 1 : 0; + + gl->flags = flags; + + ctx = nvgCreateInternal(¶ms); + if (ctx == nullptr) goto error; + + return ctx; + +error: + if (ctx != nullptr) nvgDeleteInternal(ctx); + return nullptr; +} + +void nvgDeleteGL46(NVGcontext* ctx) +{ + nvgDeleteInternal(ctx); +} + +int nvglCreateImageFromHandleGL46(NVGcontext* ctx, GLuint textureId, int w, int h, int imageFlags) +{ + GLNVGcontext* gl = (GLNVGcontext*)nvgInternalParams(ctx)->userPtr; + GLNVGtexture* tex = glnvg__allocTexture(gl); + if (tex == nullptr) return 0; + + tex->type = NVG_TEXTURE_RGBA; + tex->tex = textureId; + tex->flags = imageFlags; + tex->width = w; + tex->height = h; + + return tex->id; +} + +GLuint nvglImageHandleGL46(NVGcontext* ctx, int image) +{ + GLNVGcontext* gl = (GLNVGcontext*)nvgInternalParams(ctx)->userPtr; + GLNVGtexture* tex = glnvg__findTexture(gl, image); + return tex->tex; +} diff --git a/src/nanovg_gl46.h b/src/nanovg_gl46.h new file mode 100644 index 0000000..d69889e --- /dev/null +++ b/src/nanovg_gl46.h @@ -0,0 +1,31 @@ +// +// NanoVG OpenGL 4.6 Core Profile backend +// +// Based on nanovg_gl.h by Mikko Mononen (Copyright (c) 2009-2013) +// Adapted to use OpenGL 4.6 Direct State Access (DSA) functions, +// immutable texture storage, and explicit layout qualifiers. +// +#pragma once + +#include "glad/gl.h" + +struct NVGcontext; + +// Create flags (same as nanovg_gl.h, guarded to avoid redefinition) +#ifndef NANOVG_GL_H +enum NVGcreateFlags { + NVG_ANTIALIAS = 1 << 0, + NVG_STENCIL_STROKES = 1 << 1, + NVG_DEBUG = 1 << 2, +}; + +enum NVGimageFlagsGL { + NVG_IMAGE_NODELETE = 1 << 16, +}; +#endif + +NVGcontext* nvgCreateGL46(int flags); +void nvgDeleteGL46(NVGcontext* ctx); + +int nvglCreateImageFromHandleGL46(NVGcontext* ctx, GLuint textureId, int w, int h, int flags); +GLuint nvglImageHandleGL46(NVGcontext* ctx, int image);