Skip to content

← Blog

Post

Pushing x86 software 150x faster than it should go(Bedrock Dedicated Server)

How Minebud E2E runs real Bedrock gameplay tests at up to 3,000 TPS.

2 min read8 viewsLikes: 0Dislikes: 0PGP signature

Why

When coding unit tests, you expect for behavior to be performed as fast as possibe, often much faster than production. With Minecraft development this is different; For actual ingame unit testing server software will cap you at 20Ticks per second(TPS) A scenario that waits 200 ticks waits 10 real seconds. Our reference workload — walk 200 blocks, mine a log in survival, verify the block is air, wait 200 ticks — took 68 seconds of wall time at stock speed, most of it just standing around while the clock ticked. Multiply that by every test run and real time becomes the product's biggest cost.

In this blog post, I will outline how we made those instances run at up to ~3,000 ticks per second: reverse engineering the engine's timer in the BDS binary, and hooking it with a native shim.

Finding the timer

Start with the same binary from my RakNet post:

curl --http1.1 -fL -A 'Mozilla/5.0' https://www.minecraft.net/bedrockdedicatedserver/bin-linux/bedrock-server-1.26.52.3.zip -o bds.zip
unzip -j bds.zip bedrock_server

Everything about this project is pinned to exactly this build — BDS 1.26.52.3, SHA-256 0a490c711d4a2ce075debcd979e8862b7eaba008373f9970f0edbb67f98f9207, GNU build ID c0ff36e2eacfc191f01e189237df71f139b5d04a.

  • Timer::advanceTime starts at RVA 0xb4cef50. This is the function that decides how much game time passes in one engine update.
  • Only two call sites own gameplay: the ones returning to 0x9e929e6 and 0x9e929f5. Everything else that touches a timer is not the simulation. At those call sites, ServerInstance sits in rbx, its simulation and update timers live at +0xb0 and +0xb8, and each timer carries a float scale at +0xc — how much game time one real moment is worth.
  • The constructor at 0x9e51c50 initializes a field at ServerInstance+0x210 to 5,000 microseconds. The main loop at 0x9e92835 multiplies it by 1,000 to get nanoseconds. That is the engine's wake interval: how often the loop wakes up to advance the timers.
  • At 0xb4cf10f, one update is capped at ten ticks, no matter how much time the timer says has passed.
ServerInstance gameplay timers and advanceTime call sitesServerInstance: +0xb0 simulationTimer : Timer*, +0xb8 updateTimer : Timer*, +0x210 wakeupIntervalMicroseconds : uint64 = 5000. Timer: +0x4 accumulatedTicks : int, +0xc scale : float; advanceTime(preferredFrameTime). advanceTime call sites: RVA 0xb4cef50 entry, 0x9e929e6 update domain, 0x9e929f5 simulation domain, 0xb4cf10f ten-tick cap. ServerInstance to Timer (owns both gameplay timers). Timer to advanceTime call sites (only these two callers own gameplay).owns both gameplay timersonly these two callers own gameplayServerInstance+0xb0 simulationTimer : Timer*+0xb8 updateTimer : Timer*+0x210 wakeupIntervalMicroseconds : uint64 = 5000Timer+0x4 accumulatedTicks : int+0xc scale : floatadvanceTime(preferredFrameTime)advanceTime call sitesRVA 0xb4cef50 entry0x9e929e6 update domain0x9e929f5 simulation domain0xb4cf10f ten-tick cap

Deploying this on scale to production.

Production test infrastructure and tick accountingE2E API: Cloudflare Worker, reserve → settle credits. D1: Cloudflare Worker, tick budgets, permanent ledger. Controller: Game host (economyhosting), admission, queue, reaper, 300 s hard cap. Server container: Game host (economyhosting), gVisor/runsc, tmpfs. Bot container: Game host (economyhosting). bedrock_server 1.26.52.3: Server container, + Endstone 0.11.13. engine.c hook: Server container, advanceTime detour. pulse.c + meter.c: Server container, loopback UDP 19140. void world template: Server container, ~1 KiB level.dat. mwbot: Bot container, plays the scenario. E2E API and D1. Controller to Server container. Controller to Bot container. Server container to bedrock_server 1.26.52.3 (contains). Bot container to mwbot (contains). bedrock_server 1.26.52.3 and engine.c hook. engine.c hook and pulse.c + meter.c. bedrock_server 1.26.52.3 and void world template. E2E API and Controller (signed create /`status / report). mwbot to bedrock_server 1.26.52.3 (real RakNet /`NetherNet session). mwbot to pulse.c + meter.c (MBCLKP1`acknowledgements). pulse.c + meter.c to Controller (MBMTR01`snapshots).containscontainssigned create /status / reportreal RakNet /NetherNet sessionMBCLKP1acknowledgementsMBMTR01snapshotsE2E APICloudflare Workerreserve → settle creditsD1Cloudflare Workertick budgetspermanent ledgerControllerGame host (economyhosting)admission, queue, reaper300 s hard capServer containerGame host (economyhosting)gVisor/runsc, tmpfsBot containerGame host (economyhosting)bedrock_server 1.26.52.3Server container+ Endstone 0.11.13engine.c hookServer containeradvanceTime detourpulse.c + meter.cServer containerloopback UDP 19140void world templateServer container~1 KiB level.datmwbotBot containerplays the scenario