Skip to content

Add x87 FPU and MXCSR support to Context32 (MidHook, x86-32). - #130

Open
angelfor3v3r wants to merge 8 commits into
cursey:mainfrom
angelfor3v3r:fpu-2
Open

angelfor3v3r wants to merge 8 commits into
cursey:mainfrom
angelfor3v3r:fpu-2

Conversation

@angelfor3v3r

@angelfor3v3r angelfor3v3r commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator

Capture full x87 FPU state via FNSAVE (108 bytes: env + ST0–ST7 in logical stack order) and MXCSR via stmxcsr.
FRSTOR/ldmxcsr replay the captured image verbatim on callback return, so writes to ctx.st0..st7 or ctx.mxcsr take
effect for the hooked code without disturbing other slots or the FPU environment.

FNSAVE also resets the live FPU to FINIT defaults — callback math runs there, not in the program's env; the program's
captured env is restored on return.

API (include/safetyhook/context.hpp, x86-32 only)

  • Fpu — 10-byte 80-bit extended register slot, with:
    • as_f32() / as_f64() — read via FPU fld/fstp (MSVC) or through 80-bit long double (GCC/Clang, LDBL_MANT_DIG ==
      64).
    • set_f32(float) / set_f64(double) — write from float/double.
    • as_f80() / set_f80(long double) — lossless 10-byte memcpy (GCC/Clang only).
  • FpuF32 / FpuF64 / FpuF80 — mutable proxy types with operator= and compound-assignment (+=, -=, *=, /=), modeled on
    std::atomic_ref. Enables ctx.st0.f32() /= f;.
  • FpuEnv — 28-byte packed FNSAVE environment (FCW, FSW, FTW, FOP, FIP, FDP). Stored opaquely; top() reflects the
    captured value and is not re-derived after st_pop/st_push.
  • Context32 — now exposes fpu_env, st0..st7 (named slots), mxcsr, plus the existing XMM/GP regs. static_assert pins
    layout offsets the asm encodes.
  • st_pop() / st_push_f32() / st_push_f64() — rotate slot bytes (cf. fstp/fld). Documented caveat: fpu_env (TOP, FTW)
    is NOT re-derived; the values the hooked code sees are correct, but reading fpu_env.fsw/.ftw afterward reflects the
    pre-rotation state.

Internals

  • src/mid_hook.x86_32.asm — extended the trampoline to save/restore FNSAVE image + MXCSR around the destination
    callback.
  • src/mid_hook.cpp — regenerated the x86-32 asm_data byte array.
  • src/context.cpp (new) — f32/f64 conversions via two __cdecl converter stubs (24 bytes total: fpu_to_double,
    double_to_fpu); f32 routes through f64. MSVC ABI pins calling convention with SAFETYHOOK_CCALL. Stubs are allocated
    RW, copied, then vm_protect-ed to RX (W^X). GCC/Clang path uses 80-bit long double memcpy, no stubs. Functions lazily
    allocated once via std::call_once, freed via vm_free.
  • module/safetyhook.cppm — exports Fpu, FpuEnv, FpuF32, FpuF64, FpuF80 (the last under LDBL_MANT_DIG == 64).

Tests (test/mid_hook.cpp, x86-32 only)

  • ReadAndWriteAllStRegisters — round-trip ST0–ST7 via as_f32/as_f64 and set_f32.
  • ReadAndWriteMxcsr — flip MXCSR.RC and observe effect via cvtss2si(3.5f): truncate→3, round-to-nearest→4.
  • StPopAndPushHookMutatesLiveStack — mirrors ThirteenAG's issue 32-bit Context can't interact with the FPU registers in 32-bit apps #81 fmul/fmulp scenario: callback pops the top then
    pushes 42; original fstp via trampoline observes 42.
  • StProxyArithmeticOps — exercises ctx.st0.f32() /= f; style compound assignment.
  • StF80LosslessRoundTrip — as_f80/set_f80 preserve all 10 bytes (GCC/Clang).

