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,89 @@ 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 usually 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 | Start (450KB) | Mid (685KB) | Late (1.19MB) | Core Count (1 vs. 2+ CPUs) |
195+ * +-------------------------------+---------------+---------------+---------------+------------------------------+
196+ * | Weak / Virtualized | 0.12 - 0.35ms | 0.18 - 0.50ms | 0.30 - 0.85ms | 1 vCPU: Wine, Client & Bot |
197+ * | (1-vCPU Docker / shared VM, | | | | share 1 core; small copy |
198+ * | ~3-5 GB/s + scheduler jitter) | | | | avoids L3 & CFS preemption. |
199+ * +-------------------------------+---------------+---------------+---------------+------------------------------+
200+ * | Normal Desktop / Tourney VM | 0.04 - 0.08ms | 0.06 - 0.12ms | 0.10 - 0.20ms | 2+ CPUs: Copy runs in |
201+ * | (2-4 cores, DDR4, ~12 GB/s) | | | | parallel with Bot wakeup. |
202+ * +-------------------------------+---------------+---------------+---------------+------------------------------+
203+ * | Fast Modern CPU | 0.01 - 0.03ms | 0.02 - 0.05ms | 0.04 - 0.08ms | 4+ CPUs: Fits in per-core |
204+ * | (4+ cores, DDR5, ~30+ GB/s) | | | | L2/L3; done before wakeup. |
205+ * +-------------------------------+---------------+---------------+---------------+------------------------------+
206+ */
154207 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 );
208+ long srcAddr = source .getAddress ();
209+ long dstAddr = destination .getAddress ();
210+
211+ int eventCount = clamp (source .getInt (EVENT_COUNT_OFFSET ), 0 , MAX_EVENTS );
212+ int eventStringCount = clamp (source .getInt (EVENT_STRING_COUNT_OFFSET ), 0 , MAX_EVENT_STRINGS );
213+ int activeUnits = updateMaxUnitCount (source , eventCount );
214+
215+ // Slice 1a (dynamic size): header, forces, players, initialUnitCount, and active units [0 .. activeUnits)
216+ copySlice (srcAddr , dstAddr , 0 , 0 , UNITS_START + activeUnits * ClientData .UnitData .SIZE );
217+ // Slice 1b (static size): unitArray, bullets, nukeDots, frameCount, and other post-unit Slice 1 fields
218+ copySlice (srcAddr , dstAddr , UNITS_END , 0 , PackedWrappedBuffer .STATICTILES_START - UNITS_END );
219+ // Slice 2 (static size): tile visibility, exploration, creep, and occupation flags
220+ copySlice (srcAddr , dstAddr , PackedWrappedBuffer .STATICTILES_END , PackedWrappedBuffer .DELTA_RANGE2 , PackedWrappedBuffer .DYNAMIC_SLICE2_SIZE );
221+ // Slice 3a (dynamic size): startLocations, game state flags, selectedUnits, self/enemy/neutral, eventCount, and active events [0 .. eventCount)
222+ copySlice (srcAddr , dstAddr , PackedWrappedBuffer .REGION_END , PackedWrappedBuffer .DELTA_RANGE3 , EVENTS_HEADER_SIZE + eventCount * ClientData .Event .SIZE );
223+ // Slice 3b (dynamic size): eventStringCount and active eventStrings [0 .. eventStringCount)
224+ copySlice (srcAddr , dstAddr , EVENT_STRING_COUNT_OFFSET , PackedWrappedBuffer .DELTA_RANGE3 , 4 + eventStringCount * EVENT_STRING_SIZE );
225+ }
226+
227+ int getMaxUnitCount () {
228+ return maxUnitCount ;
229+ }
230+
231+ private static int clamp (int value , int min , int max ) {
232+ return Math .max (min , Math .min (max , value ));
233+ }
234+
235+ private int updateMaxUnitCount (WrappedBuffer source , int eventCount ) {
236+ int initialUnits = clamp (source .getInt (INITIAL_UNIT_COUNT_OFFSET ), 0 , MAX_UNITS );
237+ if (initialUnits > maxUnitCount ) {
238+ maxUnitCount = initialUnits ;
239+ }
240+
241+ for (int i = 0 ; i < eventCount ; i ++) {
242+ int eventOffset = EVENTS_START + i * ClientData .Event .SIZE ;
243+ if (source .getInt (eventOffset ) == EventType .UnitDiscover .id ) {
244+ int unitId = source .getInt (eventOffset + 4 );
245+ if (unitId >= 0 && unitId < MAX_UNITS && unitId + 1 > maxUnitCount ) {
246+ maxUnitCount = unitId + 1 ;
247+ }
248+ }
249+ }
250+ return maxUnitCount ;
251+ }
252+
253+ private static void copySlice (long srcAddr , long dstAddr , int srcOffset , int delta , int length ) {
254+ unsafe .copyMemory (srcAddr + srcOffset , dstAddr + srcOffset - delta , length );
158255 }
159256}
0 commit comments