Skip to content

Commit 7c0f795

Browse files
committed
English fix
1 parent 12def3c commit 7c0f795

141 files changed

Lines changed: 11232 additions & 10944 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.gitignore‎

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,27 @@
1-
# --- Oyunun kendisi ve ondan tureyen her sey: ASLA repoya girmez ---
2-
# Oyun verisi + SPEED2.EXE (EA'nin telifli icerigi)
1+
# --- The game itself and everything derived from it: NEVER goes into the repo ---
2+
# Game data + SPEED2.EXE (EA's copyrighted content)
33
/Need for Speed Underground 2*/
4-
# Lift edilmis C kaynagi: SPEED2.EXE'nin makine kodunun cevirisi (turev eser).
5-
# tools/reloc2.py + tools/genall.py + tools/argcount.py ile birebir yeniden uretilir.
4+
# Lifted C source: a translation of SPEED2.EXE's machine code (derivative work).
5+
# Regenerated exactly with tools/reloc2.py + tools/genall.py + tools/argcount.py.
66
/gen/
7-
# Derlenmis ikililer, .app paketleri, dagitim zip'i, gomulu oyun verisi
7+
# Built binaries, .app bundles, distribution zip, embedded game data
88
/out/
99

10-
# --- Kisisel / oturum kayitlari ---
10+
# --- Personal / session recordings ---
1111
/session.jsonl
1212
/Claude outputs/
13-
# Ham oturum ekran goruntuleri (secme/ ve rapor/ haric)
14-
/sunum_ss/*
15-
!/sunum_ss/secme/
16-
!/sunum_ss/rapor/
13+
# Raw session screenshots (except selected/ and report/)
14+
/screenshots/*
15+
!/screenshots/selected/
16+
!/screenshots/report/
1717

18-
# --- Ortam / gecici ---
18+
# --- Environment / temporary ---
1919
/.venv/
2020
__pycache__/
2121
*.pyc
2222
.DS_Store
2323
*.o
2424
*.log
2525

26-
# Eski surum arsivi (gen/ ve out/ turevleri; bkz. arsiv/v1/OKUBENI.txt)
26+
# Old version archive (gen/ and out/ derivatives; see arsiv/v1/OKUBENI.txt)
2727
/arsiv/

‎D3DX_HLE.md‎

Lines changed: 437 additions & 424 deletions
Large diffs are not rendered by default.

‎GRAFIK_ARASTIRMA.md‎

Lines changed: 0 additions & 686 deletions
This file was deleted.

‎GRAPHICS_RESEARCH.md‎

Lines changed: 703 additions & 0 deletions
Large diffs are not rendered by default.

‎LIFTER_EFFICIENCY.md‎

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
# Efficiency of the code the lifter generates
2+
3+
Goal: **measure** the cost of the code the x86 → C static translation produces,
4+
find its source **at the instruction level**, and improve the lifter. Every step
5+
is generated in a separate directory and can be compared with `diff`; the step
6+
promoted to the main version becomes `gen/all`, and the previous version is moved
7+
under `arsiv/vN/` (version number: `VERSION` at the repository root).
8+
9+
**Versions:** v1 = baseline (archive: `arsiv/v1/`). **v2 = opt6, the main version
10+
since 2026-10-18** (`gen/all`, `out/nfsu2_boot`, `out/nfsu2_hd_boot`; generated by
11+
`tools/gen_v2.sh`).
12+
13+
The table below lists the steps up to that date. All the directories are now under
14+
`arsiv/v1/gen/` and `arsiv/v1/out/`; opt6 = today's `gen/all`.
15+
16+
| directory | binary | contents |
17+
|---|---|---|
18+
| `gen/all` (v1) | `out/nfsu2_boot` | baseline (as generated at the time) |
19+
| `gen/prof` | `out/nfsu2_prof_boot` | baseline + every x86 instruction as a comment (`COMMENTS=1`), `-gline-tables-only` — profiling only |
20+
| `gen/opt` | `out/nfsu2_opt_boot` | + FLAGABI |
21+
| `gen/opt2` | `out/nfsu2_opt2_boot` | + FLAGABI + X87OPT |
22+
| `gen/opt3` | `out/nfsu2_opt3_boot` | opt2 + INLINE=50 (negative result, for comparison) |
23+
| `gen/opt4` | `out/nfsu2_opt4_boot` | opt2 + RETEIP=0 |
24+
| `gen/opt5` | `out/nfsu2_opt5_boot` | opt4 + FCMPBL=1 (branchless fcompare) |
25+
| `gen/opt6` → **v2** | `out/nfsu2_opt6_boot` | opt4 + MEMARG=1, compiled with `-fno-strict-aliasing` |
26+
| `gen/cov` | `out/nfsu2_cov_boot` | opt2 + call counter (`COVERAGE=1`) — measurement only |
27+
| (same sources) | `out/nfsu2_{nsa,opt4nsa,opt5nsa}_boot` | `gen/all`/opt4/opt5 with `-fno-strict-aliasing` (the runtime too: `out/obj_rtnsa`) |
28+
29+
Regeneration (from the repository root; the `OVERRIDE` list is the same as in
30+
`PROJECT.md`):
31+
32+
```bash
33+
./.venv/bin/python tools/flagabi.py <SPEED2.EXE> out/flagabi.json # ~30 s
34+
OVERRIDE=... GENOUT=gen/opt2 FLAGABI=out/flagabi.json X87OPT=1 \
35+
./.venv/bin/python tools/genall.py <SPEED2.EXE> 128
36+
# gen/opt2 -> out/obj_opt2 (same flags as build.sh), runtime objects from out/obj
37+
```
38+
39+
The switches are off by default: `genall.py` without switches produces **v1**. To
40+
produce v2 use `tools/gen_v2.sh` (four switches + the override list; reproduces
41+
`gen/all` exactly).
42+
43+
## Measurement method
44+
45+
- **Metric:** the **ARM instructions and cycles executed** by the game thread per
46+
frame (`thread_selfcounts`, the `CPUWIN` report). Much more stable than time:
47+
two runs of the same binary differ by 0.7% (instructions) / 3.6% (time).
48+
- **Scenario:** automatic Quick Race (`AUTOSEQ`), `PACE60=1 CPUWIN=6200,8800`,
49+
3024x1964, 2 alternating runs per version.
50+
- **Correctness:** the differential fuzzer (`tools/fuzzisa.py`, real x86 =
51+
Rosetta; 3969 cases), image comparison at frame 8900 (compared against the
52+
difference between two runs of the same binary).
53+
- **Instruction-level profile:** `PERFLOG` samples (1 ms, guest thread) →
54+
`atos -i` → `gen/prof` line → the preceding `/* address instruction */` comment
55+
(`tools/lift_profile.py`).
56+
57+
## Starting point: where the time goes (race, inside lifted code)
58+
59+
| kind | share |
60+
|---|---|
61+
| line 0 + function entry (compiler: register saves, memory traffic of `c->` fields) | 20.2% |
62+
| `mov` memory | 14.9% |
63+
| `push`/`pop` | 11.2% |
64+
| `cmp`/`test` (flags) | 10.3% |
65+
| x87 (load/store 8.7, arithmetic 8.5, compare 5.8) | 23.0% |
66+
| `call` + `ret` | 6.8% |
67+
68+
Targets of memory accesses in the hot lifted functions: `c->esp` 10.1%, flags
69+
~12% (`zf` 4.4, `sf` 3.3, `af` 2.5, `cf` 1.9), general registers ~10%.
70+
71+
## 1. FLAGABI — cross-function flag liveness
72+
73+
**Problem:** the flag liveness analysis counted "all flags read" at `call` and
74+
"all flags live" at `ret`. All six flags of the last flag-producing instruction
75+
before a call (very often `add esp,N`, `xor eax,eax; ret`) were computed and
76+
written to memory.
77+
78+
**Change** (`tools/lift.py`: `abi_cfg`, `abi_liveness`, `_flag_defuse_abi`;
79+
`tools/flagabi.py`): MSVC code doesn't read flags returned from a call, and the
80+
callee doesn't read the caller's flags on entry. This assumption is **proven over
81+
the whole program**: at a fixed point, each function's entry-live flags and the
82+
call sites "where flags are read after the call" are computed; conservative
83+
behavior is kept only for those. A flag left undefined by x86 counts as defined
84+
(otherwise `and/test` never kill AF, and AF spread falsely everywhere: 3171 false
85+
"flag-returning" functions in the first attempt).
86+
87+
**Audit result:** across 23274 functions, 14 "flag-returning", 210 entry-live, 12
88+
sites reading flags after an indirect call; every indirect site examined is either
89+
a data byte decoded as code (`retf 0x50ff`, `int3`, a random `adc`) or a
90+
conservative outer edge.
91+
92+
## 2. X87OPT — x87 register allocation within a block
93+
94+
**Problem:** `ST(n) = c->st[(c->top+n)&7]` is indexed at run time; clang can
95+
never put x87 values in registers, and every operation is a `top` read + address
96+
+ load/store.
97+
98+
**Change** (`tools/lift.py`: `X87Region`): within a basic block the stack depth
99+
is known at compile time. At the start of the region `_T = c->top`; `ST(n)` →
100+
`_x[off+n+8]` (constant index → ARM FP register); `FPUSH/FPOP` only change the
101+
static `off`. A slot read before being written is loaded once; at the end of the
102+
region (branch, call, ret, an instruction touching `c->st/c->top`, block end) only
103+
the modified slots still on the stack are written back, and `c->top` is updated
104+
once. `fnstsw` gets the static top via `fsw_get_top`.
105+
106+
## Result
107+
108+
| version | instr/frame | cycles/frame | CPU ms/frame | binary |
109+
|---|---|---|---|---|
110+
| baseline | 43.22 M | 16.50 M | 5.97 | 46.1 MB |
111+
| + FLAGABI | 40.24 M (−6.9%) | 15.76 M (−4.5%) | 5.86 | 40.3 MB |
112+
| + X87OPT | **38.51 M (−10.9%)** | **14.83 M (−10.1%)** | **5.44** | **37.3 MB (−19%)** |
113+
114+
Fuzzer: 3969/3969 matched (X87OPT on and off). The image difference (mean 0.21)
115+
is at the same level as the difference between two runs of the same binary (0.16).
116+
117+
## 3. Call model
118+
119+
**Measurement** (`gen/cov`, `COVREPORT` + `COVRESET_AT=6200`, 2800 frames): in a
120+
race **~104 thousand** lifted function calls per frame (~370 ARM instructions per
121+
call on average). 37% of the calls go to ≤50-instruction leaf functions that make
122+
no further calls (~38 thousand per frame); the most called is `__ftol2`
123+
(`sub_0075bed4`, 39 instructions) at 10.5 thousand/frame. The call cost in the
124+
machine code of a small function: 3 `stp` + 3 `ldp` at entry/exit, reloading
125+
`c->esp` and `g_mem` after every call, writing the return address to `c->eip` at
126+
every `ret`.
127+
128+
**3a. INLINE (negative).** `genall.py INLINE=50`: the bodies of 4703 leaf
129+
functions are put into every shard as a
130+
`static inline __attribute__((always_inline)) inl_X` copy, and direct calls are
131+
redirected to it. Result (2 runs): instructions 37.82 → 38.20 M (+1%), cycles
132+
14.44 → 14.93 M (+3.4%), binary 37.3 → 43.6 MB. The fixed cost per call is smaller
133+
than estimated; the inlined bodies increase register pressure in the already large
134+
goto functions. ThinLTO (−1.8%, within noise) had given the same result.
135+
136+
**3b. RETEIP.** `ret` → `c->esp += 4(+n); return;` instead of
137+
`POP32(c->eip); return;`. The only place reading `c->eip` after a lifted call was
138+
`genexec.c`; it now uses the return address it pushed (`next`) (the model already
139+
ignored modified return addresses). Result (opt2 → opt4, 2 runs): instructions
140+
37.83 → 37.59 M (−0.7%), cycles 13.95 → 13.58 M (**−2.7%**), image the same.
141+
142+
## 4. Strict aliasing undefined behavior
143+
144+
**Symptom.** opt5 (`FCMPBL=1`, `fcompare` → branchless `fcompare_bl`) noticeably
145+
changed the race frame (Y 57.3 → 59.5, the same in two runs). The two functions
146+
produce the same bits for every pair of `double`s (exhaustive test), and the
147+
generated C differs only in the function name.
148+
149+
**Narrowing down** (no guessing, every step measured):
150+
1. bisecting shards (mixing opt4/opt5 objects and checking frame 8900 brightness)
151+
→ `shard_0058`;
152+
2. bisecting functions (`fcompare_bl` only in selected functions) →
153+
`sub_005f0a40` (a quadtree search: compares the point against box midpoints and
154+
descends into a child);
155+
3. a function-level differential harness: runs the two builds side by side with
156+
random trees/points/boxes (±0, NaN, equality frequent) and compares the final
157+
state. opt5 `-O3` diverges **from its own `-O0`** (7854 in 100 thousand), opt4
158+
doesn't; the difference is the sign of the midpoint on the stack (−0/+0);
159+
4. `-mllvm -opt-bisect-limit` → the breaking pass is **GVNPass** (pass 262 of 386);
160+
5. IR: an `i32` is written to the same guest stack address and a `float` is read,
161+
with different TBAA tags.
162+
163+
**Root cause.** Lifted code accesses guest memory at the same address with
164+
different types (writing `MR32` and reading `MRF32`, `MR16`/`MR32` overlap). In C
165+
this is a strict aliasing violation; the compiler may assume that accesses of
166+
different types don't overlap and move the load ahead of the store. The build
167+
flags **never had** `-fno-strict-aliasing` — including the baseline `gen/all` and
168+
the runtime. With the flag opt5 = opt4 = baseline (image), difference 0.
169+
`fcompare_bl` is correct; the UB only became visible when the shape of the code
170+
changed.
171+
172+
**Verification and cost** (against the Windows reference, frames 1300–8900, port
173+
`NFSRES=1280x960 VCLOCK=fixed`): with the flag, baseline/opt4/opt5 give the same
174+
image; no visible effect of this UB was measured in the baseline build. Raw cost
175+
(mean of 2 runs):
176+
177+
| | instr/frame | cycles/frame |
178+
|---|---|---|
179+
| baseline | 42.61 M | 14.34 M |
180+
| baseline + `-fno-strict-aliasing` | 45.24 M (+6.2%) | 15.14 M (+5.6%) |
181+
| opt4 | 38.54 M | 12.81 M |
182+
| opt4 + `-fno-strict-aliasing` | 39.61 M (+2.8%) | 12.95 M (+1.1%) |
183+
184+
Source of the cost (disassembly): without type information the value of the
185+
global `g_mem` is re-read after every guest write (`sub_005f0a40`: 2 → 10
186+
`ldr x, [&g_mem]`).
187+
188+
## 5. MEMARG — the guest memory base as a parameter
189+
190+
**Observation.** With or without the flag, CPU fields like `c->eax` are re-read
191+
after every write to guest memory: even though `c` is `restrict`, `g_mem` is a
192+
pointer read from memory, and since `c` is passed to calls it has "escaped"; LLVM
193+
can't prove that a guest write doesn't touch `*c`.
194+
195+
**Change** (`tools/genall.py`, `MEMARG=1`, a post-generation transform): every
196+
function becomes `void sub_X_m(CPU *restrict c, uint8_t *restrict _mb)`; the shard
197+
has `#define g_mem _mb`; lifted code calls each other with `sub_Y_m(c, _mb)`. The
198+
externally visible `void sub_X(CPU*)` stays as a thin wrapper (fntab, indirect
199+
calls, runtime → lifted calls and overrides don't change; the `_m` bridge for
200+
overrides and missing functions is in `stubs.c`). Two separate `noalias`
201+
parameters never alias in LLVM, with certainty — no escape analysis needed. On
202+
ARM64 `_mb` arrives in `x1`.
203+
204+
In a single function (`sub_005f0a40`, `-fno-strict-aliasing`): CPU field reads
205+
25 → 4, guest reads 18 → 8; an int write followed by a float read is now forwarded
206+
directly (`str w12` → `fmov s0, w12`).
207+
208+
**Result** (opt4 + `-fno-strict-aliasing` ↔ opt6, alternating; instructions 2+2,
209+
cycles excluding the noisy p4 run (IPC 2.72)): instructions 39.33 → **35.50 M
210+
(−9.7%)**, cycles 12.94 → **11.51 M (−11%)**, binary 38.1 → 34.8 MB. Image: all 16
211+
frames under Windows conditions identical to opt4+flag. MEMARG doesn't just win
212+
back the cost of the correctness fix, it goes beyond it.
213+
214+
## Total (with a correct build)
215+
216+
| version | instr/frame | cycles/frame | binary |
217+
|---|---|---|---|
218+
| baseline (`gen/all`, strict aliasing UB present) | 42.61 M | 14.34 M | 46.1 MB |
219+
| opt6 (FLAGABI + X87OPT + RETEIP + MEMARG, `-fno-strict-aliasing`) | **35.50 M (−16.7%)** | **11.51 M (−19.7%)** | **34.8 MB (−25%)** |
220+
221+
## 6. v2: opt6 as the main version (2026-10-18)
222+
223+
opt6 was first played as a separate build with the current runtime. Once the user
224+
approved it, it became the main version (v2): `gen/opt6` → `gen/all`, v1 was moved
225+
under `arsiv/v1/`.
226+
227+
```bash
228+
tools/gen_v2.sh <SPEED2.EXE> # gen/all (131 files, 0 differences from gen/opt6)
229+
./build.sh && ./link.sh # everything with -fno-strict-aliasing
230+
./pack.sh --embed [--hd] # out/personal/NFSU2[ HD].app, version "2.0"
231+
```
232+
233+
- The `tools/gen_v2.sh` recipe reproduced `gen/opt6` exactly; the 130 objects
234+
compiled with `build.sh` are byte-for-byte identical to the opt6 objects.
235+
`impargs.c` is the same as in v1.
236+
- Tests on v2: 439/439 ISA, 15/15 function verification.
237+
238+
**Verification, image** (Windows NFSRef conditions, 16 frames): difference from the
239+
Windows reference in the menu 0.18–0.55. This is at the same level as the v1
240+
build's 0.18–0.70, and the difference comes from the animation phase. Sharpness
241+
and edge profile identical. HD v2 vs HD v1: 0.07–0.64. Command buffer errors 0,
242+
unresolved indirect calls 0.
243+
244+
**Verification, CPU** (same runtime, `PACE60 CPUWIN=6200,8800`, 3024x1964,
245+
alternating 3+3 runs):
246+
247+
| | CPU ms/frame | instr/frame | cycles/frame |
248+
|---|---|---|---|
249+
| v1 | 5.71 | 41.99 M | 15.49 M |
250+
| v2 (opt6) | **4.44 (−22.3%)** | **34.17 M (−18.6%)** | **11.19 M (−27.7%)** |
251+
252+
Since the game is at its 120 fps limit, the gain doesn't show in fps. Where it
253+
shows is the time the game thread spends asleep, and watts (see
254+
`tools/session.sh`, powermetrics).
255+
256+
## Next candidates
257+
258+
- **Call model:** `c->esp` and the general registers are written to memory and
259+
read back at every call (~20% of memory accesses in hot code).
260+
- **Fusing `fcom → fnstsw → test → jcc`:** reducing it to a single C comparison.
261+
- **Extending the x87 region across blocks** (merge points with consistent depth).

0 commit comments

Comments
 (0)