Refs: #81

   Capture full x87 FPU state via FNSAVE (108 bytes: env + ST0-ST7 in
   logical stack order) and MXCSR via stmxcsr; FRSTOR/ldmxcsr replay the
   captured image verbatim on callback return, so writes to ctx.st[n] or
   ctx.mxcsr take effect for the hooked code without disturbing other
   slots or the FPU environment.

   API:

     - Fpu (10-byte 80-bit extended register) with as_f32/as_f64/set_f32/
       set_f64. Conversions run through native FPU fld/fstp stubs (no
       inline asm; MSVC ABI long double == 8 bytes gives no C++ way in).
     - FpuEnv (28-byte packed FNSAVE environment: FCW, FSW, FTW, FOP,
       FIP, FDP). Stored opaquely; top() accessor is informational and
       may be stale on microarchitectures that reset the env as part of
       FNSAVE.
     - Context32::st_pop / st_push_f32 / st_push_f64 rotate slot bytes
       in the buffer (cf. fstp/fld). Documented caveat: FpuEnv fields
       (TOP, FTW) are NOT re-derived after rotation -- the bytes the
       hooked code observes through FPU ops are correct, but reading
       fpu_env.fsw / fpu_env.ftw afterward shows the pre-rotation state.

   Internals:

     - src/mid_hook.x86_32.asm: extended to save/restore FNSAVE image +
       MXCSR around the destination callback. Destination reloc offset
       updated from 0x59 to 0x65 (the new call site).
     - src/mid_hook.cpp: regenerated 214-byte x86-32 asm_data array and
       fixed the destination relocation to 0x65.
     - src/context.cpp (new): four __cdecl asm converter stubs (48 bytes
       total: fpu_to_float, float_to_fpu, fpu_to_double, double_to_fpu),
       lazily vm_allocate'd as RWX and called through function pointers.

   Tests (test/mid_hook.cpp, x86-32 only):

     - ReadAndWriteAllStRegisters: round-trip ST0-ST7 via as_f32/as_f64
       and set_f32, verified by the function's fstp sequence.
     - ReadAndWriteMxcsr: read/flip MXCSR.RC and observe the effect via
       cvtss2si(3.5f): truncate->3, round-to-nearest->4.
     - StPopAndPushHookMutatesLiveStack: mirrors ThirteenAG's issue cursey#81
       fmul/fmulp scenario -- callback pops the top then pushes 42; the
       original fstp runs via the trampoline and observes 42.

   All 21 tests pass (clang 21 targeting i686-pc-windows-msvc).

   Refs: cursey#81

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds x87 FPU register-stack (ST0–ST7) and MXCSR save/restore support to the x86-32 MidHook context, enabling callbacks to read and modify floating-point state and have those changes reflected when execution resumes (addresses #81).

Changes:

  • Extend the x86-32 mid-hook stub to save/restore a full FNSAVE image plus MXCSR around the destination callback.
  • Add Fpu/FpuEnv + Context32 helpers and converter stubs for 80-bit x87 values.
  • Add x86-32-only tests validating ST register round-trips, MXCSR mutation, and logical stack mutation via st_pop/st_push_*.

Reviewed changes

Copilot reviewed 6 out of 7 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
test/mid_hook.cpp Adds x86-32-only tests using Xbyak to validate x87 ST + MXCSR behavior through MidHook.
src/mid_hook.x86_32.asm Updates the x86-32 stub to save/restore FNSAVE image + MXCSR and adjusts stack-frame offsets.
src/mid_hook.cpp Regenerates the embedded x86-32 stub bytes and updates relocation offsets; minor cleanups.
src/context.cpp Introduces runtime-emitted x87 conversion stubs and implements Fpu + Context32 stack helpers.
src/CMakeLists.txt Adds context.cpp to the library build.
include/safetyhook/context.hpp Extends Context32 API/types to expose x87 FPU state + MXCSR and adds stack helpers.
.gitignore Ignores an additional build directory (build-x86).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread include/safetyhook/context.hpp Outdated
Comment thread include/safetyhook/context.hpp
Comment thread include/safetyhook/context.hpp Outdated
Comment thread include/safetyhook/context.hpp Outdated
Comment thread src/context.cpp Outdated
@ThirteenAG

Copy link
Copy Markdown

My test results so far:

  1. all previous inline asm code breaks (expected), perhaps a warning about this breaking change would be needed on release.

  2. tested this converted code:

void Find3rdPersonCamTargetVectorFMUL(SafetyHookContext& ctx)
{
    float f = CDraw::GetAspectRatio();
    ctx.st[0].set_f32(ctx.st[0].as_f32() * f); //_asm {fmul dword ptr[f]}
}
void StretchX(SafetyHookContext& ctx)
{
    float f = (CDraw::GetAspectRatio() / (4.0f / 3.0f));
    ctx.st[0].set_f32(ctx.st[0].as_f32() / f); //_asm {fdiv dword ptr[f]}
}

Works fine, didn't notice any regressions or crashes.

  1. writing ctx.st[0].set_f32(ctx.st[0].as_f32() / f); is quite cumbersome, is there no way to simplify a bit to resemble how xmm variables work?
    Ideally something like
ctx.st[0].as_f32() = ctx.st[0].as_f32() / f;

or

ctx.st[0].as_f32() /= f;

@ThirteenAG

Copy link
Copy Markdown

Push and pop also works, and I think Fpu st[8]; -> Fpu st0, st1, st2, st3, st4, st5, st6, st7; would me more convenient to use, similar to Xmm xmm0, xmm1, xmm2, xmm3, xmm4, xmm5, xmm6, xmm7;

@angelfor3v3r

Copy link
Copy Markdown
Collaborator Author

Good suggestions. I'll try some stuff out soonish.

   Replace `Fpu st[8]` with named members `Fpu st0, st1, st2, st3, st4,
   st5, st6, st7;`, exposing each slot directly as `ctx.st0..st7`.
   `st_pop` / `st_push_f32` / `st_push_f64` now operate on `&st0` base +
   `memmove`/`memset` instead of array indexing.

   Add ergonomic proxy types FpuF32 / FpuF64 / FpuF80 exposing each Fpu
   slot as a read-modify-write handle: implicit `operator T()` read,
   assignment, and compound-assignment (`+=`, `-=`, `*=`, `/=`). Add
   `Fpu::f32()` / `f64()` / `f80()` factory methods.

   Modeled on `std::atomic_ref<T>`:
   - In-class member compound-assignment (no free binary operators -- a
     prvalue proxy would dangle since it must alias a concrete slot).
   - No `++`/`--` (atomic_ref provides those only for integral T; the
     doc comment cites the precedent).
   - Comparisons and streaming handled by implicit conversion.

   f80 path (GCC/Clang, `__LDBL_MANT_DIG__ == 64`): `long double` is
   80-bit, bit-identical to `Fpu::raw`, so `as_f80`/`set_f80` are lossless
   `memcpy`s. f32/f64 route through the f80 path (compiler emits
   `fld`/`fstp tbyte`); no JIT stubs on this path.

   MSVC (`__LDBL_MANT_DIG__ == 53`): f80 absent. Renames the existing
   two-`__cdecl`-stub pool types (`FpuToDoubleFn`/`DoubleToFpuFn`,
   `ConverterCode`, `VmDeleter`, `CONVERTER_CODE` 24-byte array) already
   in context.cpp; f32 routes through f64 via `static_cast`. No behavior
   change vs. the prior stub implementation.

   Tighten the doc comments on Fpu / FpuF32 / FpuF64 / FpuF80 / FpuEnv /
   Context32::st_pop / st_push_f32 / st_push_f64 to be concise: state what
   each does and the one caveat (bit-63 unsafe; fpu_env not re-derived
   after slot rotation; callback runs at FINIT defaults). Pre-existing
   top-level `@file`, `Context64`, and the original three `Context32` notes
   (eip / esp / trampoline_esp) are left untouched.

   Pin the full Context32 layout the hand-written x87 trampoline in
   `src/mid_hook.x86_32.asm` encodes, via additional static_asserts:
     offsetof(fpu_env)=0, st0=28, st7=98, mxcsr=108, xmm0=112,
     eflags=240, eip=280; sizeof=284 (GCC i386 SysV) || 288 (MSVC tail
     pad for Xmm alignment). asm uses literal offsets + movdqu and
     allocates its own frame, so the MSVC size divergence is harmless.

   Drop the "(may reflect post-save reset)" note from `FpuEnv::top()` --
   FNSAVE stores the *captured* TOP, and post-rotation reads of fpu_env
   return exactly what will be FRSTOR'd (the rotation never touches
   fpu_env, which is the documented behavior).

   Migrate existing MidHookX87 tests in `test/mid_hook.cpp` to
   `ctx.st0..st7`. Add new tests:
   - StProxyArithmeticOps: proxy `*=`, `/=`, `+=`, `-=`, `=`
   - StF80LosslessRoundTrip (GCC/Clang only): verify as_f80/f80() are
     bit-exact using 1e-30L as a probe that as_f64 cannot round-trip

   Export Fpu / FpuEnv / FpuF32 / FpuF64 / FpuF80 (gated) from
   `module/safetyhook.cppm`.

   asm unchanged.

   Breaking changes (x86-32 MidHook):

   - `Context32::st[8]` replaced by named `Fpu st0, st1, st2, st3, st4, st5, st6, st7;`. Code referencing `ctx.st[n]`
 must migrate to `ctx.st0..st7`.

   - Inline asm in MidHook callbacks that operated on the live x87 FPU (e.g. `_asm { fmul dword ptr[f] }`) no longer
 sees the program's register values: FNSAVE captures ST(0)..ST(7) into the `ctx.st0..st7` byte image and resets the
 live FPU to FINIT defaults for the duration of the callback. The program's captured state is restored on return.
 Replace inline asm with the `Fpu` accessors (`ctx.st0.as_f32()`, `ctx.st0.set_f32(...)`) or the
 `FpuF32`/`FpuF64`/`FpuF80` proxies (`ctx.st0.f32() += x`, `ctx.st0.f32() = ...`). Proxy ops touch only the targeted
 slot's 10 bytes and never disturb `fpu_env` or neighboring slots.

   - Proxy arithmetic decays to the arithmetic type (`float`/`double`/`long double`), matching `std::atomic_ref<T>`:
 `ctx.st0.f32() + x` yields a `float`, not a proxy. No free binary operators are provided (a prvalue proxy would dangle
 since it must alias a concrete slot).

   Verified: GCC 13.3 / Linux x86-32 (22/22) + x86-64 (20/20); Clang 21.1
   / Windows MSVC target x86-32 (22/22, f80 path absent as expected).

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 8 changed files in this pull request and generated 2 comments.

