514 Commits

Author SHA1 Message Date
mcrcortex
e8dd71defe better debug logging + fix tracking crash 2025-05-29 23:11:04 +10:00
mcrcortex
e3329f210b woops 2025-05-29 23:10:37 +10:00
mcrcortex
5841b36469 Updated thing 2025-05-29 22:57:32 +10:00
mcrcortex
fe2e6522ed Refactored voxy frontend to use "WorldIdentifier" system 2025-05-29 22:03:39 +10:00
mcrcortex
ea930ad917 Fix flickering due to incorrect state restoration 2025-05-29 18:51:31 +10:00
mcrcortex
f9d1d9961b Attempt to fix some woopsies 2025-05-28 12:11:42 +10:00
mcrcortex
efda204001 Merge remote-tracking branch 'origin/mc_1215' into mc_1215 2025-05-27 15:00:51 +10:00
mcrcortex
0c5882af8e Am a Fking idiot ;-; 2025-05-27 15:00:22 +10:00
MCRcortex
56b75e38e2 Merge pull request #105 from zea64/mc_1215
Implement setThreadAffinity for Linux.
2025-05-27 13:30:54 +10:00
mcrcortex
b899dde5fe Fixes 2025-05-26 18:17:25 +10:00
mcrcortex
50e0634a94 Move things to correct viewport 2025-05-26 18:08:29 +10:00
mcrcortex
28f2e1e881 Fix future leak 2025-05-26 16:11:04 +10:00
mcrcortex
e8e89f022b Wip on geometry cache 2025-05-26 16:10:01 +10:00
mcrcortex
5d8cc2b3c4 barrier 2025-05-26 13:10:05 +10:00
mcrcortex
3b261d9989 tweek 2025-05-26 13:09:27 +10:00
Natalya McKay
4660ab927c Implement setThreadAffinity for Linux. 2025-05-25 00:31:44 -04:00
mcrcortex
c283f6eac4 Update iris 2025-05-24 11:14:06 +10:00
mcrcortex
d7d930c4fa coment ret false 2025-05-23 15:22:29 +10:00
mcrcortex
b8a16fb087 just use the currently bound fb (and pray its correct) 2025-05-23 12:13:38 +10:00
mcrcortex
74dac668fb Hardcode leaves to solid layers 2025-05-23 12:10:14 +10:00
mcrcortex
a1ace12042 things 2025-05-23 09:48:16 +10:00
mcrcortex
90a3a16cc2 Fix intel 2025-05-23 00:21:57 +10:00
mcrcortex
0882b71a9f tweek setting 2025-05-22 22:31:13 +10:00
mcrcortex
cdfa15c1f6 Add support for FREX flawless frames, failed attempt to fix intel igpus 2025-05-22 19:50:25 +10:00
mcrcortex
a314c26b89 Prefix sum based translucency 2025-05-22 11:45:28 +10:00
mcrcortex
5c2024bab4 Fix multi gson 2025-05-20 10:09:48 +10:00
mcrcortex
5c8f2a43cb Fix cleaning, add assertion 2025-05-20 10:09:37 +10:00
mcrcortex
055be2aa2a Reduce compression to 1 2025-05-20 09:30:33 +10:00
mcrcortex
973f61e428 Missed runtime only component 2025-05-19 21:46:53 +10:00
mcrcortex
3a13071edb add for future ref 2025-05-19 21:40:14 +10:00
mcrcortex
68e6621e7e Accept world 2025-05-19 21:38:28 +10:00
mcrcortex
d808486cd0 Tweeked buildscript 2025-05-19 21:38:15 +10:00
mcrcortex
128f8eda98 Bind threads to cores 2025-05-19 12:40:46 +10:00
mcrcortex
4a5961d656 more util 2025-05-19 12:20:47 +10:00
mcrcortex
8adc708e4d Add cpu layout 2025-05-19 12:20:41 +10:00
mcrcortex
2962b47939 Add validation of top level position + MiB if path 2025-05-18 12:41:26 +10:00
mcrcortex
fd22ea4153 move thingie to insert 2025-05-18 10:39:51 +10:00
mcrcortex
9be68ac2f4 Beans 2025-05-18 10:35:03 +10:00
mcrcortex
fc612c608f Fix world importer breaking if there are no files to import 2025-05-17 15:01:47 +10:00
mcrcortex
40c6d50d5e dont remap args 2025-05-17 14:21:26 +10:00
mcrcortex
5d91a5bc09 Disable voxy if on unsupported system 2025-05-17 14:18:02 +10:00
mcrcortex
2249e96496 Gl debug msg logging 2025-05-17 12:12:47 +10:00
mcrcortex
5bee5dd1d1 suppress warnings 2025-05-17 11:50:00 +10:00
mcrcortex
0b1d8b9fd9 Added gpu compute memcpy + cpu side timing statistics 2025-05-17 11:35:56 +10:00
mcrcortex
f3aecbe944 Format change 2025-05-17 11:35:01 +10:00
mcrcortex
b9e5870a7f Added reset 2025-05-17 11:34:53 +10:00
mcrcortex
f872b995c3 Barrier 2025-05-17 11:34:39 +10:00
mcrcortex
74ecc39921 Tweeked priotiy 2025-05-17 11:34:18 +10:00
mcrcortex
947802d5ed move from coheriant stuff 2025-05-17 11:34:00 +10:00
mcrcortex
95fbd77a0e Model fixes (namely on intel) 2025-05-17 11:33:35 +10:00
mcrcortex
2618c6a978 random things (tinker with barriers :ohno:) 2025-05-16 14:25:16 +10:00
mcrcortex
717def2b1a Add implicit alignment 2025-05-16 00:47:43 +10:00
mcrcortex
a8e2876586 Downgrade from error to warning 2025-05-16 00:34:06 +10:00
mcrcortex
802b853b75 random dep cleanup 2025-05-15 23:15:10 +10:00
mcrcortex
9bc0f46be9 remove print 2025-05-15 22:27:16 +10:00
mcrcortex
73f27b65b1 metadata 2025-05-15 22:10:07 +10:00
mcrcortex
28ef19a1dc Todo 2025-05-15 22:09:51 +10:00
mcrcortex
7d9b735f8b uhoh 2025-05-15 21:40:35 +10:00
mcrcortex
e58cbaa6de Geometry cleaner works again 2025-05-15 21:39:32 +10:00
mcrcortex
48799d8ea1 Continue work on adding node cleaner support 2025-05-15 20:00:46 +10:00
mcrcortex
c49f5309cd Fixed multiple issues with mipmaps and alpha discard 2025-05-15 20:00:14 +10:00
mcrcortex
566e1da6e7 oops 2025-05-15 10:56:38 +10:00
mcrcortex
88b166d297 beans 2025-05-15 10:55:47 +10:00
mcrcortex
c2be58b0f2 Micro improvements 2025-05-14 22:52:49 +10:00
mcrcortex
3221702fc2 compute scatter based setting 2025-05-14 20:19:50 +10:00
mcrcortex
0d27de5ba7 bean 2025-05-14 10:14:45 +10:00
mcrcortex
3ade56c582 things 2025-05-13 17:26:49 +10:00
mcrcortex
0028e5ec8f Continues work 2025-05-12 22:25:38 +10:00
mcrcortex
d1957680e8 inital implementation of async manager 2025-05-12 19:48:17 +10:00
mcrcortex
1acb063b83 start 2025-05-09 21:36:17 +10:00
mcrcortex
86ff9cebd2 A 2025-05-09 20:49:14 +10:00
mcrcortex
93ea97b8dc Attempt to fix crash 2025-05-09 09:52:27 +10:00
mcrcortex
ef8a5af94b Added missing .0, and shader dumping 2025-05-09 09:46:00 +10:00
mcrcortex
4aa04c7cce increase budget 2025-05-09 00:41:49 +10:00
mcrcortex
d9c4116449 log rate limiter 2025-05-09 00:40:30 +10:00
mcrcortex
a10aa444bb tweeked miping 2025-05-09 00:08:59 +10:00
mcrcortex
aa4120433e fuck 2025-05-08 22:59:01 +10:00
mcrcortex
79f42760b0 Fix a bunch of lighting issues 2025-05-08 22:56:58 +10:00
mcrcortex
cbc8428e23 note 2025-05-08 19:25:06 +10:00
mcrcortex
28507cc8b0 Barrier pain 2025-05-08 19:19:31 +10:00
mcrcortex
af863ae608 move barrier 2025-05-08 17:44:03 +10:00
mcrcortex
6a42bcccc7 move inside if 2025-05-08 16:20:42 +10:00
mcrcortex
09666e2dfa select right render distance 2025-05-08 16:03:57 +10:00
mcrcortex
1b5c787b3a match sodium culling 2025-05-08 16:00:14 +10:00
mcrcortex
ff4c1b267e Cleanup 2025-05-07 09:48:21 +10:00
mcrcortex
ec866fa1b8 double max traversal size 2025-05-07 09:30:24 +10:00
mcrcortex
3851b07294 barriers 2025-05-07 09:24:36 +10:00
mcrcortex
e42802a2bd Remove old render factory, rename RenderDataFactory45 to just RenderDataFactory 2025-05-07 01:19:22 +10:00
mcrcortex
fb52dea6fa Finished the modelfactory stuff, is now renders block entities like shulkers 2025-05-06 22:52:00 +10:00
mcrcortex
d072f474a4 Mostly finish non-opaque model support 2025-05-06 22:07:22 +10:00
mcrcortex
6087dba91b notes and tweeks 2025-05-06 20:33:21 +10:00
mcrcortex
7a4e01faab Added task failure counter to f3 2025-05-06 01:53:24 +10:00
mcrcortex
45aaf9b981 rewrote the render side of texture bakery, is very significantly better 2025-05-06 01:14:32 +10:00
mcrcortex
553935626e some? improvements maybe? 2025-05-04 23:25:20 +10:00
mcrcortex
636b680c87 Threading stuff, locking stuff, hopefully fix for section tracker data race?? 2025-05-04 22:23:10 +10:00
mcrcortex
da36c6abd1 woops 2025-05-04 15:17:13 +10:00
mcrcortex
99f0335d36 locks and replacing O(N) size queries with counters 2025-05-04 15:11:35 +10:00
mcrcortex
94223738ec tinker 2025-05-04 11:50:49 +10:00
mcrcortex
74bda196f0 WIP extended meshing 2025-05-04 11:50:39 +10:00
mcrcortex
365ae58c70 timing + other 2025-05-03 15:34:34 +10:00
mcrcortex
834253f0ae thing 2025-05-03 15:10:41 +10:00
mcrcortex
86c4c8f09e replace full locks with stamped locks 2025-05-03 13:05:21 +10:00
mcrcortex
79cd18d310 Remaining things for outline culling 2025-05-02 23:45:18 +10:00
mcrcortex
fce99fbde3 stamps and notes 2025-05-02 23:44:49 +10:00
mcrcortex
917e308ece releaseNow when trying to shutdown service 2025-05-02 23:44:12 +10:00
mcrcortex
f74992a37a fix issue with autoboxing 2025-05-02 23:43:55 +10:00
mcrcortex
7c4a3fe7b4 more jank 2025-05-02 23:08:28 +10:00
mcrcortex
e82d2a875c _spent 3 hours on a single missing *32_ 2025-05-02 22:58:55 +10:00
mcrcortex
8b04be72ee other 2025-05-02 18:39:14 +10:00
mcrcortex
6aebddb773 Attempted improvements to frametime (dont think it helped) 2025-05-02 17:48:12 +10:00
mcrcortex
b78d389a06 change height 2025-05-02 16:46:04 +10:00
mcrcortex
b064032d8f remove update thingie 2025-05-02 16:45:18 +10:00
mcrcortex
ad0d1ede41 giveup _screams_ 2025-05-02 16:31:23 +10:00
mcrcortex
cc1f63b443 sighN 2025-05-02 16:30:17 +10:00
mcrcortex
da1c382259 sigh3 2025-05-02 16:25:06 +10:00
mcrcortex
50cc7d983c sigh2 2025-05-02 16:23:34 +10:00
mcrcortex
7ad3d8c1c7 sigh 2025-05-02 16:16:01 +10:00
mcrcortex
2829ddb085 b 2025-05-02 16:13:30 +10:00
mcrcortex
d2ec4c359f a 2025-05-02 16:13:05 +10:00
mcrcortex
84a32581b7 Attempted improvements for outline performance 2025-05-02 16:12:47 +10:00
mcrcortex
3fa50a8965 Hyper jank hackfix for loading issues 2025-05-02 00:22:14 +10:00
mcrcortex
0b88a6f824 Added min chunk bounds + fix other bugs in model baking and other places 2025-05-01 23:25:06 +10:00
mcrcortex
fbcef60f67 change things to use the constant 2025-05-01 14:36:28 +10:00
mcrcortex
1d7e985593 dont include musl rocksdb natives 2025-05-01 12:27:52 +10:00
mcrcortex
ba788062c5 remove single use daemon 2025-05-01 12:08:10 +10:00
mcrcortex
8a8cc8c423 Shuffle classes and remove some unused classes 2025-05-01 12:05:48 +10:00
mcrcortex
fce9ca3824 dont add src jar (trying improve build speed) 2025-05-01 12:05:18 +10:00
mcrcortex
d12a0f1ba4 work on chunk outline renderer 2025-05-01 11:20:55 +10:00
mcrcortex
dd46c43c73 Attempt to fix stupid concurrency issue by force stealing jobs 2025-04-30 23:25:16 +10:00
mcrcortex
b526d5a51a Wip fix overdraw 2025-04-30 13:19:08 +10:00
mcrcortex
c209cc82af a 2025-04-29 09:24:11 +10:00
mcrcortex
fc439459cb tweeking 2025-04-28 23:12:25 +10:00
mcrcortex
a2cfe942c6 Heavily reworked the render service priority ordering 2025-04-28 22:54:31 +10:00
mcrcortex
7a88e1f893 Major tinkering with improving frametimes 2025-04-28 21:23:19 +10:00
mcrcortex
d91d41de79 Timing stats and not uploading the entire TLN node list every fking frame 2025-04-28 11:58:37 +10:00
mcrcortex
aed8366206 tweeks 2025-04-27 11:53:39 +10:00
mcrcortex
6d99f45412 Attempt at even more agressive optimizations 2025-04-27 02:15:14 +10:00
mcrcortex
4a3291f4a6 hahaha screams _even more_ 2025-04-27 01:48:37 +10:00
mcrcortex
9cdf1282da hahaha _screams_ 2025-04-27 01:47:47 +10:00
mcrcortex
a5361eaad2 Further try to improve stuttering (it did not go well) 2025-04-27 00:30:47 +10:00
mcrcortex
65480c5e9f Attempt to speedup workflow build 2025-04-26 21:46:52 +10:00
mcrcortex
75685d57d6 work 2025-04-26 21:32:09 +10:00
mcrcortex
b6aa53483d no daemon 2025-04-26 21:32:01 +10:00
mcrcortex
e032ea43e2 error catch 2025-04-26 21:12:02 +10:00
mcrcortex
ec42f47d9e TODO: note 2025-04-26 10:39:50 +10:00
mcrcortex
5f1c89144c AMD crashfix 2025-04-25 22:40:09 +10:00
mcrcortex
71c181fb2c Mem leak fix 2025-04-25 21:39:06 +10:00
mcrcortex
b34792ff45 More tinker 2025-04-25 21:25:24 +10:00
mcrcortex
985e0beafd Try to reduce stutter when loading 2025-04-25 20:55:36 +10:00
mcrcortex
7c27a4d8fd Slightly optimize model factory 2025-04-25 18:30:23 +10:00
mcrcortex
bbe7cd2099 Temporal coherance 2025-04-25 18:30:07 +10:00
mcrcortex
862afc498e Start prep for temporal 2025-04-25 16:37:24 +10:00
mcrcortex
f023f8e8f1 change git workflow 2025-04-25 14:18:41 +10:00
mcrcortex
987427b893 multi gson 2025-04-25 12:15:50 +10:00
mcrcortex
03e79db0ba Tweeks 2025-04-25 00:27:54 +10:00
mcrcortex
f7e8ceaa3f better hiz 2025-04-22 09:27:12 +10:00
mcrcortex
07bb8e0475 More work on fixing remaining hiz issues 2025-04-22 09:20:45 +10:00
mcrcortex
306951ebee Depth patch 2025-04-21 20:18:37 +10:00
mcrcortex
3684174b67 Cap save queue 2025-04-21 16:51:33 +10:00
mcrcortex
c2e95876d0 Change upload stream full from warning to error 2025-04-21 01:56:41 +10:00
mcrcortex
0c7421f36c comment 2025-04-21 01:39:48 +10:00
mcrcortex
d3bf885042 Add proper frustum check 2025-04-21 00:16:41 +10:00
mcrcortex
4bed271258 newline 2025-04-21 00:16:27 +10:00
mcrcortex
bbfc451956 change hiz 2025-04-21 00:16:14 +10:00
mcrcortex
539ac56548 Enable output caching 2025-04-20 21:17:33 +10:00
mcrcortex
b00223a351 Embed xz stream (even if only used for dh importer) 2025-04-20 21:13:52 +10:00
mcrcortex
c81a1d015e Fix more issues 2025-04-20 20:52:26 +10:00
mcrcortex
543a683a5f fixed deprecations in buildscript 2025-04-20 20:37:38 +10:00
mcrcortex
89eddde7d4 Merge remote-tracking branch 'origin/mc_1215' into mc_1215 2025-04-20 19:53:28 +10:00
mcrcortex
67b4aab39b Check that it builds 2025-04-20 19:53:12 +10:00
MCRcortex
68aaa878d3 Merge pull request #103 from Samalando/mc_1215
fix: graldew perms
2025-04-20 19:47:17 +10:00
Samalando
4629a9f96c fix: graldew perms 2025-04-20 10:46:33 +01:00
mcrcortex
e7c123fcf8 action to build on commit (verifies build) 2025-04-20 19:38:02 +10:00
mcrcortex
1456756ea7 Improve build script significantly 2025-04-20 17:55:40 +10:00
mcrcortex
5d01bc8f03 screenspace tweeks 2025-04-20 12:44:42 +10:00
mcrcortex
7d0cbefe17 (sob) 2025-04-19 16:43:06 +10:00
mcrcortex
1794b1ac4b Tweeks to frustum issues with how did things 2025-04-19 16:34:19 +10:00
mcrcortex
62f4116cc8 Ahahaah PAIN 2025-04-19 15:33:04 +10:00
mcrcortex
550a75460f Hate java loading classes order thing 2025-04-19 15:31:59 +10:00
mcrcortex
d226146213 Added or 2025-04-19 15:24:09 +10:00
mcrcortex
b9d4a4116a fix block states inflight not being correct 2025-04-19 14:55:58 +10:00
mcrcortex
becd958c5e Fixed incorrect typing of fluid quads when generating mesh 2025-04-19 11:32:06 +10:00
mcrcortex
d1322990b3 Fix biome dependency check to include embedded fluid state 2025-04-19 11:16:14 +10:00
mcrcortex
325983152a more testing stuff 2025-04-19 11:00:46 +10:00
mcrcortex
d847f7f7b0 Am a complete and utter idiot 2025-04-19 10:59:26 +10:00
mcrcortex
0234c09495 Add check for lzma 2025-04-18 20:54:01 +10:00
mcrcortex
3b2614631d Conditionally add DH import command 2025-04-18 20:51:54 +10:00
mcrcortex
432d078212 Am unsure if fix thing, hate this meshing code 2025-04-18 08:55:30 +10:00
mcrcortex
1ffcaea80f Fix uhoh 2025-04-18 08:24:28 +10:00
mcrcortex
8ef53b3c4f AAA 2025-04-18 08:08:51 +10:00
mcrcortex
b0bd063ed6 Work on node manager 2025-04-16 09:21:42 +10:00
mcrcortex
99cfbb07b9 x fluid geometry emittion 2025-04-15 12:40:32 +10:00
mcrcortex
7e3a73671b Build tweek 2025-04-14 17:45:50 +10:00
mcrcortex
523cb55889 barrier 2025-04-14 14:46:14 +10:00
mcrcortex
1121b5e6dd compile/runtime 2025-04-14 12:16:55 +10:00
mcrcortex
79b7ddda8d sigh 2025-04-14 11:41:40 +10:00
mcrcortex
d6c49ea142 Modmenu not required 2025-04-14 11:15:36 +10:00
mcrcortex
a521e34f49 Beans 2025-04-14 11:02:12 +10:00
mcrcortex
c6aa690eee Random things 2025-04-14 01:28:57 +10:00
mcrcortex
5163cbd26e Fix issues with world importer 2025-04-14 01:18:50 +10:00
mcrcortex
f98ee30491 thingires 2025-04-13 09:40:00 +10:00
mcrcortex
2a128bd69c Config option for enabling/disabling vanilla fog (only applies when the voxy renderer is active) 2025-04-11 09:24:09 +10:00
mcrcortex
198f0f9c31 readd funny frustum thing (fustum is still borked as fk) 2025-04-10 21:00:28 +10:00
mcrcortex
b7713815a4 slightly improve hiz 2025-04-10 20:53:51 +10:00
mcrcortex
47592b4e6d qq 2025-04-09 18:32:58 +10:00
mcrcortex
802cea001c qq 2025-04-09 18:31:48 +10:00
mcrcortex
c5cf08365f thigies 2025-04-08 16:26:01 +10:00
mcrcortex
7af77296ed Tweeks to tracker 2025-04-08 14:26:39 +10:00
mcrcortex
6ccef6d1a5 Note + bound 2025-04-08 13:24:10 +10:00
mcrcortex
4fd84cb811 Added random extra check that is probably wrong 2025-04-08 13:23:57 +10:00
mcrcortex
7cd2ecfcde Greatly accelerated geometry cleaner gpu code 2025-04-08 12:35:02 +10:00
mcrcortex
3569e1d889 fence + getter 2025-04-08 00:43:02 +10:00
mcrcortex
89dfc3c9f2 Nonlinear subdiv slider 2025-04-08 00:27:51 +10:00
mcrcortex
a7cf766c27 remove verify 2025-04-07 20:42:11 +10:00
mcrcortex
63983f6c26 Minor changes 2025-04-07 20:19:39 +10:00
mcrcortex
183746ba4d Replace cloth config with sodium gui 2025-04-07 20:19:29 +10:00
mcrcortex
4d77096589 fix lighting uhoh 2025-04-07 17:13:25 +10:00
mcrcortex
1dc6d2a6bf LRU change 2025-04-07 14:51:50 +10:00
mcrcortex
7027c34941 what the fuck opengl 2025-04-07 14:51:31 +10:00
mcrcortex
2ff6f4cb4e partial yz fluid rendering 2025-04-06 12:41:50 +10:00
mcrcortex
d7d9365a63 add note + check 2025-04-05 18:14:31 +10:00
mcrcortex
7cc92a533d basic translucency 2025-04-05 16:55:47 +10:00
mcrcortex
9e5e5e654d tweeks 2025-04-04 15:09:42 +10:00
mcrcortex
856b5b5b56 A 2025-04-04 13:20:20 +10:00
mcrcortex
3c079397cd Tweeks to iteration 2025-04-02 12:31:27 +10:00
mcrcortex
3f972856ad Partial improvement to selector 2025-04-01 19:40:59 +10:00
mcrcortex
74630504ae Fix more issues 1.21.5 2025-04-01 18:04:37 +10:00
mcrcortex
e67ed69cc9 1.21.5 partial 2025-04-01 16:10:21 +10:00
mcrcortex
9e442ef854 1.21.5 partial 2025-04-01 16:10:10 +10:00
mcrcortex
7f3be8c1ac Merge branch 'hierachial_rewrite_improved_meshing' into mc_1215 2025-04-01 11:54:30 +10:00
mcrcortex
0b86620a4e slight rework on how world unload works, tweek rocksdb, move to WorldUpdater 2025-04-01 11:53:33 +10:00
mcrcortex
b85e6367b4 new save system + rework some things 2025-03-31 16:56:19 +10:00
mcrcortex
40c4101bec Added render distance slider and implementation 2025-03-31 00:57:54 +10:00
mcrcortex
aba9129460 Funny dodgy gpu selector 2025-03-30 11:11:10 +10:00
mcrcortex
9cf7652ae3 Aaa 2025-03-30 11:10:27 +10:00
mcrcortex
9f604dfc2e Changes and removals 2025-03-28 10:05:20 +10:00
mcrcortex
12404348e5 remove #line 1 2025-03-28 09:54:47 +10:00
mcrcortex
e0314de4f6 A 2025-03-28 09:54:32 +10:00
mcrcortex
8b3f8812de Changes 2025-03-28 09:49:12 +10:00
mcrcortex
426ae35a44 start 0.5 2025-03-26 19:39:33 +10:00
mcrcortex
e812b13ef7 Things 2025-03-26 16:03:17 +10:00
mcrcortex
d73f844b10 Reshuffle to methods 2025-03-25 11:43:13 +10:00
mcrcortex
76bad0be68 Revert 2025-03-25 11:42:57 +10:00
mcrcortex
560ca9092c Things 2025-03-25 11:20:56 +10:00
mcrcortex
3702467178 Bound computation 2025-03-25 11:20:42 +10:00
mcrcortex
f6eefdb336 Fix importing very old updated worlds 2025-03-25 01:04:27 +10:00
mcrcortex
33f5772ecd Add global index pallet 2025-03-25 00:16:16 +10:00
mcrcortex
1d7e24537f fix mesa detection 2025-03-24 22:31:14 +10:00
mcrcortex
02b4fee507 sodium update 2025-03-24 22:15:30 +10:00
mcrcortex
b387ab069c Mesa depth stencil bug bypass 2025-03-24 22:15:22 +10:00
mcrcortex
6cadf85b6a Changed config, fixed crashes and issues with world importing 2025-03-24 17:48:17 +10:00
mcrcortex
4a613ff145 Fix crash when changing dimensions 2025-03-24 13:57:52 +10:00
mcrcortex
68a17f0578 Fixes and tweeks for things 2025-03-24 13:49:15 +10:00
mcrcortex
8b7a3c4bb1 Rewired import system 2025-03-24 13:48:14 +10:00
mcrcortex
a992a44f40 Rewired data imports 2025-03-23 23:00:22 +10:00
mcrcortex
5e5c63d109 stuff 2025-03-22 17:54:13 +10:00
mcrcortex
cca9ac0e56 Fix critical issue with meshing 2025-03-22 15:59:37 +10:00
mcrcortex
7a2fe748ea Logging for gpu stream overflow 2025-03-22 15:50:37 +10:00
mcrcortex
2d95cdfd01 Fix fluids overriding the colour parameter 2025-03-22 15:46:18 +10:00
mcrcortex
1c23394415 Disable partial non opaque meshing 2025-03-22 11:34:06 +10:00
mcrcortex
41c31976d6 patch 2025-03-21 22:56:03 +10:00
mcrcortex
eb7172aaba Stuff kinda works 2025-03-21 22:47:16 +10:00
mcrcortex
ac1427d125 a 2025-03-19 16:54:56 +10:00
mcrcortex
755d410430 partial geometry removal imp 2025-03-19 15:02:21 +10:00
mcrcortex
78b2f09e76 Replace WorldSection sync array cache with concurrent implementation 2025-03-18 22:00:46 +10:00
mcrcortex
4858ad586b DH importer v1 2025-03-17 00:19:42 +10:00
mcrcortex
daa3cab313 Remove old stuff 2025-03-16 17:03:19 +10:00
mcrcortex
ba30742630 Change error to warning 2025-03-16 16:55:02 +10:00
mcrcortex
ef389b7ed0 Am such a bloody clown 2025-03-16 16:46:26 +10:00
mcrcortex
104bdf070c Config and fixes for world managing 2025-03-16 16:10:57 +10:00
mcrcortex
aea98cb6e4 Handle empty child list in traversal shader 2025-03-16 14:51:09 +10:00
mcrcortex
0dd1eb443f Testing system for NodeManager as well as completion of basic functionality of NodeManager 2025-03-16 14:46:11 +10:00
mcrcortex
77e3b95d6b stuff2 2025-03-15 11:17:35 +10:00
mcrcortex
4368f5bc4d stuff 2025-03-15 11:17:26 +10:00
mcrcortex
699ccb90b2 aa 2025-03-14 11:19:43 +10:00
mcrcortex
b6a9adce28 Improve world importer 2025-03-14 10:27:46 +10:00
mcrcortex
7b11670d55 More work 2025-03-13 11:26:13 +10:00
mcrcortex
c6fd3b19f9 Shuffling 2025-03-11 08:09:52 +10:00
mcrcortex
aae089404d more da more thing 2025-03-10 12:40:36 +10:00
mcrcortex
fd2ee3f71f More things 2025-03-09 12:19:45 +10:00
mcrcortex
60b878d53c More error logging 2025-03-09 12:05:08 +10:00
mcrcortex
fd4d1d7910 More stuff 2025-03-09 11:53:46 +10:00
mcrcortex
c0a578cdd5 aaa 2025-03-06 14:37:23 +10:00
mcrcortex
ff82c2d761 Tinker 2025-03-04 12:14:28 +10:00
mcrcortex
743c423ba5 Random things 2025-03-04 12:05:30 +10:00
mcrcortex
fc55d5fc3b More work on manager 2025-03-03 20:25:07 +10:00
mcrcortex
a8971166b4 Method to allow mod to run without mc 2025-03-03 10:45:15 +10:00
mcrcortex
ce5f792500 Convert ingest to be per section 2025-03-03 10:44:45 +10:00
mcrcortex
a6ed760304 Tinkering 2025-03-03 10:44:22 +10:00
mcrcortex
d475b114ee remove print 2025-02-24 12:00:31 +10:00
mcrcortex
6e05679b3e use exact screenspace area of AABB instead of screenspace bounding box of AABB for computing decending 2025-02-24 11:57:39 +10:00
mcrcortex
241897dbd9 woops 2025-02-07 11:18:06 +10:00
mcrcortex
f37c5353d0 Fix import shutdown 2025-02-06 06:48:03 +10:00
mcrcortex
5d187589b4 Fix crash on server 2025-02-05 18:03:50 +10:00
mcrcortex
6d683503cd Improve ingest and import speed 2025-02-05 17:08:44 +10:00
mcrcortex
d77addb416 Command improvements 2025-02-05 15:15:29 +10:00
mcrcortex
49aa9c2dd9 Add support for importing directly from zip files 2025-02-05 14:05:44 +10:00
mcrcortex
b849686564 "Remove" lzma 2025-02-05 12:38:48 +10:00
mcrcortex
57a8661b67 Remove old stuff 2025-02-05 11:19:35 +10:00
mcrcortex
e336cee557 Fix likly corruption from multiple dimensions accessing things incorrectly 2025-02-05 11:16:14 +10:00
mcrcortex
cf0afbb545 Refactor storage to include a section backend 2025-02-05 11:15:44 +10:00
mcrcortex
db43e26b8a Wip stuff 2025-02-05 09:39:55 +10:00
mcrcortex
9bd84ae3e8 Am going to scream 2025-02-04 21:34:06 +10:00
mcrcortex
613c4c744a Change default config value 2025-02-02 17:53:43 +10:00
mcrcortex
4ff28bc323 Save config if not exist 2025-02-02 01:40:27 +10:00
mcrcortex
8f1d89cb95 debug util 2025-02-02 01:26:47 +10:00
mcrcortex
34666c7994 nonOpaqueMask 2025-02-02 01:06:59 +10:00
mcrcortex
ac135d1907 More logging system use 2025-02-01 05:39:01 +10:00
mcrcortex
f29f626a2f Use logging system 2025-02-01 05:38:00 +10:00
mcrcortex
bc72eaeb0f Eliminated file seeking and removed bytebuffers instead using raw memory under the assumption the system is always little endian 2025-02-01 05:37:10 +10:00
mcrcortex
d373b806ed Replace spinlock with a wait loop, freeing up a cpu thread during import 2025-01-31 18:39:05 +10:00
mcrcortex
64067d9ba5 Change default zstd compression level to 3 2025-01-31 04:37:27 +10:00
mcrcortex
4d20f4747a "Remove" default serialization config from config 2025-01-31 04:37:09 +10:00
mcrcortex
72ef321bfb Compile only lzma (its terrible so not included) 2025-01-31 04:29:58 +10:00
mcrcortex
ec67222855 Add LZ4 compressor
- provides significant performance improvement for saving (37% in testing)
- at the cost of double the storage size (in testing)
2025-01-31 01:50:28 +10:00
mcrcortex
a0132b8c67 configuration for cache size 2025-01-30 21:25:43 +10:00
mcrcortex
71349d7404 Add fps based adaptive quality 2025-01-30 21:24:56 +10:00
mcrcortex
28af11ca2e Start decomposing minecraft getters for terrain ingesting 2025-01-30 07:14:25 +10:00
mcrcortex
9e20eac7ab improved lru cache and improved world import speed slightly 2025-01-30 06:55:39 +10:00
mcrcortex
772ec27ea1 Secondary caching system implemented with ~1gb of heap allocated to it 2025-01-30 05:15:55 +10:00
mcrcortex
5421452429 Cache 2025-01-30 00:06:21 +10:00
mcrcortex
900e72d1fc Things 2025-01-29 23:24:04 +10:00
mcrcortex
b6289a901c More crying 2025-01-28 06:18:06 +10:00
mcrcortex
4d6859db43 node clean something, its still broken mode 2025-01-28 06:03:06 +10:00
mcrcortex
aac3db90d3 Fix MixinThreadExecutor 2025-01-28 04:11:39 +10:00
mcrcortex
1048d38b4d Work on geometry cleaning 2025-01-28 03:14:14 +10:00
mcrcortex
9ded4d4b13 Sorting works 2025-01-27 07:28:08 +10:00
mcrcortex
d0e969dcf6 Remove unused import 2025-01-26 20:00:51 +10:00
mcrcortex
783f87f22d More force crash on failed initial load packet 2025-01-26 20:00:09 +10:00
mcrcortex
402d682d6e Revert render service 2025-01-25 04:15:06 +10:00
mcrcortex
d6500d2227 Continued work on geometry removal 2025-01-25 04:14:00 +10:00
mcrcortex
d0b5f32e2d Update the child mask after removing nodes 2025-01-25 02:19:15 +10:00
mcrcortex
7454179313 Add auto fragmenting storage option 2025-01-22 23:37:28 +10:00
mcrcortex
194cf672ee lower value + extra debug 2025-01-22 08:07:47 +10:00
mcrcortex
5a3ef0fe69 Performance benching 2025-01-22 05:49:17 +10:00
mcrcortex
7f62b76691 Woops 2025-01-21 21:51:44 +10:00
mcrcortex
75ad35e057 Add scratch buffer for position.
Fixes lods "disappearing" when you zoom in
2025-01-21 21:50:44 +10:00
mcrcortex
44c66d5c26 Wip 2025-01-21 05:35:22 +10:00
mcrcortex
3dcfd196a1 More logging 2025-01-15 03:40:14 +10:00
mcrcortex
b7d8f82a3d Added subpixel range 2025-01-15 03:00:55 +10:00
mcrcortex
dbf1188b12 Added opacity based mipping 2025-01-15 01:24:37 +10:00
mcrcortex
6539e69fb0 added devauth
Added some extra checks
Made RD 1024
2025-01-14 19:27:48 +10:00
mcrcortex
07b00bc710 Wip on frame tracking,
add import statistics
2025-01-13 06:42:28 +10:00
mcrcortex
d090703d32 Sigh build script 2025-01-13 00:27:22 +10:00
mcrcortex
b08ba23889 Force crash on join game packet failure 2025-01-11 20:37:42 +10:00
mcrcortex
b8fda63ef3 Update to 1.21.4 2025-01-11 20:04:37 +10:00
mcrcortex
316f42cf03 Change loading time 2025-01-05 08:40:20 +10:00
mcrcortex
7b9c31b770 git commit time Closure 2025-01-05 08:35:55 +10:00
mcrcortex
67347048b5 Who knows if this works 2025-01-05 08:31:34 +10:00
mcrcortex
73cc91fa0d Tweek build thing temporarily 2025-01-05 08:29:21 +10:00
mcrcortex
a1ed14e8cb Dont know if this is a nice place to put it 2025-01-05 02:17:26 +10:00
mcrcortex
8321d3feee Imports 2025-01-05 02:17:04 +10:00
mcrcortex
46283fec0e Ref D 2025-01-05 02:14:33 +10:00
mcrcortex
65244bc43d Ref C 2025-01-05 02:13:43 +10:00
mcrcortex
ed0bd1a2b5 Ref B 2025-01-05 02:13:11 +10:00
mcrcortex
7def1b487c Ref A 2025-01-05 02:09:38 +10:00
mcrcortex
2f7f7f4f6b Exclude unused shared objects/jni, reducing the jar final size by ~4.5x
Thanks earth for helping with hell
2025-01-04 00:22:22 +10:00
mcrcortex
1a3ad7d701 Support other depth types 2024-12-25 11:59:00 +10:00
mcrcortex
5242104d87 Other stuff 2024-12-24 12:57:17 +10:00
mcrcortex
a369d61024 Sort region files 2024-12-24 12:57:02 +10:00
mcrcortex
df2611f0b7 Compute utility 2024-12-24 12:56:30 +10:00
mcrcortex
663a352586 Fix error in resizing 2024-12-24 12:56:05 +10:00
mcrcortex
412768c57e Wip on node removal support on child existence change 2024-12-23 09:08:25 +10:00
mcrcortex
5bb91bb1eb Wip on many things
Added partial inner node child update support
2024-12-22 18:10:28 +10:00
mcrcortex
d9d433f47c Things 2024-12-16 14:10:58 +10:00
mcrcortex
c3701ad903 gl debug utils 2024-12-15 12:59:34 +10:00
mcrcortex
6ed4c92c94 Reduced and reuse memory section, remove copies during section fetch pipeline, remove stack gathering and string forming from IdNotYetComputedException wip on cleaner 2024-12-14 16:23:46 +10:00
mcrcortex
fdeed5c257 Wip, shader tweeks, manager tweeks, wip on cleaner 2024-12-14 13:42:14 +10:00
mcrcortex
aa1ba08f70 More debug 2024-12-13 23:03:32 +10:00
mcrcortex
a62413ed0b Thanks gore for the help, add extra frustum check 2024-12-13 15:57:51 +10:00
mcrcortex
2057ced33d More 2024-12-13 15:14:42 +10:00
mcrcortex
f8d9cb8855 Fixing hiz Slowly 2024-12-13 15:03:17 +10:00
mcrcortex
09b1029030 Hackfix meshing to remove gaps 2024-12-13 14:36:59 +10:00
mcrcortex
2c778ce5ec Remove todo 2024-12-13 12:50:15 +10:00
mcrcortex
029a7df71a Added taskbar progress on windows when importing worlds
Fixed memory leak in RenderDataFactory4
Moved shader debug options
Inital work on visibility sorting and node cleanup
Added optional cleanup system to services.
Track (and added to f3) GlBuffer and MemoryBuffer count and sizes.

More work on finishing node manager (its still broken when importing for some reason, requiring reload)
2024-12-13 12:47:59 +10:00
mcrcortex
8a3d7a9da5 Forgot a single bitshift, breaking many things 2024-12-07 00:57:37 +10:00
mcrcortex
69f0c63831 Dont reload when not changing reloadables 2024-12-05 22:50:19 +10:00
mcrcortex
523fd89f97 Added option to change subdivision size 2024-12-05 22:38:31 +10:00
mcrcortex
d9fc4b3c05 Added extra state checking and fixed empty geometry not returning child existence bits, added lighting back 2024-12-05 19:39:18 +10:00
mcrcortex
8c36021746 Added compression and decompression contexts 2024-12-05 12:15:05 +10:00
mcrcortex
1b82b3020f Fix broken memcpy 2024-12-05 11:36:58 +10:00
mcrcortex
eeeecb982c Attempted to move to native buffer directly 2024-12-05 10:40:49 +10:00
mcrcortex
70a1fe36c7 Merged zy into single method 2024-12-05 10:19:43 +10:00
mcrcortex
622c40f188 Change err.println to Logging.error 2024-12-05 00:28:35 +10:00
mcrcortex
3b25d2d535 Fix postprocessing being weird 2024-12-05 00:28:06 +10:00
mcrcortex
017bf78d4c removed unused config options 2024-12-04 21:47:42 +10:00
mcrcortex
775bf46b8e Fix issue when overflow happens 2024-12-03 23:34:16 +10:00
mcrcortex
9c05fdc11e Increase performance when sections to be rendered are wait on model bakery 2024-12-03 23:33:55 +10:00
mcrcortex
58ae91afbe Change some settings 2024-12-03 22:39:02 +10:00
mcrcortex
acb7f3aa17 Greatly accelerated bitset consecutive allocation 2024-12-03 22:24:02 +10:00
mcrcortex
3c5d79ce77 Made x-axis processing _much much_ faster 2024-12-03 21:23:06 +10:00
mcrcortex
812342b4da A 2024-12-03 21:22:39 +10:00
mcrcortex
c6fe0a1bed Much changes, wip 2024-12-03 17:35:08 +10:00
mcrcortex
00a123b150 Tweeked mesher 2024-12-02 22:56:06 +10:00
mcrcortex
56d96ddd84 Heavily reduced allocation rates 2024-12-02 22:55:52 +10:00
mcrcortex
5a6819757b Very fast scanline mesher 2024-12-02 20:26:27 +10:00
mcrcortex
3618c136f7 New mesher 2024-12-02 18:27:18 +10:00
mcrcortex
4312472534 fixed/greatly improved hiz culling maybe 2024-12-01 23:35:25 +10:00
mcrcortex
bdf3319204 Perf tester 2024-11-28 18:12:48 +10:00
mcrcortex
53672d3ace Stuff 2024-11-28 17:36:53 +10:00
mcrcortex
fac00a81d3 Manually expanded shader 2024-11-17 22:40:01 +10:00
mcrcortex
c0be96e797 fix build script if not git cloned 2024-11-17 22:06:18 +10:00
mcrcortex
5979b17891 Fix lighting 2024-11-17 21:18:05 +10:00
mcrcortex
5e5d0c8051 partial 1.21.3 update 2024-11-17 21:09:31 +10:00
mcrcortex
0a7c0c573c blub 2024-11-17 19:26:11 +10:00
mcrcortex
a15978d1fb Funny very incorrect tinkering of culling 2024-11-01 17:28:30 +10:00
mcrcortex
7fa62375c6 QQQQ 2024-10-24 23:38:29 +10:00
mcrcortex
182e66daff cc 2024-09-30 13:18:33 +10:00
mcrcortex
640da6c1be oops 2024-09-29 10:44:50 +10:00
mcrcortex
3d2129c1d4 BB 2024-09-29 10:44:16 +10:00
mcrcortex
5e72b945c4 debug pain 2024-09-24 13:40:15 +10:00
mcrcortex
3a800ec9ca Added build task removal 2024-09-24 11:05:58 +10:00
mcrcortex
10391c48c7 AaAaA 2024-09-24 10:40:51 +10:00
mcrcortex
00cf0c73e3 Fixed other issues? 2024-09-24 00:19:25 +10:00
mcrcortex
f53a81099e Fixed culling? 2024-09-24 00:11:10 +10:00
mcrcortex
f9a8f9b1c2 Tweeks 2024-09-23 22:48:47 +10:00
mcrcortex
27ed49fcd9 BBBB 2024-09-23 10:03:09 +10:00
mcrcortex
ad7660e4d6 More and more working? 2024-09-23 03:02:18 +10:00
mcrcortex
7eaa8483b8 aaa 2024-09-23 01:58:33 +10:00
mcrcortex
87238bdb45 It works EVEN BETTER 2024-09-23 01:10:43 +10:00
mcrcortex
aafc475843 IT WORKS!!!!!! 2024-09-22 23:57:08 +10:00
mcrcortex
78d5c224a8 More node 2024-09-22 11:09:25 +10:00
mcrcortex
b875d5fc25 Prep v2 2024-09-19 00:28:36 +10:00
mcrcortex
df5c34c626 Even more pain, but gpu side should fully work ™️ 2024-09-17 01:00:03 +10:00
mcrcortex
0aeb0fbf21 implemented gpu traversal i think? 2024-09-16 15:46:09 +10:00
mcrcortex
d5045731ad Prepare 2024-09-16 15:08:48 +10:00
mcrcortex
1c0e0cc666 Added memory verification option 2024-09-15 19:54:00 +10:00
mcrcortex
b9a3d18b56 Incremental traversal system works 2024-09-15 12:10:32 +10:00
mcrcortex
76aaf3824d AA 2024-09-11 22:49:00 +10:00
mcrcortex
756431b581 World and work stuff 2024-09-02 08:10:13 +10:00
mcrcortex
c0cc236c40 Per world voxel engine 2024-08-23 00:02:11 +10:00
mcrcortex
1a42f115fb More work 2024-08-22 09:19:29 +10:00
mcrcortex
fbdd65bc00 Rework to SectionRouter 2024-08-20 10:36:18 +10:00
mcrcortex
306956839a Continue work 2024-08-16 16:22:22 +10:00
mcrcortex
d4714989b4 More work on node manager 2024-08-13 09:18:56 +10:00
mcrcortex
1d1e244f03 Working on leaf 2024-08-12 08:28:34 +10:00
mcrcortex
f055234463 More work on update types + hacky add override to dummy render everything as true 2024-08-09 22:15:20 +10:00
mcrcortex
6450069d8a Move to SectionUpdate as a data wrapper 2024-08-09 00:09:32 +10:00
mcrcortex
c8bcfc3b6d Fixed counting issue 2024-08-08 20:14:06 +10:00
mcrcortex
3ba1c76414 Only import full chunk sections 2024-08-08 19:54:22 +10:00
mcrcortex
7f25f5ff5d fixed uhoh 2024-08-08 19:46:39 +10:00
mcrcortex
15391a6f91 child emptiness tracking 2024-08-08 19:37:38 +10:00
mcrcortex
b81fd46929 Another flag 2024-08-08 02:05:23 +10:00
mcrcortex
fce77cbd5f add launch flag to disable verifying world section access is correct 2024-08-08 01:44:09 +10:00
mcrcortex
163d1f9f43 Move thread package 2024-08-08 00:41:33 +10:00
mcrcortex
d2483283f5 Attempt to improve lighting 2024-08-08 00:40:17 +10:00
mcrcortex
f536b0cd74 cache 2024-08-07 23:10:42 +10:00
mcrcortex
445c583a5b Config for threading 2024-08-07 14:11:44 +10:00
mcrcortex
a5122d31a4 Attempted more fixes for ServiceThreadPool race conditions when removing services 2024-08-07 12:42:36 +10:00
mcrcortex
0135c63b88 Attempted last resort fix for thread pool 2024-08-07 02:18:06 +10:00
mcrcortex
395112fc40 A 2024-08-07 02:01:22 +10:00
mcrcortex
323cd95bff Swap to chunk count estimation 2024-08-07 01:54:21 +10:00
mcrcortex
4bb05252aa woops 2024-08-07 01:50:27 +10:00
mcrcortex
8797ee56a8 World importer uses service system 2024-08-07 01:47:22 +10:00
mcrcortex
6eed71656d Optimizations aswell as the option to disable section verification 2024-08-07 00:06:02 +10:00
mcrcortex
ee5bca7db5 Swap storage backend type from ByteBuffer to MemoryBuffer 2024-08-06 23:41:31 +10:00
mcrcortex
082143ed15 Improve object tracking configurations 2024-08-06 23:15:57 +10:00
mcrcortex
377c51f365 Tweeks 2024-08-06 22:57:15 +10:00
mcrcortex
cd3b40399c Fix service pools 2024-08-06 22:09:37 +10:00
mcrcortex
10ff5dda7d Merge branch 'master' into hierachial_rewrite
# Conflicts:
#	src/main/java/me/cortex/voxy/client/config/VoxyConfigScreenFactory.java
#	src/main/java/me/cortex/voxy/client/core/VoxelCore.java
#	src/main/java/me/cortex/voxy/client/mixin/chunky/MixinFabricWorld.java
2024-08-06 21:52:50 +10:00
mcrcortex
403317fd29 Swap to a single ServiceThreadPool workload 2024-08-06 21:50:05 +10:00
mcrcortex
351fac9052 Work on nodes 2024-08-06 12:49:28 +10:00
mcrcortex
2d6d948e80 Update forwarding and checking 2024-08-05 22:10:21 +10:00
mcrcortex
593f222760 Fix biome 2024-08-05 16:42:12 +10:00
mcrcortex
0fdba1bf64 Remove old render stuff 2024-08-04 22:10:14 +10:00
mcrcortex
e4fe056586 Render basics 2024-08-04 22:08:49 +10:00
mcrcortex
0bb04b2f8e Very Close 2024-08-04 19:17:56 +10:00
mcrcortex
264e26cb90 aa 2024-08-04 12:56:06 +10:00
mcrcortex
0366c42cf3 _basic_ Rendering 2024-08-04 12:52:33 +10:00
mcrcortex
7d883913bd dont leak memory 2024-08-03 14:42:55 +10:00
mcrcortex
57ad9f56be Premark area for gen 2024-08-03 14:41:25 +10:00
mcrcortex
082885a5eb Remove nvmesh and gl46 meshlets for the time being 2024-08-03 13:27:23 +10:00
mcrcortex
945cd68672 Work on basic memory manager for section rendering 2024-08-03 13:26:08 +10:00
mcrcortex
3194f1d23e Added global shader debug printf util 2024-08-03 00:03:30 +10:00
mcrcortex
154f016a96 More work on setting up render system 2024-08-02 23:38:30 +10:00
mcrcortex
0362399ba7 Set the default clear value to 0 2024-08-02 22:02:33 +10:00
mcrcortex
7dc8bbcd9a AA 2024-08-02 14:09:32 +10:00
mcrcortex
5225bca007 More work 2024-08-02 12:20:04 +10:00
mcrcortex
9b08c6e5ff Fixes for tasks never finishing 2024-08-01 15:51:20 +10:00
mcrcortex
7f5b84e6e7 Remove debug blit 2024-08-01 13:30:22 +10:00
mcrcortex
2f07b8420b Fk enhanced forloop bullshit 2024-08-01 13:27:05 +10:00
mcrcortex
6fdcde856b Compute shader buffer transfer converter 2024-08-01 12:44:09 +10:00
mcrcortex
004bd1e751 "works" 2024-07-31 18:58:13 +10:00
mcrcortex
aa65197749 Prep for async texture fetch 2024-07-31 09:54:51 +10:00
mcrcortex
7703e2fb0f Aa 2024-07-30 15:08:49 +10:00
mcrcortex
433f3ace9f Fix bakery ™️ 2024-07-30 10:42:33 +10:00
mcrcortex
87f7d71c94 Use onthread bakery until all the issues related to offthread baking can be resolved (related to tile entity baking) 2024-07-30 09:14:36 +10:00
mcrcortex
ca899dd506 Uhhhhh refactor??? ig??? 2024-07-29 09:09:57 +10:00
mcrcortex
25712a850e Bebap 2024-07-21 21:00:26 +10:00
mcrcortex
9329a7885d CCC 2024-07-20 20:27:22 +10:00
mcrcortex
c04bf8b351 Automaticly ingest chunks created by chunky 2024-07-20 20:27:17 +10:00
mcrcortex
d5cf684215 Beans3 2024-07-18 19:59:23 +10:00
mcrcortex
4f02eca7c9 Beans2 2024-07-18 12:33:08 +10:00
mcrcortex
d7b555fca6 Beans 2024-07-18 11:22:57 +10:00
mcrcortex
9d4f912808 AA 2024-07-18 10:52:43 +10:00
mcrcortex
26825e358f Fixes and tweeks 2024-07-16 20:47:49 +10:00
mcrcortex
5d58e9e4da ITS WORKING EVEN MORE 2024-07-16 18:02:52 +10:00
mcrcortex
72e35557a4 it works! 2024-07-16 00:14:53 +10:00
mcrcortex
0ff2db1881 Working even better 2024-07-15 20:40:13 +10:00
mcrcortex
4cbd7af3d0 Working better 2024-07-15 19:34:22 +10:00
mcrcortex
5b5939855f World tinkering 2024-07-15 12:24:50 +10:00
mcrcortex
91e93dea2b Performance tinkering 2024-07-15 10:41:04 +10:00
mcrcortex
37b85057c3 Restructure B 2024-07-13 13:19:47 +10:00
mcrcortex
3cec8b281c Restructure A 2024-07-13 10:47:11 +10:00
mcrcortex
86f1770af3 much wow 2024-07-12 10:47:46 +10:00
mcrcortex
707c0b1c85 Created PrintfInjector to use printf inside shaders 2024-07-11 22:34:59 +10:00
mcrcortex
6ba56a133e Small change 2024-07-11 19:55:19 +10:00
mcrcortex
1e855a0ed0 Shader compiles 2024-07-11 16:50:32 +10:00
mcrcortex
716bf097bf Alot of gpu work 2024-07-10 23:02:00 +10:00
mcrcortex
8ed121f71e Slight reorg 2024-07-10 00:16:32 +09:00
mcrcortex
e5333cb2e9 Merge branch 'master' into meshlet_culling 2024-07-10 00:04:13 +09:00
mcrcortex
a7c6768449 Much work much wow 2024-07-10 00:03:59 +09:00
mcrcortex
e1c7bb18a9 Fix format hopefully 2024-07-02 22:35:28 +09:00
mcrcortex
12b2dce779 Node 2024-07-02 22:35:14 +09:00
289 changed files with 21011 additions and 8640 deletions

View File

@@ -0,0 +1,27 @@
name: check-does-build-pr
on: [ pull_request ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v4
- name: Verify wrapper
uses: gradle/actions/wrapper-validation@v3
- 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

27
.github/workflows/check-does-build.yml vendored Normal file
View File

@@ -0,0 +1,27 @@
name: check-does-build
on: [ push ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v4
- name: Verify wrapper
uses: gradle/actions/wrapper-validation@v3
- 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
- name: Gradle build
run: ./gradlew build

3
.gitignore vendored
View File

@@ -1,3 +1,6 @@
# Project exclude paths
/.gradle/
/.idea/
/build/
/run/
/out/

View File

@@ -1,11 +1,17 @@
plugins {
id 'fabric-loom' version "1.7.1"
id 'fabric-loom' version "1.10.1"
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,38 @@ 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/" }
}
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 +63,59 @@ 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.5-0.6.13-fabric"
modCompileOnly "maven.modrinth:sodium:mc1.21.5-0.6.13-fabric"
modImplementation("maven.modrinth:lithium:mc1.21.5-0.16.0-fabric")
//modRuntimeOnly "maven.modrinth:nvidium:0.2.6-beta"
modCompileOnly "maven.modrinth:nvidium:0.2.8-beta"
//modCompileOnly "maven.modrinth:nvidium:0.2.8-beta"
modCompileOnly("maven.modrinth:modmenu:14.0.0-rc.2")
modRuntimeOnlyMsk("maven.modrinth:modmenu:14.0.0-rc.2")
modCompileOnly("maven.modrinth:iris:1.8.11+1.21.5-fabric")
modRuntimeOnlyMsk("maven.modrinth:iris:1.8.11+1.21.5-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.36-fabric")
modRuntimeOnlyMsk("maven.modrinth:chunky:1.4.36-fabric")
modRuntimeOnlyMsk("maven.modrinth:spark:1.10.121-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")
}
@@ -91,16 +136,63 @@ 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 {
delete getDestinationDirectory().get()
def hash = gitCommitHash();
if (!hash.equals("<UnknownCommit>")) {
archiveClassifier.set(hash);
}
}
@@ -111,6 +203,7 @@ repositories {
mavenCentral()
}
dependencies {
implementation platform("org.lwjgl:lwjgl-bom:$lwjglVersion")
@@ -124,9 +217,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:8.10.0'))
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'
}
}
}

View File

@@ -1,15 +1,19 @@
# Done to increase the memory available to gradle.
org.gradle.jvmargs=-Xmx1G
org.gradle.jvmargs=-Xmx2G
org.gradle.caching=true
org.gradle.parallel=true
# 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.5
yarn_mappings=1.21.5+build.1
loader_version=0.16.10
# Fabric API
fabric_version=0.119.5+1.21.5
# Mod Properties
mod_version = 0.1.6-alpha
mod_version = 0.2.0-alpha
maven_group = me.cortex
archives_base_name = voxy
fabric_version=0.100.1+1.21

View File

@@ -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.12-bin.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

0
gradlew vendored Normal file → Executable file
View File

View File

@@ -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;
}
}

View File

@@ -0,0 +1,61 @@
package me.cortex.voxy.client;
import me.cortex.voxy.client.taskbar.Taskbar;
import me.cortex.voxy.common.Logger;
import me.cortex.voxy.common.thread.ServiceThreadPool;
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;
import java.util.function.BooleanSupplier;
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);
}
}

View 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);
}
}

View File

@@ -0,0 +1,7 @@
package me.cortex.voxy.client;
public class LoadException extends RuntimeException {
public LoadException(String msg, Throwable cause) {
super(msg, cause);
}
}

View File

@@ -0,0 +1,12 @@
package me.cortex.voxy.client;
import me.cortex.voxy.common.world.WorldEngine;
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];
}

View File

@@ -0,0 +1,94 @@
package me.cortex.voxy.client;
import java.lang.invoke.VarHandle;
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;
VarHandle.fullFence();
this.timestamp = System.nanoTime();
VarHandle.fullFence();
}
public void stop() {
if (!this.running) {
throw new IllegalStateException();
}
this.running = false;
VarHandle.fullFence();
this.runtime += System.nanoTime() - this.timestamp;
VarHandle.fullFence();
}
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();
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,48 @@
package me.cortex.voxy.client;
import me.cortex.voxy.client.core.gl.Capabilities;
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.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
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<>();
@Override
public void onInitializeClient() {
ClientLifecycleEvents.CLIENT_STARTED.register(client->{
boolean systemSupported = Capabilities.INSTANCE.compute && Capabilities.INSTANCE.indirectParameters;
if (systemSupported) {
VoxyCommon.setInstanceFactory(VoxyClientInstance::new);
} else {
Logger.error("Voxy is unsupported on your system.");
}
});
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();
}
}

View File

@@ -0,0 +1,247 @@
package me.cortex.voxy.client;
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.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class VoxyClientInstance extends VoxyInstance {
public static boolean isInGame = false;
private final SectionStorageConfig storageConfig;
private final Path basePath = getBasePath();
public VoxyClientInstance() {
super(VoxyConfig.CONFIG.serviceThreads);
this.storageConfig = getCreateStorageConfig(this.basePath);
}
@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, getWorldId(identifier));
ctx.pushPath(ConfigBuildCtx.DEFAULT_STORAGE_PATH);
return this.storageConfig.build(ctx);
}
private static String bytesToHex(byte[] hash) {
StringBuilder hexString = new StringBuilder(2 * hash.length);
for (byte b : hash) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString();
}
private static String getWorldId(WorldIdentifier identifier) {
String data = identifier.biomeSeed + identifier.key.toString();
try {
return bytesToHex(MessageDigest.getInstance("SHA-256").digest(data.getBytes())).substring(0, 32);
} catch (
NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
private static SectionStorageConfig getCreateStorageConfig(Path path) {
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) {
throw new IllegalStateException("Config deserialization null, reverting to default");
}
if (config.sectionStorageConfig == null) {
throw new IllegalStateException("Config section storage null, reverting to default");
}
} 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);
}
}
try {
config = DEFAULT_STORAGE_CONFIG;
try {
Files.writeString(json, Serialization.GSON.toJson(config));
} catch (IOException e) {
throw new RuntimeException(e);
}
} catch (Exception e) {
throw new RuntimeException("Failed to deserialize the default config, aborting!", e);
}
if (config == null) {
throw new IllegalStateException("Config is still null\n");
}
return config.sectionStorageConfig;
}
private static class Config {
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();
}
/*
private static void testDbPerformance(WorldEngine engine) {
Random r = new Random(123456);
r.nextLong();
long start = System.currentTimeMillis();
int c = 0;
long tA = 0;
long tR = 0;
for (int i = 0; i < 1_000_000; i++) {
if (i == 20_000) {
c = 0;
start = System.currentTimeMillis();
}
c++;
int x = (r.nextInt(256*2+2)-256);//-32
int z = (r.nextInt(256*2+2)-256);//-32
int y = r.nextInt(2)-1;
int lvl = 0;//r.nextInt(5);
long t = System.nanoTime();
var sec = engine.acquire(WorldEngine.getWorldSectionId(lvl, x>>lvl, y>>lvl, z>>lvl));
tA += System.nanoTime()-t;
t = System.nanoTime();
sec.release();
tR += System.nanoTime()-t;
}
long delta = System.currentTimeMillis() - start;
System.out.println("Total "+delta+"ms " + ((double)delta/c) + "ms average tA: " + tA + " tR: " + tR);
}
private static void testDbPerformance2(WorldEngine engine) {
Random r = new Random(123456);
r.nextLong();
ConcurrentLinkedDeque<Long> queue = new ConcurrentLinkedDeque<>();
var ser = engine.instanceIn.getThreadPool().createServiceNoCleanup("aa", 1, ()-> () ->{
var sec = engine.acquire(queue.poll());
sec.release();
});
int priming = 1_000_000;
for (int i = 0; i < 2_000_000+priming; i++) {
int x = (r.nextInt(256*2+2)-256)>>2;//-32
int z = (r.nextInt(256*2+2)-256)>>2;//-32
int y = r.nextInt(2)-1;
int lvl = 0;//r.nextInt(5);
queue.add(WorldEngine.getWorldSectionId(lvl, x>>lvl, y>>lvl, z>>lvl));
}
for (int i = 0; i < priming; i++) {
ser.execute();
}
ser.blockTillEmpty();
int c = queue.size();
long start = System.currentTimeMillis();
for (int i = 0; i < c; i++) {
ser.execute();
}
ser.blockTillEmpty();
long delta = System.currentTimeMillis() - start;
ser.shutdown();
System.out.println("Total "+delta+"ms " + ((double)delta/c) + "ms average total, avg wrt threads: " + (((double)delta/c)*engine.instanceIn.getThreadPool().getThreadCount()) + "ms");
}
private void verifyTopNodeChildren(int X, int Y, int Z) {
var world = this.getOrMakeRenderWorld(MinecraftClient.getInstance().world);
for (int lvl = 0; lvl < 5; lvl++) {
for (int y = (Y<<5)>>lvl; y < ((Y+1)<<5)>>lvl; y++) {
for (int x = (X<<5)>>lvl; x < ((X+1)<<5)>>lvl; x++) {
for (int z = (Z<<5)>>lvl; z < ((Z+1)<<5)>>lvl; z++) {
if (lvl == 0) {
var own = world.acquire(lvl, x, y, z);
if ((own.getNonEmptyChildren() != 0) ^ (own.getNonEmptyBlockCount() != 0)) {
Logger.error("Lvl 0 node not marked correctly " + WorldEngine.pprintPos(own.key));
}
own.release();
} else {
byte msk = 0;
for (int child = 0; child < 8; child++) {
var section = world.acquire(lvl-1, (child&1)+(x<<1), ((child>>2)&1)+(y<<1), ((child>>1)&1)+(z<<1));
msk |= (byte) (section.getNonEmptyBlockCount()!=0?(1<<child):0);
section.release();
}
var own = world.acquire(lvl, x, y, z);
if (own.getNonEmptyChildren() != msk) {
Logger.error("Section empty child mask not correct " + WorldEngine.pprintPos(own.key) + " got: " + String.format("%8s", Integer.toBinaryString(Byte.toUnsignedInt(own.getNonEmptyChildren()))).replace(' ', '0') + " expected: " + String.format("%8s", Integer.toBinaryString(Byte.toUnsignedInt(msk))).replace(' ', '0'));
}
own.release();
}
}
}
}
}
}
*/
}

View File

@@ -0,0 +1,227 @@
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.common.world.WorldEngine;
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 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) {
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) {
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().player.clientWorld);
if (engine==null)return 1;
return instance.getImportManager().makeAndRunIfNone(engine, ()->
new DHImporter(dbFile_, engine, MinecraftClient.getInstance().player.clientWorld, 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().player.clientWorld);
if (engine==null) return false;
return instance.getImportManager().makeAndRunIfNone(engine, ()->{
var importer = new WorldImporter(engine, MinecraftClient.getInstance().player.clientWorld, instance.getThreadPool(), instance.savingServiceRateLimiter);
importer.importRegionDirectoryAsync(directory);
return importer;
});
}
private static int importRaw(CommandContext<FabricClientCommandSource> ctx) {
return fileBasedImporter(new File(ctx.getArgument("path", String.class)))?0:1;
}
private static int importBobby(CommandContext<FabricClientCommandSource> ctx) {
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) {
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) {
return 1;
}
String finalInnerDir = innerDir;
var engine = WorldIdentifier.ofEngine(MinecraftClient.getInstance().player.clientWorld);
if (engine != null) {
return instance.getImportManager().makeAndRunIfNone(engine, () -> {
var importer = new WorldImporter(engine, MinecraftClient.getInstance().player.clientWorld, instance.getThreadPool(), instance.savingServiceRateLimiter);
importer.importZippedRegionDirectoryAsync(zip, finalInnerDir);
return importer;
}) ? 0 : 1;
}
return 1;
}
private static int cancelImport(CommandContext<FabricClientCommandSource> fabricClientCommandSourceCommandContext) {
var instance = (VoxyClientInstance)VoxyCommon.getInstance();
if (instance == null) {
return 1;
}
var world = WorldIdentifier.ofEngineNullable(MinecraftClient.getInstance().player.clientWorld);
if (world != null) {
return instance.getImportManager().cancelImport(world)?0:1;
}
return 1;
}
}

View File

@@ -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
}
}

View File

@@ -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;
}
};
}
}

View File

@@ -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,56 @@ 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 = Math.max(CpuLayout.CORES.length/2, 1);
public float subDivisionSize = 64;
public boolean renderVanillaFog = false;
public boolean renderStatistics = false;
public static VoxyConfig loadOrCreate() {
if (VoxyCommon.isAvailable()) {
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;
var conf = GSON.fromJson(reader, VoxyConfig.class);
if (conf != null) {
conf.save();
return conf;
} else {
Logger.error("Failed to load voxy config, resetting");
}
return cfg;
} catch (IOException e) {
System.err.println("Could not parse config");
e.printStackTrace();
Logger.error("Could not parse config", e);
}
}
var config = new VoxyConfig();
config.defaultSaveConfig = ContextSelectionSystem.DEFAULT_STORAGE_CONFIG;
config.save();
return config;
} else {
var config = new VoxyConfig();
config.enabled = false;
config.enableRendering = false;
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 +74,8 @@ public class VoxyConfig {
.resolve("voxy-config.json");
}
public boolean useMeshShaders() {
return this.useMeshShaderIfPossible && Capabilities.INSTANCE.meshShaders;
@Override
public VoxyConfig getData() {
return this;
}
}

View File

@@ -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());
}
}

View File

@@ -0,0 +1,168 @@
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();
if (vrsh != null && s.enableRendering) {
vrsh.createRenderer();
}
}
}, s -> s.serviceThreads)
.setImpact(OptionImpact.HIGH)
.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.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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}*/

View File

@@ -1,9 +0,0 @@
package me.cortex.voxy.client.core;
import me.cortex.voxy.client.core.VoxelCore;
public interface IGetVoxelCore {
VoxelCore getVoxelCore();
void reloadVoxelCore();
}

View File

@@ -0,0 +1,8 @@
package me.cortex.voxy.client.core;
public interface IGetVoxyRenderSystem {
VoxyRenderSystem getVoxyRenderSystem();
VoxyRenderSystem getVoxyOverworldRenderSystem();
void shutdownRenderer();
void createRenderer();
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,428 @@
package me.cortex.voxy.client.core;
import com.mojang.blaze3d.opengl.GlConst;
import com.mojang.blaze3d.opengl.GlStateManager;
import com.mojang.blaze3d.systems.RenderSystem;
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.rendering.ChunkBoundRenderer;
import me.cortex.voxy.client.core.rendering.RenderDistanceTracker;
import me.cortex.voxy.client.core.rendering.RenderService;
import me.cortex.voxy.client.core.rendering.building.RenderDataFactory;
import me.cortex.voxy.client.core.rendering.building.RenderGenerationService;
import me.cortex.voxy.client.core.rendering.post.PostProcessing;
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.SharedIndexBuffer;
import me.cortex.voxy.client.core.rendering.util.UploadStream;
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.common.world.WorldSection;
import me.cortex.voxy.common.world.other.Mapper;
import me.cortex.voxy.commonImpl.VoxyCommon;
import net.caffeinemc.mods.sodium.client.render.chunk.ChunkRenderMatrices;
import net.caffeinemc.mods.sodium.client.render.chunk.terrain.DefaultTerrainRenderPasses;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gl.GlBackend;
import net.minecraft.client.render.Camera;
import net.minecraft.client.render.Frustum;
import org.joml.Matrix4f;
import org.joml.Matrix4fc;
import org.lwjgl.opengl.GL11;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;
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.GL_VIEWPORT;
import static org.lwjgl.opengl.GL11.glGetIntegerv;
import static org.lwjgl.opengl.GL11C.*;
import static org.lwjgl.opengl.GL14.glBlendFuncSeparate;
import static org.lwjgl.opengl.GL30C.GL_DRAW_FRAMEBUFFER_BINDING;
import static org.lwjgl.opengl.GL30C.glBindFramebuffer;
import static org.lwjgl.opengl.GL33.glBindSampler;
public class VoxyRenderSystem {
private final RenderService renderer;
private final PostProcessing postProcessing;
private final WorldEngine worldIn;
private final RenderDistanceTracker renderDistanceTracker;
public final ChunkBoundRenderer chunkBoundRenderer;
public VoxyRenderSystem(WorldEngine world, ServiceThreadPool threadPool) {
this(world, threadPool, 1L<<32);
}
public VoxyRenderSystem(WorldEngine world, ServiceThreadPool threadPool, long maxGeometryCapacity) {
//Trigger the shared index buffer loading
SharedIndexBuffer.INSTANCE.id();
Capabilities.init();//Ensure clinit is called
this.worldIn = world;
this.renderer = new RenderService(world, threadPool, maxGeometryCapacity);
this.postProcessing = new PostProcessing();
int minSec = MinecraftClient.getInstance().world.getBottomSectionCoord()>>5;
int maxSec = (MinecraftClient.getInstance().world.getTopSectionCoord()-1)>>5;
//Do some very cheeky stuff for MiB
if (false) {
minSec = -8;
maxSec = 7;
}
this.renderDistanceTracker = new RenderDistanceTracker(20,
minSec,
maxSec,
this.renderer::addTopLevelNode,
this.renderer::removeTopLevelNode);
this.renderDistanceTracker.setRenderDistance(VoxyConfig.CONFIG.sectionRenderDistance);
this.chunkBoundRenderer = new ChunkBoundRenderer();
//Keep the world loaded
this.worldIn.acquireRef();
}
public void setRenderDistance(int renderDistance) {
this.renderDistanceTracker.setRenderDistance(renderDistance);
}
//private static final ModelTextureBakery mtb = new ModelTextureBakery(16, 16);
//private static final RawDownloadStream downstream = new RawDownloadStream(1<<20);
public void renderSetup(Frustum frustum, Camera camera) {
TimingStatistics.resetSamplers();
/*
if (false) {
int allocation = downstream.download(2 * 4 * 6 * 16 * 16, ptr -> {
ColourDepthTextureData[] textureData = new ColourDepthTextureData[6];
final int FACE_SIZE = 16 * 16;
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, 16, 16);
}
if (textureData[0].colour()[0] == 0) {
int a = 0;
}
});
mtb.renderFacesToStream(Blocks.AIR.getDefaultState(), 123456, false, downstream.getBufferId(), allocation);
downstream.submit();
downstream.tick();
}*/
}
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.renderer.getMeshQueueCount() < 5000;
float CHANGE_PER_SECOND = 30;
//Auto fps targeting
if (MinecraftClient.getInstance().getCurrentFps() < 45) {
VoxyConfig.CONFIG.subDivisionSize = Math.min(VoxyConfig.CONFIG.subDivisionSize + CHANGE_PER_SECOND / Math.max(1f, MinecraftClient.getInstance().getCurrentFps()), 256);
}
if (55 < MinecraftClient.getInstance().getCurrentFps() && canDecreaseSize) {
VoxyConfig.CONFIG.subDivisionSize = Math.max(VoxyConfig.CONFIG.subDivisionSize - CHANGE_PER_SECOND / Math.max(1f, MinecraftClient.getInstance().getCurrentFps()), 30);
}
}
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));
}
public void renderOpaque(ChunkRenderMatrices matrices, double cameraX, double cameraY, double cameraZ) {
if (IrisUtil.irisShadowActive()) {
return;
}
//Do some very cheeky stuff for MiB
if (false) {
int sector = (((int)Math.floor(cameraX)>>4)+512)>>10;
cameraX -= sector<<14;//10+4
cameraY += (16+(256-32-sector*30))*16;
}
long startTime = System.nanoTime();
TimingStatistics.all.start();
TimingStatistics.main.start();
int oldFB = GL11.glGetInteger(GL_DRAW_FRAMEBUFFER_BINDING);
int boundFB = oldFB;
//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();
var projection = computeProjectionMat(matrices.projection());//RenderSystem.getProjectionMatrix();
//var projection = new Matrix4f(matrices.projection());
int[] dims = new int[4];
glGetIntegerv(GL_VIEWPORT, dims);
var viewport = this.renderer.getViewport();
viewport
.setProjection(projection)
.setModelView(new Matrix4f(matrices.modelView()))
.setCamera(cameraX, cameraY, cameraZ)
.setScreenSize(dims[2], dims[3])
.update();
viewport.frameId++;
TimingStatistics.E.start();
this.chunkBoundRenderer.render(viewport);
TimingStatistics.E.stop();
TimingStatistics.F.start();
this.postProcessing.setup(viewport.width, viewport.height, boundFB);
TimingStatistics.F.stop();
this.renderer.renderFarAwayOpaque(viewport, this.chunkBoundRenderer.getDepthBoundTexture(), startTime);
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(projection, matrices.projection(), boundFB);
TimingStatistics.F.stop();
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(cameraX, cameraZ) && VoxyClient.isFrexActive());//While FF is active, run until everything is processed
//Done here as is allows less gl state resetup
this.renderer.tickModelService(Math.max(3_000_000-(System.nanoTime()-startTime), 500_000));
}
TimingStatistics.postDynamic.stop();
glBindFramebuffer(GlConst.GL_FRAMEBUFFER, oldFB);
{//Reset state manager stuffs
GlStateManager._glBindVertexArray(0);//Clear binding
GlStateManager._activeTexture(GlConst.GL_TEXTURE0);
GlStateManager._bindTexture(0);
glBindSampler(0, 0);
GlStateManager._activeTexture(GlConst.GL_TEXTURE1);
GlStateManager._bindTexture(0);
glBindSampler(1, 0);
GlStateManager._activeTexture(GlConst.GL_TEXTURE2);
GlStateManager._bindTexture(0);
glBindSampler(2, 0);
}
TimingStatistics.all.stop();
}
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.renderer.addDebugData(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 {this.renderer.shutdown();this.chunkBoundRenderer.free();} catch (Exception e) {Logger.error("Error shutting down renderer", e);}
Logger.info("Shutting down post processor");
if (this.postProcessing!=null){try {this.postProcessing.shutdown();} catch (Exception e) {Logger.error("Error shutting down post processor", e);}}
//Release hold on the world
this.worldIn.releaseRef();
}
private void testMeshingPerformance() {
var modelService = new ModelBakerySubsystem(this.worldIn.getMapper());
var factory = new RenderDataFactory(this.worldIn, modelService.factory, false);
List<WorldSection> sections = new ArrayList<>();
System.out.println("Loading sections");
for (int x = -17; x <= 17; x++) {
for (int z = -17; z <= 17; z++) {
for (int y = -1; y <= 4; y++) {
var section = this.worldIn.acquire(0, x, y, z);
int nonAir = 0;
for (long state : section.copyData()) {
nonAir += Mapper.isAir(state)?0:1;
modelService.requestBlockBake(Mapper.getBlockId(state));
}
if (nonAir > 500 && Math.abs(x) <= 16 && Math.abs(z) <= 16) {
sections.add(section);
} else {
section.release();
}
}
}
}
System.out.println("Baking models");
{
//Bake everything
while (!modelService.areQueuesEmpty()) {
modelService.tick(5_000_000);
glFinish();
}
}
System.out.println("Ready!");
{
int iteration = 0;
while (true) {
long start = System.currentTimeMillis();
for (var section : sections) {
var mesh = factory.generateMesh(section);
mesh.free();
}
long delta = System.currentTimeMillis() - start;
System.out.println("Iteration: " + (iteration++) + " took " + delta + "ms, for an average of " + ((float)delta/sections.size()) + "ms per section");
//System.out.println("Quad count: " + factory.quadCount);
}
}
}
private void testFullMesh() {
var modelService = new ModelBakerySubsystem(this.worldIn.getMapper());
var completedCounter = new AtomicInteger();
var generationService = new RenderGenerationService(this.worldIn, modelService, VoxyCommon.getInstance().getThreadPool(), false);
generationService.setResultConsumer(a-> {completedCounter.incrementAndGet(); a.free();});
var r = new Random(12345);
{
for (int i = 0; i < 10_000; i++) {
int x = (r.nextInt(256*2+2)-256)>>1;//-32
int z = (r.nextInt(256*2+2)-256)>>1;//-32
int y = r.nextInt(10)-2;
int lvl = 0;//r.nextInt(5);
long key = WorldEngine.getWorldSectionId(lvl, x>>lvl, y>>lvl, z>>lvl);
generationService.enqueueTask(key);
}
int i = 0;
while (true) {
modelService.tick(5_000_000);
if (i++%5000==0)
System.out.println(completedCounter.get());
glFinish();
List<String> a = new ArrayList<>();
generationService.addDebugData(a);
if (a.getFirst().endsWith(" 0")) {
break;
}
}
}
System.out.println("Running benchmark");
while (true)
{
completedCounter.set(0);
long start = System.currentTimeMillis();
int C = 200_000;
for (int i = 0; i < C; i++) {
int x = (r.nextInt(256 * 2 + 2) - 256) >> 1;//-32
int z = (r.nextInt(256 * 2 + 2) - 256) >> 1;//-32
int y = r.nextInt(10) - 2;
int lvl = 0;//r.nextInt(5);
long key = WorldEngine.getWorldSectionId(lvl, x >> lvl, y >> lvl, z >> lvl);
generationService.enqueueTask(key);
}
//int i = 0;
while (true) {
//if (i++%5000==0)
// System.out.println(completedCounter.get());
modelService.tick(5_000_000);
glFinish();
List<String> a = new ArrayList<>();
generationService.addDebugData(a);
if (a.getFirst().endsWith(" 0")) {
break;
}
}
long delta = (System.currentTimeMillis()-start);
System.out.println("Time "+delta+"ms count: " + completedCounter.get() + " avg per mesh: " + ((double)delta/completedCounter.get()) + "ms");
if (false)
break;
}
generationService.shutdown();
modelService.shutdown();
}
}

