acdream/docs/research/2026-08-13-389-fov-mechanism-review.md
Erik d13d63d0a5 fix #389 review round: settings v3 FOV migration + live apply; AD-90
Dual-lens Opus review of 7e0c1303 (reports committed under
docs/research/). The law, gate, and vertical application are CONFIRMED
at instruction-byte level against the PDB-paired acclient.exe (the BN
text FPU-elides this whole area); the fix round addresses the findings:

- Blast MUST-FIX 1: real schema migration instead of a hand-edited dev
  file. SettingsStore v2->v3: a pre-v3 display.fieldOfView was the
  applied vertical FOV in degrees; v3 means retail's m_fGameFOV.
  LoadDisplay migrates on read - the untouched old default 60 maps to
  the retail default 90; a deliberate other value preserves its visible
  16:9 framing (x (16/9 - 0.1)), clamped to the registered [10,160];
  the next save stamps v3 and migration never reruns. The dev
  settings.json hand-edit was reverted so the migration owns it.
- Blast MUST-FIX 2 / mechanism M2: the Field of View now applies LIVE on
  Save (retail: Render::GRPCallback_OnRenderPreferenceChanged @0x0054d999
  -> SmartBox::SetDefaultFov). RuntimeSettingsTargets gains the camera
  graph and applies through ApplyDisplayWindowState - the update-phase
  seam, deliberately NOT the render-phase preview path (the review's
  WATCH-3 cull-vs-raster landmine).
- Mechanism M1 -> register row AD-90: retail's divisor aspect runs
  through the Render.AspectRatio preference (ComputeAspectForViewport
  @0x0054f150, (w/h) x pref x 0.75) - exactly raw w/h at the registered
  default, which is what acdream assumes; retail's NaN-through-the-gate
  quirk (M3) is folded into the same row as deliberately not reproduced.
- Docs: RetailFieldOfView now cites the decisive vertical proof
  (D3DXMatrixPerspectiveFovLH fovy slot @0x0059ab71), the unconditional
  SmartBox::RenderNormalMode site, and M4's exact horizontal numbers
  (89.0/83.9/80.6 deg); the Config FOV row comment updated to LIVE.
- Blast WATCH 4 disposition: the 15 replay-harness PI/3 constants stay -
  they are CAPTURE-TIME camera parameters for recorded fixtures, not
  production framing; changing them would invalidate the replays.

Tests: +6 SettingsStore migration facts, +1 live-apply fact.
App suite 4,962/3 skips; UI.Abstractions 922.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 17:27:15 +02:00

24 KiB
Raw Blame History

#389 SmartboxFOV — MECHANISM review of commit 7e0c1303

Date: 2026-08-13 Reviewer lens: MECHANISM (is the ported law the law retail actually runs?) Subject: 7e0c1303fix #389: port retail's SmartboxFOV law; retire AD-89 (display slice 1) Oracle: docs/research/named-retail/acclient_2013_pseudo_c.txt + docs/research/named-retail/acclient.h, plus byte-level disassembly of the PDB-paired binary C:\Users\erikn\Downloads\acclient.exe (check_exe_pdb.pyMATCH, GUID 9e847e2f-777c-4bd9-886c-22256bb87f32, linker 2013-09-06). Every x87 comparison and division in this area is FPU-elided or ambiguous in the Binary Ninja pseudo-C, so all polarity and operand-order claims below are decided from the instruction bytes, not from BN's rendering. Raw disassembly is in the appendix.

Report only — no files under src/ or tests/ were modified.


Verdict summary