Comment thread src/context.cpp Outdated
Comment thread src/context.cpp
   - Replace `std::memset(&st7, 0, sizeof(Fpu))` with `st7 = Fpu{};` to
     silence GCC -Wclass-memaccess (Fpu's `uint8_t raw[10]{}` member-init
     makes its default ctor non-trivial). Same zero bytes, no warning.

   - Pin calling convention on the MSVC FpuToDoubleFn / DoubleToFpuFn
     function-pointer types via SAFETYHOOK_CCALL so /Gz (stdcall) / /Gr
     (fastcall) can't mismatch the __cdecl stubs' ABI.

   - Replace magic `code + 13` offset with a named
     `constexpr size_t FPU_TO_DOUBLE_LEN = 13;` so the dependency on the
     first stub's size is auditable.

   Verified: GCC 13.3 / Linux x86-32 (22/22) + x86-64 (20/20); Clang 21.1
   / Windows MSVC target x86-32 (22/22), all under -Werror / /WX.
@angelfor3v3r

Copy link
Copy Markdown
Collaborator Author

@ThirteenAG Let me know if this is any nicer to use. You can do stuff like ctx.st0.f32() /= f now. Kept the other functions just in case for now.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 8 changed files in this pull request and generated 3 comments.

Comment thread src/context.cpp
Comment thread src/context.cpp
Comment thread src/mid_hook.x86_32.asm
@ThirteenAG

Copy link
Copy Markdown

Checking the code I have, and encountered this:

            float temp = 0.0f;
            _asm {fdiv    st, st(2)}
            _asm {fstp    dword ptr[temp]}
            *(float*)(regs.esp + 0x2C) = temp;

            regs.st0.f32() /= regs.st2.f32();
            float temp = regs.st0.f32();
            regs.st_pop();
            *(float*)(regs.esp + 0x2C) = temp;

How about making regs.st_pop32() that would return st0?

regs.st0.f32() /= regs.st2.f32();
*(float*)(regs.esp + 0x2C) = regs.st_pop32();

@ThirteenAG

ThirteenAG commented Jun 21, 2026

Copy link
Copy Markdown

Found a breakage:

.text:004460D0                         sub_4460D0      proc near               ; CODE XREF: sub_41A9A0+5B5↑p
.text:004460D0                                                                 ; sub_41A9A0+602↑p ...
.text:004460D0
.text:004460D0                         var_C           = dword ptr -0Ch
.text:004460D0                         arg_0           = dword ptr  4
.text:004460D0                         arg_4           = dword ptr  8
.text:004460D0
.text:004460D0 53                                      push    ebx
.text:004460D1 8B 5C 24 08                             mov     ebx, [esp+4+arg_0]
.text:004460D5 83 EC 08                                sub     esp, 8
.text:004460D8 8B 44 24 14                             mov     eax, [esp+0Ch+arg_4]
.text:004460DC 8B 10                                   mov     edx, [eax]
.text:004460DE 89 53 68                                mov     [ebx+68h], edx
.text:004460E1 8B 40 04                                mov     eax, [eax+4]
.text:004460E4 89 43 6C                                mov     [ebx+6Ch], eax
.text:004460E7 D9 05 A8 01 5B 00                       fld     ds:flt_5B01A8
.text:004460ED D8 73 68                                fdiv    dword ptr [ebx+68h]
.text:004460F0 D9 05 B0 01 5B 00                       fld     ds:flt_5B01B0
.text:004460F6 D8 73 6C                                fdiv    dword ptr [ebx+6Ch]
.text:004460F9 D9 C9                                   fxch    st(1)
.text:004460FB 8B 43 04                                mov     eax, [ebx+4]
.text:004460FE 85 C0                                   test    eax, eax
.text:00446100 D9 5B 70                                fstp    dword ptr [ebx+70h]
.text:00446103 D9 5B 74                                fstp    dword ptr [ebx+74h]
.text:00446106 74 08                                   jz      short loc_446110
.text:00446108 89 04 24                                mov     [esp+0Ch+var_C], eax
.text:0044610B E8 C0 DC FF FF                          call    sub_443DD0
.text:00446110
.text:00446110                         loc_446110:                             ; CODE XREF: sub_4460D0+36↑j
.text:00446110 83 C4 08                                add     esp, 8
.text:00446113 8B C3                                   mov     eax, ebx
.text:00446115 5B                                      pop     ebx
.text:00446116 C3                                      retn
.text:00446116                         sub_4460D0      endp

flt_5B01A8 is 1.0 and affects scaling in a game Deer Avenger 4.

I'm replacing .text:004460E7 D9 05 A8 01 5B 00 fld ds:flt_5B01A8

with

    MakeNOP(0x4460E7, 6);
    static auto _ = safetyhook::create_mid(0x4460E7, [](SafetyHookContext& regs)
    {
        regs.st_push_f32(1.0f);
    });

and all rendering breaks. I can see in the debugger that the value is pushed to st0, so perhaps it's some other things that break.

    static auto _ = safetyhook::create_mid(0x4460E7 + 6, [](SafetyHookContext& regs)
    {
        regs.st0.f32() /= 2.0f;
    });

this, however, works.

Originally I had inline asm there

_asm fld    dword ptr[fScaleValue]

and it also worked.

@angelfor3v3r

angelfor3v3r commented Jun 21, 2026

Copy link
Copy Markdown
Collaborator Author

What worries me is if the environment isn't set up right to reflect the changes or something, it's all very confusing to me. It probably breaks because when we push/pop we need to fix TOP/FTW and such.

Maybe your push code would work if you recalculated the fpu enviornment and such. I'll try to poke around more in the intel docs.

@ThirteenAG

Copy link
Copy Markdown

I asked copilot for a possible explanation:

