feat(rendering): port retail building degrade walk
Add exact retail building degrade selection, shared FPS/degrade ownership, complete-body gating, selected-shell submission, ordinary ladder mesh residency, frame-scoped retry rearm, Config controls, installed-DAT census, and lifecycle/allocation proofs. Reviews: OpenAI retail pass 3 PASS; OpenAI production pass 5 PASS. Gates: Release 0W/0E; focused 285/285; hermetic 16811/16811; InstalledDat 469 pass, 10 documented fail, 1 documented skip; both manifests 30/30.
This commit is contained in:
parent
d0f45f6a99
commit
c673f767e9
41 changed files with 1829 additions and 167 deletions
|
|
@ -40,6 +40,7 @@ internal sealed record FrameRootDependencies(
|
|||
EntityEffectPoseRegistry EffectPoses,
|
||||
WorldRenderRangeState RenderRange,
|
||||
RuntimeSettingsController Settings,
|
||||
BuildingDegradeController BuildingDegrades,
|
||||
DisplayFramePacingController DisplayFramePacing,
|
||||
WorldSceneDebugState WorldSceneDebugState,
|
||||
RetailAlphaQueue RetailAlphaQueue,
|
||||
|
|
@ -506,7 +507,8 @@ internal sealed class FrameRootCompositionPhase
|
|||
?? throw new InvalidOperationException(
|
||||
"The retail frame walk requires the landscape registry."),
|
||||
d.CellVisibility,
|
||||
d.PhysicsEngine.ShadowObjects),
|
||||
d.PhysicsEngine.ShadowObjects,
|
||||
d.BuildingDegrades),
|
||||
retailPViewPassExecutor),
|
||||
retailPViewCells,
|
||||
worldScenePasses,
|
||||
|
|
@ -798,7 +800,8 @@ internal sealed class FrameRootCompositionPhase
|
|||
privatePresentation,
|
||||
live.FrameDiagnostics,
|
||||
postDiagnostics,
|
||||
NullRenderFrameFailureRecovery.Instance);
|
||||
NullRenderFrameFailureRecovery.Instance,
|
||||
d.BuildingDegrades);
|
||||
Fault(FrameRootCompositionPoint.RenderRootCreated);
|
||||
|
||||
var liveFrameCoordinator = new RetailLiveFrameCoordinator(
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ internal sealed record InteractionRetainedUiDependencies(
|
|||
// Save button writes here, same file GameWindow's startup load reads.
|
||||
string KeyBindingsFilePath,
|
||||
RuntimeSettingsController Settings,
|
||||
BuildingDegradeController BuildingDegrades,
|
||||
GameRuntime Runtime,
|
||||
IRuntimeCombatAttackOperations CombatAttackOperations,
|
||||
RuntimeCombatTargetOperationsSlot CombatTargetOperations,
|
||||
|
|
@ -310,6 +311,18 @@ internal interface IInteractionRetainedUiCompositionFactory
|
|||
internal sealed class RetailInteractionRetainedUiCompositionFactory
|
||||
: IInteractionRetainedUiCompositionFactory
|
||||
{
|
||||
internal static FpsRuntimeBindings CreateFpsBindings(
|
||||
BuildingDegradeController buildingDegrades,
|
||||
Func<bool> isVisible)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(buildingDegrades);
|
||||
ArgumentNullException.ThrowIfNull(isVisible);
|
||||
return new FpsRuntimeBindings(
|
||||
() => buildingDegrades.Fps,
|
||||
() => buildingDegrades.ActiveMultiplier,
|
||||
isVisible);
|
||||
}
|
||||
|
||||
public IDisposable BindCombatTarget(
|
||||
InteractionRetainedUiDependencies d,
|
||||
DeferredSelectionUiAuthority selection) =>
|
||||
|
|
@ -787,9 +800,8 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
|||
d.ClientTime),
|
||||
JumpPowerbar: new JumpPowerbarRuntimeBindings(
|
||||
() => d.PlayerController.Controller?.JumpCharge ?? default),
|
||||
Fps: new FpsRuntimeBindings(
|
||||
() => d.FrameDiagnostics.Snapshot.Fps,
|
||||
() => 1.0,
|
||||
Fps: CreateFpsBindings(
|
||||
d.BuildingDegrades,
|
||||
() => d.Settings.DisplayPreview.ShowFps),
|
||||
VividTarget: new VividTargetRuntimeBindings(
|
||||
d.Actions.Selection,
|
||||
|
|
|
|||
173
src/AcDream.App/Rendering/BuildingDegradeController.cs
Normal file
173
src/AcDream.App/Rendering/BuildingDegradeController.cs
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
using AcDream.UI.Abstractions.Panels.Settings;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// Renderer-lifetime owner of retail's FPS history and automatic degrade
|
||||
/// multiplier. Scene/world replacement never recreates this owner.
|
||||
/// </summary>
|
||||
internal sealed class BuildingDegradeController : IBuildingDegradeFrameTick
|
||||
{
|
||||
internal const int FpsHistoryLength = 20;
|
||||
internal const int CandidateHistoryLength = 30;
|
||||
internal const float MinimumFps = 8f;
|
||||
internal const float IdealFps = 10f;
|
||||
internal const float MaximumFps = 20f;
|
||||
|
||||
private readonly Func<DisplaySettings> _settings;
|
||||
private readonly float[] _frameSeconds = new float[FpsHistoryLength];
|
||||
private readonly float[] _candidateHistory = new float[CandidateHistoryLength];
|
||||
private float _automaticMultiplier;
|
||||
private float _fps;
|
||||
|
||||
internal BuildingDegradeController(Func<DisplaySettings> settings)
|
||||
=> _settings = settings ?? throw new ArgumentNullException(nameof(settings));
|
||||
|
||||
internal float Fps => _fps;
|
||||
internal float AutomaticMultiplier => _automaticMultiplier;
|
||||
internal float DegradeDistance => _settings().DegradeDistance;
|
||||
internal float ActiveMultiplier
|
||||
{
|
||||
get
|
||||
{
|
||||
DisplaySettings settings = _settings();
|
||||
return settings.AutomaticDegrades
|
||||
? _automaticMultiplier
|
||||
: settings.GraphicsPerformance;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>SceneTool::UpdateFPSCounter followed by CalcDegLevel. The
|
||||
/// FPS sum observes the prior 20 samples; the just-finished frame is
|
||||
/// shifted in only after that calculation.</summary>
|
||||
internal void Tick(double elapsedSeconds)
|
||||
{
|
||||
double total = 0d;
|
||||
for (int i = 0; i < _frameSeconds.Length; i++)
|
||||
total += _frameSeconds[i];
|
||||
_fps = total > 0.000199999995f
|
||||
? (float)(FpsHistoryLength / total)
|
||||
: 0f;
|
||||
|
||||
AdvanceFrameHistory(_frameSeconds, (float)elapsedSeconds);
|
||||
_automaticMultiplier = AdvanceAutomaticMultiplier(
|
||||
_candidateHistory,
|
||||
_settings().AutomaticDegrades,
|
||||
_fps,
|
||||
_automaticMultiplier);
|
||||
}
|
||||
|
||||
void IBuildingDegradeFrameTick.Tick(double elapsedSeconds) => Tick(elapsedSeconds);
|
||||
|
||||
/// <summary>Retail's physical history direction: old slots 0..18 move
|
||||
/// to 1..19 and the just-finished binary32 duration is stored at slot 0.</summary>
|
||||
internal static void AdvanceFrameHistory(Span<float> history, float frameSeconds)
|
||||
{
|
||||
if (history.Length != FpsHistoryLength)
|
||||
throw new ArgumentException(
|
||||
$"Retail FPS history must contain exactly {FpsHistoryLength} slots.",
|
||||
nameof(history));
|
||||
|
||||
history[..^1].CopyTo(history[1..]);
|
||||
history[0] = frameSeconds;
|
||||
}
|
||||
|
||||
/// <summary>Literal state transition of <c>Render::CalcDegLevel</c>:
|
||||
/// shift first, compare the candidate against all thirty prior values,
|
||||
/// conditionally commit, then store the resulting current multiplier.</summary>
|
||||
internal static float AdvanceAutomaticMultiplier(
|
||||
Span<float> history,
|
||||
bool automatic,
|
||||
float fps,
|
||||
float current)
|
||||
{
|
||||
if (history.Length != CandidateHistoryLength)
|
||||
throw new ArgumentException(
|
||||
$"Retail degrade history must contain exactly {CandidateHistoryLength} slots.",
|
||||
nameof(history));
|
||||
|
||||
history[1..].CopyTo(history);
|
||||
if (!automatic)
|
||||
{
|
||||
history[^1] = current;
|
||||
return current;
|
||||
}
|
||||
|
||||
float candidate = CalculateCandidate(fps, current);
|
||||
bool stable = IsCandidateStable(history, candidate);
|
||||
|
||||
if (stable)
|
||||
current = candidate;
|
||||
history[^1] = current;
|
||||
return current;
|
||||
}
|
||||
|
||||
/// <summary>The x87 comparison promotes both binary32 stores and tests
|
||||
/// their wide difference against retail's qword 0.01 constant.</summary>
|
||||
internal static bool IsCandidateStable(ReadOnlySpan<float> history, float candidate)
|
||||
{
|
||||
if (history.Length != CandidateHistoryLength)
|
||||
throw new ArgumentException(
|
||||
$"Retail degrade history must contain exactly {CandidateHistoryLength} slots.",
|
||||
nameof(history));
|
||||
|
||||
for (int i = 0; i < CandidateHistoryLength; i++)
|
||||
{
|
||||
if (!(Math.Abs((double)history[i] - candidate) < 0.01))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>The exact five-weight candidate calculation from
|
||||
/// <c>Render::CalcDegLevel @0x0054CAF0</c>, separated so fixed-vector
|
||||
/// tests do not need to infer the math through controller history.</summary>
|
||||
internal static float CalculateCandidate(float fps, float current)
|
||||
{
|
||||
double fpsWide = fps;
|
||||
double minimumFps = MinimumFps;
|
||||
double idealFps = IdealFps;
|
||||
double maximumFps = MaximumFps;
|
||||
double w0 = LowShoulder(fpsWide, minimumFps * 0.75,
|
||||
(minimumFps + idealFps) * 0.5);
|
||||
double w1 = Triangle(fpsWide, minimumFps,
|
||||
minimumFps * 0.25 + idealFps * 0.75);
|
||||
double w2 = Triangle(fpsWide,
|
||||
(minimumFps + idealFps) * 0.5,
|
||||
(idealFps + maximumFps) * 0.5);
|
||||
double w3 = Triangle(fpsWide,
|
||||
idealFps * 0.75 + maximumFps * 0.25,
|
||||
maximumFps);
|
||||
double w4 = HighShoulder(fpsWide,
|
||||
(idealFps + maximumFps) * 0.5,
|
||||
maximumFps * 1.25);
|
||||
double weight = w0 + w1 + w2 + w3 + w4;
|
||||
float numeratorAfterW0 = (float)((double)-0.150000006f * w0);
|
||||
float numeratorAfterW1 = (float)(
|
||||
(double)numeratorAfterW0 - (double)0.02f * w1);
|
||||
float numeratorAfterW2 = (float)(
|
||||
(double)numeratorAfterW1 + (double)0f * w2);
|
||||
float numeratorAfterW3 = (float)(
|
||||
(double)numeratorAfterW2 + (double)0.01f * w3);
|
||||
double numerator = (double)numeratorAfterW3 + (double)0.1f * w4;
|
||||
double adjustment = weight > 0d
|
||||
? numerator / weight
|
||||
: 0d;
|
||||
double candidate = (double)current + adjustment;
|
||||
if (candidate > 1d)
|
||||
candidate = 1d;
|
||||
else if (candidate < -1d)
|
||||
candidate = -1d;
|
||||
return (float)candidate;
|
||||
}
|
||||
|
||||
private static double Triangle(double value, double low, double high)
|
||||
=> Math.Max(0d, 1d - Math.Abs(2d * value - (high + low)) / (high - low));
|
||||
|
||||
private static double LowShoulder(double value, double peak, double zero)
|
||||
=> value < peak ? 1d : Math.Max(0d, 1d - Math.Abs(value - peak) / (zero - peak));
|
||||
|
||||
private static double HighShoulder(double value, double zero, double peak)
|
||||
=> value > peak ? 1d : Math.Max(0d, 1d - Math.Abs(value - peak) / (peak - zero));
|
||||
}
|
||||
|
|
@ -193,6 +193,7 @@ public sealed class GameWindow :
|
|||
private Exception? _runFailure;
|
||||
private readonly DisplayFramePacingController _displayFramePacing;
|
||||
private readonly RuntimeSettingsController _runtimeSettings;
|
||||
private readonly BuildingDegradeController _buildingDegrades;
|
||||
|
||||
// Phase A.1: streaming fields replacing the one-shot _entities list.
|
||||
private AcDream.App.Streaming.LandblockStreamer? _streamer;
|
||||
|
|
@ -784,6 +785,8 @@ public sealed class GameWindow :
|
|||
_applicationPaths.SettingsFile),
|
||||
log: Console.WriteLine,
|
||||
characterOptionValue: _runtime.CharacterOwner.Options.GetOptionBit);
|
||||
_buildingDegrades = new BuildingDegradeController(
|
||||
() => _runtimeSettings.DisplayPreview);
|
||||
_animationDiagnostics = AnimationPresentationDiagnostics.FromEnvironment();
|
||||
_uiRegistry = uiRegistry;
|
||||
_renderPackRegistry = renderPackRegistry;
|
||||
|
|
@ -1552,6 +1555,7 @@ public sealed class GameWindow :
|
|||
_localPlayerTeleportSink,
|
||||
_applicationPaths.KeyBindingsFile,
|
||||
_runtimeSettings,
|
||||
_buildingDegrades,
|
||||
_runtime,
|
||||
_combatAttackOperations,
|
||||
_combatTargetOperations,
|
||||
|
|
@ -1746,6 +1750,7 @@ public sealed class GameWindow :
|
|||
_effectPoses,
|
||||
_renderRange,
|
||||
_runtimeSettings,
|
||||
_buildingDegrades,
|
||||
_displayFramePacing,
|
||||
_worldSceneDebugState,
|
||||
_retailAlphaQueue,
|
||||
|
|
|
|||
|
|
@ -52,6 +52,11 @@ internal interface IRenderFrameLifetime
|
|||
void EndFrame();
|
||||
}
|
||||
|
||||
internal interface IBuildingDegradeFrameTick
|
||||
{
|
||||
void Tick(double elapsedSeconds);
|
||||
}
|
||||
|
||||
internal interface IRenderFrameResourcePhase
|
||||
{
|
||||
void Prepare(RenderFrameInput input);
|
||||
|
|
@ -180,6 +185,7 @@ internal sealed class RenderFrameOrchestrator : IGameRenderFrameRoot
|
|||
private readonly IRenderFrameDiagnosticsPhase _diagnostics;
|
||||
private readonly IRenderFramePostDiagnosticsPhase _postDiagnostics;
|
||||
private readonly IRenderFrameFailureRecovery _recovery;
|
||||
private readonly IBuildingDegradeFrameTick? _buildingDegrades;
|
||||
|
||||
public RenderFrameOrchestrator(
|
||||
IRenderFrameLifetime lifetime,
|
||||
|
|
@ -189,7 +195,8 @@ internal sealed class RenderFrameOrchestrator : IGameRenderFrameRoot
|
|||
IPrivatePresentationFramePhase presentation,
|
||||
IRenderFrameDiagnosticsPhase diagnostics,
|
||||
IRenderFramePostDiagnosticsPhase postDiagnostics,
|
||||
IRenderFrameFailureRecovery recovery)
|
||||
IRenderFrameFailureRecovery recovery,
|
||||
IBuildingDegradeFrameTick? buildingDegrades = null)
|
||||
{
|
||||
_lifetime = lifetime ?? throw new ArgumentNullException(nameof(lifetime));
|
||||
_gpuMeasurement = gpuMeasurement
|
||||
|
|
@ -201,6 +208,7 @@ internal sealed class RenderFrameOrchestrator : IGameRenderFrameRoot
|
|||
_postDiagnostics = postDiagnostics
|
||||
?? throw new ArgumentNullException(nameof(postDiagnostics));
|
||||
_recovery = recovery ?? throw new ArgumentNullException(nameof(recovery));
|
||||
_buildingDegrades = buildingDegrades;
|
||||
}
|
||||
|
||||
public RenderFrameOutcome Render(RenderFrameInput input)
|
||||
|
|
@ -212,6 +220,7 @@ internal sealed class RenderFrameOrchestrator : IGameRenderFrameRoot
|
|||
if (input.ViewportWidth <= 0 || input.ViewportHeight <= 0)
|
||||
return RenderFrameOutcome.ZeroArea;
|
||||
|
||||
_buildingDegrades?.Tick(input.DeltaSeconds);
|
||||
_lifetime.BeginFrame();
|
||||
Exception? renderFailure = null;
|
||||
try
|
||||
|
|
|
|||
|
|
@ -72,7 +72,8 @@ internal sealed class RetailPViewRenderer
|
|||
Walk.WalkBuildingRegistry walkBuildings,
|
||||
Walk.WalkLandscapeAssembler walkLandscape,
|
||||
CellVisibility walkCellRegistry,
|
||||
ShadowObjectRegistry shadows)
|
||||
ShadowObjectRegistry shadows,
|
||||
BuildingDegradeController? buildingDegrades = null)
|
||||
{
|
||||
_renderSceneShadow = renderSceneShadow
|
||||
?? throw new ArgumentNullException(nameof(renderSceneShadow));
|
||||
|
|
@ -85,6 +86,9 @@ internal sealed class RetailPViewRenderer
|
|||
_walkWorldData = new Walk.WalkProductionWorldData(
|
||||
_walkBuildings,
|
||||
shadows ?? throw new ArgumentNullException(nameof(shadows)));
|
||||
_frameWalk = buildingDegrades is null
|
||||
? new Walk.RetailFrameWalk()
|
||||
: new Walk.RetailFrameWalk(buildingDegrades);
|
||||
_walkFlushLandscapeAction = FlushWalkLandscape;
|
||||
_walkClearInteriorDepthAction = ClearWalkInteriorDepth;
|
||||
_walkDrawExitSealsFunc = DrawWalkExitSeals;
|
||||
|
|
@ -518,7 +522,7 @@ internal sealed class RetailPViewRenderer
|
|||
}
|
||||
}
|
||||
|
||||
private readonly Walk.RetailFrameWalk _frameWalk = new();
|
||||
private readonly Walk.RetailFrameWalk _frameWalk;
|
||||
|
||||
/// <summary>S3 chunk 2 (§8.2 B3): the flush half of the former combined
|
||||
/// <c>ClearWalkInteriorDepth</c> — retail's <c>D3DPolyRender::FlushAlphaList(0f)</c>
|
||||
|
|
|
|||
|
|
@ -68,6 +68,9 @@ public interface IRetailFrameWalkContext : IWalkBuildingFrameContext
|
|||
/// </summary>
|
||||
public sealed class RetailFrameWalk
|
||||
{
|
||||
private readonly BuildingDegradeController? _degradation;
|
||||
private readonly float? _fixedDegradeDistance;
|
||||
private readonly float? _fixedDegradeMultiplier;
|
||||
// RenderDeviceD3D::Init @0x0059efb0: indoor_pview = PView(…, 1) — the
|
||||
// interior pview DRAWS the landscape through its surviving exit views.
|
||||
private readonly WalkPView _interiorPView = new() { DrawLandscape = true };
|
||||
|
|
@ -86,14 +89,19 @@ public sealed class RetailFrameWalk
|
|||
/// regardless of per-cell visibility.</summary>
|
||||
public bool AlwaysDrawObjects = true;
|
||||
|
||||
/// <summary>Retail global <c>Render::deg_mul</c> — DYNAMIC: the
|
||||
/// auto-tuner (<c>auto_update_deg_mul</c>) swings it with frame load
|
||||
/// (positive ⇒ degrade thresholds slide toward each level's max;
|
||||
/// negative ⇒ toward its min). The oracle captures pin ≈+0.99 for every
|
||||
/// fixture except doorway-still, whose capture ran under cdb load with
|
||||
/// the multiplier depressed (the recon session's live dump read −0.99
|
||||
/// under the same conditions).</summary>
|
||||
public float DegradeMultiplier = WalkBuilding.DefaultDegradeMultiplier;
|
||||
public RetailFrameWalk() { }
|
||||
|
||||
internal RetailFrameWalk(BuildingDegradeController degradation)
|
||||
=> _degradation = degradation ?? throw new ArgumentNullException(nameof(degradation));
|
||||
|
||||
/// <summary>Fixture-only capture-state constructor. Production always
|
||||
/// supplies the renderer-lifetime controller; historical retail captures
|
||||
/// retain their recorded preference/multiplier without becoming defaults.</summary>
|
||||
internal RetailFrameWalk(float degradeDistance, float degradeMultiplier)
|
||||
{
|
||||
_fixedDegradeDistance = degradeDistance;
|
||||
_fixedDegradeMultiplier = degradeMultiplier;
|
||||
}
|
||||
|
||||
/// <summary>FW4 slice 1: the interior root's <c>outside_view</c> — the
|
||||
/// exit-view polygons THIS walk's own <c>ConstructView</c> installed
|
||||
|
|
@ -272,8 +280,8 @@ public sealed class RetailFrameWalk
|
|||
/// portal pass, and the shell draw — sits inside retail's
|
||||
/// <c>if (part->gfxobj[part->deg_level] != 0)</c> @0x0059f2d3; a
|
||||
/// degraded-out slot draws NOTHING beyond the BLD event.
|
||||
/// <c>HasGeometry</c> + a non-null <c>SelectDrawingBsp</c> together model
|
||||
/// that one gate. Inside the gate, retail's own order
|
||||
/// The selected GfxObj id alone models that gate; its drawing BSP is an
|
||||
/// optional portal-walk input. Inside the gate, retail's own order
|
||||
/// (@0x0059f30b–0x0059f345) is <c>D3DPolyRender::FlushAlphaList(0f)</c> →
|
||||
/// <c>CPhysicsPart::Draw(parts, 1)</c> (the PORTAL flavor — the two-pass
|
||||
/// punch/look-in walk below) → <c>CPhysicsPart::Draw(parts, 0)</c> (the
|
||||
|
|
@ -285,47 +293,47 @@ public sealed class RetailFrameWalk
|
|||
IRetailFrameWalkContext ctx, IWalkEventSink sink)
|
||||
{
|
||||
sink.Emit(WalkEvent.Building(building.PositionCellId));
|
||||
if (!building.HasGeometry) return;
|
||||
// Retail walks the CURRENT degrade level's drawing BSP
|
||||
// (part->gfxobj[deg_level]); a degraded-out slot skips everything
|
||||
// after publishing the portal list.
|
||||
WalkBspNode? bsp = building.SelectDrawingBsp(
|
||||
WalkBuildingSelection selection = building.Select(
|
||||
ctx.ViewerDistanceTo(building),
|
||||
degradeMultiplier: DegradeMultiplier);
|
||||
if (bsp is null) return;
|
||||
_degradation?.DegradeDistance ?? _fixedDegradeDistance ?? 50f,
|
||||
_degradation?.ActiveMultiplier ?? _fixedDegradeMultiplier ?? 0f);
|
||||
if (selection.GfxObjId == 0)
|
||||
return;
|
||||
|
||||
// Additive (Campaign FW3.2b-1): the alpha barrier
|
||||
// (D3DPolyRender::FlushAlphaList(0f) @0x0059f30b) — gated by the
|
||||
// SAME part->gfxobj[deg_level]!=0 check as everything below it, so
|
||||
// this fires only now that both HasGeometry and the bsp lookup have
|
||||
// passed.
|
||||
// this fires after the selected-id gate, independently of whether
|
||||
// that selected object's drawing BSP exists.
|
||||
sink.OnBuildingTurn(building);
|
||||
|
||||
int viewCount = Math.Max(activeViews.ViewCount, 0);
|
||||
var passSink = new PortalPassSink(building, sink);
|
||||
Vector3 viewpoint = ctx.ViewpointInBuilding(building);
|
||||
for (int v = 0; v < viewCount; v++)
|
||||
if (selection.DrawingBsp is WalkBspNode bsp)
|
||||
{
|
||||
// Retail pins building_view = the CURRENT view for the whole
|
||||
// two-pass walk (DrawMeshInternal @0x0059f3bf) — the punch fans
|
||||
// it emits clip against THAT view, so the sink carries the index.
|
||||
passSink.ActiveViewIndex = v;
|
||||
ctx.SetActiveView(activeViews, v);
|
||||
WalkBuildingPortals.BuildDrawPortalsOnly(
|
||||
bsp, 1, viewpoint,
|
||||
(portalRef, pass) => WalkBuildingPortals.DrawPortal(
|
||||
_outdoorPView, building, portalRef, pass, ctx, passSink));
|
||||
WalkBuildingPortals.BuildDrawPortalsOnly(
|
||||
bsp, 2, viewpoint,
|
||||
(portalRef, pass) => WalkBuildingPortals.DrawPortal(
|
||||
_outdoorPView, building, portalRef, pass, ctx, passSink));
|
||||
int viewCount = Math.Max(activeViews.ViewCount, 0);
|
||||
var passSink = new PortalPassSink(building, sink);
|
||||
Vector3 viewpoint = ctx.ViewpointInBuilding(building);
|
||||
for (int v = 0; v < viewCount; v++)
|
||||
{
|
||||
// Retail pins building_view = the CURRENT view for the whole
|
||||
// two-pass walk (DrawMeshInternal @0x0059f3bf).
|
||||
passSink.ActiveViewIndex = v;
|
||||
ctx.SetActiveView(activeViews, v);
|
||||
WalkBuildingPortals.BuildDrawPortalsOnly(
|
||||
bsp, 1, viewpoint,
|
||||
(portalRef, pass) => WalkBuildingPortals.DrawPortal(
|
||||
_outdoorPView, building, portalRef, pass, ctx, passSink));
|
||||
WalkBuildingPortals.BuildDrawPortalsOnly(
|
||||
bsp, 2, viewpoint,
|
||||
(portalRef, pass) => WalkBuildingPortals.DrawPortal(
|
||||
_outdoorPView, building, portalRef, pass, ctx, passSink));
|
||||
}
|
||||
}
|
||||
|
||||
// Additive (Campaign FW3.2b-1): CPhysicsPart::Draw(parts, 0)
|
||||
// @0x0059f331 — the building's own shell mesh — runs AFTER the
|
||||
// portal walk completes (CPhysicsPart::Draw(parts, 1) just above),
|
||||
// not before it.
|
||||
sink.OnBuildingShellTurn(building);
|
||||
sink.OnBuildingShellTurn(building, selection);
|
||||
}
|
||||
|
||||
/// <summary>Returns the extracted cell-id array so <see cref="DrawInside"/>
|
||||
|
|
|
|||
|
|
@ -40,7 +40,21 @@ public sealed class WalkBspNode
|
|||
/// <summary>One degrade-ladder level: the drawing BSP of that level's
|
||||
/// GfxObj (portal-only view) and the level's authored distance bands.</summary>
|
||||
public readonly record struct WalkBuildingDegradeLevel(
|
||||
float MinDist, float IdealDist, float MaxDist, WalkBspNode? DrawingBsp);
|
||||
uint GfxObjId,
|
||||
uint Mode,
|
||||
float MinDist,
|
||||
float IdealDist,
|
||||
float MaxDist,
|
||||
WalkBspNode? DrawingBsp);
|
||||
|
||||
/// <summary>The complete result of retail's part-zero degrade selection.
|
||||
/// A nonzero GfxObj id is drawable even when its drawing BSP is null; the
|
||||
/// BSP is only the optional portal-walk input.</summary>
|
||||
public readonly record struct WalkBuildingSelection(
|
||||
uint GfxObjId,
|
||||
WalkBspNode? DrawingBsp,
|
||||
int Level,
|
||||
uint Mode);
|
||||
|
||||
/// <summary>The walk's building model (retail <c>CBuildingObj</c> +
|
||||
/// <c>BuildInfo</c> as the frame walk consumes them).</summary>
|
||||
|
|
@ -56,6 +70,11 @@ public sealed class WalkBuilding
|
|||
/// Used directly when the model has no degrade ladder.</summary>
|
||||
public WalkBspNode? DrawingBsp;
|
||||
|
||||
/// <summary>Part zero's direct/base GfxObj. Zero is retail's authored
|
||||
/// complete-body sentinel and suppresses the alpha, portal and shell
|
||||
/// turns after the unconditional BLD publication.</summary>
|
||||
public uint GfxObjId;
|
||||
|
||||
/// <summary>The degrade ladder (near→far). Retail walks the CURRENT
|
||||
/// level's drawing BSP (<c>part->gfxobj[deg_level]</c>) — and building
|
||||
/// degrade models beyond the first band carry NO portal nodes, which is
|
||||
|
|
@ -63,27 +82,18 @@ public sealed class WalkBuilding
|
|||
/// every Holtburg building has ports at level 0 only).</summary>
|
||||
public WalkBuildingDegradeLevel[] DegradeLevels = [];
|
||||
|
||||
/// <summary><c>part->gfxobj[deg_level] != 0</c> — a degraded-out slot
|
||||
/// skips the whole building AFTER publishing the portal list.</summary>
|
||||
public bool HasGeometry = true;
|
||||
|
||||
/// <summary>The base GfxObj's sort center — retail measures the viewer
|
||||
/// distance to the part's SCALED sort center, not the position origin
|
||||
/// (<c>CPhysicsPart::UpdateViewerDistance</c> @0x0050e030).</summary>
|
||||
public Vector3 SortCenter;
|
||||
|
||||
/// <summary>Retail <c>Render::s_rDegradeDistance</c> — a subtractive
|
||||
/// slack before the ladder applies (live-dumped 100 on the capture
|
||||
/// client; registry-configurable).</summary>
|
||||
public const float DefaultDegradeDistance = 100f;
|
||||
/// <summary>Part-zero Setup resting/default-scale transform. Direct Gfx
|
||||
/// buildings use identity.</summary>
|
||||
public Matrix4x4 PartZeroTransform = Matrix4x4.Identity;
|
||||
|
||||
/// <summary>The live capture client's <c>Render::deg_mul</c> arm:
|
||||
/// magnitude 0.99 pinned by the walkout-F2 fixture (building a9b4001e,
|
||||
/// effective 30.3, level 0 ideal/max 24/48 — retail floods, so its
|
||||
/// threshold ≈ max ⇒ the POSITIVE arm; the recon note's "−0.99" sign was
|
||||
/// a misread — the negative arm's threshold ≈ min contradicts the
|
||||
/// fixture from both directions). Re-dump at the next retail session.</summary>
|
||||
public const float DefaultDegradeMultiplier = 0.99f;
|
||||
/// <summary>The selected part scale's Z component used by retail's
|
||||
/// UpdateViewerDistance division.</summary>
|
||||
public float PartZeroScaleZ = 1f;
|
||||
|
||||
/// <summary>
|
||||
/// <c>GfxObjDegradeInfo::get_degrade</c> @0x0051e4b0 (BN flag-mush on
|
||||
|
|
@ -96,22 +106,41 @@ public sealed class WalkBuilding
|
|||
/// mul is <c>Render::deg_mul</c> when <c>auto_update_deg_mul</c> is on,
|
||||
/// else <c>s_rUserSuppliedDegradeBias</c>.
|
||||
/// </summary>
|
||||
public WalkBspNode? SelectDrawingBsp(
|
||||
public WalkBuildingSelection Select(
|
||||
float viewerDistance,
|
||||
float degradeDistance = DefaultDegradeDistance,
|
||||
float degradeMultiplier = DefaultDegradeMultiplier)
|
||||
float degradeDistance,
|
||||
float degradeMultiplier,
|
||||
bool degradesDisabled = false,
|
||||
int forcedLevel = -1)
|
||||
{
|
||||
if (DegradeLevels.Length == 0) return DrawingBsp;
|
||||
if (DegradeLevels.Length == 0)
|
||||
return new WalkBuildingSelection(GfxObjId, DrawingBsp, 0, 1u);
|
||||
if (degradesDisabled)
|
||||
return SelectionAt(0);
|
||||
if (forcedLevel >= 0)
|
||||
return SelectionAt(Math.Min(forcedLevel, DegradeLevels.Length - 1));
|
||||
|
||||
float effective = MathF.Max(0f, MathF.Abs(viewerDistance) - degradeDistance);
|
||||
foreach (WalkBuildingDegradeLevel level in DegradeLevels)
|
||||
for (int i = 0; i < DegradeLevels.Length; i++)
|
||||
{
|
||||
float threshold = degradeMultiplier >= 0f
|
||||
? level.IdealDist - (level.IdealDist - level.MaxDist) * degradeMultiplier
|
||||
: level.IdealDist + (level.IdealDist - level.MinDist) * degradeMultiplier;
|
||||
WalkBuildingDegradeLevel level = DegradeLevels[i];
|
||||
double threshold = degradeMultiplier >= 0f
|
||||
? (double)level.IdealDist
|
||||
- ((double)level.IdealDist - level.MaxDist) * degradeMultiplier
|
||||
: (double)level.IdealDist
|
||||
+ ((double)level.IdealDist - level.MinDist) * degradeMultiplier;
|
||||
if (effective < threshold)
|
||||
return level.DrawingBsp;
|
||||
return new WalkBuildingSelection(
|
||||
level.GfxObjId, level.DrawingBsp, i, level.Mode);
|
||||
}
|
||||
return SelectionAt(DegradeLevels.Length - 1);
|
||||
|
||||
WalkBuildingSelection SelectionAt(int index)
|
||||
{
|
||||
WalkBuildingDegradeLevel level = DegradeLevels[index];
|
||||
return new WalkBuildingSelection(
|
||||
level.GfxObjId, level.DrawingBsp, index, level.Mode);
|
||||
}
|
||||
return DegradeLevels[^1].DrawingBsp;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
using System.Numerics;
|
||||
using AcDream.Content;
|
||||
using AcDream.Core.Meshing;
|
||||
using AcDream.Core.World;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Enums;
|
||||
using DatReaderWriter.Types;
|
||||
|
|
@ -37,7 +39,21 @@ public static class WalkBuildingFactory
|
|||
/// placement transform (<paramref name="WorldTransform"/> already has the
|
||||
/// caller's block offset baked in — see <see cref="Build"/>).</summary>
|
||||
public sealed record Entry(
|
||||
WalkBuilding Building, Matrix4x4 WorldTransform, Matrix4x4 InverseWorldTransform);
|
||||
WalkBuilding Building, Matrix4x4 WorldTransform, Matrix4x4 InverseWorldTransform)
|
||||
{
|
||||
public Matrix4x4 PartZeroWorldTransform { get; } =
|
||||
Building.PartZeroTransform * WorldTransform;
|
||||
|
||||
public Matrix4x4 InversePartZeroWorldTransform { get; } =
|
||||
Invert(Building.PartZeroTransform * WorldTransform);
|
||||
|
||||
private static Matrix4x4 Invert(Matrix4x4 value)
|
||||
{
|
||||
if (!Matrix4x4.Invert(value, out Matrix4x4 inverse))
|
||||
throw new InvalidOperationException("A building part-zero transform is not invertible.");
|
||||
return inverse;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Builds every building of one landblock. <paramref name="lbOffset"/>
|
||||
/// is the SAME landblock-local world offset every other production
|
||||
|
|
@ -84,8 +100,32 @@ public static class WalkBuildingFactory
|
|||
|
||||
WalkBspNode? bsp = null;
|
||||
Vector3 sortCenter = Vector3.Zero;
|
||||
uint partZeroGfxObjId = 0;
|
||||
Matrix4x4 partZeroTransform = Matrix4x4.Identity;
|
||||
float partZeroScaleZ = 1f;
|
||||
var degradeLevels = new List<WalkBuildingDegradeLevel>();
|
||||
if (dats.Get<GfxObj>(buildingInfo.ModelId) is GfxObj gfxObj)
|
||||
if (dats.Get<GfxObj>(buildingInfo.ModelId) is GfxObj directGfxObj)
|
||||
{
|
||||
partZeroGfxObjId = buildingInfo.ModelId;
|
||||
PopulateGfx(directGfxObj);
|
||||
}
|
||||
else if (dats.Get<Setup>(buildingInfo.ModelId) is Setup setup)
|
||||
{
|
||||
IReadOnlyList<MeshRef> parts = SetupMesh.Flatten(setup);
|
||||
if (parts.Count > 0)
|
||||
{
|
||||
MeshRef partZero = parts[0];
|
||||
partZeroGfxObjId = partZero.GfxObjId;
|
||||
partZeroTransform = partZero.PartTransform;
|
||||
partZeroScaleZ = setup.DefaultScale.Count > 0
|
||||
? setup.DefaultScale[0].Z
|
||||
: 1f;
|
||||
if (dats.Get<GfxObj>(partZero.GfxObjId) is GfxObj setupGfxObj)
|
||||
PopulateGfx(setupGfxObj);
|
||||
}
|
||||
}
|
||||
|
||||
void PopulateGfx(GfxObj gfxObj)
|
||||
{
|
||||
bsp = ConvertDrawingBsp(gfxObj, gfxObj.DrawingBSP?.Root);
|
||||
sortCenter = new Vector3(gfxObj.SortCenter.X, gfxObj.SortCenter.Y, gfxObj.SortCenter.Z);
|
||||
|
|
@ -102,7 +142,13 @@ public static class WalkBuildingFactory
|
|||
levelBsp = ConvertDrawingBsp(levelGfx, levelGfx.DrawingBSP?.Root);
|
||||
}
|
||||
degradeLevels.Add(
|
||||
new WalkBuildingDegradeLevel(level.MinDist, level.IdealDist, level.MaxDist, levelBsp));
|
||||
new WalkBuildingDegradeLevel(
|
||||
(uint)level.Id,
|
||||
level.DegradeMode,
|
||||
level.MinDist,
|
||||
level.IdealDist,
|
||||
level.MaxDist,
|
||||
levelBsp));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -116,9 +162,12 @@ public static class WalkBuildingFactory
|
|||
{
|
||||
PositionCellId = positionCellId,
|
||||
Portals = portals,
|
||||
GfxObjId = partZeroGfxObjId,
|
||||
DrawingBsp = bsp,
|
||||
DegradeLevels = degradeLevels.ToArray(),
|
||||
SortCenter = sortCenter,
|
||||
PartZeroTransform = partZeroTransform,
|
||||
PartZeroScaleZ = partZeroScaleZ,
|
||||
},
|
||||
worldTransform,
|
||||
inverse));
|
||||
|
|
|
|||
|
|
@ -176,9 +176,8 @@ public interface IWalkEventSink
|
|||
/// draw) in <c>if (part->gfxobj[part->deg_level] != 0)</c>
|
||||
/// @0x0059f2d3 — a degraded-out slot draws NOTHING beyond the
|
||||
/// unconditional <see cref="WalkEventKind.Building"/> <see cref="Emit"/>
|
||||
/// call. <see cref="WalkBuilding.HasGeometry"/> and a non-null
|
||||
/// <see cref="WalkBuilding.SelectDrawingBsp"/> result together model that
|
||||
/// one gate, so this hook fires only after both have passed. This is the
|
||||
/// call. The selected GfxObj id alone models that gate; a null drawing
|
||||
/// BSP only suppresses the portal sub-walk. This is the
|
||||
/// ALPHA BARRIER turn (<c>D3DPolyRender::FlushAlphaList(0f)</c>
|
||||
/// @0x0059f30b) — retail's own order runs it BEFORE the portal pass
|
||||
/// (<c>CPhysicsPart::Draw(parts, 1)</c>), which in turn runs BEFORE the
|
||||
|
|
@ -198,7 +197,9 @@ public interface IWalkEventSink
|
|||
/// fires when <see cref="OnBuildingTurn"/> also fired (same gate; see its
|
||||
/// doc comment) — a degraded-out slot reaches neither. Default no-op.
|
||||
/// </summary>
|
||||
void OnBuildingShellTurn(WalkBuilding building) { }
|
||||
void OnBuildingShellTurn(
|
||||
WalkBuilding building,
|
||||
WalkBuildingSelection selection) { }
|
||||
|
||||
/// <summary>
|
||||
/// Fires when the two-pass portal machinery
|
||||
|
|
|
|||
|
|
@ -81,7 +81,8 @@ internal interface IWalkFrameWorldData
|
|||
/// <summary>Building-local → world, for transforming a punch polygon
|
||||
/// before <see cref="IWalkFrameLeafRenderer.DrawPunchFan"/> — the
|
||||
/// production implementation is <see cref="WalkBuildingRegistry.TryGetEntry"/>'s
|
||||
/// <c>WorldTransform</c> (FW3.2b-2 wiring).</summary>
|
||||
/// <c>PartZeroWorldTransform</c> (the Setup part-zero frame already composed
|
||||
/// with the building root).</summary>
|
||||
Matrix4x4 GetBuildingWorldTransform(WalkBuilding building);
|
||||
}
|
||||
|
||||
|
|
@ -1590,7 +1591,9 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
_currentDcStage = WalkDrawStage.LookInStatic;
|
||||
}
|
||||
|
||||
void IWalkEventSink.OnBuildingShellTurn(WalkBuilding building)
|
||||
void IWalkEventSink.OnBuildingShellTurn(
|
||||
WalkBuilding building,
|
||||
WalkBuildingSelection selection)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(building);
|
||||
RequireOpenFrame();
|
||||
|
|
@ -1602,10 +1605,27 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
// look-in flood appended (keeps every range single-stage).
|
||||
MarkIfGrown();
|
||||
WalkFrameStaticRecords shell = _worldData.GetBuildingShellStatics(building);
|
||||
_populator.PopulateCell(
|
||||
_stream, WalkDrawStage.BuildingShell, building.PositionCellId,
|
||||
shell.Records, shell.TupleLandblockId, _cameraWorldPosition, _viewProjection,
|
||||
alphaSubmissions: _alphaSubmissions);
|
||||
if (shell.Records.Count > 1)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Building 0x{building.PositionCellId:X8} has "
|
||||
+ $"{shell.Records.Count} retained shell records; expected at most one.");
|
||||
}
|
||||
if (shell.Records.Count == 1)
|
||||
{
|
||||
ref readonly RenderProjectionRecord record =
|
||||
ref shell.Records.AsSpan()[0];
|
||||
_populator.PopulateBuildingShell(
|
||||
_stream,
|
||||
building.PositionCellId,
|
||||
in record,
|
||||
shell.TupleLandblockId,
|
||||
_cameraWorldPosition,
|
||||
_viewProjection,
|
||||
in selection,
|
||||
building.PartZeroTransform,
|
||||
_alphaSubmissions);
|
||||
}
|
||||
if (_alphaSubmissions.Count != _alphaSubmitMark)
|
||||
{
|
||||
MarkIfGrown();
|
||||
|
|
|
|||
|
|
@ -173,21 +173,24 @@ public sealed class WalkProductionFrameContext : IWalkFrameContext, IRetailFrame
|
|||
}
|
||||
|
||||
public Vector3 ViewpointInBuilding(WalkBuilding building)
|
||||
=> Vector3.Transform(WorldViewpoint, GetEntry(building).InverseWorldTransform);
|
||||
=> Vector3.Transform(WorldViewpoint, GetEntry(building).InversePartZeroWorldTransform);
|
||||
|
||||
/// <summary><c>CPhysicsPart::UpdateViewerDistance</c> @0x0050e030: the
|
||||
/// distance to the part's SCALED sort center, not the position origin.</summary>
|
||||
public float ViewerDistanceTo(WalkBuilding building)
|
||||
{
|
||||
WalkBuildingFactory.Entry entry = GetEntry(building);
|
||||
return Vector3.Distance(
|
||||
WorldViewpoint, Vector3.Transform(building.SortCenter, entry.WorldTransform));
|
||||
float distance = Vector3.Distance(
|
||||
WorldViewpoint,
|
||||
Vector3.Transform(building.SortCenter, entry.PartZeroWorldTransform));
|
||||
return distance / building.PartZeroScaleZ;
|
||||
}
|
||||
|
||||
public int ClipBuildingPolygon(
|
||||
WalkBuilding building, WalkPolygon polygon, int side, Span<WalkScreenPoint> output)
|
||||
{
|
||||
Matrix4x4 objectToClip = GetEntry(building).WorldTransform * _viewProjection;
|
||||
Matrix4x4 objectToClip =
|
||||
GetEntry(building).PartZeroWorldTransform * _viewProjection;
|
||||
Span<WalkScreenPoint> projected = stackalloc WalkScreenPoint[polygon.Vertices.Length];
|
||||
for (int i = 0; i < polygon.Vertices.Length; i++)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -543,6 +543,6 @@ internal sealed class WalkProductionWorldData : IWalkFrameWorldData
|
|||
throw new InvalidOperationException(
|
||||
$"walk building 0x{building.PositionCellId:X8} has no committed registry entry");
|
||||
}
|
||||
return entry.WorldTransform;
|
||||
return entry.PartZeroWorldTransform;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,6 +82,34 @@ internal sealed class WalkStaticStreamPopulator
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>Classifies exactly the selected part-zero GfxObj against the
|
||||
/// one retained building-shell record. Its retained identity/material/
|
||||
/// palette/effect/picking payload is borrowed unchanged; only the mesh
|
||||
/// id and precomputed part-zero transform come from degrade selection.</summary>
|
||||
internal void PopulateBuildingShell(
|
||||
OrderedDrawStream stream,
|
||||
uint cellId,
|
||||
in RenderProjectionRecord record,
|
||||
uint tupleLandblockId,
|
||||
Vector3 cameraWorldPosition,
|
||||
Matrix4x4 viewProjection,
|
||||
in WalkBuildingSelection selection,
|
||||
Matrix4x4 partZeroTransform,
|
||||
List<WbDrawDispatcher.WalkClassifiedBatch> alphaSubmissions)
|
||||
{
|
||||
ClassifyAndAppend(
|
||||
stream,
|
||||
WalkDrawStage.BuildingShell,
|
||||
cellId,
|
||||
in record,
|
||||
tupleLandblockId,
|
||||
cameraWorldPosition,
|
||||
viewProjection,
|
||||
alphaSubmissions: alphaSubmissions,
|
||||
buildingSelection: selection,
|
||||
buildingPartTransform: partZeroTransform);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The landscape entry point: outdoor static content
|
||||
/// (<c>RenderProjectionClass.OutdoorStatic</c> —
|
||||
|
|
@ -275,7 +303,9 @@ internal sealed class WalkStaticStreamPopulator
|
|||
bool liveDynamic = false,
|
||||
IWalkLookInViewSource? lookInViews = null,
|
||||
int lookInRouteIndex = -1,
|
||||
List<WbDrawDispatcher.WalkClassifiedBatch>? alphaSubmissions = null)
|
||||
List<WbDrawDispatcher.WalkClassifiedBatch>? alphaSubmissions = null,
|
||||
WalkBuildingSelection? buildingSelection = null,
|
||||
Matrix4x4 buildingPartTransform = default)
|
||||
{
|
||||
_batchScratch.Clear();
|
||||
_selectionScratch.Clear();
|
||||
|
|
@ -288,7 +318,9 @@ internal sealed class WalkStaticStreamPopulator
|
|||
lookInViews,
|
||||
lookInRouteIndex,
|
||||
cellId,
|
||||
diagnosticViewProjection: viewProjection);
|
||||
diagnosticViewProjection: viewProjection,
|
||||
buildingSelection: buildingSelection,
|
||||
buildingPartTransform: buildingPartTransform);
|
||||
|
||||
for (int i = 0; i < _batchScratch.Count; i++)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ public sealed class EnvCellLandblockBuild
|
|||
VisibilityCells = visibilityCells.ToImmutableArray();
|
||||
Shells = shells.ToImmutableArray();
|
||||
WalkBuildings = (walkBuildings ?? Enumerable.Empty<WalkBuildingFactory.Entry>()).ToImmutableArray();
|
||||
WalkBuildingMeshDependencies = CollectWalkBuildingMeshDependencies(WalkBuildings);
|
||||
WalkMaxZ = walkMaxZ;
|
||||
WalkMinZ = walkMinZ;
|
||||
|
||||
|
|
@ -67,6 +68,14 @@ public sealed class EnvCellLandblockBuild
|
|||
/// <c>LandblockRenderPublisher.AdvanceCompleteOne</c>.</summary>
|
||||
public ImmutableArray<WalkBuildingFactory.Entry> WalkBuildings { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Unique nonzero GfxObj ids referenced by the buildings' degrade ladders.
|
||||
/// This worker-built immutable carrier is acquired as ordinary mesh
|
||||
/// ownership with the containing near-tier landblock; EnvCell shell ids
|
||||
/// remain on the separate prepared-geometry path.
|
||||
/// </summary>
|
||||
public ImmutableArray<ulong> WalkBuildingMeshDependencies { get; }
|
||||
|
||||
/// <summary>Campaign FW3.1: this landblock's retail z-slab
|
||||
/// (<c>heightTable[maxByte] + 200</c>) — the walk landscape's per-block
|
||||
/// visibility bound (<c>WalkLandscapeAssembler.PublishLandblock</c>).
|
||||
|
|
@ -77,6 +86,24 @@ public sealed class EnvCellLandblockBuild
|
|||
/// <summary>Campaign FW3.1: this landblock's retail z-slab
|
||||
/// (<c>heightTable[minByte] − 1</c>). See <see cref="WalkMaxZ"/>.</summary>
|
||||
public float WalkMinZ { get; }
|
||||
|
||||
private static ImmutableArray<ulong> CollectWalkBuildingMeshDependencies(
|
||||
ImmutableArray<WalkBuildingFactory.Entry> buildings)
|
||||
{
|
||||
var seen = new HashSet<uint>();
|
||||
var dependencies = ImmutableArray.CreateBuilder<ulong>();
|
||||
for (int buildingIndex = 0; buildingIndex < buildings.Length; buildingIndex++)
|
||||
{
|
||||
WalkBuildingDegradeLevel[] levels = buildings[buildingIndex].Building.DegradeLevels;
|
||||
for (int levelIndex = 0; levelIndex < levels.Length; levelIndex++)
|
||||
{
|
||||
uint id = levels[levelIndex].GfxObjId;
|
||||
if (id != 0 && seen.Add(id))
|
||||
dependencies.Add(id);
|
||||
}
|
||||
}
|
||||
return dependencies.ToImmutable();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -76,7 +76,9 @@ public sealed class LandblockSpawnAdapter
|
|||
/// </summary>
|
||||
public void OnLandblockLoaded(
|
||||
LoadedLandblock landblock,
|
||||
IEnumerable<ulong>? additionalReadinessIds = null)
|
||||
IEnumerable<ulong>? additionalReadinessIds = null,
|
||||
IEnumerable<ulong>? additionalOrdinaryIds = null,
|
||||
bool replaceExisting = false)
|
||||
{
|
||||
System.ArgumentNullException.ThrowIfNull(landblock);
|
||||
|
||||
|
|
@ -90,6 +92,8 @@ public sealed class LandblockSpawnAdapter
|
|||
foreach (var meshRef in entity.MeshRefs)
|
||||
unique.Add((ulong)meshRef.GfxObjId);
|
||||
}
|
||||
if (additionalOrdinaryIds is not null)
|
||||
unique.UnionWith(additionalOrdinaryIds.Where(static id => id != 0));
|
||||
|
||||
HashSet<ulong>? preparedIds = additionalReadinessIds is null
|
||||
? null
|
||||
|
|
@ -110,6 +114,11 @@ public sealed class LandblockSpawnAdapter
|
|||
MarkAllUndesired(registration.Prepared);
|
||||
registration.WantsLoaded = true;
|
||||
}
|
||||
else if (replaceExisting)
|
||||
{
|
||||
MarkAllUndesired(registration.Ordinary);
|
||||
MarkAllUndesired(registration.Prepared);
|
||||
}
|
||||
|
||||
MarkDesired(registration.Ordinary, unique);
|
||||
if (preparedIds is not null)
|
||||
|
|
|
|||
|
|
@ -47,11 +47,9 @@ namespace AcDream.App.Rendering.Wb;
|
|||
/// (<c>TranslucencyFadeManager</c>/<c>EntityOpacity</c>) is NOT threaded
|
||||
/// through: that mechanic is <c>TransparentPartHook</c>, a retail LIVE-entity
|
||||
/// behavior keyed by ServerGuid, not something world statics undergo — every
|
||||
/// classified batch here carries <c>Alpha = 1f</c>. Likewise the async
|
||||
/// mesh-miss self-heal request (<c>_missRequested</c>/<c>EnsureLoaded</c>,
|
||||
/// frame-scoped state cleared by <c>BeginEntityDispatch</c>) is not fired
|
||||
/// here — this seam has no production frame to be scoped to yet; a missing
|
||||
/// mesh is simply skipped, matching every other unwired FW2/FW3.2a path.</para>
|
||||
/// classified batch here carries <c>Alpha = 1f</c>. A mesh miss uses the
|
||||
/// existing frame-scoped <c>_missRequested</c>/<c>EnsureLoaded</c> seam and
|
||||
/// skips this frame; the caller retries naturally on the following frame.</para>
|
||||
/// </summary>
|
||||
public sealed partial class WbDrawDispatcher
|
||||
{
|
||||
|
|
@ -93,6 +91,7 @@ public sealed partial class WbDrawDispatcher
|
|||
"A walk part frame was opened before the previous scope closed.");
|
||||
}
|
||||
|
||||
_missRequested.Clear();
|
||||
_walkDrawnParts.Clear();
|
||||
BeginFacilityStairSubmissionProbeFrame();
|
||||
_walkPartFrameActive = true;
|
||||
|
|
@ -274,7 +273,9 @@ public sealed partial class WbDrawDispatcher
|
|||
IWalkLookInViewSource? lookInViews = null,
|
||||
int lookInRouteIndex = -1,
|
||||
uint lookInCellId = 0,
|
||||
Matrix4x4 diagnosticViewProjection = default)
|
||||
Matrix4x4 diagnosticViewProjection = default,
|
||||
WalkBuildingSelection? buildingSelection = null,
|
||||
Matrix4x4 buildingPartTransform = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(batches);
|
||||
ArgumentNullException.ThrowIfNull(selectionParts);
|
||||
|
|
@ -316,12 +317,22 @@ public sealed partial class WbDrawDispatcher
|
|||
paletteIdentity = TextureCache.GetPaletteIdentity(entity.PaletteOverride);
|
||||
|
||||
IReadOnlyList<MeshRef>? meshRefs = projection.EntityPayload.MeshRefs;
|
||||
if (meshRefs is null)
|
||||
if (buildingSelection is null && meshRefs is null)
|
||||
return;
|
||||
|
||||
for (int partIndex = 0; partIndex < meshRefs.Count; partIndex++)
|
||||
IReadOnlyDictionary<uint, uint>? buildingSurfaceOverrides =
|
||||
buildingSelection is not null && meshRefs is { Count: > 0 }
|
||||
? meshRefs[0].SurfaceOverrides
|
||||
: null;
|
||||
int meshRefCount = buildingSelection is null ? meshRefs!.Count : 1;
|
||||
for (int partIndex = 0; partIndex < meshRefCount; partIndex++)
|
||||
{
|
||||
MeshRef meshRef = meshRefs[partIndex];
|
||||
MeshRef meshRef = buildingSelection is WalkBuildingSelection selected
|
||||
? new MeshRef(selected.GfxObjId, buildingPartTransform)
|
||||
{
|
||||
SurfaceOverrides = buildingSurfaceOverrides,
|
||||
}
|
||||
: meshRefs![partIndex];
|
||||
if (_meshAdapter.IsRuntimeHiddenMarker(meshRef.GfxObjId))
|
||||
continue;
|
||||
|
||||
|
|
@ -333,7 +344,9 @@ public sealed partial class WbDrawDispatcher
|
|||
continue;
|
||||
}
|
||||
|
||||
if (renderData.IsSetup && renderData.SetupParts.Count > 0)
|
||||
if (buildingSelection is null
|
||||
&& renderData.IsSetup
|
||||
&& renderData.SetupParts.Count > 0)
|
||||
{
|
||||
// Same entity-scoped OR the classic/packed classifiers compute
|
||||
// — a Setup composite's parts are separate GfxObjs with their
|
||||
|
|
|
|||
|
|
@ -22,7 +22,9 @@ internal sealed record GpuLandblockSpatialPublication(
|
|||
IReadOnlyList<ulong> AdditionalRenderIds,
|
||||
IReadOnlyList<WorldEntity> StaticEntities,
|
||||
bool RequiresActivation = true,
|
||||
uint RenderTraversalOrder = 0);
|
||||
uint RenderTraversalOrder = 0,
|
||||
IReadOnlyList<ulong>? AdditionalOrdinaryRenderIds = null,
|
||||
bool ReplacesMeshRegistration = false);
|
||||
|
||||
/// <summary>
|
||||
/// Render-thread-owned registry of currently-loaded landblocks and their
|
||||
|
|
@ -975,13 +977,19 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
|||
internal GpuLandblockSpatialPublication CommitLandblockSpatial(
|
||||
LoadedLandblock landblock,
|
||||
IEnumerable<ulong>? additionalRenderIds,
|
||||
LandblockStreamTier tier) =>
|
||||
CommitLandblockSpatialCore(landblock, additionalRenderIds, tier);
|
||||
LandblockStreamTier tier,
|
||||
IEnumerable<ulong>? additionalOrdinaryRenderIds = null) =>
|
||||
CommitLandblockSpatialCore(
|
||||
landblock,
|
||||
additionalRenderIds,
|
||||
tier,
|
||||
additionalOrdinaryRenderIds);
|
||||
|
||||
private GpuLandblockSpatialPublication CommitLandblockSpatialCore(
|
||||
LoadedLandblock landblock,
|
||||
IEnumerable<ulong>? additionalRenderIds,
|
||||
LandblockStreamTier tier)
|
||||
LandblockStreamTier tier,
|
||||
IEnumerable<ulong>? additionalOrdinaryRenderIds = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(landblock);
|
||||
|
||||
|
|
@ -1036,8 +1044,10 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
|||
tier = LandblockStreamTier.Near;
|
||||
}
|
||||
|
||||
bool replacesMeshRegistration = false;
|
||||
if (RemoveLoadedLandblock(landblock.LandblockId, out displaced))
|
||||
{
|
||||
replacesMeshRegistration = true;
|
||||
RemoveLoadedLandblockFromFlatView(displaced);
|
||||
_animatedIndexByLandblock.Remove(landblock.LandblockId);
|
||||
}
|
||||
|
|
@ -1048,7 +1058,9 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
|||
return CreateSpatialPublication(
|
||||
_loaded[landblock.LandblockId],
|
||||
(IEnumerable<ulong>?)mergedRenderIds ?? Array.Empty<ulong>(),
|
||||
GetRenderTraversalOrder(landblock.LandblockId));
|
||||
GetRenderTraversalOrder(landblock.LandblockId),
|
||||
additionalOrdinaryRenderIds,
|
||||
replacesMeshRegistration);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1099,7 +1111,9 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
|||
|
||||
_wbSpawnAdapter?.OnLandblockLoaded(
|
||||
publication.Landblock,
|
||||
publication.AdditionalRenderIds);
|
||||
publication.AdditionalRenderIds,
|
||||
publication.AdditionalOrdinaryRenderIds,
|
||||
publication.ReplacesMeshRegistration);
|
||||
|
||||
// C.1.5b: fire DefaultScript for dat-hydrated entities only.
|
||||
// EntityScriptActivator owns a retryable per-static acquisition ledger,
|
||||
|
|
@ -1115,10 +1129,15 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
|||
private static GpuLandblockSpatialPublication CreateSpatialPublication(
|
||||
LoadedLandblock landblock,
|
||||
IEnumerable<ulong> additionalRenderIds,
|
||||
uint renderTraversalOrder)
|
||||
uint renderTraversalOrder,
|
||||
IEnumerable<ulong>? additionalOrdinaryRenderIds = null,
|
||||
bool replacesMeshRegistration = false)
|
||||
{
|
||||
ulong[] renderIds = additionalRenderIds as ulong[]
|
||||
?? additionalRenderIds.ToArray();
|
||||
ulong[] ordinaryRenderIds = additionalOrdinaryRenderIds as ulong[]
|
||||
?? additionalOrdinaryRenderIds?.ToArray()
|
||||
?? Array.Empty<ulong>();
|
||||
WorldEntity[] staticEntities = landblock.Entities
|
||||
.Where(static entity => entity.ServerGuid == 0)
|
||||
.ToArray();
|
||||
|
|
@ -1127,7 +1146,9 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
|||
landblock,
|
||||
renderIds,
|
||||
staticEntities,
|
||||
RenderTraversalOrder: renderTraversalOrder);
|
||||
RenderTraversalOrder: renderTraversalOrder,
|
||||
AdditionalOrdinaryRenderIds: ordinaryRenderIds,
|
||||
ReplacesMeshRegistration: replacesMeshRegistration);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -2014,6 +2035,7 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
|||
landblockId,
|
||||
entities,
|
||||
additionalRenderIds,
|
||||
additionalOrdinaryRenderIds: null,
|
||||
parkIfMissing: true);
|
||||
if (publication is null)
|
||||
return false;
|
||||
|
|
@ -2025,13 +2047,15 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
|||
internal GpuLandblockSpatialPublication CommitEntitiesToExistingLandblockSpatial(
|
||||
uint landblockId,
|
||||
IReadOnlyList<WorldEntity> entities,
|
||||
IEnumerable<ulong>? additionalRenderIds)
|
||||
IEnumerable<ulong>? additionalRenderIds,
|
||||
IEnumerable<ulong>? additionalOrdinaryRenderIds = null)
|
||||
{
|
||||
GpuLandblockSpatialPublication? publication =
|
||||
CommitEntitiesToExistingLandblockSpatialCore(
|
||||
landblockId,
|
||||
entities,
|
||||
additionalRenderIds,
|
||||
additionalOrdinaryRenderIds,
|
||||
parkIfMissing: false);
|
||||
return publication
|
||||
?? throw new InvalidOperationException(
|
||||
|
|
@ -2042,6 +2066,7 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
|||
uint landblockId,
|
||||
IReadOnlyList<WorldEntity> entities,
|
||||
IEnumerable<ulong>? additionalRenderIds,
|
||||
IEnumerable<ulong>? additionalOrdinaryRenderIds,
|
||||
bool parkIfMissing)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entities);
|
||||
|
|
@ -2113,7 +2138,9 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
|||
_loaded[canonical],
|
||||
effectiveRenderIds,
|
||||
staticEntities,
|
||||
RenderTraversalOrder: GetRenderTraversalOrder(canonical));
|
||||
RenderTraversalOrder: GetRenderTraversalOrder(canonical),
|
||||
AdditionalOrdinaryRenderIds:
|
||||
additionalOrdinaryRenderIds?.ToArray() ?? Array.Empty<ulong>());
|
||||
}
|
||||
|
||||
private void DrainVisibilityTransitions()
|
||||
|
|
|
|||
|
|
@ -957,6 +957,8 @@ public sealed class LandblockPresentationPipeline
|
|||
IEnumerable<ulong>? renderIds =
|
||||
transaction.Build.EnvCells?.Shells.Select(
|
||||
static shell => shell.GeometryId);
|
||||
IEnumerable<ulong>? ordinaryRenderIds =
|
||||
transaction.Build.EnvCells?.WalkBuildingMeshDependencies;
|
||||
transaction.SpatialPublication = transaction.Kind switch
|
||||
{
|
||||
PublicationKind.Loaded
|
||||
|
|
@ -965,12 +967,14 @@ public sealed class LandblockPresentationPipeline
|
|||
_state.CommitLandblockSpatial(
|
||||
transaction.Build.Landblock,
|
||||
renderIds,
|
||||
transaction.Tier),
|
||||
transaction.Tier,
|
||||
ordinaryRenderIds),
|
||||
PublicationKind.PromoteExisting =>
|
||||
_state.CommitEntitiesToExistingLandblockSpatial(
|
||||
transaction.LandblockId,
|
||||
transaction.Build.Landblock.Entities,
|
||||
renderIds),
|
||||
renderIds,
|
||||
ordinaryRenderIds),
|
||||
_ => throw new InvalidOperationException(
|
||||
$"Unknown landblock publication kind {transaction.Kind}."),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -118,11 +118,14 @@ namespace AcDream.App.UI.Layout;
|
|||
/// Brightness (OP6 rework, review S2: its OWN <c>DisplaySettings.ScreenBrightness</c>
|
||||
/// field, range [-1,1] default 0 — NOT the pre-existing <c>Gamma</c>
|
||||
/// multiplier, a different unit system with its own live legacy-panel
|
||||
/// consumer; no gamma-correction render pass exists for either); Automatic
|
||||
/// Degrades/Graphics Performance/Degrade Distance/the four texture-detail-
|
||||
/// family menus/Multi-Pass Alpha (the renderer is Vulkan + one aggregate
|
||||
/// <c>QualityPreset</c>, no per-feature knobs). Building Detail Textures is
|
||||
/// LIVE: #226 consumes it directly in the retail building/EnvCell detail pass;
|
||||
/// consumer; no gamma-correction render pass exists for either); Landscape
|
||||
/// Texture Detail/Environment Texture Detail/Texture Filtering/Multi-Pass
|
||||
/// Alpha (the renderer is Vulkan + one aggregate <c>QualityPreset</c>, no
|
||||
/// per-feature knobs). Automatic Degrades, Graphics Performance, and Degrade
|
||||
/// Distance are LIVE through S5-c3's shared building-degrade owner. Landscape
|
||||
/// Draw Distance is LIVE through <c>ApplyLandscapeDrawDistance</c>. Building
|
||||
/// Detail Textures is LIVE: #226 consumes it directly in the retail
|
||||
/// building/EnvCell detail pass;
|
||||
/// Camera Stiffness/Adjustment Speed/Align To Slope/Mouse Look
|
||||
/// Sensitivity/Invert Mouselook Y Axis/Use Mouse Turning (TS-74 — no
|
||||
/// persistent mouse-turning camera mode exists for ANY of these six to
|
||||
|
|
@ -138,8 +141,8 @@ namespace AcDream.App.UI.Layout;
|
|||
///
|
||||
/// <para>
|
||||
/// <b>Caption dimming (AD-78, user-directed, 2026-08-11, gate 2).</b> Every
|
||||
/// STORE-ONLY row above (the 20 rows named in the paragraph before this one
|
||||
/// -- AP-198's nine, AP-199's three, and TS-74's six Camera/Input rows plus
|
||||
/// STORE-ONLY row above (the 16 rows named in the paragraph before this one
|
||||
/// -- AP-198's five, AP-199's three, and TS-74's six Camera/Input rows plus
|
||||
/// AP-200's two Chat-font rows) renders its caption in
|
||||
/// <see cref="UiRenderContext.StoreOnlyCaptionColor"/> instead of the normal
|
||||
/// white/DAT-authored color. The row stays fully interactive -- it still
|
||||
|
|
@ -1231,14 +1234,14 @@ public static class ConfigOptionsPageController
|
|||
listBox, "ID_Graphics_AdaptiveDegrade", defaultValue: false, page, resolveString,
|
||||
read: () => bindings.LoadDisplay().AutomaticDegrades,
|
||||
apply: value => bindings.SaveDisplay(bindings.LoadDisplay() with { AutomaticDegrades = value }),
|
||||
storeOnly: true); // AP-198
|
||||
storeOnly: false);
|
||||
|
||||
BuildSliderRow(
|
||||
listBox, RangedSliderTemplateIndex, "ID_Graphics_AdaptiveDegradeBias",
|
||||
min: -1f, max: 1f, defaultValue: 0f, page, resolveString,
|
||||
read: () => bindings.LoadDisplay().GraphicsPerformance,
|
||||
apply: value => bindings.SaveDisplay(bindings.LoadDisplay() with { GraphicsPerformance = value }),
|
||||
storeOnly: true, // AP-198
|
||||
storeOnly: false,
|
||||
rangeLowKey: "ID_Graphics_Value_Speed", rangeHighKey: "ID_Graphics_Value_Detail");
|
||||
|
||||
BuildSliderRow(
|
||||
|
|
@ -1246,7 +1249,7 @@ public static class ConfigOptionsPageController
|
|||
min: 0f, max: 100f, defaultValue: 50.0f, page, resolveString,
|
||||
read: () => bindings.LoadDisplay().DegradeDistance,
|
||||
apply: value => bindings.SaveDisplay(bindings.LoadDisplay() with { DegradeDistance = value }),
|
||||
storeOnly: true, // AP-198
|
||||
storeOnly: false,
|
||||
rangeLowKey: "ID_Graphics_Value_Close", rangeHighKey: "ID_Graphics_Value_Far");
|
||||
|
||||
display = bindings.LoadDisplay();
|
||||
|
|
|
|||
|
|
@ -156,11 +156,10 @@ public sealed record DisplaySettings(
|
|||
bool ShowFps,
|
||||
QualityPreset Quality,
|
||||
ParticleRange ParticleRange,
|
||||
// Campaign OP slice OP6: the Config tab's "Graphics Options" +
|
||||
// "Rendering Quality Options" rows with no acdream renderer consumer —
|
||||
// the world renderer is Vulkan + one aggregate QualityPreset, not
|
||||
// per-feature knobs (register row, OP6). Persisted faithfully; every
|
||||
// default below is retail's own byte-verified
|
||||
// Campaign OP slice OP6: Config's graphics preferences. S5-c3 makes
|
||||
// AutomaticDegrades, GraphicsPerformance and DegradeDistance live for
|
||||
// building part-zero selection; the remaining per-feature knobs stay
|
||||
// store-only under AP-198. Every default below is retail's byte-verified
|
||||
// gmClient::InitUIPreferences / gmConfigUI::InitOptions literal.
|
||||
//
|
||||
// OP6 rework (2026-08-11, review S2): Screen Brightness gets its OWN
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue