Zum Hauptinhalt springen
Inoffizieller Guild Wars Reforged App — ein Fan-Projekt, nicht mit ArenaNet oder NCSoft verbunden

UR-10: long sessions stop at the 2 GiB heap cap

Build 38797 has a fixed 2 GiB WebAssembly heap. We can prove the ceiling and the failed allocation, but cache retention, fragmentation, and live working set still require an ArenaNet heap profile.

Some content-heavy sessions end when the WebAssembly heap reaches exactly 2,147,483,648 bytes. That maximum is declared in both artifacts ArenaNet ships: the WASM memory section and its generated JavaScript glue. The next allocation that needs more capacity is refused and the client aborts.

What we are asking ArenaNet to do: profile settled live ownership across a short multi-zone route, correct the responsible lifecycle, cache budget, or allocation pattern, and raise the wasm32 maximum to 4 GiB minus one page as additional headroom. The larger maximum must be built with the matching unsigned JavaScript pointer support; changing only the WASM declaration is not safe.

The ceiling is proven; the underlying leak is not
The 2 GiB ceiling and the allocation refused at that ceiling are properties of ArenaNet's build 38797. We cannot yet classify the preceding memory use as a leak, cache retention, fragmentation, or legitimate referenced working set. The allocation that loses at the boundary does not necessarily own the memory allocated before it.

What the client says when it stops

One captured failure showed:

WASM heap at crash: 2048 MiB of 2048 MiB
ASSERTION FAILED: Out of memory!
../../../Engine/Gr/Gles3/GlTex.cpp:397

This proves that the final failed request was in ArenaNet's GLES3 texture path. It does not prove that textures retained the entire heap. At the limit, whichever allocation asks for the next block raises its own assertion.

The cap is compiled into both artifacts

Build 38797's Gw.jspi.wasm declares one memory:

  • initial: 4,096 pages, or 256 MiB;
  • maximum: 32,768 pages, or 2 GiB.

The matching Gw.jspi.js contains:

js
var getHeapMax = () =>
  // Stay one Wasm page short of 4GB: ...
  2147483648;

The host does not impose a smaller ceiling. Growth succeeds until the declared maximum is reached. wasm32 cannot provide more than a 4 GiB address space, so a larger maximum buys headroom but cannot make unbounded growth safe.

Reports and measured sessions

Reported failures include open-world sessions lasting several hours and texture-dense instanced content reaching the same boundary in roughly half an hour. Players have also reported shorter failures in a Resplendent Makuun vanquish and Dunes of Despair Hard Mode.

Our longest recorded non-crashing staircase reached 1,619 MiB after 136 minutes:

ElapsedHeap after stepElapsedHeap after step
0.1 m256 MiB84.8 m1,124 MiB
1.4 m442 MiB94.5 m1,220 MiB
13.1 m627 MiB97.7 m1,316 MiB
46.6 m724 MiB109.5 m1,427 MiB
67.5 m930 MiB127.2 m1,523 MiB
82.5 m1,027 MiB128.7 m1,619 MiB

That session did not converge before the player quit. Newer short sessions do sometimes converge: one multi-zone run reached 733 MiB and stayed there while the host processed substantial additional image traffic. Both results are real. The route, cache warmth, and allocator reuse affect whether a particular session reproduces the growth.

WASM memory capacity never shrinks within a process. A flat heap means current allocations fit into existing capacity; a rising heap means they did not. A rising capacity alone cannot distinguish retained live bytes from fragmentation.

What allocation attribution found

Our first allocator experiment was too expensive to keep: it added about 47% steady-state overhead. Its useful historical snapshots identified several large ArenaNet allocation owners rather than one texture-only explanation:

Exact build-38797 functionReverse-engineered ownerObserved retained large bytes
2797GlTex117–148 MiB
1586ImgMem46–96 MiB
4491model loading29–129 MiB
3421unresolved secondary owner24–49 MiB

The replacement research probe wraps only exact lifecycle functions for ImgMem, models, and GlTex. It exports aggregates rather than pointers or per-allocation events.

Static analysis found a complete reference-safe model lifecycle already in the client:

  1. release function 4629 moves zero-reference models to an unused list with a one-second expiry;
  2. acquire function 4628 removes a reused model from that list;
  3. sweep function 4649 destroys expired unused entries through the normal virtual destructor;
  4. the normal model update calls that sweep approximately every ten seconds.

A corrected short capture observed 6,092 normal model evictions and only 1.9 MiB in the zero-reference aggregate, but its bootstrap completeness flag was false. That data cannot authorize an external trim. It does show that the normal model sweep is active.