You're hitting a real SafetyHookContext bug/limitation on x86.
regs.st_push_f32(1.0f) in external/injector/safetyhook/src/context.cpp only shifts st0..st7 bytes and writes new st0, but does not update x87 env (fpu_env.fsw TOP bits + fpu_env.ftw tag word).
Then mid_hook.x86_32.asm restores with frstor [esp], which uses that stale env. Result: x87 stack metadata is inconsistent, and downstream FPU ops break rendering.
You can see this documented in your own header (context.hpp, notes around st_push_f32/st_pop).

Fix
Update TOP/tag when doing push/pop.

void Context32::st_pop() noexcept {
    const uint8_t old_top = fpu_env.top();
    const uint8_t new_top = static_cast<uint8_t>((old_top + 1u) & 7u);

    std::memmove(&st0, &st1, sizeof(Fpu) * 7);
    st7 = Fpu{};

    // TOP = new_top
    fpu_env.fsw = static_cast<uint16_t>((fpu_env.fsw & ~(uint16_t(7u) << 11u)) | (uint16_t(new_top) << 11u));
    // old physical ST(0) becomes empty
    fpu_env.ftw = static_cast<uint16_t>(fpu_env.ftw | (uint16_t(0b11u) << (old_top * 2u)));
}

void Context32::st_push_f32(float value) noexcept {
    const uint8_t old_top = fpu_env.top();
    const uint8_t new_top = static_cast<uint8_t>((old_top - 1u) & 7u);

    std::memmove(&st1, &st0, sizeof(Fpu) * 7);
    st0.set_f32(value);

    // TOP = new_top
    fpu_env.fsw = static_cast<uint16_t>((fpu_env.fsw & ~(uint16_t(7u) << 11u)) | (uint16_t(new_top) << 11u));
    // new physical ST(0) is now non-empty (valid)
    fpu_env.ftw = static_cast<uint16_t>(fpu_env.ftw & ~(uint16_t(0b11u) << (new_top * 2u)));
}

void Context32::st_push_f64(double value) noexcept {
    const uint8_t old_top = fpu_env.top();
    const uint8_t new_top = static_cast<uint8_t>((old_top - 1u) & 7u);

    std::memmove(&st1, &st0, sizeof(Fpu) * 7);
    st0.set_f64(value);

    // TOP = new_top
    fpu_env.fsw = static_cast<uint16_t>((fpu_env.fsw & ~(uint16_t(7u) << 11u)) | (uint16_t(new_top) << 11u));
    // new physical ST(0) is now non-empty (valid)
    fpu_env.ftw = static_cast<uint16_t>(fpu_env.ftw & ~(uint16_t(0b11u) << (new_top * 2u)));
}

I will compile with this edit and report back.

@ThirteenAG

Copy link
Copy Markdown

UPD: Game still breaks with this code.

@angelfor3v3r

Copy link
Copy Markdown
Collaborator Author