# Question Verdict
1 Formula transcription exact (operand order, 0.1, aspect variable, direct feed) CONFIRMED, with one qualification: retail's m_ViewportAspectRatio is not raw w/h in general — see M1
2 What m_bUseSmartboxFOV == 0 selects; does acdream apply the law where retail uses the fixed branch CONFIRMED — no misapplication. Paperdoll/appraisal/char-gen = fixed branch (π/4, byte-verified); portal space = smartbox law, and acdream's portal camera derives from the world projection
3 Does 0x00453b14 differ from the CreatureMode site CONFIRMED (differs, benignly) — it is SmartBox::RenderNormalMode, a different class, with no m_bUseSmartboxFOV gate at all; the arithmetic is byte-identical
4 Gate is open (0, π), silent keep-previous; SetFOVInternal applies it vertically CONFIRMED — open on both ends, return 0 without touching Render::fov; and the value reaches D3DXMatrixPerspectiveFovLH's fovy slot. The port is not inverted. One NaN corner: M3
5 0x00451e6a is the FOV option's setter; [10,160]/default 90 registered CONFIRMEDSmartBox::SetDefaultFov, fed by Render::UpdateFromPreferences; range and default byte-verified
6 Other writers of Render::fov / other SetFOVRad callers the port ignores CONFIRMED (none ignored) — only 3 SetFOVRad call sites exist; the one other FOV writer (Render::set_vdst, the teleport override) is already ported in TeleportViewPlaneController, and my byte-decode vindicates its 2·atan(1/d) against BN's misleading __fpatan(arg1, 1.0)

No MUST-FIX defect was found in the law, the gate, or its application. Three bookkeeping/doc items are ranked below; the highest (M1) is a divergence-register obligation created by this commit, not a wrong number at retail's defaults.


Q1 — Is the formula transcription exact?

CONFIRMED for the expression; QUALIFIED for what "the aspect" is.

Both smartbox sites are byte-identical arithmetic. CreatureMode::Render (pseudo-C line 91727, 0x00452b0d0x00452b2f):