View File

@@ -0,0 +1,79 @@
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 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 Capabilities() {
var cap = GL.getCapabilities();
this.compute = cap.glDispatchComputeIndirect != 0;
this.indirectParameters = cap.glMultiDrawElementsIndirectCountARB != 0;
this.meshShaders = cap.GL_NV_mesh_shader && cap.GL_NV_representative_fragment_test;
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;
}
""");
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");
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
}

View File

@@ -1,16 +1,20 @@
package me.cortex.voxy.client.core.gl;
import me.cortex.voxy.common.util.TrackedObject;
import org.lwjgl.opengl.GL11;
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 static int COUNT;
private static long TOTAL_SIZE;
public GlBuffer(long size) {
this(size, 0);
}
@@ -19,15 +23,54 @@ public class GlBuffer extends TrackedObject {
this.id = glCreateBuffers();
this.size = size;
glNamedBufferStorage(this.id, size, flags);
this.zero();
COUNT++;
TOTAL_SIZE += size;
}
@Override
public void free() {
this.free0();
glDeleteBuffers(this.id);
COUNT--;
TOTAL_SIZE -= this.size;
}
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);
glClearNamedBufferData(this.id, GL_R32UI, GL_RED_INTEGER, GL_UNSIGNED_INT, new int[]{data});
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);
}
}

View 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;
}
}

View File

@@ -19,6 +19,11 @@ public class GlFramebuffer extends TrackedObject {
return this;
}
public GlFramebuffer bind(int attachment, GlRenderBuffer buffer) {
glNamedFramebufferRenderbuffer(this.id, attachment, GL_RENDERBUFFER, buffer.id);
return this;
}
@Override
public void free() {
super.free0();
@@ -32,4 +37,9 @@ public class GlFramebuffer extends TrackedObject {
}
return this;
}
public GlFramebuffer name(String name) {
return GlDebug.name(name, this);
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,21 @@
package me.cortex.voxy.client.core.gl;
import me.cortex.voxy.common.util.TrackedObject;
import static org.lwjgl.opengl.GL11C.*;
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);
}
}

View File

@@ -4,13 +4,23 @@ 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 +28,104 @@ 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);
}
int immFormat = glGetTextureParameteri(texture, GL_TEXTURE_IMMUTABLE_FORMAT);
if (immFormat == 0) {
throw new IllegalStateException("Texture: " + texture + " is not immutable");
public int getWidth() {
this.assertAllocated();
return this.width;
}
return immFormat;
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_RGBA8, GL_DEPTH24_STENCIL8 -> 4;
case GL_DEPTH_COMPONENT24 -> 4;//TODO: check this is right????
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;
}
return size;
}
public void assertAllocated() {
if (!this.hasAllocated) {
throw new IllegalStateException("Texture not yet allocated");
}
}
public static int getCount() {
return COUNT;
}
public static long getEstimatedTotalSize() {
return ESTIMATED_TOTAL_SIZE;
}
}

View File

@@ -0,0 +1,72 @@
package me.cortex.voxy.client.core.gl;
import me.cortex.voxy.common.util.TrackedObject;
import java.util.Arrays;
import static org.lwjgl.opengl.GL45C.*;
public class GlVertexArray extends TrackedObject {
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;
}
}

View File

@@ -0,0 +1,124 @@
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<>();
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) {
//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.textureBindings.add(new TextureBinding(unit, sampler, texture));
return this;
}
@Override
public void bind() {
super.bind();
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);
}
}
}
}
}

View File

@@ -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);
}
}

View File

@@ -1,8 +1,15 @@
package me.cortex.voxy.client.core.gl.shader;
import me.cortex.voxy.client.core.gl.Capabilities;
import me.cortex.voxy.client.core.gl.GlBuffer;
import me.cortex.voxy.client.core.gl.GlDebug;
import me.cortex.voxy.common.Logger;
import me.cortex.voxy.common.util.TrackedObject;
import org.lwjgl.opengl.GL20C;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.stream.Collectors;
@@ -11,25 +18,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 +35,88 @@ 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> 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 +144,19 @@ 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);
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);
}
}
@@ -133,12 +182,15 @@ public class Shader extends TrackedObject {
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;
}
}
}

View File

@@ -1,7 +1,8 @@
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) {

View File

@@ -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());
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,150 @@
package me.cortex.voxy.client.core.model;
import it.unimi.dsi.fastutil.ints.IntLinkedOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import me.cortex.voxy.client.TimingStatistics;
import me.cortex.voxy.client.core.gl.GlFramebuffer;
import me.cortex.voxy.client.core.rendering.building.BuiltSection;
import me.cortex.voxy.client.core.rendering.util.RawDownloadStream;
import me.cortex.voxy.common.world.other.Mapper;
import net.minecraft.client.MinecraftClient;
import net.minecraft.registry.RegistryKeys;
import net.minecraft.util.Identifier;
import org.lwjgl.opengl.GL11;
import java.lang.invoke.VarHandle;
import java.util.List;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.StampedLock;
import static org.lwjgl.opengl.ARBFramebufferObject.GL_COLOR_ATTACHMENT0;
import static org.lwjgl.opengl.GL11.GL_COLOR_BUFFER_BIT;
import static org.lwjgl.opengl.GL11.glGetInteger;
import static org.lwjgl.opengl.GL11C.GL_NEAREST;
import static org.lwjgl.opengl.GL30.GL_FRAMEBUFFER;
import static org.lwjgl.opengl.GL30.GL_FRAMEBUFFER_BINDING;
import static org.lwjgl.opengl.GL30C.GL_DRAW_FRAMEBUFFER_BINDING;
import static org.lwjgl.opengl.GL30C.glBindFramebuffer;
import static org.lwjgl.opengl.GL45.glBlitNamedFramebuffer;
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 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.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();
long start = System.nanoTime();
VarHandle.fullFence();
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();
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 StampedLock seenIdsLock = new StampedLock();
private final IntOpenHashSet seenIds = new IntOpenHashSet(6000);
public void requestBlockBake(int blockId) {
long stamp = this.seenIdsLock.writeLock();
if (!this.seenIds.add(blockId)) {
this.seenIdsLock.unlockWrite(stamp);
return;
}
this.seenIdsLock.unlockWrite(stamp);
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();
}
}

View File

@@ -1,20 +1,19 @@
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.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.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;
@@ -36,16 +35,12 @@ 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 +50,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;
private final Biome DEFAULT_BIOME = MinecraftClient.getInstance().world.getRegistryManager().getOrThrow(RegistryKeys.BIOME).get(BiomeKeys.PLAINS);
public final ModelTextureBakery bakery;
private final GlBuffer modelBuffer;
private final GlBuffer modelColourBuffer;
private final GlTexture textures;
private final int blockSampler = glGenSamplers();
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 +105,116 @@ 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<>();
//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 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 +222,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 +236,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 +255,44 @@ public class ModelManager {
this.fluidStateLUT[modelId] = clientFluidStateId;
}
var colourProvider = MinecraftClient.getInstance().getBlockColors().providers.get(Registries.BLOCK.getRawId(blockState.getBlock()));
RenderLayer blockRenderLayer = null;
if (blockState.getBlock() instanceof FluidBlock) {
blockRenderLayer = RenderLayers.getFluidLayer(blockState.getFluidState());
} else {
if (blockState.getBlock() instanceof LeavesBlock) {
blockRenderLayer = RenderLayer.getSolid();
} else {
blockRenderLayer = RenderLayers.getBlockLayer(blockState);
}
}
int checkMode = blockRenderLayer==RenderLayer.getSolid()?TextureUtils.WRITE_CHECK_STENCIL:TextureUtils.WRITE_CHECK_ALPHA;
if (Capabilities.INSTANCE.isMesa) {
//Mesa does not work with GL_DEPTH_STENCIL_TEXTURE_MODE GL_STENCIL_INDEX
// the sampler in the compute shader always reads zero even when stencil is guarenteed not to be zero
// (e.g. clearing with stencil 10)
checkMode = 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 +325,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 +337,16 @@ public class ModelManager {
//Each face gets 1 byte, with the top 2 bytes being for whatever
long metadata = 0;
metadata |= hasBiomeColourResolver?1:0;
metadata |= isBiomeColourDependent?1:0;
metadata |= blockRenderLayer == RenderLayer.getTranslucent()?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: 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,13 +356,17 @@ 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;
@@ -294,9 +378,10 @@ public class ModelManager {
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,13 +392,13 @@ 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 == RenderLayer.getTranslucent())?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);
faceSize[i] = Math.round((((float)faceSize[i])/(MODEL_TEXTURE_SIZE-1))*15);
}
int faceModelData = 0;
@@ -335,6 +420,12 @@ public class ModelManager {
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,24 +433,26 @@ 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 == RenderLayer.getTranslucent()?4:0;//Is translucent
modelFlags |= blockRenderLayer == RenderLayer.getCutout()?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);
//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);
} else if (!isBiomeColourDependent) {
MemoryUtil.memPutInt(uploadPtr + 4, 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);
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;
}
@@ -374,13 +467,30 @@ public class ModelManager {
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) {
System.err.println("Biome added was a duplicate");
}
int i = 0;
@@ -391,14 +501,22 @@ public class ModelManager {
}
//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 +624,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);
throw new IdNotYetComputedException(blockId, true);
}
map = this.idMappings[blockId];
return map;
}
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();
//}
//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);
private static int computeSizeWithMips(int size) {
int total = 0;
for (;size!=0;size>>=1) total += size*size;
return total;
}
return map;
}
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);
//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);
}
}
current = next;
next = new int[current.length>>1];
//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 getBufferId() {
return this.modelBuffer.id;
}
public int getTextureId() {
return this.textures.id;
}
int X = (id&0xFF) * MODEL_TEXTURE_SIZE*3;
int Y = ((id>>8)&0xFF) * MODEL_TEXTURE_SIZE*2;
public int getSamplerId() {
return this.blockSampler;
}
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0);
glPixelStorei(GL_UNPACK_SKIP_ROWS, 0);
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
public int getColourBufferId() {
return this.modelColourBuffer.id;
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();
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,52 @@
package me.cortex.voxy.client.core.model;
import me.cortex.voxy.client.core.gl.GlBuffer;
import me.cortex.voxy.client.core.gl.GlTexture;
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, 4, ModelFactory.MODEL_TEXTURE_SIZE*3*256,ModelFactory.MODEL_TEXTURE_SIZE*2*256).name("ModelTextures");
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);
}
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);
}
}

View File

@@ -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();
}
}

View File

@@ -1,6 +1,6 @@
package me.cortex.voxy.client.core.model;
import me.jellysquid.mods.sodium.client.util.color.ColorSRGB;
import net.caffeinemc.mods.sodium.client.util.color.ColorSRGB;
import net.minecraft.util.math.ColorHelper;
//Texturing utils to manipulate data from the model bakery
@@ -92,10 +92,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;
@@ -179,40 +181,43 @@ public class TextureUtils {
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;
} 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);
//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 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);
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);
}
}

View File

@@ -0,0 +1,79 @@
package me.cortex.voxy.client.core.model.bakery;
import com.mojang.blaze3d.textures.GpuTexture;
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.gl.GlGpuBuffer;
import net.minecraft.client.render.*;
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());
}
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(ModelTextureBakery.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()));
}
}

View File

@@ -0,0 +1,96 @@
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();
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);
}
}

View File

@@ -0,0 +1,80 @@
package me.cortex.voxy.client.core.model.bakery;
import me.cortex.voxy.client.core.gl.GlBuffer;
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 static org.lwjgl.opengl.ARBDirectStateAccess.glClearNamedFramebufferfv;
import static org.lwjgl.opengl.ARBDirectStateAccess.glTextureParameteri;
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.GL11.GL_STENCIL_INDEX;
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;
final GlFramebuffer framebuffer;
private final Shader copyOutShader;
public GlViewCapture(int width, int height) {
this.width = width;
this.height = height;
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_DEPTH_STENCIL_ATTACHMENT, this.depthTex).verify().name("ModelFramebuffer");
glTextureParameteri(this.stencilTex.id, GL_DEPTH_STENCIL_TEXTURE_MODE, GL_STENCIL_INDEX);
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("BUFFER_OUT_BINDING", 3)
.add(ShaderType.COMPUTE, "voxy:bakery/bufferreorder.comp")
.compile()
.name("ModelBakeryOut")
.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, 3, 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);
}
public void clear() {
glClearNamedFramebufferfv(this.framebuffer.id, GL_COLOR, 0, new float[]{0,0,0,0});
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.copyOutShader.free();
}
}

View File

@@ -0,0 +1,326 @@
package me.cortex.voxy.client.core.model.bakery;
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.render.RenderLayer;
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.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.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(RenderLayer layer) {
boolean hasDiscard = layer == RenderLayer.getCutout() ||
layer == RenderLayer.getCutoutMipped() ||
layer == RenderLayer.getTripwire();
boolean isMipped = layer == RenderLayer.getCutoutMipped() ||
layer == RenderLayer.getSolid() ||
layer == RenderLayer.getTranslucent() ||
layer == RenderLayer.getTripwire();
int meta = hasDiscard?1:0;
meta |= isMipped?2:0;
return meta;
}
private void bakeBlockModel(BlockState state, RenderLayer layer) {
var model = MinecraftClient.getInstance()
.getBakedModelManager()
.getBlockModels()
.getModel(state);
int meta = getMetaFromLayer(layer);
for (Direction direction : new Direction[]{Direction.DOWN, Direction.UP, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST, null}) {
for (var part : model.getParts(new LocalRandom(42L))) {
var quads = part.getQuads(direction);
for (var quad : quads) {
//TODO: add meta specifiying quad has a tint
this.vc.quad(quad, meta);
}
}
}
}
private void bakeFluidState(BlockState state, RenderLayer layer, int face) {
this.vc.setDefaultMeta(getMetaFromLayer(layer));//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;
RenderLayer layer;
if (state.getBlock() instanceof FluidBlock) {
layer = RenderLayers.getFluidLayer(state.getFluidState());
isBlock = false;
} else {
if (state.getBlock() instanceof LeavesBlock) {
layer = RenderLayer.getSolid();
} 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 == RenderLayer.getTranslucent()) {
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++) {
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++) {
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++) {
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 == RenderLayer.getTranslucent()) {
//reset the blend func
GL14.glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
}
}
static {
//TODO: FIXME: need to bake in the correct orientation, HOWEVER some orientations require a flipped winding order!!!!
addView(0, -90,0, 0, false);//Direction.DOWN
addView(1, 90,0, 0, false);//Direction.UP
addView(2, 0,180, 0, true);//Direction.NORTH
addView(3, 0,0, 0, false);//Direction.SOUTH
//TODO: check these arnt the wrong way round
addView(4, 0,90, 270, false);//Direction.EAST
addView(5, 0,270, 270, false);//Direction.WEST
}
private static void addView(int i, 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);
VIEWS[i] = new Matrix4f(stack.peek().getPositionMatrix());
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}

View File

@@ -0,0 +1,238 @@
package me.cortex.voxy.client.core.rendering;
import it.unimi.dsi.fastutil.longs.Long2ByteOpenHashMap;
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import me.cortex.voxy.client.core.gl.GlBuffer;
import me.cortex.voxy.client.core.gl.GlFramebuffer;
import me.cortex.voxy.client.core.gl.GlTexture;
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.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.GL_UNIFORM_BUFFER;
import static org.lwjgl.opengl.GL31.glDrawElementsInstanced;
import static org.lwjgl.opengl.GL42.glDrawElementsInstancedBaseInstance;
import static org.lwjgl.opengl.GL45.glClearNamedFramebufferfv;
//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 = Shader.makeAuto()
.add(ShaderType.VERTEX, "voxy:chunkoutline/outline.vsh")
.add(ShaderType.FRAGMENT, "voxy:chunkoutline/outline.fsh")
.compile()
.ubo(0, this.uniformBuffer)
.ssbo(1, this.chunkPosBuffer);
private GlTexture depthBuffer = new GlTexture().store(GL_DEPTH_COMPONENT24, 1, 128, 128);
private final GlFramebuffer frameBuffer = new GlFramebuffer().bind(GL_DEPTH_ATTACHMENT, this.depthBuffer).verify();
private final LongOpenHashSet addQueue = new LongOpenHashSet();
private final LongOpenHashSet remQueue = new LongOpenHashSet();
public ChunkBoundRenderer() {
this.chunk2idx.defaultReturnValue(-1);
}
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
glClearNamedFramebufferfv(this.frameBuffer.id, GL_DEPTH, 0, new float[]{0});
}
}
if (this.depthBuffer.getWidth() != viewport.width || this.depthBuffer.getHeight() != viewport.height) {
this.depthBuffer.free();
this.depthBuffer = new GlTexture().store(GL_DEPTH_COMPONENT24, 1, viewport.width, viewport.height);
this.frameBuffer.bind(GL_DEPTH_ATTACHMENT, this.depthBuffer).verify();
}
if (this.chunk2idx.isEmpty() && this.addQueue.isEmpty()) return;
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
glClearNamedFramebufferfv(this.frameBuffer.id, GL_DEPTH, 0, new float[]{0});
glEnable(GL_CULL_FACE);
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_GREATER);
}
glBindVertexArray(RenderService.STATIC_VAO);
glBindFramebuffer(GL_FRAMEBUFFER, this.frameBuffer.id);
this.rasterShader.bind();
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, SharedIndexBuffer.INSTANCE_BB_BYTE.id());
//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.depthBuffer.free();
this.frameBuffer.free();
this.rasterShader.free();
this.uniformBuffer.free();
this.chunkPosBuffer.free();
}
public GlTexture getDepthBoundTexture() {
return this.depthBuffer;
}
}

View File

@@ -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);
}
}
}

View File

@@ -0,0 +1,65 @@
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;
import java.util.concurrent.locks.StampedLock;
//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();
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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));
}
}
}

View File

@@ -0,0 +1,241 @@
package me.cortex.voxy.client.core.rendering;
import me.cortex.voxy.client.RenderStatistics;
import me.cortex.voxy.client.TimingStatistics;
import me.cortex.voxy.client.VoxyClient;
import me.cortex.voxy.client.core.gl.Capabilities;
import me.cortex.voxy.client.core.gl.GlTexture;
import me.cortex.voxy.client.core.model.ModelBakerySubsystem;
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.geometry.*;
import me.cortex.voxy.client.core.rendering.section.IUsesMeshlets;
import me.cortex.voxy.client.core.rendering.section.MDICSectionRenderer;
import me.cortex.voxy.client.core.rendering.util.DownloadStream;
import me.cortex.voxy.client.core.rendering.util.UploadStream;
import me.cortex.voxy.common.Logger;
import me.cortex.voxy.common.world.WorldEngine;
import me.cortex.voxy.common.thread.ServiceThreadPool;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import static org.lwjgl.opengl.GL42.*;
public class RenderService<T extends AbstractSectionRenderer<J, Q>, J extends Viewport<J>, Q extends IGeometryData> {
public static final int STATIC_VAO = glGenVertexArrays();
private final ViewportSelector<?> viewportSelector;
private final Q geometryData;
private final AbstractSectionRenderer<J, Q> sectionRenderer;
private final AsyncNodeManager nodeManager;
private final NodeCleaner nodeCleaner;
private final HierarchicalOcclusionTraverser traversal;
private final ModelBakerySubsystem modelService;
private final RenderGenerationService renderGen;
private final WorldEngine world;
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() - 512*1024*1024;
// Give a minimum of 512 mb requirement
limit = Math.max(512*1024*1024, limit);
geometryCapacity = Math.min(geometryCapacity, limit);
}
//geometryCapacity = 1<<24;
//geometryCapacity = 1<<30;//1GB test
return geometryCapacity;
}
@SuppressWarnings("unchecked")
public RenderService(WorldEngine world, ServiceThreadPool serviceThreadPool, long maxGeometryCapacity) {
this.world = world;
this.modelService = new ModelBakerySubsystem(world.getMapper());
long geometryCapacity = getGeometryBufferSize();
geometryCapacity = Math.min(maxGeometryCapacity, geometryCapacity);
this.geometryData = (Q) new BasicSectionGeometryData(1<<20, geometryCapacity);
//Max sections: ~500k
this.sectionRenderer = (T) new MDICSectionRenderer(this.modelService.getStore(), (BasicSectionGeometryData) this.geometryData);
Logger.info("Using renderer: " + this.sectionRenderer.getClass().getSimpleName() + " with geometry buffer of: " + geometryCapacity + " bytes");
//Do something incredibly hacky, we dont need to keep the reference to this around, so just connect and discard
this.viewportSelector = new ViewportSelector<>(this.sectionRenderer::createViewport);
this.renderGen = new RenderGenerationService(world, this.modelService, serviceThreadPool,
this.sectionRenderer.getGeometryManager() instanceof IUsesMeshlets,
()->true);
this.nodeManager = new AsyncNodeManager(1<<21, this.geometryData, this.renderGen);
this.nodeCleaner = new NodeCleaner(this.nodeManager);
this.traversal = new HierarchicalOcclusionTraverser(this.nodeManager, this.nodeCleaner);
world.setDirtyCallback(this.nodeManager::worldEvent);
Arrays.stream(world.getMapper().getBiomeEntries()).forEach(this.modelService::addBiome);
world.getMapper().setBiomeCallback(this.modelService::addBiome);
this.nodeManager.start();
}
public void addTopLevelNode(long pos) {
this.nodeManager.addTopLevel(pos);
}
public void removeTopLevelNode(long pos) {
this.nodeManager.removeTopLevel(pos);
}
public void tickModelService(long budget) {
this.modelService.tick(budget);
}
public void renderFarAwayOpaque(J viewport, GlTexture depthBoundTexture, long frameStart) {
//LightMapHelper.tickLightmap();
//Render previous geometry with the abstract renderer
//Execute the hieracial selector
// render delta sections
//Hieracial is not an abstract thing but
// the section renderer is as it might have different backends, but they all accept a buffer containing the section list
TimingStatistics.G.start();
this.sectionRenderer.renderOpaque(viewport, depthBoundTexture);
TimingStatistics.G.stop();
do {
//NOTE: need to do the upload and download tick here, after the section renderer renders the world, to ensure "stable"
// sections
{
TimingStatistics.main.stop();
TimingStatistics.dynamic.start();
/*
this.sectionUpdateQueue.consume(128);
//if (this.modelService.getProcessingCount() < 750)
{//Very bad hack to try control things
this.geometryUpdateQueue.consumeNano(Math.max(3_000_000 - (System.nanoTime() - frameStart), 50_000));
}
if (this.nodeManager.writeChanges(this.traversal.getNodeBuffer())) {//TODO: maybe move the node buffer out of the traversal class
UploadStream.INSTANCE.commit();
}*/
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);
int depthBuffer = glGetFramebufferAttachmentParameteri(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME);
//if (depthBuffer == 0) {
// depthBuffer = glGetFramebufferAttachmentParameteri(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME);
//}
TimingStatistics.I.start();
this.traversal.doTraversal(viewport, depthBuffer);
TimingStatistics.I.stop();
if (VoxyClient.isFrexActive()) {//If frex is running we must tick everything to ensure correctness
UploadStream.INSTANCE.tick();
//Done here as is allows less gl state resetup
this.tickModelService(100_000_000);
glFinish();
}
} while (VoxyClient.isFrexActive() && (this.nodeManager.hasWork() || this.renderGen.getTaskCount()!=0 || !this.modelService.areQueuesEmpty()));
TimingStatistics.H.start();
this.sectionRenderer.buildDrawCalls(viewport);
TimingStatistics.H.stop();
TimingStatistics.G.start();
this.sectionRenderer.renderTemporal(viewport, depthBoundTexture);
TimingStatistics.G.stop();
}
public void renderFarAwayTranslucent(J viewport, GlTexture depthBoundTexture) {
this.sectionRenderer.renderTranslucent(viewport, depthBoundTexture);
}
public void addDebugData(List<String> debug) {
this.modelService.addDebugData(debug);
this.renderGen.addDebugData(debug);
this.sectionRenderer.addDebug(debug);
this.nodeManager.addDebug(debug);
if (RenderStatistics.enabled) {
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;
}
public void shutdown() {
//Cleanup callbacks
this.world.setDirtyCallback(null);
this.world.getMapper().setBiomeCallback(null);
this.world.getMapper().setStateCallback(null);
this.nodeManager.stop();
this.modelService.shutdown();
this.renderGen.shutdown();
this.viewportSelector.free();
this.sectionRenderer.free();
this.traversal.free();
this.nodeCleaner.free();
this.geometryData.free();
}
public Viewport<?> getViewport() {
return this.viewportSelector.getViewport();
}
public int getMeshQueueCount() {
return this.renderGen.getTaskCount();
}
}

View File

@@ -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));
}
}

View File

@@ -0,0 +1,159 @@
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&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 = set.getOrDefault(position, (byte) 0);
lock.unlockRead(stamp);
if (types!=0) {
if ((type&WorldEngine.UPDATE_TYPE_CHILD_EXISTENCE_BIT)!=0) {
this.childUpdateCallback.accept(section);
}
if ((type& UPDATE_TYPE_BLOCK_BIT)!=0) {
this.renderMeshGen.accept(section.key);
}
}
}
private static int getSliceIndex(long value) {
value = (value ^ value >>> 30) * -4658895280553007687L;
value = (value ^ value >>> 27) * -7723592293110705685L;
return (int) ((value ^ value >>> 31)&(SLICES-1));
}
}

View File

@@ -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;
}
}

View File

@@ -1,28 +1,56 @@
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.HiZBuffer;
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 HiZBuffer hiZBuffer = new HiZBuffer();
private static final Field planesField;
static {
try {
planesField = FrustumIntersection.class.getDeclaredField("planes");
planesField.setAccessible(true);
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}
protected Viewport(AbstractFarWorldRenderer renderer) {
this.renderer = renderer;
public int width;
public int height;
public int frameId;
public Matrix4f projection;
public Matrix4f modelView;
public final FrustumIntersection frustum = new FrustumIntersection();
public final Vector4f[] frustumPlanes;
public double cameraX;
public double cameraY;
public double cameraZ;
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();
}
public A setProjection(Matrix4f projection) {
this.projection = projection;
@@ -46,4 +74,27 @@ public abstract class Viewport <A extends Viewport<A>> {
this.height = height;
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)));
return (A) this;
}
public abstract GlBuffer getRenderList();
}

View File

@@ -1,6 +1,5 @@
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 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;

View File

@@ -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() {

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -1,199 +1,369 @@
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.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 me.cortex.voxy.common.thread.ServiceSlice;
import me.cortex.voxy.common.thread.ServiceThreadPool;
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, 5))*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<>(320000, (a,b)-> Long.compareUnsigned(a.priority, b.priority));
private final StampedLock taskMapLock = new StampedLock();
private final Long2ObjectOpenHashMap<BuildTask> taskMap = new Long2ObjectOpenHashMap<>(320000);
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.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();
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;
}
//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;
}
var section = task.sectionSupplier.get();
if (section == null) {
continue;
if (this.resultConsumer != null) {
this.resultConsumer.accept(BuiltSection.empty(task.position));
}
return;
}
section.assertNotFree();
BuiltSection mesh = null;
{
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);
}
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;
}
}
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);
}
}
}
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?3:0;//Single time addin which gives the models time to bake before the task executes
}
//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;
}
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) {
//TODO: if the mesh is null, need to clear the cache at that point
this.resultConsumer.accept(mesh.clone());
if (!this.meshCache.putMesh(mesh)) {
}
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();
}
}
} 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")));
}
}
}
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);
public void enqueueTask(long pos) {
if (!this.threads.isAlive()) {
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;
}
});
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);
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();
}
}
//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 enqueueTask(int lvl, int x, int y, int z) {
this.enqueueTask(WorldEngine.getWorldSectionId(lvl, x, y, z));
}
*/
public void shutdown() {
boolean anyAlive = false;
for (var worker : this.workers) {
anyAlive |= worker.isAlive();
//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);
}
}
if (!anyAlive) {
System.err.println("Render gen workers already dead on shutdown! this is very very bad, check log for errors from this thread");
return;
}
//Since this is just render data, dont care about any tasks needing to finish
this.running = false;
this.taskCounter.release(1000);
//Wait for thread to join
try {
for (var worker : this.workers) {
worker.join();
}
} 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();
}
this.meshCache.free();
long stamp = this.taskMapLock.writeLock();
if (this.taskMap.remove(task.position) != task) {
throw new IllegalStateException();
}
this.taskMapLock.unlockWrite(stamp);
}
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();
}
}

View File

@@ -0,0 +1,982 @@
package me.cortex.voxy.client.core.rendering.hierachical;
import it.unimi.dsi.fastutil.ints.*;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import me.cortex.voxy.client.TimingStatistics;
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.GeometryCache;
import me.cortex.voxy.client.core.rendering.SectionUpdateRouter;
import me.cortex.voxy.client.core.rendering.building.BuiltSection;
import me.cortex.voxy.client.core.rendering.building.RenderGenerationService;
import me.cortex.voxy.client.core.rendering.section.geometry.BasicAsyncGeometryManager;
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.UploadStream;
import me.cortex.voxy.common.Logger;
import me.cortex.voxy.common.util.AllocationArena;
import me.cortex.voxy.common.util.MemoryBuffer;
import me.cortex.voxy.common.world.WorldEngine;
import me.cortex.voxy.common.world.WorldSection;
import org.lwjgl.system.MemoryUtil;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
import java.util.List;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.LockSupport;
import java.util.concurrent.locks.StampedLock;
import static org.lwjgl.opengl.ARBUniformBufferObject.glBindBufferBase;
import static org.lwjgl.opengl.GL30C.glUniform1ui;
import static org.lwjgl.opengl.GL42C.GL_UNIFORM_BARRIER_BIT;
import static org.lwjgl.opengl.GL42C.glMemoryBarrier;
import static org.lwjgl.opengl.GL43C.*;
//TODO: create an "async upload stream", that is, the upload stream is a raw mapped buffer pointer that can be written to
// which is then synced to the gpu on "render thread sync",
//An "async host" for a NodeManager, has specific synchonius entry and exit points
// this is done off thread to reduce the amount of work done on the render thread, improving frame stability and reducing runtime overhead
public class AsyncNodeManager {
private static final VarHandle RESULT_HANDLE;
private static final VarHandle RESULT_CACHE_1_HANDLE;
private static final VarHandle RESULT_CACHE_2_HANDLE;
static {
try {
RESULT_HANDLE = MethodHandles.lookup().findVarHandle(AsyncNodeManager.class, "results", SyncResults.class);
RESULT_CACHE_1_HANDLE = MethodHandles.lookup().findVarHandle(AsyncNodeManager.class, "resultCache1", SyncResults.class);
RESULT_CACHE_2_HANDLE = MethodHandles.lookup().findVarHandle(AsyncNodeManager.class, "resultCache2", SyncResults.class);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
private final Thread thread;
public final int maxNodeCount;
private final long geometryCapacity;
private volatile boolean running = true;
private final NodeManager manager;
private final BasicAsyncGeometryManager geometryManager;
private final IGeometryData geometryData;
private final SectionUpdateRouter router;
private final GeometryCache geometryCache = new GeometryCache(1L<<32);
private final AtomicInteger workCounter = new AtomicInteger();
@SuppressWarnings("FieldMayBeFinal")
private volatile SyncResults results = null, resultCache1 = new SyncResults(), resultCache2 = new SyncResults();
//locals for during iteration
private final IntOpenHashSet tlnIdChange = new IntOpenHashSet();//"Encoded" add/remove id, first bit indicates if its add or remove, 1 is add
//Top bit indicates clear or reset
private final IntOpenHashSet cleanerIdResetClear = new IntOpenHashSet();//Tells the cleaner if it needs to clear the id to 0, or reset the id to the current frame
private boolean needsWaitForSync = false;
public AsyncNodeManager(int maxNodeCount, IGeometryData geometryData, RenderGenerationService renderService) {
//Note the current implmentation of ISectionWatcher is threadsafe
//Note: geometry data is the data store/source, not the management, it is just a raw store of data
// it MUST ONLY be accessed on the render thread
// AsyncNodeManager will use an AsyncGeometryManager as the manager for the data store, and sync the results on the render thread
this.geometryData = geometryData;
this.geometryCapacity = ((BasicSectionGeometryData)geometryData).getGeometryCapacityBytes();
this.maxNodeCount = maxNodeCount;
this.thread = new Thread(()->{
try {
while (this.running) {
this.run();
}
} catch (Exception e) {
Logger.error("Critical error occurred in async processor, things will be broken", e);
}
});
this.thread.setName("Async Node Manager");
this.geometryManager = new BasicAsyncGeometryManager(((BasicSectionGeometryData)geometryData).getMaxSectionCount(), this.geometryCapacity);
this.router = new SectionUpdateRouter();
this.router.setCallbacks(pos->{//On initial render gen, try get from geometry cache
var cachedGeometry = this.geometryCache.remove(pos);
if (cachedGeometry != null) {//Use the cached geometry
this.submitGeometryResult(cachedGeometry);
} else {//Else we need to request it
renderService.enqueueTask(pos);
}
}, renderService::enqueueTask, this::submitChildChange);
renderService.setResultConsumer(this::submitGeometryResult);
this.manager = new NodeManager(maxNodeCount, this.geometryManager, this.router);
//Dont do the move... is just to much effort
this.manager.setClear(new NodeManager.ICleaner() {
@Override
public void alloc(int id) {
AsyncNodeManager.this.cleanerIdResetClear.remove(id);//Remove clear
AsyncNodeManager.this.cleanerIdResetClear.add(id|(1<<31));//Add reset
}
@Override
public void move(int from, int to) {
//noop (sorry :( will cause some perf loss/incorrect cleaning )
}
@Override
public void free(int id) {
AsyncNodeManager.this.cleanerIdResetClear.remove(id|(1<<31));//Remove reset
AsyncNodeManager.this.cleanerIdResetClear.add(id);//Add clear
}
});
this.manager.setTLNCallbacks(id->{
if (!this.tlnIdChange.remove(id)) {
if (!this.tlnIdChange.add(id|(1<<31))) {
throw new IllegalStateException();
}
}
}, id -> {
if (!this.tlnIdChange.remove(id|(1<<31))) {
if (!this.tlnIdChange.add(id)) {
throw new IllegalStateException();
}
}
});
}
private SyncResults getMakeResultObject() {
SyncResults resultSet = (SyncResults)RESULT_CACHE_1_HANDLE.getAndSet(this, null);
if (resultSet == null) {//Not in the first object
resultSet = (SyncResults)RESULT_CACHE_2_HANDLE.getAndSet(this, null);
}
if (resultSet == null) {
throw new IllegalStateException("There should always be an object in the result set cache pair");
}
//Reset everything to default
resultSet.reset();
return resultSet;
}
private final Shader scatterWrite = Shader.make()
.define("INPUT_BUFFER_BINDING", 0)
.define("OUTPUT_BUFFER1_BINDING", 1)
.define("OUTPUT_BUFFER2_BINDING", 2)
.add(ShaderType.COMPUTE, "voxy:util/scatter.comp")
.compile();
private final Shader multiMemcpy = Shader.make()
.define("INPUT_HEADER_BUFFER_BINDING", 0)
.define("INPUT_DATA_BUFFER_BINDING", 1)
.define("OUTPUT_BUFFER_BINDING", 2)
.add(ShaderType.COMPUTE, "voxy:util/memcpy.comp")
.compile();
private void run() {
if (this.workCounter.get() <= 0) {
LockSupport.park();
if (this.workCounter.get() <= 0 || !this.running) {//No work
return;
}
//This is a funny thing, wait a bit, this allows for better batching, but this thread is independent of everything else so waiting a bit should be mostly ok
try {
Thread.sleep(25);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
if (!this.running) {
return;
}
int workDone = 0;
{
LongOpenHashSet add = null;
LongOpenHashSet rem = null;
long stamp = this.tlnLock.writeLock();
if (!this.tlnAdd.isEmpty()) {
add = new LongOpenHashSet(this.tlnAdd);
this.tlnAdd.clear();
}
if (!this.tlnRem.isEmpty()) {
rem = new LongOpenHashSet(this.tlnRem);
this.tlnRem.clear();
}
this.tlnLock.unlockWrite(stamp);
int work = 0;
if (rem != null) {
var iter = rem.longIterator();
while (iter.hasNext()) {
this.manager.removeTopLevelNode(iter.nextLong());
work++;
}
}
if (add != null) {
var iter = add.longIterator();
while (iter.hasNext()) {
this.manager.insertTopLevelNode(iter.nextLong());
work++;
}
}
workDone += work;
}
do {
var job = this.childUpdateQueue.poll();
if (job == null)
break;
workDone++;
this.manager.processChildChange(job.key, job.getNonEmptyChildren());
job.release();
} while (true);
//Limit uploading as well as by geometry capacity being available
// must have 50 mb of free geometry space to upload
for (int limit = 0; limit < 200 && (this.geometryCapacity-this.geometryManager.getGeometryUsedBytes())>50_000_000; limit++)
{
var job = this.geometryUpdateQueue.poll();
if (job == null)
break;
workDone++;
this.manager.processGeometryResult(job);
}
while (true) {//Process all request batches
var job = this.requestBatchQueue.poll();
if (job == null)
break;
workDone++;
long ptr = job.address;
int count = MemoryUtil.memGetInt(ptr);
ptr += 8;//Its 8 to keep alignment
if (job.size < count * 8L + 8) {
throw new IllegalStateException();
}
for (int i = 0; i < count; i++) {
long pos = ((long) MemoryUtil.memGetInt(ptr)) << 32; ptr += 4;
pos |= Integer.toUnsignedLong(MemoryUtil.memGetInt(ptr)); ptr += 4;
this.manager.processRequest(pos);
}
job.free();
}
do {
var job = this.removeBatchQueue.poll();
if (job == null)
break;
workDone++;
long ptr = job.address;
for (int i = 0; i < NodeCleaner.OUTPUT_COUNT; i++) {
long pos = ((long) MemoryUtil.memGetInt(ptr)) << 32; ptr += 4;
pos |= Integer.toUnsignedLong(MemoryUtil.memGetInt(ptr)); ptr += 4;
if (pos == -1) {
//TODO: investigate how or what this happens
continue;
}
if (pos == 0) {
//THIS SHOULD BE IMPOSSIBLE
//TODO: VVVVV MUCH MEGA FIX
continue;
}
this.manager.removeNodeGeometry(pos);
}
job.free();
} while (true);
if (this.workCounter.addAndGet(-workDone) < 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//Due to synchronization "issues", wait a millis (give up this time slice)
if (this.workCounter.get() < 0) {
Logger.error("Work counter less than zero, hope it fixes itself...");
//return;
}
}
if (workDone == 0) {//Nothing happened, which is odd, but just return
return;
}
//=====================
//process output events and atomically sync to results
//Events into manager
//manager.insertTopLevelNode();
//manager.removeTopLevelNode();
//manager.removeNodeGeometry();
//manager.processRequest();
//manager.processChildChange();
//manager.processGeometryResult();
//Outputs from manager
//manager.setClear();
//manager.setTLNCallbacks();
//manager.writeChanges()
//Run in a loop, process all the input events, collect the output events merge with previous and publish
// note: inner event processing is a loop, is.. should be synced to attomic/volatile variable that is being watched
// when frametime comes around, want to exit out as quick as possible, or make the event publishing
// "effectivly immediately", that is, atomicly swap out the render side event updates
//like
// var current = <new events>
// var old = getAndSet(this.events, null);
// if (old != null) {current = merge(old, current);}
// getAndSet(this.events, current);
// if (old == null) {cleanAllEventsUpToThisPoint();}//(i.e. clear any buffers or maps containing data revolving around uncommited render thread data events)
// this creates a lock free event update loop, allowing the render thread to never stall on waiting
//TODO: NOTE: THIS MUST BE A SINGLE OBJECT THAT IS EXCHANGED
// for it to be effectivly synchonized all outgoing events/effects _MUST_ happen at the same time
// for this to be lock free an entire object containing ALL the events that must be synced must be exchanged
//TODO: also note! this can be done for the processing of rendered out block models!!
// (it might be able to also be put in this thread, maybe? but is proabably worth putting in own thread for latency reasons)
if (this.needsWaitForSync) {
while (RESULT_HANDLE.get(this) != null && this.running) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
var prev = (SyncResults) RESULT_HANDLE.getAndSet(this, null);
SyncResults results = null;
if (prev == null) {
this.needsWaitForSync = false;
results = this.getMakeResultObject();
//Clear old data (if it exists), create a new result set
results.tlnDelta.addAll(this.tlnIdChange);
this.tlnIdChange.clear();
if (!this.geometryManager.getUploads().isEmpty()){//Put in new data into sync set
var iter = this.geometryManager.getUploads().int2ObjectEntrySet().fastIterator();
while (iter.hasNext()) {
var val = iter.next();
results.geometryUpload.upload(val.getIntKey(), val.getValue());
val.getValue().free();
}
this.geometryManager.getUploads().clear();
}
this.geometryManager.getHeapRemovals().clear();//We dont do removals on new data (as there is "none")
results.cleanerOperations.addAll(this.cleanerIdResetClear); this.cleanerIdResetClear.clear();
} else {
results = prev;
// merge with the previous result set
if (!this.tlnIdChange.isEmpty()) {//Merge top level node id changes
var iter = this.tlnIdChange.intIterator();
while (iter.hasNext()) {
int val = iter.nextInt();
if (!results.tlnDelta.remove(val ^ (1 << 31))) {//Remove opposite
results.tlnDelta.add(val);//Add this if not added
}
}
this.tlnIdChange.clear();
}
if (!this.cleanerIdResetClear.isEmpty()) {//Merge top level node id changes
var iter = this.cleanerIdResetClear.intIterator();
while (iter.hasNext()) {
int val = iter.nextInt();
results.cleanerOperations.remove(val^(1<<31));//Remove opposite
results.cleanerOperations.add(val);//Add this
}
this.cleanerIdResetClear.clear();
}
if (!this.geometryManager.getHeapRemovals().isEmpty()) {//Remove and free all the removed geometry uploads
var rem = this.geometryManager.getHeapRemovals();
var iter = rem.intIterator();
while (iter.hasNext()) {
results.geometryUpload.remove(iter.nextInt());
}
rem.clear();
}
if (!this.geometryManager.getUploads().isEmpty()) {//Add all the new uploads to the result set
var add = this.geometryManager.getUploads();
var iter = add.int2ObjectEntrySet().fastIterator();
while (iter.hasNext()) {
var val = iter.next();
results.geometryUpload.upload(val.getIntKey(), val.getValue());
val.getValue().free();
}
add.clear();
}
}
{//This is the same regardless of if is a merge or new result
//Geometry id metadata updates
if (!this.geometryManager.getUpdateIds().isEmpty()) {
var ids = this.geometryManager.getUpdateIds();
var iter = ids.intIterator();
while (iter.hasNext()) {
int val = iter.nextInt();
int scatterAddr = (val<<1)|(1<<31);//Since we write to the second buffer
//Geometry buffer is index of 1, so mutate to put it in that location, it is also 32 bytes, so needs to be split into 2 separate scatter writes
long ptrA = results.getScatterWritePtr(scatterAddr+0, 1);
long ptrB = results.getScatterWritePtr(scatterAddr+1, 0);
//Write update data
this.geometryManager.writeMetadataSplit(val, ptrA, ptrB);
}
ids.clear();
}
//Node updates
if (!this.manager.getNodeUpdates().isEmpty()) {
var ids = this.manager.getNodeUpdates();
var iter = ids.intIterator();
while (iter.hasNext()) {
int val = iter.nextInt();
//Dont need to modify the write location since we write to buffer 0
long ptr = results.getScatterWritePtr(val);
//Write updated data
this.manager.writeNode(val, ptr);
}
ids.clear();
}
}
results.geometrySectionCount = this.geometryManager.getSectionCount();
results.usedGeometry = this.geometryManager.getGeometryUsedBytes();
results.currentMaxNodeId = this.manager.getCurrentMaxNodeId();
this.needsWaitForSync |= results.geometryUpload.currentElemCopyAmount*8L > 4L<<20;//4mb limit per frame
if (!RESULT_HANDLE.compareAndSet(this, null, results)) {
throw new IllegalArgumentException("Should always have null");
}
}
private IntConsumer tlnAddCallback; private IntConsumer tlnRemoveCallback;
//Render thread synchronization
public void tick(GlBuffer nodeBuffer, NodeCleaner cleaner) {//TODO: dont pass nodeBuffer here??, do something else thats better
var results = (SyncResults)RESULT_HANDLE.getAndSet(this, null);//Acquire the results
if (results == null) {//There are no new results to process, return
return;
}
//top level node add/remove
if (!results.tlnDelta.isEmpty()) {
var iter = results.tlnDelta.intIterator();
while (iter.hasNext()) {
int val = iter.nextInt();
if ((val&(1<<31))!=0) {//Add node
this.tlnAddCallback.accept(val&(-1>>>1));
} else {
this.tlnRemoveCallback.accept(val);
}
}
//Dont need to clear as is not used again
}
{//Update basic geometry data
var store = (BasicSectionGeometryData)this.geometryData;
store.setSectionCount(results.geometrySectionCount);
var upload = results.geometryUpload;
if (!upload.dataUploadPoints.isEmpty()) {
TimingStatistics.A.start();
int copies = upload.dataUploadPoints.size();
int scratchSize = (int) upload.arena.getSize() * 8;
long ptr = UploadStream.INSTANCE.rawUploadAddress(scratchSize + copies * 16);
MemoryUtil.memCopy(upload.scratchHeaderBuffer.address, UploadStream.INSTANCE.getBaseAddress() + ptr, copies * 16L);
MemoryUtil.memCopy(upload.scratchDataBuffer.address, UploadStream.INSTANCE.getBaseAddress() + ptr + copies * 16L, scratchSize);
UploadStream.INSTANCE.commit();//Commit the buffer
this.multiMemcpy.bind();
glBindBufferRange(GL_SHADER_STORAGE_BUFFER, 0, UploadStream.INSTANCE.getRawBufferId(), ptr, copies*16L);
glBindBufferRange(GL_SHADER_STORAGE_BUFFER, 1, UploadStream.INSTANCE.getRawBufferId(), ptr+copies*16L, scratchSize);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, ((BasicSectionGeometryData) this.geometryData).getGeometryBuffer().id);
if (copies > 500) {
Logger.warn("Large amount of copies, lag will probably happen: " + copies);
}
glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT);
glDispatchCompute(copies, 1, 1);//Execute the copies
glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT);
TimingStatistics.A.stop();
}
}
TimingStatistics.B.start();
if (!results.scatterWriteLocationMap.isEmpty()) {//Scatter write
int count = results.scatterWriteLocationMap.size();//Number of writes, not chunks or uvec4 count
int chunks = (count+3)/4;
int streamSize = chunks*80;//80 bytes per chunk, it is guaranteed the buffer is big enough
long ptr = UploadStream.INSTANCE.rawUploadAddress(streamSize + 16);//Ensure it is 16 byte aligned
ptr = (ptr+15L)&~0xFL;//Align up to 16 bytes
MemoryUtil.memCopy(results.scatterWriteBuffer.address, UploadStream.INSTANCE.getBaseAddress() + ptr, streamSize);
UploadStream.INSTANCE.commit();//Commit the buffer
this.scatterWrite.bind();
glBindBufferRange(GL_SHADER_STORAGE_BUFFER, 0, UploadStream.INSTANCE.getRawBufferId(), ptr, streamSize);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, nodeBuffer.id);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, ((BasicSectionGeometryData) this.geometryData).getMetadataBuffer().id);
glUniform1ui(0, count);
glMemoryBarrier(GL_UNIFORM_BARRIER_BIT|GL_SHADER_STORAGE_BARRIER_BIT);
glDispatchCompute((count+127)/128, 1, 1);
glMemoryBarrier(GL_UNIFORM_BARRIER_BIT|GL_SHADER_STORAGE_BARRIER_BIT);
}
TimingStatistics.B.stop();
TimingStatistics.C.start();
if (!results.cleanerOperations.isEmpty()) {
cleaner.updateIds(results.cleanerOperations);
}
TimingStatistics.C.stop();
this.currentMaxNodeId = results.currentMaxNodeId;
this.usedGeometryAmount = results.usedGeometry;
//Insert the result set into the cache
if (!RESULT_CACHE_1_HANDLE.compareAndSet(this, null, results)) {
//Failed to insert into result set 1, insert it into result set 2
if (!RESULT_CACHE_2_HANDLE.compareAndSet(this, null, results)) {
throw new IllegalStateException("Could not insert result into cache");
}
}
}
public void setTLNAddRemoveCallbacks(IntConsumer add, IntConsumer remove) {
this.tlnAddCallback = add;
this.tlnRemoveCallback = remove;
}
private int currentMaxNodeId = 0;
public int getCurrentMaxNodeId() {
return this.currentMaxNodeId;
}
private long usedGeometryAmount = 0;
public long getUsedGeometryCapacity() {
return this.usedGeometryAmount;
}
public long getGeometryCapacity() {
return this.geometryCapacity;
}
//==================================================================================================================
//Incoming events
//TODO: add atomic counters for each event type probably
private final ConcurrentLinkedDeque<MemoryBuffer> requestBatchQueue = new ConcurrentLinkedDeque<>();
private final ConcurrentLinkedDeque<WorldSection> childUpdateQueue = new ConcurrentLinkedDeque<>();
private final ConcurrentLinkedDeque<BuiltSection> geometryUpdateQueue = new ConcurrentLinkedDeque<>();
private final ConcurrentLinkedDeque<MemoryBuffer> removeBatchQueue = new ConcurrentLinkedDeque<>();
private final StampedLock tlnLock = new StampedLock();
private final LongOpenHashSet tlnAdd = new LongOpenHashSet();
private final LongOpenHashSet tlnRem = new LongOpenHashSet();
private void addWork() {
if (!this.running) throw new IllegalStateException("Not running");
if (this.workCounter.getAndIncrement() == 0) {
LockSupport.unpark(this.thread);
}
}
public void submitRequestBatch(MemoryBuffer batch) {//Only called from render thread
this.requestBatchQueue.add(batch);
this.addWork();
}
private void submitChildChange(WorldSection section) {
if (!this.running) {
return;
}
section.acquire();//We must acquire the section before putting in the queue
this.childUpdateQueue.add(section);
this.addWork();
}
private void submitGeometryResult(BuiltSection geometry) {
if (!this.running) {
geometry.free();
return;
}
this.geometryUpdateQueue.add(geometry);
this.addWork();
}
public void submitRemoveBatch(MemoryBuffer batch) {//Only called from render thread
this.removeBatchQueue.add(batch);
this.addWork();
}
public void addTopLevel(long section) {//Only called from render thread
if (!this.running) throw new IllegalStateException("Not running");
long stamp = this.tlnLock.writeLock();
int state = 0;
if (!this.tlnRem.remove(section)) {
state += this.tlnAdd.add(section)?1:0;
} else {
state -= 1;
}
if (state != 0) {
if (this.workCounter.getAndAdd(state) == 0) {
LockSupport.unpark(this.thread);
}
}
this.tlnLock.unlockWrite(stamp);
}
public void removeTopLevel(long section) {//Only called from render thread
if (!this.running) throw new IllegalStateException("Not running");
long stamp = this.tlnLock.writeLock();
int state = 0;
if (!this.tlnAdd.remove(section)) {
state += this.tlnRem.add(section)?1:0;
} else {
state -= 1;
}
if (state != 0) {
if (this.workCounter.getAndAdd(state) == 0) {
LockSupport.unpark(this.thread);
}
}
this.tlnLock.unlockWrite(stamp);
}
//==================================================================================================================
public void start() {
this.thread.start();
}
public void stop() {
if (!this.running) {
throw new IllegalStateException();
}
this.running = false;
LockSupport.unpark(this.thread);
try {
while (this.thread.isAlive()) {
LockSupport.unpark(this.thread);
this.thread.join(1000);
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
while (true) {
var buffer = this.requestBatchQueue.poll();
if (buffer == null) break;
buffer.free();
}
while (true) {
var buffer = this.removeBatchQueue.poll();
if (buffer == null) break;
buffer.free();
}
while (true) {
var buffer = this.geometryUpdateQueue.poll();
if (buffer == null) break;
buffer.free();
}
while (true) {
var section = this.childUpdateQueue.poll();
if (section == null) break;
section.release();
}
if (RESULT_HANDLE.get(this) != null) {
var result = (SyncResults)RESULT_HANDLE.getAndSet(this, null);
result.geometryUpload.free();
result.scatterWriteBuffer.free();
}
if (RESULT_CACHE_1_HANDLE.get(this) != null) {//Clear cache 1
var result = (SyncResults)RESULT_CACHE_1_HANDLE.getAndSet(this, null);
result.geometryUpload.free();
result.scatterWriteBuffer.free();
}
if (RESULT_CACHE_2_HANDLE.get(this) != null) {//Clear cache 2
var result = (SyncResults)RESULT_CACHE_2_HANDLE.getAndSet(this, null);
result.geometryUpload.free();
result.scatterWriteBuffer.free();
}
this.scatterWrite.free();
this.multiMemcpy.free();
this.geometryCache.free();
}
public void addDebug(List<String> debug) {
debug.add("UC/GC: " + (this.getUsedGeometryCapacity()/(1<<20))+"/"+(this.getGeometryCapacity()/(1<<20)));
}
public boolean hasWork() {
return this.workCounter.get()!=0 && RESULT_HANDLE.get(this) != null;
}
public void worldEvent(WorldSection section, int flags) {
//If there is any change, we need to clear the geometry cache before emitting update
this.geometryCache.clear(section.key);
this.router.forwardEvent(section, flags);
}
//Results object, which is to be synced between the render thread and worker thread
private static final class SyncResults {
//Contains
// geometry uploads and id invalidations and the data
// node ids to invalidate/update and its data
// top level node ids to add/remove
// cleaner move and set operations
//Node id updates + size
private int currentMaxNodeId;// the id of the ending of the node ids
//TLN add/rem
private final IntOpenHashSet tlnDelta = new IntOpenHashSet();
//Deltas for geometry store
private int geometrySectionCount;
private long usedGeometry;
private final ComputeMemoryCopy geometryUpload = new ComputeMemoryCopy();
//Gpu geometry downloads
//Scatter writes for both geometry and node metadata
private MemoryBuffer scatterWriteBuffer = new MemoryBuffer(8192*2);
private final Int2IntOpenHashMap scatterWriteLocationMap = new Int2IntOpenHashMap(1024);
{this.scatterWriteLocationMap.defaultReturnValue(-1);}
//Cleaner operations
private final IntOpenHashSet cleanerOperations = new IntOpenHashSet();
public void reset() {
this.cleanerOperations.clear();
this.scatterWriteLocationMap.clear();
this.currentMaxNodeId = 0;
this.tlnDelta.clear();
this.geometrySectionCount = 0;
this.usedGeometry = 0;
this.geometryUpload.reset();
}
//Get or create a scatter write address for the given location
public long getScatterWritePtr(int location) {
return this.getScatterWritePtr(location, 0);
}
//ensureExtra is used to ensure that allocations are "effectivly" in the same memory block (kinda?)
public long getScatterWritePtr(int location, int ensureExtra) {
int loc = this.scatterWriteLocationMap.get(location);
if (loc == -1) {//Location doesnt exist, create it
this.ensureScatterBufferCapacity(1+ensureExtra);//Ensure can contain capacity for this + extra
int baseId = this.scatterWriteLocationMap.size();
int chunkBase = (baseId/4)*5;//Base uvec4 index
int innerId = baseId&3;
MemoryUtil.memPutInt(this.scatterWriteBuffer.address + (chunkBase*16L) + (innerId*4L), location);//Set the write location
int writeLocation = (chunkBase+1+innerId);//Write location in uvec4
this.scatterWriteLocationMap.put(location, writeLocation);
return this.scatterWriteBuffer.address + (writeLocation*16L);
} else {
return this.scatterWriteBuffer.address + (16L*loc);
}
}
private void ensureScatterBufferCapacity(int extra) {
int requiredChunks = ((this.scatterWriteLocationMap.size()+extra)+3)/4;//4 entries in a chunk
long requiredSize = requiredChunks*5L*16L;//5 uvec4 per chunk, 16 bytes per uvec4
if (this.scatterWriteBuffer.size <= requiredSize) {//Needs resize
long newSize = (long) ((this.scatterWriteBuffer.size*1.5) + extra*80L);
newSize = ((newSize+79)/80)*80;//Ceil to chunk size
Logger.info("Expanding scatter update buffer to " + newSize);
var newBuffer = new MemoryBuffer(newSize);
this.scatterWriteBuffer.cpyTo(newBuffer.address);
this.scatterWriteBuffer.free();
this.scatterWriteBuffer = newBuffer;
}
}
}
private static class ComputeMemoryCopy {
public int currentElemCopyAmount;
private MemoryBuffer scratchHeaderBuffer = new MemoryBuffer(1<<16);
private MemoryBuffer scratchDataBuffer = new MemoryBuffer(1<<20);
private final AllocationArena arena = new AllocationArena();
private final Int2IntOpenHashMap dataUploadPoints = new Int2IntOpenHashMap();//Points to the header index
{this.dataUploadPoints.defaultReturnValue(-1);}
public void remove(int point) {
int header = this.dataUploadPoints.remove(point);
if (header == -1) {//No upload for point
return;
}
int size = MemoryUtil.memGetInt(this.scratchHeaderBuffer.address + header*16L + 8L);
this.currentElemCopyAmount -= size;
//Free the old memory addr from arena
if (this.arena.free(MemoryUtil.memGetInt(this.scratchHeaderBuffer.address + header*16L)) != size) {
throw new IllegalStateException("Freed memory not same size as expected");
}
if (MemoryUtil.memGetInt(this.scratchHeaderBuffer.address + header*16L + 4L) != point) {
throw new IllegalStateException("Destination not the same as point");
}
//If we were the end upload header, return as we dont need to shuffle
if (header == this.dataUploadPoints.size()) {
long A = this.scratchHeaderBuffer.address + header*16L;
//Zero the memory, for consistancy
MemoryUtil.memPutLong(A, 0);
MemoryUtil.memPutLong(A+8, 0);
return;
}
//Else: we need to move the ending upload header from the end to where the freed point was
int endingPoint = MemoryUtil.memGetInt(this.scratchHeaderBuffer.address + this.dataUploadPoints.size()*16L + 4);
if (this.dataUploadPoints.get(endingPoint) != this.dataUploadPoints.size()) {
throw new IllegalStateException("ending header not pointing at end point");
}
//Move the end header to the old header location
long A = this.scratchHeaderBuffer.address + this.dataUploadPoints.size()*16L;
long B = this.scratchHeaderBuffer.address + header*16L;
MemoryUtil.memPutLong(B, MemoryUtil.memGetLong(A)); MemoryUtil.memPutLong(A, 0);
MemoryUtil.memPutLong(B+8, MemoryUtil.memGetLong(A+8)); MemoryUtil.memPutLong(A+8, 0);
//Update the map
this.dataUploadPoints.put(endingPoint, header);
}
public void upload(int point, MemoryBuffer data) {
if ((data.size%8)!=0) throw new IllegalStateException("Data must be of size multiple 8");
int elemSize = (int) (data.size / 8);
int header = this.dataUploadPoints.get(point);
if (header != -1) {
//If we already have a header location, we just need to reallocate the data
long headerPtr = this.scratchHeaderBuffer.address + header*16L;
if (MemoryUtil.memGetInt(headerPtr+4L) != point) {
throw new IllegalStateException("Existing destination not the point");
}
int pSize = MemoryUtil.memGetInt(headerPtr+8L);//Previous size
if (pSize == elemSize) {
//The data we are replacing is the same size, so just overwrite it, this is the easiest
data.cpyTo(this.scratchDataBuffer.address+MemoryUtil.memGetInt(headerPtr)*8L);
} else {
//Dealloc
if (this.arena.free(MemoryUtil.memGetInt(headerPtr)) != pSize) {
throw new IllegalStateException("Freed allocation not size as expected");
}
this.currentElemCopyAmount -= pSize;
this.currentElemCopyAmount += elemSize;
int alloc = this.allocScratchDataPos(elemSize);//New allocation position
//Copy data into position
data.cpyTo(this.scratchDataBuffer.address+alloc*8L);
//Update the header
MemoryUtil.memPutInt(headerPtr, alloc);
MemoryUtil.memPutInt(headerPtr+8, elemSize);
}
} else {
//We need to create and allocate a new header for the upload
header = this.dataUploadPoints.size();
this.dataUploadPoints.put(point, header);
if (this.scratchHeaderBuffer.size<=header*16L) {
//We must resize the header buffer
long newSize = Math.max(this.scratchHeaderBuffer.size*2, header*16L);
Logger.info("Resizing scratch header buffer to: " + newSize);
var newScratch = new MemoryBuffer(newSize);
this.scratchHeaderBuffer.cpyTo(newScratch.address);
this.scratchHeaderBuffer.free();
this.scratchHeaderBuffer = newScratch;
}
long headerPtr = this.scratchHeaderBuffer.address + header*16L;//Header resize has happened so this is a stable address
this.currentElemCopyAmount += elemSize;
int alloc = this.allocScratchDataPos(elemSize);//New allocation position
//Copy data into position
data.cpyTo(this.scratchDataBuffer.address+alloc*8L);
//Set header data
MemoryUtil.memPutInt(headerPtr, alloc);
MemoryUtil.memPutInt(headerPtr+4, point);
MemoryUtil.memPutInt(headerPtr+8, elemSize);
}
}
//This is done here as it enables easily doing scratch data resizing
private int allocScratchDataPos(int size) {
int pos = (int) this.arena.alloc(size);
if (this.scratchDataBuffer.size <= (pos+size)*8L) {
//We must resize :cri:
long newSize = Math.max(this.scratchDataBuffer.size*2, (pos+size)*8L);
Logger.info("Resizing scratch data buffer to: " + newSize);
var newScratch = new MemoryBuffer(newSize);
this.scratchDataBuffer.cpyTo(newScratch.address);
this.scratchDataBuffer.free();
this.scratchDataBuffer = newScratch;
}
return pos;
}
public void reset() {
this.currentElemCopyAmount = 0;
this.dataUploadPoints.clear();
this.arena.reset();
}
public void free() {
this.scratchHeaderBuffer.free(); this.scratchHeaderBuffer = null;
this.scratchDataBuffer.free(); this.scratchDataBuffer = null;
}
}
}

View File

@@ -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.shader.Shader;
import me.cortex.voxy.client.core.gl.shader.ShaderType;
import me.cortex.voxy.client.core.rendering.RenderService;
import me.cortex.voxy.client.core.rendering.util.SharedIndexBuffer;
import me.cortex.voxy.client.core.rendering.Viewport;
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(RenderService.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();
}
}

View File

@@ -0,0 +1,355 @@
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.util.PrintfDebugUtil;
import me.cortex.voxy.client.core.rendering.util.HiZBuffer;
import me.cortex.voxy.client.core.rendering.Viewport;
import me.cortex.voxy.client.core.rendering.util.DownloadStream;
import me.cortex.voxy.client.core.rendering.util.UploadStream;
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 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 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("REQUEST_QUEUE_SIZE", 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) {
this.nodeCleaner = nodeCleaner;
this.nodeManager = nodeManager;
this.requestBuffer = new GlBuffer(REQUEST_QUEUE_SIZE*8L+8).zero();
this.nodeBuffer = new GlBuffer(nodeManager.maxNodeCount*16L).fill(-1);
glSamplerParameteri(this.hizSampler, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST);
glSamplerParameteri(this.hizSampler, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glSamplerParameteri(this.hizSampler, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glSamplerParameteri(this.hizSampler, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glSamplerParameteri(this.hizSampler, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE);
glSamplerParameteri(this.hizSampler, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL);
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
glClearNamedBufferSubData(this.topNodeIds.id, GL_R32UI, aid*4L, 4, GL_RED_INTEGER, GL_UNSIGNED_INT, new int[]{id});
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
glClearNamedBufferSubData(this.topNodeIds.id, GL_R32UI, idx*4L, 4, GL_RED_INTEGER, GL_UNSIGNED_INT, new int[]{endTLNId});
}
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;
viewport.innerTranslation.getToAddress(ptr); ptr += 4*3;
MemoryUtil.memPutFloat(ptr, viewport.height); ptr += 4;
setFrustum(viewport, ptr); ptr += 4*4*6;
MemoryUtil.memPutInt(ptr, (int) (viewport.getRenderList().size()/4-1)); 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;
//VisibilityId
MemoryUtil.memPutInt(ptr, this.nodeCleaner.visibilityId); 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, int depthBuffer) {
//Compute the mip chain
viewport.hiZBuffer.buildMipChain(depthBuffer, viewport.width, viewport.height);
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) {
throw new IllegalStateException("Count unexpected extreme value: " + count);
}
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);
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,175 @@
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.PrintfDebugUtil;
import me.cortex.voxy.client.core.rendering.util.DownloadStream;
import me.cortex.voxy.client.core.rendering.util.UploadStream;
import org.lwjgl.system.MemoryUtil;
import static org.lwjgl.opengl.ARBDirectStateAccess.glCopyNamedBufferSubData;
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);
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);
}
}
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

View File

@@ -0,0 +1,314 @@
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);
MemoryUtil.memPutLong(ptr + 16, -1);
MemoryUtil.memPutLong(ptr + 24, -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();
}
}

View File

@@ -0,0 +1,48 @@
package me.cortex.voxy.client.core.rendering.hierachical;
class SingleNodeRequest {
private final long nodePos;
private int mesh;
private byte childExistence;
private int setMsk;
SingleNodeRequest(long nodePos) {
this.nodePos = nodePos;
this.mesh = -1;
}
public void setChildExistence(byte childExistence) {
this.setMsk |= 2;
this.childExistence = childExistence;
}
public int setMesh(int mesh) {
this.setMsk |= 1;
int prev = this.mesh;
this.mesh = mesh;
return prev;
}
public boolean isSatisfied() {
return this.setMsk == 3;
}
public long getPosition() {
return this.nodePos;
}
public int getMesh() {
return this.mesh;
}
public byte getChildExistence() {
return this.childExistence;
}
public boolean hasChildExistenceSet() {
return (this.setMsk&2)!=0;
}
public boolean hasMeshSet() {
return (this.setMsk&1)!=0;
}
}

View File

@@ -0,0 +1,633 @@
package me.cortex.voxy.client.core.rendering.hierachical;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.longs.*;
import me.cortex.voxy.client.core.rendering.ISectionWatcher;
import me.cortex.voxy.client.core.rendering.building.BuiltSection;
import me.cortex.voxy.client.core.rendering.section.geometry.AbstractSectionGeometryManager;
import me.cortex.voxy.common.Logger;
import me.cortex.voxy.common.util.HierarchicalBitSet;
import me.cortex.voxy.common.util.MemoryBuffer;
import me.cortex.voxy.common.world.WorldEngine;
import org.lwjgl.system.MemoryUtil;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.stream.IntStream;
import static me.cortex.voxy.common.world.WorldEngine.*;
public class TestNodeManager {
private static final class MemoryGeometryManager extends AbstractSectionGeometryManager {
private record Entry(long pos, long size) {}
private long memoryInUse = 0;
private final HierarchicalBitSet allocation;
private final Int2ObjectOpenHashMap<Entry> sections = new Int2ObjectOpenHashMap<>();
public MemoryGeometryManager(int maxSections, long geometryCapacity) {
super(maxSections, geometryCapacity);
this.allocation = new HierarchicalBitSet(maxSections);
}
@Override
public int uploadReplaceSection(int oldId, BuiltSection section) {
if (section.isEmpty()) {
throw new IllegalArgumentException();
}
if (oldId != -1) {
this.removeSection(oldId);
}
int newId = this.allocation.allocateNext();
var entry = new Entry(section.position, section.geometryBuffer.size);
if (this.sections.put(newId, entry) != null) {
throw new IllegalStateException();
}
this.memoryInUse += entry.size;
section.free();
Logger.info("Creating geometry with id", newId, "and size", entry.size, "at pos", WorldEngine.pprintPos(entry.pos));
return newId;
}
@Override
public void removeSection(int id) {
if (!this.allocation.free(id)) {
throw new IllegalStateException();
}
var old = this.sections.remove(id);
if (old == null) {
throw new IllegalStateException();
}
this.memoryInUse -= old.size;
Logger.info("Removing geometry with id", id, "it was at pos", WorldEngine.pprintPos(old.pos));
}
@Override
public void downloadAndRemove(int id, Consumer<BuiltSection> callback) {
this.removeSection(id);
}
@Override
public long getUsedCapacity() {
return this.memoryInUse;
}
}
private static class CleanerImp implements NodeManager.ICleaner {
private final IntOpenHashSet active = new IntOpenHashSet();
@Override
public void alloc(int id) {
if (!this.active.add(id)) {
throw new IllegalStateException();
}
}
@Override
public void move(int from, int to) {
}
@Override
public void free(int id) {
if (!this.active.remove(id)) {
throw new IllegalStateException();
}
}
}
private static class Watcher implements ISectionWatcher {
private final Long2ByteOpenHashMap updateTypes = new Long2ByteOpenHashMap();
@Override
public boolean watch(long position, int types) {
byte current = 0;
boolean had = false;
if (this.updateTypes.containsKey(position)) {
current = this.updateTypes.get(position);
had = true;
}
if (had && current == 0) {
throw new IllegalStateException();
}
this.updateTypes.put(position, (byte) (current | types));
byte delta = (byte) (types&(~current));
Logger.info("Watching pos", WorldEngine.pprintPos(position), "with types", getPrettyTypes(types), "was", getPrettyTypes(current));
return delta!=0;//returns true if new types where set
}
@Override
public boolean unwatch(long position, int types) {
if (!this.updateTypes.containsKey(position)) {
throw new IllegalStateException("Pos not in map: " + WorldEngine.pprintPos(position));
}
byte current = this.updateTypes.get(position);
byte newTypes = (byte) (current&(~types));
if (newTypes == 0) {
this.updateTypes.remove(position);
} else {
this.updateTypes.put(position, newTypes);
}
Logger.info("UnWatching pos", WorldEngine.pprintPos(position), "removing types", getPrettyTypes(types), "was watching", getPrettyTypes(current), "new types", getPrettyTypes(newTypes));
return newTypes == 0;//Returns true on removal
}
@Override
public int get(long position) {
return this.updateTypes.getOrDefault(position, (byte) 0);
}
private static String[] getPrettyTypes(int msk) {
if ((msk&~UPDATE_FLAGS)!=0) {
throw new IllegalStateException();
}
String[] types = new String[Integer.bitCount(msk)];
int i = 0;
if ((msk&UPDATE_TYPE_BLOCK_BIT)!=0) {
types[i++] = "BLOCK";
}
if ((msk&UPDATE_TYPE_CHILD_EXISTENCE_BIT)!=0) {
types[i++] = "CHILD";
}
return types;
}
}
private static class TestBase {
public final MemoryGeometryManager geometryManager;
public final NodeManager nodeManager;
public final Watcher watcher;
public final CleanerImp cleaner;
public TestBase() {
this.watcher = new Watcher();
this.cleaner = new CleanerImp();
this.geometryManager = new MemoryGeometryManager(1<<20, 1<<30);
this.nodeManager = new NodeManager(1 << 21, this.geometryManager, this.watcher);
this.nodeManager.setClear(this.cleaner);
}
public void putTopPos(long pos) {
this.nodeManager.insertTopLevelNode(pos);
}
public void meshUpdate(long pos, int childExistence, int geometrySize) {
if (childExistence == -1) {
childExistence = 0xFF;
}
if (childExistence>255) {
throw new IllegalArgumentException();
}
MemoryBuffer buff = null;
if (geometrySize != 0) {
buff = new MemoryBuffer(geometrySize);
}
var builtGeometry = new BuiltSection(pos, (byte) childExistence, -2, buff, null);
this.nodeManager.processGeometryResult(builtGeometry);
}
public void request(long pos) {
this.nodeManager.processRequest(pos);
}
public void childUpdate(long pos, int existence) {
if (existence == -1) {
existence = 0xFF;
}
if (existence>255) {
throw new IllegalArgumentException();
}
this.nodeManager.processChildChange(pos, (byte) existence);
}
public boolean printNodeChanges() {
var changes = this.nodeManager._generateChangeList();
if (changes == null) {
return false;
}
for (int c = 0; c < changes.size/20; c++) {
long ptr = changes.address+20L*c;
int nodeId = MemoryUtil.memGetInt(ptr); ptr+=4;
long pos = Integer.toUnsignedLong(MemoryUtil.memGetInt(ptr))<<32; ptr += 4;
pos |= Integer.toUnsignedLong(MemoryUtil.memGetInt(ptr)); ptr += 4;
int z = MemoryUtil.memGetInt(ptr); ptr += 4;
int w = MemoryUtil.memGetInt(ptr); ptr += 4;
int childPtr = w&0xFFFFFF;
int geometry = z&0xFFFFFF;
short flags = 0;
flags |= (short) ((z>>>24)&0xFF);
flags |= (short) (((w>>>24)&0xFF)<<8);
Logger.info("Node update, id:",nodeId,"pos:",WorldEngine.pprintPos(pos),"childPtr:",childPtr,"geometry:",geometry,"flags:",flags);
}
changes.free();
return true;
}
public void removeNodeGeometry(long pos) {
this.nodeManager.removeNodeGeometry(pos);
}
public void verifyIntegrity() {
this.nodeManager.verifyIntegrity(this.watcher.updateTypes.keySet(), this.cleaner.active);
}
public void remTopPos(long pos) {
this.nodeManager.removeTopLevelNode(pos);
}
}
private static void fillInALl(TestBase test, long pos, Long2IntFunction converter) {
test.request(pos);
int ce = converter.get(pos);
//Satisfy request for all the children
for (int i = 0; i<8;i++) {
if ((ce&(1<<i))==0) continue;
long p = makeChildPos(pos, i);
test.meshUpdate(p, converter.get(p), 8);
}
if (WorldEngine.getLevel(pos) == 1) {
return;
}
for (int i = 0; i<8;i++) {
if ((ce&(1<<i))==0) continue;
fillInALl(test, makeChildPos(pos, i), converter);
}
}
private static class Node {
private final long pos;
private final Node[] children = new Node[8];
private byte childExistenceMask;
private int meshId;
private Node(long pos) {
this.pos = pos;
}
}
public static void main(String[] args) {
Logger.INSERT_CLASS = false;
int ITER_COUNT = 5_000;
int INNER_ITER_COUNT = 1_000_000;
boolean GEO_REM = true;
AtomicInteger finished = new AtomicInteger();
HashSet<List<StackTraceElement>> seenTraces = new HashSet<>();
Logger.SHUTUP = true;
if (true) {
for (int q = 0; q < ITER_COUNT; q++) {
//Logger.info("Iteration "+ q);
if (runTest(INNER_ITER_COUNT, q, seenTraces, GEO_REM)) {
finished.incrementAndGet();
}
}
} else {
IntStream.range(0, ITER_COUNT).parallel().forEach(i->{
if (runTest(INNER_ITER_COUNT, i, seenTraces, GEO_REM)) {
finished.incrementAndGet();
}
});
}
System.out.println("Finished " + finished.get() + " iterations out of " + ITER_COUNT);
}
private static long rPos(Random r, LongList tops) {
int lvl = r.nextInt(5);
long top = tops.getLong(r.nextInt(tops.size()));
if (lvl==4) {
return top;
}
int bound = 16>>lvl;
return WorldEngine.getWorldSectionId(lvl, r.nextInt(bound)+(WorldEngine.getX(top)<<4), r.nextInt(bound)+(WorldEngine.getY(top)<<4), r.nextInt(bound)+(WorldEngine.getZ(top)<<4));
}
private static boolean runTest(int ITERS, int testIdx, Set<List<StackTraceElement>> traces, boolean geoRemoval) {
Random r = new Random(testIdx * 1234L);
try {
var test = new TestBase();
LongList tops = new LongArrayList();
int R = 1;
if (r.nextBoolean()) {
R++;
if (r.nextBoolean()) {
R++;
if (r.nextBoolean()) {
R++;
}
}
}
//Fuzzy bruteforce everything
for (int x = -R; x<=R; x++) {
for (int z = -R; z<=R; z++) {
for (int y = -8; y<=7; y++) {
tops.add(WorldEngine.getWorldSectionId(4, x, y, z));
}
}
}
for (long p : tops) {
test.putTopPos(p);
test.meshUpdate(p, -1, 18);
fillInALl(test, p, a->-1);
test.printNodeChanges();
test.verifyIntegrity();
}
for (int i = 0; i < ITERS; i++) {
long pos = rPos(r, tops);
int op = r.nextInt(5);
int extra = r.nextInt(256);
boolean hasGeometry = r.nextBoolean();
boolean addRemTLN = r.nextInt(64) == 0;
boolean extraBool = r.nextBoolean();
if (op == 0 && addRemTLN) {
pos = WorldEngine.getWorldSectionId(4, r.nextInt(5)-2, r.nextInt(32)-16, r.nextInt(5)-2);
boolean cont = tops.contains(pos);
if (cont&&extraBool&&tops.size()>1) {
extraBool = true;
test.remTopPos(pos);
tops.rem(pos);
} else if (!cont) {
extraBool = false;
test.putTopPos(pos);
tops.add(pos);
}
} else if (op == 0) {
test.request(pos);
}
if (op == 1) {
test.childUpdate(pos, extra);
}
if (op == 2) {
test.meshUpdate(pos, extra, hasGeometry ? 100 : 0);
}
if (op == 3 && geoRemoval) {
test.nodeManager.removeNodeGeometry(pos);
}
test.printNodeChanges();
test.verifyIntegrity();
}
for (long top : tops) {
test.remTopPos(top);
}
test.printNodeChanges();
test.verifyIntegrity();
if (test.nodeManager.getCurrentMaxNodeId() != -1) {
throw new IllegalStateException();
}
if (!test.cleaner.active.isEmpty()) {
throw new IllegalStateException();
}
if (!test.watcher.updateTypes.isEmpty()) {
throw new IllegalStateException();
}
if (test.geometryManager.memoryInUse != 0) {
throw new IllegalStateException();
}
return true;
} catch (Exception e) {
var trace = new ArrayList<>(List.of(e.getStackTrace()));
while (!trace.getLast().getMethodName().equals("runTest")) trace.removeLast();//Very hacky budget filter
synchronized (traces) {
if (traces.add(trace)) {
e.printStackTrace();
}
}
return false;
}
}
public static void main3(String[] args) {
Logger.INSERT_CLASS = false;
if (false) {
var test = new TestBase();
long POS_A = WorldEngine.getWorldSectionId(4, 0, 0, 0);
test.putTopPos(POS_A);
test.meshUpdate(POS_A, 3, 0);
test.printNodeChanges();
test.request(POS_A);
test.meshUpdate(makeChildPos(POS_A, 0), -1, 100);
test.meshUpdate(makeChildPos(POS_A, 1), -1, 100);
test.printNodeChanges();
Logger.info("TEST: Created full node");
test.childUpdate(POS_A, 0);
test.printNodeChanges();
Logger.info("BB");
test.meshUpdate(POS_A, 0, 0);
test.printNodeChanges();
}
if (false) {
var test = new TestBase();
long POS_A = WorldEngine.getWorldSectionId(4, 0, 0, 0);
test.putTopPos(POS_A);
test.meshUpdate(POS_A, 1, 0);
test.request(POS_A);
test.meshUpdate(makeChildPos(POS_A, 0), -1, 100);
test.childUpdate(POS_A, 0);
}
if (false) {//Crash E
var test = new TestBase();
long POS_A = WorldEngine.getWorldSectionId(4, 0, 0, 0);
test.putTopPos(POS_A);
test.meshUpdate(POS_A, 3, 0);
test.printNodeChanges();
test.request(POS_A);
test.meshUpdate(makeChildPos(POS_A, 0), -1, 100);
test.meshUpdate(makeChildPos(POS_A, 1), -1, 100);
test.printNodeChanges();
Logger.info("TEST: Created full node");
test.childUpdate(POS_A, 0b110);
test.printNodeChanges();
test.childUpdate(POS_A, 0b10);
test.printNodeChanges();
}
if (false) {//Crash D
var test = new TestBase();
long POS_A = WorldEngine.getWorldSectionId(4, 0, 0, 0);
test.putTopPos(POS_A);
test.meshUpdate(POS_A, 3, 0);
test.printNodeChanges();
test.request(POS_A);
test.meshUpdate(makeChildPos(POS_A, 0), -1, 100);
test.meshUpdate(makeChildPos(POS_A, 1), -1, 100);
test.printNodeChanges();
Logger.info("TEST: Created full node");
test.childUpdate(POS_A, 0b110);
test.printNodeChanges();
test.meshUpdate(makeChildPos(POS_A, 2), -1, 100);
test.printNodeChanges();
}
if (false) {//Crash c
var test = new TestBase();
long POS_A = WorldEngine.getWorldSectionId(4, 0, 0, 0);
test.putTopPos(POS_A);
test.meshUpdate(POS_A, 3, 0);
test.printNodeChanges();
test.request(POS_A);
test.meshUpdate(makeChildPos(POS_A, 0), -1, 100);
test.meshUpdate(makeChildPos(POS_A, 1), -1, 100);
test.printNodeChanges();
Logger.info("TEST: Created full node");
test.childUpdate(POS_A, 0b1111);
test.printNodeChanges();
test.meshUpdate(makeChildPos(POS_A, 2), -1, 100);
test.printNodeChanges();
Logger.info("TEST: Executing funny");
test.childUpdate(POS_A, 0b0110);
test.printNodeChanges();
}
if (false) {//Crash b
var test = new TestBase();
long POS_A = WorldEngine.getWorldSectionId(4, 0, 0, 0);
test.putTopPos(POS_A);
test.meshUpdate(POS_A, 3, 0);
test.printNodeChanges();
test.request(POS_A);
test.meshUpdate(makeChildPos(POS_A, 0), -1, 100);
test.meshUpdate(makeChildPos(POS_A, 1), -1, 100);
test.printNodeChanges();
Logger.info("TEST: Created full node");
test.childUpdate(POS_A, 0b1111);
test.printNodeChanges();
test.meshUpdate(makeChildPos(POS_A, 2), -1, 100);
test.printNodeChanges();
test.childUpdate(POS_A, 0b1110);
test.printNodeChanges();
test.childUpdate(POS_A, 0b0110);
test.printNodeChanges();
}
if (false) {// Test case A, known crash
var test = new TestBase();
long POS_A = WorldEngine.getWorldSectionId(4, 0, 0, 0);
test.putTopPos(POS_A);
test.meshUpdate(POS_A, 7, 0);
test.printNodeChanges();
test.request(POS_A);
test.meshUpdate(makeChildPos(POS_A, 0), -1, 100);
test.meshUpdate(makeChildPos(POS_A, 1), -1, 100);
test.meshUpdate(makeChildPos(POS_A, 2), -1, 100);
test.printNodeChanges();
Logger.error("CHANGING CHILD A");
test.childUpdate(POS_A, 1);
test.printNodeChanges();
Logger.error("CHANGING CHILD B");
test.childUpdate(POS_A, 3);
test.printNodeChanges();
test.meshUpdate(makeChildPos(POS_A, 1), -1, 100);
test.printNodeChanges();
}
}
public static void main2(String[] args) {
Logger.INSERT_CLASS = false;
var test = new TestBase();
long POS_A = WorldEngine.getWorldSectionId(4, 0, 0, 0);
test.putTopPos(POS_A);
test.meshUpdate(POS_A, -1, 0);
fillInALl(test, POS_A, a->-1);
test.printNodeChanges();
Logger.info("\n\n");
test.removeNodeGeometry(WorldEngine.getWorldSectionId(0,0,0,0));
test.printNodeChanges();
test.removeNodeGeometry(WorldEngine.getWorldSectionId(3,0,0,0));
test.printNodeChanges();
Logger.info("changing child existance");
test.childUpdate(WorldEngine.getWorldSectionId(4,0,0,0), 1);
test.childUpdate(WorldEngine.getWorldSectionId(3,0,0,0), 1);
test.childUpdate(WorldEngine.getWorldSectionId(2,0,0,0), 1);
test.childUpdate(WorldEngine.getWorldSectionId(1,0,0,0), 1);
test.printNodeChanges();
}
public static void main1(String[] args) {
Logger.INSERT_CLASS = false;
Random r = new Random(1234);
Long2IntOpenHashMap aa = new Long2IntOpenHashMap();
Long2IntFunction cc = p-> aa.computeIfAbsent(p, poss->{int b = r.nextInt()&0xFF;
while (b==0) b = r.nextInt()&0xFF;
return b;});
var test = new TestBase();
long POS_A = WorldEngine.getWorldSectionId(4, 0, 0, 0);
test.putTopPos(POS_A);
test.meshUpdate(POS_A, cc.get(POS_A), 0);
fillInALl(test, POS_A, cc);
test.printNodeChanges();
Logger.info("\n\n");
var positions = new ArrayList<>(aa.keySet().stream().filter(k->{
return WorldEngine.getLevel(k)!=0;
}).toList());
positions.sort(Long::compareTo);
Collections.shuffle(positions, r);
Logger.info("Removing", WorldEngine.pprintPos(positions.get(0)));
test.removeNodeGeometry(positions.get(0));
test.printNodeChanges();
}
private static int getChildIdx(long pos) {
int x = WorldEngine.getX(pos);
int y = WorldEngine.getY(pos);
int z = WorldEngine.getZ(pos);
return (x&1)|((y&1)<<2)|((z&1)<<1);
}
private static long makeChildPos(long basePos, int addin) {
int lvl = WorldEngine.getLevel(basePos);
if (lvl == 0) {
throw new IllegalArgumentException("Cannot create a child lower than lod level 0");
}
return WorldEngine.getWorldSectionId(lvl-1,
(WorldEngine.getX(basePos)<<1)|(addin&1),
(WorldEngine.getY(basePos)<<1)|((addin>>2)&1),
(WorldEngine.getZ(basePos)<<1)|((addin>>1)&1));
}
private long makeParentPos(long pos) {
int lvl = WorldEngine.getLevel(pos);
if (lvl == MAX_LOD_LAYER) {
throw new IllegalArgumentException("Cannot create a parent higher than LoD " + (MAX_LOD_LAYER));
}
return WorldEngine.getWorldSectionId(lvl+1,
WorldEngine.getX(pos)>>1,
WorldEngine.getY(pos)>>1,
WorldEngine.getZ(pos)>>1);
}
}

View File

@@ -1,15 +0,0 @@
package me.cortex.voxy.client.core.rendering.hierarchical;
import me.cortex.voxy.client.core.rendering.building.BuiltSection;
import java.util.function.Consumer;
//Interface for node manager to interact with the outside world
public interface INodeInteractor {
void watchUpdates(long pos);//marks pos as watching for updates, i.e. any LoD updates will trigger a callback
void unwatchUpdates(long pos);//Unmarks a position for updates
void requestMesh(long pos);//Explicitly requests a mesh at a position, run the callback
void setMeshUpdateCallback(Consumer<BuiltSection> mesh);
}

View File

@@ -1,12 +0,0 @@
package me.cortex.voxy.client.core.rendering.hierarchical;
public interface ITrimInterface {
//Last recorded/known use time of a nodes mesh, returns -1 if node doesnt have a mesh
int lastUsedTime(int node);
//Returns an integer with the bottom 24 bits being the ptr top 8 bits being count or something
int getChildren(int node);
//Returns a size of the nodes mesh, -1 if the node doesnt have a mesh
int getNodeSize(int node);
}

View File

@@ -1,33 +0,0 @@
package me.cortex.voxy.client.core.rendering.hierarchical;
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;
//Manages the actual meshes, whether they are meshlets or whole meshes, the returned values are ids into a section
// array which contains metadata about the section
public class MeshManager {
private final BufferArena geometryArena;
private final GlBuffer sectionMetaBuffer;
public MeshManager() {
this.geometryArena = null;
this.sectionMetaBuffer = null;
}
//Uploads the section geometry to the arena, there can be multiple meshes for the same geometry in the arena at the same time
// it is not the MeshManagers responsiblity
//The return value is arbitary as long as it can identify the mesh its uploaded until it is freed
public int uploadMesh(BuiltSection section) {
return uploadReplaceMesh(-1, section);
}
//Varient of uploadMesh that releases the previous mesh at the same time, this is a performance optimization
public int uploadReplaceMesh(int old, BuiltSection section) {
return -1;
}
public void removeMesh(int mesh) {
}
}

View File

@@ -1,5 +0,0 @@
package me.cortex.voxy.client.core.rendering.hierarchical;
//Uses a persistently mapped coherient buffer with off thread polling to pull in requests
public class NodeLoadSystem {
}

View File

@@ -1,219 +0,0 @@
package me.cortex.voxy.client.core.rendering.hierarchical;
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
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.DownloadStream;
import me.cortex.voxy.common.util.HierarchicalBitSet;
import org.lwjgl.system.MemoryUtil;
import static org.lwjgl.opengl.GL11.GL_UNSIGNED_INT;
import static org.lwjgl.opengl.GL30.GL_R32UI;
import static org.lwjgl.opengl.GL30C.GL_RED_INTEGER;
import static org.lwjgl.opengl.GL45.nglClearNamedBufferSubData;
public class NodeManager {
public static final int MAX_NODE_COUNT = 1<<22;
public static final int MAX_REQUESTS = 1024;
private final HierarchicalBitSet bitSet = new HierarchicalBitSet(MAX_NODE_COUNT);
private final GlBuffer nodeBuffer = new GlBuffer(MAX_NODE_COUNT*16);//Node size is 16 bytes
//TODO: maybe make this a coherent persistent mapped read buffer, instead of download synced buffer copy thing
//a request payload is a single uint, first 8 bits are flags followed by 24 bit node identifier
// (e.g. load child nodes, load child nodes + meshs, load self meshes )
private final int REQUEST_QUEUE_SIZE = 4 + MAX_REQUESTS * 4;//TODO: add a priority system
private final GlBuffer requestQueue = new GlBuffer(4 + MAX_REQUESTS * 4);
//Buffer containing the index of the root nodes
private final GlBuffer roots = new GlBuffer(1024*4);
//500mb TODO: SEE IF CAN SHRINK IT BY EITHER NOT NEEDING AS MUCH SPACE or reducing max node count
private final long[] localNodes = new long[MAX_NODE_COUNT * 3];//1.5x the size of the gpu copy to store extra metadata
//LocalNodes have an up value pointing to the parent, enabling full traversal
private final INodeInteractor interactor;
public NodeManager(INodeInteractor interactor) {
this.interactor = interactor;
this.pos2meshId.defaultReturnValue(NO_NODE);
}
//Returns true if it has its own mesh loaded
private static boolean nodeHasMeshLoaded(long metaA, long metaB) {
return false;
}
private static final int REQUEST_SELF = 0;
private static final int REQUEST_CHILDREN = 1;
//A node can be loaded in the tree but have no mesh associated with it
// this is so that higher level nodes dont waste mesh space
//The reason that nodes have both child and own mesh pointers
// is so that on an edge of the screen or when moving, nodes arnt constantly being swapped back and forth
// it basicly acts as an inline cache :tm: however it does present some painpoints
// especially in managing the graph
//It might be easier to have nodes strictly either point to child nodes or meshes
// if a parent needs to be rendered instead of the child, request for node change to self
// while this will generate a shitton more requests it should be alot easier to manage graph wise
// can probably add a caching service via a compute shader that ingests a request list
// sees if the requested nodes are already cached, if so swap them in, otherwise dispatch a request
// to cpu
private void processRequestQueue(long ptr, long size) {
int count = MemoryUtil.memGetInt(ptr); ptr += 4;
for (int i = 0; i < count; i++) {
int request = MemoryUtil.memGetInt(ptr + i*4L);
int args = request&(0xFF<<24);
int nodeId = request&(0xFFFFFF);
long pos = this.localNodes[nodeId*3];
long metaA = this.localNodes[nodeId*3 + 1];
long metaB = this.localNodes[nodeId*3 + 2];
int type = args&0b11;//2 bits for future request types such as parent and ensure stable (i.e. both parent and child loaded)
if (type == REQUEST_SELF) {
//Requires own mesh loaded (it can have 2 different priorites, it can fallback to using its children to render if they are loaded)
// else it is critical priority
if (nodeHasMeshLoaded(metaA, metaB)) {
throw new IllegalStateException("Node requested a mesh load, but mesh is already loaded: " + pos);
}
//watch the mesh and request it
this.interactor.watchUpdates(pos);
this.interactor.requestMesh(pos);
} else if (type == REQUEST_CHILDREN) {
//Node requires children to be loaded NOTE: when this is the case, it doesnt just mean the nodes,
// it means the meshes aswell,
// meshes may be unloaded later
//when this case is hit it means that the child nodes arnt even loaded, so it becomes a bit more complex
// basicly, need to request all child nodes be loaded in a batch
// then in the upload tick need to do update many things
} else {
throw new IllegalArgumentException("Unknown update type: " + type + " @pos:" + pos);
}
}
}
public void uploadPhase() {
//All uploads
//Have a set of upload tasks for nodes,
// this could include updating the mesh ptr
// or child ptr or uploading new nodes
// NOTE: when uploading a set of new nodes (must be clustered as children)
// have to update parent
// same when removing a set of children
//Note: child node upload tasks need to all be complete before they can be uploaded
//The way the graph works and can be cut is that all the leaf nodes _must_ at all times contain a mesh
// this is critical to prevent "cracks"/no geometry being rendered
// when the render mesh buffer is "full" (or even just periodicly), trimming of the tree must occur to keep
// size within reason
//Note tho that there is a feedback delay and such so geometry buffer should probably be trimmed when it reaches
// 80-90% capacity so that new geometry can still be uploaded without being blocked on geometry clearing
// it becomes a critical error if the geometry buffer becomes full while the tree is fully trimmed
//NOTE: while trimming the tree, need to also trim the parents down i.e. the top level should really not have its mesh
// loaded while it isnt really ever used
// however as long as the rule that all leaf nodes have a mesh loaded is held then there should never be
// any geometry holes
}
//Download and upload point, called once per frame
public void downloadPhase() {
DownloadStream.INSTANCE.download(this.requestQueue, 0, REQUEST_QUEUE_SIZE, this::processRequestQueue);
DownloadStream.INSTANCE.commit();
//Clear the queue counter, TODO: maybe do it some other way to batch clears
nglClearNamedBufferSubData(this.requestQueue.id, GL_R32UI, 0, 4, GL_RED_INTEGER, GL_UNSIGNED_INT, 0);
//TODO: compute cleanup here of loaded nodes, and what needs to be uploaded
// i.e. if there is more upload stuff than there is free memory, cull nodes in the tree
// to fit upload points, can also create errors if all nodes in the tree are requested but no memory to put
}
//Inserts a top level node into the graph, it has geometry and no children loaded as it is a leaf node
public void insertTopLevelNode(long position) {
}
//Removes a top level node from the graph, doing so also removes all child nodes and associate geometry
// the allocated slots when removing nodes are stored and roped off until it is guarenteed that all requests have
// passed
public void removeTopLevelNode(long position) {
}
//Tracking for nodes that specifically need meshes, if a node doesnt have or doesnt need a mesh node, it is not in the map
// the map should be identical to the currently watched set of sections
//NOTE: that if the id is negative its part of a mesh request
private final Long2IntOpenHashMap pos2meshId = new Long2IntOpenHashMap();
private static final int NO_NODE = -1;
//Need to make this system attatched with a batched worker system, since a mesh update can be a few things
// it can be a mesh update of a tracked render section, in this case we must ensure that it is still tracked and hasnt been removed bla bla bla
// if its still valid and tracked then upload it and update the node aswell ensuring sync bla bla bla
// if it was part of a request, then we need to first check that the request still exists and hasnt been discarded B) probably upload it immediatly still
// B) set the request with that section to have been, well, uploaded and the mesh set, (note if the mesh was updated while a request was inprogress/other requests not fufilled, need to remove the old and replace with the updated)
// if all the meshes in the request are satisfied, upload the request nodes and update its parent
// NOTE! batch requests where this is needed are only strictly required when children are requested in order to guarentee that all
// propertiy of leaf nodes must have meshes remains
//(TODO: see when sync with main thread should be, in the renderer or here since the updates are dispatched offthread)
// Note that the geometry buffer should have idk 20% free? that way meshes can always be inserted (same for the node buffer ig) maybe 10%? idk need to experiement
// if the buffer goes over this threshold, the tree/graph culler must start culling last/least used nodes somehow
// it should be an error if the geometry or node buffer fills up but there are no nodes/meshes to cull/remove
public void meshUpdate(BuiltSection mesh) {
int id = this.pos2meshId.get(mesh.position);
if (id == NO_NODE) {
//The built mesh section is no longer needed, discard it
// TODO: could probably?? cache the mesh in ram that way if its requested? it can be immediatly fetched while a newer mesh is built??
mesh.free();
return;
}
if ((id&(1<<31))!=0) {
//The mesh is part of a batched request
id = id^(1<<31);
} else {
//The mesh is an update for an existing node
//this.localNodes[id*3]
}
}
//A node has a position (64 bit)
// a ptr to its own mesh (24 bit)
// a ptr to children nodes (24 bit)
// flags (16 bit)
// Total of 128 bits (16 bytes)
//First 2 flag bits are a requested dispatch type (0 meaning no request and the 3 remaining states for different request types)
// this ensures that over multiple frames the same node is not requested
//Bits exist for whether or not the children have meshes loaded or if the parents have meshes loaded
// the idea is to keep +-1 lod meshes loaded into vram to enable seemless transitioning
// the only critical state is that if a mesh wants to be rendered it should be able to be rendered
//Basicly, there are multiple things, it depends on the screensize error
// if a node is close to needing its children loaded but they arnt, then request it but with a lower priority
// if a node must need its children then request at a high prioirty
// if a node doesnt have a mesh but all its children do than dispatch a medium priority to have its own mesh loaded
// but then just use the child meshes for rendering
}

View File

@@ -1,345 +0,0 @@
package me.cortex.voxy.client.core.rendering.hierarchical;
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
import me.cortex.voxy.client.core.rendering.building.BuiltSection;
import me.cortex.voxy.client.core.rendering.util.MarkedObjectList;
import me.cortex.voxy.common.util.HierarchicalBitSet;
import me.cortex.voxy.common.world.WorldEngine;
import org.lwjgl.system.MemoryUtil;
import java.util.Arrays;
//TODO:FIXME: TODO, Must fix/have some filtering for section updates based on time or something
// as there can be a cursed situation where an update occures requiring expensive meshing for a section but then in the same
// tick it becomes air requiring no meshing thus basicly instantly emitting a result
//TODO: Make it an sparse voxel octree, that is if all of the children bar 1 are empty/air, remove the self node and replace it with the non empty child
public class NodeManager2 {
//A request for making a new child nodes
private static final class LeafRequest {
//LoD position identifier
public long position;
//Node id of the node the leaf request is for, note! While there is a leaf request, the node should not be unloaded or removed
public int nodeId;
//The mask of what child nodes are required
//public byte requiredChildMask;
//The mask of currently supplied child node data
public byte currentChildMask;
//Mesh ids for all the child nodes
private final int[] meshIds = new int[8];
{Arrays.fill(this.meshIds, -1);}
//Positions for all the child nodes, should make SVO easier
private final long[] childPositions = new long[8];
{Arrays.fill(this.childPositions, -1);}
//Reset/clear the request so that it may be reused
public void clear() {
this.position = 0;
this.nodeId = 0;
//this.requiredChildMask = 0;
this.currentChildMask = 0;
Arrays.fill(this.meshIds, -1);
Arrays.fill(this.childPositions, -1);
}
//Returns true if the request is satisfied
public boolean isSatisfied() {
return this.currentChildMask == -1;//(this.requiredChildMask&this.currentChildMask)==this.requiredChildMask;
}
public int getMeshId(int inner) {
if (!this.isSet(inner)) {
return -1;
}
return this.meshIds[inner];
}
public boolean isSet(int inner) {
return (this.currentChildMask&(1<<inner))!=0;
}
public void put(int innerId, int mesh, long position) {
this.currentChildMask |= (byte) (1<<innerId);
this.meshIds[innerId] = mesh;
this.childPositions[innerId] = position;
}
public void make(int id, long position) {
this.nodeId = id;
this.position = position;
}
public byte nonAirMask() {
byte res = 0;
for (int i = 0; i < 8; i++) {
if (this.meshIds[i] != -1) {
res |= (byte) (1<<i);
}
}
return res;
}
}
public static final int MAX_NODE_COUNT = 1<<22;
public static final int NODE_MSK = MAX_NODE_COUNT-1;
//Local data layout
// first long is position (todo! might not be needed)
// next long contains mesh position ig/id
private final long[] localNodeData = new long[MAX_NODE_COUNT * 3];
private final HierarchicalBitSet nodeAllocations = new HierarchicalBitSet(MAX_NODE_COUNT);
private final INodeInteractor interactor;
private final MeshManager meshManager;
public NodeManager2(INodeInteractor interactor, MeshManager meshManager) {
this.interactor = interactor;
this.pos2meshId.defaultReturnValue(NO_NODE);
this.interactor.setMeshUpdateCallback(this::meshUpdate);
this.meshManager = meshManager;
}
public void insertTopLevelNode(long position) {
//NOTE! when initally adding a top level node, set it to air and request a meshing of the mesh
// (if the mesh returns as air uhhh idk what to do cause a top level air node is kinda... not valid but eh)
// that way the node will replace itself with its meshed varient when its ready aswell as prevent
// the renderer from exploding, as it should ignore the empty sections entirly
}
public void removeTopLevelNode(long position) {
}
//Returns the mesh offset/id for the given node or -1 if it doesnt exist
private int getNodeMesh(int node) {
return -1;
}
private long getNodePos(int node) {
return -1;
}
private boolean isLeafNode(int node) {
return true;
}
private void setMeshId(int node, int mesh) {
}
private static long makeChildPos(long basePos, int addin) {
int lvl = WorldEngine.getLevel(basePos);
if (lvl == 0) {
throw new IllegalArgumentException("Cannot create a child lower than lod level 0");
}
return WorldEngine.getWorldSectionId(lvl-1,
(WorldEngine.getX(basePos)<<1)|(addin&1),
(WorldEngine.getY(basePos)<<1)|((addin>>1)&1),
(WorldEngine.getZ(basePos)<<1)|((addin>>2)&1));
}
//IDEA, since a graph node can be in effectivly only 3 states, if inner node -> may or may not have mesh, and, if leaf node -> has mesh, no children
// the request queue only needs to supply the node id, since if its an inner node, it must be requesting for a mesh, while if its a leaf node, it must be requesting for children
private void processRequestQueue(long ptr, long size) {
int count = MemoryUtil.memGetInt(ptr); ptr += 4;
for (int i = 0; i < count; i++) {
int requestOp = MemoryUtil.memGetInt(ptr + i*4L);
int node = requestOp&NODE_MSK;
if (this.isLeafNode(node)) {
//If its a leaf node and it has a request, it must need the children
if (this.getNodeMesh(node) == -1) {
throw new IllegalStateException("Leaf node doesnt have mesh");
}
//Create a new request
int idx = this.leafRequests.allocate();
var request = this.leafRequests.get(idx);
{
long nodePos = this.getNodePos(node);
request.make(node, nodePos);//Request all child nodes
int requestIdx = idx|(1<<31);//First bit is set to 1 to indicate a request index instead of a node index
//Loop over all child positions and insert them into the queue
for (int j = 0; j < 8; j++) {
long child = makeChildPos(nodePos, j);
int prev = this.pos2meshId.putIfAbsent(child, requestIdx);
if (prev != NO_NODE) {
throw new IllegalArgumentException("Node pos already in request map");
}
//Mark the position as watching and force request an update
this.interactor.watchUpdates(child);
this.interactor.requestMesh(child);
}
}
//NOTE: dont unmark the node yet, as the request hasnt been satisfied
} else {
//If its not a leaf node, it must be missing the inner mesh so request it
if (this.getNodeMesh(node) != -1) {
//Node already has a mesh, ignore it, but might be a sign that an error has occured
System.err.println("Requested a mesh for node, however the node already has a mesh");
//TODO: need to unmark the node that requested it, either that or only clear node data when a mesh has been removed
} else {
//Put it into the map + watch and request
long pos = this.getNodePos(node);
long prev = this.pos2meshId.putIfAbsent(pos, node);
if (prev != NO_NODE) {
throw new IllegalStateException("Pos already has a node id attached");
}
this.interactor.watchUpdates(pos);
this.interactor.requestMesh(pos);
}
}
}
}
//Tracking for nodes that specifically need meshes, if a node doesnt have or doesnt need a mesh node, it is not in the map
// the map should be identical to the currently watched set of sections
//NOTE: that if the id is negative its part of a mesh request
private final Long2IntOpenHashMap pos2meshId = new Long2IntOpenHashMap();
private static final int NO_NODE = -1;
//The request queue should be like some array that can reuse objects to prevent gc nightmare + like a bitset to find an avalible free slot
// hashmap might work bar the gc overhead
private final MarkedObjectList<LeafRequest> leafRequests = new MarkedObjectList<>(LeafRequest[]::new, LeafRequest::new);
private static int pos2octnode(long pos) {
return (WorldEngine.getX(pos)&1)|((WorldEngine.getY(pos)&1)<<1)|((WorldEngine.getZ(pos)&1)<<2);
}
//TODO: if all the children of a node become empty/removed traverse up the chain until a non empty parent node is hit and
// remove all from the chain
//TODO: FIXME: CRITICAL: if a section is empty when created, it wont get allocated a slot, however, the section might
// become unempty due to an update!!! THIS IS REALLY BAD. since it doesnt have an allocation
//TODO: test and fix the possible race condition of if a section is not empty then becomes empty in the same tick
// that is, there is a request that is satisfied bar 1 section, that section is supplied as non emptpty but then becomes empty in the same tick
private void meshUpdate(BuiltSection mesh) {
int id = this.pos2meshId.get(mesh.position);
if (id == NO_NODE) {
//The built mesh section is no longer needed, discard it
// TODO: could probably?? cache the mesh in ram that way if its requested? it can be immediatly fetched while a newer mesh is built??
//This might be a warning? or maybe info?
mesh.free();
return;
}
if ((id&(1<<31))!=0) {
//The mesh is part of a batched request
id = id^(1<<31);//Basically abs it
int innerId = pos2octnode(mesh.position);
//There are a few cases for this branch
// the section could be replacing an existing mesh that is part of the request (due to an update)
// the section mesh could be new to the request
// in this case the section mesh could be the last entry needed to satisfy the request
// in which case! we must either A) mark the request as ready to be uploaded
// and then uploaded after all the mesh updates are processed, or upload it immediately
LeafRequest request = this.leafRequests.get(id);
//TODO: Get the mesh id if a mesh for the request at the same pos has already been submitted
// then call meshManager.uploadReplaceMesh to get the new id, then put that into the request
//TODO: could basicly make it a phase, where it then enqueues finished requests that then get uploaded later
// that is dont immediatly submit request results, wait until the end of the frame
// NOTE: COULD DO THIS WITH MESH RESULTS TOO, or could prefilter them per batch/tick
int meshId;
int prevMeshId = request.getMeshId(innerId);
if (mesh.isEmpty()) {
//since its empty, remove the previous mesh if it existed
if (prevMeshId != -1) {
this.meshManager.removeMesh(prevMeshId);
}
meshId = -1;//FIXME: this is a hack to still result in the mesh being put in, but it is an empty mesh upload
} else {
if (prevMeshId != -1) {
meshId = this.meshManager.uploadReplaceMesh(prevMeshId, mesh);
} else {
meshId = this.meshManager.uploadMesh(mesh);
}
}
request.put(innerId, meshId, mesh.position);
if (request.isSatisfied()) {
//If request is now satisfied update the internal nodes, create the children and reset + release the request set
this.completeRequest(request);
//Reset + release
request.clear();
this.leafRequests.release(id);
}
//If the request is not yet satisfied, that is ok, continue ingesting new meshes until it is satisfied
} else {
//The mesh is an update for an existing node
//Sanity check
if (this.getNodePos(id) != mesh.position) {
throw new IllegalStateException("Node position not same as mesh position");
}
int prevMesh = this.getNodeMesh(id);
// TODO: If the mesh to upload is air, the node should be removed (however i believe this is only true if all the children are air! fuuuuu)
if (prevMesh != -1) {
//Node has a mesh attached, remove and replace it
this.setMeshId(id, this.meshManager.uploadReplaceMesh(prevMesh, mesh));
} else {
//Node didnt have a mesh attached, so just set the current mesh
this.setMeshId(id, this.meshManager.uploadMesh(mesh));
}
//Push the updated node to the gpu
this.pushNode(id);
}
}
private void completeRequest(LeafRequest request) {
//TODO: need to actually update all of the pos2meshId of the children to point to there new nodes
int msk = Byte.toUnsignedInt(request.nonAirMask());
int baseIdx = this.nodeAllocations.allocateNextConsecutiveCounted(Integer.bitCount(msk));
for (int i = 0; i < 8; i++) {
if ((msk&(1<<i))!=0) {
//It means the section actually exists,
} else {
//The section was empty, so just remove it
}
}
}
//Invalidates the node and tells it to be pushed to the gpu next slot, NOTE: pushing a node, clears any gpu side flags
private void pushNode(int node) {
}
}

View File

@@ -1,23 +0,0 @@
package me.cortex.voxy.client.core.rendering.hierarchical;
//System to determine what nodes to remove from the hericial tree while retaining the property that all
// leaf nodes should have meshes
//This system is critical to prevent the geometry buffer from growing to large or for too many nodes to fill up
// the node system
public class TreeTrimmer {
//Used to interact with the outside world
private final ITrimInterface trimInterface;
public TreeTrimmer(ITrimInterface trimInterface) {
this.trimInterface = trimInterface;
}
public void computeTrimPoints() {
//Do a bfs to find ending points to trim needs to be based on some, last used, metric
//First stratagy is to compute a bfs and or generate a list of nodes sorted by last use time
// the thing is that if we cull a mesh, it cannot be a leaf node
// if it is a leaf node its parent node must have a mesh loaded
}
}

View File

@@ -7,21 +7,17 @@ import me.cortex.voxy.client.core.gl.shader.ShaderType;
import me.cortex.voxy.client.core.rendering.util.GlStateCapture;
import net.minecraft.client.util.math.MatrixStack;
import org.joml.Matrix4f;
import org.joml.Matrix4fc;
import org.lwjgl.opengl.GL11C;
import static org.lwjgl.opengl.ARBComputeShader.glDispatchCompute;
import static org.lwjgl.opengl.ARBFramebufferObject.*;
import static org.lwjgl.opengl.ARBShaderImageLoadStore.glBindImageTexture;
import static org.lwjgl.opengl.GL11.*;
import static org.lwjgl.opengl.GL13.*;
import static org.lwjgl.opengl.GL15.GL_READ_WRITE;
import static org.lwjgl.opengl.GL20.glUniformMatrix4fv;
import static org.lwjgl.opengl.GL20C.glGetUniformLocation;
import static org.lwjgl.opengl.GL20C.glGetUniformfv;
import static org.lwjgl.opengl.GL30C.*;
import static org.lwjgl.opengl.GL30C.GL_DEPTH24_STENCIL8;
import static org.lwjgl.opengl.GL43.GL_DEPTH_STENCIL_TEXTURE_MODE;
import static org.lwjgl.opengl.GL44C.glBindImageTextures;
import static org.lwjgl.opengl.GL45C.glBlitNamedFramebuffer;
import static org.lwjgl.opengl.GL45C.glTextureParameterf;
import static org.lwjgl.opengl.GL45C.*;
public class PostProcessing {
private final GlFramebuffer framebuffer;
@@ -32,6 +28,7 @@ public class PostProcessing {
private GlTexture colourSSAO;
private GlTexture depthStencil;
private boolean didSSAO;
private final FullscreenBlit setDepth0 = new FullscreenBlit("voxy:post/depth0.frag");
private final FullscreenBlit emptyBlit = new FullscreenBlit("voxy:post/noop.frag");
//private final FullscreenBlit blitTexture = new FullscreenBlit("voxy:post/blit_texture_cutout.frag");
private final FullscreenBlit blitTexture = new FullscreenBlit("voxy:post/blit_texture_depth_cutout.frag");
@@ -71,16 +68,15 @@ public class PostProcessing {
glTextureParameterf(this.colour.id, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTextureParameterf(this.colourSSAO.id, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTextureParameterf(this.colourSSAO.id, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
//glTextureParameterf(this.depthStencil.id, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
//glTextureParameterf(this.depthStencil.id, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTextureParameterf(this.depthStencil.id, GL_DEPTH_STENCIL_TEXTURE_MODE, GL_DEPTH_COMPONENT);
this.framebuffer.bind(GL_COLOR_ATTACHMENT0, this.colour);
this.framebuffer.bind(GL_DEPTH_STENCIL_ATTACHMENT, this.depthStencil);
this.framebuffer.verify();
this.framebuffer.bind(GL_COLOR_ATTACHMENT0, this.colour)
.bind(GL_DEPTH_STENCIL_ATTACHMENT, this.depthStencil)
.verify();
this.framebufferSSAO.bind(GL_COLOR_ATTACHMENT0, this.colourSSAO);
this.framebufferSSAO.bind(GL_DEPTH_STENCIL_ATTACHMENT, this.depthStencil);
this.framebufferSSAO.verify();
this.framebufferSSAO.bind(GL_COLOR_ATTACHMENT0, this.colourSSAO)
.bind(GL_DEPTH_STENCIL_ATTACHMENT, this.depthStencil)
.verify();
}
}
@@ -93,16 +89,23 @@ public class PostProcessing {
if (this.colour != null) this.colour.free();
if (this.depthStencil != null) this.depthStencil.free();
this.emptyBlit.delete();
this.setDepth0.delete();
this.blitTexture.delete();
this.ssaoComp.free();
}
public void setup(int width, int height, int sourceFB) {
//TODO: use the raw depth texture instead
//TODO: when blitting, also set the depth value of where the mask is created to 0 (I.E. closest to the camera)
// cause of hiz computing, it makes alot of sections visible
this.didSSAO = false;
this.glStateCapture.capture();
this.setSize(width, height);
glBindFramebuffer(GL_FRAMEBUFFER, this.framebuffer.id);
//glClearColor(0,0,0,0);//TODO: RESTORE THIS TO THE ORIGINAL VALUE
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
glBlitNamedFramebuffer(sourceFB, this.framebuffer.id, 0,0, width, height, 0,0, width, height, GL_DEPTH_BUFFER_BIT, GL_NEAREST);
@@ -116,16 +119,16 @@ public class PostProcessing {
glDepthMask(false);
glColorMask(false,false,false,false);
this.emptyBlit.blit();
glColorMask(true,true,true,true);
glDepthMask(true);
glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
//Set depth to 0 w.r.t mask
glStencilFunc(GL_EQUAL, 0, 0xFF);
this.setDepth0.blit();
glDisable(GL_DEPTH_TEST);
//Clear the depth buffer we copied cause else it will interfear with results (not really i think idk)
glClear(GL_DEPTH_BUFFER_BIT);
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);
@@ -134,24 +137,19 @@ public class PostProcessing {
//Computes ssao on the current framebuffer data and updates it
// this means that translucency wont be effected etc
public void computeSSAO(Matrix4f projection, MatrixStack stack) {
public void computeSSAO(Matrix4f mvp) {
this.didSSAO = true;
this.ssaoComp.bind();
float[] data = new float[4*4];
var mat = new Matrix4f(projection).mul(stack.peek().getPositionMatrix());
mat.get(data);
mvp.get(data);
glUniformMatrix4fv(3, false, data);//MVP
mat.invert();
mat.get(data);
mvp.invert(new Matrix4f()).get(data);
glUniformMatrix4fv(4, false, data);//invMVP
glBindImageTexture(0, this.colourSSAO.id, 0, false,0, GL_READ_WRITE, GL_RGBA8);
glActiveTexture(GL_TEXTURE1);
GL11C.glBindTexture(GL_TEXTURE_2D, this.depthStencil.id);
glTexParameteri (GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE, GL_DEPTH_COMPONENT);
glActiveTexture(GL_TEXTURE2);
GL11C.glBindTexture(GL_TEXTURE_2D, this.colour.id);
glBindTextureUnit(1, this.depthStencil.id);
glBindTextureUnit(2, this.colour.id);
glDispatchCompute((this.width+31)/32, (this.height+31)/32, 1);
@@ -160,7 +158,7 @@ public class PostProcessing {
//Executes the post processing and emits to whatever framebuffer is currently bound via a blit
public void renderPost(Matrix4f fromProjection, Matrix4f tooProjection, int outputFB) {
public void renderPost(Matrix4f fromProjection, Matrix4fc tooProjection, int outputFB) {
glDisable(GL_STENCIL_TEST);
@@ -181,11 +179,11 @@ public class PostProcessing {
glUniformMatrix4fv(3, false, data);//tooProjection
glActiveTexture(GL_TEXTURE1);
GL11C.glBindTexture(GL_TEXTURE_2D, this.depthStencil.id);
glTexParameteri (GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE, GL_DEPTH_COMPONENT);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, this.didSSAO?this.colourSSAO.id:this.colour.id);
glBindTextureUnit(0, this.didSSAO?this.colourSSAO.id:this.colour.id);
glBindTextureUnit(1, this.depthStencil.id);
//glTextureParameteri(this.depthStencil.id, GL_DEPTH_STENCIL_TEXTURE_MODE, GL_DEPTH_COMPONENT);
glEnable(GL_DEPTH_TEST);
glDepthMask(true);
this.blitTexture.blit();

View File

@@ -0,0 +1,32 @@
package me.cortex.voxy.client.core.rendering.section;
import me.cortex.voxy.client.core.gl.GlTexture;
import me.cortex.voxy.client.core.model.ModelStore;
import me.cortex.voxy.client.core.rendering.Viewport;
import me.cortex.voxy.client.core.rendering.section.geometry.IGeometryData;
import java.util.List;
//Takes in mesh ids from the hierachical traversal and may perform more culling then renders it
public abstract class AbstractSectionRenderer <T extends Viewport<T>, J extends IGeometryData> {
protected final J geometryManager;
protected final ModelStore modelStore;
protected AbstractSectionRenderer(ModelStore modelStore, J geometryManager) {
this.geometryManager = geometryManager;
this.modelStore = modelStore;
}
public abstract void renderOpaque(T viewport, GlTexture depthBoundTexture);
public abstract void buildDrawCalls(T viewport);
public abstract void renderTemporal(T viewport, GlTexture depthBoundTexture);
public abstract void renderTranslucent(T viewport, GlTexture depthBoundTexture);
public abstract T createViewport();
public abstract void free();
public J getGeometryManager() {
return this.geometryManager;
}
public void addDebug(List<String> lines) {}
}

Some files were not shown because too many files have changed in this diff Show More