Super odd, I don't know what causes it. x87 is really annoying and I feared things like this.

@ThirteenAG

Copy link
Copy Markdown

I kept the original FLD instruction on top of having the hook, and it doesn't break rendering. So I guess there's something FLD does that's not happening with the current implementation of st_push_f32.

@ThirteenAG

ThirteenAG commented Jun 21, 2026

Copy link
Copy Markdown

Here's a conclusion deepseek made:

After all this analysis, I'm now certain the issue is that st_push_f32 modifies the saved FNSAVE image without updating FIP/FDP/FOP — the FPU instruction pointer, data pointer, and opcode fields. These track the last FPU instruction and some games' rendering pipelines read them via FSTENV. A real fld updates them; our manual byte manipulation doesn't.

The only way to get these fields correct is to let the actual x87 hardware execute the fld. Here is the definitive fix:

void Context32::st_push_f32(float value) noexcept {
    __asm {
        push    eax
        mov     eax, ecx            // ecx = this (MSVC thiscall)
        frstor  [eax]               // restore game's FPU state to live FPU
        fld     dword ptr [value]   // push value — updates FIP/FDP/FOP/FSW/FTW correctly
        fnsave  [eax]               // save modified state back to the FNSAVE image
        fwait
        pop     eax
    }
}

void Context32::st_push_f64(double value) noexcept {
    __asm {
        push    eax
        mov     eax, ecx
        frstor  [eax]
        fld     qword ptr [value]
        fnsave  [eax]
        fwait
        pop     eax
    }
}

void Context32::st_pop() noexcept {
    __asm {
        push    eax
        mov     eax, ecx
        frstor  [eax]
        fstp    st(0)               // pop ST(0) — updates FIP/FDP/FOP/FSW/FTW correctly
        fnsave  [eax]
        fwait
        pop     eax
    }
}
FRSTOR requires a 108-byte image produced by FNSAVE from a live FPU that executed the instruction. You were essentially doing the equivalent of hand-editing a save-file and the game rejected it.

The FNSAVE 28-byte environment contains 8 reserved bytes (offsets 14–15, 22–23, 24–27). The Intel manual marks them "reserved." On real silicon, the FPU writes implementation-specific values to them that act as an internal consistency marker for the other 100 bytes. When you update fip, fdp, fop, fsw, and ftw manually but leave the reserved bytes stale (they still correspond to the old FPU state before the hook), FRSTOR detects the inconsistency and enters an undefined or partially-restored state.

Here's a practical test to prove it. You already confirmed the inline asm works. Now try this: dump the entire 108 bytes of the FNSAVE image after both approaches and memcmp them. They will differ in the reserved bytes.

This is a hardware constraint, not code quality. There is no pure-C++ workaround — you cannot set undocumented, implementation-specific silicon state. The hardware round-trip (frstor → real instruction → fnsave) is the only path. Keep the inline asm.

With these indeed works, however I'm not sure if the conclusion is correct, just don't know enough about the subject. The game doesn't seem to use FSTENV instruction anywhere.

@angelfor3v3r

Copy link
Copy Markdown
Collaborator Author

I've made a lot of changes but I haven't gotten around to finishing it all up. Might be a while until I push my new changes, but I did implement frstor and fnsave into the stubs. I think it makes sense to let the CPU handle updating all the flags and stuff instead of doing it by hand.

Thanks for the help and stuff, I'll get around to this again soon hopefully.

@ThirteenAG

Copy link
Copy Markdown

Alright, in any event I think allowing to modify st values without using inline asm should suffice, and not saving/restoring fpu context should still allow usage of inline asm where needed. If everything could be achieved without inline asm, that would be ideal of course.

@angelfor3v3r

Copy link
Copy Markdown
Collaborator Author

I'm hoping just having frstor and fnsave will fix the weirdness of the FPU environment getting corrupted. Should be good to go after that + a few other ideas you mentioned 🙂

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants