-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathMain_min.cpp
More file actions
7742 lines (6908 loc) · 309 KB
/
Copy pathMain_min.cpp
File metadata and controls
7742 lines (6908 loc) · 309 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// A 64-bit game launcher for Crysis 2, booting the x64 engine DLLs from the Mod SDK.
//
// It is deliberately STL-free and built with the VC90 compiler from WDK 7.1. That is not
// legacy baggage: retail CrySystem contains a bucket allocator that stores slab addresses
// truncated to 32 bits, so in a 64-bit process whose heap sits above the 4 GB line those
// pointers get corrupted. Building against msvcr90 as the primary CRT (/MD) puts the heap
// low, exactly where it lands in the editor, and the truncation becomes harmless.
// CrySystem is imported statically so it loads alongside msvcr90 in the right order.
//
// A few pieces can be compiled out, which is how the cause of a startup failure was narrowed
// down once: build variants that differ by exactly one thing, run each three times, and compare.
// Working forward from a build that starts, rather than backward from one that does not, is what
// makes that useful - see docs/FINDINGS.md.
//
// /DNO_DIAG skip the diagnostic report
// /DNO_TIMER_LOAD do not load WINMM or raise the timer resolution
// /DNO_DETECTOR do not install the access-violation handler
// /DNO_SLABFIX leave out the engine pointer-width patch entirely
#include <windows.h>
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
#include "cry_min.h"
// Declared locally rather than including psapi.h, to avoid adding a static import - see the
// note above LoadTimerApi for why the launcher's import list must stay minimal.
typedef struct _MODULEINFO_LOCAL {
LPVOID lpBaseOfDll;
DWORD SizeOfImage;
LPVOID EntryPoint;
} MODULEINFO;
// VC90 CRT side-by-side dependency. /MD adds it as well; stated explicitly to be safe.
#pragma comment(linker, "/manifestdependency:\"type='win32' name='Microsoft.VC90.CRT' version='9.0.21022.8' processorArchitecture='amd64' publicKeyToken='1fc8b3b9a1e18e3b'\"")
static void SetCwdToGameRoot()
{
char exePath[MAX_PATH];
GetModuleFileNameA(NULL, exePath, MAX_PATH);
char* p = strrchr(exePath, '\\'); if (p) *p = 0; // .../Bin64/launcher64.exe -> .../Bin64
p = strrchr(exePath, '\\'); if (p) *p = 0; // .../Bin64 -> the game root
SetCurrentDirectoryA(exePath);
}
// Patch a single byte in memory. Used to defuse the CryAction release asserts below.
static void PatchByte(unsigned char* addr, unsigned char val)
{
DWORD oldProt = 0;
if (VirtualProtect(addr, 1, PAGE_EXECUTE_READWRITE, &oldProt)) {
*addr = val;
VirtualProtect(addr, 1, oldProt, &oldProt);
}
}
// Vectored exception handler covering the CryMovie fixes below.
//
// Unloading a layer while a cutscene is precaching leaves dangling entries in CryMovie's
// structures: the memory gets reused, so vtable pointers end up addressing the heap, freed
// shader data, or unmapped holes. The inline guards in section 1c reject obvious garbage
// cheaply, but a pointer whose high bits are zero and which happens to address an unmapped
// page passes them and faults on dereference.
//
// Instead of crashing, each case below resumes inside the original function at a point that
// skips the corrupt element and continues iterating. The worst outcome is a broken node
// losing its animation.
//
// Note that it catches any exception code while RIP is inside the code cave, not just
// 0xC0000005: a bad pointer landing in a guard page raises STATUS_GUARD_PAGE_VIOLATION
// instead, and nothing in the cave can fault except that dereference.
static unsigned char* g_movieCave = 0;
static unsigned long long g_movieRetSkip = 0;
// Reallocation-aware iteration of CMovieSystem::Update.
//
// The original iterates a std::vector by iterator while the loop body can grow that vector:
// Animate() starts nested sequences, which push_back into m_playingSequences and reallocate
// it, leaving the iterator pointing into the freed buffer. (Crytek's own source acknowledges
// this - Movie.cpp carries the comment that Animate can invalidate the iterator.)
//
// This holds the begin pointer seen on the previous iteration; when the trampoline notices
// that begin has moved, it rebases the iterator into the new buffer and carries on.
static unsigned long long g_movieOldBegin = 0;
// Module base of CryMovie.dll, used by the handler above to locate its patched ranges.
static unsigned long long g_cryMovieBase = 0;
// How often each of the workarounds below had to fire, indexed A..G in their own order.
//
// These are not a curiosity. Every hit is an element of a cutscene that was skipped, so a
// non-zero count means the scene on screen is missing something it was supposed to show - a
// node, a track, a key. A sequence that plays correctly leaves all seven at zero. -moviestats
// writes them to movie_skips.txt while the game runs.
static volatile LONG g_movieSkips[7] = { 0, 0, 0, 0, 0, 0, 0 };
static const char* const kMovieSkipNames[7] = {
"A element unmapped in the update loop",
"B virtual call from the update loop went astray",
"C track key accessor read past its array",
"D node hierarchy walk hit a reused object",
"E node hierarchy virtual call landed on data",
"F sequence node skipped (reused memory)",
"G track reported as empty (freed under precache)"
};
static LONG CALLBACK MovieVEH(EXCEPTION_POINTERS* ep)
{
if (ep && ep->ExceptionRecord && g_movieCave) {
unsigned long long caveLo = (unsigned long long)g_movieCave;
// The whole cave, not the 128 bytes the body used to fit in: widening the guards to
// full 64-bit comparisons pushed it past that, and a fault in the tail would have gone
// unhandled - the crashes this workaround exists to absorb.
unsigned long long caveHi = caveLo + 512;
unsigned long long rip = (unsigned long long)ep->ContextRecord->Rip;
// A: faulted inside the cave itself, dereferencing an element pointer that was unmapped.
if (rip >= caveLo && rip < caveHi) {
InterlockedIncrement(&g_movieSkips[0]);
ep->ContextRecord->Rip = g_movieRetSkip; // skip this element, continue the loop
return EXCEPTION_CONTINUE_EXECUTION;
}
// B: the virtual call jumped to a garbage or null target, so RIP is outside the cave -
// but the call had already pushed its return address, which still points into it.
// Recognise that, pop the return address and skip the element.
if (ep->ExceptionRecord->ExceptionCode == 0xC0000005) {
unsigned long long rsp = (unsigned long long)ep->ContextRecord->Rsp;
unsigned long long ret = *(unsigned long long*)rsp;
if (ret >= caveLo && ret < caveHi) {
ep->ContextRecord->Rsp = rsp + 8; // drop the failed call's return address
InterlockedIncrement(&g_movieSkips[1]);
ep->ContextRecord->Rip = g_movieRetSkip; // skip this element
return EXCEPTION_CONTINUE_EXECUTION;
}
}
// C: the track key accessor read past its array because the index was corrupt. Return
// 0.0f and continue. This only fires on the fault itself; valid calls are untouched.
if (ep->ExceptionRecord->ExceptionCode == 0xC0000005 && g_cryMovieBase) {
unsigned long long accLo = g_cryMovieBase + 0x2B5F0;
unsigned long long accHi = g_cryMovieBase + 0x2B604;
if (rip >= accLo && rip < accHi) {
ep->ContextRecord->Xmm0.Low = 0; ep->ContextRecord->Xmm0.High = 0; // return 0.0f
InterlockedIncrement(&g_movieSkips[2]);
ep->ContextRecord->Rip = g_cryMovieBase + 0x2B604; // ret
return EXCEPTION_CONTINUE_EXECUTION;
}
// D: walking the node hierarchy reached a sub-object whose memory was reused, leaving a
// garbage vtable. Leave through the function's own "not found" exit, which unwinds
// the stack correctly.
unsigned long long h_lo = g_cryMovieBase + 0x3A630;
unsigned long long h_hi = g_cryMovieBase + 0x3A69B;
if (rip >= h_lo && rip < h_hi) {
InterlockedIncrement(&g_movieSkips[3]);
ep->ContextRecord->Rip = g_cryMovieBase + 0x3A67A; // xor eax,eax; add rsp,0x20; pop rbx; ret
return EXCEPTION_CONTINUE_EXECUTION;
}
// E: the same walk, but the virtual call landed on data rather than code, so RIP is
// outside the function entirely. Recognise it by the return address the call pushed,
// then leave the same way as D.
if (!(rip >= h_lo && rip < h_hi)) {
unsigned long long rsp = (unsigned long long)ep->ContextRecord->Rsp;
unsigned long long ret = *(unsigned long long*)rsp;
if (ret >= h_lo && ret < h_hi) {
ep->ContextRecord->Rsp = rsp + 8; // drop the failed call's return
InterlockedIncrement(&g_movieSkips[4]);
ep->ContextRecord->Rip = g_cryMovieBase + 0x3A67A; // leave as "not found"
return EXCEPTION_CONTINUE_EXECUTION;
}
}
// F: walking a sequence's node vector, one node's memory had been reused, so its vtable
// points into the heap and the virtual call goes astray. Skip that node; the rest of the
// sequence survives, since the fault happens before any state is modified.
if (ep->ExceptionRecord->ExceptionCode == 0xC0000005 && g_cryMovieBase) {
unsigned long long fl = g_cryMovieBase + 0x2104;
unsigned long long fh = g_cryMovieBase + 0x2148;
if (rip >= fl && rip < fh) { // faulted inside the loop body
InterlockedIncrement(&g_movieSkips[5]);
ep->ContextRecord->Rip = g_cryMovieBase + 0x213E; // skip this node, keep iterating
return EXCEPTION_CONTINUE_EXECUTION;
} else { // the call jumped to data
unsigned long long rsp = (unsigned long long)ep->ContextRecord->Rsp;
unsigned long long ret = *(unsigned long long*)rsp;
if (ret >= fl && ret < fh) {
ep->ContextRecord->Rsp = rsp + 8; // drop the failed call's return
InterlockedIncrement(&g_movieSkips[5]);
ep->ContextRecord->Rip = g_cryMovieBase + 0x213E; // skip this node
return EXCEPTION_CONTINUE_EXECUTION;
}
}
}
// G: a node's track was freed and its memory handed to the cutscene's shader precache,
// so reading the track faults. Emulate "this track has no keys" and let the function
// take its own path to the next one - counters and registers stay consistent.
if (ep->ExceptionRecord->ExceptionCode == 0xC0000005 && g_cryMovieBase) {
unsigned long long gl = g_cryMovieBase + 0x5AE4F;
unsigned long long gh = g_cryMovieBase + 0x5AE5C;
if (rip >= gl && rip < gh) { // faulted reading the track
ep->ContextRecord->Rax = 0; // report zero keys
InterlockedIncrement(&g_movieSkips[6]);
ep->ContextRecord->Rip = g_cryMovieBase + 0x5AE5C; // → je 0x5bacf → inc r14 → next track
return EXCEPTION_CONTINUE_EXECUTION;
} else { // the call jumped to data
unsigned long long rsp = (unsigned long long)ep->ContextRecord->Rsp;
unsigned long long ret = *(unsigned long long*)rsp;
if (ret >= gl && ret < gh) {
ep->ContextRecord->Rsp = rsp + 8; // drop the failed call's return
ep->ContextRecord->Rax = 0;
InterlockedIncrement(&g_movieSkips[6]);
ep->ContextRecord->Rip = g_cryMovieBase + 0x5AE5C;
return EXCEPTION_CONTINUE_EXECUTION;
}
}
}
}
}
return EXCEPTION_CONTINUE_SEARCH;
}
// The ~64 FPS ceiling of the x64 build is the Windows timer quantum, not load.
//
// Nobody raises the timer resolution: Crysis2.exe, Editor.exe and CrySystem.dll all import
// only timeGetTime from WINMM, never timeBeginPeriod. The default resolution is 15.625 ms,
// and 1000 / 15.625 = 64.0. The main thread waits on the physics barrier (measured at about
// 15.5 ms per frame, with physics itself at 0.1 ms and the GPU at 5.6 ms - the hardware is
// idle), and Windows rounds that wait up to a full scheduler quantum, so one frame costs one
// quantum.
//
// It also explains the bimodal behaviour players reported: either pinned at 64 or spiking to
// 200-300, with nothing in between. Frames that skip the wait run free; frames that hit it
// pay a whole quantum.
//
// Since Windows 10 2004 the call is per-process, so this affects only the game.
// WINMM is loaded at runtime rather than imported statically, and this is load-bearing.
// A static import is resolved by the loader before any of our code runs, pulling WINMM and its
// dependencies in ahead of the CRT and shifting the process address space. That defeats the whole
// point of building against msvcr90 (see the note at the top of this file): the heap has to land
// below the 4 GB line, or retail CrySystem's truncated slab pointers come back corrupt and the
// engine dies with "Failed CMTSafeHeap::m_pBigPool allocation" before it finishes starting.
// Whether this matters depends on how a given system lays out the address space, so it can
// hold on one machine and fail on another.
typedef unsigned (__stdcall *TimePeriodFn)(unsigned);
static TimePeriodFn g_timeBeginPeriod = 0;
static TimePeriodFn g_timeEndPeriod = 0;
static void LoadTimerApi()
{
HMODULE winmm = LoadLibraryA("winmm.dll");
if (!winmm) return;
g_timeBeginPeriod = (TimePeriodFn)GetProcAddress(winmm, "timeBeginPeriod");
g_timeEndPeriod = (TimePeriodFn)GetProcAddress(winmm, "timeEndPeriod");
}
// Borderless windowed fullscreen.
//
// With the framerate unlocked, exclusive fullscreen tears. VSync is not a good answer: the
// engine has no refresh-rate CVar at all (CryRenderD3D11 exposes only r_Fullscreen and
// r_VSync), so DXGI hands it 60 Hz and VSync pins the game to 60 FPS with input lag, even on
// a 165 Hz display.
//
// Windowed mode behaves differently: frames go through the Windows compositor, which
// synchronises presentation itself. Tearing is impossible by design, the framerate stays
// unlocked, and there is no VSync input lag. So the launcher runs the game in a borderless
// window sized to the screen - it looks like fullscreen and feels like windowed.
//
// This also fixes Alt-Tab crashes: a windowed swapchain has no exclusive device to lose on a
// focus switch.
//
// If the game ends up in exclusive fullscreen anyway, its window is already WS_POPUP covering
// the screen, so the needFix check below finds nothing to do and nothing is touched.
// Disable with -noborderless.
struct SBorderlessSearch { DWORD pid; HWND found; };
static BOOL CALLBACK BorderlessEnumProc(HWND h, LPARAM lp)
{
SBorderlessSearch* s = (SBorderlessSearch*)lp;
DWORD pid = 0;
GetWindowThreadProcessId(h, &pid);
if (pid != s->pid) return TRUE;
if (!IsWindowVisible(h)) return TRUE;
if (GetWindow(h, GW_OWNER) != NULL) return TRUE; // skip dialogs and splash windows
RECT r;
if (!GetWindowRect(h, &r)) return TRUE;
if ((r.right - r.left) < 320 || (r.bottom - r.top) < 240) return TRUE;
s->found = h;
return FALSE;
}
static void ApplyBorderless(HWND h, int w, int hgt)
{
LONG_PTR st = GetWindowLongPtrA(h, GWL_STYLE);
st &= ~(LONG_PTR)(WS_CAPTION | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_SYSMENU | WS_BORDER | WS_DLGFRAME);
st |= WS_POPUP;
SetWindowLongPtrA(h, GWL_STYLE, st);
LONG_PTR ex = GetWindowLongPtrA(h, GWL_EXSTYLE);
ex &= ~(LONG_PTR)(WS_EX_WINDOWEDGE | WS_EX_CLIENTEDGE | WS_EX_DLGMODALFRAME | WS_EX_STATICEDGE);
SetWindowLongPtrA(h, GWL_EXSTYLE, ex);
SetWindowPos(h, HWND_TOP, 0, 0, w, hgt, SWP_FRAMECHANGED | SWP_SHOWWINDOW | SWP_NOOWNERZORDER);
}
static DWORD WINAPI BorderlessThread(LPVOID)
{
const int w = GetSystemMetrics(SM_CXSCREEN);
const int hgt = GetSystemMetrics(SM_CYSCREEN);
bool logged = false;
// A separate thread, never the frame loop. It keeps watching rather than applying once,
// because the engine creates its window late and can recreate it later.
for (int tick = 0; ; tick++)
{
// Poll often for the first ten seconds: the window is created during startup and half a
// second of it having a frame is visible to the eye. After that this is only a safety net
// for the engine recreating its window on a video mode change.
Sleep(tick < 200 ? 50 : 500);
SBorderlessSearch s;
s.pid = GetCurrentProcessId();
s.found = NULL;
EnumWindows(BorderlessEnumProc, (LPARAM)&s);
if (!s.found) continue;
if (IsIconic(s.found)) continue; // minimised: don't fight Alt-Tab
LONG_PTR st = GetWindowLongPtrA(s.found, GWL_STYLE);
RECT r;
if (!GetWindowRect(s.found, &r)) continue;
const bool needFix = ((st & WS_CAPTION) != 0)
|| (r.left != 0) || (r.top != 0)
|| ((r.right - r.left) != w) || ((r.bottom - r.top) != hgt);
if (!needFix) continue;
ApplyBorderless(s.found, w, hgt);
if (!logged) {
FILE* f = fopen("launcher_diag.txt", "a");
if (f) { fprintf(f, "borderless : applied to window %p at %dx%d\n", (void*)s.found, w, hgt); fclose(f); }
logged = true;
}
}
}
// Diagnostic report for testers.
//
// Startup behaviour varies between systems in ways that cannot be reproduced elsewhere: the
// same build can hit a framerate cap or an Alt-Tab crash on one machine and neither on another.
// Without a record of the hardware and of the mode the launcher started in, such reports cannot
// be acted on. This file makes them concrete.
//
// Only technical information is collected: no user name, no profile paths, no serials,
// nothing about the network.
static void DiagLine(FILE* f, const char* fmt, ...)
{
va_list ap;
va_start(ap, fmt);
vfprintf(f, fmt, ap);
va_end(ap);
fputc('\n', f);
}
// ---------------------------------------------------------------------------------------
// The pointer truncation that makes this engine build fail on some machines and not others.
//
// Retail CrySystem's bucket allocator keeps the head of its free-page list in a global.
// Every read of that global is 64-bit, but the one instruction that writes it is 32-bit:
//
// RVA 0x0A16F1 / 0x0A1715 / 0x0A1A30 mov rXX, [rip+...] 64-bit reads
// RVA 0x0A1A49 mov [rip+...], ebp 32-bit write
//
// So the top half of the pointer is dropped on the way in and read back as zero. While the
// process heap happens to sit below the 4 GB line the dropped half is zero anyway and nothing
// notices. The moment one block lands above that line, the value read back addresses a low
// address that was never mapped. That is the "Failed CMTSafeHeap::m_pBigPool allocation" box
// at startup, and the access violations on unmapped low addresses once a level is loaded.
//
// Where the heap lands is decided by the layout of the address space, which is why the same
// files work on one machine and fail on the next, and why the same machine can differ between
// two runs: driver DLLs, overlays and ASLR all move the boundary.
//
// The instruction is exactly one REX.W prefix short of being correct. Immediately before it:
//
// mov [rsp+40h], rax
// mov rax, [rsp+40h] <- reloads the register just stored, and the next instruction
// (lea rax, [rbp+80000h]) overwrites rax regardless
//
// That reload is dead. Replacing it with padding frees the byte the prefix needs, and since
// the store still ends at the same address its RIP-relative displacement stays correct.
//
// Bin64 comes from the Mod SDK and is byte-identical for everyone, so a fixed offset is safe
// here - but the patch still verifies the exact bytes and declines if they differ.
// ---------------------------------------------------------------------------------------
// Detector for truncated pointers.
//
// A pointer that lost its top half addresses somewhere in the low 4 GB, which in a 64-bit
// process is almost always unmapped. So an access violation on a low address is the signature
// of this bug, and the fault tells us exactly which instruction dereferenced it.
//
// The handler only observes: it writes a record and lets the exception continue to whoever
// would have handled it, so behaviour is unchanged. Used together with -forcehighheap, which
// makes the fault happen on demand rather than by luck, this turns "find the truncations" from
// guesswork into a loop: run, read the address, find who wrote that pointer, fix, run again.
//
// Deliberately avoids the CRT: at fault time the heap may be the very thing that is broken.
#define MAX_FAULT_RECORDS 32
static volatile long g_faultsLogged = 0;
static void AppendTextFile(const char* path, const char* text, unsigned long len)
{
HANDLE h = CreateFileA(path, FILE_APPEND_DATA, FILE_SHARE_READ, NULL,
OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (h == INVALID_HANDLE_VALUE) return;
DWORD written = 0;
SetFilePointer(h, 0, NULL, FILE_END);
WriteFile(h, text, len, &written, NULL);
CloseHandle(h);
}
static void AppendFaultLog(const char* text, unsigned long len)
{
AppendTextFile("launcher_faults.txt", text, len);
}
// Marks a new session in the fault log, and keeps the engine's log from the previous one.
//
// launcher_faults.txt is appended to, so faults from several play sessions pile up in one file
// with nothing to separate them - and the engine rewrites Game.log on every start, which is the
// only place that says which level was loading when a fault happened. Both are needed together
// to make sense of a crash reported hours later.
static void StartFaultSession(const char* cmdLine)
{
SYSTEMTIME st;
GetLocalTime(&st);
// Keep what the previous run left behind: the engine overwrites all of it on start, and
// a crash report is worth nothing once the next launch has erased it. error.log carries the
// engine's own stack trace, error.bmp the frame it died on.
static const char* const kKeep[] = { "Game.log", "error.log", "error.dmp", "error.bmp" };
for (int i = 0; i < 4; i++)
{
if (GetFileAttributesA(kKeep[i]) == INVALID_FILE_ATTRIBUTES) continue;
CreateDirectoryA("launcher_logs", NULL);
const char* dot = strrchr(kKeep[i], '.');
char stem[32];
const size_t n = dot ? (size_t)(dot - kKeep[i]) : strlen(kKeep[i]);
memcpy(stem, kKeep[i], n);
stem[n] = 0;
char kept[MAX_PATH];
sprintf(kept, "launcher_logs%c%s_%04u%02u%02u_%02u%02u%02u%s", 92, stem,
st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond,
dot ? dot : "");
MoveFileA(kKeep[i], kept);
}
char line[512];
int n = sprintf(line,
"%s=== session %04u-%02u-%02u %02u:%02u:%02u args: %s ===%s",
"\n", st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond,
(cmdLine && *cmdLine) ? cmdLine : "(none)", "\n");
AppendFaultLog(line, (unsigned long)n);
}
// Names the module an address belongs to, without pulling in psapi: the allocation base of a
// mapped image is its module handle.
static const char* ModuleAt(ULONG_PTR addr, ULONG_PTR* rvaOut)
{
static char path[MAX_PATH];
MEMORY_BASIC_INFORMATION mbi;
if (!VirtualQuery((LPCVOID)addr, &mbi, sizeof(mbi))) return 0;
if (!mbi.AllocationBase) return 0;
if (!GetModuleFileNameA((HMODULE)mbi.AllocationBase, path, MAX_PATH)) return 0;
*rvaOut = addr - (ULONG_PTR)mbi.AllocationBase;
const char* slash = strrchr(path, 92); // last backslash
return slash ? slash + 1 : path;
}
static bool AddressIsMapped(ULONG_PTR addr)
{
MEMORY_BASIC_INFORMATION mbi;
if (!VirtualQuery((LPCVOID)addr, &mbi, sizeof(mbi))) return false;
return mbi.State == MEM_COMMIT;
}
// Reads one pointer-sized value, returning false instead of faulting on an unmapped page.
static bool SafePeek(const void* at, ULONG_PTR* out)
{
MEMORY_BASIC_INFORMATION mbi;
if (!VirtualQuery(at, &mbi, sizeof(mbi))) return false;
if (mbi.State != MEM_COMMIT) return false;
if (mbi.Protect & (PAGE_NOACCESS | PAGE_GUARD)) return false;
*out = *(const ULONG_PTR*)at;
return true;
}
// Writes a dump that still has the memory in it.
//
// The engine writes its own error.dmp, but it is a thin one: the Downtown crash left a stack
// saying a virtual call went to address zero, and the object it was called on was not in the
// dump at all - so there was no way to see whose object it was or what had happened to it.
//
// MiniDumpWithIndirectlyReferencedMemory adds the memory pointed at by registers and by values
// on the stack, which is exactly the missing piece, without the cost of dumping three gigabytes.
// dbghelp.dll is already loaded by the engine; failing to find it just means no extra dump.
static volatile LONG g_dumpsWritten = 0;
typedef BOOL (WINAPI *PFN_MiniDumpWriteDump)(HANDLE, DWORD, HANDLE, DWORD,
void*, void*, void*);
static void WriteRichDump(EXCEPTION_POINTERS* ep)
{
if (InterlockedIncrement(&g_dumpsWritten) > 2) return; // two is plenty
HMODULE dbg = GetModuleHandleA("dbghelp.dll");
if (!dbg) dbg = LoadLibraryA("dbghelp.dll");
if (!dbg) return;
PFN_MiniDumpWriteDump write = (PFN_MiniDumpWriteDump)GetProcAddress(dbg, "MiniDumpWriteDump");
if (!write) return;
SYSTEMTIME st;
GetLocalTime(&st);
CreateDirectoryA("launcher_logs", NULL);
char path[MAX_PATH];
sprintf(path, "launcher_logs%clauncher_%04u%02u%02u_%02u%02u%02u.dmp", 92,
st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond);
HANDLE h = CreateFileA(path, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL, NULL);
if (h == INVALID_HANDLE_VALUE) return;
// MINIDUMP_EXCEPTION_INFORMATION, declared inline to avoid pulling in dbghelp.h.
struct { DWORD ThreadId; EXCEPTION_POINTERS* Pointers; BOOL ClientPointers; } mei;
mei.ThreadId = GetCurrentThreadId();
mei.Pointers = ep;
mei.ClientPointers = FALSE;
// Deliberately NOT MiniDumpWithFullMemory (0x2): that writes the whole address space, a
// three-gigabyte file a dying process rarely finishes. The first attempt used it by
// mistake and left a zero-byte dump behind.
const DWORD kType = 0x00000004 // WithHandleData
| 0x00000040 // WithIndirectlyReferencedMemory - the piece that matters
| 0x00000800 // WithFullMemoryInfo - the map of what is mapped
| 0x00001000; // WithThreadInfo
// With no exception to describe, the structure must not be passed at all: handing
// MiniDumpWriteDump an exception record whose pointers are null takes the call down
// with it, which is how the self-test produced a zero-byte file twice.
const BOOL ok = write(GetCurrentProcess(), GetCurrentProcessId(), h, kType,
ep ? &mei : NULL, NULL, NULL);
const DWORD err = ok ? 0 : GetLastError();
const DWORD size = GetFileSize(h, NULL);
CloseHandle(h);
char line[MAX_PATH + 96];
int n = sprintf(line, " %s dump: %s (%u bytes, error %u)%s",
ok ? "wrote" : "FAILED to write", path,
(unsigned)size, (unsigned)err, "\n");
AppendFaultLog(line, (unsigned long)n);
}
static LONG CALLBACK TruncationVEH(EXCEPTION_POINTERS* ep)
{
if (!ep || !ep->ExceptionRecord || !ep->ContextRecord) return EXCEPTION_CONTINUE_SEARCH;
if (g_faultsLogged >= MAX_FAULT_RECORDS) return EXCEPTION_CONTINUE_SEARCH;
// Every way the process can die hard, not only the truncation signature.
//
// Narrowing this to access violations on low addresses was right while that was the one
// failure being hunted, but it made every other death invisible: the engine went down with a
// divide by zero inside the allocator and neither log said a word about where. A handler
// that only sees what it already expects is not a detector.
const DWORD code = ep->ExceptionRecord->ExceptionCode;
const char* kind =
code == EXCEPTION_ACCESS_VIOLATION ? "access violation" :
code == EXCEPTION_INT_DIVIDE_BY_ZERO ? "integer divide by zero" :
code == EXCEPTION_ILLEGAL_INSTRUCTION ? "illegal instruction" :
code == EXCEPTION_PRIV_INSTRUCTION ? "privileged instruction" :
code == EXCEPTION_STACK_OVERFLOW ? "stack overflow" :
code == 0xC0000374 ? "heap corruption" :
code == EXCEPTION_INT_OVERFLOW ? "integer overflow" : 0;
if (!kind) return EXCEPTION_CONTINUE_SEARCH;
const bool isAV = (code == EXCEPTION_ACCESS_VIOLATION &&
ep->ExceptionRecord->NumberParameters >= 2);
const ULONG_PTR addr = isAV ? (ULONG_PTR)ep->ExceptionRecord->ExceptionInformation[1] : 0;
const ULONG_PTR op = isAV ? (ULONG_PTR)ep->ExceptionRecord->ExceptionInformation[0] : 0;
InterlockedIncrement(&g_faultsLogged);
const CONTEXT* c = ep->ContextRecord;
const ULONG_PTR regs[16] = {
c->Rax, c->Rcx, c->Rdx, c->Rbx, c->Rsp, c->Rbp, c->Rsi, c->Rdi,
c->R8, c->R9, c->R10, c->R11, c->R12, c->R13, c->R14, c->R15
};
static const char* const names[16] = {
"rax", "rcx", "rdx", "rbx", "rsp", "rbp", "rsi", "rdi",
"r8", "r9", "r10", "r11", "r12", "r13", "r14", "r15"
};
char buf[6144]; // registers, ten stack frames, and a line per register pointer
int n = 0;
ULONG_PTR rva = 0;
const char* mod = ModuleAt((ULONG_PTR)c->Rip, &rva);
n += sprintf(buf + n, "=== %s ===%s", kind, "\n");
n += sprintf(buf + n, " faulting code : %s+0x%08llX%s",
mod ? mod : "(unknown)", (unsigned long long)rva, "\n");
if (isAV)
{
n += sprintf(buf + n, " operation : %s%s",
op == 0 ? "read" : (op == 1 ? "write" : "execute"), "\n");
n += sprintf(buf + n, " address : 0x%016llX (%s)%s", (unsigned long long)addr,
addr >= 0x100000000 ? "high address, mapped or not" :
(addr < 0x10000 ? "null-ish, probably not truncation" : "unmapped low address"),
"\n");
}
// The most useful line: a register whose low half equals the faulting address but whose top
// half is still intact is the original pointer, and names what was truncated on the way in.
for (int i = 0; i < 16 && addr; ++i) {
if ((regs[i] & 0xFFFFFFFF) == (addr & 0xFFFFFFFF) && (regs[i] >> 32) != 0)
n += sprintf(buf + n, " intact copy : %s = 0x%016llX <- pointer before truncation%s",
names[i], (unsigned long long)regs[i], "\n");
}
for (int i = 0; i < 16 && addr; ++i) {
if (regs[i] == addr)
n += sprintf(buf + n, " held in : %s%s", names[i], "\n");
}
n += sprintf(buf + n, " registers :%s", "\n");
for (int i = 0; i < 16; i += 4) {
n += sprintf(buf + n, " %s=%016llX %s=%016llX %s=%016llX %s=%016llX%s",
names[i], (unsigned long long)regs[i],
names[i+1], (unsigned long long)regs[i+1],
names[i+2], (unsigned long long)regs[i+2],
names[i+3], (unsigned long long)regs[i+3], "\n");
}
// Who called in. The faulting instruction is often inside a system library that was
// simply handed something wrong; the frames above it name the code that did the handing.
n += sprintf(buf + n, " callers :%s", "\n");
{
const ULONG_PTR* sp = (const ULONG_PTR*)c->Rsp;
int shown = 0;
for (int i = 0; i < 256 && shown < 10; i++)
{
ULONG_PTR v = 0;
if (!SafePeek(&sp[i], &v)) break;
ULONG_PTR r = 0;
const char* m = ModuleAt(v, &r);
if (!m) continue;
n += sprintf(buf + n, " [rsp+%04X] %s+0x%08llX%s", (unsigned)(i * 8), m,
(unsigned long long)r, "\n");
shown++;
}
}
n += sprintf(buf + n, "%s", "\n");
// What the registers point at. Two crashes in a row came down to an object whose table
// of methods held something that was not a table, and neither the engine's dump nor a
// dump written from inside the fault handler kept that memory - dbghelp faulted trying.
// Sixteen bytes read here, with the same guarded read used everywhere else, answers the
// question directly: an object still alive starts with a pointer into a module.
n += sprintf(buf + n, " what the registers point at:%s", "\n");
for (int i = 0; i < 16; i++)
{
const ULONG_PTR v = regs[i];
if (v < 0x10000 || v >= 0x0000800000000000ull) continue;
ULONG_PTR first = 0;
if (!SafePeek((const void*)v, &first)) continue;
ULONG_PTR rva = 0;
const char* mod = ModuleAt(first, &rva);
n += sprintf(buf + n, " [%s] 0x%016llX -> 0x%016llX %s%s", names[i],
(unsigned long long)v, (unsigned long long)first,
mod ? "" : "(not a module address)", "\n");
if (mod)
n += sprintf(buf + n, " vtable of %s+0x%llX%s", mod,
(unsigned long long)rva, "\n");
}
AppendFaultLog(buf, (unsigned long)n);
// The dbghelp dump is NOT written from here: on the CentralStation crash it faulted
// inside dbghelp and produced a zero-byte file. It stays available under -dumptest.
return EXCEPTION_CONTINUE_SEARCH;
}
// ---------------------------------------------------------------------------------------
// A proxy over the engine's main allocation entry point.
//
// The constructor of the pak heap calls it through a pointer and gives up if the result is
// null (CrySystem RVA 0x0A2253 calls, 0x0A2280 tests, 0x0A2285 raises the fatal error). The
// store and the test are both 64-bit and correct, so the allocator really does return zero -
// the question is what it was asked for at that moment, and this answers it.
//
// The pointer lives in a table that CrySystem fills lazily from RVA 0x0369E0. That function
// takes no arguments (the call at 0x0A2246 sets none), so calling it ourselves first is safe
// and leaves the table populated, after which the entry can be swapped without racing anyone.
//
// This is also the shape of the eventual fix: the same swap, with an allocator of our own on
// the other side instead of a passthrough.
#define CRT_TABLE_INIT_RVA 0x0369E0
#define CRYMALLOC_PTR_RVA 0x6EF6A8
#define BIG_ALLOC_INTEREST (16 * 1024)
typedef void (*CrtTableInitFn)(void);
typedef void* (*CryMallocFn)(size_t size, size_t* allocated);
// Low address space held back so that one allocation has to go above the 4 GB line.
//
// The blocks are remembered rather than simply reserved, because the whole point is to let go
// of them the moment the arena has been allocated - see the note in ProxyCryMalloc.
#define MAX_LOW_BLOCKS 128
static LPVOID g_lowBlocks[MAX_LOW_BLOCKS];
static unsigned g_lowBlockCount = 0;
static bool g_lowHeld = false;
static void ReleaseLowAddressSpace(void)
{
if (!g_lowHeld) return;
g_lowHeld = false;
for (unsigned i = 0; i < g_lowBlockCount; i++)
{
if (g_lowBlocks[i]) VirtualFree(g_lowBlocks[i], 0, MEM_RELEASE);
g_lowBlocks[i] = 0;
}
g_lowBlockCount = 0;
}
// Threshold for -highslab: allocations of at least this many bytes are served from above the
// 4 GB line instead of from the engine's own heap. Zero disables it.
static SIZE_T g_highThreshold = 0;
static unsigned g_highTaken = 0; // large allocations moved above 4 GB
static unsigned g_highMissed = 0; // large allocations that could not be moved
static CryMallocFn g_origCryMalloc = 0;
static volatile long g_allocLogged = 0;
static void* ProxyCryMalloc(size_t size, size_t* allocated)
{
// Note the request before making it. A call that fails by never returning - because the
// allocator raises a fatal error of its own - is otherwise invisible.
if (size >= BIG_ALLOC_INTEREST && g_allocLogged < 200)
{
char ask[96];
int m = sprintf(ask, " ask %9u bytes ...%s", (unsigned)size, "\n");
AppendFaultLog(ask, (unsigned long)m);
}
// No squeeze here. This is the wrong door: the allocator takes its arena through
// CrySystemCrtMalloc, not through CryMalloc - see ProxyCrtMalloc below. Pushing what
// arrives here above the 4 GB line moved memory that had nothing to do with the bug.
void* p = g_origCryMalloc(size, allocated);
// Only the interesting ones: every failure, and the large blocks the pools are made of.
// Small allocations run into the millions and would drown the log.
if ((!p || size >= BIG_ALLOC_INTEREST) && g_allocLogged < 200)
{
InterlockedIncrement(&g_allocLogged);
char line[160];
int n = sprintf(line, " alloc %9u bytes -> 0x%016llX%s%s",
(unsigned)size, (unsigned long long)(ULONG_PTR)p,
p ? ((ULONG_PTR)p >= (ULONG_PTR)0x100000000 ? " ABOVE 4GB" : "") : " FAILED",
"\n");
AppendFaultLog(line, (unsigned long)n);
}
return p;
}
// How much contiguous address space is actually left below the 4 GB line. The engine's pools
// need single blocks of up to 14 MB, so this is the number that decides whether startup
// succeeds, and it is invisible from anywhere else.
static size_t LargestFreeBlockBelow4GB(size_t* totalFreeOut)
{
const ULONG_PTR limit = (ULONG_PTR)0x100000000;
size_t largest = 0, total = 0;
ULONG_PTR a = 0x10000;
MEMORY_BASIC_INFORMATION mbi;
while (a < limit && VirtualQuery((LPCVOID)a, &mbi, sizeof(mbi)) == sizeof(mbi))
{
ULONG_PTR next = (ULONG_PTR)mbi.BaseAddress + mbi.RegionSize;
if (mbi.State == MEM_FREE)
{
SIZE_T sz = mbi.RegionSize;
if ((ULONG_PTR)mbi.BaseAddress + sz > limit)
sz = (SIZE_T)(limit - (ULONG_PTR)mbi.BaseAddress);
total += sz;
if (sz > largest) largest = sz;
}
if (next <= a) break;
a = next;
}
if (totalFreeOut) *totalFreeOut = total;
return largest;
}
static bool g_crtTableReady = false;
// Filling the table has to happen exactly once. Making it conditional on the slot being
// empty leaves the table unfilled and the game dies fifteen seconds in; calling it again
// after a proxy is installed puts the original pointer back and quietly removes the proxy.
static void EnsureCrtTable(unsigned char* base)
{
if (g_crtTableReady) return;
((CrtTableInitFn)(base + CRT_TABLE_INIT_RVA))();
g_crtTableReady = true;
}
static const char* InstallAllocProxy(void)
{
HMODULE cs = GetModuleHandleA("CrySystem.dll");
if (!cs) return "CrySystem not loaded";
unsigned char* base = (unsigned char*)cs;
CryMallocFn* slot = (CryMallocFn*)(base + CRYMALLOC_PTR_RVA);
if (*slot == ProxyCryMalloc) return "already installed";
// Populate the table, so the entry read below is the real one.
EnsureCrtTable(base);
if (!*slot) return "allocator pointer still empty";
g_origCryMalloc = *slot;
DWORD oldProt = 0;
if (!VirtualProtect(slot, sizeof(*slot), PAGE_READWRITE, &oldProt)) return "VirtualProtect failed";
*slot = ProxyCryMalloc;
VirtualProtect(slot, sizeof(*slot), oldProt, &oldProt);
return "installed";
}
// The arena the bucket allocator runs on never came from CryMalloc.
//
// CrySystem fills a small table of allocation entry points from its own exports (the function
// at RVA 0x0369E0): CryMalloc lands at 0x6EF6A8, CryRealloc at 0x6EF6B8, CryGetMemSize at
// 0x6EF6C0, and CrySystemCrtMalloc at 0x6EF6C8. That last one is a jump straight to
// MSVCR90!malloc, and it is what the bucket allocator calls to build its 0x80000-byte arena
// (the call at 0xA1922). Requests over 0x200 bytes take the same route (0xA12CE); smaller ones
// are served out of the arena, which is the whole point of the thing.
//
// So proxying CryMalloc - which is what -highslab did until now - never saw an arena being
// created at all. The 512 KB blocks it caught and pushed above the 4 GB line belonged to
// something else entirely, the allocator's own memory stayed low the whole time, and the
// pointer corrections had nothing to correct. That is the answer to why -enginefix made no
// difference: the bug it fixes was never given a chance to fire.
#define CRT_MALLOC_PTR_RVA 0x6EF6C8
#define BUCKET_ARENA_BYTES 0x80000
typedef void* (*CrtMallocFn)(size_t size);
static CrtMallocFn g_origCrtMalloc = 0;
static bool g_arenaExact = true; // match the arena size exactly, not a threshold
static unsigned g_arenaSeen = 0; // arena-sized requests observed
// Serving every arena from above the 4 GB line, instead of squeezing one of them up there.
//
// The squeeze only works once. It lets go of the low address space the moment the first arena is
// taken, so arena number two and the hundred after it come straight back down - watching the
// globals while the game runs shows exactly that: one arena high, then 0x4BD16BE0 for the next
// minute. A condition that holds for a fraction of a second proves nothing either way.
//
// So the arena is served directly instead. The allocator gets its 0x80000-byte block like any
// other, only from a region reserved above the line; CrySystemCrtFree and CrySystemCrtSize are
// hooked alongside it so the block behaves like a CRT one for its whole life. That was what went
// wrong the last time this was tried: memory handed over without the rest of its life accounted
// for. Nothing low is taken away, so the renderer is untouched - which is what made
// -forcehighheap useless as a test.
#define CRT_FREE_PTR_RVA 0x6EF6D0
#define CRY_FREE_PTR_RVA 0x6EF6C0
#define CRT_SIZE_PTR_RVA 0x6EF6D8
#define ARENA_POOL_SLOTS 2048
// Arenas are spaced two slots apart, so no arena ends exactly where the next one begins.
// The allocator decides which arena a pointer belongs to with `base <= p <= end`, end
// inclusive, and real malloc never hands out blocks that touch - a pool that does would be
// asking a question the engine was never written to answer.
#define ARENA_SLOT_STRIDE (BUCKET_ARENA_BYTES * 2)
// The one call site that builds an arena: CrySystem RVA 0xA1922, and the instruction is
// "call qword ptr [rip+disp32]" - six bytes, so the return address is 0xA1928. Requests
// of exactly the arena size also arrive from 0xA12CE, which is the ordinary path for any
// block over 0x200 bytes; serving those from the pool as well would move memory that has
// nothing to do with the experiment.
#define ARENA_CALL_SITE_RVA 0x0A1928
extern "C" void* _ReturnAddress(void);
#pragma intrinsic(_ReturnAddress)
typedef void (*CrtFreeFn)(void* p);
typedef size_t (*CrtSizeFn)(void* p);
static CrtFreeFn g_origCrtFree = 0;
static CrtFreeFn g_origCryFree = 0;
static unsigned g_arenaFreedElsewhere = 0; // arenas released through CryFree, not CrtFree
static CrtSizeFn g_origCrtSize = 0;
static bool g_arenaHigh = false;
static const char* g_highArenaMsg = "off"; // reported in the diagnostic file
static unsigned char* g_arenaPool = 0; // reserved, ARENA_POOL_SLOTS * 0x80000
static volatile LONG g_arenaSlot[ARENA_POOL_SLOTS]; // 0 free, 1 in use
static volatile LONG g_arenaLive = 0; // arenas served and not yet freed
static volatile LONG g_arenaNext = 0; // next slot to hand out, never rewound
static unsigned g_arenaPeak = 0;
static unsigned g_arenaFull = 0; // times the pool had nothing left
static unsigned g_arenaFreed = 0;
static unsigned g_arenaSkipped = 0; // arena-sized requests from anywhere else
static const void* g_arenaCallSite = 0;
// Slots are handed out in order and never reused, even after the arena is freed.
//
// Reusing one deadlocked the engine: it keeps arenas on a chain it walks by address
// ([arena+0x2010]), and handing back an address it already has on that chain can close the
// chain into a ring. The walk then never ends - the thread stops answering, the engine's own
// watchdog reports "Runaway thread", and the process is killed sixty seconds in. Addresses are
// cheap here: the pool is reserved address space, and a session used 186 of 2048.
static void* ArenaAlloc(void)
{
if (!g_arenaPool) return 0;
const LONG slot = InterlockedIncrement(&g_arenaNext) - 1;
if (slot < 0 || slot >= (LONG)ARENA_POOL_SLOTS)
{
g_arenaFull++;
return 0;
}
void* p = VirtualAlloc(g_arenaPool + (size_t)slot * ARENA_SLOT_STRIDE,
BUCKET_ARENA_BYTES, MEM_COMMIT, PAGE_READWRITE);
if (!p)
{
g_arenaFull++;
return 0;
}
InterlockedExchange(&g_arenaSlot[slot], 1);
const LONG live = InterlockedIncrement(&g_arenaLive);
if ((unsigned)live > g_arenaPeak) g_arenaPeak = (unsigned)live;
return p;
}
static bool ArenaOwns(void* p, unsigned* slotOut)
{
if (!g_arenaPool || !p) return false;
const ULONG_PTR a = (ULONG_PTR)p;
const ULONG_PTR lo = (ULONG_PTR)g_arenaPool;
const ULONG_PTR hi = lo + (ULONG_PTR)ARENA_POOL_SLOTS * ARENA_SLOT_STRIDE;
if (a < lo || a >= hi) return false;
if (((a - lo) % ARENA_SLOT_STRIDE) != 0) return false; // inside an arena, not its start
*slotOut = (unsigned)((a - lo) / ARENA_SLOT_STRIDE);
return true;
}
static void ProxyCrtFree(void* p)
{
unsigned slot = 0;
if (ArenaOwns(p, &slot))
{
// The pages stay committed on purpose. Decommitting them turns a stale pointer into
// an access violation, and the allocator is not the only one holding pointers into an
// arena - the renderer reads through them too. Real malloc does not unmap freed blocks
// either, so keeping them mapped is the behaviour being imitated, not a workaround.
InterlockedExchange(&g_arenaSlot[slot], 2); // retired, not reusable
InterlockedDecrement(&g_arenaLive);
g_arenaFreed++;
return;
}
g_origCrtFree(p);
}
// CryFree, the entry point the rest of the engine uses. An arena should never arrive here,
// but if one does, it must not be handed to the real CRT - that block was never its.
static void ProxyCryFree(void* p)
{
unsigned slot = 0;
if (ArenaOwns(p, &slot))
{
g_arenaFreedElsewhere++;
InterlockedExchange(&g_arenaSlot[slot], 2); // retired, not reusable
InterlockedDecrement(&g_arenaLive);
return;
}
g_origCryFree(p);
}
static size_t ProxyCrtSize(void* p)
{
unsigned slot = 0;
if (ArenaOwns(p, &slot)) return BUCKET_ARENA_BYTES;
return g_origCrtSize(p);
}
static void* ProxyCrtMalloc(size_t size)
{
if (size == BUCKET_ARENA_BYTES)
{
g_arenaSeen++;
if (g_arenaHigh && _ReturnAddress() != g_arenaCallSite)
{
// Report the first one. An off-by-one in the return address silently turns every
// arena away, and the run then looks exactly like a run with the flag switched off.
if (++g_arenaSkipped == 1)
{
char sk[160];
int k = sprintf(sk, " arena-sized request from 0x%016llX, expected 0x%016llX\n",
(unsigned long long)(ULONG_PTR)_ReturnAddress(),
(unsigned long long)(ULONG_PTR)g_arenaCallSite);
AppendFaultLog(sk, (unsigned long)k);
}
}
else if (g_arenaHigh)
{
void* h = ArenaAlloc();
if (h)
{
if (g_arenaSeen <= 3 || (g_arenaSeen % 64) == 0)
{
char hl[160];