1212 */
1313class FrameBuffer {
1414 static final int BUFFER_SIZE = PackedWrappedBuffer .BUFFER_SIZE ;
15+
16+ static final int INITIAL_UNIT_COUNT_OFFSET = 69648 ;
17+ static final int UNITS_START = 69656 ;
18+ static final int MAX_UNITS = 10000 ;
19+ static final int UNITS_END = UNITS_START + MAX_UNITS * ClientData .UnitData .SIZE ;
20+
21+ static final int EVENT_COUNT_OFFSET = 10586620 ;
22+ static final int EVENTS_START = 10586624 ;
23+ static final int MAX_EVENTS = 10000 ;
24+ static final int EVENTS_HEADER_SIZE = EVENTS_START - PackedWrappedBuffer .REGION_END ;
25+
26+ static final int EVENT_STRING_COUNT_OFFSET = 10706624 ;
27+ static final int EVENT_STRING_SIZE = 256 ;
28+ static final int MAX_EVENT_STRINGS = 1000 ;
29+
1530 private static final Unsafe unsafe = UnsafeTools .getUnsafe ();
1631
1732 private WrappedBuffer liveData ;
@@ -20,6 +35,7 @@ class FrameBuffer {
2035 private final int capacity ;
2136 private int stepGame = 0 ;
2237 private int stepBot = 0 ;
38+ private int maxUnitCount = 0 ;
2339 private final ArrayList <WrappedBuffer > dataBuffer = new ArrayList <>();
2440
2541 private final Lock lockWrite = new ReentrantLock ();
@@ -42,6 +58,7 @@ void initialize(WrappedBuffer liveData, PerformanceMetrics performanceMetrics) {
4258 this .performanceMetrics = performanceMetrics ;
4359 stepGame = 0 ;
4460 stepBot = 0 ;
61+ maxUnitCount = 0 ;
4562 }
4663
4764 /**
@@ -151,9 +168,88 @@ void dequeue() {
151168 } finally { lockSize .unlock (); }
152169 }
153170
171+ /**
172+ * The speed at which we copy data into the frame buffer is a major cost of JBWAPI's asynchronous operation.
173+ * Copy times observed in the wild for the complete 33 MB shared memory buffer used to range from 2.6ms - 19ms
174+ * and are prone to large amounts of variance.
175+ *
176+ * To minimize both memory footprint and copy time, we:
177+ * 1. Omit static map and region data (groundHeight, isWalkable, isBuildable, regions), which are read directly
178+ * from the main shared memory buffer via AsyncClientData.
179+ * 2. Omit client-to-server output buffers (strings, shapes, commands, unitCommands), which are flushed directly
180+ * to live shared memory by SideEffectQueue at the end of the frame.
181+ * 3. Pack the remaining 3 dynamic ranges into a 4.08 MB PackedWrappedBuffer (using pre-shifted slice views so
182+ * static BWAPI field offsets work without runtime branching).
183+ * 4. Within those ranges, only copy the currently active prefixes of units[0..maxUnitCount),
184+ * events[0..eventCount), and eventStrings[0..eventStringCount), skipping their unused tails.
185+ *
186+ * Approximate bytes copied per frame (Fastest game speed ~= 23.8 frames/sec, ~1,428 frames/minute):
187+ * - Fixed base (header, forces, players, bullets, dynamic tile flags): ~349 KB (0.35 MB)
188+ * - Game start (Frame 0, ~300 initial units): ~450 KB (0.45 MB)
189+ * - Mid-game (~8-10 min / ~7,500 frames, ~1,000 cumulative units): ~685 KB (0.69 MB)
190+ * - Late-game (~20+ min / ~20,000 frames, ~2,500 cumulative units): ~1.19 MB (max cap at 10,000 units: 4.08 MB)
191+ *
192+ * Estimated per-frame copy times across hardware and CPU core counts:
193+ * +-----------------------------------------------+---------------+---------------+---------------+---------------+
194+ * | Environment / Hardware & Core Count | Start (450KB) | Mid (685KB) | Late (1.19MB) | Max (4.08MB) |
195+ * +-----------------------------------------------+---------------+---------------+---------------+---------------+
196+ * | Weak / Virtualized (1-vCPU Docker / shared VM | 0.12 - 0.35ms | 0.18 - 0.50ms | 0.30 - 0.85ms | 1.00 - 2.80ms |
197+ * | ~3-5 GB/s; Wine, Client & Bot share 1 core) | | | | |
198+ * +-----------------------------------------------+---------------+---------------+---------------+---------------+
199+ * | Normal Desktop / Tourney VM (2-4 cores, DDR4, | 0.04 - 0.08ms | 0.06 - 0.12ms | 0.10 - 0.20ms | 0.30 - 0.60ms |
200+ * | ~12 GB/s; Copy runs in parallel with Bot) | | | | |
201+ * +-----------------------------------------------+---------------+---------------+---------------+---------------+
202+ * | Fast Modern CPU (4+ cores, DDR5, ~30+ GB/s; | 0.01 - 0.03ms | 0.02 - 0.05ms | 0.04 - 0.08ms | 0.12 - 0.25ms |
203+ * | fits in L2/L3, finishes before Bot wakes) | | | | |
204+ * +-----------------------------------------------+---------------+---------------+---------------+---------------+
205+ */
154206 void copyBuffer (WrappedBuffer source , WrappedBuffer destination ) {
155- unsafe .copyMemory (source .getAddress () + 0 , destination .getAddress () + PackedWrappedBuffer .DEST_SLICE1_OFFSET , PackedWrappedBuffer .DYNAMIC_SLICE1_SIZE );
156- unsafe .copyMemory (source .getAddress () + PackedWrappedBuffer .STATICTILES_END , destination .getAddress () + PackedWrappedBuffer .DEST_SLICE2_OFFSET , PackedWrappedBuffer .DYNAMIC_SLICE2_SIZE );
157- unsafe .copyMemory (source .getAddress () + PackedWrappedBuffer .REGION_END , destination .getAddress () + PackedWrappedBuffer .DEST_SLICE3_OFFSET , PackedWrappedBuffer .DYNAMIC_SLICE3_SIZE );
207+ long srcAddr = source .getAddress ();
208+ long dstAddr = destination .getAddress ();
209+
210+ int eventCount = clamp (source .getInt (EVENT_COUNT_OFFSET ), 0 , MAX_EVENTS );
211+ int eventStringCount = clamp (source .getInt (EVENT_STRING_COUNT_OFFSET ), 0 , MAX_EVENT_STRINGS );
212+ int activeUnits = updateMaxUnitCount (source , eventCount );
213+
214+ // Slice 1a (dynamic size): header, forces, players, initialUnitCount, and active units [0 .. activeUnits)
215+ copySlice (srcAddr , dstAddr , 0 , 0 , UNITS_START + activeUnits * ClientData .UnitData .SIZE );
216+ // Slice 1b (static size): unitArray, bullets, nukeDots, frameCount, and other post-unit Slice 1 fields
217+ copySlice (srcAddr , dstAddr , UNITS_END , 0 , PackedWrappedBuffer .STATICTILES_START - UNITS_END );
218+ // Slice 2 (static size): tile visibility, exploration, creep, and occupation flags
219+ copySlice (srcAddr , dstAddr , PackedWrappedBuffer .STATICTILES_END , PackedWrappedBuffer .DELTA_RANGE2 , PackedWrappedBuffer .DYNAMIC_SLICE2_SIZE );
220+ // Slice 3a (dynamic size): startLocations, game state flags, selectedUnits, self/enemy/neutral, eventCount, and active events [0 .. eventCount)
221+ copySlice (srcAddr , dstAddr , PackedWrappedBuffer .REGION_END , PackedWrappedBuffer .DELTA_RANGE3 , EVENTS_HEADER_SIZE + eventCount * ClientData .Event .SIZE );
222+ // Slice 3b (dynamic size): eventStringCount and active eventStrings [0 .. eventStringCount)
223+ copySlice (srcAddr , dstAddr , EVENT_STRING_COUNT_OFFSET , PackedWrappedBuffer .DELTA_RANGE3 , 4 + eventStringCount * EVENT_STRING_SIZE );
224+ }
225+
226+ int getMaxUnitCount () {
227+ return maxUnitCount ;
228+ }
229+
230+ private static int clamp (int value , int min , int max ) {
231+ return Math .max (min , Math .min (max , value ));
232+ }
233+
234+ private int updateMaxUnitCount (WrappedBuffer source , int eventCount ) {
235+ int initialUnits = clamp (source .getInt (INITIAL_UNIT_COUNT_OFFSET ), 0 , MAX_UNITS );
236+ if (initialUnits > maxUnitCount ) {
237+ maxUnitCount = initialUnits ;
238+ }
239+
240+ for (int i = 0 ; i < eventCount ; i ++) {
241+ int eventOffset = EVENTS_START + i * ClientData .Event .SIZE ;
242+ if (source .getInt (eventOffset ) == EventType .UnitDiscover .id ) {
243+ int unitId = source .getInt (eventOffset + 4 );
244+ if (unitId >= 0 && unitId < MAX_UNITS && unitId + 1 > maxUnitCount ) {
245+ maxUnitCount = unitId + 1 ;
246+ }
247+ }
248+ }
249+ return maxUnitCount ;
250+ }
251+
252+ private static void copySlice (long srcAddr , long dstAddr , int srcOffset , int delta , int length ) {
253+ unsafe .copyMemory (srcAddr + srcOffset , dstAddr + srcOffset - delta , length );
158254 }
159255}
0 commit comments