00452b0d  mov  ecx, [0x86f330]          ; RenderDevice::render_device
00452b13  fld  dword [ecx + 0xa8]       ; ST0 = m_ViewportAspectRatio
00452b1a  fsub dword [0x7948cc]         ; ST0 = aspect - 0.1f      (0x3dcccccd)
00452b20  fdivr dword [eax + 0xc0]      ; ST0 = m_fGameFOV / ST0   <-- fdivr: mem / ST0
00452b26  fstp dword [esp]              ; float-rounded quotient
00452b2f  call 0x54b2d0                 ; Render::SetFOVRad(quotient)
  • Operand order: fdivr is reverse divide — ST0 = memory / ST0, i.e. m_fGameFOV / (aspect 0.1). The numerator is the game FOV. Correct in the port. (This is exactly the operand-order trap the question was aimed at; BN's ((long double)m_fGameFOV) / (aspect - 0.1) happens to agree, but only the bytes prove it.)
  • The 0.1 literal: [0x7948cc] = 0x3dcccccd = 0.10000000149011612. The port's AspectBias = 0.100000001f parses to that identical float. Exact.
  • Direct feed: the quotient is stored to the stack slot and immediately passed to SetFOVRad; no clamp, scale, or half-angle in between. SetFOVRadSetFOVInternal(arg)D3DXMatrixPerspectiveFovLH(pOut, arg, aspect, zn, zf) (0x0059ab71). No further transformation. Correct in the port.
  • Retail evaluates in x87 80-bit and stores one float; acdream evaluates in float. Sub-ULP; immaterial.

The qualification (see M1): which variable holds the aspect

The divisor's left operand is RenderDevice::m_ViewportAspectRatio (offset 0xa8, confirmed by the store in RenderDevice::SetViewport at 0054f22e fstp dword [esi+0xa8]). That field is not width/height in the path the game view uses. RenderDevice::ComputeAspectForViewport (0x0054f150, pseudo-C line 345708) is FPU-elided in BN to a meaningless return arg4 / arg5. The bytes say:

0054f150  mov al, [esp+0x14]      ; arg6
0054f154  fild dword [esp+0xc]    ; width
0054f15a  je  0x54f181            ; arg6 == 0 -> second path
          ; arg6 != 0 :  ST0 = width / height                      <-- raw viewport aspect
0054f17c  fdivp st(1)
0054f17e  ret 0x14
0054f181: ; arg6 == 0 :
0054f1a1  fdivp st(1)             ; ST0 = width / height
0054f1a3  fmul dword [ecx+0xa4]   ; * m_DisplayAspectRatio
0054f1a9  fmul dword [0x79b6dc]   ; * 0.75
0054f1af  ret 0x14

m_DisplayAspectRatio (offset 0xa4, confirmed by the stores at 0059fc19 / 0059fc24 / 0059fc33) comes from the Render.AspectRatio user preference (RenderDeviceD3D::SetupDisplayAspectRatio 0x0059fbd0, pseudo-C 428476): 1 → 1.33333337f, 2 → 1.77777779f, otherwise the display's own w/h.

The game view's viewport is installed with arg6 = 0 (gmSmartBoxUI::RecvNotice_UpdateGameView, 0x004d62f5), so in normal gameplay:

m_ViewportAspectRatio = (viewportW / viewportH) × m_DisplayAspectRatio × 0.75

At the registered default AspectRatio = 1 (static initializer Render::m_RenderPrefs.AspectRatio = 0x1 at 0x0081efa8; also the safe-settings reset at 0054ef25), 1.33333337f × 0.75f rounds to exactly 1.0f, so m_ViewportAspectRatio == w/h and the port matches retail bit-for-bit. It diverges only if the user selects the "widescreen" preference (× 4/3, a noticeably narrower vertical FOV) — a preference acdream does not implement. That is the M1 bookkeeping item, not a wrong number today.


Q2 — What m_bUseSmartboxFOV == 0 selects, and does acdream misapply the law?

CONFIRMED — the port does not apply the smartbox law anywhere retail uses the fixed branch.

The flag lives on CreatureMode (acclient.h:52561), which is the base of UIElement_Viewport (acclient.h:52574) — i.e. it only exists for embedded 3-D UI viewports, never for the world. CreatureMode::Render 0x00452af0:

00452ae8  mov al, [esi+0x6c]   ; m_bUseSmartboxFOV
00452af0  je  0x452b2b         ; == 0  -> fixed branch
00452b2b  mov edx, [esi+0x68]  ; m_fFOVRadians
00452b2f  call SetFOVRad

m_fFOVRadians (offset 0x68) has exactly one read and zero writes in the whole binary — its value is the constructor default, byte-decoded at 004543de mov dword [esi+0x68], 0x3f490fdb = π/4 = 45° (BN hid this inside a 17-byte memcpy literal). So the fixed branch is a constant, aspect-independent 45° vertical FOV.

Who takes which branch (every caller in the binary):

Site Caller Branch
0x004a5aa9 gmPaperDollUI (paperdoll) — UseSharpMode fixed 45°
0x004e0440 char-gen 3-D view — UseSharpMode fixed 45°
0x004ee9bc gmCG3DViewUseSharpMode fixed 45°
0x004d6db3 gmSmartBoxUI::PostInitm_pPortalSpaceUseSmartboxFOV smartbox law

CreatureMode::UseSmartboxFOV (0x00452380, line 91348) is the only writer of the flag and only ever sets it to 1.

acdream's side, checked exhaustively (grep FovY, grep CreatePerspectiveFieldOfView):

  • The four cameras driven by CameraController (Orbit, Fly, Chase, RetailChase) are all world cameras — retail's world render is SmartBox::RenderNormalMode, which is unconditionally on the smartbox law. Correct.
  • DollCamera.FovRadians = MathF.PI / 4f — matches the byte-decoded fixed-branch constant. The commit's "paperdoll is exempt by design" claim is not just plausible, it is numerically right, and it is outside CameraController, so the law can never reach it.
  • CreatureAppraisalPresentation's camera is likewise π/4 and outside the controller — the same fixed-branch class of viewport. Correct.
  • Portal space: retail's portal space does take the smartbox law (0x004d6db3). acdream's PortalTunnelCamera.UseSmartBoxFov(smartBoxProjection) recovers the FOV from the world camera's projection matrix (fov = 2·atan(1/M22)) each draw (PortalTunnelPresentation.Draw, line 314), so it now inherits the new law automatically. Correct — and the "read the SmartBox each draw" shape matches retail's per-frame re-application.
  • VulkanRhiScene's hardcoded MathF.PI / 3f is the RHI smoke-test scene (quadrant markers, wobble backdrop), not a world path. Not a finding.

Startup ordering was also checked, because the law makes FOV aspect-dependent: HostInputCameraCompositionPhase calls FramebufferResize.Resize(InitialFramebufferSize) immediately after binding the camera target (HostInputCameraComposition.cs:320), so the real framebuffer aspect — not DefaultAppliedFovY's assumed 16:9 — is in force before the first frame.


Q3 — Does 0x00453b14 differ from the CreatureMode site?

CONFIRMED — different class, one structural difference, identical arithmetic.

0x00453b14 is inside SmartBox::RenderNormalMode (0x00453aa0, pseudo-C line 92639) — the world render, not CreatureMode. Differences:

  1. No m_bUseSmartboxFOV gate exists there at all. The function goes straight to the m_bUseViewDistance test (00453ae6 mov cl,[esi+0xc8]). The world is always on the smartbox law (modulo the teleport override). This is the stronger statement the port relies on, and it holds.
  2. m_fGameFOV is read from this (the SmartBox, [esi+0xc0]) rather than from the SmartBox::smartbox global ([eax+0xc0] after a158ca8300 mov eax,[0x83ca58]). Same object in practice — CreatureMode::Render loads the global explicitly because this is a viewport, not the SmartBox.
  3. Otherwise the instruction sequence is the same four ops (fld [dev+0xa8] / fsub 0.1f / fdivr m_fGameFOV / fstp), then call SetFOVRad.

Both sites share the second gate the commit message does not mention: if m_bUseViewDistance != 0, retail calls Render::set_vdst(m_fViewDistFOV) instead — see Q6.


Q4 — The gate, and is the value vertical?

CONFIRMED on both halves. The port is not inverted.

The acceptance interval — open (0, π), silent keep-previous

Render::SetFOVRad (0x0054b2d0, pseudo-C line 342158), bytes:

0054b2d0  fld   dword [esp+4]
0054b2d4  fcomp dword [0x795344]     ; 0x795344 = 0.0f
0054b2da  fnstsw ax
0054b2dc  test  ah, 0x41             ; C0 (less) | C3 (equal)
0054b2df  jnp   0x54b312             ; PF=0 <=> exactly one of C0/C3 <=> arg<0 or arg==0  -> reject
0054b2e1  fld   dword [esp+4]
0054b2e5  fcomp qword [0x7bdd30]     ; 0x7bdd30 = 3.141592653589793
0054b2ed  test  ah, 1                ; C0 (less)
0054b2f0  je    0x54b312             ; not-less (>= pi)  -> reject
0054b2fc  mov   dword [0x81ec84], 0x3dcccccd   ; Render::znear = 0.1f
0054b306  call  [eax+0x1c]           ; SetFOVInternal(arg)
0054b30c  mov   eax, 1
0054b311  ret
0054b312  xor   eax, eax             ; return 0 — no SetFOVInternal, Render::fov untouched
0054b314  ret
  • arg == 0 is rejected (C3 path) and arg == π is rejected (needs strict C0). Interval is open on both ends. Matches fovY > 0f && fovY < MathF.PI.
  • The reject path is xor eax,eax; ret — it never reaches SetFOVInternal, so Render::fov, Render::vdst, Render::znear and the view-to-clip matrix all keep their previous values, silently (the int32_t return is discarded at every call site). CameraController.ApplyProjection's "return without touching FovY" is the exact contract.
  • Note the port also short-circuits divisor <= 0. Retail reaches the same verdict by arithmetic: a negative divisor yields a negative quotient (rejected by test 1), a zero divisor yields ±∞ (rejected by test 2). Behaviourally equivalent — except for NaN, see M3.

Vertical, not horizontal — three independent proofs

  1. Render::SetFOVInternal (0x0054b340): Render::vdst = ty / tan(arg × 0.5) where ty = (viewportHeight 1) × 0.5 × yinvscale — half the viewport height over a distance is tan(half **vertical** FOV).
  2. PrimD3DRender::SetFOVInternal (0x0059ab40) passes the same arg2 straight into D3DXMatrixPerspectiveFovLH(&m, arg2, m_ViewportAspectRatio, znear, zfar). That API's second parameter is fovy and its third is width/height. Decisive.
  3. SmartBox::GetOverrideFovDistance (0x00451be0) halves the same smartbox expression before fptan and reciprocates it (fmul 0.5fptanfdivr 1.0), i.e. it returns cot(fov/2) — only meaningful if the expression is a full vertical angle. (It also proves view-plane distance == M22 of a perspective matrix, which is what TeleportViewPlaneController.Begin assumes.)

acdream feeds the value to Matrix4x4.CreatePerspectiveFieldOfView(FovY, Aspect, …) — same slot, same meaning. Not inverted.


Q5 — Is 0x00451e6a the Field of View option's setter, with [10,160]/default 90?

CONFIRMED on every part.

  • Enclosing function: SmartBox::SetDefaultFov(SmartBox* this, float degrees) at 0x00451e60 (pseudo-C line 90988) — m_fGameFOV = degrees × 0.017453292519943295.
  • Its only caller in the binary is Render::UpdateFromPreferences() (0x0054d850, call at 0x0054d999, pseudo-C 344365), guarded by a change test against Current_Render_FieldOfView and fed from Render::m_RenderPrefs.FieldOfView. So the chain is option slider → preference → UpdateFromPreferencesSetDefaultFovm_fGameFOV.
  • Range: UIPreferences::SetPreferenceRange(&Render_FieldOfView, 10f, 160f) at 0x004043b2, inside gmClient::InitUIPreferences (0x004035b0, pseudo-C line 2740) — exactly as the commit claims. Corroborated by the console command at 0x00455d20 which validates >= 0xa && <= 0xa0 before UIPreferences::ModifyPreference(&Render_FieldOfView, …).
  • Default: the static initializer Render::m_RenderPrefs at 0x0081efb8 reads float FieldOfView = 90. The UI row is registered via PlayerOptionPage::AddSliderOption(&Render_FieldOfView, 1) (0x0049e53e).
  • Ctor default of m_fGameFOV: 0x00454649 in SmartBox::SmartBox sets 1.57079637f = π/2 = 90°, consistent with the degrees default. Also m_fViewDistFOV = 0, m_bUseViewDistance = 0.
  • acdream's DisplaySettings.Default.FieldOfView = 90f and the Config row (ConfigOptionsPageController min: 10f, max: 160f, defaultValue: 90.0f) now agree with each other and with retail. The pre-#389 mismatch (record default 60, slider default 90) is genuinely closed.

One behavioural difference, pre-existing (see M4): retail applies an FOV change immediatelyRender::UpdateFromPreferences is reached from SceneTool::PrepareGraphicsDevice (0x0043e4f0), a per-frame device-prep step. acdream applies DisplaySettings.FieldOfView only through RuntimeSettingsController.ApplyStartup (next launch), except while a Config draft preview is open (WorldRenderFrameBuilder.ApplyApplyFieldOfView).


Q6 — Any other writer of Render::fov / SetFOVRad caller the port ignores?

CONFIRMED — none ignored. The one "other law" is already ported, and my byte-decode corrects the decomp in the port's favour.

SetFOVRad has exactly three call sites in the binary (grep of the full pseudo-C): 0x00452b2f (CreatureMode, both branches converge on this one call) and 0x00453b14 (SmartBox::RenderNormalMode). Nothing else calls it.

Render::fov is written only inside Render::SetFOVInternal, reached from:

  1. SetFOVRad — the ported path.
  2. Render::set_vdst(float) (0x0054b240) — the view-distance override. Selected by m_bUseViewDistance != 0 at both smartbox sites, i.e. it replaces the smartbox law while armed. Armed/disarmed by SmartBox::SetOverrideFovDistance (0x00451bc0), whose only callers are the teleport animation in gmSmartBoxUI: EndTeleportAnimation (0x004d65d5, disarm) and UseTime (0x004d71c4, 0x004d725f, 0x004d73ce arm; 0x004d747f disarm), interpolating teleportCurVDist between the game's value and TRANSITION_VIEW_PLANE_DISTANCE over TELEPORT_ANIM_FADE_TIME.
  3. Render::set_zfar (0x0054b320) — re-applies the existing Render::fov after changing the far plane. Not an independent FOV writer.

acdream already ports (2) as TeleportViewPlaneController, and it is correct:

0054b240  fld   qword [0x7928c0]   ; 1.0
0054b246  fld   dword [esp+4]      ; d       (ST0=d, ST1=1.0)
0054b24a  fpatan                   ; atan(ST1/ST0) = atan(1/d)
0054b24c  fadd  st(0), st(0)       ; fov = 2*atan(1/d)
0054b252  fcomp dword [0x7ca9f0]   ; 0.4f
0054b25d  jp    -> znear = 0.1f    ; d < 0.4
0054b263  fmul  dword [0x7c8a04]   ; else znear = d * 0.25
          ; then clamp fov to [0.001 (0x794720), pi (0x7bdd30)] and call SetFOVInternal

BN renders line 342123 as __fpatan(arg1, 1.0), which reads as atan(d) and would have made the port's 2f * MathF.Atan(1f / distance) look wrong. The bytes show x87 fpatan computes atan(ST1/ST0) = atan(1/d)the port is right and the decomp text is the misleading party. near = MathF.Max(0.1f, distance*0.25f) is likewise equivalent to retail's d < 0.4 ? 0.1 : d*0.25 at every input.

Residual (pre-existing, not #389): acdream's world cameras use a 5000 m far plane where Render::zfar defaults to 4000 (0x0081ec88); PortalTunnelCamera already documents 4000.


Findings, ranked

M1 — REGISTER/DOC (highest): "the viewport aspect" is a preference-scaled quantity

RetailFieldOfView's class doc and the commit message both describe the divisor's left operand as the viewport aspect ratio. Retail's m_ViewportAspectRatio for the game view is (w/h) × m_DisplayAspectRatio × 0.75, where m_DisplayAspectRatio is driven by the Render.AspectRatio preference (Normal→4:3, widescreen→16:9, otherwise the display's own ratio). At the registered default (1) the factor is exactly 1.0f and acdream matches retail bit-for-bit; at the widescreen setting retail's effective aspect is 4/3 larger and the vertical FOV correspondingly narrower. acdream implements no aspect-ratio preference.

Why it matters: #389 is the commit that made this field load-bearing, and the project rule is that a commit introducing a deviation carries its register row. Action: add a divergence-register row ("acdream feeds the raw framebuffer aspect; retail scales it by the Render.AspectRatio preference — identical at retail's default, divergent if that preference is ever implemented") and correct the one-line description in RetailFieldOfView's doc to name ComputeAspectForViewport @0x0054f150 as the definition. No behaviour change.

M2 — NOTE: FOV changes do not take effect until next launch

Retail's option applies within one frame (SceneTool::PrepareGraphicsDeviceRender::UpdateFromPreferencesSetDefaultFov; the smartbox re-reads m_fGameFOV every RenderNormalMode). acdream applies it via ApplyStartup only (live only during a Config draft preview). Pre-existing and documented in DisplaySettings/ConfigOptionsPageController, but the semantics change in this commit makes the lag more visible (the slider now changes the shape of the view, not just its zoom). Worth an issue or a register row; not a #389 defect.

M3 — NOTE: NaN corner of the gate is inverted

Retail's first test is test ah,0x41 / jnp: an unordered compare sets both C0 and C3 → even parity → not taken, and the second test's test ah,1 sees C0 set → accepted. So retail would pass a NaN FOV through to SetFOVInternal. acdream's fovY > 0f && fovY < MathF.PI rejects NaN. Reaching it requires m_fGameFOV NaN or an exact 0/0, neither of which the option range permits. Record it, do not "fix" it — rejecting NaN is strictly safer and unreachable.

M4 — DOC nit: the "~8590° horizontal" claim

At the 90° default the law yields horizontal FOV ≈ 89.0° (4:3), 83.9° (16:9), 80.6° (21:9). The class doc says "~8590°"; the test asserts [80°, 90°] (correct). Suggest "~8089°, near-constant" in the doc so the doc and the test agree.

Verified-good (no action)

  • AspectBias = 0.100000001f0x3dcccccd. Exact.
  • DefaultGameFovRadians = 1.57079637f ≡ the 0x00454649 ctor literal. Exact.
  • Golden test values recomputed from the bytes: 4:3 → 1.27362 rad, 16:9 → 0.93624 rad, 21:9 → 0.70334 rad (test asserts 0.70327 at precision: 4 — both round to 0.7033).
  • DollCamera / CreatureAppraisalPresentation at π/4 match the byte-decoded CreatureMode::m_fFOVRadians default 0x3f490fdb.
  • Startup applies the real framebuffer aspect before the first frame.
  • TeleportViewPlaneController's 2·atan(1/d) and max(0.1, 0.25d) are correct against the bytes, contradicting the BN text.

Appendix — primary citations

Address Pseudo-C line Symbol / meaning
0x00452af00x00452b2f 9171791727 CreatureMode::Render FOV branch (fixed vs smartbox vs vdst)
0x00453ae60x00453b14 9265592660 SmartBox::RenderNormalMode smartbox site (no smartbox-flag gate)
0x00452380 91348 CreatureMode::UseSmartboxFOV — sets the flag to 1 (only writer)
0x00452390 91356 CreatureMode::UseSharpMode
0x004a5aa9 / 0x004e0440 / 0x004ee9bc 175534 / 228563 / 241997 paperdoll + char-gen viewports (fixed branch)
0x004d6db3 219378 gmSmartBoxUI::PostInit — portal space takes the smartbox law
0x004543de (in 93102 ctor) m_fFOVRadians = 0x3f490fdb = π/4 (byte-decoded)
0x00451be00x00451c18 9079290806 SmartBox::GetOverrideFovDistance = cot(fov/2)
0x00451bc0 90783 SmartBox::SetOverrideFovDistance
0x00451e60/0x00451e6a 9098890991 SmartBox::SetDefaultFov — degrees → radians
0x00454649 93302 SmartBox ctor m_fGameFOV = 1.57079637f
0x0054b240 342121 Render::set_vdstfov = 2·atan(1/d), znear rule, [0.001, π] clamp
0x0054b2d0 342158 Render::SetFOVRad — open (0, π) gate, silent reject
0x0054b340 342196 Render::SetFOVInternalvdst = ty / tan(fov/2) (vertical)
0x0059ab40 423666 PrimD3DRender::SetFOVInternalD3DXMatrixPerspectiveFovLH(fovy, aspect, …)
0x0054f150 345708 RenderDevice::ComputeAspectForViewport(w/h)·DAR·0.75 when arg6 == 0
0x0054f1c0/0x0054f22e 345720/345749 RenderDevice::SetViewport — writes m_ViewportAspectRatio (+0xa8)
0x0059fbd0 428476 RenderDeviceD3D::SetupDisplayAspectRatio — preference → m_DisplayAspectRatio
0x0054d850/0x0054d999 344244/344365 Render::UpdateFromPreferencesSetDefaultFov
0x0043e4f0 69105 SceneTool::PrepareGraphicsDeviceUpdateFromPreferences (per-frame)
0x004035b0/0x004043b2 2740/3261 gmClient::InitUIPreferencesSetPreferenceRange(10, 160)
0x0081efb8 / 0x0081efa8 1102208 ff. m_RenderPrefs.FieldOfView = 90, .AspectRatio = 1
0x004d65d5, 0x004d71c4, 0x004d725f, 0x004d73ce, 0x004d747f 219002219775 teleport-animation FOV override arm/disarm

Constants byte-read from the paired binary: [0x7948cc] = 0.1f (0x3dcccccd), [0x79b6dc] = 0.75f, [0x795344] = 0.0f, [0x7bdd30] = π (f64), [0x794720] = 0.001 (f64), [0x7928c0] = 1.0 (f64), [0x7ca9f0] = 0.4f, [0x7c8a04] = 0.25f.