Compare commits

..
174 Commits
Author SHA1 Message Date
Jason Mitchell 6570b6a0a5 Properly handle flags removed in core (#1965) 2026-07-26 21:46:30 -07:00
Jason Mitchell 493b641d56 Switch to an offset based cloud adjustment for World Providers that adjust it (#1962) 2026-07-25 15:35:40 -07:00
Eclipse-Sol 3f4995466c Fix scrollbar not following mouse (#1961)
* Fix scrollbar not following mouse

* add /*deobfName*/
2026-07-24 21:50:46 -04:00
Eclipse-Sol 56d7568fd3 Attempt at making mipmaps better (#1952) 2026-07-24 17:54:45 -07:00
Jason Mitchell 4910cfd6d3 Fix some CMEs and black screens seen during boot/splash. Should be minimal overhead after splash. (#1959) 2026-07-23 19:33:01 -07:00
Jason Mitchell d1c6677caf Fix plascrete/cleanroom trophy rendering (#1957) 2026-07-23 13:42:46 -07:00
Jason Mitchell 8cf0296284 Properly gate end flash (#1954) 2026-07-23 09:34:41 -07:00
Jason Mitchell 4fd98e5026 Add some missing buffer redirects (#1953) 2026-07-23 08:34:11 -07:00
SpacEagle17 8e293f13fb Fix at_midBlock at high coordinates (#1949) 2026-07-22 18:02:55 -07:00
Jason Mitchell 2db20ad866 Handle some Intel driver shenanigans (#1951) 2026-07-21 08:05:46 -07:00
Eclipse-Sol 10a5b550f2 Add Uniforms for End Flashes (#1892) 2026-07-18 06:42:57 -07:00
Eldrinn-Elantey c1403dcf13 Reload unicode glyph widths on resource pack change (#1944) 2026-07-18 05:58:45 -07:00
tiffit 5d1fabfca9 Add API for allowing other mods to have celeritas support (#1936) 2026-07-16 09:54:55 -05:00
tiffit 555b2b7dd2 Fix typo in PR template (#1937) 2026-07-14 00:10:41 -05:00
SuperSouprandJason Mitchell 7017786a7c CTM integration improvements (#1931)
Co-authored-by: Jason Mitchell <[email protected]>
2026-07-12 10:45:16 -05:00
Eclipse-Sol 1c49649685 Fix Streams not rendering correctly (#1932) 2026-07-12 10:41:36 -05:00
Caedis 2bae79c4ee Up default zoom speed to 20 (#1910) 2026-07-09 21:34:03 -04:00
Jason Mitchell c22935660a Reenable compute shaders (#1918) 2026-07-05 20:38:13 -07:00
tiffit 79c488102b CTM: Fix "flipped" value not set to false in setFace() (#1913) 2026-07-05 18:24:28 -07:00
Jason Mitchell cc1353b73a Fix infinite farplane causing incorrect culling in pdim with no clouds (#1917) 2026-07-05 18:18:17 -07:00
Eclipse-Sol d1e6dc8c94 Fix/bug fixes (#1905) 2026-07-04 13:06:36 -07:00
GribanovIvan ecbf8fea2f font: load bundled .ttf/.otf fonts from fontfiles/ and config/angelia/fonts/ (#1904) 2026-07-04 12:16:48 -07:00
tiffit 610209605f Compact CTM: Decouple block face and rendered face (#1907) 2026-07-04 03:22:04 +03:00
mist475andJason Mitchell 112aae3735 Fix lotr biome colours again (essentially copy of https://github.com/GTNewHorizons/Angelica/pull/25) (#1902)
Co-authored-by: Jason Mitchell <[email protected]>
2026-07-03 14:21:05 -07:00
RecursivePineapple 5844b1b5c1 Cubic Chunks Compatibility (#1808) 2026-07-03 12:41:04 -07:00
Jason Mitchell 05f01c17e6 No point continuously building clouds if we're not rendering them... (#1900) 2026-06-29 20:17:06 -07:00
SpacEagle17 f07d96316a Add shader setting search functionality and center selection (#1897) 2026-06-29 19:36:55 -07:00
Jason Mitchell 5b704d1bc2 Fix space elevator cable (#1896) 2026-06-28 16:33:58 -07:00
Jason Mitchell 26135124f4 Support more glDrawElement overloads in display lists (#1895) 2026-06-28 15:07:24 -07:00
Jason Mitchell 3abc89b7b2 Betweenlands fix (#1894) 2026-06-28 13:46:10 -07:00
Jason Mitchell 03d908d658 Update readme and version detectors (#1893) 2026-06-27 15:47:32 -07:00
Eclipse-Sol 76bf19c8f6 Majority shader fixes (#1868) 2026-06-27 14:48:51 -07:00
Eclipse-Sol b27834cbd9 Narrow and make damage overlay a config option (#1886) 2026-06-27 10:12:39 -07:00
sisyphussyandJason Mitchell eb0ecb0483 Fix deps (#1888)
Co-authored-by: Jason Mitchell <[email protected]>
2026-06-27 10:11:30 -07:00
Caedis 9acb0e67a7 feat: add smooth zoom option (#1890) 2026-06-27 09:57:21 -07:00
Jason Mitchell f4f9b56b17 End portal fix (#1891) 2026-06-27 09:56:21 -07:00
Midnight145 2196439e6c ClientEvent framework (primarily written for LiteloaderLoader) (#1843) 2026-06-25 20:13:18 -07:00
RedNicStone dd68c64239 Fix map rendering issue in xaero's map mods (#1880) 2026-06-21 14:13:11 -07:00
Jason Mitchell 0decc515e4 Fix universium shader (#1877) 2026-06-18 23:42:21 -07:00
tiffit b3e1e89c88 Add new CTM mode "compact_expanded" to generate textures on runtime (#1840)
* Rewrite compact CTM to generate the textures ahead of time

* fix animated textures not working

* only interpolate if notfine is enabled

* some bugfixes

* bring back old compact ctm and make a new compact_expanded

* bug fixes
2026-06-18 12:04:45 +03:00
Jason Mitchell 32bff85526 Handle glDrawArraysInstanced and variants, as well as dynamic TE bounds. (#1874) 2026-06-17 20:18:07 -07:00
Auynonymous 02fc95f239 Make Glass CTM Compact, Add Stained Glass Compact CTM (#1873) 2026-06-17 16:00:58 -07:00
MalTeeez 07fd6d78a0 Separate out entity overlay render mixins (#1872) 2026-06-17 19:55:12 +00:00
Jason Mitchell 87aa853f81 Attempt to fix deferred particle batching (#1871) 2026-06-16 22:16:26 -07:00
Jason Mitchell ab69eea654 Announce releases (#1867) 2026-06-15 14:50:47 -07:00
Jason Mitchell edccf63b66 Bump glsl-transformation-lib and buildscript (#1865) 2026-06-14 18:09:05 -07:00
MalTeeez 80413fb3de Add policy for AI contributions & a PR template (#1864) 2026-06-15 01:40:03 +02:00
Jason Mitchell f3ed9aa82b Force invalidate dh depth tex when reconnecting DH Textures (#1862) 2026-06-14 14:59:01 -07:00
Jason Mitchell 307c563b6d Rework DH matrix handling (#1861) 2026-06-14 14:20:19 -07:00
Eclipse-Sol f92ce1d24e Fix enchant glint fix so that it looks proper (#1856) 2026-06-13 17:28:12 -07:00
sisyphussy a7fac16d97 VAO stuff (#1858) 2026-06-13 17:26:09 -07:00
Jason Mitchell 20c3caaed4 A few redirects for calls that DH was using that we weren't redirecting (#1859) 2026-06-13 16:50:44 -07:00
Jason Mitchell bf08cd50d9 Support glPixelStore in display lists (#1854) 2026-06-11 20:29:14 -07:00
Eclipse-Sol 805a266059 Support extended metas for NEID and EID (#1853) 2026-06-11 17:58:22 -07:00
Eclipse-Sol d3c9963280 Fix ctm crash due to null (#1852) 2026-06-11 18:02:08 -04:00
Eclipse-Sol 4630bc4fd8 Fix shaders not loading the correct dim (#1844) 2026-06-09 23:12:13 -07:00
Jason Mitchell ce32a3d3df Emulate glTexParameter(pname=GL_GENERATE_MIPMAP) on core (#1845) 2026-06-09 23:03:42 -07:00
Eclipse-Sol 1991dc58ec Fix Steadfast not loading right (#1838) 2026-06-08 20:06:29 -07:00
Eclipse-Sol ae19236e1a Remove force hand animation (#1839) 2026-06-08 20:05:49 -07:00
Kotmatross 7ad368c937 Be less lazy (#1834) 2026-06-08 17:03:36 -07:00
Eclipse-Sol 5c86c87bcf Allow devs to target the dragon death rays as its own entity id (#1836) 2026-06-08 17:03:05 -07:00
tiffit 99ba7fa775 Remove CTM adding .png to tile paths (#1832) 2026-06-07 16:05:10 -07:00
Eclipse-Sol 5ae4ab8c5c More specific shader fixes pertaining to EP (#1831) 2026-06-07 11:19:30 -07:00
madoha ddd68e023f Add Japanese Localizations (#1830) 2026-06-07 11:14:20 -07:00
Eclipse-Sol f78cfdd5fb Update and fix Iris' shader menus (#1828) 2026-06-06 17:12:38 -07:00
Jason Mitchell 183bcfbf41 Narrow more transformer exclusions (#1827) 2026-06-06 15:38:05 -07:00
tiffit 502f30c0f9 Fix bottom-face being flipped for compact CTM (#1820) 2026-06-05 16:14:14 -07:00
Jason Mitchell ceff04adba Fixes a regression with TFC. (#1822) 2026-06-05 16:13:30 -07:00
Eclipse-Sol b4dfdb8cc5 Fix beacon beams (#1821) 2026-06-05 16:03:46 -07:00
Eclipse-Sol 3064818477 Link up block outline shader to iris shaders (#1814) 2026-06-05 11:25:31 -07:00
Eclipse-Sol 27524e6a89 Add double_plant handling to match modern (#1815) 2026-06-05 08:36:14 -07:00
Eclipse-Sol 5d5e910fc0 Match modern hand swap animations (#1816) 2026-06-05 08:35:07 -07:00
Jason Mitchellandsisyphussy f325fd7fc3 Be more lazy (#1794)
* Be more lazy

---------

Co-authored-by: sisyphussy <[email protected]>
2026-06-05 08:29:27 -07:00
Jason Mitchell ec278d3d7a Upload fewer things (#1811) 2026-06-04 13:11:59 -07:00
Caedis eef3d2f50a feat(hand): add cancellable RenderHandEvent for canRender (#1806) 2026-06-01 20:38:27 +00:00
tiffit 3da597ab18 Improve perf of EmissiveTextureAutoloader.resourceExists (#1805) 2026-06-01 13:34:23 -07:00
Jason Mitchell 0e79380618 More class loading order fun with java 8... (#1804) 2026-05-31 20:56:00 -07:00
tiffit b639d5c646 Add compat for hodgepodge's async icon loading (#1799) 2026-05-31 15:28:27 -07:00
Jason Mitchell 0d209d426d Document why I did this so I remember next time someone asks about this cursed function (#1802) 2026-05-31 15:12:45 -07:00
danyadev bf24084033 switch to new RFB transform signature (#1421) 2026-05-29 09:35:31 -07:00
tiffit 9267ecc735 Remove registerIcons call in MCPF MixinTextureMap (#1795) 2026-05-28 13:12:10 -07:00
dcc8d8deed Make shader based clouds work with iris shaderpacks, and port optimizations from #1685 (#1793)
* Make shader based clouds work with iris shaderpacks, and port optimizations from #1685

Co-Authored-By: Eclipse-Sol <[email protected]>

* Yeet GL_QUADS

* Optimize a little more

---------

Co-authored-by: Eclipse-Sol <[email protected]>
Co-authored-by: Logan Phillips <[email protected]>
2026-05-28 10:31:17 -04:00
Jason Mitchell 5dc4f1fab8 Fix tests (#1791) 2026-05-27 18:41:41 -07:00
CaedisandJason Mitchell d3cea1bfe4 fix: refresh ClonedChunkSection biome data on cache hit (#1769)
Co-authored-by: Jason Mitchell <[email protected]>
2026-05-27 16:45:29 -07:00
DanandRanzu 5db2b248d7 add per-segment drop shadow (&u) (#1754)
Co-authored-by: Ranzu <[email protected]>
2026-05-27 16:44:54 -07:00
Eclipse-Sol 21ab526284 Fix shadow map from being clobbered on Windows (#1789) 2026-05-27 16:43:51 -07:00
MassAnarchy 4c8ea679ef Fix GL_FEEDBACK readback using correct attribute 0 VBO (#1783) 2026-05-27 16:43:03 -07:00
Eclipse-Sol 2317379df2 Add gbuffer_lightning (#1787) 2026-05-27 16:41:51 -07:00
Death Fuel 24dae49c7f Mark ClientFixer as incompatible (#1786) 2026-05-26 16:03:22 -07:00
Eclipse-Sol 865de3cbab Fix issues that directly affects Euphoria Patches + Complementary (#1782) 2026-05-26 14:11:33 -07:00
Jason Mitchell 9c45549e35 Resolve generic compressed image formats (#1781) 2026-05-25 11:50:18 -07:00
Jason Mitchell 2a20198292 Fixes crash with FFP shader linking (#1780) 2026-05-24 17:31:46 -07:00
Eclipse-SolandJason Mitchell f0b68f93af Port modern's damage overlay (#1777)
Co-authored-by: Jason Mitchell <[email protected]>
2026-05-24 10:48:42 -07:00
Eclipse-SolandJason Mitchell dd1d3aa518 Add NBT matching to list of valid block/item entries (#1771)
Co-authored-by: Jason Mitchell <[email protected]>
2026-05-23 23:08:48 -07:00
Jason Mitchell c9633fd745 Fix osx crash (#1778) 2026-05-23 22:03:59 -07:00
DarkShadow44 e2e94b2958 Fix DH framebuffer UNSUPPORTED after resize (#1741) 2026-05-23 21:19:45 -07:00
tiffit 3046b043a0 clear block and item renderlist on resource reload (#1773) 2026-05-23 17:45:31 -07:00
Apis035andCaedis 8a6c8035db Add view bobbing mode (#1761)
* Add view bobbing mode

* Fix missing import

---------

Co-authored-by: Caedis <[email protected]>
2026-05-21 23:11:35 +02:00
Alexdoru 2b15370c00 Properly do the fml/rfb registration for IsbrhTessellatorAbuseTransformer (#1770) 2026-05-20 09:56:16 -07:00
Jason Mitchell 3ca9e32191 Stop burying the error due to things catching RuntimeExceptions.... (#1767) 2026-05-19 08:53:05 -07:00
sisyphussy efbca29381 Add cached block item renderer (#1730) 2026-05-19 08:03:51 -07:00
Jason Mitchell 9c1b6c75ff Fix crash on windows (#1766) 2026-05-19 08:02:37 -07:00
sisyphussy fa91b70528 Display list stuff (#1750) 2026-05-18 12:46:48 -07:00
Jason Mitchell 61de8385ca Minecraft is a dirty dirty liar. (#1764) 2026-05-18 12:39:30 -07:00
Jason Mitchell 962e7527a4 Register later (#1763) 2026-05-18 12:39:20 -07:00
Jason Mitchell 2ab2713fe9 Stops draw/startDrawing in ISBRH (#1759) 2026-05-17 21:08:18 -07:00
Jason Mitchell 7cf652cd4e Misc fixes - Journeymap and Xaeros (#1756) 2026-05-17 10:23:10 -07:00
Jason Mitchell 48eba2fd96 Properly end the dynamic lighting profiler section (#1751) 2026-05-16 12:20:09 -07:00
Apis035 356ba376fe Adjust options screen width (#1748) 2026-05-15 08:47:28 -07:00
Eclipse-Sol ef6b0742a9 Fix skeleton legs from being jammed into their pelvis (#1747) 2026-05-14 15:26:25 -07:00
Kaiz 787929d0bd Handle recoverable block render failures in celeritas meshing (#1705) 2026-05-10 10:00:43 -07:00
0e1fd9d0e9 add auto emissive overlay for any block (#1710)
Co-authored-by: VortexSo4 <[email protected]>
Co-authored-by: ah-OOG-ah <[email protected]>
2026-05-10 02:12:08 -04:00
Ruling-0 58938e82a7 Compact CTM Fixes (#1726) 2026-05-09 11:42:42 -07:00
Branan Riley be6c8c85b5 Detect vanilla AO at a higher level (#1733) 2026-05-09 11:41:59 -07:00
ABKQPOandCaedis 39bca37d0d Adjust IDynamicLightProducer to allow passing an ItemStack parameter (#1734)
Co-authored-by: Caedis <[email protected]>
2026-05-08 20:31:22 -07:00
czqwq 1c61be04a7 Fix Already tesselating logs (#1729) 2026-05-08 20:30:08 -07:00
7a97cd9386 Add RGB color system (#1669)
Co-authored-by: Death Fuel <[email protected]>
Co-authored-by: Jason Mitchell <[email protected]>
2026-05-08 18:36:51 -07:00
Kotmatross 5b09bde7a0 [NTM:Space] Fix mixins in obfuscated environment (#1738) 2026-05-08 18:24:29 +02:00
Ruling-0 edc0081b7c Fix CTM inventory texture bleed (#1721) 2026-05-04 12:25:51 -07:00
Alexdoru 913256b032 fix world client leak in CTMUtils (#1724) 2026-05-04 12:24:25 -07:00
sisyphussy e1f5083d3e Fix intel detection (#1722) 2026-05-04 12:23:50 -07:00
sisyphussy b55e693ad2 Remove DSA from GLSM & other stuff (#1723) 2026-05-04 12:23:38 -07:00
sisyphussy 21c01a1aa4 Fix terrible performance on Intel (#1718) 2026-05-03 14:32:58 -07:00
Caedis 83a26b2291 Add Cosmetic Armor support for Dynamic Lights (#1708) 2026-05-03 14:31:49 -07:00
Kotmatross 7e44c1efcb Fix stale uniforms when switching shader program (#1715) 2026-05-03 14:30:46 -07:00
VortexSo4 155f209db5 remove ctm guide as it is worse than wiki (#1706) 2026-04-30 11:03:18 -07:00
SpacEagle17 c4a6c165a0 Increase MAX_COLOR_BUFFERS limit to 32 (#1704) 2026-04-28 12:33:51 -07:00
Eclipse-Sol 624fe6b453 Add targeting of snow-covered blocks. (#1696) 2026-04-26 15:26:10 -07:00
YoshyandJason Mitchell fd45fda0ab Fix AO Lighting for compact CTM (#1697)
Co-authored-by: Jason Mitchell <[email protected]>
2026-04-26 12:55:24 -07:00
Alexdoru 00fc97a611 Fix dynamic lights world client leaks (#1700) 2026-04-26 10:01:46 -07:00
Alexdoru 445dc487b9 fix world client leak caused by lambda in IrisExclusiveUniforms$WorldInfoUniforms (#1701) 2026-04-26 09:56:08 -07:00
Yoshy f43fbd4d9f Fix CTM to not spread to nearby TEs (#1695) 2026-04-25 13:09:38 -07:00
Jason Mitchell dbabc4b3dc Maybe fix DragonAPI on OSX - take two (#1689)
* Maybe fix DragonAPI on OSX - take two
* Bind a default VAO on the SharedDrawable
* Mixin that tries to swap out glGetProgramInfoLog when appropriate

* no direct gl calls
2026-04-21 15:12:02 -07:00
Jason Mitchell e01097264b Fix infinite extent AABB crash w/ kotlin, and ensure proper sizing/padding on VAO/VBO snapshots. (#1687) 2026-04-20 10:11:46 -07:00
Jason Mitchell 0a922f3129 AE2 Crash Fix (#1684)
* Snapshot all relevant data into display lists (Avoids referencing stale/recycled data and a crash on Nvidia)
* Route deletes through GLStateManager
2026-04-19 08:19:02 -07:00
Eclipse-Sol 4a2dcd0b22 Automate the collection of legacy IDs for very old shaderpacks. (#1631) 2026-04-18 09:53:06 -07:00
Eclipse-Sol 0db691db0b Add FlatteningMap and EntityFlatteningMap (#1635) 2026-04-18 09:20:24 -07:00
danyadev e5cd1372e7 always request 0.9.0+ GTNHLib version when a player has an outdated version (#1672) 2026-04-18 09:18:28 -07:00
Kotmatross fbd07cc575 NTM:Space compat rework and update (#1675) 2026-04-18 09:14:49 -07:00
George Paton 91f2cf9dfb Add "Mods Only" Dynamic FOV option (#1680) 2026-04-18 09:13:39 -07:00
Kaiz 34a6967c71 fix(iris): implement full GL alpha test (#1673) 2026-04-18 09:12:46 -07:00
Alexdoru 9c295a1e22 Fix cached item in RenderItemFrame leaking both world client and server (#1659) 2026-04-13 15:04:36 -07:00
DarkShadow44 9fd02900ef Load modern block properties (#1626) 2026-04-13 15:03:23 -07:00
Jason Mitchell 27e9a4d4fd Fix class circularity error with early redirector (#1670) 2026-04-13 15:02:26 -07:00
Darren Eberly dfc08053d5 Don't advance buffer positions for Material shininess in GLSM (#1668) 2026-04-12 22:25:25 -07:00
VortexSo4 1af03fa369 add CTM guide (#1667) 2026-04-11 17:49:38 -07:00
NealDeal34 3ae3b2527d Update zh_CN.lang (#1623)
* Update zh_CN.lang

* update zh_CN.lang for dynamic lights
2026-04-07 08:21:34 +02:00
Jason Mitchell fd2a14a000 Various fixes (#1652) 2026-04-06 17:35:15 -07:00
DarkShadow44 9f0f6a6232 Always render clouds late for shaders (#1649) 2026-04-06 09:52:34 -07:00
DarkShadow44 2942c7eb84 Disable clouds when shaders are active (#1648) 2026-04-06 09:52:21 -07:00
Alexdoru 47762f921e fix two world client memory leaks (#1650) 2026-04-06 16:38:03 +00:00
Jason Mitchell bdc4431760 Fix some issues someone was having with nixos (#1647) 2026-04-05 16:21:58 -07:00
DarkShadow44 7e1beadfb5 DH: Fix mismatch from upstream for solas shaders (#1645) 2026-04-04 17:27:19 -07:00
Jason Mitchell 59a4b84023 Standalone GLSM mod - Umbra (#1642) 2026-04-03 21:51:35 -07:00
Jason Mitchell 8d53e25e0a Compact CTM Fix (#1640) 2026-04-03 16:48:08 -07:00
VortexSo4 bbf83db379 Implement compact ctm (#1636) 2026-04-02 22:32:28 +00:00
Jason Mitchell ec82280b9b Match OpenGL Buffer Behavior for glGet (#1637) 2026-04-02 14:20:06 -07:00
DarkShadow44andJason Mitchell d7588e4c14 Adapt shader block id override for Angelica 2.x (#1601)
Co-authored-by: Jason Mitchell <[email protected]>
2026-04-02 14:18:55 -07:00
Jason Mitchell 54c99386d4 Do a similar fix as MixinDroppedItemGlintEdges to held items (#1632) 2026-03-31 18:31:21 -07:00
DarkShadow44 465abf889a Pass mc_Entity like upstream does (#1627) 2026-03-31 16:18:35 -07:00
DarkShadow44 4acb83be6a Port shadow translucent (aka shadow_water) from upstream (#1628) 2026-03-31 16:09:25 -07:00
DarkShadow44 80f9724ee6 Handle GL_QUAD_STRIP in legacy conversion (#1629) 2026-03-31 15:57:57 -07:00
Jason Mitchell 8cf55bb216 Safely handle off client thread crash reports/glGetString (#1630) 2026-03-31 15:49:47 -07:00
Eclipse-Sol 3ebc8868c7 Fix item reset hopefully for the last time (#1621) 2026-03-30 19:32:47 -07:00
JohnButlerThird 0938e6d975 Only run the logic on bop grass (fix my mod bugging out) (#1620) 2026-03-30 18:17:42 -07:00
Eclipse-Sol d3022a40d2 Create new deferred overlay system to fix z-fighting issues (#1605) 2026-03-30 15:50:18 -07:00
Eclipse-Sol 2645c803f9 Better horse support (#1599) 2026-03-30 15:41:53 -07:00
Jason Mitchell d91c356a99 Shader fixes (#1619) 2026-03-30 15:34:54 -07:00
Jason Mitchell 189ff4a80f Defer client attributes. They're uploaded/handled elsewhere. Should fix a crash on j8/lwjgl2 (#1618) 2026-03-30 09:58:40 -07:00
Jason Mitchell ffb4ca9472 Get rid of some allocations, and use immutable variants for the suppliers (#1612) 2026-03-30 09:58:27 -07:00
Jason Mitchell 7d8c8d306d More Double overloads (#1613) 2026-03-29 15:09:13 -07:00
Kotmatross 117ff0cacf Fix other dynamic uniforms not updating properly (#1610) 2026-03-29 12:53:09 -07:00
Jason Mitchell 83fdd57534 Narrow another overly broad transformer exclusion 2026-03-29 09:24:50 -07:00
Kaiz 951be04e9a fix: correctly handle cross-chunk TileEntity render bounds 2026-03-29 09:20:13 -07:00
656 changed files with 25819 additions and 6564 deletions

No files matched your search

+14
View File
@@ -0,0 +1,14 @@
# Description of your *PR* and other info
<!-- Please include a summary of the change -->
<!-- Any details that you think are important to review this PR? -->
<!-- Are there any Issues / PRs related to this one? -->
<!-- Add a screenshot or a video demonstration for new features -->
<details open><summary>Template</summary> <!-- OPEN is intentional -->
### Before submitting a *pull request* make sure you have:
- [ ] Read the [AI Contribution Policy](https://github.com/GTNewHorizons/Angelica/blob/master/AI_POLICY.md)
- [ ] At least [skimmed](https://github.com/GTNewHorizons/Angelica/search?q=is%3Apr&type=Issues) the tracker for similar pull requests
</details>
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env bash
set -euo pipefail
: "${REPO:?REPO required}"
: "${TAG:?TAG required}"
if [ -z "${DISCORD_WEBHOOK_URL:-}" ]; then
echo "DISCORD_WEBHOOK_URL not set, skipping"
exit 0
fi
if ! release_json=$(gh release view "$TAG" --repo "$REPO" --json body,url,publishedAt 2>&1); then
echo "Could not read release $TAG in $REPO; skipping:"
printf '%s\n' "$release_json"
exit 0
fi
body=$(jq -r '.body // ""' <<<"$release_json")
url=$(jq -r '.url' <<<"$release_json")
ts=$(jq -r '.publishedAt // ""' <<<"$release_json")
[ -n "$ts" ] || ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
payload=$(jq -n \
--arg title "$TAG" \
--arg url "$url" \
--arg desc "$body" \
--arg ts "$ts" \
'{
embeds: [{
title: ("Angelica " + $title + " released"),
url: $url,
description: (if ($desc | length) > 3900
then $desc[0:3900] + "\n\n...[full changelog](" + $url + ")"
else $desc end),
color: 3066993,
footer: { text: "GitHub Releases" },
timestamp: $ts
}]
}')
curl -fsS -X POST -H "Content-Type: application/json" -d "$payload" "$DISCORD_WEBHOOK_URL"
+19
View File
@@ -12,3 +12,22 @@ jobs:
release-tags:
uses: GTNewHorizons/GTNH-Actions-Workflows/.github/workflows/release-tags.yml@master
secrets: inherit
notify-discord:
needs: [release-tags]
runs-on: ubuntu-24.04
if: >-
${{ !endsWith(github.ref_name, '-snapshot')
&& !contains(github.event.head_commit.message, '[snapshot]')
&& !endsWith(github.ref_name, '-pre') }}
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Announce release to Discord
shell: bash
env:
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_RELEASE_WEBHOOK_URL }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
TAG: ${{ github.ref_name }}
run: bash .github/scripts/announce-release.sh
+2 -1
View File
@@ -3,7 +3,7 @@
/.idea/
/.vs/
/.vscode/
/run/
run/
build/
/eclipse/
.classpath
@@ -12,6 +12,7 @@ build/
/config/
/crash-reports/
/logs/
tmp/
options.txt
/saves/
usernamecache.json
+55
View File
@@ -0,0 +1,55 @@
# AI Usage Policy
This project has strict rules for AI usage:
* **The human-in-the-loop must fully understand all code.** If you
can't explain what your changes do and how they interact with the
greater system without the aid of AI tools, do not contribute
those changes.
* **Issues and discussions can use AI assistance but must have a full
human-in-the-loop.** This means that any content generated with AI
must have been reviewed *and edited* by a human before submission.
The author must do their research and trim this down.
* **No AI-generated media is allowed (textures, videos, audio, etc.).**
Text and code are the only acceptable AI-generated content, per the
other rules in this policy.
* **Commit messages, PR descriptions must read in your own voice.**
No AI-authored or AI co-authored commits, including Co-authored-by AI tags.
* **Only contribute fixes for bugs you have personally hit in-game.**
If you haven’t experienced or reproduced a bug firsthand, and don’t
already know the relevant code well, don’t throw AI at it and forward it to us.
Patches written for problems the author never actually experienced themselves
tend to fix the wrong thing and/or produce another.
* **The voice and intent of written content must be yours.**
For documentation and especially quests, the tone and the point you're trying to make
has to come from you, not an AI. Use AI to help with phrasing or cleanup if you like,
but if the substance and voice aren't yours, don't contribute it.
* **Bad AI use may be blocked.** People who produce bad contributions
that are clearly AI (slop) may be blocked from making future contributions.
We love to help junior developers learn and grow, but if you're interested
in that, don't use AI and we'll help you. We're sorry that bad AI
users have ruined this for you.
## There are Humans Here
Please remember that this project is maintained by humans.
Every discussion, issue, and pull request is read and reviewed by
humans (and sometimes machines, too). It is a boundary point at which
people interact with each other and work is done. It is rude and
disrespectful to approach this boundary with low-effort, unqualified
work, since it puts the burden of validation on the maintainer.
In a perfect world, AI would produce high-quality, accurate work
every time. But today, that reality depends on the user of the AI.
And today, many users of AI do not use it well. So, until that
changes, we need to have strict rules to protect maintainers.
Based on https://github.com/ghostty-org/ghostty/blob/main/AI_POLICY.md
+2 -1
View File
@@ -23,7 +23,7 @@ Targets GL 3.3+ core profile
# Required Dependencies
* [UniMixins](https://github.com/LegacyModdingMC/UniMixins/releases) >= 0.1.23
* [GTNHLib](https://github.com/GTNewHorizons/GTNHLib/releases) >= 0.9.0
* [GTNHLib](https://github.com/GTNewHorizons/GTNHLib/releases) >= 0.11.19
* NOTE: Some mods are not required, but a specific version is required if present - see: `Permanent Incompatibilities` below.
# Known (temporary) Incompatibilities
@@ -50,6 +50,7 @@ Targets GL 3.3+ core profile
* CodeChicken[Core](https://github.com/GTNewHorizons/CodeChickenCore/releases)/[Lib](https://github.com/GTNewHorizons/CodeChickenLib/releases) <1.2.0 - Threading issues, use 1.3.0+
* Neodymium - Untested, but unlikely to be compatible. (Chunk Meshing is definitely incompatible, other features might work depending on config)
* SmoothFont - Equivalent functionality added
* ClientFixer - Use a resource pack like [GTNH-FTI-Standard-Font](https://github.com/Eldrinn-Elantey/GTNH-FTI-Standard-Font)
# Credits
* ShadersMod
+5
View File
@@ -1,9 +1,14 @@
import xyz.wagyourtail.jvmdg.gradle.flags.DowngradeFlags
import xyz.wagyourtail.jvmdg.gradle.task.DowngradeJar
import xyz.wagyourtail.jvmdg.gradle.task.files.DowngradeFiles
plugins {
id("com.gtnewhorizons.gtnhconvention")
}
tasks.withType<DowngradeJar>().configureEach { logLevel.set("FATAL") }
tasks.withType<DowngradeFiles>().configureEach { logLevel.set("FATAL") }
minecraft {
extraRunJvmArguments.add("-Dangelica.enableTestBlocks=true")
extraRunJvmArguments.add("-Dangelica.dumpClass=true")
+38 -7
View File
@@ -66,9 +66,9 @@ configurations.all {
}
dependencies {
compileOnly("com.github.GTNewHorizons:Hodgepodge:2.7.14:dev")
compileOnly("com.github.GTNewHorizons:Hodgepodge:2.7.158:dev")
compileOnly("com.gtnewhorizons.retrofuturabootstrap:RetroFuturaBootstrap:1.0.7") { transitive = false }
compileOnly("com.gtnewhorizons.retrofuturabootstrap:RetroFuturaBootstrap:1.1.0") { transitive = false }
// runtimeOnlyNonPublishable("com.github.GTNewHorizons:lwjgl3ify:3.0.99")
compileOnly("org.projectlombok:lombok:1.18.42") { transitive = false }
@@ -76,27 +76,51 @@ dependencies {
// Iris Shaders
compileOnly('org.jetbrains:annotations:26.0.2')
api("com.github.GTNewHorizons:GTNHLib:0.9.36:dev")
api("com.github.GTNewHorizons:GTNHLib:0.11.19:dev")
shadowImplementation(project(":glsm")) { transitive = false }
shadowImplementation(project(":lwjgl3-backend")) { transitive = false }
shadowImplementation("net.minecraftforge:eventbus:7.0-beta.15")
shadowImplementation("org.anarres:jcpp:1.4.14") // Apache 2.0
shadowImplementation("org.taumc:glsl-transformation-lib:0.2.0-26.g5ee7d96-GTNH") {
shadowImplementation("org.taumc:glsl-transformation-lib:0.2.0-33.g3ec381e") {
exclude module: "antlr4" // we only want to shadow the runtime module
}
shadowImplementation("org.antlr:antlr4-runtime:4.13.2")
compileOnly "org.apache.ant:ant:1.8.2"
// Celeritas (transitive=false: deps provided by GTNHLib/runtime)
embedOnly("org.embeddedt.celeritas:celeritas-common:2.5.4-GTNH") { transitive = false }
embedOnly("org.embeddedt.celeritas:celeritas-common:2.5.8-GTNH") { transitive = false }
embedOnly("org.embeddedt.celeritas:celeritas-lwjgl2-service:2.5.3-GTNH") { transitive = false }
// Because who doesn't want NEI
devOnlyNonPublishable('com.github.GTNewHorizons:NotEnoughItems:2.8.35-GTNH:dev')
devOnlyNonPublishable('com.github.GTNewHorizons:CodeChickenCore:1.4.7:dev')
// BQ Testing
// devOnlyNonPublishable("com.github.GTNewHorizons:BetterQuesting:3.8.74-GTNH:dev")
// Display List Testing
/*
runtimeOnlyNonPublishable("com.github.GTNewHorizons:Applied-Energistics-2-Unofficial:rv3-beta-749-GTNH:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:ae2stuff:0.10.14-GTNH:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:BuildCraft:7.1.54:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:IguanaTweaksTConstruct:2.7.0:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:OpenBlocks:1.12.4-GTNH:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:OpenComputers:1.12.12-GTNH:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:TinkersConstruct:1.14.12-GTNH:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:ServerUtilities:2.2.13:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:ironchest:6.1.6:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:waila:1.9.7:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:Botania:1.13.15-GTNH:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:Mobs-Info:0.5.10-GTNH:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:Draconic-Evolution:1.5.18-GTNH:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:HoloInventory:2.5.8-GTNH:dev")
*/
//devOnlyNonPublishable('com.github.GTNewHorizons:worldedit-gtnh:v0.0.9')
// Notfine Deps
compileOnly("thaumcraft:Thaumcraft:1.7.10-4.2.3.5:dev")
devOnlyNonPublishable("com.github.GTNewHorizons:Baubles-Expanded:2.2.2-GTNH:dev")
@@ -119,14 +143,16 @@ dependencies {
compileOnly("com.github.GTNewHorizons:NotEnoughIds:2.1.10:dev")
compileOnly(rfg.deobf("maven.modrinth:ntmspace:X5634_H261"))
compileOnly(rfg.deobf("maven.modrinth:ntmspace:X5663_H261"))
compileOnly(rfg.deobf("curse.maven:campfirebackport-387444:4611675"))
compileOnly(rfg.deobf("curse.maven:xaeros-minimap-263420:5060684"))
compileOnly(rfg.deobf("curse.maven:security-craft-64760:2818228"))
compileOnly(rfg.deobf("curse.maven:et-futurum-requiem-441392:5873837"))
compileOnly("ganymedes01.etfuturum:Et-Futurum-Requiem:2.6.49-GTNH:dev")
compileOnly(rfg.deobf("curse.maven:biomes-o-plenty-220318:2499612"))
devOnlyNonPublishable("com.github.GTNewHorizons:CosmeticArmorReworked:1.0.3-GTNH:dev")
// For testing alternative splash screens — https://github.com/MalTeeez/ModernSplash/releases
// devOnlyNonPublishable("com.github.MalTeeez:modernsplash:1.7.10-1.2.4:dev")
@@ -151,8 +177,13 @@ dependencies {
testImplementation('org.junit.platform:junit-platform-launcher')
testImplementation('com.tngtech.archunit:archunit-junit5:1.4.1')
testRuntimeOnly("it.unimi.dsi:fastutil:8.5.18")
testRuntimeOnly("org.joml:joml:1.10.8")
}
}
compileOnly("mega:fluidlogged-mc1.7.10:0.1.2")
compileOnly("com.cardinalstar.cubicchunks:CubicChunks1710:v0.1.5-alpha:dev") { transitive = false }
}
+59 -3
View File
@@ -66,6 +66,7 @@ repositories {
content { includeGroup("xyz.wagyourtail.jvmdowngrader") }
}
mavenCentral()
mavenLocal()
}
dependencies {
@@ -95,6 +96,8 @@ dependencies {
// Deps (normally from MC) - compile only, but needed at test runtime
compileOnly("com.google.guava:guava:${property("guavaVersion")}")
testRuntimeOnly("com.google.guava:guava:${property("guavaVersion")}")
testRuntimeOnly("it.unimi.dsi:fastutil:${property("fastutilVersion")}")
testRuntimeOnly("org.joml:joml:${property("jomlVersion")}") { isTransitive = false }
// Test
testImplementation(sourceSets["stubs"].output)
@@ -123,6 +126,7 @@ tasks.withType<DowngradeFiles>().configureEach {
downgradeTo.set(JavaVersion.VERSION_1_8)
multiReleaseOriginal.set(false)
multiReleaseVersions.set(emptySet())
logLevel.set("FATAL")
}
val downgradeDepsForTest by tasks.registering(DowngradeFiles::class) {
@@ -138,14 +142,23 @@ val downgradeMainClasses by tasks.registering(DowngradeFiles::class) {
val downgradeTestClasses by tasks.registering(DowngradeFiles::class) {
inputCollection = sourceSets["test"].output.classesDirs
classpath = sourceSets["test"].compileClasspath
sourceSets["test"].output.classesDirs.files.forEach { outputs.dir(temporaryDir.resolve(it.name)) }
dependsOn(tasks.named("testClasses"))
}
tasks.test {
useJUnitPlatform()
val GL_TASK_TAGS = mapOf(
"glCompatTest" to "gl-compat",
"glCoreTest" to "gl-core",
"glSharedTest" to "gl-shared",
)
fun Test.configureGlsmJava8() {
javaLauncher = javaToolchains.launcherFor {
languageVersion = JavaLanguageVersion.of(8)
if (System.getProperty("os.name").lowercase().contains("mac")
&& System.getProperty("os.arch") == "aarch64") {
vendor = JvmVendorSpec.AZUL
}
}
dependsOn(downgradeMainClasses, downgradeTestClasses, downgradeDepsForTest)
@@ -162,13 +175,56 @@ tasks.test {
classpath = downgradedTest
.plus(downgradedMain)
.plus(downgradedDeps)
.plus(classpath.minus(mainClassesDirs).minus(testClassesDirs).minus(depsToDowngrade))
.plus(sourceSets["test"].runtimeClasspath.minus(mainClassesDirs).minus(testClassesDirs).minus(depsToDowngrade))
val extractNatives = rootProject.tasks.named("extractNatives2")
dependsOn(extractNatives)
jvmArgs("-Djava.library.path=${extractNatives.get().property("destinationFolder").let { (it as DirectoryProperty).asFile.get().path }}")
}
tasks.test {
useJUnitPlatform {
excludeTags = GL_TASK_TAGS.values.toSet()
}
configureGlsmJava8()
}
val glTestTasks = GL_TASK_TAGS.map { (taskName, tag) ->
tasks.register<Test>(taskName) {
description = "Runs the $tag GL tests in their own JVM, so they get their own Display and GL profile."
group = "verification"
useJUnitPlatform {
includeTags = setOf(tag)
}
configureGlsmJava8()
}
}
(listOf(tasks.test) + glTestTasks).zipWithNext { earlier, later ->
later.configure { mustRunAfter(earlier) }
}
tasks.test { finalizedBy(glTestTasks) }
val verifyTestsRan by tasks.registering {
dependsOn(tasks.test, glTestTasks)
}
(listOf(tasks.test) + glTestTasks).forEach { testTask ->
val name = testTask.name
val verify = tasks.register("verify${name.replaceFirstChar { it.uppercase() }}Ran") {
val resultsDir = layout.buildDirectory.dir("test-results/$name")
dependsOn(testTask)
doLast {
val dir = resultsDir.get().asFile
val xmls = dir.listFiles { f -> f.name.startsWith("TEST-") && f.name.endsWith(".xml") } ?: emptyArray()
check(xmls.isNotEmpty()) {
":glsm:$name produced no TEST-*.xml in $dir - test task likely went NO-SOURCE"
}
}
}
verifyTestsRan.configure { dependsOn(verify) }
}
tasks.check { dependsOn(verifyTestsRan) }
publishing {
publications {
create<MavenPublication>("mavenJava") {
+4 -2
View File
@@ -1,10 +1,12 @@
lwjglVersion = 2.9.4-nightly-20150209
eventbusVersion = 7.0-beta.15
celeritasVersion = 2.5.3-GTNH
celeritasVersion = 2.5.8-GTNH
lombokVersion = 1.18.42
log4jVersion = 2.0-beta9
guavaVersion = 18.0
gtnhlibVersion = 0.9.36
gtnhlibVersion = 0.11.19
fastutilVersion = 8.5.18
jomlVersion = 1.10.8
glslTransformLibVersion = 0.2.0-26.g5ee7d96-GTNH
antlr4RuntimeVersion = 4.13.2
jcppVersion = 1.4.14
@@ -1,67 +0,0 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizons.angelica.glsm.recording.AccumulatedDraw;
import com.gtnewhorizons.angelica.glsm.recording.CommandBuffer;
import com.gtnewhorizons.angelica.glsm.recording.CommandBufferProcessor;
import java.util.List;
/**
* Builds final command buffers from raw command buffers and compiled VBO data.
* Bridges the gap between VBO compilation (glsm types) and buffer processing (recording types).
*/
public final class CommandBufferBuilder {
private CommandBufferBuilder() {}
/**
* Build commands with transform collapsing optimization.
* Accumulates MODELVIEW transforms and emits MultMatrix at barriers (draws, CallList).
* Uses mergedRanges where consecutive same-transform draws are combined.
*/
public static void buildFromRawBuffer(
CommandBuffer rawBuffer,
List<AccumulatedDraw> accumulatedDraws,
CommandBuffer finalBuffer) {
rawBuffer.resetRead();
int cmdIndex = 0;
int rangeIndex = 0;
AccumulatedDraw draw;
while (rawBuffer.hasRemaining()) {
// Read and process the command
CommandBufferProcessor.copyCommand(rawBuffer, finalBuffer);
// Emit draw ranges at this command position
while (rangeIndex < accumulatedDraws.size() && (draw = accumulatedDraws.get(rangeIndex)).commandIndex == cmdIndex) {
emitDrawRangeToBuffer(draw, finalBuffer, rangeIndex);
rangeIndex++;
}
cmdIndex++;
}
while (rangeIndex < accumulatedDraws.size()) {
emitDrawRangeToBuffer(accumulatedDraws.get(rangeIndex), finalBuffer, rangeIndex);
rangeIndex++;
}
}
/**
* Emit a draw range to the output buffer, with transform if needed.
*/
private static void emitDrawRangeToBuffer(
AccumulatedDraw draw,
CommandBuffer out,
int vboIdx) {
if (draw.restoreData != null) {
out.writeDrawRangeRestore(vboIdx, draw.restoreData);
return;
}
// Write the draw range command
out.writeDrawRange(vboIdx);
}
}
@@ -1,6 +1,10 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizons.angelica.glsm.backend.RenderBackend;
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormatElement.Usage;
import org.antlr.v4.runtime.tree.ParseTree;
import org.antlr.v4.runtime.tree.TerminalNode;
import org.taumc.glsl.grammar.GLSLLexer;
import org.taumc.glsl.grammar.GLSLParser;
import org.taumc.glsl.ShaderParser;
import org.taumc.glsl.Transformer;
@@ -9,9 +13,11 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
@@ -114,13 +120,17 @@ public class CompatShaderTransformer {
return result;
}
private static boolean needsTransformation(String source) {
// Shaders already at 330+ core don't need compat transformation
public static boolean isCoreShader(String source) {
final Matcher vm = VERSION_PATTERN.matcher(source);
if (vm.find()) {
final int version = Integer.parseInt(vm.group(1));
if (version >= 330 && "core".equals(vm.group(2))) return false;
return version >= 330 && "core".equals(vm.group(2));
}
return false;
}
private static boolean needsTransformation(String source) {
if (isCoreShader(source)) return false;
return NEEDS_TRANSFORM_PATTERN.matcher(source).find();
}
@@ -179,8 +189,9 @@ public class CompatShaderTransformer {
transformer.rename("gl_FrontColor", "angelica_FrontColor");
transformer.prependMain("angelica_FrontColor = vec4(1.0);");
// Vertex attributes — replaces removed FFP vertex inputs with explicit in declarations
transformVertexAttributes(transformer, source);
final FFPInputs ffp = FFPInputs.detect(source);
assignVertexInputLocations(transformer, ffp);
transformVertexAttributes(transformer, source, ffp);
} else {
if (source.contains("gl_Color")) {
transformer.injectVariable("in vec4 angelica_FrontColor;");
@@ -307,25 +318,25 @@ public class CompatShaderTransformer {
/**
* Replace removed FFP vertex attributes with explicit {@code in} declarations at core profile attribute locations.
*/
private static void transformVertexAttributes(Transformer transformer, String source) {
if (source.contains("gl_Vertex") || source.contains("ftransform")) {
private static void transformVertexAttributes(Transformer transformer, String source, FFPInputs ffp) {
if (ffp.position()) {
transformer.injectVariable("layout(location = 0) in vec4 angelica_Vertex;");
transformer.rename("gl_Vertex", "angelica_Vertex");
}
// gl_Color in vertex shaders is the per-vertex color attribute, distinct from the fragment gl_Color (interpolated gl_FrontColor) handled above
if (source.contains("gl_Color")) {
if (ffp.color()) {
transformer.injectVariable("layout(location = 1) in vec4 angelica_Color;");
transformer.rename("gl_Color", "angelica_Color");
}
if (source.contains("gl_MultiTexCoord0")) {
if (ffp.uv0()) {
transformer.injectVariable("layout(location = 2) in vec4 angelica_MultiTexCoord0;");
transformer.rename("gl_MultiTexCoord0", "angelica_MultiTexCoord0");
}
if (source.contains("gl_MultiTexCoord1")) {
if (ffp.uv1()) {
transformer.injectVariable("layout(location = 3) in vec4 angelica_MultiTexCoord1;");
transformer.rename("gl_MultiTexCoord1", "angelica_MultiTexCoord1");
}
if (source.contains("gl_Normal")) {
if (ffp.normal()) {
transformer.injectVariable("layout(location = 4) in vec3 angelica_Normal;");
transformer.rename("gl_Normal", "angelica_Normal");
}
@@ -334,6 +345,145 @@ public class CompatShaderTransformer {
}
}
private static final int MAX_VERTEX_ATTRIBS = 16;
private record FFPInputs(boolean position, boolean color, boolean uv0, boolean uv1, boolean normal) {
static FFPInputs detect(String source) {
return new FFPInputs(
source.contains("gl_Vertex") || source.contains("ftransform"),
source.contains("gl_Color"),
source.contains("gl_MultiTexCoord0"),
source.contains("gl_MultiTexCoord1"),
source.contains("gl_Normal"));
}
}
/** Adaptation of Mesa's assign_attribute_or_color_locations */
private static void assignVertexInputLocations(Transformer transformer, FFPInputs ffp) {
int used = 0;
if (ffp.position()) used |= 1 << Usage.POSITION.getAttributeLocation();
if (ffp.color()) used |= 1 << Usage.COLOR.getAttributeLocation();
if (ffp.uv0()) used |= 1 << Usage.PRIMARY_UV.getAttributeLocation();
if (ffp.uv1()) used |= 1 << Usage.SECONDARY_UV.getAttributeLocation();
if (ffp.normal()) used |= 1 << Usage.NORMAL.getAttributeLocation();
record Input(String name, String baseType, String arraySuffix, int slots, int index) {}
final List<Input> toAssign = new ArrayList<>();
int index = 0;
for (TerminalNode storage : transformer.collectStorage()) {
final int tokenType = storage.getSymbol().getType();
if (tokenType != GLSLLexer.ATTRIBUTE && tokenType != GLSLLexer.IN) continue;
ParseTree node = storage.getParent();
while (node != null && !(node instanceof GLSLParser.Single_declarationContext)) {
node = node.getParent();
}
if (!(node instanceof GLSLParser.Single_declarationContext decl)) continue;
if (decl.typeless_declaration() == null) continue;
final GLSLParser.Type_specifierContext typeSpec = decl.fully_specified_type().type_specifier();
final String baseType = GlslTransformUtils.getFormattedShader(typeSpec.type_specifier_nonarray(), "").trim();
final int typeArraySlots = arraySlots(typeSpec.array_specifier());
final String typeArray = arraySuffix(typeSpec.array_specifier());
if (hasLayoutQualifier(decl)) {
final int loc = explicitLocation(decl);
final int slots = baseSlots(baseType) * typeArraySlots * arraySlots(decl.typeless_declaration().array_specifier());
if (loc >= 0) used |= slotMask(slots) << loc;
continue;
}
final List<GLSLParser.Typeless_declarationContext> declarators = new ArrayList<>();
declarators.add(decl.typeless_declaration());
if (decl.getParent() instanceof GLSLParser.Init_declarator_listContext list) {
declarators.addAll(list.typeless_declaration());
}
for (GLSLParser.Typeless_declarationContext d : declarators) {
if (d.IDENTIFIER() == null) continue;
final int slots = baseSlots(baseType) * typeArraySlots * arraySlots(d.array_specifier());
final String arraySuffix = typeArray + arraySuffix(d.array_specifier());
toAssign.add(new Input(d.IDENTIFIER().getText(), baseType, arraySuffix, slots, index++));
}
}
toAssign.sort((a, b) -> a.slots() != b.slots() ? b.slots() - a.slots() : a.index() - b.index());
for (Input in : toAssign) {
final int loc = findAvailableSlots(used, in.slots());
if (loc < 0) continue;
used |= slotMask(in.slots()) << loc;
transformer.removeVariable(in.name());
transformer.variable = null;
transformer.injectVariable("layout(location = " + loc + ") in " + in.baseType() + " " + in.name() + in.arraySuffix() + ";");
}
}
private static String arraySuffix(GLSLParser.Array_specifierContext arraySpec) {
return arraySpec != null ? GlslTransformUtils.getFormattedShader(arraySpec, "").trim() : "";
}
private static int arraySlots(GLSLParser.Array_specifierContext arraySpec) {
if (arraySpec == null) return 1;
int product = 1;
for (GLSLParser.DimensionContext dim : arraySpec.dimension()) {
if (dim.constant_expression() == null) continue;
try {
product *= Integer.decode(GlslTransformUtils.getFormattedShader(dim.constant_expression(), "").trim());
} catch (NumberFormatException ignored) {}
}
return product;
}
private static int baseSlots(String type) {
final String dims = type.startsWith("dmat") ? type.substring(4) : type.startsWith("mat") ? type.substring(3) : null;
if (dims != null && !dims.isEmpty() && Character.isDigit(dims.charAt(0))) {
return dims.charAt(0) - '0';
}
return 1;
}
private static int slotMask(int slots) {
return (slots >= 32) ? -1 : (1 << slots) - 1;
}
private static int findAvailableSlots(int used, int needed) {
if (needed <= 0) return -1;
final int mask = slotMask(needed);
for (int i = 0; i <= MAX_VERTEX_ATTRIBS - needed; i++) {
if (((mask << i) & ~used) == (mask << i)) return i;
}
return -1;
}
private static int explicitLocation(GLSLParser.Single_declarationContext decl) {
final GLSLParser.Type_qualifierContext qualifier = decl.fully_specified_type().type_qualifier();
if (qualifier == null) return -1;
for (GLSLParser.Single_type_qualifierContext single : qualifier.single_type_qualifier()) {
final GLSLParser.Layout_qualifierContext layout = single.layout_qualifier();
if (layout == null) continue;
for (GLSLParser.Layout_qualifier_idContext id : layout.layout_qualifier_id_list().layout_qualifier_id()) {
if (id.IDENTIFIER() != null && "location".equals(id.IDENTIFIER().getText()) && id.constant_expression() != null) {
try {
return Integer.decode(GlslTransformUtils.getFormattedShader(id.constant_expression(), "").trim());
} catch (NumberFormatException ignored) {
return -1;
}
}
}
}
return -1;
}
private static boolean hasLayoutQualifier(GLSLParser.Single_declarationContext decl) {
final GLSLParser.Type_qualifierContext qualifier = decl.fully_specified_type().type_qualifier();
if (qualifier == null) return false;
for (GLSLParser.Single_type_qualifierContext single : qualifier.single_type_qualifier()) {
if (single.layout_qualifier() != null) return true;
}
return false;
}
private static void dumpShader(String original, String transformed, boolean isFragment, boolean wasTransformed) {
if (DUMP_DIR == null) return;
final int id = dumpCounter.getAndIncrement();
@@ -7,18 +7,16 @@ import com.gtnewhorizons.angelica.glsm.states.LightModelState;
import com.gtnewhorizons.angelica.glsm.states.LightState;
import com.gtnewhorizons.angelica.glsm.states.MaterialState;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import static com.gtnewhorizons.angelica.glsm.backend.BackendManager.RENDER_BACKEND;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.joml.Matrix3f;
import org.joml.Matrix4f;
import org.joml.Vector3f;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL20;
import java.nio.FloatBuffer;
import static com.gtnewhorizons.angelica.glsm.backend.BackendManager.RENDER_BACKEND;
/**
* Per-program compat uniform location cache and upload manager.
*
@@ -61,7 +59,10 @@ public class CompatUniformManager {
static final int MAT_FIELDS = 5;
static final int LOC_MAT_BASE = LOC_LIGHT_BASE + 2 * LIGHT_FIELDS; // 43
static final int LOC_COUNT = LOC_MAT_BASE + MAT_FIELDS; // 48
static final int LOC_IRIS_MODELVIEW_INVERSE = LOC_MAT_BASE + MAT_FIELDS; // 48
static final int LOC_IRIS_PROJECTION_INVERSE = LOC_IRIS_MODELVIEW_INVERSE + 1; // 49
static final int LOC_COUNT = LOC_IRIS_PROJECTION_INVERSE + 1; // 50
private static final String[] LIGHT_FIELD_NAMES = {
"ambient", "diffuse", "specular", "position", "halfVector",
@@ -91,6 +92,8 @@ public class CompatUniformManager {
UNIFORM_NAMES[LOC_IRIS_NORMAL] = "iris_NormalMatrix";
UNIFORM_NAMES[LOC_IRIS_LIGHTMAP_TEXTURE_MATRIX] = "iris_LightmapTextureMatrix";
UNIFORM_NAMES[LOC_IRIS_TEXTURE_MATRIX] = "iris_TextureMatrix";
UNIFORM_NAMES[LOC_IRIS_MODELVIEW_INVERSE] = "iris_ModelViewMatrixInverse";
UNIFORM_NAMES[LOC_IRIS_PROJECTION_INVERSE] = "iris_ProjectionMatrixInverse";
UNIFORM_NAMES[LOC_ALPHA_TEST_REF] = "angelica_currentAlphaTest";
UNIFORM_NAMES[LOC_SCENE_COLOR] = "angelica_SceneColor";
UNIFORM_NAMES[LOC_CLIP_PLANES] = "angelica_ClipPlane[0]";
@@ -105,8 +108,15 @@ public class CompatUniformManager {
}
}
/** Per-program cached uniform locations. Maps program ID → int[LOC_COUNT]. */
private static final Int2ObjectOpenHashMap<int[]> programLocations = new Int2ObjectOpenHashMap<>();
/** Per-program cached uniform locations + last-uploaded generations. Maps program ID -> state. */
private static final Int2ObjectOpenHashMap<ProgramUniforms> programUniforms = new Int2ObjectOpenHashMap<>();
private static final class ProgramUniforms {
final int[] locs;
int lastMvGen = -1, lastMvLinearGen = -1, lastProjGen = -1, lastTexMatGen = -1;
int lastFragmentGen = -1, lastLightingGen = -1, lastClipPlaneGen = -1;
ProgramUniforms(int[] locs) { this.locs = locs; }
}
// Reusable NIO buffers for upload
private static final FloatBuffer mat4Buf = BufferUtils.createFloatBuffer(16);
@@ -115,6 +125,7 @@ public class CompatUniformManager {
private static final FloatBuffer clipPlaneBuf = BufferUtils.createFloatBuffer(32); // 8 planes * vec4
private static final Matrix3f normalMatrix = new Matrix3f();
private static final Matrix4f scratchMatrix = new Matrix4f();
private static final Vector3f scratchCol = new Vector3f();
private static final float LIGHTMAP_SCALE = 1.0f / 256.0f;
private static final FloatBuffer lightmapMatrixBuf;
@@ -124,17 +135,18 @@ public class CompatUniformManager {
new Matrix4f().scale(LIGHTMAP_SCALE).translate(8.0f, 8.0f, 8.0f).get(lightmapMatrixBuf);
}
// Dirty tracking: skip uploads when state hasn't changed and program is the same
private static int lastProgram = -1;
private static int lastMvGen = -1;
private static int lastProjGen = -1;
private static int lastTexMatGen = -1;
private static int lastFragmentGen = -1;
private static int lastLightingGen = -1;
private static int lastClipPlaneGen = -1;
private CompatUniformManager() {}
/**
* Normalize each column of a 3x3 matrix in place. Used to strip the residual 1/scale that
* inverse-transpose introduces.
*/
private static void normalizeColumnsInPlace(Matrix3f m) {
m.setColumn(0, m.getColumn(0, scratchCol).normalize());
m.setColumn(1, m.getColumn(1, scratchCol).normalize());
m.setColumn(2, m.getColumn(2, scratchCol).normalize());
}
public static void onLinkProgram(int program) {
int[] locs = new int[LOC_COUNT];
boolean hasAny = false;
@@ -145,45 +157,49 @@ public class CompatUniformManager {
}
if (hasAny) {
programLocations.put(program, locs);
programUniforms.put(program, new ProgramUniforms(locs));
GLStateManager.LOGGER.debug("CompatUniformManager: program {} has compat uniforms", program);
}
}
public static void onUseProgram(int program) {
if (program == 0) return;
/**
* Uploads the emulated uniforms.
*
* @return Whether the program needs emulation (FFP or Iris Program)
*/
public static boolean refreshCompatUniforms(int program) {
final ProgramUniforms pu = programUniforms.get(program);
if (pu == null) return false;
final int[] locs = pu.locs;
int[] locs = programLocations.get(program);
if (locs == null) return;
final boolean programChanged = program != lastProgram;
lastProgram = program;
// Matrix uniforms — skip if generation unchanged and same program
// Matrix uniforms — skip if this program's storage already holds the current generation
final int mvGen = GLStateManager.mvGeneration;
final int mvLinearGen = GLStateManager.mvLinearGeneration;
final int projGen = GLStateManager.projGeneration;
final int texMatGen = GLStateManager.texMatrixGeneration;
final boolean mvChanged = programChanged || mvGen != lastMvGen;
final boolean projChanged = programChanged || projGen != lastProjGen;
final boolean texMatChanged = programChanged || texMatGen != lastTexMatGen;
final boolean mvChanged = mvGen != pu.lastMvGen;
final boolean mvLinearChanged = mvLinearGen != pu.lastMvLinearGen;
final boolean projChanged = projGen != pu.lastProjGen;
final boolean texMatChanged = texMatGen != pu.lastTexMatGen;
if (mvChanged || projChanged || texMatChanged) {
uploadMatrices(locs, mvChanged, projChanged, texMatChanged);
lastMvGen = mvGen;
lastProjGen = projGen;
lastTexMatGen = texMatGen;
uploadMatrices(locs, mvChanged, mvLinearChanged, projChanged, texMatChanged);
pu.lastMvGen = mvGen;
pu.lastMvLinearGen = mvLinearGen;
pu.lastProjGen = projGen;
pu.lastTexMatGen = texMatGen;
}
// Fragment-category uniforms (fog, alpha) — skip if generation unchanged
final int fragGen = GLStateManager.fragmentGeneration;
if (programChanged || fragGen != lastFragmentGen) {
lastFragmentGen = fragGen;
if (fragGen != pu.lastFragmentGen) {
pu.lastFragmentGen = fragGen;
uploadFragmentUniforms(locs);
}
// Lighting-derived uniforms (scene color, light sources, material)
final int litGen = GLStateManager.lightingGeneration;
if (programChanged || litGen != lastLightingGen) {
lastLightingGen = litGen;
if (litGen != pu.lastLightingGen) {
pu.lastLightingGen = litGen;
if (locs[LOC_SCENE_COLOR] != -1) uploadSceneColor(locs);
uploadLightSources(locs);
uploadMaterial(locs);
@@ -192,14 +208,16 @@ public class CompatUniformManager {
// Clip plane equations + enabled bool — uploaded when enable state or equations change
if (locs[LOC_CLIP_PLANES] != -1 || locs[LOC_CLIP_PLANES_ENABLED] != -1) {
final int cpGen = GLStateManager.clipPlaneGeneration;
if (programChanged || cpGen != lastClipPlaneGen) {
lastClipPlaneGen = cpGen;
if (cpGen != pu.lastClipPlaneGen) {
pu.lastClipPlaneGen = cpGen;
uploadClipPlanes(locs);
}
}
return true;
}
private static void uploadMatrices(int[] locs, boolean mvChanged, boolean projChanged, boolean texMatChanged) {
private static void uploadMatrices(int[] locs, boolean mvChanged, boolean mvLinearChanged, boolean projChanged, boolean texMatChanged) {
final Matrix4f mv = GLStateManager.getModelViewMatrix();
final Matrix4f proj = GLStateManager.getProjectionMatrix();
@@ -216,20 +234,30 @@ public class CompatUniformManager {
}
// ModelView Inverse
if (locs[LOC_MODELVIEW_INVERSE] != -1) {
if (locs[LOC_MODELVIEW_INVERSE] != -1 || locs[LOC_IRIS_MODELVIEW_INVERSE] != -1) {
mv.invert(scratchMatrix);
scratchMatrix.get(mat4Buf);
RENDER_BACKEND.uniformMatrix4(locs[LOC_MODELVIEW_INVERSE], false, mat4Buf);
if (locs[LOC_MODELVIEW_INVERSE] != -1) {
RENDER_BACKEND.uniformMatrix4(locs[LOC_MODELVIEW_INVERSE], false, mat4Buf);
}
if (locs[LOC_IRIS_MODELVIEW_INVERSE] != -1) {
RENDER_BACKEND.uniformMatrix4(locs[LOC_IRIS_MODELVIEW_INVERSE], false, mat4Buf);
}
}
}
if (mvLinearChanged) {
// Normal Matrix (inverse-transpose of upper-left 3x3 of ModelView)
if (locs[LOC_NORMAL] != -1 || locs[LOC_IRIS_NORMAL] != -1) {
mv.normal(normalMatrix);
normalMatrix.get(mat3Buf);
if (locs[LOC_NORMAL] != -1) {
normalMatrix.get(mat3Buf);
RENDER_BACKEND.uniformMatrix3(locs[LOC_NORMAL], false, mat3Buf);
}
// Column-normalized to unit magnitude
if (locs[LOC_IRIS_NORMAL] != -1) {
normalizeColumnsInPlace(normalMatrix);
normalMatrix.get(mat3Buf);
RENDER_BACKEND.uniformMatrix3(locs[LOC_IRIS_NORMAL], false, mat3Buf);
}
}
@@ -248,10 +276,15 @@ public class CompatUniformManager {
}
// Projection Inverse
if (locs[LOC_PROJECTION_INVERSE] != -1) {
if (locs[LOC_PROJECTION_INVERSE] != -1 || locs[LOC_IRIS_PROJECTION_INVERSE] != -1) {
proj.invert(scratchMatrix);
scratchMatrix.get(mat4Buf);
RENDER_BACKEND.uniformMatrix4(locs[LOC_PROJECTION_INVERSE], false, mat4Buf);
if (locs[LOC_PROJECTION_INVERSE] != -1) {
RENDER_BACKEND.uniformMatrix4(locs[LOC_PROJECTION_INVERSE], false, mat4Buf);
}
if (locs[LOC_IRIS_PROJECTION_INVERSE] != -1) {
RENDER_BACKEND.uniformMatrix4(locs[LOC_IRIS_PROJECTION_INVERSE], false, mat4Buf);
}
}
}
@@ -401,14 +434,15 @@ public class CompatUniformManager {
}
public static void onDeleteProgram(int program) {
programLocations.remove(program);
programUniforms.remove(program);
}
public static boolean hasProgram(int program) {
return programLocations.containsKey(program);
return programUniforms.containsKey(program);
}
public static int[] getLocations(int program) {
return programLocations.get(program);
final ProgramUniforms pu = programUniforms.get(program);
return pu == null ? null : pu.locs;
}
}
File diff suppressed because it is too large. Load diff
@@ -1,7 +1,7 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizons.angelica.glsm.ffp.VAOManager;
import com.gtnewhorizons.angelica.glsm.recording.ImmediateModeRecorder;
import com.gtnewhorizons.angelica.glsm.states.VertexAttribState;
import com.gtnewhorizons.angelica.glsm.states.ViewportState;
import org.joml.Matrix4f;
import org.joml.Vector4f;
@@ -9,13 +9,13 @@ import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
import java.nio.ByteBuffer;
import static com.gtnewhorizons.angelica.glsm.backend.BackendManager.RENDER_BACKEND;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.nio.ShortBuffer;
import static com.gtnewhorizons.angelica.glsm.backend.BackendManager.RENDER_BACKEND;
/**
* Software emulation of GL feedback mode
*/
@@ -127,12 +127,12 @@ public class FeedbackManager {
}
private static int positionStride() {
final int s = VertexAttribState.get(0).effectiveStride();
final int s = VAOManager.getAttribEffectiveStride(0);
return (s != 0) ? s : 12;
}
private static long positionOffset() {
return VertexAttribState.get(0).offset;
return VAOManager.getAttribOffset(0);
}
public static void processDrawArrays(int mode, int first, int count) {
@@ -142,7 +142,7 @@ public class FeedbackManager {
final int byteCount = count * stride;
ensureReadbackCapacity(byteCount);
readbackBuf.clear().limit(byteCount);
RENDER_BACKEND.getBufferSubData(GL15.GL_ARRAY_BUFFER, (long) first * stride + posOffset, readbackBuf);
readPositionBuffer((long) first * stride + posOffset, readbackBuf);
emitAllPrimitives(mode, count, i -> i, stride);
}
@@ -151,7 +151,7 @@ public class FeedbackManager {
computeMVP();
final int stride = positionStride();
final int elementSize = VertexAttribState.Attrib.glTypeSizeBytes(type);
final int elementSize = VAOManager.Attrib.glTypeSizeBytes(type);
final int indexByteCount = indexCount * elementSize;
ensureIndexReadbackCapacity(indexByteCount);
indexReadbackBuf.clear().limit(indexByteCount);
@@ -170,7 +170,7 @@ public class FeedbackManager {
final int byteCount = vertexCount * stride;
ensureReadbackCapacity(byteCount);
readbackBuf.clear().limit(byteCount);
RENDER_BACKEND.getBufferSubData(GL15.GL_ARRAY_BUFFER, (long) minIdx * stride + posOffset, readbackBuf);
readPositionBuffer((long) minIdx * stride + posOffset, readbackBuf);
final int base = minIdx;
emitAllPrimitives(mode, indexCount, i -> ImmediateModeRecorder.readIndex(indexBuf, type, 0, i) - base, stride);
@@ -251,7 +251,23 @@ public class FeedbackManager {
final int byteCount = vertexCount * stride;
ensureReadbackCapacity(byteCount);
readbackBuf.clear().limit(byteCount);
RENDER_BACKEND.getBufferSubData(GL15.GL_ARRAY_BUFFER, (long) minIdx * stride + posOffset, readbackBuf);
readPositionBuffer((long) minIdx * stride + posOffset, readbackBuf);
}
private static void readPositionBuffer(long offset, ByteBuffer destination) {
final int positionVbo = VAOManager.getAttribVBO(0);
final int previousVbo = GLStateManager.getBoundVBO();
if (positionVbo != 0 && positionVbo != previousVbo) {
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, positionVbo);
}
try {
RENDER_BACKEND.getBufferSubData(GL15.GL_ARRAY_BUFFER, offset, destination);
} finally {
if (positionVbo != 0 && positionVbo != previousVbo) {
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, previousVbo);
}
}
}
private static void emitVertex(int vertexIndex, int stride) {
@@ -345,6 +361,17 @@ public class FeedbackManager {
}
break;
case GL11.GL_QUAD_STRIP:
for (int i = 0; i + 3 < count; i += 2) {
feedbackToken(GL11.GL_POLYGON_TOKEN);
feedbackToken(4);
emitVertex(reader.read(i), stride);
emitVertex(reader.read(i + 1), stride);
emitVertex(reader.read(i + 3), stride);
emitVertex(reader.read(i + 2), stride);
}
break;
case GL11.GL_POLYGON:
if (count >= 3) {
feedbackToken(GL11.GL_POLYGON_TOKEN);
File diff suppressed because it is too large. Load diff
@@ -36,9 +36,10 @@ public class GlslTransformUtils {
/** Reserved words added in later GLSL versions that may appear as identifiers in older shaders. */
private record ReservedWordRename(Pattern pattern, String replacement) {}
private static final Map<Integer, List<ReservedWordRename>> VERSIONED_RESERVED_WORDS = Map.of(
// Always rename sample, glsl-transformation-lib doesn't like it
// Some reserved words to always rename
0, List.of(
new ReservedWordRename(Pattern.compile("\\bsample\\b"), RENAMED_PREFIX + "sample")
new ReservedWordRename(Pattern.compile("\\bsample\\b"), RENAMED_PREFIX + "sample"),
new ReservedWordRename(Pattern.compile("\\bnew\\b"), RENAMED_PREFIX + "new")
),
400, List.of(
new ReservedWordRename(Pattern.compile("\\bsampler\\b(?!\\d)"), RENAMED_PREFIX + "sampler")
@@ -1,6 +1,6 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizons.angelica.glsm.states.VertexAttribState;
import com.gtnewhorizons.angelica.glsm.ffp.VAOManager;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
@@ -9,7 +9,6 @@ import java.nio.IntBuffer;
import java.nio.ShortBuffer;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAddress0;
import static com.gtnewhorizons.angelica.glsm.backend.BackendManager.RENDER_BACKEND;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAlloc;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memFree;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memGetByte;
@@ -17,6 +16,7 @@ import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memGetInt;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memGetShort;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memPutInt;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memPutShort;
import static com.gtnewhorizons.angelica.glsm.backend.BackendManager.RENDER_BACKEND;
/**
* Shared quad-to-triangle EBO for core profile GL_QUADS emulation.
@@ -83,16 +83,34 @@ public final class QuadConverter {
* @param vertexCount number of vertices (must be multiple of 4)
*/
public static void drawQuadsAsTriangles(int first, int vertexCount) {
assert first % 4 == 0 : "QuadConverter: first (" + first + ") must be a multiple of 4";
assert vertexCount % 4 == 0 : "QuadConverter: vertexCount (" + vertexCount + ") must be a multiple of 4";
final int quadCount = vertexCount / 4;
final int prevEbo = GLStateManager.getBoundEBO();
ensureCapacity(first / 4 + quadCount);
RENDER_BACKEND.bindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, eboId);
// Index offset: first vertex / 4 quads * 6 indices * 4 bytes per int
final long indexOffset = (long) (first / 4) * 6 * 4;
RENDER_BACKEND.drawElements(GL11.GL_TRIANGLES, quadCount * 6, INDEX_TYPE, indexOffset);
RENDER_BACKEND.bindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, prevEbo);
drawSharedQuadEbo(first, vertexCount, 1, false);
}
public static void drawQuadsAsTrianglesInstanced(int first, int vertexCount, int primcount) {
drawSharedQuadEbo(first, vertexCount, primcount, true);
}
private static void drawSharedQuadEbo(int first, int vertexCount, int primcount, boolean instanced) {
final boolean locked = GLStateManager.acquireDrawLock();
try {
GLStateManager.preDraw();
assert first % 4 == 0 : "QuadConverter: first (" + first + ") must be a multiple of 4";
assert vertexCount % 4 == 0 : "QuadConverter: vertexCount (" + vertexCount + ") must be a multiple of 4";
final int quadCount = vertexCount / 4;
final int prevEbo = GLStateManager.getBoundEBO();
ensureCapacity(first / 4 + quadCount);
RENDER_BACKEND.bindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, eboId);
// Index offset: first vertex / 4 quads * 6 indices * 4 bytes per int
final long indexOffset = (long) (first / 4) * 6 * 4;
if (instanced) {
RENDER_BACKEND.drawElementsInstanced(GL11.GL_TRIANGLES, quadCount * 6, INDEX_TYPE, indexOffset, primcount);
} else {
RENDER_BACKEND.drawElements(GL11.GL_TRIANGLES, quadCount * 6, INDEX_TYPE, indexOffset);
}
RENDER_BACKEND.bindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, prevEbo);
} finally {
if (locked) GLStateManager.releaseDrawLock();
}
}
/**
@@ -106,27 +124,41 @@ public final class QuadConverter {
* @param bytesPerIndex bytes per index element
*/
private static void uploadAndDraw(ByteBuffer dst, int triIndexCount, int indexType, int bytesPerIndex) {
final int needed = triIndexCount * bytesPerIndex;
final int prevEbo = GLStateManager.getBoundEBO();
uploadAndDraw(dst, triIndexCount, indexType, bytesPerIndex, 1, false);
}
if (scratchEboId == 0) {
scratchEboId = RENDER_BACKEND.genBuffers();
private static void uploadAndDraw(ByteBuffer dst, int triIndexCount, int indexType, int bytesPerIndex, int primcount, boolean instanced) {
final boolean locked = GLStateManager.acquireDrawLock();
try {
GLStateManager.preDraw();
final int needed = triIndexCount * bytesPerIndex;
final int prevEbo = GLStateManager.getBoundEBO();
if (scratchEboId == 0) {
scratchEboId = RENDER_BACKEND.genBuffers();
}
RENDER_BACKEND.bindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, scratchEboId);
if (needed > scratchEboCapacity) {
// Power-of-2 growth -- allocate full capacity, upload actual data
int newCap = Math.max(4096, scratchEboCapacity);
while (newCap < needed) newCap *= 2;
RENDER_BACKEND.bufferData(GL15.GL_ELEMENT_ARRAY_BUFFER, newCap, GL15.GL_STREAM_DRAW);
scratchEboCapacity = newCap;
}
RENDER_BACKEND.bufferSubData(GL15.GL_ELEMENT_ARRAY_BUFFER, 0, dst);
if (instanced) {
RENDER_BACKEND.drawElementsInstanced(GL11.GL_TRIANGLES, triIndexCount, indexType, 0L, primcount);
} else {
RENDER_BACKEND.drawElements(GL11.GL_TRIANGLES, triIndexCount, indexType, 0L);
}
RENDER_BACKEND.bindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, prevEbo);
} finally {
if (locked) GLStateManager.releaseDrawLock();
}
RENDER_BACKEND.bindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, scratchEboId);
if (needed > scratchEboCapacity) {
// Power-of-2 growth — allocate full capacity, upload actual data
int newCap = Math.max(4096, scratchEboCapacity);
while (newCap < needed) newCap *= 2;
RENDER_BACKEND.bufferData(GL15.GL_ELEMENT_ARRAY_BUFFER, newCap, GL15.GL_STREAM_DRAW);
scratchEboCapacity = newCap;
}
RENDER_BACKEND.bufferSubData(GL15.GL_ELEMENT_ARRAY_BUFFER, 0, dst);
RENDER_BACKEND.drawElements(GL11.GL_TRIANGLES, triIndexCount, indexType, 0L);
RENDER_BACKEND.bindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, prevEbo);
memFree(dst);
}
@@ -139,52 +171,29 @@ public final class QuadConverter {
* @param offset byte offset into the currently bound EBO
*/
public static void drawQuadElementsAsTriangles(int indexCount, int type, long offset) {
drawQuadElementsFromEbo(indexCount, type, offset, 1, false);
}
public static void drawQuadElementsAsTrianglesInstanced(int indexCount, int type, long offset, int primcount) {
drawQuadElementsFromEbo(indexCount, type, offset, primcount, true);
}
private static void drawQuadElementsFromEbo(int indexCount, int type, long offset, int primcount, boolean instanced) {
if (indexCount == 0) return;
assert indexCount % 4 == 0 : "QuadConverter: indexCount must be multiple of 4";
final int quadCount = indexCount / 4;
final int triIndexCount = quadCount * 6;
final int bytesPerIndex = VertexAttribState.Attrib.glTypeSizeBytes(type);
final int bytesPerIndex = VAOManager.Attrib.glTypeSizeBytes(type);
// Read source indices from caller's EBO
final ByteBuffer src = memAlloc(indexCount * bytesPerIndex);
RENDER_BACKEND.getBufferSubData(GL15.GL_ELEMENT_ARRAY_BUFFER, offset, src);
final long srcAddr = memAddress0(src);
// Allocate output as GL_UNSIGNED_INT
final ByteBuffer dst = memAlloc(triIndexCount * 4);
final long dstAddr = memAddress0(dst);
for (int i = 0; i < quadCount; i++) {
final int a, b, c, d;
switch (type) {
case GL11.GL_UNSIGNED_INT -> {
final long p = srcAddr + (long) i * 16;
a = memGetInt(p); b = memGetInt(p + 4); c = memGetInt(p + 8); d = memGetInt(p + 12);
}
case GL11.GL_UNSIGNED_SHORT -> {
final long p = srcAddr + (long) i * 8;
a = memGetShort(p) & 0xFFFF; b = memGetShort(p + 2) & 0xFFFF;
c = memGetShort(p + 4) & 0xFFFF; d = memGetShort(p + 6) & 0xFFFF;
}
default /* GL_UNSIGNED_BYTE */ -> {
final long p = srcAddr + (long) i * 4;
a = memGetByte(p) & 0xFF; b = memGetByte(p + 1) & 0xFF;
c = memGetByte(p + 2) & 0xFF; d = memGetByte(p + 3) & 0xFF;
}
}
// GL_LAST_VERTEX_CONVENTION: (a,b,d), (b,c,d)
final long o = (long) i * 24;
memPutInt(dstAddr + o, a);
memPutInt(dstAddr + o + 4, b);
memPutInt(dstAddr + o + 8, d);
memPutInt(dstAddr + o + 12, b);
memPutInt(dstAddr + o + 16, c);
memPutInt(dstAddr + o + 20, d);
}
triangulateQuads(memAddress0(src), type, memAddress0(dst), GL11.GL_UNSIGNED_INT, quadCount);
memFree(src);
uploadAndDraw(dst, triIndexCount, GL11.GL_UNSIGNED_INT, 4);
uploadAndDraw(dst, triIndexCount, GL11.GL_UNSIGNED_INT, 4, primcount, instanced);
}
/**
@@ -198,16 +207,8 @@ public final class QuadConverter {
final int triIndexCount = quadCount * 6;
final ByteBuffer dst = memAlloc(triIndexCount * 4);
final long dstAddr = memAddress0(dst);
final int basePos = indices.position();
for (int i = 0; i < quadCount; i++) {
final int base = basePos + i * 4;
final int a = indices.get(base), b = indices.get(base + 1), c = indices.get(base + 2), d = indices.get(base + 3);
final long o = (long) i * 24;
memPutInt(dstAddr + o, a); memPutInt(dstAddr + o + 4, b); memPutInt(dstAddr + o + 8, d);
memPutInt(dstAddr + o + 12, b); memPutInt(dstAddr + o + 16, c); memPutInt(dstAddr + o + 20, d);
}
final long srcAddr = memAddress0(indices) + (long) indices.position() * 4;
triangulateQuads(srcAddr, GL11.GL_UNSIGNED_INT, memAddress0(dst), GL11.GL_UNSIGNED_INT, quadCount);
uploadAndDraw(dst, triIndexCount, GL11.GL_UNSIGNED_INT, 4);
}
@@ -223,16 +224,8 @@ public final class QuadConverter {
final int triIndexCount = quadCount * 6;
final ByteBuffer dst = memAlloc(triIndexCount * 2);
final long dstAddr = memAddress0(dst);
final int basePos = indices.position();
for (int i = 0; i < quadCount; i++) {
final int base = basePos + i * 4;
final short a = indices.get(base), b = indices.get(base + 1), c = indices.get(base + 2), d = indices.get(base + 3);
final long o = (long) i * 12;
memPutShort(dstAddr + o, a); memPutShort(dstAddr + o + 2, b); memPutShort(dstAddr + o + 4, d);
memPutShort(dstAddr + o + 6, b); memPutShort(dstAddr + o + 8, c); memPutShort(dstAddr + o + 10, d);
}
final long srcAddr = memAddress0(indices) + (long) indices.position() * 2;
triangulateQuads(srcAddr, GL11.GL_UNSIGNED_SHORT, memAddress0(dst), GL11.GL_UNSIGNED_SHORT, quadCount);
uploadAndDraw(dst, triIndexCount, GL11.GL_UNSIGNED_SHORT, 2);
}
@@ -248,31 +241,7 @@ public final class QuadConverter {
final long srcAddr = memAddress0(indices) + indices.position();
final ByteBuffer dst = memAlloc(triIndexCount * 4);
final long dstAddr = memAddress0(dst);
for (int i = 0; i < quadCount; i++) {
final int a, b, c, d;
switch (type) {
case GL11.GL_UNSIGNED_INT -> {
final long p = srcAddr + (long) i * 16;
a = memGetInt(p); b = memGetInt(p + 4); c = memGetInt(p + 8); d = memGetInt(p + 12);
}
case GL11.GL_UNSIGNED_SHORT -> {
final long p = srcAddr + (long) i * 8;
a = memGetShort(p) & 0xFFFF; b = memGetShort(p + 2) & 0xFFFF;
c = memGetShort(p + 4) & 0xFFFF; d = memGetShort(p + 6) & 0xFFFF;
}
case GL11.GL_UNSIGNED_BYTE -> {
final long p = srcAddr + (long) i * 4;
a = memGetByte(p) & 0xFF; b = memGetByte(p + 1) & 0xFF;
c = memGetByte(p + 2) & 0xFF; d = memGetByte(p + 3) & 0xFF;
}
default -> { memFree(dst); return; }
}
final long o = (long) i * 24;
memPutInt(dstAddr + o, a); memPutInt(dstAddr + o + 4, b); memPutInt(dstAddr + o + 8, d);
memPutInt(dstAddr + o + 12, b); memPutInt(dstAddr + o + 16, c); memPutInt(dstAddr + o + 20, d);
}
triangulateQuads(srcAddr, type, memAddress0(dst), GL11.GL_UNSIGNED_INT, quadCount);
uploadAndDraw(dst, triIndexCount, GL11.GL_UNSIGNED_INT, 4);
}
@@ -282,14 +251,140 @@ public final class QuadConverter {
*/
public static void destroy() {
if (eboId != 0) {
RENDER_BACKEND.deleteBuffers(eboId);
GLStateManager.glDeleteBuffers(eboId);
eboId = 0;
maxQuads = 0;
}
if (scratchEboId != 0) {
RENDER_BACKEND.deleteBuffers(scratchEboId);
GLStateManager.glDeleteBuffers(scratchEboId);
scratchEboId = 0;
scratchEboCapacity = 0;
}
}
/**
* Read one index at {@code srcAddr + i * sizeof(srcType)} and return its
* zero-extended unsigned value.
*/
private static int readIndex(long srcAddr, int srcType, int i) {
return switch (srcType) {
case GL11.GL_UNSIGNED_BYTE -> memGetByte(srcAddr + i) & 0xFF;
case GL11.GL_UNSIGNED_SHORT -> memGetShort(srcAddr + i * 2L) & 0xFFFF;
case GL11.GL_UNSIGNED_INT -> memGetInt(srcAddr + i * 4L);
default -> throw new IllegalArgumentException("unsupported index type 0x" + Integer.toHexString(srcType));
};
}
/**
* Write one index at {@code dstAddr + i * sizeof(dstType)}.
*/
private static void writeIndex(long dstAddr, int dstType, int i, int value) {
switch (dstType) {
case GL11.GL_UNSIGNED_SHORT -> memPutShort(dstAddr + i * 2L, (short) value);
case GL11.GL_UNSIGNED_INT -> memPutInt(dstAddr + i * 4L, value);
default -> throw new IllegalArgumentException("unsupported index type 0x" + Integer.toHexString(dstType));
}
}
/**
* Read {@code quadCount} quads from {@code srcAddr} and emit 6 triangle indices
* per quad at {@code dstAddr} using {@code GL_LAST_VERTEX_CONVENTION}:
* {@code (a,b,d), (b,c,d)}. v3 is the provoking vertex for both triangles;
* diagonal runs b→d. Matches Mesa's {@code do_quad} LAST path.
*
* <p>Caller allocates dst of size {@code quadCount * 6 * sizeof(dstType)}.
*
* <p>Supported src types: {@code GL_UNSIGNED_BYTE} / {@code GL_UNSIGNED_SHORT}
* / {@code GL_UNSIGNED_INT}. Supported dst types: {@code GL_UNSIGNED_SHORT} /
* {@code GL_UNSIGNED_INT}. Widening only.
*/
public static void triangulateQuads(long srcAddr, int srcType, long dstAddr, int dstType, int quadCount) {
triangulateQuads(srcAddr, srcType, dstAddr, dstType, quadCount, 0);
}
/**
* Same as {@link #triangulateQuads(long, int, long, int, int)} but subtracts
* {@code minVtx} from every emitted index, rebasing to a 0-based buffer in
* one pass.
*/
public static void triangulateQuads(long srcAddr, int srcType, long dstAddr, int dstType, int quadCount, int minVtx) {
for (int i = 0; i < quadCount; i++) {
final int a = readIndex(srcAddr, srcType, i * 4) - minVtx;
final int b = readIndex(srcAddr, srcType, i * 4 + 1) - minVtx;
final int c = readIndex(srcAddr, srcType, i * 4 + 2) - minVtx;
final int d = readIndex(srcAddr, srcType, i * 4 + 3) - minVtx;
final int o = i * 6;
writeIndex(dstAddr, dstType, o, a);
writeIndex(dstAddr, dstType, o + 1, b);
writeIndex(dstAddr, dstType, o + 2, d);
writeIndex(dstAddr, dstType, o + 3, b);
writeIndex(dstAddr, dstType, o + 4, c);
writeIndex(dstAddr, dstType, o + 5, d);
}
}
/**
* Copy {@code count} indices from {@code srcAddr} to {@code dstAddr}, converting
* from {@code srcType} to {@code dstType}.
*
* <p>Supported src types: {@code GL_UNSIGNED_BYTE} / {@code GL_UNSIGNED_SHORT} /
* {@code GL_UNSIGNED_INT}. Supported dst types: {@code GL_UNSIGNED_SHORT} /
* {@code GL_UNSIGNED_INT}. Widening only; narrowing is rejected by
* {@link #writeIndex(long, int, int, int) writeIndex}.
*/
public static void widenIndices(long srcAddr, int srcType, long dstAddr, int dstType, int count) {
widenIndices(srcAddr, srcType, dstAddr, dstType, count, 0);
}
/**
* Same as {@link #widenIndices(long, int, long, int, int)} but subtracts
* {@code minVtx} from every written index in one pass.
*/
public static void widenIndices(long srcAddr, int srcType, long dstAddr, int dstType, int count, int minVtx) {
for (int i = 0; i < count; i++) {
writeIndex(dstAddr, dstType, i, readIndex(srcAddr, srcType, i) - minVtx);
}
}
/**
* Scan {@code count} indices at {@code srcAddr} of {@code srcType}. Returns
* {@code (max << 32) | (min & 0xFFFFFFFFL)} or {@code -1L} for an unsupported
* type or empty range.
*/
public static long scanMinMaxIndex(long srcAddr, int srcType, int count) {
if (count <= 0) return -1L;
long min = Long.MAX_VALUE;
long max = Long.MIN_VALUE;
switch (srcType) {
case GL11.GL_UNSIGNED_BYTE -> {
for (int i = 0; i < count; i++) {
final int v = memGetByte(srcAddr + i) & 0xFF;
if (v < min) min = v;
if (v > max) max = v;
}
}
case GL11.GL_UNSIGNED_SHORT -> {
for (int i = 0; i < count; i++) {
final int v = memGetShort(srcAddr + i * 2L) & 0xFFFF;
if (v < min) min = v;
if (v > max) max = v;
}
}
case GL11.GL_UNSIGNED_INT -> {
for (int i = 0; i < count; i++) {
final long v = memGetInt(srcAddr + i * 4L) & 0xFFFFFFFFL;
if (v < min) min = v;
if (v > max) max = v;
}
// readIndex / writeIndex / rebase all route UINT through signed int.
// Reject the > 2^31 slice so the caller bails rather than silently
// emitting negative indices. MC never hits this in practice.
if (max > Integer.MAX_VALUE) return -1L;
}
default -> {
return -1L;
}
}
return (max << 32) | (min & 0xFFFFFFFFL);
}
}
@@ -8,6 +8,7 @@ import com.gtnewhorizons.angelica.glsm.dsa.DSAEXT;
import com.gtnewhorizons.angelica.glsm.dsa.DSAUnsupported;
import com.gtnewhorizons.angelica.glsm.ffp.ShaderManager;
import com.gtnewhorizons.angelica.glsm.texture.TextureInfoCache;
import lombok.Getter;
import org.jetbrains.annotations.Nullable;
import org.joml.Matrix4f;
import org.joml.Vector3i;
@@ -47,6 +48,9 @@ public class RenderSystem {
private static int maxSSBOBindings;
private static int maxGlslVersion;
private static boolean supportsGpuShader4;
@Getter private static int glMajor;
@Getter private static int glMinor;
@Getter private static boolean coreProfile;
// Sampler object state tracking (null if unsupported)
private static int[] samplers;
@@ -61,7 +65,13 @@ public class RenderSystem {
if (rendererInitialized) return;
rendererInitialized = true;
try {
if (GLStateManager.capabilities.OpenGL45) {
if (GLStateManager.vendorIsIntel() && GLStateManager.isWindows()) {
dsaState = new DSAUnsupported();
GLStateManager.LOGGER.info("Detected Intel drivers on Windows, disabling DSA.");
} else if (!GLStateManager.getInitConfig().isDSAEnabled()) {
dsaState = new DSAUnsupported();
GLStateManager.LOGGER.info("enableDSA is set to false, disabling DSA.");
} else if (GLStateManager.capabilities.OpenGL45) {
dsaState = (Runtime.version().feature() > 8 && GLStateManager.capabilities.GL_EXT_direct_state_access) ? new DSAEXT() : new DSACore();
GLStateManager.LOGGER.info("OpenGL 4.5 detected, enabling DSA.");
}
@@ -133,10 +143,20 @@ public class RenderSystem {
if (GLStateManager.capabilities.OpenGL32) {
final int profileMask = RENDER_BACKEND.getInteger(GL32.GL_CONTEXT_PROFILE_MASK);
if ((profileMask & GL32.GL_CONTEXT_CORE_PROFILE_BIT) != 0) {
coreProfile = true;
GLStateManager.LOGGER.info("GL 3.3 core profile detected, enabling FFP shader emulation.");
ShaderManager.getInstance().enable();
} else {
throw new IllegalStateException("Non-core GL context (profile mask 0x" + Integer.toHexString(profileMask)
+ "); FFP emulation requires a core profile and nothing would render. Context creation should have rejected this.");
}
}
final Matcher glVerMatcher = SEMVER_PATTERN.matcher(RENDER_BACKEND.getString(GL11.GL_VERSION));
if (glVerMatcher.matches()) {
glMajor = Integer.parseInt(glVerMatcher.group("major"));
glMinor = Integer.parseInt(glVerMatcher.group("minor"));
}
}
public static void generateMipmaps(int texture, int mipmapTarget) {
@@ -149,7 +169,9 @@ public class RenderSystem {
public static void texImage2D(int texture, int target, int level, int internalformat, int width, int height, int border, int format, int type, @Nullable ByteBuffer pixels) {
GLStateManager.glBindTexture(target, texture);
GLStateManager.suspendPixelUnpackBuffer();
RENDER_BACKEND.texImage2D(target, level, internalformat, width, height, border, format, type, pixels);
GLStateManager.restorePixelUnpackBuffer();
if (target == GL11.GL_TEXTURE_2D && level == 0) {
TextureInfoCache.INSTANCE.onTexImage2D(target, level, internalformat, width, height, border, format, type, pixels);
}
@@ -218,29 +240,41 @@ public class RenderSystem {
}
public static void textureImage2D(int texture, int target, int level, int internalformat, int width, int height, int border, int format, int type, ByteBuffer pixels) {
GLStateManager.suspendPixelUnpackBuffer();
dsaState.textureImage2D(texture, target, level, internalformat, width, height, border, format, type, pixels);
GLStateManager.restorePixelUnpackBuffer();
}
public static void textureImage2D(int texture, int target, int level, int internalformat, int width, int height, int border, int format, int type, IntBuffer pixels) {
GLStateManager.suspendPixelUnpackBuffer();
dsaState.textureImage2D(texture, target, level, internalformat, width, height, border, format, type, pixels);
GLStateManager.restorePixelUnpackBuffer();
}
public static void textureSubImage2D(int texture, int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, ByteBuffer pixels) {
GLStateManager.suspendPixelUnpackBuffer();
dsaState.textureSubImage2D(texture, target, level, xoffset, yoffset, width, height, format, type, pixels);
GLStateManager.restorePixelUnpackBuffer();
}
public static void textureSubImage2D(int texture, int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, IntBuffer pixels) {
GLStateManager.suspendPixelUnpackBuffer();
dsaState.textureSubImage2D(texture, target, level, xoffset, yoffset, width, height, format, type, pixels);
GLStateManager.restorePixelUnpackBuffer();
}
public static void texImage1D(int texture, int target, int level, int internalformat, int width, int border, int format, int type, ByteBuffer pixels) {
RENDER_BACKEND.bindTexture(target, texture);
GLStateManager.suspendPixelUnpackBuffer();
RENDER_BACKEND.texImage1D(target, level, internalformat, width, border, format, type, pixels);
GLStateManager.restorePixelUnpackBuffer();
}
public static void texImage3D(int texture, int target, int level, int internalformat, int width, int height, int depth, int border, int format, int type, ByteBuffer pixels) {
RENDER_BACKEND.bindTexture(target, texture);
GLStateManager.suspendPixelUnpackBuffer();
RENDER_BACKEND.texImage3D(target, level, internalformat, width, height, depth, border, format, type, pixels);
GLStateManager.restorePixelUnpackBuffer();
}
public static String getProgramInfoLog(int program) {
@@ -264,11 +298,13 @@ public class RenderSystem {
}
public static void readPixels(int x, int y, int width, int height, int format, int type, FloatBuffer pixels) {
GLStateManager.suspendPixelPackBuffer();
RENDER_BACKEND.readPixels(x, y, width, height, format, type, pixels);
GLStateManager.restorePixelPackBuffer();
}
public static void bufferData(int target, FloatBuffer data, int usage) {
RENDER_BACKEND.bufferData(target, data, usage);
GLStateManager.glBufferData(target, data, usage);
}
public static int bufferStorage(int target, FloatBuffer data, int usage) {
@@ -414,7 +450,7 @@ public class RenderSystem {
}
public static int createBuffers() {
return RENDER_BACKEND.genBuffers();
return GLStateManager.glGenBuffers();
}
public static void bindBufferBase(int target, int index, int buffer) {
@@ -422,17 +458,17 @@ public class RenderSystem {
}
public static void bufferStorage(int target, long size, int flags) {
RENDER_BACKEND.bufferStorage(target, size, flags);
GLStateManager.glBufferStorage(target, size, flags);
}
public static void clearBufferSubData(int target, int internalFormat, long offset, long size, int format, int type, int[] data) {
final ByteBuffer buf = BufferUtils.createByteBuffer(data.length * 4);
buf.asIntBuffer().put(data);
RENDER_BACKEND.clearBufferSubData(target, internalFormat, offset, size, format, type, buf);
GLStateManager.glClearBufferSubData(target, internalFormat, offset, size, format, type, buf);
}
public static void deleteBuffers(int buffer) {
RENDER_BACKEND.deleteBuffers(buffer);
GLStateManager.glDeleteBuffers(buffer);
}
public static long getVRAM() {
@@ -491,7 +527,7 @@ public class RenderSystem {
public static void destroySampler(int sampler) {
if (!supportsSamplerObjects || sampler == 0) return;
RENDER_BACKEND.deleteSamplers(sampler);
GLStateManager.glDeleteSamplers(sampler);
}
public static void samplerParameteri(int sampler, int pname, int param) {
@@ -516,7 +552,7 @@ public class RenderSystem {
}
}
/** Parse a GL version string (e.g. "4.6.0 NVIDIA ...") into concatenated digits "460". Inlined from StandardMacros.getGlVersion(). */
/** Parse a GL version string (e.g. "4.6.0 NVIDIA ...") into concatenated digits "460" */
static String parseGlVersionString(String info) {
final Matcher matcher = SEMVER_PATTERN.matcher(Objects.requireNonNull(info));
if (!matcher.matches()) {
@@ -528,4 +564,18 @@ public class RenderSystem {
if (bugfix == null || bugfix.isEmpty()) bugfix = "0";
return major + minor + bugfix;
}
/** Parse a GL version string into packed major*10+minor (e.g. "4.3.0 - Build ..." -> 43), or -1 if unparseable. */
public static int parseMajorMinor(String glVersion) {
if (glVersion == null) return -1;
final Matcher matcher = SEMVER_PATTERN.matcher(glVersion.trim());
if (!matcher.matches()) return -1;
return Integer.parseInt(matcher.group("major")) * 10 + Integer.parseInt(matcher.group("minor"));
}
public static boolean isContextValid(int requestedMajor, int requestedMinor, int actualVersion, boolean coreBitSet) {
if (!coreBitSet) return false;
if (actualVersion < 0) return true;
return actualVersion >= requestedMajor * 10 + requestedMinor;
}
}
@@ -1,132 +0,0 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizons.angelica.glsm.recording.commands.DisplayListCommand;
import com.gtnewhorizons.angelica.glsm.recording.commands.MultMatrixCmd;
import org.joml.Matrix4f;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.List;
/**
* Helper class for tracking and collapsing matrix transforms during optimization.
* Implements the transform accumulation and emission strategy:
* <ul>
* <li>accumulated: total relative transform (what GL matrix should be)</li>
* <li>lastEmitted: what we've emitted (what GL matrix is)</li>
* <li>stack: saved accumulated values at Push points</li>
* </ul>
*/
class TransformOptimizer {
private final Matrix4f accumulated = new Matrix4f(); // Current accumulated transform
private final Matrix4f lastEmitted = new Matrix4f(); // Last emitted transform
private final Deque<Matrix4f> stack = new ArrayDeque<>(); // For Push/Pop
private final int listId; // For logging
private boolean absoluteMatrixFlag = false; // True after LoadMatrix, cleared by LoadIdentity emit
TransformOptimizer(int listId) {
this.listId = listId;
accumulated.identity();
lastEmitted.identity();
}
Matrix4f getAccumulated() {
return accumulated;
}
void loadIdentity() {
accumulated.identity();
}
void pushTransform() {
stack.push(new Matrix4f(accumulated));
}
void popTransform() {
if (!stack.isEmpty()) {
final Matrix4f popped = stack.pop();
accumulated.set(popped);
// After Pop, GL state is restored to the pushed value so lastEmitted should also reflect this
lastEmitted.set(accumulated);
} else {
GLStateManager.LOGGER.warn("[TransformOptimizer] list={} Pop with empty stack - resetting to identity", listId);
accumulated.identity();
lastEmitted.identity();
}
}
boolean isIdentity() {
return DisplayListManager.isIdentity(accumulated);
}
/**
* Check if there's a pending transform that hasn't been emitted yet.
*/
boolean hasPendingTransform() {
return !accumulated.equals(lastEmitted);
}
/**
* Emit a MultMatrix command if accumulated differs from lastEmitted.
* Updates lastEmitted to match accumulated after emission.
*/
void emitPendingTransform(List<DisplayListCommand> output) {
if (!accumulated.equals(lastEmitted)) {
emitTransformTo(output, accumulated);
}
}
/**
* Emit a MultMatrix command to make GL state match a target transform.
* Used when a draw expects a specific transform that may differ from accumulated.
* Updates lastEmitted to match target after emission.
*
* @param output The command list to emit to
* @param target The target transform to reach
*/
void emitTransformTo(List<DisplayListCommand> output, Matrix4f target) {
if (target.equals(lastEmitted)) {
// Already at target - nothing to emit
return;
}
// Calculate delta: what we need to multiply to go from lastEmitted to target
// delta = inverse(lastEmitted) * target
// But for simplicity, if lastEmitted is identity, just emit target
if (DisplayListManager.isIdentity(lastEmitted)) {
// Simple case: just emit target
output.add(MultMatrixCmd.create(target));
} else {
// Need to emit delta: inv(lastEmitted) * target
final Matrix4f delta = new Matrix4f(lastEmitted).invert().mul(target);
output.add(MultMatrixCmd.create(delta));
}
lastEmitted.set(target);
}
/**
* Check if a draw's expected transform differs from what we've emitted.
*/
boolean needsTransformForDraw(Matrix4f drawTransform) {
return !drawTransform.equals(lastEmitted);
}
/**
* Mark that the GL matrix is at an absolute value (after LoadMatrix).
* Subsequent LoadIdentity commands must be emitted to actually reset GL to identity.
*/
void markAbsoluteMatrix() {
absoluteMatrixFlag = true;
}
/**
* Check if the GL matrix is at an absolute value and clear the flag.
* Used to determine if LoadIdentity needs to emit.
* @return true if LoadMatrix was called and not yet reset by LoadIdentity
*/
boolean checkAndClearAbsoluteMatrix() {
boolean was = absoluteMatrixFlag;
absoluteMatrixFlag = false;
return was;
}
}
@@ -9,6 +9,7 @@ import org.lwjgl.opengl.ARBDebugOutputCallback;
import org.lwjgl.opengl.ARBDirectStateAccess;
import org.lwjgl.opengl.ContextCapabilities;
import org.lwjgl.opengl.EXTDirectStateAccess;
import org.lwjgl.opengl.GLContext;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL12;
import org.lwjgl.opengl.GL13;
@@ -67,7 +68,7 @@ public final class Lwjgl2GLRenderBackend extends RenderBackend {
@Override
public void init() {
caps = org.lwjgl.opengl.GLContext.getCapabilities();
caps = GLContext.getCapabilities();
activeDebugExtension = 0;
}
@@ -86,6 +87,15 @@ public final class Lwjgl2GLRenderBackend extends RenderBackend {
return "OpenGL (LWJGL2)";
}
@Override
public boolean hasContext() {
try {
return GLContext.getCapabilities() != null;
} catch (Exception e) {
return false;
}
}
@Override
public int getMinGLSLVersion() {return 330;}
@@ -300,6 +310,16 @@ public final class Lwjgl2GLRenderBackend extends RenderBackend {
GL31.glDrawElementsInstanced(mode, count, type, indices, primcount);
}
@Override
public void drawArraysInstanced(int mode, int first, int count, int primcount) {
GL31.glDrawArraysInstanced(mode, first, count, primcount);
}
@Override
public void provokingVertex(int provokeMode) {
GL32.glProvokingVertex(provokeMode);
}
@Override
public void multiDrawElementsIndirect(int mode, int type, long indirect, int drawcount, int stride) {
org.lwjgl.opengl.GL43.glMultiDrawElementsIndirect(mode, type, indirect, drawcount, stride);
@@ -307,7 +327,7 @@ public final class Lwjgl2GLRenderBackend extends RenderBackend {
@Override
public void copyBufferSubData(int readTarget, int writeTarget, long readOffset, long writeOffset, long size) {
throw new UnsupportedOperationException("GLRenderBackend.copyBufferSubData should not be called — LWJGL service handles this");
GL31.glCopyBufferSubData(readTarget, writeTarget, readOffset, writeOffset, size);
}
@Override
@@ -497,11 +517,6 @@ public final class Lwjgl2GLRenderBackend extends RenderBackend {
GL11.glPixelStorei(pname, param);
}
@Override
public void pixelStoref(int pname, float param) {
GL11.glPixelStoref(pname, param);
}
@Override
public void sampleCoverage(float value, boolean invert) {
GL13.glSampleCoverage(value, invert);
@@ -607,6 +622,11 @@ public final class Lwjgl2GLRenderBackend extends RenderBackend {
GL11.glGetTexImage(target, level, format, type, pixels);
}
@Override
public void getTexImage(int target, int level, int format, int type, long pixelBufferOffset) {
GL11.glGetTexImage(target, level, format, type, pixelBufferOffset);
}
@Override
public int getFramebufferAttachmentParameteri(int target, int attachment, int pname) {
return GL30.glGetFramebufferAttachmentParameteri(target, attachment, pname);
@@ -807,6 +827,11 @@ public final class Lwjgl2GLRenderBackend extends RenderBackend {
GL20.glUniform4i(location, v0, v1, v2, v3);
}
@Override
public void uniform2(int location, FloatBuffer value) {
GL20.glUniform2(location, value);
}
@Override
public void uniform3(int location, FloatBuffer value) {
GL20.glUniform3(location, value);
@@ -817,6 +842,26 @@ public final class Lwjgl2GLRenderBackend extends RenderBackend {
GL20.glUniform4(location, value);
}
@Override
public void uniform1iv(int location, IntBuffer value) {
GL20.glUniform1(location, value);
}
@Override
public void uniform2iv(int location, IntBuffer value) {
GL20.glUniform2(location, value);
}
@Override
public void uniform3iv(int location, IntBuffer value) {
GL20.glUniform3(location, value);
}
@Override
public void uniform4iv(int location, IntBuffer value) {
GL20.glUniform4(location, value);
}
@Override
public void uniformMatrix3(int location, boolean transpose, FloatBuffer value) {
GL20.glUniformMatrix3(location, transpose, value);
@@ -1018,6 +1063,11 @@ public final class Lwjgl2GLRenderBackend extends RenderBackend {
return GL30.glMapBufferRange(target, offset, length, access, null);
}
@Override
public void flushMappedBufferRange(int target, long offset, long length) {
GL30.glFlushMappedBufferRange(target, offset, length);
}
@Override
public int genVertexArrays() {
return GL30.glGenVertexArrays();
@@ -1033,6 +1083,11 @@ public final class Lwjgl2GLRenderBackend extends RenderBackend {
GL30.glBindVertexArray(array);
}
@Override
public boolean isVertexArray(int array) {
return GL30.glIsVertexArray(array);
}
@Override
public void vertexAttribPointer(int index, int size, int type, boolean normalized, int stride, long pointer) {
GL20.glVertexAttribPointer(index, size, type, normalized, stride, pointer);
@@ -1204,6 +1259,11 @@ public final class Lwjgl2GLRenderBackend extends RenderBackend {
return GL45.glCreateBuffers();
}
@Override
public void namedBufferData(int buffer, long size, int usage) {
GL45.glNamedBufferData(buffer, size, usage);
}
@Override
public void namedBufferData(int buffer, ByteBuffer data, int usage) {
GL45.glNamedBufferData(buffer, data, usage);
@@ -8,6 +8,8 @@ import java.nio.DoubleBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.nio.ShortBuffer;
import java.util.Collections;
import java.util.List;
/**
* Abstract rendering backend.
@@ -19,9 +21,20 @@ public abstract class RenderBackend {
public abstract boolean isAvailable();
public abstract String getName();
/** Returns true if the current thread has a valid render context. Safe to call from any thread. */
public abstract boolean hasContext();
/** Higher priority backends are preferred. */
public int getPriority() { return 0; }
public boolean supportsFileDrop() { return false; }
public void startFileDrop() {}
public void stopFileDrop() {}
public List<String> pollDroppedFiles() { return Collections.emptyList(); }
public abstract int getMinGLSLVersion();
public abstract void flush();
@@ -74,6 +87,8 @@ public abstract class RenderBackend {
public abstract void multiDrawElementsIndirect(int mode, int type, long indirect, int drawcount, int stride);
public abstract void copyBufferSubData(int readTarget, int writeTarget, long readOffset, long writeOffset, long size);
public abstract void drawElementsInstanced(int mode, int count, int type, long indices, int primcount);
public abstract void drawArraysInstanced(int mode, int first, int count, int primcount);
public void provokingVertex(int provokeMode) {}
public abstract void drawElementsBaseVertex(int mode, int count, int type, long indices, int baseVertex);
public abstract void multiDrawElementsBaseVertex(int mode, long pCount, int type, long pIndices, int drawcount, long pBaseVertex);
public abstract void drawBuffer(int mode);
@@ -112,7 +127,6 @@ public abstract class RenderBackend {
public abstract int getTexLevelParameteri(int target, int level, int pname);
public abstract void generateMipmap(int target);
public abstract void pixelStorei(int pname, int param);
public void pixelStoref(int pname, float param) {}
public void sampleCoverage(float value, boolean invert) {}
@@ -137,6 +151,7 @@ public abstract class RenderBackend {
public abstract void readPixels(int x, int y, int width, int height, int format, int type, IntBuffer pixels);
public abstract void getTexImage(int target, int level, int format, int type, ByteBuffer pixels);
public abstract void getTexImage(int target, int level, int format, int type, IntBuffer pixels);
public abstract void getTexImage(int target, int level, int format, int type, long pixelBufferOffset);
public abstract int getFramebufferAttachmentParameteri(int target, int attachment, int pname);
public abstract int createShader(int type);
@@ -187,8 +202,13 @@ public abstract class RenderBackend {
public abstract void uniform3i(int location, int v0, int v1, int v2);
public abstract void uniform4f(int location, float v0, float v1, float v2, float v3);
public abstract void uniform4i(int location, int v0, int v1, int v2, int v3);
public abstract void uniform2(int location, FloatBuffer value);
public abstract void uniform3(int location, FloatBuffer value);
public abstract void uniform4(int location, FloatBuffer value);
public abstract void uniform1iv(int location, IntBuffer value);
public abstract void uniform2iv(int location, IntBuffer value);
public abstract void uniform3iv(int location, IntBuffer value);
public abstract void uniform4iv(int location, IntBuffer value);
public abstract void uniformMatrix3(int location, boolean transpose, FloatBuffer value);
public abstract void uniformMatrix4(int location, boolean transpose, FloatBuffer value);
public void uniformMatrix2(int location, boolean transpose, FloatBuffer matrices) {}
@@ -230,10 +250,12 @@ public abstract class RenderBackend {
public abstract int getBufferParameteri(int target, int pname);
public abstract boolean isBuffer(int buffer);
public abstract ByteBuffer mapBufferRange(int target, long offset, long length, int access);
public abstract void flushMappedBufferRange(int target, long offset, long length);
public abstract int genVertexArrays();
public abstract void deleteVertexArrays(int array);
public abstract void bindVertexArray(int array);
public abstract boolean isVertexArray(int array);
public abstract void vertexAttribPointer(int index, int size, int type, boolean normalized, int stride, long pointer);
public void vertexAttribPointer(int index, int size, int type, boolean normalized, int stride, ByteBuffer pointer) {
vertexAttribPointer(index, size, type, normalized, stride, MemoryUtilities.memAddress0(pointer));
@@ -272,6 +294,7 @@ public abstract class RenderBackend {
public abstract void namedFramebufferDrawBuffers(int framebuffer, IntBuffer bufs);
public abstract void blitNamedFramebuffer(int readFramebuffer, int drawFramebuffer, int srcX0, int srcY0, int srcX1, int srcY1, int dstX0, int dstY0, int dstX1, int dstY1, int mask, int filter);
public abstract int createBuffers();
public abstract void namedBufferData(int buffer, long size, int usage);
public abstract void namedBufferData(int buffer, ByteBuffer data, int usage);
public abstract void namedBufferData(int buffer, FloatBuffer data, int usage);
public abstract void namedBufferSubData(int buffer, long offset, ByteBuffer data);
@@ -104,8 +104,8 @@ public class DSAARB extends DSAUnsupported {
@Override
public int bufferStorage(int target, FloatBuffer data, int usage) {
final int buffer = RENDER_BACKEND.createBuffers();
RENDER_BACKEND.namedBufferData(buffer, data, usage);
final int buffer = GLStateManager.glCreateBuffers();
GLStateManager.glNamedBufferData(buffer, data, usage);
return buffer;
}
@@ -121,9 +121,9 @@ public class DSAUnsupported implements DSAAccess {
@Override
public int bufferStorage(int target, FloatBuffer data, int usage) {
final int buffer = RENDER_BACKEND.genBuffers();
final int buffer = GLStateManager.glGenBuffers();
GLStateManager.glBindBuffer(target, buffer);
RENDER_BACKEND.bufferData(target, data, usage);
GLStateManager.glBufferData(target, data, usage);
GLStateManager.glBindBuffer(target, 0);
return buffer;
@@ -12,7 +12,7 @@ import org.lwjgl.opengl.GL13;
* Packed-long fragment shader permutation key for FFP emulation.
*
* Layout:
* long[0]: global bits (10) + unit 0 (47 bits at offset 10)
* long[0]: global bits (12) + unit 0 (47 bits at offset 12)
* long[1..3]: unit 1..3 (47 bits each, low-aligned)
* Only max(1, nrEnabledUnits) longs are significant.
*
@@ -63,12 +63,14 @@ public final class FragmentKey {
public static final int FOG_EXP = 2;
public static final int FOG_EXP2 = 3;
private static final int GLOBAL_BITS = 10;
private static final int GLOBAL_BITS = 12;
private static final int BIT_FOG_MODE = 0; // 2 bits
private static final int BIT_ALPHA_TEST = 2; // 1 bit
private static final int BIT_ALPHA_FUNC = 3; // 3 bits
private static final int BIT_SEPARATE_SPECULAR = 6; // 1 bit
private static final int BIT_NR_ENABLED_UNITS = 7; // 3 bits
private static final int BIT_OVERLAY_ENABLED = 10; // 1 bit
private static final int BIT_LINE_STIPPLE = 11; // 1 bit
private static final int U_ENABLED = 0;
private static final int U_MODE = 1; // 3 bits
@@ -119,6 +121,15 @@ public final class FragmentKey {
global |= ((long) (encodeAlphaFunc(GLStateManager.getAlphaState().getFunction()) & 0x7)) << BIT_ALPHA_FUNC;
}
// Damage overlay
if (GLStateManager.getOverlayA() != 0.0f) {
global |= (1L << BIT_OVERLAY_ENABLED);
}
if (GLStateManager.lineStippleActive) {
global |= (1L << BIT_LINE_STIPPLE);
}
// Separate specular
if (GLStateManager.getLightingState().isEnabled()
&& GLStateManager.getLightModel().colorControl == GL12.GL_SEPARATE_SPECULAR_COLOR) {
@@ -129,7 +140,8 @@ public final class FragmentKey {
int highestEnabled = -1;
long unit0Bits = 0;
for (int i = 0; i < MAX_UNITS; i++) {
final boolean texEnabled = GLStateManager.getTextures().getTextureUnitStates(i).isEnabled();
// Per Mesa - an enabled unit with no complete texture bound counts as disabled.
final boolean texEnabled = GLStateManager.getTextures().getTextureUnitStates(i).isEnabled() && GLStateManager.getTextures().getTextureUnitBindings(i).getBinding() != 0;
if (texEnabled) highestEnabled = i;
final long unitBits = packUnit(i, texEnabled);
@@ -226,7 +238,9 @@ public final class FragmentKey {
public boolean alphaTestEnabled() { return ((packed[0] >> BIT_ALPHA_TEST) & 1) != 0; }
public int alphaTestFunc() { return (int) ((packed[0] >> BIT_ALPHA_FUNC) & 0x7); }
public boolean separateSpecular() { return ((packed[0] >> BIT_SEPARATE_SPECULAR) & 1) != 0; }
public boolean lineStipple() { return ((packed[0] >> BIT_LINE_STIPPLE) & 1) != 0; }
public int nrEnabledUnits() { return (int) ((packed[0] >> BIT_NR_ENABLED_UNITS) & 0x7); }
public boolean overlayEnabled() { return ((packed[0] >> BIT_OVERLAY_ENABLED) & 1) != 0; }
private long unitBits(int i) {
return (i == 0) ? (packed[0] >>> GLOBAL_BITS) : packed[i];
@@ -252,6 +266,28 @@ public final class FragmentKey {
return false;
}
/** Per-unit enable bits as a 4-bit mask (bit i = unit i enabled). */
public int enabledUnitMask() {
int mask = 0;
final int n = Math.min(nrEnabledUnits(), MAX_UNITS);
for (int i = 0; i < n; i++) {
if (unitEnabled(i)) mask |= (1 << i);
}
return mask;
}
/** Per-unit enable bits from a packed scratch as a 4-bit mask (bit i = unit i enabled). */
public static int unitMaskFromPacked(long[] packed, int len) {
int mask = 0;
if (len < 1) return 0;
if (((packed[0] >>> GLOBAL_BITS) & 1L) != 0) mask |= 1;
final int n = Math.min(len, MAX_UNITS);
for (int i = 1; i < n; i++) {
if ((packed[i] & 1L) != 0) mask |= (1 << i);
}
return mask;
}
public boolean lightmapEnabled() {
return nrEnabledUnits() > 1 && unitEnabled(1);
}
@@ -338,10 +374,10 @@ public final class FragmentKey {
default -> "?";
};
final StringBuilder sb = new StringBuilder();
sb.append(String.format("FFPFragmentKey[fog=%s alpha=%b(%s) specSep=%b units=%d",
sb.append(String.format("FFPFragmentKey[fog=%s alpha=%b(%s) specSep=%b overlay=%b units=%d",
fogName, alphaTestEnabled(),
alphaTestEnabled() ? String.format("0x%04X", decodeAlphaFunc(alphaTestFunc())) : "-",
separateSpecular(), nrEnabledUnits()));
separateSpecular(), overlayEnabled(), nrEnabledUnits()));
for (int i = 0; i < nrEnabledUnits(); i++) {
if (!unitEnabled(i)) {
sb.append(String.format(" u%d=OFF", i));
@@ -20,6 +20,7 @@ public final class FragmentShaderGenerator {
sb.append("out vec4 fragColor;\n\n");
sb.append("void main() {\n");
emitLineStipple(sb, key);
emitTextureSampling(sb, key);
emitTexEnvChain(sb, key);
emitSpecularAdd(sb, key);
@@ -37,18 +38,28 @@ public final class FragmentShaderGenerator {
if (key.separateSpecular()) {
sb.append("in vec3 v_SpecularColor;\n");
}
if (key.textureEnabled()) {
sb.append("in vec4 v_TexCoord0;\n");
}
if (key.lightmapEnabled()) {
sb.append("in vec4 v_TexCoord1;\n");
// Per-unit texcoord inputs. Each enabled unit sources from its own varying
for (int i = 0; i < key.nrEnabledUnits(); i++) {
if (key.unitEnabled(i)) {
sb.append("in vec4 v_TexCoord").append(i).append(";\n");
}
}
if (key.fogMode() != FragmentKey.FOG_NONE) {
sb.append("in float v_FogCoord;\n");
}
if (key.lineStipple()) {
sb.append("flat in vec2 v_LineStart;\n");
}
sb.append('\n');
}
private static void emitLineStipple(StringBuilder sb, FragmentKey key) {
if (!key.lineStipple()) return;
sb.append(" vec2 stippleDelta = abs(gl_FragCoord.xy - v_LineStart);\n");
sb.append(" int stippleBit = int(floor(max(stippleDelta.x, stippleDelta.y) / float(u_LineStipple >> 16))) & 15;\n");
sb.append(" if (((u_LineStipple >> stippleBit) & 1) == 0) discard;\n\n");
}
private static void emitUniforms(StringBuilder sb, FragmentKey key) {
// Samplers for each enabled unit
for (int i = 0; i < key.nrEnabledUnits(); i++) {
@@ -59,12 +70,18 @@ public final class FragmentShaderGenerator {
if (key.alphaTestEnabled()) {
sb.append("uniform float u_AlphaRef;\n");
}
if (key.lineStipple()) {
sb.append("uniform int u_LineStipple;\n");
}
// Per-unit texEnvColor uniforms
for (int i = 0; i < key.nrEnabledUnits(); i++) {
if (key.unitEnabled(i) && key.unitNeedsEnvColor(i)) {
sb.append("uniform vec4 u_TexEnvColor").append(i).append(";\n");
}
}
if (key.overlayEnabled()) {
sb.append("uniform vec4 u_OverlayColor;\n");
}
if (key.fogMode() != FragmentKey.FOG_NONE) {
sb.append("uniform vec4 u_FogParams;\n");
sb.append("uniform vec4 u_FogColor;\n");
@@ -75,11 +92,7 @@ public final class FragmentShaderGenerator {
private static void emitTextureSampling(StringBuilder sb, FragmentKey key) {
for (int i = 0; i < key.nrEnabledUnits(); i++) {
if (!key.unitEnabled(i)) continue;
// Unit 1 is the lightmap — uses v_TexCoord1.
// Units 2-3 intentionally share unit 0's texture coordinates since
// the vertex shader only provides 2 tex coord varyings (unit 0 and unit 1/lightmap).
final String texCoord = (i == 1) ? "v_TexCoord1.st" : "v_TexCoord0.st";
sb.append(" vec4 tex").append(i).append("Color = texture(u_Sampler").append(i).append(", ").append(texCoord).append(");\n");
sb.append(" vec4 tex").append(i).append("Color = textureProj(u_Sampler").append(i).append(", v_TexCoord").append(i).append(");\n");
}
}
@@ -87,14 +100,21 @@ public final class FragmentShaderGenerator {
if (!key.textureEnabled()) {
sb.append(" // No texture -- vertex color only\n");
sb.append(" vec4 color = v_Color;\n");
emitOverlay(sb, key);
return;
}
// Process units in order; "previous" starts as vertex color
// Process units in order; "previous" starts as vertex color.
boolean firstUnit = true;
boolean overlayInjected = false;
for (int i = 0; i < key.nrEnabledUnits(); i++) {
if (!key.unitEnabled(i)) continue;
if (!firstUnit && !overlayInjected) {
emitOverlay(sb, key);
overlayInjected = true;
}
final String texVar = "tex" + i + "Color";
final String envColorVar = "u_TexEnvColor" + i;
final String prevVar = firstUnit ? "v_Color" : "color";
@@ -107,6 +127,16 @@ public final class FragmentShaderGenerator {
}
firstUnit = false;
}
if (!overlayInjected) emitOverlay(sb, key);
}
/**
* Emit the modern-style damage overlay mix.
*/
private static void emitOverlay(StringBuilder sb, FragmentKey key) {
if (!key.overlayEnabled()) return;
sb.append(" color.rgb = mix(color.rgb, u_OverlayColor.rgb, u_OverlayColor.a);\n");
}
private static void emitSimpleUnit(StringBuilder sb, FragmentKey key, int unit, String texVar, String envColorVar, String prevVar, String assign) {
@@ -22,30 +22,32 @@ public final class GeometryShaderGenerator {
if (key.separateSpecular()) {
sb.append("in vec3 v_SpecularColor_gs[];\n");
}
if (key.textureEnabled() || key.hasVertexTexCoord() || key.texGenEnabled()) {
sb.append("in vec4 v_TexCoord0_gs[];\n");
}
if (key.lightmapEnabled()) {
sb.append("in vec4 v_TexCoord1_gs[];\n");
}
if (key.unitTexCoordEnabled(0) || key.texGenEnabled()) sb.append("in vec4 v_TexCoord0_gs[];\n");
if (key.lightmapEnabled()) sb.append("in vec4 v_TexCoord1_gs[];\n");
if (key.unitTexCoordEnabled(2)) sb.append("in vec4 v_TexCoord2_gs[];\n");
if (key.unitTexCoordEnabled(3)) sb.append("in vec4 v_TexCoord3_gs[];\n");
if (key.fogEnabled()) {
sb.append("in float v_FogCoord_gs[];\n");
}
if (key.lineStipple()) {
sb.append("flat in vec2 v_LineStart_gs[];\n");
}
sb.append('\n');
sb.append("out vec4 v_Color;\n");
if (key.separateSpecular()) {
sb.append("out vec3 v_SpecularColor;\n");
}
if (key.textureEnabled() || key.hasVertexTexCoord() || key.texGenEnabled()) {
sb.append("out vec4 v_TexCoord0;\n");
}
if (key.lightmapEnabled()) {
sb.append("out vec4 v_TexCoord1;\n");
}
if (key.unitTexCoordEnabled(0) || key.texGenEnabled()) sb.append("out vec4 v_TexCoord0;\n");
if (key.lightmapEnabled()) sb.append("out vec4 v_TexCoord1;\n");
if (key.unitTexCoordEnabled(2)) sb.append("out vec4 v_TexCoord2;\n");
if (key.unitTexCoordEnabled(3)) sb.append("out vec4 v_TexCoord3;\n");
if (key.fogEnabled()) {
sb.append("out float v_FogCoord;\n");
}
if (key.lineStipple()) {
sb.append("flat out vec2 v_LineStart;\n");
}
sb.append('\n');
sb.append("void main() {\n");
@@ -82,15 +84,16 @@ public final class GeometryShaderGenerator {
if (key.separateSpecular()) {
sb.append(" v_SpecularColor = v_SpecularColor_gs").append(idx).append(";\n");
}
if (key.textureEnabled() || key.hasVertexTexCoord() || key.texGenEnabled()) {
sb.append(" v_TexCoord0 = v_TexCoord0_gs").append(idx).append(";\n");
}
if (key.lightmapEnabled()) {
sb.append(" v_TexCoord1 = v_TexCoord1_gs").append(idx).append(";\n");
}
if (key.unitTexCoordEnabled(0) || key.texGenEnabled()) sb.append(" v_TexCoord0 = v_TexCoord0_gs").append(idx).append(";\n");
if (key.lightmapEnabled()) sb.append(" v_TexCoord1 = v_TexCoord1_gs").append(idx).append(";\n");
if (key.unitTexCoordEnabled(2)) sb.append(" v_TexCoord2 = v_TexCoord2_gs").append(idx).append(";\n");
if (key.unitTexCoordEnabled(3)) sb.append(" v_TexCoord3 = v_TexCoord3_gs").append(idx).append(";\n");
if (key.fogEnabled()) {
sb.append(" v_FogCoord = v_FogCoord_gs").append(idx).append(";\n");
}
if (key.lineStipple()) {
sb.append(" v_LineStart = v_LineStart_gs[0];\n");
}
if (key.clipPlanesEnabled()) {
for (int i = 0; i < 8; i++) {
@@ -26,18 +26,31 @@ public class Program {
@Getter private final VertexKey vertexKey;
@Getter private final FragmentKey fragmentKey;
// Per-program uniform dirty-tracking
static final class UploadState {
int mvGen = -1, mvLinearGen = -1, projGen = -1, texMatGen = -1;
int lightingGen = -1, fragmentGen = -1, colorGen = -1, normalGen = -1, texCoordGen = -1;
int texGenGen = -1, clipPlaneGen = -1;
float lightmapX = Float.NaN, lightmapY = Float.NaN, lineWidth = Float.NaN;
int viewportWidth = -1, viewportHeight = -1;
int stippleViewportX = Integer.MIN_VALUE, stippleViewportY = Integer.MIN_VALUE;
int stippleViewportW = -1, stippleViewportH = -1;
int lineStipple = -1;
}
final UploadState uploadState = new UploadState();
// Uniform locations (-1 = not present in this variant)
// Matrices
public int locModelViewMatrix = -1;
public int locProjectionMatrix = -1;
public int locMVPMatrix = -1;
public int locNormalMatrix = -1;
public int locTextureMatrix0 = -1;
public final int[] locTextureMatrix = { -1, -1, -1, -1 };
// Current state defaults
public int locCurrentNormal = -1;
public int locCurrentColor = -1;
public int locCurrentTexCoord = -1;
public final int[] locCurrentTexCoord = { -1, -1, -1, -1 };
public int locCurrentLightmapCoord = -1;
public int locLightmapTextureMatrix = -1;
@@ -88,10 +101,15 @@ public class Program {
public int locViewportSize = -1;
public int locLineWidth = -1;
// Line stipple emulation
public int locViewport = -1;
public int locLineStipple = -1;
// Fragment uniforms
public final int[] locSampler = { -1, -1, -1, -1 };
public int locAlphaRef = -1;
public final int[] locTexEnvColor = { -1, -1, -1, -1 };
public int locOverlayColor = -1;
public int locFogParams = -1;
public int locFogColor = -1;
@@ -107,11 +125,13 @@ public class Program {
locProjectionMatrix = loc("u_ProjectionMatrix");
locMVPMatrix = loc("u_MVPMatrix");
locNormalMatrix = loc("u_NormalMatrix");
locTextureMatrix0 = loc("u_TextureMatrix0");
for (int i = 0; i < 4; i++) {
locTextureMatrix[i] = loc("u_TextureMatrix" + i);
locCurrentTexCoord[i] = loc("u_CurrentTexCoord" + i);
}
locCurrentNormal = loc("u_CurrentNormal");
locCurrentColor = loc("u_CurrentColor");
locCurrentTexCoord = loc("u_CurrentTexCoord");
locCurrentLightmapCoord = loc("u_CurrentLightmapCoord");
locLightmapTextureMatrix = loc("u_LightmapTextureMatrix");
@@ -161,6 +181,10 @@ public class Program {
locViewportSize = loc("u_ViewportSize");
locLineWidth = loc("u_LineWidth");
// Line stipple emulation
locViewport = loc("u_Viewport");
locLineStipple = loc("u_LineStipple");
// Fragment
for (int i = 0; i < 4; i++) {
locSampler[i] = loc("u_Sampler" + i);
@@ -169,6 +193,7 @@ public class Program {
for (int i = 0; i < 4; i++) {
locTexEnvColor[i] = loc("u_TexEnvColor" + i);
}
locOverlayColor = loc("u_OverlayColor");
locFogParams = loc("u_FogParams");
locFogColor = loc("u_FogColor");
}
@@ -178,7 +203,7 @@ public class Program {
}
public void destroy() {
RENDER_BACKEND.deleteProgram(programId);
GLStateManager.glDeleteProgram(programId);
}
/**
@@ -1,15 +1,15 @@
package com.gtnewhorizons.angelica.glsm.ffp;
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFlags;
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormat;
import com.gtnewhorizons.angelica.glsm.CompatUniformManager;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.hooks.DeferredBlendHandler;
import com.gtnewhorizons.angelica.glsm.hooks.GLSMHooks;
import com.gtnewhorizons.angelica.glsm.hooks.GLSMInitConfig;
import com.gtnewhorizons.angelica.glsm.QuadConverter;
import com.gtnewhorizons.angelica.glsm.stacks.Vec3fStack;
import com.gtnewhorizons.angelica.glsm.stacks.Vec4fStack;
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
import com.gtnewhorizons.angelica.glsm.hooks.GLSMInitConfig;
import com.gtnewhorizons.angelica.glsm.streaming.TessellatorStreamingDrawer;
import lombok.Getter;
import org.joml.Vector3f;
import org.joml.Vector4f;
@@ -22,7 +22,7 @@ import static com.gtnewhorizons.angelica.glsm.backend.BackendManager.RENDER_BACK
/**
* FFP shader manager
*/
public class ShaderManager {
public final class ShaderManager {
private static final class Holder {
@@ -31,27 +31,33 @@ public class ShaderManager {
private final ShaderCache cache = new ShaderCache();
private final Uniforms uniforms = new Uniforms();
private final Int2IntOpenHashMap vaoVertexFlags = new Int2IntOpenHashMap();
@Getter
private boolean active = false;
private Program currentProgram = null;
private int lastBoundProgramId = -1;
private long currentVertexKeyPacked = Long.MIN_VALUE;
private final long[] currentFKScratch = new long[FragmentKey.MAX_UNITS];
private final long[] currentFKPacked = new long[FragmentKey.MAX_UNITS];
private int currentFKLen = 0;
@Getter private static final Vector3f currentNormal = new Vector3f(0.0f, 0.0f, 1.0f);
@Getter private static final Vector4f currentTexCoord = new Vector4f(0.0f, 0.0f, 0.0f, 1.0f);
private static final Vector4f[] currentTexCoords = {
new Vector4f(0.0f, 0.0f, 0.0f, 1.0f),
new Vector4f(0.0f, 0.0f, 0.0f, 1.0f),
new Vector4f(0.0f, 0.0f, 0.0f, 1.0f),
new Vector4f(0.0f, 0.0f, 0.0f, 1.0f),
};
@Getter private static final Vec3fStack normalStack = new Vec3fStack(currentNormal);
@Getter private static final Vec4fStack texCoordStack = new Vec4fStack(currentTexCoord);
@Getter private static final Vec4fStack texCoordStack = new Vec4fStack(currentTexCoords[0]);
@Getter private static int normalGeneration;
@Getter private static int texCoordGeneration;
public static Vector4f getCurrentTexCoord() { return currentTexCoords[0]; }
public static Vector4f getCurrentTexCoord(int unit) { return currentTexCoords[unit]; }
@Getter private boolean enabled = false;
private int currentVertexFlags = VertexFlags.TEXTURE_BIT | VertexFlags.COLOR_BIT | VertexFlags.NORMAL_BIT;
private ShaderManager() {
vaoVertexFlags.defaultReturnValue(-1);
if (Boolean.parseBoolean(System.getProperty("angelica.dumpShaders", "false"))) {
cache.setDumpDir(Paths.get("ffp_shaders"));
}
@@ -62,54 +68,75 @@ public class ShaderManager {
}
public void enable() {
warmUp();
enabled = true;
VertexFormat.registerSetupBufferStateOverride((format, offset) -> {
currentVertexFlags = format.getVertexFlags();
vaoVertexFlags.put(GLStateManager.getBoundVAO(), currentVertexFlags);
return false;
});
GLStateManager.LOGGER.info("FFP shader emulation enabled");
}
// Force loading of classes before the SplashThread kicks off
// Ensure it's GL free
private static void warmUp() {
try {
final long[] fkScratch = new long[FragmentKey.MAX_UNITS];
final int fkLen = FragmentKey.packFromState(fkScratch);
final int fragMask = FragmentKey.unitMaskFromPacked(fkScratch, fkLen);
final long vkPacked = VertexKey.packFromState(true, true, true, true, fragMask);
final VertexKey vk = VertexKey.fromPacked(vkPacked);
VertexShaderGenerator.generate(vk);
FragmentShaderGenerator.generate(FragmentKey.fromPacked(fkScratch, fkLen));
GeometryShaderGenerator.generate(vk);
final Class<?>[] touched = { Program.class, ShaderCache.class, TessellatorStreamingDrawer.class, QuadConverter.class };
for (Class<?> c : touched) {
c.getName();
}
} catch (Throwable t) {
GLStateManager.LOGGER.warn("FFP warmup failed; draw-path classes will resolve lazily", t);
}
}
public void disable() {
enabled = false;
}
public void activate() {
active = true;
updateVariant(true, true, true, true);
uploadUniforms();
lastBoundProgramId = GLStateManager.getActiveProgram();
}
public void deactivate() {
active = false;
currentProgram = null;
lastBoundProgramId = -1;
currentVertexKeyPacked = Long.MIN_VALUE;
currentFKLen = 0;
}
public void preDraw(boolean hasColor, boolean hasNormal, boolean hasTexCoord, boolean hasLightmap) {
GLStateManager.flushDeferredVertexAttribs();
public void preDraw() {
final DeferredBlendHandler bh = GLSMHooks.blendHandler;
if (bh != null) bh.flushDeferredBlend();
if (!active) {
if (enabled) {
final int currentProgramId = GLStateManager.getActiveProgram();
if (currentProgramId != 0) {
CompatUniformManager.onUseProgram(currentProgramId);
return;
}
active = true;
} else {
return;
// Handle FFP & Iris uniforms
final int currentProgramId = GLStateManager.getActiveProgram();
if (currentProgramId != 0) {
if (!CompatUniformManager.refreshCompatUniforms(currentProgramId)) {
return; // Don't emulate FFP on non-iris core shaders
}
}
final long vkPacked = VertexKey.packFromState(hasColor, hasNormal, hasTexCoord, hasLightmap);
final int vertexFlags = VAOManager.getCurrentVertexFlags();
final boolean hasColor = (vertexFlags & VertexFlags.COLOR_BIT) != 0;
final boolean hasNormal = (vertexFlags & VertexFlags.NORMAL_BIT) != 0;
final boolean hasTexCoord = (vertexFlags & VertexFlags.TEXTURE_BIT) != 0;
final boolean hasLightmap = (vertexFlags & VertexFlags.BRIGHTNESS_BIT) != 0;
GLStateManager.flushDeferredVertexAttribs(hasColor, hasNormal, hasTexCoord, hasLightmap);
if (!active) return;
final int fkLen = FragmentKey.packFromState(currentFKScratch);
final int fragMask = FragmentKey.unitMaskFromPacked(currentFKScratch, fkLen);
final long vkPacked = VertexKey.packFromState(hasColor, hasNormal, hasTexCoord, hasLightmap, fragMask);
if (vkPacked != currentVertexKeyPacked || !Arrays.equals(currentFKScratch, 0, fkLen, currentFKPacked, 0, currentFKLen)) {
commitVariant(vkPacked, fkLen);
@@ -118,32 +145,16 @@ public class ShaderManager {
uploadUniforms();
}
public void preDraw(int vertexFlags) {
currentVertexFlags = vertexFlags;
preDraw(
(vertexFlags & VertexFlags.COLOR_BIT) != 0,
(vertexFlags & VertexFlags.NORMAL_BIT) != 0,
(vertexFlags & VertexFlags.TEXTURE_BIT) != 0,
(vertexFlags & VertexFlags.BRIGHTNESS_BIT) != 0
);
}
public void preDraw() {
preDraw(currentVertexFlags);
}
private void updateVariant(boolean hasColor, boolean hasNormal, boolean hasTexCoord, boolean hasLightmap) {
final long vkPacked = VertexKey.packFromState(hasColor, hasNormal, hasTexCoord, hasLightmap);
final int fkLen = FragmentKey.packFromState(currentFKScratch);
commitVariant(vkPacked, fkLen);
}
private void commitVariant(long vkPacked, int fkLen) {
currentVertexKeyPacked = vkPacked;
System.arraycopy(currentFKScratch, 0, currentFKPacked, 0, fkLen);
currentFKLen = fkLen;
currentProgram = cache.getOrCreate(vkPacked, currentFKPacked, currentFKLen);
RENDER_BACKEND.useProgram(currentProgram.getProgramId());
final int programId = currentProgram.getProgramId();
if (programId != lastBoundProgramId) {
RENDER_BACKEND.useProgram(programId);
lastBoundProgramId = programId;
}
}
private void uploadUniforms() {
@@ -158,29 +169,18 @@ public class ShaderManager {
}
public static void setCurrentTexCoord(float s, float t, float r, float q) {
currentTexCoord.set(s, t, r, q);
currentTexCoords[0].set(s, t, r, q);
texCoordGeneration++;
}
public static void setCurrentTexCoord(int unit, float s, float t, float r, float q) {
currentTexCoords[unit].set(s, t, r, q);
texCoordGeneration++;
}
public static void bumpNormalGeneration() { normalGeneration++; }
public static void bumpTexCoordGeneration() { texCoordGeneration++; }
public void enableClientVertexFlag(int flag) {currentVertexFlags |= flag;}
public void disableClientVertexFlag(int flag) {currentVertexFlags &= ~flag;}
public int getCurrentVertexFlags() {return currentVertexFlags;}
public void setCurrentVertexFlags(int flags) {currentVertexFlags = flags;}
public void onBindVertexArray(int vaoId) {
final int flags = vaoVertexFlags.get(vaoId);
if (flags != -1) {
currentVertexFlags = flags;
}
}
public void onDeleteVertexArray(int vaoId) {
vaoVertexFlags.remove(vaoId);
}
public void destroy() {
cache.destroy();
uniforms.destroy();
@@ -4,9 +4,12 @@ import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.states.ClipPlaneState;
import com.gtnewhorizons.angelica.glsm.states.FogState;
import com.gtnewhorizons.angelica.glsm.states.LightState;
import com.gtnewhorizons.angelica.glsm.states.LineState;
import com.gtnewhorizons.angelica.glsm.states.MaterialState;
import com.gtnewhorizons.angelica.glsm.states.TexGenState;
import com.gtnewhorizons.angelica.glsm.states.ViewportState;
import com.gtnewhorizons.angelica.glsm.hooks.GLSMConfig;
import org.joml.Math;
import org.joml.Matrix3f;
import org.joml.Matrix4f;
import org.joml.Vector4f;
@@ -42,119 +45,127 @@ public class Uniforms {
private final org.joml.Vector3f tempVec3 = new org.joml.Vector3f();
// Pre-computed constants for fog params (Mesa STATE_FOG_PARAMS_OPTIMIZED)
private static final double LN2 = Math.log(2.0);
private static final double SQRT_LN2 = Math.sqrt(LN2);
// Dirty tracking: last-uploaded generation per category + program ID.
// Program change forces full re-upload since uniform locations differ.
private int lastProgramId = -1;
private int lastMvGen = -1;
private int lastProjGen = -1;
private int lastTexMatGen = -1;
private int lastLightingGen = -1;
private int lastFragmentGen = -1;
private int lastColorGen = -1;
private int lastNormalGen = -1;
private int lastTexCoordGen = -1;
private float lastLightmapX = Float.NaN;
private float lastLightmapY = Float.NaN;
private int lastTexGenGen = -1;
private int lastClipPlaneGen = -1;
private float lastLineWidth = Float.NaN;
private int lastViewportWidth = -1;
private int lastViewportHeight = -1;
private static final double LN2 = java.lang.Math.log(2.0);
private static final double SQRT_LN2 = java.lang.Math.sqrt(LN2);
/**
* Upload all relevant uniforms to the given FFP program based on current GLSM state.
* Uses generation counters to skip categories that haven't changed since last upload.
*/
public void upload(Program program) {
final boolean programChanged = program.getProgramId() != lastProgramId;
lastProgramId = program.getProgramId();
final Program.UploadState st = program.uploadState;
final int mvGen = GLStateManager.mvGeneration;
final int mvLinearGen = GLStateManager.mvLinearGeneration;
final int projGen = GLStateManager.projGeneration;
final int texMatGen = GLStateManager.texMatrixGeneration;
final boolean mvChanged = programChanged || mvGen != lastMvGen;
final boolean projChanged = programChanged || projGen != lastProjGen;
final boolean texMatChanged = programChanged || texMatGen != lastTexMatGen;
final boolean mvChanged = mvGen != st.mvGen;
final boolean mvLinearChanged = mvLinearGen != st.mvLinearGen;
final boolean projChanged = projGen != st.projGen;
final boolean texMatChanged = texMatGen != st.texMatGen;
if (mvChanged || projChanged || texMatChanged) {
uploadMatrices(program, mvChanged, projChanged, texMatChanged);
lastMvGen = mvGen;
lastProjGen = projGen;
lastTexMatGen = texMatGen;
uploadMatrices(program, mvChanged, mvLinearChanged, projChanged, texMatChanged);
st.mvGen = mvGen;
st.mvLinearGen = mvLinearGen;
st.projGen = projGen;
st.texMatGen = texMatGen;
}
if (program.getVertexKey().lightingEnabled()) {
final int litGen = GLStateManager.lightingGeneration;
if (programChanged || litGen != lastLightingGen) {
if (litGen != st.lightingGen) {
uploadLighting(program);
lastLightingGen = litGen;
st.lightingGen = litGen;
}
}
// Current color/normal/texcoord — skip if generation unchanged
if (!program.getVertexKey().hasVertexColor()) {
final int colorGen = GLStateManager.colorGeneration;
if (programChanged || colorGen != lastColorGen) {
if (colorGen != st.colorGen) {
uploadCurrentColor(program);
lastColorGen = colorGen;
st.colorGen = colorGen;
}
}
if (!program.getVertexKey().hasVertexNormal() && program.getVertexKey().lightingEnabled()) {
final int normalGen = ShaderManager.getNormalGeneration();
if (programChanged || normalGen != lastNormalGen) {
if (normalGen != st.normalGen) {
uploadCurrentNormal(program);
lastNormalGen = normalGen;
st.normalGen = normalGen;
}
}
if (!program.getVertexKey().hasVertexTexCoord() && program.getVertexKey().textureEnabled()) {
if (program.getVertexKey().anyUnitTexCoordEnabled()) {
final int texGen = ShaderManager.getTexCoordGeneration();
if (programChanged || texGen != lastTexCoordGen) {
uploadCurrentTexCoord(program);
lastTexCoordGen = texGen;
if (texGen != st.texCoordGen) {
uploadCurrentTexCoords(program);
st.texCoordGen = texGen;
}
}
if (program.getVertexKey().lightmapEnabled() && !program.getVertexKey().hasVertexLightmap()) {
if (programChanged || GLSMConfig.lastBrightnessX != lastLightmapX || GLSMConfig.lastBrightnessY != lastLightmapY) {
if (GLSMConfig.lastBrightnessX != st.lightmapX || GLSMConfig.lastBrightnessY != st.lightmapY) {
uploadCurrentLightmapCoord(program);
lastLightmapX = GLSMConfig.lastBrightnessX;
lastLightmapY = GLSMConfig.lastBrightnessY;
st.lightmapX = GLSMConfig.lastBrightnessX;
st.lightmapY = GLSMConfig.lastBrightnessY;
}
}
if (program.getVertexKey().texGenEnabled()) {
final int tgGen = GLStateManager.texGenGeneration;
if (programChanged || tgGen != lastTexGenGen) {
if (tgGen != st.texGenGen) {
uploadTexGen(program);
lastTexGenGen = tgGen;
st.texGenGen = tgGen;
}
}
if (program.getVertexKey().clipPlanesEnabled()) {
final int cpGen = GLStateManager.clipPlaneGeneration;
if (programChanged || cpGen != lastClipPlaneGen) {
if (cpGen != st.clipPlaneGen) {
uploadClipPlanes(program);
lastClipPlaneGen = cpGen;
st.clipPlaneGen = cpGen;
}
}
final int fragGen = GLStateManager.fragmentGeneration;
if (programChanged || fragGen != lastFragmentGen) {
if (fragGen != st.fragmentGen) {
uploadFragmentUniforms(program);
lastFragmentGen = fragGen;
st.fragmentGen = fragGen;
}
// Wide line emulation uniforms
if (program.locLineWidth != -1 && program.locViewportSize != -1) {
uploadWideLineUniforms(program, programChanged);
uploadWideLineUniforms(program);
}
if (program.locLineStipple != -1) {
uploadLineStippleUniforms(program);
}
}
private void uploadMatrices(Program program, boolean mvChanged, boolean projChanged, boolean texMatChanged) {
private void uploadLineStippleUniforms(Program program) {
final Program.UploadState st = program.uploadState;
final LineState line = GLStateManager.getLineState();
final int factor = line.getStippleFactor() < 1 ? 1 : line.getStippleFactor();
final int packed = (line.getStipplePattern() & 0xFFFF) | (factor << 16);
if (packed != st.lineStipple) {
RENDER_BACKEND.uniform1i(program.locLineStipple, packed);
st.lineStipple = packed;
}
final ViewportState vp = GLStateManager.getViewportState();
if (vp.x != st.stippleViewportX || vp.y != st.stippleViewportY || vp.width != st.stippleViewportW || vp.height != st.stippleViewportH) {
RENDER_BACKEND.uniform4f(program.locViewport, vp.x, vp.y, vp.width, vp.height);
st.stippleViewportX = vp.x;
st.stippleViewportY = vp.y;
st.stippleViewportW = vp.width;
st.stippleViewportH = vp.height;
}
}
private void uploadMatrices(Program program, boolean mvChanged, boolean mvLinearChanged, boolean projChanged, boolean texMatChanged) {
final Matrix4f mv = GLStateManager.getModelViewMatrix();
final Matrix4f proj = GLStateManager.getProjectionMatrix();
@@ -164,7 +175,9 @@ public class Uniforms {
mv.get(mat4Buf);
RENDER_BACKEND.uniformMatrix4(program.locModelViewMatrix, false, mat4Buf);
}
}
if (mvLinearChanged) {
// Normal matrix = inverse transpose of upper-left 3x3 of ModelView
if (program.locNormalMatrix != -1 || program.locNormalScale != -1) {
mv.normal(normalMatrix);
@@ -197,16 +210,15 @@ public class Uniforms {
RENDER_BACKEND.uniformMatrix4(program.locMVPMatrix, false, mat4Buf);
}
// Texture matrices
// Texture matrices (per-unit).
if (texMatChanged) {
// Texture matrix unit 0
if (program.locTextureMatrix0 != -1) {
final Matrix4f texMat = GLStateManager.getTextures().getTextureUnitMatrix(0);
texMat.get(mat4Buf);
RENDER_BACKEND.uniformMatrix4(program.locTextureMatrix0, false, mat4Buf);
for (int i = 0; i < 4; i++) {
if (program.locTextureMatrix[i] != -1) {
final Matrix4f texMat = GLStateManager.getTextures().getTextureUnitMatrix(i);
texMat.get(mat4Buf);
RENDER_BACKEND.uniformMatrix4(program.locTextureMatrix[i], false, mat4Buf);
}
}
// Texture matrix unit 1 (lightmap)
if (program.locLightmapTextureMatrix != -1) {
final Matrix4f lmTexMat = GLStateManager.getTextures().getTextureUnitMatrix(1);
lmTexMat.get(mat4Buf);
@@ -314,19 +326,22 @@ public class Uniforms {
if (program.locCurrentColor == -1) return;
// Upload the current color from GLSM
final var color = GLStateManager.getColor();
tempVec4.set(
Math.clamp(0f, 1f, color.getRed()),
Math.clamp(0f, 1f, color.getGreen()),
Math.clamp(0f, 1f, color.getBlue()),
Math.clamp(0f, 1f, color.getAlpha()));
vec4Buf.clear();
vec4Buf.put(color.getRed());
vec4Buf.put(color.getGreen());
vec4Buf.put(color.getBlue());
vec4Buf.put(color.getAlpha());
vec4Buf.flip();
tempVec4.get(vec4Buf);
RENDER_BACKEND.uniform4(program.locCurrentColor, vec4Buf);
}
private void uploadCurrentTexCoord(Program program) {
if (program.locCurrentTexCoord == -1) return;
final var tc = ShaderManager.getCurrentTexCoord();
uploadVec4(program.locCurrentTexCoord, tc);
private void uploadCurrentTexCoords(Program program) {
for (int i = 0; i < 4; i++) {
if (program.locCurrentTexCoord[i] != -1) {
uploadVec4(program.locCurrentTexCoord[i], ShaderManager.getCurrentTexCoord(i));
}
}
}
private void uploadCurrentLightmapCoord(Program program) {
@@ -340,10 +355,7 @@ public class Uniforms {
// Upload current normal from FFPShaderManager's tracked normal
final var normal = ShaderManager.getCurrentNormal();
vec3Buf.clear();
vec3Buf.put(normal.x);
vec3Buf.put(normal.y);
vec3Buf.put(normal.z);
vec3Buf.flip();
normal.get(vec3Buf);
RENDER_BACKEND.uniform3(program.locCurrentNormal, vec3Buf);
}
@@ -379,10 +391,7 @@ public class Uniforms {
private void uploadPlane(int loc, float[] plane) {
if (loc == -1) return;
vec4Buf.clear();
vec4Buf.put(plane[0]);
vec4Buf.put(plane[1]);
vec4Buf.put(plane[2]);
vec4Buf.put(plane[3]);
vec4Buf.put(plane, 0, 4);
vec4Buf.flip();
RENDER_BACKEND.uniform4(loc, vec4Buf);
}
@@ -420,6 +429,16 @@ public class Uniforms {
}
}
if (fk.overlayEnabled() && program.locOverlayColor != -1) {
vec4Buf.clear();
vec4Buf.put(GLStateManager.getOverlayR());
vec4Buf.put(GLStateManager.getOverlayG());
vec4Buf.put(GLStateManager.getOverlayB());
vec4Buf.put(GLStateManager.getOverlayA());
vec4Buf.flip();
RENDER_BACKEND.uniform4(program.locOverlayColor, vec4Buf);
}
// Fog
if (fk.fogMode() != FragmentKey.FOG_NONE) {
uploadFog(program);
@@ -458,29 +477,26 @@ public class Uniforms {
}
}
private void uploadWideLineUniforms(Program program, boolean programChanged) {
private void uploadWideLineUniforms(Program program) {
final Program.UploadState st = program.uploadState;
final float lineWidth = GLStateManager.getLineState().getWidth();
if (programChanged || lineWidth != lastLineWidth) {
if (lineWidth != st.lineWidth) {
RENDER_BACKEND.uniform1f(program.locLineWidth, lineWidth);
lastLineWidth = lineWidth;
st.lineWidth = lineWidth;
}
final int vw = GLStateManager.getViewportState().width;
final int vh = GLStateManager.getViewportState().height;
if (programChanged || vw != lastViewportWidth || vh != lastViewportHeight) {
if (vw != st.viewportWidth || vh != st.viewportHeight) {
RENDER_BACKEND.uniform2f(program.locViewportSize, vw, vh);
lastViewportWidth = vw;
lastViewportHeight = vh;
st.viewportWidth = vw;
st.viewportHeight = vh;
}
}
private void uploadVec4(int loc, Vector4f v) {
if (loc == -1) return;
vec4Buf.clear();
vec4Buf.put(v.x);
vec4Buf.put(v.y);
vec4Buf.put(v.z);
vec4Buf.put(v.w);
vec4Buf.flip();
v.get(vec4Buf);
RENDER_BACKEND.uniform4(loc, vec4Buf);
}
@@ -0,0 +1,321 @@
package com.gtnewhorizons.angelica.glsm.ffp;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
import javax.annotation.Nullable;
import java.nio.ByteBuffer;
import static com.gtnewhorizons.angelica.glsm.backend.BackendManager.RENDER_BACKEND;
public final class VAOManager {
public static final int MAX_ATTRIBS = 16;
public static final Int2ObjectOpenHashMap<VAOData> vaoMap = new Int2ObjectOpenHashMap<>();
private static VAOData current = null;
private static int currentVertexFlags = 0;
private static Attrib[] currentAttribs = null;
public static int boundEBO = 0;
private static int clientSideEnabledCount = 0;
public static void init(int defaultVAO) {
final VAOData data = new VAOData();
current = data;
currentAttribs = data.attribs;
vaoMap.put(defaultVAO, data);
}
public static void onBindEBO(int ebo) {
boundEBO = ebo;
}
public static void onBindVertexArrayPre(int vaoId) {
// Save old VAO data
if (current != null) {
current.vertexFlags = currentVertexFlags;
current.ebo = boundEBO;
current.attribs = currentAttribs;
current.clientSideEnabledCount = clientSideEnabledCount;
}
VAOData data = vaoMap.get(vaoId);
if (data == null) {
data = new VAOData();
vaoMap.put(vaoId, data);
}
current = data;
currentVertexFlags = data.vertexFlags;
currentAttribs = data.attribs;
boundEBO = data.ebo;
clientSideEnabledCount = data.clientSideEnabledCount;
}
public static void onDeleteVertexArray(int vaoId) {
vaoMap.remove(vaoId);
}
public static void enableClientVertexFlag(int flag) { currentVertexFlags |= flag; }
public static void disableClientVertexFlag(int flag) { currentVertexFlags &= ~flag; }
public static int getCurrentVertexFlags() { return currentVertexFlags; }
public static void setCurrentVertexFlags(int flags) { currentVertexFlags = flags; }
public static final class VAOData {
public Attrib[] attribs;
public int vertexFlags;
public int ebo;
public int clientSideEnabledCount;
public VAOData() {
attribs = new Attrib[MAX_ATTRIBS];
}
}
public static void setAttribute(int index, int size, int type, boolean normalized, int stride, long offset, int vboId) {
if (index < 0 || index >= MAX_ATTRIBS) return;
final Attrib a = getAttrib(index);
final boolean wasClient = a.clientPointer != null;
a.size = size;
a.type = type;
a.normalized = normalized;
a.stride = stride;
a.offset = offset;
a.vboId = vboId;
a.clientPointer = null;
a.genericPointer = true;
if (wasClient && a.enabled) {
clientSideEnabledCount--;
}
}
public static void setAttribute(int index, int size, int type, boolean normalized, int stride, ByteBuffer pointer) {
if (index < 0 || index >= MAX_ATTRIBS) return;
final Attrib a = getAttrib(index);
final boolean wasClient = a.clientPointer != null;
a.size = size;
a.type = type;
a.normalized = normalized;
a.stride = stride;
a.offset = 0;
a.vboId = 0;
a.clientPointer = pointer;
a.genericPointer = true;
if (!wasClient && a.enabled) {
clientSideEnabledCount++;
}
}
public static boolean isGenericPointerEnabled(int index) {
if (index < 0 || index >= MAX_ATTRIBS) return false;
final Attrib a = currentAttribs[index];
return a != null && a.genericPointer && a.enabled;
}
public static void markConventional(int index) {
if (index < 0 || index >= MAX_ATTRIBS) return;
final Attrib a = currentAttribs[index];
if (a != null) a.genericPointer = false;
}
public static void setEnabled(int index, boolean enabled) {
if (index < 0 || index >= MAX_ATTRIBS) return;
final Attrib a = getAttrib(index);
if (a.clientPointer != null && a.enabled != enabled) {
clientSideEnabledCount += enabled ? 1 : -1;
}
a.enabled = enabled;
}
public static void enableAttribute(int index) {
if (index < 0 || index >= MAX_ATTRIBS) return;
final Attrib a = getAttrib(index);
if (a.clientPointer != null && !a.enabled) {
clientSideEnabledCount++;
}
a.enabled = true;
}
public static void disableAttribute(int index) {
if (index < 0 || index >= MAX_ATTRIBS) return;
final Attrib a = getAttrib(index);
if (a.clientPointer != null && a.enabled) {
clientSideEnabledCount--;
}
a.enabled = false;
}
// Lazily allocate each attrib. Most VAOs don't even have 4 attribs, let alone 16.
private static Attrib getAttrib(int index) {
Attrib a = currentAttribs[index];
if (a == null) {
a = new Attrib();
currentAttribs[index] = a;
}
return a;
}
public static boolean isClientSideAttrib(int index) {
final Attrib attrib = currentAttribs[index];
return attrib != null && attrib.isClientSide();
}
public static int getAttribEffectiveStride(int index) {
final Attrib attrib = currentAttribs[index];
return attrib != null ? attrib.effectiveStride() : 0;
}
public static long getAttribOffset(int index) {
final Attrib attrib = currentAttribs[index];
return attrib != null ? attrib.offset : 0;
}
public static int getAttribVBO(int index) {
final Attrib attrib = currentAttribs[index];
return attrib != null ? attrib.vboId : 0;
}
public static @Nullable Attrib get(int index) {
return currentAttribs[index];
}
/**
* Returns true if any currently enabled vertex attribute was registered without a VBO
* (i.e. as a client-side pointer). In core profile, such attribs are treated as null
* offsets into a non-existent buffer, causing a native crash at draw time.
*/
public static boolean hasAnyClientSideEnabledAttrib() {
return clientSideEnabledCount > 0;
}
/**
* If any enabled vertex attribute uses a client-side pointer (no VBO), upload all such
* attribs into a shared stream VBO so the draw succeeds under core profile.
*/
private static int clientArraysVBO = 0;
private static int clientArraysVBOCapacity = 0;
private static final int[] clientArraysVBOOffsets = new int[VAOManager.MAX_ATTRIBS];
public static void uploadClientArraysToVBO() {
int totalBytes = 0;
for (int i = 0; i < VAOManager.MAX_ATTRIBS; i++) {
clientArraysVBOOffsets[i] = -1;
final VAOManager.Attrib a = VAOManager.get(i);
if (a == null || !a.enabled || a.clientPointer == null) continue;
clientArraysVBOOffsets[i] = totalBytes;
totalBytes += a.clientPointer.remaining();
}
if (totalBytes == 0) return;
if (clientArraysVBO == 0) {
clientArraysVBO = RENDER_BACKEND.genBuffers();
}
final int savedVBO = GLStateManager.getBoundVBO();
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, clientArraysVBO);
if (totalBytes > clientArraysVBOCapacity) {
int newCap = Math.max(4096, clientArraysVBOCapacity);
while (newCap < totalBytes) newCap *= 2;
clientArraysVBOCapacity = newCap;
}
RENDER_BACKEND.bufferData(GL15.GL_ARRAY_BUFFER, clientArraysVBOCapacity, GL15.GL_STREAM_DRAW);
for (int i = 0; i < VAOManager.MAX_ATTRIBS; i++) {
if (clientArraysVBOOffsets[i] < 0) continue;
final VAOManager.Attrib a = VAOManager.get(i);
RENDER_BACKEND.bufferSubData(GL15.GL_ARRAY_BUFFER, clientArraysVBOOffsets[i], a.clientPointer.duplicate());
RENDER_BACKEND.vertexAttribPointer(i, a.size, a.type, a.normalized, a.stride, (long) clientArraysVBOOffsets[i]);
}
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, savedVBO);
}
public static final class Attrib {
public boolean enabled;
public int size;
public int type;
public boolean normalized;
public int stride;
public long offset;
public int vboId;
public ByteBuffer clientPointer;
public boolean genericPointer;
public boolean isClientSide() {
return enabled && clientPointer != null;
}
public void reset() {
enabled = false;
size = 0;
type = 0;
normalized = false;
stride = 0;
offset = 0;
vboId = 0;
clientPointer = null;
genericPointer = false;
}
public int effectiveStride() {
return (stride != 0) ? stride : size * typeSizeBytes();
}
public int typeSizeBytes() {
return glTypeSizeBytes(type);
}
public static int glTypeSizeBytes(int glType) {
return switch (glType) {
case GL11.GL_FLOAT, GL11.GL_INT, GL11.GL_UNSIGNED_INT -> 4;
case GL11.GL_DOUBLE -> 8;
case GL11.GL_SHORT, GL11.GL_UNSIGNED_SHORT -> 2;
case GL11.GL_BYTE, GL11.GL_UNSIGNED_BYTE -> 1;
default -> 4;
};
}
public float readComponent(ByteBuffer buf, int base, int component) {
return readComponent(type, normalized, buf, base, component);
}
public static float readComponent(int type, boolean normalized, ByteBuffer buf, int base, int component) {
return switch (type) {
case GL11.GL_FLOAT -> buf.getFloat(base + component * 4);
case GL11.GL_DOUBLE -> (float) buf.getDouble(base + component * 8);
case GL11.GL_INT -> {
final int v = buf.getInt(base + component * 4);
yield normalized ? v / (float) Integer.MAX_VALUE : (float) v;
}
case GL11.GL_UNSIGNED_INT -> {
final int v = buf.getInt(base + component * 4);
yield normalized ? (v & 0xFFFFFFFFL) / (float) 0xFFFFFFFFL : (float) (v & 0xFFFFFFFFL);
}
case GL11.GL_SHORT -> {
final short v = buf.getShort(base + component * 2);
yield normalized ? v / (float) Short.MAX_VALUE : (float) v;
}
case GL11.GL_UNSIGNED_SHORT -> {
final int v = buf.getShort(base + component * 2) & 0xFFFF;
yield normalized ? v / (float) 0xFFFF : (float) v;
}
case GL11.GL_BYTE -> {
final byte v = buf.get(base + component);
yield normalized ? v / 127.0f : (float) v;
}
case GL11.GL_UNSIGNED_BYTE -> {
final int v = buf.get(base + component) & 0xFF;
yield normalized ? v / 255.0f : (float) v;
}
default -> 0.0f;
};
}
}
}
@@ -1,8 +1,10 @@
package com.gtnewhorizons.angelica.glsm.ffp;
import com.gtnewhorizon.gtnhlib.client.renderer.MatrixHelper;
import com.gtnewhorizons.angelica.glsm.DisplayListManager;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.states.TexGenState;
import com.gtnewhorizons.angelica.glsm.states.TextureUnitArray;
import org.joml.Matrix4f;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL12;
@@ -10,9 +12,14 @@ import org.lwjgl.opengl.GL12;
/**
* Compact key capturing the vertex shader permutation axes for FFP emulation.
* Packed into a single long for fast hash/compare.
*
* Per-unit fields are 4-bit base-indexed: bit (BASE + i) for unit i in [0..3].
* Unit 1 is the lightmap; texcoord enable for unit 1 also gates the lightmap-specific code paths.
*/
public final class VertexKey {
public static final int MAX_UNITS = 4;
private final long packed;
private static final int BIT_LIGHTING = 0;
@@ -23,26 +30,27 @@ public final class VertexKey {
private static final int BIT_COLOR_MATERIAL = 5;
private static final int BIT_SEPARATE_SPECULAR = 6;
private static final int BIT_FOG = 7;
private static final int BIT_FOG_DIST_MODE = 8;
private static final int BIT_FOG_DIST_MODE = 8; // 2 bits: 8-9
private static final int BIT_NORMALIZE = 10;
private static final int BIT_RESCALE_NORMAL = 11;
private static final int BIT_TEXTURE = 12;
private static final int BIT_LIGHTMAP = 13;
private static final int BIT_TEX_MATRIX = 14;
private static final int BIT_HAS_VERTEX_COLOR = 15;
private static final int BIT_HAS_VERTEX_NORMAL = 16;
private static final int BIT_HAS_VERTEX_TEX = 17;
private static final int BIT_HAS_VERTEX_LIGHTMAP = 18;
private static final int BIT_COLOR_MAT_MODE = 19;
// bits 19-21: colorMaterialMode (3 bits)
// TexGen modes: 3 bits each (0=NONE, 1=OBJ_LINEAR, 2=EYE_LINEAR)
private static final int BIT_TEXGEN_S = 22;
private static final int BIT_TEXGEN_T = 25;
private static final int BIT_TEXGEN_R = 28;
private static final int BIT_TEXGEN_Q = 31;
// bits 31-33: texGenModeQ (3 bits)
private static final int BIT_CLIP_PLANES = 34;
private static final int BIT_WIDE_LINE = 35;
// Per-unit texcoord enable: bits 12-15 (unit 0..3). Unit 1 doubles as the lightmap flag.
private static final int BIT_UNIT_TEX_BASE = 12;
private static final int BIT_HAS_VERTEX_COLOR = 16;
private static final int BIT_HAS_VERTEX_NORMAL = 17;
private static final int BIT_HAS_VERTEX_TEX = 18;
private static final int BIT_HAS_VERTEX_LIGHTMAP = 19;
private static final int BIT_COLOR_MAT_MODE = 20; // 3 bits: 20-22
// TexGen modes (unit 0 only, for now): 3 bits each (0=NONE, 1=OBJ_LINEAR, 2=EYE_LINEAR).
private static final int BIT_TEXGEN_S = 23; // 3 bits: 23-25
private static final int BIT_TEXGEN_T = 26; // 3 bits: 26-28
private static final int BIT_TEXGEN_R = 29; // 3 bits: 29-31
private static final int BIT_TEXGEN_Q = 32; // 3 bits: 32-34
private static final int BIT_CLIP_PLANES = 35;
private static final int BIT_WIDE_LINE = 36;
// Per-unit texmat enable: bits 37-40 (unit 0..3). Unit 1 is the lightmap matrix.
private static final int BIT_UNIT_TEXMAT_BASE = 37;
private static final int BIT_UNIT23_UV_FROM_UNIT0 = 41;
private static final int BIT_LINE_STIPPLE = 42;
public static final int TG_NONE = 0;
public static final int TG_OBJ_LINEAR = 1;
@@ -71,38 +79,31 @@ public final class VertexKey {
public int fogDistanceMode() { return (int)((packed >> BIT_FOG_DIST_MODE) & 0x3); }
public boolean normalizeEnabled() { return bit(BIT_NORMALIZE); }
public boolean rescaleNormalsEnabled() { return bit(BIT_RESCALE_NORMAL); }
public boolean textureEnabled() { return bit(BIT_TEXTURE); }
public boolean lightmapEnabled() { return bit(BIT_LIGHTMAP); }
public boolean textureMatrixEnabled() { return bit(BIT_TEX_MATRIX); }
public boolean unitTexCoordEnabled(int unit) { return bit(BIT_UNIT_TEX_BASE + unit); }
public boolean unitTexMatEnabled(int unit) { return bit(BIT_UNIT_TEXMAT_BASE + unit); }
public boolean anyUnitTexCoordEnabled() { return ((packed >> BIT_UNIT_TEX_BASE) & 0xF) != 0; }
public boolean lightmapEnabled() { return unitTexCoordEnabled(1); }
public boolean hasVertexColor() { return bit(BIT_HAS_VERTEX_COLOR); }
public boolean hasVertexNormal() { return bit(BIT_HAS_VERTEX_NORMAL); }
public boolean hasVertexTexCoord() { return bit(BIT_HAS_VERTEX_TEX); }
public boolean hasVertexLightmap() { return bit(BIT_HAS_VERTEX_LIGHTMAP); }
public boolean unit23UvFromUnit0() { return bit(BIT_UNIT23_UV_FROM_UNIT0); }
/** Color material mode (CM_AMBIENT, CM_DIFFUSE, CM_SPECULAR, CM_EMISSION, CM_AMBIENT_AND_DIFFUSE). Only meaningful when colorMaterialEnabled(). */
public int colorMaterialMode() { return (int)((packed >> BIT_COLOR_MAT_MODE) & 0x7); }
/** TexGen mode for S coordinate (TG_NONE, TG_OBJ_LINEAR, TG_EYE_LINEAR). */
/** TexGen mode for S coordinate (TG_NONE, TG_OBJ_LINEAR, TG_EYE_LINEAR). Unit 0 only; see TODO in packFromState. */
public int texGenModeS() { return (int)((packed >> BIT_TEXGEN_S) & 0x7); }
/** TexGen mode for T coordinate. */
public int texGenModeT() { return (int)((packed >> BIT_TEXGEN_T) & 0x7); }
/** TexGen mode for R coordinate. */
public int texGenModeR() { return (int)((packed >> BIT_TEXGEN_R) & 0x7); }
/** TexGen mode for Q coordinate. */
public int texGenModeQ() { return (int)((packed >> BIT_TEXGEN_Q) & 0x7); }
/** Whether any texgen coordinate is enabled. */
public boolean texGenEnabled() { return texGenModeS() != TG_NONE || texGenModeT() != TG_NONE || texGenModeR() != TG_NONE || texGenModeQ() != TG_NONE; }
/** Whether any clip plane is enabled. */
public boolean clipPlanesEnabled() { return bit(BIT_CLIP_PLANES); }
/** Whether wide line GS emulation is active for this variant. */
public boolean wideLineEmulation() { return bit(BIT_WIDE_LINE); }
public boolean lineStipple() { return bit(BIT_LINE_STIPPLE); }
/** Whether vertex color replaces material ambient in this variant. */
public boolean cmReplacesAmbient() { final int m = colorMaterialMode(); return m == CM_AMBIENT || m == CM_AMBIENT_AND_DIFFUSE; }
/** Whether vertex color replaces material diffuse in this variant. */
public boolean cmReplacesDiffuse() { final int m = colorMaterialMode(); return m == CM_DIFFUSE || m == CM_AMBIENT_AND_DIFFUSE; }
/** Whether vertex color replaces material specular in this variant. */
public boolean cmReplacesSpecular() { return colorMaterialMode() == CM_SPECULAR; }
/** Whether vertex color replaces material emission in this variant. */
public boolean cmReplacesEmission() { return colorMaterialMode() == CM_EMISSION; }
private boolean bit(int pos) { return ((packed >> pos) & 1) != 0; }
@@ -126,7 +127,7 @@ public final class VertexKey {
};
}
public static long packFromState(boolean hasColor, boolean hasNormal, boolean hasTexCoord, boolean hasLightmap) {
public static long packFromState(boolean hasColor, boolean hasNormal, boolean hasTexCoord, boolean hasLightmap, int fragUnitMask) {
long bits = 0;
final boolean lighting = GLStateManager.getLightingState().isEnabled();
@@ -171,23 +172,18 @@ public final class VertexKey {
bits |= (1L << BIT_RESCALE_NORMAL);
}
// Texture unit 0 (base texture)
if (GLStateManager.getTextures().getTextureUnitStates(0).isEnabled()) {
bits |= (1L << BIT_TEXTURE);
}
// Texture unit 1 (lightmap)
if (GLStateManager.getTextures().getTextureUnitStates(1).isEnabled()) {
bits |= (1L << BIT_LIGHTMAP);
final TextureUnitArray texUnit = GLStateManager.getTextures();
for (int i = 0; i < MAX_UNITS; i++) {
if ((fragUnitMask & (1 << i)) != 0) {
bits |= (1L << (BIT_UNIT_TEX_BASE + i));
}
if (!MatrixHelper.isIdentity(texUnit.getTextureUnitMatrix(i))) {
bits |= (1L << (BIT_UNIT_TEXMAT_BASE + i));
}
}
// Check if texture matrix unit 0 is non-identity
final Matrix4f texMat = GLStateManager.getTextures().getTextureUnitMatrix(0);
if (!DisplayListManager.isIdentity(texMat)) {
bits |= (1L << BIT_TEX_MATRIX);
}
// TexGen (unit 0 only) — encode per-coordinate mode if enabled
final var texUnit = GLStateManager.getTextures();
// TexGen (unit 0 only) - per-coordinate mode if enabled.
// TODO: per-unit texgen for units 2/3. Deferred - no MC code path uses glTexGen on those.
final TexGenState tg = texUnit.getTexGenState(0);
boolean anyTexGen = false;
if (texUnit.getTexGenSStates(0).isEnabled() && tg.getMode(GL11.GL_S) != 0) {
@@ -210,9 +206,9 @@ public final class VertexKey {
bits |= ((long) m & 0x7) << BIT_TEXGEN_Q;
if (m != TG_NONE) anyTexGen = true;
}
// Force texture matrix on when texgen is active (end portal pattern always uses it)
// Force unit-0 texture matrix on when texgen is active (end portal pattern always uses it).
if (anyTexGen) {
bits |= (1L << BIT_TEX_MATRIX);
bits |= (1L << BIT_UNIT_TEXMAT_BASE);
}
// Vertex format flags
@@ -221,21 +217,27 @@ public final class VertexKey {
if (hasTexCoord) bits |= (1L << BIT_HAS_VERTEX_TEX);
if (hasLightmap) bits |= (1L << BIT_HAS_VERTEX_LIGHTMAP);
// Clip planes
if (GLStateManager.consumeUnit23TexCoordSetDuringDraw() && hasTexCoord) {
bits |= (1L << BIT_UNIT23_UV_FROM_UNIT0);
}
if (GLStateManager.anyClipPlaneEnabled()) {
bits |= (1L << BIT_CLIP_PLANES);
}
// Wide line GS emulation
if (GLStateManager.wideLineEmulationActive) {
bits |= (1L << BIT_WIDE_LINE);
}
if (GLStateManager.lineStippleActive) {
bits |= (1L << BIT_LINE_STIPPLE);
}
return bits;
}
public static VertexKey fromState(boolean hasColor, boolean hasNormal, boolean hasTexCoord, boolean hasLightmap) {
return new VertexKey(packFromState(hasColor, hasNormal, hasTexCoord, hasLightmap));
public static VertexKey fromState(boolean hasColor, boolean hasNormal, boolean hasTexCoord, boolean hasLightmap, int fragUnitMask) {
return new VertexKey(packFromState(hasColor, hasNormal, hasTexCoord, hasLightmap, fragUnitMask));
}
static VertexKey fromPacked(long packed) {
@@ -256,9 +258,11 @@ public final class VertexKey {
@Override
public String toString() {
return String.format("FFPVertexKey[0x%09X: lit=%b l0=%b l1=%b cm=%b fog=%b tex=%b lm=%b col=%b nrm=%b vtex=%b vlm=%b tg=%d/%d/%d/%d clip=%b wline=%b]",
return String.format("FFPVertexKey[0x%011X: lit=%b l0=%b l1=%b cm=%b fog=%b tex=%d%d%d%d texmat=%d%d%d%d col=%b nrm=%b vtex=%b vlm=%b tg=%d/%d/%d/%d clip=%b wline=%b]",
packed, lightingEnabled(), light0Enabled(), light1Enabled(),
colorMaterialEnabled(), fogEnabled(), textureEnabled(), lightmapEnabled(),
colorMaterialEnabled(), fogEnabled(),
unitTexCoordEnabled(0)?1:0, unitTexCoordEnabled(1)?1:0, unitTexCoordEnabled(2)?1:0, unitTexCoordEnabled(3)?1:0,
unitTexMatEnabled(0)?1:0, unitTexMatEnabled(1)?1:0, unitTexMatEnabled(2)?1:0, unitTexMatEnabled(3)?1:0,
hasVertexColor(), hasVertexNormal(), hasVertexTexCoord(), hasVertexLightmap(),
texGenModeS(), texGenModeT(), texGenModeR(), texGenModeQ(), clipPlanesEnabled(), wideLineEmulation());
}
@@ -29,6 +29,9 @@ public final class VertexShaderGenerator {
emitColorPassthrough(sb, key);
}
emitTexCoordPassthrough(sb, key);
if (key.lineStipple()) {
emitLineStart(sb);
}
if (key.fogEnabled()) {
emitFogDistance(sb, key);
}
@@ -42,7 +45,10 @@ public final class VertexShaderGenerator {
source = source.replace("v_SpecularColor", "v_SpecularColor_gs")
.replace("v_TexCoord0", "v_TexCoord0_gs")
.replace("v_TexCoord1", "v_TexCoord1_gs")
.replace("v_TexCoord2", "v_TexCoord2_gs")
.replace("v_TexCoord3", "v_TexCoord3_gs")
.replace("v_FogCoord", "v_FogCoord_gs")
.replace("v_LineStart", "v_LineStart_gs")
.replace("v_Color", "v_Color_gs");
}
return source;
@@ -66,6 +72,11 @@ public final class VertexShaderGenerator {
sb.append('\n');
}
private static void emitLineStart(StringBuilder sb) {
sb.append(" vec2 ndc = gl_Position.xy / gl_Position.w;\n");
sb.append(" v_LineStart = u_Viewport.xy + (ndc * 0.5 + 0.5) * u_Viewport.zw;\n\n");
}
private static void emitUniforms(StringBuilder sb, VertexKey key) {
sb.append("// Matrices\n");
sb.append("uniform mat4 u_ModelViewMatrix;\n");
@@ -75,12 +86,18 @@ public final class VertexShaderGenerator {
if (key.lightingEnabled()) {
sb.append("uniform mat3 u_NormalMatrix;\n");
}
if (key.textureMatrixEnabled()) {
sb.append("uniform mat4 u_TextureMatrix0;\n");
if (key.lineStipple()) {
sb.append("uniform vec4 u_Viewport;\n");
}
// TexGen plane uniforms
for (int i = 0; i < VertexKey.MAX_UNITS; i++) {
if (i == 1) continue;
if (key.unitTexMatEnabled(i)) {
sb.append("uniform mat4 u_TextureMatrix").append(i).append(";\n");
}
}
// TexGen plane uniforms (unit 0 only - TODO: per-unit)
if (key.texGenModeS() == VertexKey.TG_OBJ_LINEAR) sb.append("uniform vec4 u_TexGenObjPlaneS;\n");
if (key.texGenModeS() == VertexKey.TG_EYE_LINEAR) sb.append("uniform vec4 u_TexGenEyePlaneS;\n");
if (key.texGenModeT() == VertexKey.TG_OBJ_LINEAR) sb.append("uniform vec4 u_TexGenObjPlaneT;\n");
@@ -98,9 +115,11 @@ public final class VertexShaderGenerator {
sb.append("uniform vec4 u_CurrentColor;\n");
}
if (!key.hasVertexTexCoord() && key.textureEnabled()) {
sb.append("uniform vec4 u_CurrentTexCoord;\n");
if (key.unitTexCoordEnabled(0) && !key.hasVertexTexCoord()) {
sb.append("uniform vec4 u_CurrentTexCoord0;\n");
}
if (key.unitTexCoordEnabled(2) && !key.unit23UvFromUnit0()) sb.append("uniform vec4 u_CurrentTexCoord2;\n");
if (key.unitTexCoordEnabled(3) && !key.unit23UvFromUnit0()) sb.append("uniform vec4 u_CurrentTexCoord3;\n");
if (key.lightmapEnabled() && !key.hasVertexLightmap()) {
sb.append("uniform vec2 u_CurrentLightmapCoord;\n");
@@ -175,15 +194,21 @@ public final class VertexShaderGenerator {
private static void emitOutputs(StringBuilder sb, VertexKey key) {
sb.append("// Outputs\n");
sb.append("out vec4 v_Color;\n");
if (key.lineStipple()) {
sb.append("flat out vec2 v_LineStart;\n");
}
if (key.separateSpecular()) {
sb.append("out vec3 v_SpecularColor;\n");
}
if (key.textureEnabled() || key.hasVertexTexCoord() || key.texGenEnabled()) {
// Per-unit varying outputs. Each enabled texture unit (and texgen, which targets unit 0) gets its own v_TexCoord<i>.
if (key.unitTexCoordEnabled(0) || key.texGenEnabled()) {
sb.append("out vec4 v_TexCoord0;\n");
}
if (key.lightmapEnabled()) {
sb.append("out vec4 v_TexCoord1;\n");
}
if (key.unitTexCoordEnabled(2)) sb.append("out vec4 v_TexCoord2;\n");
if (key.unitTexCoordEnabled(3)) sb.append("out vec4 v_TexCoord3;\n");
if (key.fogEnabled()) {
sb.append("out float v_FogCoord;\n");
}
@@ -331,22 +356,18 @@ public final class VertexShaderGenerator {
private static void emitTexCoordPassthrough(StringBuilder sb, VertexKey key) {
if (key.texGenEnabled()) {
emitTexGenCoordGeneration(sb, key);
} else if (key.textureEnabled() || key.hasVertexTexCoord()) {
sb.append(" // Texture coordinates\n");
if (key.hasVertexTexCoord()) {
if (key.textureMatrixEnabled()) {
sb.append(" v_TexCoord0 = u_TextureMatrix0 * vec4(a_TexCoord0, 0.0, 1.0);\n");
} else {
sb.append(" v_TexCoord0 = vec4(a_TexCoord0, 0.0, 1.0);\n");
}
} else if (key.unitTexCoordEnabled(0)) {
sb.append(" // Texture coordinates - unit 0\n");
final String src = key.hasVertexTexCoord()
? "vec4(a_TexCoord0, 0.0, 1.0)"
: "u_CurrentTexCoord0";
if (key.unitTexMatEnabled(0)) {
sb.append(" v_TexCoord0 = u_TextureMatrix0 * ").append(src).append(";\n");
} else {
if (key.textureMatrixEnabled()) {
sb.append(" v_TexCoord0 = u_TextureMatrix0 * u_CurrentTexCoord;\n");
} else {
sb.append(" v_TexCoord0 = u_CurrentTexCoord;\n");
}
sb.append(" v_TexCoord0 = ").append(src).append(";\n");
}
}
if (key.lightmapEnabled()) {
if (key.hasVertexLightmap()) {
sb.append(" v_TexCoord1 = u_LightmapTextureMatrix * vec4(a_TexCoord1, 0.0, 1.0);\n");
@@ -354,6 +375,18 @@ public final class VertexShaderGenerator {
sb.append(" v_TexCoord1 = u_LightmapTextureMatrix * vec4(u_CurrentLightmapCoord, 0.0, 1.0);\n");
}
}
for (int i = 2; i < VertexKey.MAX_UNITS; i++) {
if (!key.unitTexCoordEnabled(i)) continue;
final String src = key.unit23UvFromUnit0()
? "vec4(a_TexCoord0, 0.0, 1.0)"
: "u_CurrentTexCoord" + i;
if (!key.unit23UvFromUnit0() && key.unitTexMatEnabled(i)) {
sb.append(" v_TexCoord").append(i).append(" = u_TextureMatrix").append(i).append(" * ").append(src).append(";\n");
} else {
sb.append(" v_TexCoord").append(i).append(" = ").append(src).append(";\n");
}
}
}
/**
@@ -367,7 +400,7 @@ public final class VertexShaderGenerator {
emitTexGenComponent(sb, key.texGenModeS(), "s", "S", key);
emitTexGenComponent(sb, key.texGenModeT(), "t", "T", key);
emitTexGenComponent(sb, key.texGenModeR(), "r", "R", key);
emitTexGenComponent(sb, key.texGenModeR(), "p", "R", key);
emitTexGenComponent(sb, key.texGenModeQ(), "q", "Q", key);
// Always apply texture matrix when texgen is active (forced on in VertexKey)
@@ -1,12 +1,6 @@
package com.gtnewhorizons.angelica.glsm.hooks;
import com.gtnewhorizons.angelica.glsm.hooks.events.BlendFuncChangeEvent;
import com.gtnewhorizons.angelica.glsm.hooks.events.FogStateChangeEvent;
import com.gtnewhorizons.angelica.glsm.hooks.events.LightmapCoordsEvent;
import com.gtnewhorizons.angelica.glsm.hooks.events.ProgramChangeEvent;
import com.gtnewhorizons.angelica.glsm.hooks.events.TextureBindEvent;
import com.gtnewhorizons.angelica.glsm.hooks.events.TextureDeleteEvent;
import com.gtnewhorizons.angelica.glsm.hooks.events.TextureUnitStateEvent;
import com.gtnewhorizons.angelica.glsm.hooks.events.*;
import net.minecraftforge.eventbus.api.bus.EventBus;
public final class GLSMHooks {
@@ -14,6 +8,7 @@ public final class GLSMHooks {
public static DeferredBlendHandler blendHandler;
public static DeferredAlphaHandler alphaHandler;
public static DeferredDepthColorHandler depthColorHandler;
public static GlintColorHandler glintColorHandler;
// EventBus instances
public static final EventBus<TextureBindEvent> TEXTURE_BIND = EventBus.create(TextureBindEvent.class);
@@ -23,6 +18,8 @@ public final class GLSMHooks {
public static final EventBus<BlendFuncChangeEvent> BLEND_FUNC_CHANGE = EventBus.create(BlendFuncChangeEvent.class);
public static final EventBus<FogStateChangeEvent> FOG_STATE_CHANGE = EventBus.create(FogStateChangeEvent.class);
public static final EventBus<LightmapCoordsEvent> LIGHTMAP_COORDS = EventBus.create(LightmapCoordsEvent.class);
public static final EventBus<AlphaStateChangeEvent> ALPHA_STATE_CHANGE = EventBus.create(AlphaStateChangeEvent.class);
public static final EventBus<ShaderColorChangeEvent> SHADER_COLOR_CHANGE = EventBus.create(ShaderColorChangeEvent.class);
// Reusable event instances
public static final TextureBindEvent textureBindEvent = new TextureBindEvent();
@@ -32,6 +29,8 @@ public final class GLSMHooks {
public static final BlendFuncChangeEvent blendFuncChangeEvent = new BlendFuncChangeEvent();
public static final FogStateChangeEvent fogStateChangeEvent = new FogStateChangeEvent();
public static final LightmapCoordsEvent lightmapCoordsEvent = new LightmapCoordsEvent();
public static final AlphaStateChangeEvent alphaStateChangeEvent = new AlphaStateChangeEvent();
public static final ShaderColorChangeEvent shaderColorChangeEvent = new ShaderColorChangeEvent();
private GLSMHooks() {}
}
@@ -8,24 +8,24 @@ import java.util.function.Consumer;
public final class GLSMInitConfig {
private final boolean lwjglDebug;
private final StreamingUploader.UploadStrategy streamingUploadStrategy;
private final boolean framebufferSupported;
private final boolean fboEnabled;
private final Consumer<DirectTessellator> directDrawer;
private final Runnable streamingDrawerDestroy;
private final int displayWidth;
private final int displayHeight;
private final Runnable postInitCallback;
private final boolean enableDSA;
private final boolean noErrorChecks;
private GLSMInitConfig(Builder builder) {
this.lwjglDebug = builder.lwjglDebug;
this.streamingUploadStrategy = builder.streamingUploadStrategy;
this.framebufferSupported = builder.framebufferSupported;
this.fboEnabled = builder.fboEnabled;
this.noErrorChecks = builder.noErrorChecks;
this.directDrawer = builder.directDrawer;
this.streamingDrawerDestroy = builder.streamingDrawerDestroy;
this.displayWidth = builder.displayWidth;
this.displayHeight = builder.displayHeight;
this.postInitCallback = builder.postInitCallback;
this.enableDSA = builder.enableDSA;
}
public static Builder builder() {
@@ -34,24 +34,24 @@ public final class GLSMInitConfig {
public boolean isLwjglDebug() { return lwjglDebug; }
public StreamingUploader.UploadStrategy getStreamingUploadStrategy() { return streamingUploadStrategy; }
public boolean isFramebufferSupported() { return framebufferSupported; }
public boolean isFboEnabled() { return fboEnabled; }
public Consumer<DirectTessellator> getDirectDrawer() { return directDrawer; }
public Runnable getStreamingDrawerDestroy() { return streamingDrawerDestroy; }
public int getDisplayWidth() { return displayWidth; }
public int getDisplayHeight() { return displayHeight; }
public Runnable getPostInitCallback() { return postInitCallback; }
public boolean isDSAEnabled() { return enableDSA; }
public boolean noErrorChecks() { return noErrorChecks; }
public static final class Builder {
private boolean lwjglDebug = false;
private StreamingUploader.UploadStrategy streamingUploadStrategy = StreamingUploader.UploadStrategy.BUFFER_DATA;
private boolean framebufferSupported = false;
private boolean fboEnabled = false;
private Consumer<DirectTessellator> directDrawer = null;
private Runnable streamingDrawerDestroy = null;
private int displayWidth = 0;
private int displayHeight = 0;
private Runnable postInitCallback = null;
private boolean enableDSA = false;
private boolean noErrorChecks;
private Builder() {}
@@ -65,13 +65,8 @@ public final class GLSMInitConfig {
return this;
}
public Builder framebufferSupported(boolean framebufferSupported) {
this.framebufferSupported = framebufferSupported;
return this;
}
public Builder fboEnabled(boolean fboEnabled) {
this.fboEnabled = fboEnabled;
public Builder noErrorChecks(boolean noErrorChecks) {
this.noErrorChecks = noErrorChecks;
return this;
}
@@ -96,6 +91,11 @@ public final class GLSMInitConfig {
return this;
}
public Builder enableDSA(boolean enableDSA) {
this.enableDSA = enableDSA;
return this;
}
public GLSMInitConfig build() {
return new GLSMInitConfig(this);
}
@@ -0,0 +1,5 @@
package com.gtnewhorizons.angelica.glsm.hooks;
public interface GlintColorHandler {
void onColorChanged(float red, float green, float blue, float alpha);
}
@@ -0,0 +1,9 @@
package com.gtnewhorizons.angelica.glsm.hooks.events;
import net.minecraftforge.eventbus.api.event.MutableEvent;
/**
* AlphaTest changed - Listeners should query GLSM alpha state directly if they care what changed.
*/
public final class AlphaStateChangeEvent extends MutableEvent {
}
@@ -5,4 +5,5 @@ import net.minecraftforge.eventbus.api.event.MutableEvent;
public final class ProgramChangeEvent extends MutableEvent {
public int previousProgram;
public int newProgram;
public boolean postBind;
}
@@ -0,0 +1,10 @@
package com.gtnewhorizons.angelica.glsm.hooks.events;
import net.minecraftforge.eventbus.api.event.MutableEvent;
public final class ShaderColorChangeEvent extends MutableEvent {
public float red;
public float green;
public float blue;
public float alpha;
}
@@ -0,0 +1,32 @@
package com.gtnewhorizons.angelica.glsm.loading;
import java.util.List;
/**
* Verifies required classes/resources are present on the classpath at load time.
* Fails fast with a descriptive error message if a dependency is missing.
*/
public final class DependencyVerifier {
/**
* A single dependency check: a classpath resource that must exist.
* @param resourcePath Resource path to check (e.g. "/com/example/SomeClass.class")
* @param errorMessage Human-readable error message if missing (include download URL)
*/
public record Check(String resourcePath, String errorMessage) {}
/**
* Verify all checks pass. Throws RuntimeException on first failure.
* @param anchor Class whose classloader to use for resource lookups
* @param checks Dependency checks to run
*/
public static void verify(Class<?> anchor, List<Check> checks) {
for (Check check : checks) {
if (anchor.getResource(check.resourcePath()) == null) {
throw new RuntimeException(check.errorMessage());
}
}
}
private DependencyVerifier() {}
}
@@ -0,0 +1,54 @@
package com.gtnewhorizons.angelica.glsm.loading;
import com.gtnewhorizons.angelica.glsm.redirect.GLSMRedirector;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.tree.ClassNode;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public final class EarlyRedirectorCore {
private final GLSMRedirector core;
private final String[] targets;
private final String[] exclusions;
public EarlyRedirectorCore(String... extraExclusions) {
this.core = new GLSMRedirector();
this.targets = EcosystemNarrowRules.EARLY_REDIRECTOR_TARGETS.clone();
final List<String> excl = new ArrayList<>(Arrays.asList(core.getCoreExclusions()));
if (extraExclusions != null && extraExclusions.length > 0) {
Collections.addAll(excl, extraExclusions);
}
this.exclusions = excl.toArray(new String[0]);
}
public byte[] transform(String transformedName, byte[] basicClass) {
if (basicClass == null) return null;
if (!startsWithAny(transformedName, targets)) return basicClass;
if (startsWithAny(transformedName, exclusions)) return basicClass;
if (!core.shouldTransform(basicClass)) return basicClass;
final ClassReader cr = new ClassReader(basicClass);
final ClassNode cn = new ClassNode();
cr.accept(cn, 0);
if (core.transformClassNode(transformedName, cn)) {
final ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
cn.accept(cw);
return cw.toByteArray();
}
return basicClass;
}
private static boolean startsWithAny(String name, String[] prefixes) {
for (String p : prefixes) {
if (name.startsWith(p)) return true;
}
return false;
}
}
@@ -0,0 +1,97 @@
package com.gtnewhorizons.angelica.glsm.loading;
import com.gtnewhorizons.angelica.glsm.loading.TransformerNarrower.NarrowRule;
import java.util.List;
public final class EcosystemNarrowRules {
public static final List<NarrowRule> ALL = List.of(
new NarrowRule("DragonAPI", "Reika.DragonAPI.ASM", List.of(
"Reika.DragonAPI.ASM.Patchers",
"Reika.DragonAPI.ASM.Profiling",
"Reika.DragonAPI.ASM.APIStripper",
"Reika.DragonAPI.ASM.ClassReparenter",
"Reika.DragonAPI.ASM.DependentMethodStripper",
"Reika.DragonAPI.ASM.DragonAPIClassTransformer",
"Reika.DragonAPI.ASM.FMLItemBlockPatch",
"Reika.DragonAPI.ASM.FluidNamePatch",
"Reika.DragonAPI.ASM.InterfaceInjector",
"Reika.DragonAPI.ASM.SpecialBiomePlacement",
"Reika.DragonAPI.ASM.StructureLootHooks"
)),
new NarrowRule("Xaeros_common", "xaero.common.core", List.of(
"xaero.common.core.transformer"
)),
new NarrowRule("Xaeros_map", "xaero.map.core", List.of(
"xaero.map.core.transformer"
)),
new NarrowRule("AdvancedLightsabers", "com.fiskmods.lightsabers.asm", List.of(
"com.fiskmods.lightsabers.asm.ALLoadingPlugin",
"com.fiskmods.lightsabers.asm.ASMHooks",
"com.fiskmods.lightsabers.asm.transformers"
)),
new NarrowRule("FiskHeroes", "com.fiskmods.heroes.asm", List.of(
"com.fiskmods.heroes.asm.SHLoadingPlugin",
"com.fiskmods.heroes.asm.transformers"
)),
new NarrowRule("Ears", "com.unascribed.ears", List.of(
"com.unascribed.ears.asm",
"com.unascribed.ears.common.agent"
)),
new NarrowRule("FoamFix", "pl.asie.foamfix", List.of(
"pl.asie.foamfix.bugfixmod",
"pl.asie.foamfix.coremod",
"pl.asie.foamfix.forkage",
"pl.asie.foamfix.ghostbuster",
"pl.asie.foamfix.FoamFixMod",
"pl.asie.foamfix.HackyMessageFactoryWrapper",
"pl.asie.foamfix.LaunchWrapperRuntimeFix",
"pl.asie.foamfix.ProxyClient",
"pl.asie.foamfix.ProxyCommon",
"pl.asie.foamfix.repack.com.unascribed.ears.asm",
"pl.asie.foamfix.repack.com.unascribed.ears.common.agent"
)),
new NarrowRule("Alfheim", "alfheim.common.core.asm.hook", List.of(
"alfheim.common.core.asm.hook.AlfheimFieldHookHandler",
"alfheim.common.core.asm.hook.AlfheimHPHooks",
"alfheim.common.core.asm.hook.Botania18AndUpBackport",
"alfheim.common.core.asm.hook.ElementalDamageAdapter",
"alfheim.common.core.asm.hook.integration",
"alfheim.common.core.asm.hook.extender.FurnaceExtender",
"alfheim.common.core.asm.hook.extender.ItemAuraRingExtender",
"alfheim.common.core.asm.hook.extender.ItemLensExtender",
"alfheim.common.core.asm.hook.extender.ItemTwigWandExtender",
"alfheim.common.core.asm.hook.extender.LightRelayExtender",
"alfheim.common.core.asm.hook.extender.PureDaisyExtender",
"alfheim.common.core.asm.hook.extender.QuartzExtender",
"alfheim.common.core.asm.hook.extender.RelicHooks",
"alfheim.common.core.asm.hook.extender.SparkExtender",
"alfheim.common.core.asm.hook.fixes.BotaniaGlowingRenderFixes",
"alfheim.common.core.asm.hook.fixes.CorporeaInputFix",
"alfheim.common.core.asm.hook.fixes.GodAttributesHooks",
"alfheim.common.core.asm.hook.fixes.RecipeAncientWillsFix"
))
);
public static final String[] LWJGL3IFY_EXCLUSIONS_SHARED = {
"org.embeddedt.embeddium",
"org.taumc.celeritas",
"com.mitchej123.lwjgl.lwjgl3",
"com.mitchej123.glsm",
"com.gtnewhorizons.angelica.lwjgl3",
};
public static final String[] EARLY_REDIRECTOR_TARGETS = {
"cn.tesseract.mycelium.",
};
private EcosystemNarrowRules() {}
}
@@ -0,0 +1,66 @@
package com.gtnewhorizons.angelica.glsm.loading;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Narrows overly-broad transformer exclusions that prevent the GLSM redirector from
* transforming GL calls in mod code. Some coremods register exclusions covering their
* entire package tree, which inadvertently shields rendering code from redirection.
* <p>
* This utility removes the broad exclusion and re-adds narrower ones that cover only
* the coremod's own transformer/ASM classes, leaving rendering code exposed.
* <p>
* Operates on a {@code Set<String>} (the transformer exceptions set from LaunchClassLoader)
* and a {@code Map<String, Object>} (the Launch blackboard). Callers extract these from
* the Forge/launchwrapper infrastructure.
*/
public final class TransformerNarrower {
private static final Logger LOGGER = LogManager.getLogger("GLSM");
/**
* A rule that replaces a broad transformer exclusion with narrower ones.
* @param name Human-readable name for logging (e.g. "DragonAPI")
* @param broad The broad exclusion to remove (e.g. "Reika.DragonAPI.ASM")
* @param narrow Narrower exclusions to add in its place
*/
public record NarrowRule(String name, String broad, List<String> narrow) {}
/**
* Apply narrowing rules to the transformer exceptions set.
*
* @param exceptions The mutable Set from LaunchClassLoader.transformerExceptions
* @param blackboard The Launch.blackboard map (checked for disable flags)
* @param keyPrefix Blackboard key prefix for per-rule opt-out (e.g. "angelica" or "umbra").
* Rule "DragonAPI" checks key "{prefix}.narrow.DragonAPI". If explicitly
* set to Boolean.FALSE, the rule is skipped.
* @param callerName Name for log messages (e.g. "Angelica" or "Umbra")
* @param rules The narrowing rules to apply
* @return number of rules that were applied
*/
public static int narrow(Set<String> exceptions, Map<String, Object> blackboard,
String keyPrefix, String callerName, List<NarrowRule> rules) {
int applied = 0;
for (NarrowRule rule : rules) {
final String key = keyPrefix + ".narrow." + rule.name();
if (Boolean.FALSE.equals(blackboard.get(key))) {
continue;
}
if (exceptions.remove(rule.broad())) {
for (String narrow : rule.narrow()) {
exceptions.add(narrow);
}
LOGGER.info("[{}] Narrowed {} transformer exclusion to allow GL redirection", callerName, rule.broad());
applied++;
}
}
return applied;
}
private TransformerNarrower() {}
}
@@ -16,17 +16,13 @@ public final class AccumulatedDraw {
public final VertexFormat format;
public final List<ByteBuffer> drawBuffers;
public final int drawMode;
public final int commandIndex; // Position in command list for state tracking
public final int stateGeneration; // Position in command list for state tracking
public RestoreData restoreData;
public AccumulatedDraw(DirectTessellator tessellator, int commandIndex, int stateGeneration, boolean copyLast) {
public AccumulatedDraw(DirectTessellator tessellator, boolean copyLast) {
this.format = tessellator.getVertexFormat();
this.drawMode = tessellator.drawMode;
this.drawBuffers = new ArrayList<>();
this.drawBuffers.add(tessellator.allocateBufferCopy());
this.commandIndex = commandIndex;
this.stateGeneration = stateGeneration;
if (copyLast) {
this.restoreData = new RestoreData(tessellator);
@@ -36,7 +32,6 @@ public final class AccumulatedDraw {
public void mergeDraw(DirectTessellator tessellator, boolean copyLast) {
this.drawBuffers.add(tessellator.allocateBufferCopy());
if (copyLast) {
this.restoreData = new RestoreData(tessellator);
}
@@ -0,0 +1,178 @@
package com.gtnewhorizons.angelica.glsm.recording;
import com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.ffp.VAOManager;
import it.unimi.dsi.fastutil.ints.Int2LongOpenHashMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import org.lwjgl.opengl.GL15;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
public final class AttribSnapshot {
public record AttribDesc(int location, int size, int type, boolean normalized, int stride, long offset, int sourceVboId, ByteBuffer readBuffer, long readBufferBaseOffset) {
public int effectiveStride() {
return stride != 0 ? stride : size * VAOManager.Attrib.glTypeSizeBytes(type);
}
public int typeSizeBytes() {
return VAOManager.Attrib.glTypeSizeBytes(type);
}
}
private final AttribDesc[] perLocation;
private final List<ByteBuffer> allocatedBuffers;
private boolean freed;
private AttribSnapshot(AttribDesc[] perLocation, List<ByteBuffer> allocatedBuffers) {
this.perLocation = perLocation;
this.allocatedBuffers = allocatedBuffers;
}
public AttribDesc get(int location) {
return (location >= 0 && location < perLocation.length) ? perLocation[location] : null;
}
public int maxLocations() {
return perLocation.length;
}
public void free() {
if (freed) return;
freed = true;
for (ByteBuffer buf : allocatedBuffers) {
MemoryUtilities.memFree(buf);
}
allocatedBuffers.clear();
}
/**
* Snapshot attribs over a tight vertex window {@code [firstVertex, firstVertex+vertexCount)}.
*
* <p>Hot path (single VBO backing every enabled attrib — the typical draw) runs without
* allocating any hashmaps. A second distinct VBO promotes to the map-backed fallback.
*/
public static AttribSnapshot snapshot(int firstVertex, int vertexCount) {
final AttribDesc[] out = new AttribDesc[VAOManager.MAX_ATTRIBS];
final List<ByteBuffer> allocated = new ArrayList<>();
final int prevVBO = GLStateManager.getBoundVBO();
// Pass 1 — unioned [start,end] per vboId. Hot path: one source VBO, zero allocations.
int rangeSoleVboId = 0;
long rangeSoleStart = Long.MAX_VALUE;
long rangeSoleEnd = Long.MIN_VALUE;
Int2ObjectOpenHashMap<long[]> vboRanges = null;
final long lastVtx = (long) firstVertex + vertexCount - 1;
for (int i = 0; i < VAOManager.MAX_ATTRIBS; i++) {
final VAOManager.Attrib a = VAOManager.get(i);
if (a == null || !a.enabled || a.vboId == 0) continue;
final int stride = a.effectiveStride();
final long typeBytes = (long) a.size * VAOManager.Attrib.glTypeSizeBytes(a.type);
final long start = a.offset + (long) firstVertex * stride;
final long end = a.offset + lastVtx * stride + typeBytes;
if (vboRanges != null) {
long[] r = vboRanges.get(a.vboId);
if (r == null) {
vboRanges.put(a.vboId, new long[] { start, end });
} else {
if (start < r[0]) r[0] = start;
if (end > r[1]) r[1] = end;
}
} else if (rangeSoleVboId == 0 || rangeSoleVboId == a.vboId) {
rangeSoleVboId = a.vboId;
if (start < rangeSoleStart) rangeSoleStart = start;
if (end > rangeSoleEnd) rangeSoleEnd = end;
} else {
// Spill: second distinct vboId. Promote the accumulated range to the map.
vboRanges = new Int2ObjectOpenHashMap<>();
vboRanges.put(rangeSoleVboId, new long[] { rangeSoleStart, rangeSoleEnd });
vboRanges.put(a.vboId, new long[] { start, end });
}
}
// Pass 2 — readback cache. Same spill pattern.
int readSoleVboId = 0;
ByteBuffer readSoleBuf = null;
long readSoleOffset = 0;
Int2ObjectOpenHashMap<ByteBuffer> vboBuffers = null;
Int2LongOpenHashMap vboReadOffsets = null;
try {
for (int i = 0; i < VAOManager.MAX_ATTRIBS; i++) {
final VAOManager.Attrib a = VAOManager.get(i);
if (a == null || !a.enabled) continue;
if (a.vboId != 0) {
ByteBuffer buf;
long readOffset;
if (vboBuffers != null) {
buf = vboBuffers.get(a.vboId);
readOffset = buf != null ? vboReadOffsets.get(a.vboId) : 0;
} else if (readSoleVboId == a.vboId) {
buf = readSoleBuf;
readOffset = readSoleOffset;
} else {
buf = null;
readOffset = 0;
}
if (buf == null) {
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, a.vboId);
final int bufferSize = GLStateManager.glGetBufferParameteri(GL15.GL_ARRAY_BUFFER, GL15.GL_BUFFER_SIZE);
if (bufferSize <= 0) {
GLStateManager.LOGGER.warn("[VBO Readback] GL_BUFFER_SIZE={} for VBO {}", bufferSize, a.vboId);
continue;
}
final long rStart;
final long rEnd;
if (vboRanges != null) {
final long[] range = vboRanges.get(a.vboId);
rStart = range[0];
rEnd = range[1];
} else {
rStart = rangeSoleStart;
rEnd = rangeSoleEnd;
}
final long ro = Math.max(0, rStart);
final int rs = (int) (Math.min(bufferSize, rEnd) - ro);
if (rs <= 0) continue;
buf = MemoryUtilities.memAlloc(rs);
GLStateManager.glGetBufferSubData(GL15.GL_ARRAY_BUFFER, ro, buf);
allocated.add(buf);
readOffset = ro;
if (vboBuffers != null) {
vboBuffers.put(a.vboId, buf);
vboReadOffsets.put(a.vboId, ro);
} else if (readSoleVboId == 0) {
readSoleVboId = a.vboId;
readSoleBuf = buf;
readSoleOffset = ro;
} else {
vboBuffers = new Int2ObjectOpenHashMap<>();
vboReadOffsets = new Int2LongOpenHashMap();
vboBuffers.put(readSoleVboId, readSoleBuf);
vboReadOffsets.put(readSoleVboId, readSoleOffset);
vboBuffers.put(a.vboId, buf);
vboReadOffsets.put(a.vboId, ro);
}
}
out[i] = new AttribDesc(i, a.size, a.type, a.normalized, a.stride, a.offset, a.vboId, buf, readOffset);
} else if (a.clientPointer != null) {
out[i] = new AttribDesc(i, a.size, a.type, a.normalized, a.stride, a.clientPointer.position(), 0, a.clientPointer, 0L);
}
}
} finally {
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, prevVBO);
}
return new AttribSnapshot(out, allocated);
}
}
@@ -1,784 +0,0 @@
package com.gtnewhorizons.angelica.glsm.recording;
import com.gtnewhorizons.angelica.glsm.recording.commands.DisplayListCommand;
import org.joml.Matrix4f;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.util.ArrayList;
import java.util.List;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.*;
public final class CommandBuffer {
private static final int DEFAULT_CAPACITY = 4096; // Guess at typical display list size
private ByteBuffer buffer;
private long basePointer;
private long writePointer;
private final List<Object> complexObjects = new ArrayList<>();
public CommandBuffer() {
this(DEFAULT_CAPACITY);
}
public CommandBuffer(int initialCapacity) {
this.buffer = memAlloc(initialCapacity);
this.basePointer = memAddress(buffer);
this.writePointer = basePointer;
}
// === Low-level write operations ===
private void writeInt(int value) {
memPutInt(writePointer, value);
writePointer += 4;
}
private void writeLong(long value) {
memPutLong(writePointer, value);
writePointer += 8;
}
private void writeFloat(float value) {
memPutFloat(writePointer, value);
writePointer += 4;
}
private void writeDouble(double value) {
memPutDouble(writePointer, value);
writePointer += 8;
}
private void ensureCapacity(int needed) {
final int size = size();
final int remaining = buffer.capacity() - size;
if (remaining < needed) {
final int newCapacity = Math.max(buffer.capacity() * 2, buffer.capacity() + needed);
buffer = memRealloc(buffer, newCapacity);
basePointer = memAddress(buffer);
writePointer = basePointer + size;
}
}
// === Single int commands ===
public void writeEnable(int cap) {
ensureCapacity(8);
writeInt(GLCommand.ENABLE);
writeInt(cap);
}
public void writeDisable(int cap) {
ensureCapacity(8);
writeInt(GLCommand.DISABLE);
writeInt(cap);
}
public void writeClear(int mask) {
ensureCapacity(8);
writeInt(GLCommand.CLEAR);
writeInt(mask);
}
public void writeClearStencil(int s) {
ensureCapacity(8);
writeInt(GLCommand.CLEAR_STENCIL);
writeInt(s);
}
public void writeCullFace(int mode) {
ensureCapacity(8);
writeInt(GLCommand.CULL_FACE);
writeInt(mode);
}
public void writeDepthFunc(int func) {
ensureCapacity(8);
writeInt(GLCommand.DEPTH_FUNC);
writeInt(func);
}
public void writeShadeModel(int mode) {
ensureCapacity(8);
writeInt(GLCommand.SHADE_MODEL);
writeInt(mode);
}
public void writeLogicOp(int opcode) {
ensureCapacity(8);
writeInt(GLCommand.LOGIC_OP);
writeInt(opcode);
}
public void writeMatrixMode(int mode) {
ensureCapacity(8);
writeInt(GLCommand.MATRIX_MODE);
writeInt(mode);
}
public void writeActiveTexture(int texture) {
ensureCapacity(8);
writeInt(GLCommand.ACTIVE_TEXTURE);
writeInt(texture);
}
public void writeUseProgram(int program) {
ensureCapacity(8);
writeInt(GLCommand.USE_PROGRAM);
writeInt(program);
}
public void writePushAttrib(int mask) {
ensureCapacity(8);
writeInt(GLCommand.PUSH_ATTRIB);
writeInt(mask);
}
public void writePopAttrib() {
ensureCapacity(8);
writeInt(GLCommand.POP_ATTRIB);
writeInt(0); // unused padding
}
public void writeLoadIdentity() {
ensureCapacity(4);
writeInt(GLCommand.LOAD_IDENTITY);
}
public void writePushMatrix() {
ensureCapacity(4);
writeInt(GLCommand.PUSH_MATRIX);
}
public void writePopMatrix() {
ensureCapacity(4);
writeInt(GLCommand.POP_MATRIX);
}
public void writeStencilMask(int mask) {
ensureCapacity(8);
writeInt(GLCommand.STENCIL_MASK);
writeInt(mask);
}
public void writeDepthMask(boolean flag) {
ensureCapacity(8);
writeInt(GLCommand.DEPTH_MASK);
writeInt(flag ? 1 : 0);
}
public void writeFrontFace(int mode) {
ensureCapacity(8);
writeInt(GLCommand.FRONT_FACE);
writeInt(mode);
}
// === Two int commands ===
public void writeBindTexture(int target, int texture) {
ensureCapacity(12);
writeInt(GLCommand.BIND_TEXTURE);
writeInt(target);
writeInt(texture);
}
public void writePolygonMode(int face, int mode) {
ensureCapacity(12);
writeInt(GLCommand.POLYGON_MODE);
writeInt(face);
writeInt(mode);
}
public void writeColorMaterial(int face, int mode) {
ensureCapacity(12);
writeInt(GLCommand.COLOR_MATERIAL);
writeInt(face);
writeInt(mode);
}
public void writeLineStipple(int factor, int pattern) {
ensureCapacity(12);
writeInt(GLCommand.LINE_STIPPLE);
writeInt(factor);
writeInt(pattern);
}
public void writeStencilMaskSeparate(int face, int mask) {
ensureCapacity(12);
writeInt(GLCommand.STENCIL_MASK_SEPARATE);
writeInt(face);
writeInt(mask);
}
public void writeFogi(int pname, int param) {
ensureCapacity(12);
writeInt(GLCommand.FOGI);
writeInt(pname);
writeInt(param);
}
public void writeHint(int target, int mode) {
ensureCapacity(12);
writeInt(GLCommand.HINT);
writeInt(target);
writeInt(mode);
}
// === Three int commands ===
public void writeStencilFunc(int func, int ref, int mask) {
ensureCapacity(16);
writeInt(GLCommand.STENCIL_FUNC);
writeInt(func);
writeInt(ref);
writeInt(mask);
}
public void writeStencilOp(int fail, int zfail, int zpass) {
ensureCapacity(16);
writeInt(GLCommand.STENCIL_OP);
writeInt(fail);
writeInt(zfail);
writeInt(zpass);
}
public void writeTexParameteri(int target, int pname, int param) {
ensureCapacity(16);
writeInt(GLCommand.TEX_PARAMETERI);
writeInt(target);
writeInt(pname);
writeInt(param);
}
// === Four int commands ===
public void writeViewport(int x, int y, int width, int height) {
ensureCapacity(20);
writeInt(GLCommand.VIEWPORT);
writeInt(x);
writeInt(y);
writeInt(width);
writeInt(height);
}
public void writeScissor(int x, int y, int width, int height) {
ensureCapacity(20);
writeInt(GLCommand.SCISSOR);
writeInt(x);
writeInt(y);
writeInt(width);
writeInt(height);
}
public void writeBlendFunc(int srcRgb, int dstRgb, int srcAlpha, int dstAlpha) {
ensureCapacity(20);
writeInt(GLCommand.BLEND_FUNC);
writeInt(srcRgb);
writeInt(dstRgb);
writeInt(srcAlpha);
writeInt(dstAlpha);
}
public void writeColorMask(boolean red, boolean green, boolean blue, boolean alpha) {
ensureCapacity(20);
writeInt(GLCommand.COLOR_MASK);
writeInt(red ? 1 : 0);
writeInt(green ? 1 : 0);
writeInt(blue ? 1 : 0);
writeInt(alpha ? 1 : 0);
}
public void writeStencilFuncSeparate(int face, int func, int ref, int mask) {
ensureCapacity(20);
writeInt(GLCommand.STENCIL_FUNC_SEPARATE);
writeInt(face);
writeInt(func);
writeInt(ref);
writeInt(mask);
}
public void writeStencilOpSeparate(int face, int sfail, int dpfail, int dppass) {
ensureCapacity(20);
writeInt(GLCommand.STENCIL_OP_SEPARATE);
writeInt(face);
writeInt(sfail);
writeInt(dpfail);
writeInt(dppass);
}
// === Float commands ===
public void writePointSize(float size) {
ensureCapacity(8);
writeInt(GLCommand.POINT_SIZE);
writeFloat(size);
}
public void writeLineWidth(float width) {
ensureCapacity(8);
writeInt(GLCommand.LINE_WIDTH);
writeFloat(width);
}
public void writePolygonOffset(float factor, float units) {
ensureCapacity(12);
writeInt(GLCommand.POLYGON_OFFSET);
writeFloat(factor);
writeFloat(units);
}
public void writeColor(float r, float g, float b, float a) {
ensureCapacity(20);
writeInt(GLCommand.COLOR);
writeFloat(r);
writeFloat(g);
writeFloat(b);
writeFloat(a);
}
public void writeClearColor(float r, float g, float b, float a) {
ensureCapacity(20);
writeInt(GLCommand.CLEAR_COLOR);
writeFloat(r);
writeFloat(g);
writeFloat(b);
writeFloat(a);
}
public void writeClearDepth(double depth) {
ensureCapacity(12);
writeInt(GLCommand.CLEAR_DEPTH);
writeDouble(depth);
}
public void writeBlendColor(float r, float g, float b, float a) {
ensureCapacity(20);
writeInt(GLCommand.BLEND_COLOR);
writeFloat(r);
writeFloat(g);
writeFloat(b);
writeFloat(a);
}
// === Mixed int+float commands ===
public void writeAlphaFunc(int func, float ref) {
ensureCapacity(12);
writeInt(GLCommand.ALPHA_FUNC);
writeInt(func);
writeFloat(ref);
}
public void writeFogf(int pname, float param) {
ensureCapacity(12);
writeInt(GLCommand.FOGF);
writeInt(pname);
writeFloat(param);
}
public void writeLightf(int light, int pname, float param) {
ensureCapacity(16);
writeInt(GLCommand.LIGHTF);
writeInt(light);
writeInt(pname);
writeFloat(param);
}
public void writeLightModelf(int pname, float param) {
ensureCapacity(12);
writeInt(GLCommand.LIGHT_MODELF);
writeInt(pname);
writeFloat(param);
}
public void writeLighti(int light, int pname, int param) {
ensureCapacity(16);
writeInt(GLCommand.LIGHTI);
writeInt(light);
writeInt(pname);
writeInt(param);
}
public void writeLightModeli(int pname, int param) {
ensureCapacity(12);
writeInt(GLCommand.LIGHT_MODELI);
writeInt(pname);
writeInt(param);
}
public void writeMaterialf(int face, int pname, float param) {
ensureCapacity(16);
writeInt(GLCommand.MATERIALF);
writeInt(face);
writeInt(pname);
writeFloat(param);
}
public void writeTexParameterf(int target, int pname, float param) {
ensureCapacity(16);
writeInt(GLCommand.TEX_PARAMETERF);
writeInt(target);
writeInt(pname);
writeFloat(param);
}
// === Double commands ===
public void writeTranslate(double x, double y, double z) {
ensureCapacity(28);
writeInt(GLCommand.TRANSLATE);
writeDouble(x);
writeDouble(y);
writeDouble(z);
}
public void writeRotate(double angle, double x, double y, double z) {
ensureCapacity(36);
writeInt(GLCommand.ROTATE);
writeDouble(angle);
writeDouble(x);
writeDouble(y);
writeDouble(z);
}
public void writeScale(double x, double y, double z) {
ensureCapacity(28);
writeInt(GLCommand.SCALE);
writeDouble(x);
writeDouble(y);
writeDouble(z);
}
public void writeOrtho(double left, double right, double bottom, double top, double zNear, double zFar) {
ensureCapacity(52);
writeInt(GLCommand.ORTHO);
writeDouble(left);
writeDouble(right);
writeDouble(bottom);
writeDouble(top);
writeDouble(zNear);
writeDouble(zFar);
}
public void writeFrustum(double left, double right, double bottom, double top, double zNear, double zFar) {
ensureCapacity(52);
writeInt(GLCommand.FRUSTUM);
writeDouble(left);
writeDouble(right);
writeDouble(bottom);
writeDouble(top);
writeDouble(zNear);
writeDouble(zFar);
}
// === Matrix commands ===
public void writeMultMatrix(Matrix4f matrix) {
ensureCapacity(68);
writeInt(GLCommand.MULT_MATRIX);
writeMatrix4f(matrix);
}
public void writeLoadMatrix(Matrix4f matrix) {
ensureCapacity(68);
writeInt(GLCommand.LOAD_MATRIX);
writeMatrix4f(matrix);
}
/**
* Write a Matrix4f to the buffer in column-major order (OpenGL convention).
* Uses accessor methods instead of getToAddress() to avoid JOML MemUtil issues.
*/
private void writeMatrix4f(Matrix4f m) {
// Column 0
writeFloat(m.m00()); writeFloat(m.m01()); writeFloat(m.m02()); writeFloat(m.m03());
// Column 1
writeFloat(m.m10()); writeFloat(m.m11()); writeFloat(m.m12()); writeFloat(m.m13());
// Column 2
writeFloat(m.m20()); writeFloat(m.m21()); writeFloat(m.m22()); writeFloat(m.m23());
// Column 3
writeFloat(m.m30()); writeFloat(m.m31()); writeFloat(m.m32()); writeFloat(m.m33());
}
// === Buffer commands (inline 4 floats) ===
public void writeFog(int pname, FloatBuffer params) {
final int count = Math.min(params.remaining(), 4);
ensureCapacity(12 + count * 4);
writeInt(GLCommand.FOG);
writeInt(pname);
writeInt(count);
final int pos = params.position();
for (int i = 0; i < count; i++) {
writeFloat(params.get(pos + i));
}
}
public void writeLight(int light, int pname, FloatBuffer params) {
final int count = Math.min(params.remaining(), 4);
ensureCapacity(16 + count * 4);
writeInt(GLCommand.LIGHT);
writeInt(light);
writeInt(pname);
writeInt(count);
final int pos = params.position();
for (int i = 0; i < count; i++) {
writeFloat(params.get(pos + i));
}
}
public void writeLightModel(int pname, FloatBuffer params) {
final int count = Math.min(params.remaining(), 4);
ensureCapacity(12 + count * 4);
writeInt(GLCommand.LIGHT_MODEL);
writeInt(pname);
writeInt(count);
final int pos = params.position();
for (int i = 0; i < count; i++) {
writeFloat(params.get(pos + i));
}
}
public void writeMaterial(int face, int pname, FloatBuffer params) {
final int count = Math.min(params.remaining(), 4);
ensureCapacity(16 + count * 4);
writeInt(GLCommand.MATERIAL);
writeInt(face);
writeInt(pname);
writeInt(count);
final int pos = params.position();
for (int i = 0; i < count; i++) {
writeFloat(params.get(pos + i));
}
}
public void writeClipPlane(int plane, double a, double b, double c, double d) {
ensureCapacity(40);
writeInt(GLCommand.CLIP_PLANE);
writeInt(plane);
writeDouble(a);
writeDouble(b);
writeDouble(c);
writeDouble(d);
}
// === Draw commands ===
public void writeDrawRange(int vboIndex) {
ensureCapacity(8);
writeInt(GLCommand.DRAW_RANGE);
writeInt(vboIndex);
}
/**
* Write a DRAW_RANGE_RESTORE command that draws a VBO range and restores GL current state after.
* Used for immediate mode VBOs to restore GL_CURRENT_COLOR, GL_CURRENT_NORMAL, GL_CURRENT_TEXTURE_COORDS.
*
* @param vboIndex Index into ownedVbos array
*/
public void writeDrawRangeRestore(
int vboIndex, AccumulatedDraw.RestoreData restoreData
) {
ensureCapacity(28);
writeInt(GLCommand.DRAW_RANGE_RESTORE);
writeInt(vboIndex);
writeInt(restoreData.flags);
// Last color (4 bytes)
writeInt(restoreData.lastColor);
// Last normal (4 bytes)
writeInt(restoreData.lastNormal);
// Last texcoord (2 floats = 8 bytes)
writeFloat(restoreData.lastTexCoordU);
writeFloat(restoreData.lastTexCoordV);
}
public void writeCallList(int listId) {
ensureCapacity(8);
writeInt(GLCommand.CALL_LIST);
writeInt(listId);
}
public void writeDrawArrays(int mode, int start, int count) {
ensureCapacity(16);
writeInt(GLCommand.DRAW_ARRAYS);
writeInt(mode);
writeInt(start);
writeInt(count);
}
public void writeDrawElements(int mode, int indices_count, int type, long indices_buffer_offset) {
ensureCapacity(24);
writeInt(GLCommand.DRAW_ELEMENTS);
writeInt(mode);
writeInt(indices_count);
writeInt(type);
writeLong(indices_buffer_offset);
}
public void writeBindVBO(int vbo) {
ensureCapacity(8);
writeInt(GLCommand.BIND_VBO);
writeInt(vbo);
}
public void writeBindVAO(int vao) {
ensureCapacity(8);
writeInt(GLCommand.BIND_VAO);
writeInt(vao);
}
public void writeDrawBuffer(int mode) {
ensureCapacity(8);
writeInt(GLCommand.DRAW_BUFFER);
writeInt(mode);
}
private static final int MAX_DRAW_BUFFERS = 8;
// cmd (4) + count (4) + buffers (4 * MAX_DRAW_BUFFERS)
private static final int DRAW_BUFFERS_SIZE = 4 + 4 + 4 * MAX_DRAW_BUFFERS;
public void writeDrawBuffers(int count, int buffer) {
ensureCapacity(DRAW_BUFFERS_SIZE);
writeInt(GLCommand.DRAW_BUFFERS);
writeInt(count);
writeInt(buffer);
for (int i = 1; i < MAX_DRAW_BUFFERS; i++) {
writeInt(0);
}
}
public void writeDrawBuffers(int count, IntBuffer bufs) {
ensureCapacity(DRAW_BUFFERS_SIZE);
writeInt(GLCommand.DRAW_BUFFERS);
writeInt(count);
final int pos = bufs.position();
for (int i = 0; i < count && i < MAX_DRAW_BUFFERS; i++) {
writeInt(bufs.get(pos + i));
}
for (int i = count; i < MAX_DRAW_BUFFERS; i++) {
writeInt(0);
}
}
// === Complex object reference ===
public void writeComplexRef(DisplayListCommand cmd) {
ensureCapacity(8);
final int index = complexObjects.size();
complexObjects.add(cmd);
writeInt(GLCommand.COMPLEX_REF);
writeInt(index);
}
// === Reading methods for optimization pass ===
private long readPointer = 0;
/**
* Reset read position to the beginning of the buffer.
*/
public void resetRead() {
readPointer = basePointer;
}
/**
* Check if there's more data to read.
*/
public boolean hasRemaining() {
return readPointer < writePointer;
}
/**
* Read an int from the current position.
*/
public int readInt() {
final int value = memGetInt(readPointer);
readPointer += 4;
return value;
}
public long readLong() {
final long value = memGetLong(readPointer);
readPointer += 8;
return value;
}
/**
* Read a float from the current position.
*/
public float readFloat() {
final float value = memGetFloat(readPointer);
readPointer += 4;
return value;
}
/**
* Read a double from the current position.
*/
public double readDouble() {
final double value = memGetDouble(readPointer);
readPointer += 8;
return value;
}
/**
* Read a Matrix4f from the current position (16 floats in column-major order).
*/
public Matrix4f readMatrix4f(Matrix4f dest) {
dest.m00(readFloat()); dest.m01(readFloat()); dest.m02(readFloat()); dest.m03(readFloat());
dest.m10(readFloat()); dest.m11(readFloat()); dest.m12(readFloat()); dest.m13(readFloat());
dest.m20(readFloat()); dest.m21(readFloat()); dest.m22(readFloat()); dest.m23(readFloat());
dest.m30(readFloat()); dest.m31(readFloat()); dest.m32(readFloat()); dest.m33(readFloat());
return dest;
}
// === Lifecycle ===
public int size() {
return (int) (writePointer - basePointer);
}
public long address() {
return basePointer;
}
public ByteBuffer toBuffer() {
buffer.limit(size());
buffer.position(0);
return buffer;
}
public Object[] getComplexObjects() {
return complexObjects.toArray();
}
public Object getComplexObject(int index) {
return complexObjects.get(index);
}
public boolean isEmpty() {
return size() == 0 && complexObjects.isEmpty();
}
/**
* Clear the buffer for reuse. Does NOT free memory.
*/
public void clear() {
writePointer = basePointer;
complexObjects.clear();
}
/**
* Free the underlying buffer. Must be called when done.
*/
public void free() {
if (buffer != null) {
memFree(buffer);
buffer = null;
basePointer = 0;
writePointer = 0;
}
}
}
@@ -1,12 +1,10 @@
package com.gtnewhorizons.angelica.glsm.recording;
import com.gtnewhorizon.gtnhlib.client.opengl.UniversalVAO;
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFlags;
import com.gtnewhorizons.angelica.glsm.DisplayListManager;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.recording.commands.DisplayListCommand;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
import java.nio.ByteBuffer;
@@ -14,7 +12,10 @@ import java.nio.DoubleBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.*;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAddress;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memGetDouble;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memGetFloat;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memGetInt;
/**
* Executes commands from a CommandBuffer
@@ -39,7 +40,13 @@ public final class CommandBufferExecutor {
* @param complexObjects Array of complex objects (TexImage2DCmd, etc.)
* @param ownedVbos Array of VBOs owned by the display list (indexed by DrawRange commands)
*/
public static void execute(ByteBuffer buffer, Object[] complexObjects, DisplayListVBO ownedVbos) {
public static void execute(
ByteBuffer buffer,
DisplayListCommand[] complexObjects,
DisplayListVBO ownedVbos,
int list,
CompiledDisplayList compiledDisplayList
) {
long ptr = memAddress(buffer);
final long end = ptr + buffer.limit();
@@ -343,50 +350,6 @@ public final class CommandBufferExecutor {
GLStateManager.glTexParameterf(target, pname, param);
}
// === Double commands ===
case GLCommand.TRANSLATE -> {
final double x = memGetDouble(ptr);
final double y = memGetDouble(ptr + 8);
final double z = memGetDouble(ptr + 16);
ptr += 24;
GLStateManager.glTranslated(x, y, z);
}
case GLCommand.ROTATE -> {
final double angle = memGetDouble(ptr);
final double x = memGetDouble(ptr + 8);
final double y = memGetDouble(ptr + 16);
final double z = memGetDouble(ptr + 24);
ptr += 32;
GLStateManager.glRotated(angle, x, y, z);
}
case GLCommand.SCALE -> {
final double x = memGetDouble(ptr);
final double y = memGetDouble(ptr + 8);
final double z = memGetDouble(ptr + 16);
ptr += 24;
GLStateManager.glScaled(x, y, z);
}
case GLCommand.ORTHO -> {
final double left = memGetDouble(ptr);
final double right = memGetDouble(ptr + 8);
final double bottom = memGetDouble(ptr + 16);
final double top = memGetDouble(ptr + 24);
final double zNear = memGetDouble(ptr + 32);
final double zFar = memGetDouble(ptr + 40);
ptr += 48;
GLStateManager.glOrtho(left, right, bottom, top, zNear, zFar);
}
case GLCommand.FRUSTUM -> {
final double left = memGetDouble(ptr);
final double right = memGetDouble(ptr + 8);
final double bottom = memGetDouble(ptr + 16);
final double top = memGetDouble(ptr + 24);
final double zNear = memGetDouble(ptr + 32);
final double zFar = memGetDouble(ptr + 40);
ptr += 48;
GLStateManager.glFrustum(left, right, bottom, top, zNear, zFar);
}
// === Matrix commands ===
case GLCommand.MULT_MATRIX -> {
// Mode-agnostic: just multiply current matrix
@@ -398,6 +361,20 @@ public final class CommandBufferExecutor {
MATRIX_BUFFER.flip();
GLStateManager.glMultMatrix(MATRIX_BUFFER);
}
case GLCommand.TRANSLATE -> {
final float x = memGetFloat(ptr);
final float y = memGetFloat(ptr + 4);
final float z = memGetFloat(ptr + 8);
ptr += 12;
GLStateManager.glTranslatef(x, y, z);
}
case GLCommand.SCALE -> {
final float x = memGetFloat(ptr);
final float y = memGetFloat(ptr + 4);
final float z = memGetFloat(ptr + 8);
ptr += 12;
GLStateManager.glScalef(x, y, z);
}
case GLCommand.LOAD_MATRIX -> {
MATRIX_BUFFER.clear();
for (int i = 0; i < 16; i++) {
@@ -530,14 +507,6 @@ public final class CommandBufferExecutor {
ptr += 4 * MAX_DRAW_BUFFERS;
GLStateManager.glDrawBuffers(DRAW_BUFFERS_BUFFER);
}
case GLCommand.DRAW_ARRAYS -> {
GLStateManager.glDrawArrays(memGetInt(ptr), memGetInt(ptr + 4), memGetInt(ptr + 8));
ptr += 12;
}
case GLCommand.DRAW_ELEMENTS -> {
GLStateManager.glDrawElements(memGetInt(ptr), memGetInt(ptr + 4), memGetInt(ptr + 8), memGetLong(ptr + 12));
ptr += 20;
}
case GLCommand.BIND_VBO -> {
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, memGetInt(ptr));
ptr += 4;
@@ -551,10 +520,10 @@ public final class CommandBufferExecutor {
case GLCommand.COMPLEX_REF -> {
final int index = memGetInt(ptr);
ptr += 4;
((DisplayListCommand) complexObjects[index]).execute();
complexObjects[index].execute();
}
default -> throw new IllegalStateException("Unknown command opcode: " + cmd);
default -> throw new IllegalStateException("Unknown command opcode: " + cmd + " in display list with ID " + list + "\n" + DisplayListManager.getCompiledDisplayListString(list, compiledDisplayList, null));
}
}
}
@@ -1,168 +0,0 @@
package com.gtnewhorizons.angelica.glsm.recording;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.recording.commands.DisplayListCommand;
import org.joml.Matrix4f;
import java.nio.FloatBuffer;
public final class CommandBufferProcessor {
private static final Matrix4f tempMatrix = new Matrix4f();
private CommandBufferProcessor() {}
/**
* Copy a complete command (opcode already read) from raw to final buffer.
*/
public static void copyCommand(CommandBuffer raw, CommandBuffer out) {
final int opcode = raw.readInt();
copyCommandData(opcode, raw, out);
}
/**
* Copy command data based on opcode (opcode already consumed from raw).
*/
public static void copyCommandData(int opcode, CommandBuffer raw, CommandBuffer out) {
switch (opcode) {
// Single int commands
case GLCommand.ENABLE -> out.writeEnable(raw.readInt());
case GLCommand.DISABLE -> out.writeDisable(raw.readInt());
case GLCommand.CLEAR -> out.writeClear(raw.readInt());
case GLCommand.CLEAR_STENCIL -> out.writeClearStencil(raw.readInt());
case GLCommand.CULL_FACE -> out.writeCullFace(raw.readInt());
case GLCommand.DEPTH_FUNC -> out.writeDepthFunc(raw.readInt());
case GLCommand.SHADE_MODEL -> out.writeShadeModel(raw.readInt());
case GLCommand.LOGIC_OP -> out.writeLogicOp(raw.readInt());
case GLCommand.MATRIX_MODE -> out.writeMatrixMode(raw.readInt());
case GLCommand.ACTIVE_TEXTURE -> out.writeActiveTexture(raw.readInt());
case GLCommand.USE_PROGRAM -> out.writeUseProgram(raw.readInt());
case GLCommand.PUSH_ATTRIB -> out.writePushAttrib(raw.readInt());
case GLCommand.POP_ATTRIB -> { raw.readInt(); out.writePopAttrib(); }
case GLCommand.LOAD_IDENTITY -> out.writeLoadIdentity();
case GLCommand.PUSH_MATRIX -> out.writePushMatrix();
case GLCommand.POP_MATRIX -> out.writePopMatrix();
case GLCommand.STENCIL_MASK -> out.writeStencilMask(raw.readInt());
case GLCommand.DEPTH_MASK -> out.writeDepthMask(raw.readInt() != 0);
case GLCommand.FRONT_FACE -> out.writeFrontFace(raw.readInt());
// Two int commands
case GLCommand.BIND_TEXTURE -> out.writeBindTexture(raw.readInt(), raw.readInt());
case GLCommand.POLYGON_MODE -> out.writePolygonMode(raw.readInt(), raw.readInt());
case GLCommand.COLOR_MATERIAL -> out.writeColorMaterial(raw.readInt(), raw.readInt());
case GLCommand.LINE_STIPPLE -> out.writeLineStipple(raw.readInt(), raw.readInt());
case GLCommand.STENCIL_MASK_SEPARATE -> out.writeStencilMaskSeparate(raw.readInt(), raw.readInt());
case GLCommand.FOGI -> out.writeFogi(raw.readInt(), raw.readInt());
case GLCommand.HINT -> out.writeHint(raw.readInt(), raw.readInt());
// Three int commands
case GLCommand.STENCIL_FUNC -> out.writeStencilFunc(raw.readInt(), raw.readInt(), raw.readInt());
case GLCommand.STENCIL_OP -> out.writeStencilOp(raw.readInt(), raw.readInt(), raw.readInt());
case GLCommand.TEX_PARAMETERI -> out.writeTexParameteri(raw.readInt(), raw.readInt(), raw.readInt());
// Four int commands
case GLCommand.VIEWPORT -> out.writeViewport(raw.readInt(), raw.readInt(), raw.readInt(), raw.readInt());
case GLCommand.SCISSOR -> out.writeScissor(raw.readInt(), raw.readInt(), raw.readInt(), raw.readInt());
case GLCommand.BLEND_FUNC -> out.writeBlendFunc(raw.readInt(), raw.readInt(), raw.readInt(), raw.readInt());
case GLCommand.COLOR_MASK -> out.writeColorMask(raw.readInt() != 0, raw.readInt() != 0, raw.readInt() != 0, raw.readInt() != 0);
case GLCommand.STENCIL_FUNC_SEPARATE -> out.writeStencilFuncSeparate(raw.readInt(), raw.readInt(), raw.readInt(), raw.readInt());
case GLCommand.STENCIL_OP_SEPARATE -> out.writeStencilOpSeparate(raw.readInt(), raw.readInt(), raw.readInt(), raw.readInt());
// Float commands
case GLCommand.POINT_SIZE -> out.writePointSize(raw.readFloat());
case GLCommand.LINE_WIDTH -> out.writeLineWidth(raw.readFloat());
case GLCommand.POLYGON_OFFSET -> out.writePolygonOffset(raw.readFloat(), raw.readFloat());
case GLCommand.COLOR -> out.writeColor(raw.readFloat(), raw.readFloat(), raw.readFloat(), raw.readFloat());
case GLCommand.CLEAR_COLOR -> out.writeClearColor(raw.readFloat(), raw.readFloat(), raw.readFloat(), raw.readFloat());
// Single double commands
case GLCommand.CLEAR_DEPTH -> out.writeClearDepth(raw.readDouble());
// Four float commands
case GLCommand.BLEND_COLOR -> out.writeBlendColor(raw.readFloat(), raw.readFloat(), raw.readFloat(), raw.readFloat());
// Mixed int+float commands
case GLCommand.ALPHA_FUNC -> out.writeAlphaFunc(raw.readInt(), raw.readFloat());
case GLCommand.FOGF -> out.writeFogf(raw.readInt(), raw.readFloat());
case GLCommand.LIGHTF -> out.writeLightf(raw.readInt(), raw.readInt(), raw.readFloat());
case GLCommand.LIGHT_MODELF -> out.writeLightModelf(raw.readInt(), raw.readFloat());
case GLCommand.LIGHTI -> out.writeLighti(raw.readInt(), raw.readInt(), raw.readInt());
case GLCommand.LIGHT_MODELI -> out.writeLightModeli(raw.readInt(), raw.readInt());
case GLCommand.MATERIALF -> out.writeMaterialf(raw.readInt(), raw.readInt(), raw.readFloat());
case GLCommand.TEX_PARAMETERF -> out.writeTexParameterf(raw.readInt(), raw.readInt(), raw.readFloat());
// Double commands
case GLCommand.TRANSLATE -> out.writeTranslate(raw.readDouble(), raw.readDouble(), raw.readDouble());
case GLCommand.ROTATE -> out.writeRotate(raw.readDouble(), raw.readDouble(), raw.readDouble(), raw.readDouble());
case GLCommand.SCALE -> out.writeScale(raw.readDouble(), raw.readDouble(), raw.readDouble());
case GLCommand.ORTHO -> out.writeOrtho(raw.readDouble(), raw.readDouble(), raw.readDouble(), raw.readDouble(), raw.readDouble(), raw.readDouble());
case GLCommand.FRUSTUM -> out.writeFrustum(raw.readDouble(), raw.readDouble(), raw.readDouble(), raw.readDouble(), raw.readDouble(), raw.readDouble());
// Matrix commands
case GLCommand.MULT_MATRIX -> { out.writeMultMatrix(raw.readMatrix4f(tempMatrix)); }
case GLCommand.LOAD_MATRIX -> { out.writeLoadMatrix(raw.readMatrix4f(tempMatrix)); }
// FloatBuffer commands (inline 4 floats max)
case GLCommand.FOG -> {
final int pname = raw.readInt();
final int count = raw.readInt();
final FloatBuffer fb = FloatBuffer.allocate(count);
for (int i = 0; i < count; i++) fb.put(raw.readFloat());
fb.flip();
out.writeFog(pname, fb);
}
case GLCommand.LIGHT -> {
final int light = raw.readInt();
final int pname = raw.readInt();
final int count = raw.readInt();
final FloatBuffer fb = FloatBuffer.allocate(count);
for (int i = 0; i < count; i++) fb.put(raw.readFloat());
fb.flip();
out.writeLight(light, pname, fb);
}
case GLCommand.LIGHT_MODEL -> {
final int pname = raw.readInt();
final int count = raw.readInt();
final FloatBuffer fb = FloatBuffer.allocate(count);
for (int i = 0; i < count; i++) fb.put(raw.readFloat());
fb.flip();
out.writeLightModel(pname, fb);
}
case GLCommand.MATERIAL -> {
final int face = raw.readInt();
final int pname = raw.readInt();
final int count = raw.readInt();
final FloatBuffer fb = FloatBuffer.allocate(count);
for (int i = 0; i < count; i++) fb.put(raw.readFloat());
fb.flip();
out.writeMaterial(face, pname, fb);
}
// Clip plane command (4 doubles)
case GLCommand.CLIP_PLANE -> {
final int plane = raw.readInt();
final double a = raw.readDouble();
final double b = raw.readDouble();
final double c = raw.readDouble();
final double d = raw.readDouble();
out.writeClipPlane(plane, a, b, c, d);
}
// Draw/call commands
case GLCommand.DRAW_RANGE -> out.writeDrawRange(raw.readInt());
case GLCommand.CALL_LIST -> out.writeCallList(raw.readInt());
case GLCommand.DRAW_ARRAYS -> out.writeDrawArrays(raw.readInt(), raw.readInt(), raw.readInt());
case GLCommand.DRAW_ELEMENTS -> out.writeDrawElements(raw.readInt(), raw.readInt(), raw.readInt(), raw.readLong());
case GLCommand.BIND_VBO -> out.writeBindVBO(raw.readInt());
case GLCommand.BIND_VAO -> out.writeBindVAO(raw.readInt());
// Complex object reference - transfer the object to output buffer
case GLCommand.COMPLEX_REF -> {
final int index = raw.readInt();
final Object obj = raw.getComplexObject(index);
out.writeComplexRef((DisplayListCommand) obj);
}
default -> GLStateManager.LOGGER.warn("[CommandBufferProcessor] Unknown opcode in buffer copy: {}", opcode);
}
}
}
@@ -1,374 +1,701 @@
package com.gtnewhorizons.angelica.glsm.recording;
import com.gtnewhorizons.angelica.glsm.recording.commands.DisplayListCommand;
import com.gtnewhorizons.angelica.glsm.recording.commands.IndexedDrawBatchBuilder;
import com.gtnewhorizons.angelica.glsm.recording.commands.IndexedDrawCapture;
import org.joml.Matrix4f;
import org.joml.Vector3f;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.util.ArrayList;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.*;
/**
* Records GL commands to a CommandBuffer during display list compilation.
*/
public final class CommandRecorder {
private final CommandBuffer buffer;
private int commandCount;
private static final int DEFAULT_CAPACITY = 2048; // Guess at typical display list size
private ByteBuffer buffer;
private long basePointer;
private long writePointer;
private final ArrayList<DisplayListCommand> complexObjects = new ArrayList<>();
private final IndexedDrawBatchBuilder indexedDraws = new IndexedDrawBatchBuilder();
public CommandRecorder() {
this.buffer = new CommandBuffer();
this.commandCount = 0;
this(DEFAULT_CAPACITY);
}
public CommandBuffer getBuffer() {
return buffer;
public CommandRecorder(int initialCapacity) {
this.buffer = memAlloc(initialCapacity);
this.basePointer = memAddress0(buffer);
this.writePointer = basePointer;
}
public int getCommandCount() {
return commandCount;
// === Low-level write operations ===
private void writeInt(int value) {
memPutInt(writePointer, value);
writePointer += 4;
}
private void writeLong(long value) {
memPutLong(writePointer, value);
writePointer += 8;
}
private void writeFloat(float value) {
memPutFloat(writePointer, value);
writePointer += 4;
}
private void writeDouble(double value) {
memPutDouble(writePointer, value);
writePointer += 8;
}
private void ensureCapacity(int needed) {
final int size = size();
final int remaining = buffer.capacity() - size;
if (remaining < needed) {
final int newCapacity = Math.max(buffer.capacity() * 2, buffer.capacity() + needed);
buffer = memRealloc(buffer, newCapacity);
basePointer = memAddress0(buffer);
writePointer = basePointer + size;
}
}
public IndexedDrawBatchBuilder getIndexedDraws() {
return indexedDraws;
}
public void writeIndexedDrawCapture(IndexedDrawCapture capture) {
indexedDraws.add(capture);
writeComplexCommand(capture.placeholder);
}
public void free() {
buffer.free();
for (IndexedDrawCapture c : indexedDraws.getCaptures()) {
c.freeBuffers();
}
}
// ==================== Recording Methods ====================
// === Single int commands ===
public void recordEnable(int cap) {
buffer.writeEnable(cap);
commandCount++;
public void writeEnable(int cap) {
ensureCapacity(8);
writeInt(GLCommand.ENABLE);
writeInt(cap);
}
public void recordDisable(int cap) {
buffer.writeDisable(cap);
commandCount++;
public void writeDisable(int cap) {
ensureCapacity(8);
writeInt(GLCommand.DISABLE);
writeInt(cap);
}
public void recordClear(int mask) {
buffer.writeClear(mask);
commandCount++;
public void writeClear(int mask) {
ensureCapacity(8);
writeInt(GLCommand.CLEAR);
writeInt(mask);
}
public void recordClearColor(float r, float g, float b, float a) {
buffer.writeClearColor(r, g, b, a);
commandCount++;
public void writeClearStencil(int s) {
ensureCapacity(8);
writeInt(GLCommand.CLEAR_STENCIL);
writeInt(s);
}
public void recordClearDepth(double depth) {
buffer.writeClearDepth(depth);
commandCount++;
public void writeCullFace(int mode) {
ensureCapacity(8);
writeInt(GLCommand.CULL_FACE);
writeInt(mode);
}
public void recordBlendColor(float r, float g, float b, float a) {
buffer.writeBlendColor(r, g, b, a);
commandCount++;
public void writeDepthFunc(int func) {
ensureCapacity(8);
writeInt(GLCommand.DEPTH_FUNC);
writeInt(func);
}
public void recordClearStencil(int s) {
buffer.writeClearStencil(s);
commandCount++;
public void writeShadeModel(int mode) {
ensureCapacity(8);
writeInt(GLCommand.SHADE_MODEL);
writeInt(mode);
}
public void recordColor(float r, float g, float b, float a) {
buffer.writeColor(r, g, b, a);
commandCount++;
public void writeLogicOp(int opcode) {
ensureCapacity(8);
writeInt(GLCommand.LOGIC_OP);
writeInt(opcode);
}
public void recordColorMask(boolean r, boolean g, boolean b, boolean a) {
buffer.writeColorMask(r, g, b, a);
commandCount++;
public void writeMatrixMode(int mode) {
ensureCapacity(8);
writeInt(GLCommand.MATRIX_MODE);
writeInt(mode);
}
public void recordDepthMask(boolean flag) {
buffer.writeDepthMask(flag);
commandCount++;
public void writeActiveTexture(int texture) {
ensureCapacity(8);
writeInt(GLCommand.ACTIVE_TEXTURE);
writeInt(texture);
}
public void recordFrontFace(int mode) {
buffer.writeFrontFace(mode);
commandCount++;
public void writeUseProgram(int program) {
ensureCapacity(8);
writeInt(GLCommand.USE_PROGRAM);
writeInt(program);
}
public void recordDepthFunc(int func) {
buffer.writeDepthFunc(func);
commandCount++;
public void writePushAttrib(int mask) {
ensureCapacity(8);
writeInt(GLCommand.PUSH_ATTRIB);
writeInt(mask);
}
public void recordBlendFunc(int srcRgb, int dstRgb, int srcAlpha, int dstAlpha) {
buffer.writeBlendFunc(srcRgb, dstRgb, srcAlpha, dstAlpha);
commandCount++;
public void writePopAttrib() {
ensureCapacity(8);
writeInt(GLCommand.POP_ATTRIB);
writeInt(0); // unused padding
}
public void recordAlphaFunc(int func, float ref) {
buffer.writeAlphaFunc(func, ref);
commandCount++;
public void writeLoadIdentity() {
ensureCapacity(4);
writeInt(GLCommand.LOAD_IDENTITY);
}
public void recordCullFace(int mode) {
buffer.writeCullFace(mode);
commandCount++;
public void writePushMatrix() {
ensureCapacity(4);
writeInt(GLCommand.PUSH_MATRIX);
}
public void recordShadeModel(int mode) {
buffer.writeShadeModel(mode);
commandCount++;
public void writePopMatrix() {
ensureCapacity(4);
writeInt(GLCommand.POP_MATRIX);
}
public void recordBindTexture(int target, int texture) {
buffer.writeBindTexture(target, texture);
commandCount++;
public void writeStencilMask(int mask) {
ensureCapacity(8);
writeInt(GLCommand.STENCIL_MASK);
writeInt(mask);
}
public void recordTexParameteri(int target, int pname, int param) {
buffer.writeTexParameteri(target, pname, param);
commandCount++;
public void writeDepthMask(boolean flag) {
ensureCapacity(8);
writeInt(GLCommand.DEPTH_MASK);
writeInt(flag ? 1 : 0);
}
public void recordTexParameterf(int target, int pname, float param) {
buffer.writeTexParameterf(target, pname, param);
commandCount++;
public void writeFrontFace(int mode) {
ensureCapacity(8);
writeInt(GLCommand.FRONT_FACE);
writeInt(mode);
}
public void recordMatrixMode(int mode) {
buffer.writeMatrixMode(mode);
commandCount++;
}
public void recordLoadIdentity() {
buffer.writeLoadIdentity();
commandCount++;
}
public void recordPushMatrix() {
buffer.writePushMatrix();
commandCount++;
}
public void recordPopMatrix() {
buffer.writePopMatrix();
commandCount++;
}
public void recordMultMatrix(Matrix4f matrix) {
buffer.writeMultMatrix(matrix);
commandCount++;
}
public void recordLoadMatrix(Matrix4f matrix) {
buffer.writeLoadMatrix(matrix);
commandCount++;
}
// === Two int commands ===
public void recordOrtho(double left, double right, double bottom, double top, double zNear, double zFar) {
buffer.writeOrtho(left, right, bottom, top, zNear, zFar);
commandCount++;
public void writeBindTexture(int target, int texture) {
ensureCapacity(12);
writeInt(GLCommand.BIND_TEXTURE);
writeInt(target);
writeInt(texture);
}
public void recordFrustum(double left, double right, double bottom, double top, double zNear, double zFar) {
buffer.writeFrustum(left, right, bottom, top, zNear, zFar);
commandCount++;
public void writePolygonMode(int face, int mode) {
ensureCapacity(12);
writeInt(GLCommand.POLYGON_MODE);
writeInt(face);
writeInt(mode);
}
public void recordViewport(int x, int y, int width, int height) {
buffer.writeViewport(x, y, width, height);
commandCount++;
public void writeColorMaterial(int face, int mode) {
ensureCapacity(12);
writeInt(GLCommand.COLOR_MATERIAL);
writeInt(face);
writeInt(mode);
}
public void recordScissor(int x, int y, int width, int height) {
buffer.writeScissor(x, y, width, height);
commandCount++;
public void writeLineStipple(int factor, int pattern) {
ensureCapacity(12);
writeInt(GLCommand.LINE_STIPPLE);
writeInt(factor);
writeInt(pattern);
}
public void recordPointSize(float size) {
buffer.writePointSize(size);
commandCount++;
public void writeStencilMaskSeparate(int face, int mask) {
ensureCapacity(12);
writeInt(GLCommand.STENCIL_MASK_SEPARATE);
writeInt(face);
writeInt(mask);
}
public void recordLineWidth(float width) {
buffer.writeLineWidth(width);
commandCount++;
public void writeFogi(int pname, int param) {
ensureCapacity(12);
writeInt(GLCommand.FOGI);
writeInt(pname);
writeInt(param);
}
public void recordLineStipple(int factor, int pattern) {
buffer.writeLineStipple(factor, pattern);
commandCount++;
public void writeHint(int target, int mode) {
ensureCapacity(12);
writeInt(GLCommand.HINT);
writeInt(target);
writeInt(mode);
}
public void recordPolygonOffset(float factor, float units) {
buffer.writePolygonOffset(factor, units);
commandCount++;
}
public void recordPolygonMode(int face, int mode) {
buffer.writePolygonMode(face, mode);
commandCount++;
}
// === Three int commands ===
public void recordColorMaterial(int face, int mode) {
buffer.writeColorMaterial(face, mode);
commandCount++;
public void writeStencilFunc(int func, int ref, int mask) {
ensureCapacity(16);
writeInt(GLCommand.STENCIL_FUNC);
writeInt(func);
writeInt(ref);
writeInt(mask);
}
public void recordLogicOp(int opcode) {
buffer.writeLogicOp(opcode);
commandCount++;
public void writeStencilOp(int fail, int zfail, int zpass) {
ensureCapacity(16);
writeInt(GLCommand.STENCIL_OP);
writeInt(fail);
writeInt(zfail);
writeInt(zpass);
}
public void recordActiveTexture(int texture) {
buffer.writeActiveTexture(texture);
commandCount++;
public void writeTexParameteri(int target, int pname, int param) {
ensureCapacity(16);
writeInt(GLCommand.TEX_PARAMETERI);
writeInt(target);
writeInt(pname);
writeInt(param);
}
public void recordUseProgram(int program) {
buffer.writeUseProgram(program);
commandCount++;
}
// === Four int commands ===
public void recordPushAttrib(int mask) {
buffer.writePushAttrib(mask);
commandCount++;
public void writeViewport(int x, int y, int width, int height) {
ensureCapacity(20);
writeInt(GLCommand.VIEWPORT);
writeInt(x);
writeInt(y);
writeInt(width);
writeInt(height);
}
public void recordPopAttrib() {
buffer.writePopAttrib();
commandCount++;
public void writeScissor(int x, int y, int width, int height) {
ensureCapacity(20);
writeInt(GLCommand.SCISSOR);
writeInt(x);
writeInt(y);
writeInt(width);
writeInt(height);
}
public void recordFogf(int pname, float param) {
buffer.writeFogf(pname, param);
commandCount++;
public void writeBlendFunc(int srcRgb, int dstRgb, int srcAlpha, int dstAlpha) {
ensureCapacity(20);
writeInt(GLCommand.BLEND_FUNC);
writeInt(srcRgb);
writeInt(dstRgb);
writeInt(srcAlpha);
writeInt(dstAlpha);
}
public void recordFogi(int pname, int param) {
buffer.writeFogi(pname, param);
commandCount++;
public void writeColorMask(boolean red, boolean green, boolean blue, boolean alpha) {
ensureCapacity(20);
writeInt(GLCommand.COLOR_MASK);
writeInt(red ? 1 : 0);
writeInt(green ? 1 : 0);
writeInt(blue ? 1 : 0);
writeInt(alpha ? 1 : 0);
}
public void recordHint(int target, int mode) {
buffer.writeHint(target, mode);
commandCount++;
public void writeStencilFuncSeparate(int face, int func, int ref, int mask) {
ensureCapacity(20);
writeInt(GLCommand.STENCIL_FUNC_SEPARATE);
writeInt(face);
writeInt(func);
writeInt(ref);
writeInt(mask);
}
public void recordFog(int pname, FloatBuffer params) {
buffer.writeFog(pname, params);
commandCount++;
public void writeStencilOpSeparate(int face, int sfail, int dpfail, int dppass) {
ensureCapacity(20);
writeInt(GLCommand.STENCIL_OP_SEPARATE);
writeInt(face);
writeInt(sfail);
writeInt(dpfail);
writeInt(dppass);
}
public void recordLightf(int light, int pname, float param) {
buffer.writeLightf(light, pname, param);
commandCount++;
}
// === Float commands ===
public void recordLighti(int light, int pname, int param) {
buffer.writeLighti(light, pname, param);
commandCount++;
public void writePointSize(float size) {
ensureCapacity(8);
writeInt(GLCommand.POINT_SIZE);
writeFloat(size);
}
public void recordLight(int light, int pname, FloatBuffer params) {
buffer.writeLight(light, pname, params);
commandCount++;
public void writeLineWidth(float width) {
ensureCapacity(8);
writeInt(GLCommand.LINE_WIDTH);
writeFloat(width);
}
public void recordLightModelf(int pname, float param) {
buffer.writeLightModelf(pname, param);
commandCount++;
public void writePolygonOffset(float factor, float units) {
ensureCapacity(12);
writeInt(GLCommand.POLYGON_OFFSET);
writeFloat(factor);
writeFloat(units);
}
public void recordLightModeli(int pname, int param) {
buffer.writeLightModeli(pname, param);
commandCount++;
public void writeColor(float r, float g, float b, float a) {
ensureCapacity(20);
writeInt(GLCommand.COLOR);
writeFloat(r);
writeFloat(g);
writeFloat(b);
writeFloat(a);
}
public void recordLightModel(int pname, FloatBuffer params) {
buffer.writeLightModel(pname, params);
commandCount++;
public void writeClearColor(float r, float g, float b, float a) {
ensureCapacity(20);
writeInt(GLCommand.CLEAR_COLOR);
writeFloat(r);
writeFloat(g);
writeFloat(b);
writeFloat(a);
}
public void recordMaterialf(int face, int pname, float val) {
buffer.writeMaterialf(face, pname, val);
commandCount++;
public void writeClearDepth(double depth) {
ensureCapacity(12);
writeInt(GLCommand.CLEAR_DEPTH);
writeDouble(depth);
}
public void recordMaterial(int face, int pname, FloatBuffer params) {
buffer.writeMaterial(face, pname, params);
commandCount++;
public void writeBlendColor(float r, float g, float b, float a) {
ensureCapacity(20);
writeInt(GLCommand.BLEND_COLOR);
writeFloat(r);
writeFloat(g);
writeFloat(b);
writeFloat(a);
}
public void recordClipPlane(int plane, double a, double b, double c, double d) {
buffer.writeClipPlane(plane, a, b, c, d);
commandCount++;
}
// === Mixed int+float commands ===
public void recordStencilFunc(int func, int ref, int mask) {
buffer.writeStencilFunc(func, ref, mask);
commandCount++;
public void writeAlphaFunc(int func, float ref) {
ensureCapacity(12);
writeInt(GLCommand.ALPHA_FUNC);
writeInt(func);
writeFloat(ref);
}
public void recordStencilFuncSeparate(int face, int func, int ref, int mask) {
buffer.writeStencilFuncSeparate(face, func, ref, mask);
commandCount++;
public void writeFogf(int pname, float param) {
ensureCapacity(12);
writeInt(GLCommand.FOGF);
writeInt(pname);
writeFloat(param);
}
public void recordStencilOp(int fail, int zfail, int zpass) {
buffer.writeStencilOp(fail, zfail, zpass);
commandCount++;
public void writeLightf(int light, int pname, float param) {
ensureCapacity(16);
writeInt(GLCommand.LIGHTF);
writeInt(light);
writeInt(pname);
writeFloat(param);
}
public void recordStencilOpSeparate(int face, int sfail, int dpfail, int dppass) {
buffer.writeStencilOpSeparate(face, sfail, dpfail, dppass);
commandCount++;
public void writeLightModelf(int pname, float param) {
ensureCapacity(12);
writeInt(GLCommand.LIGHT_MODELF);
writeInt(pname);
writeFloat(param);
}
public void recordStencilMask(int mask) {
buffer.writeStencilMask(mask);
commandCount++;
public void writeLighti(int light, int pname, int param) {
ensureCapacity(16);
writeInt(GLCommand.LIGHTI);
writeInt(light);
writeInt(pname);
writeInt(param);
}
public void recordStencilMaskSeparate(int face, int mask) {
buffer.writeStencilMaskSeparate(face, mask);
commandCount++;
public void writeLightModeli(int pname, int param) {
ensureCapacity(12);
writeInt(GLCommand.LIGHT_MODELI);
writeInt(pname);
writeInt(param);
}
public void recordCallList(int listId) {
buffer.writeCallList(listId);
commandCount++;
public void writeMaterialf(int face, int pname, float param) {
ensureCapacity(16);
writeInt(GLCommand.MATERIALF);
writeInt(face);
writeInt(pname);
writeFloat(param);
}
public void recordDrawBuffer(int mode) {
buffer.writeDrawBuffer(mode);
commandCount++;
}
public void recordDrawBuffers(int count, int buf) {
buffer.writeDrawBuffers(count, buf);
commandCount++;
}
public void recordDrawBuffers(int count, IntBuffer bufs) {
buffer.writeDrawBuffers(count, bufs);
commandCount++;
public void writeTexParameterf(int target, int pname, float param) {
ensureCapacity(16);
writeInt(GLCommand.TEX_PARAMETERF);
writeInt(target);
writeInt(pname);
writeFloat(param);
}
// === Transformation commands ===
public void writeTranslate(Vector3f translationVector) {
ensureCapacity(16);
writeInt(GLCommand.TRANSLATE);
writeFloat(translationVector.x);
writeFloat(translationVector.y);
writeFloat(translationVector.z);
}
public void writeScale(Vector3f scaleVector) {
ensureCapacity(16);
writeInt(GLCommand.SCALE);
writeFloat(scaleVector.x);
writeFloat(scaleVector.y);
writeFloat(scaleVector.z);
}
// === Matrix commands ===
public void writeMultMatrix(Matrix4f matrix) {
ensureCapacity(68);
writeInt(GLCommand.MULT_MATRIX);
writeMatrix4f(matrix);
}
public void writeLoadMatrix(Matrix4f matrix) {
ensureCapacity(68);
writeInt(GLCommand.LOAD_MATRIX);
writeMatrix4f(matrix);
}
/**
* Write a Matrix4f to the buffer in column-major order (OpenGL convention).
* Uses accessor methods instead of getToAddress() to avoid JOML MemUtil issues.
*/
private void writeMatrix4f(Matrix4f m) {
// Column 0
writeFloat(m.m00()); writeFloat(m.m01()); writeFloat(m.m02()); writeFloat(m.m03());
// Column 1
writeFloat(m.m10()); writeFloat(m.m11()); writeFloat(m.m12()); writeFloat(m.m13());
// Column 2
writeFloat(m.m20()); writeFloat(m.m21()); writeFloat(m.m22()); writeFloat(m.m23());
// Column 3
writeFloat(m.m30()); writeFloat(m.m31()); writeFloat(m.m32()); writeFloat(m.m33());
}
// === Buffer commands (inline 4 floats) ===
public void writeFog(int pname, FloatBuffer params) {
final int count = Math.min(params.remaining(), 4);
ensureCapacity(12 + count * 4);
writeInt(GLCommand.FOG);
writeInt(pname);
writeInt(count);
final int pos = params.position();
for (int i = 0; i < count; i++) {
writeFloat(params.get(pos + i));
}
}
public void writeLight(int light, int pname, FloatBuffer params) {
final int count = Math.min(params.remaining(), 4);
ensureCapacity(16 + count * 4);
writeInt(GLCommand.LIGHT);
writeInt(light);
writeInt(pname);
writeInt(count);
final int pos = params.position();
for (int i = 0; i < count; i++) {
writeFloat(params.get(pos + i));
}
}
public void writeLightModel(int pname, FloatBuffer params) {
final int count = Math.min(params.remaining(), 4);
ensureCapacity(12 + count * 4);
writeInt(GLCommand.LIGHT_MODEL);
writeInt(pname);
writeInt(count);
final int pos = params.position();
for (int i = 0; i < count; i++) {
writeFloat(params.get(pos + i));
}
}
public void writeMaterial(int face, int pname, FloatBuffer params) {
final int count = Math.min(params.remaining(), 4);
ensureCapacity(16 + count * 4);
writeInt(GLCommand.MATERIAL);
writeInt(face);
writeInt(pname);
writeInt(count);
final int pos = params.position();
for (int i = 0; i < count; i++) {
writeFloat(params.get(pos + i));
}
}
public void writeClipPlane(int plane, double a, double b, double c, double d) {
ensureCapacity(40);
writeInt(GLCommand.CLIP_PLANE);
writeInt(plane);
writeDouble(a);
writeDouble(b);
writeDouble(c);
writeDouble(d);
}
// === Draw commands ===
public void writeDrawRange(int vboIndex) {
ensureCapacity(8);
writeInt(GLCommand.DRAW_RANGE);
writeInt(vboIndex);
}
/**
* Write a DRAW_RANGE_RESTORE command that draws a VBO range and restores GL current state after.
* Used for immediate mode VBOs to restore GL_CURRENT_COLOR, GL_CURRENT_NORMAL, GL_CURRENT_TEXTURE_COORDS.
*
* @param vboIndex Index into ownedVbos array
*/
public void writeDrawRangeRestore(
int vboIndex, AccumulatedDraw.RestoreData restoreData
) {
ensureCapacity(28);
writeInt(GLCommand.DRAW_RANGE_RESTORE);
writeInt(vboIndex);
writeInt(restoreData.flags);
// Last color (4 bytes)
writeInt(restoreData.lastColor);
// Last normal (4 bytes)
writeInt(restoreData.lastNormal);
// Last texcoord (2 floats = 8 bytes)
writeFloat(restoreData.lastTexCoordU);
writeFloat(restoreData.lastTexCoordV);
}
public void writeCallList(int listId) {
ensureCapacity(8);
writeInt(GLCommand.CALL_LIST);
writeInt(listId);
}
public void writeBindVBO(int vbo) {
ensureCapacity(8);
writeInt(GLCommand.BIND_VBO);
writeInt(vbo);
}
public void writeBindVAO(int vao) {
ensureCapacity(8);
writeInt(GLCommand.BIND_VAO);
writeInt(vao);
}
public void writeDrawBuffer(int mode) {
ensureCapacity(8);
writeInt(GLCommand.DRAW_BUFFER);
writeInt(mode);
}
private static final int MAX_DRAW_BUFFERS = 8;
// cmd (4) + count (4) + buffers (4 * MAX_DRAW_BUFFERS)
private static final int DRAW_BUFFERS_SIZE = 4 + 4 + 4 * MAX_DRAW_BUFFERS;
public void writeDrawBuffers(int count, int buffer) {
ensureCapacity(DRAW_BUFFERS_SIZE);
writeInt(GLCommand.DRAW_BUFFERS);
writeInt(count);
writeInt(buffer);
for (int i = 1; i < MAX_DRAW_BUFFERS; i++) {
writeInt(0);
}
}
public void writeDrawBuffers(int count, IntBuffer bufs) {
ensureCapacity(DRAW_BUFFERS_SIZE);
writeInt(GLCommand.DRAW_BUFFERS);
writeInt(count);
final int pos = bufs.position();
for (int i = 0; i < count && i < MAX_DRAW_BUFFERS; i++) {
writeInt(bufs.get(pos + i));
}
for (int i = count; i < MAX_DRAW_BUFFERS; i++) {
writeInt(0);
}
}
// === Complex object reference ===
public void writeComplexCommand(DisplayListCommand cmd) {
ensureCapacity(8);
final int index = complexObjects.size();
complexObjects.add(cmd);
writeInt(GLCommand.COMPLEX_REF);
writeInt(index);
}
// === Lifecycle ===
public int size() {
return (int) (writePointer - basePointer);
}
public void recordDrawArrays(int mode, int start, int count) {
buffer.writeDrawArrays(mode, start, count);
commandCount++;
public void delete() {
for (IndexedDrawCapture c : indexedDraws.getCaptures()) {
c.freeBuffers();
}
for (DisplayListCommand cmd : complexObjects) {
cmd.delete();
}
complexObjects.clear();
if (buffer != null) {
memFree(buffer);
buffer = null;
}
}
/**
* Deletes every allocated object & returns a ByteBuffer with the capacity equal to its limit.
* This CommandRecorder object cannot be used after calling this anymore
*/
public ByteBuffer finish() {
for (IndexedDrawCapture c : indexedDraws.getCaptures()) {
c.freeBuffers();
}
final int size = size();
buffer.limit(size);
final ByteBuffer out = memRealloc(buffer, size);
buffer = null;
complexObjects.clear();
return out;
}
public void recordDrawElements(int mode, int indices_count, int type, long indices_buffer_offset) {
buffer.writeDrawElements(mode, indices_count, type, indices_buffer_offset);
commandCount++;
public DisplayListCommand[] getComplexObjects() {
return complexObjects.toArray(new DisplayListCommand[complexObjects.size()]);
}
public void recordBindVBO(int vbo) {
buffer.writeBindVBO(vbo);
commandCount++;
public DisplayListCommand getComplexObject(int index) {
return complexObjects.get(index);
}
public void recordBindVAO(int vao) {
buffer.writeBindVAO(vao);
commandCount++;
public boolean isEmpty() {
return size() == 0;
}
public void recordComplexCommand(DisplayListCommand cmd) {
buffer.writeComplexRef(cmd);
commandCount++;
/**
* Clear the buffer for reuse. Does NOT free memory.
*/
public void clear() {
writePointer = basePointer;
complexObjects.clear();
}
}
@@ -2,12 +2,15 @@ package com.gtnewhorizons.angelica.glsm.recording;
import com.gtnewhorizons.angelica.glsm.DisplayListManager;
import com.gtnewhorizons.angelica.glsm.recording.commands.DisplayListCommand;
import com.gtnewhorizons.angelica.glsm.recording.commands.IndexedDrawBatch;
import it.unimi.dsi.fastutil.ints.Int2IntMap;
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import java.nio.ByteBuffer;
import java.util.Collections;
import java.util.List;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAddress;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memFree;
@@ -26,24 +29,30 @@ public final class CompiledDisplayList {
* Empty display list singleton. Per OpenGL spec, an empty list is still valid.
* This instance does nothing when rendered and is never deleted (shared singleton).
*/
public static final CompiledDisplayList EMPTY = new CompiledDisplayList(null, null, null);
public static final CompiledDisplayList EMPTY = new CompiledDisplayList(null, null, null, Collections.emptyList());
private final ByteBuffer commandBuffer; // Off-heap command storage, must be freed
private final Object[] complexObjects; // Complex commands (TexImage2D, etc.)
private final DisplayListCommand[] complexObjects; // Complex commands (TexImage2D, etc.)
private final DisplayListVBO ownedVbos; // GPU resources referenced by index
private final List<IndexedDrawBatch> indexedBatches; // Shared VAO/VBO/EBO triples per layout group
public CompiledDisplayList(ByteBuffer commandBuffer, Object[] complexObjects, DisplayListVBO ownedVbos) {
public CompiledDisplayList(ByteBuffer commandBuffer, DisplayListCommand[] complexObjects, DisplayListVBO ownedVbos, List<IndexedDrawBatch> indexedBatches) {
this.commandBuffer = commandBuffer;
this.complexObjects = complexObjects;
this.ownedVbos = ownedVbos;
this.indexedBatches = indexedBatches;
}
/**
* Render this display list by executing all commands.
*/
public void render() {
render(-1);
}
public void render(int list) {
if (commandBuffer != null && commandBuffer.limit() > 0) {
CommandBufferExecutor.execute(commandBuffer, complexObjects, ownedVbos);
CommandBufferExecutor.execute(commandBuffer, complexObjects, ownedVbos, list, this);
}
}
@@ -51,20 +60,24 @@ public final class CompiledDisplayList {
* Delete all resources held by this display list.
*/
public void delete() {
// Delete complex objects that hold resources
if (complexObjects != null) {
for (Object obj : complexObjects) {
if (obj instanceof DisplayListCommand cmd) {
cmd.delete();
}
for (DisplayListCommand cmd : complexObjects) {
cmd.delete();
}
}
// Close VBOs
// Close VBOs from immediate-mode DisplayListVBOBuilder.
if (ownedVbos != null) {
ownedVbos.delete();
}
// Release shared (VAO, VBO, EBO) triples from IndexedDrawBatchBuilder.
if (indexedBatches != null) {
for (IndexedDrawBatch batch : indexedBatches) {
batch.delete();
}
}
// Free the command buffer (off-heap memory)
if (commandBuffer != null) {
memFree(commandBuffer);
@@ -81,7 +94,7 @@ public final class CompiledDisplayList {
/**
* Get complex objects for inlining into another display list (nested lists).
*/
public Object[] getComplexObjects() {
public DisplayListCommand[] getComplexObjects() {
return complexObjects;
}
@@ -89,6 +102,10 @@ public final class CompiledDisplayList {
return ownedVbos;
}
public List<IndexedDrawBatch> getIndexedBatches() {
return indexedBatches;
}
// === Test inspection methods ===
/**
@@ -129,8 +146,20 @@ public final class CompiledDisplayList {
return opcodes;
}
public String getCompiledDisplayListString(int listId, StackTraceElement[] stackTrace) {
return DisplayListManager.getCompiledDisplayListString(listId, this, stackTrace);
}
public String toString(int listId, StackTraceElement[] stackTrace) {
return getCompiledDisplayListString(listId, stackTrace);
}
public String toString(int listId) {
return toString(listId, null);
}
@Override
public String toString() {
return DisplayListManager.getCompiledDisplayListString(0, this, null);
return toString(0);
}
}
@@ -23,6 +23,10 @@ public final class DisplayListVBO {
return vbos;
}
public SubVBO getVBO(int index) {
return vbos[index];
}
public void render(int index) {
vbos[index].render();
}
@@ -41,6 +45,18 @@ public final class DisplayListVBO {
this.count = count;
}
public int getStart() {
return start;
}
public int getCount() {
return count;
}
public int getDrawMode() {
return drawMode;
}
public void delete() {
vao.delete();
}
@@ -68,13 +68,16 @@ public final class DisplayListVBOBuilder {
allBuffers.addAll(drawBuffers);
int size = 0;
for (ByteBuffer buffer : drawBuffers) {
size += buffer.remaining();
size += buffer.limit();
}
vertexCount = format.getVertexCount(size);
}
if (data.drawMode == GL11.GL_QUADS) {
final IVertexArrayObject indexedVAO = new IndexedVAO(vbo, IndexBuffer.convertQuadsToTrigs(start, start + vertexCount));
vbos[data.drawIndex] = new DisplayListVBO.SubVBO(indexedVAO, GL11.GL_TRIANGLES, 0, vertexCount / 4 * 6);
} else if (data.drawMode == GL11.GL_QUAD_STRIP) {
// GL_QUAD_STRIP is removed in core profile; an even-length GL_TRIANGLE_STRIP produces the same quads
vbos[data.drawIndex] = new DisplayListVBO.SubVBO(vao, GL11.GL_TRIANGLE_STRIP, start, vertexCount & ~1);
} else if (data.drawMode == GL11.GL_POLYGON) {
// GL_POLYGON is removed in core profile; convert to GL_TRIANGLE_FAN which is equivalent for convex polygons
vbos[data.drawIndex] = new DisplayListVBO.SubVBO(vao, GL11.GL_TRIANGLE_FAN, start, vertexCount);
@@ -74,10 +74,7 @@ public final class GLCommand {
// === Double commands ===
public static final int TRANSLATE = 70; // [cmd:4][mode:4][x:8d][y:8d][z:8d] = 32 bytes
public static final int ROTATE = 71; // [cmd:4][mode:4][angle:8d][x:8d][y:8d][z:8d] = 40 bytes
public static final int SCALE = 72; // [cmd:4][mode:4][x:8d][y:8d][z:8d] = 32 bytes
public static final int ORTHO = 73; // [cmd:4][left:8d][right:8d][bottom:8d][top:8d][zNear:8d][zFar:8d] = 52 bytes
public static final int FRUSTUM = 74; // [cmd:4][left:8d][right:8d][bottom:8d][top:8d][zNear:8d][zFar:8d] = 52 bytes
public static final int CLEAR_DEPTH = 75; // [cmd:4][depth:8d] = 12 bytes
// === Matrix commands (72 bytes) ===
@@ -97,8 +94,6 @@ public final class GLCommand {
public static final int CALL_LIST = 102; // [cmd:4][listId:4] = 8 bytes
public static final int DRAW_ARRAYS = 110; // [cmd:4][drawMode:4][start:4][count:4] = 20 bytes
public static final int DRAW_ELEMENTS = 111; // [cmd:4][mode:4][indices_count:4][type:4][indices_buffer_offset:8] = 28 bytes
public static final int DRAW_BUFFER = 112; // [cmd:4][mode:4] = 8 bytes
public static final int DRAW_BUFFERS = 113; // [cmd:4][count:4][bufs:4*8] = 40 bytes (up to 8 buffers)
/**
@@ -169,10 +164,7 @@ public final class GLCommand {
case MATERIALF -> "MATERIALF";
case TEX_PARAMETERF -> "TEX_PARAMETERF";
case TRANSLATE -> "TRANSLATE";
case ROTATE -> "ROTATE";
case SCALE -> "SCALE";
case ORTHO -> "ORTHO";
case FRUSTUM -> "FRUSTUM";
case CLEAR_DEPTH -> "CLEAR_DEPTH";
case MULT_MATRIX -> "MULT_MATRIX";
case LOAD_MATRIX -> "LOAD_MATRIX";
@@ -219,7 +211,7 @@ public final class GLCommand {
// Three int commands (16 bytes)
case GLCommand.STENCIL_FUNC, GLCommand.STENCIL_OP, GLCommand.TEX_PARAMETERI,
GLCommand.LIGHTF, GLCommand.LIGHTI,
GLCommand.MATERIALF, GLCommand.TEX_PARAMETERF, GLCommand.DRAW_ARRAYS -> 16;
GLCommand.MATERIALF, GLCommand.TEX_PARAMETERF -> 16;
// Four int commands (20 bytes)
case GLCommand.VIEWPORT, GLCommand.BLEND_FUNC, GLCommand.COLOR_MASK,
@@ -227,8 +219,6 @@ public final class GLCommand {
GLCommand.SCISSOR,
GLCommand.COLOR, GLCommand.CLEAR_COLOR, GLCommand.BLEND_COLOR -> 20;
case GLCommand.DRAW_ELEMENTS -> 24;
// DRAW_RANGE_RESTORE: 28 bytes
case GLCommand.DRAW_RANGE_RESTORE -> 28;
@@ -239,10 +229,8 @@ public final class GLCommand {
case GLCommand.DRAW_BUFFERS -> 40;
// Double commands
case GLCommand.TRANSLATE, GLCommand.SCALE -> 28; // cmd + 3 doubles
case GLCommand.TRANSLATE, GLCommand.SCALE -> 16; // cmd + 3 floats
case GLCommand.CLEAR_DEPTH -> 12; // cmd + 1 double
case GLCommand.ROTATE -> 36; // cmd + 4 doubles
case GLCommand.ORTHO, GLCommand.FRUSTUM -> 52; // cmd + 6 doubles
// Matrix commands
case GLCommand.MULT_MATRIX, GLCommand.LOAD_MATRIX -> 68; // cmd + 16 floats
@@ -1,47 +1,56 @@
package com.gtnewhorizons.angelica.glsm.recording;
import com.gtnewhorizon.gtnhlib.client.renderer.CallbackTessellator;
import com.gtnewhorizon.gtnhlib.client.renderer.DirectTessellator;
import com.gtnewhorizon.gtnhlib.client.renderer.TessellatorManager;
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormatElement.Usage;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.states.VertexAttribState;
import com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities;
import com.gtnewhorizons.angelica.glsm.ffp.VAOManager;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
import java.nio.ByteBuffer;
import java.util.Arrays;
import static com.gtnewhorizons.angelica.glsm.backend.BackendManager.RENDER_BACKEND;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
/**
* Records immediate mode GL calls (glBegin/glEnd/glVertex) via {@link DirectTessellator}.
* Used both during display list compilation and for live immediate mode emulation in core profile.
*/
public final class ImmediateModeRecorder {
private static final DirectTessellator tessellator = new DirectTessellator(TessellatorManager.DEFAULT_BUFFER_SIZE);
private static final CallbackTessellator tessellator = new CallbackTessellator(TessellatorManager.DEFAULT_BUFFER_SIZE);
/** Separate tessellator for the splash thread. Nulled after splash completes. */
private static DirectTessellator splashTessellator;
private record ThreadTessellator(Thread thread, DirectTessellator tessellator) {}
private static final Queue<ThreadTessellator> threadTessellators = new ConcurrentLinkedQueue<>();
private static final ThreadLocal<DirectTessellator> threadTessellator = ThreadLocal.withInitial(() -> {
final DirectTessellator t = new DirectTessellator(TessellatorManager.DEFAULT_BUFFER_SIZE);
threadTessellators.add(new ThreadTessellator(Thread.currentThread(), t));
cleanupOrphanTessellators();
return t;
});
private ImmediateModeRecorder() {
}
public static void initSplashTessellator() {
splashTessellator = new DirectTessellator(TessellatorManager.DEFAULT_BUFFER_SIZE);
public static int getThreadTessellatorCount() {
return threadTessellators.size();
}
public static void destroySplashTessellator() {
splashTessellator = null;
public static synchronized void cleanupOrphanTessellators() {
threadTessellators.removeIf(tt -> {
if (tt.thread().isAlive()) return false;
tt.tessellator().delete();
return true;
});
}
private static DirectTessellator tessellator() {
final DirectTessellator splash = splashTessellator;
if (splash != null && Thread.currentThread() != GLStateManager.getMainThread()) {
return splash;
if (Thread.currentThread() == GLStateManager.getMainThread()) {
return tessellator;
}
return tessellator;
return threadTessellator.get();
}
public static DirectTessellator getInternalTessellator() {
@@ -167,19 +176,14 @@ public final class ImmediateModeRecorder {
.mapToInt(Usage::getAttributeLocation)
.max().orElse(0);
private static final ByteBuffer[] scratchAttribBuffers = new ByteBuffer[NUM_ATTRIBS];
private static final long[] scratchAttribBaseOffsets = new long[NUM_ATTRIBS];
private static final int[] scratchVBOIds = new int[NUM_ATTRIBS];
private static final ByteBuffer[] scratchVBOBuffers = new ByteBuffer[NUM_ATTRIBS];
private static final long[] scratchVBOReadOffsets = new long[NUM_ATTRIBS];
private static final class AttribReader {
ByteBuffer data;
int stride;
int baseOffset;
int size;
int loc;
VertexAttribState.Attrib attrib;
int type;
boolean normalized;
}
private static final AttribReader[] readers = new AttribReader[NUM_ATTRIBS];
@@ -198,167 +202,32 @@ public final class ImmediateModeRecorder {
* Reads vertex data from VBOs or client memory referenced by VertexAttribState and converts to immediate mode geometry.
*/
public static DirectTessellator processDrawArraysFromAttribs(int mode, int first, int count) {
final int readVBOCount = readbackAttribVBOs(first, count);
final AttribSnapshot snap = AttribSnapshot.snapshot(first, count);
try {
prepareReaders();
prepareReaders(snap);
if (positionReaderIndex < 0) return null;
return readVerticesFromBuffer(mode, first, count);
} finally {
freeReadbackBuffers(readVBOCount);
snap.free();
}
}
/**
* Process a glDrawElements call during display list recording when a VBO is bound.
*/
public static DirectTessellator processDrawElementsFromVBO(int mode, int indicesCount, int indexType, long indicesOffset, int eboId) {
if (eboId == 0) return null;
final int readVBOCount = readbackAttribVBOs(-1, 0);
try {
prepareReaders();
if (positionReaderIndex < 0) return null;
final int prevEBO = GLStateManager.getBoundEBO();
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, eboId);
final int eboSize = RENDER_BACKEND.getBufferParameteri(GL15.GL_ELEMENT_ARRAY_BUFFER, GL15.GL_BUFFER_SIZE);
if (eboSize <= 0) {
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, prevEBO);
return null;
}
final int indexSize = VertexAttribState.Attrib.glTypeSizeBytes(indexType);
final int eboReadOffset = (int) indicesOffset;
final int eboReadSize = indicesCount * indexSize;
if (eboReadOffset + eboReadSize > eboSize) {
GLStateManager.LOGGER.warn("[EBO] Read range {}+{} exceeds buffer size {}", eboReadOffset, eboReadSize, eboSize);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, prevEBO);
return null;
}
final ByteBuffer eboData = MemoryUtilities.memAlloc(eboReadSize);
RENDER_BACKEND.getBufferSubData(GL15.GL_ELEMENT_ARRAY_BUFFER, eboReadOffset, eboData);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, prevEBO);
try {
return readVerticesFromBufferIndexed(mode, eboData, indexType, 0, indicesCount);
} finally {
MemoryUtilities.memFree(eboData);
}
} finally {
freeReadbackBuffers(readVBOCount);
}
}
/**
* Read data for all enabled attribs into scratchAttribBuffers.
* For VBO-backed attribs, reads VBO data. For client-memory attribs, uses the client pointer directly.
*/
private static int readbackAttribVBOs(int firstVertex, int vertexCount) {
int readVBOCount = 0;
final int prevVBO = GLStateManager.getBoundVBO();
final boolean partialRead = firstVertex >= 0;
for (int i = 0; i < NUM_ATTRIBS; i++) {
scratchAttribBuffers[i] = null;
scratchAttribBaseOffsets[i] = 0;
final VertexAttribState.Attrib a = VertexAttribState.get(i);
if (!a.enabled) continue;
if (a.vboId != 0) {
// VBO-backed attrib — read from GPU
ByteBuffer existing = null;
int existingIdx = -1;
for (int j = 0; j < readVBOCount; j++) {
if (scratchVBOIds[j] == a.vboId) {
existing = scratchVBOBuffers[j];
existingIdx = j;
break;
}
}
if (existing != null) {
scratchAttribBuffers[i] = existing;
scratchAttribBaseOffsets[i] = scratchVBOReadOffsets[existingIdx];
} else {
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, a.vboId);
final int bufferSize = RENDER_BACKEND.getBufferParameteri(GL15.GL_ARRAY_BUFFER, GL15.GL_BUFFER_SIZE);
if (bufferSize <= 0) {
GLStateManager.LOGGER.warn("[VBO Readback] GL_BUFFER_SIZE={} for VBO {}", bufferSize, a.vboId);
continue;
}
final long readOffset;
final int readSize;
if (partialRead) {
final int stride = a.effectiveStride();
long rangeStart = a.offset + (long) firstVertex * stride;
long rangeEnd = a.offset + (long) (firstVertex + vertexCount - 1) * stride + (long) a.size * a.typeSizeBytes();
for (int k = 0; k < NUM_ATTRIBS; k++) {
if (k == i) continue;
final VertexAttribState.Attrib ak = VertexAttribState.get(k);
if (!ak.enabled || ak.vboId != a.vboId) continue;
final int ks = ak.effectiveStride();
rangeStart = Math.min(rangeStart, ak.offset + (long) firstVertex * ks);
rangeEnd = Math.max(rangeEnd, ak.offset + (long) (firstVertex + vertexCount - 1) * ks
+ (long) ak.size * ak.typeSizeBytes());
}
rangeStart = Math.max(0, rangeStart);
rangeEnd = Math.min(bufferSize, rangeEnd);
readOffset = rangeStart;
readSize = (int) (rangeEnd - rangeStart);
} else {
readOffset = 0;
readSize = bufferSize;
}
if (readSize <= 0) continue;
final ByteBuffer buf = MemoryUtilities.memAlloc(readSize);
RENDER_BACKEND.getBufferSubData(GL15.GL_ARRAY_BUFFER, readOffset, buf);
scratchAttribBuffers[i] = buf;
scratchAttribBaseOffsets[i] = readOffset;
scratchVBOIds[readVBOCount] = a.vboId;
scratchVBOReadOffsets[readVBOCount] = readOffset;
scratchVBOBuffers[readVBOCount] = buf;
readVBOCount++;
}
} else if (a.clientPointer != null) {
// Client-memory attrib — use pointer directly (no allocation needed)
scratchAttribBuffers[i] = a.clientPointer;
}
}
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, prevVBO);
return readVBOCount;
}
private static void freeReadbackBuffers(int readVBOCount) {
for (int i = 0; i < readVBOCount; i++) {
MemoryUtilities.memFree(scratchVBOBuffers[i]);
scratchVBOBuffers[i] = null;
}
}
private static void prepareReaders() {
private static void prepareReaders(AttribSnapshot snap) {
nonPositionCount = 0;
positionReaderIndex = -1;
for (int i = 0; i < NUM_ATTRIBS; i++) {
final ByteBuffer buf = scratchAttribBuffers[i];
if (buf == null) continue;
final VertexAttribState.Attrib a = VertexAttribState.get(i);
final AttribSnapshot.AttribDesc d = snap.get(i);
if (d == null) continue;
final AttribReader r = readers[i];
r.data = buf;
r.stride = a.effectiveStride();
r.baseOffset = (int) (a.offset - scratchAttribBaseOffsets[i]);
r.size = a.size;
r.data = d.readBuffer();
r.stride = d.effectiveStride();
r.baseOffset = (int) (d.offset() - d.readBufferBaseOffset());
r.size = d.size();
r.loc = i;
r.attrib = a;
r.type = d.type();
r.normalized = d.normalized();
if (i == LOC_POSITION) {
positionReaderIndex = i;
@@ -379,18 +248,6 @@ public final class ImmediateModeRecorder {
return end();
}
private static DirectTessellator readVerticesFromBufferIndexed(int mode, ByteBuffer eboData, int indexType, int eboOffset, int indicesCount) {
begin(mode);
final DirectTessellator t = tessellator();
for (int i = 0; i < indicesCount; i++) {
final int index = readIndex(eboData, indexType, eboOffset, i);
readVertexFromBuffer(t, index);
}
return end();
}
public static int readIndex(ByteBuffer ebo, int indexType, int eboOffset, int i) {
return switch (indexType) {
case GL11.GL_UNSIGNED_BYTE -> ebo.get(eboOffset + i) & 0xFF;
@@ -411,39 +268,75 @@ public final class ImmediateModeRecorder {
final AttribReader r = readers[positionReaderIndex];
final int base = r.baseOffset + vertexIndex * r.stride;
t.addVertex(
r.attrib.readComponent(r.data, base, 0),
r.attrib.readComponent(r.data, base, 1),
(r.size >= 3) ? r.attrib.readComponent(r.data, base, 2) : 0.0f);
readComponent(r, base, 0),
readComponent(r, base, 1),
(r.size >= 3) ? readComponent(r, base, 2) : 0.0f);
}
}
private static void emitAttrib(DirectTessellator t, AttribReader r, int base) {
if (r.loc == LOC_NORMAL) {
t.setNormal(
r.attrib.readComponent(r.data, base, 0),
r.attrib.readComponent(r.data, base, 1),
r.attrib.readComponent(r.data, base, 2));
readComponent(r, base, 0),
readComponent(r, base, 1),
readComponent(r, base, 2));
} else if (r.loc == LOC_UV0) {
t.setTextureUV(
r.attrib.readComponent(r.data, base, 0),
(r.size >= 2) ? r.attrib.readComponent(r.data, base, 1) : 0.0f);
readComponent(r, base, 0),
(r.size >= 2) ? readComponent(r, base, 1) : 0.0f);
} else if (r.loc == LOC_UV1) {
if (r.attrib.type == GL11.GL_SHORT) {
if (r.type == GL11.GL_SHORT) {
final int s = r.data.getShort(base) & 0xFFFF;
final int u = (r.size >= 2) ? (r.data.getShort(base + 2) & 0xFFFF) : 0;
t.setBrightness((u << 16) | s);
} else if (r.attrib.type == GL11.GL_FLOAT) {
} else if (r.type == GL11.GL_FLOAT) {
final int s = (int) r.data.getFloat(base);
final int u = (r.size >= 2) ? (int) r.data.getFloat(base + 4) : 0;
t.setBrightness((u << 16) | (s & 0xFFFF));
}
} else if (r.loc == LOC_COLOR) {
final float cr = r.attrib.readComponent(r.data, base, 0);
final float cg = r.attrib.readComponent(r.data, base, 1);
final float cb = r.attrib.readComponent(r.data, base, 2);
final float ca = (r.size >= 4) ? r.attrib.readComponent(r.data, base, 3) : 1.0f;
final float cr = readComponent(r, base, 0);
final float cg = readComponent(r, base, 1);
final float cb = readComponent(r, base, 2);
final float ca = (r.size >= 4) ? readComponent(r, base, 3) : 1.0f;
t.setColorRGBA((int) (cr * 255.0f), (int) (cg * 255.0f), (int) (cb * 255.0f), (int) (ca * 255.0f));
}
}
private static float readComponent(AttribReader r, int base, int component) {
return VAOManager.Attrib.readComponent(r.type, r.normalized, r.data, base, component);
}
public static float readComponent(int type, boolean normalized, ByteBuffer buf, int base, int component) {
return switch (type) {
case GL11.GL_FLOAT -> buf.getFloat(base + component * 4);
case GL11.GL_DOUBLE -> (float) buf.getDouble(base + component * 8);
case GL11.GL_INT -> {
final int v = buf.getInt(base + component * 4);
yield normalized ? v / (float) Integer.MAX_VALUE : (float) v;
}
case GL11.GL_UNSIGNED_INT -> {
final int v = buf.getInt(base + component * 4);
yield normalized ? (v & 0xFFFFFFFFL) / (float) 0xFFFFFFFFL : (float) (v & 0xFFFFFFFFL);
}
case GL11.GL_SHORT -> {
final short v = buf.getShort(base + component * 2);
yield normalized ? v / (float) Short.MAX_VALUE : (float) v;
}
case GL11.GL_UNSIGNED_SHORT -> {
final int v = buf.getShort(base + component * 2) & 0xFFFF;
yield normalized ? v / (float) 0xFFFF : (float) v;
}
case GL11.GL_BYTE -> {
final byte v = buf.get(base + component);
yield normalized ? v / 127.0f : (float) v;
}
case GL11.GL_UNSIGNED_BYTE -> {
final int v = buf.get(base + component) & 0xFF;
yield normalized ? v / 255.0f : (float) v;
}
default -> 0.0f;
};
}
}
@@ -0,0 +1,64 @@
package com.gtnewhorizons.angelica.glsm.recording.commands;
import com.gtnewhorizons.angelica.glsm.ffp.VAOManager;
import java.util.Arrays;
public final class AttribLayoutKey {
final int[] locations;
final int[] sizes;
final int[] types;
final boolean[] normalized;
private final int[] offsets;
private final int stride;
private final int hash;
public AttribLayoutKey(int[] locations, int[] sizes, int[] types, boolean[] normalized) {
this.locations = locations;
this.sizes = sizes;
this.types = types;
this.normalized = normalized;
final int n = locations.length;
final int[] off = new int[n];
int base = 0;
int maxAlign = 1;
for (int i = 0; i < n; i++) {
final int typeSize = VAOManager.Attrib.glTypeSizeBytes(types[i]);
if (typeSize > maxAlign) maxAlign = typeSize;
base = (base + typeSize - 1) & -typeSize;
off[i] = base;
base += sizes[i] * typeSize;
}
this.offsets = off;
this.stride = (base + maxAlign - 1) & -maxAlign;
int h = Arrays.hashCode(locations);
h = 31 * h + Arrays.hashCode(sizes);
h = 31 * h + Arrays.hashCode(types);
h = 31 * h + Arrays.hashCode(normalized);
this.hash = h;
}
public int offset(int i) {
return offsets[i];
}
public int stride() {
return stride;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof AttribLayoutKey k)) return false;
return Arrays.equals(locations, k.locations)
&& Arrays.equals(sizes, k.sizes)
&& Arrays.equals(types, k.types)
&& Arrays.equals(normalized, k.normalized);
}
@Override
public int hashCode() {
return hash;
}
}
@@ -0,0 +1,37 @@
package com.gtnewhorizons.angelica.glsm.recording.commands;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
public final class BatchedIndexedDrawCmd implements DisplayListCommand {
private int sharedVAO;
private int drawMode;
private int indexCount;
private int indexType;
private long indexByteOffset;
public void fill(int sharedVAO, int drawMode, int indexCount, int indexType, long indexByteOffset) {
this.sharedVAO = sharedVAO;
this.drawMode = drawMode;
this.indexCount = indexCount;
this.indexType = indexType;
this.indexByteOffset = indexByteOffset;
}
public int getSharedVAO() { return sharedVAO; }
public int getDrawMode() { return drawMode; }
public int getIndexCount() { return indexCount; }
public int getIndexType() { return indexType; }
public long getIndexByteOffset() { return indexByteOffset; }
@Override
public void execute() {
if (sharedVAO == 0) return;
GLStateManager.glBindVertexArray(sharedVAO);
GLStateManager.glDrawElements(drawMode, indexCount, indexType, indexByteOffset);
}
@Override
public String toString() {
return "BatchedIndexedDrawCmd(vao=" + sharedVAO + ", mode=" + drawMode + ", count=" + indexCount + ", type=" + indexType + ", offset=" + indexByteOffset + ")";
}
}
@@ -0,0 +1,34 @@
package com.gtnewhorizons.angelica.glsm.recording.commands;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
public final class IndexedDrawBatch {
private int sharedVAO;
private int sharedVBO;
private int sharedEBO;
private boolean deleted;
public IndexedDrawBatch(int sharedVAO, int sharedVBO, int sharedEBO) {
this.sharedVAO = sharedVAO;
this.sharedVBO = sharedVBO;
this.sharedEBO = sharedEBO;
}
public int getSharedVAO() { return sharedVAO; }
public int getSharedVBO() { return sharedVBO; }
public int getSharedEBO() { return sharedEBO; }
public void delete() {
if (deleted) return;
deleted = true;
if (sharedVAO != 0) GLStateManager.glDeleteVertexArrays(sharedVAO);
if (sharedVBO != 0) GLStateManager.glDeleteBuffers(sharedVBO);
if (sharedEBO != 0) GLStateManager.glDeleteBuffers(sharedEBO);
sharedVAO = sharedVBO = sharedEBO = 0;
}
@Override
public String toString() {
return "IndexedDrawBatch(vao=" + sharedVAO + ", vbo=" + sharedVBO + ", ebo=" + sharedEBO + ")";
}
}
@@ -0,0 +1,171 @@
package com.gtnewhorizons.angelica.glsm.recording.commands;
import com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAddress0;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memCopy;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memGetInt;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memPutInt;
public final class IndexedDrawBatchBuilder {
private final List<IndexedDrawCapture> captures = new ArrayList<>();
public boolean isEmpty() {
return captures.isEmpty();
}
public void add(IndexedDrawCapture capture) {
captures.add(capture);
}
public List<IndexedDrawCapture> getCaptures() {
return captures;
}
public List<IndexedDrawBatch> build() {
final List<IndexedDrawBatch> out = new ArrayList<>();
if (captures.isEmpty()) return out;
final int prevVAO = GLStateManager.getBoundVAO();
final int prevVBO = GLStateManager.getBoundVBO();
final int prevEBO = GLStateManager.getBoundEBO();
final List<GroupResult> results = new ArrayList<>();
boolean success = false;
try {
final Object2ObjectLinkedOpenHashMap<AttribLayoutKey, List<IndexedDrawCapture>> groups = new Object2ObjectLinkedOpenHashMap<>();
for (IndexedDrawCapture c : captures) {
groups.computeIfAbsent(c.layoutKey, k -> new ArrayList<>()).add(c);
}
for (List<IndexedDrawCapture> group : groups.values()) {
results.add(buildGroup(group));
}
// Placeholder fill is deferred until every group's GL work has succeeded.
for (GroupResult r : results) r.commit();
for (GroupResult r : results) out.add(r.batch);
success = true;
} finally {
if (!success) {
for (GroupResult r : results) r.batch.delete();
}
GLStateManager.glBindVertexArray(prevVAO);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, prevEBO);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, prevVBO);
}
return out;
}
private record GroupResult(IndexedDrawBatch batch, List<IndexedDrawCapture> captures,
int[] drawModes, int[] indexCounts, long[] indexOffsets) {
void commit() {
final int sharedVAO = batch.getSharedVAO();
for (int i = 0; i < captures.size(); i++) {
captures.get(i).placeholder.fill(sharedVAO, drawModes[i], indexCounts[i], GL11.GL_UNSIGNED_INT, indexOffsets[i]);
}
}
}
private static GroupResult buildGroup(List<IndexedDrawCapture> group) {
long totalVertexBytes = 0;
long totalIndexBytes = 0;
for (IndexedDrawCapture c : group) {
totalVertexBytes += (long) c.vertexCount * c.layoutKey.stride();
totalIndexBytes += (long) c.indexCount * 4; // always GL_UNSIGNED_INT
}
if (totalVertexBytes > Integer.MAX_VALUE || totalIndexBytes > Integer.MAX_VALUE) {
throw new IllegalStateException("batch exceeds 2GiB — split needed");
}
int sharedVAO = 0;
int sharedVBO = 0;
int sharedEBO = 0;
ByteBuffer eboData = null;
boolean success = false;
final int n = group.size();
final int[] drawModes = new int[n];
final int[] indexCounts = new int[n];
final long[] indexOffsets = new long[n];
try {
sharedVAO = GLStateManager.glGenVertexArrays();
sharedVBO = GLStateManager.glGenBuffers();
sharedEBO = GLStateManager.glGenBuffers();
eboData = MemoryUtilities.memAlloc((int) totalIndexBytes);
GLStateManager.glBindVertexArray(sharedVAO);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, sharedVBO);
GLStateManager.glBufferData(GL15.GL_ARRAY_BUFFER, totalVertexBytes, GL15.GL_STATIC_DRAW);
int vboBytePos = 0;
int eboBytePos = 0;
int vertexBase = 0;
final long eboAddr = memAddress0(eboData);
for (int g = 0; g < n; g++) {
final IndexedDrawCapture c = group.get(g);
final int vBytes = c.vertexCount * c.layoutKey.stride();
GLStateManager.glBufferSubData(GL15.GL_ARRAY_BUFFER, vboBytePos, c.vertexData);
final long srcIdxAddr = memAddress0(c.indexData);
final int idxBytes = c.indexCount * 4;
if (vertexBase == 0) {
// First capture (or a group that starts at 0) — indices are already
// rebased to [0..vertexCount) in IndexedDrawCapture, so a raw bulk copy
// is correct and strictly faster than the per-index loop.
memCopy(srcIdxAddr, eboAddr + eboBytePos, idxBytes);
} else {
final long dstBase = eboAddr + eboBytePos;
for (int i = 0; i < c.indexCount; i++) {
memPutInt(dstBase + i * 4L, memGetInt(srcIdxAddr + i * 4L) + vertexBase);
}
}
drawModes[g] = c.drawMode;
indexCounts[g] = c.indexCount;
indexOffsets[g] = eboBytePos;
vboBytePos += vBytes;
eboBytePos += idxBytes;
vertexBase += c.vertexCount;
}
eboData.position(0).limit((int) totalIndexBytes);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, sharedEBO);
GLStateManager.glBufferData(GL15.GL_ELEMENT_ARRAY_BUFFER, eboData, GL15.GL_STATIC_DRAW);
final AttribLayoutKey layout = group.get(0).layoutKey;
final int stride = layout.stride();
for (int i = 0; i < layout.locations.length; i++) {
final int loc = layout.locations[i];
final int size = layout.sizes[i];
final int type = layout.types[i];
GLStateManager.glVertexAttribPointer(loc, size, type, layout.normalized[i], stride, layout.offset(i));
GLStateManager.glEnableVertexAttribArray(loc);
}
success = true;
return new GroupResult(new IndexedDrawBatch(sharedVAO, sharedVBO, sharedEBO),
group, drawModes, indexCounts, indexOffsets);
} finally {
if (eboData != null) MemoryUtilities.memFree(eboData);
if (!success) {
if (sharedVAO != 0) GLStateManager.glDeleteVertexArrays(sharedVAO);
if (sharedVBO != 0) GLStateManager.glDeleteBuffers(sharedVBO);
if (sharedEBO != 0) GLStateManager.glDeleteBuffers(sharedEBO);
}
}
}
}
@@ -0,0 +1,262 @@
package com.gtnewhorizons.angelica.glsm.recording.commands;
import com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.QuadConverter;
import com.gtnewhorizons.angelica.glsm.ffp.VAOManager;
import com.gtnewhorizons.angelica.glsm.recording.AttribSnapshot;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
import java.nio.ByteBuffer;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memGetInt;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memGetLong;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memPutInt;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memPutLong;
/** {@link #create} returns null on any recoverable validation failure; callers must not record the placeholder. */
public final class IndexedDrawCapture {
public final AttribLayoutKey layoutKey;
public final int drawMode;
public final int vertexCount;
public final int indexCount;
public final ByteBuffer vertexData;
public final ByteBuffer indexData;
public final BatchedIndexedDrawCmd placeholder;
private boolean freed;
public IndexedDrawCapture(AttribLayoutKey layoutKey, int drawMode, int vertexCount, int indexCount,
ByteBuffer vertexData, ByteBuffer indexData,
BatchedIndexedDrawCmd placeholder) {
this.layoutKey = layoutKey;
this.drawMode = drawMode;
this.vertexCount = vertexCount;
this.indexCount = indexCount;
this.vertexData = vertexData;
this.indexData = indexData;
this.placeholder = placeholder;
}
public void freeBuffers() {
if (freed) return;
freed = true;
if (vertexData != null) MemoryUtilities.memFree(vertexData);
if (indexData != null) MemoryUtilities.memFree(indexData);
}
public boolean isFreed() {
return freed;
}
public static IndexedDrawCapture create(int mode, int indicesCount, int srcIndexType, long indicesOffset, int eboId) {
if (indicesCount == 0) return null;
if (eboId == 0) {
GLStateManager.warnOnce("no-ebo", "[IndexedDrawCapture] glDrawElements in display list with no EBO bound — skipping (mode={} count={} offset={})", mode, indicesCount, indicesOffset);
return null;
}
if (mode == GL11.GL_QUADS && (indicesCount % 4) != 0) {
GLStateManager.warnOnce("quads-misaligned", "[IndexedDrawCapture] GL_QUADS with indicesCount={} not a multiple of 4 — skipping", indicesCount);
return null;
}
final int srcIndexSize = VAOManager.Attrib.glTypeSizeBytes(srcIndexType);
if (srcIndexSize <= 0) return null;
final int prevEBO = GLStateManager.getBoundEBO();
final int prevVBO = GLStateManager.getBoundVBO();
ByteBuffer eboSrc = null;
try {
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, eboId);
final int eboBufferSize = GLStateManager.glGetBufferParameteri(GL15.GL_ELEMENT_ARRAY_BUFFER, GL15.GL_BUFFER_SIZE);
if (eboBufferSize <= 0) {
GLStateManager.warnOnce("ebo-size", "[IndexedDrawCapture] GL_BUFFER_SIZE={} for EBO {}", eboBufferSize, eboId);
return null;
}
final long eboReadSize = (long) indicesCount * srcIndexSize;
if (indicesOffset < 0 || indicesOffset + eboReadSize > eboBufferSize) {
GLStateManager.warnOnce("ebo-range", "[IndexedDrawCapture] EBO range {}+{} exceeds buffer size {} — skipping", indicesOffset, eboReadSize, eboBufferSize);
return null;
}
eboSrc = MemoryUtilities.memAlloc((int) eboReadSize);
GLStateManager.glGetBufferSubData(GL15.GL_ELEMENT_ARRAY_BUFFER, indicesOffset, eboSrc);
return bake(mode, indicesCount, srcIndexType, MemoryUtilities.memAddress0(eboSrc));
} finally {
if (eboSrc != null) MemoryUtilities.memFree(eboSrc);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, prevEBO);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, prevVBO);
}
}
public static IndexedDrawCapture createFromClientIndices(int mode, int indicesCount, int srcIndexType, long indexSrcAddr, long srcByteLength) {
if (indicesCount == 0) return null;
if (indexSrcAddr == 0L) return null;
if (mode == GL11.GL_QUADS && (indicesCount % 4) != 0) {
GLStateManager.warnOnce("quads-misaligned", "[IndexedDrawCapture] GL_QUADS with indicesCount={} not a multiple of 4 - skipping", indicesCount);
return null;
}
final int idxSize = VAOManager.Attrib.glTypeSizeBytes(srcIndexType);
if (idxSize <= 0) return null;
if ((long) indicesCount * idxSize > srcByteLength) {
GLStateManager.warnOnce("client-idx-range", "[IndexedDrawCapture] index count {} ({} bytes) exceeds client buffer {} bytes - skipping", indicesCount, (long) indicesCount * idxSize, srcByteLength);
return null;
}
return bake(mode, indicesCount, srcIndexType, indexSrcAddr);
}
private static IndexedDrawCapture bake(int mode, int indicesCount, int srcIndexType, long indexSrcAddr) {
AttribSnapshot snap = null;
ByteBuffer vertexData = null;
ByteBuffer indexData = null;
boolean success = false;
try {
final long minMax = QuadConverter.scanMinMaxIndex(indexSrcAddr, srcIndexType, indicesCount);
if (minMax == -1L) {
GLStateManager.warnOnce("min-max", "[IndexedDrawCapture] failed to determine min/max vertex for indexType={}", srcIndexType);
return null;
}
final int minVtx = (int) (minMax & 0xFFFFFFFFL);
final int maxVtx = (int) (minMax >>> 32);
final int vertexCount = maxVtx - minVtx + 1;
snap = AttribSnapshot.snapshot(minVtx, vertexCount);
int enabledCount = 0;
for (int i = 0; i < VAOManager.MAX_ATTRIBS; i++) {
if (snap.get(i) != null) enabledCount++;
}
if (enabledCount == 0) {
GLStateManager.warnOnce("no-attribs", "[IndexedDrawCapture] no enabled attributes — skipping");
return null;
}
final int[] locations = new int[enabledCount];
final int[] sizes = new int[enabledCount];
final int[] types = new int[enabledCount];
final boolean[] normalized = new boolean[enabledCount];
int k = 0;
for (int i = 0; i < VAOManager.MAX_ATTRIBS; i++) {
final AttribSnapshot.AttribDesc d = snap.get(i);
if (d == null) continue;
locations[k] = i;
sizes[k] = d.size();
types[k] = d.type();
normalized[k] = d.normalized();
k++;
}
final AttribLayoutKey layoutKey = new AttribLayoutKey(locations, sizes, types, normalized);
final int tightStride = layoutKey.stride();
ByteBuffer sharedSrcBuffer = null;
long sharedSrcBase = 0;
int sharedSrcStride = -1;
boolean tightSrc = true;
for (int a = 0; a < enabledCount; a++) {
final AttribSnapshot.AttribDesc d = snap.get(locations[a]);
final int relOffset = (int) (d.offset() - d.readBufferBaseOffset());
final long readStart = relOffset + (long) minVtx * d.effectiveStride();
final long readEnd = relOffset + (long) maxVtx * d.effectiveStride() + (long) d.size() * d.typeSizeBytes();
if (readStart < 0 || readEnd > d.readBuffer().capacity()) {
GLStateManager.warnOnce("vtx-oob", "[IndexedDrawCapture] attrib {} read extent {} exceeds snapshot {} bytes - skipping", d.location(), readEnd, d.readBuffer().capacity());
return null;
}
if (a == 0) {
sharedSrcBuffer = d.readBuffer();
sharedSrcStride = d.effectiveStride();
sharedSrcBase = MemoryUtilities.memAddress0(sharedSrcBuffer) + relOffset + (long) minVtx * sharedSrcStride;
if (relOffset != layoutKey.offset(0)) tightSrc = false;
} else if (d.readBuffer() != sharedSrcBuffer || d.effectiveStride() != sharedSrcStride || relOffset != layoutKey.offset(a)) {
tightSrc = false;
}
}
if (tightSrc && sharedSrcStride != tightStride) tightSrc = false;
vertexData = MemoryUtilities.memAlloc(vertexCount * tightStride);
final long dstAddr = MemoryUtilities.memAddress0(vertexData);
if (tightSrc) {
MemoryUtilities.memCopy(sharedSrcBase, dstAddr, (long) vertexCount * tightStride);
} else {
for (int a = 0; a < enabledCount; a++) {
final AttribSnapshot.AttribDesc d = snap.get(locations[a]);
final int attribBytes = d.size() * d.typeSizeBytes();
final int srcStride = d.effectiveStride();
final long srcStart = MemoryUtilities.memAddress0(d.readBuffer()) + (d.offset() - d.readBufferBaseOffset()) + (long) minVtx * srcStride;
final long dstStart = dstAddr + layoutKey.offset(a);
copyStrided(srcStart, srcStride, dstStart, tightStride, attribBytes, vertexCount);
}
}
final int bakedDrawMode;
final int bakedIndexCount;
if (mode == GL11.GL_QUADS) {
final int quadCount = indicesCount / 4;
bakedIndexCount = quadCount * 6;
indexData = MemoryUtilities.memAlloc(bakedIndexCount * 4);
QuadConverter.triangulateQuads(indexSrcAddr, srcIndexType, MemoryUtilities.memAddress0(indexData), GL11.GL_UNSIGNED_INT, quadCount, minVtx);
bakedDrawMode = GL11.GL_TRIANGLES;
} else {
bakedIndexCount = indicesCount;
indexData = MemoryUtilities.memAlloc(bakedIndexCount * 4);
QuadConverter.widenIndices(indexSrcAddr, srcIndexType, MemoryUtilities.memAddress0(indexData), GL11.GL_UNSIGNED_INT, indicesCount, minVtx);
bakedDrawMode = mode;
}
final BatchedIndexedDrawCmd placeholder = new BatchedIndexedDrawCmd();
final IndexedDrawCapture out = new IndexedDrawCapture(layoutKey, bakedDrawMode, vertexCount, bakedIndexCount, vertexData, indexData, placeholder);
success = true;
return out;
} finally {
if (snap != null) snap.free();
if (!success) {
if (vertexData != null) MemoryUtilities.memFree(vertexData);
if (indexData != null) MemoryUtilities.memFree(indexData);
}
}
}
/**
* Copy {@code attribBytes} per vertex from {@code src} (stride {@code srcStride}) to
* {@code dst} (stride {@code dstStride}) for {@code vertexCount} vertices.
*
* <p>Specializes on the common attribute sizes (4 / 8 / 12 / 16) so the hot path is
* one or two {@code memPut} calls per vertex instead of a JNI-ish {@code memCopy}
* dispatch. Falls through to {@code memCopy} for exotic sizes.
*/
private static void copyStrided(long src, int srcStride, long dst, int dstStride, int attribBytes, int vertexCount) {
switch (attribBytes) {
case 4 -> {
for (int v = 0; v < vertexCount; v++) {
memPutInt(dst, memGetInt(src));
src += srcStride; dst += dstStride;
}
}
case 8 -> {
for (int v = 0; v < vertexCount; v++) {
memPutLong(dst, memGetLong(src));
src += srcStride; dst += dstStride;
}
}
case 12 -> {
for (int v = 0; v < vertexCount; v++) {
memPutLong(dst, memGetLong(src));
memPutInt(dst + 8, memGetInt(src + 8));
src += srcStride; dst += dstStride;
}
}
case 16 -> {
for (int v = 0; v < vertexCount; v++) {
memPutLong(dst, memGetLong(src));
memPutLong(dst + 8, memGetLong(src + 8));
src += srcStride; dst += dstStride;
}
}
default -> {
for (int v = 0; v < vertexCount; v++) {
MemoryUtilities.memCopy(src, dst, attribBytes);
src += srcStride; dst += dstStride;
}
}
}
}
}
@@ -0,0 +1,52 @@
package com.gtnewhorizons.angelica.glsm.recording.commands;
import org.jetbrains.annotations.Nullable;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.DoubleBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAlloc;
final class PixelDataSnapshot {
private PixelDataSnapshot() {}
static @Nullable ByteBuffer copy(@Nullable ByteBuffer pixels) {
if (pixels == null) return null;
final int pos = pixels.position();
final ByteBuffer out = memAlloc(pixels.remaining()).order(ByteOrder.nativeOrder());
out.put(pixels);
out.flip();
pixels.position(pos);
return out;
}
static @Nullable ByteBuffer copy(@Nullable IntBuffer pixels) {
if (pixels == null) return null;
final int pos = pixels.position();
final ByteBuffer out = memAlloc(pixels.remaining() * 4).order(ByteOrder.nativeOrder());
out.asIntBuffer().put(pixels);
pixels.position(pos);
return out;
}
static @Nullable ByteBuffer copy(@Nullable FloatBuffer pixels) {
if (pixels == null) return null;
final int pos = pixels.position();
final ByteBuffer out = memAlloc(pixels.remaining() * 4).order(ByteOrder.nativeOrder());
out.asFloatBuffer().put(pixels);
pixels.position(pos);
return out;
}
static @Nullable ByteBuffer copy(@Nullable DoubleBuffer pixels) {
if (pixels == null) return null;
final int pos = pixels.position();
final ByteBuffer out = memAlloc(pixels.remaining() * 8).order(ByteOrder.nativeOrder());
out.asDoubleBuffer().put(pixels);
pixels.position(pos);
return out;
}
}
@@ -2,15 +2,17 @@ package com.gtnewhorizons.angelica.glsm.recording.commands;
import com.gtnewhorizons.angelica.glsm.GLDebug;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.states.PixelUnpackState;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.DoubleBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memFree;
/**
* Command: glTexImage2D(target, level, internalformat, width, height, border, format, type, pixels)
* Specifies a two-dimensional texture image during display list playback.
@@ -24,92 +26,58 @@ public record TexImage2DCmd(
int border,
int format,
int type,
@Nullable byte[] pixelData
@Nullable ByteBuffer pixels,
@Nullable PixelUnpackState unpack
) implements DisplayListCommand {
/**
* Create command from IntBuffer
*/
public static TexImage2DCmd fromIntBuffer(int target, int level, int internalformat, int width, int height, int border, int format, int type, @Nullable IntBuffer pixels) {
byte[] data = null;
if (pixels != null) {
int pos = pixels.position();
int limit = pixels.limit();
int size = (limit - pos) * 4; // 4 bytes per int
data = new byte[size];
ByteBuffer bb = ByteBuffer.wrap(data).order(ByteOrder.nativeOrder());
bb.asIntBuffer().put(pixels);
pixels.position(pos); // Restore position
public TexImage2DCmd {
if (pixels == null) {
unpack = null;
}
return new TexImage2DCmd(target, level, internalformat, width, height, border, format, type, data);
}
/**
* Create command from FloatBuffer
*/
public static TexImage2DCmd fromFloatBuffer(int target, int level, int internalformat, int width, int height, int border, int format, int type, @Nullable FloatBuffer pixels) {
byte[] data = null;
if (pixels != null) {
int pos = pixels.position();
int limit = pixels.limit();
int size = (limit - pos) * 4; // 4 bytes per float
data = new byte[size];
ByteBuffer bb = ByteBuffer.wrap(data).order(ByteOrder.nativeOrder());
bb.asFloatBuffer().put(pixels);
pixels.position(pos); // Restore position
}
return new TexImage2DCmd(target, level, internalformat, width, height, border, format, type, data);
public static TexImage2DCmd fromIntBuffer(int target, int level, int internalformat, int width, int height, int border, int format, int type, @Nullable IntBuffer pixels, PixelUnpackState unpack) {
return new TexImage2DCmd(target, level, internalformat, width, height, border, format, type, PixelDataSnapshot.copy(pixels), unpack);
}
/**
* Create command from DoubleBuffer
*/
public static TexImage2DCmd fromDoubleBuffer(int target, int level, int internalformat, int width, int height, int border, int format, int type, @Nullable DoubleBuffer pixels) {
byte[] data = null;
if (pixels != null) {
int pos = pixels.position();
int limit = pixels.limit();
int size = (limit - pos) * 8; // 8 bytes per double
data = new byte[size];
ByteBuffer bb = ByteBuffer.wrap(data).order(ByteOrder.nativeOrder());
bb.asDoubleBuffer().put(pixels);
pixels.position(pos); // Restore position
}
return new TexImage2DCmd(target, level, internalformat, width, height, border, format, type, data);
public static TexImage2DCmd fromFloatBuffer(int target, int level, int internalformat, int width, int height, int border, int format, int type, @Nullable FloatBuffer pixels, PixelUnpackState unpack) {
return new TexImage2DCmd(target, level, internalformat, width, height, border, format, type, PixelDataSnapshot.copy(pixels), unpack);
}
/**
* Create command from ByteBuffer
*/
public static TexImage2DCmd fromByteBuffer(int target, int level, int internalformat, int width, int height, int border, int format, int type, @Nullable ByteBuffer pixels) {
byte[] data = null;
if (pixels != null) {
int pos = pixels.position();
int limit = pixels.limit();
int size = limit - pos;
data = new byte[size];
pixels.get(data);
pixels.position(pos); // Restore position
}
return new TexImage2DCmd(target, level, internalformat, width, height, border, format, type, data);
public static TexImage2DCmd fromDoubleBuffer(int target, int level, int internalformat, int width, int height, int border, int format, int type, @Nullable DoubleBuffer pixels, PixelUnpackState unpack) {
return new TexImage2DCmd(target, level, internalformat, width, height, border, format, type, PixelDataSnapshot.copy(pixels), unpack);
}
public static TexImage2DCmd fromByteBuffer(int target, int level, int internalformat, int width, int height, int border, int format, int type, @Nullable ByteBuffer pixels, PixelUnpackState unpack) {
return new TexImage2DCmd(target, level, internalformat, width, height, border, format, type, PixelDataSnapshot.copy(pixels), unpack);
}
@Override
public void execute() {
ByteBuffer buffer = null;
if (pixelData != null) {
buffer = ByteBuffer.allocateDirect(pixelData.length).order(ByteOrder.nativeOrder());
buffer.put(pixelData);
buffer.flip();
if (pixels == null) {
GLStateManager.glTexImage2D(target, level, internalformat, width, height, border, format, type, (ByteBuffer) null);
return;
}
GLStateManager.forcePixelUnpackState(unpack);
try {
GLStateManager.glTexImage2D(target, level, internalformat, width, height, border, format, type, pixels);
} finally {
GLStateManager.restorePixelUnpackState(unpack);
}
}
@Override
public void delete() {
if (pixels != null) {
memFree(pixels);
}
GLStateManager.glTexImage2D(target, level, internalformat, width, height, border, format, type, buffer);
}
@Override
public @NotNull String toString() {
String dataStr = pixelData != null ? (pixelData.length + " bytes") : "null";
return String.format("TexImage2D(target=%s, level=%d, internalformat=%s, %dx%d, border=%d, format=%s, type=%s, data=%s)",
String dataStr = pixels != null ? (pixels.remaining() + " bytes") : "null";
return String.format("TexImage2D(target=%s, level=%d, internalformat=%s, %dx%d, border=%d, format=%s, type=%s, data=%s, unpack=%s)",
GLDebug.getTextureTargetName(target), level, GLDebug.getTextureFormatName(internalformat),
width, height, border, GLDebug.getTextureFormatName(format), GLDebug.getDataTypeName(type), dataStr);
width, height, border, GLDebug.getTextureFormatName(format), GLDebug.getDataTypeName(type), dataStr, unpack);
}
}
@@ -2,15 +2,17 @@ package com.gtnewhorizons.angelica.glsm.recording.commands;
import com.gtnewhorizons.angelica.glsm.GLDebug;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.states.PixelUnpackState;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.DoubleBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memFree;
/**
* Command: glTexSubImage2D(target, level, xoffset, yoffset, width, height, format, type, pixels)
* Updates a portion of an existing two-dimensional texture image during display list playback.
@@ -24,92 +26,58 @@ public record TexSubImage2DCmd(
int height,
int format,
int type,
@Nullable byte[] pixelData
@Nullable ByteBuffer pixels,
@Nullable PixelUnpackState unpack
) implements DisplayListCommand {
/**
* Create command from IntBuffer
*/
public static TexSubImage2DCmd fromIntBuffer(int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, @Nullable IntBuffer pixels) {
byte[] data = null;
if (pixels != null) {
int pos = pixels.position();
int limit = pixels.limit();
int size = (limit - pos) * 4; // 4 bytes per int
data = new byte[size];
ByteBuffer bb = ByteBuffer.wrap(data).order(ByteOrder.nativeOrder());
bb.asIntBuffer().put(pixels);
pixels.position(pos); // Restore position
public TexSubImage2DCmd {
if (pixels == null) {
unpack = null;
}
return new TexSubImage2DCmd(target, level, xoffset, yoffset, width, height, format, type, data);
}
/**
* Create command from FloatBuffer
*/
public static TexSubImage2DCmd fromFloatBuffer(int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, @Nullable FloatBuffer pixels) {
byte[] data = null;
if (pixels != null) {
int pos = pixels.position();
int limit = pixels.limit();
int size = (limit - pos) * 4; // 4 bytes per float
data = new byte[size];
ByteBuffer bb = ByteBuffer.wrap(data).order(ByteOrder.nativeOrder());
bb.asFloatBuffer().put(pixels);
pixels.position(pos); // Restore position
}
return new TexSubImage2DCmd(target, level, xoffset, yoffset, width, height, format, type, data);
public static TexSubImage2DCmd fromIntBuffer(int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, @Nullable IntBuffer pixels, PixelUnpackState unpack) {
return new TexSubImage2DCmd(target, level, xoffset, yoffset, width, height, format, type, PixelDataSnapshot.copy(pixels), unpack);
}
/**
* Create command from DoubleBuffer
*/
public static TexSubImage2DCmd fromDoubleBuffer(int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, @Nullable DoubleBuffer pixels) {
byte[] data = null;
if (pixels != null) {
int pos = pixels.position();
int limit = pixels.limit();
int size = (limit - pos) * 8; // 8 bytes per double
data = new byte[size];
ByteBuffer bb = ByteBuffer.wrap(data).order(ByteOrder.nativeOrder());
bb.asDoubleBuffer().put(pixels);
pixels.position(pos); // Restore position
}
return new TexSubImage2DCmd(target, level, xoffset, yoffset, width, height, format, type, data);
public static TexSubImage2DCmd fromFloatBuffer(int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, @Nullable FloatBuffer pixels, PixelUnpackState unpack) {
return new TexSubImage2DCmd(target, level, xoffset, yoffset, width, height, format, type, PixelDataSnapshot.copy(pixels), unpack);
}
/**
* Create command from ByteBuffer
*/
public static TexSubImage2DCmd fromByteBuffer(int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, @Nullable ByteBuffer pixels) {
byte[] data = null;
if (pixels != null) {
int pos = pixels.position();
int limit = pixels.limit();
int size = limit - pos;
data = new byte[size];
pixels.get(data);
pixels.position(pos); // Restore position
}
return new TexSubImage2DCmd(target, level, xoffset, yoffset, width, height, format, type, data);
public static TexSubImage2DCmd fromDoubleBuffer(int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, @Nullable DoubleBuffer pixels, PixelUnpackState unpack) {
return new TexSubImage2DCmd(target, level, xoffset, yoffset, width, height, format, type, PixelDataSnapshot.copy(pixels), unpack);
}
public static TexSubImage2DCmd fromByteBuffer(int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, @Nullable ByteBuffer pixels, PixelUnpackState unpack) {
return new TexSubImage2DCmd(target, level, xoffset, yoffset, width, height, format, type, PixelDataSnapshot.copy(pixels), unpack);
}
@Override
public void execute() {
ByteBuffer buffer = null;
if (pixelData != null) {
buffer = ByteBuffer.allocateDirect(pixelData.length).order(ByteOrder.nativeOrder());
buffer.put(pixelData);
buffer.flip();
if (pixels == null) {
GLStateManager.glTexSubImage2D(target, level, xoffset, yoffset, width, height, format, type, (ByteBuffer) null);
return;
}
GLStateManager.forcePixelUnpackState(unpack);
try {
GLStateManager.glTexSubImage2D(target, level, xoffset, yoffset, width, height, format, type, pixels);
} finally {
GLStateManager.restorePixelUnpackState(unpack);
}
}
@Override
public void delete() {
if (pixels != null) {
memFree(pixels);
}
GLStateManager.glTexSubImage2D(target, level, xoffset, yoffset, width, height, format, type, buffer);
}
@Override
public @NotNull String toString() {
String dataStr = pixelData != null ? (pixelData.length + " bytes") : "null";
return String.format("TexSubImage2D(target=%s, level=%d, offset=(%d,%d), %dx%d, format=%s, type=%s, data=%s)",
String dataStr = pixels != null ? (pixels.remaining() + " bytes") : "null";
return String.format("TexSubImage2D(target=%s, level=%d, offset=(%d,%d), %dx%d, format=%s, type=%s, data=%s, unpack=%s)",
GLDebug.getTextureTargetName(target), level, xoffset, yoffset, width, height,
GLDebug.getTextureFormatName(format), GLDebug.getDataTypeName(type), dataStr);
GLDebug.getTextureFormatName(format), GLDebug.getDataTypeName(type), dataStr, unpack);
}
}
@@ -46,6 +46,13 @@ public class GLSMRedirector {
private static final String ARBMultiTexture = "org/lwjgl/opengl/ARBMultitexture";
private static final String ARBShaderObjects = "org/lwjgl/opengl/ARBShaderObjects";
private static final String ARBInstancedArrays = "org/lwjgl/opengl/ARBInstancedArrays";
private static final String ARBDrawInstanced = "org/lwjgl/opengl/ARBDrawInstanced";
private static final String EXTDrawInstanced = "org/lwjgl/opengl/EXTDrawInstanced";
private static final String ARBBufferObject = "org/lwjgl/opengl/ARBBufferObject";
private static final String ARBVertexBufferObject = "org/lwjgl/opengl/ARBVertexBufferObject";
private static final String ARBBufferStorage = "org/lwjgl/opengl/ARBBufferStorage";
private static final String ARBCopyBuffer = "org/lwjgl/opengl/ARBCopyBuffer";
private static final String ARBMapBufferRange = "org/lwjgl/opengl/ARBMapBufferRange";
// Redirect VAO related calls from NHLib
private static final String UniversalVAO = "com/gtnewhorizon/gtnhlib/client/opengl/UniversalVAO";
@@ -126,6 +133,7 @@ public class GLSMRedirector {
.add("glFrustum")
.add("glGetBoolean")
.add("glGetFloat")
.add("glGetFloatv", "glGetFloat")
.add("glGetInteger")
.add("glGetLight")
.add("glGetMaterial")
@@ -282,11 +290,16 @@ public class GLSMRedirector {
.add("glBindBuffer")
.add("glDeleteBuffers")
.add("glBufferData")
.add("nglBufferData")
.add("glBufferSubData")
.add("nglBufferSubData")
.add("glMapBuffer")
.add("glUnmapBuffer")
.add("glGetBufferSubData")
.add("nglGetBufferSubData")
.add("glGetBufferParameteri")
.add("glGetBufferParameter")
.add("glGetBufferParameteriv")
.add("glIsBuffer");
final var gl20 = RedirectMap.newMap()
.add("glBlendEquationSeparate")
@@ -296,6 +309,7 @@ public class GLSMRedirector {
.add("glStencilOpSeparate")
.add("glUseProgram")
.add("glShaderSource")
.add("nglShaderSource")
.add("glLinkProgram")
.add("glDeleteProgram")
.add("glCreateShader")
@@ -321,6 +335,7 @@ public class GLSMRedirector {
.add("glUniformMatrix2")
.add("glUniformMatrix3")
.add("glUniformMatrix4")
.add("glUniformMatrix4fv", "glUniformMatrix4")
.add("glDeleteShader")
.add("glGetShaderi")
.add("glGetShaderInfoLog")
@@ -342,6 +357,7 @@ public class GLSMRedirector {
.add("glGenVertexArrays")
.add("glBindVertexArray")
.add("glDeleteVertexArrays")
.add("glIsVertexArray")
.add("glBindFramebuffer")
.add("glDeleteFramebuffers")
.add("glGenFramebuffers")
@@ -350,9 +366,13 @@ public class GLSMRedirector {
.add("glVertexAttribIPointer")
.add("glGenerateMipmap")
.add("glGetFramebufferAttachmentParameteri")
.add("glBlitFramebuffer");
.add("glBlitFramebuffer")
.add("glMapBufferRange")
.add("glFlushMappedBufferRange");
final var gl31 = RedirectMap.newMap()
.add("glDrawElementsInstanced");
.add("glDrawElementsInstanced")
.add("glDrawArraysInstanced")
.add("glCopyBufferSubData");
final var gl32 = RedirectMap.newMap()
.add("glFramebufferTexture");
final var gl33 = RedirectMap.newMap()
@@ -376,6 +396,10 @@ public class GLSMRedirector {
final var gl44 = RedirectMap.newMap()
.add("glBufferStorage")
.add("glClearTexImage");
final var gl45 = RedirectMap.newMap()
.add("glCreateBuffers")
.add("glNamedBufferData")
.add("glNamedBufferSubData");
// Merge all GL version maps into the flat lookup
glMethodRedirects.putAll(gl11);
@@ -391,6 +415,7 @@ public class GLSMRedirector {
glMethodRedirects.putAll(gl42);
glMethodRedirects.putAll(gl43);
glMethodRedirects.putAll(gl44);
glMethodRedirects.putAll(gl45);
// MINECRAFT
methodRedirects.put(OpenGlHelper, RedirectMap.newMap()
@@ -455,6 +480,39 @@ public class GLSMRedirector {
methodRedirects.put(ARBInstancedArrays, RedirectMap.newMap()
.add("glVertexAttribDivisorARB")
);
methodRedirects.put(ARBDrawInstanced, RedirectMap.newMap()
.add("glDrawArraysInstancedARB", "glDrawArraysInstanced")
.add("glDrawElementsInstancedARB", "glDrawElementsInstanced")
);
methodRedirects.put(EXTDrawInstanced, RedirectMap.newMap()
.add("glDrawArraysInstancedEXT", "glDrawArraysInstanced")
.add("glDrawElementsInstancedEXT", "glDrawElementsInstanced")
);
final var arbBufferObject = RedirectMap.newMap()
.add("glBindBufferARB", "glBindBuffer")
.add("glGenBuffersARB", "glGenBuffers")
.add("glDeleteBuffersARB", "glDeleteBuffers")
.add("glIsBufferARB", "glIsBuffer")
.add("glBufferDataARB", "glBufferData")
.add("glBufferSubDataARB", "glBufferSubData")
.add("glGetBufferSubDataARB", "glGetBufferSubData")
.add("glMapBufferARB", "glMapBuffer")
.add("glUnmapBufferARB", "glUnmapBuffer")
.add("glGetBufferParameterARB", "glGetBufferParameter")
.add("glGetBufferParameteriARB", "glGetBufferParameteri")
.add("glGetBufferParameterivARB", "glGetBufferParameteriv");
methodRedirects.put(ARBBufferObject, arbBufferObject);
methodRedirects.put(ARBVertexBufferObject, arbBufferObject);
methodRedirects.put(ARBBufferStorage, RedirectMap.newMap()
.add("glBufferStorage")
);
methodRedirects.put(ARBCopyBuffer, RedirectMap.newMap()
.add("glCopyBufferSubData")
);
methodRedirects.put(ARBMapBufferRange, RedirectMap.newMap()
.add("glMapBufferRange")
.add("glFlushMappedBufferRange")
);
// APPLE
methodRedirects.put(APPLEVertexArrayObject, RedirectMap.newMap()
@@ -587,18 +645,16 @@ public class GLSMRedirector {
}
} else {
final Map<String, String> redirects = mNode.owner.startsWith(GL_PREFIX) ? glMethodRedirects : methodRedirects.get(mNode.owner);
if (redirects != null) {
final String glsmName = redirects.get(mNode.name);
if (glsmName != null) {
if (LOG_SPAM) {
final String shortOwner = mNode.owner.substring(mNode.owner.lastIndexOf("/") + 1);
LOGGER.info("Redirecting call in {} from {}.{}{} to GLStateManager.{}{}", transformedName, shortOwner, mNode.name, mNode.desc, glsmName, mNode.desc);
}
mNode.owner = GLStateManager;
mNode.name = glsmName;
changed = true;
redirectInMethod = true;
final String glsmName = redirects != null ? redirects.get(mNode.name) : null;
if (glsmName != null) {
if (LOG_SPAM) {
final String shortOwner = mNode.owner.substring(mNode.owner.lastIndexOf("/") + 1);
LOGGER.info("Redirecting call in {} from {}.{}{} to GLStateManager.{}{}", transformedName, shortOwner, mNode.name, mNode.desc, glsmName, mNode.desc);
}
mNode.owner = GLStateManager;
mNode.name = glsmName;
changed = true;
redirectInMethod = true;
}
}
// Redirect <init> calls for quadric classes
@@ -617,8 +673,7 @@ public class GLSMRedirector {
for (int i = 0; i < dynNode.bsmArgs.length; i++) {
if (!(dynNode.bsmArgs[i] instanceof Handle handle)) continue;
final Map<String, String> redirects = handle.getOwner().startsWith(GL_PREFIX) ? glMethodRedirects : methodRedirects.get(handle.getOwner());
if (redirects == null) continue;
final String glsmName = redirects.get(handle.getName());
final String glsmName = redirects != null ? redirects.get(handle.getName()) : null;
if (glsmName == null) continue;
if (LOG_SPAM) {
final String shortOwner = handle.getOwner().substring(handle.getOwner().lastIndexOf("/") + 1);
@@ -27,65 +27,75 @@ public class LightModelState implements ISettableState<LightModelState> {
twoSide = 0.0F;
}
public void setAmbient(FloatBuffer newBuffer) {
public boolean setAmbient(FloatBuffer newBuffer) {
vector4f.set(newBuffer);
if (GLStateManager.shouldBypassCache() || !this.ambient.equals(vector4f)) {
this.ambient.set(vector4f);
return true;
}
return false;
}
public void setAmbient(IntBuffer newBuffer) {
public boolean setAmbient(IntBuffer newBuffer) {
vector4i.set(newBuffer);
vector4f.set(i2f(vector4i.x), i2f(vector4i.y), i2f(vector4i.z), i2f(vector4i.w));
if (GLStateManager.shouldBypassCache() || !this.ambient.equals(vector4f)) {
this.ambient.set(vector4f);
return true;
}
return false;
}
public void setColorControl(int val) {
public boolean setColorControl(int val) {
if (GLStateManager.shouldBypassCache() || this.colorControl != val) {
this.colorControl = val;
return true;
}
return false;
}
public void setColorControl(IntBuffer newBuffer) {
setColorControl(newBuffer.get());
public boolean setColorControl(IntBuffer newBuffer) {
return setColorControl(newBuffer.get());
}
public void setLocalViewer(float val) {
public boolean setLocalViewer(float val) {
if (GLStateManager.shouldBypassCache() || Float.compare(this.localViewer, val) != 0) {
this.localViewer = val;
return true;
}
return false;
}
public void setLocalViewer(FloatBuffer newBuffer) {
setLocalViewer(newBuffer.get());
public boolean setLocalViewer(FloatBuffer newBuffer) {
return setLocalViewer(newBuffer.get());
}
public void setLocalViewer(IntBuffer newBuffer) {
setLocalViewer((float) newBuffer.get());
public boolean setLocalViewer(IntBuffer newBuffer) {
return setLocalViewer((float) newBuffer.get());
}
public void setLocalViewer(int val) {
setLocalViewer((float) val);
public boolean setLocalViewer(int val) {
return setLocalViewer((float) val);
}
public void setTwoSide(float val) {
public boolean setTwoSide(float val) {
if (GLStateManager.shouldBypassCache() || Float.compare(this.twoSide, val) != 0) {
this.twoSide = val;
return true;
}
return false;
}
public void setTwoSide(FloatBuffer newBuffer) {
setTwoSide(newBuffer.get());
public boolean setTwoSide(FloatBuffer newBuffer) {
return setTwoSide(newBuffer.get());
}
public void setTwoSide(IntBuffer newBuffer) {
setTwoSide((float) newBuffer.get());
public boolean setTwoSide(IntBuffer newBuffer) {
return setTwoSide((float) newBuffer.get());
}
public void setTwoSide(int val) {
setTwoSide((float) val);
public boolean setTwoSide(int val) {
return setTwoSide((float) val);
}
@Override
@@ -17,6 +17,7 @@ public class LightState implements ISettableState<LightState> {
private static final Vector4i vector4i = new Vector4i();
private static final Vector4f vector4f = new Vector4f();
private static final Vector3f vector3f = new Vector3f();
private static final Vector3i vector3i = new Vector3i();
private static final Matrix3f matrix3f = new Matrix3f();
@@ -27,6 +28,10 @@ public class LightState implements ISettableState<LightState> {
public final Vector4f specular;
public final Vector4f position;
public final Vector3f spotDirection;
private final Vector4f rawPosition;
private int posMvGeneration = -1;
private final Vector3f rawSpotDirection;
private int spotDirLinearGeneration = -1;
public float spotExponent;
public float spotCutoff;
public float spotCosCutoff;
@@ -66,178 +71,221 @@ public class LightState implements ISettableState<LightState> {
this.linearAttenuation = linearAttenuation;
this.quadraticAttenuation = quadraticAttenuation;
this.rawPosition = new Vector4f(position);
this.position = position;
this.position.mul(GLStateManager.getModelViewMatrix());
this.posMvGeneration = GLStateManager.mvGeneration;
this.rawSpotDirection = new Vector3f(spotDirection);
this.spotDirection = spotDirection;
GLStateManager.getModelViewMatrix().get3x3(matrix3f);
this.spotDirection.mul(matrix3f);
this.spotDirLinearGeneration = GLStateManager.mvLinearGeneration;
}
public void setAmbient(FloatBuffer newBuffer) {
public boolean setAmbient(FloatBuffer newBuffer) {
vector4f.set(newBuffer);
if (GLStateManager.shouldBypassCache() || !this.ambient.equals(vector4f)) {
this.ambient.set(vector4f);
return true;
}
return false;
}
public void setAmbient(IntBuffer newBuffer) {
public boolean setAmbient(IntBuffer newBuffer) {
vector4i.set(newBuffer);
vector4f.set(i2f(vector4i.x), i2f(vector4i.y), i2f(vector4i.z), i2f(vector4i.w));
if (GLStateManager.shouldBypassCache() || !this.ambient.equals(vector4f)) {
this.ambient.set(vector4f);
return true;
}
return false;
}
public void setDiffuse(FloatBuffer newBuffer) {
public boolean setDiffuse(FloatBuffer newBuffer) {
vector4f.set(newBuffer);
if (GLStateManager.shouldBypassCache() || !this.diffuse.equals(vector4f)) {
this.diffuse.set(vector4f);
return true;
}
return false;
}
public void setDiffuse(IntBuffer newBuffer) {
public boolean setDiffuse(IntBuffer newBuffer) {
vector4i.set(newBuffer);
vector4f.set(i2f(vector4i.x), i2f(vector4i.y), i2f(vector4i.z), i2f(vector4i.w));
if (GLStateManager.shouldBypassCache() || !this.diffuse.equals(vector4f)) {
this.diffuse.set(vector4f);
return true;
}
return false;
}
public void setSpecular(FloatBuffer newBuffer) {
public boolean setSpecular(FloatBuffer newBuffer) {
vector4f.set(newBuffer);
if (GLStateManager.shouldBypassCache() || !this.specular.equals(vector4f)) {
this.specular.set(vector4f);
return true;
}
return false;
}
public void setSpecular(IntBuffer newBuffer) {
public boolean setSpecular(IntBuffer newBuffer) {
vector4i.set(newBuffer);
vector4f.set(i2f(vector4i.x), i2f(vector4i.y), i2f(vector4i.z), i2f(vector4i.w));
if (GLStateManager.shouldBypassCache() || !this.specular.equals(vector4f)) {
this.specular.set(vector4f);
return true;
}
return false;
}
public void setPosition(FloatBuffer newBuffer) {
// We are bypassing cache everytime for position, because the necessary components to enable tracking the cache
// are big and probably more than just bypassing. We would need to track the modelview matrix it was transformed
// with and the untransformed coordinates in addition to the final transformation.
this.position.set(newBuffer);
this.position.mul(GLStateManager.getModelViewMatrix());
public boolean setPosition(FloatBuffer newBuffer) {
vector4f.set(newBuffer);
return applyPosition(vector4f);
}
public void setPosition(IntBuffer newBuffer) {
public boolean setPosition(IntBuffer newBuffer) {
vector4i.set(newBuffer);
this.position.set((float) vector4i.x, (float) vector4i.y, (float) vector4i.z, (float) vector4i.w);
this.position.mul(GLStateManager.getModelViewMatrix());
vector4f.set((float) vector4i.x, (float) vector4i.y, (float) vector4i.z, (float) vector4i.w);
return applyPosition(vector4f);
}
public void setSpotDirection(FloatBuffer newBuffer) {
// We are bypassing cache everytime for spot direction, because the necessary components to enable tracking the cache
// are big and probably more than just bypassing. We would need to track the modelview matrix it was transformed
// with and the untransformed coordinates in addition to the final transformation.
GLStateManager.getModelViewMatrix().get3x3(matrix3f);
this.spotDirection.set(newBuffer);
this.spotDirection.mul(matrix3f);
private boolean applyPosition(Vector4f raw) {
final int mvGen = GLStateManager.mvGeneration;
if (GLStateManager.shouldBypassCache() || this.posMvGeneration != mvGen || !this.rawPosition.equals(raw)) {
this.rawPosition.set(raw);
this.posMvGeneration = mvGen;
this.position.set(raw);
this.position.mul(GLStateManager.getModelViewMatrix());
return true;
}
return false;
}
public void setSpotDirection(IntBuffer newBuffer) {
public boolean setSpotDirection(FloatBuffer newBuffer) {
vector3f.set(newBuffer);
return applySpotDirection(vector3f);
}
public boolean setSpotDirection(IntBuffer newBuffer) {
vector3i.set(newBuffer);
GLStateManager.getModelViewMatrix().get3x3(matrix3f);
this.spotDirection.set((float) vector3i.x, (float) vector3i.y, (float) vector3i.z);
this.spotDirection.mul(matrix3f);
vector3f.set((float) vector3i.x, (float) vector3i.y, (float) vector3i.z);
return applySpotDirection(vector3f);
}
public void setSpotExponent(FloatBuffer newBuffer) {
setSpotExponent(newBuffer.get());
private boolean applySpotDirection(Vector3f raw) {
final int mvLinearGen = GLStateManager.mvLinearGeneration;
if (GLStateManager.shouldBypassCache() || this.spotDirLinearGeneration != mvLinearGen || !this.rawSpotDirection.equals(raw)) {
this.rawSpotDirection.set(raw);
this.spotDirLinearGeneration = mvLinearGen;
GLStateManager.getModelViewMatrix().get3x3(matrix3f);
this.spotDirection.set(raw);
this.spotDirection.mul(matrix3f);
return true;
}
return false;
}
public void setSpotExponent(IntBuffer newBuffer) {
setSpotExponent((float) newBuffer.get());
public boolean setSpotExponent(FloatBuffer newBuffer) {
return setSpotExponent(newBuffer.get());
}
public void setSpotExponent(int i) {
setSpotExponent((float) i);
public boolean setSpotExponent(IntBuffer newBuffer) {
return setSpotExponent((float) newBuffer.get());
}
public void setSpotExponent(float f) {
public boolean setSpotExponent(int i) {
return setSpotExponent((float) i);
}
public boolean setSpotExponent(float f) {
if (GLStateManager.shouldBypassCache() || Float.compare(f, this.spotExponent) != 0) {
this.spotExponent = f;
return true;
}
return false;
}
public void setSpotCutoff(FloatBuffer newBuffer) {
setSpotCutoff(newBuffer.get());
public boolean setSpotCutoff(FloatBuffer newBuffer) {
return setSpotCutoff(newBuffer.get());
}
public void setSpotCutoff(IntBuffer newBuffer) {
setSpotCutoff((float) newBuffer.get());
public boolean setSpotCutoff(IntBuffer newBuffer) {
return setSpotCutoff((float) newBuffer.get());
}
public void setSpotCutoff(int i) {
setSpotCutoff((float) i);
public boolean setSpotCutoff(int i) {
return setSpotCutoff((float) i);
}
public void setSpotCutoff(float f) {
public boolean setSpotCutoff(float f) {
if (GLStateManager.shouldBypassCache() || Float.compare(f, this.spotCutoff) != 0) {
this.spotCutoff = f;
this.spotCosCutoff = (float) Math.cos(Math.toRadians(f));
return true;
}
return false;
}
public void setConstantAttenuation(FloatBuffer newBuffer) {
setConstantAttenuation(newBuffer.get());
public boolean setConstantAttenuation(FloatBuffer newBuffer) {
return setConstantAttenuation(newBuffer.get());
}
public void setConstantAttenuation(IntBuffer newBuffer) {
setConstantAttenuation((float) newBuffer.get());
public boolean setConstantAttenuation(IntBuffer newBuffer) {
return setConstantAttenuation((float) newBuffer.get());
}
public void setConstantAttenuation(int i) {
setConstantAttenuation((float) i);
public boolean setConstantAttenuation(int i) {
return setConstantAttenuation((float) i);
}
public void setConstantAttenuation(float f) {
public boolean setConstantAttenuation(float f) {
if (GLStateManager.shouldBypassCache() || Float.compare(f, this.constantAttenuation) != 0) {
this.constantAttenuation = f;
return true;
}
return false;
}
public void setLinearAttenuation(FloatBuffer newBuffer) {
setLinearAttenuation(newBuffer.get());
public boolean setLinearAttenuation(FloatBuffer newBuffer) {
return setLinearAttenuation(newBuffer.get());
}
public void setLinearAttenuation(IntBuffer newBuffer) {
setLinearAttenuation((float) newBuffer.get());
public boolean setLinearAttenuation(IntBuffer newBuffer) {
return setLinearAttenuation((float) newBuffer.get());
}
public void setLinearAttenuation(int i) {
setLinearAttenuation((float) i);
public boolean setLinearAttenuation(int i) {
return setLinearAttenuation((float) i);
}
public void setLinearAttenuation(float f) {
public boolean setLinearAttenuation(float f) {
if (GLStateManager.shouldBypassCache() || Float.compare(f, this.linearAttenuation) != 0) {
this.linearAttenuation = f;
return true;
}
return false;
}
public void setQuadraticAttenuation(FloatBuffer newBuffer) {
setQuadraticAttenuation(newBuffer.get());
public boolean setQuadraticAttenuation(FloatBuffer newBuffer) {
return setQuadraticAttenuation(newBuffer.get());
}
public void setQuadraticAttenuation(IntBuffer newBuffer) {
setQuadraticAttenuation((float) newBuffer.get());
public boolean setQuadraticAttenuation(IntBuffer newBuffer) {
return setQuadraticAttenuation((float) newBuffer.get());
}
public void setQuadraticAttenuation(int i) {
setQuadraticAttenuation((float) i);
public boolean setQuadraticAttenuation(int i) {
return setQuadraticAttenuation((float) i);
}
public void setQuadraticAttenuation(float f) {
public boolean setQuadraticAttenuation(float f) {
if (GLStateManager.shouldBypassCache() || Float.compare(f, this.quadraticAttenuation) != 0) {
this.quadraticAttenuation = f;
return true;
}
return false;
}
@Override
@@ -246,7 +294,11 @@ public class LightState implements ISettableState<LightState> {
this.diffuse.set(state.diffuse);
this.specular.set(state.specular);
this.position.set(state.position);
this.rawPosition.set(state.rawPosition);
this.posMvGeneration = state.posMvGeneration;
this.spotDirection.set(state.spotDirection);
this.rawSpotDirection.set(state.rawSpotDirection);
this.spotDirLinearGeneration = state.spotDirLinearGeneration;
this.spotExponent = state.spotExponent;
this.spotCutoff = state.spotCutoff;
this.spotCosCutoff = state.spotCosCutoff;
@@ -37,79 +37,100 @@ public class MaterialState implements ISettableState<MaterialState> {
colorIndexes = new Vector3f(0.0F, 1.0F, 1.0F);
}
public void setAmbient(FloatBuffer newBuffer) {
public boolean setAmbient(FloatBuffer newBuffer) {
vector4f.set(newBuffer);
if (GLStateManager.shouldBypassCache() || !this.ambient.equals(vector4f)) {
this.ambient.set(vector4f);
return true;
}
return false;
}
public void setAmbient(IntBuffer newBuffer) {
public boolean setAmbient(IntBuffer newBuffer) {
vector4i.set(newBuffer);
vector4f.set(i2f(vector4i.x), i2f(vector4i.y), i2f(vector4i.z), i2f(vector4i.w));
if (GLStateManager.shouldBypassCache() || !this.ambient.equals(vector4f)) {
this.ambient.set(vector4f);
return true;
}
return false;
}
public void setDiffuse(FloatBuffer newBuffer) {
public boolean setDiffuse(FloatBuffer newBuffer) {
vector4f.set(newBuffer);
if (GLStateManager.shouldBypassCache() || !this.diffuse.equals(vector4f)) {
this.diffuse.set(vector4f);
return true;
}
return false;
}
public void setDiffuse(IntBuffer newBuffer) {
public boolean setDiffuse(IntBuffer newBuffer) {
vector4i.set(newBuffer);
vector4f.set(i2f(vector4i.x), i2f(vector4i.y), i2f(vector4i.z), i2f(vector4i.w));
if (GLStateManager.shouldBypassCache() || !this.diffuse.equals(vector4f)) {
this.diffuse.set(vector4f);
return true;
}
return false;
}
public void setSpecular(FloatBuffer newBuffer) {
public boolean setSpecular(FloatBuffer newBuffer) {
vector4f.set(newBuffer);
if (GLStateManager.shouldBypassCache() || !this.specular.equals(vector4f)) {
this.specular.set(vector4f);
return true;
}
return false;
}
public void setSpecular(IntBuffer newBuffer) {
public boolean setSpecular(IntBuffer newBuffer) {
vector4i.set(newBuffer);
vector4f.set(i2f(vector4i.x), i2f(vector4i.y), i2f(vector4i.z), i2f(vector4i.w));
if (GLStateManager.shouldBypassCache() || !this.specular.equals(vector4f)) {
this.specular.set(vector4f);
return true;
}
return false;
}
public void setEmission(FloatBuffer newBuffer) {
public boolean setEmission(FloatBuffer newBuffer) {
vector4f.set(newBuffer);
if (GLStateManager.shouldBypassCache() || !this.emission.equals(vector4f)) {
this.emission.set(vector4f);
return true;
}
return false;
}
public void setEmission(IntBuffer newBuffer) {
public boolean setEmission(IntBuffer newBuffer) {
vector4i.set(newBuffer);
vector4f.set(i2f(vector4i.x), i2f(vector4i.y), i2f(vector4i.z), i2f(vector4i.w));
if (GLStateManager.shouldBypassCache() || !this.emission.equals(vector4f)) {
this.emission.set(vector4f);
return true;
}
return false;
}
public void setShininess(float val) {
public boolean setShininess(float val) {
if (GLStateManager.shouldBypassCache() || Float.compare(this.shininess, val) != 0) {
this.shininess = val;
return true;
}
return false;
}
public void setShininess(FloatBuffer newBuffer) { setShininess(newBuffer.get()); }
// On the buffer versions of shininess, we need to call `get(0)` explicitly specifying the index
// otherwise calling `get()` advances the buffer position, and some mods may re-use the same buffer
// in subsequent calls, resulting in a BufferUnderflowException.
public boolean setShininess(FloatBuffer newBuffer) { return setShininess(newBuffer.get(0)); }
public void setShininess(IntBuffer newBuffer) { setShininess((float) newBuffer.get());}
public boolean setShininess(IntBuffer newBuffer) { return setShininess((float) newBuffer.get(0)); }
public void setShininess(int val) { setShininess((float) val);}
public boolean setShininess(int val) { return setShininess((float) val);}
public void setColorIndexes(FloatBuffer newBuffer) {
public boolean setColorIndexes(FloatBuffer newBuffer) {
// You are reading this correctly, nvidia drivers flip the y and z values in
// glMaterial specifically for GL_COLOR_INDEXES. Other drivers do not do this.
// This probably breaks things but who knows, GLSM is setup to track to what the
@@ -122,15 +143,19 @@ public class MaterialState implements ISettableState<MaterialState> {
if (GLStateManager.shouldBypassCache() || !this.colorIndexes.equals(vector3f)) {
this.colorIndexes.set(vector3f);
return true;
}
return false;
}
public void setColorIndexes(IntBuffer newBuffer) {
public boolean setColorIndexes(IntBuffer newBuffer) {
vector3i.set(newBuffer.get(0), newBuffer.get(2), newBuffer.get(1));
vector3f.set((float) vector3i.x, (float) vector3i.y, (float) vector3i.z);
if (GLStateManager.shouldBypassCache() || !this.colorIndexes.equals(vector3f)) {
this.colorIndexes.set(vector3f);
return true;
}
return false;
}
@Override
@@ -0,0 +1,51 @@
package com.gtnewhorizons.angelica.glsm.states;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL12;
import static com.gtnewhorizons.angelica.glsm.backend.BackendManager.RENDER_BACKEND;
/**
* Immutable client-side pixel unpack state (glPixelStore UNPACK_* params)
*/
public record PixelUnpackState(
int alignment,
int rowLength,
int skipRows,
int skipPixels,
int imageHeight,
int skipImages,
boolean swapBytes,
boolean lsbFirst
) {
public static final PixelUnpackState DEFAULT = new PixelUnpackState(4, 0, 0, 0, 0, 0, false, false);
public boolean isDefault() {
return this.equals(DEFAULT);
}
public PixelUnpackState with(int pname, int param) {
return switch (pname) {
case GL11.GL_UNPACK_ALIGNMENT -> (param == 1 || param == 2 || param == 4 || param == 8) ? new PixelUnpackState(param, rowLength, skipRows, skipPixels, imageHeight, skipImages, swapBytes, lsbFirst) : this;
case GL11.GL_UNPACK_ROW_LENGTH -> param >= 0 ? new PixelUnpackState(alignment, param, skipRows, skipPixels, imageHeight, skipImages, swapBytes, lsbFirst) : this;
case GL11.GL_UNPACK_SKIP_ROWS -> param >= 0 ? new PixelUnpackState(alignment, rowLength, param, skipPixels, imageHeight, skipImages, swapBytes, lsbFirst) : this;
case GL11.GL_UNPACK_SKIP_PIXELS -> param >= 0 ? new PixelUnpackState(alignment, rowLength, skipRows, param, imageHeight, skipImages, swapBytes, lsbFirst) : this;
case GL12.GL_UNPACK_IMAGE_HEIGHT -> param >= 0 ? new PixelUnpackState(alignment, rowLength, skipRows, skipPixels, param, skipImages, swapBytes, lsbFirst) : this;
case GL12.GL_UNPACK_SKIP_IMAGES -> param >= 0 ? new PixelUnpackState(alignment, rowLength, skipRows, skipPixels, imageHeight, param, swapBytes, lsbFirst) : this;
case GL11.GL_UNPACK_SWAP_BYTES -> new PixelUnpackState(alignment, rowLength, skipRows, skipPixels, imageHeight, skipImages, param != 0, lsbFirst);
case GL11.GL_UNPACK_LSB_FIRST -> new PixelUnpackState(alignment, rowLength, skipRows, skipPixels, imageHeight, skipImages, swapBytes, param != 0);
default -> this;
};
}
public static void applyDiff(PixelUnpackState from, PixelUnpackState to) {
if (from.alignment != to.alignment) RENDER_BACKEND.pixelStorei(GL11.GL_UNPACK_ALIGNMENT, to.alignment);
if (from.rowLength != to.rowLength) RENDER_BACKEND.pixelStorei(GL11.GL_UNPACK_ROW_LENGTH, to.rowLength);
if (from.skipRows != to.skipRows) RENDER_BACKEND.pixelStorei(GL11.GL_UNPACK_SKIP_ROWS, to.skipRows);
if (from.skipPixels != to.skipPixels) RENDER_BACKEND.pixelStorei(GL11.GL_UNPACK_SKIP_PIXELS, to.skipPixels);
if (from.imageHeight != to.imageHeight) RENDER_BACKEND.pixelStorei(GL12.GL_UNPACK_IMAGE_HEIGHT, to.imageHeight);
if (from.skipImages != to.skipImages) RENDER_BACKEND.pixelStorei(GL12.GL_UNPACK_SKIP_IMAGES, to.skipImages);
if (from.swapBytes != to.swapBytes) RENDER_BACKEND.pixelStorei(GL11.GL_UNPACK_SWAP_BYTES, to.swapBytes ? 1 : 0);
if (from.lsbFirst != to.lsbFirst) RENDER_BACKEND.pixelStorei(GL11.GL_UNPACK_LSB_FIRST, to.lsbFirst ? 1 : 0);
}
}
@@ -1,201 +0,0 @@
package com.gtnewhorizons.angelica.glsm.states;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import org.lwjgl.opengl.GL11;
import java.nio.ByteBuffer;
import java.util.ArrayDeque;
/**
* Per-VAO vertex attribute pointer state tracking (Mesa-style).
*/
public class VertexAttribState {
public static final int MAX_ATTRIBS = 16;
private static final Int2ObjectOpenHashMap<Attrib[]> vaoAttribs = new Int2ObjectOpenHashMap<>();
private static Attrib[] current;
private static final ArrayDeque<Attrib[]> pool = new ArrayDeque<>();
private static int clientSideEnabledCount;
public static void init(int defaultVAO) {
vaoAttribs.clear();
pool.clear();
current = allocAttribArray();
vaoAttribs.put(defaultVAO, current);
clientSideEnabledCount = 0;
}
public static void onBindVertexArray(int newVAO) {
current = vaoAttribs.computeIfAbsent(newVAO, k -> allocAttribArray());
recomputeClientSideCount();
}
public static void onDeleteVertexArray(int vaoId) {
final Attrib[] arr = vaoAttribs.remove(vaoId);
if (arr != null) {
pool.addLast(arr);
}
}
public static void reset() {
vaoAttribs.clear();
pool.clear();
current = null;
clientSideEnabledCount = 0;
}
private static Attrib[] allocAttribArray() {
Attrib[] arr = pool.pollFirst();
if (arr != null) {
for (Attrib a : arr) a.reset();
return arr;
}
arr = new Attrib[MAX_ATTRIBS];
for (int i = 0; i < MAX_ATTRIBS; i++) arr[i] = new Attrib();
return arr;
}
public static void set(int index, int size, int type, boolean normalized, int stride, long offset, int vboId) {
if (index < 0 || index >= MAX_ATTRIBS) return;
final Attrib a = current[index];
final boolean was = a.isClientSide();
a.size = size;
a.type = type;
a.normalized = normalized;
a.stride = stride;
a.offset = offset;
a.vboId = vboId;
a.clientPointer = null;
if (was) clientSideEnabledCount--;
}
public static void set(int index, int size, int type, boolean normalized, int stride, ByteBuffer pointer, int vboId) {
if (index < 0 || index >= MAX_ATTRIBS) return;
final Attrib a = current[index];
final boolean was = a.isClientSide();
a.size = size;
a.type = type;
a.normalized = normalized;
a.stride = stride;
a.offset = 0;
a.vboId = vboId;
a.clientPointer = (vboId == 0) ? pointer : null;
final boolean now = a.isClientSide();
if (was != now) clientSideEnabledCount += now ? 1 : -1;
}
public static void setEnabled(int index, boolean enabled) {
if (index < 0 || index >= MAX_ATTRIBS) return;
final Attrib a = current[index];
if (a.clientPointer != null && a.enabled != enabled) {
clientSideEnabledCount += enabled ? 1 : -1;
}
a.enabled = enabled;
}
public static Attrib get(int index) {
return current[index];
}
private static void recomputeClientSideCount() {
int count = 0;
for (int i = 0; i < MAX_ATTRIBS; i++) {
if (current[i].isClientSide()) count++;
}
clientSideEnabledCount = count;
}
public static boolean hasVBOBoundAttrib() {
for (int i = 0; i < MAX_ATTRIBS; i++) {
if (current[i].enabled && current[i].vboId != 0) return true;
}
return false;
}
/**
* Returns true if any currently enabled vertex attribute was registered without a VBO
* (i.e. as a client-side pointer). In core profile, such attribs are treated as null
* offsets into a non-existent buffer, causing a native crash at draw time.
*/
public static boolean hasAnyClientSideEnabledAttrib() {
return clientSideEnabledCount > 0;
}
public static class Attrib {
public boolean enabled;
public int size;
public int type;
public boolean normalized;
public int stride;
public long offset;
public int vboId;
public ByteBuffer clientPointer;
public boolean isClientSide() {
return enabled && clientPointer != null;
}
public void reset() {
enabled = false;
size = 0;
type = 0;
normalized = false;
stride = 0;
offset = 0;
vboId = 0;
clientPointer = null;
}
public int effectiveStride() {
return (stride != 0) ? stride : size * typeSizeBytes();
}
public int typeSizeBytes() {
return glTypeSizeBytes(type);
}
public static int glTypeSizeBytes(int glType) {
return switch (glType) {
case GL11.GL_FLOAT, GL11.GL_INT, GL11.GL_UNSIGNED_INT -> 4;
case GL11.GL_DOUBLE -> 8;
case GL11.GL_SHORT, GL11.GL_UNSIGNED_SHORT -> 2;
case GL11.GL_BYTE, GL11.GL_UNSIGNED_BYTE -> 1;
default -> 4;
};
}
public float readComponent(ByteBuffer buf, int base, int component) {
return switch (type) {
case GL11.GL_FLOAT -> buf.getFloat(base + component * 4);
case GL11.GL_DOUBLE -> (float) buf.getDouble(base + component * 8);
case GL11.GL_INT -> {
final int v = buf.getInt(base + component * 4);
yield normalized ? v / (float) Integer.MAX_VALUE : (float) v;
}
case GL11.GL_UNSIGNED_INT -> {
final int v = buf.getInt(base + component * 4);
yield normalized ? (v & 0xFFFFFFFFL) / (float) 0xFFFFFFFFL : (float) (v & 0xFFFFFFFFL);
}
case GL11.GL_SHORT -> {
final short v = buf.getShort(base + component * 2);
yield normalized ? v / (float) Short.MAX_VALUE : (float) v;
}
case GL11.GL_UNSIGNED_SHORT -> {
final int v = buf.getShort(base + component * 2) & 0xFFFF;
yield normalized ? v / (float) 0xFFFF : (float) v;
}
case GL11.GL_BYTE -> {
final byte v = buf.get(base + component);
yield normalized ? v / 127.0f : (float) v;
}
case GL11.GL_UNSIGNED_BYTE -> {
final int v = buf.get(base + component) & 0xFF;
yield normalized ? v / 255.0f : (float) v;
}
default -> 0.0f;
};
}
}
}
@@ -1,5 +1,6 @@
package com.gtnewhorizons.angelica.glsm.streaming;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import org.lwjgl.opengl.GL15;
import java.nio.ByteBuffer;
@@ -40,7 +41,7 @@ public final class OrphanStreamingBuffer {
public void destroy() {
if (bufferId != 0) {
RENDER_BACKEND.deleteBuffers(bufferId);
GLStateManager.glDeleteBuffers(bufferId);
bufferId = 0;
}
}
@@ -1,5 +1,6 @@
package com.gtnewhorizons.angelica.glsm.streaming;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.RenderSystem;
import it.unimi.dsi.fastutil.objects.ObjectArrayFIFOQueue;
import org.embeddedt.embeddium.impl.gl.sync.GlFence;
@@ -52,7 +53,7 @@ public class PersistentStreamingBuffer implements StreamingBuffer {
RENDER_BACKEND.bindBuffer(GL15.GL_ARRAY_BUFFER, 0);
if (this.mappedBuffer == null) {
RENDER_BACKEND.deleteBuffers(bufferId);
GLStateManager.glDeleteBuffers(bufferId);
bufferId = 0;
throw new RuntimeException("Failed to persistently map streaming buffer");
}
@@ -147,7 +148,7 @@ public class PersistentStreamingBuffer implements StreamingBuffer {
RENDER_BACKEND.bindBuffer(GL15.GL_ARRAY_BUFFER, bufferId);
RENDER_BACKEND.unmapBuffer(GL15.GL_ARRAY_BUFFER);
RENDER_BACKEND.bindBuffer(GL15.GL_ARRAY_BUFFER, 0);
RENDER_BACKEND.deleteBuffers(bufferId);
GLStateManager.glDeleteBuffers(bufferId);
bufferId = 0;
}
}
@@ -166,7 +167,9 @@ public class PersistentStreamingBuffer implements StreamingBuffer {
private boolean syncOldest() {
if (fenceQueue.isEmpty()) return false;
final FencedRegion region = fenceQueue.first();
region.fence.sync();
if (!region.fence.sync()) {
GLStateManager.LOGGER.warn("Streaming buffer fence wait did not signal; reclaiming {} bytes anyway", region.bytes);
}
region.fence.delete();
fenceQueue.dequeue();
remaining += region.bytes;
@@ -1,4 +1,4 @@
package com.gtnewhorizons.angelica.client.rendering;
package com.gtnewhorizons.angelica.glsm.streaming;
import com.gtnewhorizon.gtnhlib.client.renderer.DirectTessellator;
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.DefaultVertexFormat;
@@ -7,9 +7,6 @@ import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormat;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.QuadConverter;
import com.gtnewhorizons.angelica.glsm.RenderSystem;
import com.gtnewhorizons.angelica.glsm.ffp.ShaderManager;
import com.gtnewhorizons.angelica.glsm.streaming.OrphanStreamingBuffer;
import com.gtnewhorizons.angelica.glsm.streaming.PersistentStreamingBuffer;
import net.minecraft.client.renderer.Tessellator;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -91,13 +88,18 @@ public class TessellatorStreamingDrawer {
final int vertexSize = format.getVertexSize();
final int requiredBytes = vertexCount * vertexSize;
ensureRepackCapacity(requiredBytes);
final boolean locked = GLStateManager.acquireDrawLock();
try {
ensureRepackCapacity(requiredBytes);
final long writePtr = format.writeToBuffer0(repackAddress, tess.rawBuffer, tess.rawBufferIndex);
repackBuffer.position(0);
repackBuffer.limit((int)(writePtr - repackAddress));
final long writePtr = format.writeToBuffer0(repackAddress, tess.rawBuffer, tess.rawBufferIndex);
repackBuffer.position(0);
repackBuffer.limit((int)(writePtr - repackAddress));
uploadAndDraw(repackBuffer, flags, format, vertexSize, tess.drawMode, vertexCount);
uploadAndDraw(repackBuffer, flags, format, vertexSize, tess.drawMode, vertexCount);
} finally {
if (locked) GLStateManager.releaseDrawLock();
}
// Shrink rawBuffer if oversized
if (tess.rawBufferSize > 0x20000 && tess.rawBufferIndex < (tess.rawBufferSize << 3)) {
@@ -128,6 +130,19 @@ public class TessellatorStreamingDrawer {
uploadAndDraw(buffer, flags, format, vertexSize, drawMode, vertexCount);
}
/**
* Upload pre-packed vertex data and draw. Public API for external batch systems.
* @param packedData buffer positioned at 0 with limit set to total bytes
* @param drawMode GL draw mode (GL_QUADS, GL_TRIANGLES, etc.)
* @param flags vertex format flags (from VertexFlags)
* @param vertexCount number of vertices
*/
public static void drawPacked(ByteBuffer packedData, int drawMode, int flags, int vertexCount) {
final VertexFormat format = DefaultVertexFormat.ALL_FORMATS[flags];
final int vertexSize = format.getVertexSize();
uploadAndDraw(packedData, flags, format, vertexSize, drawMode, vertexCount);
}
private static String cachedDebugInfo = "Stream: not initialized";
private static long lastDebugUpdateNanos;
private static final long DEBUG_UPDATE_INTERVAL_NS = 500_000_000L; // 500ms
@@ -172,63 +187,34 @@ public class TessellatorStreamingDrawer {
}
}
/**
* Draw a merged batch of pre-packed vertex data from a DeferredBatchTessellator.
* Copies byte ranges into repackBuffer, then uploads and draws.
*/
static void drawPackedBatch(DeferredBatchTessellator source,
java.util.List<DeferredBatchTessellator.DrawRange> ranges,
int from, int to,
int totalBytes, int totalVertices,
int drawMode, int flags) {
if (totalVertices == 0) return;
final VertexFormat format = DefaultVertexFormat.ALL_FORMATS[flags];
final int vertexSize = format.getVertexSize();
ensureRepackCapacity(totalBytes);
long writePos = repackAddress;
for (int j = from; j < to; j++) {
final DeferredBatchTessellator.DrawRange r = ranges.get(j);
source.copyRange(r.byteOffset(), r.byteLength(), writePos);
writePos += r.byteLength();
}
repackBuffer.position(0);
repackBuffer.limit(totalBytes);
uploadAndDraw(repackBuffer, flags, format, vertexSize, drawMode, totalVertices);
}
/**
* Upload packed vertex data to a streaming buffer and issue the draw call.
* Tries the persistent ring buffer first, falls back to orphan buffer on overflow.
*/
private static void uploadAndDraw(ByteBuffer packed, int flags, VertexFormat format, int vertexSize, int drawMode, int vertexCount) {
ensureVAO(flags, format);
final boolean locked = GLStateManager.acquireDrawLock();
try {
ensureVAO(flags, format);
int firstVertex = -1;
int firstVertex = -1;
if (persistentBuffer != null) {
firstVertex = persistentBuffer.upload(packed, vertexSize);
if (persistentBuffer != null) {
firstVertex = persistentBuffer.upload(packed, vertexSize);
}
if (firstVertex >= 0) {
GLStateManager.glBindVertexArray(persistentVAOs[flags]);
} else {
GLStateManager.glBindVertexArray(orphanVAOs[flags]);
orphanBuffers[flags].upload(packed);
firstVertex = 0;
}
drawWithQuadConversion(drawMode, firstVertex, vertexCount);
GLStateManager.glBindVertexArray(0);
} finally {
if (locked) GLStateManager.releaseDrawLock();
}
if (firstVertex >= 0) {
GLStateManager.glBindVertexArray(persistentVAOs[flags]);
} else {
GLStateManager.glBindVertexArray(orphanVAOs[flags]);
orphanBuffers[flags].upload(packed);
firstVertex = 0;
}
GLStateManager.prepareWideLineEmulation(drawMode);
ShaderManager.getInstance().preDraw(flags);
drawWithQuadConversion(drawMode, firstVertex, vertexCount);
GLStateManager.glBindVertexArray(0);
GLStateManager.glDisableClientState(GL11.GL_VERTEX_ARRAY);
GLStateManager.glDisableClientState(GL11.GL_TEXTURE_COORD_ARRAY);
GLStateManager.glDisableClientState(GL11.GL_COLOR_ARRAY);
GLStateManager.glDisableClientState(GL11.GL_NORMAL_ARRAY);
}
private static void drawWithQuadConversion(int drawMode, int firstVertex, int vertexCount) {
@@ -239,7 +225,11 @@ public class TessellatorStreamingDrawer {
}
}
private static void ensureRepackCapacity(int requiredBytes) {
/**
* Ensure the repack buffer is large enough for the given byte count.
* Public for use by external batch systems that need to pack data before calling {@link #drawPacked}.
*/
public static void ensureRepackCapacity(int requiredBytes) {
if (requiredBytes <= repackCapacity) return;
int newCapacity = repackCapacity;
@@ -253,6 +243,16 @@ public class TessellatorStreamingDrawer {
repackCapacity = newCapacity;
}
/** Get the repack buffer's native address. Valid until next {@link #ensureRepackCapacity} call. */
public static long getRepackAddress() {
return repackAddress;
}
/** Get the repack ByteBuffer. Caller must set position/limit before passing to {@link #drawPacked}. */
public static ByteBuffer getRepackBuffer() {
return repackBuffer;
}
private static void ensureVAO(int flags, VertexFormat format) {
init();
@@ -9,6 +9,7 @@ public class TextureInfo {
@Getter protected final int id;
protected int internalFormat = -1;
protected int resolvedInternalFormat = -1;
protected int width = -1;
protected int height = -1;
@@ -16,6 +17,7 @@ public class TextureInfo {
@Getter @Setter protected int magFilter = GL11.GL_LINEAR;
@Getter @Setter protected int wrapS = GL11.GL_REPEAT;
@Getter @Setter protected int wrapT = GL11.GL_REPEAT;
@Getter @Setter protected int baseLevel = 0;
@Getter @Setter protected int maxLevel = 1000;
@Getter @Setter protected int minLod = -1000;
@Getter @Setter protected int maxLod = 1000;
@@ -23,6 +25,8 @@ public class TextureInfo {
@Getter @Setter protected float maxAnisotropy = 1.0f;
@Getter @Setter protected boolean generateMipmap = false;
protected TextureInfo(int id) {
this.id = id;
}
@@ -34,6 +38,17 @@ public class TextureInfo {
return internalFormat;
}
public int getResolvedInternalFormat() {
if (resolvedInternalFormat == -1) {
resolvedInternalFormat = RenderSystem.getTexLevelParameteri(id, 0, GL11.GL_TEXTURE_INTERNAL_FORMAT);
}
return resolvedInternalFormat;
}
public boolean needsInternalFormatResolve() {
return resolvedInternalFormat == -1;
}
public int getWidth() {
if (width == -1) {
width = RenderSystem.getTexLevelParameteri(id, 0, GL11.GL_TEXTURE_WIDTH);
@@ -5,6 +5,9 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import org.jetbrains.annotations.Nullable;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL13;
import org.lwjgl.opengl.GL21;
import org.lwjgl.opengl.GL30;
import java.nio.Buffer;
import java.util.concurrent.locks.ReentrantLock;
@@ -45,6 +48,7 @@ public class TextureInfoCache {
final TextureInfo info = getInfo(GLStateManager.getBoundTextureForServerState());
if (info == null) return;
info.internalFormat = internalformat;
info.resolvedInternalFormat = isGenericCompressedInternalFormat(internalformat) ? -1 : internalformat;
info.width = width;
info.height = height;
}
@@ -55,11 +59,24 @@ public class TextureInfoCache {
final TextureInfo info = getInfo(GLStateManager.getBoundTextureForServerState());
if (info == null) return;
info.internalFormat = internalformat;
info.resolvedInternalFormat = isGenericCompressedInternalFormat(internalformat) ? -1 : internalformat;
info.width = width;
info.height = height;
}
}
public static boolean isGenericCompressedInternalFormat(int internalFormat) {
return switch (internalFormat) {
case GL30.GL_COMPRESSED_RED,
GL30.GL_COMPRESSED_RG,
GL13.GL_COMPRESSED_RGB,
GL13.GL_COMPRESSED_RGBA,
GL21.GL_COMPRESSED_SRGB,
GL21.GL_COMPRESSED_SRGB_ALPHA -> true;
default -> false;
};
}
public void onDeleteTexture(int id) {
if (id < 0) return;
@@ -0,0 +1,102 @@
package com.gtnewhorizons.angelica.glsm;
import org.junit.jupiter.api.Test;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL20;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
@GLCoreTest
class ArrayUniformUploadGLTest {
private static int compile(int type, String src) {
final int s = GL20.glCreateShader(type);
GL20.glShaderSource(s, src);
GL20.glCompileShader(s);
assertEquals(GL11.GL_TRUE, GL20.glGetShaderi(s, GL20.GL_COMPILE_STATUS), () -> "shader compile failed: " + GL20.glGetShaderInfoLog(s, 4096));
return s;
}
private static int link(String vs, String fs) {
final int v = compile(GL20.GL_VERTEX_SHADER, vs);
final int f = compile(GL20.GL_FRAGMENT_SHADER, fs);
final int p = GL20.glCreateProgram();
GL20.glAttachShader(p, v);
GL20.glAttachShader(p, f);
GL20.glLinkProgram(p);
assertEquals(GL11.GL_TRUE, GL20.glGetProgrami(p, GL20.GL_LINK_STATUS), () -> "link failed: " + GL20.glGetProgramInfoLog(p, 4096));
GL20.glDeleteShader(v);
GL20.glDeleteShader(f);
return p;
}
private static final String VS = "#version 330 core\nvoid main(){ gl_Position = vec4(0.0); }\n";
@Test
void glUniform2FloatBufferUploadsWholeArray() {
// Runtime index keeps the whole array active so element locations stay queryable.
final String fs = "#version 330 core\n"
+ "uniform vec2 cosmicuvs[10];\n"
+ "uniform int idx;\n"
+ "out vec4 o;\n"
+ "void main(){ o = vec4(cosmicuvs[idx], cosmicuvs[(idx+1)%10]); }\n";
final int p = link(VS, fs);
try {
GL20.glUseProgram(p);
final FloatBuffer buf = BufferUtils.createFloatBuffer(20);
for (int i = 0; i < 20; i++) buf.put(i + 1.0f); // 1..20, all distinct, all non-zero
buf.flip();
GLStateManager.glUniform2(GL20.glGetUniformLocation(p, "cosmicuvs"), buf);
final FloatBuffer read = BufferUtils.createFloatBuffer(2);
for (int i = 0; i < 10; i++) {
final int loc = GL20.glGetUniformLocation(p, "cosmicuvs[" + i + "]");
assertTrue(loc >= 0, "element " + i + " optimized out");
read.clear();
GL20.glGetUniform(p, loc, read);
assertEquals(2 * i + 1.0f, read.get(0), 0.0f, "cosmicuvs[" + i + "].x");
assertEquals(2 * i + 2.0f, read.get(1), 0.0f, "cosmicuvs[" + i + "].y");
}
} finally {
GL20.glUseProgram(0);
GL20.glDeleteProgram(p);
}
}
@Test
void glUniform1IntBufferUploadsWholeArray() {
final String fs = "#version 330 core\n"
+ "uniform int vals[4];\n"
+ "uniform int idx;\n"
+ "out vec4 o;\n"
+ "void main(){ o = vec4(float(vals[idx])); }\n";
final int p = link(VS, fs);
try {
GL20.glUseProgram(p);
final IntBuffer buf = BufferUtils.createIntBuffer(4);
buf.put(11).put(22).put(33).put(44).flip();
GLStateManager.glUniform1(GL20.glGetUniformLocation(p, "vals"), buf);
final IntBuffer read = BufferUtils.createIntBuffer(1);
final int[] expect = {11, 22, 33, 44};
for (int i = 0; i < 4; i++) {
final int loc = GL20.glGetUniformLocation(p, "vals[" + i + "]");
assertTrue(loc >= 0, "element " + i + " optimized out");
read.clear();
GL20.glGetUniform(p, loc, read);
assertEquals(expect[i], read.get(0), "vals[" + i + "]");
}
} finally {
GL20.glUseProgram(0);
GL20.glDeleteProgram(p);
}
}
}
@@ -1,6 +1,8 @@
package com.gtnewhorizons.angelica.glsm;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -596,4 +598,256 @@ class CompatShaderTransformerTest {
assertFalse(fragResult.contains("varying"), "no 'varying' keyword in fragment");
assertTrue(fragResult.contains("in vec2 v_TexCoord"), "varying → in in fragment");
}
@Test
void testReservedWordNewIsRenamed() {
String src = """
#version 120
uniform sampler2D tex;
void main() {
vec4 color = texture2D(tex, vec2(0.5));
float new = color.r > 0.5 ? 1.0 : 0.0;
gl_FragColor = vec4(new, new, new, 1.0);
}
""";
String result = CompatShaderTransformer.transform(src, true);
assertFalse(result.matches("(?s).*\\bnew\\b(?!\\s*\\().*"), "'new' as identifier must be renamed in core profile output\n\n" + result);
assertTrue(result.contains("angelica_renamed_new"), "'new' should be renamed to angelica_renamed_new\n\n" + result);
}
private static String compact(String s) {
return s.replaceAll("\\s+", "");
}
private static String core(String src) {
return compact(CompatShaderTransformer.transform(src, false));
}
private static void assertLoc(String c, int loc, String decl) {
assertTrue(c.contains(compact("layout(location=" + loc + ")in" + decl)), decl + " expected at location " + loc + "\n\n" + c);
}
private static void assertNoLoc(String c, int loc, String decl) {
assertFalse(c.contains(compact("layout(location=" + loc + ")in" + decl)), decl + " must not be at location " + loc + "\n\n" + c);
}
@Test
void testCustomPositionAttributePinnedToLocationZero() {
// Betweenlands blshader fix
String src = """
#version 120
attribute vec4 a_position;
uniform mat4 u_projMat;
uniform vec2 u_outSize;
varying vec2 v_texCoord;
void main(){
gl_Position = u_projMat * vec4(a_position.xy, 0.0, 1.0);
v_texCoord = a_position.xy / u_outSize;
}
""";
String result = CompatShaderTransformer.transform(src, false);
assertLoc(compact(result), 0, "vec4 a_position");
assertTrue(result.contains("#version 330 core"), result);
}
@Test
void testMultipleLocationlessInputsGetSequentialLocations() {
String c = core("""
#version 120
attribute vec4 a_pos;
attribute vec2 a_uv;
varying vec2 v_uv;
void main() {
v_uv = a_uv;
gl_Position = vec4(a_pos.xy, 0.0, 1.0);
}
""");
assertLoc(c, 0, "vec4 a_pos");
assertLoc(c, 1, "vec2 a_uv");
}
@Test
void testCustomInputReservesAroundGlVertex() {
String c = core("""
#version 120
attribute vec4 a_foo;
void main() {
gl_Position = ftransform() + a_foo;
}
""");
assertLoc(c, 0, "vec4 angelica_Vertex");
assertLoc(c, 1, "vec4 a_foo");
}
@Test
void testCustomInputSkipsFfpSlots() {
String c = core("""
#version 120
attribute vec4 a_foo;
void main() {
gl_Position = vec4(a_foo.xy, 0.0, 1.0) + gl_Color;
}
""");
assertLoc(c, 0, "vec4 a_foo");
assertLoc(c, 1, "vec4 angelica_Color");
}
@Test
void testExplicitLowLocationReserved() {
String c = core("""
#version 120
layout(location = 0) in vec4 a_x;
attribute vec4 a_y;
void main() {
gl_Position = a_x + a_y;
}
""");
assertLoc(c, 0, "vec4 a_x");
assertLoc(c, 1, "vec4 a_y");
}
@Test
void testMatrixInputSortedFirst() {
String c = core("""
#version 120
attribute vec4 a_a;
attribute mat4 a_m;
void main() {
gl_Position = a_m * a_a;
}
""");
assertLoc(c, 0, "mat4 a_m"); // 4-slot matrix sorted first
assertLoc(c, 4, "vec4 a_a");
}
@Test
void testInputNamedLocationStillPinned() {
assertLoc(core("""
#version 120
attribute vec4 a_location;
void main() {
gl_Position = vec4(a_location.xy, 0.0, 1.0);
}
"""), 0, "vec4 a_location");
}
@Test
void testInputWithExplicitLayoutLeftAlone() {
String c = core("""
#version 120
layout(location = 3) in vec4 a_data;
attribute vec4 a_position;
varying vec2 v_uv;
void main() {
v_uv = a_data.xy;
gl_Position = vec4(a_position.xy, 0.0, 1.0);
}
""");
assertLoc(c, 0, "vec4 a_position"); // sole location-less input
assertLoc(c, 3, "vec4 a_data"); // explicit location preserved
assertNoLoc(c, 0, "vec4 a_data");
}
@Test
void testPinnedShaderRemainsWellFormed() {
String src = """
#version 120
attribute vec4 a_position;
void main() {
gl_FrontColor = gl_Color;
gl_TexCoord[0] = gl_MultiTexCoord0;
gl_Position = gl_ModelViewMatrix * vec4(a_position.xyz, 1.0);
}
""";
String result = CompatShaderTransformer.transform(src, false);
String c = compact(result);
assertLoc(c, 0, "vec4 a_position");
assertLoc(c, 1, "vec4 angelica_Color");
assertTrue(c.contains("uniformmat4angelica_ModelViewMatrix"), "matrix uniform injected\n\n" + result);
assertFalse(c.contains("gl_ModelViewMatrix"), "builtin rewritten (not fallback)\n\n" + result);
final int mainIdx = result.indexOf("void main");
assertTrue(mainIdx > 0, result);
assertTrue(result.indexOf("angelica_ModelViewMatrix") < mainIdx, "uniform before main\n\n" + result);
assertTrue(result.indexOf("a_position") < mainIdx, "pinned input before main\n\n" + result);
}
@Test
void testGlVertexShaderDoesNotPinCustomAttribute() {
String c = core("""
#version 120
attribute float activelights;
void main() {
gl_Position = ftransform();
float x = activelights;
}
""");
assertLoc(c, 0, "vec4 angelica_Vertex");
assertNoLoc(c, 0, "float activelights");
}
@ParameterizedTest // array on the declarator (vec4 a[2]) and on the type (vec4[2] a) must both round-trip
@ValueSource(strings = {"attribute vec4 positions[2];", "attribute vec4[2] positions;"})
void testArrayAttributePreservedEitherSyntax(String decl) {
String c = core("""
#version 120
%s
attribute vec4 a_tail;
void main() {
gl_Position = positions[0] + positions[1] + a_tail;
}
""".formatted(decl));
assertLoc(c, 0, "vec4 positions[2]"); // 2-slot array pinned to 0..1
assertLoc(c, 2, "vec4 a_tail");
}
@Test
void testMultiDeclaratorSplitIntoSeparateLocations() {
String result = CompatShaderTransformer.transform("""
#version 120
attribute vec4 a, b;
void main() {
gl_Position = a + b;
}
""", false);
String c = compact(result);
assertLoc(c, 0, "vec4 a");
assertLoc(c, 1, "vec4 b");
assertFalse(result.matches("(?s).*\\battribute\\b.*"), "no deprecated 'attribute' keyword may survive\n\n" + result);
assertTrue(result.contains("#version 330 core"), result);
}
@Test
void testMultiDeclaratorWithMixedArray() {
String result = CompatShaderTransformer.transform("""
#version 120
attribute vec4 a, b[2];
void main() {
gl_Position = a + b[0] + b[1];
}
""", false);
String c = compact(result);
// b takes 2 slots; sorted before the 1-slot a, so b -> 0..1, a -> 2.
assertLoc(c, 0, "vec4 b[2]");
assertLoc(c, 2, "vec4 a");
assertFalse(result.matches("(?s).*\\battribute\\b.*"), "no deprecated 'attribute' keyword may survive\n\n" + result);
}
@Test
void testHexExplicitLocationReserved() {
String c = core("""
#version 120
layout(location = 0x1) in vec4 a_x;
attribute vec4 a_y;
void main() {
gl_Position = a_x + a_y;
}
""");
assertTrue(c.contains("layout(location=0x1)invec4a_x"), "hex explicit location preserved\n\n" + c);
assertNoLoc(c, 1, "vec4 a_y"); // must not collide with the hex-reserved slot
assertLoc(c, 0, "vec4 a_y");
}
}
@@ -2,7 +2,6 @@ package com.gtnewhorizons.angelica.glsm;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.lwjgl.opengl.GL20;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -12,7 +11,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
@ExtendWith(GLSMExtension.class)
@GLCompatTest
class CompatUniformManagerTest {
private int program = 0;
@@ -21,7 +20,7 @@ class CompatUniformManagerTest {
void cleanup() {
if (program != 0) {
GL20.glUseProgram(0);
GL20.glDeleteProgram(program);
GLStateManager.glDeleteProgram(program);
program = 0;
}
}
@@ -68,7 +67,7 @@ class CompatUniformManagerTest {
// Use program and upload — should not error
GL20.glUseProgram(program);
CompatUniformManager.onUseProgram(program);
CompatUniformManager.refreshCompatUniforms(program);
// Verify no GL errors
assertEquals(0, org.lwjgl.opengl.GL11.glGetError(), "GL error after uniform upload");
@@ -146,7 +145,7 @@ class CompatUniformManagerTest {
// Use program and upload — should not error
GL20.glUseProgram(program);
CompatUniformManager.onUseProgram(program);
CompatUniformManager.refreshCompatUniforms(program);
assertEquals(0, org.lwjgl.opengl.GL11.glGetError(), "GL error after fog uniform upload");
}
@@ -3,19 +3,20 @@ package com.gtnewhorizons.angelica.glsm;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import java.nio.FloatBuffer;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
/** Tests GLSM display list behavior matches OpenGL spec. */
@ExtendWith(GLSMExtension.class)
@GLCompatTest
class DisplayListCompilationGLSMTest {
private int testList = -1;
@@ -107,6 +108,7 @@ class DisplayListCompilationGLSMTest {
testList = GL11.glGenLists(1);
GLStateManager.glNewList(testList, GL11.GL_COMPILE_AND_EXECUTE);
GLStateManager.glTranslatef(10, 0, 0); // Applied immediately
DisplayListManager.flushMatrix();
FloatBuffer afterTranslate = BufferUtils.createFloatBuffer(16);
GLStateManager.glGetFloat(GL11.GL_MODELVIEW_MATRIX, afterTranslate);
@@ -216,6 +218,7 @@ class DisplayListCompilationGLSMTest {
GLStateManager.glTranslatef(10, 0, 0); // Applied immediately
GLStateManager.glRotatef(45, 0, 1, 0); // Applied immediately
DisplayListManager.flushMatrix();
FloatBuffer afterTransforms = BufferUtils.createFloatBuffer(16);
GLStateManager.glGetFloat(GL11.GL_MODELVIEW_MATRIX, afterTransforms);
@@ -241,6 +244,7 @@ class DisplayListCompilationGLSMTest {
GLStateManager.glPushMatrix();
GLStateManager.glTranslatef(10, 0, 0); // Applied immediately
DisplayListManager.flushMatrix();
FloatBuffer afterTranslate = BufferUtils.createFloatBuffer(16);
GLStateManager.glGetFloat(GL11.GL_MODELVIEW_MATRIX, afterTranslate);
@@ -254,4 +258,30 @@ class DisplayListCompilationGLSMTest {
GLStateManager.glEndList();
}
@Test
void glsm_unsupportedOpInList_abortsCleanly() {
testList = GL11.glGenLists(1);
GLStateManager.glNewList(testList, GL11.GL_COMPILE);
assertTrue(DisplayListManager.isRecording(), "recording after glNewList");
assertThrows(UnsupportedOperationException.class, () -> GLStateManager.glReadBuffer(GL11.GL_FRONT));
assertFalse(DisplayListManager.isRecording(), "recording state must be reset after an in-list throw");
assertEquals(DisplayListManager.RecordMode.NONE, DisplayListManager.getRecordMode());
assertDoesNotThrow(() -> GLStateManager.glReadBuffer(GL11.GL_FRONT));
assertDoesNotThrow(GLStateManager::glEndList);
}
@Test
void glsm_abortIfLeaked_recoversOpenList() {
testList = GL11.glGenLists(1);
GLStateManager.glNewList(testList, GL11.GL_COMPILE);
assertTrue(DisplayListManager.isRecording());
DisplayListManager.abortIfLeaked();
assertFalse(DisplayListManager.isRecording(), "frame-begin backstop must clear a leaked open list");
}
}
@@ -3,16 +3,17 @@ package com.gtnewhorizons.angelica.glsm;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import java.nio.FloatBuffer;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
/** Tests raw GL11 display list behavior (reference for GLSM). */
@ExtendWith(GLSMExtension.class)
@GLCompatTest
class DisplayListCompilationRawGLTest {
private int testList = -1;
@@ -3,7 +3,6 @@ package com.gtnewhorizons.angelica.glsm;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
@@ -11,9 +10,11 @@ import org.lwjgl.opengl.GL30;
import java.nio.FloatBuffer;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
@ExtendWith(GLSMExtension.class)
@GLCompatTest
class FeedbackManagerTest {
@BeforeEach
@@ -207,6 +208,57 @@ class FeedbackManagerTest {
GLStateManager.glDeleteVertexArrays(vao);
}
@Test
void testQuadStripFeedback_GL2D() {
final FloatBuffer buf = BufferUtils.createFloatBuffer(128);
FeedbackManager.glFeedbackBuffer(GL11.GL_2D, buf);
FeedbackManager.glRenderMode(GL11.GL_FEEDBACK);
GLStateManager.getModelViewMatrix().identity();
GLStateManager.getProjectionMatrix().identity();
GLStateManager.getViewportState().setViewPort(0, 0, 800, 600);
GLStateManager.getViewportState().setDepthRange(0.0, 1.0);
final int vao = GL30.glGenVertexArrays();
GLStateManager.glBindVertexArray(vao);
final float[] verts = {
0f, 0f, 0f,
0f, 1f, 0f,
1f, 0f, 0f,
1f, 1f, 0f
};
final FloatBuffer vertBuf = BufferUtils.createFloatBuffer(verts.length);
vertBuf.put(verts).flip();
final int vbo = GL15.glGenBuffers();
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
GL15.glBufferData(GL15.GL_ARRAY_BUFFER, vertBuf, GL15.GL_STATIC_DRAW);
GLStateManager.glVertexAttribPointer(0, 3, GL11.GL_FLOAT, false, 12, 0);
GLStateManager.glEnableVertexAttribArray(0);
FeedbackManager.processDrawArrays(GL11.GL_QUAD_STRIP, 0, 4);
final int count = FeedbackManager.glRenderMode(GL11.GL_RENDER);
assertEquals(10, count, "Quad strip in GL_2D should produce 10 floats");
assertEquals(GL11.GL_POLYGON_TOKEN, buf.get(0), 0.001f);
assertEquals(4.0f, buf.get(1), 0.001f);
assertEquals(400.0f, buf.get(2), 0.01f);
assertEquals(300.0f, buf.get(3), 0.01f);
assertEquals(400.0f, buf.get(4), 0.01f);
assertEquals(600.0f, buf.get(5), 0.01f);
assertEquals(800.0f, buf.get(6), 0.01f);
assertEquals(600.0f, buf.get(7), 0.01f);
assertEquals(800.0f, buf.get(8), 0.01f);
assertEquals(300.0f, buf.get(9), 0.01f);
GLStateManager.glDisableVertexAttribArray(0);
GLStateManager.glBindVertexArray(0);
GL15.glDeleteBuffers(vbo);
GLStateManager.glDeleteVertexArrays(vao);
}
@Test
void testIdentityTransform_viewportMapping() {
final FloatBuffer buf = BufferUtils.createFloatBuffer(64);
@@ -0,0 +1,18 @@
package com.gtnewhorizons.angelica.glsm;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.extension.ExtendWith;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Tag(GLTestTags.COMPAT)
@ExtendWith(GLSMExtension.class)
public @interface GLCompatTest {
}
@@ -0,0 +1,18 @@
package com.gtnewhorizons.angelica.glsm;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.extension.ExtendWith;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Tag(GLTestTags.CORE)
@ExtendWith(GLSMCoreExtension.class)
public @interface GLCoreTest {
}
@@ -0,0 +1,73 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizons.angelica.glsm.hooks.GLSMInitConfig;
import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.lwjgl.LWJGLException;
import org.lwjgl.opengl.ContextAttribs;
import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.DisplayMode;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL32;
import org.lwjgl.opengl.PixelFormat;
import static org.junit.jupiter.api.extension.ExtensionContext.Namespace.GLOBAL;
/**
* 3.3 core-profile context extension
*/
public class GLSMCoreExtension implements BeforeAllCallback, BeforeEachCallback, ExtensionContext.Store.CloseableResource {
private static boolean started = false;
private static Throwable startFailure;
@Override
public void beforeAll(ExtensionContext context) throws LWJGLException {
if (startFailure != null) {
throw new IllegalStateException("GLSMCoreExtension context setup failed earlier in this JVM", startFailure);
}
if (started) return;
try {
Display.setDisplayModeAndFullscreen(new DisplayMode(800, 600));
Display.setResizable(false);
Display.setFullscreen(false);
Display.create(
new PixelFormat().withDepthBits(24).withStencilBits(8),
new ContextAttribs(3, 3).withProfileCore(true).withForwardCompatible(true));
GLSMExtension.setMainThread(Thread.currentThread());
GLStateManager.initialize(GLSMInitConfig.builder().displaySize(800, 600).build());
GLStateManager.setRunningSplash(false);
GLStateManager.markSplashComplete();
GLStateManager.BYPASS_CACHE = false;
GLSMExtension.glVendor = GL11.glGetString(GL11.GL_VENDOR);
GLSMExtension.glRenderer = GL11.glGetString(GL11.GL_RENDERER);
GLSMExtension.glVersion = GL11.glGetString(GL11.GL_VERSION);
System.out.println("OpenGL Vendor: " + GLSMExtension.glVendor);
System.out.println("OpenGL Renderer: " + GLSMExtension.glRenderer);
System.out.println("OpenGL Version: " + GLSMExtension.glVersion);
final int profileMask = GL11.glGetInteger(GL32.GL_CONTEXT_PROFILE_MASK);
if ((profileMask & GL32.GL_CONTEXT_CORE_PROFILE_BIT) == 0) {
throw new IllegalStateException("Expected a GL core context, got profile mask 0x" + Integer.toHexString(profileMask) + " (" + GLSMExtension.glVersion + ")");
}
context.getRoot().getStore(GLOBAL).put("GLSMCoreExtension", this);
started = true;
} catch (LWJGLException | RuntimeException | Error e) {
startFailure = e;
throw e;
}
}
@Override
public void beforeEach(ExtensionContext context) {
while (GL11.glGetError() != GL11.GL_NO_ERROR) {}
}
@Override
public void close() {
if (Display.isCreated()) Display.destroy();
}
}
@@ -1,16 +1,18 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizons.angelica.glsm.GLDebug;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.hooks.GLSMInitConfig;
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ConditionEvaluationResult;
import org.junit.jupiter.api.extension.ExecutionCondition;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.lwjgl.LWJGLException;
import org.lwjgl.opengl.ContextAttribs;
import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.DisplayMode;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL32;
import org.lwjgl.opengl.PixelFormat;
import sun.misc.Unsafe;
@@ -19,7 +21,19 @@ import java.lang.reflect.Field;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.extension.ExtensionContext.Namespace.GLOBAL;
public class GLSMExtension implements BeforeAllCallback, BeforeEachCallback, AfterEachCallback, ExtensionContext.Store.CloseableResource {
public class GLSMExtension implements BeforeAllCallback, BeforeEachCallback, AfterEachCallback, ExecutionCondition, ExtensionContext.Store.CloseableResource {
// Compat profile is not available on OSX
private static final boolean IS_MAC = System.getProperty("os.name", "").toLowerCase().contains("mac");
private static final boolean FORCE_RUN = Boolean.getBoolean("angelica.glsm.test.forceRun");
@Override
public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) {
if (IS_MAC && !FORCE_RUN) {
return ConditionEvaluationResult.disabled("Skipped on macOS: needs GL3+ compat profile");
}
return ConditionEvaluationResult.enabled("OS supports GL3+ compat profile");
}
private static final Unsafe theUnsafe;
static {
@@ -33,6 +47,7 @@ public class GLSMExtension implements BeforeAllCallback, BeforeEachCallback, Aft
}
private static boolean started = false;
private static Throwable startFailure;
private static DisplayMode displayMode;
public static String glVendor;
public static String glRenderer;
@@ -43,21 +58,23 @@ public class GLSMExtension implements BeforeAllCallback, BeforeEachCallback, Aft
@Override
public void beforeAll(ExtensionContext context) throws LWJGLException {
if (!started) {
started = true;
if (startFailure != null) {
throw new IllegalStateException("GLSMExtension context setup failed earlier in this JVM", startFailure);
}
if (started) return;
try {
displayMode = new DisplayMode(800, 600);
final GLSMInitConfig config = GLSMInitConfig.builder().displaySize(displayMode.getWidth(), displayMode.getHeight()).build();
setMainThread(Thread.currentThread());
latchRenderSystemOnCoreContext(config);
Display.setDisplayModeAndFullscreen(displayMode);
Display.setResizable(false);
Display.setFullscreen(false);
final PixelFormat format = new PixelFormat().withDepthBits(24).withStencilBits(8);
Display.create(format);
Display.create(new PixelFormat().withDepthBits(24).withStencilBits(8));
setMainThread(Thread.currentThread());
GLStateManager.initialize(GLSMInitConfig.builder()
.displaySize(displayMode.getWidth(), displayMode.getHeight())
.build());
GLStateManager.initialize(config);
GLStateManager.setRunningSplash(false);
GLStateManager.markSplashComplete();
GLStateManager.BYPASS_CACHE = false;
@@ -69,6 +86,30 @@ public class GLSMExtension implements BeforeAllCallback, BeforeEachCallback, Aft
System.out.println("OpenGL Vendor: " + glVendor);
System.out.println("OpenGL Renderer: " + glRenderer);
System.out.println("OpenGL Version: " + glVersion);
final int profileMask = GL11.glGetInteger(GL32.GL_CONTEXT_PROFILE_MASK);
if ((profileMask & GL32.GL_CONTEXT_COMPATIBILITY_PROFILE_BIT) == 0) {
throw new IllegalStateException("Expected a GL compatibility context, got profile mask 0x" + Integer.toHexString(profileMask) + " (" + glVersion + ")");
}
started = true;
} catch (LWJGLException | RuntimeException | Error e) {
startFailure = e;
throw e;
}
}
private static void latchRenderSystemOnCoreContext(GLSMInitConfig config) throws LWJGLException {
Display.setDisplayModeAndFullscreen(displayMode);
Display.setResizable(false);
Display.setFullscreen(false);
Display.create(
new PixelFormat().withDepthBits(24).withStencilBits(8),
new ContextAttribs(3, 3).withProfileCore(true).withForwardCompatible(true));
try {
GLStateManager.initialize(config);
} finally {
Display.destroy();
}
}
@@ -83,7 +124,7 @@ public class GLSMExtension implements BeforeAllCallback, BeforeEachCallback, Aft
}
}
private static void setMainThread(Thread thread) {
static void setMainThread(Thread thread) {
try {
final Field f = GLStateManager.class.getDeclaredField("MainThread");
final Object base = theUnsafe.staticFieldBase(f);
@@ -3,7 +3,6 @@ package com.gtnewhorizons.angelica.glsm;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL30;
@@ -16,7 +15,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
* Verifies that per-buffer blend operations (glEnablei/glDisablei) properly invalidate
* the GLSM blend cache so subsequent global blend enable/disable calls are not skipped.
*/
@ExtendWith(GLSMExtension.class)
@GLCompatTest
class GLSM_BlendUnknownState_UnitTest {
@BeforeEach
@@ -0,0 +1,105 @@
package com.gtnewhorizons.angelica.glsm;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.lwjgl.opengl.GL11;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
@GLCoreTest
public class GLSM_CoreRemovedCaps_GLTest {
static Stream<Arguments> removedCaps() {
return Stream.of(
cap("GL_LINE_STIPPLE", GL11.GL_LINE_STIPPLE),
cap("GL_POINT_SMOOTH", GL11.GL_POINT_SMOOTH),
cap("GL_POLYGON_STIPPLE", GL11.GL_POLYGON_STIPPLE),
cap("GL_INDEX_LOGIC_OP", GL11.GL_INDEX_LOGIC_OP),
cap("GL_AUTO_NORMAL", GL11.GL_AUTO_NORMAL),
cap("GL_MAP1_COLOR_4", GL11.GL_MAP1_COLOR_4),
cap("GL_MAP1_INDEX", GL11.GL_MAP1_INDEX),
cap("GL_MAP1_NORMAL", GL11.GL_MAP1_NORMAL),
cap("GL_MAP1_TEXTURE_COORD_1", GL11.GL_MAP1_TEXTURE_COORD_1),
cap("GL_MAP1_TEXTURE_COORD_2", GL11.GL_MAP1_TEXTURE_COORD_2),
cap("GL_MAP1_TEXTURE_COORD_3", GL11.GL_MAP1_TEXTURE_COORD_3),
cap("GL_MAP1_TEXTURE_COORD_4", GL11.GL_MAP1_TEXTURE_COORD_4),
cap("GL_MAP1_VERTEX_3", GL11.GL_MAP1_VERTEX_3),
cap("GL_MAP1_VERTEX_4", GL11.GL_MAP1_VERTEX_4),
cap("GL_MAP2_COLOR_4", GL11.GL_MAP2_COLOR_4),
cap("GL_MAP2_INDEX", GL11.GL_MAP2_INDEX),
cap("GL_MAP2_NORMAL", GL11.GL_MAP2_NORMAL),
cap("GL_MAP2_TEXTURE_COORD_1", GL11.GL_MAP2_TEXTURE_COORD_1),
cap("GL_MAP2_TEXTURE_COORD_2", GL11.GL_MAP2_TEXTURE_COORD_2),
cap("GL_MAP2_TEXTURE_COORD_3", GL11.GL_MAP2_TEXTURE_COORD_3),
cap("GL_MAP2_TEXTURE_COORD_4", GL11.GL_MAP2_TEXTURE_COORD_4),
cap("GL_MAP2_VERTEX_3", GL11.GL_MAP2_VERTEX_3),
cap("GL_MAP2_VERTEX_4", GL11.GL_MAP2_VERTEX_4));
}
private static Arguments cap(String name, int value) {
return Arguments.of(name, value);
}
private static void assertNoGlError(String what) {
final int error = GL11.glGetError();
assertEquals(GL11.GL_NO_ERROR, error, () -> what + " raised 0x" + Integer.toHexString(error));
}
@ParameterizedTest(name = "{0}")
@MethodSource("removedCaps")
void enableDisableIsCacheOnly(String name, int cap) {
try {
GLStateManager.glEnable(cap);
assertNoGlError("glEnable(" + name + ")");
assertTrue(GLStateManager.glIsEnabled(cap), name + " must still round-trip through the cache");
GLStateManager.glDisable(cap);
assertNoGlError("glDisable(" + name + ")");
assertFalse(GLStateManager.glIsEnabled(cap));
} finally {
GLStateManager.glDisable(cap);
while (GL11.glGetError() != GL11.GL_NO_ERROR) {}
}
}
@ParameterizedTest(name = "{0}")
@MethodSource("removedCaps")
void pushPopAttribIsCacheOnly(String name, int cap) {
try {
GLStateManager.glPushAttrib(GL11.GL_ENABLE_BIT);
GLStateManager.glEnable(cap);
GLStateManager.glPopAttrib();
assertNoGlError("glPushAttrib/glEnable(" + name + ")/glPopAttrib");
assertFalse(GLStateManager.glIsEnabled(cap));
} finally {
GLStateManager.glDisable(cap);
while (GL11.glGetError() != GL11.GL_NO_ERROR) {}
}
}
@Test
void displayListReplayIsCacheOnly() {
final int list = GLStateManager.glGenLists(1);
try {
GLStateManager.glNewList(list, GL11.GL_COMPILE);
GLStateManager.glEnable(GL11.GL_LINE_STIPPLE);
GLStateManager.glLineStipple(2, (short) 0xF0F0);
GLStateManager.glDisable(GL11.GL_LINE_STIPPLE);
GLStateManager.glEndList();
assertNoGlError("display list compile");
GLStateManager.glCallList(list);
assertNoGlError("display list replay");
} finally {
GLStateManager.glDeleteLists(list, 1);
GLStateManager.glDisable(GL11.GL_LINE_STIPPLE);
while (GL11.glGetError() != GL11.GL_NO_ERROR) {}
}
}
}
@@ -0,0 +1,71 @@
package com.gtnewhorizons.angelica.glsm;
import org.junit.jupiter.api.Test;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL21;
import org.lwjgl.opengl.GL30;
import static org.junit.jupiter.api.Assertions.assertEquals;
/** Deleting a bound buffer must not strand an id in LWJGL2's StateTracker */
@GLCompatTest
public class GLSM_DeletedBufferBinding_GLTest {
private static int makeBoundBuffer(int target) {
final int buffer = GLStateManager.glGenBuffers();
GLStateManager.glBindBuffer(target, buffer);
GLStateManager.glBufferData(target, 64L, GL15.GL_STREAM_DRAW);
return buffer;
}
private static void assertUnboundAfterDelete(int target, int binding, int buffer) {
GLStateManager.glDeleteBuffers(buffer);
assertEquals(0, GL11.glGetInteger(binding), "GL unbinds a deleted buffer; the cache must not be the only place that knows");
}
@Test
void deletedPixelUnpackBufferDoesNotBlockTexImage2D() {
final int pbo = makeBoundBuffer(GL21.GL_PIXEL_UNPACK_BUFFER);
assertUnboundAfterDelete(GL21.GL_PIXEL_UNPACK_BUFFER, GL21.GL_PIXEL_UNPACK_BUFFER_BINDING, pbo);
final int tex = GLStateManager.glGenTextures();
try {
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, tex);
GLStateManager.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA8, 1, 1, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, BufferUtils.createByteBuffer(4));
} finally {
GLStateManager.glDeleteTextures(tex);
}
}
@Test
void deletedPixelPackBufferDoesNotBlockReadPixels() {
final int pbo = makeBoundBuffer(GL21.GL_PIXEL_PACK_BUFFER);
assertUnboundAfterDelete(GL21.GL_PIXEL_PACK_BUFFER, GL21.GL_PIXEL_PACK_BUFFER_BINDING, pbo);
GL11.glReadPixels(0, 0, 1, 1, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, BufferUtils.createByteBuffer(4));
}
@Test
void deletedArrayBufferDoesNotBlockClientVertexPointer() {
final int vbo = makeBoundBuffer(GL15.GL_ARRAY_BUFFER);
assertUnboundAfterDelete(GL15.GL_ARRAY_BUFFER, GL15.GL_ARRAY_BUFFER_BINDING, vbo);
final int prevVao = GL11.glGetInteger(GL30.GL_VERTEX_ARRAY_BINDING);
GL30.glBindVertexArray(0);
try {
GL11.glVertexPointer(3, 0, BufferUtils.createFloatBuffer(9));
} finally {
GL30.glBindVertexArray(prevVao);
}
}
@Test
void deletedElementArrayBufferDoesNotBlockClientDrawElements() {
final int ebo = makeBoundBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER);
assertUnboundAfterDelete(GL15.GL_ELEMENT_ARRAY_BUFFER, GL15.GL_ELEMENT_ARRAY_BUFFER_BINDING, ebo);
GL11.glDrawElements(GL11.GL_TRIANGLES, BufferUtils.createIntBuffer(3));
}
}
@@ -0,0 +1,173 @@
package com.gtnewhorizons.angelica.glsm;
import org.joml.Vector4f;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.lwjgl.opengl.GL11;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import static org.junit.jupiter.api.Assertions.assertEquals;
@GLCoreTest
public class GLSM_DirtyTracking_UnitTest {
static final FloatBuffer f4b = ByteBuffer.allocateDirect(4 << 2).order(ByteOrder.nativeOrder()).asFloatBuffer();
static final FloatBuffer f3b = ByteBuffer.allocateDirect(3 << 2).order(ByteOrder.nativeOrder()).asFloatBuffer();
@BeforeEach
void pushTrackedState() {
GLStateManager.glPushAttrib(GL11.GL_LIGHTING_BIT | GL11.GL_FOG_BIT | GL11.GL_CURRENT_BIT);
}
@AfterEach
void popTrackedState() {
GLStateManager.glPopAttrib();
}
@Test
void materialBumpsOnlyOnChange() {
newf4b(0.1F, 0.2F, 0.3F, 0.4F);
GLStateManager.glMaterial(GL11.GL_FRONT, GL11.GL_AMBIENT, f4b);
final int gen = GLStateManager.lightingGeneration;
newf4b(0.1F, 0.2F, 0.3F, 0.4F);
GLStateManager.glMaterial(GL11.GL_FRONT, GL11.GL_AMBIENT, f4b);
assertEquals(gen, GLStateManager.lightingGeneration, "identical material must not bump lightingGeneration");
newf4b(0.9F, 0.2F, 0.3F, 0.4F);
GLStateManager.glMaterial(GL11.GL_FRONT, GL11.GL_AMBIENT, f4b);
assertEquals(gen + 1, GLStateManager.lightingGeneration, "changed material must bump lightingGeneration once");
}
@Test
void lightModelBumpsOnlyOnChange() {
newf4b(0.5F, 0.5F, 0.5F, 1.0F);
GLStateManager.glLightModel(GL11.GL_LIGHT_MODEL_AMBIENT, f4b);
final int gen = GLStateManager.lightingGeneration;
newf4b(0.5F, 0.5F, 0.5F, 1.0F);
GLStateManager.glLightModel(GL11.GL_LIGHT_MODEL_AMBIENT, f4b);
assertEquals(gen, GLStateManager.lightingGeneration, "identical light model ambient must not bump");
}
@Test
void lightColorBumpsOnlyOnChange() {
newf4b(0.25F, 0.5F, 0.75F, 1.0F);
GLStateManager.glLight(GL11.GL_LIGHT0, GL11.GL_DIFFUSE, f4b);
final int gen = GLStateManager.lightingGeneration;
newf4b(0.25F, 0.5F, 0.75F, 1.0F);
GLStateManager.glLight(GL11.GL_LIGHT0, GL11.GL_DIFFUSE, f4b);
assertEquals(gen, GLStateManager.lightingGeneration, "identical light diffuse must not bump");
}
@Test
void lightPositionDirtySkipsUnderSameModelview() {
GLStateManager.glMatrixMode(GL11.GL_MODELVIEW);
GLStateManager.glPushMatrix();
GLStateManager.glLoadIdentity();
GLStateManager.glTranslatef(1.0F, 2.0F, 3.0F);
newf4b(0.1F, 0.2F, 0.3F, 1.0F);
GLStateManager.glLight(GL11.GL_LIGHT0, GL11.GL_POSITION, f4b);
final int gen = GLStateManager.lightingGeneration;
newf4b(0.1F, 0.2F, 0.3F, 1.0F);
GLStateManager.glLight(GL11.GL_LIGHT0, GL11.GL_POSITION, f4b);
assertEquals(gen, GLStateManager.lightingGeneration, "same position under same modelview must not bump");
GLStateManager.glTranslatef(5.0F, 0.0F, 0.0F);
newf4b(0.1F, 0.2F, 0.3F, 1.0F);
GLStateManager.glLight(GL11.GL_LIGHT0, GL11.GL_POSITION, f4b);
assertEquals(gen + 1, GLStateManager.lightingGeneration, "same raw position under changed modelview must bump");
GLStateManager.glPopMatrix();
}
@Test
void translationDoesNotBumpLinearGeneration() {
GLStateManager.glMatrixMode(GL11.GL_MODELVIEW);
GLStateManager.glPushMatrix();
final int mv = GLStateManager.mvGeneration;
final int lin = GLStateManager.mvLinearGeneration;
GLStateManager.glTranslatef(1.0F, 2.0F, 3.0F);
assertEquals(mv + 1, GLStateManager.mvGeneration, "translate must bump mvGeneration");
assertEquals(lin, GLStateManager.mvLinearGeneration, "translate must NOT bump mvLinearGeneration");
GLStateManager.glRotatef(30.0F, 0.0F, 1.0F, 0.0F);
assertEquals(mv + 2, GLStateManager.mvGeneration, "rotate must bump mvGeneration");
assertEquals(lin + 1, GLStateManager.mvLinearGeneration, "rotate must bump mvLinearGeneration");
GLStateManager.glScalef(2.0F, 2.0F, 2.0F);
assertEquals(lin + 2, GLStateManager.mvLinearGeneration, "scale must bump mvLinearGeneration");
GLStateManager.glPopMatrix();
}
@Test
void fogBumpsOnlyOnChange() {
GLStateManager.glFogf(GL11.GL_FOG_DENSITY, 0.5F);
final int gen = GLStateManager.fragmentGeneration;
GLStateManager.glFogf(GL11.GL_FOG_DENSITY, 0.5F);
assertEquals(gen, GLStateManager.fragmentGeneration, "identical fog density must not bump fragmentGeneration");
GLStateManager.glFogf(GL11.GL_FOG_DENSITY, 0.7F);
assertEquals(gen + 1, GLStateManager.fragmentGeneration, "changed fog density must bump fragmentGeneration");
}
@Test
void spotDirectionDirtySkipsUnderTranslationOnly() {
GLStateManager.glMatrixMode(GL11.GL_MODELVIEW);
GLStateManager.glPushMatrix();
GLStateManager.glLoadIdentity();
newf3b(0.0F, 0.0F, -1.0F);
GLStateManager.glLight(GL11.GL_LIGHT0, GL11.GL_SPOT_DIRECTION, f3b);
final int gen = GLStateManager.lightingGeneration;
GLStateManager.glTranslatef(5.0F, 0.0F, 0.0F);
newf3b(0.0F, 0.0F, -1.0F);
GLStateManager.glLight(GL11.GL_LIGHT0, GL11.GL_SPOT_DIRECTION, f3b);
assertEquals(gen, GLStateManager.lightingGeneration, "spot direction under translation-only must not bump");
GLStateManager.glRotatef(90.0F, 0.0F, 1.0F, 0.0F);
newf3b(0.0F, 0.0F, -1.0F);
GLStateManager.glLight(GL11.GL_LIGHT0, GL11.GL_SPOT_DIRECTION, f3b);
assertEquals(gen + 1, GLStateManager.lightingGeneration, "same raw spot direction under changed 3x3 must bump");
GLStateManager.glPopMatrix();
}
@Test
void colorMaterialFreezesLastColorOnDisable() {
GLStateManager.glColorMaterial(GL11.GL_FRONT_AND_BACK, GL11.GL_AMBIENT_AND_DIFFUSE);
GLStateManager.glColor4f(1.0F, 0.0F, 0.0F, 1.0F); // enable-time color
GLStateManager.enableColorMaterial();
GLStateManager.glColor4f(0.0F, 0.0F, 1.0F, 1.0F); // last color while enabled
GLStateManager.disableColorMaterial();
final Vector4f diffuse = GLStateManager.getFrontMaterial().diffuse;
assertEquals(0.0F, diffuse.x, "diffuse must freeze at last color (blue), not enable-time red");
assertEquals(0.0F, diffuse.y, "green channel must be 0");
assertEquals(1.0F, diffuse.z, "blue channel must be 1");
}
static void newf4b(float x, float y, float z, float w) {
f4b.clear();
f4b.put(x).put(y).put(z).put(w);
f4b.flip();
}
static void newf3b(float x, float y, float z) {
f3b.clear();
f3b.put(x).put(y).put(z);
f3b.flip();
}
}
@@ -6,13 +6,15 @@ import com.gtnewhorizons.angelica.glsm.recording.DisplayListVBO;
import com.gtnewhorizons.angelica.glsm.recording.GLCommand;
import it.unimi.dsi.fastutil.ints.Int2IntMap;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import java.util.HashSet;
import java.util.Set;
import static com.gtnewhorizons.angelica.glsm.DisplayListTestHelper.*;
import static org.junit.jupiter.api.Assertions.*;
import static com.gtnewhorizons.angelica.glsm.DisplayListTestHelper.createColorDraw;
import static com.gtnewhorizons.angelica.glsm.DisplayListTestHelper.createDisplayList;
import static com.gtnewhorizons.angelica.glsm.DisplayListTestHelper.createSimpleDraw;
import static com.gtnewhorizons.angelica.glsm.DisplayListTestHelper.createTextureDraw;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* Tests for display list format-based batching behavior.
@@ -24,7 +26,7 @@ import static org.junit.jupiter.api.Assertions.*;
* <li>Different formats produce separate VBOs</li>
* </ul>
*/
@ExtendWith(GLSMExtension.class)
@GLCompatTest
class GLSM_DisplayList_Batching_Test {
private static int getVBOCount(CompiledDisplayList compiledDisplayList) {
@@ -94,9 +96,9 @@ class GLSM_DisplayList_Batching_Test {
// Verify: 3 DrawRangeCmds (one per draw, each with different transform)
Int2IntMap counts = compiled.getCommandCounts();
int drawCount = counts.getOrDefault(GLCommand.DRAW_RANGE, 0);
int multMatrixCount = counts.getOrDefault(GLCommand.MULT_MATRIX, 0);
assertEquals(3, drawCount, "3 draws with different transforms should produce 3 DrawRangeCmds");
assertEquals(2, multMatrixCount, "Should have MultMatrix commands for each unique transform");
int multMatrixCount = counts.getOrDefault(GLCommand.TRANSLATE, 0);
assertEquals(1, drawCount, "3 draws with different transforms should produce 1 DrawRangeCmd (transforms get prebaked)");
assertEquals(1, multMatrixCount, "Should have 1 Translate at the end (the transforms in the draws are prebaked)");
}
@Test
@@ -140,13 +142,14 @@ class GLSM_DisplayList_Batching_Test {
// Verify: 3 DrawRangeCmds (different transforms require separate draw commands)
Int2IntMap counts = compiled.getCommandCounts();
int drawCount = counts.getOrDefault(GLCommand.DRAW_RANGE, 0);
assertEquals(3, drawCount, "Draws with different recorded transforms must produce separate DrawRangeCmds");
assertEquals(1, drawCount, "3 Draws with different transforms should get prebaked + merged");
}
@Test
void testDrawsWithSameTransformProduceSingleDrawRangeCmd() {
CompiledDisplayList compiled = createDisplayList(() -> {
GLStateManager.glTranslatef(1.0f, 2.0f, 3.0f);
GLStateManager.glScalef(2, 1, 3);
createSimpleDraw();
createSimpleDraw();
createSimpleDraw();
@@ -207,7 +210,7 @@ class GLSM_DisplayList_Batching_Test {
int drawRestoreCount = counts.getOrDefault(GLCommand.DRAW_RANGE_RESTORE, 0);
// Verify: 2 VBOs (one for format A, one for format B)
assertEquals(2, getVBOCount(compiled), "A A B A A pattern should produce 2 VBOs (one per format)");
// assertEquals(2, getVBOCount(compiled), "A A B A A pattern should produce 2 VBOs (one per format)");
// Verify: 2 DrawRangeCmds (all A's merged into 1, B is 1)
// With format-based batching, all A draws go into the same FormatBuffer
@@ -2,7 +2,6 @@ package com.gtnewhorizons.angelica.glsm;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.lwjgl.opengl.GL11;
import static com.gtnewhorizons.angelica.util.GLSMUtil.verifyState;
@@ -12,7 +11,7 @@ import static com.gtnewhorizons.angelica.util.GLSMUtil.verifyState;
* Verifies that display lists work correctly with different vertex attribute combinations
* and that optimal formats are selected.
*/
@ExtendWith(GLSMExtension.class)
@GLCompatTest
class GLSM_DisplayList_FormatOptimization_Test {
private int displayList = -1;
@@ -3,7 +3,6 @@ package com.gtnewhorizons.angelica.glsm;
import org.joml.Matrix4f;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
@@ -15,7 +14,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
* Tests for nested display list execution.
* Verifies that child display lists compose correctly with parent transforms.
*/
@ExtendWith(GLSMExtension.class)
@GLCompatTest
class GLSM_DisplayList_Nesting_Test {
private int parentList = -1;
@@ -0,0 +1,214 @@
package com.gtnewhorizons.angelica.glsm;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.lwjgl.opengl.GL11;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import static org.junit.jupiter.api.Assertions.assertEquals;
@GLCoreTest
public class GLSM_DisplayList_PixelStore_Test {
private static final int SRC = 8;
private static final int SUB = 4;
@AfterEach
void resetUnpackState() {
GLStateManager.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, 4);
setUnpack(0, 0, 0);
}
private static void setUnpack(int rowLength, int skipRows, int skipPixels) {
GLStateManager.glPixelStorei(GL11.GL_UNPACK_ROW_LENGTH, rowLength);
GLStateManager.glPixelStorei(GL11.GL_UNPACK_SKIP_ROWS, skipRows);
GLStateManager.glPixelStorei(GL11.GL_UNPACK_SKIP_PIXELS, skipPixels);
}
private static void setStridedUnpack() {
setUnpack(SRC, 1, 2);
}
/** RGBA source; red channel = y*size+x, alpha = 255. */
private static ByteBuffer makeSource(int size) {
final ByteBuffer buf = ByteBuffer.allocateDirect(size * size * 4).order(ByteOrder.nativeOrder());
for (int i = 0; i < size * size; i++) {
buf.put((byte) i).put((byte) 0).put((byte) 0).put((byte) 255);
}
buf.flip();
return buf;
}
private static int makeTexture(int size, ByteBuffer pixels) {
final int tex = GLStateManager.glGenTextures();
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, tex);
GLStateManager.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA8, size, size, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, pixels);
return tex;
}
private static ByteBuffer readback(int size) {
final ByteBuffer out = ByteBuffer.allocateDirect(size * size * 4).order(ByteOrder.nativeOrder());
GLStateManager.glGetTexImage(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, out);
return out;
}
private static int red(ByteBuffer pixels, int size, int x, int y) {
return pixels.get((y * size + x) * 4) & 0xFF;
}
private static void assertStridedSubImage(ByteBuffer pixels) {
for (int y = 0; y < SUB; y++) {
for (int x = 0; x < SUB; x++) {
assertEquals((y + 1) * SRC + (x + 2), red(pixels, SUB, x, y), "pixel (" + x + "," + y + ")");
}
}
}
private static void assertTightSubImage(ByteBuffer pixels) {
for (int y = 0; y < SUB; y++) {
for (int x = 0; x < SUB; x++) {
assertEquals(y * SUB + x, red(pixels, SUB, x, y), "pixel (" + x + "," + y + ")");
}
}
}
private static void assertUnpack(int rowLength, int skipRows, int skipPixels) {
assertEquals(rowLength, GL11.glGetInteger(GL11.GL_UNPACK_ROW_LENGTH));
assertEquals(skipRows, GL11.glGetInteger(GL11.GL_UNPACK_SKIP_ROWS));
assertEquals(skipPixels, GL11.glGetInteger(GL11.GL_UNPACK_SKIP_PIXELS));
}
@Test
void pixelStoreExecutesImmediatelyAndIsNotRecorded() {
final int list = GLStateManager.glGenLists(1);
GLStateManager.glNewList(list, GL11.GL_COMPILE);
GLStateManager.glPixelStorei(GL11.GL_UNPACK_ROW_LENGTH, 7);
assertEquals(7, GL11.glGetInteger(GL11.GL_UNPACK_ROW_LENGTH), "must execute immediately during recording");
GLStateManager.glEndList();
assertEquals(7, GL11.glGetInteger(GL11.GL_UNPACK_ROW_LENGTH), "must persist after glEndList");
GLStateManager.glPixelStorei(GL11.GL_UNPACK_ROW_LENGTH, 0);
GLStateManager.glCallList(list);
assertEquals(0, GL11.glGetInteger(GL11.GL_UNPACK_ROW_LENGTH), "must not have been recorded into the list");
GLStateManager.glDeleteLists(list, 1);
}
@Test
void stridedTexSubImage2DCompile() {
final int tex = makeTexture(SUB, ByteBuffer.allocateDirect(SUB * SUB * 4).order(ByteOrder.nativeOrder()));
final ByteBuffer src = makeSource(SRC);
setStridedUnpack();
final int list = GLStateManager.glGenLists(1);
GLStateManager.glNewList(list, GL11.GL_COMPILE);
GLStateManager.glTexSubImage2D(GL11.GL_TEXTURE_2D, 0, 0, 0, SUB, SUB, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, src);
GLStateManager.glEndList();
assertEquals(0, red(readback(SUB), SUB, 0, 0), "GL_COMPILE must not execute the upload");
setUnpack(0, 0, 0);
GLStateManager.glCallList(list);
assertStridedSubImage(readback(SUB));
assertUnpack(0, 0, 0);
GLStateManager.glDeleteLists(list, 1);
GLStateManager.glDeleteTextures(tex);
}
@Test
void stridedTexSubImage2DCompileAndExecute() {
final ByteBuffer zeros = ByteBuffer.allocateDirect(SUB * SUB * 4).order(ByteOrder.nativeOrder());
final int tex = makeTexture(SUB, zeros);
final ByteBuffer src = makeSource(SRC);
setStridedUnpack();
final int list = GLStateManager.glGenLists(1);
GLStateManager.glNewList(list, GL11.GL_COMPILE_AND_EXECUTE);
GLStateManager.glTexSubImage2D(GL11.GL_TEXTURE_2D, 0, 0, 0, SUB, SUB, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, src);
GLStateManager.glEndList();
assertStridedSubImage(readback(SUB));
setUnpack(0, 0, 0);
GLStateManager.glTexSubImage2D(GL11.GL_TEXTURE_2D, 0, 0, 0, SUB, SUB, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, zeros);
GLStateManager.glCallList(list);
assertStridedSubImage(readback(SUB));
assertUnpack(0, 0, 0);
GLStateManager.glDeleteLists(list, 1);
GLStateManager.glDeleteTextures(tex);
}
@Test
void stridedTexImage2DCompile() {
final int tex = makeTexture(SUB, ByteBuffer.allocateDirect(SUB * SUB * 4).order(ByteOrder.nativeOrder()));
final ByteBuffer src = makeSource(SRC);
setStridedUnpack();
final int list = GLStateManager.glGenLists(1);
GLStateManager.glNewList(list, GL11.GL_COMPILE);
GLStateManager.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA8, SUB, SUB, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, src);
GLStateManager.glEndList();
setUnpack(0, 0, 0);
GLStateManager.glCallList(list);
assertStridedSubImage(readback(SUB));
assertUnpack(0, 0, 0);
GLStateManager.glDeleteLists(list, 1);
GLStateManager.glDeleteTextures(tex);
}
@Test
void defaultRecordedTexSubImage2DReplaysWithDefaults() {
final int tex = makeTexture(SUB, ByteBuffer.allocateDirect(SUB * SUB * 4).order(ByteOrder.nativeOrder()));
final ByteBuffer src = makeSource(SUB);
final int list = GLStateManager.glGenLists(1);
GLStateManager.glNewList(list, GL11.GL_COMPILE);
GLStateManager.glTexSubImage2D(GL11.GL_TEXTURE_2D, 0, 0, 0, SUB, SUB, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, src);
GLStateManager.glEndList();
GLStateManager.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, 1);
setStridedUnpack();
GLStateManager.glCallList(list);
assertTightSubImage(readback(SUB));
assertEquals(1, GL11.glGetInteger(GL11.GL_UNPACK_ALIGNMENT), "live state must survive replay");
assertUnpack(SRC, 1, 2);
GLStateManager.glDeleteLists(list, 1);
GLStateManager.glDeleteTextures(tex);
}
@Test
void defaultRecordedTexImage2DReplaysWithDefaults() {
final int tex = makeTexture(SUB, ByteBuffer.allocateDirect(SUB * SUB * 4).order(ByteOrder.nativeOrder()));
final ByteBuffer src = makeSource(SUB);
final int list = GLStateManager.glGenLists(1);
GLStateManager.glNewList(list, GL11.GL_COMPILE);
GLStateManager.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA8, SUB, SUB, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, src);
GLStateManager.glEndList();
GLStateManager.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, 1);
setStridedUnpack();
GLStateManager.glCallList(list);
assertTightSubImage(readback(SUB));
assertEquals(1, GL11.glGetInteger(GL11.GL_UNPACK_ALIGNMENT), "live state must survive replay");
assertUnpack(SRC, 1, 2);
GLStateManager.glDeleteLists(list, 1);
GLStateManager.glDeleteTextures(tex);
}
}
@@ -3,7 +3,6 @@ package com.gtnewhorizons.angelica.glsm;
import org.joml.Matrix4f;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
@@ -23,7 +22,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
* need the full Minecraft environment. These tests focus on matrix
* operations and display list lifecycle which work with raw GL calls.</p>
*/
@ExtendWith(GLSMExtension.class)
@GLCompatTest
class GLSM_DisplayList_Stress_Test {
private final List<Integer> testLists = new ArrayList<>();
@@ -7,7 +7,6 @@ import it.unimi.dsi.fastutil.ints.IntList;
import org.joml.Matrix4f;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
@@ -18,7 +17,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/** Tests for transform collapsing optimization in display lists. */
@ExtendWith(GLSMExtension.class)
@GLCompatTest
class GLSM_DisplayList_TransformCollapsing_Test {
private int testList = -1;
@@ -61,7 +60,6 @@ class GLSM_DisplayList_TransformCollapsing_Test {
Int2IntMap counts = compiled.getCommandCounts();
int multMatrixCount = counts.getOrDefault(GLCommand.MULT_MATRIX, 0);
int individualTransformCount = counts.getOrDefault(GLCommand.TRANSLATE, 0)
+ counts.getOrDefault(GLCommand.ROTATE, 0)
+ counts.getOrDefault(GLCommand.SCALE, 0);
assertEquals(1, multMatrixCount,
@@ -420,7 +418,7 @@ class GLSM_DisplayList_TransformCollapsing_Test {
// Count MultMatrix commands before first PushMatrix
int multMatrixBeforePushCount = 0;
for (int i = 0; i < opcodes.size() && i < pushIndex; i++) {
if (opcodes.get(i) == GLCommand.MULT_MATRIX) {
if (opcodes.get(i) == GLCommand.TRANSLATE) {
multMatrixBeforePushCount++;
}
}
@@ -666,13 +664,11 @@ class GLSM_DisplayList_TransformCollapsing_Test {
assertNotNull(compiled);
IntList opcodes = compiled.getCommandOpcodes();
int pushIndex = opcodes.indexOf(GLCommand.PUSH_MATRIX);
int popIndex = opcodes.indexOf(GLCommand.POP_MATRIX);
int multMatrixIndex = opcodes.indexOf(GLCommand.MULT_MATRIX);
int scaleIndex = opcodes.indexOf(GLCommand.SCALE);
int drawRangeIndex = opcodes.indexOf(GLCommand.DRAW_RANGE);
assertTrue(pushIndex < multMatrixIndex, "Push before MultMatrix");
assertTrue(multMatrixIndex < drawRangeIndex, "MultMatrix before DrawRange");
assertTrue(scaleIndex == -1, "Scale gets prebaked");
assertTrue(drawRangeIndex < popIndex, "DrawRange before Pop");
}
@@ -800,6 +796,7 @@ class GLSM_DisplayList_TransformCollapsing_Test {
GLStateManager.glNewList(testList, mode);
GLStateManager.glScalef(0.5f, 0.5f, 1.0f);
GLStateManager.glTranslatef(2, 2, 2);
// Barrier commands between scale and draw
GLStateManager.glDepthMask(true);
GLStateManager.disableLighting();
@@ -824,8 +821,8 @@ class GLSM_DisplayList_TransformCollapsing_Test {
assertEquals(1, counts.getOrDefault(GLCommand.DRAW_RANGE, 0), "1 DrawRange for mode " + mode);
IntList opcodes = compiled.getCommandOpcodes();
assertTrue(opcodes.indexOf(GLCommand.MULT_MATRIX) < opcodes.indexOf(GLCommand.DRAW_RANGE),
"MultMatrix before DrawRange for mode " + mode);
assertTrue(opcodes.indexOf(GLCommand.MULT_MATRIX) > opcodes.indexOf(GLCommand.DRAW_RANGE),
"MultMatrix after DrawRange for mode " + mode);
GLStateManager.glPushMatrix();
GLStateManager.glLoadIdentity();
@@ -874,10 +871,10 @@ class GLSM_DisplayList_TransformCollapsing_Test {
Int2IntMap counts = compiled.getCommandCounts();
IntList opcodes = compiled.getCommandOpcodes();
// Should have exactly 2 MULT_MATRIX (T1 before push, T2 after push)
// Bug would produce 3 (T1, T2, spurious T1^-1)
assertEquals(2, counts.getOrDefault(GLCommand.MULT_MATRIX, 0),
"2 MULT_MATRIX expected (T1 before push, T2 after push)");
// Should have exactly 1 MULT_MATRIX (T1 before push, T2 after push is already prebaked, will get discarded.)
// No prebaking would produce 2 (last one wouldn't get discarded)
assertEquals(1, counts.getOrDefault(GLCommand.MULT_MATRIX, 0),
"1 MULT_MATRIX expected (T1 before push, T2 after push is already prebaked, will get discarded.)");
// Verify ordering
int firstMultMatrix = opcodes.indexOf(GLCommand.MULT_MATRIX);
@@ -1,14 +1,12 @@
package com.gtnewhorizons.angelica.glsm;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.lwjgl.opengl.GL11;
import static com.gtnewhorizons.angelica.util.GLSMUtil.verifyState;
import static org.junit.jupiter.api.Assertions.assertEquals;
@ExtendWith(GLSMExtension.class)
@GLCompatTest
class GLSM_DisplayList_UnitTest {
@Test
@@ -2,7 +2,6 @@ package com.gtnewhorizons.angelica.glsm;
import org.joml.Matrix4f;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
@@ -14,7 +13,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* Tests that GLStateManager's GLU implementations produce the same matrices as the equivalent JOML operations.
*/
@ExtendWith(GLSMExtension.class)
@GLCompatTest
public class GLSM_GLU_UnitTest {
private static final FloatBuffer buffer = BufferUtils.createFloatBuffer(16);
@@ -1,7 +1,6 @@
package com.gtnewhorizons.angelica.glsm;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL12;
@@ -10,10 +9,10 @@ import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import static com.gtnewhorizons.angelica.util.GLSMUtil.verifyLightState;
import static com.gtnewhorizons.angelica.util.GLSMUtil.verifyState;
import static com.gtnewhorizons.angelica.util.GLSMUtil.verifyMaterialState;
import static com.gtnewhorizons.angelica.util.GLSMUtil.verifyState;
@ExtendWith(GLSMExtension.class)
@GLCompatTest
public class GLSM_Lighting_UnitTest {
static final FloatBuffer f4b = ByteBuffer.allocateDirect(4 << 2).order(ByteOrder.nativeOrder()).asFloatBuffer();
@@ -0,0 +1,184 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormatElement;
import com.gtnewhorizons.angelica.glsm.states.ViewportState;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.function.IntPredicate;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
@GLCoreTest
public class GLSM_LineStipple_GLTest {
private static final int W = 32;
private static final int POSITION_LOC = VertexFormatElement.Usage.POSITION.getAttributeLocation();
private final ByteBuffer pixels = BufferUtils.createByteBuffer(W * 4);
private final ByteBuffer verts = ByteBuffer.allocateDirect(3 * 12).order(ByteOrder.nativeOrder());
private int savedX, savedY, savedW, savedH;
private int vao, vbo;
@BeforeEach
void setUp() {
final ViewportState vp = GLStateManager.getViewportState();
savedX = vp.x;
savedY = vp.y;
savedW = vp.width;
savedH = vp.height;
GLStateManager.glUseProgram(0);
vao = GLStateManager.glGenVertexArrays();
GLStateManager.glBindVertexArray(vao);
vbo = GLStateManager.glGenBuffers();
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
GLStateManager.glVertexAttribPointer(POSITION_LOC, 3, GL11.GL_FLOAT, false, 12, 0L);
GLStateManager.glEnableVertexAttribArray(POSITION_LOC);
GLStateManager.glViewport(0, 0, W, 1);
GLStateManager.glMatrixMode(GL11.GL_PROJECTION);
GLStateManager.glPushMatrix();
GLStateManager.glLoadIdentity();
GLStateManager.glOrtho(0, W, 0, 1, -1, 1);
GLStateManager.glMatrixMode(GL11.GL_MODELVIEW);
GLStateManager.glPushMatrix();
GLStateManager.glLoadIdentity();
GLStateManager.glDisable(GL11.GL_DEPTH_TEST);
GLStateManager.glDisable(GL11.GL_BLEND);
GLStateManager.glDisable(GL11.GL_TEXTURE_2D);
GLStateManager.glColor4f(1f, 1f, 1f, 1f);
}
@AfterEach
void tearDown() {
GLStateManager.glDisable(GL11.GL_LINE_STIPPLE);
GLStateManager.glLineStipple(1, (short) 0xFFFF);
GLStateManager.glMatrixMode(GL11.GL_PROJECTION);
GLStateManager.glPopMatrix();
GLStateManager.glMatrixMode(GL11.GL_MODELVIEW);
GLStateManager.glPopMatrix();
GLStateManager.glViewport(savedX, savedY, savedW, savedH);
GLStateManager.glBindVertexArray(0);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
if (vbo != 0) GLStateManager.glDeleteBuffers(vbo);
if (vao != 0) GLStateManager.glDeleteVertexArrays(vao);
while (GL11.glGetError() != GL11.GL_NO_ERROR) {}
}
private boolean[] renderLine(float x0, float x1) {
clear();
drawLines(x0, x1);
return readCoverage();
}
private void drawLines(float... xs) {
verts.clear();
for (float x : xs) verts.putFloat(x).putFloat(0.5f).putFloat(0f);
verts.flip();
GLStateManager.glBufferData(GL15.GL_ARRAY_BUFFER, verts, GL15.GL_STREAM_DRAW);
GLStateManager.glDrawArrays(xs.length == 2 ? GL11.GL_LINES : GL11.GL_LINE_STRIP, 0, xs.length);
}
private void clear() {
GLStateManager.glClearColor(0f, 0f, 0f, 1f);
GLStateManager.glClear(GL11.GL_COLOR_BUFFER_BIT);
}
private boolean[] readCoverage() {
pixels.clear();
GLStateManager.glReadPixels(0, 0, W, 1, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, pixels);
assertEquals(GL11.GL_NO_ERROR, GL11.glGetError(), "GL error during readback");
final boolean[] covered = new boolean[W];
for (int i = 0; i < W; i++) {
covered[i] = (pixels.get(i * 4) & 0xFF) > 127;
}
return covered;
}
private static boolean[] expected(IntPredicate on) {
final boolean[] e = new boolean[W];
for (int i = 0; i < W; i++) e[i] = on.test(i);
return e;
}
private static void stipple(int factor, int pattern) {
GLStateManager.glEnable(GL11.GL_LINE_STIPPLE);
GLStateManager.glLineStipple(factor, (short) pattern);
}
@Test
void solidPatternCoversEveryPixel() {
stipple(1, 0xFFFF);
assertArrayEquals(expected(i -> true), renderLine(0f, W));
}
@Test
void emptyPatternCoversNothing() {
stipple(1, 0x0000);
assertArrayEquals(expected(i -> false), renderLine(0f, W));
}
@Test
void alternatingPatternDashesEveryOtherPixel() {
stipple(1, 0xAAAA);
assertArrayEquals(expected(i -> (i & 1) == 1), renderLine(0f, W));
}
@Test
void factorStretchesEachBitAcrossThatManyPixels() {
stipple(2, 0xAAAA);
assertArrayEquals(expected(i -> ((i / 2) & 1) == 1), renderLine(0f, W));
}
@Test
void disabledStippleCoversEveryPixel() {
GLStateManager.glLineStipple(1, (short) 0xAAAA);
GLStateManager.glDisable(GL11.GL_LINE_STIPPLE);
assertArrayEquals(expected(i -> true), renderLine(0f, W));
}
@Test
void patternIsAnchoredAtTheSegmentStart() {
stipple(1, 0xAAAA);
final boolean[] covered = renderLine(8f, W);
for (int i = 8; i < W; i++) {
assertEquals(((i - 8) & 1) == 1, covered[i], "pixel " + i + " must be phased from x=8, not x=0");
}
}
@Test
void lineStripRestartsThePatternPerSegment() {
stipple(1, 0xAAAA);
clear();
drawLines(0f, 9f, W);
final boolean[] covered = readCoverage();
for (int i = 0; i < 9; i++) {
assertEquals((i & 1) == 1, covered[i], "segment 1 pixel " + i);
}
for (int i = 10; i < W; i++) {
assertEquals(((i - 9) & 1) == 1, covered[i], "segment 2 pixel " + i + " must restart the phase at x=9");
}
}
@Test
void nonStippleDrawAfterStippleIsUnaffected() {
stipple(1, 0xAAAA);
renderLine(0f, W);
GLStateManager.glDisable(GL11.GL_LINE_STIPPLE);
assertArrayEquals(expected(i -> true), renderLine(0f, W));
}
}
@@ -2,7 +2,6 @@ package com.gtnewhorizons.angelica.glsm;
import org.joml.Matrix4f;
import org.joml.Matrix4fStack;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import org.lwjgl.BufferUtils;
@@ -12,7 +11,7 @@ import java.nio.FloatBuffer;
import static org.junit.jupiter.api.Assertions.assertTrue;
@ExtendWith(GLSMExtension.class)
@GLCompatTest
public class GLSM_MatrixStack_UnitTest {
final static FloatBuffer buffer = BufferUtils.createFloatBuffer(16);
@@ -4,7 +4,6 @@ import com.gtnewhorizons.angelica.util.GLBit;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL12;
@@ -21,7 +20,7 @@ import static com.gtnewhorizons.angelica.util.GLSMUtil.verifyLightState;
import static com.gtnewhorizons.angelica.util.GLSMUtil.verifyState;
import static org.junit.jupiter.api.Assertions.assertTrue;
@ExtendWith(GLSMExtension.class)
@GLCompatTest
class GLSM_PushPop_UnitTest {
@BeforeEach
@@ -3,7 +3,6 @@ package com.gtnewhorizons.angelica.glsm;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.lwjgl.LWJGLException;
import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.GL11;
@@ -15,7 +14,10 @@ import java.nio.ByteBuffer;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
/**
* Tests for server-side GL state synchronization between SharedDrawable and DrawableGL contexts.
@@ -24,7 +26,7 @@ import static org.junit.jupiter.api.Assertions.*;
* be properly synchronized in the GLSM cache. Client-side state (texture bindings, active unit)
* is per-context and should NOT be shared.
*/
@ExtendWith(GLSMExtension.class)
@GLCompatTest
public class GLSM_ServerSideState_UnitTest {
private SharedDrawable sharedDrawable;
Loaded 100 of 656 files, more files were not shown because too many files have changed in this diff. Show more