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>
This commit is contained in:
parent
6b844c142f
commit
d13d63d0a5
11 changed files with 1032 additions and 21 deletions
|
|
@ -341,6 +341,9 @@ internal sealed class SessionPlayerCompositionPhase
|
|||
// CH6c: null when no retained UI exists (e.g. a no-window host) — the
|
||||
// Chat tab's opacity sliders then apply through NullRuntimeChatOpacityTarget.
|
||||
chatOpacity: interaction.RetainedUi?.Runtime.WindowOpacity,
|
||||
// #389 blast MUST-FIX 2: the live Field-of-View apply target —
|
||||
// see RuntimeSettingsTargets' cameras ctor doc.
|
||||
cameras: host.CameraController,
|
||||
log: d.Log,
|
||||
// OP6: null on a no-audio/headless host — ApplyAudio then
|
||||
// silently no-ops, same shape as chatOpacity above.
|
||||
|
|
|
|||
|
|
@ -9,11 +9,28 @@ namespace AcDream.App.Rendering;
|
|||
/// <code>
|
||||
/// Render::SetFOVRad(m_fGameFOV / (RenderDevice::m_ViewportAspectRatio - 0.1))
|
||||
/// </code>
|
||||
/// (<c>CreatureMode</c> smartbox sites <c>0x00452b2f</c> and <c>0x00453b14</c>;
|
||||
/// the same expression feeds a <c>tan</c> at <c>0x00451c0e</c>). The net
|
||||
/// effect: the HORIZONTAL view stays roughly constant (~85–90° of world at the
|
||||
/// 90° default) across aspect ratios while wide screens trim the vertical
|
||||
/// slice — 4:3 ≈ 73° vertical, 16:9 ≈ 53.6°, 21:9 ≈ 40°.
|
||||
/// (<c>CreatureMode</c> smartbox site <c>0x00452b2f</c> and
|
||||
/// <c>SmartBox::RenderNormalMode @0x00453b14</c> — the latter has NO
|
||||
/// <c>m_bUseSmartboxFOV</c> gate at all, so the world view is unconditionally
|
||||
/// on this law; the same expression feeds a <c>tan</c> at <c>0x00451c0e</c>).
|
||||
/// The applied value is decisively the VERTICAL FOV: the projection consumer
|
||||
/// is <c>D3DXMatrixPerspectiveFovLH</c> with it in the fovy slot
|
||||
/// (<c>0x0059ab71</c>; instruction-byte-verified by the 2026-08-13 mechanism
|
||||
/// review — the BN text FPU-elides every comparison in this area). The net
|
||||
/// effect: the HORIZONTAL view stays roughly constant across aspect ratios
|
||||
/// (89.0° at 4:3, 83.9° at 16:9, 80.6° at 21:9, at the 90° default) while
|
||||
/// wide screens trim the vertical slice — 4:3 ≈ 73° vertical,
|
||||
/// 16:9 ≈ 53.6°, 21:9 ≈ 40°.
|
||||
///
|
||||
/// <para>Aspect nuance (mechanism review M1, register row AD-90): retail's
|
||||
/// divisor aspect is not raw width/height —
|
||||
/// <c>RenderDevice::ComputeAspectForViewport @0x0054f150</c> yields
|
||||
/// <c>(w/h) × m_DisplayAspectRatio × 0.75</c>, where
|
||||
/// <c>m_DisplayAspectRatio</c> comes from the registered
|
||||
/// <c>Render.AspectRatio</c> preference. At that preference's DEFAULT
|
||||
/// (4:3 → factor exactly 1.0f) the expression collapses to raw w/h, which is
|
||||
/// what acdream uses; acdream has no AspectRatio preference, so a retail
|
||||
/// user who changed it would see a framing this port does not reproduce.</para>
|
||||
///
|
||||
/// <para><c>m_fGameFOV</c> is the user-facing number: ctor default
|
||||
/// <c>1.57079637f</c> = π/2 = 90° (<c>0x00454649</c>), set in DEGREES by the
|
||||
|
|
|
|||
|
|
@ -286,6 +286,7 @@ internal sealed class RuntimeSettingsTargets : IRuntimeSettingsTargets
|
|||
private readonly ICommandBus _commands;
|
||||
private readonly Action<string> _log;
|
||||
private readonly OpenAlAudioEngine? _audio;
|
||||
private readonly CameraController? _cameras;
|
||||
|
||||
public RuntimeSettingsTargets(
|
||||
IRuntimeDisplayWindowTarget displayWindow,
|
||||
|
|
@ -301,7 +302,14 @@ internal sealed class RuntimeSettingsTargets : IRuntimeSettingsTargets
|
|||
// ApplyAudio's doc. Optional/trailing so every pre-existing
|
||||
// construction site keeps compiling unchanged (matches
|
||||
// chatOpacity/log's own optional-trailing shape).
|
||||
OpenAlAudioEngine? audio = null)
|
||||
OpenAlAudioEngine? audio = null,
|
||||
// #389 blast-review MUST-FIX 2: retail's FOV preference applies LIVE
|
||||
// (Render::GRPCallback_OnRenderPreferenceChanged @0x0054d999 →
|
||||
// SmartBox::SetDefaultFov → m_fGameFOV, re-read by the smartbox
|
||||
// sites every render) — the saved Field of View must reach the
|
||||
// cameras on Save, not on the next launch. Null on hosts with no
|
||||
// camera graph (headless / fixture callers).
|
||||
CameraController? cameras = null)
|
||||
: this(
|
||||
displayWindow,
|
||||
new RuntimeQualityApplicationTarget(
|
||||
|
|
@ -317,7 +325,8 @@ internal sealed class RuntimeSettingsTargets : IRuntimeSettingsTargets
|
|||
chatOpacity is null
|
||||
? NullRuntimeChatOpacityTarget.Instance
|
||||
: new RuntimeChatOpacityTarget(chatOpacity),
|
||||
audio)
|
||||
audio,
|
||||
cameras)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -328,7 +337,8 @@ internal sealed class RuntimeSettingsTargets : IRuntimeSettingsTargets
|
|||
ICommandBus commands,
|
||||
Action<string>? log = null,
|
||||
IRuntimeChatOpacityTarget? chatOpacity = null,
|
||||
OpenAlAudioEngine? audio = null)
|
||||
OpenAlAudioEngine? audio = null,
|
||||
CameraController? cameras = null)
|
||||
{
|
||||
_displayWindow = displayWindow
|
||||
?? throw new ArgumentNullException(nameof(displayWindow));
|
||||
|
|
@ -338,10 +348,20 @@ internal sealed class RuntimeSettingsTargets : IRuntimeSettingsTargets
|
|||
_commands = commands ?? throw new ArgumentNullException(nameof(commands));
|
||||
_log = log ?? Console.WriteLine;
|
||||
_audio = audio;
|
||||
_cameras = cameras;
|
||||
}
|
||||
|
||||
public void ApplyDisplayWindowState(DisplaySettings display) =>
|
||||
public void ApplyDisplayWindowState(DisplaySettings display)
|
||||
{
|
||||
_displayWindow.Apply(display);
|
||||
// #389 blast MUST-FIX 2 (see the ctor's cameras doc): the Field of
|
||||
// View applies live on Save, from the update-phase save handler —
|
||||
// deliberately NOT from the render-phase preview seam
|
||||
// (WorldRenderFrameBuilder.Apply), whose mid-frame camera mutation
|
||||
// is the review's WATCH-3 cull-vs-raster landmine.
|
||||
if (_cameras is not null)
|
||||
RuntimeSettingsStartupTargets.ApplyFieldOfView(_cameras, display.FieldOfView);
|
||||
}
|
||||
|
||||
/// <summary>Campaign OP slice OP6: reuses the SAME static helper the
|
||||
/// startup path (<see cref="RuntimeSettingsStartupTargets.ApplyAudio"/>)
|
||||
|
|
|
|||
|
|
@ -575,10 +575,11 @@ public static class ConfigOptionsPageController
|
|||
storeOnly: true, // TS-74
|
||||
rangeLowKey: "ID_Graphics_Value_Slow", rangeHighKey: "ID_Graphics_Value_Fast");
|
||||
|
||||
// Field of View: NEXT-LAUNCH via DisplaySettings.FieldOfView + the
|
||||
// existing RuntimeSettingsController.ApplyStartup path — matches
|
||||
// the pre-existing (dev-tools Settings panel era) behaviour, not a
|
||||
// new gap this slice introduces.
|
||||
// Field of View: LIVE as of the #389 fix round — the stored value is
|
||||
// retail's m_fGameFOV in degrees and applies on Save through
|
||||
// RuntimeSettingsTargets.ApplyDisplayWindowState (retail applies its
|
||||
// FOV preference live too: Render::GRPCallback_OnRenderPreferenceChanged
|
||||
// @0x0054d999 → SmartBox::SetDefaultFov).
|
||||
BuildSliderRow(
|
||||
listBox, RangedSliderTemplateIndex, "ID_Graphics_FieldOfView",
|
||||
min: 10f, max: 160f, defaultValue: 90.0f, page, resolveString,
|
||||
|
|
|
|||
|
|
@ -20,10 +20,14 @@ public readonly record struct UiWindowPosition(float X, float Y);
|
|||
/// <see cref="Input.KeyBindings.LoadOrDefault"/> path.
|
||||
///
|
||||
/// <para>
|
||||
/// Schema (current version 2):
|
||||
/// Schema (current version 3 — v3 changed the MEANING of
|
||||
/// <c>display.fieldOfView</c>: it was the applied vertical FOV in degrees,
|
||||
/// it is now retail's <c>m_fGameFOV</c> in degrees (#389, the SmartboxFOV
|
||||
/// port). <see cref="LoadDisplay"/> migrates pre-v3 values on read — see
|
||||
/// <see cref="MigrateLegacyVerticalFovDegrees"/>):
|
||||
/// <code>
|
||||
/// {
|
||||
/// "version": 2,
|
||||
/// "version": 3,
|
||||
/// "display": { "resolution": "1920x1080", "fullscreen": false, ... }
|
||||
/// "windowLayouts": { "Character": { "1920x1080": { "chat": { ... } } } }
|
||||
/// }
|
||||
|
|
@ -39,7 +43,7 @@ public readonly record struct UiWindowPosition(float X, float Y);
|
|||
/// </summary>
|
||||
public sealed class SettingsStore
|
||||
{
|
||||
private const int CurrentSchemaVersion = 2;
|
||||
private const int CurrentSchemaVersion = 3;
|
||||
private readonly string _path;
|
||||
|
||||
public SettingsStore(string path)
|
||||
|
|
@ -66,11 +70,18 @@ public sealed class SettingsStore
|
|||
return DisplaySettings.Default;
|
||||
|
||||
var d = DisplaySettings.Default;
|
||||
// #389 (schema v3): a pre-v3 file's stored fieldOfView was the
|
||||
// applied vertical FOV; the same number now means retail's
|
||||
// gameFOV. Migrate on read — an absent key needs none (it falls
|
||||
// back to the already-v3 default).
|
||||
float fieldOfView = ReadFloat(disp, "fieldOfView", d.FieldOfView);
|
||||
if (ReadSchemaVersion(root) < 3 && disp.TryGetProperty("fieldOfView", out _))
|
||||
fieldOfView = MigrateLegacyVerticalFovDegrees(fieldOfView);
|
||||
return new DisplaySettings(
|
||||
Resolution: ReadString (disp, "resolution", d.Resolution),
|
||||
Fullscreen: ReadBool (disp, "fullscreen", d.Fullscreen),
|
||||
VSync: ReadBool (disp, "vsync", d.VSync),
|
||||
FieldOfView: ReadFloat (disp, "fieldOfView", d.FieldOfView),
|
||||
FieldOfView: fieldOfView,
|
||||
Gamma: ReadFloat (disp, "gamma", d.Gamma),
|
||||
ShowFps: ReadBool (disp, "showFps", d.ShowFps),
|
||||
Quality: ReadQuality (disp, "quality", d.Quality),
|
||||
|
|
@ -94,6 +105,38 @@ public sealed class SettingsStore
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>The file's top-level schema version; a file predating the
|
||||
/// version key reads as 1 (the same convention the v1 radar-layout
|
||||
/// migration below already uses).</summary>
|
||||
private static int ReadSchemaVersion(JsonElement root) =>
|
||||
root.TryGetProperty("version", out var v) && v.ValueKind == JsonValueKind.Number
|
||||
? v.GetInt32()
|
||||
: 1;
|
||||
|
||||
/// <summary>
|
||||
/// #389 blast-review MUST-FIX 1 — the v3 fieldOfView migration. The
|
||||
/// pre-v3 number was the applied VERTICAL FOV in degrees (written
|
||||
/// straight onto the cameras); the v3 number is retail's
|
||||
/// <c>m_fGameFOV</c> in degrees, run through the smartbox law. Two
|
||||
/// cases:
|
||||
/// - exactly the old default 60 → the new default 90: 60 was acdream's
|
||||
/// own invented constant, not a user preference, and mapping it to
|
||||
/// retail's registered default gives every untouched file the retail
|
||||
/// experience;
|
||||
/// - anything else was a DELIBERATE choice → preserve the user's
|
||||
/// visible framing at the era's default 16:9 aspect:
|
||||
/// gameFOV = vFOV × (16/9 − 0.1) (the smartbox law inverted at that
|
||||
/// aspect), clamped into the slider's registered [10,160].
|
||||
/// Runs on READ for any pre-v3 file; the next save stamps version 3 and
|
||||
/// the value stops migrating.
|
||||
/// </summary>
|
||||
internal static float MigrateLegacyVerticalFovDegrees(float legacyVerticalFovDegrees)
|
||||
{
|
||||
if (legacyVerticalFovDegrees == 60f)
|
||||
return 90f;
|
||||
return Math.Clamp(legacyVerticalFovDegrees * (16f / 9f - 0.1f), 10f, 160f);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save Display settings, preserving any other top-level keys the file
|
||||
/// already contains (e.g. an <c>audio</c> section written by a newer
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue