Compare commits

..
648 Commits
Author SHA1 Message Date
Eclipse-Sol 0a4d48964e Cache Iris Shaders (#2206) 2026-10-03 20:06:39 -07:00
Ranzu 231a2ab216 Add Darkmode Transformer target (#2205) 2026-10-03 15:28:37 -07:00
Jason Mitchell 61bb779dea Fix crash when mods measure text off-thread with a custom font (#2204) 2026-10-03 15:28:15 -07:00
Jason Mitchell 7cd97d1216 Fix HUD caching flicker and match non-cached GL state (#2201) 2026-10-03 13:19:29 -07:00
Eclipse-Sol fe82cd320a Plenty of fixes for GTNH (#2202) 2026-10-03 13:19:15 -07:00
Li Xiangrong c7a0f3a208 Default raw custom textures to bilinear filtering and clamping (#2198) 2026-10-03 11:57:30 -07:00
Matt Van Horn 338d94197c Settle frame pacing after an inconsistent vsync gate (#2197) 2026-10-03 11:51:30 -07:00
Death Fuel 75a0278138 Fix splash issues (#2200) 2026-10-03 11:42:57 -07:00
Jason Mitchell c97fb7004f Fix (#2196) 2026-10-02 22:49:14 -07:00
Jason Mitchell bdcb4d1882 Fix Complementary + Euphoria Patches ULTRA on SDL-GPU (#2195) 2026-10-02 22:40:06 -07:00
Jason Mitchell 37afddab5d Cache transformed and compiled shaders on disk (#2194) 2026-10-02 22:35:08 -07:00
Jason Mitchell d876a203da Cut more per-frame allocations in terrain and rendering (#2193) 2026-10-02 22:06:57 -07:00
Jason Mitchell 85892c0f7f Fix DSA binding restore and liquid colors (#2192) 2026-10-02 21:48:24 -07:00
Jason Mitchell 1587a12cd7 Work around SDL Metal fence bugs and pump during present (#2191) 2026-10-02 21:28:29 -07:00
Jason Mitchell 38b1d9a0c3 Speed up boot (#2190) 2026-10-02 20:33:42 -07:00
Jason Mitchell 12fa08ab46 Cut redundant GLSM state tracking (#2189) 2026-10-02 20:11:37 -07:00
Jason Mitchell 80e5b544c0 Fix voxeliation performance under SDL GPU (#2188) 2026-10-02 19:46:17 -07:00
Eclipse-Sol db7d328481 Stop shaders from forever breaking entity instancing (#2187) 2026-10-02 17:02:45 -07:00
Eclipse-Sol 9f19d94c5f Fix the look of EFR Boats under batching (#2186) 2026-10-02 16:33:30 -07:00
Eclipse-Sol bc08e4baed Fix signs being ungodly bright (#2185) 2026-10-02 22:49:38 +00:00
Ranzu 8387b4e9cd Add Darkmode Transformer target (#2184) 2026-10-02 15:20:26 -07:00
Jason Mitchell 6c888a634a Cut per-frame allocations in rendering mixins (#2182) 2026-10-02 15:15:18 -07:00
Eclipse-Sol fe67a22d5a Improve shader support for custom block rendering (#2176) 2026-10-01 20:37:51 -07:00
Ranzu 66d2a548fc Add more targets for darkmode transformer (#2178) 2026-10-01 20:36:51 -07:00
Death Fuel d16c1e7cf6 Internal format fix (#2179) 2026-10-01 20:36:21 -07:00
Eclipse-Sol a8e95f70c1 Improve biome blending and improve compat (#2180) 2026-10-01 20:35:02 -07:00
9cb35c67cd Dark mode text recoloring with resource packs configs (#2118)
Co-authored-by: Ranzu <[email protected]>
Co-authored-by: Alexdoru <[email protected]>
Co-authored-by: Jason Mitchell <[email protected]>
2026-09-30 16:12:54 -07:00
Li XiangrongandEclipse-Sol cbd16b1a76 Fix shadow pass receiving a full-bright lightmap constant and misreporting terrain renderStage (#2170)
* Fix shadow pass lightmap availability

* Report per-layer terrain renderStage

---------

Co-authored-by: Eclipse-Sol <[email protected]>
2026-09-30 12:51:38 -04:00
Jason Mitchell cd0a9608d3 Fix async atlas with KaizPatchX (#2175) 2026-09-30 08:53:41 -07:00
KAMKEEL fa71eda3db Font Texture Crash Fix on Splash Screen (#2169) 2026-09-29 19:18:16 -07:00
KAMKEEL 9705722469 Fix Depth Mask Issue for Nametags, etc for Text (#2168) 2026-09-29 18:49:05 -07:00
Lumarin 977db3d604 Add option to change mouse movement from Cinematic to Standard while in Angelica Zoom (#2166) 2026-09-29 17:02:55 -07:00
Jason Mitchell c10d30b2da Parallelize atlas loading and fix SDL-GPU mip-level targets (#2167) 2026-09-29 16:57:53 -07:00
micvogandEclipse-Sol 2f4ac2a9b7 Apply the baked transform to vertices written directly into BakedTransformCapture (#2164)
While a direct capture is active, Tessellator.instance is the capture itself, so
RenderBlocks.renderBlockAsItem writes its vertices directly instead of going
through interceptDraw. Its own rotate/translate was therefore never baked into
the cached block mesh, which left held and dropped blocks off-center (e.g. the
snow golem's pumpkin).

Co-authored-by: Eclipse-Sol <[email protected]>
2026-09-29 18:26:31 -04:00
Death Fuel 9081f4c3e8 Various font improvements and fixes (#2165) 2026-09-29 14:58:53 -07:00
Eclipse-Sol 1f274a64ea TESR state and batching leak fixes (#2162) 2026-09-28 19:28:45 -07:00
micvog 9814a96b10 Fix attrib stack overflow from world-less TESR item renderers (#2159) 2026-09-28 07:05:46 -07:00
Eclipse-Sol af599f292e Fix Iris issues with instancing and batching (#2158) 2026-09-27 17:21:39 -07:00
Jason Mitchell 7bb5bfd45c Emulate color logic ops and fix pixel readback on SDL-GPU (#2157) 2026-09-27 15:33:04 -07:00
Jason Mitchell 74ea5c4709 Add in-game Tracy capture (#2156) 2026-09-27 14:16:01 -07:00
Eclipse-Sol 233c74e0d7 Better Biome Blending (#2152) 2026-09-27 13:19:48 -07:00
Jason Mitchell e17b45c842 Fix NEI items vanishing with ModularUI2 screens open on SDL-GPU (#2153) 2026-09-27 12:26:53 -07:00
Jason Mitchell c9d46e70e0 Update to Celeritas 2.5.12 with translucency sorting v3 (#2151) 2026-09-27 09:37:40 -07:00
Jason Mitchell a8c29fa3f9 Give each GL drawable its own GLSM context (#2142) 2026-09-26 22:31:07 -07:00
Eclipse-Sol 7c28a69bfe Optimize Enchant Glint Rendering (#2140)
* initial commit for enchant-glint-optimizations

* Fix spacing issues and remove GlintCompatibility.java
2026-09-26 19:21:21 -04:00
Eclipse-Sol 7f3fb35ebb Use actual camera when doing cloud related culling and fuctions (#2139) 2026-09-26 14:58:03 -07:00
Jason Mitchell 44c930f4ab Fix scissor clears wiping the whole screen on SDL-GPU (#2138) 2026-09-26 13:54:10 -07:00
Jason Mitchell b76284048a Fix shader packs in modded dimensions (#2137)
* Use world0 for unmapped dimensions
* Fix iris warnings for shaderpacks

Fixes https://github.com/GTNewHorizons/Angelica/issues/2128
2026-09-26 13:18:17 -04:00
Jason Mitchell 762c5661eb Stop blocking chunk workers on deferred ISBRH blocks (#2136) 2026-09-26 08:44:22 -07:00
Jason Mitchell 1babfd968c Properly reset/flush state for crash interceptors (#2133) 2026-09-25 20:43:05 -07:00
Eclipse-Sol 888a4de0c9 Add Galaxy Space compat and fix culling issues (#2131) 2026-09-25 19:18:45 -07:00
DarkShadow44 a23cefba92 Only enable shader DH compat if DH is enabled (#2130) 2026-09-23 17:42:45 -07:00
Eclipse-Sol f2c07dea28 Weather instancing (#2127)
* Initial commit for weather-instancing

* attempt to make the builder less ugly
2026-09-22 19:27:05 -04:00
Jason Mitchell 53776684c8 Properly handle display lists with the push/pop optimizations (#2126) 2026-09-22 09:23:49 -07:00
Jason Mitchell 9dd60f6a75 Render pass and state stack optimizations (#2124) 2026-09-21 20:52:51 -07:00
Alexdoru 87c83f0645 fix asm class dumper following rfb changes (#2123) 2026-09-21 12:35:37 -07:00
Eclipse-Sol c578970a66 Fix edge cases with cloud wraps (#2122) 2026-09-20 17:16:03 +00:00
Eclipse-Sol ef68af5850 Fix texture filtering sampling regression (#2115) 2026-09-19 10:12:14 -07:00
James b6a4497fb3 Redirected Util.checkGLError to respect disableErrorChecks (#2110) 2026-09-18 22:33:17 -07:00
Eclipse-Sol 939e31461a More Cloud Optimizations (#2114) 2026-09-18 21:53:50 -07:00
Eclipse-Sol 02f0b0fca1 Fix mipmaps bleeding (#2112) 2026-09-17 20:29:53 -07:00
Nick Smith b7d439e6b1 Added abutting block detection and processing (#2068) 2026-09-16 16:09:40 -07:00
KAMKEEL c36647af34 Hex Text and other Font Mod Compat System (#1972) 2026-09-16 16:01:16 -07:00
Eclipse-Sol f787801a0e Fix borking modded mobs with broken eyes (#2108) 2026-09-16 16:00:09 -07:00
Algent f192793395 Fix microblock transparency in the hotbar (#2107) 2026-09-16 15:48:26 -07:00
Jason Mitchell 3876f7e693 Instance more things (#2104)
* Flyby: pin the start origin and fly just above the route's terrain

* Instancing: particles, dropped items, entity cubes + shadow batching and some SDL-GPU fixes
2026-09-16 14:56:24 -07:00
danyadev 94ee37bfed fix resize handling on non-main thread for lwjgl2 (#2106) 2026-09-16 13:59:15 -07:00
Eclipse-Sol 26c161f0e8 Improve upon Texture Filtering Work (#2096) 2026-09-15 16:00:43 -07:00
Jason Mitchell 75639d3b73 Flyby improvements (#2100) 2026-09-14 19:32:31 -07:00
Jason Mitchell a07cecb879 Fix some GLSM bypasses (#2099) 2026-09-14 14:22:19 -07:00
kurrycat 81b82748ce SDL: flush vertex attribs + deferred texture uploads before readback (#2095) 2026-09-13 11:00:20 -07:00
Arvid JunehagandMartin Robertz 8d0c90c423 Updated the list of blocks to get rotated (#2094)
Co-authored-by: Martin Robertz <[email protected]>
2026-09-13 01:33:21 +00:00
kurrycat 8ce6a2f8c6 Fix cloud elevation setting (#2093) 2026-09-12 14:31:26 -07:00
DarkShadow44 2aa4ac7509 DH: Fix shaderpack fading (#2091) 2026-09-12 09:43:28 -07:00
DarkShadow44 8f98151049 Remove superflous glVertexAttribPointer call (#2090) 2026-09-12 09:16:27 -07:00
Eclipse-Sol 2b70318327 Fix issues with Schematica (#2088) 2026-09-09 17:51:39 -07:00
Jason Mitchell 3b4ebcf611 Update Celeritas (#2087) 2026-09-09 08:31:31 -07:00
Jason Mitchell 2845c9e078 FPS pacer work (#2086) 2026-09-08 23:01:21 -07:00
danyadev 6f054a09a9 properly clear PBRAtlasTexture (#2085) 2026-09-08 17:38:54 -07:00
Eldrinn Elantey 37a10eee3f Fix tile entity pass markers skipped on newer class files (#2075) 2026-09-06 16:03:08 -07:00
Caedis 744c1f971f Replace natural texture rotation hash (#2073) 2026-09-06 10:29:28 -07:00
Philip Xu 32d1c27b9c Fix for texture being mistakenly turned off by Iris (#2072) 2026-09-06 09:53:49 -07:00
Philip Xu f180894da7 Fixes TESR texts (travel anchors) depth test bug (#2071) 2026-09-06 09:44:29 -07:00
danyadev bb67f82701 optimize resource reload (#2070) 2026-09-06 09:43:18 -07:00
danyadev 73f5556595 support custom fonts on macos (#2066) 2026-09-05 21:09:41 -07:00
Jarnexis 19bc7cbaed Add Ukrainian (uk_UA) localization and translate documentation (#1990) 2026-09-06 03:40:54 +00:00
danyadev d189a644e2 Fix custom font rendering on BetterLoadingScreen splash (#2063) 2026-09-04 16:03:43 -07:00
danyadev 94ba40ae4d Activate FFP when enabling with program 0 (#2064) 2026-09-04 16:03:13 -07:00
Jason Mitchell 961b23fd2d Fix tests (#2065) 2026-09-04 15:45:27 -07:00
Nick Smith 61b5301f1a CTM Transitions - neighbor detection and influence (#2047) 2026-09-03 22:33:18 -07:00
Jakob Erber 0e539ae034 Synchronize CubicChunks tile entity mutations (#2031) 2026-09-03 22:32:49 -07:00
Chitak 985 e581326641 Fix animate-only-visible-textures setting (#2054) 2026-09-03 22:32:14 -07:00
kurrycat c707cc51f1 Use bound texture instead of hardcoded vanilla textures for cached vanilla model rendering (#2037) 2026-09-03 22:31:41 -07:00
Nick Smith 7d890bf3e7 Enable CTM mining/breaking overlays (#2052) 2026-09-03 22:30:39 -07:00
Eclipse-SolandJason Mitchell ffc324bab6 POM Support (#1927)
Co-authored-by: Jason Mitchell <[email protected]>
2026-09-03 22:28:20 -07:00
Jason Mitchell 0dfffca88e Flyby and OSX improvements (#2061) 2026-09-03 22:27:35 -07:00
Alexdoru a66db1517a fix world client leaks in bop fog handlers (#2056) 2026-09-02 11:15:25 -07:00
Jason Mitchell 269b7245d3 Make dynamic lights config less heavy (#2051) 2026-08-30 13:02:07 -07:00
Wokafishh 67f2484976 Blockrotations/Natural Textures (#2020) 2026-08-30 11:16:21 -07:00
KAMKEELandJason Mitchell 1c8687e6fd Add Secondary Color Support to Angelica (#2046)
Co-authored-by: Jason Mitchell <[email protected]>
2026-08-30 10:26:08 -07:00
Jason Mitchell e6bbd155da Stop hardcoding some translations (#2049) 2026-08-30 09:40:27 -07:00
Ranzu 2f64fef6a6 Fix color leak getting into guis (Broken ColorUtils) (#2045) 2026-08-27 14:19:55 -07:00
boubou19 b0fd1ea46e Fix some CTM Allocations (#2044) 2026-08-27 11:59:02 -07:00
ah-OOG-ah 6d8cbf9c2f Add a flag to StateAwareTesselator for per-quad directional shading (#1989)
* Add a flag for no directional shading

* Mark StateAwareTessellator as an API

* Reset internal state better
2026-08-19 18:16:04 +00:00
boubou19 096dd01446 Avoid rescanning atlas for each CTM tile (#2029) 2026-08-19 09:32:42 -07:00
Eclipse-SolandMartin Robertz 22c9479946 Many bug fixes (#2017)
Co-authored-by: Martin Robertz <[email protected]>
2026-08-19 08:55:43 -07:00
Jason Mitchell b2f597a73a SDL presenter fixes and frame pacer adjustments (#2025) 2026-08-16 17:59:33 -07:00
Jason Mitchell 553dec5b6d Mark tiles at transform time instead of using reflection (#2024) 2026-08-16 16:05:04 -07:00
Jason Mitchell bc8116006c Tracy 0.14 (#2019) 2026-08-15 19:09:36 -07:00
Jason Mitchell bf9492dffb Defer ticking on TEs we eagerly create until they get a description packet (#2018) 2026-08-15 17:04:50 -07:00
Jason Mitchell 69aed42b89 Sign Fix (#2016) 2026-08-15 14:32:50 -07:00
KAMKEELandJason Mitchell ffe54f61b9 Fixes World Lighting to Text Rendering so its not Fullbright (#1976)
* Fixes World Lighting to Text Rendering so its not Fullbright

* Carry lightmap state per text segment

* Text lighting: per-segment lightmap uniform, fix GL_RESCALE_NORMAL
* per segment uniform instead of stride increase
* Shade text based on GL light state
* fix u_NormalScale
* GL_LIGHT > 1 usage warnings (not currently handled)

---------

Co-authored-by: Jason Mitchell <[email protected]>
2026-08-14 21:34:58 -07:00
Jason Mitchell c86e1de9f6 TE and world access thread safety for rendering, and remesh on first TE update packet. (#2014) 2026-08-14 20:21:13 -07:00
brandyn 6af401c834 fix starminer rotation & optimize starminer rotateCorpseByGravity (#2005)
* fix starminer rotation & optimize starminer rotateCorpseByGravity

fixes #487

* remove Object[] allocation

* add shared helper,  fix NPEs, fallbacks
2026-08-13 11:27:43 -07:00
Jason Mitchell b17bb42c49 vsync adjustments: on/mailbox/off + windows input lag fix + glsl parsing optimizations (#2009) 2026-08-12 21:20:10 -07:00
Jason Mitchell 3659b59db9 Adjust vsync and fps limiters, and fix WSR with player models (#2002) 2026-08-09 20:06:53 -07:00
Jason Mitchell 8c31eec525 SDL followup fixes (#2001)
* Deploy fixes
* Narrow transformer exclusions for Legends mods, and mixin priority for CustomNPC
* Journeymap 6 stencil/depth fix
* OpenLTW swizzle workaround
2026-08-09 13:43:46 -07:00
Jason Mitchell 166116d692 More fixes (#1995) 2026-08-08 23:53:06 -07:00
Jason Mitchell 4b21bc77b6 A few fixes (#1994) 2026-08-08 18:29:26 -07:00
Jason Mitchell 8da561d3b6 SDL GPU backend, TESR Batching/caching API, Tracy Integration (#1993) 2026-08-08 15:53:58 -07:00
ah-OOG-ah 413df76ad4 Fix compat for the original battlegear 2 (#1992)
* Fix compat for the original battlegear 2

* oops
2026-08-07 15:54:59 -04:00
boubou19 18fe05eda8 Do not concat strings from glCallList when debug is off (#1988) 2026-08-04 13:37:17 -07:00
ah-OOG-ah 4fa8cfe3e4 Bump Gradle and 3ify dep (#1986) 2026-08-04 05:05:23 +00:00
Eclipse-Sol b5dd1c04ce Generalize non-terrain block's material ID setting (#1981) 2026-08-02 13:10:38 -07:00
Algent 6c5eedb7c1 Optimize item frame rendering (#1964) 2026-08-01 20:16:16 -07:00
Eldrinn-Elantey 9d5eb01091 Remove dead getCharWidth ModifyConstant (#1983) 2026-08-01 16:01:10 -07:00
XLin0mu 0d54a65026 Fix Mekanism crash when it cast chunkTileEntityMap to HashMap (#1982) 2026-08-01 21:41:28 +00:00
Midnight145 8148d8f927 RenderChunkEvent for fluxloading to avoid mixin (#1974) 2026-08-01 14:27:25 -07:00
Jason Mitchell 5c441e4418 Stupid splash thread (#1973) 2026-07-28 18:39:27 -07:00
Jason Mitchell 78562f12d9 Do less work (#1971) 2026-07-28 15:24:36 -07:00
CRIMSTAL 031456f224 fix(DisplayListVBO): guard against null VAO and zero-count draw during display list replay (#1969) 2026-07-28 13:28:59 -07:00
Jason Mitchell ff35c684ca Cache a few more things on glsm (#1970) 2026-07-28 13:28:25 -07:00
Eclipse-Sol 5c4ef022d5 Make mipmap attribution hook not suck and thread-safe (#1967) 2026-07-27 19:14:19 -07:00
Jason Mitchell 4eb837a2c0 Chunk -> world coords (#1966) 2026-07-27 10:54:26 -07:00
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
Jason Mitchell 0ad69f3433 FINE, we'll work with java8/forge. 2026-03-28 20:09:42 -07:00
Fabian Maurer 0dabecc784 Handle GL_POLYGON in legacy conversion 2026-03-28 14:41:23 -07:00
Jason Mitchell 9b3fad4c6d Ensure sufficient coverage + optimize the check 2026-03-28 13:16:54 -07:00
Kaiz e1e8256861 fix: crash when client-side vertex attrib pointers are used in core profile 2026-03-28 13:16:54 -07:00
Jason Mitchell e65c01c2a3 Handle method handles in invokedynamic 2026-03-28 12:46:19 -07:00
Jason Mitchell b9de4df18e Update alpha test to hopefully work with RTM and AE2 (#1597) 2026-03-28 09:40:10 -07:00
Kotmatross a5d31cd1b6 Return phaseChangeNotifier (#1596) 2026-03-28 09:15:46 -07:00
DarkShadow44 29f8d718cc Pass texturemap to composite shader and honor texture replacements (#1589) 2026-03-27 19:07:45 -07:00
12411234 52b0db2566 Fix missing glGetTexImage method crash when using XaerosWorldMap (#1592) 2026-03-27 19:02:51 -07:00
Eclipse-Sol e9eb45f4f2 Centers the Wither Armor (#1590) 2026-03-27 18:37:17 -07:00
Jason Mitchell 92e28a4cd5 Blur Compat (#1593) 2026-03-27 18:32:24 -07:00
Jason Mitchell f46a86889c Fix crash with fancy block particles. (#1591) 2026-03-27 17:52:14 -07:00
Jason MitchellandAlexdoru c827d446d5 External Redirector API (#1580)
Co-authored-by: Alexdoru <[email protected]>
2026-03-27 11:20:29 -07:00
Eclipse-Sol 00ef355ce8 Fix sky lighting on drawCrossSquare rendered blocks. (#1576) 2026-03-25 18:58:40 -07:00
Kotmatross 1282056fff Fix blend leak in BatchingFontRenderer (#1575) 2026-03-25 15:34:43 -07:00
Alexdoru 6c422d47a2 Make the mod no-op on server (#1574) 2026-03-25 12:59:23 -07:00
DarkShadow44 057ea8035c Distant Horizons shader support (#1553) 2026-03-25 10:48:14 -07:00
Jason Mitchell 3ea47c8385 Various fixes (#1571) 2026-03-24 22:17:40 -07:00
Eclipse-Sol 306d732d81 Fixes 3rd person enchant glints z-fighting with the item model (#1569) 2026-03-24 17:24:23 -07:00
Jason Mitchell a483669579 Maybe fix botania mana pool rendering 2026-03-23 21:09:33 -07:00
Eclipse-SolandJason Mitchell 53996500d7 Fix modded enchant glints not being animated (#1566)
Co-authored-by: Jason Mitchell <[email protected]>
2026-03-23 22:41:29 -04:00
Jason Mitchell 04dd8ce6c7 narrow scope of mixin per Eclipse 2026-03-23 19:24:16 -07:00
Jason Mitchell 8c9edfa4c3 Fix alpha test 2026-03-23 19:24:16 -07:00
Jason Mitchell 3f44195756 Change compat config 2026-03-23 09:27:56 -07:00
Jason Mitchell c98f056d74 ASM Hook Compat
* Adds a config to allow disabling
* Narrows Advanced LightSabers
* Narrows Alfheim (unreleased versions have compat, might make this version restricted)
2026-03-23 09:00:24 -07:00
Jason Mitchell 7e25f5d03e tweak wording 2026-03-22 18:41:58 -07:00
Jason Mitchell b5553654d0 More pass throughs 2026-03-22 18:41:58 -07:00
Jason Mitchell a3fffb47be Ban GL usage.
* Everything must go through GLSM/Render Backend
2026-03-22 11:56:08 -07:00
Jason Mitchell 09cc557be0 Clear cache on reload shaders 2026-03-22 11:07:37 -07:00
SpacEagle17 aff5ff7e7b Add a second profiles button to Angelica 2026-03-22 10:09:23 -07:00
Jason Mitchell e7d08a53b6 Add debug context 2026-03-22 10:08:49 -07:00
Jason Mitchell 3b2a0af5c4 Add missing overrides 2026-03-22 10:08:49 -07:00
Jason Mitchell 8c44e79990 Add always translucent config for brandyn 2026-03-22 10:08:49 -07:00
Jason Mitchell d2e491ed47 Misc fixes
* Add missing glMapBuffer overload
* Keep track of bound pixel buffer and suspend when needed
2026-03-21 22:07:15 -07:00
Jason Mitchell bccc5b00ca Supernova Support 2026-03-21 22:06:38 -07:00
Jason Mitchellandbrandyn 5ed26f3af7 Fix font alpha test transparency and BlockGrass subclass rendering with Celeritas AO
Co-Authored-By: brandyn <[email protected]>
2026-03-21 11:21:39 -07:00
DarkShadow44 9e7d19b883 Add missing DH lwjgl3 functions (#1546) 2026-03-21 10:56:19 -07:00
Jason Mitchell 5f61e387fb Use GLSM instead of relying on the redirector 2026-03-21 10:56:19 -07:00
Jason Mitchell 70cfa2ae90 Swappable RenderBackend
* Adds Lwjgl2GLRenderBackend & Lwjgl3GLRenderBackend
* Lwjgl3GLRenderBackend uses lwjgl3 natively w/o lwjgl3ify
* GLSMRedirector now redirects LWJGL3 methods as well for mods with a hard dep on lwjgl3 (distant horizons standalone)
* GLSM uses RENDER_BACKEND instead of GL directly
2026-03-21 10:56:19 -07:00
Jason Mitchell 37717bd4db GLSM Extraction (#1527) 2026-03-19 22:53:47 -07:00
Eclipse-Sol b179340a0f Fix mob's eyes from z-fighting (#1519) 2026-03-19 09:33:52 -07:00
Alexdoru 465e36148c use mixin to change the f3 text background instead of canceling the event (#1537) 2026-03-19 09:32:56 -07:00
Caedis a3f0efd629 Make fog controls work with BoP fog (#1539) 2026-03-19 09:24:40 -07:00
Death Fuel 2386d630c5 Update FragmentShaderGenerator.java (#1526) 2026-03-16 17:35:01 -07:00
Alexdoru 6a784d8d3b use classnode name in CeleritasBlockTransform (#1521) 2026-03-15 15:49:41 +01:00
Jason Mitchell fe9028d300 Log once instead of every time. (#1520) 2026-03-14 19:15:06 -07:00
Death Fuel 4c5e5df290 Replace compatibility mode texture formats (#1518) 2026-03-14 18:42:26 -07:00
Jason Mitchell 39ee364418 Improvements (#1516) 2026-03-14 15:06:30 -07:00
sisyphussy 4c9cae5e0b Optimize Streaming (#1515) 2026-03-14 13:13:40 -07:00
DarkShadow44 11b53c9f8f Add glTexImage3D with IntBuffer (#1506) 2026-03-11 11:55:47 -07:00
Jason Mitchell c769a08a4e Fix matching across newlines (#1505) 2026-03-11 09:52:21 -07:00
Jason Mitchell 90dac9f39d Add some missing glGetTexEnv* (#1503) 2026-03-10 19:18:58 -07:00
Jason Mitchell c5c4b290b1 Add missing overload (#1495) 2026-03-10 11:50:58 -07:00
Jason Mitchell 4b46de4566 Botania fix take two (#1501) 2026-03-10 10:22:06 -07:00
Jason Mitchell 9f2a71be4e Add missing overloads (#1500) 2026-03-10 10:06:12 -07:00
Jason Mitchell 79fe08417c Fix overwrite visibility crash (#1499) 2026-03-10 09:45:50 -07:00
Jason Mitchell 5de64abdef Maybe make Xaero work on j8 (#1494) 2026-03-09 19:47:41 -07:00
Death Fuel 2e07465166 UI Fix (#1492) 2026-03-09 16:22:44 -07:00
Death Fuel 2b610b9aea Better font rendering MC parity + book mode (#1489) 2026-03-09 11:49:33 -07:00
MalTeeez ab192ddd2a Split MixinRenderManager into two for DAPI compat (#1480) 2026-03-08 06:07:01 +01:00
Jason Mitchell cb5e2ed7d4 Lazy init entity lighting maps (#1476) 2026-03-07 14:01:55 -08:00
ah-OOG-ah e914ef7563 Actually sleep in FPS limiter (#1471) 2026-03-07 12:20:28 -08:00
Jason Mitchell 748dbd55a1 So secret you can't see them. (#1475) 2026-03-07 11:48:24 -08:00
Jason Mitchell 140c0c2285 Snapshot VAO/VBO data during display list compilation, like mesa does. (#1473) 2026-03-07 11:17:08 -08:00
sisyphussy af26442b98 Fix intel tests (#1442) 2026-03-07 10:12:56 -08:00
danyadevandAlexdoru ef07ee9f77 Refactor CeleritasBlockTransform (#1455)
Co-authored-by: Alexdoru <[email protected]>
2026-03-07 17:26:39 +01:00
Jason Mitchell 1b8087e8a3 Emulate glLineWidth on core using geom shaders (#1468) 2026-03-06 19:05:06 -08:00
Jason Mitchell 9107ea9ae5 Fix load -> enter -> leave: panorama rendering, cleanup state & fix test (#1467) 2026-03-06 19:01:43 -08:00
Jason Mitchell 938a6bda90 Replace panorama with modern opengl equivalent (#1466) 2026-03-06 16:01:27 -08:00
Jason Mitchell e9f6f72741 More core profile followons (#1464) 2026-03-06 15:50:30 -08:00
Jason Mitchell 379330f997 Core followups (#1445) 2026-03-05 11:35:05 -08:00
sisyphussy 2fecb3a05b Add MixinWavefrontObject back (#1452) 2026-03-05 11:07:45 -08:00
Raven Szewczyk c085af35bd Update buildscript with latest lwjgl3ify fixes (#1456) 2026-03-05 11:00:53 -08:00
Caedis a36aec36a2 Invert Horizon and Terrain Fog toggles (#1454) 2026-03-05 17:36:56 +01:00
sisyphussy bdd6259c3f Optimize fontrenderer (#1447) 2026-03-04 18:37:50 -08:00
Jason Mitchell d7d2228a6c Target OpenGL 3.3 core profile (#1412) 2026-03-02 22:18:11 -08:00
Alexdoru cf9993fce4 change rfb exclusions (#1440) 2026-03-02 15:11:34 -08:00
Alexdoru 3ae57ca594 Fixes for backhand compat (#1437) 2026-03-01 13:04:11 -08:00
sisyphussyandMustafarYasmin 9816cb2148 Replace framebufferRender with a VBO (#1432)
Co-authored-by: MustafarYasmin <[email protected]>
2026-03-01 13:03:27 -08:00
Alexdoru b1e496f56d remove boxing unboxing in RenderPassHelper 2026-03-01 13:02:04 -08:00
danyadevandAlexdoru c8e50227fb Speedup asm redirector transformers and fix missing classes in AngelicaRedirector (#1433)
* add shouldTransform & make it significantly faster

* do not optimize non-FRB CeleritasBlockTransformer

* move closer to usage

---------

Co-authored-by: Alexdoru <[email protected]>
2026-02-28 20:12:29 +01:00
Jason Mitchell 066381a7c0 Stop demoting, per face, once we see a non demoted material 2026-02-27 21:29:20 -08:00
Jason Mitchell 9a0ea1ad65 Workaround apparent JVM 25 bug... 🤣 2026-02-27 21:29:20 -08:00
Alexdoru 8e26a0179b Speedup block transformer by transforming every class directly (#1420)
* speedup block transformer by transforming every class directly

* stop using the cstpoolparser.add method
2026-02-24 15:45:35 +01:00
Alexdoru 1d96af8e3a re-enable Block Transformer when not running with rfb (#1418) 2026-02-23 14:58:40 -08:00
Yan Huang f7eb86cdb6 Parallelizing CTMUtils (#1393) 2026-02-23 20:21:35 +00:00
Alexdoru b38fafa831 ASM redirectors micro optimizations (#1416)
* redirector transformer micro optimizations

* use map for block fields redirect
2026-02-23 20:20:38 +01:00
Jason Mitchell 7dc2625f8c Maybe less leaky (#1414) 2026-02-23 08:57:21 -08:00
Jason Mitchell 3d97977050 Maybe fix holo inventory again - v2 - final.xlsx (#1413) 2026-02-22 20:37:08 -08:00
Eclipse-Sol 5f41c60bc2 Remove force blend as commit 0325a96b fixed this properly. (#1409) 2026-02-22 11:35:35 -08:00
Jason Mitchell 0325a96bc2 Add unknown blend state and BufferBlendOverride handling from modern Iris (#1408) 2026-02-21 15:27:22 -08:00
Caedis ad123d0bd5 Now with 200% more screen shake (#1402) 2026-02-18 09:20:47 -05:00
Caedis 4b89997985 Add slider for hurt screen shake intensity (#1401) 2026-02-18 04:23:57 +01:00
Death Fuel f5c9fa3301 slightly less stupid glyph size loading (#1390) 2026-02-15 18:49:44 -08:00
Death Fuel d6a6d3b525 Less logging + small refactor (#1379)
* less logging

* refactor

* missed a spot

* consistency
2026-02-15 18:48:51 -08:00
Ruling-0 f079bed977 TESRs Translucency Sorting (#1384)
* Iterate blocks in region towards the camera

* Sort translucent TESRs when translucency sorting is enabled
2026-02-15 18:48:05 -08:00
Caedis c9ec40e94b use mc.renderViewEntity instead of mc.thePlayer in shader shadow rendering (#1392) 2026-02-15 18:47:19 -08:00
embeddedt 8380f3853d Detect quads rendered by vanilla AO pipeline and only recalculate lighting for those (#1391)
* Detect quads rendered by vanilla AO pipeline and only recalculate lighting for those

* Handle oldLighting correctly
2026-02-15 18:46:58 -08:00
Yan Huang 534ae6b05c Remove support for NotEnoughIds-Legacy and update BetterCrashes dependency version, as the old files have been removed from the GTNH source. (#1388) 2026-02-15 09:35:09 -08:00
Jason Mitchell 68d523d3df Sign Culling (#1386) 2026-02-14 04:09:00 -08:00
Jason Mitchell 2f22a7b3ad Swap vanilla clipping helper with joml (#1385) 2026-02-13 14:29:31 -08:00
Jason Mitchell 7b79b90dd6 Fix crash without speedupAnimations enabled (#1377) 2026-02-09 12:00:36 -08:00
Jason Mitchell 417f23f826 Add runtime toggle for HUD Caching, and disable by default (for now at least) (#1374) 2026-02-07 14:44:49 -08:00
TeterosandTeteros ca3c46c873 Add PFAA Geologica cobble/bricks to the BlockCrackFix Whitelist (#1370)
Co-authored-by: Teteros <[email protected]>
2026-02-06 11:36:41 -08:00
Jason Mitchell 82fa9ade2b Add new emitter modes (#1368)
* Indirect
* Individual
2026-02-05 20:58:10 -08:00
Jason Mitchell aaab8fd0bc Thread safe biome cache & VAO Redirection (#1367)
* Thread safe biome cache

* VAO Redirection
* Delegate GLSM VAO calls to UniversalVAO
* Redirect usages of UniversalVAO & LWJGLService VAO functions to GLSM
* Avoid infinite recursion
2026-02-05 13:56:44 -08:00
Jason Mitchell bd56ccc4b8 Totally not experimental 2026-02-04 22:42:03 -08:00
sisyphussy e17f60308d Display List Refactor (#1360) 2026-02-04 10:39:20 -08:00
Kotmatross 1dd98978ad [Hotfix] Fix NTM:Space mixin targets for X5593_H261 (#1356) 2026-02-02 10:02:36 -08:00
Jason Mitchell dae986eb0b Fix crash with clearBufferSubData (#1355) 2026-02-01 12:41:53 -08:00
Jason Mitchell 5fdf84edd0 Camera and Menu Tweaks (#1354) 2026-02-01 12:14:32 -08:00
Jason Mitchell b19d023c11 Bump gtnhgradle 2026-02-01 09:26:19 -08:00
Jason Mitchell 41eafbd151 GLSL Downgrading (#1348) 2026-01-31 13:23:02 -08:00
sisyphussy dd970b4c6a Fix nested display lists rendering twice (#1349)
* Fix nested display lists rendering twice

* cleanup
2026-01-31 05:45:48 -08:00
Eclipse-Sol f79ad7f22a Fix arrows inheriting item material IDs (#1347) 2026-01-30 18:35:04 -08:00
Jason Mitchell 6947ffa3e2 Add IS_IRIS define as an option. 2026-01-30 18:10:27 -08:00
Jason Mitchell 969d2c3710 Backports tessellation shader support, safe zone shadow culling, and shader pack rendering directives. 2026-01-30 18:10:27 -08:00
Jason Mitchell c5988d29c3 Fix camera position and nether portal 3rd party rendering 2026-01-30 18:10:27 -08:00
Jason Mitchell dc68c2c265 Fix end portal w/ Shaders 2026-01-30 18:10:27 -08:00
Jason Mitchell 51533a81ec Misc Shaderpack loading fixes (#1343) 2026-01-30 14:53:41 -08:00
Eclipse-Sol 90eabb5d40 Add hardcoded entities and player_cape (#1327) 2026-01-30 14:53:26 -08:00
sisyphussyandJason Mitchell 74843cb1f9 Glc redirects (#1340)
Co-authored-by: Jason Mitchell <[email protected]>
2026-01-30 14:50:54 -08:00
Death Fuel 5e0344a529 remove unnecessary resource refresh (#1342) 2026-01-30 14:40:51 -08:00
Jason Mitchell 3cdd934987 First pass at setup/begin pass support (#1335) 2026-01-29 07:53:04 -08:00
Léa Gris 9d90ff137c fix(render): adapt hodgepodge fixBottomFaceUV (#1334) 2026-01-28 10:55:53 -08:00
Jason Mitchell bee9541797 Shadow Compute Shaders (#1333) 2026-01-28 00:03:04 -08:00
Jason Mitchell b75a5d8f7d Ya that won't work (#1332) 2026-01-27 15:32:42 -08:00
Jason Mitchell 8d3ab41c53 Use entity position instead of camera pos (#1330) 2026-01-26 22:19:54 -08:00
Jason Mitchell 48d032e654 Tweaks (#1329) 2026-01-26 21:08:09 -08:00
sisyphussy 1968914582 Fix hud flickering (#1325) 2026-01-26 10:19:49 -08:00
Guus Waals a70bd76ff3 Fix deadlock in CTMUtils (#1189) 2026-01-26 10:08:32 -08:00
mist475 db919b5f37 add biomeDictionary and lotr compat to BiomeUniforms (#1324) 2026-01-26 09:58:08 -08:00
MalTeeez cde47ea423 Add option to disable f3 additions (#1308) 2026-01-26 09:56:11 -08:00
Jason Mitchell 5ef6cb11c0 Don't double check infinite bounds (#1321) 2026-01-25 21:13:14 -08:00
Eclipse-Sol b7015bf10e Adds "name_tag" feature (#1318) 2026-01-25 20:00:49 -08:00
Jason Mitchell 46602ab921 Chunk Thread Saftey (#1320) 2026-01-25 19:58:33 -08:00
Jason Mitchell 63e9fa0df1 Dynamic Lights Improvements (#1319) 2026-01-25 19:07:11 -08:00
Eclipse-Sol dcea970a77 Various Shader Fixes (#1315) 2026-01-25 16:56:54 -08:00
Jason Mitchell b29240d6da Use the proper blockAccess on the main thread (#1317) 2026-01-25 16:55:45 -08:00
Jason Mitchell 9d5a02ed21 Optimize TileEntities (#1314) 2026-01-25 10:54:45 -08:00
Jason Mitchell b454483aae Add a nvidia workarounds from celeritas (#1286) 2026-01-24 18:52:52 -08:00
Jason Mitchell 72b13543c6 AABB Caching (#1309) 2026-01-24 18:52:22 -08:00
Jason Mitchell 36d98fcb22 Fix currentRenderedItemId for ItemBlocks (#1307) 2026-01-24 12:43:03 -08:00
Jason Mitchell 44c5b9617a Lighting fixes (#1306) 2026-01-24 12:23:59 -08:00
Kotmatross f3f1677db8 [Shaders] Fix NTM:Space sunset not rendering {2.0 regression} (#1275) 2026-01-24 19:47:30 +00:00
Jason Mitchell d396b611ad Buffer fixes (#1304) 2026-01-24 11:30:05 -08:00
Jason Mitchell 08f4c0f9db Make feature flag defines available in shader.properties preprocessing (#1303) 2026-01-24 10:10:13 -08:00
Eclipse-Sol 50b0d22dc2 Fix entity material handling. (#1298) 2026-01-23 20:24:27 -08:00
Jason Mitchell 7fed54264e No more OpenGLHelper (#1297) 2026-01-23 19:48:52 -08:00
embeddedt be87401be6 Improve behavior of Celeritas-based lighting pipeline 2026-01-23 19:37:57 -08:00
Jason Mitchell 94254006df Properly cleanup (#1294) 2026-01-23 18:19:46 -08:00
Jason Mitchell f79774df65 Respect shader render layer overrides (#1293) 2026-01-23 16:43:01 -08:00
Eclipse-Sol 2f2fc22df3 Add support for dimension.properties (#1283) 2026-01-22 19:17:16 -08:00
Jason Mitchell 26300bbb1b Celeritas Smooth Lighting is now default and working. UseSeparateAO, Emissives, and oldLighting=false now work. 2026-01-22 16:32:15 -08:00
Jason Mitchell 88e4d2fd6c Use Separate AO & SmoothLightPipeline 2026-01-22 16:32:15 -08:00
Jason Mitchell bb52476d27 Custom images, textures, SSBO, and ShaderPack/ShaderProperties backports 2026-01-22 16:32:15 -08:00
Jason Mitchell 6d60477b93 Actually allocate the (minimal) display lists so we stop clobbering the hand renderer on shader reload (#1278) 2026-01-19 13:05:27 -08:00
Jason MitchellandEclipse-Sol 362724610c Add Various Features (#1209) (#1277)
Co-authored-by: Eclipse-Sol <[email protected]>
2026-01-19 13:03:56 -08:00
Jason Mitchell ea4a3cdf0b Fix a race condition (#1273) 2026-01-16 16:46:36 -08:00
Jason Mitchell 65959268d0 Support glCallLists (#1265) 2026-01-13 10:41:09 -08:00
Jason Mitchell 58d820cd7e Fix OSX F3 crash
Fixes #1256
2026-01-13 09:37:28 -08:00
Death Fuel 685d68bd9c Improve glyph replacement logic (#1247) 2026-01-12 15:17:20 -08:00
Jason MitchellandCaedis ad63d2d62c Dynamic light improvements (#1255)
Co-authored-by: Caedis <[email protected]>
2026-01-10 23:50:32 -08:00
Jason Mitchell d2730ccca8 Better video settings search (#1254) 2026-01-10 21:25:03 -08:00
Jason Mitchell e9257b8739 Fixes (#1251) 2026-01-10 15:20:19 -08:00
Jason Mitchell 3984225faa Better OSX J8 Support (#1248) 2026-01-09 12:58:42 -08:00
Jason Mitchell 52db6b56d6 Fix GLSL 120 support (#1243) 2026-01-08 21:34:40 -08:00
Jason Mitchell 34bfe513f1 Maybe less deadlock-y (#1241) 2026-01-08 20:53:32 -08:00
Jason Mitchell 42d7ed58aa 2.0.0 Alpha1 Fixes (#1240) 2026-01-08 19:26:56 -08:00
Jason Mitchell 9a4f0f9b99 Adapt some Beddium features & add FPS Cap info 2026-01-07 13:22:16 -08:00
Jason Mitchell be90a01725 Angelica 2.0
* GLStateManagerService & RenderSystemService
* Wire up Celeritas
* Iris working
* Dynamic lights working
* Goodbye Sodium 0.2
* Add Multi-Threading for check meshing again
2026-01-07 13:22:16 -08:00
Jason Mitchell e5e8250d44 JVMDG 2026-01-07 13:22:16 -08:00
Wilhelm Schuster 062a03ffc2 Add more menu backgrounds (#1225) 2026-01-07 12:46:33 -08:00
Jason Mitchell 9f5afaaec3 Better Splash support (#1220) 2026-01-05 10:24:12 -08:00
Death Fuel b05f17a477 Disable custom fonts with GraphicsEnvironment.isHeadless() (#1218)
* disable custom fonts with GraphicsEnvironment.isHeadless()
2026-01-04 10:19:17 -08:00
Death Fuel 57caf08212 remove a horizontal shift (#1216) 2026-01-03 13:34:23 -08:00
Jason Mitchell 085ec79c21 Set defaults once per TE pass instead of once per TE (#1213) 2026-01-03 12:46:40 -08:00
Wilhelm Schuster 38641268ae Random menu backgrounds (#1217) 2026-01-03 12:01:48 -08:00
Jason Mitchell fe8e723638 Update 000-report-bug.yml 2025-12-31 22:35:40 -08:00
Jason Mitchell 16debfd518 Move EntityID shader mixin to shaders, and optimize (#1208) 2025-12-29 18:23:48 -08:00
Jason Mitchell 8cfc3e1f4a Why were these bypassing GLSM? (#1207) 2025-12-28 22:42:39 -05:00
Eclipse-Sol 2c808fae43 Update entity.properties formatting (#1197)
* Update entity.properties formatting

1.7.10 doesn't have namespaces like modern does, this code updates this so that Angelica can handle names such as XPOrb or Creeper.

* Allows shaders to target Lightning Bolts

Lightning bolts within 1.7.10 are not registered, this allows shader devs to target the lightning bolt like it does in modern.

* Additional fixes

* Adds back getEntityId as a fallback
2025-12-27 09:04:37 -08:00
Eclipse-Sol 1649c3c0c6 Adds entity_flame ID to Angelic (#1202) 2025-12-26 13:05:08 -08:00
Jason Mitchell b84286ba3b Nvidia test fix (#1201)
confirmed working in #meta-dev (nvidia/windows 11)
2025-12-25 13:36:19 -08:00
Jason Mitchell ee6c79802a Display List Overhaul (#1200) 2025-12-24 20:30:48 -08:00
Jason Mitchell d3366ad31a Update Buildscript (#1199) 2025-12-23 19:56:48 -08:00
Jason Mitchell 92b6dcc010 Texture cache fixes (#1198) 2025-12-23 17:38:52 -08:00
Jason Mitchell 89132f3ef9 Parallel shader transformation and use Transformer instead of Utils (#1191) 2025-12-23 09:11:32 -08:00
Eclipse-Sol f0d45b0d62 Fix metadata parsing by removing space in split (#1196) 2025-12-23 01:53:31 +00:00
Jason Mitchell 173d986a5b Add a test for the server side texture cache fix (#1194) 2025-12-21 12:14:53 -08:00
mist475 473a690054 fix #1190: remove remap = false from GuiIngameForge (#1192) 2025-12-21 10:34:01 -08:00
sisyphussy 2515b3520f Always delete texture parameter cache (#1193) 2025-12-21 10:33:44 -08:00
glowredman a32f9ca244 Only catch Exceptions not Errors (#1186) 2025-12-21 14:14:50 +01:00
Jason Mitchell 941fc88484 Shader boot fix + more DSA (#1184) 2025-12-16 16:54:50 -08:00
Creeper-banner 4106dd3865 Fix broken text rendering after certain characters (#1178)
* Font render fix

* re-fix

* Update fix for consistency
2025-12-14 07:10:12 -08:00
Jason Mitchell 61f6aa19a8 glDrawBuffers Support 2025-12-09 18:46:47 -08:00
Jason Mitchell 77875f7f91 Stop recording normals... we're already baking them into a VBO 2025-12-08 15:28:07 -08:00
Jason Mitchell 241c32def1 Add some transformer exclusions 2025-12-08 14:57:27 -08:00
sisyphussy a8107f97e8 Add VAO's (#1126) 2025-12-07 16:51:32 -08:00
Death FuelandJason Mitchell 8bec433699 GT5 custom glyph handling and visual fixes (#1148)
Co-authored-by: Jason Mitchell <[email protected]>
2025-12-06 09:48:03 -08:00
Kotmatross 2d39fb2397 [BCF] Fix z-fighting on GT ores 2: here we go again edition (#1161) 2025-12-06 08:17:26 -08:00
Jason Mitchell cfd8eb1411 fix compatability profile detection (#1159) 2025-12-05 17:01:20 -08:00
Jason Mitchell 59c270a5b9 glFrontFace, glHint, and glBlendColor (#1156) 2025-12-05 16:11:04 -08:00
Jason Mitchell 87ac03281c Add clear depth in display lists 2025-12-04 09:21:22 -08:00
Jason Mitchell 36b82fa2ce Update README.MD 2025-12-03 16:01:26 -08:00
Jason Mitchell 2fc2e2e38d Fix some small things
* Bump nhlib dep due to crash when Iris is disabled
* Use jabel for tests
* use joml isIdentity
* remove Integer usage
2025-12-03 10:54:20 -08:00
Jason Mitchell 33de1de9c4 Add display list compilation/optimization (#1141) 2025-12-02 21:43:17 -08:00
Jason Mitchell 55811b9d7b Set some defaults for tile entity rendering: (#1144) 2025-12-02 09:54:48 -08:00
Alexdoru a10d92f52e [Memory-opti:fix leak] Fix world leak in MixinRenderBlockFluid (#1131) 2025-11-30 10:26:49 -08:00
Christoffer Hjalmarsson 7d2fe12ce9 Fix PBR atlas failing to load all textures (#1132) 2025-11-30 10:26:09 -08:00
Death Fuel 006ded801e Font config UI improvements (#1128) 2025-11-30 10:25:21 -08:00
Alexdoru 03afd6939e Revert "limit the amount of dropped items rendered exactly to the config value (#973)" (#1125)
This reverts commit 891715b113.
2025-11-19 19:41:29 +01:00
sisyphussy adc5421e00 Optimize CloudRenderer (#1123) 2025-11-18 09:38:27 -08:00
sisyphussy dfe60e4b3e Optimize hud caching (#1103) 2025-11-17 19:52:23 +00:00
MalTeeez 72c6cdfa60 Disable conflicting mixins when DragonAPI is present (#1119)
* Exclude dapi from problematic mixins

* Don't force hodgepodge at runtime
2025-11-15 18:27:44 -05:00
Nikita Manchuk c5daea86f8 Fix: entity/item textures and lighting break with shaders when name tags render (batched font GL state) (#1091) 2025-11-10 08:57:13 -08:00
Kotmatross f8958ee990 [BCF] Fix z-fighting on GT ores (#1113) 2025-11-09 16:19:04 -08:00
sisyphussy 073260060b Fix GLSM display list (#1110) 2025-11-08 19:20:20 -08:00
Jason Mitchell 771fee124d Fall back to display lists if not using VBOs 2025-11-08 13:05:37 -08:00
NealDeal34 680285bf54 Update zh_CN.lang (#1036) 2025-11-08 10:19:48 -08:00
Tomasz Sobczyk df8784536e Use separate queue and workers for chunk renders that can block and wait for the main thread. Increase number of renders done on main thread to 16. (#1084) 2025-11-07 15:11:24 -08:00
KotmatrossandJason Mitchell c8991d810c Port FIX_BLOCK_CRACK from FalseTweaks 2 (#1023)
Co-authored-by: Jason Mitchell <[email protected]>
2025-11-07 15:07:04 -08:00
Alexdoru e3b02642ac split AngelicaRedirector from SodiumBlockTransform (#1018) 2025-11-07 14:49:46 -08:00
ALongStringOfNumbers 2759dd6689 Increase Scroll Speed in Settings GUIs (#1106) 2025-11-07 14:48:09 -08:00
Kotmatross 996dbaef2c [Shaders] Basic compatibility with NTM:Space skybox & more (#1096) 2025-11-07 14:44:38 -08:00
Jason Mitchell 72ca675c93 Uniform Additions and fixes (#1102) 2025-11-07 14:39:10 -08:00
sisyphussy e6e230e270 Add more VAO tests (#1093) 2025-11-07 19:42:05 +00:00
ALongStringOfNumbers 43d7282ba5 Set maxShiftBy to fix some log warnings (#1105) 2025-11-07 17:47:16 +00:00
ah-OOG-ah 4b2d57a32d Update model API (#842)
* Bump GTNHLib

* Change test block's properties

This makes it easier to test the block overlay

* Update VanillaModels.java

# Conflicts:
#	src/main/java/com/gtnewhorizons/angelica/models/VanillaModels.java

* Revert rename

* Rename models

* Sledgehammer out removed abstractions

* Switch to ModelISBRH

* Replace old models

With a single new and interesting model!

* Adapt to GTNHLib

* Remove dead code

* Fix JSON model detection + remove dead code

* Remove direct model renderer

* yet more yote

* Remove ColorABGR

* Remove unused line methods

* Yeet color fully

* publicize nextLong

Technically you don't *need* this on Java 8, but it silences a warning and doesn't break anything so 🤷

* yoten

* Register Angelica for automatic texture loading

* Remove TX-Loader dep

* Make the list mutable

* Remove rebase mistake

* Bump BS + deps
2025-11-06 17:40:45 +00:00
Darren Eberly 06bf71126f Add several missing built-in uniforms (#1101) 2025-11-05 15:38:57 -08:00
Darren Eberly 4c0f2f2685 Disable hardcoded custom uniforms by default 2025-11-05 12:25:18 -08:00
Jason Mitchell 78334717e7 Fix TAA:
* Return an immutable view of the underlying camera position to avoid mutating it
* Update Camera Uniforms to utilize the view instead of mutating in place
2025-11-05 09:59:47 -08:00
MalTeeez a77765a2c9 Allow avoiding other mods spash screen handlers (#1080) 2025-10-31 16:06:28 -07:00
sisyphussy 1a9fe45ee9 Add VAO test (#1089) 2025-10-31 10:10:01 -07:00
Jason Mitchell 21ef5905c8 Roll back some of the hacks (#1085) 2025-10-30 20:50:43 -07:00
ABKQPO 5b67eaa474 Quick fix PBR Resourcepack loader error (#1043) 2025-10-29 08:01:52 -07:00
SuperSoupr c9d59c3b90 Allow overriding of blend function in batching font renderer (#1075) 2025-10-29 07:59:27 -07:00
Death Fuel fa7ff74fec make an exception for the splash screen (#1069) 2025-10-29 07:57:41 -07:00
Kotmatross a34a3ebd39 Fix missing depth buffer writing before GUI rendering (#1073) 2025-10-29 07:56:45 -07:00
Nikita Manchuk 2e7476d287 render: set frustum position before BE culling (#1077) 2025-10-29 07:23:25 -07:00
Death Fuel 3244071012 font shader changes (#1071) 2025-10-24 14:44:32 -07:00
DarkShadow44 7035dbde02 Rework ANGELICA_VERSION into an integer (#873) 2025-10-22 01:19:15 +00:00
sisyphussy 97ae04c2fa Fix CME (#1008) 2025-10-20 14:38:30 -07:00
sisyphussy e7fa7f62d0 Fix framebuffer depth not clearing (#1051) 2025-10-20 14:36:02 -07:00
Wilhelm Schuster ea39712749 Add null check for zoom reset (#1061) 2025-10-20 14:35:29 -07:00
Death Fuel 66dd3eaa94 Custom/Smooth Font follow-up (#1040) 2025-10-20 14:35:06 -07:00
ah-OOG-ah 270697cd10 Refactor vendor detection (#1049)
* Refactor vendor detection

Only one vendor can be active at any given time, so make it an enum.

* lowercase vendor string
2025-10-13 15:48:21 -07:00
sisyphussy 2b53a851ef Fix intel build tests (#1048) 2025-10-13 10:31:48 -07:00
Jason Mitchell 34b6efde7b Maybe fix TAA (#1047) 2025-10-12 17:40:41 -07:00
Jason Mitchell 86edb721da GLSM updates (#1046) 2025-10-12 14:51:56 -07:00
Kotmatross 39c5f4bf7b Fix missing GL20 in cstPoolParser (#1042) 2025-10-09 12:26:55 -04:00
Death Fuel 5dc54bb1da remove implicit float cast (#1037) 2025-10-05 05:03:50 -07:00
Death Fuel 7924769ab1 Custom/Smooth Font (#1017) 2025-10-04 11:20:38 -07:00
sisyphussy 8afe2e2ccf Optimize in-world item rendering (#1034) 2025-10-01 09:05:06 -07:00
Wilhelm Schuster e8dd60e088 Check if module is enabled before resetting zoom (#1032) 2025-09-28 09:01:23 -07:00
Kotmatross 9a621405ba Fix Tileentities not casting shadows (#1024) 2025-09-26 12:04:46 -07:00
slprime 45a677030d Disabled force unicode (#1029) 2025-09-26 09:59:08 -07:00
Wilhelm Schuster 4d531022d2 Improve zoom key handling (reset on GUI enter and compatibility with mouse buttons) (#1016) 2025-09-26 09:41:08 -07:00
Pedro Ortiz Bledow 48ba2160ad Fix fast clouds rendering with setting off after game restart (#1028) 2025-09-26 13:04:47 +02:00
Eldrinn-Elantey 334ec81f9b Font rendering: configurable shadow & glyph fix (#1011) 2025-09-03 08:53:39 -07:00
Alexdoru 86c726e50b Revert "Port FIX_BLOCK_CRACK from FalseTweaks (#999)" (#1005)
This reverts commit dce5013ffb.
2025-08-16 19:09:18 -07:00
AlexdoruandJason Mitchell e6d778dad3 ASM Housekeeping (#1003)
Co-authored-by: Jason Mitchell <[email protected]>
2025-08-16 17:29:50 -07:00
Kotmatross dce5013ffb Port FIX_BLOCK_CRACK from FalseTweaks (#999) 2025-08-16 22:10:55 +00:00
glowredman 6eef63619c Update BatchingFontRenderer.java (#1002) 2025-08-10 22:27:29 +02:00
Alexdoru a8153fa0f7 remove access transformer to invoke EntityRenderer#getNightVisionBrightness (#1001) 2025-08-10 16:31:30 +00:00
Niclas Klugmann 49c04cf776 German translation (#997) 2025-07-27 20:02:26 +07:00
Alexdoru 85b0277a66 use EnumChatFormatting and not Realms enum to avoid crash (#996) 2025-07-21 10:59:04 +02:00
Alexdoru 0525b31262 Cache Tessellator#instance in local var (#995) 2025-07-18 12:44:03 -07:00
boubou19 fdb2053c50 Speed up BlockError event registration (#993) 2025-07-17 20:38:13 +02:00
Alexdoru 31abbcce32 Convert to new IMixins and MixinsBuilder system (#991) 2025-07-15 08:54:06 -07:00
063b2242e8 Initial FluidLogged compat (#985)
* Initial FluidLogged compat

* FluidLogged works(probably mostly)

* Make FluidLogged optional

* Remove a leftover print

* Fix fluidlogged fluids rendering incorrectly with shaders on

* Rmemove unused code

* Cleanup unused imports

---------

Co-authored-by: embeddedt <[email protected]>
Co-authored-by: Jason Mitchell <[email protected]>
2025-07-09 10:31:45 -04:00
Rad 577f905dc5 Update README.MD (#981) 2025-07-06 13:56:20 -07:00
Jason MitchellandAlexdoru 071d2d4a39 Add a guard to only init once (#984)
Co-authored-by: Alexdoru <[email protected]>
2025-07-01 10:33:49 -07:00
Alexdoru 891715b113 limit the amount of dropped items rendered exactly to the config value (#973) 2025-06-16 07:55:28 +02:00
jss2a98aj 30a35ef25e Misc. CTM resource tweaks 2025-06-13 18:04:31 -07:00
jss2a98aj 33453b3b8c Fix crash with OpenAutism
Brandyyn says this fixes the crash but I have not tested it myself.
2025-06-13 18:04:31 -07:00
jss2a98aj 62e699f6d0 Remove unneeded debug print 2025-06-13 18:04:31 -07:00
Jason Mitchell f107d173b7 Stop loading HoloInventory too early in the compat class, which was causing fake fluids to not render properly. 2025-06-09 14:22:12 -07:00
glowredman d0948b36f3 No longer use Reflection for SplashProgress.fontRenderer (#966)
* Access SplashProgress font renderer via mixins

* Use AT
2025-06-07 12:07:14 +02:00
YannickMG ae1211f280 Battlegear 2 for Backhand compat 2025-05-31 08:01:28 -07:00
David Lindström 3d76cb3073 Use instanceof instead of direct cast to protect against scenarios where mods like FMB use the same rendering code for world, inventory and preview, where the two latter happens in passes that are not rendered using a CapturingTesselator 2025-05-27 14:10:07 -07:00
kotmatross28729 45fb120223 Fix COLOR_BUFFER_BIT leak in ShaderPackScreen 2025-05-25 08:25:59 -07:00
Jason Mitchell d7a196eb5c Use invokeExact instead of invoke 2025-05-25 08:25:05 -07:00
Jason Mitchell 4d53e2af9b Holoinventory compat via reflection 2025-05-25 08:25:05 -07:00
Jason Mitchell cb6336a365 Backhand Compat via Reflection 2025-05-25 08:25:05 -07:00
Jason Mitchell 5095021e0e Capture shader outline 2025-05-16 18:08:27 -07:00
Caedis f88ecbf68d Replace Baubles with Baubles Expanded (#949) 2025-05-13 10:26:50 -04:00
Darren Eberly 7da9b9eb1c Compat for security craft (#948) 2025-05-12 23:34:15 -04:00
Pxx500 02c394108f enable ctm, disable everything unnecessary 2025-05-05 12:44:46 -07:00
Fabian Maurer 720aaca1b7 Add functionality to fake blockId for shaders 2025-04-24 14:11:24 -07:00
Midnight145 1e71b1c4d8 liteloader docs update 2025-04-17 17:41:58 -07:00
くぁーりぃ df4c423e93 fix config load (#914) 2025-04-07 19:10:39 +00:00
JuliaandJason Mitchell 78e0074e5a Apply linear filter to unifont glyphs at odd scale factors (#907)
* Apply linear filter to unifont glyphs at odd scale factors

* Prevent glyph texture bleeding by adjusting UV coordinates with a slight offset

* Tiny bit of tuning

---------

Co-authored-by: Jason Mitchell <[email protected]>
2025-04-07 17:37:46 +02:00
くぁーりぃ 0350ea2cb6 fix stencil buffer does not exist (#912) 2025-03-20 07:53:27 -07:00
Darren Eberly bed724ba59 Support for custom uniforms (#902)
* wip backport of custom uniforms

* Functioning custom uniforms maybe?

* Workon separating dynamic/non-dynamic uniforms and making variables available to custom uniforms (currently the JVM dies horribly)

* wip

* It no longer crashes horrifically

* star imports
2025-03-18 10:04:31 -07:00
Jason Mitchell 6fa973dbc8 Adds two new overloads (#906) 2025-03-13 08:24:57 -07:00
RecursivePineapple c4ad460f34 Update (#899) 2025-03-09 23:04:46 +00:00
Glease 2a8404c6c2 add an API to retrieve world instance from WorldSlice (#897) 2025-03-08 23:00:58 -08:00
ah-OOG-ah 108db9031b Add an option to disable terrain fog (#896)
* Add an option to disable terrain fog

* forgot a remap = false
2025-03-09 05:38:30 +00:00
amyandJason Mitchell d6d8a5dad6 Catch CancellationException while building chunks (#889)
Co-authored-by: Jason Mitchell <[email protected]>
2025-03-08 20:45:10 -08:00
RecursivePineappleandJason Mitchell ad2b73edf9 Fix hologlasses crash (#891)
Co-authored-by: Jason Mitchell <[email protected]>
2025-03-08 20:44:36 -08:00
glowredman bb5526598e Various Mixin-related Fixes (#890)
* Update deps

* Remove dead code

* Add missing `@Override` annotations

* Fix generics

* Add `serialVersionUID`s

* Address deprecation warnings

* Use `@WrapWithCondition` v2

* Misc cleanup

* Use AccessTransformers if possible

* Fix mixin conflicts

* Add `@Mutable` annotations

* Fix injection targets

* Update deps
2025-03-08 20:41:47 -08:00
RecursivePineapple 611bbf02c8 Optimize NEI rendering 2025-02-28 11:32:26 -08:00
OBlankenship ca16036604 Update GTNHLib dependency version in README (#885) 2025-02-27 09:22:04 +01:00
Caedis f118211257 Fix IC2 universal cell rendering w/offhand (#876) 2025-02-18 10:49:29 -08:00
RecursivePineapple 81d03ea552 Fix PBR specular & normal textures getting ignored (#874)
* Fix PBR textures not loading

- Stack was being overfilled by MemoryStack
- Added tons of logging for ignored important exceptions
- Added missing interface on mixin (should it be moved to gtnhlib?)

* Move PBR debug to angelica config

* Free malloc'd memory
2025-02-18 18:34:38 +00:00
RecursivePineapple 67c8628aeb Fix a handful of broken animations (#868)
* Fix warning

* Improve animated sprite tracking

* Fix NPE in animation code

* Fix animations for abnormally rendered items

* Swap Stack for ArrayDeque
2025-02-18 18:28:54 +00:00
embeddedt f9b9873637 Avoid checking frustum visibility of infinite bounding boxes 2025-02-18 10:24:09 -08:00
DarkShadow44 c66bf8658e Fix issue with dynamic lights world mixin not respecting opaque blocks (#872) 2025-02-17 10:36:34 -06:00
DarkShadow44 052824017d Fix crash when keycode is negative (#871) 2025-02-16 21:43:15 +00:00
Caedis 6a5d914a43 add offhand support via Backhand (up required version to 1.6.9) (#859) 2025-02-04 12:41:47 -05:00
Calvin Reveraster 208373deaa Fix any dimension without a sky getting flagged as the nether (#857)
* fix crash when connected textures are disabled, but custom colors are enabled.

* Dimension Check Fix

Dimension check set any dimension with no sky as the nether; this includes the end, and several custom dims such as Witchery's mirror dim, etc.
2025-02-01 09:18:29 -08:00
TotallyNotOndre f22a3f612b Fix slider dragging in iris options (#858) 2025-02-01 09:17:26 -08:00
TotallyNotOndre 9ff1015d19 Fix shadow render distance reset to 32 (#856) 2025-01-31 11:18:09 -08:00
Calvin Reveraster 0023fe3da2 fix crash when connected textures are disabled, but custom colors are enabled. (#855) 2025-01-29 08:19:50 -08:00
Raven Szewczyk 7e2399714c Use GTNHLib MemoryUtils instead of the builtin compat ones (#853) 2025-01-26 11:20:43 -08:00
George Paton 9029182353 Fix Hodgepodge animation speedup breaking animated textures (#850) 2025-01-25 15:34:51 -08:00
Caedis e6039691b5 Remove forced even unicode gui scaling (#840) 2025-01-15 14:39:21 -06:00
paulzzh 5db1edba33 fix MC-76356 Bold unicode characters appear doubled (#834) 2025-01-05 15:24:16 +00:00
Martin Robertz d450461726 update bs + deps (#823) 2025-01-04 13:50:46 -08:00
embeddedt 95f8afc0f3 Remove use of empty build tasks, submit empty data directly to result queue (#821) 2024-12-27 21:39:05 -08:00
Caedis 19635e20ec Disable optimizeInWorldItemRendering by default for now (#820) 2024-12-25 23:25:42 -06:00
BucketBrigade ead4e11376 Expose texture updates to API (#805)
* Expose texture updates to API

* Expose for blocks as well

* Fix mistake

* Fixed API
2024-12-19 19:04:24 -08:00
FM Bellomy b0ceb5306a the most boring typo PR of all time (#810) 2024-12-19 13:25:56 -08:00
embeddedt 99a81369f0 fix origin weirdness (#799) 2024-12-17 10:09:22 -08:00
nshepperd e2ee822948 Only clear the translucency sorting render queue when we're not rendering shadows. (#798) 2024-12-16 13:30:20 -08:00
nshepperd 9fe7e800f6 Remove broken reflection in ChunkBufferSorter. (#797) 2024-12-16 21:11:05 +00:00
embeddedt debfcdfda0 Properly prevent block face culling when TS is active (#796) 2024-12-16 13:07:00 -08:00
embeddedt df662188f4 Fix terrain rendering incorrectly when far from origin (#790) 2024-12-13 18:14:00 +00:00
embeddedt 842916405f Fix rebuild check being wrong when shaders are on (#788) 2024-12-13 16:55:58 +00:00
embeddedt 09afe81290 Fix chunk render manager state not swapping, causing lower render distance with shaders on 2024-12-13 08:52:32 -08:00
Caedis 292809d8e8 Actually fix xaero's crosshair (#781) 2024-12-11 21:50:04 +00:00
Caedis 47503ce254 Fix invisible crosshair with Xaero's Minimap (#775) 2024-12-11 16:32:49 -05:00
ah-OOG-ah f738adbdef Frametime exploration (#742)
* Sleep before swap

* Add a config

* I think this is slightly less jittery
2024-12-06 09:09:26 -08:00
Alexdoru 70d15f2383 Set render distance for mob inside mod spawner to 16 blocks (#761)
* set render distance for mob inside mod spawner to 16 blocks

* add config in angelica

* add slider in video setting menu
2024-12-02 15:20:44 -10:00
kurrycatandDarren Eberly d66b5577f4 add missing GL calls in GLStateManager (#767)
Co-authored-by: Darren Eberly <[email protected]>
2024-12-02 09:12:44 -05:00
ah-OOG-ah e293f12fb8 Refresh Notfine options on world load (#760) 2024-12-01 00:03:21 +00:00
kurrycat b69eca6749 add missing Pipeline::syncProgram for glCallList (#762) 2024-11-30 10:38:00 -10:00
Jason Mitchell 03342f08c6 Update README.MD 2024-11-27 09:03:01 -08:00
ah-OOG-ah b85614f983 Null check (#756) 2024-11-26 22:19:05 +00:00
2491 changed files with 225440 additions and 36578 deletions

No files matched your search

+8
View File
@@ -20,3 +20,11 @@ indent_size = 2
[*.lang]
trim_trailing_whitespace = false
[*.java]
max_line_length = 240
ij_java_wrap_long_lines = false
ij_java_use_single_class_imports = true
ij_java_class_count_to_use_import_on_demand = 999
ij_java_names_count_to_use_import_on_demand = 999
ij_java_packages_to_use_import_on_demand = empty
@@ -39,6 +39,7 @@ body:
- Java 19
- Java 20
- Java 21
- Java 25
- Other (Please Specify)
validations:
required: true
+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"
+125
View File
@@ -0,0 +1,125 @@
#!/usr/bin/env bash
# Builds the libTracyClient and AngelicaTracyCapture shared libraries for the tracy-client module.
set -euo pipefail
TRACY_VERSION="${TRACY_VERSION:-v0.14.1}"
TRACY_VERSION_BARE="${TRACY_VERSION#v}"
OUT_DIR="${OUT_DIR:-dist/tracy-natives}"
TRACY_REPO="https://github.com/wolfpld/tracy"
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
case "$(uname -s)" in
Darwin) PLAT_OS="macos" ;;
Linux) PLAT_OS="linux" ;;
*) PLAT_OS="windows" ;;
esac
case "$(uname -m)" in
arm64|aarch64) PLAT_ARCH="arm64" ;;
*) PLAT_ARCH="x64" ;;
esac
RES_DIR="${RES_DIR:-$REPO_ROOT/tracy-client/src/main/resources/natives/tracy/$TRACY_VERSION_BARE/$PLAT_OS-$PLAT_ARCH}"
WORK_DIR="$(mktemp -d)"
trap 'rm -rf "$WORK_DIR"' EXIT
if [[ -n "${TRACY_SRC:-}" ]]; then
SRC_DIR="$WORK_DIR/tracy"
git clone --shared --no-checkout "$TRACY_SRC" "$SRC_DIR"
git -C "$SRC_DIR" checkout --detach "$TRACY_VERSION"
else
SRC_DIR="$WORK_DIR/tracy"
git clone --depth 1 --branch "$TRACY_VERSION" "$TRACY_REPO" "$SRC_DIR"
fi
export MACOSX_DEPLOYMENT_TARGET="${MACOSX_DEPLOYMENT_TARGET:-11.0}"
CLIENT_BUILD_DIR="$WORK_DIR/build-client"
cmake -S "$REPO_ROOT/tracy-client/src/main/native" -B "$CLIENT_BUILD_DIR" \
-DTRACY_SRC_DIR="$SRC_DIR" \
-DCMAKE_BUILD_TYPE=Release \
-DBUILD_SHARED_LIBS=ON \
-DTRACY_STATIC=OFF \
-DTRACY_ENABLE=ON \
-DTRACY_ON_DEMAND=ON \
-DTRACY_MANUAL_LIFETIME=ON \
-DTRACY_NO_SAMPLING=ON \
-DTRACY_NO_SYSTEM_TRACING=ON \
-DTRACY_NO_CRASH_HANDLER=ON \
-DTRACY_LTO=OFF
cmake --build "$CLIENT_BUILD_DIR" --config Release --parallel
CPM_CACHE_DIR="$WORK_DIR/cpm"
CAPTURE_BUILD_DIR="$WORK_DIR/build-capture"
cmake -S "$REPO_ROOT/tracy-client/src/main/native-capture" -B "$CAPTURE_BUILD_DIR" \
-DTRACY_SRC_DIR="$SRC_DIR" \
-DCMAKE_BUILD_TYPE=Release \
-DCPM_SOURCE_CACHE="$CPM_CACHE_DIR"
cmake --build "$CAPTURE_BUILD_DIR" --config Release --parallel
find_one() {
find "$1" -name "$2" 2>/dev/null | head -1
}
CLIENT_LIB_NAME=""
CLIENT_LIB=""
for NAME in libTracyClient.so libTracyClient.dylib TracyClient.dll; do
LIB="$(find_one "$CLIENT_BUILD_DIR" "$NAME")"
if [[ -n "$LIB" ]]; then
CLIENT_LIB="$LIB"
CLIENT_LIB_NAME="$NAME"
break
fi
done
if [[ -z "$CLIENT_LIB" ]]; then
echo "error: libTracyClient not found under $CLIENT_BUILD_DIR" >&2
exit 1
fi
CAPTURE_LIB_NAME=""
CAPTURE_LIB=""
for NAME in libAngelicaTracyCapture.so libAngelicaTracyCapture.dylib AngelicaTracyCapture.dll; do
LIB="$(find_one "$CAPTURE_BUILD_DIR" "$NAME")"
if [[ -n "$LIB" ]]; then
CAPTURE_LIB="$LIB"
CAPTURE_LIB_NAME="$NAME"
break
fi
done
if [[ -z "$CAPTURE_LIB" ]]; then
echo "error: AngelicaTracyCapture not found under $CAPTURE_BUILD_DIR" >&2
exit 1
fi
STRIPPED_CAPTURE_LIB="$WORK_DIR/$CAPTURE_LIB_NAME"
cp -L "$CAPTURE_LIB" "$STRIPPED_CAPTURE_LIB"
case "$PLAT_OS" in
macos) strip -x "$STRIPPED_CAPTURE_LIB" ;;
linux) strip --strip-unneeded "$STRIPPED_CAPTURE_LIB" ;;
*) ;;
esac
LICENSE_ZSTD="$(find_one "$CPM_CACHE_DIR/zstd" LICENSE)"
LICENSE_CAPSTONE="$(find_one "$CPM_CACHE_DIR/capstone" LICENSE.TXT)"
LICENSE_PPQSORT="$(find_one "$CPM_CACHE_DIR/ppqsort" LICENSE)"
LICENSE_NAMES=(LICENSE LICENSE-zstd LICENSE-capstone LICENSE-ppqsort)
LICENSE_SRCS=("$SRC_DIR/LICENSE" "$LICENSE_ZSTD" "$LICENSE_CAPSTONE" "$LICENSE_PPQSORT")
for i in "${!LICENSE_NAMES[@]}"; do
if [[ -z "${LICENSE_SRCS[$i]}" || ! -f "${LICENSE_SRCS[$i]}" ]]; then
echo "error: ${LICENSE_NAMES[$i]} source not found under $CPM_CACHE_DIR" >&2
exit 1
fi
done
mkdir -p "$OUT_DIR" "$RES_DIR"
for DEST in "$OUT_DIR" "$RES_DIR"; do
cp -L "$CLIENT_LIB" "$DEST/$CLIENT_LIB_NAME"
cp -L "$STRIPPED_CAPTURE_LIB" "$DEST/$CAPTURE_LIB_NAME"
for i in "${!LICENSE_NAMES[@]}"; do
cp "${LICENSE_SRCS[$i]}" "$DEST/${LICENSE_NAMES[$i]}"
done
done
echo "built: $OUT_DIR/$CLIENT_LIB_NAME, $OUT_DIR/$CAPTURE_LIB_NAME ($TRACY_VERSION)"
echo "staged: $RES_DIR/$CLIENT_LIB_NAME, $RES_DIR/$CAPTURE_LIB_NAME"
+13
View File
@@ -0,0 +1,13 @@
#!/usr/bin/env bash
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "$REPO_ROOT/umbra"
gradle_args=(:compileJava :compileMixinJava :test --no-daemon --console=plain --stacktrace)
if [[ -n "${UMBRA_GRADLE_ARGS:-}" ]]; then
read -ra extra_args <<< "$UMBRA_GRADLE_ARGS"
gradle_args+=("${extra_args[@]}")
fi
sh ./gradlew "${gradle_args[@]}"
+81
View File
@@ -0,0 +1,81 @@
name: Build Tracy natives
on:
push:
tags:
- "tracy-v*"
workflow_dispatch:
inputs:
publish:
description: Publish angelica-tracy.jar to the GTNH maven
type: boolean
required: false
default: false
jobs:
build:
strategy:
matrix:
platform: [linux-x64, windows-x64, macos-x64, macos-arm64]
include:
- platform: linux-x64
os: ubuntu-22.04
- platform: windows-x64
os: windows-2025
- platform: macos-x64
os: macos-15-intel
- platform: macos-arm64
os: macos-15
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v5
- name: Build Tracy natives
shell: bash
env:
OUT_DIR: dist/tracy-natives
run: |
export TRACY_VERSION="v$(sed -nE 's/^tracy = "(.*)"$/\1/p' gradle/libs.versions.toml)"
bash .github/scripts/build-tracy-natives.sh
- uses: actions/upload-artifact@v4
with:
name: tracy-natives-${{ matrix.platform }}
path: dist/tracy-natives/*
if-no-files-found: error
assemble:
needs: build
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v5
- uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 21
- uses: gradle/actions/setup-gradle@v4
- uses: actions/download-artifact@v5
with:
pattern: tracy-natives-*
path: staging
- name: Stage natives into tracy-client resources
shell: bash
run: |
version="$(sed -nE 's/^tracy = "(.*)"$/\1/p' gradle/libs.versions.toml)"
for d in staging/tracy-natives-*; do
dest="tracy-client/src/main/resources/natives/tracy/$version/${d#staging/tracy-natives-}"
mkdir -p "$dest"
cp "$d"/* "$dest/"
done
find tracy-client/src/main/resources/natives -type f
- name: Build angelica-tracy.jar
run: ./gradlew :tracy-client:distJar
- uses: actions/upload-artifact@v4
with:
name: angelica-tracy
path: tracy-client/build/libs/angelica-tracy-*.jar
if-no-files-found: error
- name: Publish to GTNH maven
if: github.ref_type == 'tag' || inputs.publish
env:
MAVEN_USER: ${{ secrets.MAVEN_USER }}
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
run: ./gradlew -PpublishTracy=true :tracy-client:publish
+20 -1
View File
@@ -3,7 +3,7 @@ name: Release tagged build
on:
push:
tags: [ '*' ]
tags: [ '*', '!tracy-v*' ]
permissions:
contents: write
@@ -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
+25
View File
@@ -0,0 +1,25 @@
name: Umbra build
on:
pull_request:
branches: [ master, main ]
push:
branches: [ master, main ]
jobs:
umbra-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- uses: actions/setup-java@v5
with:
distribution: zulu
java-version: |
21
25
- uses: gradle/actions/setup-gradle@v5
with:
validate-wrappers: true
- run: .github/scripts/umbra-build.sh
+14 -4
View File
@@ -1,10 +1,12 @@
.gradle
.kotlin/
.lwjgl/
.settings
/.idea/
/.vs/
/.vscode/
/run/
/build/
run/
build/
/eclipse/
.classpath
.project
@@ -12,6 +14,7 @@
/config/
/crash-reports/
/logs/
tmp/
options.txt
/saves/
usernamecache.json
@@ -36,5 +39,12 @@ addon.local.gradle
addon.local.gradle.kts
addon.late.local.gradle
addon.late.local.gradle.kts
/*.csv
!gl_functions.csv
local/
tracy-client/src/main/resources/natives/
tracy-client/angelica/
dist/tracy-natives/
*.tracy
*.jfr
tracy-client/angelica/
shaderc_failures/
+57
View File
@@ -0,0 +1,57 @@
# AI Usage Policy
🌐 **Languages:** [English](AI_POLICY.md) | [Українська](AI_POLICY_uk.md)
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
+56
View File
@@ -0,0 +1,56 @@
# Політика використання ШІ
🌐 **Мови:** [English](AI_POLICY.md) | [Українська](AI_POLICY_uk.md)
Цей проєкт має суворі правила щодо використання штучного інтелекту (ШІ):
* **Людина, яка контролює процес, повинна повністю розуміти весь код.**
Якщо ви не можете пояснити, що роблять ваші зміни та як вони взаємодіють із
більшою системою без допомоги інструментів ШІ, не пропонуйте ці зміни.
* **Обговорення та задачі можуть створюватися за допомогою ШІ, але повинні перебувати
під повним контролем людини.** Це означає, що будь-який контент, згенерований за допомогою ШІ,
перед надсиланням має бути перевірений *та відредагований* людиною.
Автор повинен сам розібратися в питанні та відсіяти все зайве.
* **Медіаконтент, згенерований ШІ, заборонений (текстури, відео, аудіо тощо).**
Текст і код є єдиним контентом, згенерованим ШІ, який дозволено використовувати
згідно з іншими правилами цієї політики.
* **Повідомлення до комітів та описи PR повинні бути написані у вашому власному стилі.**
Коміти, повністю або частково створені ШІ, заборонені, включно з тегами `Co-authored-by` з вказівкою ШІ.
* **Подавайте виправлення лише для тих багів, із якими ви особисто зіткнулися в грі.**
Якщо ви особисто не стикалися з багом, не відтворили його власноруч і ще не знаєте
добре відповідний код, не натравлюйте на нього ШІ, щоб потім перенаправити результат нам.
Патчі, написані для проблем, із якими автор особисто ніколи не стикався, зазвичай
виправляють не те, що потрібно, та/або створюють нові баги.
* **Стиль та суть письмового контенту повинні належати вам.**
Для документації та особливо для квестів тон і думка, яку ви намагаєтеся донести, мають
виходити від вас, а не від ШІ. Ви можете використовувати ШІ для покращення формулювань
або вичитки, якщо бажаєте, але якщо суть і стиль не ваші, не пропонуйте такий контент.
* **За зловживання ШІ можна отримати блокування.** Користувачі, які створюють
низькоякісний контент, очевидно згенерований ШІ (слоп), можуть бути заблоковані для подальших внесків.
Ми залюбки допомагаємо розробникам-початківцям навчатися та рости, але якщо ви зацікавлені
в цьому, не використовуйте ШІ, і ми вам допоможемо. Нам прикро, що недобросовісні
користувачі ШІ зіпсували це для вас.
## Тут працюють люди
Будь ласка, пам’ятайте, що цей проєкт підтримують люди.
Кожне обговорення, задача та запит на злиття читаються й перевіряються
людьми (а іноді й машинами). Це точка перетину, де люди взаємодіють
один з одним та виконують роботу. Неґречно та неповажно приходити сюди
з неякісною, некваліфікованою та зробленою абияк роботою, оскільки це
перекладає тягар перевірки на мейнтейнерів.
В ідеальному світі ШІ щоразу створював би високоякісну та точну роботу.
Але сьогодні ця реальність залежить від користувача ШІ. І сьогодні чимало
користувачів використовують ШІ неналежним чином. Тому, поки це не зміниться,
ми змушені мати суворі правила, щоб захистити мейнтейнерів.
Створено на основі https://github.com/ghostty-org/ghostty/blob/main/AI_POLICY.md
+17
View File
@@ -0,0 +1,17 @@
# Angelica dev commands
# Build with headless display
build:
xvfb-run --server-args="-screen 0 1366x768x24" ./gradlew build
# Run tests with headless display
test:
xvfb-run --server-args="-screen 0 1366x768x24" ./gradlew test
# Build jar only (no tests)
jar:
./gradlew jar
# Clean build directory
clean:
./gradlew clean
+34 -13
View File
@@ -2,45 +2,57 @@
<sup>**[CurseForge](https://www.curseforge.com/minecraft/mc-mods/angelica) | [Modrinth](https://modrinth.com/mod/angelica)**</sup>
🌐 **Languages:** [English](README.MD) | [Українська](README_uk.MD)
* The little Angle [sic] that supports shaders while saving your fps from a certain death -boubou
* Is it obtuse, or accute? -Caedis
* It's a cute angle -mitchej123
* It's the angel that pulls you out of OptiFine's hell -Roadhog360
## **Beta Quality - Here there be Dragons**
* Shaders sneak preview - Please don't report issues yet, but please do submit fixes
* Complementary & BSL mostly function, but have some bugs
* Sildurs, Seus, and any other shader probably doesn't work
## **Beta Quality - Here there be Dragons**
* Shaders - Beta
* Complementary + Euphoria Patches is the primary compatibility target
* Sildurs, Seus, and any other shaders might work
## Core Profile
Targets GL 3.3+ core profile
* FFP emulation via generated GLSL shaders
* Display list emulation
* Software matrix stacks and state management through GLSM
* GL_QUADS → GL_TRIANGLES conversion
* Automatic transformation of mod shaders that use compat-only GLSL builtins (`gl_ModelViewMatrix`, `texture2D`, etc.)
# Required Dependencies
* [UniMixins](https://github.com/LegacyModdingMC/UniMixins/releases) >= 0.1.14
* [GTNHLib](https://github.com/GTNewHorizons/GTNHLib/releases) >= 0.4.2
* [UniMixins](https://github.com/LegacyModdingMC/UniMixins/releases) >= 0.1.23
* [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
* Forge Relocation - Currently non Thread Safe; safe if not in use
* ProjectRed Frames - Currently non Thread Safe; safe if not in use'
* ProjectRed Frames - Currently non Thread Safe; safe if not in use
# Potential Incompatibilities
* EndlessIDs - Compat commits have been made, might work, might not - not fully tested
# Known Incompatibilities
* FalseTweaks - Not currently planned (Looking for volunteers) [Might work if you effectively disable most of the mod]
* SmoothFont - Not currently planned (Looking for volunteers)
* RPLE - Not currently planned (Looking for volunteers)
* LiteLoader - Install [LiteloaderLoader](https://github.com/Midnight145/LiteloaderLoader)
# Permanent Incompatibilities
* OptiFine [Disabled, won't fix]
* FastCraft [Disabled, won't fix]
* BetterFPS [Disabled, won't fix]
* OptimizationsAndTweaks [Disabled, won't fix]
* [Amazing Trophies 1.2.1 and below](https://github.com/GTNewHorizons/Amazing-Trophies/releases) - use 1.2.2+
* [Amazing Trophies 1.2.1 and below](https://github.com/GTNewHorizons/Amazing-Trophies/releases) - Use 1.2.2+
* [LWJGL3ify 1.5.4 and below](https://github.com/GTNewHorizons/lwjgl3ify/releases/) - Use the latest version available, 1.5.7+
* [ArchaicFix 0.6.2 and below](https://www.curseforge.com/minecraft/mc-mods/archaicfix/files/all?page=1&pageSize=20) - Use 0.7.0 or above
* [Hodgepodge 2.4.3 and below](https://github.com/GTNewHorizons/Hodgepodge/releases) - Use 0.4.5 or above
* [Hodgepodge 2.4.3 and below](https://github.com/GTNewHorizons/Hodgepodge/releases) - Use 2.4.5 or above
* 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
@@ -55,16 +67,16 @@
* The [Iris](https://github.com/IrisShaders/Iris) Team: coderbot, IMS212, Justsnoopy30, FoundationGames
* Asek3 for the Iris Forge port [Oculus](https://github.com/Asek3/Oculus/tree/839ce8eca8cf0c4f6b7a1322b906e5c99125ca69)
* Backported code under the LGPL
* Sodium
* Sodium / Celeritas
* JellySquid & the CaffeineMC team, for making Sodium in the first place
* Embeddedt for [Embeddium](https://github.com/embeddedt/embeddium/tree/dc59ca357c25beefd6288f0d1d40b4cd8e670ab8) under the [LGPL 3.0](https://github.com/embeddedt/embeddium/blob/16.x/forge/LICENSE)
* Embeddedt for Celeritas used under the LGPL 3.0
* Reese's Sodium Options
* FlashyReese for [Reese's Sodium Options](https://github.com/FlashyReese/reeses-sodium-options/tree/aa4ea67047cf53e54976e93c6bb1ab5ba16b142b) under the [MIT license](https://github.com/FlashyReese/reeses-sodium-options/blob/aa4ea67047cf53e54976e93c6bb1ab5ba16b142b/LICENSE.txt)
* Neodymium
* Makamys for [Neodymium](https://github.com/makamys/Neodymium) - Various Tessellator -> Quad functionality taken under the LGPL 3
* MCPatcherForge
* Original [MCPatcher](https://bitbucket.org/prupe/mcpatcher/src/master/): Paul Rupe
* [Forge version](https://bitbucket.org/prupe/mcpatcher/src/master/): Mist475 under LGPL 3.0 with default ctm textures for glass and bookshelves by DarkBum under CC-BY-SA
* [Forge version](https://github.com/mist475/MCPatcherForge): Mist475 under LGPL 3.0 with default ctm textures for glass and bookshelves by DarkBum under CC-BY-SA
* HUDCaching
* Created by [Moulberry](https://github.com/Moulberry/MCHUDCaching), backported from [Patcher](https://github.com/Sk1erLLC/Patcher) mod by Alexdoru under the CC-BY-SA-NC 4.0 license
* Angelica
@@ -82,3 +94,12 @@
* Code from both glsl-transformation-lib and Monocle taken under their respective LGPL 3.0 licenses
* FalseTweaks
* FalsePattern for [FalseTweaks](https://github.com/falsepattern/falsetweaks) - Item Rendering Optimizations taken from FalseTweaks under the LGPL 3.0 license
* Beddium
* Venn and Falsepattern for [Beddium](https://github.com/vfx-dev/Beddium) - MinimapRenderer, SafeChunkTracker, and some debug commands adapted under the LGPL 3.0 License
* [Mesa](https://gitlab.freedesktop.org/mesa/mesa) — Code ported and/or inspired from Mesa - under the MIT licensed
* Display list ID allocator ported from [util_idalloc](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/util/u_idalloc.c) (Copyright 2017 Valve Corporation)
* FFP vertex shader generation inspired by ffvertex_prog.c (Copyright 2007 VMware, Inc.)
* FFP fragment shader generation inspired by ff_fragment_shader.c (Copyright 2007, 2009 VMware, Inc. / 2010-2011 Intel Corporation)
* Fog parameter optimization inspired by st_nir_lower_fog.c (Copyright 2023 Google LLC) and prog_statevars.c (Copyright 1999-2007 Brian Paul)
* RGSS Terrain Texture Filtering
* Indiesoft for [defold-sharp-sprite](https://github.com/indiesoftby/defold-sharp-sprite), used as the reference implementation under the MIT license
+103
View File
@@ -0,0 +1,103 @@
# Angelica<img src="img/logo-readme.png" align="right">
<sup>**[CurseForge](https://www.curseforge.com/minecraft/mc-mods/angelica) | [Modrinth](https://modrinth.com/mod/angelica)**</sup>
🌐 **Мови:** [English](README.MD) | [Українська](README_uk.MD)
* Маленький Кут [sic], що підтримує шейдери, водночас рятуючи ваші FPS від вірної смерті — boubou
* Він тупий чи гострий? — Caedis
* Це милий кут — mitchej123
* Це янгол, який витягує вас із пекла OptiFine — Roadhog360
## **Бета-якість — Тут водяться дракони**
* Шейдери — Бета
* Complementary + Euphoria Patches є головною ціллю для сумісності
* Sildurs, Seus та будь-які інші шейдери можуть працювати
## Базовий профіль
Цільовим є базовий профіль GL 3.3+
* Емуляція FFP через згенеровані GLSL-шейдери
* Емуляція списків відображення
* Програмні стеки матриць та керування станом через GLSM
* Конвертація GL_QUADS → GL_TRIANGLES
* Автоматична трансформація шейдерів з модів, які використовують вбудовані функції GLSL виключно для сумісності (`gl_ModelViewMatrix`, `texture2D` тощо)
# Обов’язкові залежності
* [UniMixins](https://github.com/LegacyModdingMC/UniMixins/releases) >= 0.1.23
* [GTNHLib](https://github.com/GTNewHorizons/GTNHLib/releases) >= 0.11.19
* ПРИМІТКА: Деякі моди не є обов’язковими, але якщо вони встановлені, вимагається їхня конкретна версія — див. розділ `Постійна несумісність` нижче.
# Відома (тимчасова) несумісність
* Forge Relocation — Наразі не є потокобезпечним; безпечно, якщо не використовується
* ProjectRed Frames — Наразі не є потокобезпечним; безпечно, якщо не використовується
# Можлива несумісність
* EndlessIDs — Коміти для сумісності були зроблені, може працювати, а може й ні — не повністю протестовано
# Відома несумісність
* FalseTweaks — Наразі не планується (Шукаємо волонтерів) [Може працювати, якщо фактично вимкнути більшу частину мода]
* RPLE — Наразі не планується (Шукаємо волонтерів)
* LiteLoader — Встановіть [LiteloaderLoader](https://github.com/Midnight145/LiteloaderLoader)
# Постійна несумісність
* OptiFine [Вимкнено, виправлятись не буде]
* FastCraft [Вимкнено, виправлятись не буде]
* BetterFPS [Вимкнено, виправлятись не буде]
* OptimizationsAndTweaks [Вимкнено, виправлятись не буде]
* [Amazing Trophies 1.2.1 і нижче](https://github.com/GTNewHorizons/Amazing-Trophies/releases) — Використовуйте 1.2.2+
* [LWJGL3ify 1.5.4 і нижче](https://github.com/GTNewHorizons/lwjgl3ify/releases/) — Використовуйте найновішу доступну версію, 1.5.7+
* [ArchaicFix 0.6.2 і нижче](https://www.curseforge.com/minecraft/mc-mods/archaicfix/files/all?page=1&pageSize=20) — Використовуйте 0.7.0 або вище
* [Hodgepodge 2.4.3 і нижче](https://github.com/GTNewHorizons/Hodgepodge/releases) — Використовуйте 2.4.5 або вище
* CodeChicken[Core](https://github.com/GTNewHorizons/CodeChickenCore/releases)/[Lib](https://github.com/GTNewHorizons/CodeChickenLib/releases) <1.2.0 — Проблеми з багатопотоковістю, використовуйте 1.3.0+
* Neodymium — Не перевірено, але навряд чи буде сумісним. (Побудова сітки чанків точно несумісна, інші функції можуть працювати залежно від конфігурації)
* SmoothFont — Додано еквівалентний функціонал
* ClientFixer — Використовуйте пакет ресурсів, як-от [GTNH-FTI-Standard-Font](https://github.com/Eldrinn-Elantey/GTNH-FTI-Standard-Font)
# Подяки
* ShadersMod
* Оригінальні ShadersMods: karyonix, sonic ether, id_miner, daxnitro
* eigenraven, Caedis, glowredman та mitchej123 за конвертацію ASM у Mixin
* eigenraven та vlaetansky за те, що змусили оригінальний ShadersMod знову компілюватися
* ArchaicFix
* Включає графічні покращення з [ArchaicFix](https://github.com/embeddedt/ArchaicFix/tree/4afb943751883b27a1f0aa7c5ba71698bc897e95) від embeddedt за ліцензією [LGPL](https://github.com/embeddedt/ArchaicFix/blob/main/LICENSE.md)
* NotFine
* Включає модифіковану версію [NotFine](https://github.com/jss2a98aj/NotFine/tree/a2652e24b5654f437fb7138f6d1f5e947a7d7125) від jss2a98aj за ліцензією LGPL
* Iris Shaders
* Команда [Iris](https://github.com/IrisShaders/Iris): coderbot, IMS212, Justsnoopy30, FoundationGames
* Asek3 за порт Iris для Forge [Oculus](https://github.com/Asek3/Oculus/tree/839ce8eca8cf0c4f6b7a1322b906e5c99125ca69)
* Бекпортований код за ліцензією LGPL
* Sodium / Celeritas
* JellySquid та команді CaffeineMC за створення Sodium
* Embeddedt за Celeritas, що використовується за ліцензією LGPL 3.0
* Reese's Sodium Options
* FlashyReese за [Reese's Sodium Options](https://github.com/FlashyReese/reeses-sodium-options/tree/aa4ea67047cf53e54976e93c6bb1ab5ba16b142b) за ліцензією [MIT](https://github.com/FlashyReese/reeses-sodium-options/blob/aa4ea67047cf53e54976e93c6bb1ab5ba16b142b/LICENSE.txt)
* Neodymium
* Makamys за [Neodymium](https://github.com/makamys/Neodymium) — Різноманітний функціонал Tessellator -> Quad взято за ліцензією LGPL 3
* MCPatcherForge
* Оригінальний [MCPatcher](https://bitbucket.org/prupe/mcpatcher/src/master/): Paul Rupe
* [Версія для Forge](https://github.com/mist475/MCPatcherForge): Mist475 за ліцензією LGPL 3.0 з типовими ctm-текстурами для скла та книжкових полиць від DarkBum за ліцензією CC-BY-SA
* HUDCaching
* Створено [Moulberry](https://github.com/Moulberry/MCHUDCaching), бекпортовано з мода [Patcher](https://github.com/Sk1erLLC/Patcher) від Alexdoru за ліцензією CC-BY-SA-NC 4.0
* Angelica
* mitchej123, Omni, Makamys, Embeddedt, NanoLive, Caedis, Cardinalstar, Alexdoru, Eigenraven, mist475, Clepto, kurrycat за допомогу в запуску та налаштуванні Angelica!
* IMS, jss2a98aj, || dbp ||, roadhog360 за поради та моральну підтримку
* Forge 1.12+
* Деякі файли були взяті з Minecraft Forge за ліцензією LGPL 2.1 і позначені відповідно
* FRAPI та Indigo
* API для побудови моделей значною мірою натхненний/скопійований з Fabric; автори оригінального коду (upstream) можуть використовувати його за оригінальною ліцензією Apache 2.0,
див. LICENSE у `com.gtnewhorizons.angelica.models`
* Dynamic Lights
* LambdAurora за [LambDynamicLights](https://github.com/LambdAurora/LambDynamicLights) за ліцензією MIT, та anthxnymc за порт для Forge [DynamicLightsReforged](https://github.com/anthxnymc/DynamicLightsReforged), також за ліцензією MIT
* Embeddedt та Ferri_Arnus за [glsl-transformation-lib](https://github.com/TauMC/glsl-transformation-lib)
* Також за роботу над [Monocle](https://github.com/ferriarnus/Monocle), який використовувався як еталон для реалізації glsl-transformation-lib
* Код з обох glsl-transformation-lib та Monocle взято за їхніми відповідними ліцензіями LGPL 3.0
* FalseTweaks
* FalsePattern за [FalseTweaks](https://github.com/falsepattern/falsetweaks) — Оптимізації рендерингу предметів взято з FalseTweaks за ліцензією LGPL 3.0
* Beddium
* Venn та Falsepattern за [Beddium](https://github.com/vfx-dev/Beddium) — MinimapRenderer, SafeChunkTracker та деякі команди для налагодження адаптовано за ліцензією LGPL 3.0
* [Mesa](https://gitlab.freedesktop.org/mesa/mesa) — Код портований з Mesa та/або натхненний нею — за ліцензією MIT
* Розподілювач ID списків відображення портовано з [util_idalloc](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/util/u_idalloc.c) (Copyright 2017 Valve Corporation)
* Генерація вершинних шейдерів FFP натхненна ffvertex_prog.c (Copyright 2007 VMware, Inc.)
* Генерація фрагментних шейдерів FFP натхненна ff_fragment_shader.c (Copyright 2007, 2009 VMware, Inc. / 2010-2011 Intel Corporation)
* Оптимізація параметрів туману натхненна st_nir_lower_fog.c (Copyright 2023 Google LLC) та prog_statevars.c (Copyright 1999-2007 Brian Paul)
+171 -15
View File
@@ -1,27 +1,79 @@
import com.modrinth.minotaur.TaskModrinthUpload
import net.darkhax.curseforgegradle.TaskPublishCurseForge
import xyz.wagyourtail.jvmdg.gradle.task.files.DowngradeFiles
plugins {
id("com.gtnewhorizons.gtnhconvention")
}
minecraft {
extraRunJvmArguments.add("-Dangelica.enableTestBlocks=true")
val lwjglDebug = false
val gpuHud = false
val renderdoc = false
val rgp = false
extra["lwjglDebug"] = lwjglDebug
minecraft {
extraRunJvmArguments.add("-Dangelica.debug.testBlocks=true")
extraRunJvmArguments.add("-Dangelica.tracy=true")
extraRunJvmArguments.add("-Dangelica.sdlgpu.enable=true")
extraRunJvmArguments.add("-Dangelica.unmappedGL=fail")
extraRunJvmArguments.add("-Dorg.lwjgl.util.Debug=$lwjglDebug")
extraRunJvmArguments.add("-Dsun.net.client.defaultConnectTimeout=5000")
extraRunJvmArguments.add("-Dsun.net.client.defaultReadTimeout=5000")
// extraRunJvmArguments.addAll("-Dlegacy.debugClassLoadingSave=true")
// extraRunJvmArguments.addAll("-Drfb.dumpLoadedClasses=true", "-Drfb.dumpLoadedClassesPerTransformer=true")
//extraRunJvmArguments.add("-Dangelica.debug.redirectorLogspam=true")
}
tasks.test {
useJUnitPlatform()
testLogging {
events("passed", "skipped", "failed")
apply(from = "gradle/angelica-run-common.gradle.kts")
if (gpuHud) {
tasks.withType<JavaExec>().matching { it.name.startsWith("runClient") }.configureEach {
if (org.gradle.internal.os.OperatingSystem.current().isMacOsX) {
environment("MTL_HUD_ENABLED", "1")
} else {
environment("MANGOHUD", "1")
environment("LD_PRELOAD", "/usr/\$LIB/mangohud/libMangoHud_shim.so")
}
}
javaLauncher = javaToolchains.launcherFor {
languageVersion = JavaLanguageVersion.of(8)
}
dependsOn(tasks.extractNatives2)
jvmArgs("-Djava.library.path=${tasks.extractNatives2.get().destinationFolder.asFile.get().path}")
}
tasks.processResources {
inputs.property("version", project.version.toString())
filesMatching("META-INF/rfb-plugin/*") {
expand("version" to project.version.toString())
configurations.all {
exclude(group = "com.github.GTNewHorizons", module = "Angelica")
}
val osName = System.getProperty("os.name").lowercase()
val isMacOs = org.gradle.internal.os.OperatingSystem.current().isMacOsX
val isMacOsArm64 = isMacOs && System.getProperty("os.arch") == "aarch64"
// Linux runClient: RenderDoc frame capture (F12) OR RADV RGP/SQTT capture. Mutually exclusive; RGP wins
tasks.withType<JavaExec>().configureEach {
if (name.startsWith("runClient") && !isMacOs && !osName.contains("windows")) {
environment("WAYLAND_DISPLAY", "")
environment("XDG_SESSION_TYPE", "x11")
val rgpFrame = project.findProperty("rgpFrame") as String?
val rgpCapture = rgp || project.hasProperty("rgp") || rgpFrame != null
if (rgpCapture) {
environment("MESA_VK_TRACE", "rgp")
if (rgpFrame != null) {
environment("MESA_VK_TRACE_FRAME", rgpFrame)
doFirst { logger.lifecycle("RGP: capturing frame $rgpFrame -> /tmp/java_*.rgp") }
} else {
environment("MESA_VK_TRACE_TRIGGER", "/tmp/angelica_rgp.trigger")
doFirst { logger.lifecycle("RGP: touch /tmp/angelica_rgp.trigger (or press F1) to capture -> /tmp/java_*.rgp") }
}
} else if (renderdoc || project.hasProperty("renderdoc")) {
val renderdocLib = file(project.findProperty("renderdocLib") as String? ?: "${System.getProperty("user.home")}/Downloads/renderdoc_1.44/lib/librenderdoc.so")
if (renderdocLib.isFile) {
environment("LD_PRELOAD", renderdocLib.absolutePath)
doFirst { logger.lifecycle("RENDERDOC: LD_PRELOAD set, press F12 to capture") }
} else {
doFirst { logger.lifecycle("RENDERDOC: enabled but ${renderdocLib.absolutePath} not found; set -PrenderdocLib=<path>") }
}
}
}
}
@@ -31,3 +83,107 @@ tasks.register<Copy>("copyDependencies") {
from(configurations.default)
into("testDependencies")
}
val embedOnly: Configuration by configurations
val shadowImplementation: Configuration by configurations
val downgradeEmbedOnlyForTest by tasks.registering(DowngradeFiles::class) {
inputCollection = embedOnly
outputs.dir(temporaryDir)
}
val stripModuleInfoFromShadow by tasks.registering(Jar::class) {
dependsOn(shadowImplementation)
from(shadowImplementation.map(::zipTree))
exclude("module-info.class")
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
archiveFileName = "shadow-stripped.jar"
}
val downgradeShadowImplForTest by tasks.registering(DowngradeFiles::class) {
inputCollection = files(stripModuleInfoFromShadow.map { it.archiveFile })
outputs.dir(temporaryDir)
}
val glsmTestFixtures: Configuration by configurations.creating {
isCanBeConsumed = false
isCanBeResolved = true
attributes {
attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME))
attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements.JAR))
}
}
val downgradeGlsmTestFixturesForTest by tasks.registering(DowngradeFiles::class) {
inputCollection = glsmTestFixtures
outputs.dir(temporaryDir)
}
fun downgraded(task: TaskProvider<DowngradeFiles>): FileCollection = files(task.map { it.outputMap.values })
fun Test.configureAngelicaJava8() {
javaLauncher = javaToolchains.launcherFor {
languageVersion = JavaLanguageVersion.of(8)
if (isMacOsArm64) {
vendor = JvmVendorSpec.AZUL
}
}
dependsOn(tasks.extractNatives2)
jvmArgs("-Djava.library.path=${tasks.extractNatives2.get().destinationFolder.asFile.get().path}")
if (isMacOs) {
jvmArgs("-Dapple.awt.UIElement=true")
}
testLogging { events("passed", "skipped", "failed") }
val swaps = mapOf(
embedOnly to downgradeEmbedOnlyForTest,
shadowImplementation to downgradeShadowImplForTest,
glsmTestFixtures to downgradeGlsmTestFixturesForTest,
)
dependsOn(swaps.values)
for ((original, downgrade) in swaps) {
classpath = classpath.minus(original).plus(downgraded(downgrade))
}
}
tasks.test {
useJUnitPlatform { excludeTags = setOf("gl-core") }
configureAngelicaJava8()
jvmArgs("-Dceleritas.lwjglService=org.taumc.celeritas.lwjgl.HeadlessTestLWJGLService")
dependsOn(":glsm:classes", ":lwjgl3-backend:classes", ":sdl-gpu:classes")
}
val downgradeRootMainClasses = tasks.named<DowngradeFiles>("downgradeMainClasses")
val downgradeRootTestClasses = tasks.named<DowngradeFiles>("downgradeTestClasses")
val glCoreTest by tasks.registering(Test::class) {
description = "Runs the gl-core tests in their own JVM with a real GL context."
group = "verification"
useJUnitPlatform { includeTags = setOf("gl-core") }
dependsOn(downgradeRootMainClasses, downgradeRootTestClasses)
val rawMain = sourceSets["main"].output.classesDirs
val rawTest = sourceSets["test"].output.classesDirs
val downgradedTest = downgraded(downgradeRootTestClasses)
setTestClassesDirs(downgradedTest)
classpath = downgradedTest
.plus(downgraded(downgradeRootMainClasses))
.plus(sourceSets["test"].runtimeClasspath.minus(rawMain).minus(rawTest))
configureAngelicaJava8()
mustRunAfter(tasks.test)
}
tasks.test { finalizedBy(glCoreTest) }
apply(from = "gradle/angelica-shadow-common.gradle.kts")
apply(from = "experimental-surround/surround.root.gradle.kts")
tasks.withType<TaskPublishCurseForge>().configureEach {
uploadArtifacts.forEach { it.addGameVersion("Client") }
}
tasks.withType<TaskModrinthUpload>().configureEach {
setDependsOn(dependsOn.filterNot { it == "build" } + "assemble")
}
+14
View File
@@ -0,0 +1,14 @@
plugins {
`kotlin-dsl`
}
repositories {
gradlePluginPortal()
mavenCentral()
}
// Re-export Gradle's generated typed catalog accessors so applied scripts (dependencies.gradle.kts) can use them.
dependencies {
implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location))
implementation(libs.asm.build)
}
+5
View File
@@ -0,0 +1,5 @@
dependencyResolutionManagement {
versionCatalogs {
create("libs") { from(files("../gradle/libs.versions.toml")) }
}
}
@@ -0,0 +1,49 @@
import org.objectweb.asm.AnnotationVisitor
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.ClassWriter
import org.objectweb.asm.Opcodes
import java.io.File
private const val LWJGL3_AWARE_DESC = "Lme/eigenraven/lwjgl3ify/api/Lwjgl3Aware;"
/**
* Stamps @Lwjgl3Aware onto every class under [classesDir] - classes that already carry the annotation are left untouched.
*/
fun injectLwjgl3Aware(classesDir: File) {
if (!classesDir.isDirectory) return
classesDir.walkTopDown()
.filter { it.isFile && it.extension == "class" && it.name != "module-info.class" }
.forEach { classFile ->
val reader = ClassReader(classFile.readBytes())
if (hasLwjgl3Aware(reader)) return@forEach
val writer = ClassWriter(reader, 0)
reader.accept(object : ClassVisitor(Opcodes.ASM9, writer) {
override fun visit(
version: Int,
access: Int,
name: String?,
signature: String?,
superName: String?,
interfaces: Array<out String>?
) {
super.visit(version, access, name, signature, superName, interfaces)
super.visitAnnotation(LWJGL3_AWARE_DESC, true)?.visitEnd()
}
}, 0)
classFile.writeBytes(writer.toByteArray())
}
}
private fun hasLwjgl3Aware(reader: ClassReader): Boolean {
var found = false
reader.accept(object : ClassVisitor(Opcodes.ASM9) {
override fun visitAnnotation(descriptor: String?, visible: Boolean): AnnotationVisitor? {
if (descriptor == LWJGL3_AWARE_DESC) found = true
return null
}
}, ClassReader.SKIP_CODE or ClassReader.SKIP_DEBUG or ClassReader.SKIP_FRAMES)
return found
}
-122
View File
@@ -1,122 +0,0 @@
/*
* Add your dependencies here. Supported configurations:
* - api("group:name:version:classifier"): if you use the types from this dependency in the public API of this mod
* Available at runtime and compiletime for mods depending on this mod
* - implementation("g:n:v:c"): if you need this for internal implementation details of the mod, but none of it is visible via the public API
* Available at runtime but not compiletime for mods depending on this mod
* - compileOnly("g:n:v:c"): if the mod you're building doesn't need this dependency during runtime at all, e.g. for optional mods
* Not available at all for mods depending on this mod, only visible at compiletime for this mod
* - compileOnlyApi("g:n:v:c"): like compileOnly, but also visible at compiletime for mods depending on this mod
* Available at compiletime but not runtime for mods depending on this mod
* - runtimeOnlyNonPublishable("g:n:v:c"): if you want to include a mod in this mod's runClient/runServer runs, but not publish it as a dependency
* Not available at all for mods depending on this mod, only visible at runtime for this mod
* - devOnlyNonPublishable("g:n:v:c"): a combination of runtimeOnlyNonPublishable and compileOnly for dependencies present at both compiletime and runtime,
* but not published as Maven dependencies - useful for RFG-deobfuscated dependencies or local testing
* - runtimeOnly("g:n:v:c"): if you don't need this at compile time, but want it to be present at runtime
* Available at runtime for mods depending on this mod
* - annotationProcessor("g:n:v:c"): mostly for java compiler plugins, if you know you need this, use it, otherwise don't worry
* - testCONFIG("g:n:v:c") - replace CONFIG by one of the above (except api), same as above but for the test sources instead of main
*
* - shadowImplementation("g:n:v:c"): effectively the same as API, but the dependency is included in your jar under a renamed package name
* Requires you to enable usesShadowedDependencies in gradle.properties
*
* - compile("g:n:v:c"): deprecated, replace with "api" (works like the old "compile") or "implementation" (can be more efficient)
*
* You can exclude transitive dependencies (dependencies of the chosen dependency) by appending { transitive = false } if needed,
* but use this sparingly as it can break using your mod as another mod's dependency if you're not careful.
*
* To depend on obfuscated jars you can use `devOnlyNonPublishable(rfg.deobf("dep:spec:1.2.3"))` to fetch an obfuscated jar from maven,
* or `devOnlyNonPublishable(rfg.deobf(project.files("libs/my-mod-jar.jar")))` to use a file.
*
* Gradle names for some of the configuration can be misleading, compileOnlyApi and runtimeOnly both get published as dependencies in Maven, but compileOnly does not.
* The buildscript adds runtimeOnlyNonPublishable to also have a runtime dependency that's not published.
*
* For more details, see https://docs.gradle.org/8.0.1/userguide/java_library_plugin.html#sec:java_library_configurations_graph
*/
configurations {
transformedMod // Mods that can be transformed - used for compiling angelica, but not necessary at runtime
transformedMod.canBeConsumed = false
transformedModCompileOnly
transformedModCompileOnly.canBeConsumed = false
// Add the transformed mod dependencies to the compilation classpaths, but don't publish them in the Maven metadata
compileClasspath.extendsFrom(transformedMod, transformedModCompileOnly)
testCompileClasspath.extendsFrom(transformedMod, transformedModCompileOnly)
// Can be enabled for testing if desired
// runtimeClasspath.extendsFrom(transformedMod)
// testRuntimeClasspath.extendsFrom(transformedMod)
}
dependencies {
compileOnly("com.github.GTNewHorizons:Hodgepodge:2.5.78")
compileOnly("com.gtnewhorizons.retrofuturabootstrap:RetroFuturaBootstrap:0.4.0") { transitive = false }
compileOnly("org.projectlombok:lombok:1.18.22") { transitive = false }
annotationProcessor("org.projectlombok:lombok:1.18.22")
// Use modern models+textures to inject QuadProviders
devOnlyNonPublishable('com.github.GTNewHorizons:TX-Loader:1.7.2:dev')
// Iris Shaders
compileOnly('org.jetbrains:annotations:24.0.1')
api("com.github.GTNewHorizons:GTNHLib:0.5.20:dev")
shadowImplementation("org.anarres:jcpp:1.4.14") // Apache 2.0
shadowImplementation("org.taumc:glsl-transformation-lib:0.2.0-4.g6b42bca") {
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"
// Because who doesn't want NEI
devOnlyNonPublishable('com.github.GTNewHorizons:NotEnoughItems:2.6.45-GTNH:dev')
devOnlyNonPublishable('com.github.GTNewHorizons:CodeChickenCore:1.3.10:dev')
// Notfine Deps
compileOnly("thaumcraft:Thaumcraft:1.7.10-4.2.3.5:dev")
devOnlyNonPublishable("com.github.GTNewHorizons:Baubles:1.0.4:dev")
compileOnly("com.github.GTNewHorizons:twilightforest:2.6.35:dev") { transitive = false }
compileOnly(rfg.deobf('curse.maven:witchery-69673:2234410'))
compileOnly("com.github.GTNewHorizons:TinkersConstruct:1.12.14-GTNH:dev") { transitive = false }
compileOnly("com.github.GTNewHorizons:Natura:2.7.5:dev")
compileOnly("com.github.GTNewHorizons:ThaumicHorizons:1.6.4:dev")
compileOnly("com.github.GTNewHorizons:Battlegear2:1.4.1:dev") { transitive = false }
compileOnly(rfg.deobf('maven.modrinth:backhand:1.4.1')) { transitive = false }
compileOnly("com.falsepattern:chunkapi-mc1.7.10:0.5.1:api") { transitive = false }
compileOnly("com.falsepattern:endlessids-mc1.7.10:1.5-beta0003:dev") { transitive = false }
compileOnly(rfg.deobf("curse.maven:extrautils-225561:2264383"))
compileOnly(rfg.deobf("curse.maven:dynamiclights-227874:2337326"))
compileOnly("curse.maven:minefactory-reloaded-66672:2277486")
compileOnly("com.github.GTNewHorizons:NotEnoughIds:2.1.6:dev") // Mixin Version
compileOnly("com.github.GTNewHorizons:NotEnoughIds-Legacy:1.4.7:dev") // ASM Version
compileOnly(rfg.deobf("curse.maven:campfirebackport-387444:4611675"))
compileOnly(rfg.deobf("curse.maven:xaeros-minimap-263420:5060684"))
compileOnly("com.github.GTNewHorizons:HoloInventory:2.4.13-GTNH:dev")
runtimeOnlyNonPublishable(rfg.deobf("CoreTweaks:CoreTweaks:0.3.3.2"))
// Hodgepodge
transformedMod("net.industrial-craft:industrialcraft-2:2.2.828-experimental:dev")
// Better Crashes
compileOnly("com.github.GTNewHorizons:BetterCrashes:1.4.0-GTNH:dev")
// Tests
testImplementation(platform('org.junit:junit-bom:5.9.2'))
testImplementation('org.junit.jupiter:junit-jupiter')
testImplementation("org.mockito:mockito-core:3.+")
testImplementation("org.lwjgl.lwjgl:lwjgl:2.9.4-nightly-20150209")
devOnlyNonPublishable("com.github.javaparser:javaparser-symbol-solver-core:3.26.2") {
exclude module: "guava"
}
}
+215
View File
@@ -0,0 +1,215 @@
/*
* Angelica's dependency declarations. Supported configurations:
* - api("group:name:version:classifier"): if you use the types from this dependency in the public API of this mod
* Available at runtime and compiletime for mods depending on this mod
* - implementation("g:n:v:c"): if you need this for internal implementation details of the mod, but none of it is visible via the public API
* Available at runtime but not compiletime for mods depending on this mod
* - compileOnly("g:n:v:c"): if the mod you're building doesn't need this dependency during runtime at all, e.g. for optional mods
* Not available at all for mods depending on this mod, only visible at compiletime for this mod
* - runtimeOnlyNonPublishable("g:n:v:c"): if you want to include a mod in this mod's runClient/runServer runs, but not publish it as a dependency
* - devOnlyNonPublishable("g:n:v:c"): a combination of runtimeOnlyNonPublishable and compileOnly, not published as a Maven dependency
* - shadowImplementation("g:n:v:c"): like api, but included in your jar under a renamed package name
* - embedOnly("g:n:v:c"): embedded in the jar without relocation
*
* Versions come from gradle/libs.versions.toml. Mod coordinates stay literal: nearly all carry a
* :dev classifier, which a version catalog cannot express, and curse/modrinth "versions" are file ids.
*
* To depend on obfuscated jars use `rfg.deobf("dep:spec:1.2.3")`.
*/
import com.gtnewhorizons.retrofuturagradle.modutils.ModUtils
import org.gradle.accessors.dm.LibrariesForLibs
import org.gradle.kotlin.dsl.DependencyHandlerScope
val libs = the<LibrariesForLibs>()
val rfg = (dependencies as ExtensionAware).extensions.getByType<ModUtils.RfgDependencyExtension>()
val api = configurations["api"]
val compileOnly = configurations["compileOnly"]
val annotationProcessor = configurations["annotationProcessor"]
val shadowImplementation = configurations["shadowImplementation"]
val runtimeOnlyNonPublishable = configurations["runtimeOnlyNonPublishable"]
val devOnlyNonPublishable = configurations["devOnlyNonPublishable"]
// Mods that can be transformed - used for compiling angelica, but not necessary at runtime
val transformedMod by configurations.creating { isCanBeConsumed = false }
val transformedModCompileOnly by configurations.creating { isCanBeConsumed = false }
// Embed in JAR without relocation (compile only, shadowed into JAR)
val embedOnly by configurations.creating {
isCanBeConsumed = false
isCanBeResolved = true
}
// Add the transformed mod dependencies to the compilation classpaths, but don't publish them in the Maven metadata
configurations["compileClasspath"].extendsFrom(transformedMod, transformedModCompileOnly, embedOnly)
configurations["testCompileClasspath"].extendsFrom(transformedMod, transformedModCompileOnly, embedOnly)
configurations["testRuntimeClasspath"].extendsFrom(embedOnly)
configurations["testRuntimeClasspath"].exclude(group = "com.github.GTNewHorizons", module = "lwjgl3ify")
// Prevents transitive Angelica deps from resolving against this project
configurations.configureEach {
exclude(group = "com.github.GTNewHorizons", module = "Angelica")
}
DependencyHandlerScope.of(dependencies).apply {
compileOnly("com.github.GTNewHorizons:Hodgepodge:2.7.197:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:Hodgepodge:2.7.197:dev")
runtimeOnlyNonPublishable(rfg.deobf("curse.maven:biomes-o-plenty-220318:2499612"))
runtimeOnlyNonPublishable("com.github.GTNewHorizons:Applied-Energistics-2-Unofficial:rv3-beta-994-GTNH:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:ae2stuff:0.10.19-GTNH:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:BetterLoadingScreen:1.7.9-GTNH:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:BetterQuesting:3.8.72-GTNH:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:BuildCraft:7.1.63:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:Botania:1.13.32-GTNH:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:LogisticsPipes:1.5.32-GTNH:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:OpenComputers:1.12.55-GTNH:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:Mobs-Info:0.5.18-GTNH:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:IguanaTweaksTConstruct:2.7.10:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:OpenBlocks:1.12.18-GTNH:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:Schematica:1.12.6-GTNH:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:TinkersConstruct:1.14.93-GTNH:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:ServerUtilities:2.4.1:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:ironchest:6.1.13:dev")
runtimeOnlyNonPublishable("ganymedes01.etfuturum:Et-Futurum-Requiem:2.6.49-GTNH:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:waila:1.19.30:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:Draconic-Evolution:1.5.30-GTNH:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:HoloInventory:2.5.15-GTNH:dev")
runtimeOnlyNonPublishable("thaumcraft:Thaumcraft:1.7.10-4.2.3.5:dev")
compileOnly(libs.retrofuturabootstrap) { isTransitive = false }
// runtimeOnlyNonPublishable(libs.lwjgl3ify)
compileOnly(libs.lombok) { isTransitive = false }
annotationProcessor(libs.lombok)
// Iris Shaders
compileOnly(libs.annotations)
api(libs.gtnhlib) { artifact { classifier = "dev" } }
shadowImplementation(project(":glsm")) { isTransitive = false }
shadowImplementation(project(":lwjgl3-backend")) { isTransitive = false }
shadowImplementation(project(":sdl-gpu")) { isTransitive = false }
runtimeOnlyNonPublishable(project(":tracy-client")) { isTransitive = false }
devOnlyNonPublishable(libs.async.profiler)
shadowImplementation(libs.eventbus)
shadowImplementation(libs.jcpp) // Apache 2.0
shadowImplementation(libs.glsl.transformation.lib) {
exclude(module = "antlr4") // we only want to shadow the runtime module
}
shadowImplementation(libs.antlr4.runtime)
compileOnly(libs.ant)
// Celeritas (isTransitive=false: deps provided by GTNHLib/runtime)
embedOnly(libs.celeritas.common) { isTransitive = false }
// Because who doesn't want NEI
devOnlyNonPublishable("com.github.GTNewHorizons:NotEnoughItems:2.8.109-GTNH:dev")
devOnlyNonPublishable("com.github.GTNewHorizons:CodeChickenCore:1.4.16: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-994-GTNH:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:ae2stuff:0.10.19-GTNH:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:BuildCraft:7.1.61:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:IguanaTweaksTConstruct:2.7.10:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:OpenBlocks:1.12.18-GTNH:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:OpenComputers:1.12.47-GTNH:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:TinkersConstruct:1.14.93-GTNH:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:ServerUtilities:2.4.1:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:ironchest:6.1.13:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:waila:1.19.30:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:Botania:1.13.25-GTNH:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:Mobs-Info:0.5.18-GTNH:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:Draconic-Evolution:1.5.27-GTNH:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:HoloInventory:2.5.15-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.21-GTNH:dev")
compileOnly("com.github.GTNewHorizons:twilightforest:2.7.36:dev") { isTransitive = false }
compileOnly(rfg.deobf("curse.maven:witchery-69673:2234410"))
compileOnly("com.github.GTNewHorizons:TinkersConstruct:1.14.93-GTNH:dev") { isTransitive = false }
compileOnly("com.github.GTNewHorizons:Natura:2.8.19:dev")
compileOnly("com.github.GTNewHorizons:ThaumicHorizons:1.8.20:dev")
compileOnly("com.github.GTNewHorizons:Battlegear2:1.4.3:dev") { isTransitive = false }
compileOnly("com.falsepattern:chunkapi-mc1.7.10:0.5.1:api") { isTransitive = false }
compileOnly("com.falsepattern:endlessids-mc1.7.10:1.5-beta0003:dev") { isTransitive = false }
compileOnly(rfg.deobf("curse.maven:extrautils-225561:2264383"))
compileOnly(rfg.deobf("curse.maven:dynamiclights-227874:2337326"))
compileOnly("curse.maven:minefactory-reloaded-66672:2277486")
compileOnly("com.github.GTNewHorizons:NotEnoughIds:2.1.11:dev")
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("ganymedes01.etfuturum:Et-Futurum-Requiem:2.6.49-GTNH:dev")
compileOnly("com.github.GTNewHorizons:OpenBlocks:1.12.19-GTNH:dev")
compileOnly("com.github.GTNewHorizons:Draconic-Evolution:1.5.30-GTNH:dev")
compileOnly("com.github.GTNewHorizons:Chisel:2.17.31-GTNH:dev")
compileOnly(rfg.deobf("curse.maven:biomes-o-plenty-220318:2499612"))
devOnlyNonPublishable("com.github.GTNewHorizons:CosmeticArmorReworked:1.0.6-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")
// Hodgepodge
transformedMod("net.industrial-craft:industrialcraft-2:2.2.828-experimental:dev")
// Better Crashes
compileOnly("com.github.GTNewHorizons:BetterCrashes:1.4.5-GTNH:dev")
// Distant Horizons
compileOnly(libs.distant.horizons.api)
compileOnly("mega:fluidlogged-mc1.7.10:0.1.2")
compileOnly("com.cardinalstar.cubicchunks:CubicChunks1710:v0.1.5-alpha:dev") { isTransitive = false }
}
afterEvaluate {
val glsmTestFixtures = configurations["glsmTestFixtures"]
val testImplementation = configurations["testImplementation"]
val testCompileOnly = configurations["testCompileOnly"]
val testRuntimeOnly = configurations["testRuntimeOnly"]
DependencyHandlerScope.of(dependencies).apply {
testImplementation(platform(libs.junit.bom))
testImplementation(libs.junit.jupiter)
testImplementation(libs.mockito.core)
testImplementation(libs.lwjgl2)
testImplementation(libs.junit.platform.engine)
testImplementation(libs.junit.platform.reporting)
testImplementation(libs.junit.platform.launcher)
testImplementation(libs.archunit.junit5)
testImplementation(libs.retrofuturabootstrap) { isTransitive = false }
glsmTestFixtures(testFixtures(project(":glsm")) as ModuleDependency) { isTransitive = false }
testCompileOnly(testFixtures(project(":glsm")))
testImplementation("mega:fluidlogged-mc1.7.10:0.1.2")
testRuntimeOnly(libs.fastutil)
testRuntimeOnly(libs.joml)
testImplementation(libs.distant.horizons.api)
}
}
+4 -1
View File
@@ -1,4 +1,7 @@
# A specification for models
🌐 **Languages:** [English](model-spec.md) | [Українська](model-spec_uk.md)
This is borrowed from [the unofficial Minecraft wiki](https://minecraft.wiki/w/Tutorials/Models) under the CC BY-NC-SA 3.0.
The folder `assets/<namespace>/models/block` holds the model files for all the specified variants. The names of the files can be changed, but must always correspond with the names used in the variant files.
@@ -8,7 +11,7 @@ The folder `assets/<namespace>/models/block` holds the model files for all the s
* Can be set to `"builtin/generated"` to use a model that is created out of the specified icon. Note that only the first layer is supported, and rotation can be achieved only by using block states files.
* `[TAG_byte]` **ambientocclusion**: Whether to use [ambient occlusion](http://en.wikipedia.org/wiki/Ambient_occlusion) (`true` - default), or not (`false`). Note:only works on Parent file
* `[TAG_object]` **display**: Holds the different places where item models are displayed.
* `[TAG_compound]` ***Position***: Named `thirdperson_righthand`, `thirdperson_lefthand`, `firstperson_righthand`, `firstperson_lefthand`, `gui`, `head`, `ground`, or `fixed`. Place where an item model is displayed. Holds its rotation, translation and scale for the specified situation. '''fixed''' refers to item frames, while the rest are as their name states. Note that translations are applied to the model before rotations.
* `[TAG_compound]` ***Position***: Named `thirdperson_righthand`, `thirdperson_lefthand`, `firstperson_righthand`, `firstperson_lefthand`, `gui`, `head`, `ground`, or `fixed`. Place where an item model is displayed. Holds its rotation, translation and scale for the specified situation. **'''fixed'''** refers to item frames, while the rest are as their name states. Note that translations are applied to the model before rotations.
* `[TAG_list]` **rotation**: Specifies the rotation of the model according to the scheme `[x, y, z]`.
* `[TAG_list]` **translation**: Specifies the position of the model according to the scheme `[x, y, z]`. The values are clamped between -80 and 80.
* `[TAG_list]` **scale**: Specifies the scale of the model according to the scheme `[x, y, z]`. If the value is greater than 4, it is displayed as 4.
+37
View File
@@ -0,0 +1,37 @@
# Специфікація моделей
🌐 **Мови:** [English](model-spec.md) | [Українська](model-spec_uk.md)
Запозичено з [неофіційної Minecraft wiki](https://minecraft.wiki/w/Tutorials/Models) на умовах ліцензії CC BY-NC-SA 3.0.
Папка `assets/<namespace>/models/block` містить файли моделей для усіх вказаних варіантів. Назви файлів можна змінювати, але вони завжди повинні відповідати назвам, що використовуються у файлах варіантів.
* `[TAG_compound]` Кореневий тег
* `[TAG_string]` **parent**: Завантажує іншу модель із вказаного шляху у формі [локації ресурсу](https://minecraft.wiki/w/Tutorials/Models#File_path). Якщо задано і `"parent"`, і `"elements"`, тег `"elements"` перевизначає тег `"elements"` із попередньої моделі.
* Може мати значення `"builtin/generated"`, щоб використовувати модель, створену із вказаної піктограми. Зверніть увагу: підтримується лише перший шар, а обертання можна досягти лише за допомогою файлів станів блоку.
* `[TAG_byte]` **ambientocclusion**: Визначає, чи використовувати [розсіяне затінення](http://en.wikipedia.org/wiki/Ambient_occlusion) (`true` — за замовчуванням), чи ні (`false`). Примітка: працює лише у батьківському файлі.
* `[TAG_object]` **display**: Містить різні місця відображення моделей предметів.
* `[TAG_compound]` ***Position***: Має назву `thirdperson_righthand`, `thirdperson_lefthand`, `firstperson_righthand`, `firstperson_lefthand`, `gui`, `head`, `ground` або `fixed`. Місце, де відображається модель предмета. Містить її обертання, зміщення та масштаб для вказаної ситуації. **'''fixed'''** стосується рамок для предметів, а решта відповідають своїм назвам. Зверніть увагу: зміщення застосовується до моделі перед обертанням.
* `[TAG_list]` **rotation**: Задає обертання моделі за схемою `[x, y, z]`.
* `[TAG_list]` **translation**: Задає зміщення моделі за схемою `[x, y, z]`. Значення обмежені в діапазоні від -80 до 80.
* `[TAG_list]` **scale**: Задає масштаб моделі за схемою `[x, y, z]`. Якщо значення більше за 4, воно відображається як 4.
* `[TAG_compound]` **textures**: Містить текстури моделі у формі [локації ресурсу](https://minecraft.wiki/w/Tutorials/Models#File_path) або у вигляді іншої текстурної змінної.
* `[TAG_string]` **particle**: Текстура, з якої завантажуються частинки. Ця текстура також використовується як оверлей, якщо ви перебуваєте в [порталі Незеру](https://minecraft.wiki/w/Nether_portal), а також для нерухомих текстур [води](https://minecraft.wiki/w/water) та [лави](https://minecraft.wiki/w/Lava).<sup>[[1]](https://bugs.mojang.com/browse/MC-240042)</sup> Ця текстура також вважається текстурною змінною, на яку можна посилатися як `"#particle"`. Примітка: Усі частинки руйнування для блоків без моделей вшиті в код, наприклад, для [бар’єрів](https://minecraft.wiki/w/Barrier).
* `[TAG_string]` ***Texture variable***: Визначає текстурну змінну з ім’ям `name` та призначає її текстурі або іншій змінній.
* `[TAG_list]` **elements**: Містить усі елементи моделі. Вони можуть мати лише кубічну форму. Якщо задано і `"parent"`, і `"elements"`, тег `"elements"` перевизначає тег `"elements"` із попередньої моделі.
* `[TAG_compound]` Елемент.
* `[TAG_list]` **from**: Початкова точка кубоїда за схемою `[x, y, z]`. Значення мають бути від -16 до 32.
* `[TAG_list]` **to**: Кінцева точка кубоїда за схемою `[x, y, z]`. Значення мають бути від -16 до 32.
* `[TAG_compound]` **rotation**: Визначає обертання елемента.
* `[TAG_list]` **origin**: Встановлює центр обертання за схемою `[x, y, z]`.
* `[TAG_string]` **axis**: Задає вісь обертання: може бути `"x"`, `"y"` або `"z"`.
* `[TAG_float]` **angle**: Задає кут обертання. Може бути від 45 до -45 градусів із кроком у 22,5 градуса.
* `[TAG_byte]` **rescale**: Визначає, чи масштабувати грані по всьому блоку. Може бути `true` або `false`. За замовчуванням — `false`.
* `[TAG_byte]` **shade**: Визначає, чи рендерити тіні (`true` — за замовчуванням), чи ні (`false`).
* `[TAG_compound]` **faces**: Містить усі грані кубоїда. Якщо грань пропущено, вона не рендериться.
* `[TAG_compound]` ***Face***: Має назву `down`, `up`, `north`, `south`, `west` або `east`. Містить властивості вказаної грані.
* `[TAG_list]` **uv**: Визначає ділянку текстури для використання за схемою `[x1, y1, x2, y2]`. Поведінка текстури стає непередбачуваною, якщо значення UV виходять за межі 0 або 16. Якщо значення `x1` та `x2` поміняти місцями (наприклад, з `0, 0, 16, 16` на `16, 0, 0, 16`), текстура віддзеркалюється. Параметр UV є необов’язковим; якщо його не вказано, він автоматично генерується на основі позиції елемента.
* `[TAG_string]` **texture**: Вказує текстуру у формі текстурної змінної зі знаком `#` на початку.
* `[TAG_string]` **cullface**: Визначає, чи не потрібно рендерити грань, якщо до неї в указаній позиції торкається інший блок. Позиція може бути: `down`, `up`, `north`, `south`, `west` або `east`. Цей параметр також визначає сторону блоку, з якої береться рівень освітлення для цієї грані; якщо параметр не задано, за замовчуванням використовується відповідна сторона.
* `[TAG_int]` **rotation**: Обертає текстуру за годинниковою стрілкою на вказаний кут у градусах. Може бути 0, 90, 180 або 270. За замовчуванням — 0. Обертання не впливає на те, яка частина текстури використовується. Натомість воно зводиться до перестановки вершин вибраної текстури (вибраних неявно або явно через `uv`).
* `[TAG_int]` **tintindex**: Визначає, чи потрібно тонувати текстуру за допомогою вшитого індексу відтінку. Значення за замовчуванням, -1, вказує на те, що відтінок не використовується. Будь-яке інше число передається в `BlockColors` для отримання значення відтінку, відповідного цьому індексу. Однак більшість блоків не мають визначеного значення відтінку (у такому разі використовується білий колір). Крім того, жоден ванільний блок наразі не використовує кілька значень відтінку, тому значення індексу відтінку ігнорується (якщо воно відмінне від -1); втім, його можна використовувати для модифікованих блоків, яким потрібні кілька різних значень відтінку в одному блоці.
+58
View File
@@ -0,0 +1,58 @@
plugins {
`java-library`
}
apply(from = rootProject.file("experimental-surround/surround.project.gradle.kts"))
val mixinProviderSpec = rootProject.extra["mixinProviderSpec"] as String
val refmapProbe = sourceSets.create("refmapProbe")
dependencies {
compileOnly(mixinProviderSpec)
testImplementation(mixinProviderSpec)
testImplementation(platform(libs.junit.bom))
testImplementation(libs.junit.jupiter)
testImplementation(project(":experimental-surround-runtime"))
testRuntimeOnly(libs.junit.platform.launcher)
"refmapProbeCompileOnly"(mixinProviderSpec)
"refmapProbeCompileOnly"(project(":experimental-surround-runtime"))
"refmapProbeAnnotationProcessor"(mixinProviderSpec)
"refmapProbeAnnotationProcessor"(sourceSets.main.get().output)
}
val probeRefmaps = layout.buildDirectory.dir("refmap-probe")
val probeSrg = file("src/refmapProbe/mappings.srg")
val surroundAp = "com.gtnewhorizons.angelica.experimental.surround.ap.SurroundProcessor"
val mixinAps = "org.spongepowered.tools.obfuscation.MixinObfuscationProcessorInjection,org.spongepowered.tools.obfuscation.MixinObfuscationProcessorTargets"
fun JavaCompile.probe(name: String, processors: String?) {
val refmap = probeRefmaps.map { it.file("refmap-probe/$name.json") }
val srg = probeSrg
javaCompiler = javaToolchains.compilerFor { languageVersion = JavaLanguageVersion.of(25) }
outputs.file(refmap)
inputs.file(srg)
options.compilerArgumentProviders.add(CommandLineArgumentProvider {
listOf("-proc:only", "-AoutRefMapFile=" + refmap.get().asFile.absolutePath,
"-AreobfSrgFile=" + srg.absolutePath, "-AdefaultObfuscationEnv=searge") +
(if (processors == null) emptyList() else listOf("-processor", processors))
})
}
fun JavaCompile.probeOrdered(name: String, classesDir: String, processors: String) {
source = refmapProbe.java
classpath = refmapProbe.compileClasspath
options.annotationProcessorPath = refmapProbe.annotationProcessorPath
destinationDirectory = layout.buildDirectory.dir("classes/$classesDir")
probe(name, processors)
}
tasks.named<JavaCompile>(refmapProbe.compileJavaTaskName) { probe("discovered", null) }
val probeSurroundFirst by tasks.registering(JavaCompile::class) { probeOrdered("surround-first", "refmapProbeFirst", "$surroundAp,$mixinAps") }
val probeSurroundLast by tasks.registering(JavaCompile::class) { probeOrdered("surround-last", "refmapProbeLast", "$mixinAps,$surroundAp") }
sourceSets.test { resources.srcDir(files(probeRefmaps).builtBy(refmapProbe.compileJavaTaskName, probeSurroundFirst, probeSurroundLast)) }
tasks.named<Test>("test") {
useJUnitPlatform()
}
@@ -0,0 +1,77 @@
package com.gtnewhorizons.angelica.experimental.surround.ap;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.tools.obfuscation.mirror.AnnotationHandle;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
@SupportedAnnotationTypes("com.gtnewhorizons.angelica.experimental.surround.Surround")
public class SurroundProcessor extends AbstractProcessor {
record Registration(TypeElement mixin, ExecutableElement method, AnnotationMirror surround) {
}
private final List<Registration> registrations = new ArrayList<>();
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (roundEnv.processingOver()) {
if (!this.registrations.isEmpty()) {
register();
}
return false;
}
for (TypeElement annotation : annotations) {
for (Element annotated : roundEnv.getElementsAnnotatedWith(annotation)) {
if (annotated.getEnclosingElement() instanceof TypeElement mixin && AnnotationHandle.of(mixin, Mixin.class).exists()) {
for (AnnotationMirror mirror : annotated.getAnnotationMirrors()) {
if (mirror.getAnnotationType().asElement().equals(annotation)) {
this.registrations.add(new Registration(mixin, (ExecutableElement) annotated, mirror));
break;
}
}
}
}
}
return false;
}
private void register() {
final ProcessingEnvironment env = this.processingEnv;
try {
// pinned by UniMixins 0.3.1; recheck on bump
final Class<?> annotatedMixins = Class.forName("org.spongepowered.tools.obfuscation.AnnotatedMixins", false, AnnotationHandle.class.getClassLoader());
final Method forEnvironment = annotatedMixins.getDeclaredMethod("getMixinsForEnvironment", ProcessingEnvironment.class);
final Method registerInjector = annotatedMixins.getDeclaredMethod("registerInjector", TypeElement.class, ExecutableElement.class, AnnotationHandle.class);
final Method writeReferences = annotatedMixins.getDeclaredMethod("writeReferences");
forEnvironment.setAccessible(true);
registerInjector.setAccessible(true);
writeReferences.setAccessible(true);
final Object mixins = forEnvironment.invoke(null, env);
for (Registration registration : this.registrations) {
registerInjector.invoke(mixins, registration.mixin(), registration.method(), AnnotationHandle.of(registration.surround()));
}
writeReferences.invoke(mixins);
} catch (ReflectiveOperationException | LinkageError | RuntimeException e) {
env.getMessager().printMessage(Diagnostic.Kind.ERROR, "@Surround: refmap entries were not written: " + e + (e.getCause() == null ? "" : " caused by " + e.getCause()));
}
}
}
@@ -0,0 +1 @@
com.gtnewhorizons.angelica.experimental.surround.ap.SurroundProcessor
@@ -0,0 +1,20 @@
package probe;
import com.gtnewhorizons.angelica.experimental.surround.Surround;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
@Mixin(RefmapTarget.class)
class MixinRefmapTarget {
@Surround(method = "run()V")
private void wholeMethod() {}
@Surround.Finally
private void always() {}
@Surround(method = "run(Ljava/lang/String;)V", id = "call", at = @At(value = "INVOKE", target = "Lprobe/RefmapTarget;helper()V"))
private void callForm() {}
@Surround.Finally("call")
private void alwaysCall() {}
}
@@ -0,0 +1,7 @@
package probe;
public class RefmapTarget {
void run() {}
void run(String s) { helper(); }
void helper() {}
}
@@ -0,0 +1,3 @@
MD: probe/RefmapTarget/run ()V probe/RefmapTarget/func_1_a ()V
MD: probe/RefmapTarget/run (Ljava/lang/String;)V probe/RefmapTarget/func_2_b (Ljava/lang/String;)V
MD: probe/RefmapTarget/helper ()V probe/RefmapTarget/func_3_c ()V
@@ -0,0 +1,37 @@
package com.gtnewhorizons.angelica.experimental.surround.ap;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.jupiter.api.Assertions.assertEquals;
class RefmapRegistrationTest {
private static final Pattern ENTRY = Pattern.compile("\"([^\"]+)\": \"([^\"]+)\"");
@ParameterizedTest
@ValueSource(strings = {"discovered", "surround-first", "surround-last"})
void surroundTargetsAreRemappedInTheRefmap(String variant) throws IOException {
final Map<String, String> expected = new HashMap<>();
expected.put("run()V", "Lprobe/RefmapTarget;func_1_a()V");
expected.put("run(Ljava/lang/String;)V", "Lprobe/RefmapTarget;func_2_b(Ljava/lang/String;)V");
expected.put("Lprobe/RefmapTarget;helper()V", "Lprobe/RefmapTarget;func_3_c()V");
assertEquals(expected, mappings(variant, "probe/MixinRefmapTarget"));
}
private static Map<String, String> mappings(String variant, String mixin) throws IOException {
final String json = new String(RefmapRegistrationTest.class.getResourceAsStream("/refmap-probe/" + variant + ".json").readAllBytes(), UTF_8);
final int start = json.indexOf('{', json.indexOf("\"" + mixin + "\"", json.indexOf("\"mappings\"")));
final Matcher m = ENTRY.matcher(json.substring(start, json.indexOf('}', start)));
final Map<String, String> entries = new HashMap<>();
while (m.find()) entries.put(m.group(1), m.group(2));
return entries;
}
}
@@ -0,0 +1,136 @@
import xyz.wagyourtail.jvmdg.gradle.task.files.DowngradeFiles
plugins {
`java-library`
}
apply(plugin = "xyz.wagyourtail.jvmdowngrader")
apply(from = rootProject.file("experimental-surround/surround.project.gradle.kts"))
// Required by RFG consumers (runClient17 java17Dependencies) to pick a variant.
val rfgObfAttr = Attribute.of("com.gtnewhorizons.retrofuturagradle.obfuscation", String::class.java)
val rfgTransformedAttr = Attribute.of("rfgDeobfuscatorTransformed", Boolean::class.javaObjectType)
configurations {
listOf("apiElements", "runtimeElements").forEach { name ->
named(name) {
attributes {
attribute(rfgObfAttr, "mcp")
attribute(rfgTransformedAttr, true)
}
}
}
}
val mixinProviderSpec = rootProject.extra["mixinProviderSpec"] as String
dependencies {
compileOnly(mixinProviderSpec)
testImplementation(platform(libs.junit.bom))
testImplementation(libs.junit.jupiter)
testImplementation(mixinProviderSpec)
testImplementation(libs.launchwrapper)
testImplementation(libs.commons.io)
testImplementation(libs.guava)
testImplementation(libs.log4j.core)
testRuntimeOnly(libs.junit.platform.launcher)
testRuntimeOnly(libs.jvmdowngrader.java.api) { artifact { classifier = "downgraded-8" } }
}
repositories {
maven {
name = "WagYourTail"
url = uri("https://maven.wagyourtail.xyz/releases")
content { includeGroup("xyz.wagyourtail.jvmdowngrader") }
}
}
tasks.withType<DowngradeFiles>().configureEach {
downgradeTo.set(JavaVersion.VERSION_1_8)
multiReleaseOriginal.set(false)
multiReleaseVersions.set(emptySet())
logLevel.set("FATAL")
}
fun SourceSet.registerDowngrade(taskName: String) = tasks.register<DowngradeFiles>(taskName) {
inputCollection = output.classesDirs
classpath = compileClasspath
output.classesDirs.files.forEach { outputs.dir(temporaryDir.resolve(it.name)) }
dependsOn(tasks.named(classesTaskName))
}
val downgradeMainClasses = sourceSets["main"].registerDowngrade("downgradeMainClasses")
val downgradeTestClasses = sourceSets["test"].registerDowngrade("downgradeTestClasses")
val goldenDir = layout.projectDirectory.dir("src/test/resources/golden").asFile.absolutePath
// Runs on Java 8 against the JVMDowngrader output, the bytecode production ships.
fun Test.configureSurroundTest(runDirName: String) {
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)
val downgradedTest = files(downgradeTestClasses.map { it.outputCollection })
testClassesDirs = downgradedTest
classpath = downgradedTest
.plus(files(downgradeMainClasses.map { it.outputCollection }))
.plus(sourceSets["test"].runtimeClasspath.minus(sourceSets["main"].output.classesDirs).minus(sourceSets["test"].output.classesDirs))
val dir = layout.buildDirectory.dir(runDirName).get().asFile
workingDir = dir
systemProperty("surround.golden.dir", goldenDir)
systemProperty("mixin.debug.verify", "true")
val javac = javaToolchains.compilerFor { languageVersion = JavaLanguageVersion.of(25) }
jvmArgumentProviders.add(CommandLineArgumentProvider { listOf("-Dsurround.javac=" + javac.get().executablePath.asFile.absolutePath) })
doFirst { dir.mkdirs() }
}
fun Test.configureUntaggedSurroundTest(runDirName: String, vararg systemProps: Pair<String, String>) {
useJUnitPlatform { excludeTags("mixinextras") }
for ((key, value) in systemProps) {
systemProperty(key, value)
}
configureSurroundTest(runDirName)
}
fun Test.configureTaggedSurroundTest(runDirName: String, tag: String, vararg systemProps: Pair<String, String>) {
useJUnitPlatform { includeTags(tag) }
for ((key, value) in systemProps) {
systemProperty(key, value)
}
configureSurroundTest(runDirName)
}
tasks.named<Test>("test") {
configureUntaggedSurroundTest("test-run")
}
val mixinExtrasTest by tasks.registering(Test::class) {
configureTaggedSurroundTest(
"test-run-mixinextras",
"mixinextras",
"surround.test.mixinextras" to "true"
)
}
val mixinExtrasAdditionsFirstTest by tasks.registering(Test::class) {
configureTaggedSurroundTest(
"test-run-mixinextras-additions-first",
"mixinextras",
"surround.test.mixinextras" to "true",
"surround.test.additionsFirst" to "true"
)
}
val obfTest by tasks.registering(Test::class) {
configureUntaggedSurroundTest("test-run-obf", "surround.test.obf" to "true")
filter { excludeTestsMatching("*.RejectionTest") }
}
tasks.named("check") {
dependsOn(mixinExtrasTest, mixinExtrasAdditionsFirstTest, obfTest)
}
@@ -0,0 +1,8 @@
package com.gtnewhorizons.angelica.experimental.surround;
import com.gtnewhorizons.angelica.experimental.surround.SurroundBinding.Exits;
import org.spongepowered.asm.lib.Type;
import org.spongepowered.asm.lib.tree.MethodNode;
import org.spongepowered.asm.mixin.injection.struct.InjectionNodes.InjectionNode;
record CallSiteSpec(MethodNode target, InjectionNode node, MethodNode entry, String entryDesc, Type[] operands, Type result, Exits exits, String description, int[] entryLocals, int[][] locals) {}
@@ -0,0 +1,311 @@
package com.gtnewhorizons.angelica.experimental.surround;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Slice;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Experimental and internal to Angelica; the API may change without notice.
* <p>
* Surrounds one method call ({@link #at()} set) or a whole method ({@link #at()} empty) with try/catch/finally.
* The call form replaces MixinExtras' {@code @WrapOperation} and the whole-method form replaces {@code @WrapMethod},
* without their per-call {@code Operation} and {@code Object[]} allocation. Use it when the original runs at most once
* with its original arguments.
* <p>
* A surround can:
* <ul>
* <li>run code before the original: the entry handler, the annotated method itself;</li>
* <li>hand state from that code to everything after it: {@link Carry @Surround.Carry} locals;</li>
* <li>read the call's operands, the target's parameters, and locals at the call: operands,
* {@link Local @Surround.Local};</li>
* <li>skip the original and supply its result instead: {@link Skip @Surround.Skip} + {@link Skipped @Surround.Skipped};</li>
* <li>replace the result after a normal completion: {@link Return @Surround.Return};</li>
* <li>react to an exception, rethrowing or (calls only) swallowing it and supplying the result:
* {@link Catch @Surround.Catch};</li>
* <li>run code on every way out: {@link Finally @Surround.Finally}.</li>
* </ul>
* It cannot call the original twice or with different arguments.
*
* <h2>Example</h2>
* <pre>
* &#64;Surround(method = "doRender(Lnet/minecraft/entity/item/EntityItem;DDDFF)V")
* private void enter(EntityItem entity, double x, double y, double z, float yaw, float partialTicks) {
* &#64;Surround.Carry int prevItemId = ItemIdManager.getItemId();
* ItemIdManager.setItemId(entity.getEntityItem());
* }
*
* &#64;Surround.Finally
* private void restore(&#64;Surround.Carry int prevItemId) {
* ItemIdManager.setItemIdRaw(prevItemId);
* }
* </pre>
*
* <h2>Handlers</h2>
* The annotated method is the entry handler. Exit handlers are methods in the same mixin whose {@code value} is this
* surround's {@link #id()}:
* <pre>
* &#64;Surround void enter(operands, &#64;Local params) before the original
* &#64;Surround.Skipped T skipped(operands, &#64;Local params, &#64;Carry params) instead of the original
* &#64;Surround.Return T ret(T result, operands, &#64;Local params, &#64;Carry params) after it completes normally
* &#64;Surround.Catch void caught(E error, operands, &#64;Local params, &#64;Carry params) when it throws E
* &#64;Surround.Finally void always(operands, &#64;Local params, &#64;Carry params) on every way out
* </pre>
* {@code T} is the return type of the call or method. When it is {@code void}, {@code ret} takes no result and
* {@code Skipped} returns {@code void}.
* <ul>
* <li>The entry handler returns {@code void}. Exit handlers are static exactly when the entry handler is.</li>
* <li>Per surround: at most one {@code Skipped}, one {@code Return} and one {@code Finally}, and one {@code Catch} per
* exception type. A method has one role.</li>
* <li>{@link #id()} is required when the mixin declares more than one {@code @Surround}.</li>
* </ul>
*
* <h2>Parameters</h2>
* Declare them in this order, each group optional:
* <ol>
* <li>Operands, as a leading prefix. For a call: the receiver (unless static), then the arguments. A reference
* operand may be declared as a supertype. For a whole method: the target's parameters.</li>
* <li>{@link Local @Surround.Local} parameters (call form only). Exit handlers see the value as of entry.</li>
* <li>{@link Carry @Surround.Carry} parameters (exit handlers only).</li>
* </ol>
*
* <h2>Carrying state</h2>
* Exit handlers see entry-handler locals only through {@link Carry @Surround.Carry}; see there for the rules. Two
* carries of one kind are told apart by name:
* <pre>
* &#64;Surround(method = ..., at = &#64;At(value = "INVOKE", target = ...))
* private void enter() {
* &#64;Surround.Carry("depth") int depth = GLStateManager.pushState(StateSet.BLEND);
* &#64;Surround.Carry("prev") int prevProgram = GLStateManager.getActiveProgram();
* &#64;Surround.Carry Class&lt;?&gt; prevRenderable = TesrAttribution.currentRenderable;
* }
*
* &#64;Surround.Finally
* private void exit(&#64;Surround.Carry("depth") int depth, &#64;Surround.Carry("prev") int prevProgram,
* &#64;Surround.Carry Class&lt;?&gt; prevRenderable) { ... }
* </pre>
*
* <h2>Entry-handler body</h2>
* <ul>
* <li>Every path must fall through to the end: no early {@code return}. Branch with {@code if} instead.</li>
* <li>Do not assign its parameters. To change arguments, use {@code @ModifyVariable} or {@code @WrapMethod}.</li>
* <li>In the call form it is inlined at every matched call, as are the exit-handler calls. Keep it short: move bulky
* logic into a helper and carry the helper's result. Huge targets stop being JIT-compiled past 8000 bytes.</li>
* </ul>
*
* <h2>Skipping</h2>
* A {@link Skip @Surround.Skip} local in the entry handler decides whether the original runs; see there for the rules.
*
* <h2>Order and exceptions</h2>
* <ol>
* <li>The entry handler. If it throws, nothing else runs.</li>
* <li>The original, or {@link Skipped} when skipped.</li>
* <li>{@link Return}, only if the original completed normally (not after {@code Skipped}). Its return value replaces
* the result.</li>
* <li>{@link Finally}, on every way out: normal, after a catch, or rethrowing.</li>
* </ol>
* {@link Catch} covers step 2, not {@code Return}, and only the most specific matching one runs. It rethrows when
* it returns, unless {@link Catch#handle()} is set (call form only): the exception then ends there, and the
* handler's return value becomes the call's result.
*
* <h2>Whole-method form</h2>
* <ul>
* <li>The original body becomes a private {@code name$surround} method, so stack traces gain that frame.</li>
* <li>A {@code synchronized} target holds its lock across the handlers too.</li>
* <li>An entry handler that calls the target goes through the surround again.</li>
* <li>{@code @Local}, {@link #slice()} and {@code handle} are call-form only.</li>
* </ul>
*
* <h2>With other injectors</h2>
* <ul>
* <li>A call surround encloses a {@code @Redirect} or {@code @WrapOperation} on the same call. A whole-method surround
* encloses every other injection in the method, including call surrounds.</li>
* <li>Several surrounds on one call or method nest, lowest priority innermost. An outer skip skips the inner ones
* entirely, and {@code Return} replacements chain from inner to outer.</li>
* <li>A matched call that another injector removed is skipped with a warning.</li>
* </ul>
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Surround {
String[] method() default {};
/**
* Calls to surround, as INVOKE-style {@code @At} without shift or by; each match is surrounded separately. Empty
* surrounds the whole method.
*/
At[] at() default {};
/**
* As for {@code @Inject}; calls only.
*/
Slice[] slice() default {};
/**
* Names this surround for its exit handlers. Required when the mixin declares more than one {@code @Surround};
* unique per mixin.
*/
String id() default "";
boolean remap() default true;
/**
* As for {@code @Inject}, as are {@link #expect()} and {@link #allow()}; each surrounded call or whole target counts once.
*/
int require() default -1;
int expect() default 1;
int allow() default -1;
/**
* Carries a value from the entry handler to the exit handlers.
* <p>
* Annotate a local in the entry handler, then declare a parameter with the same annotation in any exit handler that
* needs the value:
* <pre>
* &#64;Surround(method = ...)
* private void enter() {
* &#64;Surround.Carry int prevItemId = ItemIdManager.getItemId();
* ItemIdManager.setItemId(...);
* }
*
* &#64;Surround.Finally
* private void restore(&#64;Surround.Carry int prevItemId) {
* ItemIdManager.setItemIdRaw(prevItemId);
* }
* </pre>
* Declaring the local:
* <ul>
* <li>Annotate the declaration itself: {@code @Surround.Carry int x = ...}. Not a parameter of the entry handler
* (use operands for those), and not part of the type (e.g. an array component).</li>
* <li>Declare it at the top level of the entry handler and initialize it there. It must still be in scope and
* definitely assigned when the entry handler ends.</li>
* </ul>
* Receiving it:
* <ul>
* <li>Carry parameters come last, after operands and {@code @Local} parameters. An exit handler may take any
* subset.</li>
* <li>Parameters bind to locals by kind: reference, {@code int}-like ({@code boolean}, {@code byte},
* {@code char}, {@code short}, {@code int}), {@code long}, {@code float}, {@code double}. If the entry handler
* carries two locals of one kind, name every one of that kind with the same {@link #value()} on the local and on
* each parameter that reads it: {@code @Surround.Carry("depth") int depth}. Names are unique within a handler.</li>
* <li>A reference is cast to the parameter's type on read. Every exit handler must read a given carry as the same
* type.</li>
* </ul>
* Each carry lives in a local slot of the target method, with no allocation, so nested and recursive invocations
* keep separate values. The entry handler returns {@code void}; carry its results instead.
*/
@Target({ElementType.TYPE_USE, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@interface Carry {
String value() default "";
}
/**
* Decides whether the original runs.
* <p>
* Annotate one {@code boolean} or {@code int}-like local in the entry handler, declared like a {@link Carry}: at
* the top level, initialized at its declaration, in scope at the end:
* <pre>
* &#64;Surround(method = ..., at = &#64;At(value = "INVOKE", target = ...))
* private void enter(RenderBlocks renderer, Block block) {
* &#64;Surround.Skip boolean skip = FallingBlockRendering.isActive();
* }
*
* &#64;Surround.Skipped
* private boolean replacement(RenderBlocks renderer, Block block) { ... }
* </pre>
* <ul>
* <li>If the local is nonzero when the entry handler ends, the original does not run and {@link Skipped} runs in
* its place; {@link Return} does not run after it, {@link Catch} and {@link Finally} still do.</li>
* <li>{@link Skipped} is required unless the result is {@code void}, and rejected without a {@code Skip}
* local.</li>
* <li>At most one per entry handler.</li>
* </ul>
*/
@Target(ElementType.TYPE_USE)
@Retention(RetentionPolicy.RUNTIME)
@interface Skip {
}
/**
* Supplies the result in place of a skipped original. Required unless that result is {@code void}.
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface Skipped {
/**
* The {@link Surround#id()} of the surround this handler belongs to; empty for the default.
*/
String value() default "";
}
/**
* Runs after the original completes normally. Takes the result first (none for {@code void}) and returns its
* replacement.
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface Return {
/**
* The {@link Surround#id()} of the surround this handler belongs to; empty for the default.
*/
String value() default "";
}
/**
* Runs when the original or {@link Skipped} throws the first parameter's type, unless a more specific
* {@code Catch} matches. Rethrows when it returns.
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface Catch {
/**
* The {@link Surround#id()} of the surround this handler belongs to; empty for the default.
*/
String value() default "";
/**
* Call form only: the exception ends here and the returned value becomes the call's result.
*/
boolean handle() default false;
}
/**
* Runs on every way out.
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface Finally {
/**
* The {@link Surround#id()} of the surround this handler belongs to; empty for the default.
*/
String value() default "";
}
/**
* As MixinExtras' {@code @Local}, read at the call; exit handlers see its value at entry. Calls only.
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@interface Local {
int ordinal() default -1;
int index() default -1;
String[] name() default {};
boolean argsOnly() default false;
}
}
@@ -0,0 +1,120 @@
package com.gtnewhorizons.angelica.experimental.surround;
import com.llamalad7.mixinextras.utils.MixinInternals;
import org.spongepowered.asm.lib.tree.AnnotationNode;
import org.spongepowered.asm.lib.tree.ClassNode;
import org.spongepowered.asm.lib.tree.MethodNode;
import org.spongepowered.asm.mixin.MixinEnvironment;
import org.spongepowered.asm.mixin.transformer.ext.IExtension;
import org.spongepowered.asm.mixin.transformer.ext.ITargetClassContext;
import org.spongepowered.asm.util.Annotations;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.regex.Pattern;
final class SurroundApplicatorExtension implements IExtension {
private static final String MIXINEXTRAS_PACKAGE = "com.llamalad7.mixinextras.";
private static final Pattern HANDLER_NAME = Pattern.compile("surround\\$[a-z]{3,}[0-9a-f]{3,}\\$.*");
private final Map<ITargetClassContext, Pending> pending = new WeakHashMap<>();
private record Pending(List<CallSiteSpec> calls, List<CallSiteSpec> wholes, Set<String> declarers) {}
private static final class Holder {
static final SurroundApplicatorExtension INSTANCE = create();
private static SurroundApplicatorExtension create() {
final SurroundApplicatorExtension extension = new SurroundApplicatorExtension();
// MixinExtras 0.5.0 internal (pinned by UniMixins 0.3.1); recheck on bump
MixinInternals.registerExtension(extension, false);
return extension;
}
}
static SurroundApplicatorExtension instance() {
return Holder.INSTANCE;
}
void declare(ITargetClassContext context, String mixin) {
this.pending(context).declarers.add(mixin);
}
void offer(ITargetClassContext context, CallSiteSpec spec) {
final Pending pending = this.pending(context);
(spec.node() == null ? pending.wholes : pending.calls).add(spec);
}
private Pending pending(ITargetClassContext context) {
return this.pending.computeIfAbsent(context, key -> new Pending(new ArrayList<>(), new ArrayList<>(), new HashSet<>()));
}
@Override
public boolean checkActive(MixinEnvironment environment) {
return true;
}
@Override
public void preApply(ITargetClassContext context) {
// MixinExtras 0.5.0 internal (pinned by UniMixins 0.3.1); recheck on bump
final List<IExtension> active = MixinInternals.getExtensions().getActiveExtensions();
boolean seen = false;
for (int i = 0, n = active.size(); i < n; i++) {
final IExtension extension = active.get(i);
if (extension == this) {
seen = true;
} else if (seen && extension.getClass().getName().startsWith(MIXINEXTRAS_PACKAGE)) {
MixinInternals.unregisterExtension(this);
MixinInternals.registerExtension(this, false);
return;
}
}
}
@Override
public void postApply(ITargetClassContext context) {
final ClassNode targetClass = context.getClassNode();
final Pending pending = this.pending.remove(context);
checkHandlersRenamed(targetClass, pending == null ? Set.of() : pending.declarers);
if (pending == null) {
return;
}
final List<CallSiteSpec> calls = pending.calls;
for (int i = 0, n = calls.size(); i < n; i++) {
SurroundCallSite.apply(targetClass, calls.get(i));
}
final List<CallSiteSpec> wholes = pending.wholes;
for (int i = 0, n = wholes.size(); i < n; i++) {
SurroundCallSite.apply(targetClass, wholes.get(i));
}
}
@Override
public void export(MixinEnvironment env, String name, boolean force, ClassNode classNode) {
}
static void checkHandlersRenamed(ClassNode classNode, Set<String> declarers) {
for (MethodNode method : classNode.methods) {
if (method.visibleAnnotations == null) {
continue;
}
for (AnnotationNode annotation : method.visibleAnnotations) {
final String desc = annotation.desc;
if (SurroundSpec.CATCH.equals(desc) || SurroundSpec.FINALLY.equals(desc) || SurroundSpec.SKIPPED.equals(desc) || SurroundSpec.RETURN.equals(desc)) {
final String mixin = SurroundSpec.mergedBy(method);
if (mixin != null && !declarers.contains(mixin)) {
throw new IllegalStateException("@Surround " + SurroundBinding.exitRef(method) + " " + SurroundInjector.noSurroundForId(Annotations.<String>getValue(annotation, "value", ""), Set.of()));
}
} else if (SurroundSpec.SURROUND.equals(desc) && !HANDLER_NAME.matcher(method.name).matches()) {
throw new IllegalStateException(method.name + method.desc + " in " + classNode.name + " (" + SurroundSpec.describeOwner(method) + ") carries @Surround but was never parsed as an injector: the InjectionInfo registry lacked @Surround when mixins were applied to this target; SurroundBootstrap.init() must run before any class a @Surround targets is transformed");
}
}
}
}
}
@@ -0,0 +1,543 @@
package com.gtnewhorizons.angelica.experimental.surround;
import org.spongepowered.asm.lib.Opcodes;
import org.spongepowered.asm.lib.Type;
import org.spongepowered.asm.lib.TypePath;
import org.spongepowered.asm.lib.tree.AbstractInsnNode;
import org.spongepowered.asm.lib.tree.AnnotationNode;
import org.spongepowered.asm.lib.tree.FrameNode;
import org.spongepowered.asm.lib.tree.IincInsnNode;
import org.spongepowered.asm.lib.tree.LabelNode;
import org.spongepowered.asm.lib.tree.LocalVariableAnnotationNode;
import org.spongepowered.asm.lib.tree.LocalVariableNode;
import org.spongepowered.asm.lib.tree.MethodNode;
import org.spongepowered.asm.lib.tree.VarInsnNode;
import org.spongepowered.asm.util.Annotations;
import org.spongepowered.asm.util.Bytecode;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.StringJoiner;
final class SurroundBinding {
private static final int TOP = -1;
private static final String[] SUGARS = {"Local", "Share", "Cancellable"};
static final String SUGAR_HINT = ", which a @Surround handler cannot resolve; use @Surround.Carry, or @Surround.Local on a call";
enum Role {
ENTRY(SurroundSpec.SURROUND), CATCH(SurroundSpec.CATCH), FINALLY(SurroundSpec.FINALLY), SKIPPED(SurroundSpec.SKIPPED), RETURN(SurroundSpec.RETURN);
static final Role[] EXITS = {CATCH, FINALLY, SKIPPED, RETURN};
final String annotation;
Role(String annotation) {
this.annotation = annotation;
}
}
record Load(int slot, Type type) {}
static final class Handler {
final MethodNode method;
final Role role;
final Type[] params;
final boolean isStatic;
final boolean handles;
final int leading;
final int localStart;
final int carryStart;
private final String[] carryIds;
int index;
Load[] loads;
Handler(MethodNode method, Role role, int leading) {
this.method = method;
this.role = role;
this.params = Type.getArgumentTypes(method.desc);
this.isStatic = Bytecode.isStatic(method);
final AnnotationNode caught = role == Role.CATCH ? Annotations.get(method.visibleAnnotations, SurroundSpec.CATCH) : null;
this.handles = caught != null && Annotations.<Boolean>getValue(caught, "handle", Boolean.FALSE).booleanValue();
this.leading = leading;
final int count = this.params.length;
int firstLocal = count;
int firstCarry = count;
int section = 0;
String[] ids = null;
for (int i = 0; i < count; i++) {
final AnnotationNode carry = carry(method, i);
final boolean local = local(method, i) != null;
if (carry != null && role == Role.ENTRY) {
throw this.reject("has @Surround.Carry on " + paramRef(method, i) + "; only the entry handler's own locals can be carried");
}
if (local && carry != null) {
throw this.reject(paramRef(method, i) + " cannot be both @Surround.Local and @Surround.Carry");
}
if ((local || carry != null) && i < leading) {
throw this.reject("cannot mark its " + (role == Role.CATCH ? "caught Throwable" : "result") + " parameter @Surround." + (local ? "Local" : "Carry"));
}
final int next = carry != null ? 2 : local ? 1 : 0;
if (next < section) {
throw this.reject(paramRef(method, i) + " is out of order; declare operands first, then @Surround.Local, then @Surround.Carry parameters");
}
section = next;
if (local && firstLocal == count) {
firstLocal = i;
}
if (carry != null) {
if (ids == null) {
firstCarry = i;
ids = new String[count - i];
}
ids[i - firstCarry] = Annotations.<String>getValue(carry, "value", "");
}
}
this.localStart = Math.min(firstLocal, firstCarry);
this.carryStart = firstCarry;
this.carryIds = ids;
}
Type caught() {
return this.params[0];
}
String ref() {
return this.role == Role.ENTRY ? "entry handler" : exitRef(this.method);
}
SurroundRejection reject(String message) {
return new SurroundRejection(this.ref() + " " + message);
}
}
static final class Exits {
final Handler[] handlers;
final int catches;
final Handler finallyHandler;
final Handler skipped;
final Handler returnHandler;
final boolean handles;
final int skipSlot;
final Type[] carried;
final int argsSize;
Exits(Handler[] handlers, int skipSlot, Type[] carried) {
this.handlers = handlers;
int catches = 0;
Handler finallyHandler = null;
Handler skipped = null;
Handler returnHandler = null;
boolean handles = false;
int argsSize = 0;
for (int i = 0; i < handlers.length; i++) {
final Handler handler = handlers[i];
handler.index = i;
handles |= handler.handles;
argsSize = Math.max(argsSize, (handler.isStatic ? 0 : 1) + Bytecode.getArgsSize(handler.params));
switch (handler.role) {
case CATCH -> catches++;
case FINALLY -> finallyHandler = handler;
case SKIPPED -> skipped = handler;
case RETURN -> returnHandler = handler;
}
}
this.catches = catches;
this.finallyHandler = finallyHandler;
this.skipped = skipped;
this.returnHandler = returnHandler;
this.handles = handles;
this.skipSlot = skipSlot;
this.carried = carried;
this.argsSize = argsSize;
}
}
private SurroundBinding() {
}
static Type[] bind(MethodNode entry, int[] kinds, Handler[] handlers) {
checkNoParameterStores(entry);
final List<Carried> carried = carriedLocals(entry, kinds);
final Type[] slots = new Type[entry.maxLocals];
for (Handler exit : handlers) {
final Load[] loads = new Load[exit.params.length - exit.carryStart];
for (int i = exit.carryStart; i < exit.params.length; i++) {
loads[i - exit.carryStart] = resolve(carried, slots, exit, i);
}
exit.loads = loads;
}
return slots;
}
static int skipSlot(MethodNode entry, int[] kinds) {
final List<LocalVariableAnnotationNode> annotations = entry.visibleLocalVariableAnnotations;
int slot = -1;
for (int a = 0, n = annotations == null ? 0 : annotations.size(); a < n; a++) {
final LocalVariableAnnotationNode annotation = annotations.get(a);
if (!SurroundSpec.SKIP.equals(annotation.desc)) {
continue;
}
if (slot != -1) {
throw new SurroundRejection("declares two @Surround.Skip locals; there can only be one");
}
slot = annotatedSlot(entry, annotation, 0);
checkLocalAnnotation(entry, kinds, "@Surround.Skip", annotation, slot, "");
if (kinds[slot] != Type.INT) {
throw new SurroundRejection("has a @Surround.Skip local that is " + withArticle(kinds[slot]) + "; it must be boolean or int-like");
}
}
return slot;
}
private static AnnotationNode carry(MethodNode method, int index) {
final List<AnnotationNode> invisible = parameter(method.invisibleParameterAnnotations, index);
for (int i = 0, n = invisible == null ? 0 : invisible.size(); i < n; i++) {
final String name = Type.getType(invisible.get(i).desc).getClassName();
for (String sugar : SUGARS) {
if (name.endsWith("mixinextras.sugar." + sugar)) {
throw new SurroundRejection(paramRef(method, index) + " of '" + method.name + "' is annotated @" + sugar + SUGAR_HINT);
}
}
}
return Annotations.get(parameter(method.visibleParameterAnnotations, index), SurroundSpec.CARRY);
}
static AnnotationNode local(MethodNode method, int index) {
return Annotations.get(parameter(method.visibleParameterAnnotations, index), SurroundSpec.LOCAL);
}
private static List<AnnotationNode> parameter(List<AnnotationNode>[] table, int index) {
return table == null || index >= table.length ? null : table[index];
}
static String paramRef(MethodNode method, int index) {
final String name = paramName(method, index);
return name == null || name.isEmpty() ? "parameter " + index : "parameter " + index + " ('" + name + "')";
}
static String paramName(MethodNode method, int index) {
if (method.localVariables == null) {
return null;
}
final int slot = (Bytecode.isStatic(method) ? 0 : 1) + Bytecode.getArgsSize(Type.getArgumentTypes(method.desc), 0, index);
LocalVariableNode earliest = null;
int earliestPos = Integer.MAX_VALUE;
for (LocalVariableNode local : method.localVariables) {
if (local.index != slot) {
continue;
}
final int pos = method.instructions.indexOf(local.start);
if (pos < earliestPos) {
earliestPos = pos;
earliest = local;
}
}
return earliest == null ? null : earliest.name;
}
static AbstractInsnNode soleReturn(MethodNode entry) {
AbstractInsnNode sole = null;
AbstractInsnNode last = null;
int returns = 0;
for (AbstractInsnNode insn = entry.instructions.getFirst(); insn != null; insn = insn.getNext()) {
if (insn.getOpcode() < 0) {
continue;
}
last = insn;
if (insn.getOpcode() == Opcodes.RETURN) {
sole = insn;
returns++;
}
}
if (returns != 1 || sole != last) {
throw new SurroundRejection("must reach its end to hand over to the body; restructure it so every path falls through to its end, with no early return, throw or endless loop");
}
return sole;
}
private static Load resolve(List<Carried> carried, Type[] slots, Handler exit, int index) {
final Type type = exit.params[index];
final String id = exit.carryIds[index - exit.carryStart];
final int kind = kindOf(type);
final boolean named = !id.isEmpty();
boolean idDeclared = false;
int matches = 0;
Carried match = null;
for (int i = 0, n = carried.size(); i < n; i++) {
final Carried candidate = carried.get(i);
if (named && !id.equals(candidate.id())) {
continue;
}
idDeclared = true;
if (candidate.kind() == kind) {
matches++;
match = candidate;
}
}
final String param = paramRef(exit.method, index);
if (named && !idDeclared) {
throw exit.reject(param + " asks for the carried local '" + id + "', which the entry handler does not declare" + declaredList(carried));
}
if (matches == 0) {
throw exit.reject(param + " is " + type.getClassName() + " (" + withArticle(kind) + "), which matches no carried local of that kind" + declaredList(carried));
}
if (matches > 1) {
throw exit.reject(param + " is " + type.getClassName() + " (" + withArticle(kind) + "), which matches " + matches + " carried locals of that kind; name them with @Surround.Carry(\"...\") on both sides");
}
final Type existing = slots[match.slot()];
if (existing != null && !existing.equals(type)) {
throw exit.reject(param + " reads carried local " + match + " as " + type.getClassName() + ", but another exit handler reads it as " + existing.getClassName());
}
slots[match.slot()] = type;
return new Load(match.slot(), type);
}
private static List<Carried> carriedLocals(MethodNode entry, int[] kinds) {
final List<Carried> carried = new ArrayList<>();
final List<LocalVariableAnnotationNode> annotations = entry.visibleLocalVariableAnnotations;
for (int a = 0, n = annotations == null ? 0 : annotations.size(); a < n; a++) {
final LocalVariableAnnotationNode annotation = annotations.get(a);
if (!SurroundSpec.CARRY.equals(annotation.desc)) {
continue;
}
final int slot = annotatedSlot(entry, annotation, 0);
final String id = Annotations.<String>getValue(annotation, "value", "");
checkLocalAnnotation(entry, kinds, "@Surround.Carry", annotation, slot, id);
if (!id.isEmpty()) {
for (int i = 0, count = carried.size(); i < count; i++) {
if (id.equals(carried.get(i).id())) {
throw new SurroundRejection("declares two @Surround.Carry locals named '" + id + "'");
}
}
}
carried.add(new Carried(slot, kinds[slot], id));
}
return carried;
}
// FML's deobf remapper (ASM LocalVariablesSorter) corrupts annotation indices of long/double locals; the LVT stays right.
private static int annotatedSlot(MethodNode entry, LocalVariableAnnotationNode annotation, int range) {
final int raw = annotation.index.get(range).intValue();
final LabelNode start = annotation.start.get(range);
final LabelNode end = annotation.end.get(range);
boolean any = false;
boolean rawWide = false;
boolean rawFound = false;
int wide = -1;
int wideCount = 0;
for (int i = 0, n = entry.localVariables == null ? 0 : entry.localVariables.size(); i < n; i++) {
final LocalVariableNode local = entry.localVariables.get(i);
if (local.start != start || local.end != end) {
continue;
}
any = true;
final boolean isWide = Type.getType(local.desc).getSize() == 2;
if (isWide) {
wide = local.index;
wideCount++;
}
if (local.index == raw) {
rawFound = true;
rawWide = isWide;
}
}
final int slot = !any || rawFound && !(rawWide && wideCount > 1) ? raw : wideCount == 1 && !rawFound ? wide : -1;
if (slot < 0) {
throw new SurroundRejection("has a long or double annotated local that shares its scope with another; initialize it at its declaration");
}
if (slot >= entry.maxLocals) {
throw new SurroundRejection("has an annotated local outside its method's locals (slot " + slot + " of " + entry.maxLocals + "); compile with local variable tables");
}
return slot;
}
private static void checkLocalAnnotation(MethodNode entry, int[] kinds, String what, LocalVariableAnnotationNode annotation, int slot, String id) {
final String local = id.isEmpty() ? "in slot " + slot : "named '" + id + "'";
final TypePath path = annotation.typePath;
for (int i = 0, length = path == null ? 0 : path.getLength(); i < length; i++) {
if (path.getStep(i) != TypePath.ARRAY_ELEMENT) {
throw new SurroundRejection("has a " + what + " " + local + " inside the local's type (" + path + "); " + what + " must annotate the local itself, not part of its type");
}
}
for (int i = 1, count = annotation.index.size(); i < count; i++) {
if (annotatedSlot(entry, annotation, i) != slot) {
throw new SurroundRejection("has a " + what + " local " + local + " recorded in several slots " + annotation.index + "; initialize it at its declaration");
}
}
LabelNode end = annotation.end.get(0);
for (int i = 1, n = annotation.end.size(); i < n; i++) {
if (entry.instructions.indexOf(annotation.end.get(i)) > entry.instructions.indexOf(end)) {
end = annotation.end.get(i);
}
}
for (AbstractInsnNode insn = end.getNext(); insn != null; insn = insn.getNext()) {
if (insn.getOpcode() >= 0 && insn.getOpcode() != Opcodes.RETURN) {
throw new SurroundRejection("has a " + what + " local " + local + " that goes out of scope before the entry handler ends; declare it in the entry handler's outermost block");
}
}
if (kinds[slot] == TOP) {
throw new SurroundRejection("has a " + what + " local " + local + " that is not definitely assigned when the entry handler ends; initialize it at its declaration");
}
}
private static void checkNoParameterStores(MethodNode entry) {
final int limit = Bytecode.getFirstNonArgLocalIndex(entry);
for (AbstractInsnNode insn = entry.instructions.getFirst(); insn != null; insn = insn.getNext()) {
final int var;
if (insn instanceof VarInsnNode varInsn && storeKind(insn.getOpcode()) != -1) {
var = varInsn.var;
} else if (insn instanceof IincInsnNode iinc) {
var = iinc.var;
} else {
continue;
}
if (var < limit) {
throw new SurroundRejection("must not assign its parameters; use @ModifyVariable or @WrapMethod to change arguments");
}
}
}
static int[] kindsAtEnd(MethodNode entry, AbstractInsnNode soleReturn) {
final int maxLocals = entry.maxLocals;
int[] vars = initialVars(entry);
int count = vars.length;
int[] state = toSlots(vars, count, maxLocals);
for (AbstractInsnNode insn = entry.instructions.getFirst(); insn != soleReturn; insn = insn.getNext()) {
if (insn instanceof FrameNode frame) {
final int locals = frame.local == null ? 0 : frame.local.size();
switch (frame.type) {
case Opcodes.F_NEW, Opcodes.F_FULL, Opcodes.F_APPEND -> {
if (frame.type != Opcodes.F_APPEND) {
count = 0;
}
if (count + locals > vars.length) {
vars = Arrays.copyOf(vars, count + locals);
}
for (int i = 0; i < locals; i++) {
vars[count++] = frameKind(frame.local.get(i));
}
}
case Opcodes.F_CHOP -> count -= locals;
default -> {
}
}
state = toSlots(vars, count, maxLocals);
} else if (insn instanceof VarInsnNode var && storeKind(insn.getOpcode()) != -1) {
store(state, var.var, storeKind(insn.getOpcode()));
}
}
return state;
}
private static void store(int[] state, int var, int kind) {
if (var > 0 && wide(state[var - 1])) {
state[var - 1] = TOP;
}
state[var] = kind;
if (wide(kind) && var + 1 < state.length) {
state[var + 1] = TOP;
}
}
private static boolean wide(int kind) {
return kind == Type.LONG || kind == Type.DOUBLE;
}
private static int[] initialVars(MethodNode entry) {
final Type[] args = Type.getArgumentTypes(entry.desc);
final int receiver = Bytecode.isStatic(entry) ? 0 : 1;
final int[] vars = new int[receiver + args.length];
if (receiver != 0) {
vars[0] = Type.OBJECT;
}
for (int i = 0; i < args.length; i++) {
vars[receiver + i] = kindOf(args[i]);
}
return vars;
}
private static int frameKind(Object entry) {
if (entry instanceof String || Opcodes.NULL.equals(entry) || Opcodes.UNINITIALIZED_THIS.equals(entry)) {
return Type.OBJECT;
} else if (Opcodes.INTEGER.equals(entry)) {
return Type.INT;
} else if (Opcodes.FLOAT.equals(entry)) {
return Type.FLOAT;
} else if (Opcodes.LONG.equals(entry)) {
return Type.LONG;
} else if (Opcodes.DOUBLE.equals(entry)) {
return Type.DOUBLE;
}
return TOP;
}
private static int[] toSlots(int[] vars, int count, int maxLocals) {
final int[] slots = new int[maxLocals];
Arrays.fill(slots, TOP);
int slot = 0;
for (int i = 0; i < count && slot < maxLocals; i++) {
slots[slot] = vars[i];
slot += wide(vars[i]) ? 2 : 1;
}
return slots;
}
private static int storeKind(int opcode) {
return switch (opcode) {
case Opcodes.ISTORE -> Type.INT;
case Opcodes.LSTORE -> Type.LONG;
case Opcodes.FSTORE -> Type.FLOAT;
case Opcodes.DSTORE -> Type.DOUBLE;
case Opcodes.ASTORE -> Type.OBJECT;
default -> -1;
};
}
static int kindOf(Type type) {
return switch (type.getSort()) {
case Type.BOOLEAN, Type.CHAR, Type.BYTE, Type.SHORT, Type.INT -> Type.INT;
case Type.LONG -> Type.LONG;
case Type.FLOAT -> Type.FLOAT;
case Type.DOUBLE -> Type.DOUBLE;
default -> Type.OBJECT;
};
}
private static String kindDescription(int kind) {
return switch (kind) {
case Type.INT -> "int-like value";
case Type.LONG -> "long";
case Type.FLOAT -> "float";
case Type.DOUBLE -> "double";
default -> "reference";
};
}
private static String withArticle(int kind) {
return (kind == Type.INT ? "an " : "a ") + kindDescription(kind);
}
private static String declaredList(List<Carried> carried) {
final StringJoiner all = new StringJoiner(", ", "; the entry handler carries ", "").setEmptyValue("; the entry handler carries nothing");
for (Carried local : carried) {
all.add(local.toString());
}
return all.toString();
}
static String exitRef(MethodNode exit) {
return "exit handler '" + exit.name + "'";
}
private record Carried(int slot, int kind, String id) {
@Override
public String toString() {
return (this.id.isEmpty() ? "an unnamed " + kindDescription(this.kind) : "'" + this.id + "', " + withArticle(this.kind)) + " in slot " + this.slot;
}
}
}
@@ -0,0 +1,29 @@
package com.gtnewhorizons.angelica.experimental.surround;
import org.spongepowered.asm.launch.GlobalProperties;
import org.spongepowered.asm.mixin.MixinEnvironment;
import org.spongepowered.asm.mixin.injection.struct.InjectionInfo;
import org.spongepowered.asm.mixin.transformer.IMixinTransformer;
public final class SurroundBootstrap {
private SurroundBootstrap() {
}
private static final class Registration {
static {
InjectionInfo.register(SurroundInjectionInfo.class);
}
static void ensure() {
}
}
public static void init() {
Registration.ensure();
if (GlobalProperties.get(GlobalProperties.Keys.INIT) != null && MixinEnvironment.getDefaultEnvironment().getActiveTransformer() instanceof IMixinTransformer) {
SurroundApplicatorExtension.instance();
}
}
}
@@ -0,0 +1,605 @@
package com.gtnewhorizons.angelica.experimental.surround;
import com.gtnewhorizons.angelica.experimental.surround.SurroundBinding.Exits;
import com.gtnewhorizons.angelica.experimental.surround.SurroundBinding.Handler;
import com.gtnewhorizons.angelica.experimental.surround.SurroundBinding.Load;
import com.llamalad7.mixinextras.utils.ASMUtils;
import org.spongepowered.asm.lib.Opcodes;
import org.spongepowered.asm.lib.Type;
import org.spongepowered.asm.lib.tree.AbstractInsnNode;
import org.spongepowered.asm.lib.tree.ClassNode;
import org.spongepowered.asm.lib.tree.FrameNode;
import org.spongepowered.asm.lib.tree.IincInsnNode;
import org.spongepowered.asm.lib.tree.InsnList;
import org.spongepowered.asm.lib.tree.InsnNode;
import org.spongepowered.asm.lib.tree.JumpInsnNode;
import org.spongepowered.asm.lib.tree.LabelNode;
import org.spongepowered.asm.lib.tree.LocalVariableNode;
import org.spongepowered.asm.lib.tree.MethodInsnNode;
import org.spongepowered.asm.lib.tree.MethodNode;
import org.spongepowered.asm.lib.tree.TryCatchBlockNode;
import org.spongepowered.asm.lib.tree.TypeInsnNode;
import org.spongepowered.asm.lib.tree.VarInsnNode;
import org.spongepowered.asm.lib.tree.analysis.Analyzer;
import org.spongepowered.asm.lib.tree.analysis.AnalyzerException;
import org.spongepowered.asm.lib.tree.analysis.BasicInterpreter;
import org.spongepowered.asm.lib.tree.analysis.BasicValue;
import org.spongepowered.asm.lib.tree.analysis.Frame;
import org.spongepowered.asm.mixin.injection.struct.InjectionNodes.InjectionNode;
import org.spongepowered.asm.service.MixinService;
import org.spongepowered.asm.util.Bytecode;
import org.spongepowered.asm.util.Bytecode.Visibility;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
final class SurroundCallSite {
private static final String REGION = "surround:region";
private static final Type[] NOTHING = new Type[0];
record Region(LabelNode start, LabelNode end) {}
private SurroundCallSite() {
}
static Type[] operands(MethodInsnNode call, String targetClass) {
final Type[] args = Type.getArgumentTypes(call.desc);
if (call.getOpcode() == Opcodes.INVOKESTATIC) {
return args;
}
final Type[] all = new Type[args.length + 1];
all[0] = Type.getObjectType(call.getOpcode() == Opcodes.INVOKESPECIAL ? targetClass : call.owner);
System.arraycopy(args, 0, all, 1, args.length);
return all;
}
static void apply(ClassNode classNode, CallSiteSpec spec) {
final InjectionNode node = spec.node();
if (node == null) {
final MethodNode target = spec.target();
final MethodNode body = relocate(classNode, target);
final MethodInsnNode call = (MethodInsnNode) ASMUtils.getInvokeInstruction(classNode, body);
target.instructions.add(call);
target.instructions.add(new InsnNode(spec.result().getOpcode(Opcodes.IRETURN)));
final Type[] args = operands(call, classNode.name);
final int[] argSlots = new int[args.length];
for (int i = 1; i < args.length; i++) {
argSlots[i] = argSlots[i - 1] + args[i - 1].getSize();
}
emit(classNode, spec, target, call, null, Bytecode.isStatic(target) ? 0 : 1, argSlots);
return;
}
final AbstractInsnNode champion = node.getCurrentTarget();
final Region inner = node.<Region>getDecoration(REGION);
final MethodNode owner = champion == null ? null : owner(classNode, inner == null ? champion : inner.start());
if (owner == null) {
MixinService.getService().getLogger("mixin").warn(spec.description() + " skipped: the surrounded call was removed by another injector");
return;
}
if (!spec.entryDesc().equals(spec.entry().desc)) {
throw cannotFollow(spec, "the entry handler's descriptor changed from " + spec.entryDesc() + " to " + spec.entry().desc + " after injection", null);
}
if (!(champion instanceof MethodInsnNode call)) {
throw cannotFollow(spec, "the call was replaced by " + Bytecode.describeNode(champion, false) + ", which is not a method call", null);
}
final int offset = node.isReplaced() && call.getOpcode() != Opcodes.INVOKESTATIC ? 1 : 0;
checkOperands(spec, call, operands(call, classNode.name), offset);
final Exits exits = spec.exits();
final Type result = spec.result();
final Type actual = Type.getReturnType(call.desc);
if ((exits.handles || exits.skipSlot >= 0 || exits.returnHandler != null) && (actual.getSize() != result.getSize() || actual.getSize() > 0 && SurroundBinding.kindOf(actual) != SurroundBinding.kindOf(result))) {
throw cannotFollow(spec, "the call was replaced by " + Bytecode.describeNode(call, false) + ", which returns " + actual.getClassName() + " where the surround supplies or replaces a " + result.getClassName(), null);
}
emit(classNode, spec, owner, call, inner, offset, null);
}
private static void emit(ClassNode classNode, CallSiteSpec spec, MethodNode owner, MethodInsnNode call, Region inner, int offset, int[] argSlots) {
final boolean slots = argSlots != null;
final Type[] args = operands(call, classNode.name);
final Exits exits = spec.exits();
final boolean skips = exits.skipSlot >= 0;
final Handler returnHandler = exits.returnHandler;
final Type result = spec.result();
final Type actual = Type.getReturnType(call.desc);
final MethodNode entry = spec.entry();
final Type[] operands = spec.operands();
final int argBase = Bytecode.isStatic(spec.target()) ? 0 : 1;
final Type[] entryParams = Type.getArgumentTypes(entry.desc);
final int entryOperands = entryParams.length - spec.entryLocals().length;
final int firstNonArg = argBase + Bytecode.getArgsSize(entryParams);
final MethodNode inlined = inlineable(entry);
final boolean restoresBelow = exits.handles || !inlined.tryCatchBlocks.isEmpty();
final boolean relocated = owner != spec.target();
owner.maxLocals = Math.max(Math.max(owner.maxLocals, spec.target().maxLocals), usedLocals(owner));
int next = owner.maxLocals;
final AbstractInsnNode anchor = inner == null ? call : inner.start();
final Frame<BasicValue> frame = !slots && (restoresBelow || relocated && hasLocals(spec)) ? frameAt(owner, anchor, spec) : null;
final int shift = relocated ? Bytecode.getFirstNonArgLocalIndex(owner) - Bytecode.getFirstNonArgLocalIndex(spec.target()) : 0;
final Type[] below = restoresBelow ? below(spec, frame, args.length) : NOTHING;
final int spillFrom = slots || restoresBelow || skips ? 0 : offset;
final boolean spill = slots || restoresBelow || skips || args.length > offset && (entryOperands > 0 || prefixed(spec));
final int[] spillSlots = slots ? argSlots : new int[args.length];
if (spill && !slots) {
for (int i = spillFrom; i < args.length; i++) {
spillSlots[i] = next;
next += args[i].getSize();
}
}
final int[] belowSlots = new int[below.length];
for (int i = 0; i < below.length; i++) {
belowSlots[i] = next;
next += below[i].getSize();
}
final int entryBase = next;
final int delta = entryBase - firstNonArg;
final int excSlot = entryBase + Math.max(0, inlined.maxLocals - firstNonArg);
final int resSlot = excSlot + 1;
final boolean storesResult = returnHandler != null && result.getSize() > 0;
final int[] entryLocals = localSlots(spec, owner, anchor, frame, shift, spec.entryLocals(), entryParams, entryOperands);
remapLocals(inlined, argBase, firstNonArg, entryParams, entryOperands, spillSlots, offset, entryLocals, delta);
final Handler[] handlers = exits.handlers;
final int[][] locals = new int[handlers.length][];
for (Handler exit : handlers) {
locals[exit.index] = localSlots(spec, owner, anchor, frame, shift, spec.locals()[exit.index], exit.params, exit.localStart);
}
final Emitter emitter = new Emitter(classNode, exits, operands, Arrays.copyOfRange(spillSlots, offset, offset + operands.length), locals, delta);
final LabelNode sStart = new LabelNode();
final LabelNode tStart = inner == null || skips ? new LabelNode() : inner.start();
final LabelNode tEnd = inner == null ? new LabelNode() : inner.end();
final LabelNode sEnd = new LabelNode();
final LabelNode join = skips ? new LabelNode() : null;
final InsnList head = new InsnList();
head.add(sStart);
if (spill && !slots) {
for (int i = args.length - 1; i >= spillFrom; i--) {
final int j = i - offset;
if (j >= 0 && j < operands.length && operands[j].getSort() >= Type.ARRAY && !args[i].equals(operands[j])) {
head.add(new TypeInsnNode(Opcodes.CHECKCAST, operands[j].getInternalName()));
}
head.add(new VarInsnNode(args[i].getOpcode(Opcodes.ISTORE), spillSlots[i]));
}
for (int i = below.length - 1; i >= 0; i--) {
head.add(new VarInsnNode(below[i].getOpcode(Opcodes.ISTORE), belowSlots[i]));
}
}
head.add(inlined.instructions);
if (skips) {
final LabelNode callPath = new LabelNode();
head.add(new VarInsnNode(Opcodes.ILOAD, exits.skipSlot + delta));
head.add(new JumpInsnNode(Opcodes.IFEQ, callPath));
head.add(tStart);
loadBelow(head, below, belowSlots);
emitter.callSkipped(head);
head.add(new JumpInsnNode(Opcodes.GOTO, join));
head.add(callPath);
}
if (spill) {
loadBelow(head, below, belowSlots);
for (int i = spillFrom; i < args.length; i++) {
head.add(new VarInsnNode(args[i].getOpcode(Opcodes.ILOAD), spillSlots[i]));
}
}
if (inner == null && !skips) {
head.add(tStart);
}
owner.instructions.insertBefore(anchor, head);
final InsnList tail = new InsnList();
if (inner == null) {
tail.add(tEnd);
}
final LabelNode retStart = returnHandler == null ? null : new LabelNode();
final LabelNode retEnd = returnHandler == null ? null : new LabelNode();
if (returnHandler != null) {
tail.add(retStart);
if (result.getSize() > 0) {
if (result.getSort() >= Type.ARRAY && !actual.equals(result)) {
tail.add(new TypeInsnNode(Opcodes.CHECKCAST, result.getInternalName()));
}
tail.add(new VarInsnNode(result.getOpcode(Opcodes.ISTORE), resSlot));
}
emitter.callReturn(tail, result, resSlot);
tail.add(retEnd);
}
if (skips) {
tail.add(join);
}
emitter.callFinally(tail);
tail.add(new JumpInsnNode(Opcodes.GOTO, sEnd));
final AbstractInsnNode beforeHandlers = tail.getLast();
final List<TryCatchBlockNode> blocks = new ArrayList<>(inlined.tryCatchBlocks);
emitter.emitHandlers(tail, blocks, tStart, tEnd, retStart, retEnd, excSlot, below, belowSlots, sEnd);
final AbstractInsnNode firstHandler = beforeHandlers.getNext();
tail.add(sEnd);
owner.instructions.insert(inner == null ? call : inner.end(), tail);
owner.tryCatchBlocks.addAll(insertionIndex(owner, sStart, sEnd), blocks);
owner.maxLocals = Math.max(owner.maxLocals, storesResult ? resSlot + result.getSize() : excSlot + 1);
owner.maxStack = Math.max(owner.maxStack, spec.target().maxStack) + Math.max(inlined.maxStack, exits.argsSize + 2);
if (slots) {
final LabelNode end = new LabelNode();
owner.instructions.add(end);
final List<LocalVariableNode> names = new ArrayList<>();
nameParameters(names, classNode, spec, argBase, entryOperands, sStart, end);
nameEntryLocals(names, inlined, owner.instructions, exits.carried, firstNonArg, delta, end);
if (firstHandler != null) {
names.add(new LocalVariableNode("surround$error", SurroundSpec.THROWABLE_DESC, null, (LabelNode) firstHandler, end, excSlot));
}
if (storesResult) {
names.add(new LocalVariableNode("surround$result", result.getDescriptor(), null, retStart, retEnd, resSlot));
}
owner.localVariables = names;
} else {
spec.node().decorate(REGION, new Region(sStart, sEnd));
}
}
private static void nameParameters(List<LocalVariableNode> names, ClassNode classNode, CallSiteSpec spec, int argBase, int entryOperands, LabelNode start, LabelNode end) {
final Type[] params = spec.operands();
final MethodNode entry = spec.entry();
if (argBase > 0) {
names.add(new LocalVariableNode("this", Type.getObjectType(classNode.name).getDescriptor(), null, start, end, 0));
}
for (int i = 0, slot = argBase; i < params.length; slot += params[i++].getSize()) {
final String name = i < entryOperands ? SurroundBinding.paramName(entry, i) : null;
names.add(new LocalVariableNode(name != null ? name : "arg" + i, params[i].getDescriptor(), null, start, end, slot));
}
}
private static void nameEntryLocals(List<LocalVariableNode> names, MethodNode inlined, InsnList insns, Type[] carried, int firstNonArg, int delta, LabelNode end) {
final LocalVariableNode[] furthest = new LocalVariableNode[carried.length];
for (LocalVariableNode local : inlined.localVariables) {
if (!isCarried(carried, local.index)) {
continue;
}
final LocalVariableNode current = furthest[local.index];
if (current == null || insns.indexOf(local.end) > insns.indexOf(current.end)) {
furthest[local.index] = local;
}
}
for (LocalVariableNode local : inlined.localVariables) {
if (local.index < firstNonArg) {
continue;
}
if (isCarried(carried, local.index) && local == furthest[local.index]) {
final Type type = carried[local.index];
names.add(new LocalVariableNode(local.name, type.getSort() >= Type.ARRAY ? local.desc : type.getDescriptor(), null, local.start, end, local.index + delta));
} else {
local.index += delta;
names.add(local);
}
}
}
private static boolean isCarried(Type[] carried, int slot) {
return slot < carried.length && carried[slot] != null;
}
private static MethodNode inlineable(MethodNode entry) {
final MethodNode copy = new MethodNode(entry.access, entry.name, entry.desc, null, null);
entry.accept(copy);
for (AbstractInsnNode insn = copy.instructions.getFirst(); insn != null; ) {
final AbstractInsnNode next = insn.getNext();
if (insn instanceof FrameNode) {
copy.instructions.remove(insn);
}
insn = next;
}
AbstractInsnNode last = copy.instructions.getLast();
while (last.getOpcode() < 0) {
last = last.getPrevious();
}
copy.instructions.remove(last);
return copy;
}
private static MethodNode relocate(ClassNode classNode, MethodNode target) {
final MethodNode body = new MethodNode(target.access & ~Opcodes.ACC_SYNCHRONIZED, unique(classNode, target.name + "$surround"), target.desc, null, null);
Bytecode.setVisibility(body, Visibility.PRIVATE);
body.instructions = target.instructions;
body.instructions.resetLabels();
body.tryCatchBlocks = target.tryCatchBlocks;
body.localVariables = target.localVariables;
body.visibleLocalVariableAnnotations = target.visibleLocalVariableAnnotations;
body.invisibleLocalVariableAnnotations = target.invisibleLocalVariableAnnotations;
body.maxStack = target.maxStack;
body.maxLocals = target.maxLocals;
target.instructions = new InsnList();
target.tryCatchBlocks = new ArrayList<>();
target.visibleLocalVariableAnnotations = null;
target.invisibleLocalVariableAnnotations = null;
target.maxLocals = target.maxStack = Bytecode.getFirstNonArgLocalIndex(target);
classNode.methods.add(body);
return body;
}
private static String unique(ClassNode classNode, String base) {
String name = base;
for (int i = 1; ; name = base + "$" + i++) {
final String candidate = name;
if (classNode.methods.stream().noneMatch(method -> method.name.equals(candidate))) {
return name;
}
}
}
private static boolean hasLocals(CallSiteSpec spec) {
if (spec.entryLocals().length > 0) {
return true;
}
for (int[] locals : spec.locals()) {
if (locals.length > 0) {
return true;
}
}
return false;
}
private static Frame<BasicValue> frameAt(MethodNode owner, AbstractInsnNode anchor, CallSiteSpec spec) {
final int index = owner.instructions.indexOf(anchor);
for (owner.maxStack = Math.max(owner.maxStack, spec.target().maxStack) + 16; ; owner.maxStack <<= 1) {
try {
return new Analyzer<>(new BasicInterpreter()).analyze(owner.name, owner)[index];
} catch (AnalyzerException e) {
if (!(e.getCause() instanceof IndexOutOfBoundsException) || owner.maxStack > 0xFFFF) {
throw cannotFollow(spec, "cannot analyze " + owner.name + owner.desc, e);
}
}
}
}
private static Type[] below(CallSiteSpec spec, Frame<BasicValue> frame, int args) {
final int count = frame == null ? 0 : frame.getStackSize() - args;
if (count <= 0) {
return NOTHING;
}
final Type[] below = new Type[count];
for (int i = 0; i < count; i++) {
final BasicValue value = frame.getStack(i);
final Type type = value.getType();
if (type == null || type.getSort() == Type.VOID) {
throw cannotFollow(spec, "stack value " + i + " below the call is " + value + ", which cannot be restored", null);
}
below[i] = value.isReference() ? Type.getObjectType(SurroundSpec.OBJECT) : type;
}
return below;
}
private static int[] localSlots(CallSiteSpec spec, MethodNode owner, AbstractInsnNode anchor, Frame<BasicValue> frame, int shift, int[] resolved, Type[] params, int first) {
if (resolved.length == 0 || owner == spec.target()) {
return resolved;
}
final int firstNonArg = Bytecode.getFirstNonArgLocalIndex(spec.target());
final int[] slots = new int[resolved.length];
for (int i = 0; i < resolved.length; i++) {
final int slot = resolved[i] < firstNonArg ? resolved[i] : resolved[i] + shift;
checkLocal(spec, owner, anchor, frame, slot, params[first + i], resolved[i]);
slots[i] = slot;
}
return slots;
}
private static void checkLocal(CallSiteSpec spec, MethodNode owner, AbstractInsnNode anchor, Frame<BasicValue> frame, int slot, Type type, int resolved) {
String found = null;
if (owner.localVariables != null) {
final int at = owner.instructions.indexOf(anchor);
for (LocalVariableNode local : owner.localVariables) {
if (local.index == slot && owner.instructions.indexOf(local.start) <= at && at < owner.instructions.indexOf(local.end) && !type.getDescriptor().equals(local.desc)) {
found = Type.getType(local.desc).getClassName() + " '" + local.name + "'";
}
}
}
if (found == null && frame != null) {
final BasicValue value = slot < frame.getLocals() ? frame.getLocal(slot) : null;
final Type actual = value == null ? null : value.getType();
if (actual == null || actual.getSort() == Type.VOID || actual.getSize() != type.getSize() || SurroundBinding.kindOf(actual) != SurroundBinding.kindOf(type)) {
found = value == null ? "nothing" : String.valueOf(value);
}
}
if (found != null) {
throw cannotFollow(spec, "@Surround.Local " + type.getClassName() + " resolved to local " + resolved + " at injection, but after the body moved to " + owner.name + owner.desc + " local " + slot + " holds " + found, null);
}
}
private static MethodNode owner(ClassNode classNode, AbstractInsnNode insn) {
AbstractInsnNode first = insn;
while (first.getPrevious() != null) {
first = first.getPrevious();
}
final List<MethodNode> methods = classNode.methods;
for (int i = 0, n = methods.size(); i < n; i++) {
if (methods.get(i).instructions.getFirst() == first) {
return methods.get(i);
}
}
return null;
}
private static void checkOperands(CallSiteSpec spec, MethodInsnNode call, Type[] args, int offset) {
final Type[] operands = spec.operands();
if (args.length - offset < operands.length) {
throw cannotFollow(spec, "the call was replaced by " + Bytecode.describeNode(call, false) + ", which takes fewer arguments than the surrounded call", null);
}
for (int i = 0; i < operands.length; i++) {
if (SurroundBinding.kindOf(args[offset + i]) != SurroundBinding.kindOf(operands[i])) {
throw cannotFollow(spec, "the call was replaced by " + Bytecode.describeNode(call, false) + ", whose argument " + (offset + i) + " is " + args[offset + i].getClassName() + " where the surrounded call passed " + operands[i].getClassName(), null);
}
}
}
private static IllegalStateException cannotFollow(CallSiteSpec spec, String detail, Throwable cause) {
return new IllegalStateException(spec.description() + " cannot be applied, another transformation left the site in a shape it cannot follow: " + detail, cause);
}
private static boolean prefixed(CallSiteSpec spec) {
for (Handler exit : spec.exits().handlers) {
if (exit.localStart > exit.leading) {
return true;
}
}
return false;
}
private static int usedLocals(MethodNode owner) {
int max = Bytecode.getFirstNonArgLocalIndex(owner);
for (AbstractInsnNode insn = owner.instructions.getFirst(); insn != null; insn = insn.getNext()) {
if (insn instanceof VarInsnNode varInsn) {
final int opcode = insn.getOpcode();
final boolean wide = opcode == Opcodes.LLOAD || opcode == Opcodes.DLOAD || opcode == Opcodes.LSTORE || opcode == Opcodes.DSTORE;
max = Math.max(max, varInsn.var + (wide ? 2 : 1));
} else if (insn instanceof IincInsnNode iinc) {
max = Math.max(max, iinc.var + 1);
}
}
return max;
}
private static void remapLocals(MethodNode inlined, int argBase, int firstNonArg, Type[] entryParams, int entryOperands, int[] spillSlots, int offset, int[] entryLocals, int delta) {
final int[] paramSlots = new int[firstNonArg];
for (int i = 0, slot = argBase; i < entryParams.length; i++) {
paramSlots[slot] = i < entryOperands ? spillSlots[offset + i] : entryLocals[i - entryOperands];
slot += entryParams[i].getSize();
}
for (AbstractInsnNode insn = inlined.instructions.getFirst(); insn != null; insn = insn.getNext()) {
if (insn instanceof VarInsnNode var) {
var.var = remap(var.var, argBase, firstNonArg, paramSlots, delta);
} else if (insn instanceof IincInsnNode iinc) {
iinc.var = remap(iinc.var, argBase, firstNonArg, paramSlots, delta);
}
}
}
private static int remap(int var, int argBase, int firstNonArg, int[] paramSlots, int delta) {
if (var < argBase) {
return var;
}
if (var < firstNonArg) {
return paramSlots[var];
}
return var + delta;
}
private static int insertionIndex(MethodNode owner, LabelNode start, LabelNode end) {
final List<TryCatchBlockNode> blocks = owner.tryCatchBlocks;
final InsnList insns = owner.instructions;
final int from = insns.indexOf(start);
final int to = insns.indexOf(end);
for (int i = 0, n = blocks.size(); i < n; i++) {
final TryCatchBlockNode block = blocks.get(i);
final int s = insns.indexOf(block.start);
final int e = insns.indexOf(block.end);
if (s < to && e > from && (s < from || e > to)) {
return i;
}
}
return blocks.size();
}
private static void loadBelow(InsnList insns, Type[] below, int[] belowSlots) {
for (int i = 0; i < below.length; i++) {
insns.add(new VarInsnNode(below[i].getOpcode(Opcodes.ILOAD), belowSlots[i]));
}
}
private record Emitter(ClassNode classNode, Exits exits, Type[] operands, int[] operandSlots, int[][] locals, int delta) {
private void invoke(InsnList insns, Handler exit, Type leading, int leadingSlot) {
final int[] locals = this.locals[exit.index];
if (!exit.isStatic) {
insns.add(new VarInsnNode(Opcodes.ALOAD, 0));
}
if (leading != null) {
insns.add(new VarInsnNode(leading.getOpcode(Opcodes.ILOAD), leadingSlot));
}
for (int i = 0, n = exit.localStart - exit.leading; i < n; i++) {
insns.add(new VarInsnNode(this.operands[i].getOpcode(Opcodes.ILOAD), this.operandSlots[i]));
}
for (int i = exit.localStart; i < exit.carryStart; i++) {
insns.add(new VarInsnNode(exit.params[i].getOpcode(Opcodes.ILOAD), locals[i - exit.localStart]));
}
for (Load load : exit.loads) {
final Type type = load.type();
insns.add(new VarInsnNode(type.getOpcode(Opcodes.ILOAD), load.slot() + this.delta));
if (type.getSort() >= Type.ARRAY && !SurroundSpec.OBJECT.equals(type.getInternalName())) {
insns.add(new TypeInsnNode(Opcodes.CHECKCAST, type.getInternalName()));
}
}
// MixinExtras 0.5.0 internal (pinned by UniMixins 0.3.1); recheck on bump
insns.add(ASMUtils.getInvokeInstruction(this.classNode, exit.method));
}
void callFinally(InsnList insns) {
if (this.exits.finallyHandler != null) {
this.invoke(insns, this.exits.finallyHandler, null, 0);
}
}
void callSkipped(InsnList insns) {
if (this.exits.skipped != null) {
this.invoke(insns, this.exits.skipped, null, 0);
}
}
void callReturn(InsnList insns, Type result, int resSlot) {
this.invoke(insns, this.exits.returnHandler, result.getSize() > 0 ? result : null, resSlot);
}
void emitHandlers(InsnList tail, List<TryCatchBlockNode> blocks, LabelNode start, LabelNode end, LabelNode retStart, LabelNode retEnd, int excSlot, Type[] below, int[] belowSlots, LabelNode handledExit) {
final Handler[] handlers = this.exits.handlers;
final boolean hasFinally = this.exits.finallyHandler != null;
final LabelNode catchAll = hasFinally ? new LabelNode() : null;
final List<TryCatchBlockNode> catchAlls = hasFinally ? new ArrayList<>() : null;
for (int k = 0; k < this.exits.catches; k++) {
final Handler exit = handlers[k];
final LabelNode handler = new LabelNode();
final LabelNode exitStart = new LabelNode();
final LabelNode exitEnd = new LabelNode();
tail.add(handler);
tail.add(new VarInsnNode(Opcodes.ASTORE, excSlot));
if (exit.handles) {
loadBelow(tail, below, belowSlots);
}
tail.add(exitStart);
this.invoke(tail, exit, exit.caught(), excSlot);
tail.add(exitEnd);
this.callFinally(tail);
if (exit.handles) {
tail.add(new JumpInsnNode(Opcodes.GOTO, handledExit));
} else {
tail.add(new VarInsnNode(Opcodes.ALOAD, excSlot));
tail.add(new InsnNode(Opcodes.ATHROW));
}
blocks.add(new TryCatchBlockNode(start, end, handler, exit.caught().getInternalName()));
if (hasFinally) {
catchAlls.add(new TryCatchBlockNode(exitStart, exitEnd, catchAll, null));
}
}
if (hasFinally) {
tail.add(catchAll);
tail.add(new VarInsnNode(Opcodes.ASTORE, excSlot));
this.callFinally(tail);
tail.add(new VarInsnNode(Opcodes.ALOAD, excSlot));
tail.add(new InsnNode(Opcodes.ATHROW));
if (retStart != null) {
catchAlls.add(new TryCatchBlockNode(retStart, retEnd, catchAll, null));
}
catchAlls.add(new TryCatchBlockNode(start, end, catchAll, null));
blocks.addAll(catchAlls);
}
}
}
}
@@ -0,0 +1,72 @@
package com.gtnewhorizons.angelica.experimental.surround;
import org.spongepowered.asm.lib.tree.AnnotationNode;
import org.spongepowered.asm.lib.tree.MethodNode;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.code.Injector;
import org.spongepowered.asm.mixin.injection.struct.InjectionInfo;
import org.spongepowered.asm.mixin.injection.throwables.InvalidInjectionException;
import org.spongepowered.asm.mixin.transformer.MixinTargetContext;
import org.spongepowered.asm.util.Annotations;
import java.util.List;
@InjectionInfo.AnnotationType(Surround.class)
@InjectionInfo.HandlerPrefix("surround")
public final class SurroundInjectionInfo extends InjectionInfo {
boolean callSite;
public SurroundInjectionInfo(MixinTargetContext mixin, MethodNode method, AnnotationNode annotation) {
super(mixin, method, annotation);
}
@Override
protected void readInjectionPoints() {
final List<AnnotationNode> ats = Annotations.<AnnotationNode>getValue(this.annotation, this.atKey, false);
this.callSite = ats != null && !ats.isEmpty();
if (this.callSite) {
super.readInjectionPoints();
return;
}
final List<AnnotationNode> slices = Annotations.<AnnotationNode>getValue(this.annotation, "slice", false);
if (slices != null && !slices.isEmpty()) {
throw this.reject("declares @Surround.slice, which only applies when @Surround.at selects a call");
}
}
@Override
protected void parseInjectionPoints(List<AnnotationNode> ats) {
if (!this.callSite) {
this.injectionPoints.add(new SurroundInjectionPoint());
return;
}
for (int i = 0, n = ats.size(); i < n; i++) {
final AnnotationNode at = ats.get(i);
final String code = Annotations.<String>getValue(at, "value");
if (!selectsCall(code)) {
throw this.reject("has @At(\"" + code + "\"), which does not select a method call");
}
if (Annotations.<At.Shift>getValue(at, "shift", At.Shift.class, At.Shift.NONE) != At.Shift.NONE || Annotations.<Integer>getValue(at, "by", Integer.valueOf(0)).intValue() != 0) {
throw this.reject("has @At(\"" + code + "\") with a non-default shift or by, which moves the match off the call");
}
}
super.parseInjectionPoints(ats);
}
private static boolean selectsCall(String atCode) {
return switch (atCode) {
case "HEAD", "RETURN", "TAIL", "CTOR_HEAD", "FIELD", "NEW", "CONSTANT", "JUMP", "INVOKE_ASSIGN", "LOAD", "STORE" -> false;
default -> true;
};
}
private InvalidInjectionException reject(String message) {
return new InvalidInjectionException(this, "@Surround " + this.getMethodName() + " " + message);
}
@Override
protected Injector parseInjector(AnnotationNode injectAnnotation) {
return new SurroundInjector(this);
}
}
@@ -0,0 +1,20 @@
package com.gtnewhorizons.angelica.experimental.surround;
import org.spongepowered.asm.lib.tree.AbstractInsnNode;
import org.spongepowered.asm.lib.tree.InsnList;
import org.spongepowered.asm.mixin.injection.InjectionPoint;
import java.util.Collection;
final class SurroundInjectionPoint extends InjectionPoint {
@Override
public boolean checkPriority(int targetPriority, int ownerPriority) {
return true;
}
@Override
public boolean find(String desc, InsnList insns, Collection<AbstractInsnNode> nodes) {
return nodes.add(insns.getFirst());
}
}
@@ -0,0 +1,378 @@
package com.gtnewhorizons.angelica.experimental.surround;
import com.gtnewhorizons.angelica.experimental.surround.SurroundBinding.Exits;
import com.gtnewhorizons.angelica.experimental.surround.SurroundBinding.Handler;
import com.gtnewhorizons.angelica.experimental.surround.SurroundBinding.Role;
import com.llamalad7.mixinextras.utils.InjectorUtils;
import org.spongepowered.asm.lib.Opcodes;
import org.spongepowered.asm.lib.Type;
import org.spongepowered.asm.lib.tree.AbstractInsnNode;
import org.spongepowered.asm.lib.tree.AnnotationNode;
import org.spongepowered.asm.lib.tree.LocalVariableAnnotationNode;
import org.spongepowered.asm.lib.tree.MethodInsnNode;
import org.spongepowered.asm.lib.tree.MethodNode;
import org.spongepowered.asm.mixin.injection.InjectionPoint;
import org.spongepowered.asm.mixin.injection.code.Injector;
import org.spongepowered.asm.mixin.injection.modify.InvalidImplicitDiscriminatorException;
import org.spongepowered.asm.mixin.injection.modify.LocalVariableDiscriminator;
import org.spongepowered.asm.mixin.injection.modify.LocalVariableDiscriminator.Context;
import org.spongepowered.asm.mixin.injection.struct.InjectionNodes.InjectionNode;
import org.spongepowered.asm.mixin.injection.struct.Target;
import org.spongepowered.asm.mixin.injection.throwables.InvalidInjectionException;
import org.spongepowered.asm.mixin.transformer.ClassInfo;
import org.spongepowered.asm.mixin.transformer.ClassInfo.Traversal;
import org.spongepowered.asm.mixin.transformer.MixinTargetContext;
import org.spongepowered.asm.util.Annotations;
import org.spongepowered.asm.util.Bytecode;
import org.spongepowered.asm.util.asm.MethodNodeEx;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.StringJoiner;
import static com.gtnewhorizons.angelica.experimental.surround.SurroundBinding.exitRef;
import static com.gtnewhorizons.angelica.experimental.surround.SurroundBinding.paramRef;
final class SurroundInjector extends Injector {
private final String id;
private final boolean callSite;
private final MixinTargetContext mixin;
private Handler entry;
private Exits exits;
SurroundInjector(SurroundInjectionInfo info) {
super(info, "@Surround");
this.id = info.getAnnotation().getValue("id", "");
this.callSite = info.callSite;
this.mixin = (MixinTargetContext) info.getMixin();
SurroundApplicatorExtension.instance().declare(this.mixin.getTarget(), this.mixin.getClassName());
}
@Override
protected void inject(Target target, InjectionNode node) {
this.checkTargetModifiers(target, true);
try {
if (this.callSite) {
this.surroundCall(target, node);
} else {
final Type result = Type.getReturnType(target.method.desc);
this.prepare(target.arguments, result, target.toString());
this.offer(target, null, target.arguments, result, target.toString());
}
} catch (SurroundRejection e) {
throw error(e.getMessage());
}
}
private void surroundCall(Target target, InjectionNode node) {
final AbstractInsnNode insn = node.getOriginalTarget();
if (!(insn instanceof MethodInsnNode call)) {
throw error("at must select a method call, not " + Bytecode.describeNode(insn, false) + " in " + target);
}
final String site = "call " + call.owner + "." + call.name + call.desc + " in " + target;
if ("<init>".equals(call.name)) {
throw error("at must select a method call, not the constructor " + site);
}
final Type[] operands = SurroundCallSite.operands(call, this.classNode.name);
final Type result = Type.getReturnType(call.desc);
this.prepare(operands, result, site);
final boolean insideNew = isInsideNew(call);
if (insideNew && this.methodNode.tryCatchBlocks != null && !this.methodNode.tryCatchBlocks.isEmpty()) {
throw error("must not catch exceptions when it surrounds a call inside a constructor call's arguments (between NEW and <init>); move the try into a method it calls");
}
if (insideNew && this.exits.handles) {
throw error("has a @Surround.Catch" + this.forId() + " that declares handle = true, but the call is inside a constructor call's arguments (between NEW and <init>), where the values below it cannot be restored");
}
this.offer(target, node, operands, result, site);
}
private void offer(Target target, InjectionNode node, Type[] operands, Type result, String site) {
final Handler[] handlers = this.exits.handlers;
final int[][] locals = new int[handlers.length][];
for (Handler exit : handlers) {
locals[exit.index] = this.resolveLocals(target, node, exit);
}
final int[] entryLocals = this.resolveLocals(target, node, this.entry);
this.info.addCallbackInvocation(this.methodNode);
final String description = "@Surround " + this.mixin.getClassName() + "::" + this.info.getMethodName() + " surrounding " + site;
SurroundApplicatorExtension.instance().offer(this.mixin.getTarget(), new CallSiteSpec(target.method, node, this.methodNode, this.methodNode.desc, operands, result, this.exits, description, entryLocals, locals));
}
private static boolean isInsideNew(MethodInsnNode call) {
int constructed = 0;
for (AbstractInsnNode insn = call.getPrevious(); insn != null; insn = insn.getPrevious()) {
if (insn.getOpcode() == Opcodes.INVOKESPECIAL && "<init>".equals(((MethodInsnNode) insn).name)) {
constructed++;
} else if (insn.getOpcode() == Opcodes.NEW) {
if (constructed == 0) {
return true;
}
constructed--;
}
}
return false;
}
private int[] resolveLocals(Target target, InjectionNode node, Handler handler) {
final int from = handler.localStart;
final int[] slots = new int[handler.carryStart - from];
for (int i = from; i < handler.carryStart; i++) {
final Type type = handler.params[i];
final LocalVariableDiscriminator discriminator = LocalVariableDiscriminator.parse(SurroundBinding.local(handler.method, i));
// MixinExtras 0.5.0 internal (pinned by UniMixins 0.3.1); recheck on bump
final Context context = InjectorUtils.getOrCreateLocalContext(target, node, this.info, type, discriminator.isArgsOnly());
String detail;
try {
slots[i - from] = discriminator.findLocal(context);
detail = slots[i - from] < 0 ? "no " + discriminator.toString(context) : null;
} catch (InvalidImplicitDiscriminatorException e) {
detail = e.getMessage();
}
if (detail != null) {
throw error(handler.ref() + " " + paramRef(handler.method, i) + " is @Surround.Local " + type.getClassName() + ", which matches no single local at the call: " + detail);
}
}
return slots;
}
private void prepare(Type[] expected, Type result, String against) {
if (this.exits == null) {
this.analyze(result);
}
this.checkArgPrefix(this.entry, expected, against);
for (Handler exit : this.exits.handlers) {
if (exit.role == Role.SKIPPED || exit.role == Role.RETURN || exit.handles) {
final Type declared = Type.getReturnType(exit.method.desc);
if (!declared.equals(result)) {
throw error(exit.ref() + " must return " + result.getClassName() + " to match what it surrounds, not " + declared.getClassName());
}
}
if (exit.role == Role.RETURN && exit.leading == 1 && (exit.params.length == 0 || !exit.params[0].equals(result))) {
throw error(exit.ref() + " must declare the " + result.getClassName() + " result as its first parameter");
}
this.checkArgPrefix(exit, expected, against);
}
if (this.exits.skipped == null && this.exits.skipSlot >= 0 && result.getSize() > 0) {
throw error("can skip a " + result.getClassName() + " result but has no @Surround.Skipped" + this.forId() + " to supply it");
}
}
private void analyze(Type result) {
if (!this.returnType.equals(Type.VOID_TYPE)) {
throw error("must return void; carry state out with @Surround.Carry locals instead");
}
if (Bytecode.hasFlag(this.methodNode, Opcodes.ACC_ABSTRACT)) {
throw error("must not be abstract");
}
final Handler entry = new Handler(this.methodNode, Role.ENTRY, 0);
if (this.hasStrippedSugar()) {
throw error("one of its parameters is annotated @Local, @Share or @Cancellable" + SurroundBinding.SUGAR_HINT);
}
this.checkNoLocal(entry);
final AbstractInsnNode soleReturn = SurroundBinding.soleReturn(this.methodNode);
final Set<String> ids = new LinkedHashSet<>();
for (MethodNode method : this.mixin.getMixin().getClassNode(0).methods) {
final AnnotationNode surround = Annotations.get(method.visibleAnnotations, SurroundSpec.SURROUND);
if (surround == null) {
continue;
}
final String declared = Annotations.<String>getValue(surround, "id", "");
if (!ids.add(declared) && declared.equals(this.id)) {
throw error("duplicate @Surround " + describeId(this.id) + "; ids must be unique per mixin");
}
}
final List<Handler> handlers = new ArrayList<>();
for (MethodNode method : this.mixin.getClassNode().methods) {
Role role = null;
AnnotationNode annotation = null;
for (Role candidate : Role.EXITS) {
final AnnotationNode found = Annotations.get(method.visibleAnnotations, candidate.annotation);
if (found == null) {
continue;
}
if (role != null) {
throw error(exitRef(method) + " can be only one of @Surround.Catch, @Surround.Finally, @Surround.Skipped and @Surround.Return");
}
role = candidate;
annotation = found;
}
if (role == null) {
continue;
}
final String exitId = Annotations.<String>getValue(annotation, "value", "");
if (!ids.contains(exitId)) {
throw error(exitRef(method) + " " + noSurroundForId(exitId, ids));
}
if (this.id.equals(exitId)) {
handlers.add(this.exitHandler(this.own(method), role, role == Role.CATCH || role == Role.RETURN && result.getSize() > 0 ? 1 : 0));
}
}
handlers.sort(Comparator.comparing(handler -> handler.role));
boolean skipped = false;
for (int i = 0, n = handlers.size(); i < n; i++) {
final Role role = handlers.get(i).role;
skipped |= role == Role.SKIPPED;
if (i > 0 && role != Role.CATCH && role == handlers.get(i - 1).role) {
throw error("declares more than one @Surround." + role.name().charAt(0) + role.name().substring(1).toLowerCase(Locale.ROOT) + this.forId() + "; there can only be one");
}
}
final Handler[] sorted = handlers.toArray(new Handler[0]);
this.sortMostSpecificFirst(sorted);
final List<LocalVariableAnnotationNode> annotations = this.methodNode.visibleLocalVariableAnnotations;
final int[] kinds = annotations == null || annotations.isEmpty() ? null : SurroundBinding.kindsAtEnd(this.methodNode, soleReturn);
final int skipSlot = SurroundBinding.skipSlot(this.methodNode, kinds);
if (skipped && skipSlot < 0) {
throw error("has a @Surround.Skipped" + this.forId() + " but declares no @Surround.Skip local, so nothing is ever skipped");
}
final Type[] carried = SurroundBinding.bind(this.methodNode, kinds, sorted);
this.entry = entry;
this.exits = new Exits(sorted, skipSlot, carried);
}
// MixinExtras strips sugar params before parse; the annotation table can then be null.
private boolean hasStrippedSugar() {
final List<AnnotationNode>[] invisible = this.methodNode.invisibleParameterAnnotations;
if (invisible != null) {
return invisible.length != this.methodArgs.length;
}
final ClassInfo mixin = this.mixin.getClassInfo();
final String desc = this.methodNode.desc;
return mixin.findMethod(MethodNodeEx.getName(this.methodNode), desc, ClassInfo.INCLUDE_ALL) == null && mixin.findMethod(this.methodNode.name, desc, ClassInfo.INCLUDE_ALL) == null;
}
@Override
protected void sanityCheck(Target target, List<InjectionPoint> injectionPoints) {
super.sanityCheck(target, injectionPoints);
if (this.isInterface) {
throw error("cannot target an interface method " + target);
}
if (target.method.name.endsWith("init>")) {
throw error("cannot target the initializer " + target);
}
if (Bytecode.hasFlag(target.method, Opcodes.ACC_ABSTRACT) || Bytecode.hasFlag(target.method, Opcodes.ACC_NATIVE)) {
throw error("cannot target the abstract or native method " + target);
}
}
private Handler exitHandler(MethodNode method, Role role, int leading) {
final Handler exit = new Handler(method, role, leading);
if (exit.handles && !this.callSite) {
throw error(exit.ref() + " declares handle = true, which only applies when @Surround.at selects a call");
}
if (Bytecode.hasFlag(method, Opcodes.ACC_ABSTRACT)) {
throw error(exit.ref() + " must not be abstract");
}
if ((role == Role.FINALLY || role == Role.CATCH && !exit.handles) && !Type.getReturnType(method.desc).equals(Type.VOID_TYPE)) {
throw error(exit.ref() + " must return void; an exit handler cannot suppress or replace anything");
}
if (Bytecode.isStatic(method) != this.isStatic) {
throw error(exit.ref() + " must be " + (this.isStatic ? "static" : "non-static") + " to match the entry handler" + this.forId());
}
if (role == Role.CATCH) {
if (exit.params.length == 0) {
throw error(exit.ref() + " must declare the Throwable it catches as its first parameter");
}
final Type caught = exit.caught();
if (caught.getSort() != Type.OBJECT || !SurroundSpec.THROWABLE.equals(caught.getInternalName()) && !extendsClass(caught, SurroundSpec.THROWABLE)) {
throw error(exit.ref() + " " + paramRef(method, 0) + " catches " + caught.getClassName() + ", which is not a java.lang.Throwable subtype");
}
}
this.checkNoLocal(exit);
return exit;
}
private void checkNoLocal(Handler handler) {
if (!this.callSite && handler.localStart < handler.carryStart) {
throw error(handler.ref() + " " + paramRef(handler.method, handler.localStart) + " is @Surround.Local, which only applies when @Surround.at selects a call");
}
}
private void checkArgPrefix(Handler handler, Type[] expected, String against) {
final Type[] params = handler.params;
if (handler.localStart - handler.leading > expected.length) {
throw error(handler.ref() + " has more parameters than " + against);
}
for (int i = handler.leading, arg = 0; i < handler.localStart; i++, arg++) {
if (params[i].equals(expected[arg]) || this.callSite && isAssignable(params[i], expected[arg])) {
continue;
}
throw error(handler.ref() + " " + paramRef(handler.method, i) + " is " + params[i].getClassName() + ", which does not match the " + expected[arg].getClassName() + " operand of " + against);
}
}
private static boolean isAssignable(Type param, Type operand) {
if (param.getSort() != Type.OBJECT || operand.getSort() != Type.OBJECT && operand.getSort() != Type.ARRAY) {
return false;
}
if (SurroundSpec.OBJECT.equals(param.getInternalName())) {
return true;
}
if (operand.getSort() != Type.OBJECT) {
return false;
}
final ClassInfo info = ClassInfo.forName(operand.getInternalName());
return info != null && info.findSuperClass(param.getInternalName(), Traversal.ALL, true) != null;
}
private void sortMostSpecificFirst(Handler[] exits) {
for (int k = 1; k < exits.length && exits[k].role == Role.CATCH; k++) {
final Handler exit = exits[k];
final Type caught = exit.caught();
int at = k;
for (int i = 0; i < k; i++) {
final Type other = exits[i].caught();
if (caught.equals(other)) {
throw error(exit.ref() + " is a second @Surround.Catch for " + caught.getClassName() + this.forId() + "; each type may be caught once");
}
if (at == k && extendsClass(caught, other.getInternalName())) {
at = i;
}
}
System.arraycopy(exits, at, exits, at + 1, k - at);
exits[at] = exit;
}
}
private static boolean extendsClass(Type type, String superName) {
final ClassInfo info = ClassInfo.forName(type.getInternalName());
return info != null && info.hasSuperClass(superName);
}
private MethodNode own(MethodNode exit) {
final MethodNode method = Bytecode.findMethod(this.classNode, exit.name, exit.desc);
if (method == null) {
throw new IllegalStateException(exit.name + exit.desc + " is missing from " + this.classNode.name);
}
if (!this.mixin.getClassName().equals(SurroundSpec.mergedBy(method))) {
throw error(exitRef(exit) + " collides with " + exit.name + exit.desc + " in " + this.classNode.name + ", which is " + SurroundSpec.describeOwner(method));
}
return method;
}
private String forId() {
return this.id.isEmpty() ? "" : " for id '" + this.id + "'";
}
private static String describeId(String id) {
return id.isEmpty() ? "the default id" : "id '" + id + "'";
}
static String noSurroundForId(String id, Collection<String> declared) {
final StringJoiner ids = new StringJoiner(", ", "; declared ids are ", "").setEmptyValue("; this mixin has no @Surround at all");
for (String other : declared) {
ids.add(describeId(other));
}
return "refers to " + describeId(id) + " but no @Surround in this mixin declares it" + ids;
}
private InvalidInjectionException error(String message) {
return new InvalidInjectionException(this.info, "@Surround " + this.info.getMethodName() + " " + message);
}
}
@@ -0,0 +1,10 @@
package com.gtnewhorizons.angelica.experimental.surround;
final class SurroundRejection extends RuntimeException {
private static final long serialVersionUID = 1L;
SurroundRejection(String message) {
super(message, null, false, false);
}
}
@@ -0,0 +1,33 @@
package com.gtnewhorizons.angelica.experimental.surround;
import org.spongepowered.asm.lib.Type;
import org.spongepowered.asm.lib.tree.AnnotationNode;
import org.spongepowered.asm.lib.tree.MethodNode;
import org.spongepowered.asm.mixin.transformer.meta.MixinMerged;
import org.spongepowered.asm.util.Annotations;
interface SurroundSpec {
String SURROUND = Type.getDescriptor(Surround.class);
String CATCH = Type.getDescriptor(Surround.Catch.class);
String FINALLY = Type.getDescriptor(Surround.Finally.class);
String CARRY = Type.getDescriptor(Surround.Carry.class);
String SKIP = Type.getDescriptor(Surround.Skip.class);
String SKIPPED = Type.getDescriptor(Surround.Skipped.class);
String RETURN = Type.getDescriptor(Surround.Return.class);
String LOCAL = Type.getDescriptor(Surround.Local.class);
String MIXIN_MERGED = Type.getDescriptor(MixinMerged.class);
String OBJECT = Type.getInternalName(Object.class);
String THROWABLE = Type.getInternalName(Throwable.class);
String THROWABLE_DESC = Type.getDescriptor(Throwable.class);
static String mergedBy(MethodNode method) {
final AnnotationNode merged = Annotations.get(method.visibleAnnotations, MIXIN_MERGED);
return merged == null ? null : Annotations.<String>getValue(merged, "mixin");
}
static String describeOwner(MethodNode method) {
final String owner = mergedBy(method);
return owner == null ? "not merged from a mixin" : "merged from " + owner;
}
}
@@ -0,0 +1,80 @@
package com.gtnewhorizons.angelica.experimental.surround;
import com.gtnewhorizons.angelica.experimental.surround.integration.CallTarget.Entity;
import com.gtnewhorizons.angelica.experimental.surround.integration.CallTarget.ReportedException;
import com.gtnewhorizons.angelica.experimental.surround.targets.Trace;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.spongepowered.asm.lib.tree.AbstractInsnNode;
import org.spongepowered.asm.lib.tree.MethodInsnNode;
import org.spongepowered.asm.lib.tree.MethodNode;
import org.spongepowered.asm.lib.tree.TryCatchBlockNode;
import java.lang.reflect.InvocationTargetException;
import java.util.List;
import static com.gtnewhorizons.angelica.experimental.surround.MixinTestBootstrap.CALL;
import static com.gtnewhorizons.angelica.experimental.surround.MixinTestBootstrap.call;
import static com.gtnewhorizons.angelica.experimental.surround.MixinTestBootstrap.method;
import static com.gtnewhorizons.angelica.experimental.surround.MixinTestBootstrap.transformedNode;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
class CallSiteTest {
@BeforeEach
void resetTrace() {
Trace.reset();
}
@Test
void aRedirectedCallInsideJavacTryCatchesRethrowsInsideTheEnclosingTry() throws Exception {
assertEquals(true, call(CALL, "render", new Entity("pig"), 1.0d, 2.0d, 3.0d, 0.5f, 0.25f, true));
final Entity creeper = new Entity("creeper");
creeper.explode = true;
final InvocationTargetException thrown = assertThrows(InvocationTargetException.class, () -> call(CALL, "render", creeper, 1.0d, 2.0d, 3.0d, 0.5f, 0.25f, false));
final ReportedException outer = (ReportedException) thrown.getCause();
assertEquals("Rendering entity (outer)", outer.getMessage());
assertEquals("Rendering entity in world", outer.getCause().getMessage());
assertSame(RuntimeException.class, outer.getCause().getCause().getClass());
assertEquals("gpu", outer.getCause().getCause().getMessage());
Trace.assertEvents("enterDoRender pig 1", "redirect pig", "doRender pig 1.0 0.25", "exitDoRender pig 1", "shadow pig", "boundingBox pig", "enterDoRender creeper 2", "redirect creeper", "doRender creeper 1.0 0.25", "caughtDoRender creeper gpu 2", "exitDoRender creeper 2");
}
@Test
void theSurroundedCallMatchesTheGoldens() {
MixinTestBootstrap.assertGolden(CALL, "valueSkip", "call-skip");
MixinTestBootstrap.assertGolden(CALL, "valueBelow", "call-handle");
}
@Test
void nestedSurroundsShareOneCallAndOrderTheirTryRangesInnerFirst() {
final MethodNode target = method(transformedNode(CALL), "innerSkipOuterReturn");
int calls = 0;
for (AbstractInsnNode insn : target.instructions.toArray()) {
calls += insn instanceof MethodInsnNode m && m.name.equals("twice") ? 1 : 0;
}
assertEquals(1, calls);
final List<TryCatchBlockNode> blocks = target.tryCatchBlocks;
boolean nests = false;
for (int i = 0; i < blocks.size(); i++) {
for (int j = 0; j < blocks.size(); j++) {
final boolean encloses = encloses(target, blocks.get(i), blocks.get(j));
nests |= encloses;
assertTrue(!encloses || i > j, "block " + i + " encloses block " + j);
}
}
assertTrue(nests, "no try block encloses another");
}
private static boolean encloses(MethodNode target, TryCatchBlockNode outer, TryCatchBlockNode inner) {
final int outerStart = target.instructions.indexOf(outer.start);
final int outerEnd = target.instructions.indexOf(outer.end);
final int innerStart = target.instructions.indexOf(inner.start);
final int innerEnd = target.instructions.indexOf(inner.end);
return outerStart <= innerStart && innerEnd <= outerEnd && (outerStart < innerStart || innerEnd < outerEnd);
}
}
@@ -0,0 +1,134 @@
package com.gtnewhorizons.angelica.experimental.surround;
import com.gtnewhorizons.angelica.experimental.surround.targets.Trace;
import com.llamalad7.mixinextras.utils.MixinInternals;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag;
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.spongepowered.asm.lib.Opcodes;
import org.spongepowered.asm.lib.Type;
import org.spongepowered.asm.lib.tree.ClassNode;
import org.spongepowered.asm.lib.tree.InsnNode;
import org.spongepowered.asm.lib.tree.MethodNode;
import org.spongepowered.asm.mixin.transformer.ext.Extensions;
import org.spongepowered.asm.mixin.transformer.ext.IExtension;
import org.spongepowered.asm.mixin.transformer.meta.MixinMerged;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;
import static com.gtnewhorizons.angelica.experimental.surround.MixinTestBootstrap.WRAP;
import static com.gtnewhorizons.angelica.experimental.surround.MixinTestBootstrap.of;
import static com.gtnewhorizons.angelica.experimental.surround.MixinTestBootstrap.scenario;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
class CompositionTest {
private static final Set<String> RUN_BEFORE = Set.of("LateInjectionApplicatorExtension", "SugarPostProcessingExtension", "WrapMethodApplicatorExtension");
private static final String CHECK_CLASS = "ExtensionCheckClass";
@BeforeEach
void resetTrace() {
Trace.reset();
}
private static Object wrap(String name, Object... args) throws Exception {
return MixinTestBootstrap.call(WRAP, name, args);
}
static Stream<Arguments> mixinExtras() {
return Stream.of(
scenario(WRAP, "the surround is outside @WrapMethod", "wrapped", of(3), of(6), "entry 3", "wrap-before 3", "body 3", "wrap-after 6", "finally"),
scenario(WRAP, "the entry sees the operand before @WrapOperation shifts it", "originalOperands", of(5), of(210), "enter 5", "wrap 5", "twice 105", "exit 5"));
}
@ParameterizedTest(name = "{0}")
@MethodSource("mixinExtras")
@Tag("mixinextras")
void mixinExtras(String method, String target, Object[] inputs, Object[] outputs, String[] events) throws Exception {
MixinTestBootstrap.assertScenario(target, method, inputs, outputs, events);
}
@Test
@Tag("mixinextras")
void aLocalFollowsTheBodyWhenAShareShiftsItsSlot() throws Exception {
assertEquals(10, wrap("shifted", 3));
Trace.assertEvents("wrap 3", "enterShifted 4 4 L4", "twice 4", "exitShifted 4 L4");
MethodNode body = null;
for (MethodNode method : MixinTestBootstrap.transformedNode(WRAP).methods) {
body = method.name.startsWith("shifted$") ? method : body;
}
assertTrue(body != null && body.desc.startsWith("(IL"), "the body did not move and gain the share");
}
@Test
@Tag("mixinextras")
void ourExtensionRunsAfterMixinExtrasAndBeforeCheckClass() throws Exception {
MixinTestBootstrap.transformer();
final SurroundApplicatorExtension ours = SurroundApplicatorExtension.instance();
MixinTestBootstrap.transform(WRAP);
final Extensions extensions = MixinInternals.getExtensions();
for (List<IExtension> list : List.of(extensions.getExtensions(), extensions.getActiveExtensions())) {
final int at = assertSlot(list, ours);
for (String name : RUN_BEFORE) {
final int index = names(list).indexOf(name);
assertTrue(index >= 0 && index < at, names(list).toString());
assertTrue(list.get(index).getClass().getName().startsWith("com.llamalad7.mixinextras."));
}
}
assertCheckClassActiveUnderVerify(extensions);
}
@Test
void anUnrenamedSurroundHandlerIsFatal() {
final String mixin = "com.example.mixin.MixinLate";
final MethodNode method = new MethodNode(Opcodes.ACC_PRIVATE, "surround$enter", "()V", null, null);
method.instructions.add(new InsnNode(Opcodes.RETURN));
method.visitAnnotation(Type.getDescriptor(Surround.class), true).visitEnd();
method.visitAnnotation(Type.getDescriptor(MixinMerged.class), true).visit("mixin", mixin);
final ClassNode target = new ClassNode();
target.version = Opcodes.V1_8;
target.access = Opcodes.ACC_PUBLIC;
target.name = "com/example/Target";
target.superName = "java/lang/Object";
target.methods.add(method);
final String message = assertThrows(IllegalStateException.class, () -> SurroundApplicatorExtension.checkHandlersRenamed(target, Set.of())).getMessage();
assertTrue(message.startsWith("surround$enter()V in com/example/Target (merged from " + mixin + ") carries @Surround but was never parsed as an injector"), message);
assertTrue(message.contains("SurroundBootstrap.init() must run before"), message);
}
private static void assertCheckClassActiveUnderVerify(Extensions extensions) {
final List<String> active = names(extensions.getActiveExtensions());
assertEquals(Boolean.getBoolean("mixin.debug.verify"), active.contains(CHECK_CLASS), active.toString());
}
private static List<String> names(List<IExtension> list) {
final List<String> names = new ArrayList<>(list.size());
for (IExtension extension : list) {
names.add(extension.getClass().getSimpleName());
}
return names;
}
private static int assertSlot(List<IExtension> list, IExtension ours) {
final List<String> names = names(list);
assertEquals(list.indexOf(ours), list.lastIndexOf(ours), "registered twice: " + names);
final int at = list.indexOf(ours);
assertTrue(at >= 0, "missing: " + names);
for (String after : names.subList(at + 1, names.size())) {
assertFalse(RUN_BEFORE.contains(after), after + " runs after us: " + names);
}
final int check = names.indexOf(CHECK_CLASS);
assertTrue(check < 0 || check > at, CHECK_CLASS + " runs before us: " + names);
return at;
}
}
@@ -0,0 +1,36 @@
package com.gtnewhorizons.angelica.experimental.surround;
import makamys.mixingasm.api.MixinSafeTransformer;
import net.minecraft.launchwrapper.IClassTransformer;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.commons.LocalVariablesSorter;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@MixinSafeTransformer
public final class LocalSortingTransformer implements IClassTransformer {
public static final Set<String> SEEN = ConcurrentHashMap.newKeySet();
@Override
public byte[] transform(String name, String transformedName, byte[] bytes) {
SEEN.add(transformedName);
if (bytes == null || ((bytes[6] & 0xFF) << 8 | bytes[7] & 0xFF) > Opcodes.V1_8) {
return bytes;
}
final ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
new ClassReader(bytes).accept(new ClassVisitor(Opcodes.ASM5, writer) {
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
return new LocalVariablesSorter(Opcodes.ASM5, access, desc, super.visitMethod(access, name, desc, signature, exceptions)) {
};
}
}, ClassReader.EXPAND_FRAMES);
return writer.toByteArray();
}
}
@@ -0,0 +1,277 @@
package com.gtnewhorizons.angelica.experimental.surround;
import com.gtnewhorizons.angelica.experimental.surround.targets.Trace;
import com.llamalad7.mixinextras.MixinExtrasBootstrap;
import net.minecraft.launchwrapper.Launch;
import net.minecraft.launchwrapper.LaunchClassLoader;
import org.junit.jupiter.api.Named;
import org.junit.jupiter.params.provider.Arguments;
import org.spongepowered.asm.launch.MixinBootstrap;
import org.spongepowered.asm.lib.ClassReader;
import org.spongepowered.asm.lib.tree.ClassNode;
import org.spongepowered.asm.lib.tree.MethodNode;
import org.spongepowered.asm.lib.util.Textifier;
import org.spongepowered.asm.lib.util.TraceClassVisitor;
import org.spongepowered.asm.mixin.EnvironmentStateTweaker;
import org.spongepowered.asm.mixin.MixinEnvironment;
import org.spongepowered.asm.mixin.Mixins;
import org.spongepowered.asm.mixin.transformer.IMixinTransformer;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.MalformedURLException;
import java.net.URL;
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.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.fail;
public final class MixinTestBootstrap {
private MixinTestBootstrap() {
}
private static final class Holder {
static final IMixinTransformer TRANSFORMER = boot();
}
public static IMixinTransformer transformer() {
return Holder.TRANSFORMER;
}
private static final Map<String, byte[]> TRANSFORMED = new HashMap<>();
private static final Map<String, Class<?>> DEFINED = new HashMap<>();
public static byte[] transformedBytes(String binaryName) {
return TRANSFORMED.computeIfAbsent(binaryName, name -> transformer().transformClassBytes(name, name, read(name)));
}
public static Class<?> transform(String binaryName) {
return DEFINED.computeIfAbsent(binaryName, name -> define(name, transformedBytes(name)));
}
public static Object call(String target, String name, Object... args) throws Exception {
final Class<?> owner = transform(target);
for (Method method : owner.getMethods()) {
if (method.getName().equals(name) && method.getParameterCount() == args.length) {
final boolean isStatic = Modifier.isStatic(method.getModifiers());
return method.invoke(isStatic ? null : owner.getDeclaredConstructor().newInstance(), args);
}
}
throw new AssertionError("no " + name + " in " + target);
}
static final String WHOLE = "com.gtnewhorizons.angelica.experimental.surround.integration.WholeTarget";
static final String CALL = "com.gtnewhorizons.angelica.experimental.surround.integration.CallTarget";
static final String WRAP = "com.gtnewhorizons.angelica.experimental.surround.integration.WrapTarget";
static ClassNode transformedNode(String target) {
return parse(transformedBytes(target));
}
static ClassNode originalNode(String binaryName) {
return parse(read(binaryName));
}
private static ClassNode parse(byte[] bytes) {
final ClassNode node = new ClassNode();
new ClassReader(bytes).accept(node, 0);
return node;
}
static MethodNode method(ClassNode node, String name) {
for (MethodNode method : node.methods) {
if (method.name.equals(name)) {
return method;
}
}
throw new AssertionError("no method " + name + " in " + node.name);
}
static void assertGolden(String target, String method, String golden) {
final ClassNode full = transformedNode(target);
final ClassNode slim = new ClassNode();
slim.visit(full.version, full.access, full.name, null, full.superName, null);
slim.methods.add(method(full, method));
for (MethodNode candidate : full.methods) {
if (candidate.name.startsWith(method + "$surround")) {
slim.methods.add(candidate);
}
}
final StringWriter out = new StringWriter();
slim.accept(new TraceClassVisitor(null, new Textifier(), new PrintWriter(out)));
final String stripped = out.toString().replaceAll("(?m)^ *LINENUMBER \\d+ L\\d+\\r?\\n", "");
final String actual = normalize(stripped.replaceAll("\\b([a-z]+)\\$[a-z]{3,}[0-9a-f]{3,}\\$", "$1\\$\\$").replaceAll("\\bmd[0-9a-f]+\\$jvmdowngrader\\$", "md\\$jvmdowngrader\\$"));
final Path file = Paths.get(System.getProperty("surround.golden.dir")).resolve(golden + ".asm");
try {
if (!Files.exists(file)) {
Files.write(file, actual.getBytes(StandardCharsets.UTF_8));
fail("Created golden " + file + ". Inspect it, then re-run.");
}
assertEquals(normalize(new String(Files.readAllBytes(file), StandardCharsets.UTF_8)), actual, "output changed; inspect the diff before updating " + file);
} catch (IOException e) {
throw new AssertionError(e);
}
}
private static String normalize(String text) {
final StringBuilder result = new StringBuilder();
for (String line : text.replace("\r\n", "\n").split("\n", -1)) {
result.append(line.replaceAll("\\s+$", "").replaceAll("(\\$jvmdowngrader\\$concat\\$[\\w$]*?\\$\\d+)\\$[0-9a-f]+\\b", "$1")).append('\n');
}
return result.toString().trim() + "\n";
}
static final Object VOID = null;
static Arguments scenario(String target, String name, String method, Object[] inputs, Object[] outputs, String... events) {
return Arguments.of(Named.of(name, method), target, inputs, outputs, events);
}
static Object[] of(Object... values) {
return values;
}
static List<Object> args(Object... values) {
return Arrays.asList(values);
}
static void assertScenario(String target, String method, Object[] inputs, Object[] outputs, String[] events) throws Exception {
Trace.reset();
for (int i = 0; i < inputs.length; i++) {
final Object[] args = inputs[i] instanceof List<?> list ? list.toArray() : new Object[]{inputs[i]};
if (outputs[i] instanceof Class<?> type && Throwable.class.isAssignableFrom(type)) {
final InvocationTargetException thrown = assertThrows(InvocationTargetException.class, () -> call(target, method, args));
assertEquals(outputs[i], thrown.getCause().getClass());
} else {
assertEquals(outputs[i], call(target, method, args));
}
}
Trace.assertEvents(events);
}
private static final boolean MIXINEXTRAS = Boolean.getBoolean("surround.test.mixinextras");
private static final boolean ADDITIONS_FIRST = Boolean.getBoolean("surround.test.additionsFirst");
private static final boolean OBF = Boolean.getBoolean("surround.test.obf");
private static IMixinTransformer boot() {
Launch.blackboard = new HashMap<>();
Launch.classLoader = new LaunchClassLoader(classpath());
if (OBF) {
Launch.classLoader.addTransformerExclusion(LocalSortingTransformer.class.getName());
Launch.classLoader.registerTransformer(LocalSortingTransformer.class.getName());
if (Launch.classLoader.getTransformers().stream().noneMatch(t -> t.getClass().getName().equals(LocalSortingTransformer.class.getName()))) {
throw new IllegalStateException("LocalSortingTransformer was not registered");
}
}
if (!MIXINEXTRAS) {
SurroundBootstrap.init();
}
MixinBootstrap.init();
if (MIXINEXTRAS) {
if (ADDITIONS_FIRST) {
SurroundBootstrap.init();
}
MixinExtrasBootstrap.init();
if (!ADDITIONS_FIRST) {
SurroundBootstrap.init();
}
}
Mixins.addConfiguration("mixins.surround.test.json");
if (!OBF) {
Mixins.addConfiguration(RejectionCases.compileConfig(MIXINEXTRAS));
}
if (MIXINEXTRAS) {
Mixins.addConfiguration("mixins.surround.mixinextras.test.json");
}
final EnvironmentStateTweaker tweaker = new EnvironmentStateTweaker();
tweaker.injectIntoClassLoader(Launch.classLoader);
tweaker.getLaunchArguments();
return active();
}
private static IMixinTransformer active() {
final Object transformer = MixinEnvironment.getCurrentEnvironment().getActiveTransformer();
if (!(transformer instanceof IMixinTransformer mixinTransformer)) {
throw new IllegalStateException("no active mixin transformer, got " + transformer);
}
return mixinTransformer;
}
private static URL[] classpath() {
final List<URL> urls = new ArrayList<>();
for (String entry : System.getProperty("java.class.path").split(File.pathSeparator)) {
try {
urls.add(new File(entry).toURI().toURL());
} catch (MalformedURLException e) {
throw new IllegalStateException(entry, e);
}
}
return urls.toArray(new URL[urls.size()]);
}
static byte[] read(String binaryName) {
try {
final byte[] bytes = Launch.classLoader.getClassBytes(binaryName);
if (bytes == null) {
throw new IllegalStateException("missing " + binaryName);
}
return bytes;
} catch (IOException e) {
throw new IllegalStateException(binaryName, e);
}
}
private static Class<?> define(String name, byte[] bytes) {
try {
return new DefiningClassLoader(name, bytes).loadClass(name);
} catch (ClassNotFoundException e) {
throw new AssertionError(e);
}
}
private static final class DefiningClassLoader extends ClassLoader {
private final String name;
private final byte[] bytes;
DefiningClassLoader(String name, byte[] bytes) {
super(MixinTestBootstrap.class.getClassLoader());
this.name = name;
this.bytes = bytes;
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
if (!this.name.equals(name)) {
return super.loadClass(name, resolve);
}
Class<?> loaded = findLoadedClass(name);
if (loaded == null) {
loaded = defineClass(name, this.bytes, 0, this.bytes.length);
}
if (resolve) {
resolveClass(loaded);
}
return loaded;
}
}
}
@@ -0,0 +1,182 @@
package com.gtnewhorizons.angelica.experimental.surround;
import net.minecraft.launchwrapper.Launch;
import java.io.IOException;
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.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertTrue;
final class RejectionCases {
static final String PACKAGE = "rejection";
static final String CONFIG = "mixins.surround.rejection.test.json";
static final String ABS = "at = @At(value = \"INVOKE\", target = \"Ljava/lang/Math;abs(I)I\")";
static final String WHOLE_ENTRY = "@Surround(method = \"run\") private void invalid$enter(int a) {}";
static final String FINALLY = " @Surround.Finally private void invalid$always() {}";
static final String RUN = cls("public int run(int a) { return Math.abs(a); }");
static final String IN_NEW = cls("public int run(int a) { return new StringBuilder(Math.abs(a)).length(); }");
static final Case[] CASES = {
new Case("Initializer", "@Surround invalid$enter cannot target the initializer", RUN, mixin("@Surround(method = \"<init>()V\") private void invalid$enter() {}")),
new Case("Staticness", "must be non-static to match the entry handler", RUN, mixin(WHOLE_ENTRY + " @Surround.Finally private static void invalid$always() {}")),
new Case("CarriedThrowable", "cannot mark its caught Throwable parameter @Surround.Carry", RUN, mixin("@Surround(method = \"run\") private void invalid$enter(int a) {" + " @Surround.Carry Throwable state = null; }" + " @Surround.Catch private void invalid$caught(@Surround.Carry Throwable e) {}")),
new Case("LocalCall", "is @Surround.Local java.lang.String, which matches no single local at the call", RUN, mixin("@Surround(method = \"run\", " + ABS + ") private void invalid$enter(int a, @Surround.Local String s) {}")),
new Case("HandleInsideNew", "declares handle = true, but the call is inside a constructor call's arguments", IN_NEW, mixin("@Surround(method = \"run\", " + ABS + ") private void invalid$enter(int a) {}" + " @Surround.Catch(handle = true) private int invalid$caught(Throwable error) { return 0; }")),
new Case("SkippedRequired", "can skip a int result but has no @Surround.Skipped", RUN, mixin("@Surround(method = \"run\", " + ABS + ") private void invalid$enter(int a) {" + " @Surround.Skip boolean skip = a > 0; }")),
new Case("Shift", "@Surround invalid$enter has @At(\"INVOKE\") with a non-default shift or by, which moves the match off the call", RUN, mixin("@Surround(method = \"run\", at = @At(value = \"INVOKE\", target = \"Ljava/lang/Math;abs(I)I\", shift = At.Shift.AFTER)) private void invalid$enter() {}")),
new Case("AbstractTarget", "cannot target the abstract or native method", "public abstract class $T { public abstract int run(int a); }", "@Mixin(value = $T.class, remap = false) public abstract class $M {" + " @Surround(method = \"run\") private void invalid$enter(int a) {} }"),
new Case("InitAt", "at must select a method call, not the constructor call java/lang/StringBuilder.<init>()V", cls("public Object run(int a) { return new StringBuilder(); }"), mixin("@Surround(method = \"run\", at = @At(value = \"INVOKE\", target = \"Ljava/lang/StringBuilder;<init>()V\")) private void invalid$enter() {}")),
new Case("CatchingEntry", "must not catch exceptions when it surrounds a call inside a constructor call's arguments", IN_NEW, mixin("@Surround(method = \"run\", " + ABS + ") private void invalid$enter(int a) {" + " try { Math.abs(a); } catch (RuntimeException e) { Math.abs(0); } }" + FINALLY)),
new Case("TooManyOperands", "entry handler has more parameters than call java/lang/Math.abs(I)I", RUN, mixin("@Surround(method = \"run\", " + ABS + ") private void invalid$enter(int a, int b) {}")),
new Case("SkipNotAssigned", "has a @Surround.Skip local in slot 2 that is not definitely assigned", cls("public void run(int a) {}"), mixin("@Surround(method = \"run\") private void invalid$enter(int a) {" + " @Surround.Skip boolean skip; if (a > 0) { skip = true; String.valueOf(a); } }")),
new Case("Collision", "exit handler 'surround$exit' collides with surround$exit()V in " + PACKAGE + "/Collision, which is merged from " + PACKAGE + ".mixin.CollisionMixin2", RUN, "@Mixin(value = $T.class, priority = 900, remap = false) public class $M {" + " @Surround(method = \"run\") private void surround$enter(int a) {}" + " @Surround.Finally private void surround$exit() {} }", "@Mixin(value = $T.class, priority = 1100, remap = false) public class $M {" + " private void surround$exit() {} }"),
new Case("MultipleRoles", "can be only one of @Surround.Catch, @Surround.Finally, @Surround.Skipped and @Surround.Return", RUN, mixin(WHOLE_ENTRY + " @Surround.Catch @Surround.Finally private void invalid$exit(Throwable e) {}")),
new Case("DuplicateId", "duplicate @Surround the default id; ids must be unique per mixin", RUN, mixin(WHOLE_ENTRY + " @Surround(method = \"run\") private void invalid$enter2(int a) {}")),
new Case("OrphanExit", "no @Surround in this mixin declares it", RUN, mixin(WHOLE_ENTRY + " @Surround.Finally(\"typo\") private void invalid$always() {}")),
new Case("CarryOnEntryParameter", "only the entry handler's own locals can be carried", RUN, mixin("@Surround(method = \"run\") private void invalid$enter(@Surround.Carry int a) {}")),
new Case("CarryIdNotDeclared", "asks for the carried local 'frist'", RUN, mixin("@Surround(method = \"run\") private void invalid$enter(int a) { @Surround.Carry(\"first\") int first = 1; }" + " @Surround.Finally private void invalid$always(@Surround.Carry(\"frist\") int typo) {}")),
new Case("NoCarryOfKind", "matches no carried local of that kind", RUN, mixin("@Surround(method = \"run\") private void invalid$enter(int a) { @Surround.Carry int count = 1; }" + " @Surround.Finally private void invalid$always(@Surround.Carry float missing) {}")),
new Case("AmbiguousCarry", "matches 2 carried locals of that kind", RUN, mixin("@Surround(method = \"run\") private void invalid$enter(int a) { @Surround.Carry int first = 1; @Surround.Carry int second = 2; }" + " @Surround.Finally private void invalid$always(@Surround.Carry int either) {}")),
new Case("CarryReadAsDifferentType", "but another exit handler reads it as java.lang.String", RUN, mixin("@Surround(method = \"run\") private void invalid$enter(int a) { @Surround.Carry Object value = null; }" + " @Surround.Catch private void invalid$caught(java.io.IOException e, @Surround.Carry String value) {}" + " @Surround.Finally private void invalid$always(@Surround.Carry Integer value) {}")),
new Case("AmbiguousWideLocal", "shares its scope with another", RUN, mixin("@Surround(method = \"run\") private void invalid$enter(int a) { @Surround.Carry long first; long second; try { first = a; second = a + 1L; } catch (RuntimeException e) { throw e; } }")),
new Case("DuplicateCarryId", "declares two @Surround.Carry locals named 'x'", RUN, mixin("@Surround(method = \"run\") private void invalid$enter(int a) { @Surround.Carry(\"x\") int first = 1; @Surround.Carry(\"x\") long second = 2L; }")),
new Case("CarryInsideType", "@Surround.Carry must annotate the local itself, not part of its type", RUN, mixin("@Surround(method = \"run\") private void invalid$enter(int a) { java.util.List<@Surround.Carry String> l = null; }")),
new Case("DuplicateSkip", "declares two @Surround.Skip locals", RUN, mixin("@Surround(method = \"run\") private void invalid$enter(int a) { @Surround.Skip boolean first = a > 0; @Surround.Skip boolean second = a < 0; }")),
new Case("SkipNotIntLike", "it must be boolean or int-like", RUN, mixin("@Surround(method = \"run\") private void invalid$enter(int a) { @Surround.Skip long skip = a; }")),
new Case("SkippedWithoutSkip", "but declares no @Surround.Skip local, so nothing is ever skipped", RUN, mixin(WHOLE_ENTRY + " @Surround.Skipped private int invalid$skipped(int a) { return 0; }")),
new Case("CarryScopedEarly", "goes out of scope before the entry handler ends", RUN, mixin("@Surround(method = \"run\") private void invalid$enter(int a) {" + " { @Surround.Carry int scoped = a; String.valueOf(scoped); } int reused = a + 1; String.valueOf(reused); }" + " @Surround.Finally private void invalid$always(@Surround.Carry int value) {}")),
new Case("EarlyReturn", "every path falls through to its end", RUN, mixin("@Surround(method = \"run\") private void invalid$enter(int a) {" + " if (a > 0) { return; } String.valueOf(a); }" + FINALLY)),
new Case("PartialAfterInt", "is not definitely assigned when the entry handler ends", RUN, mixin("@Surround(method = \"run\") private void invalid$enter(int a) {" + " { int tmp = a + 5; String.valueOf(tmp); }" + " @Surround.Carry int flag; if (a > 0) { flag = 1; String.valueOf(flag); } }" + " @Surround.Finally private void invalid$always(@Surround.Carry int flag) {}")),
new Case("AssignsParameter", "must not assign its parameters", RUN, mixin("@Surround(method = \"run\") private void invalid$enter(int a) { a = a + 1; }" + FINALLY)),
new Case("LocalOnWholeMethod", "is @Surround.Local, which only applies when @Surround.at selects a call", RUN, mixin(WHOLE_ENTRY + " @Surround.Finally private void invalid$always(@Surround.Local int b) {}")),
new Case("IncrementsParameter", "must not assign its parameters", RUN, mixin("@Surround(method = \"run\") private void invalid$enter(int a) { a++; }" + FINALLY)),
new Case("NonCallAt", "@Surround invalid$enter has @At(\"HEAD\"), which does not select a method call", RUN, mixin("@Surround(method = \"run\", at = @At(\"HEAD\")) private void invalid$enter() {}")),
new Case("CancellableExit", "parameter 0 ('ci') of 'invalid$always' is annotated @Cancellable, which a @Surround handler cannot resolve", RUN, mixin(WHOLE_ENTRY + " @Surround.Finally private void invalid$always(@Cancellable Object ci) {}")),
new Case("DuplicateFinally", "declares more than one @Surround.Finally", RUN, mixin(WHOLE_ENTRY + FINALLY + " @Surround.Finally private void invalid$always2() {}")),
new Case("ReturnWrongType", "must return java.lang.String to match what it surrounds, not int", cls("public String run(String s) { return s.trim(); }"), mixin("@Surround(method = \"run\", at = @At(value = \"INVOKE\", target = \"Ljava/lang/String;trim()Ljava/lang/String;\")) private void invalid$enter(String s) {}" + " @Surround.Return private int invalid$ret(int result) { return result; }")),
new Case("ReturnResultNotFirst", "must declare the int result as its first parameter", RUN, mixin(WHOLE_ENTRY + " @Surround.Return private int invalid$ret(String wrong) { return 0; }")),
new Case("ReturnCarriedResult", "cannot mark its result parameter @Surround.Carry", RUN, mixin(WHOLE_ENTRY + " @Surround.Return private int invalid$ret(@Surround.Carry int result) { return result; }")),
new Case("NonVoidFinally", "must return void; an exit handler cannot suppress or replace anything", RUN, mixin(WHOLE_ENTRY + " @Surround.Finally private int invalid$always() { return 1; }")),
new Case("NonVoidEntry", "must return void; carry state out with @Surround.Carry locals instead", RUN, mixin("@Surround(method = \"run\") private long invalid$enter(int a) { return 0L; }")),
new Case("CatchNotThrowable", "parameter 0 ('notAnError') catches java.lang.String, which is not a java.lang.Throwable subtype", RUN, mixin(WHOLE_ENTRY + " @Surround.Catch private void invalid$caught(String notAnError) {}")),
new Case("DuplicateCatch", "is a second @Surround.Catch for java.io.IOException", RUN, mixin(WHOLE_ENTRY + " @Surround.Catch private void invalid$first(java.io.IOException e) {} @Surround.Catch private void invalid$second(java.io.IOException e) {}")),
new Case("CatchWithoutParameters", "must declare the Throwable it catches as its first parameter", RUN, mixin(WHOLE_ENTRY + " @Surround.Catch private void invalid$caught() {}")),
new Case("AbstractEntry", "must not be abstract", RUN, "@Mixin(value = $T.class, remap = false) public abstract class $M { @Surround(method = \"run\") protected abstract void invalid$enter(int a); }"),
new Case("AbstractCatch", "must not be abstract", RUN, "@Mixin(value = $T.class, remap = false) public abstract class $M { " + WHOLE_ENTRY + " @Surround.Catch protected abstract void invalid$caught(Throwable t); }"),
new Case("MisorderedLocalAfterCarry", "parameter 1 ('y') is out of order; declare operands first, then @Surround.Local, then @Surround.Carry parameters", RUN, mixin("@Surround(method = \"run\", " + ABS + ") private void invalid$enter(int a) {}" + " @Surround.Finally private void invalid$always(@Surround.Carry int x, @Surround.Local int y) {}")),
new Case("LocalAndCarry", "parameter 0 ('a') cannot be both @Surround.Local and @Surround.Carry", RUN, mixin("@Surround(method = \"run\", " + ABS + ") private void invalid$enter(int a) {}" + " @Surround.Finally private void invalid$always(@Surround.Local @Surround.Carry int a) {}")),
new Case("LocalCaught", "cannot mark its caught Throwable parameter @Surround.Local", RUN, mixin("@Surround(method = \"run\", " + ABS + ") private void invalid$enter(int a) {}" + " @Surround.Catch private void invalid$caught(@Surround.Local Throwable e) {}")),
new Case("HandleWrongReturn", "must return java.lang.String to match what it surrounds, not int", cls("public String run(String s) { return s.trim(); }"), mixin("@Surround(method = \"run\", at = @At(value = \"INVOKE\", target = \"Ljava/lang/String;trim()Ljava/lang/String;\")) private void invalid$enter(String s) {}" + " @Surround.Catch(handle = true) private int invalid$caught(Throwable e, String s) { return 0; }")),
new Case("HandleWithoutAt", "declares handle = true, which only applies when @Surround.at selects a call", RUN, mixin(WHOLE_ENTRY + " @Surround.Catch(handle = true) private void invalid$caught(Throwable e) {}")),
new Case("SliceWithoutAt", "declares @Surround.slice, which only applies when @Surround.at selects a call", RUN, mixin("@Surround(method = \"run\", slice = @Slice(from = @At(\"HEAD\"))) private void invalid$enter(int a) {}")),
new Case("EntryOperandMismatch", "parameter 0 ('notAnInt') is java.lang.String, which does not match the int operand of call java/lang/Math.abs(I)I", RUN, mixin("@Surround(method = \"run\", " + ABS + ") private void invalid$enter(String notAnInt) {}")),
new Case("OrphanWithoutSurround", "@Surround exit handler 'invalid$always' refers to the default id but no @Surround in this mixin declares it; this mixin has no @Surround at all", RUN, mixin(WHOLE_ENTRY), mixin("@Surround.Finally private void invalid$always() {}"))
};
static final Case SUGAR = new Case("Sugar", "@Surround invalid$enter one of its parameters is annotated @Local, @Share or @Cancellable, which a @Surround handler cannot resolve", cls("public int run(int a) { int local = a + 1; return Math.abs(local); }"), mixin("@Surround(method = \"run\", " + ABS + ") private void invalid$enter(int local, @Local(ordinal = 0) int a) {}"));
static final Case REQUIRE_ZERO_MISS = new Case("RequireZeroMiss", "", RUN, mixin("@Surround(method = \"run\", require = 0, at = @At(value = \"INVOKE\", target = \"Ljava/lang/Math;max(II)I\")) private void invalid$enter(int a, int b) {}" + FINALLY));
static String compileConfig(boolean mixinExtras) {
final List<Case> cases = new ArrayList<>();
if (mixinExtras) {
cases.add(SUGAR);
} else {
cases.addAll(Arrays.asList(CASES));
cases.add(REQUIRE_ZERO_MISS);
}
final List<Unit> units = new ArrayList<>();
final List<String> mixins = new ArrayList<>();
for (Case c : cases) {
units.add(new Unit(PACKAGE + "." + c.name, "package " + PACKAGE + "; " + c.target.replace("$T", c.name)));
for (int i = 0; i < c.mixins.length; i++) {
final String name = c.name + "Mixin" + (i + 1);
mixins.add('"' + name + '"');
units.add(new Unit(PACKAGE + ".mixin." + name, "package " + PACKAGE + ".mixin; import " + PACKAGE + ".*;" + " import com.gtnewhorizons.angelica.experimental.surround.Surround;" + " import com.llamalad7.mixinextras.sugar.Cancellable;" + " import com.llamalad7.mixinextras.sugar.Local;" + " import org.spongepowered.asm.mixin.Mixin;" + " import org.spongepowered.asm.mixin.injection.At;" + " import org.spongepowered.asm.mixin.injection.Slice;" + c.mixins[i].replace("$T", c.name).replace("$M", name)));
}
}
try {
final Path dir = Paths.get("rejection-classes").toAbsolutePath();
if (Files.exists(dir)) {
try (Stream<Path> stale = Files.walk(dir)) {
stale.sorted(Comparator.reverseOrder()).forEach(path -> path.toFile().delete());
}
}
final Path sources = Files.createDirectories(dir.resolve("src"));
final List<String> command = new ArrayList<>(Arrays.asList(System.getProperty("surround.javac"), "-classpath", System.getProperty("java.class.path"), "-d", dir.toString(), "-proc:none", "-g", "--release", "8", "-Xlint:-options"));
for (Unit unit : units) {
final Path file = sources.resolve(unit.className().replace('.', '/') + ".java");
Files.createDirectories(file.getParent());
Files.write(file, unit.source().getBytes(StandardCharsets.UTF_8));
command.add(file.toString());
}
final Process javac = new ProcessBuilder(command).redirectErrorStream(true).start();
final String output = new String(javac.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
if (javac.waitFor() != 0) {
throw new IllegalStateException(output);
}
Files.write(dir.resolve(CONFIG), ("{\"required\": true, \"minVersion\": \"0.8\", \"package\": \"" + PACKAGE + ".mixin\"," + " \"compatibilityLevel\": \"JAVA_8\", \"mixins\": " + mixins + "}").getBytes(StandardCharsets.UTF_8));
Launch.classLoader.addURL(dir.toUri().toURL());
} catch (IOException | InterruptedException e) {
throw new IllegalStateException(e);
}
return CONFIG;
}
private static String cls(String members) {
return "public class $T { " + members + " }";
}
private static String mixin(String members) {
return "@Mixin(value = $T.class, remap = false) public class $M { " + members + " }";
}
private record Unit(String className, String source) {
}
static final class Case {
final String name;
final String expected;
final String target;
final String[] mixins;
Case(String name, String expected, String target, String... mixins) {
this.name = name;
this.expected = expected;
this.target = target;
this.mixins = mixins;
}
void assertRejected() {
final String messages = causeChain(transformExpectingFailure(PACKAGE + "." + this.name));
assertTrue(messages.contains(this.expected), messages);
}
private static Throwable transformExpectingFailure(String binaryName) {
try {
MixinTestBootstrap.transformer().transformClassBytes(binaryName, binaryName, MixinTestBootstrap.read(binaryName));
} catch (Throwable t) {
return t;
}
throw new AssertionError(binaryName + " transformed without the expected failure");
}
private static String causeChain(Throwable thrown) {
final StringBuilder all = new StringBuilder();
for (Throwable t = thrown; t != null; t = t.getCause()) {
all.append(t).append('\n');
}
return all.toString();
}
}
}
@@ -0,0 +1,36 @@
package com.gtnewhorizons.angelica.experimental.surround;
import org.junit.jupiter.api.Named;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import java.util.Arrays;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
class RejectionTest {
static Stream<Named<RejectionCases.Case>> rejections() {
return Arrays.stream(RejectionCases.CASES).map(c -> Named.of(c.name, c));
}
@ParameterizedTest(name = "{0}")
@MethodSource("rejections")
void anInvalidMixinIsRejectedAtLoad(RejectionCases.Case rejection) {
rejection.assertRejected();
}
@Test
void aRequireZeroCallSurroundThatMatchesNoCallIsNotRejected() {
assertDoesNotThrow(() -> MixinTestBootstrap.transform(RejectionCases.PACKAGE + "." + RejectionCases.REQUIRE_ZERO_MISS.name));
}
@Test
@Tag("mixinextras")
void mixinExtrasSugarOnACallSiteEntryHandlerIsRejected() {
RejectionCases.SUGAR.assertRejected();
}
}
@@ -0,0 +1,75 @@
package com.gtnewhorizons.angelica.experimental.surround;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.util.stream.Stream;
import static com.gtnewhorizons.angelica.experimental.surround.MixinTestBootstrap.CALL;
import static com.gtnewhorizons.angelica.experimental.surround.MixinTestBootstrap.VOID;
import static com.gtnewhorizons.angelica.experimental.surround.MixinTestBootstrap.WHOLE;
import static com.gtnewhorizons.angelica.experimental.surround.MixinTestBootstrap.args;
import static com.gtnewhorizons.angelica.experimental.surround.MixinTestBootstrap.of;
import static com.gtnewhorizons.angelica.experimental.surround.MixinTestBootstrap.scenario;
class ScenarioTest {
static Stream<Arguments> scenarios() {
return Stream.of(
scenario(WHOLE, "a surround encloses the body and every other injector", "compute", of(7), of(14), "enter 7 1", "head 7", "body 7", "tail 14", "exit 1"),
scenario(WHOLE, "the narrowest catch handler wins and finally runs when the body throws", "boom", of(args()), of(IllegalStateException.class), "enterBoom 1", "boomHead", "boom", "caught 1 boom", "exitBoom 1"),
scenario(WHOLE, "an instance monitor covers the handlers and the body", "lockedInstance", of(7), of(14), "enterInstance true", "body true", "exitInstance true"),
scenario(WHOLE, "a unique exit handler renamed on collision still binds", "run", of(args()), of(VOID), "enter", "run", "target exit", "handler exit"),
scenario(WHOLE, "a lower priority mixin is the inner surround", "priority", of(5), of(60), "enterHigh", "enterLow", "body 5", "returnLow 5", "exitLow", "returnHigh 6", "exitHigh"),
scenario(WHOLE, "static wide arguments", "wideBody", of(args(2.5d, 5L)), of(7.5d), "enter 1", "body", "exit 1"),
scenario(WHOLE, "a throwing entry handler skips the body and the exit handler", "throwingEntry", of(5), of(IllegalArgumentException.class), "enterThrows 5"),
scenario(WHOLE, "a throwing finally replaces the return value", "throwingFinally", of(args(9, 4)), of(UnsupportedOperationException.class), "enter 1", "gt", "exitThrows 1"),
scenario(WHOLE, "pre-existing exception handlers still fire", "caught", of(4, -4), of(4, -1), "enter 1", "inner-finally", "exit 1", "enter 2", "caught neg", "inner-finally", "exit 2"),
scenario(WHOLE, "three kinds bind by kind, not position", "carryThree", of(7), of(14), "enter 8 107 L7", "body 7", "exit 8 107 L7"),
scenario(WHOLE, "frame naming an unbuilt object", "carryUninitialized", of(5), of(10), "enter pos", "body 5", "exit pos"),
scenario(WHOLE, "entry handler's own try-catch", "carryGuarded", of(-3), of(-6), "recovered", "enter -1", "body -3", "exit -1"),
scenario(WHOLE, "assigned in both branches at the end", "carrySplitAtEnd", of(-3), of(-6), "body -3", "exit 2"),
scenario(WHOLE, "wide carry ahead of an int across a frame", "carryWideBeforeFrame", of(3), of(6), "positive", "body 3", "exit 3 4"),
scenario(WHOLE, "array locals", "carryArrays", of(3), of(6), "body 3", "exit n3 3"),
scenario(WHOLE, "widths and an uncarried local between carried ones survive the handler frame", "throwWide", of(3), of(IllegalStateException.class), "enter 4 1.5 3 s3", "body 3", "caught 4 1.5 3", "exit 4 1.5 3"),
scenario(WHOLE, "a reference carry is cast to each exit handler's type and a throwing catch handler still runs finally", "throwObject", of(3), of(UnsupportedOperationException.class), "enter o3", "body 3", "caught 2 o3", "exit 2 o3"),
scenario(WHOLE, "a reused slot carries the local's value", "reused", of(3), of(6), "tmp 8", "enter 10", "body 3", "exit 10"),
scenario(CALL, "a static target takes static handlers around a static callee", "staticTarget", of(4), of(8), "enterStaticTarget 4", "twiceStatic 4", "exitStaticTarget 4 8"),
scenario(CALL, "an interface callee is typed as the interface or a supertype", "interfaceCall", of(3), of(4), "enterInterface 3 true", "apply 3", "exitInterface true"),
scenario(CALL, "a private callee's receiver is the target itself", "privateCall", of(3), of(4), "enterPrivate true 3", "priv 3", "exitPrivate true 3"),
scenario(CALL, "every match is surrounded separately", "all", of(2), of(18), "enterAll 2 1", "twice 2", "exitAll 2 1", "enterAll 3 2", "twice 3", "exitAll 3 2", "enterAll 4 3", "twice 4", "exitAll 4 3"),
scenario(CALL, "a slice restricts the matches", "sliced", of(2), of(10), "twice 2", "marker", "enterSliced 3", "twice 3", "exitSliced 3"),
scenario(CALL, "a throwing entry handler runs no exit handler and skips the call", "throwingEntry", of(args()), of(IllegalArgumentException.class), "enterThrowingEntry 1"),
scenario(CALL, "inject before, modify arg and inject after stay outside", "composed", of(5), of(210), "before 5", "modifyArg 5", "enterComposed 105", "twice 105", "exitComposed 105", "after 5"),
scenario(WHOLE, "a void target skips without a skipped handler", "skippedVoid", of(-1, 1), of(VOID, VOID), "enterSkippedVoid -1", "exitSkippedVoid", "enterSkippedVoid 1", "skippedVoid 1", "exitSkippedVoid"),
scenario(WHOLE, "an entry handler may call the target directly and then skip", "reentrant", of(1), of(24), "enterReentrant 11 false", "reentrant 11", "exitReentrant 11", "enterReentrant 1 true", "exitReentrant 1"),
scenario(WHOLE, "return does not run after a skip and a reference skipped value is returned", "skipAndReturn", of("", "a", "x"), of(null, "a!?", IllegalStateException.class), "enterSkipAndReturn ", "skippedSkipAndReturn", "exitSkipAndReturn ", "enterSkipAndReturn a", "skipAndReturn a", "returnSkipAndReturn a!", "exitSkipAndReturn a", "enterSkipAndReturn x", "skipAndReturn x", "returnSkipAndReturn x!", "exitSkipAndReturn x"),
scenario(WHOLE, "an entry handler may catch and rethrow around a direct call", "entryCatches", of(3, -1), of(6, IllegalStateException.class), "entryCatches 3", "exitEntryCatches false", "enterEntryCatches 3", "entryCatches 3", "exitEntryCatches true", "entryCatches -1", "exitEntryCatches false", "entryRethrow negative -1"),
scenario(CALL, "a local is selected by name between operands and carries", "byName", of(3), of(49), "enterByName 23 1", "twice 13", "exitByName 13 23 1"),
scenario(CALL, "wide locals bind", "wideLocals", of(3), of(31L), "enterWideLocals 30 1.5", "wide 30 1.5", "exitWideLocals 1.5 30"),
scenario(CALL, "a handling catch replaces the result over the values below", "valueBelow", of(-3, 4), of(67, 84), "enterValueBelow -3 1", "explode -3", "handleValueBelow -3 1", "exitValueBelow -3 1", "enterValueBelow 4 2", "explode 4", "exitValueBelow 4 2"),
scenario(CALL, "a skipped handler supplies the value of a skipped call", "valueSkip", of(-3, 3), of(-300, 6), "enterValueSkip -3 1", "skippedValueSkip -3 1", "exitValueSkip 1", "enterValueSkip 3 2", "twice 3", "exitValueSkip 2"),
scenario(CALL, "a void return handler observes and does not run after a handled exception", "observed", of(2, -1), of(VOID, VOID), "enterObserved 2", "maybeBoom 2", "observe 2", "exitObserved 2", "enterObserved -1", "maybeBoom -1", "handleObserved -1", "exitObserved -1"),
scenario(CALL, "a handled value bypasses the return handler", "returnAndHandle", of(4, -1), of(80, 7), "enterReturnAndHandle 4", "explode 4", "returnReturnAndHandle 8", "exitReturnAndHandle", "enterReturnAndHandle -1", "explode -1", "handleReturnAndHandle", "exitReturnAndHandle"),
scenario(CALL, "the skip path is covered by catch and finally but not by return", "skipFinallyCatch", of(2, -3, 0), of(1004, 3, IllegalStateException.class), "enterSfc 2", "explode 2", "returnSfc 4", "exitSfc 2", "enterSfc -3", "skippedSfc -3", "exitSfc -3", "enterSfc 0", "skippedSfc 0", "caughtSfc zero", "exitSfc 0"),
scenario(CALL, "an outer skip runs no inner handler and return handlers chain inner to outer", "nestedSkip", of(-2, 2), of(-1, 50), "enterNestedOuter -2", "skippedNestedOuter", "exitNestedOuter -2", "enterNestedOuter 2", "enterNestedInner 2", "twice 2", "returnNestedInner 4", "exitNestedInner 2", "returnNestedOuter 5", "exitNestedOuter 2"),
scenario(CALL, "a skip skips the redirect handler inside", "redirectedSkip", of(-1, 3), of(0, 7), "enterRedirectedSkip -1", "exitRedirectedSkip -1", "enterRedirectedSkip 3", "redirectSkip 3", "twice 3", "exitRedirectedSkip 3"),
scenario(CALL, "a final skip derived from a carry computed by a unique helper binds", "helperSkip", of(-4, 4), of(-1, 8), "part -4", "enterHelper -1", "skippedHelper", "exitHelper -1", "part 4", "enterHelper 4", "twice 4", "exitHelper 4"),
scenario(CALL, "wide operands skip and return with an int skip flag", "wideSkip", of(args(-2L, 3.0d), args(5L, 2.5d)), of(-19L, 108L), "enterWideSkip -2 3.0", "skippedWideSkip -2 1.5", "exitWideSkip -2 3.0", "enterWideSkip 5 2.5", "wide 5 2.5", "returnWideSkip 7 1.25", "exitWideSkip 5 2.5"),
scenario(CALL, "an entry-only call surround needs no exit handler even with a skip", "entryOnlySkip", of(1, -1), of(1, -1), "enterOnlySkip 1", "enterOnlySkip -1", "render -1"),
scenario(CALL, "an entry-only whole-method surround", "entryOnlyWhole", of(args()), of(VOID), "enterOnlyWhole", "entryOnlyWhole"),
scenario(CALL, "a skip restores the values below the call when the entry catches", "skipBelowRestored", of(-2, 3, 200), of(980, 1006, 1400), "enterSkipBelowRestored -2", "skippedSkipBelowRestored -2", "exitSkipBelowRestored -2", "enterSkipBelowRestored 3", "twice 3", "exitSkipBelowRestored 3", "entryCaughtSkipBelowRestored 200", "twice 200", "exitSkipBelowRestored 200"),
scenario(CALL, "an inner skip completes normally for an outer return handler", "innerSkipOuterReturn", of(-2, 3), of(-199, 7), "enterOuterReturn -2", "enterInnerSkip -2", "skippedInnerSkip -2", "exitInnerSkip -2", "returnOuterReturn -200", "exitOuterReturn -2", "enterOuterReturn 3", "enterInnerSkip 3", "twice 3", "exitInnerSkip 3", "returnOuterReturn 6", "exitOuterReturn 3"),
scenario(CALL, "a call inside a constructor call's arguments can be skipped", "skipInsideNew", of(-2, 3), of(-20, 6), "enterSkipInsideNew -2", "skippedSkipInsideNew -2", "exitSkipInsideNew -2", "enterSkipInsideNew 3", "twice 3", "exitSkipInsideNew 3"),
scenario(CALL, "a call surround skips inside a body moved by a whole-method surround", "movedBody", of(-2, 3), of(990, 1008), "enterMovedWhole -2", "enterMovedCall -1 -2", "skippedMovedCall -1 -2", "exitMovedCall -1 -2", "exitMovedWhole -2", "enterMovedWhole 3", "enterMovedCall 4 3", "twice 4", "exitMovedCall 4 3", "exitMovedWhole 3"),
scenario(WHOLE, "a branching entry handler around the whole method", "branching", of(-3), of(-6), "enterBranching negative", "body -3", "exitBranching negative")
);
}
@ParameterizedTest(name = "{0}")
@MethodSource("scenarios")
void scenarios(String method, String target, Object[] inputs, Object[] outputs, String[] events) throws Exception {
MixinTestBootstrap.assertScenario(target, method, inputs, outputs, events);
}
}
@@ -0,0 +1,99 @@
package com.gtnewhorizons.angelica.experimental.surround;
import com.gtnewhorizons.angelica.experimental.surround.targets.Trace;
import net.minecraft.launchwrapper.Launch;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
import org.spongepowered.asm.lib.tree.AbstractInsnNode;
import org.spongepowered.asm.lib.tree.ClassNode;
import org.spongepowered.asm.lib.tree.LabelNode;
import org.spongepowered.asm.lib.tree.LineNumberNode;
import org.spongepowered.asm.lib.tree.LocalVariableNode;
import org.spongepowered.asm.lib.tree.MethodNode;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import static com.gtnewhorizons.angelica.experimental.surround.MixinTestBootstrap.WHOLE;
import static com.gtnewhorizons.angelica.experimental.surround.MixinTestBootstrap.call;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class WholeMethodTest {
@BeforeEach
void resetTrace() {
Trace.reset();
}
@Test
void oneEntryHandlerCanSurroundSeveralTargets() throws Exception {
call(WHOLE, "alpha");
call(WHOLE, "beta");
Trace.assertEvents("enterBoth 1", "alpha", "exitBoth 1", "enterBoth 2", "beta", "exitBoth 2");
}
@Test
void theWrapperKeepsTheEntryHandlersLinesAndTheBodyKeepsItsOwn() {
final ClassNode merged = MixinTestBootstrap.transformedNode(WHOLE);
final List<Integer> entryLines = lines(bySuffix(merged, "$surround$enterBranching"));
assertFalse(entryLines.isEmpty(), "fixture compiled without line numbers");
assertEquals(entryLines, lines(MixinTestBootstrap.method(merged, "branching")));
assertEquals(lines(MixinTestBootstrap.method(MixinTestBootstrap.originalNode(WHOLE), "branching")), lines(MixinTestBootstrap.method(merged, "branching$surround")));
}
@Test
void aReusedSlotNamesTheCarriedLocal() {
final MethodNode wrapper = MixinTestBootstrap.method(MixinTestBootstrap.transformedNode(WHOLE), "reused");
LabelNode last = null;
for (AbstractInsnNode insn = wrapper.instructions.getLast(); last == null; insn = insn.getPrevious()) {
last = insn instanceof LabelNode label ? label : null;
}
final List<String> toEnd = new ArrayList<>();
for (LocalVariableNode local : wrapper.localVariables) {
if (local.end == last) {
toEnd.add(local.name);
}
}
assertTrue(toEnd.contains("prev") && !toEnd.contains("tmp"), "locals live to the end: " + toEnd);
}
@Test
void theWrapperMatchesTheGoldens() {
MixinTestBootstrap.assertGolden(WHOLE, "skipAndReturn", "whole-skip-return");
}
@Test
@EnabledIfSystemProperty(named = "surround.test.obf", matches = "true")
void theLaunchClassLoaderTransformersSeeTheMixinClass() throws Exception {
MixinTestBootstrap.transform(WHOLE);
final Set<?> seen = (Set<?>) Launch.classLoader.loadClass(LocalSortingTransformer.class.getName()).getField("SEEN").get(null);
assertTrue(seen.contains("com.gtnewhorizons.angelica.experimental.surround.integration.mixin.MixinWholeTarget"), seen.toString());
}
private static MethodNode bySuffix(ClassNode node, String suffix) {
for (MethodNode method : node.methods) {
if (method.name.endsWith(suffix)) {
return method;
}
}
final List<String> names = new ArrayList<>();
for (MethodNode method : node.methods) {
names.add(method.name);
}
throw new AssertionError("no method ending " + suffix + " in " + names);
}
private static List<Integer> lines(MethodNode method) {
final List<Integer> lines = new ArrayList<>();
for (AbstractInsnNode insn : method.instructions.toArray()) {
if (insn instanceof LineNumberNode ln) {
lines.add(ln.line);
}
}
return lines;
}
}
@@ -0,0 +1,230 @@
package com.gtnewhorizons.angelica.experimental.surround.integration;
import com.gtnewhorizons.angelica.experimental.surround.targets.Trace;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.IntUnaryOperator;
public class CallTarget {
public static final String CALLEE = "Lcom/gtnewhorizons/angelica/experimental/surround/integration/CallTarget$Callee;";
public static final String TWICE = CALLEE + "twice(I)I";
private final Callee callee = new Callee();
private final IntUnaryOperator op = new Callee();
private final Render renderer = new Render();
public static int staticTarget(int a) {
return Callee.twiceStatic(a);
}
public int interfaceCall(int a) {
return this.op.applyAsInt(a);
}
public int privateCall(int a) {
return this.priv(a);
}
private int priv(int a) {
Trace.add("priv " + a);
return a + 1;
}
public int all(int a) {
return this.callee.twice(a) + this.callee.twice(a + 1) + this.callee.twice(a + 2);
}
public int sliced(int a) {
final int first = this.callee.twice(a);
Callee.marker();
return first + this.callee.twice(a + 1);
}
public void throwingEntry() {
this.callee.twice(1);
}
public int composed(int a) {
return this.callee.twice(a);
}
public boolean render(Entity entity, double x, double y, double z, float yaw, float partial, boolean flag) {
Render render = null;
try {
render = this.renderer;
if (render != null && entity != null) {
try {
render.doRender(entity, x, y, z, yaw, partial);
} catch (Throwable throwable1) {
throw new ReportedException("Rendering entity in world", throwable1);
}
try {
render.doRenderShadowAndFire(entity, x, y, z, yaw, partial);
} catch (Throwable throwable2) {
throw new ReportedException("Post-rendering entity in world", throwable2);
}
if (flag) {
Trace.add("boundingBox " + entity.name);
}
}
return true;
} catch (Throwable throwable) {
throw new ReportedException("Rendering entity (outer)", throwable);
}
}
public int byName(int a) {
final int first = a + 10;
final int second = a + 20;
return this.callee.twice(first) + second;
}
public long wideLocals(int a) {
final long big = a * 10L;
final double half = a / 2.0;
return this.callee.wide(big, half);
}
public int valueBelow(int a) {
return a + this.callee.explode(a) * 10;
}
public int valueSkip(int a) {
return this.callee.twice(a);
}
public void observed(int a) {
this.callee.maybeBoom(a);
}
public int returnAndHandle(int a) {
return this.callee.explode(a);
}
public int skipFinallyCatch(int a) {
return this.callee.explode(a);
}
public int nestedSkip(int a) {
return this.callee.twice(a);
}
public int redirectedSkip(int a) {
return this.callee.twice(a);
}
public int helperSkip(int a) {
return this.callee.twice(a);
}
public long wideSkip(long a, double b) {
return this.callee.wide(a, b) + 1L;
}
public int entryOnlySkip(int a) {
this.callee.render(a);
return a;
}
public void entryOnlyWhole() {
Trace.add("entryOnlyWhole");
}
public int skipBelowRestored(int a) {
return 1000 + this.callee.twice(a);
}
public int innerSkipOuterReturn(int a) {
return this.callee.twice(a);
}
public int skipInsideNew(int a) {
return new AtomicInteger(this.callee.twice(a)).get();
}
public int movedBody(int a) {
final int b = a + 1;
return 1000 + this.callee.twice(b);
}
public static class Callee implements IntUnaryOperator {
public int twice(int a) {
Trace.add("twice " + a);
return a * 2;
}
public static int twiceStatic(int a) {
Trace.add("twiceStatic " + a);
return a * 2;
}
public static void marker() {
Trace.add("marker");
}
public long wide(long a, double b) {
Trace.add("wide " + a + " " + b);
return a + (long) b;
}
@Override
public int applyAsInt(int a) {
Trace.add("apply " + a);
return a + 1;
}
public int explode(int a) {
Trace.add("explode " + a);
if (a < 0) {
throw new IllegalStateException("negative " + a);
}
return a * 2;
}
public void maybeBoom(int a) {
Trace.add("maybeBoom " + a);
if (a < 0) {
throw new IllegalStateException("boom " + a);
}
}
public void render(int a) {
Trace.add("render " + a);
}
}
public static class Entity {
public final String name;
public boolean explode;
public Entity(String name) {
this.name = name;
}
}
public static class Render {
public void doRender(Entity entity, double x, double y, double z, float yaw, float partial) {
Trace.add("doRender " + entity.name + " " + x + " " + partial);
if (entity.explode) {
throw new RuntimeException("gpu");
}
}
public void doRenderShadowAndFire(Entity entity, double x, double y, double z, float yaw, float partial) {
Trace.add("shadow " + entity.name);
}
}
public static class ReportedException extends RuntimeException {
private static final long serialVersionUID = 1L;
public ReportedException(String description, Throwable cause) {
super(description, cause);
}
}
}
@@ -0,0 +1,169 @@
package com.gtnewhorizons.angelica.experimental.surround.integration;
import com.gtnewhorizons.angelica.experimental.surround.targets.Trace;
public class WholeTarget {
public static boolean guard;
public int compute(int a) {
Trace.add("body " + a);
return a * 2;
}
public void boom() {
Trace.add("boom");
throw new IllegalStateException("boom");
}
public void alpha() {
Trace.add("alpha");
}
public void beta() {
Trace.add("beta");
}
public synchronized int lockedInstance(int a) {
Trace.add("body " + Thread.holdsLock(this));
return a * 2;
}
public void run() {
Trace.add("run");
exit();
}
private void exit() {
Trace.add("target exit");
}
public int priority(int a) {
Trace.add("body " + a);
return a;
}
public int branching(int a) {
Trace.add("body " + a);
return a * 2;
}
public void skippedVoid(int a) {
Trace.add("skippedVoid " + a);
}
public int reentrant(int a) {
Trace.add("reentrant " + a);
return a + 1;
}
public String skipAndReturn(String a) {
Trace.add("skipAndReturn " + a);
return a + "!";
}
public static int entryCatches(int a) {
Trace.add("entryCatches " + a);
if (a < 0) {
throw new IllegalStateException("negative " + a);
}
return a * 2;
}
public int throwingFinally(int a, int b) {
return pick(a, b);
}
private static int pick(int a, int b) {
if (a > b) {
Trace.add("gt");
return a;
}
if (a < b) {
Trace.add("lt");
return b;
}
Trace.add("eq");
return 0;
}
public static double wideBody(double a, long b) {
Trace.add("body");
return a + b;
}
public void throwWide(int a) {
thrown(a);
}
public void throwObject(int a) {
thrown(a);
}
private static void thrown(int a) {
Trace.add("body " + a);
throw new IllegalStateException("boom " + a);
}
public void throwingEntry(int a) {
Trace.add("body " + a);
}
public int caught(int a) {
try {
if (a < 0) {
throw new IllegalArgumentException("neg");
}
return a;
} catch (IllegalArgumentException e) {
Trace.add("caught " + e.getMessage());
return -1;
} finally {
Trace.add("inner-finally");
}
}
public int carryThree(int a) {
return carried(a);
}
public int carryUninitialized(int a) {
return carried(a);
}
public int carryGuarded(int a) {
return carried(a);
}
public int carrySplitAtEnd(int a) {
return carried(a);
}
public int carryWideBeforeFrame(int a) {
return carried(a);
}
public int carryArrays(int a) {
return carried(a);
}
public int reused(int a) {
return carried(a);
}
private static int carried(int a) {
Trace.add("body " + a);
return a * 2;
}
public static int risky(int a) {
if (a < 0) {
throw new IllegalArgumentException("neg");
}
return a * 3;
}
public static Object asObject(Object value) {
return value;
}
}
@@ -0,0 +1,24 @@
package com.gtnewhorizons.angelica.experimental.surround.integration;
import com.gtnewhorizons.angelica.experimental.surround.integration.CallTarget.Callee;
import com.gtnewhorizons.angelica.experimental.surround.targets.Trace;
public class WrapTarget {
private final Callee callee = new Callee();
public int wrapped(int a) {
Trace.add("body " + a);
return a * 2;
}
public int originalOperands(int a) {
return this.callee.twice(a);
}
public int shifted(int a) {
final int local = a + 1;
final String label = "L" + local;
return this.callee.twice(local) + label.length();
}
}
@@ -0,0 +1,504 @@
package com.gtnewhorizons.angelica.experimental.surround.integration.mixin;
import com.gtnewhorizons.angelica.experimental.surround.Surround;
import com.gtnewhorizons.angelica.experimental.surround.integration.CallTarget;
import com.gtnewhorizons.angelica.experimental.surround.integration.CallTarget.Callee;
import com.gtnewhorizons.angelica.experimental.surround.integration.CallTarget.Entity;
import com.gtnewhorizons.angelica.experimental.surround.integration.CallTarget.Render;
import com.gtnewhorizons.angelica.experimental.surround.targets.Trace;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.ModifyArg;
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.Slice;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.util.function.IntUnaryOperator;
import static com.gtnewhorizons.angelica.experimental.surround.integration.CallTarget.CALLEE;
import static com.gtnewhorizons.angelica.experimental.surround.integration.CallTarget.TWICE;
@Mixin(value = CallTarget.class, remap = false)
public class MixinCallTarget {
private static final String WIDE = CALLEE + "wide(JD)J";
private static final String EXPLODE = CALLEE + "explode(I)I";
private static final String MAYBE_BOOM = CALLEE + "maybeBoom(I)V";
private static final String DO_RENDER = "Lcom/gtnewhorizons/angelica/experimental/surround/integration/CallTarget$Render;" + "doRender(Lcom/gtnewhorizons/angelica/experimental/surround/integration/CallTarget$Entity;DDDFF)V";
private static final long QUEUED = -1L;
@Surround(method = "staticTarget", at = @At(value = "INVOKE", target = CALLEE + "twiceStatic(I)I"), id = "staticTarget")
private static void surround$enterStaticTarget(int a) {
@Surround.Carry int doubled = a * 2;
Trace.add("enterStaticTarget " + a);
}
@Surround.Finally("staticTarget")
private static void surround$exitStaticTarget(int a, @Surround.Carry int doubled) {
Trace.add("exitStaticTarget " + a + " " + doubled);
}
@Surround(method = "interfaceCall", at = @At(value = "INVOKE", target = "Ljava/util/function/IntUnaryOperator;applyAsInt(I)I"), id = "interface")
private void surround$enterInterface(IntUnaryOperator op, int a) {
Trace.add("enterInterface " + a + " " + (op instanceof Callee));
}
@Surround.Finally("interface")
private void surround$exitInterface(Object op) {
Trace.add("exitInterface " + (op instanceof Callee));
}
@Surround(method = "privateCall", at = @At(value = "INVOKE", target = "Lcom/gtnewhorizons/angelica/experimental/surround/integration/CallTarget;priv(I)I"), id = "private")
private void surround$enterPrivate(CallTarget self, int a) {
Trace.add("enterPrivate " + (self == (Object) this) + " " + a);
}
@Surround.Finally("private")
private void surround$exitPrivate(CallTarget self, int a) {
Trace.add("exitPrivate " + (self == (Object) this) + " " + a);
}
@Surround(method = "all", at = @At(value = "INVOKE", target = TWICE), id = "all", require = 3)
private void surround$enterAll(Callee callee, int a) {
@Surround.Carry long token = Trace.nextToken();
Trace.add("enterAll " + a + " " + token);
}
@Surround.Finally("all")
private void surround$exitAll(Callee callee, int a, @Surround.Carry long token) {
Trace.add("exitAll " + a + " " + token);
}
@Surround(method = "sliced", at = @At(value = "INVOKE", target = TWICE), slice = @Slice(from = @At(value = "INVOKE", target = CALLEE + "marker()V")), id = "sliced", require = 1, allow = 1)
private void surround$enterSliced(Callee callee, int a) {
Trace.add("enterSliced " + a);
}
@Surround.Finally("sliced")
private void surround$exitSliced(Callee callee, int a) {
Trace.add("exitSliced " + a);
}
@Surround(method = "throwingEntry", at = @At(value = "INVOKE", target = TWICE), id = "throwingEntry")
private void surround$enterThrowingEntry(Callee callee, int a) {
Trace.add("enterThrowingEntry " + a);
if (a > 0) {
throw new IllegalArgumentException("entry");
}
}
@Surround.Catch("throwingEntry")
private void surround$caughtThrowingEntry(Throwable error) {
Trace.add("caughtThrowingEntry");
}
@Surround.Finally("throwingEntry")
private void surround$exitThrowingEntry() {
Trace.add("exitThrowingEntry");
}
@Inject(method = "composed", at = @At(value = "INVOKE", target = TWICE))
private void surround$before(int a, CallbackInfoReturnable<Integer> cir) {
Trace.add("before " + a);
}
@Inject(method = "composed", at = @At(value = "INVOKE", target = TWICE, shift = At.Shift.AFTER))
private void surround$after(int a, CallbackInfoReturnable<Integer> cir) {
Trace.add("after " + a);
}
@ModifyArg(method = "composed", at = @At(value = "INVOKE", target = TWICE))
private int surround$plusHundred(int a) {
Trace.add("modifyArg " + a);
return a + 100;
}
@Surround(method = "composed", at = @At(value = "INVOKE", target = TWICE), id = "composed")
private void surround$enterComposed(Callee callee, int a) {
Trace.add("enterComposed " + a);
}
@Surround.Finally("composed")
private void surround$exitComposed(Callee callee, int a) {
Trace.add("exitComposed " + a);
}
@Redirect(method = "render", at = @At(value = "INVOKE", target = DO_RENDER))
private void surround$redirectDoRender(Render render, Entity entity, double x, double y, double z, float yaw, float partial) {
Trace.add("redirect " + entity.name);
render.doRender(entity, x, y, z, yaw, partial);
}
@Surround(method = "render", at = @At(value = "INVOKE", target = DO_RENDER), id = "render", require = 1)
private void surround$enterDoRender(Render render, Entity entity, double x, double y, double z, float yaw, float partial) {
@Surround.Carry long token = Trace.nextToken();
Trace.add("enterDoRender " + entity.name + " " + token);
}
@Surround.Catch("render")
private void surround$caughtDoRender(Throwable error, Render render, Entity entity, @Surround.Carry long token) {
Trace.add("caughtDoRender " + entity.name + " " + error.getMessage() + " " + token);
}
@Surround.Finally("render")
private void surround$exitDoRender(Render render, Entity entity, @Surround.Carry long token) {
Trace.add("exitDoRender " + entity.name + " " + token);
}
@Surround(method = "byName", at = @At(value = "INVOKE", target = TWICE), id = "byName")
private void surround$enterByName(@Surround.Local(name = "second") int second) {
@Surround.Carry long token = Trace.nextToken();
Trace.add("enterByName " + second + " " + token);
}
@Surround.Finally("byName")
private void surround$exitByName(Callee callee, int first, @Surround.Local(name = "second") int second, @Surround.Carry long token) {
Trace.add("exitByName " + first + " " + second + " " + token);
}
@Surround(method = "wideLocals", at = @At(value = "INVOKE", target = WIDE), id = "wideLocals")
private void surround$enterWideLocals(Callee callee, long a, double b, @Surround.Local long big, @Surround.Local double half) {
Trace.add("enterWideLocals " + big + " " + half);
}
@Surround.Finally("wideLocals")
private void surround$exitWideLocals(@Surround.Local(index = 4) double half, @Surround.Local(index = 2) long big) {
Trace.add("exitWideLocals " + half + " " + big);
}
@Surround(method = "valueBelow", at = @At(value = "INVOKE", target = EXPLODE), id = "valueBelow")
private void surround$enterValueBelow(Callee callee, int a) {
@Surround.Carry long token = Trace.nextToken();
Trace.add("enterValueBelow " + a + " " + token);
}
@Surround.Catch(value = "valueBelow", handle = true)
private int surround$handleValueBelow(IllegalStateException error, Callee callee, int a, @Surround.Carry long token) {
Trace.add("handleValueBelow " + a + " " + token);
return 7;
}
@Surround.Finally("valueBelow")
private void surround$exitValueBelow(Callee callee, int a, @Surround.Carry long token) {
Trace.add("exitValueBelow " + a + " " + token);
}
@Surround(method = "valueSkip", at = @At(value = "INVOKE", target = TWICE), id = "valueSkip")
private void surround$enterValueSkip(Callee callee, int a) {
@Surround.Carry long token = Trace.nextToken();
@Surround.Skip boolean skip = a < 0;
Trace.add("enterValueSkip " + a + " " + token);
}
@Surround.Skipped("valueSkip")
private int surround$skippedValueSkip(Callee callee, int a, @Surround.Carry long token) {
Trace.add("skippedValueSkip " + a + " " + token);
return a * 100;
}
@Surround.Finally("valueSkip")
private void surround$exitValueSkip(@Surround.Carry long token) {
Trace.add("exitValueSkip " + token);
}
@Surround(method = "observed", at = @At(value = "INVOKE", target = MAYBE_BOOM), id = "observed")
private void surround$enterObserved(Callee callee, int a) {
Trace.add("enterObserved " + a);
}
@Surround.Return("observed")
private void surround$observe(Callee callee, int a) {
Trace.add("observe " + a);
}
@Surround.Catch(value = "observed", handle = true)
private void surround$handleObserved(IllegalStateException error, Callee callee, int a) {
Trace.add("handleObserved " + a);
}
@Surround.Finally("observed")
private void surround$exitObserved(Callee callee, int a) {
Trace.add("exitObserved " + a);
}
@Surround(method = "returnAndHandle", at = @At(value = "INVOKE", target = EXPLODE), id = "returnAndHandle")
private void surround$enterReturnAndHandle(Callee callee, int a) {
Trace.add("enterReturnAndHandle " + a);
}
@Surround.Return("returnAndHandle")
private int surround$returnReturnAndHandle(int result) {
Trace.add("returnReturnAndHandle " + result);
return result * 10;
}
@Surround.Catch(value = "returnAndHandle", handle = true)
private int surround$handleReturnAndHandle(IllegalStateException error) {
Trace.add("handleReturnAndHandle");
return 7;
}
@Surround.Finally("returnAndHandle")
private void surround$exitReturnAndHandle() {
Trace.add("exitReturnAndHandle");
}
@Surround(method = "skipFinallyCatch", at = @At(value = "INVOKE", target = EXPLODE), id = "sfc")
private void surround$enterSfc(Callee callee, int a) {
@Surround.Skip boolean skip = a <= 0;
Trace.add("enterSfc " + a);
}
@Surround.Skipped("sfc")
private int surround$skippedSfc(Callee callee, int a) {
Trace.add("skippedSfc " + a);
if (a == 0) {
throw new IllegalStateException("zero");
}
return -a;
}
@Surround.Return("sfc")
private int surround$returnSfc(int result, Callee callee, int a) {
Trace.add("returnSfc " + result);
return result + 1000;
}
@Surround.Catch("sfc")
private void surround$caughtSfc(IllegalStateException error, Callee callee, int a) {
Trace.add("caughtSfc " + error.getMessage());
}
@Surround.Finally("sfc")
private void surround$exitSfc(Callee callee, int a) {
Trace.add("exitSfc " + a);
}
@Surround(method = "nestedSkip", at = @At(value = "INVOKE", target = TWICE), id = "nestedInner")
private void surround$enterNestedInner(Callee callee, int a) {
Trace.add("enterNestedInner " + a);
}
@Surround.Return("nestedInner")
private int surround$returnNestedInner(int result) {
Trace.add("returnNestedInner " + result);
return result + 1;
}
@Surround.Finally("nestedInner")
private void surround$exitNestedInner(Callee callee, int a) {
Trace.add("exitNestedInner " + a);
}
@Surround(method = "nestedSkip", at = @At(value = "INVOKE", target = TWICE), id = "nestedOuter")
private void surround$enterNestedOuter(Callee callee, int a) {
@Surround.Skip boolean skip = a < 0;
Trace.add("enterNestedOuter " + a);
}
@Surround.Skipped("nestedOuter")
private int surround$skippedNestedOuter() {
Trace.add("skippedNestedOuter");
return -1;
}
@Surround.Return("nestedOuter")
private int surround$returnNestedOuter(int result) {
Trace.add("returnNestedOuter " + result);
return result * 10;
}
@Surround.Finally("nestedOuter")
private void surround$exitNestedOuter(Callee callee, int a) {
Trace.add("exitNestedOuter " + a);
}
@Redirect(method = "redirectedSkip", at = @At(value = "INVOKE", target = TWICE))
private int surround$redirectSkip(Callee callee, int a) {
Trace.add("redirectSkip " + a);
return callee.twice(a) + 1;
}
@Surround(method = "redirectedSkip", at = @At(value = "INVOKE", target = TWICE), id = "redirectedSkip")
private void surround$enterRedirectedSkip(Callee callee, int a) {
@Surround.Skip boolean skip = a < 0;
Trace.add("enterRedirectedSkip " + a);
}
@Surround.Skipped("redirectedSkip")
private int surround$skippedRedirectedSkip(Callee callee, int a) {
return 0;
}
@Surround.Finally("redirectedSkip")
private void surround$exitRedirectedSkip(Callee callee, int a) {
Trace.add("exitRedirectedSkip " + a);
}
@Unique
private static long surround$part(int a) {
Trace.add("part " + a);
return a < 0 ? QUEUED : a;
}
@Surround(method = "helperSkip", at = @At(value = "INVOKE", target = TWICE), id = "helper")
private void surround$enterHelper(Callee callee, int a) {
@Surround.Carry final long part = surround$part(a);
@Surround.Skip final boolean queued = part == QUEUED;
Trace.add("enterHelper " + part);
}
@Surround.Skipped("helper")
private int surround$skippedHelper(Callee callee, int a, @Surround.Carry long part) {
Trace.add("skippedHelper");
return (int) part;
}
@Surround.Finally("helper")
private void surround$exitHelper(@Surround.Carry long part) {
Trace.add("exitHelper " + part);
}
@Surround(method = "wideSkip", at = @At(value = "INVOKE", target = WIDE), id = "wideSkip")
private void surround$enterWideSkip(Callee callee, long a, double b) {
@Surround.Carry double halved = b / 2;
@Surround.Skip int skip = a < 0 ? 1 : 0;
Trace.add("enterWideSkip " + a + " " + b);
}
@Surround.Skipped("wideSkip")
private long surround$skippedWideSkip(Callee callee, long a, @Surround.Carry double halved) {
Trace.add("skippedWideSkip " + a + " " + halved);
return a * 10L;
}
@Surround.Return("wideSkip")
private long surround$returnWideSkip(long result, Callee callee, long a, double b, @Surround.Carry double halved) {
Trace.add("returnWideSkip " + result + " " + halved);
return result + 100L;
}
@Surround.Finally("wideSkip")
private void surround$exitWideSkip(Callee callee, long a, double b) {
Trace.add("exitWideSkip " + a + " " + b);
}
@Surround(method = "entryOnlySkip", at = @At(value = "INVOKE", target = CALLEE + "render(I)V"), id = "entryOnlySkip")
private void surround$enterOnlySkip(Callee callee, int a) {
@Surround.Skip boolean skip = a > 0;
Trace.add("enterOnlySkip " + a);
}
@Surround(method = "entryOnlyWhole", id = "entryOnlyWhole")
private void surround$enterOnlyWhole() {
Trace.add("enterOnlyWhole");
}
@Surround(method = "skipBelowRestored", at = @At(value = "INVOKE", target = TWICE), id = "skipBelowRestored")
private void surround$enterSkipBelowRestored(Callee callee, int a) {
@Surround.Skip boolean skip = a < 0;
try {
if (a > 100) {
throw new IllegalArgumentException("big");
}
Trace.add("enterSkipBelowRestored " + a);
} catch (IllegalArgumentException e) {
Trace.add("entryCaughtSkipBelowRestored " + a);
}
}
@Surround.Skipped("skipBelowRestored")
private int surround$skippedSkipBelowRestored(Callee callee, int a) {
Trace.add("skippedSkipBelowRestored " + a);
return a * 10;
}
@Surround.Finally("skipBelowRestored")
private void surround$exitSkipBelowRestored(Callee callee, int a) {
Trace.add("exitSkipBelowRestored " + a);
}
@Surround(method = "innerSkipOuterReturn", at = @At(value = "INVOKE", target = TWICE), id = "innerSkip")
private void surround$enterInnerSkip(Callee callee, int a) {
@Surround.Skip boolean skip = a < 0;
Trace.add("enterInnerSkip " + a);
}
@Surround.Skipped("innerSkip")
private int surround$skippedInnerSkip(Callee callee, int a) {
Trace.add("skippedInnerSkip " + a);
return a * 100;
}
@Surround.Finally("innerSkip")
private void surround$exitInnerSkip(Callee callee, int a) {
Trace.add("exitInnerSkip " + a);
}
@Surround(method = "innerSkipOuterReturn", at = @At(value = "INVOKE", target = TWICE), id = "outerReturn")
private void surround$enterOuterReturn(Callee callee, int a) {
Trace.add("enterOuterReturn " + a);
}
@Surround.Return("outerReturn")
private int surround$returnOuterReturn(int result) {
Trace.add("returnOuterReturn " + result);
return result + 1;
}
@Surround.Finally("outerReturn")
private void surround$exitOuterReturn(Callee callee, int a) {
Trace.add("exitOuterReturn " + a);
}
@Surround(method = "skipInsideNew", at = @At(value = "INVOKE", target = TWICE), id = "skipInsideNew")
private void surround$enterSkipInsideNew(Callee callee, int a) {
@Surround.Skip boolean skip = a < 0;
Trace.add("enterSkipInsideNew " + a);
}
@Surround.Skipped("skipInsideNew")
private int surround$skippedSkipInsideNew(Callee callee, int a) {
Trace.add("skippedSkipInsideNew " + a);
return a * 10;
}
@Surround.Catch("skipInsideNew")
private void surround$caughtSkipInsideNew(Throwable error) {
Trace.add("caughtSkipInsideNew");
}
@Surround.Finally("skipInsideNew")
private void surround$exitSkipInsideNew(Callee callee, int a) {
Trace.add("exitSkipInsideNew " + a);
}
@Surround(method = "movedBody", id = "movedWhole")
private void surround$enterMovedWhole(int a) {
Trace.add("enterMovedWhole " + a);
}
@Surround.Finally("movedWhole")
private void surround$exitMovedWhole(int a) {
Trace.add("exitMovedWhole " + a);
}
@Surround(method = "movedBody", at = @At(value = "INVOKE", target = TWICE), id = "movedCall")
private void surround$enterMovedCall(Callee callee, int b, @Surround.Local(argsOnly = true) int a) {
@Surround.Skip boolean skip = a < 0;
Trace.add("enterMovedCall " + b + " " + a);
}
@Surround.Skipped("movedCall")
private int surround$skippedMovedCall(Callee callee, int b, @Surround.Local(argsOnly = true) int a) {
Trace.add("skippedMovedCall " + b + " " + a);
return b * 10;
}
@Surround.Finally("movedCall")
private void surround$exitMovedCall(Callee callee, int b, @Surround.Local(argsOnly = true) int a) {
Trace.add("exitMovedCall " + b + " " + a);
}
}
@@ -0,0 +1,389 @@
package com.gtnewhorizons.angelica.experimental.surround.integration.mixin;
import com.gtnewhorizons.angelica.experimental.surround.Surround;
import com.gtnewhorizons.angelica.experimental.surround.integration.WholeTarget;
import com.gtnewhorizons.angelica.experimental.surround.targets.Trace;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(value = WholeTarget.class, remap = false)
public class MixinWholeTarget {
@Surround(method = "compute(I)I", id = "compute")
private void surround$enter(int a) {
@Surround.Carry long token = Trace.nextToken();
Trace.add("enter " + a + " " + token);
}
@Surround.Finally("compute")
private void surround$exit(@Surround.Carry long token) {
Trace.add("exit " + token);
}
@Inject(method = "compute(I)I", at = @At("HEAD"))
private void surround$head(int a, CallbackInfoReturnable<Integer> cir) {
Trace.add("head " + a);
}
@Inject(method = "compute(I)I", at = @At("RETURN"))
private void surround$tail(int a, CallbackInfoReturnable<Integer> cir) {
Trace.add("tail " + cir.getReturnValue());
}
@Surround(method = "boom()V", id = "boom")
private void surround$enterBoom() {
@Surround.Carry long token = Trace.nextToken();
Trace.add("enterBoom " + token);
}
@Surround.Catch("boom")
private void surround$caughtBoomBroad(Throwable e, @Surround.Carry long token) {
Trace.add("caughtBroad " + token + " " + e.getMessage());
}
@Surround.Catch("boom")
private void surround$caughtBoom(IllegalStateException e, @Surround.Carry long token) {
Trace.add("caught " + token + " " + e.getMessage());
}
@Surround.Finally("boom")
private void surround$exitBoom(@Surround.Carry long token) {
Trace.add("exitBoom " + token);
}
@Inject(method = "boom()V", at = @At("HEAD"))
private void surround$boomHead(CallbackInfo ci) {
Trace.add("boomHead");
}
@Surround(method = {"alpha()V", "beta()V"}, id = "both")
private void surround$enterBoth() {
@Surround.Carry long token = Trace.nextToken();
Trace.add("enterBoth " + token);
}
@Surround.Finally("both")
private void surround$exitBoth(@Surround.Carry long token) {
Trace.add("exitBoth " + token);
}
@Surround(method = "lockedInstance(I)I", id = "instance")
private void surround$enterInstance(int a) {
Trace.add("enterInstance " + Thread.holdsLock(this));
}
@Surround.Finally("instance")
private void surround$exitInstance() {
Trace.add("exitInstance " + Thread.holdsLock(this));
}
@Surround(method = "run()V", id = "run")
private void surround$enterRun() {
Trace.add("enter");
}
@Unique
@Surround.Finally("run")
private void exit() {
Trace.add("handler exit");
}
@Surround(method = "priority(I)I", id = "priority")
private void surround$enterLow(int a) {
Trace.add("enterLow");
}
@Surround.Return("priority")
private int surround$returnLow(int result) {
Trace.add("returnLow " + result);
return result + 1;
}
@Surround.Finally("priority")
private void surround$exitLow() {
Trace.add("exitLow");
}
@Surround(method = "branching(I)I", id = "branching")
private void surround$enterBranching(int a) {
@Surround.Carry String sign = a < 0 ? "negative" : "positive";
Trace.add("enterBranching " + sign);
}
@Surround.Finally("branching")
private void surround$exitBranching(int a, @Surround.Carry String sign) {
Trace.add("exitBranching " + sign);
}
@Surround(method = "skippedVoid(I)V", id = "skippedVoid")
private void surround$enterSkippedVoid(int a) {
@Surround.Skip boolean skip = a < 0;
Trace.add("enterSkippedVoid " + a);
}
@Surround.Finally("skippedVoid")
private void surround$exitSkippedVoid() {
Trace.add("exitSkippedVoid");
}
@Surround(method = "reentrant(I)I", id = "reentrant")
private void surround$enterReentrant(int a) {
@Surround.Carry int direct = 0;
@Surround.Skip boolean skip = false;
if (!WholeTarget.guard) {
WholeTarget.guard = true;
direct = ((WholeTarget) (Object) this).reentrant(a + 10);
WholeTarget.guard = false;
skip = true;
}
Trace.add("enterReentrant " + a + " " + skip);
}
@Surround.Skipped("reentrant")
private int surround$skippedReentrant(int a, @Surround.Carry int direct) {
return direct * 2;
}
@Surround.Finally("reentrant")
private void surround$exitReentrant(int a) {
Trace.add("exitReentrant " + a);
}
@Surround(method = "skipAndReturn(Ljava/lang/String;)Ljava/lang/String;", id = "skipAndReturn")
private void surround$enterSkipAndReturn(String a) {
@Surround.Skip boolean skip = a.isEmpty();
Trace.add("enterSkipAndReturn " + a);
}
@Surround.Skipped("skipAndReturn")
private String surround$skippedSkipAndReturn(String a) {
Trace.add("skippedSkipAndReturn");
return null;
}
@Surround.Return("skipAndReturn")
private String surround$returnSkipAndReturn(String result, String a) {
Trace.add("returnSkipAndReturn " + result);
if (result.equals("x!")) {
throw new IllegalStateException("return");
}
return result + "?";
}
@Surround.Catch("skipAndReturn")
private void surround$caughtSkipAndReturn(Throwable e) {
Trace.add("caughtSkipAndReturn " + e.getMessage());
}
@Surround.Finally("skipAndReturn")
private void surround$exitSkipAndReturn(String a) {
Trace.add("exitSkipAndReturn " + a);
}
@Surround(method = "entryCatches(I)I", id = "entryCatches")
private static void surround$enterEntryCatches(int a) {
@Surround.Carry final boolean direct = !WholeTarget.guard;
if (direct) {
WholeTarget.guard = true;
try {
WholeTarget.entryCatches(a);
} catch (Throwable t) {
WholeTarget.guard = false;
Trace.add("entryRethrow " + t.getMessage());
throw t;
}
Trace.add("enterEntryCatches " + a);
}
}
@Surround.Finally("entryCatches")
private static void surround$exitEntryCatches(@Surround.Carry boolean direct) {
if (direct) {
WholeTarget.guard = false;
}
Trace.add("exitEntryCatches " + direct);
}
@Surround(method = "wideBody(DJ)D", id = "wideBody")
private static void surround$enterWideBody(double a, long b) {
@Surround.Carry long token = Trace.nextToken();
Trace.add("enter " + token);
}
@Surround.Finally("wideBody")
private static void surround$exitWideBody(@Surround.Carry long token) {
Trace.add("exit " + token);
}
@Surround(method = "throwingEntry(I)V", id = "throwingEntry")
private void surround$enterThrowingEntry(int a) {
Trace.add("enterThrows " + a);
if (a > 0) {
throw new IllegalArgumentException("entry failed");
}
}
@Surround.Finally("throwingEntry")
private void surround$exitThrowingEntry() {
Trace.add("exitNothing");
}
@Surround(method = "throwingFinally(II)I", id = "throwingFinally")
private void surround$enterThrowingFinally(int a, int b) {
@Surround.Carry long token = Trace.nextToken();
Trace.add("enter " + token);
}
@Surround.Finally("throwingFinally")
private void surround$exitThrowingFinally(@Surround.Carry long token) {
Trace.add("exitThrows " + token);
throw new UnsupportedOperationException("finally failed");
}
@Surround(method = "caught(I)I", id = "caught")
private void surround$enterCaught(int a) {
@Surround.Carry long token = Trace.nextToken();
Trace.add("enter " + token);
}
@Surround.Finally("caught")
private void surround$exitCaught(@Surround.Carry long token) {
Trace.add("exit " + token);
}
@Surround(method = "carryThree(I)I", id = "carryThree")
private void surround$enterCarryThree(int a) {
@Surround.Carry int count = a + 1;
@Surround.Carry long stamp = 100L + a;
@Surround.Carry String label = "L" + a;
Trace.add("enter " + count + " " + stamp + " " + label);
}
@Surround.Finally("carryThree")
private void surround$exitCarryThree(@Surround.Carry String label, @Surround.Carry long stamp, @Surround.Carry int count) {
Trace.add("exit " + count + " " + stamp + " " + label);
}
@Surround(method = "carryUninitialized(I)I", id = "carryUninitialized")
private void surround$enterCarryUninitialized(int a) {
@Surround.Carry String label = new String(a > 0 ? "pos" : "neg");
Trace.add("enter " + label);
}
@Surround.Finally("carryUninitialized")
private void surround$exitCarryUninitialized(@Surround.Carry String label) {
Trace.add("exit " + label);
}
@Surround(method = "carryGuarded(I)I", id = "carryGuarded")
private void surround$enterCarryGuarded(int a) {
@Surround.Carry int state = 0;
try {
state = WholeTarget.risky(a);
} catch (IllegalArgumentException e) {
state = -1;
Trace.add("recovered");
}
Trace.add("enter " + state);
}
@Surround.Finally("carryGuarded")
private void surround$exitCarryGuarded(@Surround.Carry int flag) {
Trace.add("exit " + flag);
}
@Surround(method = "carrySplitAtEnd(I)I", id = "carrySplitAtEnd")
private void surround$enterCarrySplitAtEnd(int a) {
@Surround.Carry int flag;
if (a > 0) {
flag = 1;
} else {
flag = 2;
}
}
@Surround.Finally("carrySplitAtEnd")
private void surround$exitCarrySplitAtEnd(@Surround.Carry int flag) {
Trace.add("exit " + flag);
}
@Surround(method = "carryWideBeforeFrame(I)I", id = "carryWideBeforeFrame")
private void surround$enterCarryWideBeforeFrame(int a) {
@Surround.Carry long stamp = a;
@Surround.Carry int count = a + 1;
if (a > 0) {
Trace.add("positive");
}
}
@Surround.Finally("carryWideBeforeFrame")
private void surround$exitCarryWideBeforeFrame(@Surround.Carry long stamp, @Surround.Carry int count) {
Trace.add("exit " + stamp + " " + count);
}
@Surround(method = "carryArrays(I)I", id = "carryArrays")
private void surround$enterCarryArrays(int a) {
@Surround.Carry("elements") int[] elements = {a};
String @Surround.Carry("names") [] names = {"n" + a};
}
@Surround.Finally("carryArrays")
private void surround$exitCarryArrays(@Surround.Carry("names") String[] names, @Surround.Carry("elements") int[] elements) {
Trace.add("exit " + names[0] + " " + elements[0]);
}
@Surround(method = "reused(I)I", id = "reused")
private void surround$enterReused(int a) {
{
int tmp = a + 5;
Trace.add("tmp " + tmp);
}
@Surround.Carry long prev = a + 7;
Trace.add("enter " + prev);
}
@Surround.Finally("reused")
private void surround$exitPrev(@Surround.Carry long prev) {
Trace.add("exit " + prev);
}
@Surround(method = "throwWide(I)V", id = "throwWide")
private void surround$enterThrowWide(int a) {
@Surround.Carry long first = a + 1L;
String scratch = "s" + a;
@Surround.Carry double second = a / 2.0d;
@Surround.Carry int third = a;
Trace.add("enter " + first + " " + second + " " + third + " " + scratch);
}
@Surround.Catch("throwWide")
private void surround$caughtThrowWide(Throwable e, @Surround.Carry long first, @Surround.Carry double second, @Surround.Carry int third) {
Trace.add("caught " + first + " " + second + " " + third);
}
@Surround.Finally("throwWide")
private void surround$exitThrowWide(@Surround.Carry double second, @Surround.Carry int third, @Surround.Carry long first) {
Trace.add("exit " + first + " " + second + " " + third);
}
@Surround(method = "throwObject(I)V", id = "throwObject")
private void surround$enterThrowObject(int a) {
@Surround.Carry Object prev = WholeTarget.asObject("o" + a);
Trace.add("enter " + prev);
}
@Surround.Catch("throwObject")
private void surround$caughtThrowObject(IllegalStateException e, @Surround.Carry String prev) {
Trace.add("caught " + prev.length() + " " + prev);
throw new UnsupportedOperationException("replaced", e);
}
@Surround.Finally("throwObject")
private void surround$exitThrowObject(@Surround.Carry String prev) {
Trace.add("exit " + prev.length() + " " + prev);
}
}
@@ -0,0 +1,26 @@
package com.gtnewhorizons.angelica.experimental.surround.integration.mixin;
import com.gtnewhorizons.angelica.experimental.surround.Surround;
import com.gtnewhorizons.angelica.experimental.surround.integration.WholeTarget;
import com.gtnewhorizons.angelica.experimental.surround.targets.Trace;
import org.spongepowered.asm.mixin.Mixin;
@Mixin(value = WholeTarget.class, priority = 1100, remap = false)
public class MixinWholeTargetHigh {
@Surround(method = "priority(I)I")
private void surround$enterHigh(int a) {
Trace.add("enterHigh");
}
@Surround.Return
private int surround$returnHigh(int result) {
Trace.add("returnHigh " + result);
return result * 10;
}
@Surround.Finally
private void surround$exitHigh() {
Trace.add("exitHigh");
}
}
@@ -0,0 +1,70 @@
package com.gtnewhorizons.angelica.experimental.surround.integration.mixinextras;
import com.gtnewhorizons.angelica.experimental.surround.Surround;
import com.gtnewhorizons.angelica.experimental.surround.integration.CallTarget.Callee;
import com.gtnewhorizons.angelica.experimental.surround.integration.WrapTarget;
import com.gtnewhorizons.angelica.experimental.surround.targets.Trace;
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.llamalad7.mixinextras.sugar.Share;
import com.llamalad7.mixinextras.sugar.ref.LocalIntRef;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import static com.gtnewhorizons.angelica.experimental.surround.integration.CallTarget.TWICE;
@Mixin(value = WrapTarget.class, remap = false)
public class MixinWrapTarget {
@Surround(method = "wrapped", id = "wrapped")
private void surround$enterWrapped(int a) {
Trace.add("entry " + a);
}
@Surround.Finally("wrapped")
private void surround$exitWrapped() {
Trace.add("finally");
}
@WrapMethod(method = "wrapped")
private int wrapWrapped(int a, Operation<Integer> original) {
Trace.add("wrap-before " + a);
final int result = original.call(a);
Trace.add("wrap-after " + result);
return result;
}
@WrapOperation(method = "originalOperands", at = @At(value = "INVOKE", target = TWICE))
private int wrapOperation$shiftArgument(Callee callee, int a, Operation<Integer> original) {
Trace.add("wrap " + a);
return original.call(callee, a + 100);
}
@Surround(method = "originalOperands", at = @At(value = "INVOKE", target = TWICE), id = "originalOperands")
private void surround$enterOriginalOperands(Callee callee, int a) {
Trace.add("enter " + a);
}
@Surround.Finally("originalOperands")
private void surround$exitOriginalOperands(Callee callee, int a) {
Trace.add("exit " + a);
}
@WrapMethod(method = "shifted")
private int wrapShifted(int a, Operation<Integer> original, @Share("seen") LocalIntRef seen) {
seen.set(a);
Trace.add("wrap " + a);
return original.call(a);
}
@Surround(method = "shifted", at = @At(value = "INVOKE", target = TWICE), id = "shifted")
private void surround$enterShifted(Callee callee, int value, @Surround.Local(ordinal = 1) int local, @Surround.Local String label) {
Trace.add("enterShifted " + value + " " + local + " " + label);
}
@Surround.Finally("shifted")
private void surround$exitShifted(@Surround.Local(name = "local") int local, @Surround.Local String label) {
Trace.add("exitShifted " + local + " " + label);
}
}
@@ -0,0 +1,32 @@
package com.gtnewhorizons.angelica.experimental.surround.targets;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
public final class Trace {
private static final List<String> EVENTS = new ArrayList<>();
private static long counter;
private Trace() {
}
public static void reset() {
EVENTS.clear();
counter = 0;
}
public static void add(String event) {
EVENTS.add(event);
}
public static void assertEvents(String... expected) {
assertArrayEquals(expected, EVENTS.toArray(new String[EVENTS.size()]), EVENTS.toString());
}
public static long nextToken() {
return ++counter;
}
}
@@ -0,0 +1,79 @@
// class version 52.0 (52)
// access flags 0x21
public class com/gtnewhorizons/angelica/experimental/surround/integration/CallTarget {
// access flags 0x1
public valueBelow(I)I
TRYCATCHBLOCK L0 L1 L2 java/lang/IllegalStateException
TRYCATCHBLOCK L3 L4 L5 null
TRYCATCHBLOCK L0 L1 L5 null
L6
ILOAD 1
ALOAD 0
GETFIELD com/gtnewhorizons/angelica/experimental/surround/integration/CallTarget.callee : Lcom/gtnewhorizons/angelica/experimental/surround/integration/CallTarget$Callee;
ILOAD 1
ISTORE 3
ASTORE 2
ISTORE 4
L7
INVOKESTATIC com/gtnewhorizons/angelica/experimental/surround/targets/Trace.nextToken ()J
LSTORE 5
L8
ILOAD 3
LLOAD 5
INVOKESTATIC com/gtnewhorizons/angelica/experimental/surround/integration/CallTarget.md$jvmdowngrader$concat$surround$enterValueBelow$1$3f (IJ)Ljava/lang/String;
INVOKESTATIC com/gtnewhorizons/angelica/experimental/surround/targets/Trace.add (Ljava/lang/String;)V
L9
ILOAD 4
ALOAD 2
ILOAD 3
L0
INVOKEVIRTUAL com/gtnewhorizons/angelica/experimental/surround/integration/CallTarget$Callee.explode (I)I
L1
ALOAD 0
ALOAD 2
ILOAD 3
LLOAD 5
INVOKESPECIAL com/gtnewhorizons/angelica/experimental/surround/integration/CallTarget.surround$exitValueBelow (Lcom/gtnewhorizons/angelica/experimental/surround/integration/CallTarget$Callee;IJ)V
GOTO L10
L2
FRAME FULL [com/gtnewhorizons/angelica/experimental/surround/integration/CallTarget I com/gtnewhorizons/angelica/experimental/surround/integration/CallTarget$Callee I I J] [java/lang/IllegalStateException]
ASTORE 7
ILOAD 4
L3
ALOAD 0
ALOAD 7
ALOAD 2
ILOAD 3
LLOAD 5
INVOKESPECIAL com/gtnewhorizons/angelica/experimental/surround/integration/CallTarget.surround$handleValueBelow (Ljava/lang/IllegalStateException;Lcom/gtnewhorizons/angelica/experimental/surround/integration/CallTarget$Callee;IJ)I
L4
ALOAD 0
ALOAD 2
ILOAD 3
LLOAD 5
INVOKESPECIAL com/gtnewhorizons/angelica/experimental/surround/integration/CallTarget.surround$exitValueBelow (Lcom/gtnewhorizons/angelica/experimental/surround/integration/CallTarget$Callee;IJ)V
GOTO L10
L5
FRAME SAME1 java/lang/Throwable
ASTORE 7
ALOAD 0
ALOAD 2
ILOAD 3
LLOAD 5
INVOKESPECIAL com/gtnewhorizons/angelica/experimental/surround/integration/CallTarget.surround$exitValueBelow (Lcom/gtnewhorizons/angelica/experimental/surround/integration/CallTarget$Callee;IJ)V
ALOAD 7
ATHROW
L10
FRAME FULL [com/gtnewhorizons/angelica/experimental/surround/integration/CallTarget I com/gtnewhorizons/angelica/experimental/surround/integration/CallTarget$Callee I I J] [I I]
BIPUSH 10
IMUL
IADD
IRETURN
L11
LOCALVARIABLE this Lcom/gtnewhorizons/angelica/experimental/surround/integration/CallTarget; L6 L11 0
LOCALVARIABLE a I L6 L11 1
MAXSTACK = 7
MAXLOCALS = 8
}
@@ -0,0 +1,71 @@
// class version 52.0 (52)
// access flags 0x21
public class com/gtnewhorizons/angelica/experimental/surround/integration/CallTarget {
// access flags 0x1
public valueSkip(I)I
TRYCATCHBLOCK L0 L1 L2 null
L3
ALOAD 0
GETFIELD com/gtnewhorizons/angelica/experimental/surround/integration/CallTarget.callee : Lcom/gtnewhorizons/angelica/experimental/surround/integration/CallTarget$Callee;
ILOAD 1
ISTORE 3
ASTORE 2
L4
INVOKESTATIC com/gtnewhorizons/angelica/experimental/surround/targets/Trace.nextToken ()J
LSTORE 4
L5
ILOAD 3
IFGE L6
ICONST_1
GOTO L7
L6
FRAME APPEND [com/gtnewhorizons/angelica/experimental/surround/integration/CallTarget$Callee I J]
ICONST_0
L7
FRAME SAME1 I
ISTORE 6
L8
ILOAD 3
LLOAD 4
INVOKESTATIC com/gtnewhorizons/angelica/experimental/surround/integration/CallTarget.md$jvmdowngrader$concat$surround$enterValueSkip$1$50 (IJ)Ljava/lang/String;
INVOKESTATIC com/gtnewhorizons/angelica/experimental/surround/targets/Trace.add (Ljava/lang/String;)V
L9
ILOAD 6
IFEQ L10
L0
ALOAD 0
ALOAD 2
ILOAD 3
LLOAD 4
INVOKESPECIAL com/gtnewhorizons/angelica/experimental/surround/integration/CallTarget.surround$skippedValueSkip (Lcom/gtnewhorizons/angelica/experimental/surround/integration/CallTarget$Callee;IJ)I
GOTO L1
L10
FRAME APPEND [I]
ALOAD 2
ILOAD 3
INVOKEVIRTUAL com/gtnewhorizons/angelica/experimental/surround/integration/CallTarget$Callee.twice (I)I
L1
FRAME SAME1 I
ALOAD 0
LLOAD 4
INVOKESPECIAL com/gtnewhorizons/angelica/experimental/surround/integration/CallTarget.surround$exitValueSkip (J)V
GOTO L11
L2
FRAME SAME1 java/lang/Throwable
ASTORE 7
ALOAD 0
LLOAD 4
INVOKESPECIAL com/gtnewhorizons/angelica/experimental/surround/integration/CallTarget.surround$exitValueSkip (J)V
ALOAD 7
ATHROW
L11
FRAME SAME1 I
IRETURN
L12
LOCALVARIABLE this Lcom/gtnewhorizons/angelica/experimental/surround/integration/CallTarget; L3 L12 0
LOCALVARIABLE a I L3 L12 1
MAXSTACK = 5
MAXLOCALS = 8
}
@@ -0,0 +1,93 @@
// class version 52.0 (52)
// access flags 0x21
public class com/gtnewhorizons/angelica/experimental/surround/integration/WholeTarget {
// access flags 0x1
public skipAndReturn(Ljava/lang/String;)Ljava/lang/String;
TRYCATCHBLOCK L0 L1 L2 java/lang/Throwable
TRYCATCHBLOCK L3 L4 L5 null
TRYCATCHBLOCK L1 L6 L5 null
TRYCATCHBLOCK L0 L1 L5 null
L7
ALOAD 1
INVOKEVIRTUAL java/lang/String.isEmpty ()Z
ISTORE 2
L8
ALOAD 1
INVOKESTATIC com/gtnewhorizons/angelica/experimental/surround/integration/WholeTarget.md$jvmdowngrader$concat$surround$enterSkipAndReturn$1 (Ljava/lang/String;)Ljava/lang/String;
INVOKESTATIC com/gtnewhorizons/angelica/experimental/surround/targets/Trace.add (Ljava/lang/String;)V
L9
ILOAD 2
IFEQ L10
L0
ALOAD 0
ALOAD 1
INVOKESPECIAL com/gtnewhorizons/angelica/experimental/surround/integration/WholeTarget.surround$skippedSkipAndReturn (Ljava/lang/String;)Ljava/lang/String;
GOTO L6
L10
FRAME APPEND [I]
ALOAD 0
ALOAD 1
INVOKESPECIAL com/gtnewhorizons/angelica/experimental/surround/integration/WholeTarget.skipAndReturn$surround (Ljava/lang/String;)Ljava/lang/String;
L1
ASTORE 4
ALOAD 0
ALOAD 4
ALOAD 1
INVOKESPECIAL com/gtnewhorizons/angelica/experimental/surround/integration/WholeTarget.surround$returnSkipAndReturn (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
L6
FRAME SAME1 java/lang/String
ALOAD 0
ALOAD 1
INVOKESPECIAL com/gtnewhorizons/angelica/experimental/surround/integration/WholeTarget.surround$exitSkipAndReturn (Ljava/lang/String;)V
GOTO L11
L2
FRAME SAME1 java/lang/Throwable
ASTORE 3
L3
ALOAD 0
ALOAD 3
INVOKESPECIAL com/gtnewhorizons/angelica/experimental/surround/integration/WholeTarget.surround$caughtSkipAndReturn (Ljava/lang/Throwable;)V
L4
ALOAD 0
ALOAD 1
INVOKESPECIAL com/gtnewhorizons/angelica/experimental/surround/integration/WholeTarget.surround$exitSkipAndReturn (Ljava/lang/String;)V
ALOAD 3
ATHROW
L5
FRAME SAME1 java/lang/Throwable
ASTORE 3
ALOAD 0
ALOAD 1
INVOKESPECIAL com/gtnewhorizons/angelica/experimental/surround/integration/WholeTarget.surround$exitSkipAndReturn (Ljava/lang/String;)V
ALOAD 3
ATHROW
L11
FRAME SAME1 java/lang/String
ARETURN
L12
LOCALVARIABLE this Lcom/gtnewhorizons/angelica/experimental/surround/integration/WholeTarget; L7 L12 0
LOCALVARIABLE a Ljava/lang/String; L7 L12 1
LOCALVARIABLE skip Z L8 L9 2
LOCALVARIABLE surround$error Ljava/lang/Throwable; L2 L12 3
LOCALVARIABLE surround$result Ljava/lang/String; L1 L6 4
MAXSTACK = 3
MAXLOCALS = 5
// access flags 0x2
private skipAndReturn$surround(Ljava/lang/String;)Ljava/lang/String;
L0
ALOAD 1
INVOKESTATIC com/gtnewhorizons/angelica/experimental/surround/integration/WholeTarget.jvmdowngrader$concat$skipAndReturn$1 (Ljava/lang/String;)Ljava/lang/String;
INVOKESTATIC com/gtnewhorizons/angelica/experimental/surround/targets/Trace.add (Ljava/lang/String;)V
L1
ALOAD 1
INVOKESTATIC com/gtnewhorizons/angelica/experimental/surround/integration/WholeTarget.jvmdowngrader$concat$skipAndReturn$2 (Ljava/lang/String;)Ljava/lang/String;
ARETURN
L2
LOCALVARIABLE this Lcom/gtnewhorizons/angelica/experimental/surround/integration/WholeTarget; L0 L2 0
LOCALVARIABLE a Ljava/lang/String; L0 L2 1
MAXSTACK = 1
MAXLOCALS = 2
}
@@ -0,0 +1,9 @@
{
"required": true,
"minVersion": "0.8",
"package": "com.gtnewhorizons.angelica.experimental.surround.integration.mixinextras",
"compatibilityLevel": "JAVA_8",
"mixins": [
"MixinWrapTarget"
]
}
@@ -0,0 +1,11 @@
{
"required": true,
"minVersion": "0.8",
"package": "com.gtnewhorizons.angelica.experimental.surround.integration.mixin",
"compatibilityLevel": "JAVA_8",
"mixins": [
"MixinCallTarget",
"MixinWholeTarget",
"MixinWholeTargetHigh"
]
}
@@ -0,0 +1,15 @@
extensions.getByType<JavaPluginExtension>().apply {
toolchain.languageVersion.set(JavaLanguageVersion.of(25))
}
repositories {
maven {
name = "GTNH Maven"
url = uri("https://nexus.gtnewhorizons.com/repository/public/")
}
maven {
name = "Minecraft Libraries"
url = uri("https://libraries.minecraft.net")
}
mavenCentral()
}
@@ -0,0 +1,13 @@
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
dependencies {
add("shadowImplementation", project(":experimental-surround-runtime")) { isTransitive = false }
add("mixinAnnotationProcessor", project(":experimental-surround-ap"))
}
tasks.named<ShadowJar>("shadowJar") {
minimize {
exclude(project(":experimental-surround-runtime"))
}
relocate("com.gtnewhorizons.angelica.experimental.surround", "com.gtnewhorizons.angelica.experimental.surround")
}
@@ -0,0 +1,3 @@
include("experimental-surround-runtime", "experimental-surround-ap")
project(":experimental-surround-runtime").projectDir = file("runtime")
project(":experimental-surround-ap").projectDir = file("ap")
-2084
View File
File diff suppressed because it is too large. Load diff
+362
View File
@@ -0,0 +1,362 @@
import xyz.wagyourtail.jvmdg.gradle.task.files.DowngradeFiles
plugins {
`java-library`
`java-test-fixtures`
`maven-publish`
}
apply(plugin = "xyz.wagyourtail.jvmdowngrader")
version = rootProject.version
val lwjglNatives: String by extra
java {
toolchain.languageVersion.set(JavaLanguageVersion.of(21))
withSourcesJar()
}
// RFG variant attributes: the root Angelica project uses RetroFuturaGradle which expects
// these attributes on dependencies. Without them, Gradle can't resolve the subproject variant.
val rfgObfAttr = Attribute.of("com.gtnewhorizons.retrofuturagradle.obfuscation", String::class.java)
val rfgTransformedAttr = Attribute.of("rfgDeobfuscatorTransformed", Boolean::class.javaObjectType)
configurations {
listOf("apiElements", "runtimeElements", "testFixturesApiElements", "testFixturesRuntimeElements").forEach { name ->
named(name) {
attributes {
attribute(rfgObfAttr, "mcp")
attribute(rfgTransformedAttr, true)
}
}
}
}
// Compile stubs: MC class signatures needed to resolve GTNHLib's type hierarchy.
// Not included in the published jar - real MC classes are on the classpath at runtime.
sourceSets {
create("stubs") {
java.srcDir("src/stubs/java")
}
}
// Exclude stubs from all output jars
tasks.withType<Jar>().configureEach {
exclude("net/minecraft/**")
}
val stubsElements by configurations.creating {
isCanBeConsumed = true
isCanBeResolved = false
attributes {
attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage::class.java, Usage.JAVA_RUNTIME))
attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category::class.java, Category.LIBRARY))
attribute(rfgObfAttr, "mcp")
attribute(rfgTransformedAttr, true)
}
outgoing {
capability("${project.group}:glsm-stubs:${project.version}")
}
}
artifacts {
add(stubsElements.name, sourceSets["stubs"].java.classesDirectory) {
type = "java-classes-directory"
builtBy(tasks.named("stubsClasses"))
}
}
repositories {
maven {
name = "Mojang"
url = uri("https://libraries.minecraft.net")
}
maven {
name = "GTNH Maven"
url = uri("https://nexus.gtnewhorizons.com/repository/public/")
}
maven {
name = "Forge"
url = uri("https://maven.minecraftforge.net/")
}
maven {
name = "taumc"
url = uri("https://maven.taumc.org/releases")
}
maven {
name = "WagYourTail"
url = uri("https://maven.wagyourtail.xyz/releases")
content { includeGroup("xyz.wagyourtail.jvmdowngrader") }
}
maven {
name = "Sonatype Snapshots"
url = uri("https://oss.sonatype.org/content/repositories/snapshots/")
}
maven {
name = "Maven Central Snapshots"
url = uri("https://central.sonatype.com/repository/maven-snapshots/")
}
mavenLocal()
mavenCentral()
mavenLocal()
}
val spirvTestRuntime by configurations.creating {
isCanBeConsumed = false
isCanBeResolved = true
}
dependencies {
// MC compile stubs
compileOnly(sourceSets["stubs"].output)
// LWJGL
compileOnly(libs.lwjgl2)
compileOnly(libs.lwjgl2.util)
compileOnly(libs.asm.tree.compile)
// Our Deps
api(libs.gtnhlib) { artifact { classifier = "dev" } }
api(libs.eventbus)
compileOnly(libs.celeritas.common) { isTransitive = false }
api(libs.glsl.transformation.lib) { exclude(module = "antlr4") }
api(libs.antlr4.runtime)
implementation(libs.jcpp)
// shaderc + SPIRV-Cross for the SpirvShaderTranslator (GLSL -> SPIR-V -> GLSL ES).
// TODO: Remove
compileOnly(libs.lwjgl3.shaderc)
compileOnly(libs.lwjgl3.spvc)
testImplementation(libs.lwjgl3.shaderc)
testImplementation(libs.lwjgl3.spvc)
// Host natives so the JNI wrapper can find libshaderc / libspirv-cross / liblwjgl.
testRuntimeOnly(libs.lwjgl3.shaderc) { artifact { classifier = lwjglNatives } }
testRuntimeOnly(libs.lwjgl3.spvc) { artifact { classifier = lwjglNatives } }
testRuntimeOnly(libs.lwjgl3)
testRuntimeOnly(libs.lwjgl3) { artifact { classifier = lwjglNatives } }
spirvTestRuntime(libs.lwjgl3.opengl)
// @Lwjgl3Aware annotation
compileOnly(libs.lwjgl3ify) { artifact { classifier = "dev" }; isTransitive = false }
compileOnly(libs.retrofuturabootstrap) { isTransitive = false }
compileOnly(libs.lombok) { isTransitive = false }
annotationProcessor(libs.lombok)
compileOnly(libs.annotations)
compileOnly(libs.log4j.api)
// Deps (normally from MC) - compile only, but needed at test runtime
compileOnly(libs.guava)
testRuntimeOnly(libs.guava)
testRuntimeOnly(libs.fastutil)
testRuntimeOnly(libs.joml) { isTransitive = false }
testFixturesCompileOnly(sourceSets["stubs"].output)
testFixturesApi(platform(libs.junit.bom))
testFixturesApi(libs.junit.jupiter.api)
testFixturesApi(libs.lwjgl2)
// Test
testImplementation(sourceSets["stubs"].output)
testImplementation(platform(libs.junit.bom))
testImplementation(libs.junit.jupiter)
testImplementation(libs.asm.tree.test)
testImplementation(libs.lwjgl2)
testRuntimeOnly(libs.junit.platform.launcher)
testRuntimeOnly(libs.log4j.core)
testImplementation(libs.celeritas.common) { isTransitive = false }
testRuntimeOnly(libs.jvmdowngrader.java.api) { artifact { classifier = "downgraded-8" } }
testRuntimeOnly(libs.commons.lang3)
}
val depsToDowngrade by configurations.creating {
isCanBeConsumed = false
isCanBeResolved = true
}
dependencies {
depsToDowngrade(libs.eventbus)
depsToDowngrade(libs.celeritas.common) { isTransitive = false }
}
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) {
inputCollection = depsToDowngrade
}
val downgradeMainClasses by tasks.registering(DowngradeFiles::class) {
inputCollection = sourceSets["main"].output.classesDirs.plus(sourceSets["stubs"].output.classesDirs)
classpath = sourceSets["main"].compileClasspath
(sourceSets["main"].output.classesDirs.files + sourceSets["stubs"].output.classesDirs.files)
.forEach { outputs.dir(temporaryDir.resolve(it.name)) }
dependsOn(tasks.named("classes"), tasks.named("stubsClasses"))
}
val downgradeTestFixturesClasses by tasks.registering(DowngradeFiles::class) {
inputCollection = sourceSets["testFixtures"].output.classesDirs
classpath = sourceSets["testFixtures"].compileClasspath
sourceSets["testFixtures"].output.classesDirs.files.forEach { outputs.dir(temporaryDir.resolve(it.name)) }
dependsOn(tasks.named("testFixturesClasses"))
}
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"))
}
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, downgradeTestFixturesClasses, downgradeDepsForTest)
val mainClassesDirs = sourceSets["main"].output.classesDirs
.plus(sourceSets["stubs"].output.classesDirs)
val testClassesDirs = sourceSets["test"].output.classesDirs
val downgradedTest = files(downgradeTestClasses.map { it.outputCollection })
val downgradedMain = files(downgradeMainClasses.map { it.outputCollection })
val downgradedDeps = files(downgradeDepsForTest.map { it.outputCollection })
setTestClassesDirs(downgradedTest)
classpath = downgradedTest
.plus(downgradedMain)
.plus(downgradedDeps)
.plus(files(downgradeTestFixturesClasses.map { it.outputCollection }))
.plus(sourceSets["test"].runtimeClasspath.minus(mainClassesDirs).minus(testClassesDirs)
.minus(sourceSets["testFixtures"].output.classesDirs).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 }}")
if (System.getProperty("os.name").lowercase().contains("mac")) {
jvmArgs("-Dapple.awt.UIElement=true")
}
}
tasks.test {
useJUnitPlatform {
excludeTags = GL_TASK_TAGS.values.toSet() + "lwjgl3"
}
filter {
excludeTestsMatching("com.gtnewhorizons.angelica.glsm.shader.SpirvShaderTranslator*Test")
}
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) }
val spirvTest by tasks.registering(Test::class) {
description = "Runs SpirvShaderTranslator* tests against native shaderc + spvc on Java 21."
group = "verification"
useJUnitPlatform()
javaLauncher = javaToolchains.launcherFor {
languageVersion = JavaLanguageVersion.of(21)
}
testClassesDirs = sourceSets["test"].output.classesDirs
classpath = sourceSets["test"].runtimeClasspath.filter { file ->
// Strip the LWJGL2 jars - they collide with LWJGL3's sealed org.lwjgl package.
!file.name.startsWith("lwjgl-2.") && !file.name.startsWith("lwjgl_util-") && !file.name.startsWith("lwjgl-platform-")
}.plus(spirvTestRuntime)
filter {
includeTestsMatching("com.gtnewhorizons.angelica.glsm.shader.SpirvShaderTranslator*Test")
includeTestsMatching("com.gtnewhorizons.angelica.glsm.ffp.FFPUniformBlockSpirvLayoutTest")
includeTestsMatching("com.gtnewhorizons.angelica.glsm.shader.SpirvCompilerDiskCacheTest")
}
// libshaderc.so / libspirv-cross.so are extracted by extractNatives3.
val extractNatives3 = rootProject.tasks.named("extractNatives3")
dependsOn(extractNatives3)
jvmArgs(
"-Djava.library.path=${extractNatives3.get().property("destinationFolder").let { (it as DirectoryProperty).asFile.get().path }}",
"--enable-native-access=ALL-UNNAMED"
)
testLogging {
showStandardStreams = true
}
}
tasks.named("check") { dependsOn(spirvTest) }
publishing {
publications {
create<MavenPublication>("mavenJava") {
groupId = "com.gtnewhorizons.angelica"
artifactId = "glsm"
version = rootProject.version.toString()
from(components["java"])
}
}
repositories {
if (System.getenv("MAVEN_USER") != null) {
maven {
name = "GTNHMaven"
url = uri(rootProject.findProperty("mavenPublishUrl")?.toString() ?: "https://nexus.gtnewhorizons.com/repository/releases/")
credentials {
username = System.getenv("MAVEN_USER")
password = System.getenv("MAVEN_PASSWORD")
}
}
}
}
}
@@ -0,0 +1,189 @@
package com.gtnewhorizons.angelica.config;
import net.minecraft.launchwrapper.Launch;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Locale;
public final class SystemProperties {
private static final Logger LOGGER = LogManager.getLogger("SystemProperties");
// Backend selection
public static final String KEY_USE_SDL_GPU = "angelica.sdlgpu.enable";
public static final boolean USE_SDL_GPU = Boolean.getBoolean(KEY_USE_SDL_GPU);
public static final String GL_PROFILE = System.getProperty("angelica.glProfile", "");
public static final boolean DISABLE_NVIDIA_WORKAROUND = Boolean.getBoolean("angelica.disableNvidiaWorkaround");
public static final boolean DISABLE_LTW_WORKAROUND = Boolean.getBoolean("angelica.disableLtwWorkaround");
// Redirector
public static final UnmappedGLMode UNMAPPED_GL = parseEnum("angelica.unmappedGL", UnmappedGLMode.WARN, UnmappedGLMode.WARN, UnmappedGLMode.class);
public static final boolean FFP_WARN_ON_UNSUPPORTED = Boolean.getBoolean("angelica.ffp.warnOnUnsupported");
// SDL-GPU
public static final String KEY_SDL_GPU_DRIVER = "angelica.sdlgpu.driver";
public static final String SDL_GPU_DRIVER = System.getProperty(KEY_SDL_GPU_DRIVER, "");
public static final boolean SDL_GPU_DEBUG = Boolean.getBoolean("angelica.sdlgpu.debug");
private static final SdlAssertionMode ENCODER_ASSERTIONS = parseEnum("angelica.sdlgpu.encoderAssertions", SdlAssertionMode.OFF, SdlAssertionMode.WARN, SdlAssertionMode.class);
public static final boolean SDL_ENCODER_ASSERTIONS = ENCODER_ASSERTIONS != SdlAssertionMode.OFF;
public static final boolean SDL_ENCODER_ASSERTIONS_FATAL = ENCODER_ASSERTIONS == SdlAssertionMode.FATAL;
public static final boolean SDL_VERIFY_STATE_SYNC = Boolean.getBoolean("angelica.sdlgpu.verifyStateSync");
public static final boolean SDL_VERIFY_PER_FRAME_UNIFORM_BLOCK = Boolean.getBoolean("angelica.sdlgpu.verifyPerFrameUniformBlock");
public static final int SDL_FRAMES_IN_FLIGHT = Integer.getInteger("angelica.sdlgpu.framesInFlight", 2);
public static final boolean DISABLE_SDL_PRESENTER_THREAD = Boolean.getBoolean("angelica.sdlgpu.disablePresenterThread");
public static final boolean SDL_DISABLE_IN_PASS_CLEAR = Boolean.getBoolean("angelica.sdlgpu.disableInPassClear");
// Tracy
public static final Boolean TRACY_OVERRIDE = parseBooleanOverride("angelica.tracy");
public static final Boolean TRACY_FINE_ZONES_OVERRIDE = parseBooleanOverride("angelica.tracy.fineZones");
public static final String TRACY_DIR = System.getProperty("angelica.tracy.dir", "angelica" + File.separator + "natives" + File.separator + "tracy");
public static final Integer TRACY_MAX_SRC_LOCS_OVERRIDE = Integer.getInteger("angelica.tracy.maxSrcLocs");
// Profiling
public static final String PROFILE_OPTS = System.getProperty("angelica.profile.opts", "event=wall,interval=5ms,alloc=512k,lock=10ms");
public static final String PROFILE_DIR = System.getProperty("angelica.profile.dir", "angelica" + File.separator + "profiles");
public static final String PROFILE_OUTPUT = System.getProperty("angelica.profile.output", "");
// Flyby
public static final String FLYBY_ROUTE = System.getProperty("angelica.flyby.route", "");
public static final boolean FLYBY_WAIT_FOR_TRACY = Boolean.getBoolean("angelica.flyby.waitForTracy");
public static final boolean FLYBY_WAIT_FOR_FOCUS = Boolean.getBoolean("angelica.flyby.waitForFocus");
public static final int FLYBY_WARMUP_TICKS = Integer.getInteger("angelica.flyby.warmupTicks", 400);
public static final int FLYBY_LENGTH = Integer.getInteger("angelica.flyby.length", 0); // Blocks for moving routes, ticks for stationary
public static final double FLYBY_SPEED = parseDouble("angelica.flyby.speed"); // Travel speed in blocks per tick
public static final boolean FLYBY_EXIT_WHEN_DONE = Boolean.getBoolean("angelica.flyby.exitWhenDone");
public static final long FLYBY_TIME_OF_DAY = Math.floorMod(Long.getLong("angelica.flyby.timeOfDay", 6000L), 24000L);
public static final String FLYBY_COMMANDS = System.getProperty("angelica.flyby.commands", "");
public static final String FLYBY_ORIGIN = System.getProperty("angelica.flyby.origin", "");
public static final FlybyPacing FLYBY_PACING = parseEnum("angelica.flyby.pacing", FlybyPacing.UNCAPPED, FlybyPacing.UNCAPPED, FlybyPacing.class);
public static final FlybyWeather FLYBY_WEATHER = parseEnum("angelica.flyby.weather", FlybyWeather.CLEAR, FlybyWeather.CLEAR, FlybyWeather.class);
public static final boolean FLYBY_JFR = Boolean.getBoolean("angelica.flyby.jfr");
public static final float FLYBY_PITCH = parseFloat("angelica.flyby.pitch");
public static final int FLYBY_SCREENSHOTS = Integer.getInteger("angelica.flyby.screenshots", 0);
public static final boolean FLYBY_DEBUG_HUD = Boolean.getBoolean("angelica.flyby.debugHud");
public static final String FLYBY_CRASH_TEST = System.getProperty("angelica.flyby.crashTest", "");
// Debug
public static final boolean LWJGL_DEBUG = Boolean.getBoolean("org.lwjgl.util.Debug");
public static final boolean DEBUG_MARKERS = Boolean.getBoolean("angelica.debug.markers");
public static final boolean DEBUG_F3_DETAIL = Boolean.getBoolean("angelica.debug.f3Detail");
public static final boolean ENABLE_TEST_BLOCKS = Boolean.getBoolean("angelica.debug.testBlocks");
public static final boolean DUMP_CLASS = Boolean.getBoolean("angelica.debug.dumpClass");
private static final boolean DUMP_SHADERS_PROP = Boolean.getBoolean("angelica.debug.dumpShaders");
private static final boolean DEBUG_TOOLING_PROP = Boolean.getBoolean("angelica.debug.tooling");
public static final boolean REDIRECTOR_LOGSPAM = Boolean.getBoolean("angelica.debug.redirectorLogspam");
public static final boolean DEBUG_DISPLAY_LISTS = Boolean.getBoolean("angelica.debug.displayLists");
public static final boolean FFP_TRACE = Boolean.getBoolean("angelica.debug.ffpTrace");
public static final boolean LOG_DISPLAY_LIST_COMPILATION = Boolean.getBoolean("angelica.debug.displayLists.compilation");
public static final boolean FORCE_ORPHAN_STREAMING = Boolean.getBoolean("angelica.debug.forceOrphanStreaming");
public static final boolean WEATHER_REBUILD_ALWAYS = Boolean.getBoolean("angelica.debug.weatherRebuildAlways");
public static final String SHADER_DUMP_ROOT = "angelica_dumps";
// Set by us, read by celeritas
public static final String KEY_CELERITAS_ENABLE_GL_DEBUG = "celeritas.enableGLDebug";
public static final String KEY_CELERITAS_LWJGL_SERVICE = "celeritas.lwjglService";
public static boolean dumpShaders() {
return DUMP_SHADERS_PROP || isDeobf();
}
public static boolean debugTooling() {
return DEBUG_TOOLING_PROP || isDeobf();
}
public static Path shaderDumpDir(String phase) {
return dumpShaders() ? Paths.get(SHADER_DUMP_ROOT, phase) : null;
}
public enum UnmappedGLMode {
OFF,
WARN,
FAIL,
STRICT;
public boolean detects() {
return this != OFF;
}
public boolean failsUnmapped() {
return this == FAIL || this == STRICT;
}
public boolean failsAwareUnroutable() {
return this == STRICT;
}
}
private enum SdlAssertionMode {
OFF,
WARN,
FATAL
}
public enum FlybyPacing {
UNCAPPED,
CONFIGURED
}
public enum FlybyWeather {
CLEAR,
RAIN,
THUNDER;
public boolean isRaining() {
return this != CLEAR;
}
public boolean isThundering() {
return this == THUNDER;
}
}
private static <E extends Enum<E>> E parseEnum(String key, E whenAbsent, E whenInvalid, Class<E> type) {
final String raw = System.getProperty(key);
if (raw == null || raw.isEmpty()) return whenAbsent;
try {
return Enum.valueOf(type, raw.trim().toUpperCase(Locale.ROOT));
} catch (IllegalArgumentException e) {
LOGGER.warn("Unrecognized value '{}' for -D{}; using {}. Valid values: {}", raw, key, whenInvalid, Arrays.toString(type.getEnumConstants()));
return whenInvalid;
}
}
private static Boolean parseBooleanOverride(String key) {
final String raw = System.getProperty(key);
return raw == null ? null : Boolean.parseBoolean(raw);
}
private static double parseDouble(String key) {
final String raw = System.getProperty(key);
if (raw == null || raw.isEmpty()) return 0.0D;
try {
return Double.parseDouble(raw);
} catch (NumberFormatException e) {
return 0.0D;
}
}
private static float parseFloat(String key) {
final String raw = System.getProperty(key);
if (raw == null || raw.isEmpty()) return Float.NaN;
try {
return Float.parseFloat(raw);
} catch (NumberFormatException e) {
return Float.NaN;
}
}
public static boolean isDeobf() {
return Launch.blackboard != null && Boolean.TRUE.equals(Launch.blackboard.get("fml.deobfuscatedEnvironment"));
}
private SystemProperties() {}
}
@@ -0,0 +1,76 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizons.angelica.config.SystemProperties;
import com.gtnewhorizons.angelica.glsm.profiling.Tracy;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Locale;
import java.util.function.UnaryOperator;
/** Decides whether instrumentation is worth emitting this frame. */
public final class CaptureGate {
private static final Logger LOGGER = LogManager.getLogger("Angelica");
private static final boolean IS_LINUX = System.getProperty("os.name", "").toLowerCase(Locale.ROOT).contains("linux");
private static final Path PROC_SELF_MAPS = Paths.get("/proc/self/maps");
private static final String[] CAPTURE_ENV = { "RENDERDOC_CAPTURE_KEYPRESS", "ENABLE_VULKAN_RENDERDOC_CAPTURE", "RENDERDOC_HOOK_EGL" };
private static final String[] CAPTURE_LIBS = { "renderdoc", "glxtrace", "egltrace", "apitrace" };
public static final boolean FORCED = SystemProperties.DEBUG_MARKERS || SystemProperties.LWJGL_DEBUG;
public static final boolean TOOL_ATTACHED = detectCaptureTool();
public static boolean markersThisFrame = true;
private CaptureGate() {}
public static void refresh() {
markersThisFrame = gateValue(FORCED, TOOL_ATTACHED, Tracy.isConnected());
}
static boolean gateValue(boolean forced, boolean toolAttached, boolean tracyConnected) {
return forced || toolAttached || tracyConnected;
}
public static boolean enabledAtStartup() {
return FORCED || TOOL_ATTACHED;
}
private static boolean detectCaptureTool() {
if (detectFromEnv(System::getenv)) {
LOGGER.info("Capture tool detected via environment; instrumentation enabled");
return true;
}
if (!IS_LINUX) return false;
try {
if (detectFromMaps(Files.readAllLines(PROC_SELF_MAPS, StandardCharsets.ISO_8859_1))) {
LOGGER.info("Capture tool library detected in process map; instrumentation enabled");
return true;
}
} catch (IOException e) {
LOGGER.warn("Could not read {} for capture tool detection: {}", PROC_SELF_MAPS, e.toString());
}
return false;
}
static boolean detectFromEnv(UnaryOperator<String> env) {
for (String key : CAPTURE_ENV) {
if (env.apply(key) != null) return true;
}
return false;
}
static boolean detectFromMaps(List<String> mapLines) {
for (int i = 0; i < mapLines.size(); i++) {
final String line = mapLines.get(i).toLowerCase(Locale.ROOT);
for (String lib : CAPTURE_LIBS) {
if (line.contains(lib)) return true;
}
}
return false;
}
}
@@ -0,0 +1,24 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizons.angelica.glsm.stacks.BooleanStateStack;
public final class ClipPlaneBooleanState extends BooleanStateStack {
private final GLContextState owner;
private final int index;
ClipPlaneBooleanState(GLContextState owner, int index, int glCap) {
super(glCap);
this.owner = owner;
this.index = index;
}
@Override
protected void onEnabledChanged() {
if (isEnabled()) {
owner.clipPlaneEnabledMask |= (1 << index);
} else {
owner.clipPlaneEnabledMask &= ~(1 << index);
}
}
}
@@ -0,0 +1,686 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormatElement.Usage;
import com.gtnewhorizons.angelica.config.SystemProperties;
import com.gtnewhorizons.angelica.glsm.backend.RenderBackend;
import com.gtnewhorizons.angelica.glsm.shader.ShaderDiskCache;
import com.gtnewhorizons.angelica.glsm.shader.SpirvShaderTranslator;
import org.antlr.v4.runtime.tree.ParseTree;
import org.antlr.v4.runtime.tree.TerminalNode;
import org.lwjgl.opengl.GL20;
import org.taumc.glsl.grammar.GLSLLexer;
import org.taumc.glsl.grammar.GLSLParser;
import org.taumc.glsl.ShaderParser;
import org.taumc.glsl.Transformer;
import java.io.IOException;
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.HashMap;
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;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static com.gtnewhorizons.angelica.glsm.backend.BackendManager.RENDER_BACKEND;
/**
* Compat transformation for mod shaders running under core profile.
*
* <p>Handles:
* <ul>
* <li>Matrix builtin replacement (gl_ModelViewMatrix, gl_ProjectionMatrix, etc.)</li>
* <li>Vertex attribute replacement (gl_Vertex, gl_Color, gl_MultiTexCoord0/1, gl_Normal)</li>
* <li>gl_TexCoord[N] varying array -> per-index in/out declarations</li>
* <li>Texture function renames (texture2D -> texture, etc.)</li>
* <li>Fragment output handling (gl_FragColor -> layout-qualified out declarations)</li>
* <li>Fog builtins (gl_Fog, gl_FogFragCoord)</li>
* <li>gl_FrontColor -> local variable in vertex shaders</li>
* <li>shadow2D/shadow2DLod -> texture/textureLod with vec4 wrapping</li>
* <li>Version upgrade to 330 core minimum</li>
* <li>Reserved word pre-parse renaming (texture-as-variable, sample, etc.)</li>
* </ul>
*/
public class CompatShaderTransformer {
private static final Pattern VERSION_PATTERN = Pattern.compile("#version[ \\t]+(\\d+)(?:[ \\t]+(\\w+))?");
private static final Pattern DEFINE_PATTERN = Pattern.compile("^\\s*#\\s*define\\s+.+$", Pattern.MULTILINE);
/** Compat builtins that trigger AST transformation. */
private static final Set<String> COMPAT_BUILTINS = Set.of(
"gl_ModelView", "gl_Projection", "gl_NormalMatrix", "gl_TextureMatrix",
"gl_FragColor", "gl_Fog", "gl_FrontColor", "gl_Color",
"gl_Vertex", "gl_MultiTexCoord", "gl_TexCoord", "gl_Normal", "ftransform",
"texture2D", "texture3D", "texelFetch2D", "texelFetch3D", "textureSize2D",
"shadow2D", "gl_FrontLightModelProduct",
"gl_LightSource", "gl_FrontMaterial"
);
private static final Pattern NEEDS_TRANSFORM_PATTERN = Pattern.compile(
String.join("|", COMPAT_BUILTINS) + "|\\b(?:attribute|varying)\\b"
);
private static final Map<String, String> MATRIX_RENAMES = Map.of(
"gl_ModelViewMatrix", "angelica_ModelViewMatrix",
"gl_ModelViewMatrixInverse", "angelica_ModelViewMatrixInverse",
"gl_ProjectionMatrix", "angelica_ProjectionMatrix",
"gl_ProjectionMatrixInverse", "angelica_ProjectionMatrixInverse",
"gl_NormalMatrix", "angelica_NormalMatrix"
);
private static final AtomicInteger dumpCounter = new AtomicInteger(0);
private static final int CACHE_SIZE = 256;
private record CacheKey(String source, boolean isFragment) {}
private static final Map<CacheKey, String> cache = Collections.synchronizedMap(
new LinkedHashMap<>(CACHE_SIZE, 0.75f, true) {
@Override protected boolean removeEldestEntry(Map.Entry<CacheKey, String> eldest) {
return size() > CACHE_SIZE;
}
});
private record EsCacheKey(String source, int glShaderType, boolean isFragment) {}
private static final Map<EsCacheKey, String> esCache = Collections.synchronizedMap(
new LinkedHashMap<>(CACHE_SIZE, 0.75f, true) {
@Override protected boolean removeEldestEntry(Map.Entry<EsCacheKey, String> eldest) {
return size() > CACHE_SIZE;
}
});
public static void clearCache() {
cache.clear();
esCache.clear();
}
public static void prewarm(String source, int glShaderType, boolean isFragment) {
if (!RenderSystem.isGLES()) return;
final EsCacheKey key = new EsCacheKey(source, glShaderType, isFragment);
if (esCache.containsKey(key)) return;
transform(source, glShaderType, isFragment);
}
/** Transform a mod shader for core profile; under GLES, also rewrites to ES 320. */
public static String transform(String source, boolean isFragment) {
return transform(source, isFragment ? GL20.GL_FRAGMENT_SHADER : GL20.GL_VERTEX_SHADER, isFragment);
}
/** Variant carrying the exact GL shader type so the GLES re-emit dispatches to the right shaderc kind (geometry/tess/compute). */
public static String transform(String source, int glShaderType, boolean isFragment) {
if (!source.isEmpty() && source.charAt(source.length() - 1) == '\0') {
source = source.substring(0, source.length() - 1);
}
final boolean gles = RenderSystem.isGLES();
if (gles) {
final EsCacheKey esKey = new EsCacheKey(source, glShaderType, isFragment);
final String esCached = esCache.get(esKey);
if (esCached != null) {
dumpShader(source, esCached, isFragment, needsTransformation(source));
return esCached;
}
}
final boolean needsTransform = needsTransformation(source);
String result;
if (!needsTransform) {
result = fixupVersion(source);
} else {
final CacheKey key = new CacheKey(source, isFragment);
final String cached = cache.get(key);
if (cached != null) {
result = cached;
} else {
final ShaderDiskCache.Key dk = ShaderDiskCache.isEnabled() ? diskKey(source, isFragment) : null;
final String disk = dk != null ? ShaderDiskCache.getString(dk) : null;
if (disk != null) {
result = disk;
cache.put(key, result);
} else {
try {
result = transformInternal(source, isFragment);
cache.put(key, result);
if (dk != null) ShaderDiskCache.putString(dk, result);
} catch (Exception e) {
GLStateManager.LOGGER.warn("CompatShaderTransformer: AST transformation failed, falling back to version fixup only", e);
result = fixupVersion(source);
}
}
}
}
if (gles) {
final String preGles = result;
try {
result = toGLES(result, glShaderType, isFragment);
} catch (RuntimeException e) {
dumpShader(source, preGles, isFragment, needsTransform);
throw e;
}
esCache.put(new EsCacheKey(source, glShaderType, isFragment), result);
}
dumpShader(source, result, isFragment, needsTransform);
return result;
}
private static int minGlslVersion() {
return RENDER_BACKEND != null ? RENDER_BACKEND.getMinGLSLVersion() : 330;
}
static ShaderDiskCache.Key diskKey(String source, boolean isFragment) {
return ShaderDiskCache.key("compat").str(source).b(isFragment).i(minGlslVersion());
}
public static boolean isCoreShader(String source) {
final Matcher vm = VERSION_PATTERN.matcher(source);
if (vm.find()) {
final int version = Integer.parseInt(vm.group(1));
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();
}
private static String transformInternal(String source, boolean isFragment) {
final Matcher versionMatcher = VERSION_PATTERN.matcher(source);
int declaredVersion = 110;
if (versionMatcher.find()) {
declaredVersion = Integer.parseInt(versionMatcher.group(1));
}
final int targetVersion = Math.max(declaredVersion, minGlslVersion());
// Pre-parse reserved word renaming - prevents ANTLR parse failures
source = GlslTransformUtils.replaceTexture(source);
source = GlslTransformUtils.renameReservedWords(source, targetVersion);
final GlslTransformUtils.QuietParse parsed = GlslTransformUtils.parseBothQuiet(source);
final Transformer transformer = new Transformer(parsed.full());
injectMatrixUniforms(transformer);
transformFog(transformer, isFragment, source);
// gl_FrontLightModelProduct.sceneColor -> angelica_SceneColor uniform
if (source.contains("gl_FrontLightModelProduct")) {
transformer.injectVariable("uniform vec4 angelica_SceneColor;");
transformer.replaceExpression("gl_FrontLightModelProduct.sceneColor", "angelica_SceneColor");
}
// gl_LightSource[i] -> struct + uniform array (both via injectFunction to keep struct before uniform)
if (source.contains("gl_LightSource")) {
transformer.injectFunction("struct angelica_LightSourceParameters {"
+ "vec4 ambient;vec4 diffuse;vec4 specular;vec4 position;vec4 halfVector;"
+ "vec3 spotDirection;float spotExponent;float spotCutoff;float spotCosCutoff;"
+ "float constantAttenuation;float linearAttenuation;float quadraticAttenuation;"
+ "};");
transformer.injectFunction("uniform angelica_LightSourceParameters angelica_LightSource[2];");
transformer.rename("gl_LightSource", "angelica_LightSource");
}
// gl_FrontMaterial -> struct + uniform (both via injectFunction to keep struct before uniform)
if (source.contains("gl_FrontMaterial")) {
transformer.injectFunction("struct angelica_MaterialParameters {"
+ "vec4 emission;vec4 ambient;vec4 diffuse;vec4 specular;float shininess;"
+ "};");
transformer.injectFunction("uniform angelica_MaterialParameters angelica_FrontMaterial;");
transformer.rename("gl_FrontMaterial", "angelica_FrontMaterial");
}
// gl_FrontColor (vertex) -> gl_Color (fragment) varying chain
// Vertex side is unconditional: fragment may read gl_Color without vertex writing gl_FrontColor.
// Uses angelica_ prefix to avoid colliding with user-declared varyings (e.g. "v_Color").
if (!isFragment) {
transformer.injectVariable("out vec4 angelica_FrontColor;");
transformer.rename("gl_FrontColor", "angelica_FrontColor");
transformer.prependMain("angelica_FrontColor = vec4(1.0);");
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;");
transformer.rename("gl_Color", "angelica_FrontColor");
}
}
// gl_TexCoord[N] varying array -> per-index in/out declarations
final Set<Integer> texCoordIndices = new HashSet<>();
transformer.renameArray("gl_TexCoord", "angelica_TexCoord", texCoordIndices);
for (Integer i : texCoordIndices) {
final String qualifier = isFragment ? "in" : "out";
transformer.injectVariable(qualifier + " vec4 angelica_TexCoord" + i + ";");
}
// Fragment output handling + alpha test discard
if (isFragment) {
transformFragmentOutputs(transformer);
}
// texture-as-variable collision handling
if (transformer.containsCall("texture") && transformer.hasVariable("texture")) {
transformer.rename("texture", "gtexture");
}
if (transformer.hasVariable("angelica_renamed_texture")) {
transformer.rename("angelica_renamed_texture", "gtexture");
}
transformer.renameFunctionCall(GlslTransformUtils.TEXTURE_RENAMES);
transformer.renameAndWrapShadow("shadow2D", "texture");
transformer.renameAndWrapShadow("shadow2DLod", "textureLod");
final String versionDirective = "#version " + targetVersion + " core\n";
final String extensions = VERSION_PATTERN.matcher(GlslTransformUtils.getFormattedShader(parsed.pre(), "")).replaceFirst("").trim();
// Preserve #define directives
final StringBuilder defines = new StringBuilder();
final Matcher defineMatcher = DEFINE_PATTERN.matcher(source);
while (defineMatcher.find()) {
defines.append(defineMatcher.group().trim()).append('\n');
}
final String header = versionDirective + (extensions.isEmpty() ? "" : "\n" + extensions) + (defines.isEmpty() ? "" : "\n" + defines);
final StringBuilder result = new StringBuilder();
transformer.mutateTree(tree -> result.append(GlslTransformUtils.getFormattedShader(tree, header)));
// Restore pre-parse renames
String output = GlslTransformUtils.restoreReservedWords(result.toString());
// Core profile: attribute -> in, varying -> out (vertex) / in (fragment)
output = fixupQualifiers(output, isFragment);
return output;
}
/**
* Inject matrix uniforms and rename compat builtins.
*/
private static void injectMatrixUniforms(Transformer transformer) {
transformer.injectVariable("uniform mat4 angelica_ModelViewMatrix;");
transformer.injectVariable("uniform mat4 angelica_ModelViewMatrixInverse;");
transformer.injectVariable("uniform mat4 angelica_ProjectionMatrix;");
transformer.injectVariable("uniform mat4 angelica_ProjectionMatrixInverse;");
transformer.injectVariable("uniform mat3 angelica_NormalMatrix;");
transformer.injectVariable("uniform mat4 angelica_LightmapTextureMatrix;");
transformer.rename(MATRIX_RENAMES);
// Expression replacements
transformer.replaceExpression("gl_ModelViewProjectionMatrix", "(angelica_ProjectionMatrix * angelica_ModelViewMatrix)");
transformer.replaceExpression("gl_TextureMatrix[0]", "mat4(1.0)");
transformer.replaceExpression("gl_TextureMatrix[1]", "angelica_LightmapTextureMatrix");
transformer.replaceExpression(
"gl_TextureMatrix",
"mat4[8](mat4(1.0), angelica_LightmapTextureMatrix, mat4(1.0), mat4(1.0), mat4(1.0), mat4(1.0), mat4(1.0), mat4(1.0))");
}
/**
* Transform fragment outputs for core profile.
*/
private static void transformFragmentOutputs(Transformer transformer) {
if (transformer.containsCall("gl_FragColor")) {
transformer.replaceExpression("gl_FragColor", "gl_FragData[0]");
}
final Set<Integer> found = new HashSet<>();
transformer.renameArray("gl_FragData", "angelica_FragData", found);
for (Integer i : found) {
transformer.injectVariable("layout (location = " + i + ") out vec4 angelica_FragData" + i + ";");
}
// Core profile: GL_ALPHA_TEST is removed - inject runtime discard using GLSM-tracked alpha reference (uploaded by CompatUniformManager).
if (found.contains(0)) {
transformer.injectVariable("uniform float angelica_currentAlphaTest;");
transformer.appendMain("if (angelica_FragData0.a <= angelica_currentAlphaTest) discard;");
}
}
private static void transformFog(Transformer transformer, boolean isFragment, String source) {
// Vertex side is unconditional: fragment may read gl_FogFragCoord without vertex writing it
transformer.rename("gl_FogFragCoord", "angelica_FogFragCoord");
if (!isFragment) {
transformer.injectVariable("out float angelica_FogFragCoord;");
transformer.prependMain("angelica_FogFragCoord = 0.0;");
} else {
if (source.contains("gl_FogFragCoord")) {
transformer.injectVariable("in float angelica_FogFragCoord;");
}
}
transformer.rename("gl_Fog", "angelica_Fog");
transformer.injectVariable("uniform float angelica_FogDensity;");
transformer.injectVariable("uniform float angelica_FogStart;");
transformer.injectVariable("uniform float angelica_FogEnd;");
transformer.injectVariable("uniform vec4 angelica_FogColor;");
transformer.injectFunction("struct angelica_FogParameters {vec4 color;float density;float start;float end;float scale;};");
transformer.injectFunction("angelica_FogParameters angelica_Fog;");
transformer.prependMain("angelica_Fog = angelica_FogParameters("
+ "angelica_FogColor, angelica_FogDensity, angelica_FogStart, angelica_FogEnd, "
+ "1.0 / (angelica_FogEnd - angelica_FogStart));");
}
/**
* Replace removed FFP vertex attributes with explicit {@code in} declarations at core profile attribute locations.
*/
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 (ffp.color()) {
transformer.injectVariable("layout(location = 1) in vec4 angelica_Color;");
transformer.rename("gl_Color", "angelica_Color");
}
if (ffp.uv0()) {
transformer.injectVariable("layout(location = 2) in vec4 angelica_MultiTexCoord0;");
transformer.rename("gl_MultiTexCoord0", "angelica_MultiTexCoord0");
}
if (ffp.uv1()) {
transformer.injectVariable("layout(location = 3) in vec4 angelica_MultiTexCoord1;");
transformer.rename("gl_MultiTexCoord1", "angelica_MultiTexCoord1");
}
if (ffp.normal()) {
transformer.injectVariable("layout(location = 4) in vec3 angelica_Normal;");
transformer.rename("gl_Normal", "angelica_Normal");
}
if (source.contains("ftransform")) {
transformer.replaceExpression("ftransform()", "(angelica_ProjectionMatrix * angelica_ModelViewMatrix * angelica_Vertex)");
}
}
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) {
final Path dir = SystemProperties.shaderDumpDir("compat");
if (dir == null) return;
final int id = dumpCounter.getAndIncrement();
final String suffix = isFragment ? ".frag.glsl" : ".vert.glsl";
try {
Files.createDirectories(dir);
// Capture caller info for identification
final String caller = identifyCaller();
final String header = "// Compat shader dump #" + id + " (" + (isFragment ? "fragment" : "vertex") + ")"
+ "\n// Transformed: " + wasTransformed
+ "\n// Caller: " + caller + "\n\n";
Files.writeString(dir.resolve(id + "_original" + suffix), header + original, StandardCharsets.UTF_8);
Files.writeString(dir.resolve(id + "_transformed" + suffix), header + transformed, StandardCharsets.UTF_8);
} catch (IOException e) {
GLStateManager.LOGGER.warn("Failed to dump compat shader: {}", e.getMessage());
}
}
private static String identifyCaller() {
for (StackTraceElement frame : Thread.currentThread().getStackTrace()) {
final String cls = frame.getClassName();
if (!cls.startsWith("com.gtnewhorizons.angelica.glsm.")
&& !cls.startsWith("java.")
&& !cls.equals("org.lwjgl.opengl.GL20")) {
return cls + "." + frame.getMethodName() + ":" + frame.getLineNumber();
}
}
return "unknown";
}
private static final Pattern ATTRIBUTE_PATTERN = Pattern.compile("\\battribute\\b");
private static final Pattern VARYING_PATTERN = Pattern.compile("\\bvarying\\b");
/** Replace legacy storage qualifiers removed in core profile. Safe on AST-serialized output (no comments). */
public static String fixupQualifiers(String source, boolean isFragment) {
source = ATTRIBUTE_PATTERN.matcher(source).replaceAll("in");
source = VARYING_PATTERN.matcher(source).replaceAll(isFragment ? "in" : "out");
return source;
}
private static final String PRECISION_QUALIFIER = "(?:highp\\s+|mediump\\s+|lowp\\s+)?";
private static final Pattern FRAG_IN_COLOR = Pattern.compile("\\bin\\s+" + PRECISION_QUALIFIER + "(vec[234])\\s+angelica_FrontColor\\b");
private static final Pattern FRAG_IN_TEXCOORD = Pattern.compile("\\bin\\s+" + PRECISION_QUALIFIER + "(vec[234])\\s+angelica_TexCoord(\\d+)\\b");
private static final Pattern FRAG_IN_FOGCOORD = Pattern.compile("\\bin\\s+" + PRECISION_QUALIFIER + "float\\s+angelica_FogFragCoord\\b");
/**
* Generate a passthrough vertex shader for a fragment-only program.
*
* @param fragmentSource the transformed fragment shader source
* @return a complete vertex shader source string
*/
public static String generatePassthroughVertexShader(String fragmentSource) {
final StringBuilder sb = new StringBuilder(512);
sb.append("#version 330 core\n\n");
// Always have position
sb.append("layout(location = 0) in vec4 a_Position;\n");
// Matrix uniforms for position transform
sb.append("uniform mat4 angelica_ModelViewMatrix;\n");
sb.append("uniform mat4 angelica_ProjectionMatrix;\n");
final StringBuilder varyings = new StringBuilder();
final StringBuilder assignments = new StringBuilder();
final Matcher colorMatcher = FRAG_IN_COLOR.matcher(fragmentSource);
if (colorMatcher.find()) {
final String colorType = colorMatcher.group(1);
sb.append("layout(location = 1) in ").append(colorType).append(" a_Color;\n");
varyings.append("out ").append(colorType).append(" angelica_FrontColor;\n");
assignments.append(" angelica_FrontColor = a_Color;\n");
}
final Matcher texCoordMatcher = FRAG_IN_TEXCOORD.matcher(fragmentSource);
final Map<Integer, String> texCoordTypes = new HashMap<>();
while (texCoordMatcher.find()) {
texCoordTypes.putIfAbsent(Integer.parseInt(texCoordMatcher.group(2)), texCoordMatcher.group(1));
}
for (Map.Entry<Integer, String> e : texCoordTypes.entrySet()) {
final int i = e.getKey();
final String type = e.getValue();
// PRIMARY_UV=2, SECONDARY_UV=3. Only indices 0 and 1 are supported; higher indices collide at location 3.
final int location = i == 0 ? 2 : 3;
sb.append("layout(location = ").append(location).append(") in ").append(type).append(" a_TexCoord").append(i).append(";\n");
varyings.append("out ").append(type).append(" angelica_TexCoord").append(i).append(";\n");
assignments.append(" angelica_TexCoord").append(i).append(" = a_TexCoord").append(i).append(";\n");
}
if (FRAG_IN_FOGCOORD.matcher(fragmentSource).find()) {
varyings.append("out float angelica_FogFragCoord;\n");
assignments.append(" angelica_FogFragCoord = abs((angelica_ModelViewMatrix * a_Position).z);\n");
}
sb.append(varyings);
sb.append("\nvoid main() {\n");
sb.append(" gl_Position = angelica_ProjectionMatrix * angelica_ModelViewMatrix * a_Position;\n");
sb.append(assignments);
sb.append("}\n");
return sb.toString();
}
/** 330-core -> GLSL ES 320 via shaderc + SPIRV-Cross. Throws on shaderc failure. */
public static String toGLES(String source, int glShaderType, boolean isFragment) {
final Matcher m = VERSION_PATTERN.matcher(source);
if (m.find() && "es".equalsIgnoreCase(m.group(2))) return source;
if (glShaderType == 0) throw new IllegalArgumentException("toGLES requires a valid GL shader type");
final String translated = SpirvShaderTranslator.glslToGlslEs(source, glShaderType, "compat");
if (translated == null) {
throw new IllegalStateException("SpirvShaderTranslator failed for shader kind 0x"
+ Integer.toHexString(glShaderType) + "; see preceding shaderc error + run/client/angelica_dumps/compat/");
}
return translated;
}
public static String toGLES(String source, boolean isFragment) {
return toGLES(source, isFragment ? GL20.GL_FRAGMENT_SHADER : GL20.GL_VERTEX_SHADER, isFragment);
}
/** Ensure #version is at least 330 core, strip 'compatibility' profile. */
private static String fixupVersion(String source) {
final Matcher m = VERSION_PATTERN.matcher(source);
if (!m.find()) {
return "#version 330 core\n" + source;
}
final int version = Integer.parseInt(m.group(1));
final String profile = m.group(2);
if (version >= 330 && !"compatibility".equals(profile)) {
if ("core".equals(profile)) return source;
return m.replaceFirst("#version " + version + " core");
}
final int targetVersion = Math.max(version, 330);
return m.replaceFirst("#version " + targetVersion + " core");
}
}
@@ -0,0 +1,469 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizons.angelica.glsm.stacks.FogStateStack;
import com.gtnewhorizons.angelica.glsm.stacks.LightStateStack;
import com.gtnewhorizons.angelica.glsm.states.ClipPlaneState;
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 java.nio.FloatBuffer;
import static com.gtnewhorizons.angelica.glsm.backend.BackendManager.RENDER_BACKEND;
/**
* Per-program compat uniform location cache and upload manager.
*
* <p>Manages {@code angelica_*} uniforms injected by {@link CompatShaderTransformer} and {@code iris_*} matrix uniforms used by Iris/celeritas shader
* programs.
*/
public class CompatUniformManager {
// Uniform location indices within the cached array (package-private for tests)
static final int LOC_MODELVIEW = 0;
static final int LOC_MODELVIEW_INVERSE = 1;
static final int LOC_PROJECTION = 2;
static final int LOC_PROJECTION_INVERSE = 3;
static final int LOC_NORMAL = 4;
static final int LOC_LIGHTMAP_TEXTURE_MATRIX = 5;
static final int LOC_FOG_DENSITY = 6;
static final int LOC_FOG_START = 7;
static final int LOC_FOG_END = 8;
static final int LOC_FOG_COLOR = 9;
static final int LOC_IRIS_MODELVIEW = 10;
static final int LOC_IRIS_PROJECTION = 11;
static final int LOC_IRIS_NORMAL = 12;
static final int LOC_IRIS_LIGHTMAP_TEXTURE_MATRIX = 13;
static final int LOC_IRIS_TEXTURE_MATRIX = 14;
static final int LOC_ALPHA_TEST_REF = 15;
static final int LOC_SCENE_COLOR = 16;
static final int LOC_CLIP_PLANES = 17;
static final int LOC_CLIP_PLANES_ENABLED = 18;
// Light source fields: 12 per light × 2 lights = 24 locations
static final int LIGHT_FIELDS = 12;
static final int LF_AMBIENT = 0, LF_DIFFUSE = 1, LF_SPECULAR = 2, LF_POSITION = 3;
static final int LF_HALF_VECTOR = 4, LF_SPOT_DIRECTION = 5, LF_SPOT_EXPONENT = 6;
static final int LF_SPOT_CUTOFF = 7, LF_SPOT_COS_CUTOFF = 8;
static final int LF_CONSTANT_ATTEN = 9, LF_LINEAR_ATTEN = 10, LF_QUADRATIC_ATTEN = 11;
static final int LOC_LIGHT_BASE = 19;
// Material fields: 5 locations
static final int MF_EMISSION = 0, MF_AMBIENT = 1, MF_DIFFUSE = 2, MF_SPECULAR = 3, MF_SHININESS = 4;
static final int MAT_FIELDS = 5;
static final int LOC_MAT_BASE = LOC_LIGHT_BASE + 2 * LIGHT_FIELDS; // 43
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",
"spotDirection", "spotExponent", "spotCutoff", "spotCosCutoff",
"constantAttenuation", "linearAttenuation", "quadraticAttenuation",
};
private static final String[] MAT_FIELD_NAMES = {
"emission", "ambient", "diffuse", "specular", "shininess",
};
private static final String[] UNIFORM_NAMES;
static {
UNIFORM_NAMES = new String[LOC_COUNT];
UNIFORM_NAMES[LOC_MODELVIEW] = "angelica_ModelViewMatrix";
UNIFORM_NAMES[LOC_MODELVIEW_INVERSE] = "angelica_ModelViewMatrixInverse";
UNIFORM_NAMES[LOC_PROJECTION] = "angelica_ProjectionMatrix";
UNIFORM_NAMES[LOC_PROJECTION_INVERSE] = "angelica_ProjectionMatrixInverse";
UNIFORM_NAMES[LOC_NORMAL] = "angelica_NormalMatrix";
UNIFORM_NAMES[LOC_LIGHTMAP_TEXTURE_MATRIX] = "angelica_LightmapTextureMatrix";
UNIFORM_NAMES[LOC_FOG_DENSITY] = "angelica_FogDensity";
UNIFORM_NAMES[LOC_FOG_START] = "angelica_FogStart";
UNIFORM_NAMES[LOC_FOG_END] = "angelica_FogEnd";
UNIFORM_NAMES[LOC_FOG_COLOR] = "angelica_FogColor";
UNIFORM_NAMES[LOC_IRIS_MODELVIEW] = "iris_ModelViewMatrix";
UNIFORM_NAMES[LOC_IRIS_PROJECTION] = "iris_ProjectionMatrix";
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]";
UNIFORM_NAMES[LOC_CLIP_PLANES_ENABLED] = "angelica_ClipPlanesEnabled";
for (int li = 0; li < 2; li++) {
for (int fi = 0; fi < LIGHT_FIELDS; fi++) {
UNIFORM_NAMES[LOC_LIGHT_BASE + li * LIGHT_FIELDS + fi] = "angelica_LightSource[" + li + "]." + LIGHT_FIELD_NAMES[fi];
}
}
for (int fi = 0; fi < MAT_FIELDS; fi++) {
UNIFORM_NAMES[LOC_MAT_BASE + fi] = "angelica_FrontMaterial." + MAT_FIELD_NAMES[fi];
}
}
/** 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;
GLContextState owner;
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);
private static final FloatBuffer mat3Buf = BufferUtils.createFloatBuffer(9);
private static final FloatBuffer vec4Buf = BufferUtils.createFloatBuffer(4);
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;
static {
lightmapMatrixBuf = BufferUtils.createFloatBuffer(16);
new Matrix4f().scale(LIGHTMAP_SCALE).translate(8.0f, 8.0f, 8.0f).get(lightmapMatrixBuf);
}
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;
for (int i = 0; i < LOC_COUNT; i++) {
locs[i] = RENDER_BACKEND.getUniformLocation(program, UNIFORM_NAMES[i]);
if (locs[i] != -1) hasAny = true;
}
if (hasAny) {
final ProgramUniforms uniforms = new ProgramUniforms(locs);
final boolean locked = GLStateManager.acquireDrawLock();
try {
programUniforms.put(program, uniforms);
} finally {
if (locked) GLStateManager.releaseDrawLock();
}
GLStateManager.LOGGER.debug("CompatUniformManager: program {} has compat uniforms", program);
}
}
/**
* Uploads the emulated uniforms.
*
* @return Whether the program needs emulation (FFP or Iris Program)
*/
public static boolean refreshCompatUniforms(int program, GLContextState glCtx) {
final ProgramUniforms pu = programUniforms.get(program);
if (pu == null) return false;
final int[] locs = pu.locs;
if (pu.owner != glCtx) {
pu.owner = glCtx;
pu.lastMvGen = pu.lastMvLinearGen = pu.lastProjGen = pu.lastTexMatGen = -1;
pu.lastFragmentGen = pu.lastLightingGen = pu.lastClipPlaneGen = -1;
}
// Matrix uniforms — skip if this program's storage already holds the current generation
final int mvGen = glCtx.mvGeneration;
final int mvLinearGen = glCtx.mvLinearGeneration;
final int projGen = glCtx.projGeneration;
final int texMatGen = glCtx.texMatrixGeneration;
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, 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 = glCtx.fragmentGeneration;
if (fragGen != pu.lastFragmentGen) {
pu.lastFragmentGen = fragGen;
uploadFragmentUniforms(locs);
}
// Lighting-derived uniforms (scene color, light sources, material)
final int litGen = glCtx.lightingGeneration;
if (litGen != pu.lastLightingGen) {
pu.lastLightingGen = litGen;
if (locs[LOC_SCENE_COLOR] != -1) uploadSceneColor(locs);
uploadLightSources(locs);
uploadMaterial(locs);
}
// 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 = glCtx.clipPlaneGeneration;
if (cpGen != pu.lastClipPlaneGen) {
pu.lastClipPlaneGen = cpGen;
uploadClipPlanes(locs);
}
}
return true;
}
private static void uploadMatrices(int[] locs, boolean mvChanged, boolean mvLinearChanged, boolean projChanged, boolean texMatChanged) {
final Matrix4f mv = GLStateManager.getModelViewMatrix();
final Matrix4f proj = GLStateManager.getProjectionMatrix();
if (mvChanged) {
// ModelView
if (locs[LOC_MODELVIEW] != -1 || locs[LOC_IRIS_MODELVIEW] != -1) {
mv.get(mat4Buf);
if (locs[LOC_MODELVIEW] != -1) {
RENDER_BACKEND.uniformMatrix4(locs[LOC_MODELVIEW], false, mat4Buf);
}
if (locs[LOC_IRIS_MODELVIEW] != -1) {
RENDER_BACKEND.uniformMatrix4(locs[LOC_IRIS_MODELVIEW], false, mat4Buf);
}
}
// ModelView Inverse
if (locs[LOC_MODELVIEW_INVERSE] != -1 || locs[LOC_IRIS_MODELVIEW_INVERSE] != -1) {
mv.invert(scratchMatrix);
scratchMatrix.get(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);
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);
}
}
}
if (projChanged) {
// Projection
if (locs[LOC_PROJECTION] != -1 || locs[LOC_IRIS_PROJECTION] != -1) {
proj.get(mat4Buf);
if (locs[LOC_PROJECTION] != -1) {
RENDER_BACKEND.uniformMatrix4(locs[LOC_PROJECTION], false, mat4Buf);
}
if (locs[LOC_IRIS_PROJECTION] != -1) {
RENDER_BACKEND.uniformMatrix4(locs[LOC_IRIS_PROJECTION], false, mat4Buf);
}
}
// Projection Inverse
if (locs[LOC_PROJECTION_INVERSE] != -1 || locs[LOC_IRIS_PROJECTION_INVERSE] != -1) {
proj.invert(scratchMatrix);
scratchMatrix.get(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);
}
}
}
if (texMatChanged) {
// Iris lightmap texture matrix (constant)
if (locs[LOC_IRIS_LIGHTMAP_TEXTURE_MATRIX] != -1) {
RENDER_BACKEND.uniformMatrix4(locs[LOC_IRIS_LIGHTMAP_TEXTURE_MATRIX], false, lightmapMatrixBuf);
}
// angelica_LightmapTextureMatrix — actual GLSM texture unit 1 matrix (set by enableLightmap())
if (locs[LOC_LIGHTMAP_TEXTURE_MATRIX] != -1) {
GLStateManager.getTextures().getTextureUnitMatrix(1).get(mat4Buf);
RENDER_BACKEND.uniformMatrix4(locs[LOC_LIGHTMAP_TEXTURE_MATRIX], false, mat4Buf);
}
// iris_TextureMatrix — texture unit 0 matrix (animated by glints, etc.)
if (locs[LOC_IRIS_TEXTURE_MATRIX] != -1) {
GLStateManager.getTextures().getTextureUnitMatrix(0).get(mat4Buf);
RENDER_BACKEND.uniformMatrix4(locs[LOC_IRIS_TEXTURE_MATRIX], false, mat4Buf);
}
}
}
private static void uploadFragmentUniforms(int[] locs) {
// Fog uniforms
FogStateStack fog = GLStateManager.getFogState();
if (locs[LOC_FOG_DENSITY] != -1) {
RENDER_BACKEND.uniform1f(locs[LOC_FOG_DENSITY], Math.max(0.0f, fog.getDensity()));
}
if (locs[LOC_FOG_START] != -1) {
RENDER_BACKEND.uniform1f(locs[LOC_FOG_START], fog.getStart());
}
if (locs[LOC_FOG_END] != -1) {
RENDER_BACKEND.uniform1f(locs[LOC_FOG_END], fog.getEnd());
}
if (locs[LOC_FOG_COLOR] != -1) {
vec4Buf.clear();
vec4Buf.put((float) fog.getFogColor().x).put((float) fog.getFogColor().y).put((float) fog.getFogColor().z).put(fog.getFogAlpha());
vec4Buf.flip();
RENDER_BACKEND.uniform4(locs[LOC_FOG_COLOR], vec4Buf);
}
// Alpha test reference — core profile replacement for GL_ALPHA_TEST
// Return -1.0f for ALWAYS or disabled so `alpha <= ref` never discards.
if (locs[LOC_ALPHA_TEST_REF] != -1) {
final float ref;
if (!GLStateManager.getAlphaTest().isEnabled() || GLStateManager.getAlphaState().getFunction() == GL11.GL_ALWAYS) {
ref = -1.0f;
} else {
ref = GLStateManager.getAlphaState().getReference();
}
RENDER_BACKEND.uniform1f(locs[LOC_ALPHA_TEST_REF], ref);
}
}
/**
* Scene color = emission + ambient * lightModel.ambient, alpha = diffuse.a Same computation as {@code Uniforms.uploadLightingPreComputed()}.
*/
private static void uploadSceneColor(int[] locs) {
final MaterialState mat = GLStateManager.getFrontMaterial();
final LightModelState lm = GLStateManager.getLightModel();
vec4Buf.clear();
vec4Buf.put(mat.emission.x + mat.ambient.x * lm.ambient.x)
.put(mat.emission.y + mat.ambient.y * lm.ambient.y)
.put(mat.emission.z + mat.ambient.z * lm.ambient.z)
.put(mat.diffuse.w);
vec4Buf.flip();
RENDER_BACKEND.uniform4(locs[LOC_SCENE_COLOR], vec4Buf);
}
private static void uploadLightSources(int[] locs) {
// Early exit: if the first light field isn't present, no lighting uniforms exist
if (locs[LOC_LIGHT_BASE] == -1) return;
final LightStateStack[] lights = GLStateManager.getLightDataStates();
for (int li = 0; li < 2; li++) {
final int base = LOC_LIGHT_BASE + li * LIGHT_FIELDS;
final LightState light = lights[li];
if (locs[base + LF_AMBIENT] != -1)
RENDER_BACKEND.uniform4f(locs[base + LF_AMBIENT], light.ambient.x, light.ambient.y, light.ambient.z, light.ambient.w);
if (locs[base + LF_DIFFUSE] != -1)
RENDER_BACKEND.uniform4f(locs[base + LF_DIFFUSE], light.diffuse.x, light.diffuse.y, light.diffuse.z, light.diffuse.w);
if (locs[base + LF_SPECULAR] != -1)
RENDER_BACKEND.uniform4f(locs[base + LF_SPECULAR], light.specular.x, light.specular.y, light.specular.z, light.specular.w);
if (locs[base + LF_POSITION] != -1)
RENDER_BACKEND.uniform4f(locs[base + LF_POSITION], light.position.x, light.position.y, light.position.z, light.position.w);
if (locs[base + LF_HALF_VECTOR] != -1) {
// halfVector = normalize(normalize(position.xyz) + eye), eye=(0,0,1) for infinite viewer
float hx, hy, hz;
if (light.position.w == 0.0f) {
final float len = (float) Math.sqrt(light.position.x * light.position.x
+ light.position.y * light.position.y
+ light.position.z * light.position.z);
if (len > 0) {
hx = light.position.x / len;
hy = light.position.y / len;
hz = light.position.z / len + 1.0f;
} else {
hx = 0; hy = 0; hz = 1;
}
} else {
hx = 0; hy = 0; hz = 1;
}
final float hlen = (float) Math.sqrt(hx * hx + hy * hy + hz * hz);
if (hlen > 0) { hx /= hlen; hy /= hlen; hz /= hlen; }
RENDER_BACKEND.uniform4f(locs[base + LF_HALF_VECTOR], hx, hy, hz, 0.0f);
}
if (locs[base + LF_SPOT_DIRECTION] != -1) RENDER_BACKEND.uniform3f(locs[base + LF_SPOT_DIRECTION], light.spotDirection.x, light.spotDirection.y, light.spotDirection.z);
if (locs[base + LF_SPOT_EXPONENT] != -1) RENDER_BACKEND.uniform1f(locs[base + LF_SPOT_EXPONENT], light.spotExponent);
if (locs[base + LF_SPOT_CUTOFF] != -1) RENDER_BACKEND.uniform1f(locs[base + LF_SPOT_CUTOFF], light.spotCutoff);
if (locs[base + LF_SPOT_COS_CUTOFF] != -1) RENDER_BACKEND.uniform1f(locs[base + LF_SPOT_COS_CUTOFF], light.spotCosCutoff);
if (locs[base + LF_CONSTANT_ATTEN] != -1) RENDER_BACKEND.uniform1f(locs[base + LF_CONSTANT_ATTEN], light.constantAttenuation);
if (locs[base + LF_LINEAR_ATTEN] != -1) RENDER_BACKEND.uniform1f(locs[base + LF_LINEAR_ATTEN], light.linearAttenuation);
if (locs[base + LF_QUADRATIC_ATTEN] != -1) RENDER_BACKEND.uniform1f(locs[base + LF_QUADRATIC_ATTEN], light.quadraticAttenuation);
}
}
private static void uploadMaterial(int[] locs) {
// Early exit: if the first material field isn't present, no material uniforms exist
if (locs[LOC_MAT_BASE] == -1) return;
final MaterialState mat = GLStateManager.getFrontMaterial();
if (locs[LOC_MAT_BASE + MF_EMISSION] != -1)
RENDER_BACKEND.uniform4f(locs[LOC_MAT_BASE + MF_EMISSION], mat.emission.x, mat.emission.y, mat.emission.z, mat.emission.w);
if (locs[LOC_MAT_BASE + MF_AMBIENT] != -1)
RENDER_BACKEND.uniform4f(locs[LOC_MAT_BASE + MF_AMBIENT], mat.ambient.x, mat.ambient.y, mat.ambient.z, mat.ambient.w);
if (locs[LOC_MAT_BASE + MF_DIFFUSE] != -1)
RENDER_BACKEND.uniform4f(locs[LOC_MAT_BASE + MF_DIFFUSE], mat.diffuse.x, mat.diffuse.y, mat.diffuse.z, mat.diffuse.w);
if (locs[LOC_MAT_BASE + MF_SPECULAR] != -1)
RENDER_BACKEND.uniform4f(locs[LOC_MAT_BASE + MF_SPECULAR], mat.specular.x, mat.specular.y, mat.specular.z, mat.specular.w);
if (locs[LOC_MAT_BASE + MF_SHININESS] != -1)
RENDER_BACKEND.uniform1f(locs[LOC_MAT_BASE + MF_SHININESS], mat.shininess);
}
private static void uploadClipPlanes(int[] locs) {
if (locs[LOC_CLIP_PLANES_ENABLED] != -1) {
RENDER_BACKEND.uniform1i(locs[LOC_CLIP_PLANES_ENABLED], GLStateManager.anyClipPlaneEnabled() ? 1 : 0);
}
if (locs[LOC_CLIP_PLANES] != -1) {
final ClipPlaneState cps = GLStateManager.getClipPlaneState();
clipPlaneBuf.clear();
for (int i = 0; i < GLStateManager.MAX_CLIP_PLANES; i++) {
cps.putEyePlane(i, clipPlaneBuf);
}
clipPlaneBuf.flip();
RENDER_BACKEND.uniform4(locs[LOC_CLIP_PLANES], clipPlaneBuf);
}
}
public static void onDeleteProgram(int program) {
final boolean locked = GLStateManager.acquireDrawLock();
try {
programUniforms.remove(program);
} finally {
if (locked) GLStateManager.releaseDrawLock();
}
}
public static boolean hasProgram(int program) {
return programUniforms.containsKey(program);
}
public static int[] getLocations(int program) {
final ProgramUniforms pu = programUniforms.get(program);
return pu == null ? null : pu.locs;
}
}
@@ -0,0 +1,187 @@
package com.gtnewhorizons.angelica.glsm;
/*
* Ported from Mesa's util_idalloc (src/util/u_idalloc.c).
*
* Original copyright and license:
*
* Copyright 2017 Valve Corporation
* All Rights Reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sub license, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice (including the
* next paragraph) shall be included in all copies or substantial portions
* of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS AND/OR ITS SUPPLIERS BE LIABLE FOR
* ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
* Original author: Samuel Pitoiset <[email protected]>
*/
/**
* Bitmap ID allocator for display list IDs.
*
* <p>Bitmap-based: each bit in the {@code data} array represents one ID.
* Bit set = allocated, bit clear = free. ID 0 is reserved (never returned).
*/
public class DisplayListIDAllocator {
private int[] data;
private int numElements; // number of uint32 words allocated
private int numSetElements; // highest word index with any bit set + 1
private int lowestFreeIdx; // optimization: skip fully-allocated words
public DisplayListIDAllocator() {
this(32); // 1024 IDs initially
}
public DisplayListIDAllocator(int initialWords) {
if (initialWords < 1) initialWords = 1;
this.data = new int[initialWords];
this.numElements = initialWords;
this.numSetElements = 0;
this.lowestFreeIdx = 0;
// Reserve ID 0 (never returned), matching Mesa's skip_zero pattern
data[0] = 1;
numSetElements = 1;
}
public synchronized int allocRange(int count) {
if (count <= 0) throw new IllegalArgumentException("count must be > 0");
if (count == 1) return allocSingle();
return allocMultiple(count);
}
private int allocSingle() {
for (int i = lowestFreeIdx; i < numElements; i++) {
if (data[i] == 0xFFFFFFFF) continue;
int bit = Integer.numberOfTrailingZeros(~data[i]);
data[i] |= 1 << bit;
lowestFreeIdx = i;
numSetElements = Math.max(numSetElements, i + 1);
return i * 32 + bit;
}
// No slots available, grow. Record old end, then mark first bit there.
int oldNumElements = numElements;
resize(Math.max(numElements, 1) * 2);
lowestFreeIdx = oldNumElements;
data[oldNumElements] |= 1;
numSetElements = Math.max(numSetElements, oldNumElements + 1);
return oldNumElements * 32;
}
private int allocMultiple(int num) {
int numWords = divRoundUp(num, 32);
int base = findFreeBlock(lowestFreeIdx);
boolean found = false;
while (!found) {
int i;
for (i = base; i < numElements && i - base < numWords && data[i] == 0; i++)
;
if (i - base == numWords) {
found = true;
} else if (i == numElements) {
// Hit end — base is valid: data[base..end] are zero, resize extends the run into newly zeroed words.
resize(numElements * 2 + numWords);
found = true;
} else {
// Non-zero word at i, skip past it
base = i + 1;
}
}
// Mark bits as used
int fullWords = num / 32;
int remainder = num % 32;
for (int i = base; i < base + fullWords; i++) {
data[i] = 0xFFFFFFFF;
}
if (remainder != 0) {
data[base + fullWords] |= (1 << remainder) - 1;
}
if (lowestFreeIdx == base) {
lowestFreeIdx = base + num / 32;
}
numSetElements = Math.max(numSetElements, base + numWords);
return base * 32;
}
private int findFreeBlock(int start) {
for (int i = start; i < numElements; i++) {
if (data[i] == 0) return i;
}
return numElements;
}
public synchronized void free(int id) {
if (id <= 0) return; // ID 0 is reserved
int idx = id / 32;
if (idx >= numElements) return;
lowestFreeIdx = Math.min(idx, lowestFreeIdx);
data[idx] &= ~(1 << (id % 32));
if (numSetElements == idx + 1) {
while (numSetElements > 0 && data[numSetElements - 1] == 0) {
numSetElements--;
}
}
}
public synchronized void freeRange(int base, int count) {
if (count <= 0 || base <= 0) return;
int startIdx = base / 32;
if (startIdx >= numElements) return;
int end = base + count;
for (int id = base; id < end; id++) {
int idx = id / 32;
if (idx >= numElements) break;
data[idx] &= ~(1 << (id % 32));
}
lowestFreeIdx = Math.min(startIdx, lowestFreeIdx);
while (numSetElements > 0 && data[numSetElements - 1] == 0) {
numSetElements--;
}
}
public synchronized boolean isAllocated(int id) {
if (id <= 0) return false;
int idx = id / 32;
if (idx >= numSetElements) return false;
return (data[idx] & (1 << (id % 32))) != 0;
}
private void resize(int newNumElements) {
if (newNumElements <= numElements) return;
int[] newData = new int[newNumElements];
System.arraycopy(data, 0, newData, 0, numElements);
data = newData;
numElements = newNumElements;
}
private static int divRoundUp(int a, int b) {
return (a + b - 1) / b;
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,358 @@
package com.gtnewhorizons.angelica.glsm;
import com.google.common.collect.ImmutableSet;
import com.gtnewhorizon.gtnhlib.client.renderer.stacks.IStateStack;
import com.gtnewhorizons.angelica.glsm.ffp.ShaderManager;
import com.gtnewhorizons.angelica.glsm.stacks.CowStateStack;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL13;
import java.util.HashSet;
import java.util.Set;
public class Feature {
private static final int[] supportedAttribs = new int[] { GL11.GL_ACCUM_BUFFER_BIT, GL11.GL_COLOR_BUFFER_BIT, GL11.GL_CURRENT_BIT, GL11.GL_DEPTH_BUFFER_BIT,
GL11.GL_ENABLE_BIT, GL11.GL_EVAL_BIT, GL11.GL_FOG_BIT, GL11.GL_HINT_BIT, GL11.GL_LIGHTING_BIT, GL11.GL_LINE_BIT, GL11.GL_LIST_BIT,
GL13.GL_MULTISAMPLE_BIT, GL11.GL_PIXEL_MODE_BIT, GL11.GL_POINT_BIT, GL11.GL_POLYGON_BIT, GL11.GL_POLYGON_STIPPLE_BIT, GL11.GL_SCISSOR_BIT,
GL11.GL_STENCIL_BUFFER_BIT, GL11.GL_TEXTURE_BIT, GL11.GL_TRANSFORM_BIT, GL11.GL_VIEWPORT_BIT };
private static final long[] NO_MEMBERS = new long[0];
private static final Int2ObjectMap<long[]> maskToMembersMap = new Int2ObjectOpenHashMap<>();
static long[] maskToMembers(int mask) {
final long[] cached = maskToMembersMap.get(mask);
if (cached != null) {
return cached;
}
long[] result = NO_MEMBERS;
for (int attrib : Feature.supportedAttribs) {
if ((mask & attrib) == attrib) {
result = union(result, attribToMembers.get(attrib));
}
}
maskToMembersMap.put(mask, result);
return result;
}
private static long[] union(long[] a, long[] b) {
if (b == null || b.length == 0) return a;
if (a.length == 0) return b.clone();
final long[] result = new long[Math.max(a.length, b.length)];
System.arraycopy(a, 0, result, 0, a.length);
for (int i = 0; i < b.length; i++) result[i] |= b[i];
return result;
}
private static long[] toMembers(Set<IStateStack<?>> stacks) {
if (stacks.isEmpty()) return NO_MEMBERS;
int max = 0;
for (IStateStack<?> s : stacks) max = Math.max(max, ((CowStateStack<?>) s).stackId());
final long[] bits = new long[(max >> 6) + 1];
for (IStateStack<?> s : stacks) {
final int id = ((CowStateStack<?>) s).stackId();
bits[id >> 6] |= 1L << id;
}
return bits;
}
private static final Int2ObjectMap<long[]> attribToMembers = new Int2ObjectOpenHashMap<>();
/**
* Helper method to add all texture gen states (S, T, R, Q) for all texture units to a collection.
* Used by both GL_ENABLE_BIT and GL_TEXTURE_BIT.
*/
private static void addTextureGenStates(Set<IStateStack<?>> collection) {
for(int i = 0 ; i < GLStateManager.MAX_TEXTURE_UNITS; i++) {
collection.add(GLStateManager.getTextures().getTexGenSStates(i));
collection.add(GLStateManager.getTextures().getTexGenTStates(i));
collection.add(GLStateManager.getTextures().getTexGenRStates(i));
collection.add(GLStateManager.getTextures().getTexGenQStates(i));
}
}
static {
final Int2ObjectMap<Set<IStateStack<?>>> attribToFeatures = new Int2ObjectOpenHashMap<>();
attribToFeatures.put(GL11.GL_COLOR_BUFFER_BIT, ImmutableSet.of(
GLStateManager.getAlphaTest()
, GLStateManager.getAlphaState()
, GLStateManager.getBlendMode()
, GLStateManager.getBlendState()
, GLStateManager.getColorLogicOpState()
, GLStateManager.getDitherState()
, GLStateManager.getDrawBuffer()
, GLStateManager.getIndexLogicOpState()
, GLStateManager.getLogicOpMode()
, GLStateManager.getColorMask()
, GLStateManager.getClearColor()
));
attribToFeatures.put(GL11.GL_CURRENT_BIT, ImmutableSet.of(
GLStateManager.getColor()
, GLStateManager.getSecondaryColor() // Current secondary color
, ShaderManager.getNormalStack() // Current normal vector
, ShaderManager.getTexCoordStack() // Current texture coordinates
// Current color index
// Current raster position
// GL_CURRENT_RASTER_POSITION_VALID flag
// RGBA color associated with current raster position
// Color index associated with current raster position
// Texture coordinates associated with current raster position
// GL_EDGE_FLAG flag
));
attribToFeatures.put(GL11.GL_DEPTH_BUFFER_BIT, ImmutableSet.of(
GLStateManager.getDepthTest()
, GLStateManager.getDepthState()
// Depth buffer clear value
// GL_DEPTH_WRITEMASK enable bit
));
final HashSet<IStateStack<?>> enableBits = new HashSet<>(ImmutableSet.of(
GLStateManager.getAlphaTest()
, GLStateManager.getAutoNormalState()
, GLStateManager.getBlendMode()
, GLStateManager.getColorLogicOpState()
, GLStateManager.getColorMaterial()
, GLStateManager.getColorSumState()
, GLStateManager.getCullState()
, GLStateManager.getDepthTest()
, GLStateManager.getDitherState()
, GLStateManager.getFogMode()
, GLStateManager.getIndexLogicOpState()
, GLStateManager.getLightStates()[0] // GL_LIGHT0
, GLStateManager.getLightStates()[1] // GL_LIGHT1
, GLStateManager.getLightStates()[2] // GL_LIGHT2
, GLStateManager.getLightStates()[3] // GL_LIGHT3
, GLStateManager.getLightStates()[4] // GL_LIGHT4
, GLStateManager.getLightStates()[5] // GL_LIGHT5
, GLStateManager.getLightStates()[6] // GL_LIGHT6
, GLStateManager.getLightStates()[7] // GL_LIGHT7
, GLStateManager.getLightingState()
, GLStateManager.getLineSmoothState()
, GLStateManager.getLineStippleState()
, GLStateManager.getMap1Color4State()
, GLStateManager.getMap1IndexState()
, GLStateManager.getMap1NormalState()
, GLStateManager.getMap1TextureCoord1State()
, GLStateManager.getMap1TextureCoord2State()
, GLStateManager.getMap1TextureCoord3State()
, GLStateManager.getMap1TextureCoord4State()
, GLStateManager.getMap1Vertex3State()
, GLStateManager.getMap1Vertex4State()
, GLStateManager.getMap2Color4State()
, GLStateManager.getMap2IndexState()
, GLStateManager.getMap2NormalState()
, GLStateManager.getMap2TextureCoord1State()
, GLStateManager.getMap2TextureCoord2State()
, GLStateManager.getMap2TextureCoord3State()
, GLStateManager.getMap2TextureCoord4State()
, GLStateManager.getMap2Vertex3State()
, GLStateManager.getMap2Vertex4State()
, GLStateManager.getMultisampleState()
, GLStateManager.getNormalizeState()
, GLStateManager.getPointSmoothState()
, GLStateManager.getPolygonOffsetPointState()
, GLStateManager.getPolygonOffsetLineState()
, GLStateManager.getPolygonOffsetFillState()
, GLStateManager.getPolygonSmoothState()
, GLStateManager.getPolygonStippleState()
, GLStateManager.getRescaleNormalState()
, GLStateManager.getSampleAlphaToCoverageState()
, GLStateManager.getSampleAlphaToOneState()
, GLStateManager.getSampleCoverageState()
, GLStateManager.getScissorTest()
, GLStateManager.getStencilTest()
));
// Enable bits for the user-definable clipping planes
for(int i = 0; i < GLStateManager.getClipPlaneStates().length; i++) {
enableBits.add(GLStateManager.getClipPlaneStates()[i]);
}
// GL_TEXTURE_1D, GL_TEXTURE_2D, GL_TEXTURE_3D flags
for(int i = 0 ; i < GLStateManager.MAX_TEXTURE_UNITS; i++) {
enableBits.add(GLStateManager.getTextures().getTexture1DStates(i));
enableBits.add(GLStateManager.getTextures().getTextureUnitStates(i));
enableBits.add(GLStateManager.getTextures().getTexture3DStates(i));
}
// Flags GL_TEXTURE_GEN_x where x is S, T, R, or Q
addTextureGenStates(enableBits);
attribToFeatures.put(GL11.GL_ENABLE_BIT, enableBits);
attribToFeatures.put(GL11.GL_EVAL_BIT, ImmutableSet.of(
GLStateManager.getAutoNormalState()
, GLStateManager.getMap1Color4State()
, GLStateManager.getMap1IndexState()
, GLStateManager.getMap1NormalState()
, GLStateManager.getMap1TextureCoord1State()
, GLStateManager.getMap1TextureCoord2State()
, GLStateManager.getMap1TextureCoord3State()
, GLStateManager.getMap1TextureCoord4State()
, GLStateManager.getMap1Vertex3State()
, GLStateManager.getMap1Vertex4State()
, GLStateManager.getMap2Color4State()
, GLStateManager.getMap2IndexState()
, GLStateManager.getMap2NormalState()
, GLStateManager.getMap2TextureCoord1State()
, GLStateManager.getMap2TextureCoord2State()
, GLStateManager.getMap2TextureCoord3State()
, GLStateManager.getMap2TextureCoord4State()
, GLStateManager.getMap2Vertex3State()
, GLStateManager.getMap2Vertex4State()
));
attribToFeatures.put(GL11.GL_FOG_BIT, ImmutableSet.of(
GLStateManager.getFogMode()
, GLStateManager.getColorSumState()
, GLStateManager.getFogState()
// ^^ Fog density
// ^^ Linear fog start
// ^^ Linear fog end
// Fog index
// ^^ GL_FOG_MODE value
));
attribToFeatures.put(GL11.GL_HINT_BIT, ImmutableSet.of(
// GL_PERSPECTIVE_CORRECTION_HINT setting
// GL_POINT_SMOOTH_HINT setting
// GL_LINE_SMOOTH_HINT setting
// GL_POLYGON_SMOOTH_HINT setting
// GL_FOG_HINT setting
// GL_GENERATE_MIPMAP_HINT setting
// GL_TEXTURE_COMPRESSION_HINT setting
));
attribToFeatures.put(GL11.GL_LIGHTING_BIT, ImmutableSet.of(
GLStateManager.getColorMaterial()
, GLStateManager.getColorMaterialFace()
, GLStateManager.getColorMaterialParameter()
, GLStateManager.getLightModel()
, GLStateManager.getLightingState()
// Enable bit for each light
, GLStateManager.getLightStates()[0] // GL_LIGHT0
, GLStateManager.getLightStates()[1] // GL_LIGHT1
, GLStateManager.getLightStates()[2] // GL_LIGHT2
, GLStateManager.getLightStates()[3] // GL_LIGHT3
, GLStateManager.getLightStates()[4] // GL_LIGHT4
, GLStateManager.getLightStates()[5] // GL_LIGHT5
, GLStateManager.getLightStates()[6] // GL_LIGHT6
, GLStateManager.getLightStates()[7] // GL_LIGHT7
// Ambient, diffuse, and specular intensity for each light
// Direction, position, exponent, and cutoff angle for each light
// Constant, linear, and quadratic attenuation factors for each light
, GLStateManager.getLightDataStates()[0]
, GLStateManager.getLightDataStates()[1]
, GLStateManager.getLightDataStates()[2]
, GLStateManager.getLightDataStates()[3]
, GLStateManager.getLightDataStates()[4]
, GLStateManager.getLightDataStates()[5]
, GLStateManager.getLightDataStates()[6]
, GLStateManager.getLightDataStates()[7]
// Ambient, diffuse, specular, and emissive color for each material
// Ambient, diffuse, and specular color indices for each material
// Specular exponent for each material
, GLStateManager.getFrontMaterial()
, GLStateManager.getBackMaterial()
, GLStateManager.getShadeModelState()
));
attribToFeatures.put(GL11.GL_LINE_BIT, ImmutableSet.of(
GLStateManager.getLineSmoothState()
, GLStateManager.getLineStippleState()
, GLStateManager.getLineState()
));
attribToFeatures.put(GL11.GL_LIST_BIT, ImmutableSet.of(
// GL_LIST_BASE setting
));
attribToFeatures.put(GL13.GL_MULTISAMPLE_BIT, ImmutableSet.of(
GLStateManager.getMultisampleState()
, GLStateManager.getSampleAlphaToCoverageState()
, GLStateManager.getSampleAlphaToOneState()
, GLStateManager.getSampleCoverageState()
// GL_SAMPLE_COVERAGE_VALUE value
// GL_SAMPLE_COVERAGE_INVERT value
));
attribToFeatures.put(GL11.GL_PIXEL_MODE_BIT, ImmutableSet.of(
// GL_RED_BIAS and GL_RED_SCALE settings
// GL_GREEN_BIAS and GL_GREEN_SCALE values
// GL_BLUE_BIAS and GL_BLUE_SCALE
// GL_ALPHA_BIAS and GL_ALPHA_SCALE
// GL_DEPTH_BIAS and GL_DEPTH_SCALE
// GL_INDEX_OFFSET and GL_INDEX_SHIFT values
// GL_MAP_COLOR and GL_MAP_STENCIL flags
// GL_ZOOM_X and GL_ZOOM_Y factors
// GL_READ_BUFFER setting
));
attribToFeatures.put(GL11.GL_POINT_BIT, ImmutableSet.of(
GLStateManager.getPointSmoothState()
, GLStateManager.getPointState()
));
attribToFeatures.put(GL11.GL_POLYGON_BIT, ImmutableSet.of(
GLStateManager.getCullState()
, GLStateManager.getPolygonSmoothState()
, GLStateManager.getPolygonStippleState()
, GLStateManager.getPolygonOffsetFillState()
, GLStateManager.getPolygonOffsetLineState()
, GLStateManager.getPolygonOffsetPointState()
, GLStateManager.getPolygonState()
));
attribToFeatures.put(GL11.GL_POLYGON_STIPPLE_BIT, ImmutableSet.of(
// Polygon stipple pattern
));
attribToFeatures.put(GL11.GL_SCISSOR_BIT, ImmutableSet.of(
GLStateManager.getScissorTest()
, GLStateManager.getScissorState()
));
attribToFeatures.put(GL11.GL_STENCIL_BUFFER_BIT, ImmutableSet.of(
GLStateManager.getStencilTest()
, GLStateManager.getStencilState()
));
final Set<IStateStack<?>> textureAttribs = new HashSet<>(ImmutableSet.of(
GLStateManager.getActiveTextureUnitStack()
// GL_TEXTURE_ENV_MODE + GL_TEXTURE_ENV_COLOR — now per-unit in TexEnvState (added below)
// Enable bits for the four texture coordinates
// Border color for each texture image
// Minification function for each texture image
// Magnification function for each texture image
// Texture coordinates and wrap mode for each texture image
// Enable bits GL_TEXTURE_GEN_x, x is S, T, R, and Q
// GL_TEXTURE_GEN_MODE setting for S, T, R, and Q
// glTexGen plane equations for S, T, R, and Q
// Current texture bindings (for example, GL_TEXTURE_BINDING_2D) - Below
));
// Current Texture Bindings - GL_TEXTURE_BINDING_2D + per-unit TexEnvState
for(int i = 0 ; i < GLStateManager.MAX_TEXTURE_UNITS; i++) {
textureAttribs.add(GLStateManager.getTextures().getTextureUnitBindings(i));
textureAttribs.add(GLStateManager.getTextures().getTexEnvState(i));
}
// Enable bits GL_TEXTURE_GEN_x where x is S, T, R, or Q
addTextureGenStates(textureAttribs);
attribToFeatures.put(GL11.GL_TEXTURE_BIT, textureAttribs);
final Set<IStateStack<?>> transformAttribs = new HashSet<>(ImmutableSet.of(
// Coefficients of the six clipping planes
GLStateManager.getMatrixMode()
, GLStateManager.getNormalizeState()
, GLStateManager.getRescaleNormalState()
));
for(int i = 0; i < GLStateManager.getClipPlaneStates().length; i++) {
transformAttribs.add(GLStateManager.getClipPlaneStates()[i]);
}
attribToFeatures.put(GL11.GL_TRANSFORM_BIT, transformAttribs);
attribToFeatures.put(GL11.GL_VIEWPORT_BIT, ImmutableSet.of(
GLStateManager.getViewportState()
));
for (Int2ObjectMap.Entry<Set<IStateStack<?>>> entry : attribToFeatures.int2ObjectEntrySet()) {
attribToMembers.put(entry.getIntKey(), toMembers(entry.getValue()));
}
}
}
@@ -0,0 +1,403 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizons.angelica.glsm.recording.ImmediateModeRecorder;
import com.gtnewhorizons.angelica.glsm.states.ViewportState;
import org.joml.Matrix4f;
import org.joml.Vector4f;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
import java.nio.ByteBuffer;
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
*/
public class FeedbackManager {
private static final int FB_3D = 0x01;
private static final int FB_4D = 0x02;
private static final int FB_COLOR = 0x04;
private static final int FB_TEXTURE = 0x08;
private static int renderMode = GL11.GL_RENDER;
private static FloatBuffer feedbackBuffer;
private static int feedbackBufferSize;
private static int feedbackMask;
private static int feedbackCount;
private static final Matrix4f mvpMatrix = new Matrix4f();
private static final Vector4f clipVec = new Vector4f();
private static ByteBuffer readbackBuf = ByteBuffer.allocateDirect(256 * 1024).order(ByteOrder.nativeOrder());
private static ByteBuffer indexReadbackBuf = ByteBuffer.allocateDirect(4 * 1024).order(ByteOrder.nativeOrder());
@FunctionalInterface
private interface IndexReader {
int read(int i);
}
public static boolean isFeedbackMode() {
return renderMode == GL11.GL_FEEDBACK;
}
public static int getRenderMode() {
return renderMode;
}
public static void glFeedbackBuffer(int type, FloatBuffer buffer) {
if (renderMode == GL11.GL_FEEDBACK) {
GLStateManager.LOGGER.error("glFeedbackBuffer called while already in feedback mode");
return;
}
feedbackBuffer = buffer;
feedbackBufferSize = buffer.remaining();
feedbackMask = computeMask(type);
feedbackCount = 0;
}
public static int glRenderMode(int mode) {
int result = switch (renderMode) {
case GL11.GL_FEEDBACK -> (feedbackCount > feedbackBufferSize) ? -1 : feedbackCount;
case GL11.GL_RENDER, GL11.GL_SELECT -> 0;
default -> 0;
};
if (mode == GL11.GL_SELECT) {
GLStateManager.LOGGER.warn("glRenderMode(GL_SELECT): selection mode not emulated");
}
feedbackCount = 0;
renderMode = mode;
return result;
}
public static void glPassThrough(float token) {
if (renderMode == GL11.GL_FEEDBACK) {
feedbackToken(GL11.GL_PASS_THROUGH_TOKEN);
feedbackToken(token);
}
}
private static void feedbackToken(float token) {
if (feedbackCount < feedbackBufferSize) {
feedbackBuffer.put(feedbackBuffer.position() + feedbackCount, token);
}
feedbackCount++;
}
private static void feedbackVertex(float winX, float winY, float winZ, float winW) {
feedbackToken(winX);
feedbackToken(winY);
if ((feedbackMask & FB_3D) != 0) feedbackToken(winZ);
if ((feedbackMask & FB_4D) != 0) feedbackToken(winW);
if ((feedbackMask & FB_COLOR) != 0) {
feedbackToken(1f);
feedbackToken(1f);
feedbackToken(1f);
feedbackToken(1f);
}
if ((feedbackMask & FB_TEXTURE) != 0) {
feedbackToken(0f);
feedbackToken(0f);
feedbackToken(0f);
feedbackToken(1f);
}
}
private static void transformAndEmitVertex(float x, float y, float z) {
clipVec.set(x, y, z, 1.0f);
mvpMatrix.transform(clipVec);
final float w = clipVec.w;
final float ndcX = clipVec.x / w;
final float ndcY = clipVec.y / w;
final float ndcZ = clipVec.z / w;
final ViewportState vp = GLStateManager.getViewportState();
final float winX = vp.x + vp.width * (ndcX + 1.0f) * 0.5f;
final float winY = vp.y + vp.height * (ndcY + 1.0f) * 0.5f;
final float winZ = (float) ((vp.depthRangeFar - vp.depthRangeNear) * ndcZ + (vp.depthRangeFar + vp.depthRangeNear)) * 0.5f;
final float winW = 1.0f / w;
feedbackVertex(winX, winY, winZ, winW);
}
private static int positionStride(GLContextState glCtx) {
final int s = glCtx.vaos.attribEffectiveStride(0);
return (s != 0) ? s : 12;
}
private static long positionOffset(GLContextState glCtx) {
return glCtx.vaos.attribOffset(0);
}
public static void processDrawArrays(int mode, int first, int count) {
computeMVP();
final GLContextState glCtx = GLStateManager.ctx();
final int stride = positionStride(glCtx);
final long posOffset = positionOffset(glCtx);
final int byteCount = count * stride;
ensureReadbackCapacity(byteCount);
readbackBuf.clear().limit(byteCount);
readPositionBuffer((long) first * stride + posOffset, readbackBuf, glCtx);
emitAllPrimitives(mode, count, i -> i, stride);
}
public static void processDrawElements(int mode, int indexCount, int type, long eboOffset) {
computeMVP();
final GLContextState glCtx = GLStateManager.ctx();
final int stride = positionStride(glCtx);
final int elementSize = GLTypes.sizeBytes(type);
final int indexByteCount = indexCount * elementSize;
ensureIndexReadbackCapacity(indexByteCount);
indexReadbackBuf.clear().limit(indexByteCount);
RENDER_BACKEND.getBufferSubData(GL15.GL_ELEMENT_ARRAY_BUFFER, eboOffset, indexReadbackBuf);
final ByteBuffer indexBuf = indexReadbackBuf;
int minIdx = Integer.MAX_VALUE, maxIdx = Integer.MIN_VALUE;
for (int i = 0; i < indexCount; i++) {
final int idx = ImmediateModeRecorder.readIndex(indexBuf, type, 0, i);
if (idx < minIdx) minIdx = idx;
if (idx > maxIdx) maxIdx = idx;
}
final long posOffset = positionOffset(glCtx);
final int vertexCount = maxIdx - minIdx + 1;
final int byteCount = vertexCount * stride;
ensureReadbackCapacity(byteCount);
readbackBuf.clear().limit(byteCount);
readPositionBuffer((long) minIdx * stride + posOffset, readbackBuf, glCtx);
final int base = minIdx;
emitAllPrimitives(mode, indexCount, i -> ImmediateModeRecorder.readIndex(indexBuf, type, 0, i) - base, stride);
}
public static void processDrawElements(int mode, ByteBuffer indices) {
final int basePos = indices.position();
processDrawElementsImpl(mode, indices.remaining(), i -> indices.get(basePos + i) & 0xFF);
}
public static void processDrawElements(int mode, ShortBuffer indices) {
final int basePos = indices.position();
processDrawElementsImpl(mode, indices.remaining(), i -> indices.get(basePos + i) & 0xFFFF);
}
public static void processDrawElements(int mode, IntBuffer indices) {
final int basePos = indices.position();
processDrawElementsImpl(mode, indices.remaining(), i -> indices.get(basePos + i));
}
public static void processDrawElements(int mode, int count, int type, ByteBuffer indices) {
final int pos = indices.position();
final IndexReader reader = switch (type) {
case GL11.GL_UNSIGNED_BYTE -> i -> indices.get(pos + i) & 0xFF;
case GL11.GL_UNSIGNED_SHORT -> i -> indices.getShort(pos + i * 2) & 0xFFFF;
case GL11.GL_UNSIGNED_INT -> i -> indices.getInt(pos + i * 4);
default -> i -> 0;
};
processDrawElementsImpl(mode, count, reader);
}
private static void processDrawElementsImpl(int mode, int count, IndexReader reader) {
computeMVP();
final GLContextState glCtx = GLStateManager.ctx();
final int stride = positionStride(glCtx);
int minIdx = Integer.MAX_VALUE, maxIdx = Integer.MIN_VALUE;
for (int i = 0; i < count; i++) {
final int idx = reader.read(i);
if (idx < minIdx) minIdx = idx;
if (idx > maxIdx) maxIdx = idx;
}
readVerticesFromVBO(minIdx, maxIdx, stride, glCtx);
final int base = minIdx;
emitAllPrimitives(mode, count, i -> reader.read(i) - base, stride);
}
private static void computeMVP() {
mvpMatrix.set(GLStateManager.getProjectionMatrix()).mul(GLStateManager.getModelViewMatrix());
}
private static int computeMask(int type) {
return switch (type) {
case GL11.GL_2D -> 0;
case GL11.GL_3D -> FB_3D;
case GL11.GL_3D_COLOR -> FB_3D | FB_COLOR;
case GL11.GL_3D_COLOR_TEXTURE -> FB_3D | FB_COLOR | FB_TEXTURE;
case GL11.GL_4D_COLOR_TEXTURE -> FB_3D | FB_4D | FB_COLOR | FB_TEXTURE;
default -> 0;
};
}
private static void ensureReadbackCapacity(int bytes) {
if (readbackBuf.capacity() < bytes) {
readbackBuf = ByteBuffer.allocateDirect(bytes).order(ByteOrder.nativeOrder());
}
}
private static void ensureIndexReadbackCapacity(int bytes) {
if (indexReadbackBuf.capacity() < bytes) {
indexReadbackBuf = ByteBuffer.allocateDirect(bytes).order(ByteOrder.nativeOrder());
}
}
private static void readVerticesFromVBO(int minIdx, int maxIdx, int stride, GLContextState glCtx) {
final long posOffset = positionOffset(glCtx);
final int vertexCount = maxIdx - minIdx + 1;
final int byteCount = vertexCount * stride;
ensureReadbackCapacity(byteCount);
readbackBuf.clear().limit(byteCount);
readPositionBuffer((long) minIdx * stride + posOffset, readbackBuf, glCtx);
}
private static void readPositionBuffer(long offset, ByteBuffer destination, GLContextState glCtx) {
final int positionVbo = glCtx.vaos.attribVBO(0);
final int previousVbo = glCtx.boundVBO;
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) {
final float x = readbackBuf.getFloat(vertexIndex * stride);
final float y = readbackBuf.getFloat(vertexIndex * stride + 4);
final float z = readbackBuf.getFloat(vertexIndex * stride + 8);
transformAndEmitVertex(x, y, z);
}
private static void emitAllPrimitives(int mode, int count, IndexReader reader, int stride) {
switch (mode) {
case GL11.GL_POINTS:
for (int i = 0; i < count; i++) {
feedbackToken(GL11.GL_POINT_TOKEN);
emitVertex(reader.read(i), stride);
}
break;
case GL11.GL_LINES:
for (int i = 0; i + 1 < count; i += 2) {
feedbackToken(i == 0 ? GL11.GL_LINE_RESET_TOKEN : GL11.GL_LINE_TOKEN);
emitVertex(reader.read(i), stride);
emitVertex(reader.read(i + 1), stride);
}
break;
case GL11.GL_LINE_STRIP:
for (int i = 0; i + 1 < count; i++) {
feedbackToken(i == 0 ? GL11.GL_LINE_RESET_TOKEN : GL11.GL_LINE_TOKEN);
emitVertex(reader.read(i), stride);
emitVertex(reader.read(i + 1), stride);
}
break;
case GL11.GL_LINE_LOOP:
for (int i = 0; i + 1 < count; i++) {
feedbackToken(i == 0 ? GL11.GL_LINE_RESET_TOKEN : GL11.GL_LINE_TOKEN);
emitVertex(reader.read(i), stride);
emitVertex(reader.read(i + 1), stride);
}
if (count > 2) {
feedbackToken(GL11.GL_LINE_TOKEN);
emitVertex(reader.read(count - 1), stride);
emitVertex(reader.read(0), stride);
}
break;
case GL11.GL_TRIANGLES:
for (int i = 0; i + 2 < count; i += 3) {
feedbackToken(GL11.GL_POLYGON_TOKEN);
feedbackToken(3);
emitVertex(reader.read(i), stride);
emitVertex(reader.read(i + 1), stride);
emitVertex(reader.read(i + 2), stride);
}
break;
case GL11.GL_TRIANGLE_STRIP:
for (int i = 0; i + 2 < count; i++) {
feedbackToken(GL11.GL_POLYGON_TOKEN);
feedbackToken(3);
if ((i & 1) == 0) {
emitVertex(reader.read(i), stride);
emitVertex(reader.read(i + 1), stride);
} else {
emitVertex(reader.read(i + 1), stride);
emitVertex(reader.read(i), stride);
}
emitVertex(reader.read(i + 2), stride);
}
break;
case GL11.GL_TRIANGLE_FAN:
for (int i = 1; i + 1 < count; i++) {
feedbackToken(GL11.GL_POLYGON_TOKEN);
feedbackToken(3);
emitVertex(reader.read(0), stride);
emitVertex(reader.read(i), stride);
emitVertex(reader.read(i + 1), stride);
}
break;
case GL11.GL_QUADS:
for (int i = 0; i + 3 < count; i += 4) {
feedbackToken(GL11.GL_POLYGON_TOKEN);
feedbackToken(4);
emitVertex(reader.read(i), stride);
emitVertex(reader.read(i + 1), stride);
emitVertex(reader.read(i + 2), stride);
emitVertex(reader.read(i + 3), stride);
}
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);
feedbackToken(count);
for (int i = 0; i < count; i++) {
emitVertex(reader.read(i), stride);
}
}
break;
default:
throw new UnsupportedOperationException("Feedback mode: unsupported primitive mode 0x" + Integer.toHexString(mode));
}
}
static void reset() {
renderMode = GL11.GL_RENDER;
feedbackBuffer = null;
feedbackBufferSize = 0;
feedbackMask = 0;
feedbackCount = 0;
}
}
@@ -0,0 +1,313 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizon.gtnhlib.client.renderer.stacks.IStateStack;
import it.unimi.dsi.fastutil.ints.Int2IntMap;
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
import com.gtnewhorizons.angelica.glsm.stacks.AlphaStateStack;
import com.gtnewhorizons.angelica.glsm.stacks.BlendStateStack;
import com.gtnewhorizons.angelica.glsm.stacks.BooleanStateStack;
import com.gtnewhorizons.angelica.glsm.stacks.Color4Stack;
import com.gtnewhorizons.angelica.glsm.stacks.ColorMaskStack;
import com.gtnewhorizons.angelica.glsm.stacks.CowDepths;
import com.gtnewhorizons.angelica.glsm.stacks.CowDispatch;
import com.gtnewhorizons.angelica.glsm.stacks.CowStateStack;
import com.gtnewhorizons.angelica.glsm.stacks.DepthStateStack;
import com.gtnewhorizons.angelica.glsm.stacks.FogStateStack;
import com.gtnewhorizons.angelica.glsm.stacks.IntegerStateStack;
import com.gtnewhorizons.angelica.glsm.stacks.LightModelStateStack;
import com.gtnewhorizons.angelica.glsm.stacks.LightStateStack;
import com.gtnewhorizons.angelica.glsm.stacks.LineStateStack;
import com.gtnewhorizons.angelica.glsm.stacks.MaterialStateStack;
import com.gtnewhorizons.angelica.glsm.stacks.MatrixModeStack;
import com.gtnewhorizons.angelica.glsm.stacks.PointStateStack;
import com.gtnewhorizons.angelica.glsm.stacks.PolygonStateStack;
import com.gtnewhorizons.angelica.glsm.stacks.RetainedState;
import com.gtnewhorizons.angelica.glsm.stacks.ScissorStateStack;
import com.gtnewhorizons.angelica.glsm.stacks.StackIdAllocator;
import com.gtnewhorizons.angelica.glsm.stacks.StencilStateStack;
import com.gtnewhorizons.angelica.glsm.stacks.ViewPortStateStack;
import com.gtnewhorizons.angelica.glsm.states.AlphaState;
import com.gtnewhorizons.angelica.glsm.states.BlendState;
import com.gtnewhorizons.angelica.glsm.states.ClipPlaneState;
import com.gtnewhorizons.angelica.glsm.states.Color4;
import com.gtnewhorizons.angelica.glsm.states.PixelUnpackState;
import com.gtnewhorizons.angelica.glsm.states.ImageUnitArray;
import com.gtnewhorizons.angelica.glsm.states.SamplerUnitArray;
import com.gtnewhorizons.angelica.glsm.states.TextureUnitArray;
import com.gtnewhorizons.angelica.glsm.ffp.Instancing;
import com.gtnewhorizons.angelica.glsm.ffp.ShaderManager;
import com.gtnewhorizons.angelica.glsm.ffp.VAOManager;
import com.gtnewhorizons.angelica.glsm.streaming.StreamingVaos;
import org.joml.Matrix4d;
import org.joml.Matrix4f;
import org.joml.Matrix4fStack;
import org.joml.Vector4f;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL12;
import org.lwjgl.opengl.GL13;
import org.lwjgl.opengl.GL14;
import org.lwjgl.opengl.GL30;
import java.nio.DoubleBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.util.ArrayList;
import java.util.List;
public class GLContextState {
private final List<IStateStack<?>> allStacksBuilder = new ArrayList<>();
public IStateStack<?>[] allStacks;
final List<BooleanStateStack> allBooleanStates = new ArrayList<>();
private BooleanStateStack track(BooleanStateStack s) {
s.setStackId(StackIdAllocator.nextId());
allBooleanStates.add(s);
allStacksBuilder.add(s);
return s;
}
private <T extends IStateStack<?>> T member(T s) {
allStacksBuilder.add(s);
return s;
}
public int programGeneration;
public int drawFramebufferGeneration;
public boolean poppingAttributes;
// modelview matrix changes (any: translation, rotation, scale)
public int mvGeneration;
public int mvLinearGeneration;
// projection matrix changes
public int projGeneration;
// texture matrix changes
public int texMatrixGeneration;
public int lightingGeneration;
// fog + alpha ref + overlay color
public int fragmentGeneration;
// current vertex color
public int colorGeneration;
// clip plane equation changes
public int clipPlaneGeneration;
public int clipPlaneEnabledMask;
public float overlayR = 0.0f;
public float overlayG = 0.0f;
public float overlayB = 0.0f;
public float overlayA = 0.0f;
public float shaderColorR = 1.0f;
public float shaderColorG = 1.0f;
public float shaderColorB = 1.0f;
public float shaderColorA = 1.0f;
// Deferred vertex attribute upload flags - set when state changes, flushed before draw
public boolean dirtyColorAttrib = true;
public boolean dirtyNormalAttrib;
public boolean dirtyTexCoordAttrib;
public boolean dirtyLightmapAttrib = true;
public boolean unit23TexCoordSetDuringDraw = false;
public int maxBoundTextureUnit = 0;
public int maxBoundImageUnit = 0;
public final StateSet[] attribSets = new StateSet[GLStateManager.MAX_ATTRIB_STACK_DEPTH];
public RetainedState retainedOwner;
public int attribDepth = 0;
public boolean savesState(int id) {
for (int i = attribDepth - 1; i >= 0; i--) {
if (attribSets[i].contains(id)) return true;
}
return false;
}
public int batchStateGeneration;
public final int[] savedBatchStateGen = new int[GLStateManager.MAX_ATTRIB_STACK_DEPTH];
public CowStateStack<?>[] stackById;
public CowDepths[] depthsById;
public int[] restoreBitById;
public int[] restoreUnitById;
public byte[] kindById;
public int[][] modifiedIds;
public int[] modifiedCount;
public final int[] savedMvGen = new int[GLStateManager.MAX_ATTRIB_STACK_DEPTH];
public final int[] savedMvLinearGen = new int[GLStateManager.MAX_ATTRIB_STACK_DEPTH];
public final int[] savedProjGen = new int[GLStateManager.MAX_ATTRIB_STACK_DEPTH];
public final int[] savedTexMatGen = new int[GLStateManager.MAX_ATTRIB_STACK_DEPTH];
public final int[] savedLightingGen = new int[GLStateManager.MAX_ATTRIB_STACK_DEPTH];
public final int[] savedFragmentGen = new int[GLStateManager.MAX_ATTRIB_STACK_DEPTH];
public final int[] savedColorGen = new int[GLStateManager.MAX_ATTRIB_STACK_DEPTH];
public final int[] savedNormalGen = new int[GLStateManager.MAX_ATTRIB_STACK_DEPTH];
public final int[] savedTexCoordGen = new int[GLStateManager.MAX_ATTRIB_STACK_DEPTH];
public final IntegerStateStack activeTextureUnit = member(new IntegerStateStack(0, StackIdAllocator.nextId()).restoreBit(StateSet.R_ACTIVE_UNIT));
public int clientActiveTextureUnit = 0;
public final IntegerStateStack shadeModelState = member(new IntegerStateStack(GL11.GL_SMOOTH, StackIdAllocator.nextId()));
public final TextureUnitArray textures = new TextureUnitArray();
public final ImageUnitArray imageUnits = new ImageUnitArray();
public final SamplerUnitArray samplerUnits = new SamplerUnitArray();
public final BlendStateStack blendState = member(new BlendStateStack(StackIdAllocator.nextId()));
public final BooleanStateStack blendMode = track(new BooleanStateStack(GL11.GL_BLEND));
public final BooleanStateStack scissorTest = track(new BooleanStateStack(GL11.GL_SCISSOR_TEST));
public final BooleanStateStack rasterizerDiscard = track(new BooleanStateStack(GL30.GL_RASTERIZER_DISCARD));
public final DepthStateStack depthState = member(new DepthStateStack(StackIdAllocator.nextId()));
public final BooleanStateStack depthTest = track(new BooleanStateStack(GL11.GL_DEPTH_TEST));
public final FogStateStack fogState = member(new FogStateStack(StackIdAllocator.nextId()));
public final BooleanStateStack fogMode = track(new BooleanStateStack(GL11.GL_FOG, false, true));
public final Color4Stack color = member(new Color4Stack(StackIdAllocator.nextId()));
// GL_CURRENT_SECONDARY_COLOR - alpha is always 1 and unused, the color sum only adds RGB
public final Color4Stack secondaryColor = member(new Color4Stack(StackIdAllocator.nextId(), new Color4(0.0F, 0.0F, 0.0F, 1.0F)).restoreBit(StateSet.R_SECONDARY_COLOR));
public final BooleanStateStack colorSumState = track(new BooleanStateStack(GL14.GL_COLOR_SUM, false, true));
public final Color4Stack clearColor = member(new Color4Stack(StackIdAllocator.nextId(), new Color4(0.0F, 0.0F, 0.0F, 0.0F)).restoreBit(StateSet.R_CLEAR_COLOR));
public final ColorMaskStack colorMask = member(new ColorMaskStack(StackIdAllocator.nextId()));
public final IntegerStateStack drawBuffer = member(new IntegerStateStack(GLStateManager.DEFAULT_DRAW_BUFFER, StackIdAllocator.nextId()).restoreBit(StateSet.R_DRAW_BUFFER));
public final IntegerStateStack logicOpMode = member(new IntegerStateStack(GL11.GL_COPY, StackIdAllocator.nextId()).restoreBit(StateSet.R_LOGIC_OP));
public final BooleanStateStack cullState = track(new BooleanStateStack(GL11.GL_CULL_FACE));
public final AlphaState restoredAlphaScratch = new AlphaState();
public final AlphaStateStack alphaState = member(new AlphaStateStack(StackIdAllocator.nextId()));
public final BooleanStateStack alphaTest = track(new BooleanStateStack(GL11.GL_ALPHA_TEST, false, true));
public final BooleanStateStack lightingState = track(new BooleanStateStack(GL11.GL_LIGHTING, false, true));
public final BooleanStateStack rescaleNormalState = track(new BooleanStateStack(GL12.GL_RESCALE_NORMAL, false, true));
public final BooleanStateStack normalizeState = track(new BooleanStateStack(GL11.GL_NORMALIZE, false, true));
public final BooleanStateStack ditherState = track(new BooleanStateStack(GL11.GL_DITHER, true));
public final BooleanStateStack stencilTest = track(new BooleanStateStack(GL11.GL_STENCIL_TEST));
public final BooleanStateStack lineSmoothState = track(new BooleanStateStack(GL11.GL_LINE_SMOOTH));
public final BooleanStateStack lineStippleState = track(new BooleanStateStack(GL11.GL_LINE_STIPPLE, false, true));
public final BooleanStateStack pointSmoothState = track(new BooleanStateStack(GL11.GL_POINT_SMOOTH, false, true));
public final BooleanStateStack polygonSmoothState = track(new BooleanStateStack(GL11.GL_POLYGON_SMOOTH));
public final BooleanStateStack polygonStippleState = track(new BooleanStateStack(GL11.GL_POLYGON_STIPPLE, false, true));
public final BooleanStateStack multisampleState = track(new BooleanStateStack(GL13.GL_MULTISAMPLE, true));
public final BooleanStateStack sampleAlphaToCoverageState = track(new BooleanStateStack(GL13.GL_SAMPLE_ALPHA_TO_COVERAGE));
public final BooleanStateStack sampleAlphaToOneState = track(new BooleanStateStack(GL13.GL_SAMPLE_ALPHA_TO_ONE));
public final BooleanStateStack sampleCoverageState = track(new BooleanStateStack(GL13.GL_SAMPLE_COVERAGE));
public final BooleanStateStack colorLogicOpState = track(new BooleanStateStack(GL11.GL_COLOR_LOGIC_OP));
public final BooleanStateStack indexLogicOpState = track(new BooleanStateStack(GL11.GL_INDEX_LOGIC_OP, false, true));
public final BooleanStateStack polygonOffsetPointState = track(new BooleanStateStack(GL11.GL_POLYGON_OFFSET_POINT));
public final BooleanStateStack polygonOffsetLineState = track(new BooleanStateStack(GL11.GL_POLYGON_OFFSET_LINE));
public final BooleanStateStack polygonOffsetFillState = track(new BooleanStateStack(GL11.GL_POLYGON_OFFSET_FILL));
public final LineStateStack lineState = member(new LineStateStack(StackIdAllocator.nextId()));
public final PointStateStack pointState = member(new PointStateStack(StackIdAllocator.nextId()));
public final PolygonStateStack polygonState = member(new PolygonStateStack(StackIdAllocator.nextId()));
public final StencilStateStack stencilState = member(new StencilStateStack(StackIdAllocator.nextId()));
public int stencilBitMask = 0xFFFFFFFF;
public final BooleanStateStack autoNormalState = track(new BooleanStateStack(GL11.GL_AUTO_NORMAL, false, true));
public final BooleanStateStack map1Color4State = track(new BooleanStateStack(GL11.GL_MAP1_COLOR_4, false, true));
public final BooleanStateStack map1IndexState = track(new BooleanStateStack(GL11.GL_MAP1_INDEX, false, true));
public final BooleanStateStack map1NormalState = track(new BooleanStateStack(GL11.GL_MAP1_NORMAL, false, true));
public final BooleanStateStack map1TextureCoord1State = track(new BooleanStateStack(GL11.GL_MAP1_TEXTURE_COORD_1, false, true));
public final BooleanStateStack map1TextureCoord2State = track(new BooleanStateStack(GL11.GL_MAP1_TEXTURE_COORD_2, false, true));
public final BooleanStateStack map1TextureCoord3State = track(new BooleanStateStack(GL11.GL_MAP1_TEXTURE_COORD_3, false, true));
public final BooleanStateStack map1TextureCoord4State = track(new BooleanStateStack(GL11.GL_MAP1_TEXTURE_COORD_4, false, true));
public final BooleanStateStack map1Vertex3State = track(new BooleanStateStack(GL11.GL_MAP1_VERTEX_3, false, true));
public final BooleanStateStack map1Vertex4State = track(new BooleanStateStack(GL11.GL_MAP1_VERTEX_4, false, true));
public final BooleanStateStack map2Color4State = track(new BooleanStateStack(GL11.GL_MAP2_COLOR_4, false, true));
public final BooleanStateStack map2IndexState = track(new BooleanStateStack(GL11.GL_MAP2_INDEX, false, true));
public final BooleanStateStack map2NormalState = track(new BooleanStateStack(GL11.GL_MAP2_NORMAL, false, true));
public final BooleanStateStack map2TextureCoord1State = track(new BooleanStateStack(GL11.GL_MAP2_TEXTURE_COORD_1, false, true));
public final BooleanStateStack map2TextureCoord2State = track(new BooleanStateStack(GL11.GL_MAP2_TEXTURE_COORD_2, false, true));
public final BooleanStateStack map2TextureCoord3State = track(new BooleanStateStack(GL11.GL_MAP2_TEXTURE_COORD_3, false, true));
public final BooleanStateStack map2TextureCoord4State = track(new BooleanStateStack(GL11.GL_MAP2_TEXTURE_COORD_4, false, true));
public final BooleanStateStack map2Vertex3State = track(new BooleanStateStack(GL11.GL_MAP2_VERTEX_3, false, true));
public final BooleanStateStack map2Vertex4State = track(new BooleanStateStack(GL11.GL_MAP2_VERTEX_4, false, true));
public final BooleanStateStack[] clipPlaneStates = new BooleanStateStack[GLStateManager.MAX_CLIP_PLANES];
public final ClipPlaneState clipPlaneState = new ClipPlaneState();
public final FloatBuffer queryScratch = BufferUtils.createFloatBuffer(16);
public final DoubleBuffer queryScratchDouble = BufferUtils.createDoubleBuffer(16);
public final MatrixModeStack matrixMode = member(new MatrixModeStack(StackIdAllocator.nextId()));
public final Matrix4fStack modelViewMatrix = new Matrix4fStack(GLStateManager.MAX_MODELVIEW_STACK_DEPTH);
public final Matrix4fStack projectionMatrix = new Matrix4fStack(GLStateManager.MAX_PROJECTION_STACK_DEPTH);
public final BooleanStateStack[] lightStates = new BooleanStateStack[8];
public final LightStateStack[] lightDataStates = new LightStateStack[8];
public final BooleanStateStack colorMaterial = track(new BooleanStateStack(GL11.GL_COLOR_MATERIAL, false, true));
public final IntegerStateStack colorMaterialFace = member(new IntegerStateStack(GL11.GL_FRONT_AND_BACK, StackIdAllocator.nextId()));
public final IntegerStateStack colorMaterialParameter = member(new IntegerStateStack(GL11.GL_AMBIENT_AND_DIFFUSE, StackIdAllocator.nextId()));
public final LightModelStateStack lightModel = member(new LightModelStateStack(StackIdAllocator.nextId()));
public final MaterialStateStack frontMaterial = member(new MaterialStateStack(GL11.GL_FRONT, StackIdAllocator.nextId()));
public final MaterialStateStack backMaterial = member(new MaterialStateStack(GL11.GL_BACK, StackIdAllocator.nextId()));
public final ViewPortStateStack viewportState = member(new ViewPortStateStack(StackIdAllocator.nextId()));
public final ScissorStateStack scissorState = member(new ScissorStateStack(StackIdAllocator.nextId()));
public int activeProgram = 0;
public final IntegerStateStack programStack = member(new IntegerStateStack(0, StackIdAllocator.nextId()));
public int listBase = 0;
public int boundVBO;
public int boundVAO;
public int boundPixelUnpackBuffer;
public int boundPixelPackBuffer;
public int boundCopyReadBuffer;
public int boundCopyWriteBuffer;
public final Int2IntMap boundOtherBuffers = new Int2IntOpenHashMap();
public PixelUnpackState pixelUnpackState = PixelUnpackState.DEFAULT;
public final int[] clientAttribSavedTextureUnit = new int[GLStateManager.CLIENT_ATTRIB_STACK_DEPTH];
public final int[] clientAttribSavedVertexFlags = new int[GLStateManager.CLIENT_ATTRIB_STACK_DEPTH];
public int clientAttribStackPointer = 0;
public int restoreChangedMask;
public long restoreUnitChangedMask;
public final BlendState vanillaBlendBefore = new BlendState();
public final BlendState vanillaBlendAfter = new BlendState();
public boolean vanillaBlendEnabledBefore;
public int drawFramebuffer = 0;
public int readFramebuffer = 0;
public int texGenGeneration;
public int defaultVAO;
public final VAOManager vaos = new VAOManager();
public final StreamingVaos streamingVaos = new StreamingVaos();
public int fontVao;
public boolean lockBindCallback;
public int foreignDrawDepth;
public int internalGlDepth;
public int internalDrawDepth;
public int savedFilterTextureId;
public int savedFilterMin = -1;
public int savedFilterMag = -1;
public float savedFilterAniso = -1.0f;
public boolean wideLineEmulationActive = false;
public boolean lineStippleActive = false;
public Instancing ffpInstancing = Instancing.NONE;
public float lastBrightnessX;
public float lastBrightnessY;
public final Matrix4f multMatrix = new Matrix4f();
public final Matrix4d conversionMatrix4d = new Matrix4d();
public final Matrix4f conversionMatrix4f = new Matrix4f();
public final Matrix4f gluMatrix = new Matrix4f();
public final Matrix4f gluMatrix2 = new Matrix4f();
public final Vector4f gluVec4 = new Vector4f();
public final FloatBuffer gluBuffer = BufferUtils.createFloatBuffer(16);
public final float[] texGenTempPlane = new float[4];
public final IntBuffer shaderBuf = BufferUtils.createIntBuffer(8);
public final FloatBuffer projectionMatrixBuffer = BufferUtils.createFloatBuffer(16);
public final ShaderManager ffp = new ShaderManager();
GLContextState() {
allStacksBuilder.addAll(textures.idStacks());
for (int i = 0; i < lightStates.length; i++) {
lightStates[i] = track(new BooleanStateStack(GL11.GL_LIGHT0 + i, false, true));
}
for (int i = 0; i < GLStateManager.MAX_CLIP_PLANES; i++) {
clipPlaneStates[i] = track(new ClipPlaneBooleanState(this, i, GL11.GL_CLIP_PLANE0 + i));
}
}
void init() {
for (int i = 0; i < lightDataStates.length; i++) {
lightDataStates[i] = member(new LightStateStack(GL11.GL_LIGHT0 + i, StackIdAllocator.nextId()));
}
allStacksBuilder.add(ffp.normalStack);
allStacksBuilder.add(ffp.texCoordStack);
allStacks = allStacksBuilder.toArray(new IStateStack<?>[0]);
final int capacity = StackIdAllocator.allocated();
stackById = new CowStateStack<?>[capacity];
depthsById = new CowDepths[capacity];
restoreBitById = new int[capacity];
restoreUnitById = new int[capacity];
kindById = new byte[capacity];
for (final IStateStack<?> stack : allStacks) {
final CowStateStack<?> s = (CowStateStack<?>) stack;
final int id = s.stackId();
stackById[id] = s;
depthsById[id] = s.cowDepths();
depthsById[id].owner = this;
restoreBitById[id] = s.restoreBit();
restoreUnitById[id] = s.restoreUnit();
kindById[id] = CowDispatch.kindOf(s);
}
modifiedIds = new int[GLStateManager.MAX_ATTRIB_STACK_DEPTH][allStacks.length];
modifiedCount = new int[GLStateManager.MAX_ATTRIB_STACK_DEPTH];
}
}
@@ -0,0 +1,655 @@
/*
* Copyright LWJGL. All rights reserved. Modified by IMS for use in Iris (net.coderbot.iris.gl).
* License terms: https://www.lwjgl.org/license
*/
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizons.angelica.glsm.backend.BackendManager;
import com.gtnewhorizons.angelica.glsm.backend.GLDebugMessageListener;
import com.gtnewhorizons.angelica.glsm.hooks.GLSMInitConfig;
import org.lwjgl.opengl.EXTBlendColor;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL12;
import org.lwjgl.opengl.GL13;
import org.lwjgl.opengl.GL14;
import org.lwjgl.opengl.GL30;
import org.lwjgl.opengl.GL43;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.util.Arrays;
public final class GLDebug {
/**
* Sets up debug callbacks
*
* @return 0 for failure, 1 for success, 2 for restart required.
*/
public static int setupDebugMessageCallback() {
if (Thread.currentThread() != GLStateManager.getMainThread()) {
GLStateManager.LOGGER.warn("setupDebugMessageCallback called from non-main thread!");
return 0;
}
return setupDebugMessageCallbackImpl();
}
public static Throwable filterStackTrace(Throwable throwable, int offset) {
final StackTraceElement[] elems = throwable.getStackTrace();
final StackTraceElement[] filtered = new StackTraceElement[elems.length];
int j = 0;
for (int i = offset; i < elems.length; i++) {
String className = elems[i].getClassName();
if (className == null) {
className = "";
}
filtered[j++] = elems[i];
}
final StackTraceElement[] newElems = new StackTraceElement[j];
System.arraycopy(filtered, 0, newElems, 0, j);
throwable.setStackTrace(newElems);
return throwable;
}
private static String buildStackTrace() {
final StackTraceElement[] elems = filterStackTrace(new Throwable(), 4).getStackTrace();
final StringBuilder sb = new StringBuilder();
for (StackTraceElement elem : elems) {
sb.append("\n\t").append(elem.toString());
}
return sb.toString();
}
private static void logDebugMessage(int id, String source, String type, String severity, String message) {
final String fullMessage = String.format("[GL] %s %s (0x%X) from %s: %s%s", severity, type, id, source, message, buildStackTrace());
if ("HIGH".equals(severity)) {
GLStateManager.LOGGER.error(fullMessage);
} else if ("MEDIUM".equals(severity)) {
GLStateManager.LOGGER.warn(fullMessage);
} else {
GLStateManager.LOGGER.info(fullMessage);
}
}
private static int setupDebugMessageCallbackImpl() {
return BackendManager.RENDER_BACKEND.setupDebugOutput(
(source, type, id, severity, message) ->
logDebugMessage(id, getDebugSource(source), getDebugType(type), getDebugSeverity(severity), message)
);
}
public static int disableDebugMessages() {
return BackendManager.RENDER_BACKEND.disableDebugOutput();
}
private static String getDebugSource(int source) {
return switch (source) {
case GL43.GL_DEBUG_SOURCE_API -> "API";
case GL43.GL_DEBUG_SOURCE_WINDOW_SYSTEM -> "WINDOW SYSTEM";
case GL43.GL_DEBUG_SOURCE_SHADER_COMPILER -> "SHADER COMPILER";
case GL43.GL_DEBUG_SOURCE_THIRD_PARTY -> "THIRD PARTY";
case GL43.GL_DEBUG_SOURCE_APPLICATION -> "APPLICATION";
case GL43.GL_DEBUG_SOURCE_OTHER -> "OTHER";
default -> String.format("Unknown [0x%X]", source);
};
}
private static String getDebugType(int type) {
return switch (type) {
case GL43.GL_DEBUG_TYPE_ERROR -> "ERROR";
case GL43.GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR -> "DEPRECATED BEHAVIOR";
case GL43.GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR -> "UNDEFINED BEHAVIOR";
case GL43.GL_DEBUG_TYPE_PORTABILITY -> "PORTABILITY";
case GL43.GL_DEBUG_TYPE_PERFORMANCE -> "PERFORMANCE";
case GL43.GL_DEBUG_TYPE_OTHER -> "OTHER";
case GL43.GL_DEBUG_TYPE_MARKER -> "MARKER";
default -> String.format("Unknown [0x%X]", type);
};
}
private static String getDebugSeverity(int severity) {
return switch (severity) {
case GL43.GL_DEBUG_SEVERITY_NOTIFICATION -> "NOTIFICATION";
case GL43.GL_DEBUG_SEVERITY_HIGH -> "HIGH";
case GL43.GL_DEBUG_SEVERITY_MEDIUM -> "MEDIUM";
case GL43.GL_DEBUG_SEVERITY_LOW -> "LOW";
default -> String.format("Unknown [0x%X]", severity);
};
}
public static String getMatrixModeName(int mode) {
return switch (mode) {
case GL11.GL_MODELVIEW -> "MODELVIEW";
case GL11.GL_PROJECTION -> "PROJECTION";
case GL11.GL_TEXTURE -> "TEXTURE";
case GL11.GL_COLOR -> "COLOR";
default -> String.format("0x%X", mode);
};
}
public static String getCapabilityName(int cap) {
return switch (cap) {
case GL11.GL_ALPHA_TEST -> "ALPHA_TEST";
case GL11.GL_BLEND -> "BLEND";
case GL11.GL_COLOR_MATERIAL -> "COLOR_MATERIAL";
case GL11.GL_CULL_FACE -> "CULL_FACE";
case GL11.GL_DEPTH_TEST -> "DEPTH_TEST";
case GL11.GL_FOG -> "FOG";
case GL11.GL_LIGHTING -> "LIGHTING";
case GL11.GL_LINE_SMOOTH -> "LINE_SMOOTH";
case GL11.GL_NORMALIZE -> "NORMALIZE";
case GL11.GL_POINT_SMOOTH -> "POINT_SMOOTH";
case GL11.GL_POLYGON_OFFSET_FILL -> "POLYGON_OFFSET_FILL";
case GL11.GL_POLYGON_OFFSET_LINE -> "POLYGON_OFFSET_LINE";
case GL11.GL_POLYGON_SMOOTH -> "POLYGON_SMOOTH";
case GL12.GL_RESCALE_NORMAL -> "RESCALE_NORMAL";
case GL11.GL_SCISSOR_TEST -> "SCISSOR_TEST";
case GL11.GL_STENCIL_TEST -> "STENCIL_TEST";
case GL11.GL_TEXTURE_1D -> "TEXTURE_1D";
case GL11.GL_TEXTURE_2D -> "TEXTURE_2D";
case GL11.GL_LIGHT0 -> "LIGHT0";
case GL11.GL_LIGHT1 -> "LIGHT1";
case GL11.GL_LIGHT2 -> "LIGHT2";
case GL11.GL_LIGHT3 -> "LIGHT3";
case GL11.GL_LIGHT4 -> "LIGHT4";
case GL11.GL_LIGHT5 -> "LIGHT5";
case GL11.GL_LIGHT6 -> "LIGHT6";
case GL11.GL_LIGHT7 -> "LIGHT7";
default -> String.format("0x%X", cap);
};
}
public static String getComparisonFuncName(int func) {
return switch (func) {
case GL11.GL_NEVER -> "NEVER";
case GL11.GL_LESS -> "LESS";
case GL11.GL_EQUAL -> "EQUAL";
case GL11.GL_LEQUAL -> "LEQUAL";
case GL11.GL_GREATER -> "GREATER";
case GL11.GL_NOTEQUAL -> "NOTEQUAL";
case GL11.GL_GEQUAL -> "GEQUAL";
case GL11.GL_ALWAYS -> "ALWAYS";
default -> String.format("0x%X", func);
};
}
public static String getBlendFactorName(int factor) {
return switch (factor) {
case GL11.GL_ZERO -> "ZERO";
case GL11.GL_ONE -> "ONE";
case GL11.GL_SRC_COLOR -> "SRC_COLOR";
case GL11.GL_ONE_MINUS_SRC_COLOR -> "ONE_MINUS_SRC_COLOR";
case GL11.GL_DST_COLOR -> "DST_COLOR";
case GL11.GL_ONE_MINUS_DST_COLOR -> "ONE_MINUS_DST_COLOR";
case GL11.GL_SRC_ALPHA -> "SRC_ALPHA";
case GL11.GL_ONE_MINUS_SRC_ALPHA -> "ONE_MINUS_SRC_ALPHA";
case GL11.GL_DST_ALPHA -> "DST_ALPHA";
case GL11.GL_ONE_MINUS_DST_ALPHA -> "ONE_MINUS_DST_ALPHA";
case EXTBlendColor.GL_CONSTANT_COLOR_EXT -> "CONSTANT_COLOR";
case EXTBlendColor.GL_ONE_MINUS_CONSTANT_COLOR_EXT -> "ONE_MINUS_CONSTANT_COLOR";
case EXTBlendColor.GL_CONSTANT_ALPHA_EXT -> "CONSTANT_ALPHA";
case EXTBlendColor.GL_ONE_MINUS_CONSTANT_ALPHA_EXT -> "ONE_MINUS_CONSTANT_ALPHA";
case GL11.GL_SRC_ALPHA_SATURATE -> "SRC_ALPHA_SATURATE";
default -> String.format("0x%X", factor);
};
}
public static String getShadeModelName(int mode) {
return switch (mode) {
case GL11.GL_FLAT -> "FLAT";
case GL11.GL_SMOOTH -> "SMOOTH";
default -> String.format("0x%X", mode);
};
}
public static String getTextureTargetName(int target) {
return switch (target) {
case GL11.GL_TEXTURE_1D -> "TEXTURE_1D";
case GL11.GL_TEXTURE_2D -> "TEXTURE_2D";
case GL12.GL_TEXTURE_3D -> "TEXTURE_3D";
case GL13.GL_TEXTURE_CUBE_MAP -> "TEXTURE_CUBE_MAP";
default -> String.format("0x%X", target);
};
}
public static String getTexturePnameName(int pname) {
return switch (pname) {
case GL11.GL_TEXTURE_MIN_FILTER -> "TEXTURE_MIN_FILTER";
case GL11.GL_TEXTURE_MAG_FILTER -> "TEXTURE_MAG_FILTER";
case GL11.GL_TEXTURE_WRAP_S -> "TEXTURE_WRAP_S";
case GL11.GL_TEXTURE_WRAP_T -> "TEXTURE_WRAP_T";
case GL12.GL_TEXTURE_WRAP_R -> "TEXTURE_WRAP_R";
case GL12.GL_TEXTURE_MIN_LOD -> "TEXTURE_MIN_LOD";
case GL12.GL_TEXTURE_MAX_LOD -> "TEXTURE_MAX_LOD";
case GL12.GL_TEXTURE_BASE_LEVEL -> "TEXTURE_BASE_LEVEL";
case GL12.GL_TEXTURE_MAX_LEVEL -> "TEXTURE_MAX_LEVEL";
case GL11.GL_TEXTURE_BORDER_COLOR -> "TEXTURE_BORDER_COLOR";
case GL11.GL_TEXTURE_PRIORITY -> "TEXTURE_PRIORITY";
case GL14.GL_TEXTURE_LOD_BIAS -> "TEXTURE_LOD_BIAS";
case GL14.GL_GENERATE_MIPMAP -> "GENERATE_MIPMAP";
default -> String.format("0x%X", pname);
};
}
public static String getTextureFormatName(int format) {
return switch (format) {
case GL11.GL_RED -> "RED";
case GL11.GL_GREEN -> "GREEN";
case GL11.GL_BLUE -> "BLUE";
case GL11.GL_ALPHA -> "ALPHA";
case GL11.GL_RGB -> "RGB";
case GL11.GL_RGBA -> "RGBA";
case GL11.GL_LUMINANCE -> "LUMINANCE";
case GL11.GL_LUMINANCE_ALPHA -> "LUMINANCE_ALPHA";
case GL12.GL_BGR -> "BGR";
case GL12.GL_BGRA -> "BGRA";
case GL11.GL_DEPTH_COMPONENT -> "DEPTH_COMPONENT";
case GL11.GL_STENCIL_INDEX -> "STENCIL_INDEX";
case GL30.GL_DEPTH_STENCIL -> "DEPTH_STENCIL";
// Internal formats
case GL11.GL_RGB8 -> "RGB8";
case GL11.GL_RGBA8 -> "RGBA8";
case GL11.GL_ALPHA8 -> "ALPHA8";
case GL11.GL_LUMINANCE8 -> "LUMINANCE8";
case GL11.GL_LUMINANCE8_ALPHA8 -> "LUMINANCE8_ALPHA8";
default -> String.format("0x%X", format);
};
}
public static String getLightName(int light) {
return switch (light) {
case GL11.GL_LIGHT0 -> "LIGHT0";
case GL11.GL_LIGHT1 -> "LIGHT1";
case GL11.GL_LIGHT2 -> "LIGHT2";
case GL11.GL_LIGHT3 -> "LIGHT3";
case GL11.GL_LIGHT4 -> "LIGHT4";
case GL11.GL_LIGHT5 -> "LIGHT5";
case GL11.GL_LIGHT6 -> "LIGHT6";
case GL11.GL_LIGHT7 -> "LIGHT7";
default -> String.format("0x%X", light);
};
}
public static String getLightPnameName(int pname) {
return switch (pname) {
case GL11.GL_AMBIENT -> "AMBIENT";
case GL11.GL_DIFFUSE -> "DIFFUSE";
case GL11.GL_SPECULAR -> "SPECULAR";
case GL11.GL_POSITION -> "POSITION";
case GL11.GL_SPOT_DIRECTION -> "SPOT_DIRECTION";
case GL11.GL_SPOT_EXPONENT -> "SPOT_EXPONENT";
case GL11.GL_SPOT_CUTOFF -> "SPOT_CUTOFF";
case GL11.GL_CONSTANT_ATTENUATION -> "CONSTANT_ATTENUATION";
case GL11.GL_LINEAR_ATTENUATION -> "LINEAR_ATTENUATION";
case GL11.GL_QUADRATIC_ATTENUATION -> "QUADRATIC_ATTENUATION";
default -> String.format("0x%X", pname);
};
}
public static String getMaterialPnameName(int pname) {
return switch (pname) {
case GL11.GL_AMBIENT -> "AMBIENT";
case GL11.GL_DIFFUSE -> "DIFFUSE";
case GL11.GL_SPECULAR -> "SPECULAR";
case GL11.GL_EMISSION -> "EMISSION";
case GL11.GL_SHININESS -> "SHININESS";
case GL11.GL_AMBIENT_AND_DIFFUSE -> "AMBIENT_AND_DIFFUSE";
case GL11.GL_COLOR_INDEXES -> "COLOR_INDEXES";
default -> String.format("0x%X", pname);
};
}
public static String getFaceName(int face) {
return switch (face) {
case GL11.GL_FRONT -> "FRONT";
case GL11.GL_BACK -> "BACK";
case GL11.GL_FRONT_AND_BACK -> "FRONT_AND_BACK";
default -> String.format("0x%X", face);
};
}
public static String getColorMaterialModeName(int mode) {
return switch (mode) {
case GL11.GL_AMBIENT -> "AMBIENT";
case GL11.GL_DIFFUSE -> "DIFFUSE";
case GL11.GL_SPECULAR -> "SPECULAR";
case GL11.GL_EMISSION -> "EMISSION";
case GL11.GL_AMBIENT_AND_DIFFUSE -> "AMBIENT_AND_DIFFUSE";
default -> String.format("0x%X", mode);
};
}
public static String getFogPnameName(int pname) {
return switch (pname) {
case GL11.GL_FOG_MODE -> "FOG_MODE";
case GL11.GL_FOG_DENSITY -> "FOG_DENSITY";
case GL11.GL_FOG_START -> "FOG_START";
case GL11.GL_FOG_END -> "FOG_END";
case GL11.GL_FOG_INDEX -> "FOG_INDEX";
case GL11.GL_FOG_COLOR -> "FOG_COLOR";
default -> String.format("0x%X", pname);
};
}
public static String getLightModelPnameName(int pname) {
return switch (pname) {
case GL11.GL_LIGHT_MODEL_AMBIENT -> "LIGHT_MODEL_AMBIENT";
case GL11.GL_LIGHT_MODEL_LOCAL_VIEWER -> "LIGHT_MODEL_LOCAL_VIEWER";
case GL11.GL_LIGHT_MODEL_TWO_SIDE -> "LIGHT_MODEL_TWO_SIDE";
case GL12.GL_LIGHT_MODEL_COLOR_CONTROL -> "LIGHT_MODEL_COLOR_CONTROL";
default -> String.format("0x%X", pname);
};
}
public static String getClearMaskString(int mask) {
StringBuilder sb = new StringBuilder();
boolean first = true;
if ((mask & GL11.GL_COLOR_BUFFER_BIT) != 0) {
sb.append("COLOR");
first = false;
}
if ((mask & GL11.GL_DEPTH_BUFFER_BIT) != 0) {
if (!first) sb.append("|");
sb.append("DEPTH");
first = false;
}
if ((mask & GL11.GL_STENCIL_BUFFER_BIT) != 0) {
if (!first) sb.append("|");
sb.append("STENCIL");
first = false;
}
if ((mask & GL11.GL_ACCUM_BUFFER_BIT) != 0) {
if (!first) sb.append("|");
sb.append("ACCUM");
}
return sb.toString();
}
public static String getCullFaceName(int mode) {
return switch (mode) {
case GL11.GL_FRONT -> "FRONT";
case GL11.GL_BACK -> "BACK";
case GL11.GL_FRONT_AND_BACK -> "FRONT_AND_BACK";
default -> String.format("0x%X", mode);
};
}
public static String getLogicOpName(int opcode) {
return switch (opcode) {
case GL11.GL_CLEAR -> "CLEAR";
case GL11.GL_AND -> "AND";
case GL11.GL_AND_REVERSE -> "AND_REVERSE";
case GL11.GL_COPY -> "COPY";
case GL11.GL_AND_INVERTED -> "AND_INVERTED";
case GL11.GL_NOOP -> "NOOP";
case GL11.GL_XOR -> "XOR";
case GL11.GL_OR -> "OR";
case GL11.GL_NOR -> "NOR";
case GL11.GL_EQUIV -> "EQUIV";
case GL11.GL_INVERT -> "INVERT";
case GL11.GL_OR_REVERSE -> "OR_REVERSE";
case GL11.GL_COPY_INVERTED -> "COPY_INVERTED";
case GL11.GL_OR_INVERTED -> "OR_INVERTED";
case GL11.GL_NAND -> "NAND";
case GL11.GL_SET -> "SET";
default -> String.format("0x%X", opcode);
};
}
public static String getFramebufferStatusName(int status) {
return switch (status) {
case GL30.GL_FRAMEBUFFER_COMPLETE -> "COMPLETE";
case GL30.GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT -> "INCOMPLETE_ATTACHMENT";
case GL30.GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT -> "INCOMPLETE_MISSING_ATTACHMENT";
case GL30.GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER -> "INCOMPLETE_DRAW_BUFFER";
case GL30.GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER -> "INCOMPLETE_READ_BUFFER";
case GL30.GL_FRAMEBUFFER_UNSUPPORTED -> "UNSUPPORTED";
case GL30.GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE -> "INCOMPLETE_MULTISAMPLE";
case 0 -> "NO_FRAMEBUFFER_BOUND";
default -> String.format("UNKNOWN(0x%X)", status);
};
}
public static String getInternalFormatName(int format) {
return switch (format) {
// Basic
case GL11.GL_RGBA -> "RGBA";
case GL11.GL_RGB -> "RGB";
// 8-bit normalized
case GL30.GL_R8 -> "R8";
case GL30.GL_RG8 -> "RG8";
case GL11.GL_RGB8 -> "RGB8";
case GL11.GL_RGBA8 -> "RGBA8";
// 8-bit signed normalized
case 0x8F94 -> "R8_SNORM";
case 0x8F95 -> "RG8_SNORM";
case 0x8F96 -> "RGB8_SNORM";
case 0x8F97 -> "RGBA8_SNORM";
// 16-bit normalized
case GL30.GL_R16 -> "R16";
case GL30.GL_RG16 -> "RG16";
case GL11.GL_RGB16 -> "RGB16";
case GL11.GL_RGBA16 -> "RGBA16";
// 16-bit float
case GL30.GL_R16F -> "R16F";
case GL30.GL_RG16F -> "RG16F";
case GL30.GL_RGB16F -> "RGB16F";
case GL30.GL_RGBA16F -> "RGBA16F";
// 32-bit float
case GL30.GL_R32F -> "R32F";
case GL30.GL_RG32F -> "RG32F";
case GL30.GL_RGB32F -> "RGB32F";
case GL30.GL_RGBA32F -> "RGBA32F";
// Packed
case GL30.GL_R11F_G11F_B10F -> "R11F_G11F_B10F";
case GL11.GL_RGB10_A2 -> "RGB10_A2";
default -> String.format("0x%X", format);
};
}
private static DebugState debugState;
private interface DebugState {
void nameObject(int id, int object, String name);
void pushGroup(String name);
void popGroup();
void debugMessage(String name);
String getObjectLabel(int glProgram, int program);
}
private static class KHRDebugState implements DebugState {
private static final int ID = 0;
private int depth = 0;
private static final int maxDepth = BackendManager.RENDER_BACKEND.getInteger(GL43.GL_MAX_DEBUG_GROUP_STACK_DEPTH);
private final String[] groupStack = new String[maxDepth + 1];
@Override
public void nameObject(int id, int object, String name) {
BackendManager.RENDER_BACKEND.objectLabel(id, object, name);
}
@Override
public void pushGroup(String name) {
if (depth + 1 > maxDepth) {
final StringBuilder sb = new StringBuilder();
sb.append("Debug group stack overflow (maxDepth=").append(maxDepth).append("), current stack:\n");
for (int i = 0; i < depth; i++) {
sb.append(" [").append(i).append("] ").append(groupStack[i]).append('\n');
}
sb.append(" -> ").append(name).append(" (rejected)");
throw new RuntimeException(sb.toString());
}
groupStack[depth] = name;
BackendManager.RENDER_BACKEND.pushDebugGroup(GL43.GL_DEBUG_SOURCE_APPLICATION, ID, name);
depth++;
}
@Override
public void popGroup() {
if (depth - 1 < 0) {
throw new RuntimeException("Stack underflow");
}
BackendManager.RENDER_BACKEND.popDebugGroup();
depth--;
groupStack[depth] = null;
}
@Override
public void debugMessage(String message) {
BackendManager.RENDER_BACKEND.debugMessageInsert(
GL43.GL_DEBUG_SOURCE_APPLICATION,
GL43.GL_DEBUG_TYPE_MARKER,
ID,
GL43.GL_DEBUG_SEVERITY_NOTIFICATION,
message);
}
@Override
public String getObjectLabel(int glProgram, int program) {
if (program == 0) return "";
return BackendManager.RENDER_BACKEND.getObjectLabel(glProgram, program);
}
}
private static class UnsupportedDebugState implements DebugState {
@Override
public void nameObject(int id, int object, String name) {
}
@Override
public void pushGroup(String name) {
}
@Override
public void popGroup() {
}
@Override
public void debugMessage(String name) {
}
@Override
public String getObjectLabel(int glProgram, int program) {
return "";
}
}
public static void initDebugState() {
final GLSMInitConfig cfg = GLStateManager.getInitConfig();
final boolean userRequested = cfg != null && (cfg.isLwjglDebug() || CaptureGate.enabledAtStartup());
if (userRequested && BackendManager.RENDER_BACKEND.supportsDebugOutput()) {
debugState = new KHRDebugState();
} else {
debugState = new UnsupportedDebugState();
}
}
public static void nameObject(int id, int object, String name) {
if (debugState != null && Thread.currentThread() == GLStateManager.getMainThread()) {
debugState.nameObject(id, object, name);
}
}
/**
* Overload that lets you gate the String concat behind the debugState
*/
public static void pushGroup(String prefix, long value) {
if (debugState != null && Thread.currentThread() == GLStateManager.getMainThread()) {
debugState.pushGroup(prefix + value);
}
}
public static boolean isActive() {
return debugState instanceof KHRDebugState && CaptureGate.markersThisFrame && Thread.currentThread() == GLStateManager.getMainThread();
}
public static void pushGroup(String group) {
if (debugState != null && Thread.currentThread() == GLStateManager.getMainThread()) {
debugState.pushGroup(group);
}
}
public static void popGroup() {
if (debugState != null && Thread.currentThread() == GLStateManager.getMainThread()) {
debugState.popGroup();
}
}
public static void resetGroupStack() {
if (debugState instanceof KHRDebugState khr) {
khr.depth = 0;
Arrays.fill(khr.groupStack, null);
}
}
public static void debugMessage(String message) {
if (debugState != null && Thread.currentThread() == GLStateManager.getMainThread()) {
debugState.debugMessage(message);
}
}
public static String getObjectLabel(int glProgram, int program) {
if (debugState != null && Thread.currentThread() == GLStateManager.getMainThread()) {
return debugState.getObjectLabel(glProgram, program);
}
return "";
}
private static final Scope NOOP_SCOPE = () -> {};
private static final Scope ACTIVE_SCOPE = GLDebug::popGroup;
public static Scope scope(String name) {
if (isActive()) {
debugState.pushGroup(name);
return ACTIVE_SCOPE;
}
return NOOP_SCOPE;
}
@FunctionalInterface
public interface Scope extends AutoCloseable {
@Override void close();
}
private static MethodHandle khrCallbackInvoke;
private static boolean khrCallbackInvokeResolved;
/// Use reflection because of lwjgl3ify
static GLDebugMessageListener adaptDebugCallback(Object callback) {
if (callback == null) return null;
final MethodHandle invoke = resolveKhrCallbackInvoke(callback.getClass());
if (invoke == null) return null;
return (source, type, id, severity, length, message, userParam) -> {
try {
invoke.invoke(callback, source, type, id, severity, length, message, userParam);
} catch (Throwable t) {
GLStateManager.LOGGER.warn("Debug message callback threw", t);
}
};
}
private static synchronized MethodHandle resolveKhrCallbackInvoke(Class<?> callbackClass) {
if (!khrCallbackInvokeResolved) {
khrCallbackInvokeResolved = true;
try {
khrCallbackInvoke = MethodHandles.publicLookup().findVirtual(callbackClass, "invoke", MethodType.methodType(void.class, int.class, int.class, int.class, int.class, int.class, long.class, long.class));
} catch (ReflectiveOperationException e) {
GLStateManager.LOGGER.warn("{} has no public invoke(int,int,int,int,int,long,long); debug messages registered through it will not be delivered.", callbackClass.getName());
}
}
return khrCallbackInvoke;
}
}
@@ -0,0 +1,34 @@
package com.gtnewhorizons.angelica.glsm;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL13;
import org.lwjgl.opengl.GL20;
import org.lwjgl.opengl.GL30;
import org.lwjgl.opengl.GL31;
import org.lwjgl.opengl.GL32;
public final class GLESCaps {
private GLESCaps() {}
public static boolean isCapAllowed(int cap, boolean isGLES, boolean hasClipCullDistance) {
if (!isGLES) return true;
if (cap >= GL11.GL_CLIP_PLANE0 && cap < GL11.GL_CLIP_PLANE0 + GLStateManager.MAX_CLIP_PLANES) {
return hasClipCullDistance;
}
return switch (cap) {
case GL11.GL_LINE_SMOOTH,
GL11.GL_POLYGON_SMOOTH,
GL11.GL_COLOR_LOGIC_OP,
GL11.GL_POLYGON_OFFSET_POINT,
GL11.GL_POLYGON_OFFSET_LINE,
GL13.GL_MULTISAMPLE,
GL13.GL_SAMPLE_ALPHA_TO_ONE,
GL20.GL_VERTEX_PROGRAM_POINT_SIZE,
GL30.GL_FRAMEBUFFER_SRGB,
GL31.GL_PRIMITIVE_RESTART,
GL32.GL_DEPTH_CLAMP -> false;
default -> true;
};
}
}
@@ -0,0 +1,86 @@
package com.gtnewhorizons.angelica.glsm;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL12;
import org.lwjgl.opengl.GL14;
import org.lwjgl.opengl.GL30;
import org.lwjgl.opengl.GL31;
public final class GLESFormatRemap {
private GLESFormatRemap() {}
public record Result(int internalFormat, int format, int type) {}
public static Result apply(int internalformat, int format, int type, boolean isGLES) {
internalformat = promoteAlphaFormat(internalformat);
if (isGLES) {
internalformat = remapInternalFormat(internalformat);
if (type == GL12.GL_UNSIGNED_INT_8_8_8_8_REV && (format == GL12.GL_BGRA || format == GL11.GL_RGBA)) {
type = GL11.GL_UNSIGNED_BYTE;
}
if (isGenericPixelType(type)) {
type = typeForInternalFormatES32(internalformat, type);
}
}
return new Result(internalformat, format, type);
}
public static int promoteAlphaFormat(int internalformat) {
return switch (internalformat) {
case GL11.GL_ALPHA4 -> GL11.GL_RGBA4;
case GL11.GL_ALPHA8 -> GL11.GL_RGBA8;
case GL11.GL_ALPHA12 -> GL11.GL_RGBA12;
case GL11.GL_ALPHA16 -> GL11.GL_RGBA16;
default -> internalformat;
};
}
public static int remapInternalFormat(int internalformat) {
return switch (internalformat) {
case GL11.GL_RGB16, GL11.GL_RGB12 -> GL30.GL_RGB16F;
case GL11.GL_RGBA16, GL11.GL_RGBA12 -> GL30.GL_RGBA16F;
case GL30.GL_R16 -> GL30.GL_R16F;
case GL30.GL_RG16 -> GL30.GL_RG16F;
case GL11.GL_RGB10, GL11.GL_R3_G3_B2 -> GL11.GL_RGB8;
default -> internalformat;
};
}
public static int remapPixelType(int format, int type) {
if (type == GL12.GL_UNSIGNED_INT_8_8_8_8_REV && (format == GL12.GL_BGRA || format == GL11.GL_RGBA)) {
return GL11.GL_UNSIGNED_BYTE;
}
return type;
}
public static int typeForInternalFormatES32(int internalformat, int type) {
return switch (internalformat) {
case GL30.GL_R16F, GL30.GL_RG16F, GL30.GL_RGB16F, GL30.GL_RGBA16F -> GL30.GL_HALF_FLOAT;
case GL30.GL_R32F, GL30.GL_RG32F, GL30.GL_RGB32F, GL30.GL_RGBA32F -> GL11.GL_FLOAT;
case GL31.GL_R8_SNORM, GL31.GL_RG8_SNORM, GL31.GL_RGB8_SNORM, GL31.GL_RGBA8_SNORM -> GL11.GL_BYTE;
case GL30.GL_R11F_G11F_B10F, GL30.GL_RGB9_E5 -> GL30.GL_HALF_FLOAT;
case GL11.GL_RGB10_A2 -> GL12.GL_UNSIGNED_INT_2_10_10_10_REV;
case GL30.GL_R8I, GL30.GL_RG8I, GL30.GL_RGB8I, GL30.GL_RGBA8I,
GL30.GL_R16I, GL30.GL_RG16I, GL30.GL_RGB16I, GL30.GL_RGBA16I,
GL30.GL_R32I, GL30.GL_RG32I, GL30.GL_RGB32I, GL30.GL_RGBA32I -> GL11.GL_INT;
case GL30.GL_R8UI, GL30.GL_RG8UI, GL30.GL_RGB8UI, GL30.GL_RGBA8UI,
GL30.GL_R16UI, GL30.GL_RG16UI, GL30.GL_RGB16UI, GL30.GL_RGBA16UI,
GL30.GL_R32UI, GL30.GL_RG32UI, GL30.GL_RGB32UI, GL30.GL_RGBA32UI -> GL11.GL_UNSIGNED_INT;
case GL14.GL_DEPTH_COMPONENT24 -> GL11.GL_UNSIGNED_INT;
case GL30.GL_DEPTH_COMPONENT32F -> GL11.GL_FLOAT;
case GL30.GL_DEPTH24_STENCIL8 -> GL30.GL_UNSIGNED_INT_24_8;
case GL30.GL_DEPTH32F_STENCIL8 -> GL30.GL_FLOAT_32_UNSIGNED_INT_24_8_REV;
default -> type;
};
}
public static boolean isGenericPixelType(int type) {
return type == GL11.GL_UNSIGNED_BYTE
|| type == GL11.GL_UNSIGNED_SHORT
|| type == GL11.GL_UNSIGNED_INT
|| type == GL11.GL_BYTE
|| type == GL11.GL_SHORT
|| type == GL11.GL_INT;
}
}
@@ -0,0 +1,12 @@
package com.gtnewhorizons.angelica.glsm;
import org.lwjgl.opengl.OpenGLException;
final class GLErrorReporter {
private GLErrorReporter() {}
static void reportError(int error) {
throw new OpenGLException(error);
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,57 @@
package com.gtnewhorizons.angelica.glsm;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL12;
import org.lwjgl.opengl.GL30;
public final class GLTypes {
private GLTypes() {}
public static int sizeBytes(int glType) {
return switch (glType) {
case GL11.GL_BYTE, GL11.GL_UNSIGNED_BYTE -> 1;
case GL11.GL_SHORT, GL11.GL_UNSIGNED_SHORT, GL30.GL_HALF_FLOAT -> 2;
case GL11.GL_INT, GL11.GL_UNSIGNED_INT, GL11.GL_FLOAT -> 4;
case GL11.GL_DOUBLE -> 8;
default -> throw new IllegalArgumentException("Unknown GL type: 0x" + Integer.toHexString(glType));
};
}
public static int packedTexelBytes(int glType) {
return switch (glType) {
case GL12.GL_UNSIGNED_BYTE_3_3_2, GL12.GL_UNSIGNED_BYTE_2_3_3_REV -> 1;
case GL12.GL_UNSIGNED_SHORT_5_6_5, GL12.GL_UNSIGNED_SHORT_5_6_5_REV,
GL12.GL_UNSIGNED_SHORT_4_4_4_4, GL12.GL_UNSIGNED_SHORT_4_4_4_4_REV,
GL12.GL_UNSIGNED_SHORT_5_5_5_1, GL12.GL_UNSIGNED_SHORT_1_5_5_5_REV -> 2;
case GL12.GL_UNSIGNED_INT_8_8_8_8, GL12.GL_UNSIGNED_INT_8_8_8_8_REV,
GL12.GL_UNSIGNED_INT_10_10_10_2, GL12.GL_UNSIGNED_INT_2_10_10_10_REV,
GL30.GL_UNSIGNED_INT_24_8, GL30.GL_UNSIGNED_INT_10F_11F_11F_REV,
GL30.GL_UNSIGNED_INT_5_9_9_9_REV -> 4;
case GL30.GL_FLOAT_32_UNSIGNED_INT_24_8_REV -> 8;
default -> 0;
};
}
public static String name(int glType) {
return switch (glType) {
case GL11.GL_BYTE -> "BYTE";
case GL11.GL_UNSIGNED_BYTE -> "UNSIGNED_BYTE";
case GL11.GL_SHORT -> "SHORT";
case GL11.GL_UNSIGNED_SHORT -> "UNSIGNED_SHORT";
case GL11.GL_INT -> "INT";
case GL11.GL_UNSIGNED_INT -> "UNSIGNED_INT";
case GL11.GL_FLOAT -> "FLOAT";
case GL11.GL_DOUBLE -> "DOUBLE";
case GL30.GL_HALF_FLOAT -> "HALF_FLOAT";
case GL12.GL_UNSIGNED_BYTE_3_3_2 -> "UNSIGNED_BYTE_3_3_2";
case GL12.GL_UNSIGNED_SHORT_4_4_4_4 -> "UNSIGNED_SHORT_4_4_4_4";
case GL12.GL_UNSIGNED_SHORT_5_5_5_1 -> "UNSIGNED_SHORT_5_5_5_1";
case GL12.GL_UNSIGNED_INT_8_8_8_8 -> "UNSIGNED_INT_8_8_8_8";
case GL12.GL_UNSIGNED_INT_8_8_8_8_REV -> "UNSIGNED_INT_8_8_8_8_REV";
case GL12.GL_UNSIGNED_INT_10_10_10_2 -> "UNSIGNED_INT_10_10_10_2";
case GL12.GL_UNSIGNED_INT_2_10_10_10_REV -> "UNSIGNED_INT_2_10_10_10_REV";
default -> String.format("0x%X", glType);
};
}
}
@@ -0,0 +1,240 @@
package com.gtnewhorizons.angelica.glsm;
import org.antlr.v4.runtime.BufferedTokenStream;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonToken;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.Token;
import org.antlr.v4.runtime.WritableToken;
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.grammar.GLSLPreParser;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Shared GLSL shader transformation utilities.
*/
public class GlslTransformUtils {
private static final String RENAMED_PREFIX = "angelica_renamed_";
/** Texture function renames */
public static final Map<String, String> TEXTURE_RENAMES = Map.ofEntries(
Map.entry("texture2D", "texture"),
Map.entry("texture3D", "texture"),
Map.entry("texture2DLod", "textureLod"),
Map.entry("texture3DLod", "textureLod"),
Map.entry("texture2DProj", "textureProj"),
Map.entry("texture3DProj", "textureProj"),
Map.entry("texture2DGrad", "textureGrad"),
Map.entry("texture2DGradARB", "textureGrad"),
Map.entry("texture3DGrad", "textureGrad"),
Map.entry("texelFetch2D", "texelFetch"),
Map.entry("texelFetch3D", "texelFetch"),
Map.entry("textureSize2D", "textureSize")
);
private static final Pattern TEXTURE_PATTERN = Pattern.compile("\\btexture\\s*\\(|(\\btexture\\b)");
/** 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(
// Some reserved words to always rename
0, List.of(
new ReservedWordRename(Pattern.compile("sample(?<=\\bsample)\\b"), RENAMED_PREFIX + "sample"),
new ReservedWordRename(Pattern.compile("new(?<=\\bnew)\\b"), RENAMED_PREFIX + "new")
),
400, List.of(
new ReservedWordRename(Pattern.compile("sampler(?<=\\bsampler)\\b(?!\\d)"), RENAMED_PREFIX + "sampler")
)
);
public static String replaceTexture(String input) {
final Matcher matcher = TEXTURE_PATTERN.matcher(input);
final StringBuilder builder = new StringBuilder();
while (matcher.find()) {
if (matcher.group(1) != null) {
matcher.appendReplacement(builder, RENAMED_PREFIX + "texture");
} else {
matcher.appendReplacement(builder, Matcher.quoteReplacement(matcher.group(0)));
}
}
matcher.appendTail(builder);
return builder.toString();
}
public static String renameReservedWords(String source, int targetVersion) {
for (var entry : VERSIONED_RESERVED_WORDS.entrySet()) {
// Rename identifiers that become reserved words at or above this GLSL version
if (targetVersion >= entry.getKey()) {
for (var rename : entry.getValue()) {
source = rename.pattern().matcher(source).replaceAll(rename.replacement());
}
}
}
return source;
}
public static String restoreReservedWords(String source) {
source = source.replace(RENAMED_PREFIX + "texture", "texture");
source = source.replace(RENAMED_PREFIX + "sampler", "sampler");
source = source.replace(RENAMED_PREFIX + "sample", "sample");
return source;
}
public static void restoreReservedWordsInTree(ParseTree tree) {
if (tree instanceof TerminalNode terminal) {
final Token token = terminal.getSymbol();
final String text = token.getText();
if (text != null && text.startsWith(RENAMED_PREFIX) && token instanceof WritableToken writable) {
final String rest = text.substring(RENAMED_PREFIX.length());
if (rest.startsWith("texture") || rest.startsWith("sample")) {
writable.setText(rest);
}
}
return;
}
for (int i = 0; i < tree.getChildCount(); i++) {
restoreReservedWordsInTree(tree.getChild(i));
}
}
public record QuietParse(GLSLParser.Translation_unitContext full, GLSLPreParser.Translation_unitContext pre) {}
public static GLSLParser.Translation_unitContext parseFullQuiet(String source) {
final GLSLLexer lexer = new GLSLLexer(CharStreams.fromString(source));
lexer.removeErrorListeners();
final GLSLParser parser = new GLSLParser(new CommonTokenStream(lexer));
parser.removeErrorListeners();
parser.setBuildParseTree(true);
return parser.translation_unit();
}
public static GLSLPreParser.Translation_unitContext parsePreQuiet(String source) {
final GLSLLexer lexer = new GLSLLexer(CharStreams.fromString(source));
lexer.removeErrorListeners();
final GLSLPreParser preParser = new GLSLPreParser(new CommonTokenStream(lexer, GLSLLexer.DIRECTIVES));
preParser.removeErrorListeners();
preParser.setBuildParseTree(true);
return preParser.translation_unit();
}
public static QuietParse parseBothQuiet(String source) {
final GLSLLexer lexer = new GLSLLexer(CharStreams.fromString(source));
lexer.removeErrorListeners();
final GLSLPreParser preParser = new GLSLPreParser(new BufferedTokenStream(lexer));
preParser.removeErrorListeners();
preParser.setBuildParseTree(true);
final GLSLPreParser.Translation_unitContext pre = preParser.translation_unit();
lexer.reset();
final GLSLParser parser = new GLSLParser(new CommonTokenStream(lexer));
parser.removeErrorListeners();
parser.setBuildParseTree(true);
return new QuietParse(parser.translation_unit(), pre);
}
public static Set<String> identifiersInDirectiveText(String source) {
final GLSLLexer lexer = new GLSLLexer(CharStreams.fromString(source));
lexer.removeErrorListeners();
final BufferedTokenStream tokens = new BufferedTokenStream(lexer);
tokens.fill();
final Set<String> names = new HashSet<>();
for (Token token : tokens.getTokens()) {
switch (token.getType()) {
case GLSLLexer.PROGRAM_TEXT, GLSLLexer.MACRO_TEXT, GLSLLexer.CONSTANT_EXPRESSION -> collectIdentifiers(token.getText(), names);
default -> { }
}
}
return names;
}
private static void collectIdentifiers(String text, Set<String> out) {
if (text == null) return;
final int len = text.length();
int i = 0;
while (i < len) {
final char c = text.charAt(i);
if (c != '_' && !Character.isLetter(c)) {
i++;
continue;
}
int end = i + 1;
while (end < len) {
final char n = text.charAt(end);
if (n != '_' && !Character.isLetterOrDigit(n)) break;
end++;
}
out.add(text.substring(i, end));
i = end;
}
}
public static String getFormattedShader(ParseTree tree, String header) {
final StringBuilder sb = new StringBuilder(header + "\n");
final String[] tabHolder = {""};
getFormattedShader(tree, sb, tabHolder, false);
return sb.toString();
}
public static String getFormattedShaderRebased(ParseTree tree, String header) {
final StringBuilder sb = new StringBuilder(header + "\n");
final String[] tabHolder = {""};
getFormattedShader(tree, sb, tabHolder, true);
return sb.toString();
}
private static void getFormattedShader(ParseTree tree, StringBuilder stringBuilder, String[] tabHolder, boolean rebaseOffsets) {
if (tree instanceof TerminalNode terminal) {
final String text = tree.getText();
if (text.equals("<EOF>")) {
return;
}
if (text.equals("#")) {
stringBuilder.append("\n#");
rebaseAt(terminal, stringBuilder.length() - 1, text, rebaseOffsets);
return;
}
stringBuilder.append(text);
if (!text.equals("}")) {
rebaseAt(terminal, stringBuilder.length() - text.length(), text, rebaseOffsets);
}
if (text.equals("{")) {
stringBuilder.append(" \n\t");
tabHolder[0] = "\t";
}
if (text.equals("}")) {
if (stringBuilder.length() >= 2) {
stringBuilder.deleteCharAt(stringBuilder.length() - 2);
}
rebaseAt(terminal, stringBuilder.length() - 1, text, rebaseOffsets);
tabHolder[0] = "";
stringBuilder.append(" \n");
} else {
stringBuilder.append(text.equals(";") ? " \n" + tabHolder[0] : " ");
}
} else {
for (int i = 0; i < tree.getChildCount(); ++i) {
getFormattedShader(tree.getChild(i), stringBuilder, tabHolder, rebaseOffsets);
}
}
}
private static void rebaseAt(TerminalNode terminal, int startIndex, String text, boolean rebaseOffsets) {
if (!rebaseOffsets) return;
if (terminal.getSymbol() instanceof CommonToken common) {
common.setText(text);
common.setStartIndex(startIndex);
common.setStopIndex(startIndex + text.length() - 1);
}
}
}
@@ -0,0 +1,34 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizons.angelica.glsm.states.PixelUnpackState;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL12;
import java.nio.ByteBuffer;
import static com.gtnewhorizons.angelica.glsm.backend.BackendManager.RENDER_BACKEND;
public final class LTWWorkaround {
private LTWWorkaround() {}
private static ByteBuffer oneTexel;
public static boolean isLtwVersionString(String glVersion) {
return glVersion != null && glVersion.contains("LTW");
}
public static void onTexImage2D(int target, int level, int format, boolean hasPixels) {
if (target != GL11.GL_TEXTURE_2D || level != 0 || format != GL12.GL_BGRA || hasPixels) return;
if (oneTexel == null) {
oneTexel = BufferUtils.createByteBuffer(4);
}
oneTexel.clear();
GLStateManager.forcePixelUnpackState(PixelUnpackState.DEFAULT);
GLStateManager.suspendPixelUnpackBuffer();
RENDER_BACKEND.texSubImage2D(target, level, 0, 0, 1, 1, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, oneTexel);
GLStateManager.restorePixelUnpackBuffer();
GLStateManager.restorePixelUnpackState(PixelUnpackState.DEFAULT);
}
}
@@ -0,0 +1,26 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizons.angelica.glsm.ffp.VertexKey;
import com.gtnewhorizons.angelica.glsm.states.LightModelState;
import com.gtnewhorizons.angelica.glsm.states.LightState;
import com.gtnewhorizons.angelica.glsm.states.MaterialState;
import org.lwjgl.opengl.GL11;
public final class LightingSnapshot {
final boolean[] lightEnabled = new boolean[VertexKey.FFP_LIGHT_COUNT];
final LightState[] lights = new LightState[VertexKey.FFP_LIGHT_COUNT];
final LightModelState lightModel = new LightModelState();
final MaterialState frontMaterial = new MaterialState(GL11.GL_FRONT);
final MaterialState backMaterial = new MaterialState(GL11.GL_BACK);
boolean colorMaterial;
int colorMaterialFace;
int colorMaterialParameter;
boolean normalize;
boolean rescaleNormal;
public LightingSnapshot() {
for (int i = 0; i < lights.length; i++) {
lights[i] = new LightState(GL11.GL_LIGHT0 + i);
}
}
}
@@ -0,0 +1,384 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizons.angelica.glsm.ffp.VAOManager;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.nio.ShortBuffer;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAddress0;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAlloc;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memFree;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memGetByte;
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.
*
* <p>Maintains a single growable element buffer with pre-computed indices mapping
* GL_QUADS vertex order to GL_TRIANGLES: {@code (0,1,3, 1,2,3)} per quad.
* Uses {@code GL_LAST_VERTEX_CONVENTION} winding (Mesa {@code do_quad} LAST path)
* so both emitted triangles share the quad's provoking vertex (v3).
*/
public final class QuadConverter {
public static final int INDEX_TYPE = GL11.GL_UNSIGNED_INT;
private static int eboId;
private static int maxQuads;
/** Scratch EBO for converted quad-element draws (drawQuadElementsAsTriangles). */
private static int scratchEboId;
private static int scratchEboCapacity; // in bytes
private QuadConverter() {}
private static void ensureCapacity(int quadCount) {
if (quadCount <= maxQuads) return;
// Round up to power of 2 for amortized growth
int newMaxQuads = Math.max(256, maxQuads);
while (newMaxQuads < quadCount) {
newMaxQuads *= 2;
}
if (eboId == 0) {
eboId = RENDER_BACKEND.genBuffers();
}
// Generate index data: 6 ints per quad
final ByteBuffer indexData = memAlloc(newMaxQuads * 6 * 4);
long ptr = memAddress0(indexData);
for (int i = 0; i < newMaxQuads; i++) {
final int base = i * 4;
// GL_LAST_VERTEX_CONVENTION: both triangles share v3 as provoking vertex
// Triangle 1: 0, 1, 3
memPutInt(ptr, base);
memPutInt(ptr + 4, base + 1);
memPutInt(ptr + 8, base + 3);
// Triangle 2: 1, 2, 3
memPutInt(ptr + 12, base + 1);
memPutInt(ptr + 16, base + 2);
memPutInt(ptr + 20, base + 3);
ptr += 24;
}
RENDER_BACKEND.bindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, eboId);
RENDER_BACKEND.bufferData(GL15.GL_ELEMENT_ARRAY_BUFFER, indexData, GL15.GL_STATIC_DRAW);
memFree(indexData);
maxQuads = newMaxQuads;
}
/**
* Convert a GL_QUADS glDrawArrays call to indexed GL_TRIANGLES.
* Binds the shared EBO to the current VAO and issues glDrawElements.
*
* @param first first vertex index (must be aligned to quad boundary, i.e. multiple of 4)
* @param vertexCount number of vertices (must be multiple of 4)
*/
public static void drawQuadsAsTriangles(GLContextState glCtx, int first, int vertexCount) {
drawSharedQuadEbo(glCtx, first, vertexCount, 1, false);
}
public static void drawQuadsAsTrianglesInstanced(GLContextState glCtx, int first, int vertexCount, int primcount) {
drawSharedQuadEbo(glCtx, first, vertexCount, primcount, true);
}
private static int misalignedDraws;
private static void drawSharedQuadEbo(GLContextState glCtx, int first, int vertexCount, int primcount, boolean instanced) {
final int quadCount = vertexCount / 4;
if ((first & 3) != 0) {
misalignedDraws++;
if ((misalignedDraws & (misalignedDraws - 1)) == 0) {
GLStateManager.LOGGER.warn("QuadConverter: quad draw with unaligned first={} count={} (#{}) -- using scratch indices", first, vertexCount, misalignedDraws);
}
final ByteBuffer dst = memAlloc(quadCount * 6 * 4);
long ptr = memAddress0(dst);
for (int i = 0; i < quadCount; i++) {
final int base = first + i * 4;
memPutInt(ptr, base);
memPutInt(ptr + 4, base + 1);
memPutInt(ptr + 8, base + 3);
memPutInt(ptr + 12, base + 1);
memPutInt(ptr + 16, base + 2);
memPutInt(ptr + 20, base + 3);
ptr += 24;
}
uploadAndDraw(glCtx, dst, quadCount * 6, GL11.GL_UNSIGNED_INT, 4, primcount, instanced);
return;
}
GLStateManager.preDraw(glCtx, GL11.GL_TRIANGLES);
final int prevEbo = glCtx.vaos.boundEBO;
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);
}
/**
* Upload converted triangle indices to the scratch EBO and draw.
* Saves/restores the caller's EBO binding so subsequent indexed draws
* (or glGetBufferSubData reads) still see the original EBO.
*
* @param dst off-heap buffer with triangle index data (freed by this method)
* @param triIndexCount number of triangle indices
* @param indexType GL_UNSIGNED_INT or GL_UNSIGNED_SHORT
* @param bytesPerIndex bytes per index element
*/
private static void uploadAndDraw(GLContextState glCtx, ByteBuffer dst, int triIndexCount, int indexType, int bytesPerIndex) {
uploadAndDraw(glCtx, dst, triIndexCount, indexType, bytesPerIndex, 1, false);
}
private static void uploadAndDraw(GLContextState glCtx, ByteBuffer dst, int triIndexCount, int indexType, int bytesPerIndex, int primcount, boolean instanced) {
GLStateManager.preDraw(glCtx, GL11.GL_TRIANGLES);
final int needed = triIndexCount * bytesPerIndex;
final int prevEbo = glCtx.vaos.boundEBO;
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);
memFree(dst);
}
/**
* Convert an indexed GL_QUADS draw to GL_TRIANGLES by reading the source EBO,
* building a triangle index buffer, and issuing the converted draw.
*
* @param indexCount number of quad indices (must be multiple of 4)
* @param type GL_UNSIGNED_INT, GL_UNSIGNED_SHORT, or GL_UNSIGNED_BYTE
* @param offset byte offset into the currently bound EBO
*/
public static void drawQuadElementsAsTriangles(GLContextState glCtx, int indexCount, int type, long offset) {
drawQuadElementsFromEbo(glCtx, indexCount, type, offset, 1, false);
}
public static void drawQuadElementsAsTrianglesInstanced(GLContextState glCtx, int indexCount, int type, long offset, int primcount) {
drawQuadElementsFromEbo(glCtx, indexCount, type, offset, primcount, true);
}
private static void drawQuadElementsFromEbo(GLContextState glCtx, 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 = GLTypes.sizeBytes(type);
final ByteBuffer src = memAlloc(indexCount * bytesPerIndex);
RENDER_BACKEND.getBufferSubData(GL15.GL_ELEMENT_ARRAY_BUFFER, offset, src);
final ByteBuffer dst = memAlloc(triIndexCount * 4);
triangulateQuads(memAddress0(src), type, memAddress0(dst), GL11.GL_UNSIGNED_INT, quadCount);
memFree(src);
uploadAndDraw(glCtx, dst, triIndexCount, GL11.GL_UNSIGNED_INT, 4, primcount, instanced);
}
/**
* Convert a client-side IntBuffer of quad indices to triangles.
*/
public static void drawQuadElementsAsTriangles(GLContextState glCtx, IntBuffer indices) {
final int indexCount = indices.remaining();
if (indexCount == 0) return;
assert indexCount % 4 == 0;
final int quadCount = indexCount / 4;
final int triIndexCount = quadCount * 6;
final ByteBuffer dst = memAlloc(triIndexCount * 4);
final long srcAddr = memAddress0(indices) + (long) indices.position() * 4;
triangulateQuads(srcAddr, GL11.GL_UNSIGNED_INT, memAddress0(dst), GL11.GL_UNSIGNED_INT, quadCount);
uploadAndDraw(glCtx, dst, triIndexCount, GL11.GL_UNSIGNED_INT, 4);
}
/**
* Convert a client-side ShortBuffer of quad indices to triangles.
*/
public static void drawQuadElementsAsTriangles(GLContextState glCtx, ShortBuffer indices) {
final int indexCount = indices.remaining();
if (indexCount == 0) return;
assert indexCount % 4 == 0;
final int quadCount = indexCount / 4;
final int triIndexCount = quadCount * 6;
final ByteBuffer dst = memAlloc(triIndexCount * 2);
final long srcAddr = memAddress0(indices) + (long) indices.position() * 2;
triangulateQuads(srcAddr, GL11.GL_UNSIGNED_SHORT, memAddress0(dst), GL11.GL_UNSIGNED_SHORT, quadCount);
uploadAndDraw(glCtx, dst, triIndexCount, GL11.GL_UNSIGNED_SHORT, 2);
}
/**
* Convert a client-side ByteBuffer of quad indices (type-agnostic) to triangles.
*/
public static void drawQuadElementsAsTriangles(GLContextState glCtx, int count, int type, ByteBuffer indices) {
if (count == 0) return;
assert count % 4 == 0;
final int quadCount = count / 4;
final int triIndexCount = quadCount * 6;
final long srcAddr = memAddress0(indices) + indices.position();
final ByteBuffer dst = memAlloc(triIndexCount * 4);
triangulateQuads(srcAddr, type, memAddress0(dst), GL11.GL_UNSIGNED_INT, quadCount);
uploadAndDraw(glCtx, dst, triIndexCount, GL11.GL_UNSIGNED_INT, 4);
}
/**
* 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);
}
}
@@ -0,0 +1,691 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizons.angelica.config.SystemProperties;
import com.gtnewhorizons.angelica.glsm.backend.BackendManager;
import com.gtnewhorizons.angelica.glsm.dsa.DSAARB;
import com.gtnewhorizons.angelica.glsm.dsa.DSAAccess;
import com.gtnewhorizons.angelica.glsm.dsa.DSACore;
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 org.taumc.celeritas.lwjgl.LWJGLServiceProvider;
import lombok.Getter;
import org.jetbrains.annotations.Nullable;
import org.joml.Matrix4f;
import org.joml.Vector3i;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.ARBShaderImageLoadStore;
import org.lwjgl.opengl.ARBShaderStorageBufferObject;
import org.lwjgl.opengl.EXTShaderImageLoadStore;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL20;
import org.lwjgl.opengl.GL30;
import org.lwjgl.opengl.GL32;
import org.lwjgl.opengl.GL42;
import org.lwjgl.opengl.GL43;
import org.lwjgl.opengl.NVXGpuMemoryInfo;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static com.gtnewhorizons.angelica.glsm.backend.BackendManager.RENDER_BACKEND;
/**
* This class is responsible for abstracting calls to OpenGL and asserting that calls are run on the render thread.
*/
public class RenderSystem {
private static DSAAccess dsaState;
private static boolean supportsCompute;
private static boolean supportsImageLoadStore;
private static boolean supportsSSBO;
private static boolean supportsMultiDrawIndirect;
private static boolean supportsBufferStorage;
private static boolean supportsClearTexture;
private static boolean supportsTesselation;
private static boolean supportsSamplerObjects;
private static boolean supportsProgramBinary;
private static int maxImageUnits;
@Getter private static int maxCombinedTextureImageUnits;
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;
private static volatile boolean isGLES;
private static volatile int glesVersion;
private static volatile boolean glesDetected;
private static volatile boolean hasClipCullDistance;
private static volatile boolean isLTW;
private static volatile boolean ltwDetected;
private static final Pattern SEMVER_PATTERN = Pattern.compile("(?<major>\\d+)\\.(?<minor>\\d+)\\.*(?<bugfix>\\d*)(.*)");
private RenderSystem() {}
private static boolean rendererInitialized;
public static void initRenderer() {
if (rendererInitialized) return;
rendererInitialized = true;
final boolean wasDetected = glesDetected;
if (isGLES() && !wasDetected) {
GLStateManager.LOGGER.info("OpenGL ES {}.{} detected", glesVersion / 100, (glesVersion / 10) % 10);
}
if (!isGLES) {
try {
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.");
}
} catch (NoSuchFieldError ignored) {
}
try {
if (dsaState == null && GLStateManager.capabilities.GL_ARB_direct_state_access) {
dsaState = new DSAARB();
GLStateManager.LOGGER.info("ARB_direct_state_access detected, enabling DSA.");
}
} catch (NoSuchFieldError ignored) {
}
}
if (dsaState == null) {
dsaState = new DSAUnsupported();
GLStateManager.LOGGER.info("{}", isGLES ? "GLES context, using DSAUnsupported (bind-based) fallback." : "No DSA support detected, falling back to legacy OpenGL.");
}
BackendManager.init();
try {
// Force init on client thread
GLStateManager.LOGGER.info("Celeritas LWJGL service: {}", LWJGLServiceProvider.LWJGL.getClass().getName());
} catch (Throwable t) {
GLStateManager.LOGGER.debug("Could not resolve the celeritas LWJGL service: {}", t.toString());
}
supportsCompute = supportsCompute();
supportsTesselation = GLStateManager.capabilities.GL_ARB_tessellation_shader || GLStateManager.capabilities.OpenGL40;
supportsImageLoadStore = GLStateManager.capabilities.OpenGL42
|| GLStateManager.capabilities.GL_ARB_shader_image_load_store
|| GLStateManager.capabilities.GL_EXT_shader_image_load_store;
supportsSSBO = GLStateManager.capabilities.OpenGL43 || GLStateManager.capabilities.GL_ARB_shader_storage_buffer_object;
supportsMultiDrawIndirect = GLStateManager.capabilities.OpenGL43 || GLStateManager.capabilities.GL_ARB_multi_draw_indirect;
supportsBufferStorage = !isGLES && (GLStateManager.capabilities.OpenGL44 || GLStateManager.capabilities.GL_ARB_buffer_storage);
supportsClearTexture = GLStateManager.capabilities.OpenGL44 || GLStateManager.capabilities.GL_ARB_clear_texture;
hasClipCullDistance = !isGLES || hasExtension("GL_EXT_clip_cull_distance");
// Cache maximum image units
if (supportsImageLoadStore) {
if (GLStateManager.capabilities.OpenGL42) {
maxImageUnits = RENDER_BACKEND.getInteger(GL42.GL_MAX_IMAGE_UNITS);
} else if (GLStateManager.capabilities.GL_ARB_shader_image_load_store) {
maxImageUnits = RENDER_BACKEND.getInteger(ARBShaderImageLoadStore.GL_MAX_IMAGE_UNITS);
} else {
maxImageUnits = RENDER_BACKEND.getInteger(EXTShaderImageLoadStore.GL_MAX_IMAGE_UNITS_EXT);
}
} else {
maxImageUnits = 0;
}
// Cache maximum SSBO bindings
if (supportsSSBO) {
if (GLStateManager.capabilities.OpenGL43) {
maxSSBOBindings = RENDER_BACKEND.getInteger(GL43.GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS);
} else {
maxSSBOBindings = RENDER_BACKEND.getInteger(ARBShaderStorageBufferObject.GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS);
}
} else {
maxSSBOBindings = 0;
}
// Check for sampler objects support (GL 3.3+ or ARB extension)
supportsSamplerObjects = GLStateManager.capabilities.OpenGL33 || GLStateManager.capabilities.GL_ARB_sampler_objects;
if (supportsSamplerObjects) {
maxCombinedTextureImageUnits = RENDER_BACKEND.getInteger(GL20.GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS);
}
supportsProgramBinary = RENDER_BACKEND.supportsProgramBinary();
GLStateManager.LOGGER.info("Program Binary: {}", supportsProgramBinary);
maxGlslVersion = Integer.parseInt(parseGlVersionString(RENDER_BACKEND.getString(GL20.GL_SHADING_LANGUAGE_VERSION)));
supportsGpuShader4 = GLStateManager.capabilities.GL_EXT_gpu_shader4;
GLStateManager.LOGGER.info("Max GLSL version: {}, GPU Shader4: {}", maxGlslVersion, supportsGpuShader4);
GLStateManager.LOGGER.info("Image Load/Store: {}, Max Image Units: {}", supportsImageLoadStore, maxImageUnits);
GLStateManager.LOGGER.info("SSBO: {}, Max SSBO Bindings: {}", supportsSSBO, maxSSBOBindings);
GLStateManager.LOGGER.info("Buffer Storage: {}, Clear Texture: {}, Sampler Objects: {}", supportsBufferStorage, supportsClearTexture, supportsSamplerObjects);
if (isGLES) {
GLStateManager.LOGGER.info("GL ES context detected, enabling shader transformer.");
ShaderManager.enable();
} else 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.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) {
dsaState.generateMipmaps(texture, mipmapTarget);
}
public static void bindAttributeLocation(int program, int index, CharSequence name) {
RENDER_BACKEND.bindAttribLocation(program, index, name);
}
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) {
final GLStateManager.GLESTexImageRemap remap = GLStateManager.remapTexImageForGLES(internalformat, format, type);
internalformat = remap.internalFormat();
format = remap.format();
type = remap.type();
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);
}
}
public static void uniformMatrix4fv(int location, boolean transpose, FloatBuffer matrix) {
RENDER_BACKEND.uniformMatrix4(location, transpose, matrix);
}
public static void copyTexImage2D(int target, int level, int internalFormat, int x, int y, int width, int height, int border) {
GLStateManager.glCopyTexImage2D(target, level, internalFormat, x, y, width, height, border);
}
public static void uniform1f(int location, float v0) {
RENDER_BACKEND.uniform1f(location, v0);
}
public static void uniform1i(int location, int v0) {
RENDER_BACKEND.uniform1i(location, v0);
}
public static void uniform2f(int location, float v0, float v1) {
RENDER_BACKEND.uniform2f(location, v0, v1);
}
public static void uniform2i(int location, int v0, int v1) {
RENDER_BACKEND.uniform2i(location, v0, v1);
}
public static void uniform3f(int location, float v0, float v1, float v2) {
RENDER_BACKEND.uniform3f(location, v0, v1, v2);
}
public static void uniform3i(int location, int v0, int v1, int v2) {RENDER_BACKEND.uniform3i(location, v0, v1, v2);}
public static void uniform4f(int location, float v0, float v1, float v2, float v3) {
RENDER_BACKEND.uniform4f(location, v0, v1, v2, v3);
}
public static void uniform4i(int location, int v0, int v1, int v2, int v3) {
RENDER_BACKEND.uniform4i(location, v0, v1, v2, v3);
}
public static int getAttribLocation(int programId, String name) {
return RENDER_BACKEND.getAttribLocation(programId, name);
}
public static int getUniformLocation(int programId, String name) {
return RENDER_BACKEND.getUniformLocation(programId, name);
}
public static void texParameteriv(int texture, int target, int pname, IntBuffer params) {
dsaState.texParameteriv(texture, target, pname, params);
}
public static void copyTexSubImage2D(int destTexture, int target, int i, int i1, int i2, int i3, int i4, int width, int height) {
dsaState.copyTexSubImage2D(destTexture, target, i, i1, i2, i3, i4, width, height);
}
public static void texParameteri(int texture, int target, int pname, int param) {
dsaState.texParameteri(texture, target, pname, param);
}
public static void texParameterf(int texture, int target, int pname, float param) {
dsaState.texParameterf(texture, target, pname, param);
}
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) {
return RENDER_BACKEND.getProgramInfoLog(program, RENDER_BACKEND.getProgrami(program, GL20.GL_INFO_LOG_LENGTH));
}
public static String getShaderInfoLog(int shader) {
return RENDER_BACKEND.getShaderInfoLog(shader, RENDER_BACKEND.getShaderi(shader, GL20.GL_INFO_LOG_LENGTH));
}
public static void drawBuffers(int framebuffer, IntBuffer buffers) {
dsaState.drawBuffers(framebuffer, buffers);
}
public static void readBuffer(int framebuffer, int buffer) {
dsaState.readBuffer(framebuffer, buffer);
}
public static String getActiveUniform(int program, int index, int maxLength, IntBuffer sizeType) {
return RENDER_BACKEND.getActiveUniform(program, index, maxLength, sizeType);
}
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) {
GLStateManager.glBufferData(target, data, usage);
}
public static int bufferStorage(int target, FloatBuffer data, int usage) {
return dsaState.bufferStorage(target, data, usage);
}
public static void vertexAttrib4f(int index, float v0, float v1, float v2, float v3) {
RENDER_BACKEND.vertexAttrib4f(index, v0, v1, v2, v3);
}
public static void detachShader(int program, int shader) {
RENDER_BACKEND.detachShader(program, shader);
}
public static void framebufferTexture2D(int fb, int fbtarget, int attachment, int target, int texture, int levels) {
dsaState.framebufferTexture2D(fb, fbtarget, attachment, target, texture, levels);
}
public static int getTexParameteri(int texture, int target, int pname) {
return dsaState.getTexParameteri(texture, target, pname);
}
public static float getTexParameterf(int texture, int target, int pname) {
return dsaState.getTexParameterf(texture, target, pname);
}
public static int getTexLevelParameteri(int texture, int level, int pname) {
return dsaState.getTexLevelParameteri(texture, level, pname);
}
public static void bindImageTexture(int unit, int texture, int level, boolean layered, int layer, int access, int format) {
GLStateManager.glBindImageTexture(unit, texture, level, layered, layer, access, format);
}
public static int getMaxImageUnits() {
return maxImageUnits;
}
public static boolean supportsImageLoadStore() {
if (RENDER_BACKEND.isSDLGPU()) return true;
return supportsImageLoadStore;
}
public static boolean hasClipCullDistance() {
return hasClipCullDistance;
}
private static boolean hasExtension(String name) {
final int count = GLStateManager.glGetInteger(GL30.GL_NUM_EXTENSIONS);
for (int i = 0; i < count; i++) {
if (name.equals(GLStateManager.glGetStringi(GL11.GL_EXTENSIONS, i))) return true;
}
return false;
}
public static boolean isGLES() {
if (!glesDetected) detectGLES();
return isGLES;
}
private static synchronized void detectGLES() {
if (glesDetected) return;
try {
final String v = RENDER_BACKEND.getString(GL11.GL_VERSION);
if (v != null && v.startsWith("OpenGL ES ")) {
isGLES = true;
glesVersion = Integer.parseInt(parseGlVersionString(v));
}
glesDetected = true;
} catch (Throwable ignored) {
// No GL context on this thread yet (splash); retry on next call.
}
}
public static boolean isLTW() {
if (!ltwDetected) detectLTW();
return isLTW;
}
private static synchronized void detectLTW() {
if (ltwDetected) return;
if (SystemProperties.DISABLE_LTW_WORKAROUND) {
ltwDetected = true;
return;
}
try {
final String v = RENDER_BACKEND.getString(GL11.GL_VERSION);
if (v == null) return;
isLTW = LTWWorkaround.isLtwVersionString(v);
ltwDetected = true;
} catch (Throwable ignored) {
}
}
public static boolean supportsSSBO() {
return supportsSSBO;
}
public static boolean supportsMultiDrawIndirect() {
return supportsMultiDrawIndirect;
}
public static boolean supportsBufferStorage() {
return supportsBufferStorage;
}
public static boolean supportsClearTexture() {
return supportsClearTexture;
}
public static int getMaxSSBOBindings() {
return maxSSBOBindings;
}
public static void getProgramiv(int program, int value, IntBuffer storage) {
RENDER_BACKEND.getProgramiv(program, value, storage);
}
public static void dispatchCompute(int workX, int workY, int workZ) {
RENDER_BACKEND.dispatchCompute(workX, workY, workZ);
}
public static void dispatchCompute(Vector3i workGroups) {
RENDER_BACKEND.dispatchCompute(workGroups.x, workGroups.y, workGroups.z);
}
public static void dispatchComputeIndirect(long offset) {
RENDER_BACKEND.dispatchComputeIndirect(offset);
}
public static void bindBuffer(int target, int buffer) {
GLStateManager.glBindBuffer(target, buffer);
}
public static void memoryBarrier(int barriers) {
if (supportsCompute) {
RENDER_BACKEND.memoryBarrier(barriers);
}
}
public static boolean supportsBufferBlending() {
return GLStateManager.capabilities.GL_ARB_draw_buffers_blend || GLStateManager.capabilities.OpenGL40;
}
public static void disableBufferBlend(int buffer) {
GLStateManager.glDisablei(GL11.GL_BLEND, buffer);
}
public static void enableBufferBlend(int buffer) {
GLStateManager.glEnablei(GL11.GL_BLEND, buffer);
}
public static void blendFuncSeparatei(int buffer, int srcRGB, int dstRGB, int srcAlpha, int dstAlpha) {
GLStateManager.glBlendFuncSeparatei(buffer, srcRGB, dstRGB, srcAlpha, dstAlpha);
}
public static void bindTextureToUnit(int unit, int texture) {
if (unit != 0 && GLStateManager.getBoundTextureForServerState(unit) != texture) GLStateManager.beforeUncapturedStateChange();
dsaState.bindTextureToUnit(unit, texture);
}
public static void bindTextureToUnit(int target, int unit, int texture) {
if (unit != 0 && GLStateManager.getBoundTextureForServerState(unit) != texture) GLStateManager.beforeUncapturedStateChange();
dsaState.bindTextureToUnit(target, unit, texture);
}
public static void setupProjectionMatrix(Matrix4f matrix) {
final FloatBuffer projectionMatrixBuffer = GLStateManager.ctx().projectionMatrixBuffer;
GLStateManager.glMatrixMode(GL11.GL_PROJECTION);
GLStateManager.glPushMatrix();
matrix.get(0, projectionMatrixBuffer);
GLStateManager.glLoadMatrix(projectionMatrixBuffer);
GLStateManager.glMatrixMode(GL11.GL_MODELVIEW);
}
public static void restoreProjectionMatrix() {
GLStateManager.glMatrixMode(GL11.GL_PROJECTION);
GLStateManager.glPopMatrix();
GLStateManager.glMatrixMode(GL11.GL_MODELVIEW);
}
public static void blitFramebuffer(int source, int dest, int offsetX, int offsetY, int width, int height, int offsetX2, int offsetY2, int width2, int height2, int bufferChoice, int filter) {
final boolean scissor = GLStateManager.glIsEnabled(GL11.GL_SCISSOR_TEST);
if (scissor) GLStateManager.glDisable(GL11.GL_SCISSOR_TEST);
try {
dsaState.blitFramebuffer(source, dest, offsetX, offsetY, width, height, offsetX2, offsetY2, width2, height2, bufferChoice, filter);
} finally {
if (scissor) GLStateManager.glEnable(GL11.GL_SCISSOR_TEST);
}
}
public static int createFramebuffer() {
return dsaState.createFramebuffer();
}
public static int createTexture(int target) {
return dsaState.createTexture(target);
}
// TODO: Proper notification of compute support
public static boolean supportsCompute() {
try {
return GLStateManager.capabilities.GL_ARB_compute_shader || GLStateManager.capabilities.OpenGL43;
} catch (Exception ignored) {
return false;
}
}
public static int createBuffers() {
return GLStateManager.glGenBuffers();
}
public static int getIndexedBufferBinding(int target, int index) {
return RENDER_BACKEND.getIndexedBufferBinding(target, index);
}
public static void bindBufferBase(int target, int index, int buffer) {
GLStateManager.glBindBufferBase(target, index, buffer);
}
public static void bufferStorage(int target, long size, int 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);
GLStateManager.glClearBufferSubData(target, internalFormat, offset, size, format, type, buf);
}
public static void deleteBuffers(int buffer) {
GLStateManager.glDeleteBuffers(buffer);
}
public static long getVRAM() {
if (GLStateManager.capabilities.GL_NVX_gpu_memory_info) {
return RENDER_BACKEND.getInteger(NVXGpuMemoryInfo.GL_GPU_MEMORY_INFO_CURRENT_AVAILABLE_VIDMEM_NVX) * 1024L;
} else {
// Default to 4GB if we can't query VRAM
return 4294967296L;
}
}
public static void clearTexImage(int texture, int target, int level, int format, int type) {
RENDER_BACKEND.clearTexImage(texture, level, format, type);
}
public static int getMaxGlslVersion() {
return maxGlslVersion;
}
public static boolean supportsGpuShader4() {
return supportsGpuShader4;
}
public static boolean supportsSnormFormats() {
if (isGLES()) return false;
return GLStateManager.capabilities.OpenGL31;
}
public static boolean supportsPackedFloatRenderable() {
return GLStateManager.capabilities.OpenGL30 && maxGlslVersion >= 130;
}
public static boolean supportsTesselation() {
return supportsTesselation;
}
public static boolean supportsSamplerObjects() {
return supportsSamplerObjects;
}
public static boolean supportsProgramBinary() {
return supportsProgramBinary;
}
public static int genSampler() {
if (!supportsSamplerObjects) return 0;
return GLStateManager.glGenSamplers();
}
public static void destroySampler(int sampler) {
if (!supportsSamplerObjects || sampler == 0) return;
GLStateManager.glDeleteSamplers(sampler);
}
public static void samplerParameteri(int sampler, int pname, int param) {
if (!supportsSamplerObjects || sampler == 0) return;
GLStateManager.glSamplerParameteri(sampler, pname, param);
}
public static void samplerParameterf(int sampler, int pname, float param) {
if (!supportsSamplerObjects || sampler == 0) return;
GLStateManager.glSamplerParameterf(sampler, pname, param);
}
public static void bindSamplerToUnit(int unit, int sampler) {
if (!supportsSamplerObjects) return;
GLStateManager.glBindSampler(unit, sampler);
}
public static void unbindAllSamplers() {
if (!supportsSamplerObjects) return;
final int count = GLStateManager.getSamplerUnitCount();
for (int i = 0; i < count; i++) {
if (GLStateManager.getSamplerBinding(i) != 0) {
GLStateManager.glBindSampler(i, 0);
}
}
}
/** Parse a GL version string (e.g. "4.6.0 NVIDIA ...") into concatenated digits "460". Strips
* "OpenGL ES " / "OpenGL ES GLSL ES " prefixes so ES contexts parse too. */
public static String parseGlVersionString(String info) {
Objects.requireNonNull(info);
String stripped = info;
if (stripped.startsWith("OpenGL ES GLSL ES ")) {
stripped = stripped.substring("OpenGL ES GLSL ES ".length());
} else if (stripped.startsWith("OpenGL ES ")) {
stripped = stripped.substring("OpenGL ES ".length());
}
final Matcher matcher = SEMVER_PATTERN.matcher(stripped);
if (!matcher.matches()) {
throw new IllegalStateException("Could not parse GL version from \"" + info + "\"");
}
final String major = matcher.group("major");
final String minor = matcher.group("minor");
String bugfix = matcher.group("bugfix");
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;
}
}
@@ -0,0 +1,248 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizons.angelica.glsm.recording.commands.DisplayListCommand;
import com.gtnewhorizons.angelica.glsm.stacks.CowStateStack;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import org.lwjgl.opengl.GL11;
public final class StateSet {
public static final int R_DEPTH = 1;
public static final int R_BLEND = 1 << 1;
public static final int R_COLOR_MASK = 1 << 2;
public static final int R_CLEAR_COLOR = 1 << 3;
public static final int R_DRAW_BUFFER = 1 << 4;
public static final int R_LOGIC_OP = 1 << 5;
public static final int R_SECONDARY_COLOR = 1 << 6;
public static final int R_STENCIL = 1 << 7;
public static final int R_VIEWPORT = 1 << 8;
public static final int R_LINE = 1 << 9;
public static final int R_POINT = 1 << 10;
public static final int R_POLYGON = 1 << 11;
public static final int R_TEXTURE = 1 << 12;
public static final int R_ACTIVE_UNIT = 1 << 13;
public static final int R_PROGRAM = 1 << 14;
public static final int R_SCISSOR = 1 << 15;
public static final int B_FRAG_COLORBUF = 1;
public static final int B_FRAG_TEXTURE = 1 << 1;
public static final int B_FRAG_FOG = 1 << 2;
public static final int B_CURRENT = 1 << 3;
public static final int B_LIGHTING = 1 << 4;
public static final int B_TRANSFORM = 1 << 5;
private static final long[] NO_MEMBERS = new long[0];
private static final Int2ObjectMap<StateSet> maskToStateSetMap = new Int2ObjectOpenHashMap<>();
public final int glMask;
public final long[] members;
public final int restore;
public final int bump;
public final boolean blendSnapshot;
public final DisplayListCommand pushCommand;
StateSet(int glMask, long[] members, int restore, int bump, boolean blendSnapshot) {
this.glMask = glMask;
this.members = members;
this.restore = restore;
this.bump = bump;
this.blendSnapshot = blendSnapshot;
this.pushCommand = () -> GLStateManager.pushStateLive(this);
}
public boolean contains(int id) {
if (id < 0) return false;
final int word = id >> 6;
return word < members.length && (members[word] & (1L << id)) != 0;
}
private static long[] ids(CowStateStack<?>... stacks) {
if (stacks.length == 0) return NO_MEMBERS;
int max = 0;
for (CowStateStack<?> s : stacks) max = Math.max(max, s.stackId());
final long[] bits = new long[(max >> 6) + 1];
for (CowStateStack<?> s : stacks) {
final int id = s.stackId();
bits[id >> 6] |= 1L << id;
}
return bits;
}
public static final StateSet BATCH = buildBatch();
public static final StateSet FONT = buildFont();
public static final StateSet FONT_PIPELINE = buildFontPipeline();
public static final StateSet HAND = buildHand();
public static final StateSet BLEND = buildBlend();
public static final StateSet ALPHA = buildAlpha();
public static final StateSet CUTOUT = buildCutout();
public static final StateSet MASKS = buildMasks();
public static final StateSet CULL = buildCull();
private static StateSet buildBatch() {
return new StateSet(0,
ids(
GLStateManager.getBlendState(), GLStateManager.getAlphaState(), GLStateManager.getDepthState(),
GLStateManager.getColorMask(), GLStateManager.getPolygonState(),
GLStateManager.getTextures().getTextureUnitBindings(0), GLStateManager.getTextures().getTextureUnitBindings(1),
GLStateManager.getActiveTextureUnitStack(),
GLStateManager.getBlendMode(), GLStateManager.getAlphaTest(), GLStateManager.getDepthTest(),
GLStateManager.getLightingState(), GLStateManager.getCullState(), GLStateManager.getPolygonOffsetFillState(),
GLStateManager.getTextures().getTextureUnitStates(0), GLStateManager.getTextures().getTextureUnitStates(1),
GLStateManager.getLightStates()[0], GLStateManager.getLightStates()[1],
GLStateManager.getLightDataStates()[0], GLStateManager.getLightDataStates()[1],
GLStateManager.getColorMaterial(), GLStateManager.getColorMaterialFace(), GLStateManager.getColorMaterialParameter(),
GLStateManager.getLightModel(), GLStateManager.getFrontMaterial(), GLStateManager.getBackMaterial(),
GLStateManager.getNormalizeState(), GLStateManager.getRescaleNormalState()
),
R_BLEND | R_DEPTH | R_COLOR_MASK | R_POLYGON | R_TEXTURE | R_ACTIVE_UNIT,
B_FRAG_COLORBUF | B_LIGHTING,
true);
}
private static StateSet buildFont() {
return new StateSet(0,
ids(
GLStateManager.getBlendState(), GLStateManager.getDepthState(), GLStateManager.getShadeModelState(),
GLStateManager.getTextures().getTextureUnitBindings(0), GLStateManager.getTextures().getTextureUnitBindings(1),
GLStateManager.getActiveTextureUnitStack(), GLStateManager.getProgramStack(),
GLStateManager.getBlendMode(), GLStateManager.getAlphaTest(), GLStateManager.getDepthTest(),
GLStateManager.getTextures().getTextureUnitStates(0),
GLStateManager.getColorMask(), GLStateManager.getCullState(),
GLStateManager.getPolygonState(), GLStateManager.getPolygonOffsetFillState()
),
R_BLEND | R_DEPTH | R_COLOR_MASK | R_TEXTURE | R_ACTIVE_UNIT | R_PROGRAM | R_POLYGON,
0,
true);
}
private static StateSet buildFontPipeline() {
return new StateSet(0,
ids(
GLStateManager.getBlendState(), GLStateManager.getDepthState(),
GLStateManager.getTextures().getTextureUnitBindings(0), GLStateManager.getTextures().getTextureUnitBindings(1),
GLStateManager.getActiveTextureUnitStack(),
GLStateManager.getBlendMode(), GLStateManager.getAlphaTest(), GLStateManager.getDepthTest(),
GLStateManager.getTextures().getTextureUnitStates(0),
GLStateManager.getColorMask(), GLStateManager.getCullState(),
GLStateManager.getPolygonState(), GLStateManager.getPolygonOffsetFillState()
),
R_BLEND | R_DEPTH | R_COLOR_MASK | R_TEXTURE | R_ACTIVE_UNIT | R_POLYGON,
0,
true);
}
private static StateSet buildHand() {
return new StateSet(0,
ids(
GLStateManager.getBlendState(), GLStateManager.getAlphaState(), GLStateManager.getDepthState(),
GLStateManager.getBlendMode(), GLStateManager.getAlphaTest(), GLStateManager.getTextures().getTextureUnitStates(1)
),
R_BLEND | R_DEPTH,
B_FRAG_COLORBUF,
true);
}
private static StateSet buildBlend() {
return new StateSet(0,
ids(GLStateManager.getBlendState(), GLStateManager.getBlendMode()),
R_BLEND,
0,
true);
}
private static StateSet buildAlpha() {
return new StateSet(0,
ids(GLStateManager.getAlphaState(), GLStateManager.getAlphaTest()),
0,
B_FRAG_COLORBUF,
false);
}
private static StateSet buildCutout() {
return new StateSet(0,
ids(GLStateManager.getAlphaState(), GLStateManager.getAlphaTest(), GLStateManager.getTextures().getTextureUnitStates(1)),
0,
B_FRAG_COLORBUF,
false);
}
private static StateSet buildMasks() {
return new StateSet(0,
ids(GLStateManager.getDepthState(), GLStateManager.getColorMask()),
R_DEPTH | R_COLOR_MASK,
0,
false);
}
private static StateSet buildCull() {
return new StateSet(0,
ids(GLStateManager.getPolygonState(), GLStateManager.getCullState()),
R_POLYGON,
0,
false);
}
public static StateSet forMask(int mask) {
StateSet cached = maskToStateSetMap.get(mask);
if (cached != null) {
return cached;
}
cached = buildForMask(mask);
maskToStateSetMap.put(mask, cached);
return cached;
}
private static StateSet buildForMask(int mask) {
int restore = 0;
int bump = 0;
final boolean blendSnapshot = (mask & (GL11.GL_COLOR_BUFFER_BIT | GL11.GL_ENABLE_BIT)) != 0;
if ((mask & GL11.GL_DEPTH_BUFFER_BIT) != 0) {
restore |= R_DEPTH;
}
if ((mask & GL11.GL_COLOR_BUFFER_BIT) != 0) {
restore |= R_BLEND | R_COLOR_MASK | R_CLEAR_COLOR | R_DRAW_BUFFER | R_LOGIC_OP;
bump |= B_FRAG_COLORBUF;
}
if ((mask & GL11.GL_CURRENT_BIT) != 0) {
restore |= R_SECONDARY_COLOR;
bump |= B_CURRENT;
}
if ((mask & GL11.GL_STENCIL_BUFFER_BIT) != 0) {
restore |= R_STENCIL;
}
if ((mask & GL11.GL_VIEWPORT_BIT) != 0) {
restore |= R_VIEWPORT;
}
if ((mask & GL11.GL_SCISSOR_BIT) != 0) {
restore |= R_SCISSOR;
}
if ((mask & GL11.GL_LINE_BIT) != 0) {
restore |= R_LINE;
}
if ((mask & GL11.GL_POINT_BIT) != 0) {
restore |= R_POINT;
}
if ((mask & GL11.GL_POLYGON_BIT) != 0) {
restore |= R_POLYGON;
}
if ((mask & GL11.GL_TEXTURE_BIT) != 0) {
restore |= R_TEXTURE | R_ACTIVE_UNIT;
bump |= B_FRAG_TEXTURE;
}
if ((mask & GL11.GL_LIGHTING_BIT) != 0) {
bump |= B_LIGHTING;
}
if ((mask & GL11.GL_FOG_BIT) != 0) {
bump |= B_FRAG_FOG;
}
if ((mask & GL11.GL_TRANSFORM_BIT) != 0) {
bump |= B_TRANSFORM;
}
final long[] members = Feature.maskToMembers(mask);
return new StateSet(mask, members, restore, bump, blendSnapshot);
}
}
@@ -0,0 +1,27 @@
package com.gtnewhorizons.angelica.glsm;
/// Matches
public enum Vendor {
AMD("amd"),
INTEL("intel"),
MESA("mesa"),
NVIDIA("nvidia"),
UNKNOWN("");
final String[] names;
Vendor(String... names) {
this.names = names;
}
public static Vendor getVendor(String vendorString) {
for (var v : values()) {
for (var name : v.names) {
if (vendorString.contains(name)) return v;
}
}
// Shouldn't be needed as "" matches everything, but sanity checks good.
return UNKNOWN;
}
}
@@ -0,0 +1,53 @@
package com.gtnewhorizons.angelica.glsm.backend;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.Iterator;
import java.util.ServiceLoader;
public final class BackendManager {
private static final Logger LOGGER = LogManager.getLogger("BackendManager");
public static final RenderBackend RENDER_BACKEND = loadService();
private BackendManager() {}
public static void init() {
RENDER_BACKEND.init();
LOGGER.info("Initialized backend: {} (priority {})", RENDER_BACKEND.getName(), RENDER_BACKEND.getPriority());
}
public static void shutdown() {
RENDER_BACKEND.shutdown();
}
private static RenderBackend loadService() {
final ServiceLoader<RenderBackend> loader = ServiceLoader.load(RenderBackend.class, RenderBackend.class.getClassLoader());
RenderBackend best = null;
final Iterator<RenderBackend> iterator = loader.iterator();
int attempts = 0;
while (attempts < 16) {
try {
attempts++;
if (!iterator.hasNext()) break;
final RenderBackend backend = iterator.next();
LOGGER.info("Found backend: {} (priority {})", backend.getName(), backend.getPriority());
if (backend.isAvailable() && (best == null || backend.getPriority() > best.getPriority())) {
best = backend;
}
} catch (Throwable t) {
LOGGER.debug("Skipping unavailable backend: {}", t.getMessage());
}
}
if (best == null) {
throw new IllegalStateException("No available render backend!");
}
LOGGER.info("Selected backend: {} (priority {})", best.getName(), best.getPriority());
return best;
}
}
@@ -0,0 +1,6 @@
package com.gtnewhorizons.angelica.glsm.backend;
@FunctionalInterface
public interface DebugMessageHandler {
void handleMessage(int source, int type, int id, int severity, String message);
}
@@ -0,0 +1,5 @@
package com.gtnewhorizons.angelica.glsm.backend;
public interface GLDebugMessageListener {
void onMessage(int source, int type, int id, int severity, int length, long message, long userParam);
}
@@ -0,0 +1,160 @@
package com.gtnewhorizons.angelica.glsm.backend;
import com.gtnewhorizons.angelica.glsm.profiling.Tracy;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.lwjgl.input.Keyboard;
import org.lwjgl.input.Mouse;
import java.util.concurrent.Executor;
import java.util.function.BooleanSupplier;
public final class MainThreadPump {
private static final Logger LOGGER = LogManager.getLogger("MainThreadPump");
private static final Tracy.ZoneId Z_DISPLAY_MESSAGES = Tracy.zoneId("displayMessages", Tracy.COLOR_CLIENT);
private static final long PARK_MILLIS = 20L;
private static final long STALL_WARN_NANOS = 5_000_000_000L;
public static void pollInput() {
Keyboard.poll();
Mouse.poll();
}
private final Executor mainExecutor;
private final BooleanSupplier isMainThread;
private final Runnable pumpBody;
private final Runnable pollBody;
private final Runnable task = this::runPump;
private final Object lock = new Object();
private boolean done;
private Throwable failure;
private ClassLoader callerLoader;
private boolean presentPumpPending;
private boolean presentInFlight;
private Throwable presentFailure;
private ClassLoader presentLoader;
public MainThreadPump(Executor mainExecutor, BooleanSupplier isMainThread, Runnable pumpBody, Runnable pollBody) {
this.mainExecutor = mainExecutor;
this.isMainThread = isMainThread;
this.pumpBody = pumpBody;
this.pollBody = pollBody;
}
public void pumpMessages() {
Tracy.beginZone(Z_DISPLAY_MESSAGES);
try {
if (isMainThread.getAsBoolean()) {
pumpBody.run();
} else {
synchronized (lock) {
await(true);
final Throwable t = presentFailure;
presentFailure = null;
if (t != null) throw asUnchecked(t);
if (!presentInFlight) runPumpOnMainThread();
}
}
pollBody.run();
} finally {
Tracy.endZone();
}
}
private void runPumpOnMainThread() {
final Thread self = Thread.currentThread();
callerLoader = self.getContextClassLoader();
done = false;
mainExecutor.execute(task);
await(false);
final Throwable t = failure;
if (t != null) {
throw asUnchecked(t);
}
}
private boolean waiting(boolean presentPump) {
return presentPump ? presentPumpPending : !done;
}
private void await(boolean presentPump) {
boolean interrupted = false;
boolean stallReported = false;
final long stallAt = System.nanoTime() + STALL_WARN_NANOS;
while (waiting(presentPump)) {
try {
lock.wait(PARK_MILLIS);
} catch (InterruptedException e) {
interrupted = true;
}
if (waiting(presentPump) && !stallReported && System.nanoTime() - stallAt >= 0L) {
stallReported = true;
LOGGER.warn("Display message pump has been waiting on the RFB main thread for over {} ms", STALL_WARN_NANOS / 1_000_000L);
}
}
if (interrupted) Thread.currentThread().interrupt();
}
public void beginPresent() {
synchronized (lock) {
presentInFlight = true;
presentPumpPending = true;
presentLoader = Thread.currentThread().getContextClassLoader();
}
}
public void runPresentPump() {
final ClassLoader loader;
synchronized (lock) {
loader = presentLoader;
}
final Throwable t = pumpWith(loader);
synchronized (lock) {
if (t != null) presentFailure = t;
presentPumpPending = false;
lock.notifyAll();
}
}
public void endPresent() {
synchronized (lock) {
presentInFlight = false;
presentPumpPending = false;
lock.notifyAll();
}
}
private void runPump() {
final Throwable t = pumpWith(callerLoader);
synchronized (lock) {
failure = t;
done = true;
lock.notifyAll();
}
}
private Throwable pumpWith(ClassLoader loader) {
final Thread self = Thread.currentThread();
final ClassLoader saved = self.getContextClassLoader();
try {
if (loader != null) self.setContextClassLoader(loader);
pumpBody.run();
return null;
} catch (Throwable t) {
return t;
} finally {
self.setContextClassLoader(saved);
}
}
private static RuntimeException asUnchecked(Throwable t) {
if (t instanceof RuntimeException runtime) return runtime;
if (t instanceof Error error) throw error;
return new RuntimeException(t);
}
}
@@ -0,0 +1,714 @@
package com.gtnewhorizons.angelica.glsm.backend;
import com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities;
import com.gtnewhorizons.angelica.glsm.GLESFormatRemap;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.RenderSystem;
import com.gtnewhorizons.angelica.glsm.texture.TextureStaging;
import org.lwjgl.LWJGLException;
import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL12;
import org.lwjgl.opengl.GL20;
import java.nio.ByteBuffer;
import java.nio.DoubleBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.nio.ShortBuffer;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.locks.LockSupport;
import java.util.function.Predicate;
/**
* Abstract rendering backend.
*/
public abstract class RenderBackend {
public void onPostWindowCreate(long window) {}
public abstract void init();
public abstract void shutdown();
public abstract boolean isAvailable();
public abstract String getName();
/** Returns true if the current thread has a valid render context. */
public abstract boolean hasContext();
/** Returns true if the calling thread may issue render commands. */
public boolean hasContextOnThread() { return hasContext(); }
/** Returns true if the calling thread owns the "GL Context" */
public abstract boolean isCurrent();
/** 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 boolean isIndirectRequired() { return false; }
public boolean supportsGeometryShaders() { return true; }
public boolean isSDLGPU() { return false; }
public boolean framebufferCompletenessIsMeaningful() { return true; }
public void onPersistentBufferWrite(int glId, long offset, long size) {}
public boolean isAnisotropicSupported() { return true; }
public boolean supportsGpuDrivenCulling() { return RenderSystem.supportsCompute(); }
public void beginComputeDispatchBatch() {}
public void endComputeDispatchBatch() {}
public abstract int getMinGLSLVersion();
public abstract void flush();
public abstract void finish();
private final ByteBuffer[] clientStagingCache = new ByteBuffer[16];
private VSyncMode effectiveVSyncMode = VSyncMode.ON;
private VSyncMode preferredTearFreeMode;
public final void setVSyncEnabled(boolean enabled) {
setVSyncMode(enabled ? preferredTearFreeMode() : VSyncMode.OFF);
}
public final void setVSyncMode(VSyncMode preferred) {
setVSyncPreference(preferred);
final VSyncMode mode = applyVSyncMode(preferred);
vsyncHonored = applyHonored();
publishVSyncMode(mode);
}
public final void setVSyncPreference(VSyncMode preferred) {
if (preferred.tearFree()) preferredTearFreeMode = preferred;
}
public final VSyncMode preferredTearFreeMode() {
return resolveTearFreeMode(preferredTearFreeMode, this::supportsVSyncMode);
}
static VSyncMode resolveTearFreeMode(VSyncMode preferred, Predicate<VSyncMode> supported) {
if (preferred != null && preferred.tearFree() && supported.test(preferred)) return preferred;
return supported.test(VSyncMode.MAILBOX) ? VSyncMode.MAILBOX : VSyncMode.ON;
}
protected abstract VSyncMode applyVSyncMode(VSyncMode preferred);
protected boolean applyHonored() { return true; }
private boolean vsyncHonored = true;
public final boolean vsyncHonored() { return vsyncHonored; }
public boolean supportsVSyncMode(VSyncMode mode) { return mode != VSyncMode.MAILBOX; }
protected final void publishVSyncMode(VSyncMode mode) {
effectiveVSyncMode = mode;
bumpDisplayGeneration();
}
public final VSyncMode getEffectiveVSyncMode() { return effectiveVSyncMode; }
private int displayGeneration;
public final int displayGeneration() { return displayGeneration; }
protected final void bumpDisplayGeneration() { displayGeneration++; }
private static final long REFRESH_POLL_NANOS = 1_000_000_000L;
private static final long MIN_PLAUSIBLE_PERIOD_NANOS = 1_000_000L;
private static final long MAX_PLAUSIBLE_PERIOD_NANOS = 50_000_000L;
private long refreshPollNanos;
private long cachedRefreshPeriodNanos;
public final long refreshPeriodNanos() {
final long now = System.nanoTime();
if (refreshPollNanos == 0L || now - refreshPollNanos >= REFRESH_POLL_NANOS) {
refreshPollNanos = now;
final long fresh = plausiblePeriodNanos(queryRefreshPeriodNanos());
if (fresh != cachedRefreshPeriodNanos) bumpDisplayGeneration();
cachedRefreshPeriodNanos = fresh;
}
return cachedRefreshPeriodNanos;
}
protected long queryRefreshPeriodNanos() { return 0L; }
static long plausiblePeriodNanos(long periodNanos) {
return (periodNanos >= MIN_PLAUSIBLE_PERIOD_NANOS && periodNanos <= MAX_PLAUSIBLE_PERIOD_NANOS) ? periodNanos : 0L;
}
public static long periodFromRational(int numerator, int denominator, float fallbackHz) {
if (numerator > 0 && denominator > 0) {
final long scaledDenominator = 1_000_000_000L * denominator;
return (scaledDenominator + numerator / 2) / numerator;
}
return fallbackHz > 0.0f ? Math.round(1_000_000_000.0 / fallbackHz) : 0L;
}
private static final int[] TRUNCATED_NTSC_HZ = {23, 29, 59, 71, 119, 143, 239};
private static boolean isTruncatedNtscRate(int hz) {
for (int candidate : TRUNCATED_NTSC_HZ) {
if (candidate == hz) return true;
}
return false;
}
public static long periodFromIntegerHz(int hz) {
if (hz <= 0) return 0L;
if (isTruncatedNtscRate(hz)) {
final long denominator = (hz + 1) * 1000L;
return Math.round(1_000_000_000.0 * 1001 / denominator);
}
return Math.round(1_000_000_000.0 / hz);
}
public static int hzFromPeriod(long periodNanos) {
return periodNanos <= 0L ? 0 : (int) Math.round(1_000_000_000.0 / periodNanos);
}
public boolean hasSwapchainBackpressure() { return false; }
public static final class GateSample {
public long durationNanos;
public long endNanos;
public long gpuWaitNanos;
}
static final class GateRecorder {
private long gateDurationNanos;
private long gateEndNanos;
private long gpuWaitNanos;
synchronized void recordGate(long durationNanos, long endNanos) {
if (gateEndNanos != 0L && gateDurationNanos >= durationNanos) return;
gateDurationNanos = durationNanos;
gateEndNanos = endNanos;
}
synchronized void recordGpuWait(long nanos) {
gpuWaitNanos += nanos;
}
synchronized void readGateSample(GateSample out) {
final long end = gateEndNanos;
out.durationNanos = end == 0L ? 0L : gateDurationNanos;
out.endNanos = end;
out.gpuWaitNanos = gpuWaitNanos;
gpuWaitNanos = 0L;
gateEndNanos = 0L;
}
}
private final GateRecorder gateRecorder = new GateRecorder();
public boolean wantsDisplayUpdateGateTiming() { return true; }
public final void recordGate(long durationNanos, long endNanos) {
gateRecorder.recordGate(durationNanos, endNanos);
}
public final void recordGpuWait(long nanos) {
gateRecorder.recordGpuWait(nanos);
}
public final void readGateSample(GateSample out) {
gateRecorder.readGateSample(out);
}
public void awaitPresent() {}
public void parkNanos(long nanos) {
LockSupport.parkNanos(nanos);
}
public void pumpDisplayMessages() {
Display.processMessages();
}
private boolean presentSuppressed;
public final void setPresentSuppressed(boolean suppressed) { presentSuppressed = suppressed; }
public final boolean isPresentSuppressed() { return presentSuppressed; }
public void onFrameBegin() {
if (GLStateManager.takeStateSeedPending()) {
GLStateManager.replayStateToBackend();
}
}
public void invalidateStateMirror() {}
public void onFrameEnd() {}
/** Fired by lwjgl3ify Display before a mutation that recreates the swapchain. */
public void onPreSwapchainInvalidatingChange(Object change) { bumpDisplayGeneration(); }
public boolean handleMakeCurrent(Object drawable) { return false; }
public boolean handleReleaseContext(Object drawable) { return false; }
public boolean handleSwapBuffers() { return false; }
public void onRenderThreadReleased(Thread thread) {}
public void updateDisplayFromWorkerThread(boolean processMessages) throws LWJGLException {
GLStateManager.swapBuffers();
if (processMessages) {
pumpDisplayMessages();
}
}
public String getTransferDebugInfo() { return null; }
public long beginVoxelizationBatch(int ssboBinding) { return 0L; }
public void voxelizeRegion(long pass, boolean rebindVertexBuffer, float x, float y, float z, int rangeBase, int rangeCount, int vertexTotal) {}
public void endVoxelizationBatch(long pass) {}
public abstract void enable(int cap);
public abstract void enablei(int cap, int index);
public abstract void disable(int cap);
public abstract void disablei(int cap, int index);
public abstract void blendFuncSeparatei(int buf, int srcRGB, int dstRGB, int srcAlpha, int dstAlpha);
public abstract void blendFunc(int sfactor, int dfactor);
public abstract void blendFuncSeparate(int srcRGB, int dstRGB, int srcAlpha, int dstAlpha);
public abstract void blendEquation(int mode);
public abstract void blendEquationSeparate(int modeRGB, int modeAlpha);
public abstract void blendColor(float red, float green, float blue, float alpha);
public abstract void depthFunc(int func);
public abstract void depthMask(boolean flag);
public abstract void colorMask(boolean red, boolean green, boolean blue, boolean alpha);
public abstract void cullFace(int mode);
public abstract void frontFace(int mode);
public abstract void polygonMode(int face, int mode);
public abstract void polygonOffset(float factor, float units);
public void polygonOffsetClamp(float factor, float units, float clamp) { throw new UnsupportedOperationException(getName() + ": polygon offset clamp unsupported"); }
public abstract void stencilFunc(int func, int ref, int mask);
public abstract void stencilOp(int sfail, int dpfail, int dppass);
public abstract void stencilMask(int mask);
public abstract void stencilFuncSeparate(int face, int func, int ref, int mask);
public abstract void stencilOpSeparate(int face, int sfail, int dpfail, int dppass);
public abstract void stencilMaskSeparate(int face, int mask);
public abstract void viewport(int x, int y, int width, int height);
public abstract void depthRange(double nearVal, double farVal);
public abstract void scissor(int x, int y, int width, int height);
public abstract void clearColor(float red, float green, float blue, float alpha);
public abstract void clearDepth(double depth);
public abstract void clearStencil(int s);
public abstract void clear(int mask);
public abstract void lineWidth(float width);
public abstract void pointSize(float size);
public abstract void logicOp(int opcode);
public abstract void hint(int target, int mode);
public abstract void drawArrays(int mode, int first, int count);
public abstract void drawElements(int mode, int count, int type, long indices);
public void drawElements(int mode, ByteBuffer indices) {}
public void drawElements(int mode, IntBuffer indices) {}
public void drawElements(int mode, ShortBuffer indices) {}
public void drawElements(int mode, int count, int type, ByteBuffer indices) {}
public abstract void multiDrawElementsIndirect(int mode, int type, long indirect, int drawcount, int stride);
public void multiDrawArraysIndirectCount(int mode, long indirect, long drawcount, int maxdrawcount, int stride) { throw new UnsupportedOperationException(getName() + ": indirect draw-count buffers unsupported"); }
public void multiDrawElementsIndirectCount(int mode, int type, long indirect, long drawcount, int maxdrawcount, int stride) { throw new UnsupportedOperationException(getName() + ": indirect draw-count buffers unsupported"); }
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 void drawRangeElements(int mode, int start, int end, int count, int type, long indices) {
drawElements(mode, count, type, indices);
}
public void drawRangeElementsBaseVertex(int mode, int start, int end, int count, int type, long indices, int baseVertex) {
drawElementsBaseVertex(mode, count, type, indices, baseVertex);
}
public abstract void multiDrawArrays(int mode, IntBuffer firsts, IntBuffer counts);
public abstract void primitiveRestartIndex(int index);
public abstract void pointParameterf(int pname, float param);
public abstract void pointParameteri(int pname, int param);
public abstract void drawBuffer(int mode);
public abstract void dispatchCompute(int numGroupsX, int numGroupsY, int numGroupsZ);
public abstract void dispatchComputeIndirect(long offset);
public abstract int genTextures();
public abstract void genTextures(IntBuffer textures);
public abstract void deleteTextures(int texture);
public abstract void bindTexture(int target, int texture);
public abstract void activeTexture(int texture);
public abstract void texImage2D(int target, int level, int internalFormat, int width, int height, int border, int format, int type, ByteBuffer pixels);
public abstract void texImage2D(int target, int level, int internalFormat, int width, int height, int border, int format, int type, DoubleBuffer pixels);
public abstract void texImage2D(int target, int level, int internalFormat, int width, int height, int border, int format, int type, FloatBuffer pixels);
public abstract void texImage2D(int target, int level, int internalFormat, int width, int height, int border, int format, int type, IntBuffer pixels);
public abstract void texImage2D(int target, int level, int internalFormat, int width, int height, int border, int format, int type, long pixelBufferOffset);
public abstract void texSubImage2D(int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, ByteBuffer pixels);
public abstract void texSubImage2D(int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, IntBuffer pixels);
public abstract void copyTexSubImage2D(int target, int level, int xoffset, int yoffset, int x, int y, int width, int height);
public void texImage1D(int target, int level, int internalFormat, int width, int border, int format, int type, ByteBuffer pixels) {}
public void texImage3D(int target, int level, int internalFormat, int width, int height, int depth, int border, int format, int type, ByteBuffer pixels) {}
public void texImage3D(int target, int level, int internalFormat, int width, int height, int depth, int border, int format, int type, IntBuffer pixels) {}
public void texSubImage1D(int target, int level, int xoffset, int width, int format, int type, ByteBuffer pixels) {}
public void texSubImage2D(int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, long pboOffset) {}
public TextureStaging beginTextureStaging(int level, int x, int y, int width, int height) {
final int size = Math.multiplyExact(Math.multiplyExact(width, height), 4);
ByteBuffer buffer = null;
for (int i = 0; i < clientStagingCache.length; i++) {
final ByteBuffer cached = clientStagingCache[i];
if (cached != null && cached.capacity() == size) {
clientStagingCache[i] = null;
buffer = cached;
break;
}
}
if (buffer == null) buffer = MemoryUtilities.memAlloc(size);
buffer.clear();
return new ClientTextureStaging(buffer, level, x, y, width, height);
}
public boolean commitTextureStaging(TextureStaging staging) {
final int type = RenderSystem.isGLES() ? GLESFormatRemap.remapPixelType(GL12.GL_BGRA, GL12.GL_UNSIGNED_INT_8_8_8_8_REV) : GL12.GL_UNSIGNED_INT_8_8_8_8_REV;
try {
final ClientTextureStaging s = (ClientTextureStaging) staging;
texSubImage2D(GL11.GL_TEXTURE_2D, s.level(), s.x(), s.y(), s.width(), s.height(), GL12.GL_BGRA, type, s.buffer());
} finally {
recycleClientStaging(staging.buffer());
}
return true;
}
public void abandonTextureStaging(TextureStaging staging) {
recycleClientStaging(staging.buffer());
}
public void trimTextureStaging() {
for (int i = 0; i < clientStagingCache.length; i++) {
MemoryUtilities.memFree(clientStagingCache[i]);
clientStagingCache[i] = null;
}
}
private record ClientTextureStaging(ByteBuffer buffer, int level, int x, int y, int width, int height) implements TextureStaging {
@Override
public boolean bgra() {
return true;
}
}
private void recycleClientStaging(ByteBuffer buffer) {
int victim = -1;
for (int i = 0; i < clientStagingCache.length; i++) {
final ByteBuffer cached = clientStagingCache[i];
if (cached == null) {
clientStagingCache[i] = buffer;
return;
}
if (cached.capacity() < buffer.capacity() && (victim < 0 || cached.capacity() < clientStagingCache[victim].capacity())) {
victim = i;
}
}
if (victim < 0) {
MemoryUtilities.memFree(buffer);
return;
}
MemoryUtilities.memFree(clientStagingCache[victim]);
clientStagingCache[victim] = buffer;
}
public void texSubImage3D(int target, int level, int xoffset, int yoffset, int zoffset, int width, int height, int depth, int format, int type, ByteBuffer pixels) {}
public void copyTexImage1D(int target, int level, int internalFormat, int x, int y, int width, int border) {}
public void copyTexImage2D(int target, int level, int internalFormat, int x, int y, int width, int height, int border) {}
public void copyTexSubImage1D(int target, int level, int xoffset, int x, int y, int width) {}
public void copyTexSubImage3D(int target, int level, int xoffset, int yoffset, int zoffset, int x, int y, int width, int height) {}
public abstract void texParameteri(int target, int pname, int param);
public abstract void texParameterf(int target, int pname, float param);
public abstract void texParameteriv(int target, int pname, IntBuffer params);
public abstract void texParameterfv(int target, int pname, FloatBuffer params);
public abstract int getTexParameteri(int target, int pname);
public abstract float getTexParameterf(int target, int pname);
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 sampleCoverage(float value, boolean invert) {}
public abstract int genSamplers();
public abstract void deleteSamplers(int sampler);
public abstract void bindSampler(int unit, int sampler);
public abstract void samplerParameteri(int sampler, int pname, int param);
public abstract void samplerParameterf(int sampler, int pname, float param);
public abstract int genFramebuffers();
public abstract void deleteFramebuffers(int framebuffer);
public abstract void bindFramebuffer(int target, int framebuffer);
public abstract void framebufferTexture2D(int target, int attachment, int textarget, int texture, int level);
public abstract void framebufferTexture(int target, int attachment, int texture, int level);
public abstract int genRenderbuffers();
public abstract void deleteRenderbuffers(int renderbuffer);
public abstract void bindRenderbuffer(int target, int renderbuffer);
public abstract void renderbufferStorage(int target, int internalformat, int width, int height);
public abstract void renderbufferStorageMultisample(int target, int samples, int internalformat, int width, int height);
public abstract void framebufferRenderbuffer(int target, int attachment, int renderbuffertarget, int renderbuffer);
public abstract int checkFramebufferStatus(int target);
public abstract void blitFramebuffer(int srcX0, int srcY0, int srcX1, int srcY1, int dstX0, int dstY0, int dstX1, int dstY1, int mask, int filter);
public abstract void drawBuffers(int buffer);
public abstract void drawBuffers(IntBuffer bufs);
public abstract void readBuffer(int mode);
public abstract void readPixels(int x, int y, int width, int height, int format, int type, ByteBuffer pixels);
public abstract void readPixels(int x, int y, int width, int height, int format, int type, FloatBuffer pixels);
public abstract void readPixels(int x, int y, int width, int height, int format, int type, IntBuffer pixels);
public abstract void readPixels(int x, int y, int width, int height, int format, int type, long pixelBufferOffset);
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);
public abstract void deleteShader(int shader);
public abstract void shaderSource(int shader, CharSequence source);
public abstract void compileShader(int shader);
public void specializeShader(int shader, CharSequence entryPoint, IntBuffer constantIndex, IntBuffer constantValue) { throw new UnsupportedOperationException(getName() + ": externally supplied SPIR-V binaries unsupported"); }
public abstract int createProgram();
public abstract void deleteProgram(int program);
public abstract void attachShader(int program, int shader);
public abstract void detachShader(int program, int shader);
public abstract void linkProgram(int program);
public abstract void useProgram(int program);
public abstract String getShaderInfoLog(int shader, int maxLength);
public String getShaderInfoLog(int shader) {
return getShaderInfoLog(shader, getShaderi(shader, GL20.GL_INFO_LOG_LENGTH));
}
public abstract void getShaderInfoLog(int shader, IntBuffer length, ByteBuffer infoLog);
public abstract String getProgramInfoLog(int program, int maxLength);
public String getProgramInfoLog(int program) {
return getProgramInfoLog(program, getProgrami(program, GL20.GL_INFO_LOG_LENGTH));
}
public abstract void getProgramInfoLog(int program, IntBuffer length, ByteBuffer infoLog);
public abstract int getShaderi(int shader, int pname);
public abstract int getProgrami(int program, int pname);
public abstract void getProgramiv(int program, int pname, IntBuffer params);
public abstract String getActiveUniform(int program, int index, int maxLength, IntBuffer sizeType);
public abstract void getActiveUniform(int program, int index, IntBuffer length, IntBuffer size, IntBuffer type, ByteBuffer name);
public abstract void getActiveAttrib(int program, int index, IntBuffer length, IntBuffer size, IntBuffer type, ByteBuffer name);
public abstract String getActiveAttrib(int program, int index, int maxLength, IntBuffer sizeType);
public abstract void bindAttribLocation(int program, int index, CharSequence name);
public void bindFragDataLocation(int program, int colorNumber, CharSequence name) {}
public abstract int getAttribLocation(int program, CharSequence name);
public abstract int getAttribLocation(int program, ByteBuffer name);
public abstract int getUniformLocation(int program, CharSequence name);
public abstract int getUniformLocation(int program, ByteBuffer name);
public boolean isShader(int obj) { return false; }
public boolean isProgram(int obj) { return false; }
public boolean supportsProgramBinary() { return false; }
public void programParameteri(int program, int pname, int value) {}
public void getProgramBinary(int program, IntBuffer length, IntBuffer binaryFormat, ByteBuffer binary) {}
public void programBinary(int program, int binaryFormat, ByteBuffer binary) {}
public void validateProgram(int program) {}
public void getAttachedShaders(int program, IntBuffer count, IntBuffer shaders) {
if (count != null) count.put(0, 0);
}
public String getShaderSource(int shader, int maxLength) { return ""; }
public abstract void getShaderSource(int shader, IntBuffer length, ByteBuffer source);
public abstract void uniform1i(int location, int v0);
public abstract void uniform1f(int location, float v0);
public void uniform1fv(int location, FloatBuffer values) {}
public void uniform3fv(int location, float[] values) {}
public void uniform4fv(int location, float[] values) {}
public abstract void uniform2f(int location, float v0, float v1);
public abstract void uniform2i(int location, int v0, int v1);
public abstract void uniform3f(int location, float v0, float v1, float v2);
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) {}
public void getUniformfv(int program, int location, FloatBuffer params) {}
public void getUniformiv(int program, int location, IntBuffer params) {}
public abstract void vertexAttrib2f(int index, float v0, float v1);
public abstract void vertexAttrib3f(int index, float v0, float v1, float v2);
public abstract void vertexAttrib4f(int index, float v0, float v1, float v2, float v3);
public abstract int genBuffers();
public abstract void deleteBuffers(int buffer);
public abstract void deleteBuffers(IntBuffer buffers);
public abstract void bindBuffer(int target, int buffer);
public abstract void bindBufferBase(int target, int index, int buffer);
public abstract void bindBufferRange(int target, int index, int buffer, long offset, long size);
public abstract int getUniformBlockIndex(int program, CharSequence name);
public abstract void uniformBlockBinding(int program, int blockIndex, int binding);
public abstract int getIndexedBufferBinding(int target, int index);
public abstract int getIntegerIndexed(int pname, int index);
public abstract void bufferData(int target, long size, int usage);
public abstract void bufferData(int target, ByteBuffer data, int usage);
public abstract void bufferData(int target, FloatBuffer data, int usage);
public abstract void bufferData(int target, ShortBuffer data, int usage);
public abstract void bufferData(int target, IntBuffer data, int usage);
public abstract void bufferData(int target, DoubleBuffer data, int usage);
public abstract void bufferData(int target, int[] data, int usage);
public abstract void bufferData(int target, float[] data, int usage);
public abstract void bufferSubData(int target, long offset, ByteBuffer data);
public abstract void bufferSubData(int target, long offset, ShortBuffer data);
public abstract void bufferSubData(int target, long offset, IntBuffer data);
public abstract void bufferSubData(int target, long offset, FloatBuffer data);
public abstract void bufferSubData(int target, long offset, DoubleBuffer data);
public abstract ByteBuffer mapBuffer(int target, int access);
public abstract ByteBuffer mapBuffer(int target, int access, long length, ByteBuffer old_buffer);
public abstract boolean unmapBuffer(int target);
public abstract void bufferStorage(int target, ByteBuffer data, int flags);
public abstract void bufferStorage(int target, long size, int flags);
public abstract void getBufferSubData(int target, long offset, ByteBuffer data);
public abstract void getBufferSubData(int target, long offset, ShortBuffer data);
public abstract void getBufferSubData(int target, long offset, IntBuffer data);
public abstract void getBufferSubData(int target, long offset, FloatBuffer data);
public abstract void getBufferSubData(int target, long offset, DoubleBuffer data);
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 long mapBufferRangeAddress(int target, long offset, long length, int access) {
final ByteBuffer buf = mapBufferRange(target, offset, length, access);
return buf == null ? 0L : MemoryUtilities.memAddress0(buf);
}
public abstract boolean isTexture(int texture);
public abstract boolean isFramebuffer(int framebuffer);
public abstract boolean isRenderbuffer(int renderbuffer);
public abstract boolean isSampler(int sampler);
public abstract boolean isQuery(int query);
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));
}
public void vertexAttribIPointer(int index, int size, int type, int stride, ByteBuffer pointer) {
vertexAttribIPointer(index, size, type, stride, MemoryUtilities.memAddress0(pointer));
}
public abstract void vertexAttribIPointer(int index, int size, int type, int stride, long pointer);
public abstract void enableVertexAttribArray(int index);
public abstract void disableVertexAttribArray(int index);
public abstract void vertexAttribDivisor(int index, int divisor);
public abstract void bindVertexBuffer(int bindingindex, int buffer, long offset, int stride);
public abstract void vertexAttribFormat(int attribindex, int size, int type, boolean normalized, int relativeoffset);
public abstract void vertexAttribIFormat(int attribindex, int size, int type, int relativeoffset);
public abstract void vertexAttribBinding(int attribindex, int bindingindex);
public abstract int createTextures(int target);
public abstract void bindTextureUnit(int unit, int texture);
public abstract void textureParameteri(int texture, int target, int pname, int param);
public abstract void textureParameterf(int texture, int target, int pname, float param);
public abstract void textureParameteriv(int texture, int target, int pname, IntBuffer params);
public abstract void texStorage2D(int target, int levels, int internalFormat, int width, int height);
public abstract void generateTextureMipmap(int texture);
public abstract void textureImage2DEXT(int texture, int target, int level, int internalformat, int width, int height, int border, int format, int type, ByteBuffer pixels);
public abstract void textureImage2DEXT(int texture, int target, int level, int internalformat, int width, int height, int border, int format, int type, IntBuffer pixels);
public abstract void textureSubImage2D(int texture, int level, int xoffset, int yoffset, int width, int height, int format, int type, ByteBuffer pixels);
public abstract void textureSubImage2D(int texture, int level, int xoffset, int yoffset, int width, int height, int format, int type, IntBuffer pixels);
public abstract int createFramebuffers();
public abstract void namedFramebufferTexture(int framebuffer, int attachment, int texture, int level);
public abstract void namedFramebufferReadBuffer(int framebuffer, int mode);
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);
public abstract void copyTextureSubImage2D(int texture, int target, int level, int xoffset, int yoffset, int x, int y, int width, int height);
public abstract int getTextureParameteri(int texture, int target, int pname);
public abstract float getTextureParameterf(int texture, int target, int pname);
public abstract int getTextureLevelParameteri(int texture, int level, int pname);
public abstract int getInteger(int pname);
public abstract void getInteger(int pname, IntBuffer params);
public void getInteger(int pname, int[] params) { if (params.length > 0) params[0] = getInteger(pname); }
public abstract float getFloat(int pname);
public abstract void getFloat(int pname, FloatBuffer params);
public double getDouble(int pname) { return getFloat(pname); }
public void getDouble(int pname, DoubleBuffer params) { if (params.remaining() > 0) params.put(params.position(), getDouble(pname)); }
public abstract boolean getBoolean(int pname);
public abstract void getBoolean(int pname, ByteBuffer params);
public abstract String getString(int pname);
public abstract String getStringi(int name, int index);
public abstract int getError();
public abstract long fenceSync(int condition, int flags);
public abstract int clientWaitSync(long sync, int flags, long timeout);
public abstract void deleteSync(long sync);
public void waitSync(long sync, int flags, long timeout) { throw new UnsupportedOperationException("sync objects unsupported"); }
public int getSynci(long sync, int pname, IntBuffer length) { throw new UnsupportedOperationException("sync objects unsupported"); }
public void genQueries(IntBuffer ids) { throw new UnsupportedOperationException("occlusion queries unsupported"); }
public int genQueries() { throw new UnsupportedOperationException("occlusion queries unsupported"); }
public void deleteQueries(int id) { throw new UnsupportedOperationException("occlusion queries unsupported"); }
public void beginQuery(int target, int id) { throw new UnsupportedOperationException("occlusion queries unsupported"); }
public void endQuery(int target) { throw new UnsupportedOperationException("occlusion queries unsupported"); }
public void getQueryObjectui(int id, int pname, IntBuffer params) { throw new UnsupportedOperationException("occlusion queries unsupported"); }
public int getQueryObjecti(int id, int pname) { throw new UnsupportedOperationException("occlusion queries unsupported"); }
public void queryCounter(int id, int target) { throw new UnsupportedOperationException("timer queries unsupported"); }
public long getQueryObjectui64(int id, int pname) { throw new UnsupportedOperationException("timer queries unsupported"); }
public boolean supportsGpuProfiling() { return false; }
public int genQuery() { throw new UnsupportedOperationException("timer queries unsupported"); }
public void deleteQuery(int query) { throw new UnsupportedOperationException("timer queries unsupported"); }
public void queryCounter(int query) { throw new UnsupportedOperationException("timer queries unsupported"); }
public boolean isQueryResultAvailable(int query) { throw new UnsupportedOperationException("timer queries unsupported"); }
public long getQueryResult64(int query) { throw new UnsupportedOperationException("timer queries unsupported"); }
public long getGpuTimestamp() { throw new UnsupportedOperationException("timer queries unsupported"); }
public abstract void clearBufferSubData(int target, int internalFormat, long offset, long size, int format, int type, ByteBuffer data);
public abstract void clearBufferData(int target, int internalformat, int format, int type, ByteBuffer data);
public abstract void clearTexImage(int texture, int level, int format, int type);
public abstract void bindImageTexture(int unit, int texture, int level, boolean layered, int layer, int access, int format);
public abstract void memoryBarrier(int barriers);
public abstract void copyImageSubData(int srcName, int srcTarget, int srcLevel, int srcX, int srcY, int srcZ,
int dstName, int dstTarget, int dstLevel, int dstX, int dstY, int dstZ,
int srcWidth, int srcHeight, int srcDepth);
// Debug output
public boolean supportsDebugOutput() { return false; }
public int setupDebugOutput(DebugMessageHandler handler) { return 0; }
public int disableDebugOutput() { return 0; }
// Object labeling
public void objectLabel(int identifier, int name, CharSequence label) {}
public String getObjectLabel(int identifier, int name) { return ""; }
// Debug groups
public void pushDebugGroup(int source, int id, CharSequence message) {}
public void popDebugGroup() {}
// Debug message insertion
public void debugMessageInsert(int source, int type, int id, int severity, CharSequence message) {}
public void debugMessageControl(int source, int type, int severity, IntBuffer ids, boolean enabled) {}
public void debugMessageCallback(GLDebugMessageListener listener, long userParam) {}
public int getDebugMessageLog(int count, IntBuffer sources, IntBuffer types, IntBuffer ids, IntBuffer severities, IntBuffer lengths, ByteBuffer messageLog) { return 0; }
}
@@ -0,0 +1,11 @@
package com.gtnewhorizons.angelica.glsm.backend;
public enum VSyncMode {
OFF,
ON,
MAILBOX;
public boolean tearFree() {
return this != OFF;
}
}
@@ -0,0 +1,11 @@
package com.gtnewhorizons.angelica.glsm.compat;
/**
* Simple holder for fog color values captured from vanilla rendering.
* Used by terrain renderers to access fog state.
*/
public class FogHelper {
public static float red;
public static float green;
public static float blue;
}
@@ -0,0 +1,142 @@
package com.gtnewhorizons.angelica.glsm.compat.lwjgl;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import org.lwjgl.util.glu.Cylinder;
import static org.lwjgl.opengl.GL11.GL_LINES;
import static org.lwjgl.opengl.GL11.GL_LINE_LOOP;
import static org.lwjgl.opengl.GL11.GL_POINTS;
import static org.lwjgl.opengl.GL11.GL_TRIANGLE_STRIP;
import static org.lwjgl.util.glu.GLU.GLU_FILL;
import static org.lwjgl.util.glu.GLU.GLU_INSIDE;
import static org.lwjgl.util.glu.GLU.GLU_LINE;
import static org.lwjgl.util.glu.GLU.GLU_POINT;
import static org.lwjgl.util.glu.GLU.GLU_SILHOUETTE;
/**
* Core-profile compatible replacement for {@link Cylinder}.
*/
@SuppressWarnings("unused")
public class AngelicaCylinder extends Cylinder {
private static final float PI = (float) Math.PI;
private void emitNormal(float x, float y, float z) {
final float mag = (float) Math.sqrt(x * x + y * y + z * z);
if (mag > 0.00001F) {
x /= mag;
y /= mag;
z /= mag;
}
GLStateManager.glNormal3f(x, y, z);
}
private void emitTexCoord(float x, float y) {
if (super.textureFlag) GLStateManager.glTexCoord2f(x, y);
}
@Override
public void draw(float baseRadius, float topRadius, float height, int slices, int stacks) {
final float da, dr, dz, nz, nsign;
float r, x, y, z;
nsign = (super.orientation == GLU_INSIDE) ? -1.0f : 1.0f;
da = 2.0f * PI / slices;
dr = (topRadius - baseRadius) / stacks;
dz = height / stacks;
nz = (baseRadius - topRadius) / height;
if (super.drawStyle == GLU_POINT) {
GLStateManager.glBegin(GL_POINTS);
for (int i = 0; i < slices; i++) {
x = cos(i * da);
y = sin(i * da);
emitNormal(x * nsign, y * nsign, nz * nsign);
z = 0.0f;
r = baseRadius;
for (int j = 0; j <= stacks; j++) {
GLStateManager.glVertex3f(x * r, y * r, z);
z += dz;
r += dr;
}
}
GLStateManager.glEnd();
} else if (super.drawStyle == GLU_LINE || super.drawStyle == GLU_SILHOUETTE) {
// Draw rings
if (super.drawStyle == GLU_LINE) {
z = 0.0f;
r = baseRadius;
for (int j = 0; j <= stacks; j++) {
GLStateManager.glBegin(GL_LINE_LOOP);
for (int i = 0; i < slices; i++) {
x = cos(i * da);
y = sin(i * da);
emitNormal(x * nsign, y * nsign, nz * nsign);
GLStateManager.glVertex3f(x * r, y * r, z);
}
GLStateManager.glEnd();
z += dz;
r += dr;
}
} else {
// draw one ring at each end
if (baseRadius != 0.0) {
GLStateManager.glBegin(GL_LINE_LOOP);
for (int i = 0; i < slices; i++) {
x = cos(i * da);
y = sin(i * da);
emitNormal(x * nsign, y * nsign, nz * nsign);
GLStateManager.glVertex3f(x * baseRadius, y * baseRadius, 0.0f);
}
GLStateManager.glEnd();
GLStateManager.glBegin(GL_LINE_LOOP);
for (int i = 0; i < slices; i++) {
x = cos(i * da);
y = sin(i * da);
emitNormal(x * nsign, y * nsign, nz * nsign);
GLStateManager.glVertex3f(x * topRadius, y * topRadius, height);
}
GLStateManager.glEnd();
}
}
// draw length lines
GLStateManager.glBegin(GL_LINES);
for (int i = 0; i < slices; i++) {
x = cos(i * da);
y = sin(i * da);
emitNormal(x * nsign, y * nsign, nz * nsign);
GLStateManager.glVertex3f(x * baseRadius, y * baseRadius, 0.0f);
GLStateManager.glVertex3f(x * topRadius, y * topRadius, height);
}
GLStateManager.glEnd();
} else if (super.drawStyle == GLU_FILL) {
final float ds = 1.0f / slices;
final float dt = 1.0f / stacks;
float t = 0.0f;
z = 0.0f;
r = baseRadius;
for (int j = 0; j < stacks; j++) {
float s = 0.0f;
GLStateManager.glBegin(GL_TRIANGLE_STRIP);
for (int i = 0; i <= slices; i++) {
final float angle = (i == slices) ? 0.0f : i * da;
x = sin(angle);
y = cos(angle);
emitNormal(x * nsign, y * nsign, nz * nsign);
emitTexCoord(s, t);
GLStateManager.glVertex3f(x * r, y * r, z);
emitNormal(x * nsign, y * nsign, nz * nsign);
emitTexCoord(s, t + dt);
GLStateManager.glVertex3f(x * (r + dr), y * (r + dr), z + dz);
s += ds;
}
GLStateManager.glEnd();
r += dr;
t += dt;
z += dz;
}
}
}
}
Loaded 100 of 2491 files, more files were not shown because too many files have changed in this diff. Show more