We found no certified standalone unused check for the image or texture destruction paths. Calling a destructor, raw free, or glDeleteTexture from the host would risk stale lookup entries and use-after-free defects.

The short internal reproduction ArenaNet can run

ArenaNet does not need to wait hours for the public abort. Source symbols and a heap profiler turn this into a short ownership test:

Record a settled baseline

Use build 38797 with allocation/free stacks. Record live bytes, the largest free block, free-bin distribution, and image/model/texture ownership.

Travel through ten distinct zones

Pause after transitions 3, 6, and 9 so unload work and the model sweep can finish. Revisit three earlier zones, then remain idle for 30 seconds.

Compare capacity with live ownership

  • Capacity rises while live bytes return near baseline: fragmentation.
  • Settled live bytes rise across unloads: retention or legitimate working set.
  • Zero-reference bytes remain high: cache budget or sweep issue.
  • Referenced bytes remain high: follow the retaining reference; do not destroy the object externally.
  • Both capacity and live bytes plateau: normal warm-up for that route.

Repeat with a warm cache

Use the same build, route, account, and settings. A controlled comparison must change only one variable.

The first source paths worth inspecting are ImgMem allocation 1586, model load 4491, and GlTex allocation 2797, plus their existing cache budgets and normal release paths.

Our interim mitigation

Safe reload warning

GWonMac's production warning monitors the effective heap capacity while there is still room to return to a town or outpost, then lets the player choose when to reload. Reloading resets the WASM heap. It is recovery, not a memory fix.

Exact-build 4 GB research profile

We have also built an off-by-default research profile for build 38797. It changes the WASM maximum to 65,535 pages—4 GiB minus one 64 KiB page—and transforms the exact generated JavaScript to treat high wasm32 pointers as unsigned addresses.

The pair is hash-gated and fails closed: an unknown JS or WASM artifact runs untouched. Raising only the WASM maximum is explicitly refused.

Qualification completed so far:

  • ArenaNet's real allocator grew to 3,025,928,192 bytes offline;
  • it returned and reused allocations above 2 GiB;
  • the live Electron client grew to 2,625 MiB;
  • live malloc returned raw signed pointer -2078907408, unsigned address 2216059888;
  • the transformed generated string glue wrote and read GW4G through that high pointer without trapping.

This proves the core approach across the former boundary. It does not prove every rare WebGL or host callback path, and it does not bound the underlying memory demand. The current profile remains a developer experiment. All five variants the production chain can emit—including Enhancements disabled—are hash-pinned. It still needs packaged high-address gameplay coverage, system-memory testing, and an explicit user setting before an experimental player release.

If released before an ArenaNet update, it will be labelled Experimental 4 GB memory mode, disabled by default, restricted to exact certified builds, easy to turn off, and recommended only on Macs with at least 16 GB of RAM.

What we will not do

  • Change only the WASM maximum.
  • Exceed wasm32's 4 GiB address space.
  • Call raw free, direct destructors, or glDeleteTexture.
  • Guess a cache function or context pointer.
  • Replace ArenaNet's allocator.
  • Automatically reload a player during a mission.
  • Present the 4 GB profile as a fix for unbounded retention.

Acceptance criteria

For ArenaNet's corrective build:

  • settled live ownership and heap capacity plateau across repeated content transitions;
  • zero-reference cache entries leave through normal reference-safe eviction;
  • freed large blocks are reusable when fragmentation was the cause;
  • a three-hour content-diverse session does not approach the effective maximum;
  • if the maximum is raised, generated JS and WASM agree and ordinary game paths work above 2 GiB;
  • the client retains a clear failure reason if it exhausts the new maximum.

For our optional mitigation, every accepted JS/WASM pair must be pinned to an exact ArenaNet build, unknown builds must remain untouched, the mode must be visible in diagnostics, and disabling it must restore ordinary client behavior after restart.

Current conclusion

The 2 GiB limit and final failed allocation are in ArenaNet's shipped client. The reason some sessions consume the available address space still needs an ArenaNet source-level heap profile. Until that is fixed, a safe reload remains the conservative mitigation and an exact-build 4 GB mode can provide optional headroom for informed testers.

GWonMac

Spiele Guild Wars auf deinem Mac. GWonMac führt ArenaNets offiziellen Client nativ auf Apple Silicon aus — kostenlos, Open Source, signiert und notarisiert.

© GWonMac. Alle Rechte vorbehalten.