Compare commits
744 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a06fc754fc | ||
|
|
8546a754c7 | ||
|
|
1438c16558 | ||
|
|
0968b25968 | ||
|
|
b364713268 | ||
|
|
ff7aecadb2 | ||
|
|
02d0d024d6 | ||
|
|
471d00b534 | ||
|
|
0cf46b3d5d | ||
|
|
bc4bf03a64 | ||
|
|
77d51dd27d | ||
|
|
474a8a7e3c | ||
|
|
ae7004f5d8 | ||
|
|
944e1c3c7f | ||
|
|
83975c8a98 | ||
|
|
6f748cbe12 | ||
|
|
7ca910e35a | ||
|
|
3f94bba49e | ||
|
|
301d587535 | ||
|
|
7b15dbbea3 | ||
|
|
e0d125906b | ||
|
|
d5ab22c7f3 | ||
|
|
000fa24b2f | ||
|
|
b1582e5a1f | ||
|
|
4ba2b1ca1f | ||
|
|
7ec41006f7 | ||
|
|
25ac827865 | ||
|
|
a6710c3e2e | ||
|
|
6539d67087 | ||
|
|
5781f17858 | ||
|
|
3ceba131f1 | ||
|
|
f0efd36674 | ||
|
|
d78653a76f | ||
|
|
b86546a178 | ||
|
|
465a55a77e | ||
|
|
1b023f859b | ||
|
|
9edb680114 | ||
|
|
a47876aca8 | ||
|
|
ad182d4170 | ||
|
|
2a4d6d085c | ||
|
|
7605ebf48d | ||
|
|
9f4282e37b | ||
|
|
81741974bb | ||
|
|
a2722b98de | ||
|
|
a012cead98 | ||
|
|
3513192907 | ||
|
|
99ee9a5ad2 | ||
|
|
cb9b41baf6 | ||
|
|
effabf95fd | ||
|
|
8615f29132 | ||
|
|
dc94b60121 | ||
|
|
8c5672d5fb | ||
|
|
75a9df969b | ||
|
|
c268306bd6 | ||
|
|
e2342a75dd | ||
|
|
37323f9170 | ||
|
|
aa48dbbf86 | ||
|
|
ae54c6ebea | ||
|
|
8384b9f88b | ||
|
|
ea884f1583 | ||
|
|
c506865d7b | ||
|
|
920d7db5f6 | ||
|
|
35f93122e5 | ||
|
|
ed04e21861 | ||
|
|
c25643525a | ||
|
|
f031d761b3 | ||
|
|
f3fa371ef9 | ||
|
|
0d09f4c11f | ||
|
|
dd4f1de695 | ||
|
|
269c7da0ac | ||
|
|
0ed453fe1d | ||
|
|
f2bcfca8e8 | ||
|
|
d8324dacd4 | ||
|
|
544c1df366 | ||
|
|
63a969e5df | ||
|
|
552a6e33a6 | ||
|
|
3533d2356c | ||
|
|
120f1e2018 | ||
|
|
c92c1d5b4a | ||
|
|
2fd686a5e6 | ||
|
|
aa185f11d7 | ||
|
|
7fab36ff3e | ||
|
|
cc609bbb07 | ||
|
|
9c74f92147 | ||
|
|
3b113e4914 | ||
|
|
33d50aed67 | ||
|
|
a9c2d57bc5 | ||
|
|
3d2693796d | ||
|
|
70a937bcaa | ||
|
|
3e50c95c91 | ||
|
|
a0f9b78162 | ||
|
|
d507429b9b | ||
|
|
634d187c11 | ||
|
|
11f7041df0 | ||
|
|
2956872970 | ||
|
|
c8b0df6ff9 | ||
|
|
16952e13e4 | ||
|
|
e9fba367c0 | ||
|
|
db06516f97 | ||
|
|
ff8e96e293 | ||
|
|
454a5a0e11 | ||
|
|
7b456e3d98 | ||
|
|
0514528a4c | ||
|
|
bdcdae791e | ||
|
|
4d59d05ad6 | ||
|
|
00928fdb88 | ||
|
|
4a34b29b46 | ||
|
|
f590ad704e | ||
|
|
99a1bb1dc9 | ||
|
|
5fe5ebc0a2 | ||
|
|
aff30961bd | ||
|
|
64d211b333 | ||
|
|
606d3b2282 | ||
|
|
132c6aa2e8 | ||
|
|
4a140c110f | ||
|
|
1c8d052544 | ||
|
|
3199b77ae5 | ||
|
|
f0e1f18379 | ||
|
|
492e2a707a | ||
|
|
7551ca3484 | ||
|
|
8f3fa2e7f2 | ||
|
|
936619ce12 | ||
|
|
d6a42f8ef3 | ||
|
|
bf43e405ff | ||
|
|
0c7c33304d | ||
|
|
f9b1d8a9e1 | ||
|
|
7b4fe4bd5c | ||
|
|
6ba3111ada | ||
|
|
258ccf89e0 | ||
|
|
3e193bb675 | ||
|
|
69b96eee96 | ||
|
|
e1ba2c4ebb | ||
|
|
dfce9dae46 | ||
|
|
f4fca865bb | ||
|
|
08fa0725d3 | ||
|
|
b92b769f7b | ||
|
|
726517a8b6 | ||
|
|
51f54c6edd | ||
|
|
f7f260777a | ||
|
|
dd9ac2819d | ||
|
|
1a7bb8498e | ||
|
|
fb2d26153d | ||
|
|
a640c0e62c | ||
|
|
784322db6f | ||
|
|
355a63c46f | ||
|
|
155eb75b82 | ||
|
|
64d4ef0c03 | ||
|
|
edb15db8fa | ||
|
|
883f140b41 | ||
|
|
90a6765e8a | ||
|
|
b8ede978c2 | ||
|
|
c1091acc6b | ||
|
|
0034940082 | ||
|
|
4d35fad772 | ||
|
|
d86c3b2eb8 | ||
|
|
b3556813a9 | ||
|
|
a94dcf1949 | ||
|
|
7fa07ae5ea | ||
|
|
cf60d31b75 | ||
|
|
e1b4e1ea6a | ||
|
|
4f6b0aa04d | ||
|
|
8b5e2780c7 | ||
|
|
0dd730d8de | ||
|
|
0f865c7afb | ||
|
|
688f24a409 | ||
|
|
dcacd279b3 | ||
|
|
37d0b755af | ||
|
|
26672ce34b | ||
|
|
d1be49f474 | ||
|
|
87072a4edc | ||
|
|
5f8679e5d2 | ||
|
|
1a7cd37741 | ||
|
|
ed181c1dcd | ||
|
|
4d839e3662 | ||
|
|
156b30756d | ||
|
|
6326870525 | ||
|
|
a360c9349a | ||
|
|
9e6276e0fa | ||
|
|
2bbc7a8999 | ||
|
|
fc3e05434f | ||
|
|
388764e9c8 | ||
|
|
3fb8323dd0 | ||
|
|
2327c6baf8 | ||
|
|
144faf5b21 | ||
|
|
dc6dd4bb11 | ||
|
|
84482e8998 | ||
|
|
072ece7a3d | ||
|
|
22d557ed01 | ||
|
|
b454d54a99 | ||
|
|
341119386a | ||
|
|
1575d7319c | ||
|
|
950e92d7c7 | ||
|
|
0e98f52580 | ||
|
|
caf2703102 | ||
|
|
b79923de3d | ||
|
|
ee6d171ef6 | ||
|
|
1c30198347 | ||
|
|
4ed9199e1c | ||
|
|
2027cb064c | ||
|
|
a00eec69b7 | ||
|
|
3aa1c94c6a | ||
|
|
84c07c4115 | ||
|
|
6398164d42 | ||
|
|
fa42ad5a03 | ||
|
|
22553eb1f9 | ||
|
|
cb599eea0b | ||
|
|
f73413e7c0 | ||
|
|
5b752d3f87 | ||
|
|
4f37d3b597 | ||
|
|
21b497d2d4 | ||
|
|
3bfc0c266d | ||
|
|
f252fa3a7a | ||
|
|
66266fb426 | ||
|
|
225e2d9d1a | ||
|
|
3b4aa75890 | ||
|
|
0c1917d56e | ||
|
|
35850082d5 | ||
|
|
d24b719a93 | ||
|
|
a0c33a439b | ||
|
|
6bbd2c521a | ||
|
|
7575c35b02 | ||
|
|
f78a8df275 | ||
|
|
8462dde374 | ||
|
|
075e8f2897 | ||
|
|
204989b909 | ||
|
|
c023e3b4f2 | ||
|
|
e7c4d6f132 | ||
|
|
9d0cf33a45 | ||
|
|
34c5c71d77 | ||
|
|
03bede4067 | ||
|
|
f624f85698 | ||
|
|
985fa4b53c | ||
|
|
6c6c08d188 | ||
|
|
fe2e6522ed | ||
|
|
ea930ad917 | ||
|
|
f9d1d9961b | ||
|
|
efda204001 | ||
|
|
0c5882af8e | ||
|
|
56b75e38e2 | ||
|
|
b899dde5fe | ||
|
|
50e0634a94 | ||
|
|
28f2e1e881 | ||
|
|
e8e89f022b | ||
|
|
5d8cc2b3c4 | ||
|
|
3b261d9989 | ||
|
|
4660ab927c | ||
|
|
c283f6eac4 | ||
|
|
d7d930c4fa | ||
|
|
b8a16fb087 | ||
|
|
74dac668fb | ||
|
|
a1ace12042 | ||
|
|
90a3a16cc2 | ||
|
|
0882b71a9f | ||
|
|
cdfa15c1f6 | ||
|
|
a314c26b89 | ||
|
|
5c2024bab4 | ||
|
|
5c8f2a43cb | ||
|
|
055be2aa2a | ||
|
|
973f61e428 | ||
|
|
3a13071edb | ||
|
|
68e6621e7e | ||
|
|
d808486cd0 | ||
|
|
128f8eda98 | ||
|
|
4a5961d656 | ||
|
|
8adc708e4d | ||
|
|
2962b47939 | ||
|
|
fd22ea4153 | ||
|
|
9be68ac2f4 | ||
|
|
fc612c608f | ||
|
|
40c6d50d5e | ||
|
|
5d91a5bc09 | ||
|
|
2249e96496 | ||
|
|
5bee5dd1d1 | ||
|
|
0b1d8b9fd9 | ||
|
|
f3aecbe944 | ||
|
|
b9e5870a7f | ||
|
|
f872b995c3 | ||
|
|
74ecc39921 | ||
|
|
947802d5ed | ||
|
|
95fbd77a0e | ||
|
|
2618c6a978 | ||
|
|
717def2b1a | ||
|
|
a8e2876586 | ||
|
|
802b853b75 | ||
|
|
9bc0f46be9 | ||
|
|
73f27b65b1 | ||
|
|
28ef19a1dc | ||
|
|
7d9b735f8b | ||
|
|
e58cbaa6de | ||
|
|
48799d8ea1 | ||
|
|
c49f5309cd | ||
|
|
566e1da6e7 | ||
|
|
88b166d297 | ||
|
|
c2be58b0f2 | ||
|
|
3221702fc2 | ||
|
|
0d27de5ba7 | ||
|
|
3ade56c582 | ||
|
|
0028e5ec8f | ||
|
|
d1957680e8 | ||
|
|
1acb063b83 | ||
|
|
86ff9cebd2 | ||
|
|
93ea97b8dc | ||
|
|
ef8a5af94b | ||
|
|
4aa04c7cce | ||
|
|
d9c4116449 | ||
|
|
a10aa444bb | ||
|
|
aa4120433e | ||
|
|
79f42760b0 | ||
|
|
cbc8428e23 | ||
|
|
28507cc8b0 | ||
|
|
af863ae608 | ||
|
|
6a42bcccc7 | ||
|
|
09666e2dfa | ||
|
|
1b5c787b3a | ||
|
|
ff4c1b267e | ||
|
|
ec866fa1b8 | ||
|
|
3851b07294 | ||
|
|
e42802a2bd | ||
|
|
fb52dea6fa | ||
|
|
d072f474a4 | ||
|
|
6087dba91b | ||
|
|
7a4e01faab | ||
|
|
45aaf9b981 | ||
|
|
553935626e | ||
|
|
636b680c87 | ||
|
|
da36c6abd1 | ||
|
|
99f0335d36 | ||
|
|
94223738ec | ||
|
|
74bda196f0 | ||
|
|
365ae58c70 | ||
|
|
834253f0ae | ||
|
|
86c4c8f09e | ||
|
|
79cd18d310 | ||
|
|
fce99fbde3 | ||
|
|
917e308ece | ||
|
|
f74992a37a | ||
|
|
7c4a3fe7b4 | ||
|
|
e82d2a875c | ||
|
|
8b04be72ee | ||
|
|
6aebddb773 | ||
|
|
b78d389a06 | ||
|
|
b064032d8f | ||
|
|
ad0d1ede41 | ||
|
|
cc1f63b443 | ||
|
|
da1c382259 | ||
|
|
50cc7d983c | ||
|
|
7ad3d8c1c7 | ||
|
|
2829ddb085 | ||
|
|
d2ec4c359f | ||
|
|
84a32581b7 | ||
|
|
3fa50a8965 | ||
|
|
0b88a6f824 | ||
|
|
fbcef60f67 | ||
|
|
1d7e985593 | ||
|
|
ba788062c5 | ||
|
|
8a8cc8c423 | ||
|
|
fce9ca3824 | ||
|
|
d12a0f1ba4 | ||
|
|
dd46c43c73 | ||
|
|
b526d5a51a | ||
|
|
c209cc82af | ||
|
|
fc439459cb | ||
|
|
a2cfe942c6 | ||
|
|
7a88e1f893 | ||
|
|
d91d41de79 | ||
|
|
aed8366206 | ||
|
|
6d99f45412 | ||
|
|
4a3291f4a6 | ||
|
|
9cdf1282da | ||
|
|
a5361eaad2 | ||
|
|
65480c5e9f | ||
|
|
75685d57d6 | ||
|
|
b6aa53483d | ||
|
|
e032ea43e2 | ||
|
|
ec42f47d9e | ||
|
|
5f1c89144c | ||
|
|
71c181fb2c | ||
|
|
b34792ff45 | ||
|
|
985e0beafd | ||
|
|
7c27a4d8fd | ||
|
|
bbe7cd2099 | ||
|
|
862afc498e | ||
|
|
f023f8e8f1 | ||
|
|
987427b893 | ||
|
|
03e79db0ba | ||
|
|
f7e8ceaa3f | ||
|
|
07bb8e0475 | ||
|
|
306951ebee | ||
|
|
3684174b67 | ||
|
|
c2e95876d0 | ||
|
|
0c7421f36c | ||
|
|
d3bf885042 | ||
|
|
4bed271258 | ||
|
|
bbfc451956 | ||
|
|
539ac56548 | ||
|
|
b00223a351 | ||
|
|
c81a1d015e | ||
|
|
543a683a5f | ||
|
|
89eddde7d4 | ||
|
|
67b4aab39b | ||
|
|
68aaa878d3 | ||
|
|
4629a9f96c | ||
|
|
e7c123fcf8 | ||
|
|
1456756ea7 | ||
|
|
5d01bc8f03 | ||
|
|
7d0cbefe17 | ||
|
|
1794b1ac4b | ||
|
|
62f4116cc8 | ||
|
|
550a75460f | ||
|
|
d226146213 | ||
|
|
b9d4a4116a | ||
|
|
becd958c5e | ||
|
|
d1322990b3 | ||
|
|
325983152a | ||
|
|
d847f7f7b0 | ||
|
|
0234c09495 | ||
|
|
3b2614631d | ||
|
|
432d078212 | ||
|
|
1ffcaea80f | ||
|
|
8ef53b3c4f | ||
|
|
b0bd063ed6 | ||
|
|
99cfbb07b9 | ||
|
|
7e3a73671b | ||
|
|
523cb55889 | ||
|
|
1121b5e6dd | ||
|
|
79b7ddda8d | ||
|
|
d6c49ea142 | ||
|
|
a521e34f49 | ||
|
|
c6aa690eee | ||
|
|
5163cbd26e | ||
|
|
f98ee30491 | ||
|
|
2a128bd69c | ||
|
|
198f0f9c31 | ||
|
|
b7713815a4 | ||
|
|
47592b4e6d | ||
|
|
802cea001c | ||
|
|
c5cf08365f | ||
|
|
7af77296ed | ||
|
|
6ccef6d1a5 | ||
|
|
4fd84cb811 | ||
|
|
7cd2ecfcde | ||
|
|
3569e1d889 | ||
|
|
89dfc3c9f2 | ||
|
|
a7cf766c27 | ||
|
|
63983f6c26 | ||
|
|
183746ba4d | ||
|
|
4d77096589 | ||
|
|
1dc6d2a6bf | ||
|
|
7027c34941 | ||
|
|
2ff6f4cb4e | ||
|
|
d7d9365a63 | ||
|
|
7cc92a533d | ||
|
|
9e5e5e654d | ||
|
|
856b5b5b56 | ||
|
|
3c079397cd | ||
|
|
3f972856ad | ||
|
|
74630504ae | ||
|
|
e67ed69cc9 | ||
|
|
9e442ef854 | ||
|
|
7f3be8c1ac | ||
|
|
0b86620a4e | ||
|
|
b85e6367b4 | ||
|
|
40c4101bec | ||
|
|
aba9129460 | ||
|
|
9cf7652ae3 | ||
|
|
9f604dfc2e | ||
|
|
12404348e5 | ||
|
|
e0314de4f6 | ||
|
|
8b3f8812de | ||
|
|
426ae35a44 | ||
|
|
e812b13ef7 | ||
|
|
d73f844b10 | ||
|
|
76bad0be68 | ||
|
|
560ca9092c | ||
|
|
3702467178 | ||
|
|
f6eefdb336 | ||
|
|
33f5772ecd | ||
|
|
1d7e24537f | ||
|
|
02b4fee507 | ||
|
|
b387ab069c | ||
|
|
6cadf85b6a | ||
|
|
4a613ff145 | ||
|
|
68a17f0578 | ||
|
|
8b7a3c4bb1 | ||
|
|
a992a44f40 | ||
|
|
5e5c63d109 | ||
|
|
cca9ac0e56 | ||
|
|
7a2fe748ea | ||
|
|
2d95cdfd01 | ||
|
|
1c23394415 | ||
|
|
41c31976d6 | ||
|
|
eb7172aaba | ||
|
|
ac1427d125 | ||
|
|
755d410430 | ||
|
|
78b2f09e76 | ||
|
|
4858ad586b | ||
|
|
daa3cab313 | ||
|
|
ba30742630 | ||
|
|
ef389b7ed0 | ||
|
|
104bdf070c | ||
|
|
aea98cb6e4 | ||
|
|
0dd1eb443f | ||
|
|
77e3b95d6b | ||
|
|
4368f5bc4d | ||
|
|
699ccb90b2 | ||
|
|
b6a9adce28 | ||
|
|
7b11670d55 | ||
|
|
c6fd3b19f9 | ||
|
|
aae089404d | ||
|
|
fd2ee3f71f | ||
|
|
60b878d53c | ||
|
|
fd4d1d7910 | ||
|
|
c0a578cdd5 | ||
|
|
ff82c2d761 | ||
|
|
743c423ba5 | ||
|
|
fc55d5fc3b | ||
|
|
a8971166b4 | ||
|
|
ce5f792500 | ||
|
|
a6ed760304 | ||
|
|
d475b114ee | ||
|
|
6e05679b3e | ||
|
|
241897dbd9 | ||
|
|
f37c5353d0 | ||
|
|
5d187589b4 | ||
|
|
6d683503cd | ||
|
|
d77addb416 | ||
|
|
49aa9c2dd9 | ||
|
|
b849686564 | ||
|
|
57a8661b67 | ||
|
|
e336cee557 | ||
|
|
cf0afbb545 | ||
|
|
db43e26b8a | ||
|
|
9bd84ae3e8 | ||
|
|
613c4c744a | ||
|
|
4ff28bc323 | ||
|
|
8f1d89cb95 | ||
|
|
34666c7994 | ||
|
|
ac135d1907 | ||
|
|
f29f626a2f | ||
|
|
bc72eaeb0f | ||
|
|
d373b806ed | ||
|
|
64067d9ba5 | ||
|
|
4d20f4747a | ||
|
|
72ef321bfb | ||
|
|
ec67222855 | ||
|
|
a0132b8c67 | ||
|
|
71349d7404 | ||
|
|
28af11ca2e | ||
|
|
9e20eac7ab | ||
|
|
772ec27ea1 | ||
|
|
5421452429 | ||
|
|
900e72d1fc | ||
|
|
b6289a901c | ||
|
|
4d6859db43 | ||
|
|
aac3db90d3 | ||
|
|
1048d38b4d | ||
|
|
9ded4d4b13 | ||
|
|
d0e969dcf6 | ||
|
|
783f87f22d | ||
|
|
402d682d6e | ||
|
|
d6500d2227 | ||
|
|
d0b5f32e2d | ||
|
|
7454179313 | ||
|
|
194cf672ee | ||
|
|
5a3ef0fe69 | ||
|
|
7f62b76691 | ||
|
|
75ad35e057 | ||
|
|
44c66d5c26 | ||
|
|
3dcfd196a1 | ||
|
|
b7d8f82a3d | ||
|
|
dbf1188b12 | ||
|
|
6539e69fb0 | ||
|
|
07b00bc710 | ||
|
|
d090703d32 | ||
|
|
b08ba23889 | ||
|
|
b8fda63ef3 | ||
|
|
316f42cf03 | ||
|
|
7b9c31b770 | ||
|
|
67347048b5 | ||
|
|
73cc91fa0d | ||
|
|
a1ed14e8cb | ||
|
|
8321d3feee | ||
|
|
46283fec0e | ||
|
|
65244bc43d | ||
|
|
ed0bd1a2b5 | ||
|
|
7def1b487c | ||
|
|
2f7f7f4f6b | ||
|
|
1a3ad7d701 | ||
|
|
5242104d87 | ||
|
|
a369d61024 | ||
|
|
df2611f0b7 | ||
|
|
663a352586 | ||
|
|
412768c57e | ||
|
|
5bb91bb1eb | ||
|
|
d9d433f47c | ||
|
|
c3701ad903 | ||
|
|
6ed4c92c94 | ||
|
|
fdeed5c257 | ||
|
|
aa1ba08f70 | ||
|
|
a62413ed0b | ||
|
|
2057ced33d | ||
|
|
f8d9cb8855 | ||
|
|
09b1029030 | ||
|
|
2c778ce5ec | ||
|
|
029a7df71a | ||
|
|
8a3d7a9da5 | ||
|
|
69f0c63831 | ||
|
|
523fd89f97 | ||
|
|
d9fc4b3c05 | ||
|
|
8c36021746 | ||
|
|
1b82b3020f | ||
|
|
eeeecb982c | ||
|
|
70a1fe36c7 | ||
|
|
622c40f188 | ||
|
|
3b25d2d535 | ||
|
|
017bf78d4c | ||
|
|
775bf46b8e | ||
|
|
9c05fdc11e | ||
|
|
58ae91afbe | ||
|
|
acb7f3aa17 | ||
|
|
3c5d79ce77 | ||
|
|
812342b4da | ||
|
|
c6fe0a1bed | ||
|
|
00a123b150 | ||
|
|
56d96ddd84 | ||
|
|
5a6819757b | ||
|
|
3618c136f7 | ||
|
|
4312472534 | ||
|
|
bdf3319204 | ||
|
|
53672d3ace | ||
|
|
fac00a81d3 | ||
|
|
c0be96e797 | ||
|
|
5979b17891 | ||
|
|
5e5d0c8051 | ||
|
|
0a7c0c573c | ||
|
|
a15978d1fb | ||
|
|
7fa62375c6 | ||
|
|
182e66daff | ||
|
|
640da6c1be | ||
|
|
3d2129c1d4 | ||
|
|
5e72b945c4 | ||
|
|
3a800ec9ca | ||
|
|
10391c48c7 | ||
|
|
00cf0c73e3 | ||
|
|
f53a81099e | ||
|
|
f9a8f9b1c2 | ||
|
|
27ed49fcd9 | ||
|
|
ad7660e4d6 | ||
|
|
7eaa8483b8 | ||
|
|
87238bdb45 | ||
|
|
aafc475843 | ||
|
|
78d5c224a8 | ||
|
|
b875d5fc25 | ||
|
|
df5c34c626 | ||
|
|
0aeb0fbf21 | ||
|
|
d5045731ad | ||
|
|
1c0e0cc666 | ||
|
|
b9a3d18b56 | ||
|
|
76aaf3824d | ||
|
|
756431b581 | ||
|
|
c0cc236c40 | ||
|
|
1a42f115fb | ||
|
|
fbdd65bc00 | ||
|
|
306956839a | ||
|
|
d4714989b4 | ||
|
|
1d1e244f03 | ||
|
|
f055234463 | ||
|
|
6450069d8a | ||
|
|
c8bcfc3b6d | ||
|
|
3ba1c76414 | ||
|
|
7f25f5ff5d | ||
|
|
15391a6f91 | ||
|
|
b81fd46929 | ||
|
|
fce77cbd5f | ||
|
|
163d1f9f43 | ||
|
|
d2483283f5 | ||
|
|
f536b0cd74 | ||
|
|
445c583a5b | ||
|
|
a5122d31a4 | ||
|
|
0135c63b88 | ||
|
|
395112fc40 | ||
|
|
323cd95bff | ||
|
|
4bb05252aa | ||
|
|
8797ee56a8 | ||
|
|
6eed71656d | ||
|
|
ee5bca7db5 | ||
|
|
082143ed15 | ||
|
|
377c51f365 | ||
|
|
cd3b40399c | ||
|
|
10ff5dda7d | ||
|
|
403317fd29 | ||
|
|
351fac9052 | ||
|
|
2d6d948e80 | ||
|
|
593f222760 | ||
|
|
0fdba1bf64 | ||
|
|
e4fe056586 | ||
|
|
0bb04b2f8e | ||
|
|
264e26cb90 | ||
|
|
0366c42cf3 | ||
|
|
7d883913bd | ||
|
|
57ad9f56be | ||
|
|
082885a5eb | ||
|
|
945cd68672 | ||
|
|
3194f1d23e | ||
|
|
154f016a96 | ||
|
|
0362399ba7 | ||
|
|
7dc8bbcd9a | ||
|
|
5225bca007 | ||
|
|
9b08c6e5ff | ||
|
|
7f5b84e6e7 | ||
|
|
2f07b8420b | ||
|
|
6fdcde856b | ||
|
|
004bd1e751 | ||
|
|
aa65197749 | ||
|
|
7703e2fb0f | ||
|
|
433f3ace9f | ||
|
|
87f7d71c94 | ||
|
|
ca899dd506 | ||
|
|
25712a850e | ||
|
|
9329a7885d | ||
|
|
c04bf8b351 | ||
|
|
d5cf684215 | ||
|
|
4f02eca7c9 | ||
|
|
d7b555fca6 | ||
|
|
9d4f912808 | ||
|
|
26825e358f | ||
|
|
5d58e9e4da | ||
|
|
72e35557a4 | ||
|
|
0ff2db1881 | ||
|
|
4cbd7af3d0 | ||
|
|
5b5939855f | ||
|
|
91e93dea2b | ||
|
|
37b85057c3 | ||
|
|
3cec8b281c | ||
|
|
86f1770af3 | ||
|
|
707c0b1c85 | ||
|
|
6ba56a133e | ||
|
|
1e855a0ed0 | ||
|
|
716bf097bf | ||
|
|
8ed121f71e | ||
|
|
e5333cb2e9 | ||
|
|
a7c6768449 | ||
|
|
e1c7bb18a9 | ||
|
|
12b2dce779 |
24
.github/workflows/check-does-build-pr.yml
vendored
Normal file
24
.github/workflows/check-does-build-pr.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
name: check-does-build-pr
|
||||
|
||||
on: [ pull_request ]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 21
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
with:
|
||||
cache-read-only: true
|
||||
|
||||
- name: Gradle build
|
||||
run: ./gradlew build
|
||||
32
.github/workflows/check-does-build.yml
vendored
Normal file
32
.github/workflows/check-does-build.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
name: check-does-build
|
||||
|
||||
on: [ push ]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 21
|
||||
|
||||
- name: Loom Cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: "**/.gradle/loom-cache"
|
||||
key: "${{ runner.os }}-gradle-${{ hashFiles('**/libs.versions.*', '**/*.gradle*', '**/gradle-wrapper.properties') }}"
|
||||
restore-keys: "${{ runner.os }}-gradle-"
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
with:
|
||||
cache-read-only: false
|
||||
cache-cleanup: never
|
||||
|
||||
- name: Gradle build
|
||||
run: ./gradlew -I init.gradle build
|
||||
31
.github/workflows/manual-artifact.yml
vendored
Normal file
31
.github/workflows/manual-artifact.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
name: manual-artifact
|
||||
|
||||
on: [ workflow_dispatch ]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 21
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
with:
|
||||
cache-read-only: false
|
||||
cache-cleanup: always
|
||||
|
||||
- name: Gradle build
|
||||
run: ./gradlew build
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: voxy-artifacts
|
||||
path: build/libs/*.jar
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -1,3 +1,7 @@
|
||||
# Project exclude paths
|
||||
/.gradle/
|
||||
/build/
|
||||
/.idea/
|
||||
/build/
|
||||
/run/
|
||||
/out/
|
||||
/logs/
|
||||
|
||||
5
LICENSE.md
Normal file
5
LICENSE.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Copyright 2025 MCRcortex
|
||||
All rights reserved.
|
||||
Do not redistribute.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
201
build.gradle
201
build.gradle
@@ -1,11 +1,17 @@
|
||||
plugins {
|
||||
id 'fabric-loom' version "1.7.1"
|
||||
id 'fabric-loom' version "1.11-SNAPSHOT"
|
||||
id 'maven-publish'
|
||||
}
|
||||
|
||||
def isInGHA = System.getenv("GITHUB_ACTIONS") == "true"
|
||||
|
||||
version = project.mod_version
|
||||
group = project.maven_group
|
||||
|
||||
base {
|
||||
archivesName = project.archives_base_name
|
||||
}
|
||||
|
||||
repositories {
|
||||
exclusiveContent {
|
||||
forRepository {
|
||||
@@ -18,15 +24,56 @@ repositories {
|
||||
includeGroup "maven.modrinth"
|
||||
}
|
||||
}
|
||||
maven { url "https://maven.shedaniel.me/" }
|
||||
maven { url "https://maven.terraformersmc.com/releases/" }
|
||||
maven { url = "https://maven.shedaniel.me/" }
|
||||
maven { url = "https://maven.terraformersmc.com/releases/" }
|
||||
|
||||
exclusiveContent {
|
||||
forRepository {
|
||||
ivy {
|
||||
name = "github"
|
||||
url = "https://github.com/"
|
||||
patternLayout {
|
||||
artifact '/[organisation]/[module]/releases/download/[revision]/[module]-[revision]-[classifier].[ext]'
|
||||
}
|
||||
metadataSources {
|
||||
artifact()
|
||||
}
|
||||
}
|
||||
}
|
||||
filter {
|
||||
includeModuleByRegex("[^\\.]+", "nvidium")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def gitCommitHash = { ->
|
||||
try {
|
||||
ExecOutput result = providers.exec {
|
||||
commandLine = ['git', 'rev-parse', '--short', 'HEAD']
|
||||
ignoreExitValue = true
|
||||
}
|
||||
|
||||
if (result.getResult().get().getExitValue() != 0) {
|
||||
return "<UnknownCommit>";
|
||||
} else {
|
||||
return result.standardOutput.asText.get().strip();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace()
|
||||
return "<UnknownCommit>";
|
||||
}
|
||||
}
|
||||
|
||||
def buildtime = {System.currentTimeSeconds()}
|
||||
processResources {
|
||||
inputs.property "version", project.version
|
||||
archivesBaseName = "voxy"
|
||||
def time = buildtime()
|
||||
def hash = gitCommitHash()
|
||||
def version = project.version
|
||||
|
||||
inputs.properties("version": version, "commit": hash, "buildtime": time)
|
||||
filesMatching("fabric.mod.json") {
|
||||
expand "version": project.version
|
||||
expand "version": version, "commit": hash, "buildtime": time
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,43 +81,61 @@ loom {
|
||||
accessWidenerPath = file("src/main/resources/voxy.accesswidener")
|
||||
}
|
||||
|
||||
def modRuntimeOnlyMsk = {arg->}
|
||||
if (!isInGHA) {
|
||||
modRuntimeOnlyMsk = { arg ->
|
||||
dependencies {
|
||||
modRuntimeOnly(arg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// To change the versions see the gradle.properties file
|
||||
minecraft "com.mojang:minecraft:${project.minecraft_version}"
|
||||
mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2"
|
||||
modImplementation "net.fabricmc:fabric-loader:${project.loader_version}"
|
||||
|
||||
modImplementation(fabricApi.module("fabric-api-base", project.fabric_version))
|
||||
modImplementation(fabricApi.module("fabric-rendering-fluids-v1", project.fabric_version))
|
||||
modImplementation(fabricApi.module("fabric-resource-loader-v0", project.fabric_version))
|
||||
modImplementation(fabricApi.module("fabric-command-api-v2", project.fabric_version))
|
||||
modImplementation("net.fabricmc.fabric-api:fabric-rendering-data-attachment-v1:0.3.38+73761d2e9a")
|
||||
|
||||
modRuntimeOnlyMsk(fabricApi.module("fabric-api-base", project.fabric_version))
|
||||
modRuntimeOnlyMsk(fabricApi.module("fabric-rendering-fluids-v1", project.fabric_version))
|
||||
modRuntimeOnlyMsk(fabricApi.module("fabric-resource-loader-v0", project.fabric_version))
|
||||
modRuntimeOnlyMsk(fabricApi.module("fabric-command-api-v2", project.fabric_version))
|
||||
|
||||
modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}"
|
||||
|
||||
//TODO: this is to eventually not need sodium installed as atm its just used for parsing shaders
|
||||
modRuntimeOnly "maven.modrinth:sodium:mc1.21-0.5.9"
|
||||
modCompileOnly "maven.modrinth:sodium:mc1.21-0.5.9"
|
||||
modRuntimeOnlyMsk "maven.modrinth:sodium:mc1.21.8-0.7.0-fabric"
|
||||
modCompileOnly "maven.modrinth:sodium:mc1.21.8-0.7.0-fabric"
|
||||
|
||||
//modRuntimeOnly "maven.modrinth:nvidium:0.2.6-beta"
|
||||
modCompileOnly "maven.modrinth:nvidium:0.2.8-beta"
|
||||
modImplementation("maven.modrinth:lithium:mc1.21.8-0.18.0-fabric")
|
||||
|
||||
//modRuntimeOnlyMsk "drouarb:nvidium:0.4.1-beta4:1.21.6@jar"
|
||||
modCompileOnly "drouarb:nvidium:0.4.1-beta4:1.21.6@jar"
|
||||
|
||||
modCompileOnly("maven.modrinth:modmenu:15.0.0-beta.3")
|
||||
modRuntimeOnlyMsk("maven.modrinth:modmenu:15.0.0-beta.3")
|
||||
|
||||
modCompileOnly("maven.modrinth:iris:1.9.1+1.21.7-fabric")
|
||||
//modRuntimeOnlyMsk("maven.modrinth:iris:1.9.1+1.21.7-fabric")
|
||||
|
||||
//modCompileOnly("maven.modrinth:starlight:1.1.3+1.20.4")
|
||||
|
||||
modImplementation("maven.modrinth:cloth-config:13.0.121+fabric")
|
||||
modImplementation("maven.modrinth:modmenu:11.0.1")
|
||||
modCompileOnly("maven.modrinth:iris:1.7.3+1.21")
|
||||
//modRuntimeOnly("maven.modrinth:iris:1.6.17+1.20.4")
|
||||
modCompileOnly("maven.modrinth:starlight:1.1.3+1.20.4")
|
||||
//modCompileOnly("maven.modrinth:immersiveportals:v5.1.7-mc1.20.4")
|
||||
modCompileOnly("maven.modrinth:vivecraft:1.20.4-1.1.6-fabric")
|
||||
|
||||
modCompileOnly("maven.modrinth:chunky:1.4.16-fabric")
|
||||
modRuntimeOnly("maven.modrinth:chunky:1.4.16-fabric")
|
||||
|
||||
modRuntimeOnly("maven.modrinth:spark:1.10.73-fabric")
|
||||
modRuntimeOnly("maven.modrinth:fabric-permissions-api:0.3.1")
|
||||
modCompileOnly("maven.modrinth:chunky:1.4.40-fabric")
|
||||
modRuntimeOnlyMsk("maven.modrinth:chunky:1.4.40-fabric")
|
||||
|
||||
modRuntimeOnlyMsk("maven.modrinth:spark:1.10.139-fabric")
|
||||
modRuntimeOnlyMsk("maven.modrinth:fabric-permissions-api:0.3.3")
|
||||
//modRuntimeOnly("maven.modrinth:nsight-loader:1.2.0")
|
||||
|
||||
modImplementation('io.github.douira:glsl-transformer:2.0.1')
|
||||
//modImplementation('io.github.douira:glsl-transformer:2.0.1')
|
||||
|
||||
modCompileOnly("maven.modrinth:vivecraft:1.21.1-1.1.14-b2-fabric")
|
||||
|
||||
modCompileOnly("maven.modrinth:flashback:rNCr1Rbs")
|
||||
}
|
||||
|
||||
|
||||
@@ -91,16 +156,67 @@ java {
|
||||
if (JavaVersion.current() < javaVersion) {
|
||||
toolchain.languageVersion = JavaLanguageVersion.of(targetJavaVersion)
|
||||
}
|
||||
archivesBaseName = project.archives_base_name
|
||||
|
||||
// Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the "build" task
|
||||
// if it is present.
|
||||
// If you remove this line, sources will not be generated.
|
||||
withSourcesJar()
|
||||
|
||||
|
||||
//withSourcesJar()
|
||||
}
|
||||
|
||||
jar {
|
||||
from("LICENSE") {
|
||||
rename { "${it}_${project.archivesBaseName}"}
|
||||
rename { "${it}_${project.archives_base_name}"}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
tasks.register('makeExcludedRocksDB', Zip) {
|
||||
archiveExtension.set("jar")
|
||||
entryCompression = ZipEntryCompression.STORED
|
||||
destinationDirectory.set temporaryDir
|
||||
|
||||
dependsOn configurations.includeInternal
|
||||
|
||||
def files = configurations.includeInternal.incoming.getArtifacts().getArtifactFiles().filter {
|
||||
it.name.startsWith('rocksdb')
|
||||
}
|
||||
|
||||
from {->zipTree(files.first())}
|
||||
archiveFileName.set(providers.provider{files.first().name})
|
||||
|
||||
exclude {
|
||||
def file = it.name
|
||||
if (file.endsWith(".jnilib")) {
|
||||
return true
|
||||
}
|
||||
if (!file.endsWith(".so")) {
|
||||
return false
|
||||
}
|
||||
return ["osx", "linux32", "musl", "s390x", "riscv64", "ppc64le", "aarch64"].any(file::contains)
|
||||
}
|
||||
}
|
||||
|
||||
processIncludeJars {
|
||||
outputs.cacheIf {true}
|
||||
|
||||
dependsOn makeExcludedRocksDB
|
||||
jars = jars.filter {!it.name.startsWith('rocksdb')}
|
||||
jars.from(makeExcludedRocksDB)
|
||||
}
|
||||
|
||||
remapJar {
|
||||
doFirst {
|
||||
delete fileTree(getDestinationDirectory().get())
|
||||
}
|
||||
|
||||
if (!isInGHA) {
|
||||
def hash = gitCommitHash();
|
||||
if (!hash.equals("<UnknownCommit>")) {
|
||||
archiveClassifier.set(hash);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,6 +227,7 @@ repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
|
||||
dependencies {
|
||||
implementation platform("org.lwjgl:lwjgl-bom:$lwjglVersion")
|
||||
|
||||
@@ -124,9 +241,33 @@ dependencies {
|
||||
include(runtimeOnly "org.lwjgl:lwjgl-lmdb:$lwjglVersion:natives-linux")
|
||||
include(runtimeOnly "org.lwjgl:lwjgl-zstd:$lwjglVersion:natives-linux")
|
||||
|
||||
include(implementation 'org.rocksdb:rocksdbjni:8.10.0')
|
||||
include(implementation 'redis.clients:jedis:5.1.0')
|
||||
include(implementation('org.rocksdb:rocksdbjni:10.2.1'))
|
||||
include(implementation 'org.apache.commons:commons-pool2:2.12.0')
|
||||
include(implementation 'org.lz4:lz4-java:1.8.0')
|
||||
include(implementation('org.tukaani:xz:1.10'))
|
||||
|
||||
if (true) {
|
||||
if (!isInGHA) {
|
||||
minecraftRuntimeLibraries('org.xerial:sqlite-jdbc:3.49.1.0')
|
||||
}
|
||||
} else {
|
||||
include(implementation('org.xerial:sqlite-jdbc:3.49.1.0'))
|
||||
}
|
||||
//implementation 'org.rocksdb:rocksdbjni:8.10.0'
|
||||
//implementation 'redis.clients:jedis:5.1.0'
|
||||
}
|
||||
|
||||
if (!isInGHA) {
|
||||
repositories {
|
||||
maven {
|
||||
url = "https://pkgs.dev.azure.com/djtheredstoner/DevAuth/_packaging/public/maven/v1"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
modRuntimeOnly('me.djtheredstoner:DevAuth-fabric:1.1.0') {
|
||||
exclude group: 'net.fabricmc', module: 'fabric-loader'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,21 @@
|
||||
# Done to increase the memory available to gradle.
|
||||
org.gradle.jvmargs=-Xmx1G
|
||||
org.gradle.jvmargs=-Xmx2G
|
||||
|
||||
org.gradle.caching=true
|
||||
org.gradle.parallel=true
|
||||
org.gradle.daemon = false
|
||||
|
||||
# Fabric Properties
|
||||
# check these on https://modmuss50.me/fabric.html
|
||||
minecraft_version=1.21
|
||||
yarn_mappings=1.21+build.2
|
||||
loader_version=0.15.11
|
||||
minecraft_version=1.21.8
|
||||
yarn_mappings=1.21.8+build.1
|
||||
loader_version=0.16.14
|
||||
loom_version=1.11-SNAPSHOT
|
||||
|
||||
# Fabric API
|
||||
fabric_version=0.129.0+1.21.8
|
||||
|
||||
# Mod Properties
|
||||
mod_version = 0.1.6-alpha
|
||||
mod_version = 0.2.5-alpha
|
||||
maven_group = me.cortex
|
||||
archives_base_name = voxy
|
||||
|
||||
fabric_version=0.100.1+1.21
|
||||
archives_base_name = voxy
|
||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip
|
||||
networkTimeout=10000
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
16
init.gradle
Normal file
16
init.gradle
Normal file
@@ -0,0 +1,16 @@
|
||||
if (System.getenv("GITHUB_ACTIONS") == "true") {
|
||||
beforeSettings { settings ->
|
||||
def cleanupTime = Long.parseLong(System.getProperty("CLEANUP_TIME", Long.toString(System.currentTimeMillis()-(60_000*10))));//Remove unused entries from more then 10 min old
|
||||
|
||||
settings.caches {
|
||||
//Note: this could be Cleanup.DEFAULT
|
||||
cleanup = Cleanup.ALWAYS
|
||||
|
||||
releasedWrappers.setRemoveUnusedEntriesOlderThan(cleanupTime)
|
||||
snapshotWrappers.setRemoveUnusedEntriesOlderThan(cleanupTime)
|
||||
downloadedResources.setRemoveUnusedEntriesOlderThan(cleanupTime)
|
||||
createdResources.setRemoveUnusedEntriesOlderThan(cleanupTime)
|
||||
buildCache.setRemoveUnusedEntriesOlderThan(cleanupTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package me.cortex.voxy.api;
|
||||
|
||||
import me.cortex.voxy.common.world.WorldSection;
|
||||
|
||||
public class VoxyApi {
|
||||
private VoxyApi(String useIdentifier) {
|
||||
|
||||
}
|
||||
|
||||
public static VoxyApi makeAPI(String identifier) {
|
||||
return new VoxyApi(identifier);
|
||||
}
|
||||
|
||||
public interface ArbitraryGenerateCallback {
|
||||
int ALL_DARK = -1;
|
||||
int OK = 0;
|
||||
int ALL_SKY = 1;
|
||||
int generate(int lodLevel, int x, int y, int z, long[] outputBuffer);
|
||||
}
|
||||
|
||||
public void setArbitraryGenerator(ArbitraryGenerateCallback generator) {
|
||||
|
||||
}
|
||||
|
||||
public WorldSection acquireSection(int level, int x, int y, int z) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
58
src/main/java/me/cortex/voxy/client/ClientImportManager.java
Normal file
58
src/main/java/me/cortex/voxy/client/ClientImportManager.java
Normal file
@@ -0,0 +1,58 @@
|
||||
package me.cortex.voxy.client;
|
||||
|
||||
import me.cortex.voxy.common.Logger;
|
||||
import me.cortex.voxy.commonImpl.ImportManager;
|
||||
import me.cortex.voxy.commonImpl.importers.IDataImporter;
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
import net.minecraft.client.gui.hud.ClientBossBar;
|
||||
import net.minecraft.entity.boss.BossBar;
|
||||
import net.minecraft.text.Text;
|
||||
import net.minecraft.util.math.MathHelper;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class ClientImportManager extends ImportManager {
|
||||
protected class ClientImportTask extends ImportTask {
|
||||
private final UUID bossbarUUID;
|
||||
private final ClientBossBar bossBar;
|
||||
protected ClientImportTask(IDataImporter importer) {
|
||||
super(importer);
|
||||
|
||||
this.bossbarUUID = MathHelper.randomUuid();
|
||||
this.bossBar = new ClientBossBar(this.bossbarUUID, Text.of("Voxy world importer"), 0.0f, BossBar.Color.GREEN, BossBar.Style.PROGRESS, false, false, false);
|
||||
MinecraftClient.getInstance().execute(()->{
|
||||
MinecraftClient.getInstance().inGameHud.getBossBarHud().bossBars.put(bossBar.getUuid(), bossBar);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean onUpdate(int completed, int outOf) {
|
||||
if (!super.onUpdate(completed, outOf)) {
|
||||
return false;
|
||||
}
|
||||
MinecraftClient.getInstance().execute(()->{
|
||||
this.bossBar.setPercent((float) (((double)completed) / ((double) Math.max(1, outOf))));
|
||||
this.bossBar.setName(Text.of("Voxy import: " + completed + "/" + outOf + " chunks"));
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCompleted(int total) {
|
||||
super.onCompleted(total);
|
||||
MinecraftClient.getInstance().execute(()->{
|
||||
MinecraftClient.getInstance().inGameHud.getBossBarHud().bossBars.remove(this.bossbarUUID);
|
||||
long delta = Math.max(System.currentTimeMillis() - this.startTime, 1);
|
||||
|
||||
String msg = "Voxy world import finished in " + (delta/1000) + " seconds, averaging " + (int)(total/(delta/1000f)) + " chunks per second";
|
||||
MinecraftClient.getInstance().inGameHud.getChatHud().addMessage(Text.literal(msg));
|
||||
Logger.info(msg);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected synchronized ImportTask createImportTask(IDataImporter importer) {
|
||||
return new ClientImportTask(importer);
|
||||
}
|
||||
}
|
||||
315
src/main/java/me/cortex/voxy/client/GPUSelectorWindows2.java
Normal file
315
src/main/java/me/cortex/voxy/client/GPUSelectorWindows2.java
Normal file
@@ -0,0 +1,315 @@
|
||||
package me.cortex.voxy.client;
|
||||
|
||||
import me.cortex.voxy.common.Logger;
|
||||
import org.lwjgl.system.JNI;
|
||||
import org.lwjgl.system.MemoryStack;
|
||||
import org.lwjgl.system.MemoryUtil;
|
||||
import org.lwjgl.system.windows.GDI32;
|
||||
import org.lwjgl.system.windows.Kernel32;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.lwjgl.system.APIUtil.apiGetFunctionAddressOptional;
|
||||
|
||||
public class GPUSelectorWindows2 {
|
||||
private static final long D3DKMTSetProperties = apiGetFunctionAddressOptional(GDI32.getLibrary(), "D3DKMTSetProperties");
|
||||
private static final long D3DKMTEnumAdapters2 = apiGetFunctionAddressOptional(GDI32.getLibrary(), "D3DKMTEnumAdapters2");
|
||||
private static final long D3DKMTCloseAdapter = apiGetFunctionAddressOptional(GDI32.getLibrary(), "D3DKMTCloseAdapter");
|
||||
private static final long D3DKMTQueryAdapterInfo = apiGetFunctionAddressOptional(GDI32.getLibrary(), "D3DKMTQueryAdapterInfo");
|
||||
|
||||
private static int setPCIProperties(int type, int vendor, int device, int subSys) {
|
||||
try (var stack = MemoryStack.stackPush()) {
|
||||
var buff = stack.calloc(0x10).order(ByteOrder.nativeOrder());
|
||||
buff.putInt(0, vendor);
|
||||
buff.putInt(4, device);
|
||||
buff.putInt(8, subSys);
|
||||
buff.putInt(12, 0);
|
||||
return setProperties(type, buff);
|
||||
}
|
||||
}
|
||||
|
||||
private static int setProperties(int type, ByteBuffer payload) {
|
||||
if (D3DKMTSetProperties == 0) {
|
||||
return -1;
|
||||
}
|
||||
try (var stack = MemoryStack.stackPush()) {
|
||||
var buff = stack.calloc(0x18).order(ByteOrder.nativeOrder());
|
||||
buff.putInt(0, type);
|
||||
buff.putInt(4, payload.remaining());
|
||||
buff.putLong(16, MemoryUtil.memAddress(payload));
|
||||
return JNI.callPI(MemoryUtil.memAddress(buff), D3DKMTSetProperties);
|
||||
}
|
||||
}
|
||||
|
||||
private static int query(int handle, int type, ByteBuffer payload) {
|
||||
if (D3DKMTQueryAdapterInfo == 0) {
|
||||
return -1;
|
||||
}
|
||||
try (var stack = MemoryStack.stackPush()) {
|
||||
var buff = stack.calloc(0x14).order(ByteOrder.nativeOrder());
|
||||
buff.putInt(0, handle);
|
||||
buff.putInt(4, type);
|
||||
buff.putLong(8, MemoryUtil.memAddress(payload));
|
||||
buff.putInt(16, payload.remaining());
|
||||
return JNI.callPI(MemoryUtil.memAddress(buff), D3DKMTQueryAdapterInfo);
|
||||
}
|
||||
}
|
||||
|
||||
private static int closeHandle(int handle) {
|
||||
if (D3DKMTCloseAdapter == 0) {
|
||||
return -1;
|
||||
}
|
||||
try (var stack = MemoryStack.stackPush()) {
|
||||
var buff = stack.calloc(0x4).order(ByteOrder.nativeOrder());
|
||||
buff.putInt(0, handle);
|
||||
return JNI.callPI(MemoryUtil.memAddress(buff), D3DKMTCloseAdapter);
|
||||
}
|
||||
}
|
||||
|
||||
private static int queryAdapterType(int handle, int[] out) {
|
||||
int ret;
|
||||
try (var stack = MemoryStack.stackPush()) {
|
||||
var buff = stack.calloc(0x4).order(ByteOrder.nativeOrder());
|
||||
//KMTQAITYPE_ADAPTERTYPE
|
||||
if ((ret=query(handle, 15, buff))<0) {
|
||||
return ret;
|
||||
}
|
||||
out[0] = buff.getInt(0);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static int queryAdapterIcd(int handle, String[] out) {
|
||||
int ret;
|
||||
try (var stack = MemoryStack.stackPush()) {
|
||||
var buff = stack.calloc(260*2+8).order(ByteOrder.nativeOrder());
|
||||
//KMTQAITYPE_UMOPENGLINFO
|
||||
if ((ret=query(handle, 2, buff))<0) {
|
||||
return ret;
|
||||
}
|
||||
int len = Math.min(MemoryUtil.memLengthNT2(buff), 260*2);
|
||||
out[0] = MemoryUtil.memUTF16(buff.limit(len));
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private record AdapterInfo(String icdPath, int type, long luid, int vendor, int device, int subSystem) {
|
||||
@Override
|
||||
public String toString() {
|
||||
String LUID = Integer.toHexString((int) ((luid>>>32)&0xFFFFFFFFL))+"-"+Integer.toHexString((int) (luid&0xFFFFFFFFL));
|
||||
return "{type=%s, luid=%s, vendor=%s, device=%s, subSys=%s, icd=\"%s\"}".formatted(Integer.toString(type),LUID, Integer.toHexString(vendor), Integer.toHexString(device), Integer.toHexString(subSystem), icdPath);
|
||||
}
|
||||
}
|
||||
|
||||
private record PCIDeviceId(int vendor, int device, int subVendor, int subSystem, int revision, int busType) {}
|
||||
private static int queryPCIAddress(int handle, int index, PCIDeviceId[] deviceOut) {
|
||||
int ret = 0;
|
||||
try (var stack = MemoryStack.stackPush()) {
|
||||
var buff = stack.calloc(4*7).order(ByteOrder.nativeOrder());
|
||||
buff.putInt(0, index);
|
||||
//KMTQAITYPE_PHYSICALADAPTERDEVICEIDS
|
||||
if ((ret = query(handle, 31, buff)) < 0) {
|
||||
return ret;
|
||||
}
|
||||
deviceOut[0] = new PCIDeviceId(buff.getInt(4),buff.getInt(8),buff.getInt(12),buff.getInt(16),buff.getInt(20),buff.getInt(24));
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static int enumAdapters(Consumer<AdapterInfo> consumer) {
|
||||
if (D3DKMTEnumAdapters2 == 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int ret = 0;
|
||||
try (var stack = MemoryStack.stackPush()) {
|
||||
var query = stack.calloc(0x10).order(ByteOrder.nativeOrder());
|
||||
if ((ret = JNI.callPI(MemoryUtil.memAddress(query), D3DKMTEnumAdapters2)) < 0) {
|
||||
return ret;
|
||||
}
|
||||
int adapterCount = query.getInt(0);
|
||||
var adapterList = stack.calloc(0x14 * adapterCount).order(ByteOrder.nativeOrder());
|
||||
query.putLong(8, MemoryUtil.memAddress(adapterList));
|
||||
if ((ret = JNI.callPI(MemoryUtil.memAddress(query), D3DKMTEnumAdapters2)) < 0) {
|
||||
return ret;
|
||||
}
|
||||
adapterCount = query.getInt(0);
|
||||
for (int adapterIndex = 0; adapterIndex < adapterCount; adapterIndex++) {
|
||||
var adapter = adapterList.slice(adapterIndex*0x14, 0x14).order(ByteOrder.nativeOrder());
|
||||
//We only care about these 2
|
||||
int handle = adapter.getInt(0);
|
||||
long luid = adapter.getLong(4);
|
||||
|
||||
int[] type = new int[1];
|
||||
if ((ret = queryAdapterType(handle, type))<0) {
|
||||
Logger.error("Query type error: " + ret);
|
||||
//We errored
|
||||
if (closeHandle(handle) < 0) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
String[] icd = new String[1];
|
||||
if ((ret = queryAdapterIcd(handle, icd))<0) {
|
||||
Logger.error("Query icd error: " + ret);
|
||||
//We errored
|
||||
if (closeHandle(handle) < 0) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
PCIDeviceId[] out = new PCIDeviceId[1];
|
||||
//Get the root adapter device
|
||||
if ((ret = queryPCIAddress(handle, 0, out)) < 0) {
|
||||
Logger.error("Query pci error: " + ret);
|
||||
//We errored
|
||||
if (closeHandle(handle) < 0) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
int subSys = (out[0].subSystem<<16)|out[0].subVendor;//It seems the pci subsystem address is a joined integer
|
||||
consumer.accept(new AdapterInfo(icd[0], type[0], luid, out[0].vendor, out[0].device, subSys));
|
||||
|
||||
if (closeHandle(handle) < 0) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
//=======================================================================================
|
||||
|
||||
private static final int[] HDC_STUB = { 0x48, 0x83, 0xC1, 0x0C, 0x48, 0xB8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x1F, 0x48, 0x89, 0x01, 0x48, 0xB8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x2F, 0x51, 0xFF, 0xD0, 0x59, 0x8B, 0x41, 0x08, 0x89, 0x41, 0xFC, 0x48, 0x31, 0xC0, 0x89, 0x41, 0x08, 0xC3 };
|
||||
private static void insertLong(long l, byte[] out, int offset) {
|
||||
for (int i = 0; i < 8; i++) {
|
||||
out[i+offset] = (byte)(l & 0xFF);
|
||||
l >>= 8;
|
||||
}
|
||||
}
|
||||
private static byte[] createFinishedHDCStub(long luid) {
|
||||
byte[] stub = new byte[HDC_STUB.length];
|
||||
for (int i = 0; i < stub.length; i++) {
|
||||
stub[i] = (byte) HDC_STUB[i];
|
||||
}
|
||||
insertLong(luid, stub, 6);
|
||||
insertLong(D3DKMTOpenAdapterFromLuid, stub, 19);
|
||||
return stub;
|
||||
}
|
||||
|
||||
private static final long D3DKMTOpenAdapterFromLuid = apiGetFunctionAddressOptional(GDI32.getLibrary(), "D3DKMTOpenAdapterFromLuid");
|
||||
private static final long D3DKMTOpenAdapterFromHdc = apiGetFunctionAddressOptional(GDI32.getLibrary(), "D3DKMTOpenAdapterFromHdc");
|
||||
private static final long VirtualProtect = apiGetFunctionAddressOptional(Kernel32.getLibrary(), "VirtualProtect");
|
||||
|
||||
private static byte[] toByteArray(int... array) {
|
||||
byte[] res = new byte[array.length];
|
||||
for (int i = 0; i < array.length; i++) {
|
||||
res[i] = (byte) array[i];
|
||||
}
|
||||
return res;
|
||||
}
|
||||
private static void VirtualProtect(long addr, long size) {
|
||||
try (var stack = MemoryStack.stackPush()) {
|
||||
var oldProtection = stack.calloc(4);
|
||||
JNI.callPPPPI(addr, size, 0x40/*PAGE_EXECUTE_READWRITE*/, MemoryUtil.memAddress(oldProtection), VirtualProtect);
|
||||
}
|
||||
}
|
||||
private static void memcpy(long ptr, byte[] data) {
|
||||
for (int i = 0; i < data.length; i++) {
|
||||
MemoryUtil.memPutByte(ptr + i, data[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private static void installHDCStub(long adapterLuid) {
|
||||
if (D3DKMTOpenAdapterFromHdc == 0 || VirtualProtect == 0 || D3DKMTOpenAdapterFromLuid == 0) {
|
||||
return;
|
||||
}
|
||||
Logger.info("AdapterLuid callback at: " + Long.toHexString(D3DKMTOpenAdapterFromLuid));
|
||||
var stub = createFinishedHDCStub(adapterLuid);
|
||||
|
||||
VirtualProtect(D3DKMTOpenAdapterFromHdc, stub.length);
|
||||
memcpy(D3DKMTOpenAdapterFromHdc, stub);
|
||||
}
|
||||
|
||||
|
||||
private static byte[] createIntelStub(long origA, long origB, long jmpA, long jmpB) {
|
||||
byte[] stub = toByteArray(0xFE, 0x0D, 0x63, 0x00, 0x00, 0x00, 0x80, 0x3D, 0x5C, 0x00, 0x00, 0x00, 0x02, 0x75, 0x07, 0x48, 0x31, 0xC0, 0x48, 0xF7, 0xD0, 0xC3, 0x48, 0xB8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x48, 0x8B, 0x0D, 0x43, 0x00, 0x00, 0x00, 0x48, 0x89, 0x08, 0x48, 0x8B, 0x0D, 0x41, 0x00, 0x00, 0x00, 0x48, 0x89, 0x48, 0x08, 0x80, 0x3D, 0x2D, 0x00, 0x00, 0x00, 0x00, 0x74, 0x1D, 0x50, 0xFF, 0xD0, 0x58, 0x48, 0x8B, 0x0D, 0x31, 0x00, 0x00, 0x00, 0x48, 0x89, 0x08, 0x48, 0x8B, 0x0D, 0x2F, 0x00, 0x00, 0x00, 0x48, 0x89, 0x48, 0x08, 0x48, 0x31, 0xC0, 0xC3, 0x48, 0x8B, 0x41, 0x08, 0xC7, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x31, 0xC0, 0xC3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0);
|
||||
insertLong(D3DKMTQueryAdapterInfo, stub, 24);
|
||||
stub[0x69] = 3;
|
||||
insertLong(origA, stub, 0x6a);
|
||||
insertLong(origB, stub, 0x6a+8);
|
||||
insertLong(jmpA, stub, 0x6a+16);
|
||||
insertLong(jmpB, stub, 0x6a+24);
|
||||
return stub;
|
||||
}
|
||||
|
||||
private static byte[] createSimpleStub(long origA, long origB) {
|
||||
byte[] stub = toByteArray(0x48, 0xB8, 0, 0, 0, 0, 0, 0, 0, 0, 0x48, 0x8B, 0x0D, 0x15, 0x00, 0x00, 0x00, 0x48, 0x89, 0x08, 0x48, 0x8B, 0x0D, 0x13, 0x00, 0x00, 0x00, 0x48, 0x89, 0x48, 0x08, 0x48, 0x31, 0xC0, 0x48, 0xF7, 0xD0, 0xC3, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0);
|
||||
insertLong(D3DKMTQueryAdapterInfo, stub, 2);
|
||||
insertLong(origA, stub, 38);
|
||||
insertLong(origB, stub, 38+8);
|
||||
return stub;
|
||||
}
|
||||
|
||||
private static void installQueryStub(boolean installIntelBypass) {
|
||||
if (D3DKMTQueryAdapterInfo == 0 || VirtualProtect == 0) {
|
||||
return;
|
||||
}
|
||||
VirtualProtect(D3DKMTQueryAdapterInfo, 0x10);
|
||||
int MAX_STUB_SIZE = 1024;
|
||||
long stubPtr = MemoryUtil.nmemAlloc(MAX_STUB_SIZE);
|
||||
VirtualProtect(stubPtr, MAX_STUB_SIZE);
|
||||
Logger.info("Do stub at: " + Long.toHexString(stubPtr));
|
||||
|
||||
long origA = MemoryUtil.memGetLong(D3DKMTQueryAdapterInfo);
|
||||
long origB = MemoryUtil.memGetLong(D3DKMTQueryAdapterInfo+8);
|
||||
|
||||
var jmpStub = new byte[]{ 0x48, (byte) 0xB8, 0, 0, 0, 0, 0, 0, 0, 0, (byte) 0xFF, (byte) 0xE0};
|
||||
insertLong(stubPtr, jmpStub, 2);
|
||||
|
||||
memcpy(D3DKMTQueryAdapterInfo, jmpStub);
|
||||
Logger.info("D3DKMTQueryAdapterInfo at: " + Long.toHexString(D3DKMTQueryAdapterInfo));
|
||||
|
||||
long jmpA = MemoryUtil.memGetLong(D3DKMTQueryAdapterInfo);
|
||||
long jmpB = MemoryUtil.memGetLong(D3DKMTQueryAdapterInfo+8);
|
||||
|
||||
byte[] stub;
|
||||
if (installIntelBypass) {
|
||||
stub = createIntelStub(origA, origB, jmpA, jmpB);
|
||||
} else {
|
||||
stub = createSimpleStub(origA, origB);
|
||||
}
|
||||
memcpy(stubPtr, stub);
|
||||
Logger.info("QueryAdapterInfo stubs installed");
|
||||
}
|
||||
|
||||
|
||||
public static void doSelector(int index) {
|
||||
List<AdapterInfo> adapters = new ArrayList<>();
|
||||
//Must support rendering and must not be software renderer
|
||||
if (enumAdapters(adapter->{if ((adapter.type&5)==1) adapters.add(adapter); }) < 0) {
|
||||
return;
|
||||
}
|
||||
for (var adapter : adapters) {
|
||||
Logger.error(adapter.toString());
|
||||
}
|
||||
var adapter = adapters.get(index);
|
||||
|
||||
installHDCStub(adapter.luid);
|
||||
installQueryStub(adapter.icdPath.matches("\\\\ig[a-z0-9]+icd(32|64)\\.dll$"));
|
||||
|
||||
setPCIProperties(1/*USER*/, adapter.vendor, adapter.device, adapter.subSystem);
|
||||
setPCIProperties(2/*USER GLOBAL*/, adapter.vendor, adapter.device, adapter.subSystem);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package me.cortex.voxy.client;
|
||||
|
||||
import net.minecraft.world.chunk.WorldChunk;
|
||||
|
||||
public interface ICheekyClientChunkManager {
|
||||
WorldChunk voxy$cheekyGetChunk(int x, int z);
|
||||
}
|
||||
7
src/main/java/me/cortex/voxy/client/LoadException.java
Normal file
7
src/main/java/me/cortex/voxy/client/LoadException.java
Normal file
@@ -0,0 +1,7 @@
|
||||
package me.cortex.voxy.client;
|
||||
|
||||
public class LoadException extends RuntimeException {
|
||||
public LoadException(String msg, Throwable cause) {
|
||||
super(msg, cause);
|
||||
}
|
||||
}
|
||||
36
src/main/java/me/cortex/voxy/client/RenderStatistics.java
Normal file
36
src/main/java/me/cortex/voxy/client/RenderStatistics.java
Normal file
@@ -0,0 +1,36 @@
|
||||
package me.cortex.voxy.client;
|
||||
|
||||
import me.cortex.voxy.common.world.WorldEngine;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class RenderStatistics {
|
||||
public static boolean enabled = false;
|
||||
|
||||
public static final int[] hierarchicalTraversalCounts = new int[WorldEngine.MAX_LOD_LAYER+1];
|
||||
public static final int[] hierarchicalRenderSections = new int[WorldEngine.MAX_LOD_LAYER+1];
|
||||
public static final int[] visibleSections = new int[WorldEngine.MAX_LOD_LAYER+1];
|
||||
public static final int[] quadCount = new int[WorldEngine.MAX_LOD_LAYER+1];
|
||||
|
||||
|
||||
public static void addDebug(List<String> debug) {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
debug.add("HTC: [" + Arrays.stream(flipCopy(RenderStatistics.hierarchicalTraversalCounts)).mapToObj(Integer::toString).collect(Collectors.joining(", "))+"]");
|
||||
debug.add("HRS: [" + Arrays.stream(flipCopy(RenderStatistics.hierarchicalRenderSections)).mapToObj(Integer::toString).collect(Collectors.joining(", "))+"]");
|
||||
debug.add("VS: [" + Arrays.stream(flipCopy(RenderStatistics.visibleSections)).mapToObj(Integer::toString).collect(Collectors.joining(", "))+"]");
|
||||
debug.add("QC: [" + Arrays.stream(flipCopy(RenderStatistics.quadCount)).mapToObj(Integer::toString).collect(Collectors.joining(", "))+"]");
|
||||
}
|
||||
|
||||
private static int[] flipCopy(int[] array) {
|
||||
int[] ret = new int[array.length];
|
||||
int i = ret.length;
|
||||
for (int j : array) {
|
||||
ret[--i] = j;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
89
src/main/java/me/cortex/voxy/client/TimingStatistics.java
Normal file
89
src/main/java/me/cortex/voxy/client/TimingStatistics.java
Normal file
@@ -0,0 +1,89 @@
|
||||
package me.cortex.voxy.client;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class TimingStatistics {
|
||||
public static double ROLLING_WEIGHT = 0.96;
|
||||
private static final ArrayList<TimeSampler> allSamplers = new ArrayList<>();
|
||||
public static final class TimeSampler {
|
||||
private boolean running;
|
||||
private long timestamp;
|
||||
private long runtime;
|
||||
|
||||
private double rolling;
|
||||
|
||||
public TimeSampler() {
|
||||
TimingStatistics.allSamplers.add(this);
|
||||
}
|
||||
|
||||
private void reset() {
|
||||
if (this.running) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
this.runtime = 0;
|
||||
}
|
||||
|
||||
public void start() {
|
||||
if (this.running) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
this.running = true;
|
||||
this.timestamp = System.nanoTime();
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
if (!this.running) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
this.running = false;
|
||||
this.runtime += System.nanoTime() - this.timestamp;
|
||||
}
|
||||
|
||||
public void subtract(TimeSampler sampler) {
|
||||
this.runtime -= sampler.runtime;
|
||||
}
|
||||
|
||||
private void update() {
|
||||
double time = ((double) (this.runtime / 1000)) / 1000;
|
||||
this.rolling = Math.max(this.rolling * ROLLING_WEIGHT + time * (1-ROLLING_WEIGHT), time);
|
||||
}
|
||||
|
||||
public double getRolling() {
|
||||
return this.rolling;
|
||||
}
|
||||
|
||||
public String pVal() {
|
||||
return String.format("%6.3f", this.rolling);
|
||||
}
|
||||
}
|
||||
|
||||
public static void resetSamplers() {
|
||||
TimingStatistics.allSamplers.forEach(TimeSampler::reset);
|
||||
}
|
||||
|
||||
private static void updateSamplers() {
|
||||
TimingStatistics.allSamplers.forEach(TimeSampler::update);
|
||||
}
|
||||
|
||||
public static TimeSampler all = new TimeSampler();
|
||||
public static TimeSampler main = new TimeSampler();
|
||||
public static TimeSampler dynamic = new TimeSampler();
|
||||
public static TimeSampler postDynamic = new TimeSampler();
|
||||
|
||||
public static TimeSampler A = new TimeSampler();
|
||||
public static TimeSampler B = new TimeSampler();
|
||||
public static TimeSampler C = new TimeSampler();
|
||||
public static TimeSampler D = new TimeSampler();
|
||||
|
||||
public static TimeSampler E = new TimeSampler();
|
||||
public static TimeSampler F = new TimeSampler();
|
||||
public static TimeSampler G = new TimeSampler();
|
||||
public static TimeSampler H = new TimeSampler();
|
||||
public static TimeSampler I = new TimeSampler();
|
||||
|
||||
|
||||
public static void update() {
|
||||
updateSamplers();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package me.cortex.voxy.client;
|
||||
|
||||
import me.cortex.voxy.client.core.VoxelCore;
|
||||
import me.cortex.voxy.client.saver.ContextSelectionSystem;
|
||||
import me.cortex.voxy.client.terrain.WorldImportCommand;
|
||||
import me.cortex.voxy.common.config.Serialization;
|
||||
import me.cortex.voxy.common.storage.compressors.ZSTDCompressor;
|
||||
import me.cortex.voxy.common.storage.config.StorageConfig;
|
||||
import net.fabricmc.api.ClientModInitializer;
|
||||
import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.fabricmc.loader.api.ModContainer;
|
||||
import net.minecraft.client.world.ClientWorld;
|
||||
|
||||
public class Voxy implements ClientModInitializer {
|
||||
public static final String VERSION;
|
||||
|
||||
static {
|
||||
ModContainer mod = (ModContainer) FabricLoader.getInstance().getModContainer("voxy").orElseThrow(NullPointerException::new);
|
||||
VERSION = mod.getMetadata().getVersion().getFriendlyString();
|
||||
Serialization.init();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInitializeClient() {
|
||||
|
||||
ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> {
|
||||
dispatcher.register(WorldImportCommand.register());
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private static final ContextSelectionSystem selector = new ContextSelectionSystem();
|
||||
|
||||
public static VoxelCore createVoxelCore(ClientWorld world) {
|
||||
var selection = selector.getBestSelectionOrCreate(world);
|
||||
return new VoxelCore(selection);
|
||||
}
|
||||
}
|
||||
60
src/main/java/me/cortex/voxy/client/VoxyClient.java
Normal file
60
src/main/java/me/cortex/voxy/client/VoxyClient.java
Normal file
@@ -0,0 +1,60 @@
|
||||
package me.cortex.voxy.client;
|
||||
|
||||
import me.cortex.voxy.client.core.gl.Capabilities;
|
||||
import me.cortex.voxy.client.core.model.bakery.BudgetBufferRenderer;
|
||||
import me.cortex.voxy.client.core.rendering.util.SharedIndexBuffer;
|
||||
import me.cortex.voxy.common.Logger;
|
||||
import me.cortex.voxy.commonImpl.VoxyCommon;
|
||||
import net.fabricmc.api.ClientModInitializer;
|
||||
import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
public class VoxyClient implements ClientModInitializer {
|
||||
private static final HashSet<String> FREX = new HashSet<>();
|
||||
|
||||
|
||||
public static void initVoxyClient() {
|
||||
Capabilities.init();//Ensure clinit is called
|
||||
|
||||
boolean systemSupported = Capabilities.INSTANCE.compute && Capabilities.INSTANCE.indirectParameters;
|
||||
if (systemSupported) {
|
||||
|
||||
SharedIndexBuffer.INSTANCE.id();
|
||||
BudgetBufferRenderer.init();
|
||||
|
||||
VoxyCommon.setInstanceFactory(VoxyClientInstance::new);
|
||||
|
||||
if (!Capabilities.INSTANCE.subgroup) {
|
||||
Logger.warn("GPU does not support subgroup operations, expect some performance degradation");
|
||||
}
|
||||
|
||||
} else {
|
||||
Logger.error("Voxy is unsupported on your system.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInitializeClient() {
|
||||
ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> {
|
||||
if (VoxyCommon.isAvailable()) {
|
||||
dispatcher.register(VoxyCommands.register());
|
||||
}
|
||||
});
|
||||
|
||||
FabricLoader.getInstance()
|
||||
.getEntrypoints("frex_flawless_frames", Consumer.class)
|
||||
.forEach(api -> ((Consumer<Function<String,Consumer<Boolean>>>)api).accept(name->active->{if (active) {
|
||||
FREX.add(name);
|
||||
} else {
|
||||
FREX.remove(name);
|
||||
}}));
|
||||
}
|
||||
|
||||
public static boolean isFrexActive() {
|
||||
return !FREX.isEmpty();
|
||||
}
|
||||
}
|
||||
152
src/main/java/me/cortex/voxy/client/VoxyClientInstance.java
Normal file
152
src/main/java/me/cortex/voxy/client/VoxyClientInstance.java
Normal file
@@ -0,0 +1,152 @@
|
||||
package me.cortex.voxy.client;
|
||||
|
||||
import me.cortex.voxy.client.compat.FlashbackCompat;
|
||||
import me.cortex.voxy.client.config.VoxyConfig;
|
||||
import me.cortex.voxy.common.Logger;
|
||||
import me.cortex.voxy.common.config.ConfigBuildCtx;
|
||||
import me.cortex.voxy.common.config.Serialization;
|
||||
import me.cortex.voxy.common.config.compressors.ZSTDCompressor;
|
||||
import me.cortex.voxy.common.config.section.SectionSerializationStorage;
|
||||
import me.cortex.voxy.common.config.section.SectionStorage;
|
||||
import me.cortex.voxy.common.config.section.SectionStorageConfig;
|
||||
import me.cortex.voxy.common.config.storage.other.CompressionStorageAdaptor;
|
||||
import me.cortex.voxy.common.config.storage.rocksdb.RocksDBStorageBackend;
|
||||
import me.cortex.voxy.commonImpl.ImportManager;
|
||||
import me.cortex.voxy.commonImpl.VoxyInstance;
|
||||
import me.cortex.voxy.commonImpl.WorldIdentifier;
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
import net.minecraft.util.WorldSavePath;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
public class VoxyClientInstance extends VoxyInstance {
|
||||
public static boolean isInGame = false;
|
||||
|
||||
private final SectionStorageConfig storageConfig;
|
||||
private final Path basePath;
|
||||
private final boolean noIngestOverride;
|
||||
public VoxyClientInstance() {
|
||||
super(VoxyConfig.CONFIG.serviceThreads);
|
||||
var path = FlashbackCompat.getReplayStoragePath();
|
||||
this.noIngestOverride = path != null;
|
||||
if (path == null) {
|
||||
path = getBasePath();
|
||||
}
|
||||
this.basePath = path;
|
||||
this.storageConfig = getCreateStorageConfig(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ImportManager createImportManager() {
|
||||
return new ClientImportManager();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SectionStorage createStorage(WorldIdentifier identifier) {
|
||||
var ctx = new ConfigBuildCtx();
|
||||
ctx.setProperty(ConfigBuildCtx.BASE_SAVE_PATH, this.basePath.toString());
|
||||
ctx.setProperty(ConfigBuildCtx.WORLD_IDENTIFIER, identifier.getWorldId());
|
||||
ctx.pushPath(ConfigBuildCtx.DEFAULT_STORAGE_PATH);
|
||||
return this.storageConfig.build(ctx);
|
||||
}
|
||||
|
||||
public static SectionStorageConfig getCreateStorageConfig(Path path) {
|
||||
try {
|
||||
Files.createDirectories(path);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
var json = path.resolve("config.json");
|
||||
Config config = null;
|
||||
if (Files.exists(json)) {
|
||||
try {
|
||||
config = Serialization.GSON.fromJson(Files.readString(json), Config.class);
|
||||
if (config == null) {
|
||||
Logger.error("Config deserialization null, reverting to default");
|
||||
} else {
|
||||
if (config.sectionStorageConfig == null) {
|
||||
Logger.error("Config section storage null, reverting to default");
|
||||
config = null;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Logger.error("Failed to load the storage configuration file, resetting it to default, this will probably break your save if you used a custom storage config", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (config == null) {
|
||||
config = DEFAULT_STORAGE_CONFIG;
|
||||
}
|
||||
try {
|
||||
Files.writeString(json, Serialization.GSON.toJson(config));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed write the config, aborting!", e);
|
||||
}
|
||||
if (config == null) {
|
||||
throw new IllegalStateException("Config is still null\n");
|
||||
}
|
||||
return config.sectionStorageConfig;
|
||||
}
|
||||
|
||||
public Path getStorageBasePath() {
|
||||
return this.basePath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isIngestEnabled(WorldIdentifier worldId) {
|
||||
return !this.noIngestOverride;
|
||||
}
|
||||
|
||||
private static class Config {
|
||||
public int version = 1;
|
||||
public SectionStorageConfig sectionStorageConfig;
|
||||
}
|
||||
private static final Config DEFAULT_STORAGE_CONFIG;
|
||||
static {
|
||||
var config = new Config();
|
||||
|
||||
//Load the default config
|
||||
var baseDB = new RocksDBStorageBackend.Config();
|
||||
|
||||
var compressor = new ZSTDCompressor.Config();
|
||||
compressor.compressionLevel = 1;
|
||||
|
||||
var compression = new CompressionStorageAdaptor.Config();
|
||||
compression.delegate = baseDB;
|
||||
compression.compressor = compressor;
|
||||
|
||||
var serializer = new SectionSerializationStorage.Config();
|
||||
serializer.storage = compression;
|
||||
config.sectionStorageConfig = serializer;
|
||||
|
||||
DEFAULT_STORAGE_CONFIG = config;
|
||||
}
|
||||
|
||||
private static Path getBasePath() {
|
||||
Path basePath = MinecraftClient.getInstance().runDirectory.toPath().resolve(".voxy").resolve("saves");
|
||||
var iserver = MinecraftClient.getInstance().getServer();
|
||||
if (iserver != null) {
|
||||
basePath = iserver.getSavePath(WorldSavePath.ROOT).resolve("voxy");
|
||||
} else {
|
||||
var netHandle = MinecraftClient.getInstance().interactionManager;
|
||||
if (netHandle == null) {
|
||||
Logger.error("Network handle null");
|
||||
basePath = basePath.resolve("UNKNOWN");
|
||||
} else {
|
||||
var info = netHandle.networkHandler.getServerInfo();
|
||||
if (info == null) {
|
||||
Logger.error("Server info null");
|
||||
basePath = basePath.resolve("UNKNOWN");
|
||||
} else {
|
||||
if (info.isRealm()) {
|
||||
basePath = basePath.resolve("realms");
|
||||
} else {
|
||||
basePath = basePath.resolve(info.address.replace(":", "_"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return basePath.toAbsolutePath();
|
||||
}
|
||||
}
|
||||
247
src/main/java/me/cortex/voxy/client/VoxyCommands.java
Normal file
247
src/main/java/me/cortex/voxy/client/VoxyCommands.java
Normal file
@@ -0,0 +1,247 @@
|
||||
package me.cortex.voxy.client;
|
||||
|
||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import com.mojang.brigadier.suggestion.Suggestions;
|
||||
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
|
||||
import me.cortex.voxy.client.core.IGetVoxyRenderSystem;
|
||||
import me.cortex.voxy.commonImpl.VoxyCommon;
|
||||
import me.cortex.voxy.commonImpl.WorldIdentifier;
|
||||
import me.cortex.voxy.commonImpl.importers.DHImporter;
|
||||
import me.cortex.voxy.commonImpl.importers.WorldImporter;
|
||||
import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager;
|
||||
import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource;
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
import net.minecraft.command.CommandSource;
|
||||
import net.minecraft.text.PlainTextContent;
|
||||
import net.minecraft.text.Text;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
|
||||
public class VoxyCommands {
|
||||
|
||||
public static LiteralArgumentBuilder<FabricClientCommandSource> register() {
|
||||
var imports = ClientCommandManager.literal("import")
|
||||
.then(ClientCommandManager.literal("world")
|
||||
.then(ClientCommandManager.argument("world_name", StringArgumentType.string())
|
||||
.suggests(VoxyCommands::importWorldSuggester)
|
||||
.executes(VoxyCommands::importWorld)))
|
||||
.then(ClientCommandManager.literal("bobby")
|
||||
.then(ClientCommandManager.argument("world_name", StringArgumentType.string())
|
||||
.suggests(VoxyCommands::importBobbySuggester)
|
||||
.executes(VoxyCommands::importBobby)))
|
||||
.then(ClientCommandManager.literal("raw")
|
||||
.then(ClientCommandManager.argument("path", StringArgumentType.string())
|
||||
.executes(VoxyCommands::importRaw)))
|
||||
.then(ClientCommandManager.literal("zip")
|
||||
.then(ClientCommandManager.argument("zipPath", StringArgumentType.string())
|
||||
.executes(VoxyCommands::importZip)
|
||||
.then(ClientCommandManager.argument("innerPath", StringArgumentType.string())
|
||||
.executes(VoxyCommands::importZip))))
|
||||
.then(ClientCommandManager.literal("cancel")
|
||||
.executes(VoxyCommands::cancelImport));
|
||||
|
||||
if (DHImporter.HasRequiredLibraries) {
|
||||
imports = imports
|
||||
.then(ClientCommandManager.literal("distant_horizons")
|
||||
.then(ClientCommandManager.argument("sqlDbPath", StringArgumentType.string())
|
||||
.executes(VoxyCommands::importDistantHorizons)));
|
||||
}
|
||||
|
||||
return ClientCommandManager.literal("voxy")//.requires((ctx)-> VoxyCommon.getInstance() != null)
|
||||
.then(ClientCommandManager.literal("reload")
|
||||
.executes(VoxyCommands::reloadInstance))
|
||||
.then(imports);
|
||||
}
|
||||
|
||||
private static int reloadInstance(CommandContext<FabricClientCommandSource> ctx) {
|
||||
var instance = (VoxyClientInstance)VoxyCommon.getInstance();
|
||||
if (instance == null) {
|
||||
ctx.getSource().sendError(Text.translatable("Voxy must be enabled in settings to use this"));
|
||||
return 1;
|
||||
}
|
||||
var wr = MinecraftClient.getInstance().worldRenderer;
|
||||
if (wr!=null) {
|
||||
((IGetVoxyRenderSystem)wr).shutdownRenderer();
|
||||
}
|
||||
|
||||
VoxyCommon.shutdownInstance();
|
||||
VoxyCommon.createInstance();
|
||||
if (wr!=null) {
|
||||
((IGetVoxyRenderSystem)wr).createRenderer();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
private static int importDistantHorizons(CommandContext<FabricClientCommandSource> ctx) {
|
||||
var instance = (VoxyClientInstance)VoxyCommon.getInstance();
|
||||
if (instance == null) {
|
||||
ctx.getSource().sendError(Text.translatable("Voxy must be enabled in settings to use this"));
|
||||
return 1;
|
||||
}
|
||||
var dbFile = new File(ctx.getArgument("sqlDbPath", String.class));
|
||||
if (!dbFile.exists()) {
|
||||
return 1;
|
||||
}
|
||||
if (dbFile.isDirectory()) {
|
||||
dbFile = dbFile.toPath().resolve("DistantHorizons.sqlite").toFile();
|
||||
if (!dbFile.exists()) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
File dbFile_ = dbFile;
|
||||
var engine = WorldIdentifier.ofEngine(MinecraftClient.getInstance().world);
|
||||
if (engine==null)return 1;
|
||||
return instance.getImportManager().makeAndRunIfNone(engine, ()->
|
||||
new DHImporter(dbFile_, engine, MinecraftClient.getInstance().world, instance.getThreadPool(), instance.savingServiceRateLimiter))?0:1;
|
||||
}
|
||||
|
||||
private static boolean fileBasedImporter(File directory) {
|
||||
var instance = (VoxyClientInstance)VoxyCommon.getInstance();
|
||||
if (instance == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var engine = WorldIdentifier.ofEngine(MinecraftClient.getInstance().world);
|
||||
if (engine==null) return false;
|
||||
return instance.getImportManager().makeAndRunIfNone(engine, ()->{
|
||||
var importer = new WorldImporter(engine, MinecraftClient.getInstance().world, instance.getThreadPool(), instance.savingServiceRateLimiter);
|
||||
importer.importRegionDirectoryAsync(directory);
|
||||
return importer;
|
||||
});
|
||||
}
|
||||
|
||||
private static int importRaw(CommandContext<FabricClientCommandSource> ctx) {
|
||||
if (VoxyCommon.getInstance() == null) {
|
||||
ctx.getSource().sendError(Text.translatable("Voxy must be enabled in settings to use this"));
|
||||
return 1;
|
||||
}
|
||||
|
||||
return fileBasedImporter(new File(ctx.getArgument("path", String.class)))?0:1;
|
||||
}
|
||||
|
||||
private static int importBobby(CommandContext<FabricClientCommandSource> ctx) {
|
||||
if (VoxyCommon.getInstance() == null) {
|
||||
ctx.getSource().sendError(Text.translatable("Voxy must be enabled in settings to use this"));
|
||||
return 1;
|
||||
}
|
||||
|
||||
var file = new File(".bobby").toPath().resolve(ctx.getArgument("world_name", String.class)).toFile();
|
||||
return fileBasedImporter(file)?0:1;
|
||||
}
|
||||
|
||||
private static CompletableFuture<Suggestions> importWorldSuggester(CommandContext<FabricClientCommandSource> ctx, SuggestionsBuilder sb) {
|
||||
return fileDirectorySuggester(MinecraftClient.getInstance().runDirectory.toPath().resolve("saves"), sb);
|
||||
}
|
||||
private static CompletableFuture<Suggestions> importBobbySuggester(CommandContext<FabricClientCommandSource> ctx, SuggestionsBuilder sb) {
|
||||
return fileDirectorySuggester(MinecraftClient.getInstance().runDirectory.toPath().resolve(".bobby"), sb);
|
||||
}
|
||||
|
||||
private static CompletableFuture<Suggestions> fileDirectorySuggester(Path dir, SuggestionsBuilder sb) {
|
||||
var str = sb.getRemaining().replace("\\\\", "\\").replace("\\", "/");
|
||||
if (str.startsWith("\"")) {
|
||||
str = str.substring(1);
|
||||
}
|
||||
if (str.endsWith("\"")) {
|
||||
str = str.substring(0,str.length()-1);
|
||||
}
|
||||
var remaining = str;
|
||||
if (str.contains("/")) {
|
||||
int idx = str.lastIndexOf('/');
|
||||
remaining = str.substring(idx+1);
|
||||
try {
|
||||
dir = dir.resolve(str.substring(0, idx));
|
||||
} catch (Exception e) {
|
||||
return Suggestions.empty();
|
||||
}
|
||||
str = str.substring(0, idx+1);
|
||||
} else {
|
||||
str = "";
|
||||
}
|
||||
|
||||
try {
|
||||
var worlds = Files.list(dir).toList();
|
||||
for (var world : worlds) {
|
||||
if (!world.toFile().isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
var wn = world.getFileName().toString();
|
||||
if (wn.equals(remaining)) {
|
||||
continue;
|
||||
}
|
||||
if (CommandSource.shouldSuggest(remaining, wn) || CommandSource.shouldSuggest(remaining, '"'+wn)) {
|
||||
wn = str+wn + "/";
|
||||
sb.suggest(StringArgumentType.escapeIfRequired(wn));
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {}
|
||||
|
||||
return sb.buildFuture();
|
||||
}
|
||||
|
||||
private static int importWorld(CommandContext<FabricClientCommandSource> ctx) {
|
||||
if (VoxyCommon.getInstance() == null) {
|
||||
ctx.getSource().sendError(Text.translatable("Voxy must be enabled in settings to use this"));
|
||||
return 1;
|
||||
}
|
||||
|
||||
var name = ctx.getArgument("world_name", String.class);
|
||||
var file = new File("saves").toPath().resolve(name);
|
||||
name = name.toLowerCase();
|
||||
if (name.endsWith("/")) {
|
||||
name = name.substring(0, name.length()-1);
|
||||
}
|
||||
if (!(name.endsWith("region"))) {
|
||||
file = file.resolve("region");
|
||||
}
|
||||
return fileBasedImporter(file.toFile())?0:1;
|
||||
}
|
||||
|
||||
private static int importZip(CommandContext<FabricClientCommandSource> ctx) {
|
||||
var zip = new File(ctx.getArgument("zipPath", String.class));
|
||||
var innerDir = "region/";
|
||||
try {
|
||||
innerDir = ctx.getArgument("innerPath", String.class);
|
||||
} catch (Exception e) {}
|
||||
|
||||
var instance = (VoxyClientInstance)VoxyCommon.getInstance();
|
||||
if (instance == null) {
|
||||
ctx.getSource().sendError(Text.translatable("Voxy must be enabled in settings to use this"));
|
||||
return 1;
|
||||
}
|
||||
String finalInnerDir = innerDir;
|
||||
|
||||
var engine = WorldIdentifier.ofEngine(MinecraftClient.getInstance().world);
|
||||
if (engine != null) {
|
||||
return instance.getImportManager().makeAndRunIfNone(engine, () -> {
|
||||
var importer = new WorldImporter(engine, MinecraftClient.getInstance().world, instance.getThreadPool(), instance.savingServiceRateLimiter);
|
||||
importer.importZippedRegionDirectoryAsync(zip, finalInnerDir);
|
||||
return importer;
|
||||
}) ? 0 : 1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int cancelImport(CommandContext<FabricClientCommandSource> ctx) {
|
||||
var instance = (VoxyClientInstance)VoxyCommon.getInstance();
|
||||
if (instance == null) {
|
||||
ctx.getSource().sendError(Text.translatable("Voxy must be enabled in settings to use this"));
|
||||
return 1;
|
||||
}
|
||||
var world = WorldIdentifier.ofEngineNullable(MinecraftClient.getInstance().world);
|
||||
if (world != null) {
|
||||
return instance.getImportManager().cancelImport(world)?0:1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package me.cortex.voxy.client.compat;
|
||||
|
||||
import com.moulberry.flashback.Flashback;
|
||||
import com.moulberry.flashback.playback.ReplayServer;
|
||||
import com.moulberry.flashback.record.FlashbackMeta;
|
||||
import me.cortex.voxy.common.Logger;
|
||||
import me.cortex.voxy.common.config.section.SectionStorageConfig;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import org.apache.commons.logging.Log;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
public class FlashbackCompat {
|
||||
public static final boolean FLASHBACK_INSTALLED = FabricLoader.getInstance().isModLoaded("flashback");
|
||||
|
||||
public static Path getReplayStoragePath() {
|
||||
if (!FLASHBACK_INSTALLED) {
|
||||
return null;
|
||||
}
|
||||
return getReplayStoragePath0();
|
||||
}
|
||||
|
||||
private static Path getReplayStoragePath0() {
|
||||
ReplayServer replayServer = Flashback.getReplayServer();
|
||||
if (replayServer != null) {
|
||||
FlashbackMeta meta = replayServer.getMetadata();
|
||||
if (meta != null) {
|
||||
var path = ((IFlashbackMeta)meta).getVoxyPath();
|
||||
if (path != null) {
|
||||
Logger.info("Flashback replay server exists and meta exists");
|
||||
if (path.exists()) {
|
||||
Logger.info("Flashback voxy path exists in filesystem, using this as lod data source");
|
||||
return path.toPath();
|
||||
} else {
|
||||
Logger.warn("Flashback meta had voxy path saved but path doesnt exist");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package me.cortex.voxy.client.compat;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public interface IFlashbackMeta {
|
||||
void setVoxyPath(File path);
|
||||
File getVoxyPath();
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package me.cortex.voxy.client.config;
|
||||
|
||||
import net.minecraft.block.Block;
|
||||
import net.minecraft.block.Blocks;
|
||||
|
||||
public class BlockConfig {
|
||||
public Block block = Blocks.AIR;
|
||||
public boolean ignoreBlock = false;
|
||||
public final Face[] faces = new Face[6];
|
||||
public float colourMultiplier = 1.0f;
|
||||
|
||||
public BlockConfig() {
|
||||
for (int i = 0; i < this.faces.length; i++) {
|
||||
this.faces[i] = new Face();
|
||||
}
|
||||
}
|
||||
|
||||
public static class Face {
|
||||
public BooleanChoice occludes = BooleanChoice.DEFAULT;
|
||||
public BooleanChoice canBeOccluded = BooleanChoice.DEFAULT;
|
||||
}
|
||||
|
||||
public enum BooleanChoice {
|
||||
DEFAULT,
|
||||
TRUE,
|
||||
FALSE
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package me.cortex.voxy.client.config;
|
||||
|
||||
import com.terraformersmc.modmenu.api.ConfigScreenFactory;
|
||||
import com.terraformersmc.modmenu.api.ModMenuApi;
|
||||
import me.cortex.voxy.common.Logger;
|
||||
import me.cortex.voxy.commonImpl.VoxyCommon;
|
||||
import net.caffeinemc.mods.sodium.client.gui.SodiumOptionsGUI;
|
||||
|
||||
public class ModMenuIntegration implements ModMenuApi {
|
||||
@Override
|
||||
public ConfigScreenFactory<?> getModConfigScreenFactory() {
|
||||
return parent -> {
|
||||
if (VoxyCommon.isAvailable()) {
|
||||
var screen = (SodiumOptionsGUI) SodiumOptionsGUI.createScreen(parent);
|
||||
//Sorry jelly and douira, please dont hurt me
|
||||
try {
|
||||
//We cant use .setPage() as that invokes rebuildGui, however the screen hasnt been initalized yet
|
||||
// causing things to crash
|
||||
var field = SodiumOptionsGUI.class.getDeclaredField("currentPage");
|
||||
field.setAccessible(true);
|
||||
field.set(screen, VoxyConfigScreenPages.voxyOptionPage);
|
||||
field.setAccessible(false);
|
||||
} catch (Exception e) {
|
||||
Logger.error("Failed to set the current page to voxy", e);
|
||||
}
|
||||
return screen;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,11 @@ package me.cortex.voxy.client.config;
|
||||
import com.google.gson.FieldNamingPolicy;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import me.cortex.voxy.client.core.Capabilities;
|
||||
import me.cortex.voxy.client.saver.ContextSelectionSystem;
|
||||
import me.cortex.voxy.common.Logger;
|
||||
import me.cortex.voxy.common.util.cpu.CpuLayout;
|
||||
import me.cortex.voxy.commonImpl.VoxyCommon;
|
||||
import net.caffeinemc.mods.sodium.client.gui.options.storage.OptionStorage;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import org.lwjgl.opengl.GL;
|
||||
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
@@ -14,53 +15,57 @@ import java.lang.reflect.Modifier;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
public class VoxyConfig {
|
||||
public class VoxyConfig implements OptionStorage<VoxyConfig> {
|
||||
private static final Gson GSON = new GsonBuilder()
|
||||
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
|
||||
.setPrettyPrinting()
|
||||
.excludeFieldsWithModifiers(Modifier.PRIVATE)
|
||||
.create();
|
||||
|
||||
public static VoxyConfig CONFIG = loadOrCreate();
|
||||
|
||||
public boolean enabled = true;
|
||||
public boolean enableRendering = true;
|
||||
public boolean ingestEnabled = true;
|
||||
public int qualityScale = 12;
|
||||
public int maxSections = 200_000;
|
||||
public int renderDistance = 128;
|
||||
public int geometryBufferSize = (1<<30)/8;
|
||||
public int ingestThreads = 2;
|
||||
public int savingThreads = 4;
|
||||
public int renderThreads = 5;
|
||||
public boolean useMeshShaderIfPossible = true;
|
||||
public String defaultSaveConfig;
|
||||
public int sectionRenderDistance = 16;
|
||||
public int serviceThreads = (int) Math.max(CpuLayout.CORES.length/1.5, 1);
|
||||
public float subDivisionSize = 64;
|
||||
public boolean renderVanillaFog = false;
|
||||
public boolean useEnvironmentalFog = false;
|
||||
public boolean renderStatistics = false;
|
||||
|
||||
|
||||
public static VoxyConfig loadOrCreate() {
|
||||
var path = getConfigPath();
|
||||
if (Files.exists(path)) {
|
||||
try (FileReader reader = new FileReader(path.toFile())) {
|
||||
var cfg = GSON.fromJson(reader, VoxyConfig.class);
|
||||
if (cfg.defaultSaveConfig == null) {
|
||||
//Shitty gson being a pain TODO: replace with a proper fix
|
||||
cfg.defaultSaveConfig = ContextSelectionSystem.DEFAULT_STORAGE_CONFIG;
|
||||
private static VoxyConfig loadOrCreate() {
|
||||
if (VoxyCommon.isAvailable()) {
|
||||
var path = getConfigPath();
|
||||
if (Files.exists(path)) {
|
||||
try (FileReader reader = new FileReader(path.toFile())) {
|
||||
var conf = GSON.fromJson(reader, VoxyConfig.class);
|
||||
if (conf != null) {
|
||||
conf.save();
|
||||
return conf;
|
||||
} else {
|
||||
Logger.error("Failed to load voxy config, resetting");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Logger.error("Could not parse config", e);
|
||||
}
|
||||
return cfg;
|
||||
} catch (IOException e) {
|
||||
System.err.println("Could not parse config");
|
||||
e.printStackTrace();
|
||||
}
|
||||
var config = new VoxyConfig();
|
||||
config.save();
|
||||
return config;
|
||||
} else {
|
||||
var config = new VoxyConfig();
|
||||
config.enabled = false;
|
||||
config.enableRendering = false;
|
||||
return config;
|
||||
}
|
||||
var config = new VoxyConfig();
|
||||
config.defaultSaveConfig = ContextSelectionSystem.DEFAULT_STORAGE_CONFIG;
|
||||
return config;
|
||||
}
|
||||
|
||||
public void save() {
|
||||
//Unsafe, todo: fixme! needs to be atomic!
|
||||
try {
|
||||
Files.writeString(getConfigPath(), GSON.toJson(this));
|
||||
} catch (IOException e) {
|
||||
System.err.println("Failed to write config file");
|
||||
e.printStackTrace();
|
||||
Logger.error("Failed to write config file", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +75,12 @@ public class VoxyConfig {
|
||||
.resolve("voxy-config.json");
|
||||
}
|
||||
|
||||
public boolean useMeshShaders() {
|
||||
return this.useMeshShaderIfPossible && Capabilities.INSTANCE.meshShaders;
|
||||
@Override
|
||||
public VoxyConfig getData() {
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean isRenderingEnabled() {
|
||||
return VoxyCommon.isAvailable() && this.enabled && this.enableRendering;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
package me.cortex.voxy.client.config;
|
||||
|
||||
import com.terraformersmc.modmenu.api.ConfigScreenFactory;
|
||||
import com.terraformersmc.modmenu.api.ModMenuApi;
|
||||
import me.cortex.voxy.client.core.IGetVoxelCore;
|
||||
import me.shedaniel.clothconfig2.ClothConfigDemo;
|
||||
import me.shedaniel.clothconfig2.api.ConfigBuilder;
|
||||
import me.shedaniel.clothconfig2.api.ConfigCategory;
|
||||
import me.shedaniel.clothconfig2.api.ConfigEntryBuilder;
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
import net.minecraft.client.gui.screen.Screen;
|
||||
import net.minecraft.text.Text;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class VoxyConfigScreenFactory implements ModMenuApi {
|
||||
private static VoxyConfig DEFAULT;
|
||||
@Override
|
||||
public ConfigScreenFactory<?> getModConfigScreenFactory() {
|
||||
return parent -> buildConfigScreen(parent, VoxyConfig.CONFIG);
|
||||
}
|
||||
|
||||
private static Screen buildConfigScreen(Screen parent, VoxyConfig config) {
|
||||
if (DEFAULT == null) {
|
||||
DEFAULT = new VoxyConfig();
|
||||
}
|
||||
ConfigBuilder builder = ConfigBuilder.create()
|
||||
.setParentScreen(parent)
|
||||
.setTitle(Text.translatable("voxy.config.title"));
|
||||
|
||||
|
||||
addGeneralCategory(builder, config);
|
||||
addThreadsCategory(builder, config);
|
||||
addStorageCategory(builder, config);
|
||||
|
||||
builder.setSavingRunnable(() -> {
|
||||
//After saving the core should be reloaded/reset
|
||||
var world = (IGetVoxelCore)MinecraftClient.getInstance().worldRenderer;
|
||||
if (world != null) {
|
||||
world.reloadVoxelCore();
|
||||
}
|
||||
VoxyConfig.CONFIG.save();
|
||||
});
|
||||
|
||||
return builder.build();//
|
||||
}
|
||||
|
||||
private static void addGeneralCategory(ConfigBuilder builder, VoxyConfig config) {
|
||||
|
||||
|
||||
ConfigCategory category = builder.getOrCreateCategory(Text.translatable("voxy.config.general"));
|
||||
ConfigEntryBuilder entryBuilder = builder.entryBuilder();
|
||||
|
||||
/*
|
||||
category.addEntry(entryBuilder.startSubCategory(Text.translatable("aaa"), List.of(entryBuilder.startBooleanToggle(Text.translatable("voxy.config.general.enabled"), config.enabled)
|
||||
.setTooltip(Text.translatable("voxy.config.general.enabled.tooltip"))
|
||||
.setSaveConsumer(val -> config.enabled = val)
|
||||
.setDefaultValue(DEFAULT.enabled)
|
||||
.build(), entryBuilder.startSubCategory(Text.translatable("bbb"), List.of(entryBuilder.startIntSlider(Text.translatable("voxy.config.general.geometryBuffer"), config.geometryBufferSize, (1<<27)/8, ((1<<31)-1)/8)
|
||||
.setTooltip(Text.translatable("voxy.config.general.geometryBuffer.tooltip"))
|
||||
.setSaveConsumer(val -> config.geometryBufferSize = val)
|
||||
.setDefaultValue(DEFAULT.geometryBufferSize)
|
||||
.build())).build()
|
||||
)).build());
|
||||
*/
|
||||
|
||||
category.addEntry(entryBuilder.startBooleanToggle(Text.translatable("voxy.config.general.enabled"), config.enabled)
|
||||
.setTooltip(Text.translatable("voxy.config.general.enabled.tooltip"))
|
||||
.setSaveConsumer(val -> config.enabled = val)
|
||||
.setDefaultValue(DEFAULT.enabled)
|
||||
.build());
|
||||
|
||||
category.addEntry(entryBuilder.startBooleanToggle(Text.translatable("voxy.config.general.ingest"), config.ingestEnabled)
|
||||
.setTooltip(Text.translatable("voxy.config.general.ingest.tooltip"))
|
||||
.setSaveConsumer(val -> config.ingestEnabled = val)
|
||||
.setDefaultValue(DEFAULT.ingestEnabled)
|
||||
.build());
|
||||
|
||||
category.addEntry(entryBuilder.startIntSlider(Text.translatable("voxy.config.general.quality"), config.qualityScale, 8, 32)
|
||||
.setTooltip(Text.translatable("voxy.config.general.quality.tooltip"))
|
||||
.setSaveConsumer(val -> config.qualityScale = val)
|
||||
.setDefaultValue(DEFAULT.qualityScale)
|
||||
.build());
|
||||
|
||||
category.addEntry(entryBuilder.startIntSlider(Text.translatable("voxy.config.general.geometryBuffer"), config.geometryBufferSize, (1<<27)/8, ((1<<31)-1)/8)
|
||||
.setTooltip(Text.translatable("voxy.config.general.geometryBuffer.tooltip"))
|
||||
.setSaveConsumer(val -> config.geometryBufferSize = val)
|
||||
.setDefaultValue(DEFAULT.geometryBufferSize)
|
||||
.build());
|
||||
|
||||
category.addEntry(entryBuilder.startIntSlider(Text.translatable("voxy.config.general.maxSections"), config.maxSections, 100_000, 400_000)
|
||||
.setTooltip(Text.translatable("voxy.config.general.maxSections.tooltip"))
|
||||
.setSaveConsumer(val -> config.maxSections = val)
|
||||
.setDefaultValue(DEFAULT.maxSections)
|
||||
.build());
|
||||
|
||||
category.addEntry(entryBuilder.startIntField(Text.translatable("voxy.config.general.renderDistance"), config.renderDistance)
|
||||
.setTooltip(Text.translatable("voxy.config.general.renderDistance.tooltip"))
|
||||
.setSaveConsumer(val -> config.renderDistance = val)
|
||||
.setDefaultValue(DEFAULT.renderDistance)
|
||||
.build());
|
||||
|
||||
category.addEntry(entryBuilder.startBooleanToggle(Text.translatable("voxy.config.general.nvmesh"), config.useMeshShaderIfPossible)
|
||||
.setTooltip(Text.translatable("voxy.config.general.nvmesh.tooltip"))
|
||||
.setSaveConsumer(val -> config.useMeshShaderIfPossible = val)
|
||||
.setDefaultValue(DEFAULT.useMeshShaderIfPossible)
|
||||
.build());
|
||||
|
||||
//category.addEntry(entryBuilder.startIntSlider(Text.translatable("voxy.config.general.compression"), config.savingCompressionLevel, 1, 21)
|
||||
// .setTooltip(Text.translatable("voxy.config.general.compression.tooltip"))
|
||||
// .setSaveConsumer(val -> config.savingCompressionLevel = val)
|
||||
// .setDefaultValue(DEFAULT.savingCompressionLevel)
|
||||
// .build());
|
||||
}
|
||||
|
||||
private static void addThreadsCategory(ConfigBuilder builder, VoxyConfig config) {
|
||||
ConfigCategory category = builder.getOrCreateCategory(Text.translatable("voxy.config.threads"));
|
||||
ConfigEntryBuilder entryBuilder = builder.entryBuilder();
|
||||
|
||||
category.addEntry(entryBuilder.startIntSlider(Text.translatable("voxy.config.threads.ingest"), config.ingestThreads, 1, Runtime.getRuntime().availableProcessors())
|
||||
.setTooltip(Text.translatable("voxy.config.threads.ingest.tooltip"))
|
||||
.setSaveConsumer(val -> config.ingestThreads = val)
|
||||
.setDefaultValue(DEFAULT.ingestThreads)
|
||||
.build());
|
||||
|
||||
category.addEntry(entryBuilder.startIntSlider(Text.translatable("voxy.config.threads.saving"), config.savingThreads, 1, Runtime.getRuntime().availableProcessors())
|
||||
.setTooltip(Text.translatable("voxy.config.threads.saving.tooltip"))
|
||||
.setSaveConsumer(val -> config.savingThreads = val)
|
||||
.setDefaultValue(DEFAULT.savingThreads)
|
||||
.build());
|
||||
|
||||
category.addEntry(entryBuilder.startIntSlider(Text.translatable("voxy.config.threads.render"), config.renderThreads, 1, Runtime.getRuntime().availableProcessors())
|
||||
.setTooltip(Text.translatable("voxy.config.threads.render.tooltip"))
|
||||
.setSaveConsumer(val -> config.renderThreads = val)
|
||||
.setDefaultValue(DEFAULT.renderThreads)
|
||||
.build());
|
||||
}
|
||||
|
||||
private static void addStorageCategory(ConfigBuilder builder, VoxyConfig config) {
|
||||
ConfigCategory category = builder.getOrCreateCategory(Text.translatable("voxy.config.storage"));
|
||||
ConfigEntryBuilder entryBuilder = builder.entryBuilder();
|
||||
|
||||
////Temporary until i figure out how to do more complex multi layer configuration for storage
|
||||
//category.addEntry(entryBuilder.startStrField(Text.translatable("voxy.config.storage.path"), config.storagePath)
|
||||
// .setTooltip(Text.translatable("voxy.config.storage.path.tooltip"))
|
||||
// .setSaveConsumer(val -> config.storagePath = val)
|
||||
// .setDefaultValue(DEFAULT.storagePath)
|
||||
// .build());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
package me.cortex.voxy.client.config;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import me.cortex.voxy.client.RenderStatistics;
|
||||
import me.cortex.voxy.client.VoxyClientInstance;
|
||||
import me.cortex.voxy.client.core.IGetVoxyRenderSystem;
|
||||
import me.cortex.voxy.common.util.cpu.CpuLayout;
|
||||
import me.cortex.voxy.commonImpl.VoxyCommon;
|
||||
import net.caffeinemc.mods.sodium.client.gui.options.*;
|
||||
import net.caffeinemc.mods.sodium.client.gui.options.control.SliderControl;
|
||||
import net.caffeinemc.mods.sodium.client.gui.options.control.TickBoxControl;
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
import net.minecraft.text.Text;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public abstract class VoxyConfigScreenPages {
|
||||
private VoxyConfigScreenPages(){}
|
||||
|
||||
public static OptionPage voxyOptionPage = null;
|
||||
|
||||
public static OptionPage page() {
|
||||
List<OptionGroup> groups = new ArrayList<>();
|
||||
VoxyConfig storage = VoxyConfig.CONFIG;
|
||||
|
||||
//General
|
||||
groups.add(OptionGroup.createBuilder()
|
||||
.add(OptionImpl.createBuilder(boolean.class, storage)
|
||||
.setName(Text.translatable("voxy.config.general.enabled"))
|
||||
.setTooltip(Text.translatable("voxy.config.general.enabled.tooltip"))
|
||||
.setControl(TickBoxControl::new)
|
||||
.setBinding((s, v)->{
|
||||
s.enabled = v;
|
||||
if (v) {
|
||||
if (VoxyClientInstance.isInGame) {
|
||||
VoxyCommon.createInstance();
|
||||
var vrsh = (IGetVoxyRenderSystem) MinecraftClient.getInstance().worldRenderer;
|
||||
if (vrsh != null && s.enableRendering) {
|
||||
vrsh.createRenderer();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var vrsh = (IGetVoxyRenderSystem) MinecraftClient.getInstance().worldRenderer;
|
||||
if (vrsh != null) {
|
||||
vrsh.shutdownRenderer();
|
||||
}
|
||||
VoxyCommon.shutdownInstance();
|
||||
}
|
||||
}, s -> s.enabled)
|
||||
.build()
|
||||
).add(OptionImpl.createBuilder(int.class, storage)
|
||||
.setName(Text.translatable("voxy.config.general.serviceThreads"))
|
||||
.setTooltip(Text.translatable("voxy.config.general.serviceThreads.tooltip"))
|
||||
.setControl(opt->new SliderControl(opt, 1,
|
||||
CpuLayout.CORES.length, //Just do core size as max
|
||||
//Runtime.getRuntime().availableProcessors(),//Note: this is threads not cores, the default value is half the core count, is fine as this should technically be the limit but CpuLayout.CORES.length is more realistic
|
||||
1, v->Text.literal(Integer.toString(v))))
|
||||
.setBinding((s, v)->{
|
||||
boolean wasEnabled = VoxyCommon.getInstance() != null;
|
||||
var vrsh = (IGetVoxyRenderSystem) MinecraftClient.getInstance().worldRenderer;
|
||||
if (wasEnabled) {
|
||||
if (vrsh != null) {
|
||||
vrsh.shutdownRenderer();
|
||||
}
|
||||
VoxyCommon.shutdownInstance();
|
||||
}
|
||||
|
||||
s.serviceThreads = v;
|
||||
|
||||
if (wasEnabled) {
|
||||
VoxyCommon.createInstance();
|
||||
}
|
||||
}, s -> s.serviceThreads)
|
||||
.setImpact(OptionImpact.HIGH)
|
||||
.setFlags(OptionFlag.REQUIRES_RENDERER_RELOAD)
|
||||
.build()
|
||||
).add(OptionImpl.createBuilder(boolean.class, storage)
|
||||
.setName(Text.translatable("voxy.config.general.ingest"))
|
||||
.setTooltip(Text.translatable("voxy.config.general.ingest.tooltip"))
|
||||
.setControl(TickBoxControl::new)
|
||||
.setBinding((s, v) -> s.ingestEnabled = v, s -> s.ingestEnabled)
|
||||
.setImpact(OptionImpact.MEDIUM)
|
||||
.build()
|
||||
).build()
|
||||
);
|
||||
|
||||
groups.add(OptionGroup.createBuilder()
|
||||
.add(OptionImpl.createBuilder(boolean.class, storage)
|
||||
.setName(Text.translatable("voxy.config.general.rendering"))
|
||||
.setTooltip(Text.translatable("voxy.config.general.rendering.tooltip"))
|
||||
.setControl(TickBoxControl::new)
|
||||
.setBinding((s, v)->{
|
||||
s.enableRendering = v;
|
||||
var vrsh = (IGetVoxyRenderSystem)MinecraftClient.getInstance().worldRenderer;
|
||||
if (vrsh != null) {
|
||||
if (v) {
|
||||
vrsh.createRenderer();
|
||||
} else {
|
||||
vrsh.shutdownRenderer();
|
||||
}
|
||||
}
|
||||
}, s -> s.enableRendering)
|
||||
.setImpact(OptionImpact.HIGH)
|
||||
.build()
|
||||
).add(OptionImpl.createBuilder(int.class, storage)
|
||||
.setName(Text.translatable("voxy.config.general.subDivisionSize"))
|
||||
.setTooltip(Text.translatable("voxy.config.general.subDivisionSize.tooltip"))
|
||||
.setControl(opt->new SliderControl(opt, 0, SUBDIV_IN_MAX, 1, v->Text.literal(Integer.toString(Math.round(ln2subDiv(v))))))
|
||||
.setBinding((s, v) -> s.subDivisionSize = ln2subDiv(v), s -> subDiv2ln(s.subDivisionSize))
|
||||
.setImpact(OptionImpact.HIGH)
|
||||
.build()
|
||||
).add(OptionImpl.createBuilder(int.class, storage)
|
||||
.setName(Text.translatable("voxy.config.general.renderDistance"))
|
||||
.setTooltip(Text.translatable("voxy.config.general.renderDistance.tooltip"))
|
||||
.setControl(opt->new SliderControl(opt, 2, 64, 1, v->Text.literal(Integer.toString(v * 32))))//Every unit is equal to 32 vanilla chunks
|
||||
.setBinding((s, v)-> {
|
||||
s.sectionRenderDistance = v;
|
||||
var vrsh = (IGetVoxyRenderSystem)MinecraftClient.getInstance().worldRenderer;
|
||||
if (vrsh != null) {
|
||||
var vrs = vrsh.getVoxyRenderSystem();
|
||||
if (vrs != null) {
|
||||
vrs.setRenderDistance(v);
|
||||
}
|
||||
}
|
||||
}, s -> s.sectionRenderDistance)
|
||||
.setImpact(OptionImpact.LOW)
|
||||
.build()
|
||||
).add(OptionImpl.createBuilder(boolean.class, storage)
|
||||
.setName(Text.translatable("voxy.config.general.environmental_fog"))
|
||||
.setTooltip(Text.translatable("voxy.config.general.environmental_fog.tooltip"))
|
||||
.setControl(TickBoxControl::new)
|
||||
.setImpact(OptionImpact.VARIES)
|
||||
.setBinding((s, v)-> s.useEnvironmentalFog = v, s -> s.useEnvironmentalFog)
|
||||
.setFlags(OptionFlag.REQUIRES_RENDERER_RELOAD)
|
||||
.build()
|
||||
).add(OptionImpl.createBuilder(boolean.class, storage)
|
||||
.setName(Text.translatable("voxy.config.general.vanilla_fog"))
|
||||
.setTooltip(Text.translatable("voxy.config.general.vanilla_fog.tooltip"))
|
||||
.setControl(TickBoxControl::new)
|
||||
.setBinding((s, v)-> s.renderVanillaFog = v, s -> s.renderVanillaFog)
|
||||
.build()
|
||||
).add(OptionImpl.createBuilder(boolean.class, storage)
|
||||
.setName(Text.translatable("voxy.config.general.render_statistics"))
|
||||
.setTooltip(Text.translatable("voxy.config.general.render_statistics.tooltip"))
|
||||
.setControl(TickBoxControl::new)
|
||||
.setBinding((s, v)-> RenderStatistics.enabled = v, s -> RenderStatistics.enabled)
|
||||
.setFlags(OptionFlag.REQUIRES_RENDERER_RELOAD)
|
||||
.build()
|
||||
).build()
|
||||
);
|
||||
return new OptionPage(Text.translatable("voxy.config.title"), ImmutableList.copyOf(groups));
|
||||
}
|
||||
|
||||
private static final int SUBDIV_IN_MAX = 100;
|
||||
private static final double SUBDIV_MIN = 28;
|
||||
private static final double SUBDIV_MAX = 256;
|
||||
private static final double SUBDIV_CONST = Math.log(SUBDIV_MAX/SUBDIV_MIN)/Math.log(2);
|
||||
|
||||
|
||||
//In range is 0->200
|
||||
//Out range is 28->256
|
||||
private static float ln2subDiv(int in) {
|
||||
return (float) (SUBDIV_MIN*Math.pow(2, SUBDIV_CONST*((double)in/SUBDIV_IN_MAX)));
|
||||
}
|
||||
|
||||
//In range is ... any?
|
||||
//Out range is 0->200
|
||||
private static int subDiv2ln(float in) {
|
||||
return (int) (((Math.log(((double)in)/SUBDIV_MIN)/Math.log(2))/SUBDIV_CONST)*SUBDIV_IN_MAX);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package me.cortex.voxy.client.config.screens;
|
||||
|
||||
import me.cortex.voxy.client.config.BlockConfig;
|
||||
import me.shedaniel.clothconfig2.api.AbstractConfigEntry;
|
||||
import me.shedaniel.clothconfig2.api.AbstractConfigListEntry;
|
||||
import me.shedaniel.clothconfig2.api.ConfigEntryBuilder;
|
||||
import net.minecraft.block.Blocks;
|
||||
import net.minecraft.text.Text;
|
||||
|
||||
public class BlockConfigScreen {
|
||||
private static final ConfigEntryBuilder ENTRY_BUILDER = ConfigEntryBuilder.create();
|
||||
public static AbstractConfigListEntry<BlockConfig> makeScreen(BlockConfig config) {
|
||||
var entry = ENTRY_BUILDER.startSubCategory(config.block.getName());
|
||||
entry.add(UtilityScreens.makeBlockSelectionScreen(Text.literal("a"), Blocks.AIR, null));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package me.cortex.voxy.client.config.screens;
|
||||
|
||||
import me.shedaniel.clothconfig2.api.AbstractConfigListEntry;
|
||||
|
||||
public class StorageConfigScreen {
|
||||
public static AbstractConfigListEntry makeScreen() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package me.cortex.voxy.client.config.screens;
|
||||
|
||||
import me.cortex.voxy.client.config.BlockConfig;
|
||||
import me.shedaniel.clothconfig2.api.AbstractConfigEntry;
|
||||
import me.shedaniel.clothconfig2.api.AbstractConfigListEntry;
|
||||
import me.shedaniel.clothconfig2.api.ConfigEntryBuilder;
|
||||
import me.shedaniel.clothconfig2.impl.builders.DropdownMenuBuilder;
|
||||
import net.minecraft.block.Block;
|
||||
import net.minecraft.block.Blocks;
|
||||
import net.minecraft.registry.Registries;
|
||||
import net.minecraft.text.Text;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class UtilityScreens {
|
||||
private static final ConfigEntryBuilder ENTRY_BUILDER = ConfigEntryBuilder.create();
|
||||
public static AbstractConfigListEntry<Block> makeBlockSelectionScreen(Text name, Block selectedBlock, Consumer<Block> onSave) {
|
||||
return makeBlockSelectionScreen(name, Blocks.AIR, selectedBlock, onSave);
|
||||
}
|
||||
public static AbstractConfigListEntry<Block> makeBlockSelectionScreen(Text name, Block defaultBlock, Block selectedBlock, Consumer<Block> onSave) {
|
||||
return ENTRY_BUILDER.startDropdownMenu(name,
|
||||
DropdownMenuBuilder.TopCellElementBuilder.ofBlockObject(selectedBlock),
|
||||
DropdownMenuBuilder.CellCreatorBuilder.ofBlockObject())
|
||||
.setDefaultValue(defaultBlock)
|
||||
.setSelections(Registries.BLOCK.stream().toList())
|
||||
.setSaveConsumer(onSave)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
package me.cortex.voxy.client.core;
|
||||
|
||||
import me.cortex.voxy.client.RenderStatistics;
|
||||
import me.cortex.voxy.client.TimingStatistics;
|
||||
import me.cortex.voxy.client.core.gl.Capabilities;
|
||||
import me.cortex.voxy.client.core.model.ModelBakerySubsystem;
|
||||
import me.cortex.voxy.client.core.rendering.Viewport;
|
||||
import me.cortex.voxy.client.core.rendering.hierachical.AsyncNodeManager;
|
||||
import me.cortex.voxy.client.core.rendering.hierachical.HierarchicalOcclusionTraverser;
|
||||
import me.cortex.voxy.client.core.rendering.hierachical.NodeCleaner;
|
||||
import me.cortex.voxy.client.core.rendering.post.FullscreenBlit;
|
||||
import me.cortex.voxy.client.core.rendering.section.AbstractSectionRenderer;
|
||||
import me.cortex.voxy.client.core.rendering.util.DownloadStream;
|
||||
import me.cortex.voxy.common.util.TrackedObject;
|
||||
import org.joml.Matrix4f;
|
||||
import org.lwjgl.opengl.GL30;
|
||||
import org.lwjgl.system.MemoryUtil;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.BooleanSupplier;
|
||||
|
||||
import static org.lwjgl.opengl.GL11C.GL_ALWAYS;
|
||||
import static org.lwjgl.opengl.GL11C.GL_DEPTH_TEST;
|
||||
import static org.lwjgl.opengl.GL11C.GL_EQUAL;
|
||||
import static org.lwjgl.opengl.GL11C.GL_KEEP;
|
||||
import static org.lwjgl.opengl.GL11C.GL_NEAREST;
|
||||
import static org.lwjgl.opengl.GL11C.GL_REPLACE;
|
||||
import static org.lwjgl.opengl.GL11C.GL_STENCIL_TEST;
|
||||
import static org.lwjgl.opengl.GL11C.glColorMask;
|
||||
import static org.lwjgl.opengl.GL11C.glDisable;
|
||||
import static org.lwjgl.opengl.GL11C.glEnable;
|
||||
import static org.lwjgl.opengl.GL11C.glStencilFunc;
|
||||
import static org.lwjgl.opengl.GL11C.glStencilMask;
|
||||
import static org.lwjgl.opengl.GL11C.glStencilOp;
|
||||
import static org.lwjgl.opengl.GL30C.GL_FRAMEBUFFER;
|
||||
import static org.lwjgl.opengl.GL30C.glBindFramebuffer;
|
||||
import static org.lwjgl.opengl.GL42.GL_DEPTH_BUFFER_BIT;
|
||||
import static org.lwjgl.opengl.GL42.GL_LEQUAL;
|
||||
import static org.lwjgl.opengl.GL42.GL_NOTEQUAL;
|
||||
import static org.lwjgl.opengl.GL42.glDepthFunc;
|
||||
import static org.lwjgl.opengl.GL42.*;
|
||||
import static org.lwjgl.opengl.GL45.glClearNamedFramebufferfi;
|
||||
import static org.lwjgl.opengl.GL45C.glBindTextureUnit;
|
||||
import static org.lwjgl.opengl.GL45C.glBlitNamedFramebuffer;
|
||||
|
||||
public abstract class AbstractRenderPipeline extends TrackedObject {
|
||||
private final BooleanSupplier frexStillHasWork;
|
||||
|
||||
private final AsyncNodeManager nodeManager;
|
||||
private final NodeCleaner nodeCleaner;
|
||||
private final HierarchicalOcclusionTraverser traversal;
|
||||
|
||||
protected AbstractSectionRenderer<?,?> sectionRenderer;
|
||||
|
||||
private final FullscreenBlit depthMaskBlit = new FullscreenBlit("voxy:post/fullscreen2.vert", "voxy:post/noop.frag");
|
||||
private final FullscreenBlit depthSetBlit = new FullscreenBlit("voxy:post/fullscreen2.vert", "voxy:post/depth0.frag");
|
||||
protected AbstractRenderPipeline(AsyncNodeManager nodeManager, NodeCleaner nodeCleaner, HierarchicalOcclusionTraverser traversal, BooleanSupplier frexSupplier) {
|
||||
this.frexStillHasWork = frexSupplier;
|
||||
this.nodeManager = nodeManager;
|
||||
this.nodeCleaner = nodeCleaner;
|
||||
this.traversal = traversal;
|
||||
}
|
||||
|
||||
//Allows pipelines to configure model baking system
|
||||
public void setupExtraModelBakeryData(ModelBakerySubsystem modelService) {}
|
||||
|
||||
public final void setSectionRenderer(AbstractSectionRenderer<?,?> sectionRenderer) {//Stupid java ordering not allowing something pre super
|
||||
if (this.sectionRenderer != null) throw new IllegalStateException();
|
||||
this.sectionRenderer = sectionRenderer;
|
||||
}
|
||||
|
||||
//Called before the pipeline starts running, used to update uniforms etc
|
||||
public void preSetup(Viewport<?> viewport) {
|
||||
|
||||
}
|
||||
|
||||
protected abstract int setup(Viewport<?> viewport, int sourceFramebuffer, int srcWidth, int srcHeight);
|
||||
protected abstract void postOpaquePreTranslucent(Viewport<?> viewport);
|
||||
protected void finish(Viewport<?> viewport, int sourceFrameBuffer, int srcWidth, int srcHeight) {
|
||||
glDisable(GL_STENCIL_TEST);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, sourceFrameBuffer);
|
||||
}
|
||||
|
||||
public void runPipeline(Viewport<?> viewport, int sourceFrameBuffer, int srcWidth, int srcHeight) {
|
||||
int depthTexture = this.setup(viewport, sourceFrameBuffer, srcWidth, srcHeight);
|
||||
|
||||
var rs = ((AbstractSectionRenderer)this.sectionRenderer);
|
||||
rs.renderOpaque(viewport);
|
||||
this.innerPrimaryWork(viewport, depthTexture);
|
||||
rs.buildDrawCalls(viewport);
|
||||
rs.renderTemporal(viewport);
|
||||
|
||||
this.postOpaquePreTranslucent(viewport);
|
||||
|
||||
rs.renderTranslucent(viewport);
|
||||
|
||||
this.finish(viewport, sourceFrameBuffer, srcWidth, srcHeight);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, sourceFrameBuffer);
|
||||
}
|
||||
|
||||
protected void initDepthStencil(int sourceFrameBuffer, int targetFb, int srcWidth, int srcHeight, int width, int height) {
|
||||
glClearNamedFramebufferfi(targetFb, GL_DEPTH_STENCIL, 0, 1.0f, 1);
|
||||
glBlitNamedFramebuffer(sourceFrameBuffer, targetFb, 0,0, srcWidth, srcHeight, 0,0, width, height, GL_DEPTH_BUFFER_BIT, GL_NEAREST);
|
||||
|
||||
glBindFramebuffer(GL30.GL_FRAMEBUFFER, targetFb);
|
||||
|
||||
/*
|
||||
if (Capabilities.INSTANCE.isMesa){
|
||||
glClearStencil(1);
|
||||
glClear(GL_STENCIL_BUFFER_BIT);
|
||||
}*/
|
||||
|
||||
//This whole thing is hell, we basicly want to create a mask stenicel/depth mask specificiclly
|
||||
// in theory we could do this in a single pass by passing in the depth buffer from the sourceFrambuffer
|
||||
// but the current implmentation does a 2 pass system
|
||||
glEnable(GL_STENCIL_TEST);
|
||||
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
|
||||
glStencilFunc(GL_ALWAYS, 0, 0xFF);
|
||||
glStencilMask(0xFF);
|
||||
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
glDepthFunc(GL_NOTEQUAL);//If != 1 pass
|
||||
glColorMask(false,false,false,false);
|
||||
//We do here
|
||||
this.depthMaskBlit.blit();
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
|
||||
//Blit depth 0 where stencil is 0
|
||||
glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
|
||||
glStencilFunc(GL_EQUAL, 0, 0xFF);
|
||||
|
||||
this.depthSetBlit.blit();
|
||||
|
||||
glDepthFunc(GL_LEQUAL);
|
||||
glColorMask(true,true,true,true);
|
||||
|
||||
//Make voxy terrain render only where there isnt mc terrain
|
||||
glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
|
||||
glStencilFunc(GL_EQUAL, 1, 0xFF);
|
||||
}
|
||||
|
||||
private static final long SCRATCH = MemoryUtil.nmemAlloc(4*4*4);
|
||||
protected static void transformBlitDepth(FullscreenBlit blitShader, int srcDepthTex, int dstFB, Viewport<?> viewport, Matrix4f targetTransform) {
|
||||
glBindFramebuffer(GL30.GL_FRAMEBUFFER, dstFB);
|
||||
|
||||
blitShader.bind();
|
||||
glBindTextureUnit(0, srcDepthTex);
|
||||
new Matrix4f(viewport.MVP).invert().getToAddress(SCRATCH);
|
||||
nglUniformMatrix4fv(1, 1, false, SCRATCH);//inverse fromProjection
|
||||
targetTransform.getToAddress(SCRATCH);//new Matrix4f(tooProjection).mul(vp.modelView).get(data);
|
||||
nglUniformMatrix4fv(2, 1, false, SCRATCH);//tooProjection
|
||||
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
//We keep the stencil test on with the emitting, only to where non terrain is rendered
|
||||
blitShader.blit();
|
||||
glDisable(GL_STENCIL_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
}
|
||||
|
||||
protected void innerPrimaryWork(Viewport<?> viewport, int depthBuffer) {
|
||||
|
||||
//Compute the mip chain
|
||||
viewport.hiZBuffer.buildMipChain(depthBuffer, viewport.width, viewport.height);
|
||||
|
||||
do {
|
||||
TimingStatistics.main.stop();
|
||||
TimingStatistics.dynamic.start();
|
||||
|
||||
TimingStatistics.D.start();
|
||||
//Tick download stream
|
||||
DownloadStream.INSTANCE.tick();
|
||||
TimingStatistics.D.stop();
|
||||
|
||||
this.nodeManager.tick(this.traversal.getNodeBuffer(), this.nodeCleaner);
|
||||
//glFlush();
|
||||
|
||||
this.nodeCleaner.tick(this.traversal.getNodeBuffer());//Probably do this here??
|
||||
|
||||
TimingStatistics.dynamic.stop();
|
||||
TimingStatistics.main.start();
|
||||
|
||||
glMemoryBarrier(GL_FRAMEBUFFER_BARRIER_BIT | GL_PIXEL_BUFFER_BARRIER_BIT);
|
||||
|
||||
TimingStatistics.I.start();
|
||||
this.traversal.doTraversal(viewport);
|
||||
TimingStatistics.I.stop();
|
||||
} while (this.frexStillHasWork.getAsBoolean());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void free0() {
|
||||
this.sectionRenderer.free();
|
||||
this.depthMaskBlit.delete();
|
||||
this.depthSetBlit.delete();
|
||||
super.free0();
|
||||
}
|
||||
|
||||
public void addDebug(List<String> debug) {
|
||||
this.sectionRenderer.addDebug(debug);
|
||||
RenderStatistics.addDebug(debug);
|
||||
}
|
||||
|
||||
//Binds the framebuffer and any other bindings needed for rendering
|
||||
public abstract void setupAndBindOpaque(Viewport<?> viewport);
|
||||
public abstract void setupAndBindTranslucent(Viewport<?> viewport);
|
||||
|
||||
|
||||
public void bindUniforms() {
|
||||
this.bindUniforms(-1);
|
||||
}
|
||||
|
||||
public void bindUniforms(int index) {
|
||||
}
|
||||
|
||||
//null means no function, otherwise return the taa injection function
|
||||
public String taaFunction(String functionName) {
|
||||
return this.taaFunction(-1, functionName);
|
||||
}
|
||||
|
||||
public String taaFunction(int uboBindingPoint, String functionName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
//null means dont transform the shader
|
||||
public String patchOpaqueShader(AbstractSectionRenderer<?,?> renderer, String input) {
|
||||
return null;
|
||||
}
|
||||
|
||||
//Returning null means apply the same patch as the opaque
|
||||
public String patchTranslucentShader(AbstractSectionRenderer<?,?> renderer, String input) {
|
||||
return null;
|
||||
}
|
||||
|
||||
//Null means no scaling factor
|
||||
public float[] getRenderScalingFactor() {return null;}
|
||||
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
package me.cortex.voxy.client.core;
|
||||
|
||||
import me.cortex.voxy.client.core.gl.shader.Shader;
|
||||
import me.cortex.voxy.client.core.gl.shader.ShaderType;
|
||||
import org.lwjgl.opengl.GL;
|
||||
import org.lwjgl.opengl.GL20C;
|
||||
|
||||
public class Capabilities {
|
||||
|
||||
public static final Capabilities INSTANCE = new Capabilities();
|
||||
|
||||
public final boolean meshShaders;
|
||||
public final boolean INT64_t;
|
||||
public Capabilities() {
|
||||
var cap = GL.getCapabilities();
|
||||
this.meshShaders = cap.GL_NV_mesh_shader && cap.GL_NV_representative_fragment_test;
|
||||
//this.INT64_t = cap.GL_ARB_gpu_shader_int64 || cap.GL_AMD_gpu_shader_int64;
|
||||
//The only reliable way to test for int64 support is to try compile a shader
|
||||
this.INT64_t = testShaderCompilesOk(ShaderType.COMPUTE, """
|
||||
#version 430
|
||||
#extension GL_ARB_gpu_shader_int64 : require
|
||||
layout(local_size_x=32) in;
|
||||
void main() {
|
||||
uint64_t a = 1234;
|
||||
}
|
||||
""");
|
||||
}
|
||||
|
||||
public static void init() {
|
||||
}
|
||||
|
||||
private static boolean testShaderCompilesOk(ShaderType type, String src) {
|
||||
int shader = GL20C.glCreateShader(type.gl);
|
||||
GL20C.glShaderSource(shader, src);
|
||||
GL20C.glCompileShader(shader);
|
||||
int result = GL20C.glGetShaderi(shader, GL20C.GL_COMPILE_STATUS);
|
||||
GL20C.glDeleteShader(shader);
|
||||
|
||||
return result == GL20C.GL_TRUE;
|
||||
}
|
||||
}
|
||||
@@ -1,460 +0,0 @@
|
||||
package me.cortex.voxy.client.core;
|
||||
|
||||
//Contains the logic to determine what is loaded and at what LoD level, dispatches render changes
|
||||
// also determines what faces are built etc
|
||||
|
||||
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
|
||||
import me.cortex.voxy.client.core.rendering.RenderTracker;
|
||||
import me.cortex.voxy.client.core.util.RingUtil;
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
|
||||
//Can use ring logic
|
||||
// i.e. when a player moves the rings of each lod change (how it was doing in the original attempt)
|
||||
// also have it do directional quad culling and rebuild the chunk if needed (this shouldent happen very often) (the reason is to significantly reduce draw calls)
|
||||
// make the rebuild range like +-5 chunks along each axis (that means at higher levels, should only need to rebuild like)
|
||||
// 4 sections or something
|
||||
public class DistanceTracker {
|
||||
private final TransitionRing2D[] loDRings;
|
||||
private final TransitionRing2D[] cacheLoadRings;
|
||||
private final TransitionRing2D[] cacheUnloadRings;
|
||||
private final TransitionRing2D mostOuterNonClampedRing;
|
||||
private final RenderTracker tracker;
|
||||
private final int minYSection;
|
||||
private final int maxYSection;
|
||||
private final int renderDistance;
|
||||
|
||||
public DistanceTracker(RenderTracker tracker, int[] lodRingScales, int renderDistance, int minY, int maxY) {
|
||||
this.loDRings = new TransitionRing2D[lodRingScales.length];
|
||||
this.cacheLoadRings = new TransitionRing2D[lodRingScales.length];
|
||||
this.cacheUnloadRings = new TransitionRing2D[lodRingScales.length];
|
||||
this.tracker = tracker;
|
||||
this.minYSection = minY;
|
||||
this.maxYSection = maxY;
|
||||
this.renderDistance = renderDistance;
|
||||
|
||||
|
||||
boolean wasRdClamped = false;
|
||||
//The rings 0+ start at 64 vanilla rd, no matter what the game is set at, that is if the game is set to 32 rd
|
||||
// there will still be 32 chunks untill the first lod drop
|
||||
// if the game is set to 16, then there will be 48 chunks until the drop
|
||||
for (int i = 0; i < this.loDRings.length; i++) {
|
||||
int scaleP = lodRingScales[i];
|
||||
boolean isTerminatingRing = ((lodRingScales[i]+2)<<(1+i) >= renderDistance)&&renderDistance>0;
|
||||
if (isTerminatingRing) {
|
||||
scaleP = Math.max(renderDistance >> (1+i), 1);
|
||||
wasRdClamped = true;
|
||||
}
|
||||
int scale = scaleP;
|
||||
|
||||
//TODO: FIXME: check that the level shift is right when inc/dec
|
||||
int capRing = i;
|
||||
this.loDRings[i] = new TransitionRing2D((isTerminatingRing?5:6)+i, isTerminatingRing?scale<<1:scale, (x, z) -> {
|
||||
if (isTerminatingRing) {
|
||||
add(capRing, x, z);
|
||||
} else
|
||||
this.dec(capRing+1, x, z);
|
||||
}, (x, z) -> {
|
||||
if (isTerminatingRing) {
|
||||
remove(capRing, x, z);
|
||||
//remove(capRing, (x<<1), (z<<1));
|
||||
} else
|
||||
this.inc(capRing+1, x, z);
|
||||
});
|
||||
|
||||
//TODO:FIXME i think the radius is wrong and (lodRingScales[i]) needs to be (lodRingScales[i]<<1) since the transition ring (the thing above)
|
||||
// acts on LoD level + 1
|
||||
|
||||
//TODO: check this is actually working lmao and make it generate parent level lods on the exit instead of entry so it looks correct when flying backwards
|
||||
if (!isTerminatingRing) {
|
||||
//TODO: COMPLETLY REDO THE CACHING SYSTEM CAUSE THE LOGIC IS COMPLETLY INCORRECT
|
||||
// we want basicly 2 rings offset by an amount such that when a position is near an lod transition point
|
||||
// it will be meshed (both the higher and lower quality lods), enabling semless loading
|
||||
// the issue is when to uncache these methods
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
//TODO: FIX AND FINISH!!!
|
||||
this.cacheLoadRings[i] = new TransitionRing2D(5 + i, (scale << 1) + 2, (x, z) -> {
|
||||
//When entering a cache ring, trigger a mesh op and inject into cache
|
||||
for (int y = this.minYSection >> capRing; y <= this.maxYSection >> capRing; y++) {
|
||||
this.tracker.addCache(capRing, x, y, z);
|
||||
}
|
||||
}, (x, z) -> {
|
||||
for (int y = this.minYSection >> capRing; y <= this.maxYSection >> capRing; y++) {
|
||||
this.tracker.removeCache(capRing, x, y, z);
|
||||
}
|
||||
});
|
||||
|
||||
this.cacheUnloadRings[i] = new TransitionRing2D(5 + i, Math.max(1, (scale << 1) - 2), (x, z) -> {
|
||||
for (int y = this.minYSection >> capRing; y <= this.maxYSection >> capRing; y++) {
|
||||
this.tracker.removeCache(capRing, x, y, z);
|
||||
}
|
||||
}, (x, z) -> {
|
||||
});
|
||||
*/
|
||||
}
|
||||
|
||||
if (isTerminatingRing) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!wasRdClamped) {
|
||||
this.mostOuterNonClampedRing = new TransitionRing2D(5+this.loDRings.length, Math.max(renderDistance, 2048)>>this.loDRings.length, (x,z)->
|
||||
add(this.loDRings.length, x, z), (x,z)->{
|
||||
if (renderDistance > 0) {
|
||||
remove(this.loDRings.length,x,z);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.mostOuterNonClampedRing = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void inc(int lvl, int x, int z) {
|
||||
for (int y = this.minYSection>>lvl; y <= this.maxYSection>>lvl; y++) {
|
||||
this.tracker.inc(lvl, x, y, z);
|
||||
}
|
||||
}
|
||||
|
||||
private void dec(int lvl, int x, int z) {
|
||||
for (int y = this.minYSection>>lvl; y <= this.maxYSection>>lvl; y++) {
|
||||
this.tracker.dec(lvl, x, y, z);
|
||||
}
|
||||
}
|
||||
|
||||
private void add(int lvl, int x, int z) {
|
||||
for (int y = this.minYSection>>lvl; y <= this.maxYSection>>lvl; y++) {
|
||||
this.tracker.add(lvl, x, y, z);
|
||||
}
|
||||
}
|
||||
|
||||
private void remove(int lvl, int x, int z) {
|
||||
for (int y = this.minYSection>>lvl; y <= this.maxYSection>>lvl; y++) {
|
||||
this.tracker.remove(lvl, x, y, z);
|
||||
this.tracker.removeCache(lvl, x, y, z);
|
||||
}
|
||||
}
|
||||
|
||||
//How it works is there are N ring zones (one zone for each lod boundary)
|
||||
// the transition zone is what determines what lods are rendered etc (and it biases higher lod levels cause its easier)
|
||||
// the transition zone is only ever checked when the player moves 1<<(4+lodlvl) blocks, its position is set
|
||||
|
||||
//if the center suddenly changes (say more than 1<<(7+lodlvl) block) then invalidate the entire ring and recompute
|
||||
// the lod sections
|
||||
public void setCenter(int x, int y, int z) {
|
||||
for (var ring : this.cacheLoadRings) {
|
||||
if (ring!=null)
|
||||
ring.update(x, z);
|
||||
}
|
||||
if (this.mostOuterNonClampedRing!=null)
|
||||
this.mostOuterNonClampedRing.update(x, z);
|
||||
|
||||
//Update in reverse order (biggest lod to smallest lod)
|
||||
for (int i = this.loDRings.length-1; -1<i; i-- ) {
|
||||
var ring = this.loDRings[i];
|
||||
if (ring != null)
|
||||
ring.update(x, z);
|
||||
}
|
||||
for (var ring : this.cacheUnloadRings) {
|
||||
if (ring!=null)
|
||||
ring.update(x, z);
|
||||
}
|
||||
}
|
||||
|
||||
public void init(int x, int z) {
|
||||
for (var ring : this.cacheLoadRings) {
|
||||
if (ring != null)
|
||||
ring.setCenter(x, z);
|
||||
}
|
||||
|
||||
for (var ring : this.cacheUnloadRings) {
|
||||
if (ring != null)
|
||||
ring.setCenter(x, z);
|
||||
}
|
||||
|
||||
for (var ring : this.loDRings) {
|
||||
if (ring != null)
|
||||
ring.setCenter(x, z);
|
||||
}
|
||||
if (this.mostOuterNonClampedRing!=null)
|
||||
this.mostOuterNonClampedRing.setCenter(x, z);
|
||||
|
||||
var thread = new Thread(()-> {
|
||||
for (var ring : this.cacheLoadRings) {
|
||||
if (ring != null)
|
||||
ring.fill(x, z);
|
||||
}
|
||||
|
||||
for (var ring : this.cacheUnloadRings) {
|
||||
if (ring != null)
|
||||
ring.fill(x, z);
|
||||
}
|
||||
|
||||
//This is an ungodly terrible hack to make the lods load in a semi ok order
|
||||
for (var ring : this.loDRings)
|
||||
if (ring != null)
|
||||
ring.fill(x, z);
|
||||
|
||||
if (this.mostOuterNonClampedRing!=null)
|
||||
this.mostOuterNonClampedRing.fill(x, z);
|
||||
|
||||
for (int i = this.loDRings.length - 1; 0 <= i; i--) {
|
||||
if (this.loDRings[i] != null) {
|
||||
this.loDRings[i].fill(x, z);
|
||||
}
|
||||
}
|
||||
});
|
||||
thread.setName("LoD Ring Initializer");
|
||||
thread.start();
|
||||
//TODO: FIXME: need to destory on shutdown
|
||||
}
|
||||
|
||||
|
||||
//TODO: add a new class thing that can track the central axis point so that
|
||||
// geometry can be rebuilt with new flags with correct facing geometry built
|
||||
// (could also make it so that it emits 3x the amount of draw calls, but that seems very bad idea)
|
||||
|
||||
|
||||
private interface Transition2DCallback {
|
||||
void callback(int x, int z);
|
||||
}
|
||||
private static final class TransitionRing2D {
|
||||
private final int triggerRangeSquared;
|
||||
private final int shiftSize;
|
||||
private final Transition2DCallback enter;
|
||||
private final Transition2DCallback exit;
|
||||
private final int[] cornerPoints;
|
||||
private final int radius;
|
||||
|
||||
private int lastUpdateX;
|
||||
private int lastUpdateZ;
|
||||
|
||||
private int currentX;
|
||||
private int currentZ;
|
||||
|
||||
//Note radius is in shiftScale
|
||||
private TransitionRing2D(int shiftSize, int radius, Transition2DCallback onEntry, Transition2DCallback onExit) {
|
||||
this(shiftSize, radius, onEntry, onExit, 0, 0, 0);
|
||||
}
|
||||
private TransitionRing2D(int shiftSize, int radius, Transition2DCallback onEntry, Transition2DCallback onExit, int ix, int iy, int iz) {
|
||||
//trigger just less than every shiftSize scale
|
||||
this.triggerRangeSquared = 1<<((shiftSize<<1) - 1);
|
||||
this.shiftSize = shiftSize;
|
||||
this.enter = onEntry;
|
||||
this.exit = onExit;
|
||||
this.cornerPoints = RingUtil.generatingBoundingCorner2D(radius);
|
||||
this.radius = radius;
|
||||
}
|
||||
|
||||
private long Prel(int x, int z) {
|
||||
return (Integer.toUnsignedLong(this.currentZ + z)<<32)|Integer.toUnsignedLong(this.currentX + x);
|
||||
}
|
||||
|
||||
public void update(int x, int z) {
|
||||
long dx = this.lastUpdateX - x;
|
||||
long dz = this.lastUpdateZ - z;
|
||||
long distSquared = dx*dx + dz*dz;
|
||||
if (distSquared < this.triggerRangeSquared) {
|
||||
return;
|
||||
}
|
||||
|
||||
//Update the last update position
|
||||
int maxStep = this.triggerRangeSquared/2;
|
||||
this.lastUpdateX += Math.min(maxStep,Math.max(-maxStep, x-this.lastUpdateX));
|
||||
this.lastUpdateZ += Math.min(maxStep,Math.max(-maxStep, z-this.lastUpdateZ));
|
||||
|
||||
//Compute movement if it happened
|
||||
int nx = x>>this.shiftSize;
|
||||
int nz = z>>this.shiftSize;
|
||||
|
||||
if (nx == this.currentX && nz == this.currentZ) {
|
||||
//No movement
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
//FIXME: not right, needs to only call load/unload on entry and exit, cause atm its acting like a loaded circle
|
||||
|
||||
Long2IntOpenHashMap ops = new Long2IntOpenHashMap();
|
||||
while (true) {
|
||||
int dir = nz < this.currentZ ? -1 : 1;
|
||||
if (nz != this.currentZ) {
|
||||
for (int corner : this.cornerPoints) {
|
||||
int cx = corner >>> 16;
|
||||
int cz = corner & 0xFFFF;
|
||||
|
||||
ops.addTo(Prel(cx, cz + Math.max(0, dir)), dir);
|
||||
ops.addTo(Prel(cx, -cz + Math.min(0, dir)), -dir);
|
||||
if (cx != 0) {
|
||||
ops.addTo(Prel(-cx, cz + Math.max(0, dir)), dir);
|
||||
ops.addTo(Prel(-cx, -cz + Math.min(0, dir)), -dir);
|
||||
}
|
||||
}
|
||||
|
||||
this.currentZ += dir;
|
||||
}
|
||||
|
||||
dir = nx < this.currentX ? -1 : 1;
|
||||
if (nx != this.currentX) {
|
||||
for (int corner : this.cornerPoints) {
|
||||
int cx = corner & 0xFFFF;
|
||||
int cz = corner >>> 16;
|
||||
|
||||
ops.addTo(Prel(cx + Math.max(0, dir), cz), dir);
|
||||
ops.addTo(Prel(-cx + Math.min(0, dir), cz), -dir);
|
||||
if (cz != 0) {
|
||||
ops.addTo(Prel(cx + Math.max(0, dir), -cz), dir);
|
||||
ops.addTo(Prel(-cx + Math.min(0, dir), -cz), -dir);
|
||||
}
|
||||
}
|
||||
|
||||
this.currentX += dir;
|
||||
}
|
||||
|
||||
//Only break once the coords match
|
||||
if (nx == this.currentX && nz == this.currentZ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
ops.forEach((pos,val)->{
|
||||
if (val > 0) {
|
||||
this.enter.callback((int) (long)pos, (int) (pos>>32));
|
||||
}
|
||||
if (val < 0) {
|
||||
this.exit.callback((int) (long)pos, (int) (pos>>32));
|
||||
}
|
||||
});
|
||||
ops.clear();
|
||||
}
|
||||
|
||||
public void fill(int x, int z) {
|
||||
this.fill(x, z, null);
|
||||
}
|
||||
|
||||
public void fill(int x, int z, Transition2DCallback outsideCallback) {
|
||||
int cx = x>>this.shiftSize;
|
||||
int cz = z>>this.shiftSize;
|
||||
|
||||
int r2 = this.radius*this.radius;
|
||||
for (int a = -this.radius; a <= this.radius; a++) {
|
||||
//IntStream.range(-this.radius, this.radius+1).parallel().forEach(a->{
|
||||
int b = (int) Math.floor(Math.sqrt(r2-(a*a)));
|
||||
for (int c = -b; c <= b; c++) {
|
||||
this.enter.callback(a + cx, c + cz);
|
||||
}
|
||||
if (outsideCallback != null) {
|
||||
for (int c = -this.radius; c < -b; c++) {
|
||||
outsideCallback.callback(a + cx, c + cz);
|
||||
}
|
||||
|
||||
for (int c = b+1; c <= this.radius; c++) {
|
||||
outsideCallback.callback(a + cx, c + cz);
|
||||
}
|
||||
}
|
||||
}//);
|
||||
}
|
||||
|
||||
public void setCenter(int x, int z) {
|
||||
int cx = x>>this.shiftSize;
|
||||
int cz = z>>this.shiftSize;
|
||||
this.currentX = cx;
|
||||
this.currentZ = cz;
|
||||
this.lastUpdateX = x + (((int)(Math.random()*4))<<(this.shiftSize-4));
|
||||
this.lastUpdateZ = z + (((int)(Math.random()*4))<<(this.shiftSize-4));
|
||||
}
|
||||
}
|
||||
}
|
||||
/*
|
||||
public void update(int x, int z) {
|
||||
int MAX_STEPS_PER_UPDATE = 1;
|
||||
|
||||
|
||||
long dx = this.lastUpdateX - x;
|
||||
long dz = this.lastUpdateZ - z;
|
||||
long distSquared = dx*dx + dz*dz;
|
||||
if (distSquared < this.triggerRangeSquared) {
|
||||
return;
|
||||
}
|
||||
|
||||
//TODO: fixme: this last update needs to be incremented by a delta since
|
||||
|
||||
//Update the last update position
|
||||
int maxStep = this.triggerRangeSquared/2;
|
||||
this.lastUpdateX += Math.min(maxStep,Math.max(-maxStep, x-this.lastUpdateX));
|
||||
this.lastUpdateZ += Math.min(maxStep,Math.max(-maxStep, z-this.lastUpdateZ));
|
||||
|
||||
|
||||
|
||||
//Compute movement if it happened
|
||||
int nx = x>>this.shiftSize;
|
||||
int nz = z>>this.shiftSize;
|
||||
|
||||
if (nx == this.currentX && nz == this.currentZ) {
|
||||
//No movement
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
//FIXME: not right, needs to only call load/unload on entry and exit, cause atm its acting like a loaded circle
|
||||
|
||||
Long2IntOpenHashMap ops = new Long2IntOpenHashMap();
|
||||
|
||||
int zcount = MAX_STEPS_PER_UPDATE;
|
||||
int dir = nz<this.currentZ?-1:1;
|
||||
while (nz != this.currentZ) {
|
||||
for (int corner : this.cornerPoints) {
|
||||
int cx = corner>>>16;
|
||||
int cz = corner&0xFFFF;
|
||||
|
||||
ops.addTo(Prel( cx, cz+Math.max(0, dir)), dir);
|
||||
ops.addTo(Prel( cx,-cz+Math.min(0, dir)),-dir);
|
||||
if (cx != 0) {
|
||||
ops.addTo(Prel(-cx, cz+Math.max(0, dir)), dir);
|
||||
ops.addTo(Prel(-cx,-cz+Math.min(0, dir)),-dir);
|
||||
}
|
||||
}
|
||||
|
||||
//ops.addTo(Prel(0, this.radius+Math.max(0, dir)), dir);
|
||||
//ops.addTo(Prel(0, -this.radius+Math.min(0, dir)), -dir);
|
||||
|
||||
this.currentZ += dir;
|
||||
|
||||
if (--zcount == 0) break;
|
||||
}
|
||||
|
||||
int xcount = MAX_STEPS_PER_UPDATE;
|
||||
dir = nx<this.currentX?-1:1;
|
||||
while (nx != this.currentX) {
|
||||
|
||||
for (int corner : this.cornerPoints) {
|
||||
int cx = corner&0xFFFF;
|
||||
int cz = corner>>>16;
|
||||
|
||||
ops.addTo(Prel( cx+Math.max(0, dir), cz), dir);
|
||||
ops.addTo(Prel(-cx+Math.min(0, dir), cz),-dir);
|
||||
if (cz != 0) {
|
||||
ops.addTo(Prel(cx + Math.max(0, dir), -cz), dir);
|
||||
ops.addTo(Prel(-cx + Math.min(0, dir), -cz), -dir);
|
||||
}
|
||||
}
|
||||
|
||||
this.currentX += dir;
|
||||
|
||||
if (--xcount == 0) break;
|
||||
}
|
||||
|
||||
|
||||
ops.forEach((pos,val)->{
|
||||
if (val > 0) {
|
||||
this.enter.callback((int) (long)pos, (int) (pos>>32));
|
||||
}
|
||||
if (val < 0) {
|
||||
this.exit.callback((int) (long)pos, (int) (pos>>32));
|
||||
}
|
||||
});
|
||||
ops.clear();
|
||||
}*/
|
||||
@@ -1,9 +0,0 @@
|
||||
package me.cortex.voxy.client.core;
|
||||
|
||||
import me.cortex.voxy.client.core.VoxelCore;
|
||||
|
||||
public interface IGetVoxelCore {
|
||||
VoxelCore getVoxelCore();
|
||||
|
||||
void reloadVoxelCore();
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package me.cortex.voxy.client.core;
|
||||
|
||||
public interface IGetVoxyRenderSystem {
|
||||
VoxyRenderSystem getVoxyRenderSystem();
|
||||
void shutdownRenderer();
|
||||
void createRenderer();
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
package me.cortex.voxy.client.core;
|
||||
|
||||
import me.cortex.voxy.client.core.gl.GlBuffer;
|
||||
import me.cortex.voxy.client.core.model.ModelBakerySubsystem;
|
||||
import me.cortex.voxy.client.core.rendering.Viewport;
|
||||
import me.cortex.voxy.client.core.rendering.hierachical.AsyncNodeManager;
|
||||
import me.cortex.voxy.client.core.rendering.hierachical.HierarchicalOcclusionTraverser;
|
||||
import me.cortex.voxy.client.core.rendering.hierachical.NodeCleaner;
|
||||
import me.cortex.voxy.client.core.rendering.post.FullscreenBlit;
|
||||
import me.cortex.voxy.client.core.rendering.section.AbstractSectionRenderer;
|
||||
import me.cortex.voxy.client.core.rendering.util.DepthFramebuffer;
|
||||
import me.cortex.voxy.client.core.rendering.util.UploadStream;
|
||||
import me.cortex.voxy.client.iris.IrisVoxyRenderPipelineData;
|
||||
import net.irisshaders.iris.shaderpack.materialmap.WorldRenderingSettings;
|
||||
import org.joml.Matrix4f;
|
||||
import org.lwjgl.opengl.GL30;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.BooleanSupplier;
|
||||
|
||||
import static org.lwjgl.opengl.GL11.GL_DEPTH_BUFFER_BIT;
|
||||
import static org.lwjgl.opengl.GL30C.*;
|
||||
import static org.lwjgl.opengl.GL31.GL_UNIFORM_BUFFER;
|
||||
import static org.lwjgl.opengl.GL45C.*;
|
||||
|
||||
public class IrisVoxyRenderPipeline extends AbstractRenderPipeline {
|
||||
private final IrisVoxyRenderPipelineData data;
|
||||
private final FullscreenBlit depthBlit = new FullscreenBlit("voxy:post/blit_texture_depth_cutout.frag");
|
||||
public final DepthFramebuffer fb = new DepthFramebuffer(GL_DEPTH24_STENCIL8);
|
||||
public final DepthFramebuffer fbTranslucent = new DepthFramebuffer(GL_DEPTH24_STENCIL8);
|
||||
|
||||
private final GlBuffer shaderUniforms;
|
||||
|
||||
public IrisVoxyRenderPipeline(IrisVoxyRenderPipelineData data, AsyncNodeManager nodeManager, NodeCleaner nodeCleaner, HierarchicalOcclusionTraverser traversal, BooleanSupplier frexSupplier) {
|
||||
super(nodeManager, nodeCleaner, traversal, frexSupplier);
|
||||
this.data = data;
|
||||
if (this.data.thePipeline != null) {
|
||||
throw new IllegalStateException("Pipeline data already bound");
|
||||
}
|
||||
this.data.thePipeline = this;
|
||||
|
||||
//Bind the drawbuffers
|
||||
var oDT = this.data.opaqueDrawTargets;
|
||||
int[] binding = new int[oDT.length];
|
||||
for (int i = 0; i < oDT.length; i++) {
|
||||
binding[i] = GL30.GL_COLOR_ATTACHMENT0+i;
|
||||
glNamedFramebufferTexture(this.fb.framebuffer.id, GL30.GL_COLOR_ATTACHMENT0+i, oDT[i], 0);
|
||||
}
|
||||
glNamedFramebufferDrawBuffers(this.fb.framebuffer.id, binding);
|
||||
|
||||
var tDT = this.data.translucentDrawTargets;
|
||||
binding = new int[tDT.length];
|
||||
for (int i = 0; i < tDT.length; i++) {
|
||||
binding[i] = GL30.GL_COLOR_ATTACHMENT0+i;
|
||||
glNamedFramebufferTexture(this.fbTranslucent.framebuffer.id, GL30.GL_COLOR_ATTACHMENT0+i, tDT[i], 0);
|
||||
}
|
||||
glNamedFramebufferDrawBuffers(this.fbTranslucent.framebuffer.id, binding);
|
||||
|
||||
this.fb.framebuffer.verify();
|
||||
this.fbTranslucent.framebuffer.verify();
|
||||
|
||||
if (data.getUniforms() != null) {
|
||||
this.shaderUniforms = new GlBuffer(data.getUniforms().size());
|
||||
} else {
|
||||
this.shaderUniforms = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setupExtraModelBakeryData(ModelBakerySubsystem modelService) {
|
||||
modelService.factory.setCustomBlockStateMapping(WorldRenderingSettings.INSTANCE.getBlockStateIds());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void free() {
|
||||
if (this.data.thePipeline != this) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
this.data.thePipeline = null;
|
||||
|
||||
this.depthBlit.delete();
|
||||
this.fb.free();
|
||||
this.fbTranslucent.free();
|
||||
|
||||
if (this.shaderUniforms != null) {
|
||||
this.shaderUniforms.free();
|
||||
}
|
||||
|
||||
super.free0();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preSetup(Viewport<?> viewport) {
|
||||
super.preSetup(viewport);
|
||||
if (this.shaderUniforms != null) {
|
||||
//Update the uniforms
|
||||
long ptr = UploadStream.INSTANCE.uploadTo(this.shaderUniforms);
|
||||
this.data.getUniforms().updater().accept(ptr);
|
||||
UploadStream.INSTANCE.commit();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int setup(Viewport<?> viewport, int sourceFramebuffer, int srcWidth, int srcHeight) {
|
||||
|
||||
this.fb.resize(viewport.width, viewport.height);
|
||||
this.fbTranslucent.resize(viewport.width, viewport.height);
|
||||
|
||||
if (false) {//TODO: only do this if shader specifies
|
||||
//Clear the colour component
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, this.fb.framebuffer.id);
|
||||
glClearColor(0, 0, 0, 0);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
}
|
||||
|
||||
if (this.data.useViewportDims) {
|
||||
srcWidth = viewport.width;
|
||||
srcHeight = viewport.height;
|
||||
}
|
||||
this.initDepthStencil(sourceFramebuffer, this.fb.framebuffer.id, srcWidth, srcHeight, viewport.width, viewport.height);
|
||||
return this.fb.getDepthTex().id;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void postOpaquePreTranslucent(Viewport<?> viewport) {
|
||||
int msk = GL_DEPTH_BUFFER_BIT|GL_STENCIL_BUFFER_BIT;
|
||||
if (true) {//TODO: make shader specified
|
||||
if (false) {//TODO: only do this if shader specifies
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, this.fbTranslucent.framebuffer.id);
|
||||
glClearColor(0, 0, 0, 0);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
}
|
||||
} else {
|
||||
msk |= GL_COLOR_BUFFER_BIT;
|
||||
}
|
||||
glBlitNamedFramebuffer(this.fb.framebuffer.id, this.fbTranslucent.framebuffer.id, 0,0, viewport.width, viewport.height, 0,0, viewport.width, viewport.height, msk, GL_NEAREST);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void finish(Viewport<?> viewport, int sourceFrameBuffer, int srcWidth, int srcHeight) {
|
||||
if (this.data.renderToVanillaDepth && srcWidth == viewport.width && srcHeight == viewport.height) {//We can only depthblit out if destination size is the same
|
||||
glColorMask(false, false, false, false);
|
||||
AbstractRenderPipeline.transformBlitDepth(this.depthBlit,
|
||||
this.fbTranslucent.getDepthTex().id, sourceFrameBuffer,
|
||||
viewport, new Matrix4f(viewport.vanillaProjection).mul(viewport.modelView));
|
||||
glColorMask(true, true, true, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void bindUniforms() {
|
||||
this.bindUniforms(UNIFORM_BINDING_POINT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindUniforms(int bindingPoint) {
|
||||
if (this.shaderUniforms != null) {
|
||||
GL30.glBindBufferBase(GL_UNIFORM_BUFFER, bindingPoint, this.shaderUniforms.id);// todo: dont randomly select this to 5
|
||||
}
|
||||
}
|
||||
|
||||
private void doBindings() {
|
||||
this.bindUniforms();
|
||||
if (this.data.getSsboSet() != null) {
|
||||
this.data.getSsboSet().bindingFunction().accept(10);
|
||||
}
|
||||
if (this.data.getImageSet() != null) {
|
||||
this.data.getImageSet().bindingFunction().accept(6);
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void setupAndBindOpaque(Viewport<?> viewport) {
|
||||
this.fb.bind();
|
||||
this.doBindings();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setupAndBindTranslucent(Viewport<?> viewport) {
|
||||
this.fbTranslucent.bind();
|
||||
this.doBindings();
|
||||
if (this.data.getBlender() != null) {
|
||||
this.data.getBlender().run();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addDebug(List<String> debug) {
|
||||
debug.add("Using: " + this.getClass().getSimpleName());
|
||||
super.addDebug(debug);
|
||||
}
|
||||
|
||||
private static final int UNIFORM_BINDING_POINT = 5;//TODO make ths binding point... not randomly 5
|
||||
|
||||
private StringBuilder buildGenericShaderHeader(AbstractSectionRenderer<?, ?> renderer, String input) {
|
||||
StringBuilder builder = new StringBuilder(input).append("\n\n\n");
|
||||
|
||||
if (this.data.getUniforms() != null) {
|
||||
builder.append("layout(binding = "+UNIFORM_BINDING_POINT+", std140) uniform ShaderUniformBindings ")
|
||||
.append(this.data.getUniforms().layout())
|
||||
.append(";\n\n");
|
||||
}
|
||||
|
||||
if (this.data.getSsboSet() != null) {
|
||||
builder.append("#define BUFFER_BINDING_INDEX_BASE 10\n");//TODO: DONT RANDOMLY MAKE THIS 10
|
||||
builder.append(this.data.getSsboSet().layout()).append("\n\n");
|
||||
}
|
||||
|
||||
if (this.data.getImageSet() != null) {
|
||||
builder.append("#define BASE_SAMPLER_BINDING_INDEX 6\n");//TODO: DONT RANDOMLY MAKE THIS 6
|
||||
builder.append(this.data.getImageSet().layout()).append("\n\n");
|
||||
}
|
||||
|
||||
return builder.append("\n\n");
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public String patchOpaqueShader(AbstractSectionRenderer<?, ?> renderer, String input) {
|
||||
var builder = this.buildGenericShaderHeader(renderer, input);
|
||||
|
||||
builder.append(this.data.opaqueFragPatch());
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String patchTranslucentShader(AbstractSectionRenderer<?, ?> renderer, String input) {
|
||||
if (this.data.translucentFragPatch() == null) return null;
|
||||
|
||||
var builder = this.buildGenericShaderHeader(renderer, input);
|
||||
builder.append(this.data.translucentFragPatch());
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String taaFunction(String functionName) {
|
||||
return this.taaFunction(UNIFORM_BINDING_POINT, functionName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String taaFunction(int uboBindingPoint, String functionName) {
|
||||
var builder = new StringBuilder();
|
||||
|
||||
if (this.data.getUniforms() != null) {
|
||||
builder.append("layout(binding = "+uboBindingPoint+", std140) uniform ShaderUniformBindings ")
|
||||
.append(this.data.getUniforms().layout())
|
||||
.append(";\n\n");
|
||||
}
|
||||
|
||||
builder.append("vec2 ").append(functionName).append("()\n");
|
||||
builder.append(this.data.TAA);
|
||||
builder.append("\n");
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public float[] getRenderScalingFactor() {
|
||||
return this.data.resolutionScale;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package me.cortex.voxy.client.core;
|
||||
|
||||
import me.cortex.voxy.client.config.VoxyConfig;
|
||||
import me.cortex.voxy.client.core.gl.GlFramebuffer;
|
||||
import me.cortex.voxy.client.core.gl.GlTexture;
|
||||
import me.cortex.voxy.client.core.gl.shader.Shader;
|
||||
import me.cortex.voxy.client.core.gl.shader.ShaderType;
|
||||
import me.cortex.voxy.client.core.rendering.Viewport;
|
||||
import me.cortex.voxy.client.core.rendering.hierachical.AsyncNodeManager;
|
||||
import me.cortex.voxy.client.core.rendering.hierachical.HierarchicalOcclusionTraverser;
|
||||
import me.cortex.voxy.client.core.rendering.hierachical.NodeCleaner;
|
||||
import me.cortex.voxy.client.core.rendering.post.FullscreenBlit;
|
||||
import me.cortex.voxy.client.core.rendering.util.DepthFramebuffer;
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
import org.joml.Matrix4f;
|
||||
import org.lwjgl.system.MemoryStack;
|
||||
|
||||
import java.util.function.BooleanSupplier;
|
||||
|
||||
import static org.lwjgl.opengl.ARBComputeShader.glDispatchCompute;
|
||||
import static org.lwjgl.opengl.ARBShaderImageLoadStore.glBindImageTexture;
|
||||
import static org.lwjgl.opengl.GL11.GL_BLEND;
|
||||
import static org.lwjgl.opengl.GL11.GL_ONE;
|
||||
import static org.lwjgl.opengl.GL11.GL_ONE_MINUS_SRC_ALPHA;
|
||||
import static org.lwjgl.opengl.GL11.GL_SRC_ALPHA;
|
||||
import static org.lwjgl.opengl.GL11.glEnable;
|
||||
import static org.lwjgl.opengl.GL11C.GL_NEAREST;
|
||||
import static org.lwjgl.opengl.GL11C.GL_RGBA8;
|
||||
import static org.lwjgl.opengl.GL14.glBlendFuncSeparate;
|
||||
import static org.lwjgl.opengl.GL15.GL_READ_WRITE;
|
||||
import static org.lwjgl.opengl.GL30C.*;
|
||||
import static org.lwjgl.opengl.GL43.GL_DEPTH_STENCIL_TEXTURE_MODE;
|
||||
import static org.lwjgl.opengl.GL45C.glBindTextureUnit;
|
||||
import static org.lwjgl.opengl.GL45C.glTextureParameterf;
|
||||
|
||||
public class NormalRenderPipeline extends AbstractRenderPipeline {
|
||||
private GlTexture colourTex;
|
||||
private GlTexture colourSSAOTex;
|
||||
private final GlFramebuffer fbSSAO = new GlFramebuffer();
|
||||
private final DepthFramebuffer fb = new DepthFramebuffer(GL_DEPTH24_STENCIL8);
|
||||
|
||||
private final boolean useEnvFog;
|
||||
private final FullscreenBlit finalBlit;
|
||||
|
||||
private final Shader ssaoCompute = Shader.make()
|
||||
.add(ShaderType.COMPUTE, "voxy:post/ssao.comp")
|
||||
.compile();
|
||||
|
||||
protected NormalRenderPipeline(AsyncNodeManager nodeManager, NodeCleaner nodeCleaner, HierarchicalOcclusionTraverser traversal, BooleanSupplier frexSupplier) {
|
||||
super(nodeManager, nodeCleaner, traversal, frexSupplier);
|
||||
this.useEnvFog = VoxyConfig.CONFIG.useEnvironmentalFog;
|
||||
this.finalBlit = new FullscreenBlit("voxy:post/blit_texture_depth_cutout.frag",
|
||||
a->a.defineIf("USE_ENV_FOG", this.useEnvFog).define("EMIT_COLOUR"));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int setup(Viewport<?> viewport, int sourceFB, int srcWidth, int srcHeight) {
|
||||
if (this.colourTex == null || this.colourTex.getHeight() != viewport.height || this.colourTex.getWidth() != viewport.width) {
|
||||
if (this.colourTex != null) {
|
||||
this.colourTex.free();
|
||||
this.colourSSAOTex.free();
|
||||
}
|
||||
this.fb.resize(viewport.width, viewport.height);
|
||||
|
||||
this.colourTex = new GlTexture().store(GL_RGBA8, 1, viewport.width, viewport.height);
|
||||
this.colourSSAOTex = new GlTexture().store(GL_RGBA8, 1, viewport.width, viewport.height);
|
||||
|
||||
this.fb.framebuffer.bind(GL_COLOR_ATTACHMENT0, this.colourTex).verify();
|
||||
this.fbSSAO.bind(GL_DEPTH_STENCIL_ATTACHMENT, this.fb.getDepthTex()).bind(GL_COLOR_ATTACHMENT0, this.colourSSAOTex).verify();
|
||||
|
||||
|
||||
glTextureParameterf(this.colourTex.id, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTextureParameterf(this.colourTex.id, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTextureParameterf(this.colourSSAOTex.id, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTextureParameterf(this.colourSSAOTex.id, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTextureParameterf(this.fb.getDepthTex().id, GL_DEPTH_STENCIL_TEXTURE_MODE, GL_DEPTH_COMPONENT);
|
||||
}
|
||||
|
||||
this.initDepthStencil(sourceFB, this.fb.framebuffer.id, viewport.width, viewport.height, viewport.width, viewport.height);
|
||||
|
||||
return this.fb.getDepthTex().id;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void postOpaquePreTranslucent(Viewport<?> viewport) {
|
||||
this.ssaoCompute.bind();
|
||||
try (var stack = MemoryStack.stackPush()) {
|
||||
long ptr = stack.nmalloc(4*4*4);
|
||||
viewport.MVP.getToAddress(ptr);
|
||||
nglUniformMatrix4fv(3, 1, false, ptr);//MVP
|
||||
viewport.MVP.invert(new Matrix4f()).getToAddress(ptr);
|
||||
nglUniformMatrix4fv(4, 1, false, ptr);//invMVP
|
||||
}
|
||||
|
||||
|
||||
glBindImageTexture(0, this.colourSSAOTex.id, 0, false,0, GL_READ_WRITE, GL_RGBA8);
|
||||
glBindTextureUnit(1, this.fb.getDepthTex().id);
|
||||
glBindTextureUnit(2, this.colourTex.id);
|
||||
|
||||
glDispatchCompute((viewport.width+31)/32, (viewport.height+31)/32, 1);
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, this.fbSSAO.id);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void finish(Viewport<?> viewport, int sourceFrameBuffer, int srcWidth, int srcHeight) {
|
||||
this.finalBlit.bind();
|
||||
if (this.useEnvFog) {
|
||||
float start = viewport.fogParameters.environmentalStart();
|
||||
float end = viewport.fogParameters.environmentalEnd();
|
||||
float invEndFogDelta = 1f/(end-start);
|
||||
float endDistance = MinecraftClient.getInstance().gameRenderer.getViewDistanceBlocks()*1.5f;
|
||||
glUniform3f(4, endDistance, invEndFogDelta, Math.abs(start)*invEndFogDelta);
|
||||
glUniform3f(5, viewport.fogParameters.red(), viewport.fogParameters.green(), viewport.fogParameters.blue());
|
||||
}
|
||||
|
||||
glBindTextureUnit(3, this.colourSSAOTex.id);
|
||||
|
||||
//Do alpha blending
|
||||
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
|
||||
AbstractRenderPipeline.transformBlitDepth(this.finalBlit, this.fb.getDepthTex().id, sourceFrameBuffer, viewport, new Matrix4f(viewport.vanillaProjection).mul(viewport.modelView));
|
||||
glDisable(GL_BLEND);
|
||||
//glBlitNamedFramebuffer(this.fbSSAO.id, sourceFrameBuffer, 0,0, viewport.width, viewport.height, 0,0, viewport.width, viewport.height, GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setupAndBindOpaque(Viewport<?> viewport) {
|
||||
this.fb.bind();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setupAndBindTranslucent(Viewport<?> viewport) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, this.fbSSAO.id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void free() {
|
||||
this.finalBlit.delete();
|
||||
this.ssaoCompute.free();
|
||||
this.fb.free();
|
||||
this.fbSSAO.free();
|
||||
if (this.colourTex != null) {
|
||||
this.colourTex.free();
|
||||
this.colourSSAOTex.free();
|
||||
}
|
||||
super.free0();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package me.cortex.voxy.client.core;
|
||||
|
||||
import me.cortex.voxy.client.core.rendering.hierachical.AsyncNodeManager;
|
||||
import me.cortex.voxy.client.core.rendering.hierachical.HierarchicalOcclusionTraverser;
|
||||
import me.cortex.voxy.client.core.rendering.hierachical.NodeCleaner;
|
||||
import me.cortex.voxy.client.core.util.IrisUtil;
|
||||
import me.cortex.voxy.client.iris.IGetIrisVoxyPipelineData;
|
||||
import me.cortex.voxy.common.Logger;
|
||||
import net.irisshaders.iris.Iris;
|
||||
import net.irisshaders.iris.api.v0.IrisApi;
|
||||
|
||||
import java.util.function.BooleanSupplier;
|
||||
|
||||
public class RenderPipelineFactory {
|
||||
public static AbstractRenderPipeline createPipeline(AsyncNodeManager nodeManager, NodeCleaner nodeCleaner, HierarchicalOcclusionTraverser traversal, BooleanSupplier frexSupplier) {
|
||||
//Note this is where will choose/create e.g. IrisRenderPipeline or normal pipeline
|
||||
AbstractRenderPipeline pipeline = null;
|
||||
if (IrisUtil.IRIS_INSTALLED && IrisUtil.SHADER_SUPPORT) {
|
||||
pipeline = createIrisPipeline(nodeManager, nodeCleaner, traversal, frexSupplier);
|
||||
}
|
||||
if (pipeline == null) {
|
||||
pipeline = new NormalRenderPipeline(nodeManager, nodeCleaner, traversal, frexSupplier);
|
||||
}
|
||||
return pipeline;
|
||||
}
|
||||
|
||||
private static AbstractRenderPipeline createIrisPipeline(AsyncNodeManager nodeManager, NodeCleaner nodeCleaner, HierarchicalOcclusionTraverser traversal, BooleanSupplier frexSupplier) {
|
||||
var irisPipe = Iris.getPipelineManager().getPipelineNullable();
|
||||
if (irisPipe == null) {
|
||||
return null;
|
||||
}
|
||||
if (irisPipe instanceof IGetIrisVoxyPipelineData getVoxyPipeData) {
|
||||
var pipeData = getVoxyPipeData.voxy$getPipelineData();
|
||||
if (pipeData == null) {
|
||||
return null;
|
||||
}
|
||||
Logger.info("Creating voxy iris render pipeline");
|
||||
try {
|
||||
return new IrisVoxyRenderPipeline(pipeData, nodeManager, nodeCleaner, traversal, frexSupplier);
|
||||
} catch (Exception e) {
|
||||
Logger.error("Failed to create iris render pipeline", e);
|
||||
IrisUtil.disableIrisShaders();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,278 +0,0 @@
|
||||
package me.cortex.voxy.client.core;
|
||||
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import me.cortex.voxy.client.Voxy;
|
||||
import me.cortex.voxy.client.config.VoxyConfig;
|
||||
import me.cortex.voxy.client.core.rendering.*;
|
||||
import me.cortex.voxy.client.core.rendering.building.RenderGenerationService;
|
||||
import me.cortex.voxy.client.core.rendering.post.PostProcessing;
|
||||
import me.cortex.voxy.client.core.util.IrisUtil;
|
||||
import me.cortex.voxy.client.saver.ContextSelectionSystem;
|
||||
import me.cortex.voxy.common.world.WorldEngine;
|
||||
import me.cortex.voxy.client.importers.WorldImporter;
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
import net.minecraft.client.gui.hud.ClientBossBar;
|
||||
import net.minecraft.client.render.Camera;
|
||||
import net.minecraft.client.render.Frustum;
|
||||
import net.minecraft.client.util.math.MatrixStack;
|
||||
import net.minecraft.entity.boss.BossBar;
|
||||
import net.minecraft.registry.RegistryKeys;
|
||||
import net.minecraft.text.Text;
|
||||
import net.minecraft.util.math.MathHelper;
|
||||
import net.minecraft.world.World;
|
||||
import net.minecraft.world.chunk.WorldChunk;
|
||||
import org.joml.Matrix4f;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.*;
|
||||
|
||||
import static org.lwjgl.opengl.GL30C.GL_DRAW_FRAMEBUFFER_BINDING;
|
||||
|
||||
//Core class that ingests new data from sources and updates the required systems
|
||||
|
||||
//3 primary services:
|
||||
// ingest service: this takes in unloaded chunk events from the client, processes the chunk and critically also updates the lod view of the world
|
||||
// render data builder service: this service builds the render data from build requests it also handles the collecting of build data for the selected region (only axis aligned single lod tasks)
|
||||
// serialization service: serializes changed world data and ensures that the database and any loaded data are in sync such that the database can never be more updated than loaded data, also performs compression on serialization
|
||||
|
||||
//there are multiple subsystems
|
||||
//player tracker system (determines what lods are loaded and used by the player)
|
||||
//updating system (triggers render data rebuilds when something from the ingest service causes an LOD change)
|
||||
//the render system simply renders what data it has, its responsable for gpu memory layouts in arenas and rendering in an optimal way, it makes no requests back to any of the other systems or services, it just applies render data updates
|
||||
|
||||
//There is strict forward only dataflow
|
||||
//Ingest -> world engine -> raw render data -> render data
|
||||
public class VoxelCore {
|
||||
private final WorldEngine world;
|
||||
private final DistanceTracker distanceTracker;
|
||||
private final RenderGenerationService renderGen;
|
||||
private final RenderTracker renderTracker;
|
||||
|
||||
private final AbstractFarWorldRenderer renderer;
|
||||
private final ViewportSelector viewportSelector;
|
||||
private final PostProcessing postProcessing;
|
||||
|
||||
//private final Thread shutdownThread = new Thread(this::shutdown);
|
||||
|
||||
private WorldImporter importer;
|
||||
public VoxelCore(ContextSelectionSystem.Selection worldSelection) {
|
||||
this.world = worldSelection.createEngine();
|
||||
var cfg = worldSelection.getConfig();
|
||||
System.out.println("Initializing voxy core");
|
||||
|
||||
//Trigger the shared index buffer loading
|
||||
SharedIndexBuffer.INSTANCE.id();
|
||||
Capabilities.init();//Ensure clinit is called
|
||||
this.renderer = this.createRenderBackend();
|
||||
this.viewportSelector = new ViewportSelector<>(this.renderer::createViewport);
|
||||
System.out.println("Renderer initialized");
|
||||
|
||||
this.renderTracker = new RenderTracker(this.world, this.renderer);
|
||||
this.renderGen = new RenderGenerationService(this.world, this.renderer.getModelManager(), VoxyConfig.CONFIG.renderThreads, this.renderTracker::processBuildResult, this.renderer.usesMeshlets());
|
||||
this.world.setDirtyCallback(this.renderTracker::sectionUpdated);
|
||||
this.renderTracker.setRenderGen(this.renderGen);
|
||||
System.out.println("Render tracker and generator initialized");
|
||||
|
||||
//To get to chunk scale multiply the scale by 2, the scale is after how many chunks does the lods halve
|
||||
int q = VoxyConfig.CONFIG.qualityScale;
|
||||
int minY = MinecraftClient.getInstance().world.getBottomSectionCoord()/2;
|
||||
int maxY = MinecraftClient.getInstance().world.getTopSectionCoord()/2;
|
||||
|
||||
if (cfg.minYOverride != Integer.MAX_VALUE) {
|
||||
minY = cfg.minYOverride;
|
||||
}
|
||||
|
||||
if (cfg.maxYOverride != Integer.MIN_VALUE) {
|
||||
maxY = cfg.maxYOverride;
|
||||
}
|
||||
|
||||
this.distanceTracker = new DistanceTracker(this.renderTracker, new int[]{q,q,q,q},
|
||||
(VoxyConfig.CONFIG.renderDistance<0?VoxyConfig.CONFIG.renderDistance:((VoxyConfig.CONFIG.renderDistance+1)/2)),
|
||||
minY, maxY);
|
||||
System.out.println("Distance tracker initialized");
|
||||
|
||||
this.postProcessing = new PostProcessing();
|
||||
|
||||
this.world.getMapper().setCallbacks(this.renderer::addBlockState, this.renderer::addBiome);
|
||||
|
||||
|
||||
////Resave the db incase it failed a recovery
|
||||
//this.world.getMapper().forceResaveStates();
|
||||
|
||||
var biomeRegistry = MinecraftClient.getInstance().world.getRegistryManager().get(RegistryKeys.BIOME);
|
||||
for (var biome : this.world.getMapper().getBiomeEntries()) {
|
||||
//this.renderer.getModelManager().addBiome(biome.id, biomeRegistry.get(new Identifier(biome.biome)));
|
||||
this.renderer.addBiome(biome);
|
||||
}
|
||||
|
||||
for (var state : this.world.getMapper().getStateEntries()) {
|
||||
//this.renderer.getModelManager().addEntry(state.id, state.state);
|
||||
this.renderer.addBlockState(state);
|
||||
}
|
||||
//this.renderer.getModelManager().updateEntry(0, Blocks.GRASS_BLOCK.getDefaultState());
|
||||
|
||||
System.out.println("Voxy core initialized");
|
||||
}
|
||||
|
||||
private AbstractFarWorldRenderer<?,?> createRenderBackend() {
|
||||
if (false) {
|
||||
System.out.println("Using Gl46MeshletFarWorldRendering");
|
||||
return new Gl46MeshletsFarWorldRenderer(VoxyConfig.CONFIG.geometryBufferSize, VoxyConfig.CONFIG.maxSections);
|
||||
} else {
|
||||
if (VoxyConfig.CONFIG.useMeshShaders()) {
|
||||
System.out.println("Using NvMeshFarWorldRenderer");
|
||||
return new NvMeshFarWorldRenderer(VoxyConfig.CONFIG.geometryBufferSize, VoxyConfig.CONFIG.maxSections);
|
||||
} else {
|
||||
System.out.println("Using Gl46FarWorldRenderer");
|
||||
return new Gl46FarWorldRenderer(VoxyConfig.CONFIG.geometryBufferSize, VoxyConfig.CONFIG.maxSections);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void enqueueIngest(WorldChunk worldChunk) {
|
||||
this.world.ingestService.enqueueIngest(worldChunk);
|
||||
}
|
||||
|
||||
boolean firstTime = true;
|
||||
public void renderSetup(Frustum frustum, Camera camera) {
|
||||
if (this.firstTime) {
|
||||
this.distanceTracker.init(camera.getBlockPos().getX(), camera.getBlockPos().getZ());
|
||||
this.firstTime = false;
|
||||
//this.renderTracker.addLvl0(0,6,0);
|
||||
}
|
||||
this.distanceTracker.setCenter(camera.getBlockPos().getX(), camera.getBlockPos().getY(), camera.getBlockPos().getZ());
|
||||
this.renderer.setupRender(frustum, camera);
|
||||
}
|
||||
|
||||
private static Matrix4f makeProjectionMatrix(float near, float far) {
|
||||
//TODO: use the existing projection matrix use mulLocal by the inverse of the projection and then mulLocal our projection
|
||||
|
||||
var projection = new Matrix4f();
|
||||
var client = MinecraftClient.getInstance();
|
||||
var gameRenderer = client.gameRenderer;//tickCounter.getTickDelta(true);
|
||||
|
||||
float fov = (float) gameRenderer.getFov(gameRenderer.getCamera(), client.getRenderTickCounter().getTickDelta(true), true);
|
||||
|
||||
projection.setPerspective(fov * 0.01745329238474369f,
|
||||
(float) client.getWindow().getFramebufferWidth() / (float)client.getWindow().getFramebufferHeight(),
|
||||
near, far);
|
||||
return projection;
|
||||
}
|
||||
|
||||
private static Matrix4f computeProjectionMat() {
|
||||
return new Matrix4f(RenderSystem.getProjectionMatrix()).mulLocal(
|
||||
makeProjectionMatrix(0.05f, MinecraftClient.getInstance().gameRenderer.getFarPlaneDistance()).invert()
|
||||
).mulLocal(makeProjectionMatrix(16, 16*3000));
|
||||
}
|
||||
|
||||
public void renderOpaque(MatrixStack matrices, double cameraX, double cameraY, double cameraZ) {
|
||||
if (IrisUtil.irisShadowActive()) {
|
||||
return;
|
||||
}
|
||||
matrices.push();
|
||||
matrices.translate(-cameraX, -cameraY, -cameraZ);
|
||||
matrices.pop();
|
||||
//this.renderer.getModelManager().updateEntry(0, Blocks.DIRT_PATH.getDefaultState());
|
||||
|
||||
//this.renderer.getModelManager().updateEntry(0, Blocks.COMPARATOR.getDefaultState());
|
||||
//this.renderer.getModelManager().updateEntry(0, Blocks.OAK_LEAVES.getDefaultState());
|
||||
|
||||
//var fb = Iris.getPipelineManager().getPipelineNullable().getSodiumTerrainPipeline().getTerrainSolidFramebuffer();
|
||||
//fb.bind();
|
||||
|
||||
var projection = computeProjectionMat();
|
||||
//var projection = RenderSystem.getProjectionMatrix();//computeProjectionMat();
|
||||
var viewport = this.viewportSelector.getViewport();
|
||||
viewport
|
||||
.setProjection(projection)
|
||||
.setModelView(matrices.peek().getPositionMatrix())
|
||||
.setCamera(cameraX, cameraY, cameraZ)
|
||||
.setScreenSize(MinecraftClient.getInstance().getFramebuffer().textureWidth, MinecraftClient.getInstance().getFramebuffer().textureHeight);
|
||||
|
||||
int boundFB = GL11.glGetInteger(GL_DRAW_FRAMEBUFFER_BINDING);
|
||||
this.postProcessing.setup(MinecraftClient.getInstance().getFramebuffer().textureWidth, MinecraftClient.getInstance().getFramebuffer().textureHeight, boundFB);
|
||||
|
||||
this.renderer.renderFarAwayOpaque(viewport);
|
||||
|
||||
//Compute the SSAO of the rendered terrain
|
||||
this.postProcessing.computeSSAO(projection, matrices);
|
||||
|
||||
//We can render the translucent directly after as it is the furthest translucent objects
|
||||
this.renderer.renderFarAwayTranslucent(viewport);
|
||||
|
||||
|
||||
this.postProcessing.renderPost(projection, RenderSystem.getProjectionMatrix(), boundFB);
|
||||
|
||||
}
|
||||
|
||||
public void addDebugInfo(List<String> debug) {
|
||||
debug.add("");
|
||||
debug.add("");
|
||||
debug.add("Voxy Core: " + Voxy.VERSION);
|
||||
/*
|
||||
debug.add("Ingest service tasks: " + this.world.ingestService.getTaskCount());
|
||||
debug.add("Saving service tasks: " + this.world.savingService.getTaskCount());
|
||||
debug.add("Render service tasks: " + this.renderGen.getTaskCount());
|
||||
*/
|
||||
debug.add("I/S/R tasks: " + this.world.ingestService.getTaskCount() + "/"+this.world.savingService.getTaskCount()+"/"+this.renderGen.getTaskCount());
|
||||
debug.add("Loaded cache sizes: " + Arrays.toString(this.world.getLoadedSectionCacheSizes()));
|
||||
debug.add("Mesh cache count: " + this.renderGen.getMeshCacheCount());
|
||||
this.renderer.addDebugData(debug);
|
||||
}
|
||||
|
||||
//Note: when doing translucent rendering, only need to sort when generating the geometry, or when crossing into the center zone
|
||||
// cause in 99.99% of cases the sections dont need to be sorted
|
||||
// since they are AABBS crossing the normal is impossible without one of the axis being equal
|
||||
|
||||
public void shutdown() {
|
||||
//if (Thread.currentThread() != this.shutdownThread) {
|
||||
// Runtime.getRuntime().removeShutdownHook(this.shutdownThread);
|
||||
//}
|
||||
|
||||
//this.world.getMapper().forceResaveStates();
|
||||
if (this.importer != null) {
|
||||
System.out.println("Shutting down importer");
|
||||
try {this.importer.shutdown();this.importer = null;} catch (Exception e) {System.err.println(e);}
|
||||
}
|
||||
System.out.println("Shutting down voxel core");
|
||||
try {this.renderGen.shutdown();} catch (Exception e) {System.err.println(e);}
|
||||
System.out.println("Render gen shut down");
|
||||
try {this.world.shutdown();} catch (Exception e) {System.err.println(e);}
|
||||
System.out.println("World engine shut down");
|
||||
try {this.renderer.shutdown(); this.viewportSelector.free();} catch (Exception e) {System.err.println(e);}
|
||||
System.out.println("Renderer shut down");
|
||||
if (this.postProcessing!=null){try {this.postProcessing.shutdown();} catch (Exception e) {System.err.println(e);}}
|
||||
System.out.println("Voxel core shut down");
|
||||
}
|
||||
|
||||
public boolean createWorldImporter(World mcWorld, File worldPath) {
|
||||
if (this.importer != null) {
|
||||
return false;
|
||||
}
|
||||
var importer = new WorldImporter(this.world, mcWorld);
|
||||
var bossBar = new ClientBossBar(MathHelper.randomUuid(), Text.of("Voxy world importer"), 0.0f, BossBar.Color.GREEN, BossBar.Style.PROGRESS, false, false, false);
|
||||
MinecraftClient.getInstance().inGameHud.getBossBarHud().bossBars.put(bossBar.getUuid(), bossBar);
|
||||
importer.importWorldAsyncStart(worldPath, 4, (a,b)->
|
||||
MinecraftClient.getInstance().executeSync(()-> {
|
||||
bossBar.setPercent(((float) a)/((float) b));
|
||||
bossBar.setName(Text.of("Voxy import: "+ a+"/"+b + " region files"));
|
||||
}),
|
||||
()-> {
|
||||
MinecraftClient.getInstance().executeSync(()-> {
|
||||
MinecraftClient.getInstance().inGameHud.getBossBarHud().bossBars.remove(bossBar.getUuid());
|
||||
String msg = "Voxy world import finished";
|
||||
MinecraftClient.getInstance().inGameHud.getChatHud().addMessage(Text.literal(msg));
|
||||
System.err.println(msg);
|
||||
});
|
||||
this.importer = null;
|
||||
});
|
||||
this.importer = importer;
|
||||
return true;
|
||||
}
|
||||
|
||||
public WorldEngine getWorldEngine() {
|
||||
return this.world;
|
||||
}
|
||||
}
|
||||
472
src/main/java/me/cortex/voxy/client/core/VoxyRenderSystem.java
Normal file
472
src/main/java/me/cortex/voxy/client/core/VoxyRenderSystem.java
Normal file
@@ -0,0 +1,472 @@
|
||||
package me.cortex.voxy.client.core;
|
||||
|
||||
import com.mojang.blaze3d.opengl.GlConst;
|
||||
import com.mojang.blaze3d.opengl.GlStateManager;
|
||||
import me.cortex.voxy.client.TimingStatistics;
|
||||
import me.cortex.voxy.client.VoxyClient;
|
||||
import me.cortex.voxy.client.config.VoxyConfig;
|
||||
import me.cortex.voxy.client.core.gl.Capabilities;
|
||||
import me.cortex.voxy.client.core.gl.GlBuffer;
|
||||
import me.cortex.voxy.client.core.gl.GlTexture;
|
||||
import me.cortex.voxy.client.core.model.ModelBakerySubsystem;
|
||||
import me.cortex.voxy.client.core.model.ModelStore;
|
||||
import me.cortex.voxy.client.core.rendering.ChunkBoundRenderer;
|
||||
import me.cortex.voxy.client.core.rendering.RenderDistanceTracker;
|
||||
import me.cortex.voxy.client.core.rendering.Viewport;
|
||||
import me.cortex.voxy.client.core.rendering.ViewportSelector;
|
||||
import me.cortex.voxy.client.core.rendering.building.RenderGenerationService;
|
||||
import me.cortex.voxy.client.core.rendering.hierachical.AsyncNodeManager;
|
||||
import me.cortex.voxy.client.core.rendering.hierachical.HierarchicalOcclusionTraverser;
|
||||
import me.cortex.voxy.client.core.rendering.hierachical.NodeCleaner;
|
||||
import me.cortex.voxy.client.core.rendering.section.AbstractSectionRenderer;
|
||||
import me.cortex.voxy.client.core.rendering.section.MDICSectionRenderer;
|
||||
import me.cortex.voxy.client.core.rendering.section.geometry.BasicSectionGeometryData;
|
||||
import me.cortex.voxy.client.core.rendering.section.geometry.IGeometryData;
|
||||
import me.cortex.voxy.client.core.rendering.util.DownloadStream;
|
||||
import me.cortex.voxy.client.core.rendering.util.PrintfDebugUtil;
|
||||
import me.cortex.voxy.client.core.rendering.util.UploadStream;
|
||||
import me.cortex.voxy.client.core.util.GPUTiming;
|
||||
import me.cortex.voxy.client.core.util.IrisUtil;
|
||||
import me.cortex.voxy.common.Logger;
|
||||
import me.cortex.voxy.common.thread.ServiceThreadPool;
|
||||
import me.cortex.voxy.common.world.WorldEngine;
|
||||
import me.cortex.voxy.commonImpl.VoxyCommon;
|
||||
import net.caffeinemc.mods.sodium.client.render.chunk.ChunkRenderMatrices;
|
||||
import net.caffeinemc.mods.sodium.client.util.FogParameters;
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
import org.joml.Matrix4f;
|
||||
import org.joml.Matrix4fc;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static org.lwjgl.opengl.GL11.GL_VIEWPORT;
|
||||
import static org.lwjgl.opengl.GL11.glGetIntegerv;
|
||||
import static org.lwjgl.opengl.GL11C.*;
|
||||
import static org.lwjgl.opengl.GL30C.*;
|
||||
import static org.lwjgl.opengl.GL33.glBindSampler;
|
||||
import static org.lwjgl.opengl.GL43.GL_SHADER_STORAGE_BUFFER;
|
||||
import static org.lwjgl.opengl.GL43C.GL_SHADER_STORAGE_BUFFER_BINDING;
|
||||
|
||||
public class VoxyRenderSystem {
|
||||
private final WorldEngine worldIn;
|
||||
|
||||
|
||||
private final ModelBakerySubsystem modelService;
|
||||
private final RenderGenerationService renderGen;
|
||||
private final IGeometryData geometryData;
|
||||
private final AsyncNodeManager nodeManager;
|
||||
private final NodeCleaner nodeCleaner;
|
||||
private final HierarchicalOcclusionTraverser traversal;
|
||||
|
||||
|
||||
private final RenderDistanceTracker renderDistanceTracker;
|
||||
public final ChunkBoundRenderer chunkBoundRenderer;
|
||||
|
||||
private final ViewportSelector<?> viewportSelector;
|
||||
|
||||
private final AbstractRenderPipeline pipeline;
|
||||
|
||||
private static AbstractSectionRenderer<?,?> createSectionRenderer(AbstractRenderPipeline pipeline, ModelStore modelStore, IGeometryData geometryData) {
|
||||
//TODO: need todo a thing where selects optimal section render based on if supports the pipeline and geometry data type
|
||||
return new MDICSectionRenderer(pipeline, modelStore, (BasicSectionGeometryData) geometryData);//We only have MDIC backend... for now
|
||||
}
|
||||
|
||||
public VoxyRenderSystem(WorldEngine world, ServiceThreadPool threadPool) {
|
||||
//Keep the world loaded, NOTE: this is done FIRST, to keep and ensure that even if the rest of loading takes more
|
||||
// than timeout, we keep the world acquired
|
||||
world.acquireRef();
|
||||
|
||||
//Fking HATE EVERYTHING AAAAAAAAAAAAAAAA
|
||||
int[] oldBufferBindings = new int[10];
|
||||
for (int i = 0; i < oldBufferBindings.length; i++) {
|
||||
oldBufferBindings[i] = glGetIntegeri(GL_SHADER_STORAGE_BUFFER_BINDING, i);
|
||||
}
|
||||
|
||||
try {
|
||||
//wait for opengl to be finished, this should hopefully ensure all memory allocations are free
|
||||
glFinish();
|
||||
glFinish();
|
||||
|
||||
this.worldIn = world;
|
||||
|
||||
long geometryCapacity = getGeometryBufferSize();
|
||||
{
|
||||
|
||||
|
||||
this.modelService = new ModelBakerySubsystem(world.getMapper());
|
||||
this.renderGen = new RenderGenerationService(world, this.modelService, threadPool, false, () -> true);
|
||||
|
||||
this.geometryData = new BasicSectionGeometryData(1 << 20, geometryCapacity);
|
||||
|
||||
this.nodeManager = new AsyncNodeManager(1 << 21, this.geometryData, this.renderGen);
|
||||
this.nodeCleaner = new NodeCleaner(this.nodeManager);
|
||||
this.traversal = new HierarchicalOcclusionTraverser(this.nodeManager, this.nodeCleaner, this.renderGen);
|
||||
|
||||
world.setDirtyCallback(this.nodeManager::worldEvent);
|
||||
|
||||
Arrays.stream(world.getMapper().getBiomeEntries()).forEach(this.modelService::addBiome);
|
||||
world.getMapper().setBiomeCallback(this.modelService::addBiome);
|
||||
|
||||
this.nodeManager.start();
|
||||
}
|
||||
|
||||
this.pipeline = RenderPipelineFactory.createPipeline(this.nodeManager, this.nodeCleaner, this.traversal, this::frexStillHasWork);
|
||||
this.pipeline.setupExtraModelBakeryData(this.modelService);//Configure the model service
|
||||
var sectionRenderer = createSectionRenderer(this.pipeline, this.modelService.getStore(), this.geometryData);
|
||||
this.pipeline.setSectionRenderer(sectionRenderer);
|
||||
this.viewportSelector = new ViewportSelector<>(sectionRenderer::createViewport);
|
||||
|
||||
{
|
||||
int minSec = MinecraftClient.getInstance().world.getBottomSectionCoord() >> 5;
|
||||
int maxSec = (MinecraftClient.getInstance().world.getTopSectionCoord() - 1) >> 5;
|
||||
|
||||
//Do some very cheeky stuff for MiB
|
||||
if (VoxyCommon.IS_MINE_IN_ABYSS) {//TODO: make this somehow configurable
|
||||
minSec = -8;
|
||||
maxSec = 7;
|
||||
}
|
||||
|
||||
this.renderDistanceTracker = new RenderDistanceTracker(20,
|
||||
minSec,
|
||||
maxSec,
|
||||
this.nodeManager::addTopLevel,
|
||||
this.nodeManager::removeTopLevel);
|
||||
|
||||
this.renderDistanceTracker.setRenderDistance(VoxyConfig.CONFIG.sectionRenderDistance);
|
||||
}
|
||||
|
||||
this.chunkBoundRenderer = new ChunkBoundRenderer(this.pipeline);
|
||||
|
||||
Logger.info("Voxy render system created with " + geometryCapacity + " geometry capacity, using pipeline '" + this.pipeline.getClass().getSimpleName() + "' with renderer '" + sectionRenderer.getClass().getSimpleName() + "'");
|
||||
} catch (RuntimeException e) {
|
||||
world.releaseRef();//If something goes wrong, we must release the world first
|
||||
throw e;
|
||||
}
|
||||
|
||||
for (int i = 0; i < oldBufferBindings.length; i++) {
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, i, oldBufferBindings[i]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public Viewport<?> setupViewport(ChunkRenderMatrices matrices, FogParameters fogParameters, double cameraX, double cameraY, double cameraZ) {
|
||||
var viewport = this.getViewport();
|
||||
if (viewport == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
//Do some very cheeky stuff for MiB
|
||||
if (VoxyCommon.IS_MINE_IN_ABYSS) {
|
||||
int sector = (((int)Math.floor(cameraX)>>4)+512)>>10;
|
||||
cameraX -= sector<<14;//10+4
|
||||
cameraY += (16+(256-32-sector*30))*16;
|
||||
}
|
||||
|
||||
//cameraY += 100;
|
||||
var projection = computeProjectionMat(matrices.projection());//RenderSystem.getProjectionMatrix();
|
||||
//var projection = ShadowMatrices.createOrthoMatrix(160, -16*300, 16*300);
|
||||
//var projection = new Matrix4f(matrices.projection());
|
||||
|
||||
int[] dims = new int[4];
|
||||
glGetIntegerv(GL_VIEWPORT, dims);
|
||||
|
||||
int width = dims[2];
|
||||
int height = dims[3];
|
||||
|
||||
{//Apply render scaling factor
|
||||
var factor = this.pipeline.getRenderScalingFactor();
|
||||
if (factor != null) {
|
||||
width = (int) (width*factor[0]);
|
||||
height = (int) (height*factor[1]);
|
||||
}
|
||||
}
|
||||
|
||||
viewport
|
||||
.setVanillaProjection(matrices.projection())
|
||||
.setProjection(projection)
|
||||
.setModelView(new Matrix4f(matrices.modelView()))
|
||||
.setCamera(cameraX, cameraY, cameraZ)
|
||||
.setScreenSize(width, height)
|
||||
.setFogParameters(fogParameters)
|
||||
.update();
|
||||
viewport.frameId++;
|
||||
|
||||
return viewport;
|
||||
}
|
||||
|
||||
public void renderOpaque(Viewport<?> viewport) {
|
||||
if (viewport == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
TimingStatistics.resetSamplers();
|
||||
|
||||
long startTime = System.nanoTime();
|
||||
TimingStatistics.all.start();
|
||||
GPUTiming.INSTANCE.marker();//Start marker
|
||||
TimingStatistics.main.start();
|
||||
|
||||
//TODO: optimize
|
||||
int[] oldBufferBindings = new int[10];
|
||||
for (int i = 0; i < oldBufferBindings.length; i++) {
|
||||
oldBufferBindings[i] = glGetIntegeri(GL_SHADER_STORAGE_BUFFER_BINDING, i);
|
||||
}
|
||||
|
||||
|
||||
int oldFB = GL11.glGetInteger(GL_DRAW_FRAMEBUFFER_BINDING);
|
||||
int boundFB = oldFB;
|
||||
|
||||
int[] dims = new int[4];
|
||||
glGetIntegerv(GL_VIEWPORT, dims);
|
||||
|
||||
glViewport(0,0, viewport.width, viewport.height);
|
||||
|
||||
//var target = DefaultTerrainRenderPasses.CUTOUT.getTarget();
|
||||
//boundFB = ((net.minecraft.client.texture.GlTexture) target.getColorAttachment()).getOrCreateFramebuffer(((GlBackend) RenderSystem.getDevice()).getFramebufferManager(), target.getDepthAttachment());
|
||||
if (boundFB == 0) {
|
||||
throw new IllegalStateException("Cannot use the default framebuffer as cannot source from it");
|
||||
}
|
||||
|
||||
//this.autoBalanceSubDivSize();
|
||||
|
||||
this.pipeline.preSetup(viewport);
|
||||
|
||||
TimingStatistics.E.start();
|
||||
if (!IrisUtil.irisShadowActive()) {
|
||||
this.chunkBoundRenderer.render(viewport);
|
||||
} else {
|
||||
viewport.depthBoundingBuffer.clear(0);
|
||||
}
|
||||
TimingStatistics.E.stop();
|
||||
|
||||
|
||||
//The entire rendering pipeline (excluding the chunkbound thing)
|
||||
this.pipeline.runPipeline(viewport, boundFB, dims[2], dims[3]);
|
||||
|
||||
|
||||
TimingStatistics.main.stop();
|
||||
TimingStatistics.postDynamic.start();
|
||||
|
||||
PrintfDebugUtil.tick();
|
||||
|
||||
//As much dynamic runtime stuff here
|
||||
{
|
||||
//Tick upload stream (this is ok to do here as upload ticking is just memory management)
|
||||
UploadStream.INSTANCE.tick();
|
||||
|
||||
while (this.renderDistanceTracker.setCenterAndProcess(viewport.cameraX, viewport.cameraZ) && VoxyClient.isFrexActive());//While FF is active, run until everything is processed
|
||||
|
||||
//Done here as is allows less gl state resetup
|
||||
this.modelService.tick(Math.max(3_000_000-(System.nanoTime()-startTime), 500_000));
|
||||
}
|
||||
GPUTiming.INSTANCE.marker();
|
||||
TimingStatistics.postDynamic.stop();
|
||||
|
||||
GPUTiming.INSTANCE.tick();
|
||||
|
||||
glBindFramebuffer(GlConst.GL_FRAMEBUFFER, oldFB);
|
||||
glViewport(dims[0], dims[1], dims[2], dims[3]);
|
||||
|
||||
{//Reset state manager stuffs
|
||||
glUseProgram(0);
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
|
||||
GlStateManager._glBindVertexArray(0);//Clear binding
|
||||
|
||||
GlStateManager._activeTexture(GlConst.GL_TEXTURE1);
|
||||
for (int i = 0; i < 12; i++) {
|
||||
GlStateManager._activeTexture(GlConst.GL_TEXTURE0+i);
|
||||
GlStateManager._bindTexture(0);
|
||||
glBindSampler(i, 0);
|
||||
}
|
||||
|
||||
IrisUtil.clearIrisSamplers();//Thanks iris (sigh)
|
||||
|
||||
//TODO: should/needto actually restore all of these, not just clear them
|
||||
//Clear all the bindings
|
||||
for (int i = 0; i < oldBufferBindings.length; i++) {
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, i, oldBufferBindings[i]);
|
||||
}
|
||||
|
||||
//((SodiumShader) Iris.getPipelineManager().getPipelineNullable().getSodiumPrograms().getProgram(DefaultTerrainRenderPasses.CUTOUT).getInterface()).setupState(DefaultTerrainRenderPasses.CUTOUT, fogParameters);
|
||||
}
|
||||
|
||||
TimingStatistics.all.stop();
|
||||
|
||||
/*
|
||||
TimingStatistics.F.start();
|
||||
this.postProcessing.setup(viewport.width, viewport.height, boundFB);
|
||||
TimingStatistics.F.stop();
|
||||
|
||||
this.renderer.renderFarAwayOpaque(viewport, this.chunkBoundRenderer.getDepthBoundTexture());
|
||||
|
||||
|
||||
TimingStatistics.F.start();
|
||||
//Compute the SSAO of the rendered terrain, TODO: fix it breaking depth or breaking _something_ am not sure what
|
||||
this.postProcessing.computeSSAO(viewport.MVP);
|
||||
TimingStatistics.F.stop();
|
||||
|
||||
TimingStatistics.G.start();
|
||||
//We can render the translucent directly after as it is the furthest translucent objects
|
||||
this.renderer.renderFarAwayTranslucent(viewport, this.chunkBoundRenderer.getDepthBoundTexture());
|
||||
TimingStatistics.G.stop();
|
||||
|
||||
|
||||
TimingStatistics.F.start();
|
||||
this.postProcessing.renderPost(viewport, matrices.projection(), boundFB);
|
||||
TimingStatistics.F.stop();
|
||||
*/
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void autoBalanceSubDivSize() {
|
||||
//only increase quality while there are very few mesh queues, this stops,
|
||||
// e.g. while flying and is rendering alot of low quality chunks
|
||||
boolean canDecreaseSize = this.renderGen.getTaskCount() < 300;
|
||||
int MIN_FPS = 55;
|
||||
int MAX_FPS = 65;
|
||||
float INCREASE_PER_SECOND = 60;
|
||||
float DECREASE_PER_SECOND = 30;
|
||||
//Auto fps targeting
|
||||
if (MinecraftClient.getInstance().getCurrentFps() < MIN_FPS) {
|
||||
VoxyConfig.CONFIG.subDivisionSize = Math.min(VoxyConfig.CONFIG.subDivisionSize + INCREASE_PER_SECOND / Math.max(1f, MinecraftClient.getInstance().getCurrentFps()), 256);
|
||||
}
|
||||
|
||||
if (MAX_FPS < MinecraftClient.getInstance().getCurrentFps() && canDecreaseSize) {
|
||||
VoxyConfig.CONFIG.subDivisionSize = Math.max(VoxyConfig.CONFIG.subDivisionSize - DECREASE_PER_SECOND / Math.max(1f, MinecraftClient.getInstance().getCurrentFps()), 28);
|
||||
}
|
||||
}
|
||||
|
||||
private static Matrix4f makeProjectionMatrix(float near, float far) {
|
||||
//TODO: use the existing projection matrix use mulLocal by the inverse of the projection and then mulLocal our projection
|
||||
|
||||
var projection = new Matrix4f();
|
||||
var client = MinecraftClient.getInstance();
|
||||
var gameRenderer = client.gameRenderer;//tickCounter.getTickDelta(true);
|
||||
|
||||
float fov = gameRenderer.getFov(gameRenderer.getCamera(), client.getRenderTickCounter().getTickProgress(true), true);
|
||||
|
||||
projection.setPerspective(fov * 0.01745329238474369f,
|
||||
(float) client.getWindow().getFramebufferWidth() / (float)client.getWindow().getFramebufferHeight(),
|
||||
near, far);
|
||||
return projection;
|
||||
}
|
||||
|
||||
//TODO: Make a reverse z buffer
|
||||
private static Matrix4f computeProjectionMat(Matrix4fc base) {
|
||||
return base.mulLocal(
|
||||
makeProjectionMatrix(0.05f, MinecraftClient.getInstance().gameRenderer.getFarPlaneDistance()).invert(),
|
||||
new Matrix4f()
|
||||
).mulLocal(makeProjectionMatrix(16, 16*3000));
|
||||
}
|
||||
|
||||
private boolean frexStillHasWork() {
|
||||
if (!VoxyClient.isFrexActive()) {
|
||||
return false;
|
||||
}
|
||||
//If frex is running we must tick everything to ensure correctness
|
||||
UploadStream.INSTANCE.tick();
|
||||
//Done here as is allows less gl state resetup
|
||||
this.modelService.tick(100_000_000);
|
||||
GL11.glFinish();
|
||||
return this.nodeManager.hasWork() || this.renderGen.getTaskCount()!=0 || !this.modelService.areQueuesEmpty();
|
||||
}
|
||||
|
||||
public void setRenderDistance(int renderDistance) {
|
||||
this.renderDistanceTracker.setRenderDistance(renderDistance);
|
||||
}
|
||||
|
||||
public Viewport<?> getViewport() {
|
||||
if (IrisUtil.irisShadowActive()) {
|
||||
return null;
|
||||
}
|
||||
return this.viewportSelector.getViewport();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public void addDebugInfo(List<String> debug) {
|
||||
debug.add("Buf/Tex [#/Mb]: [" + GlBuffer.getCount() + "/" + (GlBuffer.getTotalSize()/1_000_000) + "],[" + GlTexture.getCount() + "/" + (GlTexture.getEstimatedTotalSize()/1_000_000)+"]");
|
||||
{
|
||||
this.modelService.addDebugData(debug);
|
||||
this.renderGen.addDebugData(debug);
|
||||
this.nodeManager.addDebug(debug);
|
||||
this.pipeline.addDebug(debug);
|
||||
}
|
||||
{
|
||||
TimingStatistics.update();
|
||||
debug.add("Voxy frame runtime (millis): " + TimingStatistics.dynamic.pVal() + ", " + TimingStatistics.main.pVal()+ ", " + TimingStatistics.postDynamic.pVal()+ ", " + TimingStatistics.all.pVal());
|
||||
debug.add("Extra time: " + TimingStatistics.A.pVal() + ", " + TimingStatistics.B.pVal() + ", " + TimingStatistics.C.pVal() + ", " + TimingStatistics.D.pVal());
|
||||
debug.add("Extra 2 time: " + TimingStatistics.E.pVal() + ", " + TimingStatistics.F.pVal() + ", " + TimingStatistics.G.pVal() + ", " + TimingStatistics.H.pVal() + ", " + TimingStatistics.I.pVal());
|
||||
}
|
||||
PrintfDebugUtil.addToOut(debug);
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
Logger.info("Flushing download stream");
|
||||
DownloadStream.INSTANCE.flushWaitClear();
|
||||
Logger.info("Shutting down rendering");
|
||||
try {
|
||||
//Cleanup callbacks
|
||||
this.worldIn.setDirtyCallback(null);
|
||||
this.worldIn.getMapper().setBiomeCallback(null);
|
||||
this.worldIn.getMapper().setStateCallback(null);
|
||||
|
||||
this.nodeManager.stop();
|
||||
|
||||
this.modelService.shutdown();
|
||||
this.renderGen.shutdown();
|
||||
this.traversal.free();
|
||||
this.nodeCleaner.free();
|
||||
|
||||
this.geometryData.free();
|
||||
this.chunkBoundRenderer.free();
|
||||
|
||||
this.viewportSelector.free();
|
||||
} catch (Exception e) {Logger.error("Error shutting down renderer components", e);}
|
||||
Logger.info("Shutting down render pipeline");
|
||||
try {this.pipeline.free();} catch (Exception e){Logger.error("Error releasing render pipeline", e);}
|
||||
|
||||
|
||||
|
||||
Logger.info("Flushing download stream");
|
||||
DownloadStream.INSTANCE.flushWaitClear();
|
||||
|
||||
//Release hold on the world
|
||||
this.worldIn.releaseRef();
|
||||
Logger.info("Render shutdown completed");
|
||||
}
|
||||
|
||||
private static long getGeometryBufferSize() {
|
||||
long geometryCapacity = Math.min((1L<<(64-Long.numberOfLeadingZeros(Capabilities.INSTANCE.ssboMaxSize-1)))<<1, 1L<<32)-1024/*(1L<<32)-1024*/;
|
||||
if (Capabilities.INSTANCE.isIntel) {
|
||||
geometryCapacity = Math.max(geometryCapacity, 1L<<30);//intel moment, force min 1gb
|
||||
}
|
||||
|
||||
//Limit to available dedicated memory if possible
|
||||
if (Capabilities.INSTANCE.canQueryGpuMemory) {
|
||||
//512mb less than avalible,
|
||||
long limit = Capabilities.INSTANCE.getFreeDedicatedGpuMemory() - (long)(1.5*1024*1024*1024);//1.5gb vram buffer
|
||||
// Give a minimum of 512 mb requirement
|
||||
limit = Math.max(512*1024*1024, limit);
|
||||
|
||||
geometryCapacity = Math.min(geometryCapacity, limit);
|
||||
}
|
||||
//geometryCapacity = 1<<28;
|
||||
//geometryCapacity = 1<<30;//1GB test
|
||||
var override = System.getProperty("voxy.geometryBufferSizeOverrideMB", "");
|
||||
if (!override.isEmpty()) {
|
||||
geometryCapacity = Long.parseLong(override)*1024L*1024L;
|
||||
}
|
||||
return geometryCapacity;
|
||||
}
|
||||
|
||||
public WorldEngine getEngine() {
|
||||
return this.worldIn;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package me.cortex.voxy.client.core.gl;
|
||||
|
||||
import me.cortex.voxy.client.core.gl.shader.ShaderType;
|
||||
import org.lwjgl.opengl.GL;
|
||||
import org.lwjgl.opengl.GL20C;
|
||||
|
||||
import static org.lwjgl.opengl.GL11.*;
|
||||
import static org.lwjgl.opengl.GL32.glGetInteger64;
|
||||
import static org.lwjgl.opengl.GL43C.GL_MAX_SHADER_STORAGE_BLOCK_SIZE;
|
||||
import static org.lwjgl.opengl.NVXGPUMemoryInfo.*;
|
||||
|
||||
public class Capabilities {
|
||||
|
||||
public static final Capabilities INSTANCE = new Capabilities();
|
||||
|
||||
public final boolean repFragTest;
|
||||
public final boolean meshShaders;
|
||||
public final boolean INT64_t;
|
||||
public final long ssboMaxSize;
|
||||
public final boolean isMesa;
|
||||
public final boolean canQueryGpuMemory;
|
||||
public final long totalDedicatedMemory;//Bytes, dedicated memory
|
||||
public final long totalDynamicMemory;//Bytes, total allocation memory - dedicated memory
|
||||
public final boolean compute;
|
||||
public final boolean indirectParameters;
|
||||
public final boolean isIntel;
|
||||
public final boolean subgroup;
|
||||
public final boolean sparseBuffer;
|
||||
public final boolean isNvidia;
|
||||
|
||||
public Capabilities() {
|
||||
var cap = GL.getCapabilities();
|
||||
this.sparseBuffer = cap.GL_ARB_sparse_buffer;
|
||||
this.compute = cap.glDispatchComputeIndirect != 0;
|
||||
this.indirectParameters = cap.glMultiDrawElementsIndirectCountARB != 0;
|
||||
this.repFragTest = cap.GL_NV_representative_fragment_test;
|
||||
this.meshShaders = cap.GL_NV_mesh_shader;
|
||||
this.canQueryGpuMemory = cap.GL_NVX_gpu_memory_info;
|
||||
//this.INT64_t = cap.GL_ARB_gpu_shader_int64 || cap.GL_AMD_gpu_shader_int64;
|
||||
//The only reliable way to test for int64 support is to try compile a shader
|
||||
this.INT64_t = testShaderCompilesOk(ShaderType.COMPUTE, """
|
||||
#version 430
|
||||
#extension GL_ARB_gpu_shader_int64 : require
|
||||
layout(local_size_x=32) in;
|
||||
void main() {
|
||||
uint64_t a = 1234;
|
||||
}
|
||||
""");
|
||||
if (cap.GL_KHR_shader_subgroup) {
|
||||
this.subgroup = testShaderCompilesOk(ShaderType.COMPUTE, """
|
||||
#version 430
|
||||
#extension GL_KHR_shader_subgroup_basic : require
|
||||
#extension GL_KHR_shader_subgroup_arithmetic : require
|
||||
layout(local_size_x=32) in;
|
||||
void main() {
|
||||
uint a = subgroupExclusiveAdd(gl_LocalInvocationIndex);
|
||||
}
|
||||
""");
|
||||
} else {
|
||||
this.subgroup = false;
|
||||
}
|
||||
|
||||
this.ssboMaxSize = glGetInteger64(GL_MAX_SHADER_STORAGE_BLOCK_SIZE);
|
||||
|
||||
this.isMesa = glGetString(GL_VERSION).toLowerCase().contains("mesa");
|
||||
this.isIntel = glGetString(GL_VENDOR).toLowerCase().contains("intel");
|
||||
this.isNvidia = glGetString(GL_VENDOR).toLowerCase().contains("nvidia");
|
||||
|
||||
if (this.canQueryGpuMemory) {
|
||||
this.totalDedicatedMemory = glGetInteger64(GL_GPU_MEMORY_INFO_DEDICATED_VIDMEM_NVX)*1024;//Since its in Kb
|
||||
this.totalDynamicMemory = (glGetInteger64(GL_GPU_MEMORY_INFO_TOTAL_AVAILABLE_MEMORY_NVX)*1024) - this.totalDedicatedMemory;//Since its in Kb
|
||||
} else {
|
||||
this.totalDedicatedMemory = -1;
|
||||
this.totalDynamicMemory = -1;
|
||||
}
|
||||
}
|
||||
|
||||
public static void init() {
|
||||
}
|
||||
|
||||
private static boolean testShaderCompilesOk(ShaderType type, String src) {
|
||||
int shader = GL20C.glCreateShader(type.gl);
|
||||
GL20C.glShaderSource(shader, src);
|
||||
GL20C.glCompileShader(shader);
|
||||
int result = GL20C.glGetShaderi(shader, GL20C.GL_COMPILE_STATUS);
|
||||
GL20C.glDeleteShader(shader);
|
||||
|
||||
return result == GL20C.GL_TRUE;
|
||||
}
|
||||
|
||||
public long getFreeDedicatedGpuMemory() {
|
||||
if (!this.canQueryGpuMemory) {
|
||||
throw new IllegalStateException("Cannot query gpu memory, missing extension");
|
||||
}
|
||||
return glGetInteger64(GL_GPU_MEMORY_INFO_CURRENT_AVAILABLE_VIDMEM_NVX)*1024;//Since its in Kb
|
||||
}
|
||||
|
||||
//TODO: add gpu eviction tracking
|
||||
}
|
||||
@@ -1,33 +1,95 @@
|
||||
package me.cortex.voxy.client.core.gl;
|
||||
|
||||
import me.cortex.voxy.common.util.TrackedObject;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
import org.lwjgl.system.MemoryUtil;
|
||||
|
||||
import static org.lwjgl.opengl.ARBSparseBuffer.GL_SPARSE_STORAGE_BIT_ARB;
|
||||
import static org.lwjgl.opengl.GL11.GL_UNSIGNED_BYTE;
|
||||
import static org.lwjgl.opengl.GL15.glDeleteBuffers;
|
||||
import static org.lwjgl.opengl.GL44C.glBufferStorage;
|
||||
import static org.lwjgl.opengl.GL45C.glCreateBuffers;
|
||||
import static org.lwjgl.opengl.GL45C.glNamedBufferStorage;
|
||||
import static org.lwjgl.opengl.GL45C.*;
|
||||
|
||||
public class GlBuffer extends TrackedObject {
|
||||
public final int id;
|
||||
private final long size;
|
||||
private final int flags;
|
||||
|
||||
private static int COUNT;
|
||||
private static long TOTAL_SIZE;
|
||||
|
||||
public GlBuffer(long size) {
|
||||
this(size, 0);
|
||||
}
|
||||
public GlBuffer(long size, boolean zero) {
|
||||
this(size, 0, zero);
|
||||
}
|
||||
|
||||
public GlBuffer(long size, int flags) {
|
||||
this(size, flags, true);
|
||||
}
|
||||
|
||||
public GlBuffer(long size, int flags, boolean zero) {
|
||||
this.flags = flags;
|
||||
this.id = glCreateBuffers();
|
||||
this.size = size;
|
||||
glNamedBufferStorage(this.id, size, flags);
|
||||
if ((flags&GL_SPARSE_STORAGE_BIT_ARB)==0 && zero) {
|
||||
this.zero();
|
||||
}
|
||||
|
||||
COUNT++;
|
||||
TOTAL_SIZE += size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void free() {
|
||||
this.free0();
|
||||
glDeleteBuffers(this.id);
|
||||
|
||||
COUNT--;
|
||||
TOTAL_SIZE -= this.size;
|
||||
}
|
||||
|
||||
public boolean isSparse() {
|
||||
return (this.flags&GL_SPARSE_STORAGE_BIT_ARB)!=0;
|
||||
}
|
||||
|
||||
public long size() {
|
||||
return this.size;
|
||||
}
|
||||
|
||||
public GlBuffer zero() {
|
||||
nglClearNamedBufferData(this.id, GL_R8UI, GL_RED_INTEGER, GL_UNSIGNED_BYTE, 0);
|
||||
return this;
|
||||
}
|
||||
|
||||
public GlBuffer zeroRange(long offset, long size) {
|
||||
nglClearNamedBufferSubData(this.id, GL_R8UI, offset, size, GL_RED_INTEGER, GL_UNSIGNED_BYTE, 0);
|
||||
return this;
|
||||
}
|
||||
|
||||
public GlBuffer fill(int data) {
|
||||
//Clear unpack values
|
||||
//Fixed in mesa commit a5c3c452
|
||||
glPixelStorei(GL11.GL_UNPACK_SKIP_ROWS, 0);
|
||||
glPixelStorei(GL11.GL_UNPACK_SKIP_PIXELS, 0);
|
||||
|
||||
MemoryUtil.memPutInt(SCRATCH, data);
|
||||
nglClearNamedBufferData(this.id, GL_R32UI, GL_RED_INTEGER, GL_UNSIGNED_INT, SCRATCH);
|
||||
return this;
|
||||
}
|
||||
|
||||
public static int getCount() {
|
||||
return COUNT;
|
||||
}
|
||||
|
||||
public static long getTotalSize() {
|
||||
return TOTAL_SIZE;
|
||||
}
|
||||
|
||||
public GlBuffer name(String name) {
|
||||
return GlDebug.name(name, this);
|
||||
}
|
||||
|
||||
private static final long SCRATCH = MemoryUtil.nmemAlloc(4);
|
||||
}
|
||||
|
||||
49
src/main/java/me/cortex/voxy/client/core/gl/GlDebug.java
Normal file
49
src/main/java/me/cortex/voxy/client/core/gl/GlDebug.java
Normal file
@@ -0,0 +1,49 @@
|
||||
package me.cortex.voxy.client.core.gl;
|
||||
|
||||
import me.cortex.voxy.client.core.gl.shader.Shader;
|
||||
|
||||
import static org.lwjgl.opengl.GL43C.*;
|
||||
|
||||
public class GlDebug {
|
||||
public static final boolean GL_DEBUG = System.getProperty("voxy.glDebug", "false").equals("true");
|
||||
|
||||
|
||||
public static void push() {
|
||||
//glPushDebugGroup()
|
||||
}
|
||||
|
||||
public static GlBuffer name(String name, GlBuffer buffer) {
|
||||
if (GL_DEBUG) {
|
||||
glObjectLabel(GL_BUFFER, buffer.id, name);
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
public static <T extends Shader> T name(String name, T shader) {
|
||||
if (GL_DEBUG) {
|
||||
glObjectLabel(GL_PROGRAM, shader.id(), name);
|
||||
}
|
||||
return shader;
|
||||
}
|
||||
|
||||
public static GlFramebuffer name(String name, GlFramebuffer framebuffer) {
|
||||
if (GL_DEBUG) {
|
||||
glObjectLabel(GL_FRAMEBUFFER, framebuffer.id, name);
|
||||
}
|
||||
return framebuffer;
|
||||
}
|
||||
|
||||
public static GlTexture name(String name, GlTexture texture) {
|
||||
if (GL_DEBUG) {
|
||||
glObjectLabel(GL_TEXTURE, texture.id, name);
|
||||
}
|
||||
return texture;
|
||||
}
|
||||
|
||||
public static GlPersistentMappedBuffer name(String name, GlPersistentMappedBuffer buffer) {
|
||||
if (GL_DEBUG) {
|
||||
glObjectLabel(GL_BUFFER, buffer.id, name);
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package me.cortex.voxy.client.core.gl;
|
||||
|
||||
import me.cortex.voxy.common.util.TrackedObject;
|
||||
import org.lwjgl.system.MemoryUtil;
|
||||
|
||||
import static org.lwjgl.opengl.GL32.*;
|
||||
|
||||
@@ -12,13 +13,24 @@ public class GlFence extends TrackedObject {
|
||||
this.fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
|
||||
}
|
||||
|
||||
private static final long SCRATCH = MemoryUtil.nmemCalloc(1,4);
|
||||
|
||||
public boolean signaled() {
|
||||
if (!this.signaled) {
|
||||
/*
|
||||
int ret = glClientWaitSync(this.fence, 0, 0);
|
||||
if (ret == GL_ALREADY_SIGNALED || ret == GL_CONDITION_SATISFIED) {
|
||||
this.signaled = true;
|
||||
} else if (ret != GL_TIMEOUT_EXPIRED) {
|
||||
throw new IllegalStateException("Poll for fence failed, glError: " + glGetError());
|
||||
}*/
|
||||
MemoryUtil.memPutInt(SCRATCH, -1);
|
||||
nglGetSynciv(this.fence, GL_SYNC_STATUS, 1, 0, SCRATCH);
|
||||
int val = MemoryUtil.memGetInt(SCRATCH);
|
||||
if (val == GL_SIGNALED) {
|
||||
this.signaled = true;
|
||||
} else if (val != GL_UNSIGNALED) {
|
||||
throw new IllegalStateException("Unknown data from glGetSync: "+val);
|
||||
}
|
||||
}
|
||||
return this.signaled;
|
||||
|
||||
@@ -3,6 +3,7 @@ package me.cortex.voxy.client.core.gl;
|
||||
import me.cortex.voxy.common.util.TrackedObject;
|
||||
|
||||
import static org.lwjgl.opengl.GL45C.*;
|
||||
import static org.lwjgl.opengl.GL45C.glNamedFramebufferDrawBuffers;
|
||||
|
||||
public class GlFramebuffer extends TrackedObject {
|
||||
public final int id;
|
||||
@@ -19,6 +20,16 @@ public class GlFramebuffer extends TrackedObject {
|
||||
return this;
|
||||
}
|
||||
|
||||
public GlFramebuffer bind(int attachment, GlRenderBuffer buffer) {
|
||||
glNamedFramebufferRenderbuffer(this.id, attachment, GL_RENDERBUFFER, buffer.id);
|
||||
return this;
|
||||
}
|
||||
|
||||
public GlFramebuffer setDrawBuffers(int... buffers) {
|
||||
glNamedFramebufferDrawBuffers(this.id, buffers);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void free() {
|
||||
super.free0();
|
||||
@@ -32,4 +43,9 @@ public class GlFramebuffer extends TrackedObject {
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
public GlFramebuffer name(String name) {
|
||||
return GlDebug.name(name, this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,14 +13,14 @@ public class GlPersistentMappedBuffer extends TrackedObject {
|
||||
public GlPersistentMappedBuffer(long size, int flags) {
|
||||
this.id = glCreateBuffers();
|
||||
this.size = size;
|
||||
glNamedBufferStorage(this.id, size, GL_CLIENT_STORAGE_BIT|GL_MAP_PERSISTENT_BIT|(flags&(GL_MAP_COHERENT_BIT|GL_MAP_WRITE_BIT|GL_MAP_READ_BIT)));
|
||||
glNamedBufferStorage(this.id, size, GL_MAP_PERSISTENT_BIT|(flags&(GL_MAP_COHERENT_BIT|GL_MAP_WRITE_BIT|GL_MAP_READ_BIT|GL_CLIENT_STORAGE_BIT)));
|
||||
this.addr = nglMapNamedBufferRange(this.id, 0, size, (flags&(GL_MAP_WRITE_BIT|GL_MAP_READ_BIT|GL_MAP_UNSYNCHRONIZED_BIT|GL_MAP_FLUSH_EXPLICIT_BIT))|GL_MAP_PERSISTENT_BIT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void free() {
|
||||
this.free0();
|
||||
glUnmapBuffer(this.id);
|
||||
glUnmapNamedBuffer(this.id);
|
||||
glDeleteBuffers(this.id);
|
||||
}
|
||||
|
||||
@@ -31,4 +31,8 @@ public class GlPersistentMappedBuffer extends TrackedObject {
|
||||
public long addr() {
|
||||
return this.addr;
|
||||
}
|
||||
|
||||
public GlPersistentMappedBuffer name(String name) {
|
||||
return GlDebug.name(name, this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package me.cortex.voxy.client.core.gl;
|
||||
|
||||
import me.cortex.voxy.common.util.TrackedObject;
|
||||
|
||||
import static org.lwjgl.opengl.GL45C.*;
|
||||
|
||||
public class GlRenderBuffer extends TrackedObject {
|
||||
public final int id;
|
||||
|
||||
public GlRenderBuffer(int format, int width, int height) {
|
||||
this.id = glCreateRenderbuffers();
|
||||
glNamedRenderbufferStorage(this.id, format, width, height);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void free() {
|
||||
super.free0();
|
||||
glDeleteRenderbuffers(this.id);
|
||||
}
|
||||
}
|
||||
@@ -2,15 +2,23 @@ package me.cortex.voxy.client.core.gl;
|
||||
|
||||
import me.cortex.voxy.common.util.TrackedObject;
|
||||
|
||||
import static org.lwjgl.opengl.ARBFramebufferObject.glDeleteFramebuffers;
|
||||
import static org.lwjgl.opengl.ARBFramebufferObject.glGenFramebuffers;
|
||||
import static org.lwjgl.opengl.GL11.GL_RGBA8;
|
||||
import static org.lwjgl.opengl.GL11C.*;
|
||||
import static org.lwjgl.opengl.GL30C.glGetIntegeri;
|
||||
import static org.lwjgl.opengl.GL30.GL_DEPTH24_STENCIL8;
|
||||
import static org.lwjgl.opengl.GL45C.*;
|
||||
|
||||
public class GlTexture extends TrackedObject {
|
||||
public final int id;
|
||||
private final int type;
|
||||
private int format;
|
||||
private int width;
|
||||
private int height;
|
||||
private int levels;
|
||||
private boolean hasAllocated;
|
||||
|
||||
private static int COUNT;
|
||||
private static long ESTIMATED_TOTAL_SIZE;
|
||||
|
||||
public GlTexture() {
|
||||
this(GL_TEXTURE_2D);
|
||||
}
|
||||
@@ -18,32 +26,106 @@ public class GlTexture extends TrackedObject {
|
||||
public GlTexture(int type) {
|
||||
this.id = glCreateTextures(type);
|
||||
this.type = type;
|
||||
COUNT++;
|
||||
}
|
||||
|
||||
private GlTexture(int type, boolean useGenTypes) {
|
||||
if (useGenTypes) {
|
||||
this.id = glGenTextures();
|
||||
} else {
|
||||
this.id = glCreateTextures(type);
|
||||
}
|
||||
this.type = type;
|
||||
COUNT++;
|
||||
}
|
||||
|
||||
public GlTexture store(int format, int levels, int width, int height) {
|
||||
if (this.hasAllocated) {
|
||||
throw new IllegalStateException("Texture already allocated");
|
||||
}
|
||||
this.hasAllocated = true;
|
||||
|
||||
this.format = format;
|
||||
if (this.type == GL_TEXTURE_2D) {
|
||||
glTextureStorage2D(this.id, levels, format, width, height);
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.levels = levels;
|
||||
} else {
|
||||
throw new IllegalStateException("Unknown texture type");
|
||||
}
|
||||
ESTIMATED_TOTAL_SIZE += this.getEstimatedSize();
|
||||
return this;
|
||||
}
|
||||
|
||||
public GlTexture createView() {
|
||||
this.assertAllocated();
|
||||
var view = new GlTexture(this.type, true);
|
||||
glTextureView(view.id, this.type, this.id, this.format, 0, 1, 0, 1);
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void free() {
|
||||
if (this.hasAllocated) {
|
||||
ESTIMATED_TOTAL_SIZE -= this.getEstimatedSize();
|
||||
}
|
||||
COUNT--;
|
||||
this.hasAllocated = false;
|
||||
super.free0();
|
||||
glDeleteTextures(this.id);
|
||||
}
|
||||
|
||||
//TODO: FIXME, glGetTextureParameteri doesnt work
|
||||
public static int getRawTextureType(int texture) {
|
||||
if (!glIsTexture(texture)) {
|
||||
throw new IllegalStateException("Not texture");
|
||||
public GlTexture name(String name) {
|
||||
this.assertAllocated();
|
||||
return GlDebug.name(name, this);
|
||||
}
|
||||
|
||||
public int getWidth() {
|
||||
this.assertAllocated();
|
||||
return this.width;
|
||||
}
|
||||
|
||||
public int getHeight() {
|
||||
this.assertAllocated();
|
||||
return this.height;
|
||||
}
|
||||
|
||||
public int getLevels() {
|
||||
this.assertAllocated();
|
||||
return this.levels;
|
||||
}
|
||||
|
||||
private long getEstimatedSize() {
|
||||
this.assertAllocated();
|
||||
long elemSize = switch (this.format) {
|
||||
case GL_R32UI, GL_RGBA8, GL_DEPTH24_STENCIL8, GL_R32F -> 4;
|
||||
case GL_DEPTH_COMPONENT24 -> 4;//TODO: check this is right????
|
||||
case GL_DEPTH_COMPONENT32F -> 4;
|
||||
case GL_DEPTH_COMPONENT32 -> 4;
|
||||
|
||||
default -> throw new IllegalStateException("Unknown element size");
|
||||
};
|
||||
|
||||
long size = 0;
|
||||
for (int lvl = 0; lvl < this.levels; lvl++) {
|
||||
size += Math.max((((long)this.width)>>lvl), 1) * Math.max((((long)this.height)>>lvl), 1) * elemSize;
|
||||
}
|
||||
int immFormat = glGetTextureParameteri(texture, GL_TEXTURE_IMMUTABLE_FORMAT);
|
||||
if (immFormat == 0) {
|
||||
throw new IllegalStateException("Texture: " + texture + " is not immutable");
|
||||
return size;
|
||||
}
|
||||
|
||||
public void assertAllocated() {
|
||||
if (!this.hasAllocated) {
|
||||
throw new IllegalStateException("Texture not yet allocated");
|
||||
}
|
||||
return immFormat;
|
||||
}
|
||||
|
||||
|
||||
public static int getCount() {
|
||||
return COUNT;
|
||||
}
|
||||
|
||||
public static long getEstimatedTotalSize() {
|
||||
return ESTIMATED_TOTAL_SIZE;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package me.cortex.voxy.client.core.gl;
|
||||
|
||||
import me.cortex.voxy.common.util.TrackedObject;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import static org.lwjgl.opengl.GL30.glGenVertexArrays;
|
||||
import static org.lwjgl.opengl.GL45C.*;
|
||||
|
||||
public class GlVertexArray extends TrackedObject {
|
||||
public static final int STATIC_VAO = glGenVertexArrays();
|
||||
|
||||
public final int id;
|
||||
private int[] indices = new int[0];
|
||||
private int stride;
|
||||
public GlVertexArray() {
|
||||
this.id = glCreateVertexArrays();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void free() {
|
||||
this.free0();
|
||||
glDeleteVertexArrays(this.id);
|
||||
}
|
||||
|
||||
public void bind() {
|
||||
glBindVertexArray(this.id);
|
||||
}
|
||||
|
||||
public GlVertexArray bindBuffer(int buffer) {
|
||||
//TODO: optimization, use glVertexArrayVertexBuffers
|
||||
for (int index : this.indices) {
|
||||
glVertexArrayVertexBuffer(this.id, index, buffer, 0, this.stride);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public GlVertexArray bindElementBuffer(int buffer) {
|
||||
glVertexArrayElementBuffer(this.id, buffer);
|
||||
return this;
|
||||
}
|
||||
|
||||
public GlVertexArray setStride(int stride) {
|
||||
this.stride = stride;
|
||||
return this;
|
||||
}
|
||||
|
||||
public GlVertexArray setI(int index, int type, int count, int offset) {
|
||||
this.addIndex(index);
|
||||
glEnableVertexArrayAttrib(this.id, index);
|
||||
glVertexArrayAttribIFormat(this.id, index, count, type, offset);
|
||||
return this;
|
||||
}
|
||||
|
||||
public GlVertexArray setF(int index, int type, int count, int offset) {
|
||||
return this.setF(index, type, count, false, offset);
|
||||
}
|
||||
|
||||
public GlVertexArray setF(int index, int type, int count, boolean normalize, int offset) {
|
||||
this.addIndex(index);
|
||||
glEnableVertexArrayAttrib(this.id, index);
|
||||
glVertexArrayAttribFormat(this.id, index, count, type, normalize, offset);
|
||||
return this;
|
||||
}
|
||||
|
||||
private void addIndex(int index) {
|
||||
for (int i : this.indices) {
|
||||
if (i == index) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.indices = Arrays.copyOf(this.indices, this.indices.length+1);
|
||||
this.indices[this.indices.length-1] = index;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package me.cortex.voxy.client.core.gl.shader;
|
||||
|
||||
import me.cortex.voxy.client.core.gl.GlBuffer;
|
||||
import me.cortex.voxy.client.core.gl.GlDebug;
|
||||
import me.cortex.voxy.client.core.gl.GlTexture;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.lwjgl.opengl.ARBDirectStateAccess.glBindTextureUnit;
|
||||
import static org.lwjgl.opengl.GL30.glBindBufferBase;
|
||||
import static org.lwjgl.opengl.GL30.glBindBufferRange;
|
||||
import static org.lwjgl.opengl.GL31.GL_UNIFORM_BUFFER;
|
||||
import static org.lwjgl.opengl.GL33.glBindSampler;
|
||||
import static org.lwjgl.opengl.GL43.GL_SHADER_STORAGE_BUFFER;
|
||||
|
||||
|
||||
//TODO: rewrite the entire shader builder system
|
||||
public class AutoBindingShader extends Shader {
|
||||
|
||||
private record BufferBinding(int target, int index, GlBuffer buffer, long offset, long size) {}
|
||||
private record TextureBinding(int unit, int sampler, GlTexture texture) {}
|
||||
|
||||
private final Map<String, String> defines;
|
||||
private final List<BufferBinding> bindings = new ArrayList<>();
|
||||
private final List<TextureBinding> textureBindings = new ArrayList<>();
|
||||
|
||||
private boolean rebuild = true;
|
||||
|
||||
AutoBindingShader(Shader.Builder<AutoBindingShader> builder, int program) {
|
||||
super(program);
|
||||
this.defines = builder.defines;
|
||||
}
|
||||
|
||||
public AutoBindingShader name(String name) {
|
||||
return GlDebug.name(name, this);
|
||||
}
|
||||
|
||||
public AutoBindingShader ssboIf(String define, GlBuffer buffer) {
|
||||
if (this.defines.containsKey(define)) {
|
||||
return this.ssbo(define, buffer);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public AutoBindingShader ssbo(int index, GlBuffer binding) {
|
||||
return this.ssbo(index, binding, 0);
|
||||
}
|
||||
|
||||
public AutoBindingShader ssbo(String define, GlBuffer binding) {
|
||||
return this.ssbo(Integer.parseInt(this.defines.get(define)), binding, 0);
|
||||
}
|
||||
|
||||
public AutoBindingShader ssbo(int index, GlBuffer buffer, long offset) {
|
||||
this.insertOrReplaceBinding(new BufferBinding(GL_SHADER_STORAGE_BUFFER, index, buffer, offset, -1));
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
public AutoBindingShader ubo(String define, GlBuffer buffer) {
|
||||
return this.ubo(Integer.parseInt(this.defines.get(define)), buffer);
|
||||
}
|
||||
|
||||
public AutoBindingShader ubo(int index, GlBuffer buffer) {
|
||||
return this.ubo(index, buffer, 0);
|
||||
}
|
||||
|
||||
public AutoBindingShader ubo(int index, GlBuffer buffer, long offset) {
|
||||
this.insertOrReplaceBinding(new BufferBinding(GL_UNIFORM_BUFFER, index, buffer, offset, -1));
|
||||
return this;
|
||||
}
|
||||
|
||||
private void insertOrReplaceBinding(BufferBinding binding) {
|
||||
this.rebuild = true;
|
||||
|
||||
//Check if there is already a binding at the index with the target, if so, replace it
|
||||
for (int i = 0; i < this.bindings.size(); i++) {
|
||||
var entry = this.bindings.get(i);
|
||||
if (entry.target == binding.target && entry.index == binding.index) {
|
||||
this.bindings.set(i, binding);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
//Else add the new binding
|
||||
this.bindings.add(binding);
|
||||
}
|
||||
|
||||
public AutoBindingShader texture(String define, GlTexture texture) {
|
||||
return this.texture(define, -1, texture);
|
||||
}
|
||||
|
||||
public AutoBindingShader texture(String define, int sampler, GlTexture texture) {
|
||||
return this.texture(Integer.parseInt(this.defines.get(define)), sampler, texture);
|
||||
}
|
||||
|
||||
public AutoBindingShader texture(int unit, int sampler, GlTexture texture) {
|
||||
this.rebuild = true;
|
||||
|
||||
for (int i = 0; i < this.textureBindings.size(); i++) {
|
||||
var entry = this.textureBindings.get(i);
|
||||
if (entry.unit == unit) {
|
||||
this.textureBindings.set(i, new TextureBinding(unit, sampler, texture));
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
this.textureBindings.add(new TextureBinding(unit, sampler, texture));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind() {
|
||||
super.bind();
|
||||
//TODO: replace with multibind and use the invalidate flag
|
||||
/*
|
||||
glBindSamplers();
|
||||
glBindTextures();
|
||||
glBindBuffersBase();
|
||||
glBindBuffersRange();
|
||||
*/
|
||||
if (!this.bindings.isEmpty()) {
|
||||
for (var binding : this.bindings) {
|
||||
binding.buffer.assertNotFreed();
|
||||
if (binding.offset == 0 && binding.size == -1) {
|
||||
glBindBufferBase(binding.target, binding.index, binding.buffer.id);
|
||||
} else {
|
||||
glBindBufferRange(binding.target, binding.index, binding.buffer.id, binding.offset, binding.size);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!this.textureBindings.isEmpty()) {
|
||||
for (var binding : this.textureBindings) {
|
||||
if (binding.texture != null) {
|
||||
binding.texture.assertNotFreed();
|
||||
glBindTextureUnit(binding.unit, binding.texture.id);
|
||||
}
|
||||
if (binding.sampler != -1) {
|
||||
glBindSampler(binding.unit, binding.sampler);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package me.cortex.voxy.client.core.gl.shader;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class GenericsProcessor implements IShaderProcessor {
|
||||
private static final Pattern GENERIC_DEFINE = Pattern.compile("#defineGen (?<name>[A-Za-z0-9]+)<(?<generic>[A-Za-z0-9]*)>");
|
||||
private static final Pattern GENERIC_USE = Pattern.compile("(?<type>[A-Za-z0-9]+)<(?<generic>[A-Za-z0-9]*)>");
|
||||
@Override
|
||||
public String process(ShaderType type, String source) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
package me.cortex.voxy.client.core.gl.shader;
|
||||
|
||||
import me.cortex.voxy.client.core.gl.GlBuffer;
|
||||
import me.cortex.voxy.client.core.rendering.util.DownloadStream;
|
||||
import org.lwjgl.system.MemoryUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.lwjgl.opengl.ARBDirectStateAccess.nglClearNamedBufferData;
|
||||
import static org.lwjgl.opengl.GL11.GL_UNSIGNED_INT;
|
||||
import static org.lwjgl.opengl.GL30.GL_R32UI;
|
||||
import static org.lwjgl.opengl.GL30.glBindBufferBase;
|
||||
import static org.lwjgl.opengl.GL30C.GL_RED_INTEGER;
|
||||
import static org.lwjgl.opengl.GL43.GL_SHADER_STORAGE_BUFFER;
|
||||
import static org.lwjgl.opengl.GL45.nglClearNamedBufferSubData;
|
||||
|
||||
public class PrintfInjector implements IShaderProcessor {
|
||||
private final GlBuffer textBuffer;
|
||||
private final HashMap<String, Integer> printfStringMap = new HashMap<>();
|
||||
private final HashMap<Integer, String> idToPrintfStringMap = new HashMap<>();
|
||||
private final int bindingIndex;
|
||||
private final Consumer<String> callback;
|
||||
private final Runnable preRun;
|
||||
public PrintfInjector(int bufferSize, int bufferBindingIndex, Consumer<String> callback) {
|
||||
this(bufferSize, bufferBindingIndex, callback, null);
|
||||
}
|
||||
|
||||
public PrintfInjector(int bufferSize, int bufferBindingIndex, Consumer<String> callback, Runnable pre) {
|
||||
this.textBuffer = new GlBuffer(bufferSize*4L+4);
|
||||
nglClearNamedBufferData(this.textBuffer.id, GL_R32UI, GL_RED_INTEGER, GL_UNSIGNED_INT, 0);
|
||||
this.bindingIndex = bufferBindingIndex;
|
||||
this.callback = callback;
|
||||
this.preRun = pre;
|
||||
}
|
||||
|
||||
private static int findNextCall(String src, int after) {
|
||||
while (true) {
|
||||
int idx = src.indexOf("printf", after);
|
||||
if (idx == -1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
boolean lineComment = false;
|
||||
boolean multiLineComment = false;
|
||||
//Check for comments
|
||||
for (int i = 0; i < idx; i++) {
|
||||
if (src.charAt(i) == '/' && src.charAt(i + 1) == '/') {
|
||||
lineComment = true;
|
||||
}
|
||||
if (src.charAt(i) == '\n') {
|
||||
lineComment = false;
|
||||
}
|
||||
if ((!lineComment) && src.charAt(i) == '/' && src.charAt(i + 1) == '*') {
|
||||
multiLineComment = true;
|
||||
}
|
||||
if ((!lineComment) && src.charAt(i) == '*' && src.charAt(i + 1) == '/') {
|
||||
multiLineComment = false;
|
||||
}
|
||||
}
|
||||
if (lineComment || multiLineComment) {
|
||||
after = idx+1;
|
||||
continue;
|
||||
}
|
||||
return idx;
|
||||
}
|
||||
}
|
||||
|
||||
private static void parsePrintfTypes(String fmtStr, List<Character> types) {
|
||||
for (int i = 0; i < fmtStr.length()-1; i++) {
|
||||
if (fmtStr.charAt(i)=='%' && (i==0||fmtStr.charAt(i-1)!='%')) {
|
||||
types.add(fmtStr.charAt(i+1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public String transformInject(String src) {
|
||||
String original = src;
|
||||
//Quick exit
|
||||
if (!src.contains("printf")) {
|
||||
return src;
|
||||
}
|
||||
|
||||
int pos = 0;
|
||||
StringBuilder result = new StringBuilder();
|
||||
List<String> argVals = new ArrayList<>();
|
||||
List<Character> types = new ArrayList<>();
|
||||
|
||||
{
|
||||
int bufferInjection = Math.max(src.lastIndexOf("#version"), src.lastIndexOf("#extension"));
|
||||
bufferInjection = src.indexOf("\n", bufferInjection);
|
||||
|
||||
result.append(src, 0, bufferInjection+1);
|
||||
|
||||
result.append(String.format("""
|
||||
layout(binding = %s, std430) restrict buffer PrintfOutputStream {
|
||||
uint index;
|
||||
uint stream[];
|
||||
} printfOutputStruct;
|
||||
""", this.bindingIndex));
|
||||
|
||||
src = src.substring(bufferInjection+1);
|
||||
}
|
||||
boolean usedPrintf = false;
|
||||
while (true) {
|
||||
int nextCall = findNextCall(src, pos);
|
||||
if (nextCall == -1) {
|
||||
result.append(src, pos, src.length());
|
||||
break;
|
||||
}
|
||||
result.append(src, pos, nextCall);
|
||||
|
||||
|
||||
//Parse the printf() call
|
||||
String sub = src.substring(nextCall);
|
||||
sub = sub.substring(sub.indexOf('"')+1);
|
||||
sub = sub.substring(0, sub.indexOf(';'));
|
||||
String fmtStr = sub.substring(0, sub.indexOf('"'));
|
||||
String args = sub.substring(sub.indexOf('"'));
|
||||
|
||||
//Parse the commas in the args
|
||||
int prev = 0;
|
||||
int brace = 0;
|
||||
argVals.clear();
|
||||
for (int i = 0; i < args.length(); i++) {
|
||||
if (args.charAt(i) == '(' || args.charAt(i) == '[') brace++;
|
||||
if (args.charAt(i) == ')' || args.charAt(i) == ']') brace--;
|
||||
if ((args.charAt(i) == ',' && brace==0) || brace==-1) {
|
||||
if (prev == 0) {
|
||||
prev = i;
|
||||
continue;
|
||||
}
|
||||
String arg = args.substring(prev+1, i);
|
||||
prev = i;
|
||||
argVals.add(arg);
|
||||
|
||||
if (brace==-1) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Parse the format string
|
||||
types.clear();
|
||||
parsePrintfTypes(fmtStr, types);
|
||||
|
||||
if (types.size() != argVals.size()) {
|
||||
throw new IllegalStateException("Printf obj count dont match arg size");
|
||||
}
|
||||
|
||||
|
||||
|
||||
//Inject the printf code
|
||||
StringBuilder subCode = new StringBuilder();
|
||||
subCode.append(String.format("{" +
|
||||
"uint printfWriteIndex = atomicAdd(printfOutputStruct.index,%s);" +
|
||||
"printfOutputStruct.stream[printfWriteIndex]=%s;", types.size()+1,
|
||||
this.printfStringMap.computeIfAbsent(fmtStr, a->{int id = this.printfStringMap.size();
|
||||
this.idToPrintfStringMap.put(id, a);
|
||||
return id;})));
|
||||
|
||||
for (int i = 0; i < types.size(); i++) {
|
||||
subCode.append("printfOutputStruct.stream[printfWriteIndex+").append(i+1).append("]=");
|
||||
if (types.get(i) == 'd') {
|
||||
subCode.append("uint(").append(argVals.get(i)).append(")");
|
||||
} else if (types.get(i) == 'f') {
|
||||
subCode.append("floatBitsToUint(").append(argVals.get(i)).append(")");
|
||||
} else {
|
||||
throw new IllegalStateException("Unknown type " + types.get(i));
|
||||
}
|
||||
subCode.append(";");
|
||||
}
|
||||
subCode.append("}");
|
||||
result.append(subCode);
|
||||
usedPrintf = true;
|
||||
pos = src.indexOf(';', nextCall)+1;
|
||||
}
|
||||
if (!usedPrintf) {
|
||||
return original;
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
public void bind() {
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, this.bindingIndex, this.textBuffer.id);
|
||||
}
|
||||
|
||||
private void processResult(long ptr, long size) {
|
||||
int total = MemoryUtil.memGetInt(ptr); ptr += 4;
|
||||
if (total == 0) {
|
||||
return;
|
||||
}
|
||||
if (this.preRun != null) {
|
||||
this.preRun.run();
|
||||
}
|
||||
|
||||
int cnt = 0;
|
||||
List<Character> types = new ArrayList<>();
|
||||
while (cnt < total) {
|
||||
int id = MemoryUtil.memGetInt(ptr);
|
||||
ptr += 4;
|
||||
cnt++;
|
||||
String fmt = this.idToPrintfStringMap.get(id);
|
||||
if (fmt == null) {
|
||||
throw new IllegalStateException("Unknown id: "+ id);
|
||||
}
|
||||
types.clear();
|
||||
parsePrintfTypes(fmt, types);
|
||||
Object[] args = new Object[types.size()];
|
||||
for (int i = 0; i < types.size(); i++) {
|
||||
if (types.get(i) == 'd') {
|
||||
args[i] = MemoryUtil.memGetInt(ptr);
|
||||
ptr += 4;
|
||||
cnt++;
|
||||
}
|
||||
if (types.get(i) == 'f') {
|
||||
args[i] = Float.intBitsToFloat(MemoryUtil.memGetInt(ptr));
|
||||
ptr += 4;
|
||||
cnt++;
|
||||
}
|
||||
}
|
||||
this.callback.accept(String.format(fmt, args));
|
||||
}
|
||||
}
|
||||
|
||||
public void download() {
|
||||
DownloadStream.INSTANCE.download(this.textBuffer, this::processResult);
|
||||
nglClearNamedBufferSubData(this.textBuffer.id, GL_R32UI, 0, 4, GL_RED_INTEGER, GL_UNSIGNED_INT, 0);
|
||||
}
|
||||
|
||||
public void free() {
|
||||
this.textBuffer.free();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String process(ShaderType type, String source) {
|
||||
return this.transformInject(source);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,17 @@
|
||||
package me.cortex.voxy.client.core.gl.shader;
|
||||
|
||||
import me.cortex.voxy.client.core.gl.Capabilities;
|
||||
import me.cortex.voxy.client.core.gl.GlDebug;
|
||||
import me.cortex.voxy.common.Logger;
|
||||
import me.cortex.voxy.common.util.ThreadUtils;
|
||||
import me.cortex.voxy.common.util.TrackedObject;
|
||||
import org.lwjgl.opengl.GL20C;
|
||||
import org.lwjgl.system.MemoryStack;
|
||||
import org.lwjgl.system.MemoryUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -11,25 +20,10 @@ import static org.lwjgl.opengl.GL20.glUseProgram;
|
||||
|
||||
public class Shader extends TrackedObject {
|
||||
private final int id;
|
||||
private Shader(int program) {
|
||||
Shader(int program) {
|
||||
id = program;
|
||||
}
|
||||
|
||||
public static Builder make(IShaderProcessor... processors) {
|
||||
List<IShaderProcessor> aa = new ArrayList<>(List.of(processors));
|
||||
Collections.reverse(aa);
|
||||
IShaderProcessor applicator = (type,source)->source;
|
||||
for (IShaderProcessor processor : processors) {
|
||||
IShaderProcessor finalApplicator = applicator;
|
||||
applicator = (type, source) -> finalApplicator.process(type, processor.process(type, source));
|
||||
}
|
||||
return new Builder(applicator);
|
||||
}
|
||||
|
||||
public static Builder make() {
|
||||
return new Builder((aa,source)->source);
|
||||
}
|
||||
|
||||
public int id() {
|
||||
return this.id;
|
||||
}
|
||||
@@ -43,35 +37,95 @@ public class Shader extends TrackedObject {
|
||||
glDeleteProgram(this.id);
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
private final Map<String, String> defines = new HashMap<>();
|
||||
|
||||
public Shader name(String name) {
|
||||
return GlDebug.name(name, this);
|
||||
}
|
||||
|
||||
|
||||
public static Builder<Shader> make(IShaderProcessor... processor) {
|
||||
return makeInternal((a,b)->new Shader(b), processor);
|
||||
}
|
||||
|
||||
public static Builder<AutoBindingShader> makeAuto(IShaderProcessor... processor) {
|
||||
return makeInternal(AutoBindingShader::new, processor);
|
||||
}
|
||||
|
||||
|
||||
|
||||
static <T extends Shader> Builder<T> makeInternal(Builder.IShaderObjectConstructor<T> constructor, IShaderProcessor[] processors) {
|
||||
List<IShaderProcessor> aa = new ArrayList<>(List.of(processors));
|
||||
Collections.reverse(aa);
|
||||
IShaderProcessor applicator = (type,source)->source;
|
||||
for (IShaderProcessor processor : processors) {
|
||||
IShaderProcessor finalApplicator = applicator;
|
||||
applicator = (type, source) -> finalApplicator.process(type, processor.process(type, source));
|
||||
}
|
||||
return new Builder<>(constructor, applicator);
|
||||
}
|
||||
|
||||
public static class Builder <T extends Shader> {
|
||||
protected interface IShaderObjectConstructor <J extends Shader> {
|
||||
J make(Builder<J> builder, int program);
|
||||
}
|
||||
final Map<String, String> defines = new HashMap<>();
|
||||
private final Map<ShaderType, String> sources = new HashMap<>();
|
||||
private final IShaderProcessor processor;
|
||||
private Builder(IShaderProcessor processor) {
|
||||
private final IShaderObjectConstructor<T> constructor;
|
||||
private Builder(IShaderObjectConstructor<T> constructor, IShaderProcessor processor) {
|
||||
this.constructor = constructor;
|
||||
this.processor = processor;
|
||||
}
|
||||
|
||||
public Builder define(String name) {
|
||||
public Builder<T> clone() {
|
||||
var clone = new Builder<>(this.constructor, this.processor);
|
||||
clone.defines.putAll(this.defines);
|
||||
clone.sources.putAll(this.sources);
|
||||
return clone;
|
||||
}
|
||||
|
||||
public Builder<T> define(String name) {
|
||||
this.defines.put(name, "");
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder define(String name, int value) {
|
||||
//Useful for inline setting (such as debug)
|
||||
public Builder<T> defineIf(String name, boolean condition) {
|
||||
if (condition) {
|
||||
this.defines.put(name, "");
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder<T> defineIf(String name, boolean condition, int value) {
|
||||
if (condition) {
|
||||
this.defines.put(name, Integer.toString(value));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder<T> define(String name, int value) {
|
||||
this.defines.put(name, Integer.toString(value));
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder add(ShaderType type, String id) {
|
||||
public Builder<T> define(String name, String value) {
|
||||
this.defines.put(name, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder<T> add(ShaderType type, String id) {
|
||||
this.addSource(type, ShaderLoader.parse(id));
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder addSource(ShaderType type, String source) {
|
||||
public Builder<T> addSource(ShaderType type, String source) {
|
||||
this.sources.put(type, this.processor.process(type, source));
|
||||
return this;
|
||||
}
|
||||
|
||||
public Shader compile() {
|
||||
|
||||
private int compileToProgram() {
|
||||
int program = GL20C.glCreateProgram();
|
||||
int[] shaders = new int[this.sources.size()];
|
||||
{
|
||||
@@ -99,15 +153,20 @@ public class Shader extends TrackedObject {
|
||||
}
|
||||
printProgramLinkLog(program);
|
||||
verifyProgramLinked(program);
|
||||
return new Shader(program);
|
||||
return program;
|
||||
}
|
||||
|
||||
public T compile() {
|
||||
this.defineIf("IS_INTEL", Capabilities.INSTANCE.isIntel);
|
||||
this.defineIf("IS_WINDOWS", ThreadUtils.isWindows);
|
||||
return this.constructor.make(this, this.compileToProgram());
|
||||
}
|
||||
|
||||
private static void printProgramLinkLog(int program) {
|
||||
String log = GL20C.glGetProgramInfoLog(program);
|
||||
|
||||
if (!log.isEmpty()) {
|
||||
System.err.println(log);
|
||||
Logger.error(log);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,24 +180,33 @@ public class Shader extends TrackedObject {
|
||||
|
||||
private static int createShader(ShaderType type, String src) {
|
||||
int shader = GL20C.glCreateShader(type.gl);
|
||||
GL20C.glShaderSource(shader, src);
|
||||
{//https://github.com/CaffeineMC/sodium/blob/fc42a7b19836c98a35df46e63303608de0587ab6/src/main/java/me/jellysquid/mods/sodium/client/gl/shader/ShaderWorkarounds.java
|
||||
long ptr = MemoryUtil.memAddress(MemoryUtil.memUTF8(src, true));
|
||||
try (var stack = MemoryStack.stackPush()) {
|
||||
GL20C.nglShaderSource(shader, 1, stack.pointers(ptr).address0(), 0);
|
||||
}
|
||||
MemoryUtil.nmemFree(ptr);
|
||||
}
|
||||
GL20C.glCompileShader(shader);
|
||||
String log = GL20C.glGetShaderInfoLog(shader);
|
||||
|
||||
if (!log.isEmpty()) {
|
||||
System.err.println(log);
|
||||
Logger.warn(log);
|
||||
}
|
||||
|
||||
int result = GL20C.glGetShaderi(shader, GL20C.GL_COMPILE_STATUS);
|
||||
|
||||
if (result != GL20C.GL_TRUE) {
|
||||
GL20C.glDeleteShader(shader);
|
||||
|
||||
throw new RuntimeException("Shader compilation failed of type " + type.name() + ", see log for details");
|
||||
try {
|
||||
Files.writeString(Path.of("SHADER_DUMP.txt"), src);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
throw new RuntimeException("Shader compilation failed of type " + type.name() + ", see log for details, dumped shader");
|
||||
}
|
||||
|
||||
return shader;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
package me.cortex.voxy.client.core.gl.shader;
|
||||
|
||||
import me.jellysquid.mods.sodium.client.gl.shader.ShaderConstants;
|
||||
import me.jellysquid.mods.sodium.client.gl.shader.ShaderParser;
|
||||
|
||||
import net.caffeinemc.mods.sodium.client.gl.shader.ShaderConstants;
|
||||
import net.caffeinemc.mods.sodium.client.gl.shader.ShaderParser;
|
||||
|
||||
public class ShaderLoader {
|
||||
public static String parse(String id) {
|
||||
return ShaderParser.parseShader("#import <" + id + ">", ShaderConstants.builder().build());
|
||||
return "#version 460 core\n"+ShaderParser.parseShader("\n#import <" + id + ">\n//beans", ShaderConstants.builder().build()).src().replaceAll("\r\n", "\n").replaceFirst("\n#version .+\n", "\n");
|
||||
//return me.jellysquid.mods.sodium.client.gl.shader.ShaderLoader.getShaderSource(new Identifier(id));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
package me.cortex.voxy.client.core.model;
|
||||
|
||||
import net.minecraft.block.BlockEntityProvider;
|
||||
import net.minecraft.block.BlockState;
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
import net.minecraft.client.render.*;
|
||||
import net.minecraft.client.util.math.MatrixStack;
|
||||
import net.minecraft.util.Identifier;
|
||||
import net.minecraft.util.math.BlockPos;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.lwjgl.opengl.GL11.GL_TEXTURE_2D;
|
||||
import static org.lwjgl.opengl.GL11.glBindTexture;
|
||||
|
||||
public class BakedBlockEntityModel {
|
||||
private static final class BakedVertices implements VertexConsumer {
|
||||
private boolean makingVertex = false;
|
||||
public final RenderLayer layer;
|
||||
private float cX, cY, cZ;
|
||||
private int cR, cG, cB, cA;
|
||||
private float cU, cV;
|
||||
|
||||
private final List<int[]> vertices = new ArrayList<>();
|
||||
|
||||
private BakedVertices(RenderLayer layer) {
|
||||
this.layer = layer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VertexConsumer vertex(float x, float y, float z) {
|
||||
this.next();
|
||||
this.cX = x;
|
||||
this.cY = y;
|
||||
this.cZ = z;
|
||||
this.makingVertex = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VertexConsumer color(int red, int green, int blue, int alpha) {
|
||||
this.cR = 0;//red;
|
||||
this.cG = 0;//green;
|
||||
this.cB = 0;//blue;
|
||||
this.cA = alpha;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VertexConsumer texture(float u, float v) {
|
||||
this.cU = u;
|
||||
this.cV = v;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VertexConsumer overlay(int u, int v) {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VertexConsumer light(int u, int v) {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VertexConsumer normal(float x, float y, float z) {
|
||||
return this;
|
||||
}
|
||||
|
||||
private void next() {
|
||||
if (this.makingVertex) {
|
||||
this.makingVertex = false;
|
||||
this.vertices.add(new int[]{
|
||||
Float.floatToIntBits(this.cX), Float.floatToIntBits(this.cY), Float.floatToIntBits(this.cZ),
|
||||
this.cR, this.cG, this.cB, this.cA,
|
||||
Float.floatToIntBits(this.cU), Float.floatToIntBits(this.cV)});
|
||||
}
|
||||
}
|
||||
|
||||
public void putInto(VertexConsumer vc) {
|
||||
this.next();
|
||||
for (var vert : this.vertices) {
|
||||
vc.vertex(Float.intBitsToFloat(vert[0]), Float.intBitsToFloat(vert[1]), Float.intBitsToFloat(vert[2]))
|
||||
.color(vert[3], vert[4], vert[5], vert[6])
|
||||
.texture(Float.intBitsToFloat(vert[7]), Float.intBitsToFloat(vert[8]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final List<BakedVertices> layers;
|
||||
private BakedBlockEntityModel(List<BakedVertices> layers) {
|
||||
this.layers = layers;
|
||||
}
|
||||
|
||||
public void renderOut() {
|
||||
var vc = Tessellator.getInstance();
|
||||
for (var layer : this.layers) {
|
||||
var bb = vc.begin(VertexFormat.DrawMode.QUADS, VertexFormats.POSITION_TEXTURE_COLOR);
|
||||
if (layer.layer instanceof RenderLayer.MultiPhase mp) {
|
||||
Identifier textureId = mp.phases.texture.getId().orElse(null);
|
||||
if (textureId == null) {
|
||||
System.err.println("ERROR: Empty texture id for layer: " + layer);
|
||||
} else {
|
||||
var texture = MinecraftClient.getInstance().getTextureManager().getTexture(textureId);
|
||||
glBindTexture(GL_TEXTURE_2D, texture.getGlId());
|
||||
}
|
||||
}
|
||||
layer.putInto(bb);
|
||||
BufferRenderer.draw(bb.end());
|
||||
}
|
||||
}
|
||||
|
||||
public static BakedBlockEntityModel bake(BlockState state) {
|
||||
Map<RenderLayer, BakedVertices> map = new HashMap<>();
|
||||
var entity = ((BlockEntityProvider)state.getBlock()).createBlockEntity(BlockPos.ORIGIN, state);
|
||||
if (entity == null) {
|
||||
return null;
|
||||
}
|
||||
var renderer = MinecraftClient.getInstance().getBlockEntityRenderDispatcher().get(entity);
|
||||
if (renderer != null) {
|
||||
entity.setWorld(MinecraftClient.getInstance().world);
|
||||
try {
|
||||
renderer.render(entity, 0.0f, new MatrixStack(), layer->map.computeIfAbsent(layer, BakedVertices::new), 0, 0);
|
||||
} catch (Exception e) {
|
||||
System.err.println("Unable to bake block entity: " + entity);
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
entity.markRemoved();
|
||||
if (map.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return new BakedBlockEntityModel(map.values().stream().toList());
|
||||
}
|
||||
}
|
||||
@@ -2,21 +2,25 @@ package me.cortex.voxy.client.core.model;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public record ColourDepthTextureData(int[] colour, int[] depth, int width, int height) {
|
||||
public record ColourDepthTextureData(int[] colour, int[] depth, int width, int height, int hash) {
|
||||
public ColourDepthTextureData(int[] colour, int[] depth, int width, int height) {
|
||||
this(colour, depth, width, height, width * 312337173 * (Arrays.hashCode(colour) ^ Arrays.hashCode(depth)) ^ height);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj == null) return false;
|
||||
var other = ((ColourDepthTextureData)obj);
|
||||
return Arrays.equals(other.colour, this.colour) && Arrays.equals(other.depth, this.depth);
|
||||
return this.hash == other.hash && Arrays.equals(other.colour, this.colour) && Arrays.equals(other.depth, this.depth);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return (this.width * 312337173 * (Arrays.hashCode(this.colour) ^ Arrays.hashCode(this.depth))) ^ this.height;
|
||||
return this.hash;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ColourDepthTextureData clone() {
|
||||
return new ColourDepthTextureData(Arrays.copyOf(this.colour, this.colour.length), Arrays.copyOf(this.depth, this.depth.length), this.width, this.height);
|
||||
return new ColourDepthTextureData(Arrays.copyOf(this.colour, this.colour.length), Arrays.copyOf(this.depth, this.depth.length), this.width, this.height, this.hash);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
package me.cortex.voxy.client.core.model;
|
||||
|
||||
public class IdNotYetComputedException extends RuntimeException {
|
||||
public IdNotYetComputedException(int id) {
|
||||
super("Id not yet computed: " + id);
|
||||
public final int id;
|
||||
public final boolean isIdBlockId;
|
||||
public int auxBitMsk;
|
||||
public long[] auxData;
|
||||
public IdNotYetComputedException(int id, boolean isIdBlockId) {
|
||||
super(null, null, false, false);
|
||||
this.id = id;
|
||||
this.isIdBlockId = isIdBlockId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
package me.cortex.voxy.client.core.model;
|
||||
|
||||
|
||||
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
|
||||
import me.cortex.voxy.common.Logger;
|
||||
import me.cortex.voxy.common.world.other.Mapper;
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
import net.minecraft.registry.RegistryKeys;
|
||||
import net.minecraft.util.Identifier;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentLinkedDeque;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import static org.lwjgl.opengl.GL11.glGetInteger;
|
||||
import static org.lwjgl.opengl.GL30.GL_FRAMEBUFFER;
|
||||
import static org.lwjgl.opengl.GL30.GL_FRAMEBUFFER_BINDING;
|
||||
import static org.lwjgl.opengl.GL30C.glBindFramebuffer;
|
||||
|
||||
public class ModelBakerySubsystem {
|
||||
//Redo to just make it request the block faces with the async texture download stream which
|
||||
// basicly solves all the render stutter due to the baking
|
||||
|
||||
private final ModelStore storage = new ModelStore();
|
||||
public final ModelFactory factory;
|
||||
private final Mapper mapper;
|
||||
private final AtomicInteger blockIdCount = new AtomicInteger();
|
||||
private final ConcurrentLinkedDeque<Integer> blockIdQueue = new ConcurrentLinkedDeque<>();//TODO: replace with custom DS
|
||||
private final ConcurrentLinkedDeque<Mapper.BiomeEntry> biomeQueue = new ConcurrentLinkedDeque<>();
|
||||
|
||||
public ModelBakerySubsystem(Mapper mapper) {
|
||||
this.mapper = mapper;
|
||||
this.factory = new ModelFactory(mapper, this.storage);
|
||||
}
|
||||
|
||||
public void tick(long totalBudget) {
|
||||
//Upload all biomes
|
||||
while (!this.biomeQueue.isEmpty()) {
|
||||
var biome = this.biomeQueue.poll();
|
||||
var biomeReg = MinecraftClient.getInstance().world.getRegistryManager().getOrThrow(RegistryKeys.BIOME);
|
||||
this.factory.addBiome(biome.id, biomeReg.get(Identifier.of(biome.biome)));
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
//There should be a method to access the frame time IIRC, if the user framecap is unlimited lock it to like 60 fps for computation
|
||||
int BUDGET = 16;//TODO: make this computed based on the remaining free time in a frame (and like div by 2 to reduce overhead) (with a min of 1)
|
||||
if (!this.blockIdQueue.isEmpty()) {
|
||||
int[] est = new int[Math.min(this.blockIdQueue.size(), BUDGET)];
|
||||
int i = 0;
|
||||
synchronized (this.blockIdQueue) {
|
||||
for (;i < est.length && !this.blockIdQueue.isEmpty(); i++) {
|
||||
int blockId = this.blockIdQueue.removeFirstInt();
|
||||
if (blockId == -1) {
|
||||
i--;
|
||||
continue;
|
||||
}
|
||||
est[i] = blockId;
|
||||
}
|
||||
}
|
||||
|
||||
for (int j = 0; j < i; j++) {
|
||||
this.factory.addEntry(est[j]);
|
||||
}
|
||||
}*/
|
||||
//TimingStatistics.modelProcess.start();
|
||||
if (this.blockIdCount.get() != 0) {
|
||||
long budget = Math.min(totalBudget-150_000, totalBudget-(this.factory.resultJobs.size()*10_000L))-150_000;
|
||||
|
||||
//Always do 1 iteration minimum
|
||||
Integer i = this.blockIdQueue.poll();
|
||||
int j = 0;
|
||||
if (i != null) {
|
||||
int fbBinding = glGetInteger(GL_FRAMEBUFFER_BINDING);
|
||||
|
||||
do {
|
||||
this.factory.addEntry(i);
|
||||
j++;
|
||||
if (24<j)//budget<(System.nanoTime() - start)+1000
|
||||
break;
|
||||
i = this.blockIdQueue.poll();
|
||||
} while (i != null);
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbBinding);//This is done here as stops needing to set then unset the fb in the thing 1000x
|
||||
}
|
||||
this.blockIdCount.addAndGet(-j);
|
||||
}
|
||||
|
||||
this.factory.tick();
|
||||
|
||||
long start = System.nanoTime();
|
||||
while (!this.factory.resultJobs.isEmpty()) {
|
||||
this.factory.resultJobs.poll().run();
|
||||
if (totalBudget<(System.nanoTime()-start))
|
||||
break;
|
||||
}
|
||||
//TimingStatistics.modelProcess.stop();
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
this.factory.free();
|
||||
this.storage.free();
|
||||
}
|
||||
|
||||
//This is on this side only and done like this as only worker threads call this code
|
||||
private final ReentrantLock seenIdsLock = new ReentrantLock();
|
||||
private final IntOpenHashSet seenIds = new IntOpenHashSet(6000);//TODO: move to a lock free concurrent hashmap
|
||||
public void requestBlockBake(int blockId) {
|
||||
if (this.mapper.getBlockStateCount() < blockId) {
|
||||
Logger.error("Error, got bakeing request for out of range state id. StateId: " + blockId + " max id: " + this.mapper.getBlockStateCount(), new Exception());
|
||||
return;
|
||||
}
|
||||
this.seenIdsLock.lock();
|
||||
if (!this.seenIds.add(blockId)) {
|
||||
this.seenIdsLock.unlock();
|
||||
return;
|
||||
}
|
||||
this.seenIdsLock.unlock();
|
||||
this.blockIdQueue.add(blockId);
|
||||
this.blockIdCount.incrementAndGet();
|
||||
}
|
||||
|
||||
public void addBiome(Mapper.BiomeEntry biomeEntry) {
|
||||
this.biomeQueue.add(biomeEntry);
|
||||
}
|
||||
|
||||
public void addDebugData(List<String> debug) {
|
||||
debug.add(String.format("MQ/IF/MC: %04d, %03d, %04d", this.blockIdCount.get(), this.factory.getInflightCount(), this.factory.getBakedCount()));//Model bake queue/in flight/model baked count
|
||||
}
|
||||
|
||||
public ModelStore getStore() {
|
||||
return this.storage;
|
||||
}
|
||||
|
||||
public boolean areQueuesEmpty() {
|
||||
return this.blockIdCount.get()==0 && this.factory.getInflightCount() == 0 && this.biomeQueue.isEmpty();
|
||||
}
|
||||
|
||||
public int getProcessingCount() {
|
||||
return this.blockIdCount.get() + this.factory.getInflightCount();
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,25 @@
|
||||
package me.cortex.voxy.client.core.model;
|
||||
|
||||
import com.mojang.blaze3d.platform.GlConst;
|
||||
import com.mojang.blaze3d.platform.GlStateManager;
|
||||
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
|
||||
import it.unimi.dsi.fastutil.objects.Object2IntMap;
|
||||
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectOpenCustomHashSet;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectSet;
|
||||
import me.cortex.voxy.client.core.IGetVoxelCore;
|
||||
import me.cortex.voxy.client.core.gl.GlBuffer;
|
||||
import me.cortex.voxy.client.core.gl.GlTexture;
|
||||
import me.cortex.voxy.client.core.gl.Capabilities;
|
||||
import me.cortex.voxy.client.core.model.bakery.ModelTextureBakery;
|
||||
import me.cortex.voxy.client.core.rendering.util.RawDownloadStream;
|
||||
import me.cortex.voxy.client.core.rendering.util.UploadStream;
|
||||
import me.cortex.voxy.common.Logger;
|
||||
import me.cortex.voxy.common.util.MemoryBuffer;
|
||||
import me.cortex.voxy.common.world.other.Mapper;
|
||||
import net.minecraft.block.Block;
|
||||
import net.minecraft.block.BlockState;
|
||||
import net.minecraft.block.Blocks;
|
||||
import net.minecraft.block.FluidBlock;
|
||||
import net.minecraft.block.LeavesBlock;
|
||||
import net.minecraft.block.entity.BlockEntity;
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
import net.minecraft.client.color.block.BlockColorProvider;
|
||||
import net.minecraft.client.render.RenderLayer;
|
||||
import net.minecraft.client.render.BlockRenderLayer;
|
||||
import net.minecraft.client.render.RenderLayers;
|
||||
import net.minecraft.fluid.FluidState;
|
||||
import net.minecraft.registry.Registries;
|
||||
@@ -36,17 +37,10 @@ import org.jetbrains.annotations.Nullable;
|
||||
import org.lwjgl.system.MemoryUtil;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static me.cortex.voxy.client.core.model.ModelStore.MODEL_SIZE;
|
||||
import static org.lwjgl.opengl.ARBDirectStateAccess.nglTextureSubImage2D;
|
||||
import static org.lwjgl.opengl.GL11.*;
|
||||
import static org.lwjgl.opengl.GL11C.GL_NEAREST;
|
||||
import static org.lwjgl.opengl.GL11C.GL_NEAREST_MIPMAP_LINEAR;
|
||||
import static org.lwjgl.opengl.GL12C.GL_TEXTURE_MAX_LOD;
|
||||
import static org.lwjgl.opengl.GL12C.GL_TEXTURE_MIN_LOD;
|
||||
import static org.lwjgl.opengl.GL33.glDeleteSamplers;
|
||||
import static org.lwjgl.opengl.GL33.glGenSamplers;
|
||||
import static org.lwjgl.opengl.GL33C.glSamplerParameteri;
|
||||
import static org.lwjgl.opengl.GL45C.glTextureSubImage2D;
|
||||
|
||||
//Manages the storage and updating of model states, textures and colours
|
||||
|
||||
@@ -55,24 +49,26 @@ import static org.lwjgl.opengl.GL45C.glTextureSubImage2D;
|
||||
//TODO: support more than 65535 states, what should actually happen is a blockstate is registered, the model data is generated, then compared
|
||||
// to all other models already loaded, if it is a duplicate, create a mapping from the id to the already loaded id, this will help with meshing aswell
|
||||
// as leaves and such will be able to be merged
|
||||
public class ModelManager {
|
||||
|
||||
|
||||
|
||||
//TODO: NOTE!!! is it worth even uploading as a 16x16 texture, since automatic lod selection... doing 8x8 textures might be perfectly ok!!!
|
||||
// this _quarters_ the memory requirements for the texture atlas!!! WHICH IS HUGE saving
|
||||
public class ModelFactory {
|
||||
public static final int MODEL_TEXTURE_SIZE = 16;
|
||||
|
||||
//TODO: replace the fluid BlockState with a client model id integer of the fluidState, requires looking up
|
||||
// the fluid state in the mipper
|
||||
private record ModelEntry(List<ColourDepthTextureData> textures, int fluidBlockStateId){
|
||||
private ModelEntry(ColourDepthTextureData[] textures, int fluidBlockStateId) {
|
||||
this(Stream.of(textures).map(ColourDepthTextureData::clone).toList(), fluidBlockStateId);
|
||||
private record ModelEntry(ColourDepthTextureData down, ColourDepthTextureData up, ColourDepthTextureData north, ColourDepthTextureData south, ColourDepthTextureData west, ColourDepthTextureData east, int fluidBlockStateId) {
|
||||
public ModelEntry(ColourDepthTextureData[] textures, int fluidBlockStateId) {
|
||||
this(textures[0], textures[1], textures[2], textures[3], textures[4], textures[5], fluidBlockStateId);
|
||||
}
|
||||
}
|
||||
|
||||
public static final int MODEL_SIZE = 64;
|
||||
public final ModelTextureBakery bakery;
|
||||
private final GlBuffer modelBuffer;
|
||||
private final GlBuffer modelColourBuffer;
|
||||
private final GlTexture textures;
|
||||
private final int blockSampler = glGenSamplers();
|
||||
private final Biome DEFAULT_BIOME = MinecraftClient.getInstance().world.getRegistryManager().getOrThrow(RegistryKeys.BIOME).get(BiomeKeys.PLAINS);
|
||||
|
||||
public final ModelTextureBakery bakery;
|
||||
|
||||
private final int modelTextureSize;
|
||||
|
||||
//Model data might also contain a constant colour if the colour resolver produces a constant colour, this saves space in the
|
||||
// section buffer reverse indexing
|
||||
@@ -108,53 +104,121 @@ public class ModelManager {
|
||||
private final int[] idMappings;
|
||||
private final Object2IntOpenHashMap<ModelEntry> modelTexture2id = new Object2IntOpenHashMap<>();
|
||||
|
||||
//Contains the set of all block ids that are currently inflight/being baked
|
||||
// this is required due to "async" nature of gpu feedback
|
||||
private final IntOpenHashSet blockStatesInFlight = new IntOpenHashSet();
|
||||
|
||||
private final List<Biome> biomes = new ArrayList<>();
|
||||
private final List<Pair<Integer, BlockState>> modelsRequiringBiomeColours = new ArrayList<>();
|
||||
|
||||
private static final ObjectSet<BlockState> LOGGED_SELF_CULLING_WARNING = new ObjectOpenHashSet<>();
|
||||
|
||||
public ModelManager(int modelTextureSize) {
|
||||
this.modelTextureSize = modelTextureSize;
|
||||
this.bakery = new ModelTextureBakery(modelTextureSize, modelTextureSize);
|
||||
this.modelBuffer = new GlBuffer(MODEL_SIZE * (1<<16));
|
||||
private final Mapper mapper;
|
||||
private final ModelStore storage;
|
||||
private final RawDownloadStream downstream = new RawDownloadStream(8*1024*1024);//8mb downstream
|
||||
|
||||
this.modelColourBuffer = new GlBuffer(4 * (1<<16));
|
||||
public final Deque<Runnable> resultJobs = new ArrayDeque<>();
|
||||
|
||||
private Object2IntMap<BlockState> customBlockStateIdMapping;
|
||||
|
||||
//TODO: NOTE!!! is it worth even uploading as a 16x16 texture, since automatic lod selection... doing 8x8 textures might be perfectly ok!!!
|
||||
// this _quarters_ the memory requirements for the texture atlas!!! WHICH IS HUGE saving
|
||||
public ModelFactory(Mapper mapper, ModelStore storage) {
|
||||
this.mapper = mapper;
|
||||
this.storage = storage;
|
||||
this.bakery = new ModelTextureBakery(MODEL_TEXTURE_SIZE, MODEL_TEXTURE_SIZE);
|
||||
|
||||
//TODO: figure out how to do mipping :blobfox_pineapple:
|
||||
this.textures = new GlTexture().store(GL_RGBA8, 4, modelTextureSize*3*256,modelTextureSize*2*256);
|
||||
this.metadataCache = new long[1<<16];
|
||||
this.fluidStateLUT = new int[1<<16];
|
||||
this.idMappings = new int[1<<20];//Max of 1 million blockstates mapping to 65k model states
|
||||
Arrays.fill(this.idMappings, -1);
|
||||
Arrays.fill(this.fluidStateLUT, -1);
|
||||
|
||||
|
||||
glSamplerParameteri(this.blockSampler, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_LINEAR);
|
||||
glSamplerParameteri(this.blockSampler, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glSamplerParameteri(this.blockSampler, GL_TEXTURE_MIN_LOD, 0);
|
||||
glSamplerParameteri(this.blockSampler, GL_TEXTURE_MAX_LOD, 4);
|
||||
|
||||
this.modelTexture2id.defaultReturnValue(-1);
|
||||
this.addEntry(0);//Add air as the first entry
|
||||
}
|
||||
|
||||
|
||||
public void tick() {
|
||||
this.downstream.tick();
|
||||
}
|
||||
|
||||
public void setCustomBlockStateMapping(Object2IntMap<BlockState> mapping) {
|
||||
this.customBlockStateIdMapping = mapping;
|
||||
}
|
||||
|
||||
public boolean addEntry(int blockId) {
|
||||
if (this.idMappings[blockId] != -1) {
|
||||
return false;
|
||||
}
|
||||
//We are (probably) going to be baking the block id
|
||||
// check that it is currently not inflight, if it is, return as its already being baked
|
||||
// else add it to the flight as it is going to be baked
|
||||
if (!this.blockStatesInFlight.add(blockId)) {
|
||||
//Block baking is already in-flight
|
||||
return false;
|
||||
}
|
||||
|
||||
var blockState = this.mapper.getBlockStateFromBlockId(blockId);
|
||||
|
||||
//Before we enqueue the baking of this blockstate, we must check if it has a fluid state associated with it
|
||||
// if it does, we must ensure that it is (effectivly) baked BEFORE we bake this blockstate
|
||||
boolean isFluid = blockState.getBlock() instanceof FluidBlock;
|
||||
if ((!isFluid) && (!blockState.getFluidState().isEmpty())) {
|
||||
//Insert into the fluid LUT
|
||||
var fluidState = blockState.getFluidState().getBlockState();
|
||||
|
||||
int fluidStateId = this.mapper.getIdForBlockState(fluidState);
|
||||
|
||||
if (this.idMappings[fluidStateId] == -1) {
|
||||
//Dont have to check for inflight as that is done recursively :p
|
||||
|
||||
//This is a hack but does work :tm: due to how the download stream is setup
|
||||
// it should enforce that the fluid state is processed before our blockstate
|
||||
addEntry(fluidStateId);
|
||||
}
|
||||
}
|
||||
|
||||
int TOTAL_FACES_TEXTURE_SIZE = MODEL_TEXTURE_SIZE*MODEL_TEXTURE_SIZE*2*4*6;// since both depth and colour are packed together, 6 faces, 4 bytes per pixel
|
||||
int allocation = this.downstream.download(TOTAL_FACES_TEXTURE_SIZE, ptr -> {
|
||||
ColourDepthTextureData[] textureData = new ColourDepthTextureData[6];
|
||||
final int FACE_SIZE = MODEL_TEXTURE_SIZE*MODEL_TEXTURE_SIZE;
|
||||
for (int face = 0; face < 6; face++) {
|
||||
long faceDataPtr = ptr + (FACE_SIZE*4)*face*2;
|
||||
int[] colour = new int[FACE_SIZE];
|
||||
int[] depth = new int[FACE_SIZE];
|
||||
|
||||
//Copy out colour
|
||||
for (int i = 0; i < FACE_SIZE; i++) {
|
||||
//De-interpolate results
|
||||
colour[i] = MemoryUtil.memGetInt(faceDataPtr+ (i*4*2));
|
||||
depth[i] = MemoryUtil.memGetInt(faceDataPtr+ (i*4*2)+4);
|
||||
}
|
||||
|
||||
textureData[face] = new ColourDepthTextureData(colour, depth, MODEL_TEXTURE_SIZE, MODEL_TEXTURE_SIZE);
|
||||
}
|
||||
this.resultJobs.add(()->processTextureBakeResult(blockId, blockState, textureData));
|
||||
});
|
||||
this.bakery.renderToStream(blockState, this.downstream.getBufferId(), allocation);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
//TODO: what i need to do is seperate out fluid states from blockStates
|
||||
|
||||
|
||||
//TODO: so need a few things, per face sizes and offsets, the sizes should be computed from the pixels and find the minimum bounding pixel
|
||||
// while the depth is computed from the depth buffer data
|
||||
public int addEntry(int blockId, BlockState blockState) {
|
||||
//This is
|
||||
private void processTextureBakeResult(int blockId, BlockState blockState, ColourDepthTextureData[] textureData) {
|
||||
if (this.idMappings[blockId] != -1) {
|
||||
System.err.println("Block id already added: " + blockId + " for state: " + blockState);
|
||||
return this.idMappings[blockId];
|
||||
//This should be impossible to reach as it means that multiple bakes for the same blockId happened and where inflight at the same time!
|
||||
throw new IllegalStateException("Block id already added: " + blockId + " for state: " + blockState);
|
||||
}
|
||||
|
||||
if (!this.blockStatesInFlight.contains(blockId)) {
|
||||
throw new IllegalStateException("processing a texture bake result but the block state was not in flight!!");
|
||||
}
|
||||
|
||||
boolean isFluid = blockState.getBlock() instanceof FluidBlock;
|
||||
int modelId = -1;
|
||||
var textureData = this.bakery.renderFaces(blockState, 123456, isFluid);
|
||||
|
||||
|
||||
int clientFluidStateId = -1;
|
||||
|
||||
@@ -162,13 +226,11 @@ public class ModelManager {
|
||||
//Insert into the fluid LUT
|
||||
var fluidState = blockState.getFluidState().getBlockState();
|
||||
|
||||
//TODO:FIXME: PASS IN THE Mapper instead of grabbing it!!! THIS IS CRTICIAL TO FIX
|
||||
int fluidStateId = ((IGetVoxelCore)MinecraftClient.getInstance().worldRenderer).getVoxelCore().getWorldEngine().getMapper().getIdForBlockState(fluidState);
|
||||
|
||||
int fluidStateId = this.mapper.getIdForBlockState(fluidState);
|
||||
|
||||
clientFluidStateId = this.idMappings[fluidStateId];
|
||||
if (clientFluidStateId == -1) {
|
||||
clientFluidStateId = this.addEntry(fluidStateId, fluidState);
|
||||
throw new IllegalStateException("Block has a fluid state but fluid state is not already baked!!!");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,10 +240,15 @@ public class ModelManager {
|
||||
if (possibleDuplicate != -1) {//Duplicate found
|
||||
this.idMappings[blockId] = possibleDuplicate;
|
||||
modelId = possibleDuplicate;
|
||||
return possibleDuplicate;
|
||||
//Remove from flight
|
||||
if (!this.blockStatesInFlight.remove(blockId)) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
return;
|
||||
} else {//Not a duplicate so create a new entry
|
||||
modelId = this.modelTexture2id.size();
|
||||
this.idMappings[blockId] = modelId;
|
||||
//NOTE: we set the mapping at the very end so that race conditions with this and getMetadata dont occur
|
||||
//this.idMappings[blockId] = modelId;
|
||||
this.modelTexture2id.put(entry, modelId);
|
||||
}
|
||||
}
|
||||
@@ -192,30 +259,38 @@ public class ModelManager {
|
||||
this.fluidStateLUT[modelId] = clientFluidStateId;
|
||||
}
|
||||
|
||||
var colourProvider = MinecraftClient.getInstance().getBlockColors().providers.get(Registries.BLOCK.getRawId(blockState.getBlock()));
|
||||
|
||||
|
||||
RenderLayer blockRenderLayer = null;
|
||||
BlockRenderLayer blockRenderLayer = null;
|
||||
if (blockState.getBlock() instanceof FluidBlock) {
|
||||
blockRenderLayer = RenderLayers.getFluidLayer(blockState.getFluidState());
|
||||
} else {
|
||||
blockRenderLayer = RenderLayers.getBlockLayer(blockState);
|
||||
if (blockState.getBlock() instanceof LeavesBlock) {
|
||||
blockRenderLayer = BlockRenderLayer.SOLID;
|
||||
} else {
|
||||
blockRenderLayer = RenderLayers.getBlockLayer(blockState);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
int checkMode = blockRenderLayer==RenderLayer.getSolid()?TextureUtils.WRITE_CHECK_STENCIL:TextureUtils.WRITE_CHECK_ALPHA;
|
||||
int checkMode = blockRenderLayer==BlockRenderLayer.SOLID?TextureUtils.WRITE_CHECK_STENCIL:TextureUtils.WRITE_CHECK_ALPHA;
|
||||
|
||||
|
||||
var colourProvider = getColourProvider(blockState.getBlock());
|
||||
|
||||
|
||||
long uploadPtr = UploadStream.INSTANCE.upload(this.modelBuffer, (long) modelId * MODEL_SIZE, MODEL_SIZE);
|
||||
long uploadPtr = UploadStream.INSTANCE.upload(this.storage.modelBuffer, (long) modelId * MODEL_SIZE, MODEL_SIZE);
|
||||
|
||||
|
||||
//TODO: implement;
|
||||
// TODO: if it has a constant colour instead... idk why (apparently for things like spruce leaves)?? but premultiply the texture data by the constant colour
|
||||
boolean hasBiomeColourResolver = false;
|
||||
boolean isBiomeColourDependent = false;
|
||||
if (colourProvider != null) {
|
||||
hasBiomeColourResolver = isBiomeDependentColour(colourProvider, blockState);
|
||||
isBiomeColourDependent = isBiomeDependentColour(colourProvider, blockState);
|
||||
}
|
||||
//If it contains fluid but isnt a fluid
|
||||
if ((!isFluid) && (!blockState.getFluidState().isEmpty()) && clientFluidStateId != -1) {
|
||||
|
||||
//Or it with the fluid state biome dependency
|
||||
isBiomeColourDependent |= ModelQueries.isBiomeColoured(this.getModelMetadataFromClientId(clientFluidStateId));
|
||||
}
|
||||
|
||||
|
||||
@@ -248,7 +323,8 @@ public class ModelManager {
|
||||
|
||||
if (allFalse == allTrue) {//If only some sides where self culled then abort
|
||||
cullsSame = false;
|
||||
if (LOGGED_SELF_CULLING_WARNING.add(blockState)) System.err.println("Warning! blockstate: " + blockState + " only culled against its self some of the time");
|
||||
//if (LOGGED_SELF_CULLING_WARNING.add(blockState))
|
||||
// Logger.info("Warning! blockstate: " + blockState + " only culled against its self some of the time");
|
||||
}
|
||||
|
||||
if (allTrue) {
|
||||
@@ -259,14 +335,18 @@ public class ModelManager {
|
||||
|
||||
//Each face gets 1 byte, with the top 2 bytes being for whatever
|
||||
long metadata = 0;
|
||||
metadata |= hasBiomeColourResolver?1:0;
|
||||
metadata |= blockRenderLayer == RenderLayer.getTranslucent()?2:0;
|
||||
metadata |= isBiomeColourDependent?1:0;
|
||||
metadata |= blockRenderLayer == BlockRenderLayer.TRANSLUCENT?2:0;
|
||||
metadata |= needsDoubleSidedQuads?4:0;
|
||||
metadata |= (!blockState.getFluidState().isEmpty())?8:0;//Has a fluid state accosiacted with it
|
||||
metadata |= ((!isFluid) && !blockState.getFluidState().isEmpty())?8:0;//Has a fluid state accosiacted with it and is not itself a fluid
|
||||
metadata |= isFluid?16:0;//Is a fluid
|
||||
|
||||
metadata |= cullsSame?32:0;
|
||||
|
||||
boolean fullyOpaque = true;
|
||||
|
||||
//TODO: FIXME faces that have the same "alignment depth" e.g. (sizes[0]+sizes[1])~=1 can be merged into a double faced single quad
|
||||
|
||||
//TODO: add a bunch of control config options for overriding/setting options of metadata for each face of each type
|
||||
for (int face = 5; face != -1; face--) {//In reverse order to make indexing into the metadata long easier
|
||||
long faceUploadPtr = uploadPtr + 4L * face;//Each face gets 4 bytes worth of data
|
||||
@@ -276,27 +356,32 @@ public class ModelManager {
|
||||
metadata |= 0xFF;//Mark the face as non-existent
|
||||
//Set to -1 as safepoint
|
||||
MemoryUtil.memPutInt(faceUploadPtr, -1);
|
||||
|
||||
fullyOpaque = false;
|
||||
continue;
|
||||
}
|
||||
var faceSize = TextureUtils.computeBounds(textureData[face], checkMode);
|
||||
int writeCount = TextureUtils.getWrittenPixelCount(textureData[face], checkMode);
|
||||
|
||||
boolean faceCoversFullBlock = faceSize[0] == 0 && faceSize[2] == 0 &&
|
||||
faceSize[1] == (this.modelTextureSize-1) && faceSize[3] == (this.modelTextureSize-1);
|
||||
faceSize[1] == (MODEL_TEXTURE_SIZE-1) && faceSize[3] == (MODEL_TEXTURE_SIZE-1);
|
||||
|
||||
//TODO: use faceSize and the depths to compute if mesh can be correctly rendered
|
||||
|
||||
metadata |= faceCoversFullBlock?2:0;
|
||||
|
||||
//TODO: add alot of config options for the following
|
||||
boolean occludesFace = true;
|
||||
occludesFace &= blockRenderLayer != RenderLayer.getTranslucent();//If its translucent, it doesnt occlude
|
||||
occludesFace &= blockRenderLayer != BlockRenderLayer.TRANSLUCENT;//If its translucent, it doesnt occlude
|
||||
|
||||
//TODO: make this an option, basicly if the face is really close, it occludes otherwise it doesnt
|
||||
occludesFace &= offset < 0.1;//If the face is rendered far away from the other face, then it doesnt occlude
|
||||
|
||||
if (occludesFace) {
|
||||
occludesFace &= ((float)writeCount)/(this.modelTextureSize * this.modelTextureSize) > 0.9;// only occlude if the face covers more than 90% of the face
|
||||
occludesFace &= ((float)writeCount)/(MODEL_TEXTURE_SIZE * MODEL_TEXTURE_SIZE) > 0.9;// only occlude if the face covers more than 90% of the face
|
||||
}
|
||||
metadata |= occludesFace?1:0;
|
||||
fullyOpaque &= occludesFace;
|
||||
|
||||
|
||||
|
||||
@@ -307,34 +392,53 @@ public class ModelManager {
|
||||
metadata |= canBeOccluded?4:0;
|
||||
|
||||
//Face uses its own lighting if its not flat against the adjacent block & isnt traslucent
|
||||
metadata |= (offset != 0 || blockRenderLayer == RenderLayer.getTranslucent())?0b1000:0;
|
||||
metadata |= (offset > 0.01 || blockRenderLayer == BlockRenderLayer.TRANSLUCENT)?0b1000:0;
|
||||
|
||||
|
||||
|
||||
//Scale face size from 0->this.modelTextureSize-1 to 0->15
|
||||
for (int i = 0; i < 4; i++) {
|
||||
faceSize[i] = Math.round((((float)faceSize[i])/(this.modelTextureSize-1))*15);
|
||||
if (MODEL_TEXTURE_SIZE-1 != 15) {
|
||||
//Scale face size from 0->this.modelTextureSize-1 to 0->15
|
||||
for (int i = 0; i < 4; i++) {
|
||||
faceSize[i] = Math.round((((float) faceSize[i]) / (MODEL_TEXTURE_SIZE - 1)) * 15);
|
||||
}
|
||||
}
|
||||
|
||||
int faceModelData = 0;
|
||||
faceModelData |= faceSize[0] | (faceSize[1]<<4) | (faceSize[2]<<8) | (faceSize[3]<<12);
|
||||
faceModelData |= Math.round(offset*63)<<16;//Change the scale from 0->1 (ends inclusive) float to 0->63 (6 bits) NOTE! that 63 == 1.0f meaning its shifted all the way to the other side of the model
|
||||
//Change the scale from 0->1 (ends inclusive)
|
||||
// this is cursed also warning stuff at 63 (i.e half a pixel from the end will be clamped to the end)
|
||||
int enc = Math.round(offset*64);
|
||||
faceModelData |= Math.min(enc,63)<<16;
|
||||
//Still have 11 bits free
|
||||
|
||||
//Stuff like fences are solid, however they have extra side piece that mean it needs to have discard on
|
||||
int area = (faceSize[1]-faceSize[0]+1) * (faceSize[3]-faceSize[2]+1);
|
||||
boolean needsAlphaDiscard = ((float)writeCount)/area<0.9;//If the amount of area covered by written pixels is less than a threashold, disable discard as its not needed
|
||||
|
||||
needsAlphaDiscard |= blockRenderLayer != RenderLayer.getSolid();
|
||||
needsAlphaDiscard &= blockRenderLayer != RenderLayer.getTranslucent();//Translucent doesnt have alpha discard
|
||||
needsAlphaDiscard |= blockRenderLayer != BlockRenderLayer.SOLID;
|
||||
needsAlphaDiscard &= blockRenderLayer != BlockRenderLayer.TRANSLUCENT;//Translucent doesnt have alpha discard
|
||||
faceModelData |= needsAlphaDiscard?1<<22:0;
|
||||
|
||||
faceModelData |= ((!faceCoversFullBlock)&&blockRenderLayer != RenderLayer.getTranslucent())?1<<23:0;//Alpha discard override, translucency doesnt have alpha discard
|
||||
|
||||
faceModelData |= ((!faceCoversFullBlock)&&blockRenderLayer != BlockRenderLayer.TRANSLUCENT)?1<<23:0;//Alpha discard override, translucency doesnt have alpha discard
|
||||
|
||||
//Bits 24,25 are tint metadata
|
||||
if (colourProvider!=null) {//We have a tint
|
||||
int tintState = TextureUtils.computeFaceTint(textureData[face], checkMode);
|
||||
if (tintState == 2) {//Partial tint
|
||||
faceModelData |= 1<<24;
|
||||
} else if (tintState == 3) {//Full tint
|
||||
faceModelData |= 2<<24;
|
||||
}
|
||||
}
|
||||
|
||||
MemoryUtil.memPutInt(faceUploadPtr, faceModelData);
|
||||
}
|
||||
|
||||
metadata |= fullyOpaque?(1L<<(48+6)):0;
|
||||
|
||||
boolean canBeCorrectlyRendered = true;//This represents if a model can be correctly (perfectly) represented
|
||||
// i.e. no gaps
|
||||
|
||||
this.metadataCache[modelId] = metadata;
|
||||
|
||||
uploadPtr += 4*6;
|
||||
@@ -342,63 +446,102 @@ public class ModelManager {
|
||||
// todo: put in like the render layer type ig? along with colour resolver info
|
||||
int modelFlags = 0;
|
||||
modelFlags |= colourProvider != null?1:0;
|
||||
modelFlags |= hasBiomeColourResolver?2:0;//Basicly whether to use the next int as a colour or as a base index/id into a colour buffer for biome dependent colours
|
||||
modelFlags |= blockRenderLayer == RenderLayer.getTranslucent()?4:0;
|
||||
modelFlags |= blockRenderLayer == RenderLayer.getCutout()?0:8;
|
||||
modelFlags |= isBiomeColourDependent?2:0;//Basicly whether to use the next int as a colour or as a base index/id into a colour buffer for biome dependent colours
|
||||
modelFlags |= blockRenderLayer == BlockRenderLayer.TRANSLUCENT?4:0;//Is translucent
|
||||
modelFlags |= blockRenderLayer == BlockRenderLayer.CUTOUT?0:8;//Dont use mipmaps (AND ALSO FKING SPECIFIES IF IT HAS AO, WHY??? GREAT QUESTION, TODO FIXE THIS)
|
||||
|
||||
//modelFlags |= blockRenderLayer == RenderLayer.getSolid()?0:1;// should discard alpha
|
||||
MemoryUtil.memPutInt(uploadPtr, modelFlags);
|
||||
MemoryUtil.memPutInt(uploadPtr, modelFlags); uploadPtr += 4;
|
||||
|
||||
|
||||
//Temporary override to always be non biome specific
|
||||
if (colourProvider == null) {
|
||||
MemoryUtil.memPutInt(uploadPtr + 4, -1);//Set the default to nothing so that its faster on the gpu
|
||||
} else if (!hasBiomeColourResolver) {
|
||||
Biome defaultBiome = MinecraftClient.getInstance().world.getRegistryManager().get(RegistryKeys.BIOME).get(BiomeKeys.PLAINS);
|
||||
MemoryUtil.memPutInt(uploadPtr + 4, captureColourConstant(colourProvider, blockState, defaultBiome)|0xFF000000);
|
||||
MemoryUtil.memPutInt(uploadPtr, -1);//Set the default to nothing so that its faster on the gpu
|
||||
} else if (!isBiomeColourDependent) {
|
||||
MemoryUtil.memPutInt(uploadPtr, captureColourConstant(colourProvider, blockState, DEFAULT_BIOME)|0xFF000000);
|
||||
} else if (!this.biomes.isEmpty()) {
|
||||
//Populate the list of biomes for the model state
|
||||
int biomeIndex = this.modelsRequiringBiomeColours.size() * this.biomes.size();
|
||||
MemoryUtil.memPutInt(uploadPtr + 4, biomeIndex);
|
||||
MemoryUtil.memPutInt(uploadPtr, biomeIndex);
|
||||
this.modelsRequiringBiomeColours.add(new Pair<>(modelId, blockState));
|
||||
long clrUploadPtr = UploadStream.INSTANCE.upload(this.modelColourBuffer, biomeIndex * 4L, 4L * this.biomes.size());
|
||||
//NOTE: UploadStream.INSTANCE is called _after_ uploadPtr is finished being used, this is cause the upload pointer
|
||||
// may be invalidated as soon as another upload stream is invoked
|
||||
long clrUploadPtr = UploadStream.INSTANCE.upload(this.storage.modelColourBuffer, biomeIndex * 4L, 4L * this.biomes.size());
|
||||
for (var biome : this.biomes) {
|
||||
MemoryUtil.memPutInt(clrUploadPtr, captureColourConstant(colourProvider, blockState, biome)|0xFF000000); clrUploadPtr += 4;
|
||||
}
|
||||
}
|
||||
uploadPtr += 4;
|
||||
|
||||
//have 32 bytes of free space after here
|
||||
|
||||
//install the custom mapping id if it exists
|
||||
if (this.customBlockStateIdMapping != null && this.customBlockStateIdMapping.containsKey(blockState)) {
|
||||
MemoryUtil.memPutInt(uploadPtr, this.customBlockStateIdMapping.getInt(blockState));
|
||||
} else {
|
||||
MemoryUtil.memPutInt(uploadPtr, 0);
|
||||
} uploadPtr += 4;
|
||||
|
||||
|
||||
//Note: if the layer isSolid then need to fill all the points in the texture where alpha == 0 with the average colour
|
||||
// of the surrounding blocks but only within the computed face size bounds
|
||||
//TODO
|
||||
|
||||
//TODO callback to inject extra data into the model data
|
||||
|
||||
|
||||
this.putTextures(modelId, textureData);
|
||||
|
||||
//glGenerateTextureMipmap(this.textures.id);
|
||||
return modelId;
|
||||
|
||||
//Set the mapping at the very end
|
||||
this.idMappings[blockId] = modelId;
|
||||
|
||||
if (!this.blockStatesInFlight.remove(blockId)) {
|
||||
throw new IllegalStateException("processing a texture bake result but the block state was not in flight!!");
|
||||
}
|
||||
|
||||
//Upload/commit stream
|
||||
//TODO maybe dont do it for every uploaded block?? try to batch it
|
||||
UploadStream.INSTANCE.commit();
|
||||
}
|
||||
|
||||
public void addBiome(int id, Biome biome) {
|
||||
this.biomes.add(biome);
|
||||
if (this.biomes.size()-1 != id) {
|
||||
throw new IllegalStateException("Biome ordering not consistent with biome id for biome " + biome + " expected id: " + (this.biomes.size()-1) + " got id: " + id);
|
||||
for (int i = this.biomes.size(); i <= id; i++) {
|
||||
this.biomes.add(null);
|
||||
}
|
||||
var oldBiome = this.biomes.set(id, biome);
|
||||
|
||||
if (oldBiome != null && oldBiome != biome) {
|
||||
throw new IllegalStateException("Biome was put in an id that was not null");
|
||||
}
|
||||
if (oldBiome == biome) {
|
||||
Logger.error("Biome added was a duplicate");
|
||||
}
|
||||
|
||||
int i = 0;
|
||||
for (var entry : this.modelsRequiringBiomeColours) {
|
||||
var colourProvider = MinecraftClient.getInstance().getBlockColors().providers.get(Registries.BLOCK.getRawId(entry.getRight().getBlock()));
|
||||
var colourProvider = getColourProvider(entry.getRight().getBlock());
|
||||
if (colourProvider == null) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
//Populate the list of biomes for the model state
|
||||
int biomeIndex = (i++) * this.biomes.size();
|
||||
MemoryUtil.memPutInt( UploadStream.INSTANCE.upload(this.modelBuffer, (entry.getLeft()*MODEL_SIZE)+ 4*6 + 4, 4), biomeIndex);
|
||||
long clrUploadPtr = UploadStream.INSTANCE.upload(this.modelColourBuffer, biomeIndex * 4L, 4L * this.biomes.size());
|
||||
MemoryUtil.memPutInt(UploadStream.INSTANCE.upload(this.storage.modelBuffer, (entry.getLeft()* MODEL_SIZE)+ 4*6 + 4, 4), biomeIndex);
|
||||
long clrUploadPtr = UploadStream.INSTANCE.upload(this.storage.modelColourBuffer, biomeIndex * 4L, 4L * this.biomes.size());
|
||||
for (var biomeE : this.biomes) {
|
||||
if (biomeE == null) {
|
||||
continue;//If null, ignore
|
||||
}
|
||||
MemoryUtil.memPutInt(clrUploadPtr, captureColourConstant(colourProvider, entry.getRight(), biomeE)|0xFF000000); clrUploadPtr += 4;
|
||||
}
|
||||
}
|
||||
|
||||
UploadStream.INSTANCE.commit();
|
||||
}
|
||||
|
||||
private static BlockColorProvider getColourProvider(Block block) {
|
||||
return MinecraftClient.getInstance().getBlockColors().providers.get(Registries.BLOCK.getRawId(block));
|
||||
}
|
||||
|
||||
//TODO: add a method to detect biome dependent colours (can do by detecting if getColor is ever called)
|
||||
// if it is, need to add it to a list and mark it as biome colour dependent or something then the shader
|
||||
@@ -506,181 +649,125 @@ public class ModelManager {
|
||||
return biomeDependent[0];
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
public static boolean faceExists(long metadata, int face) {
|
||||
return ((metadata>>(8*face))&0xFF)!=0xFF;
|
||||
}
|
||||
|
||||
public static boolean faceCanBeOccluded(long metadata, int face) {
|
||||
return ((metadata>>(8*face))&0b100)==0b100;
|
||||
}
|
||||
|
||||
public static boolean faceOccludes(long metadata, int face) {
|
||||
return faceExists(metadata, face) && ((metadata>>(8*face))&0b1)==0b1;
|
||||
}
|
||||
|
||||
public static boolean faceUsesSelfLighting(long metadata, int face) {
|
||||
return ((metadata>>(8*face))&0b1000) != 0;
|
||||
}
|
||||
|
||||
public static boolean isDoubleSided(long metadata) {
|
||||
return ((metadata>>(8*6))&4) != 0;
|
||||
}
|
||||
|
||||
public static boolean isTranslucent(long metadata) {
|
||||
return ((metadata>>(8*6))&2) != 0;
|
||||
}
|
||||
|
||||
public static boolean containsFluid(long metadata) {
|
||||
return ((metadata>>(8*6))&8) != 0;
|
||||
}
|
||||
|
||||
public static boolean isFluid(long metadata) {
|
||||
return ((metadata>>(8*6))&16) != 0;
|
||||
}
|
||||
|
||||
public static boolean isBiomeColoured(long metadata) {
|
||||
return ((metadata>>(8*6))&1) != 0;
|
||||
}
|
||||
|
||||
//NOTE: this might need to be moved to per face
|
||||
public static boolean cullsSame(long metadata) {
|
||||
return ((metadata>>(8*6))&32) != 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
private float[] computeModelDepth(ColourDepthTextureData[] textures, int checkMode) {
|
||||
float[] res = new float[6];
|
||||
for (var dir : Direction.values()) {
|
||||
var data = textures[dir.getId()];
|
||||
var data = textures[dir.getIndex()];
|
||||
float fd = TextureUtils.computeDepth(data, TextureUtils.DEPTH_MODE_AVG, checkMode);//Compute the min float depth, smaller means closer to the camera, range 0-1
|
||||
int depth = Math.round(fd * this.modelTextureSize);
|
||||
//int depth = Math.round(fd * MODEL_TEXTURE_SIZE);
|
||||
//If fd is -1, it means that there was nothing rendered on that face and it should be discarded
|
||||
if (fd < -0.1) {
|
||||
res[dir.ordinal()] = -1;
|
||||
} else {
|
||||
res[dir.ordinal()] = ((float) depth)/this.modelTextureSize;
|
||||
res[dir.ordinal()] = fd;//((float) depth)/MODEL_TEXTURE_SIZE;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
//TODO:FIXME: DONT DO SPIN LOCKS :WAA:
|
||||
public long getModelMetadata(int blockId) {
|
||||
public int[] _unsafeRawAccess() {
|
||||
return this.idMappings;
|
||||
}
|
||||
|
||||
public int getModelId(int blockId) {
|
||||
int map = this.idMappings[blockId];
|
||||
if (map == -1) {
|
||||
try {
|
||||
Thread.sleep(100);
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
map = this.idMappings[blockId];
|
||||
throw new IdNotYetComputedException(blockId, true);
|
||||
}
|
||||
if (map == -1) {
|
||||
throw new IdNotYetComputedException(blockId);
|
||||
}
|
||||
return this.metadataCache[map];
|
||||
//int map = 0;
|
||||
//int i = 10;
|
||||
//while ((map = this.idMappings[blockId]) == -1) {
|
||||
// Thread.onSpinWait();
|
||||
//}
|
||||
return map;
|
||||
}
|
||||
|
||||
//long meta = 0;
|
||||
//while ((meta = this.metadataCache[map]) == 0) {
|
||||
// Thread.onSpinWait();
|
||||
//}
|
||||
public boolean hasModelForBlockId(int blockId) {
|
||||
return this.idMappings[blockId] != -1;
|
||||
}
|
||||
|
||||
public int getFluidClientStateId(int clientBlockStateId) {
|
||||
int map = this.fluidStateLUT[clientBlockStateId];
|
||||
if (map == -1) {
|
||||
throw new IdNotYetComputedException(clientBlockStateId, false);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
public long getModelMetadataFromClientId(int clientId) {
|
||||
return this.metadataCache[clientId];
|
||||
}
|
||||
|
||||
public int getModelId(int blockId) {
|
||||
int map = this.idMappings[blockId];
|
||||
if (map == -1) {
|
||||
throw new IdNotYetComputedException(blockId);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
public int getFluidClientStateId(int clientBlockStateId) {
|
||||
int map = this.fluidStateLUT[clientBlockStateId];
|
||||
if (map == -1) {
|
||||
throw new IdNotYetComputedException(clientBlockStateId);
|
||||
}
|
||||
return map;
|
||||
private static int computeSizeWithMips(int size) {
|
||||
int total = 0;
|
||||
for (;size!=0;size>>=1) total += size*size;
|
||||
return total;
|
||||
}
|
||||
|
||||
private static final MemoryBuffer SCRATCH_TEX = new MemoryBuffer((2L*3*computeSizeWithMips(MODEL_TEXTURE_SIZE))*4);
|
||||
private static final int LAYERS = Integer.numberOfTrailingZeros(MODEL_TEXTURE_SIZE);
|
||||
//TODO: redo to batch blit, instead of 6 seperate blits, and also fix mipping
|
||||
private void putTextures(int id, ColourDepthTextureData[] textures) {
|
||||
int X = (id&0xFF) * this.modelTextureSize*3;
|
||||
int Y = ((id>>8)&0xFF) * this.modelTextureSize*2;
|
||||
//if (MODEL_TEXTURE_SIZE != 16) {throw new IllegalStateException("THIS METHOD MUST BE REDONE IF THIS CONST CHANGES");}
|
||||
|
||||
for (int subTex = 0; subTex < 6; subTex++) {
|
||||
int x = X + (subTex>>1)*this.modelTextureSize;
|
||||
int y = Y + (subTex&1)*this.modelTextureSize;
|
||||
//TODO: need to use a write mask to see what pixels must be used to contribute to mipping
|
||||
// as in, using the depth/stencil info, check if pixel was written to, if so, use that pixel when blending, else dont
|
||||
|
||||
GlStateManager._pixelStore(GlConst.GL_UNPACK_ROW_LENGTH, 0);
|
||||
GlStateManager._pixelStore(GlConst.GL_UNPACK_SKIP_PIXELS, 0);
|
||||
GlStateManager._pixelStore(GlConst.GL_UNPACK_SKIP_ROWS, 0);
|
||||
GlStateManager._pixelStore(GlConst.GL_UNPACK_ALIGNMENT, 4);
|
||||
var current = textures[subTex].colour();
|
||||
var next = new int[current.length>>1];
|
||||
for (int i = 0; i < 4; i++) {
|
||||
glTextureSubImage2D(this.textures.id, i, x>>i, y>>i, this.modelTextureSize>>i, this.modelTextureSize>>i, GL_RGBA, GL_UNSIGNED_BYTE, current);
|
||||
|
||||
int size = this.modelTextureSize>>(i+1);
|
||||
for (int pX = 0; pX < size; pX++) {
|
||||
for (int pY = 0; pY < size; pY++) {
|
||||
int C00 = current[(pY*2)*size+pX*2];
|
||||
int C01 = current[(pY*2+1)*size+pX*2];
|
||||
int C10 = current[(pY*2)*size+pX*2+1];
|
||||
int C11 = current[(pY*2+1)*size+pX*2+1];
|
||||
next[pY*size+pX] = TextureUtils.mipColours(C00, C01, C10, C11);
|
||||
}
|
||||
}
|
||||
|
||||
current = next;
|
||||
next = new int[current.length>>1];
|
||||
//Copy all textures into scratch
|
||||
final long addr = SCRATCH_TEX.address;
|
||||
final int LENGTH_B = MODEL_TEXTURE_SIZE*3;
|
||||
for (int i = 0; i < 6; i++) {
|
||||
int x = (i>>1)*MODEL_TEXTURE_SIZE;
|
||||
int y = (i&1)*MODEL_TEXTURE_SIZE;
|
||||
int j = 0;
|
||||
for (int t : textures[i].colour()) {
|
||||
int o = ((y+(j>>LAYERS))*LENGTH_B + ((j&(MODEL_TEXTURE_SIZE-1))+x))*4; j++;//LAYERS here is just cause faster
|
||||
MemoryUtil.memPutInt(addr+o, t);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int getBufferId() {
|
||||
return this.modelBuffer.id;
|
||||
}
|
||||
//Mip the scratch
|
||||
long dAddr = addr;
|
||||
for (int i = 0; i < LAYERS-1; i++) {
|
||||
long sAddr = dAddr;
|
||||
dAddr += (MODEL_TEXTURE_SIZE*MODEL_TEXTURE_SIZE*3*2*4)>>(i<<1);//is.. i*2 because shrink both MODEL_TEXTURE_SIZE by >>i so is 2*i total shift
|
||||
int width = (MODEL_TEXTURE_SIZE*3)>>(i+1);
|
||||
int sWidth = (MODEL_TEXTURE_SIZE*3)>>i;
|
||||
int height = (MODEL_TEXTURE_SIZE*2)>>(i+1);
|
||||
//TODO: OPTIMZIE THIS
|
||||
for (int px = 0; px < width; px++) {
|
||||
for (int py = 0; py < height; py++) {
|
||||
long bp = sAddr + (px*2 + py*2*sWidth)*4;
|
||||
int C00 = MemoryUtil.memGetInt(bp);
|
||||
int C01 = MemoryUtil.memGetInt(bp+sWidth*4);
|
||||
int C10 = MemoryUtil.memGetInt(bp+4);
|
||||
int C11 = MemoryUtil.memGetInt(bp+sWidth*4+4);
|
||||
MemoryUtil.memPutInt(dAddr + (px+py*width) * 4L, TextureUtils.mipColours(C00, C01, C10, C11));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int getTextureId() {
|
||||
return this.textures.id;
|
||||
}
|
||||
|
||||
public int getSamplerId() {
|
||||
return this.blockSampler;
|
||||
}
|
||||
int X = (id&0xFF) * MODEL_TEXTURE_SIZE*3;
|
||||
int Y = ((id>>8)&0xFF) * MODEL_TEXTURE_SIZE*2;
|
||||
|
||||
public int getColourBufferId() {
|
||||
return this.modelColourBuffer.id;
|
||||
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
|
||||
glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0);
|
||||
glPixelStorei(GL_UNPACK_SKIP_ROWS, 0);
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
|
||||
|
||||
long cAddr = addr;
|
||||
for (int lvl = 0; lvl < LAYERS; lvl++) {
|
||||
nglTextureSubImage2D(this.storage.textures.id, lvl, X >> lvl, Y >> lvl, (MODEL_TEXTURE_SIZE*3) >> lvl, (MODEL_TEXTURE_SIZE*2) >> lvl, GL_RGBA, GL_UNSIGNED_BYTE, cAddr);
|
||||
cAddr += (MODEL_TEXTURE_SIZE*MODEL_TEXTURE_SIZE*3*2*4)>>(lvl<<1);
|
||||
}
|
||||
}
|
||||
|
||||
public void free() {
|
||||
this.downstream.free();
|
||||
this.bakery.free();
|
||||
this.modelBuffer.free();
|
||||
this.modelColourBuffer.free();
|
||||
this.textures.free();
|
||||
glDeleteSamplers(this.blockSampler);
|
||||
}
|
||||
|
||||
public void addDebugInfo(List<String> info) {
|
||||
info.add("BlockModels registered: " + this.modelTexture2id.size() + "/" + (1<<16));
|
||||
public int getBakedCount() {
|
||||
return this.modelTexture2id.size();
|
||||
}
|
||||
|
||||
public int getInflightCount() {
|
||||
return this.blockStatesInFlight.size();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package me.cortex.voxy.client.core.model;
|
||||
|
||||
public abstract class ModelQueries {
|
||||
public static boolean faceExists(long metadata, int face) {
|
||||
return ((metadata>>(8*face))&0xFF)!=0xFF;
|
||||
}
|
||||
|
||||
public static boolean faceCanBeOccluded(long metadata, int face) {
|
||||
return ((metadata>>(8*face))&0b100)==0b100;
|
||||
}
|
||||
|
||||
public static boolean faceOccludes(long metadata, int face) {
|
||||
return faceExists(metadata, face) && ((metadata>>(8*face))&0b1)==0b1;
|
||||
}
|
||||
|
||||
public static boolean faceUsesSelfLighting(long metadata, int face) {
|
||||
return ((metadata>>(8*face))&0b1000) != 0;
|
||||
}
|
||||
|
||||
public static boolean isDoubleSided(long metadata) {
|
||||
return ((metadata>>(8*6))&4) != 0;
|
||||
}
|
||||
|
||||
public static boolean isTranslucent(long metadata) {
|
||||
return ((metadata>>(8*6))&2) != 0;
|
||||
}
|
||||
|
||||
public static boolean containsFluid(long metadata) {
|
||||
return ((metadata>>(8*6))&8) != 0;
|
||||
}
|
||||
|
||||
public static boolean isFluid(long metadata) {
|
||||
return ((metadata>>(8*6))&16) != 0;
|
||||
}
|
||||
|
||||
public static boolean isBiomeColoured(long metadata) {
|
||||
return ((metadata>>(8*6))&1) != 0;
|
||||
}
|
||||
|
||||
//NOTE: this might need to be moved to per face
|
||||
public static boolean cullsSame(long metadata) {
|
||||
return ((metadata>>(8*6))&32) != 0;
|
||||
}
|
||||
|
||||
public static boolean isFullyOpaque(long metadata) {
|
||||
return ((metadata>>(8*6))&64) != 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package me.cortex.voxy.client.core.model;
|
||||
|
||||
import me.cortex.voxy.client.core.gl.GlBuffer;
|
||||
import me.cortex.voxy.client.core.gl.GlTexture;
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
import net.minecraft.client.texture.SpriteAtlasTexture;
|
||||
import net.minecraft.util.Identifier;
|
||||
|
||||
import static org.lwjgl.opengl.GL11.*;
|
||||
import static org.lwjgl.opengl.GL11C.GL_NEAREST;
|
||||
import static org.lwjgl.opengl.GL11C.GL_NEAREST_MIPMAP_LINEAR;
|
||||
import static org.lwjgl.opengl.GL12C.GL_TEXTURE_MAX_LOD;
|
||||
import static org.lwjgl.opengl.GL12C.GL_TEXTURE_MIN_LOD;
|
||||
import static org.lwjgl.opengl.GL30.glBindBufferBase;
|
||||
import static org.lwjgl.opengl.GL33.*;
|
||||
import static org.lwjgl.opengl.GL33C.glSamplerParameteri;
|
||||
import static org.lwjgl.opengl.GL43.GL_SHADER_STORAGE_BUFFER;
|
||||
import static org.lwjgl.opengl.GL45.glBindTextureUnit;
|
||||
|
||||
public class ModelStore {
|
||||
public static final int MODEL_SIZE = 64;
|
||||
final GlBuffer modelBuffer;
|
||||
final GlBuffer modelColourBuffer;
|
||||
final GlTexture textures;
|
||||
public final int blockSampler = glGenSamplers();
|
||||
|
||||
public ModelStore() {
|
||||
this.modelBuffer = new GlBuffer(MODEL_SIZE * (1<<16)).name("ModelData");
|
||||
this.modelColourBuffer = new GlBuffer(4 * (1<<16)).name("ModelColour");
|
||||
this.textures = new GlTexture().store(GL_RGBA8, Integer.numberOfTrailingZeros(ModelFactory.MODEL_TEXTURE_SIZE), ModelFactory.MODEL_TEXTURE_SIZE*3*256,ModelFactory.MODEL_TEXTURE_SIZE*2*256).name("ModelTextures");
|
||||
|
||||
|
||||
//Limit the mips of the texture to match that of the terrain atlas
|
||||
int mipLvl = ((SpriteAtlasTexture) MinecraftClient.getInstance().getTextureManager()
|
||||
.getTexture(Identifier.of("minecraft", "textures/atlas/blocks.png")))
|
||||
.mipLevel;
|
||||
|
||||
glSamplerParameteri(this.blockSampler, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_LINEAR);
|
||||
glSamplerParameteri(this.blockSampler, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glSamplerParameteri(this.blockSampler, GL_TEXTURE_MIN_LOD, 0);
|
||||
glSamplerParameteri(this.blockSampler, GL_TEXTURE_MAX_LOD, mipLvl);//Integer.numberOfTrailingZeros(ModelFactory.MODEL_TEXTURE_SIZE)
|
||||
}
|
||||
|
||||
|
||||
public void free() {
|
||||
this.modelBuffer.free();
|
||||
this.modelColourBuffer.free();
|
||||
this.textures.free();
|
||||
glDeleteSamplers(this.blockSampler);
|
||||
}
|
||||
|
||||
|
||||
public void bind(int modelBindingIndex, int colourBindingIndex, int textureBindingIndex) {
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, modelBindingIndex, this.modelBuffer.id);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, colourBindingIndex, this.modelColourBuffer.id);
|
||||
glBindTextureUnit(textureBindingIndex, this.textures.id);
|
||||
glBindSampler(textureBindingIndex, this.blockSampler);
|
||||
}
|
||||
}
|
||||
@@ -1,315 +0,0 @@
|
||||
package me.cortex.voxy.client.core.model;
|
||||
|
||||
import com.mojang.blaze3d.platform.GlConst;
|
||||
import com.mojang.blaze3d.platform.GlStateManager;
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.blaze3d.systems.VertexSorter;
|
||||
import me.cortex.voxy.client.core.gl.GlFramebuffer;
|
||||
import me.cortex.voxy.client.core.gl.GlTexture;
|
||||
import me.cortex.voxy.client.core.gl.shader.Shader;
|
||||
import me.cortex.voxy.client.core.gl.shader.ShaderType;
|
||||
import me.cortex.voxy.client.core.rendering.util.GlStateCapture;
|
||||
import net.minecraft.block.*;
|
||||
import net.minecraft.block.entity.BlockEntity;
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
import net.minecraft.client.gl.GlUniform;
|
||||
import net.minecraft.client.render.*;
|
||||
import net.minecraft.client.render.model.BakedModel;
|
||||
import net.minecraft.client.util.math.MatrixStack;
|
||||
import net.minecraft.fluid.FluidState;
|
||||
import net.minecraft.util.Identifier;
|
||||
import net.minecraft.util.math.BlockPos;
|
||||
import net.minecraft.util.math.Direction;
|
||||
import net.minecraft.util.math.RotationAxis;
|
||||
import net.minecraft.util.math.random.LocalRandom;
|
||||
import net.minecraft.world.BlockRenderView;
|
||||
import net.minecraft.world.LightType;
|
||||
import net.minecraft.world.biome.ColorResolver;
|
||||
import net.minecraft.world.chunk.light.LightingProvider;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.joml.Matrix4f;
|
||||
import org.lwjgl.opengl.GL11C;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.lwjgl.opengl.ARBFramebufferObject.*;
|
||||
import static org.lwjgl.opengl.ARBImaging.GL_FUNC_ADD;
|
||||
import static org.lwjgl.opengl.ARBImaging.glBlendEquation;
|
||||
import static org.lwjgl.opengl.ARBShaderImageLoadStore.GL_FRAMEBUFFER_BARRIER_BIT;
|
||||
import static org.lwjgl.opengl.ARBShaderImageLoadStore.glMemoryBarrier;
|
||||
import static org.lwjgl.opengl.GL11.*;
|
||||
import static org.lwjgl.opengl.GL13.*;
|
||||
import static org.lwjgl.opengl.GL14C.glBlendFuncSeparate;
|
||||
import static org.lwjgl.opengl.GL20C.glUniformMatrix4fv;
|
||||
import static org.lwjgl.opengl.GL45C.glBlitNamedFramebuffer;
|
||||
import static org.lwjgl.opengl.GL45C.glGetTextureImage;
|
||||
|
||||
//Builds a texture for each face of a model
|
||||
public class ModelTextureBakery {
|
||||
private final int width;
|
||||
private final int height;
|
||||
private final GlTexture colourTex;
|
||||
private final GlTexture depthTex;
|
||||
private final GlFramebuffer framebuffer;
|
||||
private final GlStateCapture glState = GlStateCapture.make()
|
||||
.addCapability(GL_DEPTH_TEST)
|
||||
.addCapability(GL_STENCIL_TEST)
|
||||
.addCapability(GL_BLEND)
|
||||
.addCapability(GL_CULL_FACE)
|
||||
.addTexture(GL_TEXTURE0)
|
||||
.addTexture(GL_TEXTURE1)
|
||||
.build()
|
||||
;
|
||||
private final Shader rasterShader = Shader.make()
|
||||
.add(ShaderType.VERTEX, "voxy:bakery/position_tex.vsh")
|
||||
.add(ShaderType.FRAGMENT, "voxy:bakery/position_tex.fsh")
|
||||
.compile();
|
||||
|
||||
private static final List<MatrixStack> FACE_VIEWS = new ArrayList<>();
|
||||
|
||||
|
||||
public ModelTextureBakery(int width, int height) {
|
||||
//TODO: Make this run in a seperate opengl context so that it can run in a seperate thread
|
||||
|
||||
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.colourTex = new GlTexture().store(GL_RGBA8, 1, width, height);
|
||||
this.depthTex = new GlTexture().store(GL_DEPTH24_STENCIL8, 1, width, height);
|
||||
this.framebuffer = new GlFramebuffer().bind(GL_COLOR_ATTACHMENT0, this.colourTex).bind(GL_DEPTH_STENCIL_ATTACHMENT, this.depthTex).verify();
|
||||
|
||||
//This is done to help make debugging easier
|
||||
FACE_VIEWS.clear();
|
||||
AddViews();
|
||||
}
|
||||
|
||||
private static void AddViews() {
|
||||
addView(-90,0, 0, false);//Direction.DOWN
|
||||
addView(90,0, 0, false);//Direction.UP
|
||||
addView(0,180, 0, true);//Direction.NORTH
|
||||
addView(0,0, 0, false);//Direction.SOUTH
|
||||
//TODO: check these arnt the wrong way round
|
||||
addView(0,90, 270, false);//Direction.EAST
|
||||
addView(0,270, 270, false);//Direction.WEST
|
||||
}
|
||||
|
||||
private static void addView(float pitch, float yaw, float rotation, boolean flipX) {
|
||||
var stack = new MatrixStack();
|
||||
stack.translate(0.5f,0.5f,0.5f);
|
||||
stack.multiply(RotationAxis.POSITIVE_Z.rotationDegrees(rotation));
|
||||
stack.multiply(RotationAxis.POSITIVE_X.rotationDegrees(pitch));
|
||||
stack.multiply(RotationAxis.POSITIVE_Y.rotationDegrees(yaw));
|
||||
stack.translate(-0.5f,-0.5f,-0.5f);
|
||||
FACE_VIEWS.add(stack);
|
||||
}
|
||||
|
||||
|
||||
|
||||
//TODO: For block entities, also somehow attempt to render the default block entity, e.g. chests and stuff
|
||||
// cause that will result in ok looking micro details in the terrain
|
||||
public ColourDepthTextureData[] renderFaces(BlockState state, long randomValue, boolean renderFluid) {
|
||||
this.glState.capture();
|
||||
var model = MinecraftClient.getInstance()
|
||||
.getBakedModelManager()
|
||||
.getBlockModels()
|
||||
.getModel(state);
|
||||
|
||||
var entityModel = state.hasBlockEntity()?BakedBlockEntityModel.bake(state):null;
|
||||
|
||||
int oldFB = GlStateManager.getBoundFramebuffer();
|
||||
var oldProjection = new Matrix4f(RenderSystem.getProjectionMatrix());
|
||||
GL11C.glViewport(0, 0, this.width, this.height);
|
||||
|
||||
RenderSystem.setProjectionMatrix(new Matrix4f().identity().set(new float[]{
|
||||
2,0,0,0,
|
||||
0, 2,0,0,
|
||||
0,0, -1f,0,
|
||||
-1,-1,0,1,
|
||||
}), VertexSorter.BY_Z);
|
||||
|
||||
|
||||
|
||||
RenderLayer renderLayer = null;
|
||||
if (!renderFluid) {
|
||||
renderLayer = RenderLayers.getBlockLayer(state);
|
||||
} else {
|
||||
renderLayer = RenderLayers.getFluidLayer(state.getFluidState());
|
||||
}
|
||||
|
||||
|
||||
//TODO: figure out why calling this makes minecraft render black
|
||||
//renderLayer.startDrawing();
|
||||
glClearColor(0,0,0,0);
|
||||
glClearDepth(1);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, this.framebuffer.id);
|
||||
|
||||
glEnable(GL_STENCIL_TEST);
|
||||
glDepthRange(0, 1);
|
||||
glDepthMask(true);
|
||||
glEnable(GL_BLEND);
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
glEnable(GL_CULL_FACE);
|
||||
//glDepthFunc(GL_LESS);
|
||||
|
||||
|
||||
//TODO: Find a better solution
|
||||
if (renderLayer == RenderLayer.getTranslucent()) {
|
||||
//Very hacky blend function to retain the effect of the applied alpha since we dont really want to apply alpha
|
||||
// this is because we apply the alpha again when rendering the terrain meaning the alpha is being double applied
|
||||
glBlendFuncSeparate(GL_ONE_MINUS_DST_ALPHA, GL_DST_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
|
||||
} else {
|
||||
glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
|
||||
}
|
||||
|
||||
|
||||
//glBlendFunc(GL_ONE, GL_ONE);
|
||||
|
||||
glStencilOp(GL_KEEP, GL_KEEP, GL_INCR);
|
||||
glStencilFunc(GL_ALWAYS, 1, 0xFF);
|
||||
glStencilMask(0xFF);
|
||||
|
||||
this.rasterShader.bind();
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
int texId = MinecraftClient.getInstance().getTextureManager().getTexture(Identifier.of("minecraft", "textures/atlas/blocks.png")).getGlId();
|
||||
GlUniform.uniform1(0, 0);
|
||||
|
||||
var faces = new ColourDepthTextureData[FACE_VIEWS.size()];
|
||||
for (int i = 0; i < faces.length; i++) {
|
||||
faces[i] = captureView(state, model, entityModel, FACE_VIEWS.get(i), randomValue, i, renderFluid, texId);
|
||||
//glBlitNamedFramebuffer(this.framebuffer.id, oldFB, 0,0,16,16,300*(i>>1),300*(i&1),300*(i>>1)+256,300*(i&1)+256, GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
||||
}
|
||||
|
||||
renderLayer.endDrawing();
|
||||
glDisable(GL_STENCIL_TEST);
|
||||
glDisable(GL_BLEND);
|
||||
|
||||
RenderSystem.setProjectionMatrix(oldProjection, VertexSorter.BY_DISTANCE);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, oldFB);
|
||||
GL11C.glViewport(GlStateManager.Viewport.getX(), GlStateManager.Viewport.getY(), GlStateManager.Viewport.getWidth(), GlStateManager.Viewport.getHeight());
|
||||
|
||||
//TODO: FIXME: fully revert the state of opengl
|
||||
|
||||
this.glState.restore();
|
||||
return faces;
|
||||
}
|
||||
|
||||
|
||||
private ColourDepthTextureData captureView(BlockState state, BakedModel model, BakedBlockEntityModel blockEntityModel, MatrixStack stack, long randomValue, int face, boolean renderFluid, int textureId) {
|
||||
var vc = Tessellator.getInstance();
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
|
||||
|
||||
float[] mat = new float[4*4];
|
||||
new Matrix4f(RenderSystem.getProjectionMatrix()).mul(stack.peek().getPositionMatrix()).get(mat);
|
||||
glUniformMatrix4fv(1, false, mat);
|
||||
|
||||
|
||||
if (blockEntityModel != null && !renderFluid) {
|
||||
blockEntityModel.renderOut();
|
||||
}
|
||||
|
||||
var bb = vc.begin(VertexFormat.DrawMode.QUADS, VertexFormats.POSITION_TEXTURE_COLOR);
|
||||
if (!renderFluid) {
|
||||
renderQuads(bb, state, model, new MatrixStack(), randomValue);
|
||||
} else {
|
||||
MinecraftClient.getInstance().getBlockRenderManager().renderFluid(BlockPos.ORIGIN, new BlockRenderView() {
|
||||
@Override
|
||||
public float getBrightness(Direction direction, boolean shaded) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LightingProvider getLightingProvider() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLightLevel(LightType type, BlockPos pos) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getColor(BlockPos pos, ColorResolver colorResolver) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public BlockEntity getBlockEntity(BlockPos pos) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlockState getBlockState(BlockPos pos) {
|
||||
if (pos.equals(Direction.byId(face).getVector())) {
|
||||
return Blocks.AIR.getDefaultState();
|
||||
}
|
||||
|
||||
//Fixme:
|
||||
// This makes it so that the top face of water is always air, if this is commented out
|
||||
// the up block will be a liquid state which makes the sides full
|
||||
// if this is uncommented, that issue is fixed but e.g. stacking water layers ontop of eachother
|
||||
// doesnt fill the side of the block
|
||||
|
||||
//if (pos.getY() == 1) {
|
||||
// return Blocks.AIR.getDefaultState();
|
||||
//}
|
||||
return state;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FluidState getFluidState(BlockPos pos) {
|
||||
if (pos.equals(Direction.byId(face).getVector())) {
|
||||
return Blocks.AIR.getDefaultState().getFluidState();
|
||||
}
|
||||
//if (pos.getY() == 1) {
|
||||
// return Blocks.AIR.getDefaultState().getFluidState();
|
||||
//}
|
||||
return state.getFluidState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHeight() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBottomY() {
|
||||
return 0;
|
||||
}
|
||||
}, bb, state, state.getFluidState());
|
||||
}
|
||||
|
||||
glBindTexture(GL_TEXTURE_2D, textureId);
|
||||
try {
|
||||
BufferRenderer.draw(bb.end());
|
||||
} catch (IllegalStateException e) {
|
||||
System.err.println("Got empty buffer builder! for block " + state);
|
||||
}
|
||||
|
||||
glMemoryBarrier(GL_FRAMEBUFFER_BARRIER_BIT);
|
||||
int[] colourData = new int[this.width*this.height];
|
||||
int[] depthData = new int[this.width*this.height];
|
||||
glGetTextureImage(this.colourTex.id, 0, GL_RGBA, GL_UNSIGNED_BYTE, colourData);
|
||||
glGetTextureImage(this.depthTex.id, 0, GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, depthData);
|
||||
return new ColourDepthTextureData(colourData, depthData, this.width, this.height);
|
||||
}
|
||||
|
||||
private static void renderQuads(BufferBuilder builder, BlockState state, BakedModel model, MatrixStack stack, long randomValue) {
|
||||
for (Direction direction : new Direction[]{Direction.DOWN, Direction.UP, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST, null}) {
|
||||
var quads = model.getQuads(state, direction, new LocalRandom(randomValue));
|
||||
for (var quad : quads) {
|
||||
//TODO: mark pixels that have
|
||||
int meta = quad.hasColor()?1:0;
|
||||
builder.quad(stack.peek(), quad, 255f/((meta>>16)&0xff), 255f/((meta>>8)&0xff), 255f/(meta&0xff), 1.0f, 0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void free() {
|
||||
this.framebuffer.free();
|
||||
this.colourTex.free();
|
||||
this.depthTex.free();
|
||||
this.rasterShader.free();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package me.cortex.voxy.client.core.model;
|
||||
|
||||
import me.jellysquid.mods.sodium.client.util.color.ColorSRGB;
|
||||
import net.minecraft.util.math.ColorHelper;
|
||||
import net.caffeinemc.mods.sodium.client.util.color.ColorSRGB;
|
||||
import net.minecraft.client.texture.MipmapHelper;
|
||||
|
||||
//Texturing utils to manipulate data from the model bakery
|
||||
public class TextureUtils {
|
||||
@@ -38,6 +38,37 @@ public class TextureUtils {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
|
||||
|
||||
//0: nothing written
|
||||
//1: none tinted
|
||||
//2: some tinted
|
||||
//3: all tinted
|
||||
public static int computeFaceTint(ColourDepthTextureData texture, int checkMode) {
|
||||
boolean allTinted = true;
|
||||
boolean someTinted = false;
|
||||
boolean wasWriten = false;
|
||||
|
||||
final var colourData = texture.colour();
|
||||
final var depthData = texture.depth();
|
||||
for (int i = 0; i < colourData.length; i++) {
|
||||
if (!wasPixelWritten(texture, checkMode, i)) {
|
||||
continue;
|
||||
}
|
||||
if ((colourData[i]&0xFFFFFF) == 0 || (colourData[i]>>>24)==0) {//If the pixel is fully black (or translucent)
|
||||
continue;
|
||||
}
|
||||
boolean pixelTinited = (depthData[i]&(1<<7))!=0;
|
||||
wasWriten |= true;
|
||||
allTinted &= pixelTinited;
|
||||
someTinted |= pixelTinited;
|
||||
|
||||
}
|
||||
if (!wasWriten) {
|
||||
return 0;
|
||||
}
|
||||
return someTinted?(allTinted?3:2):1;
|
||||
}
|
||||
|
||||
public static final int DEPTH_MODE_AVG = 1;
|
||||
public static final int DEPTH_MODE_MAX = 2;
|
||||
public static final int DEPTH_MODE_MIN = 3;
|
||||
@@ -92,10 +123,12 @@ public class TextureUtils {
|
||||
private static float u2fdepth(int depth) {
|
||||
float depthF = (float) ((double)depth/((1<<24)-1));
|
||||
//https://registry.khronos.org/OpenGL-Refpages/gl4/html/glDepthRange.xhtml
|
||||
// due to this and the unsigned bullshit, i believe the depth value needs to get multiplied by 2
|
||||
// due to this and the unsigned bullshit, believe the depth value needs to get multiplied by 2
|
||||
|
||||
//Shouldent be needed due to the compute bake copy
|
||||
depthF *= 2;
|
||||
if (depthF > 1.00001f) {
|
||||
System.err.println("Warning: Depth greater than 1");
|
||||
if (depthF > 1.00001f) {//Basicly only happens when a model goes out of bounds (thing)
|
||||
//System.err.println("Warning: Depth greater than 1");
|
||||
depthF = 1.0f;
|
||||
}
|
||||
return depthF;
|
||||
@@ -176,43 +209,50 @@ public class TextureUtils {
|
||||
|
||||
|
||||
public static int mipColours(int one, int two, int three, int four) {
|
||||
return weightedAverageColor(weightedAverageColor(one, two), weightedAverageColor(three, four));
|
||||
}
|
||||
|
||||
private static int weightedAverageColor(int one, int two) {
|
||||
int alphaOne = ColorHelper.Abgr.getAlpha(one);
|
||||
int alphaTwo = ColorHelper.Abgr.getAlpha(two);
|
||||
if (alphaOne == alphaTwo) {
|
||||
return averageRgb(one, two, alphaOne);
|
||||
} else if (alphaOne == 0) {
|
||||
return two & 16777215 | alphaTwo >> 2 << 24;
|
||||
} else if (alphaTwo == 0) {
|
||||
return one & 16777215 | alphaOne >> 2 << 24;
|
||||
if (true) {
|
||||
return MipmapHelper.blend(one, two, three, four, false);
|
||||
} else {
|
||||
float scale = 1.0F / (float)(alphaOne + alphaTwo);
|
||||
float relativeWeightOne = (float)alphaOne * scale;
|
||||
float relativeWeightTwo = (float)alphaTwo * scale;
|
||||
float oneR = ColorSRGB.srgbToLinear(ColorHelper.Abgr.getRed(one)) * relativeWeightOne;
|
||||
float oneG = ColorSRGB.srgbToLinear(ColorHelper.Abgr.getGreen(one)) * relativeWeightOne;
|
||||
float oneB = ColorSRGB.srgbToLinear(ColorHelper.Abgr.getBlue(one)) * relativeWeightOne;
|
||||
float twoR = ColorSRGB.srgbToLinear(ColorHelper.Abgr.getRed(two)) * relativeWeightTwo;
|
||||
float twoG = ColorSRGB.srgbToLinear(ColorHelper.Abgr.getGreen(two)) * relativeWeightTwo;
|
||||
float twoB = ColorSRGB.srgbToLinear(ColorHelper.Abgr.getBlue(two)) * relativeWeightTwo;
|
||||
float linearR = oneR + twoR;
|
||||
float linearG = oneG + twoG;
|
||||
float linearB = oneB + twoB;
|
||||
int averageAlpha = alphaOne + alphaTwo >> 1;
|
||||
return ColorSRGB.linearToSrgb(linearR, linearG, linearB, averageAlpha);
|
||||
return weightedAverageColor(weightedAverageColor(one, two), weightedAverageColor(three, four));
|
||||
}
|
||||
}
|
||||
|
||||
private static int averageRgb(int a, int b, int alpha) {
|
||||
float ar = ColorSRGB.srgbToLinear(ColorHelper.Abgr.getRed(a));
|
||||
float ag = ColorSRGB.srgbToLinear(ColorHelper.Abgr.getGreen(a));
|
||||
float ab = ColorSRGB.srgbToLinear(ColorHelper.Abgr.getBlue(a));
|
||||
float br = ColorSRGB.srgbToLinear(ColorHelper.Abgr.getRed(b));
|
||||
float bg = ColorSRGB.srgbToLinear(ColorHelper.Abgr.getGreen(b));
|
||||
float bb = ColorSRGB.srgbToLinear(ColorHelper.Abgr.getBlue(b));
|
||||
return ColorSRGB.linearToSrgb((ar + br) * 0.5F, (ag + bg) * 0.5F, (ab + bb) * 0.5F, alpha);
|
||||
//TODO: FIXME!!! ITS READING IT AS ABGR??? isnt the format RGBA??
|
||||
private static int weightedAverageColor(int a, int b) {
|
||||
//We specifically want the entire other component if the alpha is zero
|
||||
// this prevents black mips from generating due to A) non filled colours, and B) when the sampler samples everything it doesnt detonate
|
||||
if ((a&0xFF000000) == 0) {
|
||||
return b;
|
||||
}
|
||||
if ((b&0xFF000000) == 0) {
|
||||
return a;
|
||||
}
|
||||
|
||||
if (((a^b)&0xFF000000)==0) {
|
||||
return ColorSRGB.linearToSrgb(
|
||||
addHalfLinear(0, a,b),
|
||||
addHalfLinear(8, a,b),
|
||||
addHalfLinear(16, a,b),
|
||||
a>>>24);
|
||||
}
|
||||
|
||||
{
|
||||
int A = (a>>>24);
|
||||
int B = (a>>>24);
|
||||
float mul = 1.0F / (float)(A+B);
|
||||
float wA = A * mul;
|
||||
float wB = B * mul;
|
||||
return ColorSRGB.linearToSrgb(
|
||||
addMulLinear(0, a,b,wA,wB),
|
||||
addMulLinear(8, a,b,wA,wB),
|
||||
addMulLinear(16, a,b,wA,wB)
|
||||
, (A + B)/2);
|
||||
}
|
||||
}
|
||||
|
||||
private static float addHalfLinear(int shift, int a, int b) {
|
||||
return addMulLinear(shift, a, b, 0.5f, 0.5f);
|
||||
}
|
||||
private static float addMulLinear(int shift, int a, int b, float mulA, float mulB) {
|
||||
return Math.fma(ColorSRGB.srgbToLinear((a>>shift)&0xFF),mulA, ColorSRGB.srgbToLinear((b>>shift)&0xFF)*mulB);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
package me.cortex.voxy.client.core.model.bakery;
|
||||
|
||||
import me.cortex.voxy.common.Logger;
|
||||
import net.minecraft.block.BlockEntityProvider;
|
||||
import net.minecraft.block.BlockState;
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
import net.minecraft.client.render.RenderLayer;
|
||||
import net.minecraft.client.util.math.MatrixStack;
|
||||
import net.minecraft.util.Identifier;
|
||||
import net.minecraft.util.math.BlockPos;
|
||||
import net.minecraft.util.math.Vec3d;
|
||||
import org.joml.Matrix4f;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class BakedBlockEntityModel {
|
||||
private record LayerConsumer(RenderLayer layer, ReuseVertexConsumer consumer) {}
|
||||
private final List<LayerConsumer> layers;
|
||||
private BakedBlockEntityModel(List<LayerConsumer> layers) {
|
||||
this.layers = layers;
|
||||
}
|
||||
|
||||
public void render(Matrix4f matrix, int texId) {
|
||||
for (var layer : this.layers) {
|
||||
if (layer.consumer.isEmpty()) continue;
|
||||
if (layer.layer instanceof RenderLayer.MultiPhase mp) {
|
||||
Identifier textureId = mp.phases.texture.getId().orElse(null);
|
||||
if (textureId == null) {
|
||||
Logger.error("ERROR: Empty texture id for layer: " + layer);
|
||||
} else {
|
||||
texId = ((net.minecraft.client.texture.GlTexture)MinecraftClient.getInstance().getTextureManager().getTexture(textureId).getGlTexture()).getGlId();
|
||||
}
|
||||
}
|
||||
if (texId == 0) continue;
|
||||
BudgetBufferRenderer.setup(layer.consumer.getAddress(), layer.consumer.quadCount(), texId);
|
||||
BudgetBufferRenderer.render(matrix);
|
||||
}
|
||||
}
|
||||
|
||||
public void release() {
|
||||
this.layers.forEach(layer->layer.consumer.free());
|
||||
}
|
||||
|
||||
private static int getMetaFromLayer(RenderLayer layer) {
|
||||
boolean hasDiscard = layer == RenderLayer.getCutout() ||
|
||||
layer == RenderLayer.getCutoutMipped() ||
|
||||
layer == RenderLayer.getTripwire();
|
||||
|
||||
boolean isMipped = layer == RenderLayer.getCutoutMipped() ||
|
||||
layer == RenderLayer.getSolid() ||
|
||||
layer.isTranslucent() ||
|
||||
layer == RenderLayer.getTripwire();
|
||||
|
||||
int meta = hasDiscard?1:0;
|
||||
meta |= isMipped?2:0;
|
||||
return meta;
|
||||
}
|
||||
|
||||
public static BakedBlockEntityModel bake(BlockState state) {
|
||||
Map<RenderLayer, LayerConsumer> map = new HashMap<>();
|
||||
var entity = ((BlockEntityProvider)state.getBlock()).createBlockEntity(BlockPos.ORIGIN, state);
|
||||
if (entity == null) {
|
||||
return null;
|
||||
}
|
||||
var renderer = MinecraftClient.getInstance().getBlockEntityRenderDispatcher().get(entity);
|
||||
entity.setWorld(MinecraftClient.getInstance().world);
|
||||
if (renderer != null) {
|
||||
try {
|
||||
renderer.render(entity, 0.0f, new MatrixStack(), layer->map.computeIfAbsent(layer, rl -> new LayerConsumer(rl, new ReuseVertexConsumer().setDefaultMeta(getMetaFromLayer(rl)))).consumer, 0, 0, new Vec3d(0,0,0));
|
||||
} catch (Exception e) {
|
||||
Logger.error("Unable to bake block entity: " + entity, e);
|
||||
}
|
||||
}
|
||||
entity.markRemoved();
|
||||
if (map.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
for (var i : new ArrayList<>(map.values())) {
|
||||
if (i.consumer.isEmpty()) {
|
||||
map.remove(i.layer);
|
||||
i.consumer.free();
|
||||
}
|
||||
}
|
||||
if (map.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return new BakedBlockEntityModel(new ArrayList<>(map.values()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package me.cortex.voxy.client.core.model.bakery;
|
||||
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.blaze3d.textures.GpuTexture;
|
||||
import com.mojang.blaze3d.vertex.VertexFormat;
|
||||
import me.cortex.voxy.client.core.gl.GlBuffer;
|
||||
import me.cortex.voxy.client.core.gl.GlVertexArray;
|
||||
import me.cortex.voxy.client.core.gl.shader.Shader;
|
||||
import me.cortex.voxy.client.core.gl.shader.ShaderType;
|
||||
import me.cortex.voxy.client.core.rendering.util.UploadStream;
|
||||
import net.minecraft.client.gl.GlGpuBuffer;
|
||||
import net.minecraft.client.render.BuiltBuffer;
|
||||
import org.joml.Matrix4f;
|
||||
import org.lwjgl.system.MemoryUtil;
|
||||
|
||||
import static org.lwjgl.opengl.GL20.glUniformMatrix4fv;
|
||||
import static org.lwjgl.opengl.GL33.glBindSampler;
|
||||
import static org.lwjgl.opengl.GL45.*;
|
||||
|
||||
public class BudgetBufferRenderer {
|
||||
public static final int VERTEX_FORMAT_SIZE = 24;
|
||||
|
||||
private static final Shader bakeryShader = Shader.make()
|
||||
.add(ShaderType.VERTEX, "voxy:bakery/position_tex.vsh")
|
||||
.add(ShaderType.FRAGMENT, "voxy:bakery/position_tex.fsh")
|
||||
.compile();
|
||||
|
||||
|
||||
public static void init(){}
|
||||
private static final GlBuffer indexBuffer;
|
||||
static {
|
||||
var i = RenderSystem.getSequentialBuffer(VertexFormat.DrawMode.QUADS);
|
||||
int id = ((GlGpuBuffer) i.getIndexBuffer(4096*3*2)).id;
|
||||
if (i.getIndexType() != VertexFormat.IndexType.SHORT) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
indexBuffer = new GlBuffer(3*2*2*4096);
|
||||
glCopyNamedBufferSubData(id, indexBuffer.id, 0, 0, 3*2*2*4096);
|
||||
}
|
||||
|
||||
private static final int STRIDE = 24;
|
||||
private static final GlVertexArray VA = new GlVertexArray()
|
||||
.setStride(STRIDE)
|
||||
.setF(0, GL_FLOAT, 4, 0)//pos, metadata
|
||||
.setF(1, GL_FLOAT, 2, 4 * 4)//UV
|
||||
.bindElementBuffer(indexBuffer.id);
|
||||
|
||||
private static GlBuffer immediateBuffer;
|
||||
private static int quadCount;
|
||||
public static void drawFast(BuiltBuffer buffer, GpuTexture tex, Matrix4f matrix) {
|
||||
if (buffer.getDrawParameters().mode() != VertexFormat.DrawMode.QUADS) {
|
||||
throw new IllegalStateException("Fast only supports quads");
|
||||
}
|
||||
|
||||
var buff = buffer.getBuffer();
|
||||
int size = buff.remaining();
|
||||
if (size%STRIDE != 0) throw new IllegalStateException();
|
||||
size /= STRIDE;
|
||||
if (size%4 != 0) throw new IllegalStateException();
|
||||
size /= 4;
|
||||
setup(MemoryUtil.memAddress(buff), size, ((net.minecraft.client.texture.GlTexture)tex).getGlId());
|
||||
buffer.close();
|
||||
|
||||
render(matrix);
|
||||
}
|
||||
|
||||
public static void setup(long dataPtr, int quads, int texId) {
|
||||
if (quads == 0) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
quadCount = quads;
|
||||
|
||||
long size = quads * 4L * STRIDE;
|
||||
if (immediateBuffer == null || immediateBuffer.size()<size) {
|
||||
if (immediateBuffer != null) {
|
||||
immediateBuffer.free();
|
||||
}
|
||||
immediateBuffer = new GlBuffer(size*2L);//This also accounts for when immediateBuffer == null
|
||||
VA.bindBuffer(immediateBuffer.id);
|
||||
}
|
||||
long ptr = UploadStream.INSTANCE.upload(immediateBuffer, 0, size);
|
||||
MemoryUtil.memCopy(dataPtr, ptr, size);
|
||||
UploadStream.INSTANCE.commit();
|
||||
|
||||
bakeryShader.bind();
|
||||
VA.bind();
|
||||
glMemoryBarrier(GL_VERTEX_ATTRIB_ARRAY_BARRIER_BIT);
|
||||
glBindSampler(0, 0);
|
||||
glBindTextureUnit(0, texId);
|
||||
}
|
||||
|
||||
public static void render(Matrix4f matrix) {
|
||||
glUniformMatrix4fv(1, false, matrix.get(new float[16]));
|
||||
glDrawElements(GL_TRIANGLES, quadCount * 2 * 3, GL_UNSIGNED_SHORT, 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package me.cortex.voxy.client.core.model.bakery;
|
||||
|
||||
import me.cortex.voxy.client.core.gl.GlFramebuffer;
|
||||
import me.cortex.voxy.client.core.gl.GlTexture;
|
||||
import me.cortex.voxy.client.core.gl.shader.Shader;
|
||||
import me.cortex.voxy.client.core.gl.shader.ShaderType;
|
||||
import org.lwjgl.system.MemoryStack;
|
||||
import org.lwjgl.system.MemoryUtil;
|
||||
|
||||
import static org.lwjgl.opengl.ARBDirectStateAccess.*;
|
||||
import static org.lwjgl.opengl.ARBShaderImageLoadStore.GL_FRAMEBUFFER_BARRIER_BIT;
|
||||
import static org.lwjgl.opengl.ARBShaderImageLoadStore.GL_PIXEL_BUFFER_BARRIER_BIT;
|
||||
import static org.lwjgl.opengl.ARBShaderImageLoadStore.GL_SHADER_IMAGE_ACCESS_BARRIER_BIT;
|
||||
import static org.lwjgl.opengl.ARBShaderImageLoadStore.GL_TEXTURE_UPDATE_BARRIER_BIT;
|
||||
import static org.lwjgl.opengl.ARBShaderImageLoadStore.glMemoryBarrier;
|
||||
import static org.lwjgl.opengl.GL11.*;
|
||||
import static org.lwjgl.opengl.GL30.*;
|
||||
import static org.lwjgl.opengl.GL43.*;
|
||||
import static org.lwjgl.opengl.GL45.glClearNamedFramebufferfi;
|
||||
|
||||
public class GlViewCapture {
|
||||
private final int width;
|
||||
private final int height;
|
||||
private final GlTexture colourTex;
|
||||
private final GlTexture depthTex;
|
||||
private final GlTexture stencilTex;
|
||||
private final GlTexture metaTex;
|
||||
final GlFramebuffer framebuffer;
|
||||
private final Shader copyOutShader;
|
||||
|
||||
public GlViewCapture(int width, int height) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.metaTex = new GlTexture().store(GL_R32UI, 1, width*3, height*2).name("ModelBakeryMetadata");
|
||||
this.colourTex = new GlTexture().store(GL_RGBA8, 1, width*3, height*2).name("ModelBakeryColour");
|
||||
this.depthTex = new GlTexture().store(GL_DEPTH24_STENCIL8, 1, width*3, height*2).name("ModelBakeryDepth");
|
||||
//TODO: FIXME: Mesa is broken when trying to read from a sampler of GL_STENCIL_INDEX
|
||||
// it seems to just ignore the value set in GL_DEPTH_STENCIL_TEXTURE_MODE
|
||||
glTextureParameteri(this.depthTex.id, GL_DEPTH_STENCIL_TEXTURE_MODE, GL_STENCIL_INDEX);
|
||||
this.stencilTex = this.depthTex.createView();
|
||||
glTextureParameteri(this.depthTex.id, GL_DEPTH_STENCIL_TEXTURE_MODE, GL_DEPTH_COMPONENT);
|
||||
|
||||
this.framebuffer = new GlFramebuffer().bind(GL_COLOR_ATTACHMENT0, this.colourTex).bind(GL_COLOR_ATTACHMENT1, this.metaTex).setDrawBuffers(GL_COLOR_ATTACHMENT0,GL_COLOR_ATTACHMENT1).bind(GL_DEPTH_STENCIL_ATTACHMENT, this.depthTex).verify().name("ModelFramebuffer");
|
||||
|
||||
glTextureParameteri(this.stencilTex.id, GL_DEPTH_STENCIL_TEXTURE_MODE, GL_STENCIL_INDEX);
|
||||
glTextureParameteri(this.stencilTex.id, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTextureParameteri(this.stencilTex.id, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
|
||||
glTextureParameteri(this.metaTex.id, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTextureParameteri(this.metaTex.id, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
|
||||
this.copyOutShader = Shader.makeAuto()
|
||||
.define("WIDTH", width)
|
||||
.define("HEIGHT", height)
|
||||
.define("COLOUR_IN_BINDING", 0)
|
||||
.define("DEPTH_IN_BINDING", 1)
|
||||
.define("STENCIL_IN_BINDING", 2)
|
||||
.define("META_IN_BINDING", 3)
|
||||
.define("BUFFER_OUT_BINDING", 4)
|
||||
.add(ShaderType.COMPUTE, "voxy:bakery/bufferreorder.comp")
|
||||
.compile()
|
||||
.name("ModelBakeryOut")
|
||||
.texture("META_IN_BINDING", 0, this.metaTex)
|
||||
.texture("COLOUR_IN_BINDING", 0, this.colourTex)
|
||||
.texture("DEPTH_IN_BINDING", 0, this.depthTex)
|
||||
.texture("STENCIL_IN_BINDING", 0, this.stencilTex);
|
||||
}
|
||||
|
||||
public void emitToStream(int buffer, int offset) {
|
||||
this.copyOutShader.bind();
|
||||
glBindBufferRange(GL_SHADER_STORAGE_BUFFER, 4, buffer, offset, (this.width*3L)*(this.height*2L)*4L*2);//its 2*4 because colour + depth stencil
|
||||
glMemoryBarrier(GL_FRAMEBUFFER_BARRIER_BIT|GL_TEXTURE_UPDATE_BARRIER_BIT|GL_PIXEL_BUFFER_BARRIER_BIT|GL_SHADER_IMAGE_ACCESS_BARRIER_BIT);//Am not sure if barriers are right
|
||||
glDispatchCompute(3, 2, 1);
|
||||
glBindBufferRange(GL_SHADER_STORAGE_BUFFER, 4, 0, 0, 4);//WHY DOES THIS FIX FUCKING BINDING ISSUES HERE WHEN DOING THIS IN THE RENDER SYSTEM DOESNT
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
try (var stack = MemoryStack.stackPush()) {
|
||||
long ptr = stack.nmalloc(4*4);
|
||||
MemoryUtil.memPutLong(ptr, 0);
|
||||
MemoryUtil.memPutLong(ptr+8, 0);
|
||||
nglClearNamedFramebufferfv(this.framebuffer.id, GL_COLOR, 0, ptr);
|
||||
nglClearNamedFramebufferuiv(this.framebuffer.id, GL_COLOR, 1, ptr);
|
||||
//TODO: fix the draw buffer thing maybe? it might need todo multiple clears
|
||||
//nglClearNamedFramebufferfv(this.framebuffer.id, GL_COLOR, 0, ptr);
|
||||
}
|
||||
glClearNamedFramebufferfi(this.framebuffer.id, GL_DEPTH_STENCIL, 0, 1.0f, 0);
|
||||
}
|
||||
|
||||
public void free() {
|
||||
this.framebuffer.free();
|
||||
this.colourTex.free();
|
||||
this.stencilTex.free();
|
||||
this.depthTex.free();
|
||||
this.metaTex.free();
|
||||
this.copyOutShader.free();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,364 @@
|
||||
package me.cortex.voxy.client.core.model.bakery;
|
||||
|
||||
import net.minecraft.block.*;
|
||||
import net.minecraft.block.entity.BlockEntity;
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
import net.minecraft.client.render.BlockRenderLayer;
|
||||
import net.minecraft.client.render.RenderLayers;
|
||||
import net.minecraft.client.util.math.MatrixStack;
|
||||
import net.minecraft.fluid.FluidState;
|
||||
import net.minecraft.util.Identifier;
|
||||
import net.minecraft.util.math.BlockPos;
|
||||
import net.minecraft.util.math.Direction;
|
||||
import net.minecraft.util.math.random.LocalRandom;
|
||||
import net.minecraft.world.BlockRenderView;
|
||||
import net.minecraft.world.LightType;
|
||||
import net.minecraft.world.biome.ColorResolver;
|
||||
import net.minecraft.world.chunk.light.LightingProvider;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.joml.Matrix4f;
|
||||
import org.joml.Quaternionf;
|
||||
import org.joml.Vector3f;
|
||||
import org.lwjgl.opengl.GL14;
|
||||
|
||||
import static org.lwjgl.opengl.GL11.*;
|
||||
import static org.lwjgl.opengl.GL14C.glBlendFuncSeparate;
|
||||
import static org.lwjgl.opengl.GL30.*;
|
||||
import static org.lwjgl.opengl.GL45.glTextureBarrier;
|
||||
|
||||
public class ModelTextureBakery {
|
||||
//Note: the first bit of metadata is if alpha discard is enabled
|
||||
private static final Matrix4f[] VIEWS = new Matrix4f[6];
|
||||
|
||||
private final GlViewCapture capture;
|
||||
private final ReuseVertexConsumer vc = new ReuseVertexConsumer();
|
||||
|
||||
private final int width;
|
||||
private final int height;
|
||||
public ModelTextureBakery(int width, int height) {
|
||||
this.capture = new GlViewCapture(width, height);
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
public static int getMetaFromLayer(BlockRenderLayer layer) {
|
||||
boolean hasDiscard = layer == BlockRenderLayer.CUTOUT ||
|
||||
layer == BlockRenderLayer.CUTOUT_MIPPED ||
|
||||
layer == BlockRenderLayer.TRIPWIRE;
|
||||
|
||||
boolean isMipped = layer == BlockRenderLayer.CUTOUT_MIPPED ||
|
||||
layer == BlockRenderLayer.SOLID ||
|
||||
layer == BlockRenderLayer.TRANSLUCENT ||
|
||||
layer == BlockRenderLayer.TRIPWIRE;
|
||||
|
||||
int meta = hasDiscard?1:0;
|
||||
meta |= isMipped?2:0;
|
||||
return meta;
|
||||
}
|
||||
|
||||
private void bakeBlockModel(BlockState state, BlockRenderLayer layer) {
|
||||
if (state.getRenderType() == BlockRenderType.INVISIBLE) {
|
||||
return;//Dont bake if invisible
|
||||
}
|
||||
var model = MinecraftClient.getInstance()
|
||||
.getBakedModelManager()
|
||||
.getBlockModels()
|
||||
.getModel(state);
|
||||
|
||||
int meta = getMetaFromLayer(layer);
|
||||
|
||||
for (var part : model.getParts(new LocalRandom(42L))) {
|
||||
for (Direction direction : new Direction[]{Direction.DOWN, Direction.UP, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST, null}) {
|
||||
var quads = part.getQuads(direction);
|
||||
for (var quad : quads) {
|
||||
this.vc.quad(quad, meta|(quad.hasTint()?4:0));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void bakeFluidState(BlockState state, BlockRenderLayer layer, int face) {
|
||||
{
|
||||
//TODO: somehow set the tint flag per quad or something?
|
||||
int metadata = getMetaFromLayer(layer);
|
||||
//Just assume all fluids are tinted, if they arnt it should be implicitly culled in the model baking phase
|
||||
// since it wont have the colour provider
|
||||
metadata |= 4;//Has tint
|
||||
this.vc.setDefaultMeta(metadata);//Set the meta while baking
|
||||
}
|
||||
MinecraftClient.getInstance().getBlockRenderManager().renderFluid(BlockPos.ORIGIN, new BlockRenderView() {
|
||||
@Override
|
||||
public float getBrightness(Direction direction, boolean shaded) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LightingProvider getLightingProvider() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLightLevel(LightType type, BlockPos pos) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getColor(BlockPos pos, ColorResolver colorResolver) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public BlockEntity getBlockEntity(BlockPos pos) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlockState getBlockState(BlockPos pos) {
|
||||
if (shouldReturnAirForFluid(pos, face)) {
|
||||
return Blocks.AIR.getDefaultState();
|
||||
}
|
||||
|
||||
//Fixme:
|
||||
// This makes it so that the top face of water is always air, if this is commented out
|
||||
// the up block will be a liquid state which makes the sides full
|
||||
// if this is uncommented, that issue is fixed but e.g. stacking water layers ontop of eachother
|
||||
// doesnt fill the side of the block
|
||||
|
||||
//if (pos.getY() == 1) {
|
||||
// return Blocks.AIR.getDefaultState();
|
||||
//}
|
||||
return state;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FluidState getFluidState(BlockPos pos) {
|
||||
if (shouldReturnAirForFluid(pos, face)) {
|
||||
return Blocks.AIR.getDefaultState().getFluidState();
|
||||
}
|
||||
|
||||
return state.getFluidState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHeight() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBottomY() {
|
||||
return 0;
|
||||
}
|
||||
}, this. vc, state, state.getFluidState());
|
||||
this.vc.setDefaultMeta(0);//Reset default meta
|
||||
}
|
||||
|
||||
private static boolean shouldReturnAirForFluid(BlockPos pos, int face) {
|
||||
var fv = Direction.byIndex(face).getVector();
|
||||
int dot = fv.getX()*pos.getX() + fv.getY()*pos.getY() + fv.getZ()*pos.getZ();
|
||||
return dot >= 1;
|
||||
}
|
||||
|
||||
public void free() {
|
||||
this.capture.free();
|
||||
this.vc.free();
|
||||
}
|
||||
|
||||
|
||||
public void renderToStream(BlockState state, int streamBuffer, int streamOffset) {
|
||||
this.capture.clear();
|
||||
boolean isBlock = true;
|
||||
BlockRenderLayer layer;
|
||||
if (state.getBlock() instanceof FluidBlock) {
|
||||
layer = RenderLayers.getFluidLayer(state.getFluidState());
|
||||
isBlock = false;
|
||||
} else {
|
||||
if (state.getBlock() instanceof LeavesBlock) {
|
||||
layer = BlockRenderLayer.SOLID;
|
||||
} else {
|
||||
layer = RenderLayers.getBlockLayer(state);
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: support block model entities
|
||||
BakedBlockEntityModel bbem = null;
|
||||
if (state.hasBlockEntity()) {
|
||||
bbem = BakedBlockEntityModel.bake(state);
|
||||
}
|
||||
|
||||
//Setup GL state
|
||||
int[] viewdat = new int[4];
|
||||
int blockTextureId;
|
||||
|
||||
{
|
||||
glEnable(GL_STENCIL_TEST);
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
glEnable(GL_CULL_FACE);
|
||||
if (layer == BlockRenderLayer.TRANSLUCENT) {
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFuncSeparate(GL_ONE_MINUS_DST_ALPHA, GL_DST_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
|
||||
} else {
|
||||
glDisable(GL_BLEND);//FUCK YOU INTEL (screams), for _some reason_ discard or something... JUST DOESNT WORK??
|
||||
//glBlendFuncSeparate(GL_ONE, GL_ZERO, GL_ONE, GL_ONE);
|
||||
}
|
||||
|
||||
glStencilOp(GL_KEEP, GL_KEEP, GL_INCR);
|
||||
glStencilFunc(GL_ALWAYS, 1, 0xFF);
|
||||
glStencilMask(0xFF);
|
||||
|
||||
glGetIntegerv(GL_VIEWPORT, viewdat);//TODO: faster way todo this, or just use main framebuffer resolution
|
||||
|
||||
//Bind the capture framebuffer
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, this.capture.framebuffer.id);
|
||||
|
||||
var tex = MinecraftClient.getInstance().getTextureManager().getTexture(Identifier.of("minecraft", "textures/atlas/blocks.png")).getGlTexture();
|
||||
blockTextureId = ((net.minecraft.client.texture.GlTexture)tex).getGlId();
|
||||
}
|
||||
|
||||
//TODO: fastpath for blocks
|
||||
if (isBlock) {
|
||||
this.vc.reset();
|
||||
this.bakeBlockModel(state, layer);
|
||||
if (!this.vc.isEmpty()) {//only render if there... is shit to render
|
||||
|
||||
//Setup for continual emission
|
||||
BudgetBufferRenderer.setup(this.vc.getAddress(), this.vc.quadCount(), blockTextureId);//note: this.vc.buffer.address NOT this.vc.ptr
|
||||
|
||||
var mat = new Matrix4f();
|
||||
for (int i = 0; i < VIEWS.length; i++) {
|
||||
if (i==1||i==2||i==4) {
|
||||
glCullFace(GL_FRONT);
|
||||
} else {
|
||||
glCullFace(GL_BACK);
|
||||
}
|
||||
|
||||
glViewport((i % 3) * this.width, (i / 3) * this.height, this.width, this.height);
|
||||
|
||||
//The projection matrix
|
||||
mat.set(2, 0, 0, 0,
|
||||
0, 2, 0, 0,
|
||||
0, 0, -1f, 0,
|
||||
-1, -1, 0, 1)
|
||||
.mul(VIEWS[i]);
|
||||
|
||||
BudgetBufferRenderer.render(mat);
|
||||
}
|
||||
}
|
||||
glBindVertexArray(0);
|
||||
} else {//Is fluid, slow path :(
|
||||
|
||||
if (!(state.getBlock() instanceof FluidBlock)) throw new IllegalStateException();
|
||||
|
||||
var mat = new Matrix4f();
|
||||
for (int i = 0; i < VIEWS.length; i++) {
|
||||
if (i==1||i==2||i==4) {
|
||||
glCullFace(GL_FRONT);
|
||||
} else {
|
||||
glCullFace(GL_BACK);
|
||||
}
|
||||
|
||||
this.vc.reset();
|
||||
this.bakeFluidState(state, layer, i);
|
||||
if (this.vc.isEmpty()) continue;
|
||||
BudgetBufferRenderer.setup(this.vc.getAddress(), this.vc.quadCount(), blockTextureId);
|
||||
|
||||
glViewport((i % 3) * this.width, (i / 3) * this.height, this.width, this.height);
|
||||
|
||||
//The projection matrix
|
||||
mat.set(2, 0, 0, 0,
|
||||
0, 2, 0, 0,
|
||||
0, 0, -1f, 0,
|
||||
-1, -1, 0, 1)
|
||||
.mul(VIEWS[i]);
|
||||
|
||||
BudgetBufferRenderer.render(mat);
|
||||
}
|
||||
glBindVertexArray(0);
|
||||
}
|
||||
|
||||
//Render block model entity data if it exists
|
||||
if (bbem != null) {
|
||||
//Rerender everything again ;-; but is ok (is not)
|
||||
|
||||
var mat = new Matrix4f();
|
||||
for (int i = 0; i < VIEWS.length; i++) {
|
||||
if (i==1||i==2||i==4) {
|
||||
glCullFace(GL_FRONT);
|
||||
} else {
|
||||
glCullFace(GL_BACK);
|
||||
}
|
||||
|
||||
glViewport((i % 3) * this.width, (i / 3) * this.height, this.width, this.height);
|
||||
|
||||
//The projection matrix
|
||||
mat.set(2, 0, 0, 0,
|
||||
0, 2, 0, 0,
|
||||
0, 0, -1f, 0,
|
||||
-1, -1, 0, 1)
|
||||
.mul(VIEWS[i]);
|
||||
|
||||
bbem.render(mat, blockTextureId);
|
||||
}
|
||||
glBindVertexArray(0);
|
||||
|
||||
bbem.release();
|
||||
}
|
||||
|
||||
|
||||
|
||||
//"Restore" gl state
|
||||
glViewport(viewdat[0], viewdat[1], viewdat[2], viewdat[3]);
|
||||
glDisable(GL_STENCIL_TEST);
|
||||
glDisable(GL_BLEND);
|
||||
|
||||
//Finish and download
|
||||
glTextureBarrier();
|
||||
this.capture.emitToStream(streamBuffer, streamOffset);
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, this.capture.framebuffer.id);
|
||||
glClearDepth(1);
|
||||
glClear(GL_DEPTH_BUFFER_BIT);
|
||||
if (layer == BlockRenderLayer.TRANSLUCENT) {
|
||||
//reset the blend func
|
||||
GL14.glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
static {
|
||||
//the face/direction is the face (e.g. down is the down face)
|
||||
addView(0, -90,0, 0, 0);//Direction.DOWN
|
||||
addView(1, 90,0, 0, 0b100);//Direction.UP
|
||||
|
||||
addView(2, 0,180, 0, 0b001);//Direction.NORTH
|
||||
addView(3, 0,0, 0, 0);//Direction.SOUTH
|
||||
|
||||
addView(4, 0,90, 270, 0b100);//Direction.WEST
|
||||
addView(5, 0,270, 270, 0);//Direction.EAST
|
||||
}
|
||||
|
||||
private static void addView(int i, float pitch, float yaw, float rotation, int flip) {
|
||||
var stack = new MatrixStack();
|
||||
stack.translate(0.5f,0.5f,0.5f);
|
||||
stack.multiply(makeQuatFromAxisExact(new Vector3f(0,0,1), rotation));
|
||||
stack.multiply(makeQuatFromAxisExact(new Vector3f(1,0,0), pitch));
|
||||
stack.multiply(makeQuatFromAxisExact(new Vector3f(0,1,0), yaw));
|
||||
stack.multiplyPositionMatrix(new Matrix4f().scale(1-2*(flip&1), 1-(flip&2), 1-((flip>>1)&2)));
|
||||
stack.translate(-0.5f,-0.5f,-0.5f);
|
||||
VIEWS[i] = new Matrix4f(stack.peek().getPositionMatrix());
|
||||
}
|
||||
|
||||
private static Quaternionf makeQuatFromAxisExact(Vector3f vec, float angle) {
|
||||
angle = (float) Math.toRadians(angle);
|
||||
float hangle = angle / 2.0f;
|
||||
float sinAngle = (float) Math.sin(hangle);
|
||||
float invVLength = (float) (1/Math.sqrt(vec.lengthSquared()));
|
||||
return new Quaternionf(vec.x * invVLength * sinAngle,
|
||||
vec.y * invVLength * sinAngle,
|
||||
vec.z * invVLength * sinAngle,
|
||||
Math.cos(hangle));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package me.cortex.voxy.client.core.model.bakery;
|
||||
|
||||
|
||||
import me.cortex.voxy.common.util.MemoryBuffer;
|
||||
import net.minecraft.client.render.VertexConsumer;
|
||||
import net.minecraft.client.render.model.BakedQuad;
|
||||
import org.lwjgl.system.MemoryUtil;
|
||||
|
||||
import static me.cortex.voxy.client.core.model.bakery.BudgetBufferRenderer.VERTEX_FORMAT_SIZE;
|
||||
|
||||
public final class ReuseVertexConsumer implements VertexConsumer {
|
||||
private MemoryBuffer buffer = new MemoryBuffer(8192);
|
||||
private long ptr;
|
||||
private int count;
|
||||
private int defaultMeta;
|
||||
|
||||
public ReuseVertexConsumer() {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
public ReuseVertexConsumer setDefaultMeta(int meta) {
|
||||
this.defaultMeta = meta;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReuseVertexConsumer vertex(float x, float y, float z) {
|
||||
this.ensureCanPut();
|
||||
this.ptr += VERTEX_FORMAT_SIZE; this.count++; //Goto next vertex
|
||||
this.meta(this.defaultMeta);
|
||||
MemoryUtil.memPutFloat(this.ptr, x);
|
||||
MemoryUtil.memPutFloat(this.ptr + 4, y);
|
||||
MemoryUtil.memPutFloat(this.ptr + 8, z);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ReuseVertexConsumer meta(int metadata) {
|
||||
MemoryUtil.memPutInt(this.ptr + 12, metadata);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReuseVertexConsumer color(int red, int green, int blue, int alpha) {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReuseVertexConsumer texture(float u, float v) {
|
||||
MemoryUtil.memPutFloat(this.ptr + 16, u);
|
||||
MemoryUtil.memPutFloat(this.ptr + 20, v);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReuseVertexConsumer overlay(int u, int v) {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReuseVertexConsumer light(int u, int v) {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReuseVertexConsumer normal(float x, float y, float z) {
|
||||
return this;
|
||||
}
|
||||
|
||||
public ReuseVertexConsumer quad(BakedQuad quad, int metadata) {
|
||||
this.ensureCanPut();
|
||||
int[] data = quad.vertexData();
|
||||
for (int i = 0; i < 4; i++) {
|
||||
float x = Float.intBitsToFloat(data[i * 8]);
|
||||
float y = Float.intBitsToFloat(data[i * 8 + 1]);
|
||||
float z = Float.intBitsToFloat(data[i * 8 + 2]);
|
||||
this.vertex(x,y,z);
|
||||
float u = Float.intBitsToFloat(data[i * 8 + 4]);
|
||||
float v = Float.intBitsToFloat(data[i * 8 + 5]);
|
||||
this.texture(u,v);
|
||||
|
||||
this.meta(metadata);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
private void ensureCanPut() {
|
||||
if ((long) (this.count + 5) * VERTEX_FORMAT_SIZE < this.buffer.size) {
|
||||
return;
|
||||
}
|
||||
long offset = this.ptr-this.buffer.address;
|
||||
//1.5x the size
|
||||
var newBuffer = new MemoryBuffer((((int)(this.buffer.size*2)+VERTEX_FORMAT_SIZE-1)/VERTEX_FORMAT_SIZE)*VERTEX_FORMAT_SIZE);
|
||||
this.buffer.cpyTo(newBuffer.address);
|
||||
this.buffer.free();
|
||||
this.buffer = newBuffer;
|
||||
this.ptr = offset + newBuffer.address;
|
||||
}
|
||||
|
||||
public ReuseVertexConsumer reset() {
|
||||
this.defaultMeta = 0;//RESET THE DEFAULT META
|
||||
this.count = 0;
|
||||
this.ptr = this.buffer.address - VERTEX_FORMAT_SIZE;//the thing is first time this gets incremented by FORMAT_STRIDE
|
||||
return this;
|
||||
}
|
||||
|
||||
public void free() {
|
||||
this.ptr = 0;
|
||||
this.count = 0;
|
||||
this.buffer.free();
|
||||
this.buffer = null;
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return this.count == 0;
|
||||
}
|
||||
|
||||
public int quadCount() {
|
||||
if (this.count%4 != 0) throw new IllegalStateException();
|
||||
return this.count/4;
|
||||
}
|
||||
|
||||
public long getAddress() {
|
||||
return this.buffer.address;
|
||||
}
|
||||
}
|
||||
@@ -1,181 +0,0 @@
|
||||
package me.cortex.voxy.client.core.rendering;
|
||||
|
||||
//NOTE: an idea on how to do it is so that any render section, we _keep_ aquired (yes this will be very memory intensive)
|
||||
// could maybe tosomething else
|
||||
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import it.unimi.dsi.fastutil.ints.IntArrayList;
|
||||
import me.cortex.voxy.client.core.gl.GlBuffer;
|
||||
import me.cortex.voxy.client.core.model.ModelManager;
|
||||
import me.cortex.voxy.client.core.rendering.building.BuiltSection;
|
||||
import me.cortex.voxy.client.core.rendering.util.DownloadStream;
|
||||
import me.cortex.voxy.client.core.rendering.util.UploadStream;
|
||||
import me.cortex.voxy.common.world.other.Mapper;
|
||||
import net.minecraft.block.Blocks;
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
import net.minecraft.client.render.Camera;
|
||||
import net.minecraft.client.render.Frustum;
|
||||
import net.minecraft.client.util.math.MatrixStack;
|
||||
import net.minecraft.registry.RegistryKeys;
|
||||
import net.minecraft.util.Identifier;
|
||||
import net.minecraft.world.biome.Biome;
|
||||
import net.minecraft.world.biome.BiomeKeys;
|
||||
import org.joml.FrustumIntersection;
|
||||
import org.joml.Matrix4f;
|
||||
import org.lwjgl.system.MemoryUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentLinkedDeque;
|
||||
|
||||
import static org.lwjgl.opengl.ARBMultiDrawIndirect.glMultiDrawElementsIndirect;
|
||||
import static org.lwjgl.opengl.GL30.*;
|
||||
|
||||
//can make it so that register the key of the sections we have rendered, then when a section changes and is registered,
|
||||
// dispatch an update to the render section data builder which then gets consumed by the render system and updates
|
||||
// the rendered data
|
||||
|
||||
//Contains all the logic to render the world and manage gpu memory
|
||||
// processes section load,unload,update render data and renders the world each frame
|
||||
|
||||
|
||||
//Todo: tinker with having the compute shader where each thread is a position to render? maybe idk
|
||||
public abstract class AbstractFarWorldRenderer <T extends Viewport, J extends AbstractGeometryManager> {
|
||||
public static final int STATIC_VAO = glGenVertexArrays();
|
||||
|
||||
protected final GlBuffer uniformBuffer;
|
||||
protected final J geometry;
|
||||
protected final ModelManager models;
|
||||
protected final GlBuffer lightDataBuffer;
|
||||
|
||||
protected final int maxSections;
|
||||
|
||||
//Current camera base level section position
|
||||
protected int sx;
|
||||
protected int sy;
|
||||
protected int sz;
|
||||
|
||||
protected FrustumIntersection frustum;
|
||||
|
||||
private final List<T> viewports = new ArrayList<>();
|
||||
|
||||
protected IntArrayList updatedSectionIds;
|
||||
|
||||
private final ConcurrentLinkedDeque<Mapper.StateEntry> blockStateUpdates = new ConcurrentLinkedDeque<>();
|
||||
private final ConcurrentLinkedDeque<Mapper.BiomeEntry> biomeUpdates = new ConcurrentLinkedDeque<>();
|
||||
public AbstractFarWorldRenderer(J geometry) {
|
||||
this.maxSections = geometry.getMaxSections();
|
||||
this.uniformBuffer = new GlBuffer(1024);
|
||||
this.lightDataBuffer = new GlBuffer(256*4);//256 of uint
|
||||
this.geometry = geometry;
|
||||
this.models = new ModelManager(16);
|
||||
}
|
||||
|
||||
public void setupRender(Frustum frustum, Camera camera) {
|
||||
this.frustum = frustum.frustumIntersection;
|
||||
|
||||
this.sx = camera.getBlockPos().getX() >> 5;
|
||||
this.sy = camera.getBlockPos().getY() >> 5;
|
||||
this.sz = camera.getBlockPos().getZ() >> 5;
|
||||
|
||||
//TODO: move this to a render function that is only called
|
||||
// once per frame when using multi viewport mods
|
||||
//it shouldent matter if its called multiple times a frame however, as its synced with fences
|
||||
UploadStream.INSTANCE.tick();
|
||||
DownloadStream.INSTANCE.tick();
|
||||
|
||||
//Update the lightmap
|
||||
{
|
||||
long upload = UploadStream.INSTANCE.upload(this.lightDataBuffer, 0, 256*4);
|
||||
var lmt = MinecraftClient.getInstance().gameRenderer.getLightmapTextureManager().texture.getImage();
|
||||
for (int light = 0; light < 256; light++) {
|
||||
int x = light&0xF;
|
||||
int y = ((light>>4)&0xF);
|
||||
int sample = lmt.getColor(x,y);
|
||||
sample = ((sample&0xFF0000)>>16)|(sample&0xFF00)|((sample&0xFF)<<16);
|
||||
MemoryUtil.memPutInt(upload + (((x<<4)|(15-y))*4), sample|(0xFF<<28));//Skylight is inverted
|
||||
}
|
||||
}
|
||||
|
||||
//Upload any new geometry
|
||||
this.updatedSectionIds = this.geometry.uploadResults();
|
||||
{
|
||||
boolean didHaveBiomeChange = false;
|
||||
|
||||
//Do any BiomeChanges
|
||||
while (!this.biomeUpdates.isEmpty()) {
|
||||
var update = this.biomeUpdates.pop();
|
||||
var biomeReg = MinecraftClient.getInstance().world.getRegistryManager().get(RegistryKeys.BIOME);
|
||||
this.models.addBiome(update.id, biomeReg.get(Identifier.of(update.biome)));
|
||||
didHaveBiomeChange = true;
|
||||
}
|
||||
|
||||
if (didHaveBiomeChange) {
|
||||
UploadStream.INSTANCE.commit();
|
||||
}
|
||||
|
||||
int maxUpdatesPerFrame = 40;
|
||||
|
||||
//Do any BlockChanges
|
||||
while ((!this.blockStateUpdates.isEmpty()) && (maxUpdatesPerFrame-- > 0)) {
|
||||
var update = this.blockStateUpdates.pop();
|
||||
this.models.addEntry(update.id, update.state);
|
||||
}
|
||||
//this.models.bakery.renderFaces(Blocks.ROSE_BUSH.getDefaultState(), 1234, false);
|
||||
}
|
||||
|
||||
//TODO: fix this in a better way than this ungodly hacky stuff, causes clouds to dissapear
|
||||
//RenderSystem.setShaderFogColor(1f, 1f, 1f, 0f);
|
||||
RenderSystem.setShaderFogEnd(99999999);
|
||||
RenderSystem.setShaderFogStart(9999999);
|
||||
}
|
||||
|
||||
public abstract void renderFarAwayOpaque(T viewport);
|
||||
|
||||
public abstract void renderFarAwayTranslucent(T viewport);
|
||||
|
||||
public void enqueueResult(BuiltSection result) {
|
||||
this.geometry.enqueueResult(result);
|
||||
}
|
||||
|
||||
public void addBlockState(Mapper.StateEntry entry) {
|
||||
this.blockStateUpdates.add(entry);
|
||||
}
|
||||
|
||||
public void addBiome(Mapper.BiomeEntry entry) {
|
||||
this.biomeUpdates.add(entry);
|
||||
}
|
||||
|
||||
public void addDebugData(List<String> debug) {
|
||||
this.models.addDebugInfo(debug);
|
||||
debug.add("Geometry buffer usage: " + ((float)Math.round((this.geometry.getGeometryBufferUsage()*100000))/1000) + "%");
|
||||
debug.add("Render Sections: " + this.geometry.getSectionCount());
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
this.models.free();
|
||||
this.geometry.free();
|
||||
this.uniformBuffer.free();
|
||||
this.lightDataBuffer.free();
|
||||
}
|
||||
|
||||
public ModelManager getModelManager() {
|
||||
return this.models;
|
||||
}
|
||||
|
||||
public final T createViewport() {
|
||||
var viewport = createViewport0();
|
||||
this.viewports.add(viewport);
|
||||
return viewport;
|
||||
}
|
||||
|
||||
final void removeViewport(T viewport) {
|
||||
this.viewports.remove(viewport);
|
||||
}
|
||||
|
||||
protected abstract T createViewport0();
|
||||
|
||||
public boolean usesMeshlets() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package me.cortex.voxy.client.core.rendering;
|
||||
|
||||
import it.unimi.dsi.fastutil.ints.IntArrayList;
|
||||
import me.cortex.voxy.client.core.rendering.building.BuiltSection;
|
||||
|
||||
import java.util.concurrent.ConcurrentLinkedDeque;
|
||||
|
||||
public abstract class AbstractGeometryManager {
|
||||
protected int sectionCount = 0;
|
||||
protected final int maxSections;
|
||||
protected final ConcurrentLinkedDeque<BuiltSection> buildResults = new ConcurrentLinkedDeque<>();
|
||||
|
||||
protected AbstractGeometryManager(int maxSections) {
|
||||
this.maxSections = maxSections;
|
||||
}
|
||||
|
||||
abstract IntArrayList uploadResults();
|
||||
|
||||
int getMaxSections() {
|
||||
return this.maxSections;
|
||||
}
|
||||
|
||||
public void enqueueResult(BuiltSection sectionGeometry) {
|
||||
this.buildResults.add(sectionGeometry);
|
||||
}
|
||||
|
||||
public abstract float getGeometryBufferUsage();
|
||||
|
||||
public int getSectionCount() {
|
||||
return this.sectionCount;
|
||||
}
|
||||
|
||||
public abstract void free();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
package me.cortex.voxy.client.core.rendering;
|
||||
|
||||
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
|
||||
import me.cortex.voxy.client.core.AbstractRenderPipeline;
|
||||
import me.cortex.voxy.client.core.gl.GlBuffer;
|
||||
import me.cortex.voxy.client.core.gl.GlVertexArray;
|
||||
import me.cortex.voxy.client.core.gl.shader.AutoBindingShader;
|
||||
import me.cortex.voxy.client.core.gl.shader.Shader;
|
||||
import me.cortex.voxy.client.core.gl.shader.ShaderLoader;
|
||||
import me.cortex.voxy.client.core.gl.shader.ShaderType;
|
||||
import me.cortex.voxy.client.core.rendering.util.SharedIndexBuffer;
|
||||
import me.cortex.voxy.client.core.rendering.util.UploadStream;
|
||||
import me.cortex.voxy.common.Logger;
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
import net.minecraft.util.math.MathHelper;
|
||||
import org.joml.Matrix4f;
|
||||
import org.joml.Vector3f;
|
||||
import org.joml.Vector3i;
|
||||
import org.lwjgl.system.MemoryUtil;
|
||||
|
||||
import static org.lwjgl.opengl.ARBDirectStateAccess.glCopyNamedBufferSubData;
|
||||
import static org.lwjgl.opengl.GL11.GL_TRIANGLES;
|
||||
import static org.lwjgl.opengl.GL11.GL_UNSIGNED_BYTE;
|
||||
import static org.lwjgl.opengl.GL15.GL_ELEMENT_ARRAY_BUFFER;
|
||||
import static org.lwjgl.opengl.GL15.glBindBuffer;
|
||||
import static org.lwjgl.opengl.GL30.glBindVertexArray;
|
||||
import static org.lwjgl.opengl.GL30C.*;
|
||||
import static org.lwjgl.opengl.GL31.glDrawElementsInstanced;
|
||||
import static org.lwjgl.opengl.GL42.glDrawElementsInstancedBaseInstance;
|
||||
|
||||
//This is a render subsystem, its very simple in what it does
|
||||
// it renders an AABB around loaded chunks, thats it
|
||||
public class ChunkBoundRenderer {
|
||||
private static final int INIT_MAX_CHUNK_COUNT = 1<<12;
|
||||
private GlBuffer chunkPosBuffer = new GlBuffer(INIT_MAX_CHUNK_COUNT*8);//Stored as ivec2
|
||||
private final GlBuffer uniformBuffer = new GlBuffer(128);
|
||||
private final Long2IntOpenHashMap chunk2idx = new Long2IntOpenHashMap(INIT_MAX_CHUNK_COUNT);
|
||||
private long[] idx2chunk = new long[INIT_MAX_CHUNK_COUNT];
|
||||
private final Shader rasterShader;
|
||||
|
||||
private final LongOpenHashSet addQueue = new LongOpenHashSet();
|
||||
private final LongOpenHashSet remQueue = new LongOpenHashSet();
|
||||
|
||||
private final AbstractRenderPipeline pipeline;
|
||||
public ChunkBoundRenderer(AbstractRenderPipeline pipeline) {
|
||||
this.chunk2idx.defaultReturnValue(-1);
|
||||
this.pipeline = pipeline;
|
||||
|
||||
String vert = ShaderLoader.parse("voxy:chunkoutline/outline.vsh");
|
||||
String taa = pipeline.taaFunction("getTAA");
|
||||
if (taa != null) {
|
||||
vert = vert+"\n\n\n"+taa;
|
||||
}
|
||||
this.rasterShader = Shader.makeAuto()
|
||||
.addSource(ShaderType.VERTEX, vert)
|
||||
.defineIf("TAA", taa != null)
|
||||
.add(ShaderType.FRAGMENT, "voxy:chunkoutline/outline.fsh")
|
||||
.compile()
|
||||
.ubo(0, this.uniformBuffer)
|
||||
.ssbo(1, this.chunkPosBuffer);
|
||||
}
|
||||
|
||||
public void addSection(long pos) {
|
||||
if (!this.remQueue.remove(pos)) {
|
||||
this.addQueue.add(pos);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeSection(long pos) {
|
||||
if (!this.addQueue.remove(pos)) {
|
||||
this.remQueue.add(pos);
|
||||
}
|
||||
}
|
||||
|
||||
//Bind and render, changing as little gl state as possible so that the caller may configure how it wants to render
|
||||
public void render(Viewport<?> viewport) {
|
||||
if (!this.remQueue.isEmpty()) {
|
||||
boolean wasEmpty = this.chunk2idx.isEmpty();
|
||||
this.remQueue.forEach(this::_remPos);//TODO: REPLACE WITH SCATTER COMPUTE
|
||||
this.remQueue.clear();
|
||||
if (this.chunk2idx.isEmpty()&&!wasEmpty) {//When going from stuff to nothing need to clear the depth buffer
|
||||
viewport.depthBoundingBuffer.clear(0);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.chunk2idx.isEmpty() && this.addQueue.isEmpty()) return;
|
||||
|
||||
viewport.depthBoundingBuffer.clear(0);
|
||||
|
||||
long ptr = UploadStream.INSTANCE.upload(this.uniformBuffer, 0, 128);
|
||||
long matPtr = ptr; ptr += 4*4*4;
|
||||
|
||||
final float renderDistance = MinecraftClient.getInstance().options.getClampedViewDistance()*16;//In blocks
|
||||
|
||||
{//This is recomputed to be in chunk section space not worldsection
|
||||
int sx = MathHelper.floor(viewport.cameraX) >> 4;
|
||||
int sy = MathHelper.floor(viewport.cameraY) >> 4;
|
||||
int sz = MathHelper.floor(viewport.cameraZ) >> 4;
|
||||
new Vector3i(sx, sy, sz).getToAddress(ptr); ptr += 4*4;
|
||||
|
||||
var negInnerSec = new Vector3f(
|
||||
-(float) (viewport.cameraX - (sx << 4)),
|
||||
-(float) (viewport.cameraY - (sy << 4)),
|
||||
-(float) (viewport.cameraZ - (sz << 4)));
|
||||
|
||||
viewport.MVP.translate(negInnerSec, new Matrix4f()).getToAddress(matPtr);
|
||||
|
||||
negInnerSec.getToAddress(ptr); ptr += 4*3;
|
||||
MemoryUtil.memPutFloat(ptr, renderDistance); ptr += 4;
|
||||
}
|
||||
UploadStream.INSTANCE.commit();
|
||||
|
||||
|
||||
{
|
||||
//need to reverse the winding order since we want the back faces of the AABB, not the front
|
||||
|
||||
glFrontFace(GL_CW);//Reverse winding order
|
||||
|
||||
//"reverse depth buffer" it goes from 0->1 where 1 is far away
|
||||
glEnable(GL_CULL_FACE);
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
glDepthFunc(GL_GREATER);
|
||||
}
|
||||
|
||||
glBindVertexArray(GlVertexArray.STATIC_VAO);
|
||||
viewport.depthBoundingBuffer.bind();
|
||||
this.rasterShader.bind();
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, SharedIndexBuffer.INSTANCE_BB_BYTE.id());
|
||||
this.pipeline.bindUniforms();
|
||||
|
||||
//Batch the draws into groups of size 32
|
||||
int count = this.chunk2idx.size();
|
||||
if (count > 32) {
|
||||
glDrawElementsInstanced(GL_TRIANGLES, 6 * 2 * 3 * 32, GL_UNSIGNED_BYTE, 0, count/32);
|
||||
}
|
||||
if (count%32 != 0) {
|
||||
glDrawElementsInstancedBaseInstance(GL_TRIANGLES, 6 * 2 * 3 * (count%32), GL_UNSIGNED_BYTE, 0, 1, (count/32)*32);
|
||||
}
|
||||
|
||||
{
|
||||
glFrontFace(GL_CCW);//Restore winding order
|
||||
|
||||
glDepthFunc(GL_LEQUAL);
|
||||
|
||||
//TODO: check this is correct
|
||||
glEnable(GL_CULL_FACE);
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
}
|
||||
|
||||
|
||||
if (!this.addQueue.isEmpty()) {
|
||||
this.addQueue.forEach(this::_addPos);//TODO: REPLACE WITH SCATTER COMPUTE
|
||||
this.addQueue.clear();
|
||||
UploadStream.INSTANCE.commit();
|
||||
}
|
||||
}
|
||||
|
||||
private void _remPos(long pos) {
|
||||
int idx = this.chunk2idx.remove(pos);
|
||||
if (idx == -1) {
|
||||
Logger.warn("Chunk not in map: " + pos);
|
||||
return;
|
||||
}
|
||||
if (idx == this.chunk2idx.size()) {
|
||||
//Dont need to do anything as heap is already compact
|
||||
return;
|
||||
}
|
||||
if (this.idx2chunk[idx] != pos) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
//Move last entry on heap to this index
|
||||
long ePos = this.idx2chunk[this.chunk2idx.size()];// since is already removed size is correct end idx
|
||||
if (this.chunk2idx.put(ePos, idx) == -1) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
this.idx2chunk[idx] = ePos;
|
||||
|
||||
//Put the end pos into the new idx
|
||||
this.put(idx, ePos);
|
||||
}
|
||||
|
||||
private void _addPos(long pos) {
|
||||
if (this.chunk2idx.containsKey(pos)) {
|
||||
Logger.warn("Chunk already in map: " + pos);
|
||||
return;
|
||||
}
|
||||
this.ensureSize1();//Resize if needed
|
||||
|
||||
int idx = this.chunk2idx.size();
|
||||
this.chunk2idx.put(pos, idx);
|
||||
this.idx2chunk[idx] = pos;
|
||||
|
||||
this.put(idx, pos);
|
||||
}
|
||||
|
||||
private void ensureSize1() {
|
||||
if (this.chunk2idx.size() < this.idx2chunk.length) return;
|
||||
//Commit any copies, ensures is synced to new buffer
|
||||
UploadStream.INSTANCE.commit();
|
||||
|
||||
int size = (int) (this.idx2chunk.length*1.5);
|
||||
Logger.info("Resizing chunk position buffer to: " + size);
|
||||
//Need to resize
|
||||
var old = this.chunkPosBuffer;
|
||||
this.chunkPosBuffer = new GlBuffer(size * 8L);
|
||||
glCopyNamedBufferSubData(old.id, this.chunkPosBuffer.id, 0, 0, old.size());
|
||||
old.free();
|
||||
var old2 = this.idx2chunk;
|
||||
this.idx2chunk = new long[size];
|
||||
System.arraycopy(old2, 0, this.idx2chunk, 0, old2.length);
|
||||
//Replace the old buffer with the new one
|
||||
((AutoBindingShader)this.rasterShader).ssbo(1, this.chunkPosBuffer);
|
||||
}
|
||||
|
||||
private void put(int idx, long pos) {
|
||||
long ptr2 = UploadStream.INSTANCE.upload(this.chunkPosBuffer, 8L*idx, 8);
|
||||
//Need to do it in 2 parts because ivec2 is 2 parts
|
||||
MemoryUtil.memPutInt(ptr2, (int)(pos&0xFFFFFFFFL)); ptr2 += 4;
|
||||
MemoryUtil.memPutInt(ptr2, (int)((pos>>>32)&0xFFFFFFFFL));
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
this.chunk2idx.clear();
|
||||
}
|
||||
|
||||
public void free() {
|
||||
this.rasterShader.free();
|
||||
this.uniformBuffer.free();
|
||||
this.chunkPosBuffer.free();
|
||||
}
|
||||
}
|
||||
@@ -1,187 +0,0 @@
|
||||
package me.cortex.voxy.client.core.rendering;
|
||||
|
||||
import it.unimi.dsi.fastutil.ints.IntArrayList;
|
||||
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.longs.LongArrayList;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
|
||||
import me.cortex.voxy.client.core.gl.GlBuffer;
|
||||
import me.cortex.voxy.client.core.rendering.building.BuiltSection;
|
||||
import me.cortex.voxy.client.core.rendering.util.BufferArena;
|
||||
import me.cortex.voxy.client.core.rendering.util.UploadStream;
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
import net.minecraft.text.Text;
|
||||
import org.lwjgl.system.MemoryUtil;
|
||||
|
||||
import java.util.concurrent.ConcurrentLinkedDeque;
|
||||
|
||||
public class DefaultGeometryManager extends AbstractGeometryManager {
|
||||
private static final int SECTION_METADATA_SIZE = 32;
|
||||
private final Long2IntOpenHashMap pos2id = new Long2IntOpenHashMap();
|
||||
private final LongArrayList id2pos = new LongArrayList();
|
||||
private final ObjectArrayList<SectionMeta> sectionMetadata = new ObjectArrayList<>();
|
||||
private final IntArrayList markSectionIds = new IntArrayList();//Section ids to mark as visible (either due to being new, or swapping)
|
||||
|
||||
private final GlBuffer sectionMetaBuffer;
|
||||
private final BufferArena geometryBuffer;
|
||||
|
||||
private final int geometryElementSize;
|
||||
|
||||
public DefaultGeometryManager(long geometryBufferSize, int maxSections) {
|
||||
this(geometryBufferSize, maxSections, 8);//8 is default quad size
|
||||
}
|
||||
|
||||
public DefaultGeometryManager(long geometryBufferSize, int maxSections, int elementSize) {
|
||||
super(maxSections);
|
||||
this.sectionMetaBuffer = new GlBuffer(((long) maxSections) * SECTION_METADATA_SIZE);
|
||||
this.geometryBuffer = new BufferArena(geometryBufferSize, elementSize);
|
||||
this.pos2id.defaultReturnValue(-1);
|
||||
this.geometryElementSize = elementSize;
|
||||
}
|
||||
|
||||
IntArrayList uploadResults() {
|
||||
this.markSectionIds.clear();
|
||||
while (!this.buildResults.isEmpty()) {
|
||||
var result = this.buildResults.pop();
|
||||
boolean isDelete = result.isEmpty();
|
||||
if (isDelete) {
|
||||
int id = -1;
|
||||
if ((id = this.pos2id.remove(result.position)) != -1) {
|
||||
if (this.id2pos.getLong(id) != result.position) {
|
||||
throw new IllegalStateException("Removed position id not the same requested");
|
||||
}
|
||||
|
||||
var meta = this.sectionMetadata.get(id);
|
||||
this.freeMeta(meta);
|
||||
|
||||
|
||||
this.sectionCount--;
|
||||
if (id == this.sectionCount) {
|
||||
//if we are at the end of the array dont have to do anything (maybe just upload a blank data, just to be sure)
|
||||
|
||||
//Remove the last element
|
||||
this.sectionMetadata.remove(id);
|
||||
this.id2pos.removeLong(id);
|
||||
} else {
|
||||
long swapLodPos = this.id2pos.getLong(this.sectionCount);
|
||||
this.pos2id.put(swapLodPos, id);
|
||||
this.id2pos.set(id, swapLodPos);
|
||||
//Remove from the lists
|
||||
this.id2pos.removeLong(this.sectionCount);
|
||||
var swapMeta = this.sectionMetadata.remove(this.sectionCount);
|
||||
this.sectionMetadata.set(id, swapMeta);
|
||||
if (swapMeta.position != swapLodPos) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
long ptr = UploadStream.INSTANCE.upload(this.sectionMetaBuffer, (long) SECTION_METADATA_SIZE * id, SECTION_METADATA_SIZE);
|
||||
swapMeta.writeMetadata(ptr);
|
||||
this.markSectionIds.add(id);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
int id = -1;
|
||||
if ((id = this.pos2id.get(result.position)) != -1) {
|
||||
//Update the existing data
|
||||
var meta = this.sectionMetadata.get(id);
|
||||
if (meta.position != result.position) {
|
||||
throw new IllegalStateException("Meta position != result position");
|
||||
}
|
||||
//Delete the old data
|
||||
this.freeMeta(meta);
|
||||
|
||||
//Create the new meta
|
||||
meta = this.createMeta(result);
|
||||
if (meta == null) {
|
||||
continue;
|
||||
}
|
||||
this.sectionMetadata.set(id, meta);
|
||||
long ptr = UploadStream.INSTANCE.upload(this.sectionMetaBuffer, (long)SECTION_METADATA_SIZE * id, SECTION_METADATA_SIZE);
|
||||
meta.writeMetadata(ptr);
|
||||
} else {
|
||||
//Create the new meta
|
||||
var meta = this.createMeta(result);
|
||||
if (meta == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
//Add to the end of the array
|
||||
id = this.sectionCount++;
|
||||
this.pos2id.put(result.position, id);
|
||||
this.id2pos.add(result.position);
|
||||
|
||||
this.sectionMetadata.add(meta);
|
||||
long ptr = UploadStream.INSTANCE.upload(this.sectionMetaBuffer, (long)SECTION_METADATA_SIZE * id, SECTION_METADATA_SIZE);
|
||||
meta.writeMetadata(ptr);
|
||||
this.markSectionIds.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
//Assert some invarients
|
||||
if (this.id2pos.size() != this.sectionCount || this.sectionCount != this.pos2id.size()) {
|
||||
throw new IllegalStateException("Invariants broken");
|
||||
}
|
||||
|
||||
result.free();
|
||||
}
|
||||
return this.markSectionIds;
|
||||
}
|
||||
|
||||
public void free() {
|
||||
while (!this.buildResults.isEmpty()) {
|
||||
this.buildResults.pop().free();
|
||||
}
|
||||
this.sectionMetaBuffer.free();
|
||||
this.geometryBuffer.free();
|
||||
}
|
||||
|
||||
public int geometryId() {
|
||||
return this.geometryBuffer.id();
|
||||
}
|
||||
|
||||
public int metaId() {
|
||||
return this.sectionMetaBuffer.id;
|
||||
}
|
||||
|
||||
public float getGeometryBufferUsage() {
|
||||
return this.geometryBuffer.usage();
|
||||
}
|
||||
|
||||
|
||||
//=========================================================================================================================================================================================
|
||||
//=========================================================================================================================================================================================
|
||||
//=========================================================================================================================================================================================
|
||||
|
||||
|
||||
//TODO: pack the offsets of each axis so that implicit face culling can work
|
||||
//Note! the opaquePreDataCount and translucentPreDataCount are never writen to the meta buffer, as they are indexed in reverse relative to the base opaque and translucent geometry
|
||||
protected record SectionMeta(long position, int aabb, int geometryPtr, int size, int[] offsets) {
|
||||
public void writeMetadata(long ptr) {
|
||||
//THIS IS DUE TO ENDIANNESS and that we are splitting a long into 2 ints
|
||||
MemoryUtil.memPutInt(ptr, (int) (this.position>>32)); ptr += 4;
|
||||
MemoryUtil.memPutInt(ptr, (int) this.position); ptr += 4;
|
||||
MemoryUtil.memPutInt(ptr, (int) this.aabb); ptr += 4;
|
||||
MemoryUtil.memPutInt(ptr, this.geometryPtr + this.offsets[0]); ptr += 4;
|
||||
|
||||
MemoryUtil.memPutInt(ptr, (this.offsets[1]-this.offsets[0])|((this.offsets[2]-this.offsets[1])<<16)); ptr += 4;
|
||||
MemoryUtil.memPutInt(ptr, (this.offsets[3]-this.offsets[2])|((this.offsets[4]-this.offsets[3])<<16)); ptr += 4;
|
||||
MemoryUtil.memPutInt(ptr, (this.offsets[5]-this.offsets[4])|((this.offsets[6]-this.offsets[5])<<16)); ptr += 4;
|
||||
MemoryUtil.memPutInt(ptr, (this.offsets[7]-this.offsets[6])|((this.size -this.offsets[7])<<16)); ptr += 4;
|
||||
}
|
||||
}
|
||||
|
||||
protected SectionMeta createMeta(BuiltSection geometry) {
|
||||
int geometryPtr = (int) this.geometryBuffer.upload(geometry.geometryBuffer);
|
||||
if (geometryPtr == -1) {
|
||||
String msg = "Buffer arena out of memory, please increase it in settings or decrease LoD quality";
|
||||
MinecraftClient.getInstance().inGameHud.getChatHud().addMessage(Text.literal(msg));
|
||||
System.err.println(msg);
|
||||
return null;
|
||||
}
|
||||
return new SectionMeta(geometry.position, geometry.aabb, geometryPtr, (int) (geometry.geometryBuffer.size/this.geometryElementSize), geometry.offsets);
|
||||
}
|
||||
|
||||
protected void freeMeta(SectionMeta meta) {
|
||||
if (meta.geometryPtr != -1) {
|
||||
this.geometryBuffer.free(meta.geometryPtr);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package me.cortex.voxy.client.core.rendering;
|
||||
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
|
||||
import me.cortex.voxy.client.core.rendering.building.BuiltSection;
|
||||
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
//CPU side cache for section geometry, not thread safe
|
||||
public class GeometryCache {
|
||||
private final ReentrantLock lock = new ReentrantLock();
|
||||
private long maxCombinedSize;
|
||||
private long currentSize;
|
||||
private final Long2ObjectLinkedOpenHashMap<BuiltSection> cache = new Long2ObjectLinkedOpenHashMap<>();
|
||||
public GeometryCache(long maxSize) {
|
||||
this.setMaxTotalSize(maxSize);
|
||||
}
|
||||
|
||||
public void setMaxTotalSize(long size) {
|
||||
this.maxCombinedSize = size;
|
||||
}
|
||||
|
||||
//Puts the section into the cache
|
||||
public void put(BuiltSection section) {
|
||||
this.lock.lock();
|
||||
var prev = this.cache.put(section.position, section);
|
||||
this.currentSize += section.geometryBuffer.size;
|
||||
if (prev != null) {
|
||||
this.currentSize -= prev.geometryBuffer.size;
|
||||
}
|
||||
while (this.maxCombinedSize <= this.currentSize) {
|
||||
var entry = this.cache.removeFirst();
|
||||
this.currentSize -= entry.geometryBuffer.size;
|
||||
entry.free();
|
||||
}
|
||||
this.lock.unlock();
|
||||
if (prev != null) {
|
||||
prev.free();
|
||||
}
|
||||
}
|
||||
|
||||
public BuiltSection remove(long position) {
|
||||
this.lock.lock();
|
||||
var section = this.cache.remove(position);
|
||||
if (section != null) {
|
||||
this.currentSize -= section.geometryBuffer.size;
|
||||
}
|
||||
this.lock.unlock();
|
||||
return section;
|
||||
}
|
||||
|
||||
public void clear(long position) {
|
||||
var sec = this.remove(position);
|
||||
if (sec != null) {
|
||||
sec.free();
|
||||
}
|
||||
}
|
||||
|
||||
public void free() {
|
||||
this.lock.lock();
|
||||
this.cache.values().forEach(BuiltSection::free);
|
||||
this.cache.clear();
|
||||
this.lock.unlock();
|
||||
}
|
||||
}
|
||||
@@ -1,232 +0,0 @@
|
||||
package me.cortex.voxy.client.core.rendering;
|
||||
|
||||
import me.cortex.voxy.client.core.gl.GlBuffer;
|
||||
import me.cortex.voxy.client.core.gl.shader.Shader;
|
||||
import me.cortex.voxy.client.core.gl.shader.ShaderType;
|
||||
import me.cortex.voxy.client.core.rendering.util.DownloadStream;
|
||||
import me.cortex.voxy.client.core.rendering.util.UploadStream;
|
||||
import me.cortex.voxy.client.mixin.joml.AccessFrustumIntersection;
|
||||
import net.minecraft.block.Blocks;
|
||||
import net.minecraft.client.render.RenderLayer;
|
||||
import net.minecraft.client.util.math.MatrixStack;
|
||||
import org.joml.Matrix4f;
|
||||
import org.joml.Vector3f;
|
||||
import org.lwjgl.opengl.GL11C;
|
||||
import org.lwjgl.system.MemoryUtil;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.lwjgl.opengl.ARBIndirectParameters.GL_PARAMETER_BUFFER_ARB;
|
||||
import static org.lwjgl.opengl.ARBIndirectParameters.glMultiDrawElementsIndirectCountARB;
|
||||
import static org.lwjgl.opengl.GL11.GL_TRIANGLES;
|
||||
import static org.lwjgl.opengl.GL11.GL_UNSIGNED_SHORT;
|
||||
import static org.lwjgl.opengl.GL11.glGetInteger;
|
||||
import static org.lwjgl.opengl.GL14C.glBlendFuncSeparate;
|
||||
import static org.lwjgl.opengl.GL30.glBindVertexArray;
|
||||
import static org.lwjgl.opengl.GL30C.GL_R8UI;
|
||||
import static org.lwjgl.opengl.GL30C.GL_RED_INTEGER;
|
||||
import static org.lwjgl.opengl.GL40C.GL_DRAW_INDIRECT_BUFFER;
|
||||
import static org.lwjgl.opengl.GL42.*;
|
||||
import static org.lwjgl.opengl.GL42.GL_FRAMEBUFFER_BARRIER_BIT;
|
||||
import static org.lwjgl.opengl.GL43.*;
|
||||
import static org.lwjgl.opengl.GL43.GL_SHADER_STORAGE_BUFFER;
|
||||
import static org.lwjgl.opengl.GL45.glBindTextureUnit;
|
||||
import static org.lwjgl.opengl.GL45.glClearNamedBufferData;
|
||||
import static org.lwjgl.opengl.GL45C.nglClearNamedBufferData;
|
||||
|
||||
public class Gl46FarWorldRenderer extends AbstractFarWorldRenderer<Gl46Viewport, DefaultGeometryManager> {
|
||||
private final Shader commandGen = Shader.make()
|
||||
.add(ShaderType.COMPUTE, "voxy:lod/gl46/cmdgen.comp")
|
||||
.compile();
|
||||
|
||||
private final Shader lodShader = Shader.make()
|
||||
.add(ShaderType.VERTEX, "voxy:lod/gl46/quads2.vert")
|
||||
.add(ShaderType.FRAGMENT, "voxy:lod/gl46/quads.frag")
|
||||
.compile();
|
||||
|
||||
|
||||
//TODO: Note the cull shader needs a different element array since its rastering cubes not quads
|
||||
private final Shader cullShader = Shader.make()
|
||||
.add(ShaderType.VERTEX, "voxy:lod/gl46/cull/raster.vert")
|
||||
.add(ShaderType.FRAGMENT, "voxy:lod/gl46/cull/raster.frag")
|
||||
.compile();
|
||||
|
||||
private final GlBuffer glCommandBuffer;
|
||||
private final GlBuffer glCommandCountBuffer;
|
||||
|
||||
public Gl46FarWorldRenderer(int geometryBuffer, int maxSections) {
|
||||
super(new DefaultGeometryManager(geometryBuffer*8L, maxSections));
|
||||
this.glCommandBuffer = new GlBuffer(maxSections*5L*4 * 6);
|
||||
this.glCommandCountBuffer = new GlBuffer(4*2);
|
||||
nglClearNamedBufferData(this.glCommandBuffer.id, GL_R32UI, GL_RED_INTEGER, GL_UNSIGNED_INT, 0);
|
||||
}
|
||||
|
||||
protected void bindResources(Gl46Viewport viewport) {
|
||||
glBindBufferBase(GL_UNIFORM_BUFFER, 0, this.uniformBuffer.id);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, this.geometry.geometryId());
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, this.glCommandBuffer.id);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 3, this.glCommandCountBuffer.id);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 4, this.geometry.metaId());
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 5, viewport.visibilityBuffer.id);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 6, this.models.getBufferId());
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 7, this.models.getColourBufferId());
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 8, this.lightDataBuffer.id);//Lighting LUT
|
||||
glBindBuffer(GL_DRAW_INDIRECT_BUFFER, this.glCommandBuffer.id);
|
||||
glBindBuffer(GL_PARAMETER_BUFFER_ARB, this.glCommandCountBuffer.id);
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, SharedIndexBuffer.INSTANCE.id());
|
||||
|
||||
//Bind the texture atlas
|
||||
glBindSampler(0, this.models.getSamplerId());
|
||||
glBindTextureUnit(0, this.models.getTextureId());
|
||||
}
|
||||
|
||||
//FIXME: dont do something like this as it breaks multiviewport mods
|
||||
// the issue is that voxy expects the counter to be incremented by one each frame (the compute shader generating the commands)
|
||||
// checks `frameId - 1`, this means in a multiviewport, effectivly only the stuff that was marked visible by the last viewport
|
||||
// would be visible in the current viewport (if there are 2 viewports)
|
||||
|
||||
//To fix the issue, need to make a viewport api, that independently tracks the frameId and has its own glVisibilityBuffer buffer
|
||||
private void updateUniformBuffer(Gl46Viewport viewport) {
|
||||
long ptr = UploadStream.INSTANCE.upload(this.uniformBuffer, 0, this.uniformBuffer.size());
|
||||
|
||||
var mat = new Matrix4f(viewport.projection).mul(viewport.modelView);
|
||||
var innerTranslation = new Vector3f((float) (viewport.cameraX-(this.sx<<5)), (float) (viewport.cameraY-(this.sy<<5)), (float) (viewport.cameraZ-(this.sz<<5)));
|
||||
mat.translate(-innerTranslation.x, -innerTranslation.y, -innerTranslation.z);
|
||||
mat.getToAddress(ptr); ptr += 4*4*4;
|
||||
MemoryUtil.memPutInt(ptr, this.sx); ptr += 4;
|
||||
MemoryUtil.memPutInt(ptr, this.sy); ptr += 4;
|
||||
MemoryUtil.memPutInt(ptr, this.sz); ptr += 4;
|
||||
MemoryUtil.memPutInt(ptr, this.geometry.getSectionCount()); ptr += 4;
|
||||
var planes = ((AccessFrustumIntersection)this.frustum).getPlanes();
|
||||
for (var plane : planes) {
|
||||
plane.getToAddress(ptr); ptr += 4*4;
|
||||
}
|
||||
innerTranslation.getToAddress(ptr); ptr += 4*3;
|
||||
MemoryUtil.memPutInt(ptr, viewport.frameId++); ptr += 4;
|
||||
}
|
||||
|
||||
public void renderFarAwayOpaque(Gl46Viewport viewport) {
|
||||
if (this.geometry.getSectionCount() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
{//Mark all of the updated sections as being visible from last frame
|
||||
for (int id : this.updatedSectionIds) {
|
||||
long ptr = UploadStream.INSTANCE.upload(viewport.visibilityBuffer, id * 4L, 4);
|
||||
MemoryUtil.memPutInt(ptr, viewport.frameId - 1);//(visible from last frame)
|
||||
}
|
||||
}
|
||||
|
||||
glDisable(GL_BLEND);
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
|
||||
|
||||
//this.models.bakery.renderFaces(Blocks.WATER.getDefaultState().with(FluidBlock.LEVEL, 1), 1234, true);
|
||||
//this.models.bakery.renderFaces(Blocks.CHEST.getDefaultState(), 1234, false);
|
||||
|
||||
|
||||
RenderLayer.getCutoutMipped().startDrawing();
|
||||
//RenderSystem.enableBlend();
|
||||
//RenderSystem.defaultBlendFunc();
|
||||
|
||||
this.updateUniformBuffer(viewport);
|
||||
UploadStream.INSTANCE.commit();
|
||||
glBindVertexArray(AbstractFarWorldRenderer.STATIC_VAO);
|
||||
|
||||
nglClearNamedBufferData(this.glCommandCountBuffer.id, GL_R32UI, GL_RED_INTEGER, GL_UNSIGNED_INT, 0);
|
||||
this.commandGen.bind();
|
||||
this.bindResources(viewport);
|
||||
glDispatchCompute((this.geometry.getSectionCount()+127)/128, 1, 1);
|
||||
glMemoryBarrier(GL_COMMAND_BARRIER_BIT | GL_SHADER_STORAGE_BARRIER_BIT | GL_UNIFORM_BARRIER_BIT);
|
||||
|
||||
this.lodShader.bind();
|
||||
this.bindResources(viewport);
|
||||
glDisable(GL_CULL_FACE);
|
||||
//glPointSize(10);
|
||||
//TODO: replace glMultiDrawElementsIndirectCountARB with glMultiDrawElementsIndirect on intel gpus, since it performs so much better
|
||||
//glMultiDrawElementsIndirect(GL_TRIANGLES, GL_UNSIGNED_SHORT, 0, drawCnt, 0);
|
||||
//glLineWidth(2);
|
||||
glMultiDrawElementsIndirectCountARB(GL_TRIANGLES, GL_UNSIGNED_SHORT, 0, 0, (int) (this.geometry.getSectionCount()*4.4), 0);
|
||||
glEnable(GL_CULL_FACE);
|
||||
|
||||
|
||||
/*
|
||||
DownloadStream.INSTANCE.download(this.glCommandCountBuffer, 0, 4, (ptr, siz) -> {
|
||||
int cnt = MemoryUtil.memGetInt(ptr);
|
||||
drawCnt = cnt;
|
||||
});
|
||||
DownloadStream.INSTANCE.commit();
|
||||
|
||||
*/
|
||||
|
||||
|
||||
|
||||
glMemoryBarrier(GL_PIXEL_BUFFER_BARRIER_BIT | GL_FRAMEBUFFER_BARRIER_BIT);
|
||||
|
||||
this.cullShader.bind();
|
||||
this.bindResources(viewport);
|
||||
|
||||
glColorMask(false, false, false, false);
|
||||
glDepthMask(false);
|
||||
|
||||
glDrawElementsInstanced(GL_TRIANGLES, 6 * 2 * 3, GL_UNSIGNED_BYTE, (1 << 16) * 6 * 2, this.geometry.getSectionCount());
|
||||
|
||||
glDepthMask(true);
|
||||
glColorMask(true, true, true, true);
|
||||
glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT);
|
||||
|
||||
|
||||
//TODO: need to do temporal rasterization here
|
||||
|
||||
glBindVertexArray(0);
|
||||
glBindSampler(0, 0);
|
||||
glBindTextureUnit(0, 0);
|
||||
RenderLayer.getCutoutMipped().endDrawing();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void renderFarAwayTranslucent(Gl46Viewport viewport) {
|
||||
RenderLayer.getTranslucent().startDrawing();
|
||||
glBindVertexArray(AbstractFarWorldRenderer.STATIC_VAO);
|
||||
glDisable(GL_CULL_FACE);
|
||||
glEnable(GL_BLEND);
|
||||
|
||||
//TODO: maybe change this so the alpha isnt applied in the same way or something?? since atm the texture bakery uses a very hacky
|
||||
// blend equation to make it avoid double applying translucency
|
||||
glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
|
||||
glBindSampler(0, this.models.getSamplerId());
|
||||
glBindTextureUnit(0, this.models.getTextureId());
|
||||
|
||||
//RenderSystem.blendFunc(GlStateManager.SrcFactor.ONE, GlStateManager.DstFactor.ONE);
|
||||
this.lodShader.bind();
|
||||
this.bindResources(viewport);
|
||||
|
||||
glMultiDrawElementsIndirectCountARB(GL_TRIANGLES, GL_UNSIGNED_SHORT, 400_000 * 4 * 5, 4, this.geometry.getSectionCount(), 0);
|
||||
|
||||
glEnable(GL_CULL_FACE);
|
||||
glBindVertexArray(0);
|
||||
|
||||
|
||||
glBindSampler(0, 0);
|
||||
glBindTextureUnit(0, 0);
|
||||
glDisable(GL_BLEND);
|
||||
|
||||
RenderLayer.getTranslucent().endDrawing();
|
||||
}
|
||||
|
||||
protected Gl46Viewport createViewport0() {
|
||||
return new Gl46Viewport(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
super.shutdown();
|
||||
this.commandGen.free();
|
||||
this.lodShader.free();
|
||||
this.cullShader.free();
|
||||
this.glCommandBuffer.free();
|
||||
this.glCommandCountBuffer.free();
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package me.cortex.voxy.client.core.rendering;
|
||||
|
||||
import me.cortex.voxy.client.core.gl.GlBuffer;
|
||||
|
||||
import static org.lwjgl.opengl.GL30C.GL_R8UI;
|
||||
import static org.lwjgl.opengl.GL30C.GL_RED_INTEGER;
|
||||
import static org.lwjgl.opengl.GL42.GL_UNSIGNED_BYTE;
|
||||
import static org.lwjgl.opengl.GL45C.nglClearNamedBufferData;
|
||||
|
||||
public class Gl46MeshletViewport extends Viewport<Gl46MeshletViewport> {
|
||||
GlBuffer visibilityBuffer;
|
||||
public Gl46MeshletViewport(Gl46MeshletsFarWorldRenderer renderer) {
|
||||
super(renderer);
|
||||
this.visibilityBuffer = new GlBuffer(renderer.maxSections*4L);
|
||||
nglClearNamedBufferData(this.visibilityBuffer.id, GL_R8UI, GL_RED_INTEGER, GL_UNSIGNED_BYTE, 0);
|
||||
}
|
||||
|
||||
protected void delete0() {
|
||||
this.visibilityBuffer.free();
|
||||
}
|
||||
}
|
||||
@@ -1,235 +0,0 @@
|
||||
package me.cortex.voxy.client.core.rendering;
|
||||
|
||||
import me.cortex.voxy.client.core.gl.GlBuffer;
|
||||
import me.cortex.voxy.client.core.gl.shader.Shader;
|
||||
import me.cortex.voxy.client.core.gl.shader.ShaderType;
|
||||
import me.cortex.voxy.client.core.rendering.building.RenderDataFactory;
|
||||
import me.cortex.voxy.client.core.rendering.util.UploadStream;
|
||||
import me.cortex.voxy.client.mixin.joml.AccessFrustumIntersection;
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
import net.minecraft.client.render.RenderLayer;
|
||||
import org.joml.Matrix4f;
|
||||
import org.joml.Vector3f;
|
||||
import org.lwjgl.system.MemoryUtil;
|
||||
|
||||
import static org.lwjgl.opengl.ARBDirectStateAccess.glGetNamedFramebufferAttachmentParameteriv;
|
||||
import static org.lwjgl.opengl.ARBDirectStateAccess.glTextureParameteri;
|
||||
import static org.lwjgl.opengl.ARBIndirectParameters.GL_PARAMETER_BUFFER_ARB;
|
||||
import static org.lwjgl.opengl.GL11.*;
|
||||
import static org.lwjgl.opengl.GL14C.glBlendFuncSeparate;
|
||||
import static org.lwjgl.opengl.GL15.GL_ELEMENT_ARRAY_BUFFER;
|
||||
import static org.lwjgl.opengl.GL15.glBindBuffer;
|
||||
import static org.lwjgl.opengl.GL30.glBindBufferBase;
|
||||
import static org.lwjgl.opengl.GL30.glBindVertexArray;
|
||||
import static org.lwjgl.opengl.GL30C.GL_RED_INTEGER;
|
||||
import static org.lwjgl.opengl.GL31.GL_UNIFORM_BUFFER;
|
||||
import static org.lwjgl.opengl.GL31.glDrawElementsInstanced;
|
||||
import static org.lwjgl.opengl.GL33.glBindSampler;
|
||||
import static org.lwjgl.opengl.GL40.glDrawElementsIndirect;
|
||||
import static org.lwjgl.opengl.GL40C.GL_DRAW_INDIRECT_BUFFER;
|
||||
import static org.lwjgl.opengl.GL42.GL_FRAMEBUFFER_BARRIER_BIT;
|
||||
import static org.lwjgl.opengl.GL42.glMemoryBarrier;
|
||||
import static org.lwjgl.opengl.GL43.*;
|
||||
import static org.lwjgl.opengl.GL45.glBindTextureUnit;
|
||||
import static org.lwjgl.opengl.GL45.nglClearNamedBufferSubData;
|
||||
import static org.lwjgl.opengl.GL45C.nglClearNamedBufferData;
|
||||
import static org.lwjgl.opengl.NVMeshShader.glDrawMeshTasksNV;
|
||||
import static org.lwjgl.opengl.NVRepresentativeFragmentTest.GL_REPRESENTATIVE_FRAGMENT_TEST_NV;
|
||||
|
||||
//TODO: NOTE
|
||||
// can do "meshlet compaction"
|
||||
// that is, meshlets are refered not by the meshlet id but by an 8 byte alligned index, (the quad index)
|
||||
// this means that non full meshlets can have the tailing data be truncated and used in the next meshlet
|
||||
// as long as the number of quads in the meshlet is stored in the header
|
||||
// the shader can cull the verticies of any quad that has its index over the expected quuad count
|
||||
// this could potentially result in a fair bit of memory savings (especially if used in normal mc terrain rendering)
|
||||
public class Gl46MeshletsFarWorldRenderer extends AbstractFarWorldRenderer<Gl46MeshletViewport, DefaultGeometryManager> {
|
||||
private final Shader lodShader = Shader.make()
|
||||
.define("QUADS_PER_MESHLET", RenderDataFactory.QUADS_PER_MESHLET)
|
||||
.add(ShaderType.VERTEX, "voxy:lod/gl46mesh/quads.vert")
|
||||
.add(ShaderType.FRAGMENT, "voxy:lod/gl46mesh/quads.frag")
|
||||
.compile();
|
||||
|
||||
private final Shader cullShader = Shader.make()
|
||||
.define("QUADS_PER_MESHLET", RenderDataFactory.QUADS_PER_MESHLET)
|
||||
.add(ShaderType.VERTEX, "voxy:lod/gl46mesh/cull.vert")
|
||||
.add(ShaderType.FRAGMENT, "voxy:lod/gl46mesh/cull.frag")
|
||||
.compile();
|
||||
|
||||
private final Shader meshletGenerator = Shader.make()
|
||||
.define("QUADS_PER_MESHLET", RenderDataFactory.QUADS_PER_MESHLET)
|
||||
.add(ShaderType.COMPUTE, "voxy:lod/gl46mesh/cmdgen.comp")
|
||||
.compile();
|
||||
|
||||
private final Shader meshletCuller = Shader.make()
|
||||
.define("QUADS_PER_MESHLET", RenderDataFactory.QUADS_PER_MESHLET)
|
||||
.add(ShaderType.COMPUTE, "voxy:lod/gl46mesh/meshletculler.comp")
|
||||
.compile();
|
||||
|
||||
private final GlBuffer glDrawIndirect;
|
||||
private final GlBuffer meshletBuffer;
|
||||
private final HiZBuffer hiZBuffer = new HiZBuffer();
|
||||
private final int hizSampler = glGenSamplers();
|
||||
|
||||
public Gl46MeshletsFarWorldRenderer(int geometrySize, int maxSections) {
|
||||
super(new DefaultGeometryManager(alignUp(geometrySize*8L, 8* (RenderDataFactory.QUADS_PER_MESHLET+2)), maxSections, 8*(RenderDataFactory.QUADS_PER_MESHLET+2)));
|
||||
this.glDrawIndirect = new GlBuffer(4*(4+5));
|
||||
this.meshletBuffer = new GlBuffer(4*1000000);//TODO: Make max meshlet count configurable, not just 1 million (even tho thats a max of 126 million quads per frame)
|
||||
|
||||
glSamplerParameteri(this.hizSampler, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST);//This is so that using the shadow sampler works correctly
|
||||
glTextureParameteri(this.hizSampler, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTextureParameteri(this.hizSampler, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE);
|
||||
glTextureParameteri(this.hizSampler, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL);
|
||||
glTextureParameteri(this.hizSampler, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTextureParameteri(this.hizSampler, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
|
||||
nglClearNamedBufferData(this.meshletBuffer.id, GL_R32UI, GL_RED_INTEGER, GL_UNSIGNED_INT, 0);
|
||||
nglClearNamedBufferData(this.glDrawIndirect.id, GL_R32UI, GL_RED_INTEGER, GL_UNSIGNED_INT, 0);
|
||||
}
|
||||
|
||||
protected void bindResources(Gl46MeshletViewport viewport, boolean bindToDrawIndirect, boolean bindToDispatchIndirect, boolean bindHiz) {
|
||||
glBindBufferBase(GL_UNIFORM_BUFFER, 0, this.uniformBuffer.id);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, this.geometry.geometryId());
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, bindToDrawIndirect?0:this.glDrawIndirect.id);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 3, this.meshletBuffer.id);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 4, this.geometry.metaId());
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 5, viewport.visibilityBuffer.id);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 6, this.models.getBufferId());
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 7, this.models.getColourBufferId());
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 8, this.lightDataBuffer.id);//Lighting LUT
|
||||
glBindBuffer(GL_DRAW_INDIRECT_BUFFER, bindToDrawIndirect?this.glDrawIndirect.id:0);
|
||||
glBindBuffer(GL_DISPATCH_INDIRECT_BUFFER, bindToDispatchIndirect?this.glDrawIndirect.id:0);
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, SharedIndexBuffer.INSTANCE_BYTE.id());
|
||||
|
||||
if (!bindHiz) {
|
||||
//Bind the texture atlas
|
||||
glBindSampler(0, this.models.getSamplerId());
|
||||
glBindTextureUnit(0, this.models.getTextureId());
|
||||
} else {
|
||||
//Bind the hiz buffer
|
||||
glBindSampler(0, this.hizSampler);
|
||||
glBindTextureUnit(0, this.hiZBuffer.getHizTextureId());
|
||||
}
|
||||
}
|
||||
|
||||
private void updateUniformBuffer(Gl46MeshletViewport viewport) {
|
||||
long ptr = UploadStream.INSTANCE.upload(this.uniformBuffer, 0, this.uniformBuffer.size());
|
||||
|
||||
//TODO:FIXME remove this.sx/sy/sz and make it based on the viewport!!!
|
||||
|
||||
var mat = new Matrix4f(viewport.projection).mul(viewport.modelView);
|
||||
var innerTranslation = new Vector3f((float) (viewport.cameraX-(this.sx<<5)), (float) (viewport.cameraY-(this.sy<<5)), (float) (viewport.cameraZ-(this.sz<<5)));
|
||||
mat.translate(-innerTranslation.x, -innerTranslation.y, -innerTranslation.z);
|
||||
mat.getToAddress(ptr); ptr += 4*4*4;
|
||||
MemoryUtil.memPutInt(ptr, this.sx); ptr += 4;
|
||||
MemoryUtil.memPutInt(ptr, this.sy); ptr += 4;
|
||||
MemoryUtil.memPutInt(ptr, this.sz); ptr += 4;
|
||||
MemoryUtil.memPutInt(ptr, this.geometry.getSectionCount()); ptr += 4;
|
||||
var planes = ((AccessFrustumIntersection)this.frustum).getPlanes();
|
||||
for (var plane : planes) {
|
||||
plane.getToAddress(ptr); ptr += 4*4;
|
||||
}
|
||||
innerTranslation.getToAddress(ptr); ptr += 4*3;
|
||||
MemoryUtil.memPutInt(ptr, viewport.frameId++); ptr += 4;
|
||||
MemoryUtil.memPutInt(ptr, viewport.width); ptr += 4;
|
||||
MemoryUtil.memPutInt(ptr, viewport.height); ptr += 4;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void renderFarAwayOpaque(Gl46MeshletViewport viewport) {
|
||||
if (this.geometry.getSectionCount() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
{//Mark all of the updated sections as being visible from last frame
|
||||
for (int id : this.updatedSectionIds) {
|
||||
long ptr = UploadStream.INSTANCE.upload(viewport.visibilityBuffer, id * 4L, 4);
|
||||
MemoryUtil.memPutInt(ptr, viewport.frameId - 1);//(visible from last frame)
|
||||
}
|
||||
}
|
||||
|
||||
glDisable(GL_BLEND);
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
RenderLayer.getCutoutMipped().startDrawing();
|
||||
|
||||
this.updateUniformBuffer(viewport);
|
||||
UploadStream.INSTANCE.commit();
|
||||
glBindVertexArray(AbstractFarWorldRenderer.STATIC_VAO);
|
||||
|
||||
nglClearNamedBufferSubData(this.glDrawIndirect.id, GL_R32UI, 0, 4*4, GL_RED_INTEGER, GL_UNSIGNED_INT, 0);
|
||||
|
||||
this.lodShader.bind();
|
||||
this.bindResources(viewport, true, false, false);
|
||||
glDisable(GL_CULL_FACE);
|
||||
glDrawElementsIndirect(GL_TRIANGLES, GL_UNSIGNED_BYTE, 4*4);
|
||||
glEnable(GL_CULL_FACE);
|
||||
|
||||
glMemoryBarrier(GL_PIXEL_BUFFER_BARRIER_BIT | GL_FRAMEBUFFER_BARRIER_BIT);
|
||||
|
||||
this.cullShader.bind();
|
||||
this.bindResources(viewport, false, false, false);
|
||||
glDepthMask(false);
|
||||
glColorMask(false, false, false, false);
|
||||
glDrawElementsInstanced(GL_TRIANGLES, 6 * 2 * 3, GL_UNSIGNED_BYTE, (1 << 8) * 6, this.geometry.getSectionCount());
|
||||
glColorMask(true, true, true, true);
|
||||
glDepthMask(true);
|
||||
|
||||
glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT);
|
||||
|
||||
this.meshletGenerator.bind();
|
||||
this.bindResources(viewport, false, false, false);
|
||||
glDispatchCompute((this.geometry.getSectionCount()+63)/64, 1, 1);
|
||||
|
||||
glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT | GL_COMMAND_BARRIER_BIT);
|
||||
|
||||
var i = new int[1];
|
||||
glGetFramebufferAttachmentParameteriv(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME, i);
|
||||
this.hiZBuffer.buildMipChain(i[0], MinecraftClient.getInstance().getFramebuffer().textureWidth, MinecraftClient.getInstance().getFramebuffer().textureHeight);
|
||||
|
||||
this.meshletCuller.bind();
|
||||
this.bindResources(viewport, false, true, true);
|
||||
glDispatchComputeIndirect(0);
|
||||
|
||||
|
||||
|
||||
glBindVertexArray(0);
|
||||
glBindSampler(0, 0);
|
||||
glBindTextureUnit(0, 0);
|
||||
RenderLayer.getCutoutMipped().endDrawing();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void renderFarAwayTranslucent(Gl46MeshletViewport viewport) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Gl46MeshletViewport createViewport0() {
|
||||
return new Gl46MeshletViewport(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
super.shutdown();
|
||||
this.lodShader.free();
|
||||
this.cullShader.free();
|
||||
this.meshletGenerator.free();
|
||||
this.meshletCuller.free();
|
||||
|
||||
this.glDrawIndirect.free();
|
||||
this.meshletBuffer.free();
|
||||
|
||||
this.hiZBuffer.free();
|
||||
glDeleteSamplers(this.hizSampler);
|
||||
}
|
||||
|
||||
public static long alignUp(long n, long alignment) {
|
||||
return (n + alignment - 1) & -alignment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean usesMeshlets() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package me.cortex.voxy.client.core.rendering;
|
||||
|
||||
import me.cortex.voxy.client.core.gl.GlBuffer;
|
||||
|
||||
import static org.lwjgl.opengl.ARBIndirectParameters.glMultiDrawElementsIndirectCountARB;
|
||||
import static org.lwjgl.opengl.GL30C.GL_R8UI;
|
||||
import static org.lwjgl.opengl.GL30C.GL_RED_INTEGER;
|
||||
import static org.lwjgl.opengl.GL42.GL_UNSIGNED_BYTE;
|
||||
import static org.lwjgl.opengl.GL45C.glClearNamedBufferData;
|
||||
import static org.lwjgl.opengl.GL45C.nglClearNamedBufferData;
|
||||
|
||||
public class Gl46Viewport extends Viewport<Gl46Viewport> {
|
||||
GlBuffer visibilityBuffer;
|
||||
public Gl46Viewport(Gl46FarWorldRenderer renderer) {
|
||||
super(renderer);
|
||||
this.visibilityBuffer = new GlBuffer(renderer.maxSections*4L);
|
||||
nglClearNamedBufferData(this.visibilityBuffer.id, GL_R8UI, GL_RED_INTEGER, GL_UNSIGNED_BYTE, 0);
|
||||
}
|
||||
|
||||
protected void delete0() {
|
||||
this.visibilityBuffer.free();
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package me.cortex.voxy.client.core.rendering;
|
||||
|
||||
import me.cortex.voxy.client.core.gl.GlBuffer;
|
||||
import me.cortex.voxy.client.core.gl.shader.Shader;
|
||||
import me.cortex.voxy.client.core.gl.shader.ShaderType;
|
||||
import me.cortex.voxy.client.core.rendering.hierarchical.NodeManager;
|
||||
import me.cortex.voxy.common.util.HierarchicalBitSet;
|
||||
|
||||
import static org.lwjgl.opengl.GL42C.*;
|
||||
import static org.lwjgl.opengl.GL43C.GL_SHADER_STORAGE_BARRIER_BIT;
|
||||
import static org.lwjgl.opengl.GL43C.glDispatchCompute;
|
||||
import static org.lwjgl.opengl.GL44.GL_CLIENT_MAPPED_BUFFER_BARRIER_BIT;
|
||||
|
||||
public class HierarchicalOcclusionRenderer {
|
||||
private final int workgroup_dispatch_size_x;//The number of workgroups required to saturate the gpu efficiently
|
||||
private final NodeManager nodeManager = new NodeManager(null);
|
||||
private final HiZBuffer hiz = new HiZBuffer();
|
||||
|
||||
|
||||
private Shader hiercarchialShader = Shader.make()
|
||||
.add(ShaderType.COMPUTE, "voxy:lod/hierarchical/selector.comp")
|
||||
.compile();
|
||||
|
||||
public HierarchicalOcclusionRenderer(int workgroup_size) {
|
||||
this.workgroup_dispatch_size_x = workgroup_size;
|
||||
|
||||
}
|
||||
|
||||
private void bind() {
|
||||
|
||||
}
|
||||
|
||||
public void render(int depthBuffer, int width, int height) {
|
||||
//Make hiz
|
||||
this.hiz.buildMipChain(depthBuffer, width, height);
|
||||
//Node upload phase
|
||||
this.nodeManager.uploadPhase();
|
||||
//Node download phase (pulls from previous frame (should maybe result in lower latency)) also clears and resets the queues
|
||||
this.nodeManager.downloadPhase();
|
||||
//Bind all the resources
|
||||
this.bind();
|
||||
//run hierachial selection shader
|
||||
this.hiercarchialShader.bind();
|
||||
//barrier
|
||||
glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT|GL_UNIFORM_BARRIER_BIT|GL_CLIENT_MAPPED_BUFFER_BARRIER_BIT|GL_FRAMEBUFFER_BARRIER_BIT);
|
||||
//Emit enough work to fully populate the gpu
|
||||
glDispatchCompute(this.workgroup_dispatch_size_x, 1, 1);
|
||||
glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT|GL_COMMAND_BARRIER_BIT|GL_UNIFORM_BARRIER_BIT);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package me.cortex.voxy.client.core.rendering;
|
||||
|
||||
import me.cortex.voxy.common.world.WorldEngine;
|
||||
|
||||
public interface ISectionWatcher {
|
||||
default boolean watch(int lvl, int x, int y, int z, int types) {
|
||||
return this.watch(WorldEngine.getWorldSectionId(lvl, x, y, z), types);
|
||||
}
|
||||
|
||||
boolean watch(long position, int types);
|
||||
|
||||
default boolean unwatch(int lvl, int x, int y, int z, int types) {
|
||||
return this.unwatch(WorldEngine.getWorldSectionId(lvl, x, y, z), types);
|
||||
}
|
||||
|
||||
boolean unwatch(long position, int types);
|
||||
|
||||
default int get(int lvl, int x, int y, int z) {
|
||||
return this.get(WorldEngine.getWorldSectionId(lvl, x, y, z));
|
||||
}
|
||||
|
||||
int get(long position);
|
||||
}
|
||||
@@ -1,188 +0,0 @@
|
||||
package me.cortex.voxy.client.core.rendering;
|
||||
|
||||
import me.cortex.voxy.client.core.gl.shader.Shader;
|
||||
import me.cortex.voxy.client.core.gl.shader.ShaderType;
|
||||
import me.cortex.voxy.client.core.rendering.util.UploadStream;
|
||||
import me.cortex.voxy.client.mixin.joml.AccessFrustumIntersection;
|
||||
import net.minecraft.client.render.Camera;
|
||||
import net.minecraft.client.render.Frustum;
|
||||
import net.minecraft.client.render.RenderLayer;
|
||||
import org.joml.Matrix4f;
|
||||
import org.joml.Vector3f;
|
||||
import org.lwjgl.opengl.GL11C;
|
||||
import org.lwjgl.system.MemoryUtil;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.lwjgl.opengl.ARBIndirectParameters.GL_PARAMETER_BUFFER_ARB;
|
||||
import static org.lwjgl.opengl.ARBIndirectParameters.glMultiDrawElementsIndirectCountARB;
|
||||
import static org.lwjgl.opengl.GL11.*;
|
||||
import static org.lwjgl.opengl.GL14C.glBlendFuncSeparate;
|
||||
import static org.lwjgl.opengl.GL15.GL_ELEMENT_ARRAY_BUFFER;
|
||||
import static org.lwjgl.opengl.GL15.glBindBuffer;
|
||||
import static org.lwjgl.opengl.GL30.glBindBufferBase;
|
||||
import static org.lwjgl.opengl.GL30.glBindVertexArray;
|
||||
import static org.lwjgl.opengl.GL31.GL_UNIFORM_BUFFER;
|
||||
import static org.lwjgl.opengl.GL31.glDrawElementsInstanced;
|
||||
import static org.lwjgl.opengl.GL33.glBindSampler;
|
||||
import static org.lwjgl.opengl.GL40C.GL_DRAW_INDIRECT_BUFFER;
|
||||
import static org.lwjgl.opengl.GL42.GL_FRAMEBUFFER_BARRIER_BIT;
|
||||
import static org.lwjgl.opengl.GL42.glMemoryBarrier;
|
||||
import static org.lwjgl.opengl.GL43.GL_SHADER_STORAGE_BARRIER_BIT;
|
||||
import static org.lwjgl.opengl.GL43.GL_SHADER_STORAGE_BUFFER;
|
||||
import static org.lwjgl.opengl.GL45.glBindTextureUnit;
|
||||
import static org.lwjgl.opengl.NVMeshShader.glDrawMeshTasksNV;
|
||||
import static org.lwjgl.opengl.NVRepresentativeFragmentTest.GL_REPRESENTATIVE_FRAGMENT_TEST_NV;
|
||||
|
||||
public class NvMeshFarWorldRenderer extends AbstractFarWorldRenderer<NvMeshViewport, DefaultGeometryManager> {
|
||||
private final Shader terrain = Shader.make()
|
||||
.add(ShaderType.TASK, "voxy:lod/nvmesh/primary.task")
|
||||
.add(ShaderType.MESH, "voxy:lod/nvmesh/primary.mesh")
|
||||
.add(ShaderType.FRAGMENT, "voxy:lod/nvmesh/primary.frag")
|
||||
.compile();
|
||||
|
||||
private final Shader translucent = Shader.make()
|
||||
.add(ShaderType.TASK, "voxy:lod/nvmesh/translucent.task")
|
||||
.add(ShaderType.MESH, "voxy:lod/nvmesh/translucent.mesh")
|
||||
.add(ShaderType.FRAGMENT, "voxy:lod/nvmesh/primary.frag")
|
||||
.compile();
|
||||
|
||||
private final Shader cull = Shader.make()
|
||||
.add(ShaderType.VERTEX, "voxy:lod/nvmesh/cull.vert")
|
||||
.add(ShaderType.FRAGMENT, "voxy:lod/nvmesh/cull.frag")
|
||||
.compile();
|
||||
|
||||
public NvMeshFarWorldRenderer(int geometrySize, int maxSections) {
|
||||
super(new DefaultGeometryManager(geometrySize*8L, maxSections));
|
||||
}
|
||||
|
||||
|
||||
private void updateUniform(NvMeshViewport viewport) {
|
||||
long ptr = UploadStream.INSTANCE.upload(this.uniformBuffer, 0, this.uniformBuffer.size());
|
||||
|
||||
var mat = new Matrix4f(viewport.projection).mul(viewport.modelView);
|
||||
mat.getToAddress(ptr); ptr += 4*4*4;
|
||||
var innerTranslation = new Vector3f((float) (viewport.cameraX-(this.sx<<5)), (float) (viewport.cameraY-(this.sy<<5)), (float) (viewport.cameraZ-(this.sz<<5)));
|
||||
MemoryUtil.memPutInt(ptr, this.sx); ptr += 4;
|
||||
MemoryUtil.memPutInt(ptr, this.sy); ptr += 4;
|
||||
MemoryUtil.memPutInt(ptr, this.sz); ptr += 4;
|
||||
MemoryUtil.memPutInt(ptr, this.geometry.getSectionCount()); ptr += 4;
|
||||
innerTranslation.getToAddress(ptr); ptr += 4*3;
|
||||
MemoryUtil.memPutInt(ptr, viewport.frameId++); ptr += 4;
|
||||
}
|
||||
|
||||
private void bindResources(NvMeshViewport viewport) {
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, SharedIndexBuffer.INSTANCE.id());
|
||||
glBindBufferBase(GL_UNIFORM_BUFFER, 0, this.uniformBuffer.id);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, this.geometry.geometryId());
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, this.geometry.metaId());
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 3, viewport.visibilityBuffer.id);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 4, this.models.getBufferId());
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 5, this.models.getColourBufferId());
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 6, this.lightDataBuffer.id);//Lighting LUT
|
||||
|
||||
//Bind the texture atlas
|
||||
glBindSampler(0, this.models.getSamplerId());
|
||||
glBindTextureUnit(0, this.models.getTextureId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void renderFarAwayOpaque(NvMeshViewport viewport) {
|
||||
{//TODO: move all this code into a common super method renderFarAwayTranslucent and make the current method renderFarAwayTranslucent0
|
||||
if (this.geometry.getSectionCount() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
{//Mark all of the updated sections as being visible from last frame
|
||||
for (int id : this.updatedSectionIds) {
|
||||
long ptr = UploadStream.INSTANCE.upload(viewport.visibilityBuffer, id * 4L, 4);
|
||||
MemoryUtil.memPutInt(ptr, viewport.frameId - 1);//(visible from last frame)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
glDisable(GL_BLEND);
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
|
||||
//Update and upload data
|
||||
this.updateUniform(viewport);
|
||||
UploadStream.INSTANCE.commit();
|
||||
|
||||
|
||||
this.terrain.bind();
|
||||
|
||||
RenderLayer.getCutoutMipped().startDrawing();
|
||||
|
||||
glBindVertexArray(AbstractFarWorldRenderer.STATIC_VAO);
|
||||
this.bindResources(viewport);
|
||||
|
||||
glDisable(GL_CULL_FACE);
|
||||
glDrawMeshTasksNV(0, this.geometry.getSectionCount());
|
||||
glEnable(GL_CULL_FACE);
|
||||
|
||||
glMemoryBarrier(GL_FRAMEBUFFER_BARRIER_BIT|GL_SHADER_STORAGE_BARRIER_BIT);
|
||||
|
||||
this.cull.bind();
|
||||
this.bindResources(viewport);
|
||||
glColorMask(false, false, false, false);
|
||||
glDepthMask(false);
|
||||
glEnable(GL_REPRESENTATIVE_FRAGMENT_TEST_NV);
|
||||
glDrawElementsInstanced(GL_TRIANGLES, 6 * 2 * 3, GL_UNSIGNED_BYTE, (1 << 16) * 6 * 2, this.geometry.getSectionCount());
|
||||
glDisable(GL_REPRESENTATIVE_FRAGMENT_TEST_NV);
|
||||
glDepthMask(true);
|
||||
glColorMask(true, true, true, true);
|
||||
|
||||
glBindVertexArray(0);
|
||||
glBindSampler(0, 0);
|
||||
glBindTextureUnit(0, 0);
|
||||
RenderLayer.getCutoutMipped().endDrawing();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void renderFarAwayTranslucent(NvMeshViewport viewport) {
|
||||
if (this.geometry.getSectionCount()==0) {
|
||||
return;
|
||||
}
|
||||
RenderLayer.getTranslucent().startDrawing();
|
||||
glBindVertexArray(AbstractFarWorldRenderer.STATIC_VAO);
|
||||
glDisable(GL_CULL_FACE);
|
||||
glEnable(GL_BLEND);
|
||||
|
||||
//TODO: maybe change this so the alpha isnt applied in the same way or something?? since atm the texture bakery uses a very hacky
|
||||
// blend equation to make it avoid double applying translucency
|
||||
glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
|
||||
glBindSampler(0, this.models.getSamplerId());
|
||||
glBindTextureUnit(0, this.models.getTextureId());
|
||||
|
||||
this.translucent.bind();
|
||||
this.bindResources(viewport);
|
||||
|
||||
glDrawMeshTasksNV(0, this.geometry.getSectionCount());
|
||||
|
||||
glEnable(GL_CULL_FACE);
|
||||
glBindVertexArray(0);
|
||||
|
||||
|
||||
glBindSampler(0, 0);
|
||||
glBindTextureUnit(0, 0);
|
||||
glDisable(GL_BLEND);
|
||||
|
||||
RenderLayer.getTranslucent().endDrawing();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected NvMeshViewport createViewport0() {
|
||||
return new NvMeshViewport(this);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
super.shutdown();
|
||||
this.terrain.free();
|
||||
this.translucent.free();
|
||||
this.cull.free();
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package me.cortex.voxy.client.core.rendering;
|
||||
|
||||
import me.cortex.voxy.client.core.gl.GlBuffer;
|
||||
|
||||
import static org.lwjgl.opengl.GL30C.GL_R8UI;
|
||||
import static org.lwjgl.opengl.GL30C.GL_RED_INTEGER;
|
||||
import static org.lwjgl.opengl.GL42.GL_UNSIGNED_BYTE;
|
||||
import static org.lwjgl.opengl.GL45C.glClearNamedBufferData;
|
||||
|
||||
public class NvMeshViewport extends Viewport<NvMeshViewport> {
|
||||
GlBuffer visibilityBuffer;
|
||||
public NvMeshViewport(NvMeshFarWorldRenderer renderer) {
|
||||
super(renderer);
|
||||
this.visibilityBuffer = new GlBuffer(renderer.maxSections*4L);
|
||||
glClearNamedBufferData(this.visibilityBuffer.id, GL_R8UI, GL_RED_INTEGER, GL_UNSIGNED_BYTE, new int[1]);
|
||||
}
|
||||
|
||||
protected void delete0() {
|
||||
this.visibilityBuffer.free();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package me.cortex.voxy.client.core.rendering;
|
||||
|
||||
import me.cortex.voxy.client.core.util.RingTracker;
|
||||
import me.cortex.voxy.common.world.WorldEngine;
|
||||
|
||||
import java.util.function.LongConsumer;
|
||||
|
||||
public class RenderDistanceTracker {
|
||||
private static final int CHECK_DISTANCE_BLOCKS = 128;
|
||||
private final LongConsumer addTopLevelNode;
|
||||
private final LongConsumer removeTopLevelNode;
|
||||
private final int processRate;
|
||||
private final int minSec;
|
||||
private final int maxSec;
|
||||
private RingTracker tracker;
|
||||
private int renderDistance;
|
||||
private double posX;
|
||||
private double posZ;
|
||||
public RenderDistanceTracker(int rate, int minSec, int maxSec, LongConsumer addTopLevelNode, LongConsumer removeTopLevelNode) {
|
||||
this.addTopLevelNode = addTopLevelNode;
|
||||
this.removeTopLevelNode = removeTopLevelNode;
|
||||
this.renderDistance = 2;
|
||||
this.tracker = new RingTracker(this.renderDistance, 0, 0, true);
|
||||
this.processRate = rate;
|
||||
this.minSec = minSec;
|
||||
this.maxSec = maxSec;
|
||||
}
|
||||
|
||||
public void setRenderDistance(int renderDistance) {
|
||||
if (renderDistance == this.renderDistance) {
|
||||
return;
|
||||
}
|
||||
this.renderDistance = renderDistance;
|
||||
this.tracker.unload();//Mark all as unload
|
||||
this.tracker = new RingTracker(this.tracker, renderDistance, ((int)this.posX)>>9, ((int)this.posZ)>>9, true);//Steal from previous tracker
|
||||
}
|
||||
|
||||
public boolean setCenterAndProcess(double x, double z) {
|
||||
double dx = this.posX-x;
|
||||
double dz = this.posZ-z;
|
||||
if (CHECK_DISTANCE_BLOCKS*CHECK_DISTANCE_BLOCKS<dx*dx+dz*dz) {
|
||||
this.posX = x;
|
||||
this.posZ = z;
|
||||
this.tracker.moveCenter(((int)x)>>9, ((int)z)>>9);
|
||||
}
|
||||
return this.tracker.process(this.processRate, this::add, this::rem)!=0;
|
||||
}
|
||||
|
||||
private void add(int x, int z) {
|
||||
for (int y = this.minSec; y <= this.maxSec; y++) {
|
||||
this.addTopLevelNode.accept(WorldEngine.getWorldSectionId(4, x, y, z));
|
||||
}
|
||||
}
|
||||
|
||||
private void rem(int x, int z) {
|
||||
for (int y = this.minSec; y <= this.maxSec; y++) {
|
||||
this.removeTopLevelNode.accept(WorldEngine.getWorldSectionId(4, x, y, z));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,206 +0,0 @@
|
||||
package me.cortex.voxy.client.core.rendering;
|
||||
|
||||
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
|
||||
import it.unimi.dsi.fastutil.longs.LongSet;
|
||||
import me.cortex.voxy.client.core.rendering.building.BuiltSection;
|
||||
import me.cortex.voxy.client.core.rendering.building.RenderGenerationService;
|
||||
import me.cortex.voxy.common.world.WorldEngine;
|
||||
import me.cortex.voxy.common.world.WorldSection;
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
import net.minecraft.util.math.Direction;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
//Tracks active sections, dispatches updates to the build system, everything related to rendering flows through here
|
||||
public class RenderTracker {
|
||||
private final WorldEngine world;
|
||||
private RenderGenerationService renderGen;
|
||||
private final AbstractFarWorldRenderer renderer;
|
||||
private final LongSet[] sets;
|
||||
|
||||
|
||||
public RenderTracker(WorldEngine world, AbstractFarWorldRenderer renderer) {
|
||||
this.world = world;
|
||||
this.renderer = renderer;
|
||||
this.sets = new LongSet[1<<4];
|
||||
for (int i = 0; i < this.sets.length; i++) {
|
||||
this.sets[i] = new LongOpenHashSet();
|
||||
}
|
||||
}
|
||||
|
||||
public void setRenderGen(RenderGenerationService renderGen) {
|
||||
this.renderGen = renderGen;
|
||||
}
|
||||
|
||||
public static long mixStafford13(long seed) {
|
||||
seed = (seed ^ seed >>> 30) * -4658895280553007687L;
|
||||
seed = (seed ^ seed >>> 27) * -7723592293110705685L;
|
||||
return seed ^ seed >>> 31;
|
||||
}
|
||||
|
||||
private LongSet getSet(long key) {
|
||||
return this.sets[(int) (mixStafford13(key) & (this.sets.length-1))];
|
||||
}
|
||||
|
||||
private void put(long key) {
|
||||
var set = this.getSet(key);
|
||||
synchronized (set) {
|
||||
set.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
private void remove(long key) {
|
||||
var set = this.getSet(key);
|
||||
synchronized (set) {
|
||||
set.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean contains(long key) {
|
||||
var set = this.getSet(key);
|
||||
synchronized (set) {
|
||||
return set.contains(key);
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: replace this:: with a class cached lambda ref (cause doing this:: still does a lambda allocation)
|
||||
|
||||
//Adds a lvl 0 section into the world renderer
|
||||
public void addLvl0(int x, int y, int z) {
|
||||
this.put(WorldEngine.getWorldSectionId(0, x, y, z));
|
||||
this.renderGen.enqueueTask(0, x, y, z, this::shouldStillBuild);
|
||||
}
|
||||
|
||||
//Removes a lvl 0 section from the world renderer
|
||||
public void remLvl0(int x, int y, int z) {
|
||||
this.remove(WorldEngine.getWorldSectionId(0, x, y, z));
|
||||
this.renderer.enqueueResult(new BuiltSection(WorldEngine.getWorldSectionId(0, x, y, z)));
|
||||
this.renderGen.removeTask(0, x, y, z);
|
||||
}
|
||||
|
||||
//Increases from lvl-1 to lvl at the coordinates (which are in lvl space)
|
||||
public void inc(int lvl, int x, int y, int z) {
|
||||
this.remove(WorldEngine.getWorldSectionId(lvl-1, (x<<1), (y<<1), (z<<1)));
|
||||
this.remove(WorldEngine.getWorldSectionId(lvl-1, (x<<1), (y<<1), (z<<1)+1));
|
||||
this.remove(WorldEngine.getWorldSectionId(lvl-1, (x<<1), (y<<1)+1, (z<<1)));
|
||||
this.remove(WorldEngine.getWorldSectionId(lvl-1, (x<<1), (y<<1)+1, (z<<1)+1));
|
||||
this.remove(WorldEngine.getWorldSectionId(lvl-1, (x<<1)+1, (y<<1), (z<<1)));
|
||||
this.remove(WorldEngine.getWorldSectionId(lvl-1, (x<<1)+1, (y<<1), (z<<1)+1));
|
||||
this.remove(WorldEngine.getWorldSectionId(lvl-1, (x<<1)+1, (y<<1)+1, (z<<1)));
|
||||
this.remove(WorldEngine.getWorldSectionId(lvl-1, (x<<1)+1, (y<<1)+1, (z<<1)+1));
|
||||
this.put(WorldEngine.getWorldSectionId(lvl, x, y, z));
|
||||
|
||||
//TODO: make a seperate object to hold the build data and link it with the location in a
|
||||
// concurrent hashmap or something, this is so that e.g. the build data position
|
||||
// can be updated
|
||||
|
||||
//TODO: replace this:: with a class cached lambda ref (cause doing this:: still does a lambda allocation)
|
||||
this.renderGen.enqueueTask(lvl, x, y, z, this::shouldStillBuild);
|
||||
|
||||
this.renderer.enqueueResult(new BuiltSection(WorldEngine.getWorldSectionId(lvl-1, (x<<1), (y<<1), (z<<1))));
|
||||
this.renderer.enqueueResult(new BuiltSection(WorldEngine.getWorldSectionId(lvl-1, (x<<1), (y<<1), (z<<1)+1)));
|
||||
this.renderer.enqueueResult(new BuiltSection(WorldEngine.getWorldSectionId(lvl-1, (x<<1), (y<<1)+1, (z<<1))));
|
||||
this.renderer.enqueueResult(new BuiltSection(WorldEngine.getWorldSectionId(lvl-1, (x<<1), (y<<1)+1, (z<<1)+1)));
|
||||
this.renderer.enqueueResult(new BuiltSection(WorldEngine.getWorldSectionId(lvl-1, (x<<1)+1, (y<<1), (z<<1))));
|
||||
this.renderer.enqueueResult(new BuiltSection(WorldEngine.getWorldSectionId(lvl-1, (x<<1)+1, (y<<1), (z<<1)+1)));
|
||||
this.renderer.enqueueResult(new BuiltSection(WorldEngine.getWorldSectionId(lvl-1, (x<<1)+1, (y<<1)+1, (z<<1))));
|
||||
this.renderer.enqueueResult(new BuiltSection(WorldEngine.getWorldSectionId(lvl-1, (x<<1)+1, (y<<1)+1, (z<<1)+1)));
|
||||
|
||||
|
||||
this.renderGen.removeTask(lvl-1, (x<<1), (y<<1), (z<<1));
|
||||
this.renderGen.removeTask(lvl-1, (x<<1), (y<<1), (z<<1)+1);
|
||||
this.renderGen.removeTask(lvl-1, (x<<1), (y<<1)+1, (z<<1));
|
||||
this.renderGen.removeTask(lvl-1, (x<<1), (y<<1)+1, (z<<1)+1);
|
||||
this.renderGen.removeTask(lvl-1, (x<<1)+1, (y<<1), (z<<1));
|
||||
this.renderGen.removeTask(lvl-1, (x<<1)+1, (y<<1), (z<<1)+1);
|
||||
this.renderGen.removeTask(lvl-1, (x<<1)+1, (y<<1)+1, (z<<1));
|
||||
this.renderGen.removeTask(lvl-1, (x<<1)+1, (y<<1)+1, (z<<1)+1);
|
||||
}
|
||||
|
||||
//Decreases from lvl to lvl-1 at the coordinates (which are in lvl space)
|
||||
public void dec(int lvl, int x, int y, int z) {
|
||||
this.put(WorldEngine.getWorldSectionId(lvl-1, (x<<1), (y<<1), (z<<1)));
|
||||
this.put(WorldEngine.getWorldSectionId(lvl-1, (x<<1), (y<<1), (z<<1)+1));
|
||||
this.put(WorldEngine.getWorldSectionId(lvl-1, (x<<1), (y<<1)+1, (z<<1)));
|
||||
this.put(WorldEngine.getWorldSectionId(lvl-1, (x<<1), (y<<1)+1, (z<<1)+1));
|
||||
this.put(WorldEngine.getWorldSectionId(lvl-1, (x<<1)+1, (y<<1), (z<<1)));
|
||||
this.put(WorldEngine.getWorldSectionId(lvl-1, (x<<1)+1, (y<<1), (z<<1)+1));
|
||||
this.put(WorldEngine.getWorldSectionId(lvl-1, (x<<1)+1, (y<<1)+1, (z<<1)));
|
||||
this.put(WorldEngine.getWorldSectionId(lvl-1, (x<<1)+1, (y<<1)+1, (z<<1)+1));
|
||||
this.remove(WorldEngine.getWorldSectionId(lvl, x, y, z));
|
||||
|
||||
this.renderer.enqueueResult(new BuiltSection(WorldEngine.getWorldSectionId(lvl, x, y, z)));
|
||||
this.renderGen.removeTask(lvl, x, y, z);
|
||||
|
||||
//TODO: replace this:: with a class cached lambda ref (cause doing this:: still does a lambda allocation)
|
||||
this.renderGen.enqueueTask(lvl - 1, (x<<1), (y<<1), (z<<1), this::shouldStillBuild);
|
||||
this.renderGen.enqueueTask(lvl - 1, (x<<1), (y<<1), (z<<1)+1, this::shouldStillBuild);
|
||||
this.renderGen.enqueueTask(lvl - 1, (x<<1), (y<<1)+1, (z<<1), this::shouldStillBuild);
|
||||
this.renderGen.enqueueTask(lvl - 1, (x<<1), (y<<1)+1, (z<<1)+1, this::shouldStillBuild);
|
||||
this.renderGen.enqueueTask(lvl - 1, (x<<1)+1, (y<<1), (z<<1), this::shouldStillBuild);
|
||||
this.renderGen.enqueueTask(lvl - 1, (x<<1)+1, (y<<1), (z<<1)+1, this::shouldStillBuild);
|
||||
this.renderGen.enqueueTask(lvl - 1, (x<<1)+1, (y<<1)+1, (z<<1), this::shouldStillBuild);
|
||||
this.renderGen.enqueueTask(lvl - 1, (x<<1)+1, (y<<1)+1, (z<<1)+1, this::shouldStillBuild);
|
||||
}
|
||||
|
||||
//Enqueues a renderTask for a section to cache the result
|
||||
public void addCache(int lvl, int x, int y, int z) {
|
||||
this.renderGen.markCache(lvl, x, y, z);
|
||||
this.renderGen.enqueueTask(lvl, x, y, z, ((lvl1, x1, y1, z1) -> true));//TODO: replace the true identity lambda with a callback check to the render cache
|
||||
}
|
||||
|
||||
//Removes the position from the cache
|
||||
public void removeCache(int lvl, int x, int y, int z) {
|
||||
this.renderGen.unmarkCache(lvl, x, y, z);
|
||||
}
|
||||
|
||||
public void remove(int lvl, int x, int y, int z) {
|
||||
this.remove(WorldEngine.getWorldSectionId(lvl, x, y, z));
|
||||
this.renderer.enqueueResult(new BuiltSection(WorldEngine.getWorldSectionId(lvl, x, y, z)));
|
||||
}
|
||||
|
||||
public void add(int lvl, int x, int y, int z) {
|
||||
this.put(WorldEngine.getWorldSectionId(lvl, x, y, z));
|
||||
//TODO: replace this:: with a class cached lambda ref (cause doing this:: still does a lambda allocation)
|
||||
this.renderGen.enqueueTask(lvl, x, y, z, this::shouldStillBuild);
|
||||
}
|
||||
|
||||
|
||||
//Called by the world engine when a section gets dirtied
|
||||
public void sectionUpdated(WorldSection section) {
|
||||
if (this.contains(section.key)) {
|
||||
//TODO:FIXME: if the section gets updated, that means that its neighbors might need to be updated aswell
|
||||
// (due to block occlusion)
|
||||
|
||||
//TODO: FIXME: REBUILDING THE ENTIRE NEIGHBORS when probably only the internal layout changed is NOT SMART
|
||||
this.renderGen.clearCache(section.lvl, section.x, section.y, section.z);
|
||||
this.renderGen.clearCache(section.lvl, section.x-1, section.y, section.z);
|
||||
this.renderGen.clearCache(section.lvl, section.x+1, section.y, section.z);
|
||||
this.renderGen.clearCache(section.lvl, section.x, section.y, section.z-1);
|
||||
this.renderGen.clearCache(section.lvl, section.x, section.y, section.z+1);
|
||||
//TODO: replace this:: with a class cached lambda ref (cause doing this:: still does a lambda allocation)
|
||||
this.renderGen.enqueueTask(section.lvl, section.x, section.y, section.z, this::shouldStillBuild);
|
||||
this.renderGen.enqueueTask(section.lvl, section.x-1, section.y, section.z, this::shouldStillBuild);
|
||||
this.renderGen.enqueueTask(section.lvl, section.x+1, section.y, section.z, this::shouldStillBuild);
|
||||
this.renderGen.enqueueTask(section.lvl, section.x, section.y, section.z-1, this::shouldStillBuild);
|
||||
this.renderGen.enqueueTask(section.lvl, section.x, section.y, section.z+1, this::shouldStillBuild);
|
||||
}
|
||||
//this.renderGen.enqueueTask(section);
|
||||
}
|
||||
|
||||
//called by the RenderGenerationService about built geometry, the RenderTracker checks if it can use the result (e.g. the LoD hasnt changed/still correct etc)
|
||||
// and dispatches it to the renderer
|
||||
// it also batch collects the geometry sections until all the geometry for an operation is collected, then it executes the operation, its removes flickering
|
||||
public void processBuildResult(BuiltSection section) {
|
||||
//Check that we still want the section
|
||||
if (this.contains(section.position)) {
|
||||
this.renderer.enqueueResult(section);
|
||||
} else {
|
||||
section.free();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean shouldStillBuild(int lvl, int x, int y, int z) {
|
||||
return this.contains(WorldEngine.getWorldSectionId(lvl, x, y, z));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package me.cortex.voxy.client.core.rendering;
|
||||
|
||||
import it.unimi.dsi.fastutil.longs.Long2ByteOpenHashMap;
|
||||
import me.cortex.voxy.common.world.WorldEngine;
|
||||
import me.cortex.voxy.common.world.WorldSection;
|
||||
|
||||
import java.util.concurrent.locks.StampedLock;
|
||||
import java.util.function.LongConsumer;
|
||||
|
||||
import static me.cortex.voxy.common.world.WorldEngine.UPDATE_TYPE_BLOCK_BIT;
|
||||
|
||||
public class SectionUpdateRouter implements ISectionWatcher {
|
||||
private static final int SLICES = 1<<4;
|
||||
public interface IChildUpdate {void accept(WorldSection section);}
|
||||
|
||||
private final Long2ByteOpenHashMap[] slices = new Long2ByteOpenHashMap[SLICES];
|
||||
private final StampedLock[] locks = new StampedLock[SLICES];
|
||||
{
|
||||
for (int i = 0; i < this.slices.length; i++) {
|
||||
this.slices[i] = new Long2ByteOpenHashMap();
|
||||
this.locks[i] = new StampedLock();
|
||||
}
|
||||
}
|
||||
|
||||
private LongConsumer initialRenderMeshGen;
|
||||
private LongConsumer renderMeshGen;
|
||||
private IChildUpdate childUpdateCallback;
|
||||
|
||||
public void setCallbacks(LongConsumer initialRenderMeshGen, LongConsumer renderMeshGen, IChildUpdate childUpdateCallback) {
|
||||
if (this.renderMeshGen != null) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
this.initialRenderMeshGen = initialRenderMeshGen;
|
||||
this.renderMeshGen = renderMeshGen;
|
||||
this.childUpdateCallback = childUpdateCallback;
|
||||
}
|
||||
|
||||
public boolean watch(int lvl, int x, int y, int z, int types) {
|
||||
return this.watch(WorldEngine.getWorldSectionId(lvl, x, y, z), types);
|
||||
}
|
||||
|
||||
public boolean watch(long position, int types) {
|
||||
int idx = getSliceIndex(position);
|
||||
var set = this.slices[idx];
|
||||
var lock = this.locks[idx];
|
||||
byte delta = 0;
|
||||
{
|
||||
long stamp = lock.readLock();
|
||||
byte current = set.getOrDefault(position, (byte) 0);
|
||||
delta = (byte) (current&types);
|
||||
current |= (byte) types;
|
||||
delta ^= (byte) (current&types);
|
||||
if (delta != 0) {//Was change
|
||||
long ws = lock.tryConvertToWriteLock(stamp);
|
||||
if (ws == 0) {
|
||||
lock.unlockRead(stamp);
|
||||
stamp = lock.writeLock();
|
||||
//We need to recompute as we failed to acquire an immediate write lock
|
||||
current = set.getOrDefault(position, (byte) 0);
|
||||
delta = (byte) (current&types);
|
||||
current |= (byte) types;
|
||||
delta ^= (byte) (current&types);
|
||||
if (delta != 0)
|
||||
set.put(position, current);
|
||||
} else {
|
||||
stamp = ws;
|
||||
set.put(position, current);
|
||||
}
|
||||
}
|
||||
lock.unlock(stamp);
|
||||
}
|
||||
if (((delta&types)&UPDATE_TYPE_BLOCK_BIT)!=0) {
|
||||
//If we added it, immediately invoke for an update
|
||||
this.initialRenderMeshGen.accept(position);
|
||||
}
|
||||
return delta!=0;
|
||||
}
|
||||
|
||||
public boolean unwatch(int lvl, int x, int y, int z, int types) {
|
||||
return this.unwatch(WorldEngine.getWorldSectionId(lvl, x, y, z), types);
|
||||
}
|
||||
|
||||
public boolean unwatch(long position, int types) {//Types is types to unwatch
|
||||
int idx = getSliceIndex(position);
|
||||
var set = this.slices[idx];
|
||||
var lock = this.locks[idx];
|
||||
|
||||
long stamp = lock.readLock();
|
||||
|
||||
byte current = set.getOrDefault(position, (byte)0);
|
||||
if (current == 0) {
|
||||
throw new IllegalStateException("Section pos not in map " + WorldEngine.pprintPos(position));
|
||||
}
|
||||
boolean removed = false;
|
||||
if ((current&types) != 0) {//Was change
|
||||
long ws = lock.tryConvertToWriteLock(stamp);
|
||||
if (ws == 0) {//failed to get write lock, need to unlock, get write, then redo
|
||||
lock.unlockRead(stamp);
|
||||
stamp = lock.writeLock();
|
||||
|
||||
current = set.getOrDefault(position, (byte)0);
|
||||
if (current == 0) {
|
||||
throw new IllegalStateException("Section pos not in map " + WorldEngine.pprintPos(position));
|
||||
}
|
||||
} else {
|
||||
stamp = ws;
|
||||
}
|
||||
|
||||
if ((current&types) != 0) {
|
||||
current &= (byte) ~types;
|
||||
if (current == 0) {
|
||||
set.remove(position);
|
||||
removed = true;
|
||||
} else {
|
||||
set.put(position, current);
|
||||
}
|
||||
}
|
||||
}
|
||||
lock.unlock(stamp);
|
||||
return removed;
|
||||
}
|
||||
|
||||
public int get(long position) {
|
||||
int idx = getSliceIndex(position);
|
||||
var set = this.slices[idx];
|
||||
var lock = this.locks[idx];
|
||||
long stamp = lock.readLock();
|
||||
int ret = set.getOrDefault(position, (byte) 0);
|
||||
lock.unlockRead(stamp);
|
||||
return ret;
|
||||
}
|
||||
|
||||
public void forwardEvent(WorldSection section, int type) {
|
||||
final long position = section.key;
|
||||
|
||||
int idx = getSliceIndex(position);
|
||||
var set = this.slices[idx];
|
||||
var lock = this.locks[idx];
|
||||
|
||||
long stamp = lock.readLock();
|
||||
byte types = (byte) (set.getOrDefault(position, (byte) 0)&type);
|
||||
lock.unlockRead(stamp);
|
||||
|
||||
if (types!=0) {
|
||||
if ((types&WorldEngine.UPDATE_TYPE_CHILD_EXISTENCE_BIT)!=0) {
|
||||
this.childUpdateCallback.accept(section);
|
||||
}
|
||||
if ((types&UPDATE_TYPE_BLOCK_BIT)!=0) {
|
||||
this.renderMeshGen.accept(section.key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void triggerRemesh(long position) {
|
||||
int idx = getSliceIndex(position);
|
||||
var set = this.slices[idx];
|
||||
var lock = this.locks[idx];
|
||||
|
||||
long stamp = lock.readLock();
|
||||
byte types = set.getOrDefault(position, (byte) 0);
|
||||
lock.unlockRead(stamp);
|
||||
if ((types&UPDATE_TYPE_BLOCK_BIT)!=0) {
|
||||
this.renderMeshGen.accept(position);
|
||||
}
|
||||
}
|
||||
|
||||
private static int getSliceIndex(long value) {
|
||||
value = (value ^ value >>> 30) * -4658895280553007687L;
|
||||
value = (value ^ value >>> 27) * -7723592293110705685L;
|
||||
return (int) ((value ^ value >>> 31)&(SLICES-1));
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
package me.cortex.voxy.client.core.rendering;
|
||||
|
||||
import me.cortex.voxy.client.core.gl.GlBuffer;
|
||||
import me.cortex.voxy.client.core.rendering.util.UploadStream;
|
||||
import me.cortex.voxy.client.core.util.IndexUtil;
|
||||
import me.cortex.voxy.common.util.MemoryBuffer;
|
||||
import org.lwjgl.system.MemoryUtil;
|
||||
|
||||
|
||||
//Has a base index buffer of 16380 quads, and also a 1 cube byte index buffer at the end
|
||||
public class SharedIndexBuffer {
|
||||
public static final SharedIndexBuffer INSTANCE = new SharedIndexBuffer();
|
||||
public static final SharedIndexBuffer INSTANCE_BYTE = new SharedIndexBuffer(true);
|
||||
|
||||
private final GlBuffer indexBuffer;
|
||||
|
||||
public SharedIndexBuffer() {
|
||||
this.indexBuffer = new GlBuffer((1<<16)*6*2 + 6*2*3);
|
||||
var quadIndexBuff = IndexUtil.generateQuadIndicesShort(16380);
|
||||
var cubeBuff = generateCubeIndexBuffer();
|
||||
|
||||
long ptr = UploadStream.INSTANCE.upload(this.indexBuffer, 0, this.indexBuffer.size());
|
||||
quadIndexBuff.cpyTo(ptr);
|
||||
cubeBuff.cpyTo((1<<16)*2*6 + ptr);
|
||||
|
||||
quadIndexBuff.free();
|
||||
cubeBuff.free();
|
||||
}
|
||||
|
||||
private SharedIndexBuffer(boolean type2) {
|
||||
this.indexBuffer = new GlBuffer((1<<8)*6 + 6*2*3);
|
||||
var quadIndexBuff = IndexUtil.generateQuadIndicesByte(63);
|
||||
var cubeBuff = generateCubeIndexBuffer();
|
||||
|
||||
long ptr = UploadStream.INSTANCE.upload(this.indexBuffer, 0, this.indexBuffer.size());
|
||||
quadIndexBuff.cpyTo(ptr);
|
||||
cubeBuff.cpyTo((1<<8)*6 + ptr);
|
||||
|
||||
quadIndexBuff.free();
|
||||
cubeBuff.free();
|
||||
}
|
||||
|
||||
private static MemoryBuffer generateCubeIndexBuffer() {
|
||||
var buffer = new MemoryBuffer(6*2*3);
|
||||
long ptr = buffer.address;
|
||||
MemoryUtil.memSet(ptr, 0, 6*2*3 );
|
||||
|
||||
//Bottom face
|
||||
MemoryUtil.memPutByte(ptr++, (byte) 0);
|
||||
MemoryUtil.memPutByte(ptr++, (byte) 1);
|
||||
MemoryUtil.memPutByte(ptr++, (byte) 2);
|
||||
MemoryUtil.memPutByte(ptr++, (byte) 3);
|
||||
MemoryUtil.memPutByte(ptr++, (byte) 2);
|
||||
MemoryUtil.memPutByte(ptr++, (byte) 1);
|
||||
|
||||
//top face
|
||||
MemoryUtil.memPutByte(ptr++, (byte) 6);
|
||||
MemoryUtil.memPutByte(ptr++, (byte) 5);
|
||||
MemoryUtil.memPutByte(ptr++, (byte) 4);
|
||||
MemoryUtil.memPutByte(ptr++, (byte) 5);
|
||||
MemoryUtil.memPutByte(ptr++, (byte) 6);
|
||||
MemoryUtil.memPutByte(ptr++, (byte) 7);
|
||||
|
||||
//north face
|
||||
MemoryUtil.memPutByte(ptr++, (byte) 0);
|
||||
MemoryUtil.memPutByte(ptr++, (byte) 4);
|
||||
MemoryUtil.memPutByte(ptr++, (byte) 1);
|
||||
MemoryUtil.memPutByte(ptr++, (byte) 5);
|
||||
MemoryUtil.memPutByte(ptr++, (byte) 1);
|
||||
MemoryUtil.memPutByte(ptr++, (byte) 4);
|
||||
|
||||
//south face
|
||||
MemoryUtil.memPutByte(ptr++, (byte) 3);
|
||||
MemoryUtil.memPutByte(ptr++, (byte) 6);
|
||||
MemoryUtil.memPutByte(ptr++, (byte) 2);
|
||||
MemoryUtil.memPutByte(ptr++, (byte) 6);
|
||||
MemoryUtil.memPutByte(ptr++, (byte) 3);
|
||||
MemoryUtil.memPutByte(ptr++, (byte) 7);
|
||||
|
||||
//west face
|
||||
MemoryUtil.memPutByte(ptr++, (byte) 2);
|
||||
MemoryUtil.memPutByte(ptr++, (byte) 4);
|
||||
MemoryUtil.memPutByte(ptr++, (byte) 0);
|
||||
MemoryUtil.memPutByte(ptr++, (byte) 4);
|
||||
MemoryUtil.memPutByte(ptr++, (byte) 2);
|
||||
MemoryUtil.memPutByte(ptr++, (byte) 6);
|
||||
|
||||
//east face
|
||||
MemoryUtil.memPutByte(ptr++, (byte) 1);
|
||||
MemoryUtil.memPutByte(ptr++, (byte) 5);
|
||||
MemoryUtil.memPutByte(ptr++, (byte) 3);
|
||||
MemoryUtil.memPutByte(ptr++, (byte) 7);
|
||||
MemoryUtil.memPutByte(ptr++, (byte) 3);
|
||||
MemoryUtil.memPutByte(ptr++, (byte) 5);
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
public int id() {
|
||||
return this.indexBuffer.id;
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,69 @@
|
||||
package me.cortex.voxy.client.core.rendering;
|
||||
|
||||
import org.joml.Matrix4f;
|
||||
import me.cortex.voxy.client.core.gl.GlBuffer;
|
||||
import me.cortex.voxy.client.core.rendering.util.DepthFramebuffer;
|
||||
import me.cortex.voxy.client.core.rendering.util.HiZBuffer;
|
||||
import net.caffeinemc.mods.sodium.client.util.FogParameters;
|
||||
import net.minecraft.util.math.MathHelper;
|
||||
import org.joml.*;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
public abstract class Viewport <A extends Viewport<A>> {
|
||||
private final AbstractFarWorldRenderer renderer;
|
||||
int width;
|
||||
int height;
|
||||
int frameId;
|
||||
Matrix4f projection;
|
||||
Matrix4f modelView;
|
||||
double cameraX;
|
||||
double cameraY;
|
||||
double cameraZ;
|
||||
//public final HiZBuffer2 hiZBuffer = new HiZBuffer2();
|
||||
public final HiZBuffer hiZBuffer = new HiZBuffer();
|
||||
public final DepthFramebuffer depthBoundingBuffer = new DepthFramebuffer();
|
||||
|
||||
protected Viewport(AbstractFarWorldRenderer renderer) {
|
||||
this.renderer = renderer;
|
||||
private static final Field planesField;
|
||||
static {
|
||||
try {
|
||||
planesField = FrustumIntersection.class.getDeclaredField("planes");
|
||||
planesField.setAccessible(true);
|
||||
} catch (NoSuchFieldException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public int width;
|
||||
public int height;
|
||||
public int frameId;
|
||||
public Matrix4f vanillaProjection = new Matrix4f();
|
||||
public Matrix4f projection = new Matrix4f();
|
||||
public Matrix4f modelView = new Matrix4f();
|
||||
public final FrustumIntersection frustum = new FrustumIntersection();
|
||||
public final Vector4f[] frustumPlanes;
|
||||
public double cameraX;
|
||||
public double cameraY;
|
||||
public double cameraZ;
|
||||
public FogParameters fogParameters;
|
||||
|
||||
public final Matrix4f MVP = new Matrix4f();
|
||||
public final Vector3i section = new Vector3i();
|
||||
public final Vector3f innerTranslation = new Vector3f();
|
||||
|
||||
protected Viewport() {
|
||||
Vector4f[] planes = null;
|
||||
try {
|
||||
planes = (Vector4f[]) planesField.get(this.frustum);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
this.frustumPlanes = planes;
|
||||
}
|
||||
|
||||
public final void delete() {
|
||||
this.delete0();
|
||||
this.renderer.removeViewport((A) this);
|
||||
}
|
||||
|
||||
protected abstract void delete0();
|
||||
protected void delete0() {
|
||||
this.hiZBuffer.free();
|
||||
this.depthBoundingBuffer.free();
|
||||
}
|
||||
|
||||
public A setVanillaProjection(Matrix4fc projection) {
|
||||
this.vanillaProjection.set(projection);
|
||||
return (A) this;
|
||||
}
|
||||
|
||||
public A setProjection(Matrix4f projection) {
|
||||
this.projection = projection;
|
||||
@@ -46,4 +87,36 @@ public abstract class Viewport <A extends Viewport<A>> {
|
||||
this.height = height;
|
||||
return (A) this;
|
||||
}
|
||||
|
||||
public A setFogParameters(FogParameters fogParameters) {
|
||||
this.fogParameters = fogParameters;
|
||||
return (A) this;
|
||||
}
|
||||
|
||||
public A update() {
|
||||
//MVP
|
||||
this.projection.mul(this.modelView, this.MVP);
|
||||
|
||||
//Update the frustum
|
||||
this.frustum.set(this.MVP, false);
|
||||
|
||||
//Translation vectors
|
||||
int sx = MathHelper.floor(this.cameraX)>>5;
|
||||
int sy = MathHelper.floor(this.cameraY)>>5;
|
||||
int sz = MathHelper.floor(this.cameraZ)>>5;
|
||||
this.section.set(sx, sy, sz);
|
||||
|
||||
this.innerTranslation.set(
|
||||
(float) (this.cameraX-(sx<<5)),
|
||||
(float) (this.cameraY-(sy<<5)),
|
||||
(float) (this.cameraZ-(sz<<5)));
|
||||
|
||||
if (this.depthBoundingBuffer.resize(this.width, this.height)) {
|
||||
this.depthBoundingBuffer.clear(0.0f);
|
||||
}
|
||||
|
||||
return (A) this;
|
||||
}
|
||||
|
||||
public abstract GlBuffer getRenderList();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package me.cortex.voxy.client.core;
|
||||
package me.cortex.voxy.client.core.rendering;
|
||||
|
||||
import me.cortex.voxy.client.core.rendering.AbstractFarWorldRenderer;
|
||||
import me.cortex.voxy.client.core.rendering.Viewport;
|
||||
import me.cortex.voxy.client.core.util.IrisUtil;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import org.vivecraft.client_vr.ClientDataHolderVR;
|
||||
|
||||
@@ -9,7 +8,7 @@ import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class ViewportSelector <T extends Viewport> {
|
||||
public class ViewportSelector <T extends Viewport<?>> {
|
||||
public static final boolean VIVECRAFT_INSTALLED = FabricLoader.getInstance().isModLoaded("vivecraft");
|
||||
|
||||
private final Supplier<T> creator;
|
||||
@@ -21,19 +20,28 @@ public class ViewportSelector <T extends Viewport> {
|
||||
this.defaultViewport = viewportCreator.get();
|
||||
}
|
||||
|
||||
private T getOrCreate(Object holder) {
|
||||
return this.extraViewports.computeIfAbsent(holder, a->this.creator.get());
|
||||
}
|
||||
|
||||
private T getVivecraftViewport() {
|
||||
var cdh = ClientDataHolderVR.getInstance();
|
||||
var pass = cdh.currentPass;
|
||||
if (pass == null) {
|
||||
return this.defaultViewport;
|
||||
}
|
||||
return this.extraViewports.computeIfAbsent(pass, a->this.creator.get());
|
||||
return this.getOrCreate(pass);
|
||||
}
|
||||
|
||||
private static final Object IRIS_SHADOW_OBJECT = new Object();
|
||||
public T getViewport() {
|
||||
if (VIVECRAFT_INSTALLED) {
|
||||
return getVivecraftViewport();
|
||||
}
|
||||
|
||||
if (IrisUtil.irisShadowActive()) {
|
||||
return this.getOrCreate(IRIS_SHADOW_OBJECT);
|
||||
}
|
||||
return this.defaultViewport;
|
||||
}
|
||||
|
||||
@@ -1,26 +1,37 @@
|
||||
package me.cortex.voxy.client.core.rendering.building;
|
||||
|
||||
import me.cortex.voxy.common.util.MemoryBuffer;
|
||||
import me.cortex.voxy.commonImpl.VoxyCommon;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
//TODO: also have an AABB size stored
|
||||
public final class BuiltSection {
|
||||
public static final boolean VERIFY_BUILT_SECTION_OFFSETS = VoxyCommon.isVerificationFlagOn("verifyBuiltSectionOffsets");
|
||||
public final long position;
|
||||
public final byte childExistence;
|
||||
public final int aabb;
|
||||
public final MemoryBuffer geometryBuffer;
|
||||
public final int[] offsets;
|
||||
|
||||
public BuiltSection(long position) {
|
||||
this(position, -1, null, null);
|
||||
private BuiltSection(long position, byte children) {
|
||||
this(position, children, -1, null, null);
|
||||
}
|
||||
|
||||
public BuiltSection(long position, int aabb, MemoryBuffer geometryBuffer, int[] offsets) {
|
||||
public static BuiltSection empty(long position) {
|
||||
return new BuiltSection(position, (byte) 0);
|
||||
}
|
||||
public static BuiltSection emptyWithChildren(long position, byte children) {
|
||||
return new BuiltSection(position, children);
|
||||
}
|
||||
|
||||
public BuiltSection(long position, byte childExistence, int aabb, MemoryBuffer geometryBuffer, int[] offsets) {
|
||||
this.position = position;
|
||||
this.childExistence = childExistence;
|
||||
this.aabb = aabb;
|
||||
this.geometryBuffer = geometryBuffer;
|
||||
this.offsets = offsets;
|
||||
if (offsets != null) {
|
||||
if (offsets != null && VERIFY_BUILT_SECTION_OFFSETS) {
|
||||
for (int i = 0; i < offsets.length-1; i++) {
|
||||
int delta = offsets[i+1] - offsets[i];
|
||||
if (delta<0||delta>=(1<<16)) {
|
||||
@@ -31,7 +42,7 @@ public final class BuiltSection {
|
||||
}
|
||||
|
||||
public BuiltSection clone() {
|
||||
return new BuiltSection(this.position, this.aabb, this.geometryBuffer!=null?this.geometryBuffer.copy():null, this.offsets!=null?Arrays.copyOf(this.offsets, this.offsets.length):null);
|
||||
return new BuiltSection(this.position, this.childExistence, this.aabb, this.geometryBuffer!=null?this.geometryBuffer.copy():null, this.offsets!=null?Arrays.copyOf(this.offsets, this.offsets.length):null);
|
||||
}
|
||||
|
||||
public void free() {
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
package me.cortex.voxy.client.core.rendering.building;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
//TODO: Have a second level disk cache
|
||||
|
||||
//TODO: instead of storing duplicate render geometry between here and gpu memory
|
||||
// when a section is unloaded from the gpu, put it into a download stream and recover the BuiltSection
|
||||
// and put that into the cache, then remove the uploaded mesh from the cache
|
||||
public class BuiltSectionMeshCache {
|
||||
private static final BuiltSection HOLDER = new BuiltSection(-1);
|
||||
private final ConcurrentHashMap<Long, BuiltSection> renderCache = new ConcurrentHashMap<>(1000,0.75f,10);
|
||||
|
||||
public BuiltSection getMesh(long key) {
|
||||
BuiltSection[] res = new BuiltSection[1];
|
||||
this.renderCache.computeIfPresent(key, (a, value) -> {
|
||||
if (value == HOLDER) {
|
||||
return value;
|
||||
}
|
||||
res[0] = value.clone();
|
||||
return value;
|
||||
});
|
||||
return res[0];
|
||||
}
|
||||
|
||||
//Returns true if the mesh was used, (this is so the parent method can free mesh object)
|
||||
public boolean putMesh(BuiltSection mesh) {
|
||||
var mesh2 = this.renderCache.computeIfPresent(mesh.position, (id, value) -> {
|
||||
if (value != HOLDER) {
|
||||
value.free();
|
||||
}
|
||||
return mesh;
|
||||
});
|
||||
return mesh2 == mesh;
|
||||
}
|
||||
|
||||
public void clearMesh(long key) {
|
||||
this.renderCache.computeIfPresent(key, (a,val)->{
|
||||
if (val != HOLDER) {
|
||||
val.free();
|
||||
}
|
||||
return HOLDER;
|
||||
});
|
||||
}
|
||||
|
||||
public void markCache(long key) {
|
||||
this.renderCache.putIfAbsent(key, HOLDER);
|
||||
}
|
||||
|
||||
public void unmarkCache(long key) {
|
||||
var mesh = this.renderCache.remove(key);
|
||||
if (mesh != null && mesh != HOLDER) {
|
||||
mesh.free();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void free() {
|
||||
for (var mesh : this.renderCache.values()) {
|
||||
if (mesh != HOLDER) {
|
||||
mesh.free();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int getCount() {
|
||||
return this.renderCache.size();
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
package me.cortex.voxy.client.core.rendering.building;
|
||||
|
||||
|
||||
import me.cortex.voxy.client.core.util.Mesher2D;
|
||||
|
||||
|
||||
public class QuadEncoder {
|
||||
|
||||
public static int getX(long data) {
|
||||
return (int) ((data>>21)&0b11111);
|
||||
}
|
||||
public static int getY(long data) {
|
||||
return (int) ((data>>16)&0b11111);
|
||||
}
|
||||
public static int getZ(long data) {
|
||||
return (int) ((data>>11)&0b11111);
|
||||
}
|
||||
public static int getW(long data) {
|
||||
return (int) ((data>>3)&0b1111)+1;
|
||||
}
|
||||
public static int getH(long data) {
|
||||
return (int) ((data>>7)&0b1111)+1;
|
||||
}
|
||||
public static int getFace(long data) {
|
||||
return (int) (data&0b111);
|
||||
}
|
||||
|
||||
//Note: the encodedMeshedData is from the Mesher2D
|
||||
public static int encodePosition(int face, int otherAxis, int encodedMeshedData) {
|
||||
if (false&&(Mesher2D.getW(encodedMeshedData) > 16 || Mesher2D.getH(encodedMeshedData) > 16)) {
|
||||
throw new IllegalStateException("Width or height > 16");
|
||||
}
|
||||
int dat = face;
|
||||
dat |= ((Mesher2D.getW(encodedMeshedData) - 1) << 7) |
|
||||
((Mesher2D.getH(encodedMeshedData) - 1) << 3);
|
||||
|
||||
if (face>>1 == 0) {
|
||||
return dat |
|
||||
(Mesher2D.getX(encodedMeshedData) << 21) |
|
||||
(otherAxis << 16) |
|
||||
(Mesher2D.getZ(encodedMeshedData) << 11);
|
||||
}
|
||||
if (face>>1 == 1) {
|
||||
return dat |
|
||||
(Mesher2D.getX(encodedMeshedData) << 21) |
|
||||
(Mesher2D.getZ(encodedMeshedData) << 16) |
|
||||
(otherAxis << 11);
|
||||
}
|
||||
return dat |
|
||||
(otherAxis << 21) |
|
||||
(Mesher2D.getX(encodedMeshedData) << 16) |
|
||||
(Mesher2D.getZ(encodedMeshedData) << 11);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,199 +1,371 @@
|
||||
package me.cortex.voxy.client.core.rendering.building;
|
||||
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
||||
import me.cortex.voxy.client.core.model.IdNotYetComputedException;
|
||||
import me.cortex.voxy.client.core.model.ModelManager;
|
||||
import me.cortex.voxy.client.core.model.ModelBakerySubsystem;
|
||||
import me.cortex.voxy.common.thread.ServiceSlice;
|
||||
import me.cortex.voxy.common.thread.ServiceThreadPool;
|
||||
import me.cortex.voxy.common.util.Pair;
|
||||
import me.cortex.voxy.common.world.WorldEngine;
|
||||
import me.cortex.voxy.common.world.WorldSection;
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
import net.minecraft.text.Text;
|
||||
import me.cortex.voxy.common.world.other.Mapper;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Semaphore;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.PriorityBlockingQueue;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.locks.StampedLock;
|
||||
import java.util.function.BooleanSupplier;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.function.ToIntFunction;
|
||||
|
||||
//TODO: Add a render cache
|
||||
|
||||
|
||||
//TODO: to add remove functionallity add a "defunked" variable to the build task and set it to true on remove
|
||||
// and process accordingly
|
||||
public class RenderGenerationService {
|
||||
private static final int MAX_HOLDING_SECTION_COUNT = 1000;
|
||||
|
||||
public interface TaskChecker {boolean check(int lvl, int x, int y, int z);}
|
||||
private record BuildTask(Supplier<WorldSection> sectionSupplier) {}
|
||||
public static final AtomicInteger MESH_FAILED_COUNTER = new AtomicInteger();
|
||||
private static final AtomicInteger COUNTER = new AtomicInteger();
|
||||
private static final class BuildTask {
|
||||
WorldSection section;
|
||||
final long position;
|
||||
boolean hasDoneModelRequestInner;
|
||||
boolean hasDoneModelRequestOuter;
|
||||
int attempts;
|
||||
int addin;
|
||||
long priority = Long.MIN_VALUE;
|
||||
private BuildTask(long position) {
|
||||
this.position = position;
|
||||
}
|
||||
private void updatePriority() {
|
||||
int unique = COUNTER.incrementAndGet();
|
||||
int lvl = WorldEngine.MAX_LOD_LAYER-WorldEngine.getLevel(this.position);
|
||||
lvl = Math.min(lvl, 3);//Make the 2 highest quality have equal priority
|
||||
this.priority = (((lvl*3L + Math.min(this.attempts, 3))*2 + this.addin) <<32) + Integer.toUnsignedLong(unique);
|
||||
this.addin = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private volatile boolean running = true;
|
||||
private final Thread[] workers;
|
||||
private final AtomicInteger holdingSectionCount = new AtomicInteger();//Used to limit section holding
|
||||
|
||||
private final Long2ObjectLinkedOpenHashMap<BuildTask> taskQueue = new Long2ObjectLinkedOpenHashMap<>();
|
||||
private final AtomicInteger taskQueueCount = new AtomicInteger();
|
||||
private final PriorityBlockingQueue<BuildTask> taskQueue = new PriorityBlockingQueue<>(5000, (a,b)-> Long.compareUnsigned(a.priority, b.priority));
|
||||
private final StampedLock taskMapLock = new StampedLock();
|
||||
private final Long2ObjectOpenHashMap<BuildTask> taskMap = new Long2ObjectOpenHashMap<>(5000);
|
||||
|
||||
private final Semaphore taskCounter = new Semaphore(0);
|
||||
private final WorldEngine world;
|
||||
private final ModelManager modelManager;
|
||||
private final Consumer<BuiltSection> resultConsumer;
|
||||
private final BuiltSectionMeshCache meshCache = new BuiltSectionMeshCache();
|
||||
private final ModelBakerySubsystem modelBakery;
|
||||
private Consumer<BuiltSection> resultConsumer;
|
||||
private final boolean emitMeshlets;
|
||||
|
||||
public RenderGenerationService(WorldEngine world, ModelManager modelManager, int workers, Consumer<BuiltSection> consumer, boolean emitMeshlets) {
|
||||
private final ServiceSlice threads;
|
||||
|
||||
|
||||
public RenderGenerationService(WorldEngine world, ModelBakerySubsystem modelBakery, ServiceThreadPool serviceThreadPool, boolean emitMeshlets) {
|
||||
this(world, modelBakery, serviceThreadPool, emitMeshlets, ()->true);
|
||||
}
|
||||
|
||||
public RenderGenerationService(WorldEngine world, ModelBakerySubsystem modelBakery, ServiceThreadPool serviceThreadPool, boolean emitMeshlets, BooleanSupplier taskLimiter) {
|
||||
this.emitMeshlets = emitMeshlets;
|
||||
this.world = world;
|
||||
this.modelManager = modelManager;
|
||||
this.modelBakery = modelBakery;
|
||||
|
||||
this.threads = serviceThreadPool.createService("Section mesh generation service", 100, ()->{
|
||||
//Thread local instance of the factory
|
||||
var factory = new RenderDataFactory(this.world, this.modelBakery.factory, this.emitMeshlets);
|
||||
IntOpenHashSet seenMissed = new IntOpenHashSet(128);
|
||||
return new Pair<>(() -> {
|
||||
this.processJob(factory, seenMissed);
|
||||
}, factory::free);
|
||||
}, taskLimiter);
|
||||
}
|
||||
|
||||
public void setResultConsumer(Consumer<BuiltSection> consumer) {
|
||||
this.resultConsumer = consumer;
|
||||
this.workers = new Thread[workers];
|
||||
for (int i = 0; i < workers; i++) {
|
||||
this.workers[i] = new Thread(this::renderWorker);
|
||||
this.workers[i].setDaemon(true);
|
||||
this.workers[i].setName("Render generation service #" + i);
|
||||
this.workers[i].start();
|
||||
}
|
||||
|
||||
//NOTE: the biomes are always fully populated/kept up to date
|
||||
|
||||
//Asks the Model system to bake all blocks that currently dont have a model
|
||||
private void computeAndRequestRequiredModels(IntOpenHashSet seenMissedIds, int bitMsk, long[] auxData) {
|
||||
final var factory = this.modelBakery.factory;
|
||||
for (int i = 0; i < 6; i++) {
|
||||
if ((bitMsk&(1<<i))==0) continue;
|
||||
for (int j = 0; j < 32*32; j++) {
|
||||
int block = Mapper.getBlockId(auxData[j+(i*32*32)]);
|
||||
if (block != 0 && !factory.hasModelForBlockId(block)) {
|
||||
if (seenMissedIds.add(block)) {
|
||||
this.modelBakery.requestBlockBake(block);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void computeAndRequestRequiredModels(IntOpenHashSet seenMissedIds, WorldSection section) {
|
||||
//Know this is... very much not safe, however it reduces allocation rates and other garbage, am sure its "fine"
|
||||
final var factory = this.modelBakery.factory;
|
||||
for (long state : section._unsafeGetRawDataArray()) {
|
||||
int block = Mapper.getBlockId(state);
|
||||
if (block != 0 && !factory.hasModelForBlockId(block)) {
|
||||
if (seenMissedIds.add(block)) {
|
||||
this.modelBakery.requestBlockBake(block);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private WorldSection acquireSection(long pos) {
|
||||
return this.world.acquireIfExists(pos);
|
||||
}
|
||||
|
||||
private static boolean putTaskFirst(long pos) {
|
||||
//Level 3 or 4
|
||||
return WorldEngine.getLevel(pos) > 2;
|
||||
}
|
||||
|
||||
//TODO: add a generated render data cache
|
||||
private void renderWorker() {
|
||||
//Thread local instance of the factory
|
||||
var factory = new RenderDataFactory(this.world, this.modelManager, this.emitMeshlets);
|
||||
while (this.running) {
|
||||
this.taskCounter.acquireUninterruptibly();
|
||||
if (!this.running) break;
|
||||
try {
|
||||
BuildTask task;
|
||||
synchronized (this.taskQueue) {
|
||||
task = this.taskQueue.removeFirst();
|
||||
private void processJob(RenderDataFactory factory, IntOpenHashSet seenMissedIds) {
|
||||
BuildTask task = this.taskQueue.poll();
|
||||
this.taskQueueCount.decrementAndGet();
|
||||
|
||||
//long time = BuiltSection.getTime();
|
||||
boolean shouldFreeSection = true;
|
||||
|
||||
WorldSection section;
|
||||
if (task.section == null) {
|
||||
section = this.acquireSection(task.position);
|
||||
} else {
|
||||
section = task.section;
|
||||
}
|
||||
|
||||
|
||||
{//Remove the task from the map, this is done before we check for null sections as well the task map needs to be correct
|
||||
long stamp = this.taskMapLock.writeLock();
|
||||
var rtask = this.taskMap.remove(task.position);
|
||||
if (rtask != task) {
|
||||
this.taskMapLock.unlockWrite(stamp);
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
this.taskMapLock.unlockWrite(stamp);
|
||||
}
|
||||
|
||||
if (section == null) {
|
||||
if (this.resultConsumer != null) {
|
||||
this.resultConsumer.accept(BuiltSection.empty(task.position));
|
||||
}
|
||||
return;
|
||||
}
|
||||
section.assertNotFree();
|
||||
BuiltSection mesh = null;
|
||||
|
||||
|
||||
try {
|
||||
mesh = factory.generateMesh(section);
|
||||
} catch (IdNotYetComputedException e) {
|
||||
{
|
||||
long stamp = this.taskMapLock.writeLock();
|
||||
BuildTask other = this.taskMap.putIfAbsent(task.position, task);
|
||||
this.taskMapLock.unlockWrite(stamp);
|
||||
|
||||
if (other != null) {//Weve been replaced
|
||||
//Request the block
|
||||
if (e.isIdBlockId) {
|
||||
//TODO: maybe move this to _after_ task as been readded to queue??
|
||||
if (!this.modelBakery.factory.hasModelForBlockId(e.id)) {
|
||||
if (seenMissedIds.add(e.id)) {
|
||||
this.modelBakery.requestBlockBake(e.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
//Exchange info
|
||||
if (task.hasDoneModelRequestInner) {
|
||||
other.hasDoneModelRequestInner = true;
|
||||
}
|
||||
if (task.hasDoneModelRequestOuter) {
|
||||
other.hasDoneModelRequestOuter = true;
|
||||
}
|
||||
if (task.section != null) {
|
||||
this.holdingSectionCount.decrementAndGet();
|
||||
}
|
||||
task.section = null;
|
||||
shouldFreeSection = true;
|
||||
task = null;
|
||||
}
|
||||
var section = task.sectionSupplier.get();
|
||||
if (section == null) {
|
||||
continue;
|
||||
}
|
||||
if (task != null) {
|
||||
//This is our task
|
||||
|
||||
//Request the block
|
||||
if (e.isIdBlockId) {
|
||||
//TODO: maybe move this to _after_ task as been readded to queue??
|
||||
if (!this.modelBakery.factory.hasModelForBlockId(e.id)) {
|
||||
if (seenMissedIds.add(e.id)) {
|
||||
this.modelBakery.requestBlockBake(e.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
section.assertNotFree();
|
||||
BuiltSection mesh = null;
|
||||
try {
|
||||
mesh = factory.generateMesh(section);
|
||||
} catch (IdNotYetComputedException e) {
|
||||
|
||||
if (task.hasDoneModelRequestOuter || task.hasDoneModelRequestInner) {
|
||||
MESH_FAILED_COUNTER.incrementAndGet();
|
||||
}
|
||||
|
||||
if (task.hasDoneModelRequestInner && task.hasDoneModelRequestOuter) {
|
||||
task.attempts++;
|
||||
try {
|
||||
Thread.sleep(100);
|
||||
Thread.sleep(1);
|
||||
} catch (InterruptedException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
//We need to reinsert the build task into the queue
|
||||
//System.err.println("Render task failed to complete due to un-computed client id");
|
||||
synchronized (this.taskQueue) {
|
||||
this.taskQueue.computeIfAbsent(section.key, key->{this.taskCounter.release(); return task;});
|
||||
} else {
|
||||
if (task.hasDoneModelRequestInner) {
|
||||
task.attempts++;//This is because it can be baking and just model thing isnt keeping up
|
||||
}
|
||||
|
||||
if (!task.hasDoneModelRequestInner) {
|
||||
//The reason for the extra id parameter is that we explicitly add/check against the exception id due to e.g. requesting accross a chunk boarder wont be captured in the request
|
||||
if (e.auxData == null)//the null check this is because for it to be, the inner must already be computed
|
||||
this.computeAndRequestRequiredModels(seenMissedIds, section);
|
||||
task.hasDoneModelRequestInner = true;
|
||||
}
|
||||
//If this happens... aahaha painnnn
|
||||
if (task.hasDoneModelRequestOuter) {
|
||||
task.attempts++;
|
||||
}
|
||||
|
||||
if ((!task.hasDoneModelRequestOuter) && e.auxData != null) {
|
||||
this.computeAndRequestRequiredModels(seenMissedIds, e.auxBitMsk, e.auxData);
|
||||
task.hasDoneModelRequestOuter = true;
|
||||
}
|
||||
|
||||
task.addin = WorldEngine.getLevel(task.position)>2?1:0;//Single time addin which gives the models time to bake before the task executes
|
||||
}
|
||||
section.release();
|
||||
if (mesh != null) {
|
||||
//TODO: if the mesh is null, need to clear the cache at that point
|
||||
this.resultConsumer.accept(mesh.clone());
|
||||
if (!this.meshCache.putMesh(mesh)) {
|
||||
mesh.free();
|
||||
|
||||
//Keep the lock on the section, and attach it to the task, this prevents needing to re-aquire it later
|
||||
if (task.section == null) {
|
||||
if (this.holdingSectionCount.get() < MAX_HOLDING_SECTION_COUNT) {
|
||||
this.holdingSectionCount.incrementAndGet();
|
||||
task.section = section;
|
||||
shouldFreeSection = false;
|
||||
}
|
||||
} else {
|
||||
shouldFreeSection = false;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
System.err.println(e);
|
||||
MinecraftClient.getInstance().executeSync(()->MinecraftClient.getInstance().player.sendMessage(Text.literal("Voxy render service had an exception while executing please check logs and report error")));
|
||||
|
||||
task.updatePriority();
|
||||
this.taskQueue.add(task);
|
||||
this.taskQueueCount.incrementAndGet();
|
||||
|
||||
if (this.threads.isAlive()) {//Only execute if were not dead
|
||||
this.threads.execute();//Since we put in queue, release permit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldFreeSection) {
|
||||
if (task != null && task.section != null) {
|
||||
this.holdingSectionCount.decrementAndGet();
|
||||
}
|
||||
section.release();
|
||||
}
|
||||
|
||||
if (mesh != null) {//If the mesh is null it means it didnt finish, so dont submit
|
||||
if (this.resultConsumer != null) {
|
||||
this.resultConsumer.accept(mesh);
|
||||
} else {
|
||||
mesh.free();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int getMeshCacheCount() {
|
||||
return this.meshCache.getCount();
|
||||
}
|
||||
|
||||
//TODO: Add a priority system, higher detail sections must always be updated before lower detail
|
||||
// e.g. priorities NONE->lvl0 and lvl1 -> lvl0 over lvl0 -> lvl1
|
||||
|
||||
|
||||
//TODO: make it pass either a world section, _or_ coodinates so that the render thread has to do the loading of the sections
|
||||
// not the calling method
|
||||
|
||||
//TODO: maybe make it so that it pulls from the world to stop the inital loading absolutly butt spamming the queue
|
||||
// and thus running out of memory
|
||||
|
||||
//TODO: REDO THIS ENTIRE THING
|
||||
// render tasks should not be bound to a WorldSection, instead it should be bound to either a WorldSection or
|
||||
// an LoD position, the issue is that if we bound to a LoD position we loose all the info of the WorldSection
|
||||
// like if its in the render queue and if we should abort building the render data
|
||||
//1 proposal fix is a Long2ObjectLinkedOpenHashMap<WorldSection> which means we can abort if needed,
|
||||
// also gets rid of dependency on a WorldSection (kinda)
|
||||
public void enqueueTask(int lvl, int x, int y, int z) {
|
||||
this.enqueueTask(lvl, x, y, z, (l,x1,y1,z1)->true);
|
||||
}
|
||||
|
||||
|
||||
public void enqueueTask(int lvl, int x, int y, int z, TaskChecker checker) {
|
||||
long ikey = WorldEngine.getWorldSectionId(lvl, x, y, z);
|
||||
{
|
||||
var cache = this.meshCache.getMesh(ikey);
|
||||
if (cache != null) {
|
||||
this.resultConsumer.accept(cache);
|
||||
return;
|
||||
}
|
||||
}
|
||||
synchronized (this.taskQueue) {
|
||||
this.taskQueue.computeIfAbsent(ikey, key->{
|
||||
this.taskCounter.release();
|
||||
return new BuildTask(()->{
|
||||
if (checker.check(lvl, x, y, z)) {
|
||||
return this.world.acquireIfExists(lvl, x, y, z);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//Tells the render cache that the mesh at the specified position should be cached
|
||||
public void markCache(int lvl, int x, int y, int z) {
|
||||
this.meshCache.markCache(WorldEngine.getWorldSectionId(lvl, x, y, z));
|
||||
}
|
||||
|
||||
//Tells the render cache that the mesh at the specified position should not be cached/any previous cache result, freed
|
||||
public void unmarkCache(int lvl, int x, int y, int z) {
|
||||
this.meshCache.unmarkCache(WorldEngine.getWorldSectionId(lvl, x, y, z));
|
||||
}
|
||||
|
||||
//Resets a chunks cache mesh
|
||||
public void clearCache(int lvl, int x, int y, int z) {
|
||||
this.meshCache.clearMesh(WorldEngine.getWorldSectionId(lvl, x, y, z));
|
||||
}
|
||||
|
||||
public void removeTask(int lvl, int x, int y, int z) {
|
||||
synchronized (this.taskQueue) {
|
||||
if (this.taskQueue.remove(WorldEngine.getWorldSectionId(lvl, x, y, z)) != null) {
|
||||
this.taskCounter.acquireUninterruptibly();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int getTaskCount() {
|
||||
return this.taskCounter.availablePermits();
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
boolean anyAlive = false;
|
||||
for (var worker : this.workers) {
|
||||
anyAlive |= worker.isAlive();
|
||||
}
|
||||
|
||||
if (!anyAlive) {
|
||||
System.err.println("Render gen workers already dead on shutdown! this is very very bad, check log for errors from this thread");
|
||||
public void enqueueTask(long pos) {
|
||||
if (!this.threads.isAlive()) {
|
||||
return;
|
||||
}
|
||||
boolean[] isOurs = new boolean[1];
|
||||
long stamp = this.taskMapLock.writeLock();
|
||||
BuildTask task = this.taskMap.computeIfAbsent(pos, p->{
|
||||
isOurs[0] = true;
|
||||
return new BuildTask(p);
|
||||
});
|
||||
this.taskMapLock.unlockWrite(stamp);
|
||||
|
||||
//Since this is just render data, dont care about any tasks needing to finish
|
||||
this.running = false;
|
||||
this.taskCounter.release(1000);
|
||||
if (isOurs[0]) {//If its not ours we dont care about it
|
||||
//Set priority and insert into queue and execute
|
||||
task.updatePriority();
|
||||
this.taskQueue.add(task);
|
||||
this.taskQueueCount.incrementAndGet();
|
||||
this.threads.execute();
|
||||
}
|
||||
}
|
||||
|
||||
//Wait for thread to join
|
||||
try {
|
||||
for (var worker : this.workers) {
|
||||
worker.join();
|
||||
/*
|
||||
public void enqueueTask(int lvl, int x, int y, int z) {
|
||||
this.enqueueTask(WorldEngine.getWorldSectionId(lvl, x, y, z));
|
||||
}
|
||||
*/
|
||||
|
||||
public void shutdown() {
|
||||
//Steal and free as much work as possible
|
||||
while (this.threads.hasJobs()) {
|
||||
int i = this.threads.drain();
|
||||
if (i == 0) break;
|
||||
{
|
||||
long stamp = this.taskMapLock.writeLock();
|
||||
for (int j = 0; j < i; j++) {
|
||||
var task = this.taskQueue.remove();
|
||||
if (task.section != null) {
|
||||
task.section.release();
|
||||
this.holdingSectionCount.decrementAndGet();
|
||||
}
|
||||
if (this.taskMap.remove(task.position) != task) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
}
|
||||
this.taskMapLock.unlockWrite(stamp);
|
||||
this.taskQueueCount.addAndGet(-i);
|
||||
}
|
||||
} catch (InterruptedException e) {throw new RuntimeException(e);}
|
||||
}
|
||||
|
||||
//Shutdown the threads
|
||||
this.threads.shutdown();
|
||||
|
||||
//Cleanup any remaining data
|
||||
while (!this.taskQueue.isEmpty()) {
|
||||
this.taskQueue.removeFirst();
|
||||
var task = this.taskQueue.remove();
|
||||
this.taskQueueCount.decrementAndGet();
|
||||
if (task.section != null) {
|
||||
task.section.release();
|
||||
this.holdingSectionCount.decrementAndGet();
|
||||
}
|
||||
|
||||
long stamp = this.taskMapLock.writeLock();
|
||||
if (this.taskMap.remove(task.position) != task) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
this.taskMapLock.unlockWrite(stamp);
|
||||
}
|
||||
this.meshCache.free();
|
||||
if (this.taskQueueCount.get() != 0) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
}
|
||||
|
||||
private long lastChangedTime = 0;
|
||||
private int failedCounter = 0;
|
||||
public void addDebugData(List<String> debug) {
|
||||
if (System.currentTimeMillis()-this.lastChangedTime > 1000) {
|
||||
this.failedCounter = 0;
|
||||
this.lastChangedTime = System.currentTimeMillis();
|
||||
}
|
||||
this.failedCounter += MESH_FAILED_COUNTER.getAndSet(0);
|
||||
debug.add("RSSQ/TFC: " + this.taskQueueCount.get() + "/" + this.failedCounter);//render section service queue, Task Fail Counter
|
||||
|
||||
}
|
||||
|
||||
public int getTaskCount() {
|
||||
return this.taskQueueCount.get();
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,85 @@
|
||||
package me.cortex.voxy.client.core.rendering.hierachical;
|
||||
|
||||
import me.cortex.voxy.client.core.gl.GlBuffer;
|
||||
import me.cortex.voxy.client.core.gl.GlVertexArray;
|
||||
import me.cortex.voxy.client.core.gl.shader.Shader;
|
||||
import me.cortex.voxy.client.core.gl.shader.ShaderType;
|
||||
import me.cortex.voxy.client.core.rendering.Viewport;
|
||||
import me.cortex.voxy.client.core.rendering.util.SharedIndexBuffer;
|
||||
import me.cortex.voxy.client.core.rendering.util.UploadStream;
|
||||
import net.minecraft.util.math.MathHelper;
|
||||
import org.joml.Matrix4f;
|
||||
import org.joml.Vector3f;
|
||||
import org.lwjgl.opengl.GL15;
|
||||
import org.lwjgl.system.MemoryUtil;
|
||||
|
||||
import static org.lwjgl.opengl.GL11.GL_TRIANGLES;
|
||||
import static org.lwjgl.opengl.GL11.GL_UNSIGNED_BYTE;
|
||||
import static org.lwjgl.opengl.GL15.GL_ELEMENT_ARRAY_BUFFER;
|
||||
import static org.lwjgl.opengl.GL15C.glBindBuffer;
|
||||
import static org.lwjgl.opengl.GL30.*;
|
||||
import static org.lwjgl.opengl.GL31.GL_UNIFORM_BUFFER;
|
||||
import static org.lwjgl.opengl.GL40.GL_DRAW_INDIRECT_BUFFER;
|
||||
import static org.lwjgl.opengl.GL40.glDrawElementsIndirect;
|
||||
import static org.lwjgl.opengl.GL42.glMemoryBarrier;
|
||||
import static org.lwjgl.opengl.GL43.*;
|
||||
|
||||
public class DebugRenderer {
|
||||
private final Shader debugShader = Shader.make()
|
||||
.add(ShaderType.VERTEX, "voxy:lod/hierarchical/debug/node_outline.vert")
|
||||
.add(ShaderType.FRAGMENT, "voxy:lod/hierarchical/debug/frag.frag")
|
||||
.compile();
|
||||
private final Shader setupShader = Shader.make()
|
||||
.add(ShaderType.COMPUTE, "voxy:lod/hierarchical/debug/setup.comp")
|
||||
.compile();
|
||||
|
||||
private final GlBuffer uniformBuffer = new GlBuffer(1024).zero();
|
||||
private final GlBuffer drawBuffer = new GlBuffer(1024).zero();
|
||||
|
||||
private void uploadUniform(Viewport<?> viewport) {
|
||||
long ptr = UploadStream.INSTANCE.upload(this.uniformBuffer, 0, 1024);
|
||||
int sx = MathHelper.floor(viewport.cameraX)>>5;
|
||||
int sy = MathHelper.floor(viewport.cameraY)>>5;
|
||||
int sz = MathHelper.floor(viewport.cameraZ)>>5;
|
||||
|
||||
new Matrix4f(viewport.projection).mul(viewport.modelView).getToAddress(ptr); ptr += 4*4*4;
|
||||
|
||||
MemoryUtil.memPutInt(ptr, sx); ptr += 4;
|
||||
MemoryUtil.memPutInt(ptr, sy); ptr += 4;
|
||||
MemoryUtil.memPutInt(ptr, sz); ptr += 4;
|
||||
MemoryUtil.memPutInt(ptr, viewport.width); ptr += 4;
|
||||
|
||||
var innerTranslation = new Vector3f((float) (viewport.cameraX-(sx<<5)), (float) (viewport.cameraY-(sy<<5)), (float) (viewport.cameraZ-(sz<<5)));
|
||||
innerTranslation.getToAddress(ptr); ptr += 4*3;
|
||||
|
||||
MemoryUtil.memPutInt(ptr, viewport.height); ptr += 4;
|
||||
}
|
||||
|
||||
public void render(Viewport<?> viewport, GlBuffer nodeData, GlBuffer nodeList) {
|
||||
this.uploadUniform(viewport);
|
||||
UploadStream.INSTANCE.commit();
|
||||
|
||||
this.setupShader.bind();
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, this.drawBuffer.id);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, nodeList.id);
|
||||
glDispatchCompute(1,1,1);
|
||||
glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT|GL_COMMAND_BARRIER_BIT);
|
||||
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
this.debugShader.bind();
|
||||
glBindVertexArray(GlVertexArray.STATIC_VAO);
|
||||
glBindBuffer(GL_DRAW_INDIRECT_BUFFER, this.drawBuffer.id);
|
||||
GL15.glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, SharedIndexBuffer.INSTANCE_BYTE.id());
|
||||
glBindBufferBase(GL_UNIFORM_BUFFER, 0, this.uniformBuffer.id);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, nodeData.id);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, nodeList.id);
|
||||
glDrawElementsIndirect(GL_TRIANGLES, GL_UNSIGNED_BYTE, 0);
|
||||
}
|
||||
|
||||
public void free() {
|
||||
this.drawBuffer.free();
|
||||
this.uniformBuffer.free();
|
||||
this.debugShader.free();
|
||||
this.setupShader.free();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,368 @@
|
||||
package me.cortex.voxy.client.core.rendering.hierachical;
|
||||
|
||||
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
|
||||
import me.cortex.voxy.client.RenderStatistics;
|
||||
import me.cortex.voxy.client.config.VoxyConfig;
|
||||
import me.cortex.voxy.client.core.gl.GlBuffer;
|
||||
import me.cortex.voxy.client.core.gl.shader.AutoBindingShader;
|
||||
import me.cortex.voxy.client.core.gl.shader.Shader;
|
||||
import me.cortex.voxy.client.core.gl.shader.ShaderType;
|
||||
import me.cortex.voxy.client.core.rendering.Viewport;
|
||||
import me.cortex.voxy.client.core.rendering.building.RenderGenerationService;
|
||||
import me.cortex.voxy.client.core.rendering.util.DownloadStream;
|
||||
import me.cortex.voxy.client.core.rendering.util.PrintfDebugUtil;
|
||||
import me.cortex.voxy.client.core.rendering.util.UploadStream;
|
||||
import me.cortex.voxy.common.Logger;
|
||||
import me.cortex.voxy.common.util.MemoryBuffer;
|
||||
import me.cortex.voxy.common.world.WorldEngine;
|
||||
import org.lwjgl.system.MemoryUtil;
|
||||
|
||||
import static me.cortex.voxy.client.core.rendering.util.PrintfDebugUtil.PRINTF_processor;
|
||||
import static org.lwjgl.opengl.GL11.*;
|
||||
import static org.lwjgl.opengl.GL12.GL_UNPACK_IMAGE_HEIGHT;
|
||||
import static org.lwjgl.opengl.GL12.GL_UNPACK_SKIP_IMAGES;
|
||||
import static org.lwjgl.opengl.GL30.*;
|
||||
import static org.lwjgl.opengl.GL30C.GL_RED_INTEGER;
|
||||
import static org.lwjgl.opengl.GL42.glMemoryBarrier;
|
||||
import static org.lwjgl.opengl.GL43.GL_SHADER_STORAGE_BARRIER_BIT;
|
||||
import static org.lwjgl.opengl.GL45.*;
|
||||
|
||||
// TODO: swap to persistent gpu threads instead of dispatching MAX_ITERATIONS of compute layers
|
||||
public class HierarchicalOcclusionTraverser {
|
||||
public static final boolean HIERARCHICAL_SHADER_DEBUG = System.getProperty("voxy.hierarchicalShaderDebug", "false").equals("true");
|
||||
|
||||
public static final int MAX_REQUEST_QUEUE_SIZE = 50;
|
||||
public static final int MAX_QUEUE_SIZE = 200_000;
|
||||
|
||||
|
||||
private static final int MAX_ITERATIONS = WorldEngine.MAX_LOD_LAYER+1;
|
||||
private static final int LOCAL_WORK_SIZE_BITS = 5;
|
||||
|
||||
private final AsyncNodeManager nodeManager;
|
||||
private final NodeCleaner nodeCleaner;
|
||||
private final RenderGenerationService meshGen;
|
||||
|
||||
private final GlBuffer requestBuffer;
|
||||
|
||||
private final GlBuffer nodeBuffer;
|
||||
private final GlBuffer uniformBuffer = new GlBuffer(1024).zero();
|
||||
private final GlBuffer statisticsBuffer = new GlBuffer(1024).zero();
|
||||
|
||||
|
||||
private int topNodeCount;
|
||||
private final Int2IntOpenHashMap topNode2idxMapping = new Int2IntOpenHashMap();//Used to store mapping from TLN to array index
|
||||
private final int[] idx2topNodeMapping = new int[MAX_QUEUE_SIZE];//Used to map idx to TLN id
|
||||
private final GlBuffer topNodeIds = new GlBuffer(MAX_QUEUE_SIZE*4).zero();
|
||||
private final GlBuffer queueMetaBuffer = new GlBuffer(4*4*MAX_ITERATIONS).zero();
|
||||
private final GlBuffer scratchQueueA = new GlBuffer(MAX_QUEUE_SIZE*4).zero();
|
||||
private final GlBuffer scratchQueueB = new GlBuffer(MAX_QUEUE_SIZE*4).zero();
|
||||
|
||||
private static int BINDING_COUNTER = 1;
|
||||
private static final int SCENE_UNIFORM_BINDING = BINDING_COUNTER++;
|
||||
private static final int REQUEST_QUEUE_BINDING = BINDING_COUNTER++;
|
||||
private static final int RENDER_QUEUE_BINDING = BINDING_COUNTER++;
|
||||
private static final int NODE_DATA_BINDING = BINDING_COUNTER++;
|
||||
private static final int NODE_QUEUE_INDEX_BINDING = BINDING_COUNTER++;
|
||||
private static final int NODE_QUEUE_META_BINDING = BINDING_COUNTER++;
|
||||
private static final int NODE_QUEUE_SOURCE_BINDING = BINDING_COUNTER++;
|
||||
private static final int NODE_QUEUE_SINK_BINDING = BINDING_COUNTER++;
|
||||
private static final int RENDER_TRACKER_BINDING = BINDING_COUNTER++;
|
||||
private static final int STATISTICS_BUFFER_BINDING = BINDING_COUNTER++;
|
||||
|
||||
private final int hizSampler = glGenSamplers();
|
||||
|
||||
private final AutoBindingShader traversal = Shader.makeAuto(PRINTF_processor)
|
||||
.defineIf("DEBUG", HIERARCHICAL_SHADER_DEBUG)
|
||||
.define("MAX_ITERATIONS", MAX_ITERATIONS)
|
||||
.define("LOCAL_SIZE_BITS", LOCAL_WORK_SIZE_BITS)
|
||||
.define("MAX_REQUEST_QUEUE_SIZE", MAX_REQUEST_QUEUE_SIZE)
|
||||
|
||||
.define("HIZ_BINDING", 0)
|
||||
|
||||
.define("SCENE_UNIFORM_BINDING", SCENE_UNIFORM_BINDING)
|
||||
.define("REQUEST_QUEUE_BINDING", REQUEST_QUEUE_BINDING)
|
||||
.define("RENDER_QUEUE_BINDING", RENDER_QUEUE_BINDING)
|
||||
.define("NODE_DATA_BINDING", NODE_DATA_BINDING)
|
||||
|
||||
.define("NODE_QUEUE_INDEX_BINDING", NODE_QUEUE_INDEX_BINDING)
|
||||
.define("NODE_QUEUE_META_BINDING", NODE_QUEUE_META_BINDING)
|
||||
.define("NODE_QUEUE_SOURCE_BINDING", NODE_QUEUE_SOURCE_BINDING)
|
||||
.define("NODE_QUEUE_SINK_BINDING", NODE_QUEUE_SINK_BINDING)
|
||||
|
||||
.define("RENDER_TRACKER_BINDING", RENDER_TRACKER_BINDING)
|
||||
|
||||
.defineIf("HAS_STATISTICS", RenderStatistics.enabled)
|
||||
.defineIf("STATISTICS_BUFFER_BINDING", RenderStatistics.enabled, STATISTICS_BUFFER_BINDING)
|
||||
|
||||
.add(ShaderType.COMPUTE, "voxy:lod/hierarchical/traversal_dev.comp")
|
||||
.compile();
|
||||
|
||||
|
||||
public HierarchicalOcclusionTraverser(AsyncNodeManager nodeManager, NodeCleaner nodeCleaner, RenderGenerationService meshGen) {
|
||||
this.nodeCleaner = nodeCleaner;
|
||||
this.nodeManager = nodeManager;
|
||||
this.meshGen = meshGen;
|
||||
this.requestBuffer = new GlBuffer(MAX_REQUEST_QUEUE_SIZE*8L+8).zero();
|
||||
this.nodeBuffer = new GlBuffer(nodeManager.maxNodeCount*16L).fill(-1);
|
||||
|
||||
|
||||
glSamplerParameteri(this.hizSampler, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST);
|
||||
glSamplerParameteri(this.hizSampler, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glSamplerParameteri(this.hizSampler, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
glSamplerParameteri(this.hizSampler, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
|
||||
this.traversal
|
||||
.ubo("SCENE_UNIFORM_BINDING", this.uniformBuffer)
|
||||
.ssbo("REQUEST_QUEUE_BINDING", this.requestBuffer)
|
||||
.ssbo("NODE_DATA_BINDING", this.nodeBuffer)
|
||||
.ssbo("NODE_QUEUE_META_BINDING", this.queueMetaBuffer)
|
||||
.ssbo("RENDER_TRACKER_BINDING", this.nodeCleaner.visibilityBuffer)
|
||||
.ssboIf("STATISTICS_BUFFER_BINDING", this.statisticsBuffer);
|
||||
|
||||
this.topNode2idxMapping.defaultReturnValue(-1);
|
||||
this.nodeManager.setTLNAddRemoveCallbacks(this::addTLN, this::remTLN);
|
||||
}
|
||||
|
||||
private void addTLN(int id) {
|
||||
int aid = this.topNodeCount++;//Increment buffer
|
||||
if (this.topNodeCount > this.topNodeIds.size()/4) {
|
||||
throw new IllegalStateException("Top level node count greater than capacity");
|
||||
}
|
||||
|
||||
//Use clear buffer, yes know is a bad idea, TODO: replace
|
||||
//Add the new top level node to the queue
|
||||
MemoryUtil.memPutInt(SCRATCH, id);
|
||||
nglClearNamedBufferSubData(this.topNodeIds.id, GL_R32UI, aid * 4L, 4, GL_RED_INTEGER, GL_UNSIGNED_INT, SCRATCH);
|
||||
|
||||
if (this.topNode2idxMapping.put(id, aid) != -1) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
this.idx2topNodeMapping[aid] = id;
|
||||
}
|
||||
|
||||
private void remTLN(int id) {
|
||||
//Remove id
|
||||
int idx = this.topNode2idxMapping.remove(id);
|
||||
//Decrement count
|
||||
this.topNodeCount--;
|
||||
if (idx == -1) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
//Count has already been decremented so is an exact match
|
||||
//If we are at the end of the array we dont need to do anything
|
||||
if (idx == this.topNodeCount) {
|
||||
return;
|
||||
}
|
||||
|
||||
//Move the entry at the end to the current index
|
||||
int endTLNId = this.idx2topNodeMapping[this.topNodeCount];
|
||||
this.idx2topNodeMapping[idx] = endTLNId;//Set the old to the new
|
||||
if (this.topNode2idxMapping.put(endTLNId, idx) == -1)
|
||||
throw new IllegalStateException();
|
||||
|
||||
//Move it server side, from end to new idx
|
||||
MemoryUtil.memPutInt(SCRATCH, endTLNId);
|
||||
nglClearNamedBufferSubData(this.topNodeIds.id, GL_R32UI, idx*4L, 4, GL_RED_INTEGER, GL_UNSIGNED_INT, SCRATCH);
|
||||
}
|
||||
|
||||
private static void setFrustum(Viewport<?> viewport, long ptr) {
|
||||
for (int i = 0; i < 6; i++) {
|
||||
var plane = viewport.frustumPlanes[i];
|
||||
plane.getToAddress(ptr); ptr += 4*4;
|
||||
}
|
||||
}
|
||||
|
||||
private void uploadUniform(Viewport<?> viewport) {
|
||||
long ptr = UploadStream.INSTANCE.upload(this.uniformBuffer, 0, 1024);
|
||||
|
||||
viewport.MVP.getToAddress(ptr); ptr += 4*4*4;
|
||||
|
||||
viewport.section.getToAddress(ptr); ptr += 4*3;
|
||||
|
||||
//MemoryUtil.memPutFloat(ptr, viewport.width); ptr += 4;
|
||||
MemoryUtil.memPutInt(ptr, viewport.hiZBuffer.getPackedLevels()); ptr += 4;
|
||||
|
||||
viewport.innerTranslation.getToAddress(ptr); ptr += 4*3;
|
||||
|
||||
//MemoryUtil.memPutFloat(ptr, viewport.height); ptr += 4;
|
||||
|
||||
final float screenspaceAreaDecreasingSize = VoxyConfig.CONFIG.subDivisionSize*VoxyConfig.CONFIG.subDivisionSize;
|
||||
//Screen space size for descending
|
||||
MemoryUtil.memPutFloat(ptr, (float) (screenspaceAreaDecreasingSize) /(viewport.width*viewport.height)); ptr += 4;
|
||||
|
||||
setFrustum(viewport, ptr); ptr += 4*4*6;
|
||||
|
||||
MemoryUtil.memPutInt(ptr, (int) (viewport.getRenderList().size()/4-1)); ptr += 4;
|
||||
|
||||
//VisibilityId
|
||||
MemoryUtil.memPutInt(ptr, this.nodeCleaner.visibilityId); ptr += 4;
|
||||
|
||||
{
|
||||
final double TARGET_COUNT = 4000;//TODO: make this configurable, or at least dynamically computed based on throughput rate of mesh gen
|
||||
double iFillness = Math.max(0, (TARGET_COUNT - this.meshGen.getTaskCount()) / TARGET_COUNT);
|
||||
iFillness = Math.pow(iFillness, 2);
|
||||
final int requestSize = (int) Math.ceil(iFillness * MAX_REQUEST_QUEUE_SIZE);
|
||||
MemoryUtil.memPutInt(ptr, Math.max(0, Math.min(MAX_REQUEST_QUEUE_SIZE, requestSize)));ptr += 4;
|
||||
}
|
||||
}
|
||||
|
||||
private void bindings(Viewport<?> viewport) {
|
||||
glBindBuffer(GL_DISPATCH_INDIRECT_BUFFER, this.queueMetaBuffer.id);
|
||||
|
||||
//Bind the hiz buffer
|
||||
glBindTextureUnit(0, viewport.hiZBuffer.getHizTextureId());
|
||||
glBindSampler(0, this.hizSampler);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, RENDER_QUEUE_BINDING, viewport.getRenderList().id);
|
||||
}
|
||||
|
||||
public void doTraversal(Viewport<?> viewport) {
|
||||
this.uploadUniform(viewport);
|
||||
//UploadStream.INSTANCE.commit(); //Done inside traversal
|
||||
|
||||
this.traversal.bind();
|
||||
this.bindings(viewport);
|
||||
PrintfDebugUtil.bind();
|
||||
|
||||
if (RenderStatistics.enabled) {
|
||||
this.statisticsBuffer.zero();
|
||||
}
|
||||
|
||||
//Clear the render output counter
|
||||
nglClearNamedBufferSubData(viewport.getRenderList().id, GL_R32UI, 0, 4, GL_RED_INTEGER, GL_UNSIGNED_INT, 0);
|
||||
|
||||
//Traverse
|
||||
this.traverseInternal();
|
||||
|
||||
this.downloadResetRequestQueue();
|
||||
|
||||
if (RenderStatistics.enabled) {
|
||||
DownloadStream.INSTANCE.download(this.statisticsBuffer, down->{
|
||||
for (int i = 0; i < MAX_ITERATIONS; i++) {
|
||||
RenderStatistics.hierarchicalTraversalCounts[i] = MemoryUtil.memGetInt(down.address+i*4L);
|
||||
}
|
||||
|
||||
for (int i = 0; i < MAX_ITERATIONS; i++) {
|
||||
RenderStatistics.hierarchicalRenderSections[i] = MemoryUtil.memGetInt(down.address+MAX_ITERATIONS*4L+i*4L);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//Bind the hiz buffer
|
||||
glBindSampler(0, 0);
|
||||
glBindTextureUnit(0, 0);
|
||||
}
|
||||
|
||||
private void traverseInternal() {
|
||||
{
|
||||
//Fix mesa bug
|
||||
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
|
||||
glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, 0);
|
||||
glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0);
|
||||
glPixelStorei(GL_UNPACK_SKIP_ROWS, 0);
|
||||
glPixelStorei(GL_UNPACK_SKIP_IMAGES, 0);
|
||||
}
|
||||
|
||||
int firstDispatchSize = (this.topNodeCount+(1<<LOCAL_WORK_SIZE_BITS)-1)>>LOCAL_WORK_SIZE_BITS;
|
||||
/*
|
||||
//prime the queue Todo: maybe move after the traversal? cause then it is more efficient work since it doesnt need to wait for this before starting?
|
||||
glClearNamedBufferData(this.queueMetaBuffer.id, GL_RGBA32UI, GL_RGBA, GL_UNSIGNED_INT, new int[]{0,1,1,0});//Prime the metadata buffer, which also contains
|
||||
|
||||
//Set the first entry
|
||||
glClearNamedBufferSubData(this.queueMetaBuffer.id, GL_RGBA32UI, 0, 16, GL_RGBA, GL_UNSIGNED_INT, new int[]{firstDispatchSize,1,1,initialQueueSize});
|
||||
*/
|
||||
{//TODO:FIXME: THIS IS BULLSHIT BY INTEL need to fix the clearing
|
||||
long ptr = UploadStream.INSTANCE.upload(this.queueMetaBuffer, 0, 16*MAX_ITERATIONS);
|
||||
MemoryUtil.memPutInt(ptr + 0, firstDispatchSize);
|
||||
MemoryUtil.memPutInt(ptr + 4, 1);
|
||||
MemoryUtil.memPutInt(ptr + 8, 1);
|
||||
MemoryUtil.memPutInt(ptr + 12, this.topNodeCount);
|
||||
for (int i = 1; i < MAX_ITERATIONS; i++) {
|
||||
MemoryUtil.memPutInt(ptr + (i*16)+ 0, 0);
|
||||
MemoryUtil.memPutInt(ptr + (i*16)+ 4, 1);
|
||||
MemoryUtil.memPutInt(ptr + (i*16)+ 8, 1);
|
||||
MemoryUtil.memPutInt(ptr + (i*16)+12, 0);
|
||||
}
|
||||
UploadStream.INSTANCE.commit();
|
||||
}
|
||||
|
||||
//Execute first iteration
|
||||
glUniform1ui(NODE_QUEUE_INDEX_BINDING, 0);
|
||||
|
||||
//Use the top node id buffer
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, NODE_QUEUE_SOURCE_BINDING, this.topNodeIds.id);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, NODE_QUEUE_SINK_BINDING, this.scratchQueueB.id);
|
||||
|
||||
//Dont need to use indirect to dispatch the first iteration
|
||||
glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT|GL_COMMAND_BARRIER_BIT|GL_BUFFER_UPDATE_BARRIER_BIT);
|
||||
glDispatchCompute(firstDispatchSize, 1,1);
|
||||
glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT|GL_COMMAND_BARRIER_BIT);
|
||||
|
||||
//Dispatch max iterations
|
||||
for (int iter = 1; iter < MAX_ITERATIONS; iter++) {
|
||||
glUniform1ui(NODE_QUEUE_INDEX_BINDING, iter);
|
||||
|
||||
//Flipflop buffers
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, NODE_QUEUE_SOURCE_BINDING, ((iter & 1) == 0 ? this.scratchQueueA : this.scratchQueueB).id);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, NODE_QUEUE_SINK_BINDING, ((iter & 1) == 0 ? this.scratchQueueB : this.scratchQueueA).id);
|
||||
|
||||
glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT | GL_COMMAND_BARRIER_BIT);
|
||||
|
||||
//Dispatch and barrier
|
||||
glDispatchComputeIndirect(iter * 4 * 4);
|
||||
}
|
||||
|
||||
glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT | GL_COMMAND_BARRIER_BIT);
|
||||
}
|
||||
|
||||
|
||||
private void downloadResetRequestQueue() {
|
||||
glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT);
|
||||
DownloadStream.INSTANCE.download(this.requestBuffer, this::forwardDownloadResult);
|
||||
nglClearNamedBufferSubData(this.requestBuffer.id, GL_R32UI, 0, 4, GL_RED_INTEGER, GL_UNSIGNED_INT, 0);
|
||||
}
|
||||
|
||||
private void forwardDownloadResult(long ptr, long size) {
|
||||
int count = MemoryUtil.memGetInt(ptr);ptr += 8;//its 8 since we need to skip the second value (which is empty)
|
||||
if (count < 0 || count > 50000) {
|
||||
Logger.error(new IllegalStateException("Count unexpected extreme value: " + count + " things may get weird"));
|
||||
return;
|
||||
}
|
||||
if (count > (this.requestBuffer.size()>>3)-1) {
|
||||
//This should not break the synchonization between gpu and cpu as in the traversal shader is
|
||||
// `if (atomRes < REQUEST_QUEUE_SIZE) {` which forcefully clamps to the request size
|
||||
|
||||
//Logger.warn("Count over max buffer size, clamping, got count: " + count + ".");
|
||||
|
||||
count = (int) ((this.requestBuffer.size()>>3)-1);
|
||||
|
||||
//Write back the clamped count
|
||||
MemoryUtil.memPutInt(ptr-8, count);
|
||||
}
|
||||
//if (count > REQUEST_QUEUE_SIZE) {
|
||||
// Logger.warn("Count larger than 'maxRequestCount', overflow captured. Overflowed by " + (count-REQUEST_QUEUE_SIZE));
|
||||
//}
|
||||
if (count != 0) {
|
||||
this.nodeManager.submitRequestBatch(new MemoryBuffer(count*8L+8).cpyFrom(ptr-8));// the -8 is because we incremented it by 8
|
||||
}
|
||||
}
|
||||
|
||||
public GlBuffer getNodeBuffer() {
|
||||
return this.nodeBuffer;
|
||||
}
|
||||
|
||||
public void free() {
|
||||
this.traversal.free();
|
||||
this.requestBuffer.free();
|
||||
this.nodeBuffer.free();
|
||||
this.uniformBuffer.free();
|
||||
this.statisticsBuffer.free();
|
||||
this.queueMetaBuffer.free();
|
||||
this.topNodeIds.free();
|
||||
this.scratchQueueA.free();
|
||||
this.scratchQueueB.free();
|
||||
glDeleteSamplers(this.hizSampler);
|
||||
}
|
||||
|
||||
private static final long SCRATCH = MemoryUtil.nmemAlloc(32);//32 bytes of scratch memory
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package me.cortex.voxy.client.core.rendering.hierachical;
|
||||
|
||||
class NodeChildRequest {
|
||||
//Child states contain micrometadata in the top bits
|
||||
// such as isEmpty, and isEmptyButEventuallyHasNonEmptyChild
|
||||
private final long nodePos;
|
||||
|
||||
private final int[] childStates = new int[]{-1,-1,-1,-1,-1,-1,-1,-1};
|
||||
private final byte[] childChildExistence = new byte[]{(byte) 0,(byte) 0,(byte) 0,(byte) 0,(byte) 0,(byte) 0,(byte) 0,(byte) 0};
|
||||
|
||||
private byte results;
|
||||
private byte mask;
|
||||
private byte existenceMask = 0;
|
||||
|
||||
NodeChildRequest(long nodePos) {
|
||||
this.nodePos = nodePos;
|
||||
}
|
||||
|
||||
public int getChildMesh(int childIdx) {
|
||||
if ((this.mask&(1<<childIdx))==0) {
|
||||
throw new IllegalStateException("Tried getting mesh result of child not in mask");
|
||||
}
|
||||
return this.childStates[childIdx];
|
||||
}
|
||||
|
||||
public void setChildChildExistence(int childIdx, byte childExistence) {
|
||||
if ((this.mask&(1<<childIdx))==0) {
|
||||
throw new IllegalStateException("Tried setting child child existence in request when child isnt in mask");
|
||||
}
|
||||
this.childChildExistence[childIdx] = childExistence;
|
||||
this.existenceMask |= (byte) (1<<childIdx);
|
||||
}
|
||||
|
||||
public boolean hasChildChildExistence(int childId) {
|
||||
if ((this.mask&(1<<childId))==0) {
|
||||
throw new IllegalStateException("Tried getting child child existence set of child not in mask");
|
||||
}
|
||||
return (this.existenceMask&(1<<childId))!=0;
|
||||
}
|
||||
|
||||
public byte getChildChildExistence(int childIdx) {
|
||||
if (!this.hasChildChildExistence(childIdx)) {
|
||||
throw new IllegalStateException("Tried getting child child existence when child child existence for child was not set");
|
||||
}
|
||||
return this.childChildExistence[childIdx];
|
||||
}
|
||||
|
||||
public int setChildMesh(int childIdx, int mesh) {
|
||||
if ((this.mask&(1<<childIdx))==0) {
|
||||
throw new IllegalStateException("Tried setting child mesh when child isnt in mask");
|
||||
}
|
||||
//Note the mesh can be -ve meaning empty mesh, but we should still mark that node as having a result
|
||||
boolean isFirstInsert = (this.results&(1<<childIdx))==0;
|
||||
this.results |= (byte) (1<<childIdx);
|
||||
|
||||
int prev = this.childStates[childIdx];
|
||||
this.childStates[childIdx] = mesh;
|
||||
if (isFirstInsert) {
|
||||
return -1;
|
||||
} else {
|
||||
return prev;
|
||||
}
|
||||
}
|
||||
|
||||
public int removeAndUnRequire(int childIdx) {
|
||||
byte MSK = (byte) (1<<childIdx);
|
||||
if ((this.mask&MSK)==0) {
|
||||
throw new IllegalStateException("Tried removing and unmasking child that was never masked");
|
||||
}
|
||||
byte prev = this.results;
|
||||
this.results &= (byte) ~MSK;
|
||||
this.mask &= (byte) ~MSK;
|
||||
this.existenceMask &= (byte) ~MSK;
|
||||
int mesh = this.childStates[childIdx];
|
||||
this.childStates[childIdx] = -1;
|
||||
if ((prev&MSK)==0) {
|
||||
return -1;
|
||||
} else {
|
||||
return mesh;
|
||||
}
|
||||
}
|
||||
|
||||
public void addChildRequirement(int childIdx) {
|
||||
byte MSK = (byte) (1<<childIdx);
|
||||
if ((this.mask&MSK)!=0) {
|
||||
throw new IllegalStateException("Child already required!");
|
||||
}
|
||||
this.mask |= MSK;
|
||||
}
|
||||
|
||||
public boolean isSatisfied() {
|
||||
return (this.results&this.mask)==this.mask;
|
||||
}
|
||||
|
||||
public long getPosition() {
|
||||
return this.nodePos;
|
||||
}
|
||||
|
||||
public byte getMsk() {
|
||||
return this.mask;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
package me.cortex.voxy.client.core.rendering.hierachical;
|
||||
|
||||
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
|
||||
import me.cortex.voxy.client.core.gl.GlBuffer;
|
||||
import me.cortex.voxy.client.core.gl.shader.AutoBindingShader;
|
||||
import me.cortex.voxy.client.core.gl.shader.Shader;
|
||||
import me.cortex.voxy.client.core.gl.shader.ShaderType;
|
||||
import me.cortex.voxy.client.core.rendering.util.DownloadStream;
|
||||
import me.cortex.voxy.client.core.rendering.util.PrintfDebugUtil;
|
||||
import me.cortex.voxy.client.core.rendering.util.UploadStream;
|
||||
import org.lwjgl.opengl.ARBDirectStateAccess;
|
||||
import org.lwjgl.system.MemoryUtil;
|
||||
|
||||
import static org.lwjgl.opengl.GL30C.glBindBufferRange;
|
||||
import static org.lwjgl.opengl.GL42C.glMemoryBarrier;
|
||||
import static org.lwjgl.opengl.GL43C.*;
|
||||
|
||||
//Uses compute shaders to compute the last 256 rendered section (64x64 workgroup size maybe)
|
||||
// done via warp level sort, then workgroup sort (shared memory), (/w sorting network)
|
||||
// then use bubble sort (/w fast path going to middle or 2 subdivisions deep) the bubble it up
|
||||
// can do incremental sorting pass aswell, so only scan and sort a rolling sector of sections
|
||||
// (over a few frames to not cause lag, maybe)
|
||||
|
||||
|
||||
//TODO : USE THIS IN HierarchicalOcclusionTraverser instead of other shit
|
||||
public class NodeCleaner {
|
||||
//TODO: use batch_visibility_set to clear visibility data when nodes are removed!! (TODO: nodeManager will need to forward info to this)
|
||||
|
||||
|
||||
private static final int SORTING_WORKER_SIZE = 64;
|
||||
private static final int WORK_PER_THREAD = 8;
|
||||
static final int OUTPUT_COUNT = 256;
|
||||
|
||||
|
||||
private final AutoBindingShader sorter = Shader.makeAuto(PrintfDebugUtil.PRINTF_processor)
|
||||
.define("WORK_SIZE", SORTING_WORKER_SIZE)
|
||||
.define("ELEMS_PER_THREAD", WORK_PER_THREAD)
|
||||
.define("OUTPUT_SIZE", OUTPUT_COUNT)
|
||||
.define("VISIBILITY_BUFFER_BINDING", 1)
|
||||
.define("OUTPUT_BUFFER_BINDING", 2)
|
||||
.define("NODE_DATA_BINDING", 3)
|
||||
.add(ShaderType.COMPUTE, "voxy:lod/hierarchical/cleaner/sort_visibility.comp")
|
||||
.compile();
|
||||
|
||||
private final AutoBindingShader resultTransformer = Shader.makeAuto()
|
||||
.define("OUTPUT_SIZE", OUTPUT_COUNT)
|
||||
.define("MIN_ID_BUFFER_BINDING", 0)
|
||||
.define("NODE_BUFFER_BINDING", 1)
|
||||
.define("OUTPUT_BUFFER_BINDING", 2)
|
||||
.define("VISIBILITY_BUFFER_BINDING", 3)
|
||||
.add(ShaderType.COMPUTE, "voxy:lod/hierarchical/cleaner/result_transformer.comp")
|
||||
.compile();
|
||||
|
||||
private final AutoBindingShader batchClear = Shader.makeAuto()
|
||||
.define("VISIBILITY_BUFFER_BINDING", 0)
|
||||
.define("LIST_BUFFER_BINDING", 1)
|
||||
.add(ShaderType.COMPUTE, "voxy:lod/hierarchical/cleaner/batch_visibility_set.comp")
|
||||
.compile();
|
||||
|
||||
|
||||
final GlBuffer visibilityBuffer;
|
||||
private final GlBuffer outputBuffer = new GlBuffer(OUTPUT_COUNT*4+OUTPUT_COUNT*8);//Scratch + output
|
||||
|
||||
private final AsyncNodeManager nodeManager;
|
||||
int visibilityId = 0;
|
||||
|
||||
|
||||
public NodeCleaner(AsyncNodeManager nodeManager) {
|
||||
this.nodeManager = nodeManager;
|
||||
this.visibilityBuffer = new GlBuffer(nodeManager.maxNodeCount*4L).zero();
|
||||
this.visibilityBuffer.fill(-1);
|
||||
|
||||
this.batchClear
|
||||
.ssbo("VISIBILITY_BUFFER_BINDING", this.visibilityBuffer);
|
||||
|
||||
this.sorter
|
||||
.ssbo("VISIBILITY_BUFFER_BINDING", this.visibilityBuffer)
|
||||
.ssbo("OUTPUT_BUFFER_BINDING", this.outputBuffer);
|
||||
|
||||
/*
|
||||
this.nodeManager.setClear(new NodeManager.ICleaner() {
|
||||
@Override
|
||||
public void alloc(int id) {
|
||||
NodeCleaner.this.allocIds.add(id);
|
||||
NodeCleaner.this.freeIds.remove(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void move(int from, int to) {
|
||||
NodeCleaner.this.allocIds.remove(to);
|
||||
glCopyNamedBufferSubData(NodeCleaner.this.visibilityBuffer.id, NodeCleaner.this.visibilityBuffer.id, 4L*from, 4L*to, 4);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void free(int id) {
|
||||
NodeCleaner.this.freeIds.add(id);
|
||||
NodeCleaner.this.allocIds.remove(id);
|
||||
}
|
||||
});
|
||||
*/
|
||||
}
|
||||
|
||||
|
||||
public void tick(GlBuffer nodeDataBuffer) {
|
||||
this.visibilityId++;
|
||||
if (this.shouldCleanGeometry()) {
|
||||
this.outputBuffer.fill(this.nodeManager.maxNodeCount - 2);//TODO: maybe dont set to zero??
|
||||
|
||||
this.sorter.bind();
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 3, nodeDataBuffer.id);
|
||||
|
||||
//TODO: choose whether this is in nodeSpace or section/geometryId space
|
||||
//
|
||||
glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT);
|
||||
//This should (IN THEORY naturally align its self to the pow2 max boarder, if not... well undefined behavior is ok right?)
|
||||
glDispatchCompute((this.nodeManager.getCurrentMaxNodeId() + (SORTING_WORKER_SIZE*WORK_PER_THREAD) - 1) / (SORTING_WORKER_SIZE*WORK_PER_THREAD), 1, 1);
|
||||
|
||||
this.resultTransformer.bind();
|
||||
glBindBufferRange(GL_SHADER_STORAGE_BUFFER, 0, this.outputBuffer.id, 0, 4 * OUTPUT_COUNT);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, nodeDataBuffer.id);
|
||||
glBindBufferRange(GL_SHADER_STORAGE_BUFFER, 2, this.outputBuffer.id, 4 * OUTPUT_COUNT, 8 * OUTPUT_COUNT);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 3, this.visibilityBuffer.id);
|
||||
glUniform1ui(0, this.visibilityId);
|
||||
|
||||
glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT);
|
||||
glDispatchCompute(1, 1, 1);
|
||||
glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT);
|
||||
|
||||
DownloadStream.INSTANCE.download(this.outputBuffer, 4 * OUTPUT_COUNT, 8 * OUTPUT_COUNT,
|
||||
buffer -> this.nodeManager.submitRemoveBatch(buffer.copy())//Copy into buffer and emit to node manager
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean shouldCleanGeometry() {
|
||||
if (false) {
|
||||
//If used more than 75% of geometry buffer
|
||||
long used = this.nodeManager.getUsedGeometryCapacity();
|
||||
return 3 < ((double) used) / ((double) (this.nodeManager.getGeometryCapacity() - used));
|
||||
} else {
|
||||
long remaining = this.nodeManager.getGeometryCapacity() - this.nodeManager.getUsedGeometryCapacity();
|
||||
return remaining < 256_000_000;//If less than 256 mb free memory
|
||||
}
|
||||
}
|
||||
|
||||
public void updateIds(IntOpenHashSet collection) {
|
||||
if (!collection.isEmpty()) {
|
||||
int count = collection.size();
|
||||
long addr = UploadStream.INSTANCE.rawUploadAddress(count * 4 + 16);//TODO ensure alignment, create method todo alignment things
|
||||
addr = (addr+15)&~15L;//Align to 16 bytes
|
||||
|
||||
long ptr = UploadStream.INSTANCE.getBaseAddress() + addr;
|
||||
var iter = collection.iterator();
|
||||
while (iter.hasNext()) {
|
||||
MemoryUtil.memPutInt(ptr, iter.nextInt()); ptr+=4;
|
||||
}
|
||||
UploadStream.INSTANCE.commit();
|
||||
|
||||
this.batchClear.bind();
|
||||
glBindBufferRange(GL_SHADER_STORAGE_BUFFER, 1, UploadStream.INSTANCE.getRawBufferId(), addr, count*4L);
|
||||
glUniform1ui(0, count);
|
||||
glUniform1ui(1, this.visibilityId);
|
||||
glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT);
|
||||
glDispatchCompute((count+127)/128, 1, 1);
|
||||
glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT);
|
||||
}
|
||||
}
|
||||
|
||||
private void dumpDebugData() {
|
||||
int[] outData = new int[OUTPUT_COUNT*3];
|
||||
ARBDirectStateAccess.glGetNamedBufferSubData(this.outputBuffer.id, 0, outData);
|
||||
for(int i =0;i < OUTPUT_COUNT; i++) {
|
||||
System.out.println(outData[i]);
|
||||
}
|
||||
/*
|
||||
System.out.println("---------------\n");
|
||||
for(int i =0;i < OUTPUT_COUNT; i++) {
|
||||
System.out.println(data[i*2+OUTPUT_COUNT]+", "+data[i*2+OUTPUT_COUNT+1]);
|
||||
}*/
|
||||
int[] visData = new int[(int) (this.visibilityBuffer.size()/4)];
|
||||
ARBDirectStateAccess.glGetNamedBufferSubData(this.visibilityBuffer.id, 0, visData);
|
||||
int a = 0;
|
||||
}
|
||||
|
||||
public void free() {
|
||||
this.sorter.free();
|
||||
this.visibilityBuffer.free();
|
||||
this.outputBuffer.free();
|
||||
this.batchClear.free();
|
||||
this.resultTransformer.free();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,312 @@
|
||||
package me.cortex.voxy.client.core.rendering.hierachical;
|
||||
|
||||
import me.cortex.voxy.common.util.HierarchicalBitSet;
|
||||
import org.lwjgl.system.MemoryUtil;
|
||||
|
||||
public final class NodeStore {
|
||||
public static final int EMPTY_GEOMETRY_ID = -1;
|
||||
public static final int NODE_ID_MSK = ((1<<24)-1);
|
||||
public static final int REQUEST_ID_MSK = ((1<<16)-1);
|
||||
public static final int GEOMETRY_ID_MSK = (1<<24)-1;
|
||||
public static final int MAX_GEOMETRY_ID = (1<<24)-3;
|
||||
private static final int SENTINEL_NULL_GEOMETRY_ID = (1<<24)-1;
|
||||
private static final int SENTINEL_EMPTY_GEOMETRY_ID = (1<<24)-2;
|
||||
private static final int SENTINEL_NULL_NODE_ID = NODE_ID_MSK;
|
||||
private static final int LONGS_PER_NODE = 4;
|
||||
private static final int INCREMENT_SIZE = 1<<16;
|
||||
private final HierarchicalBitSet allocationSet;
|
||||
private long[] localNodeData;
|
||||
public NodeStore(int maxNodeCount) {
|
||||
if (maxNodeCount>=SENTINEL_NULL_NODE_ID) {
|
||||
throw new IllegalArgumentException("Max count too large");
|
||||
}
|
||||
//Initial count is 1024
|
||||
this.localNodeData = new long[INCREMENT_SIZE*LONGS_PER_NODE];
|
||||
this.allocationSet = new HierarchicalBitSet(maxNodeCount);
|
||||
}
|
||||
|
||||
private static int id2idx(int idx) {
|
||||
return idx*LONGS_PER_NODE;
|
||||
}
|
||||
|
||||
public int allocate() {
|
||||
int id = this.allocationSet.allocateNext();
|
||||
if (id < 0) {
|
||||
throw new IllegalStateException("Failed to allocate node slot!");
|
||||
}
|
||||
this.ensureSized(id);
|
||||
this.clear(id);
|
||||
return id;
|
||||
}
|
||||
|
||||
public int allocate(int count) {
|
||||
if (count <= 0) {
|
||||
throw new IllegalArgumentException("Count cannot be <= 0 was " + count);
|
||||
}
|
||||
int id = this.allocationSet.allocateNextConsecutiveCounted(count);
|
||||
if (id < 0) {
|
||||
throw new IllegalStateException("Failed to allocate " + count + " consecutive nodes!!");
|
||||
}
|
||||
this.ensureSized(id + count);
|
||||
for (int i = 0; i < count; i++) {
|
||||
this.clear(id + i);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
//Ensures that index is within the array, if not, resizes to contain it + buffer zone
|
||||
private void ensureSized(int index) {
|
||||
if (index*LONGS_PER_NODE >= this.localNodeData.length) {
|
||||
int newSize = Math.min((index+INCREMENT_SIZE), this.allocationSet.getLimit());
|
||||
|
||||
long[] newStore = new long[newSize * LONGS_PER_NODE];
|
||||
System.arraycopy(this.localNodeData, 0, newStore, 0, this.localNodeData.length);
|
||||
this.localNodeData = newStore;
|
||||
}
|
||||
}
|
||||
|
||||
public void free(int nodeId) {
|
||||
this.free(nodeId, 1);
|
||||
}
|
||||
|
||||
public void free(int baseNodeId, int count) {
|
||||
for (int i = 0; i < count; i++) {
|
||||
int nodeId = baseNodeId + i;
|
||||
if (!this.allocationSet.free(nodeId)) {//TODO: add batch free
|
||||
throw new IllegalStateException("Node " + nodeId + " was not allocated!");
|
||||
}
|
||||
this.clear(nodeId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void clear(int nodeId) {
|
||||
int idx = id2idx(nodeId);
|
||||
this.localNodeData[idx] = -1;//Position
|
||||
this.localNodeData[idx+1] = GEOMETRY_ID_MSK|(((long)NODE_ID_MSK)<<24);
|
||||
this.localNodeData[idx+2] = REQUEST_ID_MSK;
|
||||
this.localNodeData[idx+3] = 0;
|
||||
}
|
||||
|
||||
|
||||
//Copy from allocated index A to allocated index B
|
||||
public void copyNode(int fromId, int toId) {
|
||||
if (!(this.allocationSet.isSet(fromId)&&this.allocationSet.isSet(toId))) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
int f = id2idx(fromId);
|
||||
int t = id2idx(toId);
|
||||
this.localNodeData[t ] = this.localNodeData[f ];
|
||||
this.localNodeData[t+1] = this.localNodeData[f+1];
|
||||
this.localNodeData[t+2] = this.localNodeData[f+2];
|
||||
this.localNodeData[t+3] = this.localNodeData[f+3];
|
||||
}
|
||||
|
||||
public void setNodePosition(int node, long position) {
|
||||
this.localNodeData[id2idx(node)] = position;
|
||||
}
|
||||
|
||||
public long nodePosition(int nodeId) {
|
||||
return this.localNodeData[id2idx(nodeId)];
|
||||
}
|
||||
|
||||
public boolean nodeExists(int nodeId) {
|
||||
return this.allocationSet.isSet(nodeId);
|
||||
}
|
||||
|
||||
public int getNodeGeometry(int node) {
|
||||
long data = this.localNodeData[id2idx(node)+1];
|
||||
int geometryPtr = (int) (data&GEOMETRY_ID_MSK);
|
||||
if (geometryPtr == SENTINEL_NULL_GEOMETRY_ID) {
|
||||
return -1;
|
||||
}
|
||||
if (geometryPtr == SENTINEL_EMPTY_GEOMETRY_ID) {
|
||||
return -2;
|
||||
}
|
||||
return geometryPtr;
|
||||
}
|
||||
|
||||
public void setNodeGeometry(int node, int geometryId) {
|
||||
|
||||
if (geometryId>MAX_GEOMETRY_ID) {
|
||||
throw new IllegalArgumentException("Geometry ptr greater than MAX_GEOMETRY_ID: " + geometryId);
|
||||
}
|
||||
|
||||
if (geometryId == -1) {
|
||||
geometryId = SENTINEL_NULL_GEOMETRY_ID;
|
||||
}
|
||||
|
||||
if (geometryId == -2) {
|
||||
geometryId = SENTINEL_EMPTY_GEOMETRY_ID;
|
||||
}
|
||||
|
||||
if (geometryId < 0) {
|
||||
throw new IllegalArgumentException("Geometry ptr less than -1 : " + geometryId);
|
||||
}
|
||||
|
||||
int idx = id2idx(node)+1;
|
||||
long data = this.localNodeData[idx];
|
||||
data &= ~GEOMETRY_ID_MSK;
|
||||
data |= geometryId;
|
||||
this.localNodeData[idx] = data;
|
||||
}
|
||||
|
||||
public int getChildPtr(int nodeId) {
|
||||
long data = this.localNodeData[id2idx(nodeId)+1];
|
||||
int nodePtr = (int) ((data>>24)&NODE_ID_MSK);
|
||||
if (nodePtr == SENTINEL_NULL_NODE_ID) {
|
||||
return -1;
|
||||
}
|
||||
return nodePtr;
|
||||
}
|
||||
|
||||
public void setChildPtr(int nodeId, int ptr) {
|
||||
if (ptr>=NODE_ID_MSK || ptr<-1) {
|
||||
throw new IllegalArgumentException("Node child ptr greater GEQ NODE_ID_MSK or less than -1 : " + ptr);
|
||||
}
|
||||
if (ptr == -1) {
|
||||
ptr = SENTINEL_NULL_NODE_ID;
|
||||
}
|
||||
|
||||
int idx = id2idx(nodeId)+1;
|
||||
long data = this.localNodeData[idx];
|
||||
data &= ~(((long)NODE_ID_MSK)<<24);
|
||||
data |= ((long)ptr)<<24;
|
||||
this.localNodeData[idx] = data;
|
||||
}
|
||||
|
||||
public void setNodeRequest(int node, int requestId) {
|
||||
int id = id2idx(node)+2;
|
||||
long data = this.localNodeData[id];
|
||||
data &= ~REQUEST_ID_MSK;
|
||||
data |= requestId;
|
||||
this.localNodeData[id] = data;
|
||||
}
|
||||
|
||||
public int getNodeRequest(int node) {
|
||||
return (int) (this.localNodeData[id2idx(node)+2]&REQUEST_ID_MSK);
|
||||
}
|
||||
|
||||
public void markRequestInFlight(int nodeId) {
|
||||
this.localNodeData[id2idx(nodeId)+1] |= 1L<<63;
|
||||
}
|
||||
public void unmarkRequestInFlight(int nodeId) {
|
||||
this.localNodeData[id2idx(nodeId)+1] &= ~(1L<<63);
|
||||
}
|
||||
public boolean isNodeRequestInFlight(int nodeId) {
|
||||
return ((this.localNodeData[id2idx(nodeId)+1]>>63)&1)!=0;
|
||||
}
|
||||
|
||||
//TODO: Implement this in node manager
|
||||
public void setAllChildrenAreLeaf(int nodeId, boolean state) {
|
||||
this.localNodeData[id2idx(nodeId)+2] &= ~(1L<<16);
|
||||
this.localNodeData[id2idx(nodeId)+2] |= state?1L<<16:0;
|
||||
}
|
||||
|
||||
public boolean getAllChildrenAreLeaf(int nodeId) {
|
||||
return ((this.localNodeData[id2idx(nodeId)+2]>>16)&1)!=0;
|
||||
}
|
||||
|
||||
public void markNodeGeometryInFlight(int nodeId) {
|
||||
this.localNodeData[id2idx(nodeId)+1] |= 1L<<59;
|
||||
}
|
||||
public void unmarkNodeGeometryInFlight(int nodeId) {
|
||||
this.localNodeData[id2idx(nodeId)+1] &= ~(1L<<59);
|
||||
}
|
||||
public boolean isNodeGeometryInFlight(int nodeId) {
|
||||
return (this.localNodeData[id2idx(nodeId)+1]&(1L<<59))!=0;
|
||||
}
|
||||
|
||||
public int getNodeType(int nodeId) {
|
||||
return (int)((this.localNodeData[id2idx(nodeId)+1]>>61)&3)<<30;
|
||||
}
|
||||
|
||||
public void setNodeType(int nodeId, int type) {
|
||||
type >>>= 30;
|
||||
int idx = id2idx(nodeId)+1;
|
||||
long data = this.localNodeData[idx];
|
||||
data &= ~(3L<<61);
|
||||
data |= ((long)type)<<61;
|
||||
this.localNodeData[idx] = data;
|
||||
}
|
||||
|
||||
public byte getNodeChildExistence(int nodeId) {
|
||||
long data = this.localNodeData[id2idx(nodeId)+1];
|
||||
return (byte) ((data>>48)&0xFF);
|
||||
}
|
||||
|
||||
public void setNodeChildExistence(int nodeId, byte existence) {
|
||||
int idx = id2idx(nodeId)+1;
|
||||
long data = this.localNodeData[idx];
|
||||
data &= ~(0xFFL<<48);
|
||||
data |= Byte.toUnsignedLong(existence)<<48;
|
||||
this.localNodeData[idx] = data;
|
||||
}
|
||||
|
||||
public int getChildPtrCount(int nodeId) {
|
||||
long data = this.localNodeData[id2idx(nodeId)+1];
|
||||
return ((int)((data>>56)&0x7))+1;
|
||||
}
|
||||
|
||||
public void setChildPtrCount(int nodeId, int count) {
|
||||
if (count <= 0 || count>8) throw new IllegalArgumentException("Count: " + count);
|
||||
int idx = id2idx(nodeId)+1;
|
||||
long data = this.localNodeData[idx];
|
||||
data &= ~(7L<<56);
|
||||
data |= ((long) (count - 1)) <<56;
|
||||
this.localNodeData[idx] = data;
|
||||
}
|
||||
|
||||
//Writes out a nodes data to the ptr in the compacted/reduced format
|
||||
public void writeNode(long ptr, int nodeId) {
|
||||
if (!this.nodeExists(nodeId)) {
|
||||
MemoryUtil.memPutLong(ptr, -1);
|
||||
MemoryUtil.memPutLong(ptr + 8, -1);
|
||||
return;
|
||||
}
|
||||
long pos = this.nodePosition(nodeId);
|
||||
MemoryUtil.memPutInt(ptr, (int) (pos>>32)); ptr += 4;
|
||||
MemoryUtil.memPutInt(ptr, (int) pos); ptr += 4;
|
||||
|
||||
int z = 0;
|
||||
int w = 0;
|
||||
|
||||
short flags = 0;
|
||||
flags |= (short) (this.isNodeRequestInFlight(nodeId)?1:0);//1 bit
|
||||
flags |= (short) ((this.getChildPtrCount(nodeId)-1)<<2);//3 bit
|
||||
|
||||
boolean isEligibleForCleaning = false;
|
||||
isEligibleForCleaning |= this.getAllChildrenAreLeaf(nodeId);
|
||||
//isEligibleForCleaning |= this.getNodeType()
|
||||
|
||||
flags |= (short) (isEligibleForCleaning?1<<5:0);//1 bit
|
||||
|
||||
{
|
||||
int geometry = this.getNodeGeometry(nodeId);
|
||||
if (geometry == -2) {
|
||||
z |= 0xFFFFFF-1;//This is a special case, which basically says to the renderer that the geometry is empty (not that it doesnt exist)
|
||||
} else if (geometry == -1) {
|
||||
z |= 0xFFFFFF;//Special case null
|
||||
} else {
|
||||
z |= geometry&0xFFFFFF;//TODO: check and ensure bounds
|
||||
}
|
||||
}
|
||||
int childPtr = this.getChildPtr(nodeId);
|
||||
//TODO: check and ensure bounds
|
||||
w |= childPtr&0xFFFFFF;
|
||||
|
||||
z |= (flags&0xFF)<<24;
|
||||
w |= ((flags>>8)&0xFF)<<24;
|
||||
|
||||
MemoryUtil.memPutInt(ptr, z); ptr += 4;
|
||||
MemoryUtil.memPutInt(ptr, w); ptr += 4;
|
||||
}
|
||||
|
||||
public int getEndNodeId() {
|
||||
return this.allocationSet.getMaxIndex();
|
||||
}
|
||||
public int getNodeCount() {
|
||||
return this.allocationSet.getCount();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user