Skip to content

Commit 236efdd

Browse files
author
Jasper Geurtz
committed
Only copy active units, events, and eventStrings in FrameBuffer
1 parent e7fea36 commit 236efdd

2 files changed

Lines changed: 142 additions & 3 deletions

File tree

‎src/main/java/bwapi/FrameBuffer.java‎

Lines changed: 99 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,21 @@
1212
*/
1313
class 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
}

‎src/test/java/bwapi/AsyncClientDataTest.java‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ public void retainedViewsSurviveLiveAndPackedHandoffs() {
1212
PackedWrappedBuffer packed = new PackedWrappedBuffer(ClientData.GameData.SIZE);
1313
ClientData source = new ClientData();
1414
source.setBuffer(live);
15+
source.gameData().setEventCount(1);
1516
source.gameData().getEvents(0).setType(EventType.MatchFrame);
1617
source.gameData().getStartLocations(0).setX(123);
1718
new FrameBuffer(new BWClientConfiguration.Builder().withAsyncFrameBufferCapacity(1).build())
@@ -213,4 +214,46 @@ public void asyncClientDataWorksWithFullUnpackedBuffer() {
213214
assertTrue(asyncData.gameData().isVisible(10, 20));
214215
assertTrue(asyncData.gameData().isInGame());
215216
}
217+
218+
@Test
219+
public void copyBufferTracksMaxUnitsAndOnlyCopiesActiveUnitsAndEvents() {
220+
WrappedBuffer mainBuffer = new WrappedBuffer(ClientData.GameData.SIZE);
221+
PackedWrappedBuffer packedBuffer = new PackedWrappedBuffer();
222+
223+
ClientData mainData = new ClientData();
224+
mainData.setBuffer(mainBuffer);
225+
226+
// Set initialUnitCount = 3 and populate unit #2 and unit #50 (beyond initialUnitCount)
227+
mainData.gameData().setInitialUnitCount(3);
228+
mainData.gameData().getUnits(2).setHitPoints(100);
229+
mainData.gameData().getUnits(50).setHitPoints(250);
230+
231+
// Populate event #0 (active) and event #5 (beyond eventCount = 1)
232+
mainData.gameData().setEventCount(1);
233+
mainData.gameData().getEvents(0).setType(EventType.MatchFrame);
234+
mainData.gameData().getEvents(5).setType(EventType.MatchEnd);
235+
236+
FrameBuffer frameBuffer = new FrameBuffer(
237+
new BWClientConfiguration.Builder().withAsyncFrameBufferCapacity(1).build());
238+
frameBuffer.copyBuffer(mainBuffer, packedBuffer);
239+
240+
AsyncClientData asyncData = new AsyncClientData(mainBuffer);
241+
asyncData.setBuffer(packedBuffer);
242+
243+
assertEquals(3, frameBuffer.getMaxUnitCount());
244+
assertEquals(100, asyncData.gameData().getUnits(2).getHitPoints());
245+
// Unit #50 and Event #5 were beyond active counts, so they were skipped
246+
assertEquals(0, asyncData.gameData().getUnits(50).getHitPoints());
247+
assertEquals(EventType.MatchFrame, asyncData.gameData().getEvents(0).getType());
248+
assertEquals(EventType.MatchStart, asyncData.gameData().getEvents(5).getType());
249+
250+
// Now fire UnitDiscover for unit #50 on the next frame
251+
mainData.gameData().setEventCount(1);
252+
mainData.gameData().getEvents(0).setType(EventType.UnitDiscover);
253+
mainData.gameData().getEvents(0).setV1(50);
254+
frameBuffer.copyBuffer(mainBuffer, packedBuffer);
255+
256+
assertEquals(51, frameBuffer.getMaxUnitCount());
257+
assertEquals(250, asyncData.gameData().getUnits(50).getHitPoints());
258+
}
216259
}

0 commit comments

Comments
 (0)