88import java .nio .ByteOrder ;
99import java .nio .MappedByteBuffer ;
1010import java .nio .channels .FileChannel ;
11+ import java .util .Arrays ;
1112import java .util .function .Consumer ;
1213import net .evmodder .evmod .Main ;
1314
1415public final class PlayerPosIPC {
1516 private static final class Holder {private static final PlayerPosIPC INSTANCE = new PlayerPosIPC ();}
1617 public static final PlayerPosIPC getInstance (){return Holder .INSTANCE ;}
1718
18- // Hopefully nobody is running more than this many Minecraft accounts on 1 device...
19+ // All these are static-final to take advantage of Constant Folding
1920 private static final int MAX_SLOTS = 64 ;
2021 // PID + TS + version + data
2122 public static final int METADATA_SIZE = 8 + 8 + 8 ; //=24
@@ -37,23 +38,42 @@ private static final class Holder{private static final PlayerPosIPC INSTANCE = n
3738// private static final String MEM_TABLE_FILENAME = "minecraft_player_pos.bin";
3839 private static final VarHandle LONG_HANDLE = MethodHandles .byteBufferViewVarHandle (long [].class , ByteOrder .nativeOrder ());
3940
40- private static final long myPID = ProcessHandle .current ().pid ();
41- private final long [] lastReadVersions = new long [ MAX_SLOTS ] ; // Can't be static (static variables are shared across the entire JVM )
42- private final byte [] data = new byte [ DATA_SIZE ] ;
41+ private static final long myPID = ProcessHandle .current ().pid (); // Static for Constant Propagation (not Constant Folding)
42+ private final long [] lastReadVersions ; // No benefit to making these static (memory addresses to dynamic values )
43+ private final byte [] data ;
4344 private final MappedByteBuffer buffer ;
44- private int mySlot ;
45+
46+ private int mySlot , readCount ;
47+
48+ private static final File getFastestTempDir () {
49+ final String os = System .getProperty ("os.name" ).toLowerCase ();
50+ if (os .contains ("linux" )){
51+ // /dev/shm is a tmpfs (RAM). It is significantly faster than /tmp.
52+ final File shm = new File ("/dev/shm" );
53+ if (shm .exists () && shm .canWrite ()) return shm ;
54+ }
55+ // For Windows/Mac, java.io.tmpdir is the standard path.
56+ // On Windows, there is no native "RAM-disk" guaranteed to exist.
57+ return new File (System .getProperty ("java.io.tmpdir" ));
58+ }
4559
4660 private final void freeSlot (final long owner , final int base ){
4761 if (base == LAST_SLOT_BASE ) LONG_HANDLE .compareAndSet (buffer , base , owner , 0l );
48- else if ((long )LONG_HANDLE .getVolatile (buffer , base + SLOT_SIZE ) != 0l ) LONG_HANDLE .compareAndSet (buffer , base , owner , -1l );
62+ else if ((long )LONG_HANDLE .getAcquire (buffer , base + SLOT_SIZE ) != 0l ) LONG_HANDLE .compareAndSet (buffer , base , owner , -1l );
4963 else if (LONG_HANDLE .compareAndSet (buffer , base , owner , -2l )){
50- final long clearedVal = (long )LONG_HANDLE .getVolatile (buffer , base + SLOT_SIZE ) == 0l ? 0l : -1l ;
64+ final long clearedVal = (long )LONG_HANDLE .getAcquire (buffer , base + SLOT_SIZE ) == 0l ? 0l : -1l ;
65+ if (clearedVal == 0l ){
66+ final long version = (long )LONG_HANDLE .get (buffer , base + VERSION_OFFSET );
67+ // final long nextEvenVer = (version + 1l) & -2l;
68+ LONG_HANDLE .setRelease (buffer , base + VERSION_OFFSET , (version + 1l ) & -2l );
69+ }
5170 LONG_HANDLE .compareAndSet (buffer , base , -2l , clearedVal );
5271 }
5372 }
5473
74+
5575 private PlayerPosIPC (){
56- final File file = new File (System . getProperty ( "java.io.tmpdir" ), "minecraft_player_pos.bin" );
76+ final File file = new File (getFastestTempDir ( ), "minecraft_player_pos.bin" );
5777 try (final RandomAccessFile raf = new RandomAccessFile (file , "rw" )){
5878 final long TOTAL_SIZE = (long ) MAX_SLOTS *SLOT_SIZE ;
5979 if (raf .length () < TOTAL_SIZE ) raf .setLength (TOTAL_SIZE );
@@ -68,16 +88,21 @@ private PlayerPosIPC(){
6888 final int base = mySlot *SLOT_SIZE ;
6989 if (buffer != null && myPID == (long )LONG_HANDLE .getVolatile (buffer , base )) freeSlot (myPID , base );
7090 }));
91+ // Non-shared variables, used exclusively by this PID
92+ lastReadVersions = new long [MAX_SLOTS ];
93+ data = new byte [DATA_SIZE ];
94+ Arrays .fill (lastReadVersions , -1l );
95+ claimSlot (); // Not strictly-necessary to call this prior to postData()
7196 }
7297
7398 private final int claimSlot (){
74- final long now = System .nanoTime ();
7599 for (int j =0 ; j <CLAIM_LOOP_MAX_ATTEMPTS ; ++j ){
100+ final long now = System .nanoTime ();
76101 for (int i =0 ; i <MAX_SLOTS ; ++i ){
77102 final int base = i *SLOT_SIZE ;
78103 final long owner = (long )LONG_HANDLE .getVolatile (buffer , base );
79- if (owner > 0l && now - (long )LONG_HANDLE .getVolatile (buffer , base + TIME_OFFSET ) < TIMEOUT_NS ) continue ;
80- LONG_HANDLE .setVolatile (buffer , base + TIME_OFFSET , now ); // Update ts (reduces contention fighting for this slot)
104+ if (owner > 0l && now - (long )LONG_HANDLE .getAcquire (buffer , base + TIME_OFFSET ) < TIMEOUT_NS ) continue ;
105+ LONG_HANDLE .setOpaque (buffer , base + TIME_OFFSET , now ); // Update heartbeat (reduces contention fighting for this slot)
81106 if (LONG_HANDLE .compareAndSet (buffer , base , owner , myPID )){
82107 Main .LOGGER .info ("[EvMod] Claimed IPC slot " +i );
83108 return i ; // Nice, we snagged this slot!
@@ -89,39 +114,56 @@ private final int claimSlot(){
89114 return -1 ; // Failed to acquire a slot!!
90115 }
91116
117+ private final void reapZombies () {
118+ final long now = System .nanoTime ();
119+ for (int i =0 ; i <MAX_SLOTS ; ++i ){
120+ final int base = i *SLOT_SIZE ;
121+ // final int base = i << 7;// Assumes `SLOT_SIZE == 128`!
122+ final long owner = (long ) LONG_HANDLE .getOpaque (buffer , base );
123+ if (owner == myPID ) continue ; // Given its own line / branch prediction here, unlike in readData()
124+ if (owner <= 0l ){
125+ if (owner == 0l ) return ;
126+ continue ;
127+ }
128+ if (now - (long )LONG_HANDLE .getAcquire (buffer , base + TIME_OFFSET ) > TIMEOUT_NS ) freeSlot (owner , base );
129+ }
130+ }
131+
92132 public final void postData (final byte [] data ){
93133 assert data .length == DATA_SIZE ;
94- // Ensure my slot is still valid (and update it if not).
134+ // Proactive heartbeat (even if mySlot is expired and got claimed by someone else)
135+ LONG_HANDLE .setOpaque (buffer , mySlot *SLOT_SIZE + TIME_OFFSET , System .nanoTime ());
136+ // Ensure slot ownership, and claim a new slot if necessary
95137 if (myPID != (long )LONG_HANDLE .getVolatile (buffer , mySlot *SLOT_SIZE ) && (mySlot =claimSlot ()) == -1 ) return ;
96138 final int base = mySlot *SLOT_SIZE ;
97139 final long currentVer = (long )LONG_HANDLE .get (buffer , base + VERSION_OFFSET ); // Get current version
98140 final long nextEvenVer = (currentVer +1l )&-2l ;
99141 LONG_HANDLE .setRelease (buffer , base + VERSION_OFFSET , nextEvenVer ); // Move current version to EVEN (signals write in-progress)
100- LONG_HANDLE .setVolatile (buffer , base + TIME_OFFSET , System .nanoTime ()); // Heartbeat
101142 buffer .put (base + DATA_OFFSET , data ); // Publish data
102- LONG_HANDLE .setRelease (buffer , base + VERSION_OFFSET , nextEvenVer + 1l ); // Move current version to ODD (signals write completed)
143+ final long nextOddVer = nextEvenVer + 1l ;
144+ lastReadVersions [mySlot ] = nextOddVer ; // Ensure I don't read my own data
145+ LONG_HANDLE .setRelease (buffer , base + VERSION_OFFSET , nextOddVer ); // Move current version to ODD (signals write completed)
103146 }
104147
105148 public final void readData (final Consumer <byte []> consumer ){
106- final long now = System . nanoTime (), prevOwner ;
149+ if ((++ readCount & 1023 ) == 0 ) reapZombies (); // Only call every 1024 cycles
107150 for (int i =0 ; i <MAX_SLOTS ; ++i ){
108151 final int base = i *SLOT_SIZE ;
152+ // final int base = i << 7; // Assumes `SLOT_SIZE == 128`!
153+ final long version = (long )LONG_HANDLE .getAcquire (buffer , base + VERSION_OFFSET );
154+ if (version <= lastReadVersions [i ]) continue ;
109155 final long owner = (long )LONG_HANDLE .getOpaque (buffer , base );
110- if (owner < 0l || owner == myPID ) continue ;
111- if (owner == 0l ){
112- // Attempt cleanup of trailing -1/-2 for the last slot before 0
113- if (i > 0 && (prevOwner =(long )LONG_HANDLE .getVolatile (buffer , base -SLOT_SIZE )) < 0l ) freeSlot (prevOwner , base -SLOT_SIZE );
114- return ;
156+ if (owner <= 0l ){
157+ if (owner == 0l ) return ;
158+ continue ;
115159 }
116- if (now - (long )LONG_HANDLE .getVolatile (buffer , base + TIME_OFFSET ) > TIMEOUT_NS ){freeSlot (owner , base ); continue ;}
117- // while(((long)LONG_HANDLE.getAcquire(buffer, base + VERSION_OFFSET)&1) == 0) Thread.onSpinWait();
118- final long version = (long )LONG_HANDLE .getAcquire (buffer , base + VERSION_OFFSET );
119- if ((version &1l ) == 0l || version == lastReadVersions [i ]) continue ; // Skip busy or stale
120- VarHandle .fullFence ();
121- buffer .get (base + DATA_OFFSET , data ); // Read data
122- if (version == (long )LONG_HANDLE .getVolatile (buffer , base + VERSION_OFFSET )){ // Verify data wasn't altered mid-read
123- consumer .accept (data );
124- lastReadVersions [i ] = version ;
160+ if ((version & 1l ) != 0l ){ // Only read if non-busy
161+ buffer .get (base + DATA_OFFSET , data ); // Read data
162+ VarHandle .loadLoadFence (); // Explicitly prevent the next load from occurring before the 'get' finishes
163+ if (version == (long )LONG_HANDLE .getOpaque (buffer , base + VERSION_OFFSET )){ // Verify data wasn't altered mid-read
164+ consumer .accept (data );
165+ lastReadVersions [i ] = version ;
166+ }
125167 }
126168 }
127169 }
0 commit comments