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
File diff suppressed because one or more lines are too long
|
|
@ -40,6 +40,7 @@ internal sealed record FrameRootDependencies(
|
||||||
EntityEffectPoseRegistry EffectPoses,
|
EntityEffectPoseRegistry EffectPoses,
|
||||||
WorldRenderRangeState RenderRange,
|
WorldRenderRangeState RenderRange,
|
||||||
RuntimeSettingsController Settings,
|
RuntimeSettingsController Settings,
|
||||||
|
BuildingDegradeController BuildingDegrades,
|
||||||
DisplayFramePacingController DisplayFramePacing,
|
DisplayFramePacingController DisplayFramePacing,
|
||||||
WorldSceneDebugState WorldSceneDebugState,
|
WorldSceneDebugState WorldSceneDebugState,
|
||||||
RetailAlphaQueue RetailAlphaQueue,
|
RetailAlphaQueue RetailAlphaQueue,
|
||||||
|
|
@ -506,7 +507,8 @@ internal sealed class FrameRootCompositionPhase
|
||||||
?? throw new InvalidOperationException(
|
?? throw new InvalidOperationException(
|
||||||
"The retail frame walk requires the landscape registry."),
|
"The retail frame walk requires the landscape registry."),
|
||||||
d.CellVisibility,
|
d.CellVisibility,
|
||||||
d.PhysicsEngine.ShadowObjects),
|
d.PhysicsEngine.ShadowObjects,
|
||||||
|
d.BuildingDegrades),
|
||||||
retailPViewPassExecutor),
|
retailPViewPassExecutor),
|
||||||
retailPViewCells,
|
retailPViewCells,
|
||||||
worldScenePasses,
|
worldScenePasses,
|
||||||
|
|
@ -798,7 +800,8 @@ internal sealed class FrameRootCompositionPhase
|
||||||
privatePresentation,
|
privatePresentation,
|
||||||
live.FrameDiagnostics,
|
live.FrameDiagnostics,
|
||||||
postDiagnostics,
|
postDiagnostics,
|
||||||
NullRenderFrameFailureRecovery.Instance);
|
NullRenderFrameFailureRecovery.Instance,
|
||||||
|
d.BuildingDegrades);
|
||||||
Fault(FrameRootCompositionPoint.RenderRootCreated);
|
Fault(FrameRootCompositionPoint.RenderRootCreated);
|
||||||
|
|
||||||
var liveFrameCoordinator = new RetailLiveFrameCoordinator(
|
var liveFrameCoordinator = new RetailLiveFrameCoordinator(
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,7 @@ internal sealed record InteractionRetainedUiDependencies(
|
||||||
// Save button writes here, same file GameWindow's startup load reads.
|
// Save button writes here, same file GameWindow's startup load reads.
|
||||||
string KeyBindingsFilePath,
|
string KeyBindingsFilePath,
|
||||||
RuntimeSettingsController Settings,
|
RuntimeSettingsController Settings,
|
||||||
|
BuildingDegradeController BuildingDegrades,
|
||||||
GameRuntime Runtime,
|
GameRuntime Runtime,
|
||||||
IRuntimeCombatAttackOperations CombatAttackOperations,
|
IRuntimeCombatAttackOperations CombatAttackOperations,
|
||||||
RuntimeCombatTargetOperationsSlot CombatTargetOperations,
|
RuntimeCombatTargetOperationsSlot CombatTargetOperations,
|
||||||
|
|
@ -310,6 +311,18 @@ internal interface IInteractionRetainedUiCompositionFactory
|
||||||
internal sealed class RetailInteractionRetainedUiCompositionFactory
|
internal sealed class RetailInteractionRetainedUiCompositionFactory
|
||||||
: IInteractionRetainedUiCompositionFactory
|
: 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(
|
public IDisposable BindCombatTarget(
|
||||||
InteractionRetainedUiDependencies d,
|
InteractionRetainedUiDependencies d,
|
||||||
DeferredSelectionUiAuthority selection) =>
|
DeferredSelectionUiAuthority selection) =>
|
||||||
|
|
@ -787,9 +800,8 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
||||||
d.ClientTime),
|
d.ClientTime),
|
||||||
JumpPowerbar: new JumpPowerbarRuntimeBindings(
|
JumpPowerbar: new JumpPowerbarRuntimeBindings(
|
||||||
() => d.PlayerController.Controller?.JumpCharge ?? default),
|
() => d.PlayerController.Controller?.JumpCharge ?? default),
|
||||||
Fps: new FpsRuntimeBindings(
|
Fps: CreateFpsBindings(
|
||||||
() => d.FrameDiagnostics.Snapshot.Fps,
|
d.BuildingDegrades,
|
||||||
() => 1.0,
|
|
||||||
() => d.Settings.DisplayPreview.ShowFps),
|
() => d.Settings.DisplayPreview.ShowFps),
|
||||||
VividTarget: new VividTargetRuntimeBindings(
|
VividTarget: new VividTargetRuntimeBindings(
|
||||||
d.Actions.Selection,
|
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 Exception? _runFailure;
|
||||||
private readonly DisplayFramePacingController _displayFramePacing;
|
private readonly DisplayFramePacingController _displayFramePacing;
|
||||||
private readonly RuntimeSettingsController _runtimeSettings;
|
private readonly RuntimeSettingsController _runtimeSettings;
|
||||||
|
private readonly BuildingDegradeController _buildingDegrades;
|
||||||
|
|
||||||
// Phase A.1: streaming fields replacing the one-shot _entities list.
|
// Phase A.1: streaming fields replacing the one-shot _entities list.
|
||||||
private AcDream.App.Streaming.LandblockStreamer? _streamer;
|
private AcDream.App.Streaming.LandblockStreamer? _streamer;
|
||||||
|
|
@ -784,6 +785,8 @@ public sealed class GameWindow :
|
||||||
_applicationPaths.SettingsFile),
|
_applicationPaths.SettingsFile),
|
||||||
log: Console.WriteLine,
|
log: Console.WriteLine,
|
||||||
characterOptionValue: _runtime.CharacterOwner.Options.GetOptionBit);
|
characterOptionValue: _runtime.CharacterOwner.Options.GetOptionBit);
|
||||||
|
_buildingDegrades = new BuildingDegradeController(
|
||||||
|
() => _runtimeSettings.DisplayPreview);
|
||||||
_animationDiagnostics = AnimationPresentationDiagnostics.FromEnvironment();
|
_animationDiagnostics = AnimationPresentationDiagnostics.FromEnvironment();
|
||||||
_uiRegistry = uiRegistry;
|
_uiRegistry = uiRegistry;
|
||||||
_renderPackRegistry = renderPackRegistry;
|
_renderPackRegistry = renderPackRegistry;
|
||||||
|
|
@ -1552,6 +1555,7 @@ public sealed class GameWindow :
|
||||||
_localPlayerTeleportSink,
|
_localPlayerTeleportSink,
|
||||||
_applicationPaths.KeyBindingsFile,
|
_applicationPaths.KeyBindingsFile,
|
||||||
_runtimeSettings,
|
_runtimeSettings,
|
||||||
|
_buildingDegrades,
|
||||||
_runtime,
|
_runtime,
|
||||||
_combatAttackOperations,
|
_combatAttackOperations,
|
||||||
_combatTargetOperations,
|
_combatTargetOperations,
|
||||||
|
|
@ -1746,6 +1750,7 @@ public sealed class GameWindow :
|
||||||
_effectPoses,
|
_effectPoses,
|
||||||
_renderRange,
|
_renderRange,
|
||||||
_runtimeSettings,
|
_runtimeSettings,
|
||||||
|
_buildingDegrades,
|
||||||
_displayFramePacing,
|
_displayFramePacing,
|
||||||
_worldSceneDebugState,
|
_worldSceneDebugState,
|
||||||
_retailAlphaQueue,
|
_retailAlphaQueue,
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,11 @@ internal interface IRenderFrameLifetime
|
||||||
void EndFrame();
|
void EndFrame();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal interface IBuildingDegradeFrameTick
|
||||||
|
{
|
||||||
|
void Tick(double elapsedSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
internal interface IRenderFrameResourcePhase
|
internal interface IRenderFrameResourcePhase
|
||||||
{
|
{
|
||||||
void Prepare(RenderFrameInput input);
|
void Prepare(RenderFrameInput input);
|
||||||
|
|
@ -180,6 +185,7 @@ internal sealed class RenderFrameOrchestrator : IGameRenderFrameRoot
|
||||||
private readonly IRenderFrameDiagnosticsPhase _diagnostics;
|
private readonly IRenderFrameDiagnosticsPhase _diagnostics;
|
||||||
private readonly IRenderFramePostDiagnosticsPhase _postDiagnostics;
|
private readonly IRenderFramePostDiagnosticsPhase _postDiagnostics;
|
||||||
private readonly IRenderFrameFailureRecovery _recovery;
|
private readonly IRenderFrameFailureRecovery _recovery;
|
||||||
|
private readonly IBuildingDegradeFrameTick? _buildingDegrades;
|
||||||
|
|
||||||
public RenderFrameOrchestrator(
|
public RenderFrameOrchestrator(
|
||||||
IRenderFrameLifetime lifetime,
|
IRenderFrameLifetime lifetime,
|
||||||
|
|
@ -189,7 +195,8 @@ internal sealed class RenderFrameOrchestrator : IGameRenderFrameRoot
|
||||||
IPrivatePresentationFramePhase presentation,
|
IPrivatePresentationFramePhase presentation,
|
||||||
IRenderFrameDiagnosticsPhase diagnostics,
|
IRenderFrameDiagnosticsPhase diagnostics,
|
||||||
IRenderFramePostDiagnosticsPhase postDiagnostics,
|
IRenderFramePostDiagnosticsPhase postDiagnostics,
|
||||||
IRenderFrameFailureRecovery recovery)
|
IRenderFrameFailureRecovery recovery,
|
||||||
|
IBuildingDegradeFrameTick? buildingDegrades = null)
|
||||||
{
|
{
|
||||||
_lifetime = lifetime ?? throw new ArgumentNullException(nameof(lifetime));
|
_lifetime = lifetime ?? throw new ArgumentNullException(nameof(lifetime));
|
||||||
_gpuMeasurement = gpuMeasurement
|
_gpuMeasurement = gpuMeasurement
|
||||||
|
|
@ -201,6 +208,7 @@ internal sealed class RenderFrameOrchestrator : IGameRenderFrameRoot
|
||||||
_postDiagnostics = postDiagnostics
|
_postDiagnostics = postDiagnostics
|
||||||
?? throw new ArgumentNullException(nameof(postDiagnostics));
|
?? throw new ArgumentNullException(nameof(postDiagnostics));
|
||||||
_recovery = recovery ?? throw new ArgumentNullException(nameof(recovery));
|
_recovery = recovery ?? throw new ArgumentNullException(nameof(recovery));
|
||||||
|
_buildingDegrades = buildingDegrades;
|
||||||
}
|
}
|
||||||
|
|
||||||
public RenderFrameOutcome Render(RenderFrameInput input)
|
public RenderFrameOutcome Render(RenderFrameInput input)
|
||||||
|
|
@ -212,6 +220,7 @@ internal sealed class RenderFrameOrchestrator : IGameRenderFrameRoot
|
||||||
if (input.ViewportWidth <= 0 || input.ViewportHeight <= 0)
|
if (input.ViewportWidth <= 0 || input.ViewportHeight <= 0)
|
||||||
return RenderFrameOutcome.ZeroArea;
|
return RenderFrameOutcome.ZeroArea;
|
||||||
|
|
||||||
|
_buildingDegrades?.Tick(input.DeltaSeconds);
|
||||||
_lifetime.BeginFrame();
|
_lifetime.BeginFrame();
|
||||||
Exception? renderFailure = null;
|
Exception? renderFailure = null;
|
||||||
try
|
try
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,8 @@ internal sealed class RetailPViewRenderer
|
||||||
Walk.WalkBuildingRegistry walkBuildings,
|
Walk.WalkBuildingRegistry walkBuildings,
|
||||||
Walk.WalkLandscapeAssembler walkLandscape,
|
Walk.WalkLandscapeAssembler walkLandscape,
|
||||||
CellVisibility walkCellRegistry,
|
CellVisibility walkCellRegistry,
|
||||||
ShadowObjectRegistry shadows)
|
ShadowObjectRegistry shadows,
|
||||||
|
BuildingDegradeController? buildingDegrades = null)
|
||||||
{
|
{
|
||||||
_renderSceneShadow = renderSceneShadow
|
_renderSceneShadow = renderSceneShadow
|
||||||
?? throw new ArgumentNullException(nameof(renderSceneShadow));
|
?? throw new ArgumentNullException(nameof(renderSceneShadow));
|
||||||
|
|
@ -85,6 +86,9 @@ internal sealed class RetailPViewRenderer
|
||||||
_walkWorldData = new Walk.WalkProductionWorldData(
|
_walkWorldData = new Walk.WalkProductionWorldData(
|
||||||
_walkBuildings,
|
_walkBuildings,
|
||||||
shadows ?? throw new ArgumentNullException(nameof(shadows)));
|
shadows ?? throw new ArgumentNullException(nameof(shadows)));
|
||||||
|
_frameWalk = buildingDegrades is null
|
||||||
|
? new Walk.RetailFrameWalk()
|
||||||
|
: new Walk.RetailFrameWalk(buildingDegrades);
|
||||||
_walkFlushLandscapeAction = FlushWalkLandscape;
|
_walkFlushLandscapeAction = FlushWalkLandscape;
|
||||||
_walkClearInteriorDepthAction = ClearWalkInteriorDepth;
|
_walkClearInteriorDepthAction = ClearWalkInteriorDepth;
|
||||||
_walkDrawExitSealsFunc = DrawWalkExitSeals;
|
_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
|
/// <summary>S3 chunk 2 (§8.2 B3): the flush half of the former combined
|
||||||
/// <c>ClearWalkInteriorDepth</c> — retail's <c>D3DPolyRender::FlushAlphaList(0f)</c>
|
/// <c>ClearWalkInteriorDepth</c> — retail's <c>D3DPolyRender::FlushAlphaList(0f)</c>
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,9 @@ public interface IRetailFrameWalkContext : IWalkBuildingFrameContext
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class RetailFrameWalk
|
public sealed class RetailFrameWalk
|
||||||
{
|
{
|
||||||
|
private readonly BuildingDegradeController? _degradation;
|
||||||
|
private readonly float? _fixedDegradeDistance;
|
||||||
|
private readonly float? _fixedDegradeMultiplier;
|
||||||
// RenderDeviceD3D::Init @0x0059efb0: indoor_pview = PView(…, 1) — the
|
// RenderDeviceD3D::Init @0x0059efb0: indoor_pview = PView(…, 1) — the
|
||||||
// interior pview DRAWS the landscape through its surviving exit views.
|
// interior pview DRAWS the landscape through its surviving exit views.
|
||||||
private readonly WalkPView _interiorPView = new() { DrawLandscape = true };
|
private readonly WalkPView _interiorPView = new() { DrawLandscape = true };
|
||||||
|
|
@ -86,14 +89,19 @@ public sealed class RetailFrameWalk
|
||||||
/// regardless of per-cell visibility.</summary>
|
/// regardless of per-cell visibility.</summary>
|
||||||
public bool AlwaysDrawObjects = true;
|
public bool AlwaysDrawObjects = true;
|
||||||
|
|
||||||
/// <summary>Retail global <c>Render::deg_mul</c> — DYNAMIC: the
|
public RetailFrameWalk() { }
|
||||||
/// auto-tuner (<c>auto_update_deg_mul</c>) swings it with frame load
|
|
||||||
/// (positive ⇒ degrade thresholds slide toward each level's max;
|
internal RetailFrameWalk(BuildingDegradeController degradation)
|
||||||
/// negative ⇒ toward its min). The oracle captures pin ≈+0.99 for every
|
=> _degradation = degradation ?? throw new ArgumentNullException(nameof(degradation));
|
||||||
/// fixture except doorway-still, whose capture ran under cdb load with
|
|
||||||
/// the multiplier depressed (the recon session's live dump read −0.99
|
/// <summary>Fixture-only capture-state constructor. Production always
|
||||||
/// under the same conditions).</summary>
|
/// supplies the renderer-lifetime controller; historical retail captures
|
||||||
public float DegradeMultiplier = WalkBuilding.DefaultDegradeMultiplier;
|
/// 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
|
/// <summary>FW4 slice 1: the interior root's <c>outside_view</c> — the
|
||||||
/// exit-view polygons THIS walk's own <c>ConstructView</c> installed
|
/// 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
|
/// portal pass, and the shell draw — sits inside retail's
|
||||||
/// <c>if (part->gfxobj[part->deg_level] != 0)</c> @0x0059f2d3; a
|
/// <c>if (part->gfxobj[part->deg_level] != 0)</c> @0x0059f2d3; a
|
||||||
/// degraded-out slot draws NOTHING beyond the BLD event.
|
/// degraded-out slot draws NOTHING beyond the BLD event.
|
||||||
/// <c>HasGeometry</c> + a non-null <c>SelectDrawingBsp</c> together model
|
/// The selected GfxObj id alone models that gate; its drawing BSP is an
|
||||||
/// that one gate. Inside the gate, retail's own order
|
/// optional portal-walk input. Inside the gate, retail's own order
|
||||||
/// (@0x0059f30b–0x0059f345) is <c>D3DPolyRender::FlushAlphaList(0f)</c> →
|
/// (@0x0059f30b–0x0059f345) is <c>D3DPolyRender::FlushAlphaList(0f)</c> →
|
||||||
/// <c>CPhysicsPart::Draw(parts, 1)</c> (the PORTAL flavor — the two-pass
|
/// <c>CPhysicsPart::Draw(parts, 1)</c> (the PORTAL flavor — the two-pass
|
||||||
/// punch/look-in walk below) → <c>CPhysicsPart::Draw(parts, 0)</c> (the
|
/// punch/look-in walk below) → <c>CPhysicsPart::Draw(parts, 0)</c> (the
|
||||||
|
|
@ -285,47 +293,47 @@ public sealed class RetailFrameWalk
|
||||||
IRetailFrameWalkContext ctx, IWalkEventSink sink)
|
IRetailFrameWalkContext ctx, IWalkEventSink sink)
|
||||||
{
|
{
|
||||||
sink.Emit(WalkEvent.Building(building.PositionCellId));
|
sink.Emit(WalkEvent.Building(building.PositionCellId));
|
||||||
if (!building.HasGeometry) return;
|
WalkBuildingSelection selection = building.Select(
|
||||||
// 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(
|
|
||||||
ctx.ViewerDistanceTo(building),
|
ctx.ViewerDistanceTo(building),
|
||||||
degradeMultiplier: DegradeMultiplier);
|
_degradation?.DegradeDistance ?? _fixedDegradeDistance ?? 50f,
|
||||||
if (bsp is null) return;
|
_degradation?.ActiveMultiplier ?? _fixedDegradeMultiplier ?? 0f);
|
||||||
|
if (selection.GfxObjId == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
// Additive (Campaign FW3.2b-1): the alpha barrier
|
// Additive (Campaign FW3.2b-1): the alpha barrier
|
||||||
// (D3DPolyRender::FlushAlphaList(0f) @0x0059f30b) — gated by the
|
// (D3DPolyRender::FlushAlphaList(0f) @0x0059f30b) — gated by the
|
||||||
// SAME part->gfxobj[deg_level]!=0 check as everything below it, so
|
// SAME part->gfxobj[deg_level]!=0 check as everything below it, so
|
||||||
// this fires only now that both HasGeometry and the bsp lookup have
|
// this fires after the selected-id gate, independently of whether
|
||||||
// passed.
|
// that selected object's drawing BSP exists.
|
||||||
sink.OnBuildingTurn(building);
|
sink.OnBuildingTurn(building);
|
||||||
|
|
||||||
int viewCount = Math.Max(activeViews.ViewCount, 0);
|
if (selection.DrawingBsp is WalkBspNode bsp)
|
||||||
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
|
int viewCount = Math.Max(activeViews.ViewCount, 0);
|
||||||
// two-pass walk (DrawMeshInternal @0x0059f3bf) — the punch fans
|
var passSink = new PortalPassSink(building, sink);
|
||||||
// it emits clip against THAT view, so the sink carries the index.
|
Vector3 viewpoint = ctx.ViewpointInBuilding(building);
|
||||||
passSink.ActiveViewIndex = v;
|
for (int v = 0; v < viewCount; v++)
|
||||||
ctx.SetActiveView(activeViews, v);
|
{
|
||||||
WalkBuildingPortals.BuildDrawPortalsOnly(
|
// Retail pins building_view = the CURRENT view for the whole
|
||||||
bsp, 1, viewpoint,
|
// two-pass walk (DrawMeshInternal @0x0059f3bf).
|
||||||
(portalRef, pass) => WalkBuildingPortals.DrawPortal(
|
passSink.ActiveViewIndex = v;
|
||||||
_outdoorPView, building, portalRef, pass, ctx, passSink));
|
ctx.SetActiveView(activeViews, v);
|
||||||
WalkBuildingPortals.BuildDrawPortalsOnly(
|
WalkBuildingPortals.BuildDrawPortalsOnly(
|
||||||
bsp, 2, viewpoint,
|
bsp, 1, viewpoint,
|
||||||
(portalRef, pass) => WalkBuildingPortals.DrawPortal(
|
(portalRef, pass) => WalkBuildingPortals.DrawPortal(
|
||||||
_outdoorPView, building, portalRef, pass, ctx, passSink));
|
_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)
|
// Additive (Campaign FW3.2b-1): CPhysicsPart::Draw(parts, 0)
|
||||||
// @0x0059f331 — the building's own shell mesh — runs AFTER the
|
// @0x0059f331 — the building's own shell mesh — runs AFTER the
|
||||||
// portal walk completes (CPhysicsPart::Draw(parts, 1) just above),
|
// portal walk completes (CPhysicsPart::Draw(parts, 1) just above),
|
||||||
// not before it.
|
// not before it.
|
||||||
sink.OnBuildingShellTurn(building);
|
sink.OnBuildingShellTurn(building, selection);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Returns the extracted cell-id array so <see cref="DrawInside"/>
|
/// <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
|
/// <summary>One degrade-ladder level: the drawing BSP of that level's
|
||||||
/// GfxObj (portal-only view) and the level's authored distance bands.</summary>
|
/// GfxObj (portal-only view) and the level's authored distance bands.</summary>
|
||||||
public readonly record struct WalkBuildingDegradeLevel(
|
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> +
|
/// <summary>The walk's building model (retail <c>CBuildingObj</c> +
|
||||||
/// <c>BuildInfo</c> as the frame walk consumes them).</summary>
|
/// <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>
|
/// Used directly when the model has no degrade ladder.</summary>
|
||||||
public WalkBspNode? DrawingBsp;
|
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
|
/// <summary>The degrade ladder (near→far). Retail walks the CURRENT
|
||||||
/// level's drawing BSP (<c>part->gfxobj[deg_level]</c>) — and building
|
/// level's drawing BSP (<c>part->gfxobj[deg_level]</c>) — and building
|
||||||
/// degrade models beyond the first band carry NO portal nodes, which is
|
/// 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>
|
/// every Holtburg building has ports at level 0 only).</summary>
|
||||||
public WalkBuildingDegradeLevel[] DegradeLevels = [];
|
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
|
/// <summary>The base GfxObj's sort center — retail measures the viewer
|
||||||
/// distance to the part's SCALED sort center, not the position origin
|
/// distance to the part's SCALED sort center, not the position origin
|
||||||
/// (<c>CPhysicsPart::UpdateViewerDistance</c> @0x0050e030).</summary>
|
/// (<c>CPhysicsPart::UpdateViewerDistance</c> @0x0050e030).</summary>
|
||||||
public Vector3 SortCenter;
|
public Vector3 SortCenter;
|
||||||
|
|
||||||
/// <summary>Retail <c>Render::s_rDegradeDistance</c> — a subtractive
|
/// <summary>Part-zero Setup resting/default-scale transform. Direct Gfx
|
||||||
/// slack before the ladder applies (live-dumped 100 on the capture
|
/// buildings use identity.</summary>
|
||||||
/// client; registry-configurable).</summary>
|
public Matrix4x4 PartZeroTransform = Matrix4x4.Identity;
|
||||||
public const float DefaultDegradeDistance = 100f;
|
|
||||||
|
|
||||||
/// <summary>The live capture client's <c>Render::deg_mul</c> arm:
|
/// <summary>The selected part scale's Z component used by retail's
|
||||||
/// magnitude 0.99 pinned by the walkout-F2 fixture (building a9b4001e,
|
/// UpdateViewerDistance division.</summary>
|
||||||
/// effective 30.3, level 0 ideal/max 24/48 — retail floods, so its
|
public float PartZeroScaleZ = 1f;
|
||||||
/// 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>
|
/// <summary>
|
||||||
/// <c>GfxObjDegradeInfo::get_degrade</c> @0x0051e4b0 (BN flag-mush on
|
/// <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,
|
/// mul is <c>Render::deg_mul</c> when <c>auto_update_deg_mul</c> is on,
|
||||||
/// else <c>s_rUserSuppliedDegradeBias</c>.
|
/// else <c>s_rUserSuppliedDegradeBias</c>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public WalkBspNode? SelectDrawingBsp(
|
public WalkBuildingSelection Select(
|
||||||
float viewerDistance,
|
float viewerDistance,
|
||||||
float degradeDistance = DefaultDegradeDistance,
|
float degradeDistance,
|
||||||
float degradeMultiplier = DefaultDegradeMultiplier)
|
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);
|
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
|
WalkBuildingDegradeLevel level = DegradeLevels[i];
|
||||||
? level.IdealDist - (level.IdealDist - level.MaxDist) * degradeMultiplier
|
double threshold = degradeMultiplier >= 0f
|
||||||
: level.IdealDist + (level.IdealDist - level.MinDist) * degradeMultiplier;
|
? (double)level.IdealDist
|
||||||
|
- ((double)level.IdealDist - level.MaxDist) * degradeMultiplier
|
||||||
|
: (double)level.IdealDist
|
||||||
|
+ ((double)level.IdealDist - level.MinDist) * degradeMultiplier;
|
||||||
if (effective < threshold)
|
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 System.Numerics;
|
||||||
using AcDream.Content;
|
using AcDream.Content;
|
||||||
|
using AcDream.Core.Meshing;
|
||||||
|
using AcDream.Core.World;
|
||||||
using DatReaderWriter.DBObjs;
|
using DatReaderWriter.DBObjs;
|
||||||
using DatReaderWriter.Enums;
|
using DatReaderWriter.Enums;
|
||||||
using DatReaderWriter.Types;
|
using DatReaderWriter.Types;
|
||||||
|
|
@ -37,7 +39,21 @@ public static class WalkBuildingFactory
|
||||||
/// placement transform (<paramref name="WorldTransform"/> already has the
|
/// placement transform (<paramref name="WorldTransform"/> already has the
|
||||||
/// caller's block offset baked in — see <see cref="Build"/>).</summary>
|
/// caller's block offset baked in — see <see cref="Build"/>).</summary>
|
||||||
public sealed record Entry(
|
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"/>
|
/// <summary>Builds every building of one landblock. <paramref name="lbOffset"/>
|
||||||
/// is the SAME landblock-local world offset every other production
|
/// is the SAME landblock-local world offset every other production
|
||||||
|
|
@ -84,8 +100,32 @@ public static class WalkBuildingFactory
|
||||||
|
|
||||||
WalkBspNode? bsp = null;
|
WalkBspNode? bsp = null;
|
||||||
Vector3 sortCenter = Vector3.Zero;
|
Vector3 sortCenter = Vector3.Zero;
|
||||||
|
uint partZeroGfxObjId = 0;
|
||||||
|
Matrix4x4 partZeroTransform = Matrix4x4.Identity;
|
||||||
|
float partZeroScaleZ = 1f;
|
||||||
var degradeLevels = new List<WalkBuildingDegradeLevel>();
|
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);
|
bsp = ConvertDrawingBsp(gfxObj, gfxObj.DrawingBSP?.Root);
|
||||||
sortCenter = new Vector3(gfxObj.SortCenter.X, gfxObj.SortCenter.Y, gfxObj.SortCenter.Z);
|
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);
|
levelBsp = ConvertDrawingBsp(levelGfx, levelGfx.DrawingBSP?.Root);
|
||||||
}
|
}
|
||||||
degradeLevels.Add(
|
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,
|
PositionCellId = positionCellId,
|
||||||
Portals = portals,
|
Portals = portals,
|
||||||
|
GfxObjId = partZeroGfxObjId,
|
||||||
DrawingBsp = bsp,
|
DrawingBsp = bsp,
|
||||||
DegradeLevels = degradeLevels.ToArray(),
|
DegradeLevels = degradeLevels.ToArray(),
|
||||||
SortCenter = sortCenter,
|
SortCenter = sortCenter,
|
||||||
|
PartZeroTransform = partZeroTransform,
|
||||||
|
PartZeroScaleZ = partZeroScaleZ,
|
||||||
},
|
},
|
||||||
worldTransform,
|
worldTransform,
|
||||||
inverse));
|
inverse));
|
||||||
|
|
|
||||||
|
|
@ -176,9 +176,8 @@ public interface IWalkEventSink
|
||||||
/// draw) in <c>if (part->gfxobj[part->deg_level] != 0)</c>
|
/// draw) in <c>if (part->gfxobj[part->deg_level] != 0)</c>
|
||||||
/// @0x0059f2d3 — a degraded-out slot draws NOTHING beyond the
|
/// @0x0059f2d3 — a degraded-out slot draws NOTHING beyond the
|
||||||
/// unconditional <see cref="WalkEventKind.Building"/> <see cref="Emit"/>
|
/// unconditional <see cref="WalkEventKind.Building"/> <see cref="Emit"/>
|
||||||
/// call. <see cref="WalkBuilding.HasGeometry"/> and a non-null
|
/// call. The selected GfxObj id alone models that gate; a null drawing
|
||||||
/// <see cref="WalkBuilding.SelectDrawingBsp"/> result together model that
|
/// BSP only suppresses the portal sub-walk. This is the
|
||||||
/// one gate, so this hook fires only after both have passed. This is the
|
|
||||||
/// ALPHA BARRIER turn (<c>D3DPolyRender::FlushAlphaList(0f)</c>
|
/// ALPHA BARRIER turn (<c>D3DPolyRender::FlushAlphaList(0f)</c>
|
||||||
/// @0x0059f30b) — retail's own order runs it BEFORE the portal pass
|
/// @0x0059f30b) — retail's own order runs it BEFORE the portal pass
|
||||||
/// (<c>CPhysicsPart::Draw(parts, 1)</c>), which in turn runs BEFORE the
|
/// (<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
|
/// fires when <see cref="OnBuildingTurn"/> also fired (same gate; see its
|
||||||
/// doc comment) — a degraded-out slot reaches neither. Default no-op.
|
/// doc comment) — a degraded-out slot reaches neither. Default no-op.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
void OnBuildingShellTurn(WalkBuilding building) { }
|
void OnBuildingShellTurn(
|
||||||
|
WalkBuilding building,
|
||||||
|
WalkBuildingSelection selection) { }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fires when the two-pass portal machinery
|
/// Fires when the two-pass portal machinery
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,8 @@ internal interface IWalkFrameWorldData
|
||||||
/// <summary>Building-local → world, for transforming a punch polygon
|
/// <summary>Building-local → world, for transforming a punch polygon
|
||||||
/// before <see cref="IWalkFrameLeafRenderer.DrawPunchFan"/> — the
|
/// before <see cref="IWalkFrameLeafRenderer.DrawPunchFan"/> — the
|
||||||
/// production implementation is <see cref="WalkBuildingRegistry.TryGetEntry"/>'s
|
/// 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);
|
Matrix4x4 GetBuildingWorldTransform(WalkBuilding building);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1590,7 +1591,9 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
||||||
_currentDcStage = WalkDrawStage.LookInStatic;
|
_currentDcStage = WalkDrawStage.LookInStatic;
|
||||||
}
|
}
|
||||||
|
|
||||||
void IWalkEventSink.OnBuildingShellTurn(WalkBuilding building)
|
void IWalkEventSink.OnBuildingShellTurn(
|
||||||
|
WalkBuilding building,
|
||||||
|
WalkBuildingSelection selection)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(building);
|
ArgumentNullException.ThrowIfNull(building);
|
||||||
RequireOpenFrame();
|
RequireOpenFrame();
|
||||||
|
|
@ -1602,10 +1605,27 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
||||||
// look-in flood appended (keeps every range single-stage).
|
// look-in flood appended (keeps every range single-stage).
|
||||||
MarkIfGrown();
|
MarkIfGrown();
|
||||||
WalkFrameStaticRecords shell = _worldData.GetBuildingShellStatics(building);
|
WalkFrameStaticRecords shell = _worldData.GetBuildingShellStatics(building);
|
||||||
_populator.PopulateCell(
|
if (shell.Records.Count > 1)
|
||||||
_stream, WalkDrawStage.BuildingShell, building.PositionCellId,
|
{
|
||||||
shell.Records, shell.TupleLandblockId, _cameraWorldPosition, _viewProjection,
|
throw new InvalidOperationException(
|
||||||
alphaSubmissions: _alphaSubmissions);
|
$"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)
|
if (_alphaSubmissions.Count != _alphaSubmitMark)
|
||||||
{
|
{
|
||||||
MarkIfGrown();
|
MarkIfGrown();
|
||||||
|
|
|
||||||
|
|
@ -173,21 +173,24 @@ public sealed class WalkProductionFrameContext : IWalkFrameContext, IRetailFrame
|
||||||
}
|
}
|
||||||
|
|
||||||
public Vector3 ViewpointInBuilding(WalkBuilding building)
|
public Vector3 ViewpointInBuilding(WalkBuilding building)
|
||||||
=> Vector3.Transform(WorldViewpoint, GetEntry(building).InverseWorldTransform);
|
=> Vector3.Transform(WorldViewpoint, GetEntry(building).InversePartZeroWorldTransform);
|
||||||
|
|
||||||
/// <summary><c>CPhysicsPart::UpdateViewerDistance</c> @0x0050e030: the
|
/// <summary><c>CPhysicsPart::UpdateViewerDistance</c> @0x0050e030: the
|
||||||
/// distance to the part's SCALED sort center, not the position origin.</summary>
|
/// distance to the part's SCALED sort center, not the position origin.</summary>
|
||||||
public float ViewerDistanceTo(WalkBuilding building)
|
public float ViewerDistanceTo(WalkBuilding building)
|
||||||
{
|
{
|
||||||
WalkBuildingFactory.Entry entry = GetEntry(building);
|
WalkBuildingFactory.Entry entry = GetEntry(building);
|
||||||
return Vector3.Distance(
|
float distance = Vector3.Distance(
|
||||||
WorldViewpoint, Vector3.Transform(building.SortCenter, entry.WorldTransform));
|
WorldViewpoint,
|
||||||
|
Vector3.Transform(building.SortCenter, entry.PartZeroWorldTransform));
|
||||||
|
return distance / building.PartZeroScaleZ;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int ClipBuildingPolygon(
|
public int ClipBuildingPolygon(
|
||||||
WalkBuilding building, WalkPolygon polygon, int side, Span<WalkScreenPoint> output)
|
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];
|
Span<WalkScreenPoint> projected = stackalloc WalkScreenPoint[polygon.Vertices.Length];
|
||||||
for (int i = 0; i < polygon.Vertices.Length; i++)
|
for (int i = 0; i < polygon.Vertices.Length; i++)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -543,6 +543,6 @@ internal sealed class WalkProductionWorldData : IWalkFrameWorldData
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
$"walk building 0x{building.PositionCellId:X8} has no committed registry entry");
|
$"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>
|
/// <summary>
|
||||||
/// The landscape entry point: outdoor static content
|
/// The landscape entry point: outdoor static content
|
||||||
/// (<c>RenderProjectionClass.OutdoorStatic</c> —
|
/// (<c>RenderProjectionClass.OutdoorStatic</c> —
|
||||||
|
|
@ -275,7 +303,9 @@ internal sealed class WalkStaticStreamPopulator
|
||||||
bool liveDynamic = false,
|
bool liveDynamic = false,
|
||||||
IWalkLookInViewSource? lookInViews = null,
|
IWalkLookInViewSource? lookInViews = null,
|
||||||
int lookInRouteIndex = -1,
|
int lookInRouteIndex = -1,
|
||||||
List<WbDrawDispatcher.WalkClassifiedBatch>? alphaSubmissions = null)
|
List<WbDrawDispatcher.WalkClassifiedBatch>? alphaSubmissions = null,
|
||||||
|
WalkBuildingSelection? buildingSelection = null,
|
||||||
|
Matrix4x4 buildingPartTransform = default)
|
||||||
{
|
{
|
||||||
_batchScratch.Clear();
|
_batchScratch.Clear();
|
||||||
_selectionScratch.Clear();
|
_selectionScratch.Clear();
|
||||||
|
|
@ -288,7 +318,9 @@ internal sealed class WalkStaticStreamPopulator
|
||||||
lookInViews,
|
lookInViews,
|
||||||
lookInRouteIndex,
|
lookInRouteIndex,
|
||||||
cellId,
|
cellId,
|
||||||
diagnosticViewProjection: viewProjection);
|
diagnosticViewProjection: viewProjection,
|
||||||
|
buildingSelection: buildingSelection,
|
||||||
|
buildingPartTransform: buildingPartTransform);
|
||||||
|
|
||||||
for (int i = 0; i < _batchScratch.Count; i++)
|
for (int i = 0; i < _batchScratch.Count; i++)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ public sealed class EnvCellLandblockBuild
|
||||||
VisibilityCells = visibilityCells.ToImmutableArray();
|
VisibilityCells = visibilityCells.ToImmutableArray();
|
||||||
Shells = shells.ToImmutableArray();
|
Shells = shells.ToImmutableArray();
|
||||||
WalkBuildings = (walkBuildings ?? Enumerable.Empty<WalkBuildingFactory.Entry>()).ToImmutableArray();
|
WalkBuildings = (walkBuildings ?? Enumerable.Empty<WalkBuildingFactory.Entry>()).ToImmutableArray();
|
||||||
|
WalkBuildingMeshDependencies = CollectWalkBuildingMeshDependencies(WalkBuildings);
|
||||||
WalkMaxZ = walkMaxZ;
|
WalkMaxZ = walkMaxZ;
|
||||||
WalkMinZ = walkMinZ;
|
WalkMinZ = walkMinZ;
|
||||||
|
|
||||||
|
|
@ -67,6 +68,14 @@ public sealed class EnvCellLandblockBuild
|
||||||
/// <c>LandblockRenderPublisher.AdvanceCompleteOne</c>.</summary>
|
/// <c>LandblockRenderPublisher.AdvanceCompleteOne</c>.</summary>
|
||||||
public ImmutableArray<WalkBuildingFactory.Entry> WalkBuildings { get; }
|
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
|
/// <summary>Campaign FW3.1: this landblock's retail z-slab
|
||||||
/// (<c>heightTable[maxByte] + 200</c>) — the walk landscape's per-block
|
/// (<c>heightTable[maxByte] + 200</c>) — the walk landscape's per-block
|
||||||
/// visibility bound (<c>WalkLandscapeAssembler.PublishLandblock</c>).
|
/// visibility bound (<c>WalkLandscapeAssembler.PublishLandblock</c>).
|
||||||
|
|
@ -77,6 +86,24 @@ public sealed class EnvCellLandblockBuild
|
||||||
/// <summary>Campaign FW3.1: this landblock's retail z-slab
|
/// <summary>Campaign FW3.1: this landblock's retail z-slab
|
||||||
/// (<c>heightTable[minByte] − 1</c>). See <see cref="WalkMaxZ"/>.</summary>
|
/// (<c>heightTable[minByte] − 1</c>). See <see cref="WalkMaxZ"/>.</summary>
|
||||||
public float WalkMinZ { get; }
|
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>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,9 @@ public sealed class LandblockSpawnAdapter
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void OnLandblockLoaded(
|
public void OnLandblockLoaded(
|
||||||
LoadedLandblock landblock,
|
LoadedLandblock landblock,
|
||||||
IEnumerable<ulong>? additionalReadinessIds = null)
|
IEnumerable<ulong>? additionalReadinessIds = null,
|
||||||
|
IEnumerable<ulong>? additionalOrdinaryIds = null,
|
||||||
|
bool replaceExisting = false)
|
||||||
{
|
{
|
||||||
System.ArgumentNullException.ThrowIfNull(landblock);
|
System.ArgumentNullException.ThrowIfNull(landblock);
|
||||||
|
|
||||||
|
|
@ -90,6 +92,8 @@ public sealed class LandblockSpawnAdapter
|
||||||
foreach (var meshRef in entity.MeshRefs)
|
foreach (var meshRef in entity.MeshRefs)
|
||||||
unique.Add((ulong)meshRef.GfxObjId);
|
unique.Add((ulong)meshRef.GfxObjId);
|
||||||
}
|
}
|
||||||
|
if (additionalOrdinaryIds is not null)
|
||||||
|
unique.UnionWith(additionalOrdinaryIds.Where(static id => id != 0));
|
||||||
|
|
||||||
HashSet<ulong>? preparedIds = additionalReadinessIds is null
|
HashSet<ulong>? preparedIds = additionalReadinessIds is null
|
||||||
? null
|
? null
|
||||||
|
|
@ -110,6 +114,11 @@ public sealed class LandblockSpawnAdapter
|
||||||
MarkAllUndesired(registration.Prepared);
|
MarkAllUndesired(registration.Prepared);
|
||||||
registration.WantsLoaded = true;
|
registration.WantsLoaded = true;
|
||||||
}
|
}
|
||||||
|
else if (replaceExisting)
|
||||||
|
{
|
||||||
|
MarkAllUndesired(registration.Ordinary);
|
||||||
|
MarkAllUndesired(registration.Prepared);
|
||||||
|
}
|
||||||
|
|
||||||
MarkDesired(registration.Ordinary, unique);
|
MarkDesired(registration.Ordinary, unique);
|
||||||
if (preparedIds is not null)
|
if (preparedIds is not null)
|
||||||
|
|
|
||||||
|
|
@ -47,11 +47,9 @@ namespace AcDream.App.Rendering.Wb;
|
||||||
/// (<c>TranslucencyFadeManager</c>/<c>EntityOpacity</c>) is NOT threaded
|
/// (<c>TranslucencyFadeManager</c>/<c>EntityOpacity</c>) is NOT threaded
|
||||||
/// through: that mechanic is <c>TransparentPartHook</c>, a retail LIVE-entity
|
/// through: that mechanic is <c>TransparentPartHook</c>, a retail LIVE-entity
|
||||||
/// behavior keyed by ServerGuid, not something world statics undergo — every
|
/// behavior keyed by ServerGuid, not something world statics undergo — every
|
||||||
/// classified batch here carries <c>Alpha = 1f</c>. Likewise the async
|
/// classified batch here carries <c>Alpha = 1f</c>. A mesh miss uses the
|
||||||
/// mesh-miss self-heal request (<c>_missRequested</c>/<c>EnsureLoaded</c>,
|
/// existing frame-scoped <c>_missRequested</c>/<c>EnsureLoaded</c> seam and
|
||||||
/// frame-scoped state cleared by <c>BeginEntityDispatch</c>) is not fired
|
/// skips this frame; the caller retries naturally on the following frame.</para>
|
||||||
/// 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>
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed partial class WbDrawDispatcher
|
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.");
|
"A walk part frame was opened before the previous scope closed.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_missRequested.Clear();
|
||||||
_walkDrawnParts.Clear();
|
_walkDrawnParts.Clear();
|
||||||
BeginFacilityStairSubmissionProbeFrame();
|
BeginFacilityStairSubmissionProbeFrame();
|
||||||
_walkPartFrameActive = true;
|
_walkPartFrameActive = true;
|
||||||
|
|
@ -274,7 +273,9 @@ public sealed partial class WbDrawDispatcher
|
||||||
IWalkLookInViewSource? lookInViews = null,
|
IWalkLookInViewSource? lookInViews = null,
|
||||||
int lookInRouteIndex = -1,
|
int lookInRouteIndex = -1,
|
||||||
uint lookInCellId = 0,
|
uint lookInCellId = 0,
|
||||||
Matrix4x4 diagnosticViewProjection = default)
|
Matrix4x4 diagnosticViewProjection = default,
|
||||||
|
WalkBuildingSelection? buildingSelection = null,
|
||||||
|
Matrix4x4 buildingPartTransform = default)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(batches);
|
ArgumentNullException.ThrowIfNull(batches);
|
||||||
ArgumentNullException.ThrowIfNull(selectionParts);
|
ArgumentNullException.ThrowIfNull(selectionParts);
|
||||||
|
|
@ -316,12 +317,22 @@ public sealed partial class WbDrawDispatcher
|
||||||
paletteIdentity = TextureCache.GetPaletteIdentity(entity.PaletteOverride);
|
paletteIdentity = TextureCache.GetPaletteIdentity(entity.PaletteOverride);
|
||||||
|
|
||||||
IReadOnlyList<MeshRef>? meshRefs = projection.EntityPayload.MeshRefs;
|
IReadOnlyList<MeshRef>? meshRefs = projection.EntityPayload.MeshRefs;
|
||||||
if (meshRefs is null)
|
if (buildingSelection is null && meshRefs is null)
|
||||||
return;
|
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))
|
if (_meshAdapter.IsRuntimeHiddenMarker(meshRef.GfxObjId))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
|
@ -333,7 +344,9 @@ public sealed partial class WbDrawDispatcher
|
||||||
continue;
|
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
|
// Same entity-scoped OR the classic/packed classifiers compute
|
||||||
// — a Setup composite's parts are separate GfxObjs with their
|
// — a Setup composite's parts are separate GfxObjs with their
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,9 @@ internal sealed record GpuLandblockSpatialPublication(
|
||||||
IReadOnlyList<ulong> AdditionalRenderIds,
|
IReadOnlyList<ulong> AdditionalRenderIds,
|
||||||
IReadOnlyList<WorldEntity> StaticEntities,
|
IReadOnlyList<WorldEntity> StaticEntities,
|
||||||
bool RequiresActivation = true,
|
bool RequiresActivation = true,
|
||||||
uint RenderTraversalOrder = 0);
|
uint RenderTraversalOrder = 0,
|
||||||
|
IReadOnlyList<ulong>? AdditionalOrdinaryRenderIds = null,
|
||||||
|
bool ReplacesMeshRegistration = false);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Render-thread-owned registry of currently-loaded landblocks and their
|
/// Render-thread-owned registry of currently-loaded landblocks and their
|
||||||
|
|
@ -975,13 +977,19 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
internal GpuLandblockSpatialPublication CommitLandblockSpatial(
|
internal GpuLandblockSpatialPublication CommitLandblockSpatial(
|
||||||
LoadedLandblock landblock,
|
LoadedLandblock landblock,
|
||||||
IEnumerable<ulong>? additionalRenderIds,
|
IEnumerable<ulong>? additionalRenderIds,
|
||||||
LandblockStreamTier tier) =>
|
LandblockStreamTier tier,
|
||||||
CommitLandblockSpatialCore(landblock, additionalRenderIds, tier);
|
IEnumerable<ulong>? additionalOrdinaryRenderIds = null) =>
|
||||||
|
CommitLandblockSpatialCore(
|
||||||
|
landblock,
|
||||||
|
additionalRenderIds,
|
||||||
|
tier,
|
||||||
|
additionalOrdinaryRenderIds);
|
||||||
|
|
||||||
private GpuLandblockSpatialPublication CommitLandblockSpatialCore(
|
private GpuLandblockSpatialPublication CommitLandblockSpatialCore(
|
||||||
LoadedLandblock landblock,
|
LoadedLandblock landblock,
|
||||||
IEnumerable<ulong>? additionalRenderIds,
|
IEnumerable<ulong>? additionalRenderIds,
|
||||||
LandblockStreamTier tier)
|
LandblockStreamTier tier,
|
||||||
|
IEnumerable<ulong>? additionalOrdinaryRenderIds = null)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(landblock);
|
ArgumentNullException.ThrowIfNull(landblock);
|
||||||
|
|
||||||
|
|
@ -1036,8 +1044,10 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
tier = LandblockStreamTier.Near;
|
tier = LandblockStreamTier.Near;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool replacesMeshRegistration = false;
|
||||||
if (RemoveLoadedLandblock(landblock.LandblockId, out displaced))
|
if (RemoveLoadedLandblock(landblock.LandblockId, out displaced))
|
||||||
{
|
{
|
||||||
|
replacesMeshRegistration = true;
|
||||||
RemoveLoadedLandblockFromFlatView(displaced);
|
RemoveLoadedLandblockFromFlatView(displaced);
|
||||||
_animatedIndexByLandblock.Remove(landblock.LandblockId);
|
_animatedIndexByLandblock.Remove(landblock.LandblockId);
|
||||||
}
|
}
|
||||||
|
|
@ -1048,7 +1058,9 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
return CreateSpatialPublication(
|
return CreateSpatialPublication(
|
||||||
_loaded[landblock.LandblockId],
|
_loaded[landblock.LandblockId],
|
||||||
(IEnumerable<ulong>?)mergedRenderIds ?? Array.Empty<ulong>(),
|
(IEnumerable<ulong>?)mergedRenderIds ?? Array.Empty<ulong>(),
|
||||||
GetRenderTraversalOrder(landblock.LandblockId));
|
GetRenderTraversalOrder(landblock.LandblockId),
|
||||||
|
additionalOrdinaryRenderIds,
|
||||||
|
replacesMeshRegistration);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -1099,7 +1111,9 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
|
|
||||||
_wbSpawnAdapter?.OnLandblockLoaded(
|
_wbSpawnAdapter?.OnLandblockLoaded(
|
||||||
publication.Landblock,
|
publication.Landblock,
|
||||||
publication.AdditionalRenderIds);
|
publication.AdditionalRenderIds,
|
||||||
|
publication.AdditionalOrdinaryRenderIds,
|
||||||
|
publication.ReplacesMeshRegistration);
|
||||||
|
|
||||||
// C.1.5b: fire DefaultScript for dat-hydrated entities only.
|
// C.1.5b: fire DefaultScript for dat-hydrated entities only.
|
||||||
// EntityScriptActivator owns a retryable per-static acquisition ledger,
|
// EntityScriptActivator owns a retryable per-static acquisition ledger,
|
||||||
|
|
@ -1115,10 +1129,15 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
private static GpuLandblockSpatialPublication CreateSpatialPublication(
|
private static GpuLandblockSpatialPublication CreateSpatialPublication(
|
||||||
LoadedLandblock landblock,
|
LoadedLandblock landblock,
|
||||||
IEnumerable<ulong> additionalRenderIds,
|
IEnumerable<ulong> additionalRenderIds,
|
||||||
uint renderTraversalOrder)
|
uint renderTraversalOrder,
|
||||||
|
IEnumerable<ulong>? additionalOrdinaryRenderIds = null,
|
||||||
|
bool replacesMeshRegistration = false)
|
||||||
{
|
{
|
||||||
ulong[] renderIds = additionalRenderIds as ulong[]
|
ulong[] renderIds = additionalRenderIds as ulong[]
|
||||||
?? additionalRenderIds.ToArray();
|
?? additionalRenderIds.ToArray();
|
||||||
|
ulong[] ordinaryRenderIds = additionalOrdinaryRenderIds as ulong[]
|
||||||
|
?? additionalOrdinaryRenderIds?.ToArray()
|
||||||
|
?? Array.Empty<ulong>();
|
||||||
WorldEntity[] staticEntities = landblock.Entities
|
WorldEntity[] staticEntities = landblock.Entities
|
||||||
.Where(static entity => entity.ServerGuid == 0)
|
.Where(static entity => entity.ServerGuid == 0)
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
@ -1127,7 +1146,9 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
landblock,
|
landblock,
|
||||||
renderIds,
|
renderIds,
|
||||||
staticEntities,
|
staticEntities,
|
||||||
RenderTraversalOrder: renderTraversalOrder);
|
RenderTraversalOrder: renderTraversalOrder,
|
||||||
|
AdditionalOrdinaryRenderIds: ordinaryRenderIds,
|
||||||
|
ReplacesMeshRegistration: replacesMeshRegistration);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -2014,6 +2035,7 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
landblockId,
|
landblockId,
|
||||||
entities,
|
entities,
|
||||||
additionalRenderIds,
|
additionalRenderIds,
|
||||||
|
additionalOrdinaryRenderIds: null,
|
||||||
parkIfMissing: true);
|
parkIfMissing: true);
|
||||||
if (publication is null)
|
if (publication is null)
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -2025,13 +2047,15 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
internal GpuLandblockSpatialPublication CommitEntitiesToExistingLandblockSpatial(
|
internal GpuLandblockSpatialPublication CommitEntitiesToExistingLandblockSpatial(
|
||||||
uint landblockId,
|
uint landblockId,
|
||||||
IReadOnlyList<WorldEntity> entities,
|
IReadOnlyList<WorldEntity> entities,
|
||||||
IEnumerable<ulong>? additionalRenderIds)
|
IEnumerable<ulong>? additionalRenderIds,
|
||||||
|
IEnumerable<ulong>? additionalOrdinaryRenderIds = null)
|
||||||
{
|
{
|
||||||
GpuLandblockSpatialPublication? publication =
|
GpuLandblockSpatialPublication? publication =
|
||||||
CommitEntitiesToExistingLandblockSpatialCore(
|
CommitEntitiesToExistingLandblockSpatialCore(
|
||||||
landblockId,
|
landblockId,
|
||||||
entities,
|
entities,
|
||||||
additionalRenderIds,
|
additionalRenderIds,
|
||||||
|
additionalOrdinaryRenderIds,
|
||||||
parkIfMissing: false);
|
parkIfMissing: false);
|
||||||
return publication
|
return publication
|
||||||
?? throw new InvalidOperationException(
|
?? throw new InvalidOperationException(
|
||||||
|
|
@ -2042,6 +2066,7 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
uint landblockId,
|
uint landblockId,
|
||||||
IReadOnlyList<WorldEntity> entities,
|
IReadOnlyList<WorldEntity> entities,
|
||||||
IEnumerable<ulong>? additionalRenderIds,
|
IEnumerable<ulong>? additionalRenderIds,
|
||||||
|
IEnumerable<ulong>? additionalOrdinaryRenderIds,
|
||||||
bool parkIfMissing)
|
bool parkIfMissing)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(entities);
|
ArgumentNullException.ThrowIfNull(entities);
|
||||||
|
|
@ -2113,7 +2138,9 @@ public sealed class GpuWorldState : ILiveEntitySpatialQuery
|
||||||
_loaded[canonical],
|
_loaded[canonical],
|
||||||
effectiveRenderIds,
|
effectiveRenderIds,
|
||||||
staticEntities,
|
staticEntities,
|
||||||
RenderTraversalOrder: GetRenderTraversalOrder(canonical));
|
RenderTraversalOrder: GetRenderTraversalOrder(canonical),
|
||||||
|
AdditionalOrdinaryRenderIds:
|
||||||
|
additionalOrdinaryRenderIds?.ToArray() ?? Array.Empty<ulong>());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrainVisibilityTransitions()
|
private void DrainVisibilityTransitions()
|
||||||
|
|
|
||||||
|
|
@ -957,6 +957,8 @@ public sealed class LandblockPresentationPipeline
|
||||||
IEnumerable<ulong>? renderIds =
|
IEnumerable<ulong>? renderIds =
|
||||||
transaction.Build.EnvCells?.Shells.Select(
|
transaction.Build.EnvCells?.Shells.Select(
|
||||||
static shell => shell.GeometryId);
|
static shell => shell.GeometryId);
|
||||||
|
IEnumerable<ulong>? ordinaryRenderIds =
|
||||||
|
transaction.Build.EnvCells?.WalkBuildingMeshDependencies;
|
||||||
transaction.SpatialPublication = transaction.Kind switch
|
transaction.SpatialPublication = transaction.Kind switch
|
||||||
{
|
{
|
||||||
PublicationKind.Loaded
|
PublicationKind.Loaded
|
||||||
|
|
@ -965,12 +967,14 @@ public sealed class LandblockPresentationPipeline
|
||||||
_state.CommitLandblockSpatial(
|
_state.CommitLandblockSpatial(
|
||||||
transaction.Build.Landblock,
|
transaction.Build.Landblock,
|
||||||
renderIds,
|
renderIds,
|
||||||
transaction.Tier),
|
transaction.Tier,
|
||||||
|
ordinaryRenderIds),
|
||||||
PublicationKind.PromoteExisting =>
|
PublicationKind.PromoteExisting =>
|
||||||
_state.CommitEntitiesToExistingLandblockSpatial(
|
_state.CommitEntitiesToExistingLandblockSpatial(
|
||||||
transaction.LandblockId,
|
transaction.LandblockId,
|
||||||
transaction.Build.Landblock.Entities,
|
transaction.Build.Landblock.Entities,
|
||||||
renderIds),
|
renderIds,
|
||||||
|
ordinaryRenderIds),
|
||||||
_ => throw new InvalidOperationException(
|
_ => throw new InvalidOperationException(
|
||||||
$"Unknown landblock publication kind {transaction.Kind}."),
|
$"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>
|
/// Brightness (OP6 rework, review S2: its OWN <c>DisplaySettings.ScreenBrightness</c>
|
||||||
/// field, range [-1,1] default 0 — NOT the pre-existing <c>Gamma</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
|
/// multiplier, a different unit system with its own live legacy-panel
|
||||||
/// consumer; no gamma-correction render pass exists for either); Automatic
|
/// consumer; no gamma-correction render pass exists for either); Landscape
|
||||||
/// Degrades/Graphics Performance/Degrade Distance/the four texture-detail-
|
/// Texture Detail/Environment Texture Detail/Texture Filtering/Multi-Pass
|
||||||
/// family menus/Multi-Pass Alpha (the renderer is Vulkan + one aggregate
|
/// Alpha (the renderer is Vulkan + one aggregate <c>QualityPreset</c>, no
|
||||||
/// <c>QualityPreset</c>, no per-feature knobs). Building Detail Textures is
|
/// per-feature knobs). Automatic Degrades, Graphics Performance, and Degrade
|
||||||
/// LIVE: #226 consumes it directly in the retail building/EnvCell detail pass;
|
/// 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
|
/// Camera Stiffness/Adjustment Speed/Align To Slope/Mouse Look
|
||||||
/// Sensitivity/Invert Mouselook Y Axis/Use Mouse Turning (TS-74 — no
|
/// Sensitivity/Invert Mouselook Y Axis/Use Mouse Turning (TS-74 — no
|
||||||
/// persistent mouse-turning camera mode exists for ANY of these six to
|
/// persistent mouse-turning camera mode exists for ANY of these six to
|
||||||
|
|
@ -138,8 +141,8 @@ namespace AcDream.App.UI.Layout;
|
||||||
///
|
///
|
||||||
/// <para>
|
/// <para>
|
||||||
/// <b>Caption dimming (AD-78, user-directed, 2026-08-11, gate 2).</b> Every
|
/// <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
|
/// STORE-ONLY row above (the 16 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
|
/// -- 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
|
/// AP-200's two Chat-font rows) renders its caption in
|
||||||
/// <see cref="UiRenderContext.StoreOnlyCaptionColor"/> instead of the normal
|
/// <see cref="UiRenderContext.StoreOnlyCaptionColor"/> instead of the normal
|
||||||
/// white/DAT-authored color. The row stays fully interactive -- it still
|
/// 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,
|
listBox, "ID_Graphics_AdaptiveDegrade", defaultValue: false, page, resolveString,
|
||||||
read: () => bindings.LoadDisplay().AutomaticDegrades,
|
read: () => bindings.LoadDisplay().AutomaticDegrades,
|
||||||
apply: value => bindings.SaveDisplay(bindings.LoadDisplay() with { AutomaticDegrades = value }),
|
apply: value => bindings.SaveDisplay(bindings.LoadDisplay() with { AutomaticDegrades = value }),
|
||||||
storeOnly: true); // AP-198
|
storeOnly: false);
|
||||||
|
|
||||||
BuildSliderRow(
|
BuildSliderRow(
|
||||||
listBox, RangedSliderTemplateIndex, "ID_Graphics_AdaptiveDegradeBias",
|
listBox, RangedSliderTemplateIndex, "ID_Graphics_AdaptiveDegradeBias",
|
||||||
min: -1f, max: 1f, defaultValue: 0f, page, resolveString,
|
min: -1f, max: 1f, defaultValue: 0f, page, resolveString,
|
||||||
read: () => bindings.LoadDisplay().GraphicsPerformance,
|
read: () => bindings.LoadDisplay().GraphicsPerformance,
|
||||||
apply: value => bindings.SaveDisplay(bindings.LoadDisplay() with { GraphicsPerformance = value }),
|
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");
|
rangeLowKey: "ID_Graphics_Value_Speed", rangeHighKey: "ID_Graphics_Value_Detail");
|
||||||
|
|
||||||
BuildSliderRow(
|
BuildSliderRow(
|
||||||
|
|
@ -1246,7 +1249,7 @@ public static class ConfigOptionsPageController
|
||||||
min: 0f, max: 100f, defaultValue: 50.0f, page, resolveString,
|
min: 0f, max: 100f, defaultValue: 50.0f, page, resolveString,
|
||||||
read: () => bindings.LoadDisplay().DegradeDistance,
|
read: () => bindings.LoadDisplay().DegradeDistance,
|
||||||
apply: value => bindings.SaveDisplay(bindings.LoadDisplay() with { DegradeDistance = value }),
|
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");
|
rangeLowKey: "ID_Graphics_Value_Close", rangeHighKey: "ID_Graphics_Value_Far");
|
||||||
|
|
||||||
display = bindings.LoadDisplay();
|
display = bindings.LoadDisplay();
|
||||||
|
|
|
||||||
|
|
@ -156,11 +156,10 @@ public sealed record DisplaySettings(
|
||||||
bool ShowFps,
|
bool ShowFps,
|
||||||
QualityPreset Quality,
|
QualityPreset Quality,
|
||||||
ParticleRange ParticleRange,
|
ParticleRange ParticleRange,
|
||||||
// Campaign OP slice OP6: the Config tab's "Graphics Options" +
|
// Campaign OP slice OP6: Config's graphics preferences. S5-c3 makes
|
||||||
// "Rendering Quality Options" rows with no acdream renderer consumer —
|
// AutomaticDegrades, GraphicsPerformance and DegradeDistance live for
|
||||||
// the world renderer is Vulkan + one aggregate QualityPreset, not
|
// building part-zero selection; the remaining per-feature knobs stay
|
||||||
// per-feature knobs (register row, OP6). Persisted faithfully; every
|
// store-only under AP-198. Every default below is retail's byte-verified
|
||||||
// default below is retail's own byte-verified
|
|
||||||
// gmClient::InitUIPreferences / gmConfigUI::InitOptions literal.
|
// gmClient::InitUIPreferences / gmConfigUI::InitOptions literal.
|
||||||
//
|
//
|
||||||
// OP6 rework (2026-08-11, review S2): Screen Brightness gets its OWN
|
// OP6 rework (2026-08-11, review S2): Screen Brightness gets its OWN
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ using AcDream.Core.Spells;
|
||||||
using AcDream.Runtime;
|
using AcDream.Runtime;
|
||||||
using AcDream.Runtime.Gameplay;
|
using AcDream.Runtime.Gameplay;
|
||||||
using AcDream.UI.Abstractions.Panels.Chat;
|
using AcDream.UI.Abstractions.Panels.Chat;
|
||||||
|
using AcDream.UI.Abstractions.Panels.Settings;
|
||||||
|
|
||||||
namespace AcDream.App.Tests.Composition;
|
namespace AcDream.App.Tests.Composition;
|
||||||
|
|
||||||
|
|
@ -36,6 +37,29 @@ public sealed class InteractionRetainedUiCompositionTests
|
||||||
InteractionRetainedUiCompositionPoint.InventoryContainerBound,
|
InteractionRetainedUiCompositionPoint.InventoryContainerBound,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FpsPanelBorrowsTheExactRendererLifetimeDegradeOwner()
|
||||||
|
{
|
||||||
|
DisplaySettings settings = DisplaySettings.Default with
|
||||||
|
{
|
||||||
|
AutomaticDegrades = false,
|
||||||
|
GraphicsPerformance = -0.35f,
|
||||||
|
};
|
||||||
|
var owner = new BuildingDegradeController(() => settings);
|
||||||
|
for (int i = 0; i < 21; i++)
|
||||||
|
owner.Tick(0.05);
|
||||||
|
|
||||||
|
FpsRuntimeBindings bindings =
|
||||||
|
RetailInteractionRetainedUiCompositionFactory.CreateFpsBindings(
|
||||||
|
owner, () => true);
|
||||||
|
|
||||||
|
Assert.Equal(owner.Fps, bindings.FramesPerSecond());
|
||||||
|
Assert.Equal(-0.35, bindings.DegradeMultiplier(), 6);
|
||||||
|
settings = settings with { GraphicsPerformance = 0.65f };
|
||||||
|
Assert.Equal(0.65, bindings.DegradeMultiplier(), 6);
|
||||||
|
Assert.True(bindings.IsVisible());
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void RadarLockBindingUsesAuthoritativeRequestInsteadOfPresentationOnlySetter()
|
public void RadarLockBindingUsesAuthoritativeRequestInsteadOfPresentationOnlySetter()
|
||||||
{
|
{
|
||||||
|
|
@ -278,6 +302,8 @@ public sealed class InteractionRetainedUiCompositionTests
|
||||||
new AcDream.App.Streaming.DeferredLocalPlayerTeleportNetworkSink(),
|
new AcDream.App.Streaming.DeferredLocalPlayerTeleportNetworkSink(),
|
||||||
KeyBindingsFilePath: "keybinds.json",
|
KeyBindingsFilePath: "keybinds.json",
|
||||||
Settings: null!,
|
Settings: null!,
|
||||||
|
BuildingDegrades: new BuildingDegradeController(
|
||||||
|
() => DisplaySettings.Default),
|
||||||
Runtime: runtime,
|
Runtime: runtime,
|
||||||
CombatAttackOperations: new NoopCombatOperations(),
|
CombatAttackOperations: new NoopCombatOperations(),
|
||||||
CombatTargetOperations: new RuntimeCombatTargetOperationsSlot(),
|
CombatTargetOperations: new RuntimeCombatTargetOperationsSlot(),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,228 @@
|
||||||
|
using AcDream.UI.Abstractions.Panels.Settings;
|
||||||
|
using AcDream.App.Rendering;
|
||||||
|
|
||||||
|
namespace AcDream.App.Tests.Rendering;
|
||||||
|
|
||||||
|
[Collection(ThreadSchedulingCollection.Name)]
|
||||||
|
public sealed class BuildingDegradeControllerTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void FpsUsesPriorTwentyFramesThenInsertsJustFinishedFrame()
|
||||||
|
{
|
||||||
|
DisplaySettings settings = DisplaySettings.Default;
|
||||||
|
var controller = new BuildingDegradeController(() => settings);
|
||||||
|
|
||||||
|
for (int i = 0; i < 20; i++)
|
||||||
|
controller.Tick(0.05);
|
||||||
|
|
||||||
|
Assert.Equal(20f / 0.95f, controller.Fps, 4);
|
||||||
|
controller.Tick(0.05);
|
||||||
|
Assert.Equal(20f, controller.Fps, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FpsWideSumNarrowsOnceAtStoreAndMatchesRetailBits()
|
||||||
|
{
|
||||||
|
DisplaySettings settings = DisplaySettings.Default;
|
||||||
|
var controller = new BuildingDegradeController(() => settings);
|
||||||
|
for (int i = 0; i < 10; i++)
|
||||||
|
{
|
||||||
|
controller.Tick(0.001d);
|
||||||
|
controller.Tick(0.005d);
|
||||||
|
}
|
||||||
|
|
||||||
|
controller.Tick(0d);
|
||||||
|
|
||||||
|
Assert.Equal(0x43A6AAABu, BitConverter.SingleToUInt32Bits(controller.Fps));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FpsHistoryMovesOldSlotsUpAndStoresCurrentAtSlotZero()
|
||||||
|
{
|
||||||
|
float[] history = Enumerable.Range(1, 20).Select(static value => (float)value).ToArray();
|
||||||
|
|
||||||
|
BuildingDegradeController.AdvanceFrameHistory(history, 99f);
|
||||||
|
|
||||||
|
Assert.Equal(99f, history[0]);
|
||||||
|
for (int i = 1; i < history.Length; i++)
|
||||||
|
Assert.Equal((float)i, history[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ManualAndAutomaticModesUseTheSamePersistedSettingsWithoutResettingAutoHistory()
|
||||||
|
{
|
||||||
|
DisplaySettings settings = DisplaySettings.Default with
|
||||||
|
{
|
||||||
|
AutomaticDegrades = true,
|
||||||
|
GraphicsPerformance = -0.4f,
|
||||||
|
DegradeDistance = 73f,
|
||||||
|
};
|
||||||
|
var controller = new BuildingDegradeController(() => settings);
|
||||||
|
|
||||||
|
for (int i = 0; i < 51; i++)
|
||||||
|
controller.Tick(1d / 14d);
|
||||||
|
float automatic = controller.AutomaticMultiplier;
|
||||||
|
Assert.True(automatic > 0f);
|
||||||
|
Assert.Equal(automatic, controller.ActiveMultiplier);
|
||||||
|
Assert.Equal(73f, controller.DegradeDistance);
|
||||||
|
|
||||||
|
settings = settings with { AutomaticDegrades = false };
|
||||||
|
controller.Tick(0.05);
|
||||||
|
Assert.Equal(-0.4f, controller.ActiveMultiplier);
|
||||||
|
Assert.Equal(automatic, controller.AutomaticMultiplier);
|
||||||
|
|
||||||
|
settings = settings with { AutomaticDegrades = true };
|
||||||
|
Assert.Equal(automatic, controller.ActiveMultiplier);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WarmedTickIsAllocationFreeAndExceptionalDeltasStayDefined()
|
||||||
|
{
|
||||||
|
DisplaySettings settings = DisplaySettings.Default with
|
||||||
|
{
|
||||||
|
AutomaticDegrades = true,
|
||||||
|
};
|
||||||
|
var controller = new BuildingDegradeController(() => settings);
|
||||||
|
for (int i = 0; i < 100; i++)
|
||||||
|
controller.Tick(1d / 60d);
|
||||||
|
|
||||||
|
controller.Tick(0d);
|
||||||
|
controller.Tick(double.NaN);
|
||||||
|
controller.Tick(double.PositiveInfinity);
|
||||||
|
controller.Tick(double.NegativeInfinity);
|
||||||
|
|
||||||
|
long allocated = ZeroAllocationProbe.MeasureWarmed(
|
||||||
|
() => controller.Tick(1d / 60d),
|
||||||
|
batchSize: 10_000,
|
||||||
|
warmupBatches: 2,
|
||||||
|
samples: 5);
|
||||||
|
|
||||||
|
Assert.Equal(0, allocated);
|
||||||
|
Assert.InRange(controller.AutomaticMultiplier, -1f, 1f);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(0d, 0x41A86BCAu)]
|
||||||
|
[InlineData(0.00001d, 0x41A86B56u)]
|
||||||
|
[InlineData(double.NaN, 0u)]
|
||||||
|
[InlineData(double.PositiveInfinity, 0u)]
|
||||||
|
[InlineData(double.NegativeInfinity, 0u)]
|
||||||
|
public void ZeroTinyAndExceptionalDeltasKeepRetailFloatHistoryWithoutSanitizing(
|
||||||
|
double exceptionalDelta,
|
||||||
|
uint expectedFollowingFpsBits)
|
||||||
|
{
|
||||||
|
DisplaySettings settings = DisplaySettings.Default with
|
||||||
|
{
|
||||||
|
AutomaticDegrades = true,
|
||||||
|
};
|
||||||
|
var controller = new BuildingDegradeController(() => settings);
|
||||||
|
for (int i = 0; i < 20; i++)
|
||||||
|
controller.Tick(0.05d);
|
||||||
|
|
||||||
|
controller.Tick(exceptionalDelta);
|
||||||
|
Assert.Equal(0x41A00000u, BitConverter.SingleToUInt32Bits(controller.Fps));
|
||||||
|
Assert.Equal(0u, BitConverter.SingleToUInt32Bits(controller.AutomaticMultiplier));
|
||||||
|
|
||||||
|
controller.Tick(0.05d);
|
||||||
|
Assert.Equal(expectedFollowingFpsBits, BitConverter.SingleToUInt32Bits(controller.Fps));
|
||||||
|
Assert.Equal(0u, BitConverter.SingleToUInt32Bits(controller.AutomaticMultiplier));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NewControllerStartsAtRetailZeroWithoutSharingHistory()
|
||||||
|
{
|
||||||
|
DisplaySettings settings = DisplaySettings.Default with
|
||||||
|
{
|
||||||
|
AutomaticDegrades = true,
|
||||||
|
};
|
||||||
|
var first = new BuildingDegradeController(() => settings);
|
||||||
|
for (int i = 0; i < 51; i++)
|
||||||
|
first.Tick(1d / 14d);
|
||||||
|
|
||||||
|
var restarted = new BuildingDegradeController(() => settings);
|
||||||
|
Assert.NotEqual(0f, first.AutomaticMultiplier);
|
||||||
|
Assert.Equal(0f, restarted.AutomaticMultiplier);
|
||||||
|
Assert.Equal(0f, restarted.Fps);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(0f, -0.150000006f)]
|
||||||
|
[InlineData(10f, 0f)]
|
||||||
|
[InlineData(14f, 0.0054545454f)]
|
||||||
|
[InlineData(20f, 0.1f)]
|
||||||
|
[InlineData(25f, 0.1f)]
|
||||||
|
public void ExactFiveWeightFormulaMatchesFixedRetailVectors(
|
||||||
|
float fps,
|
||||||
|
float expected)
|
||||||
|
{
|
||||||
|
Assert.Equal(
|
||||||
|
expected,
|
||||||
|
BuildingDegradeController.CalculateCandidate(fps, 0f),
|
||||||
|
7);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FiveWeightFormulaKeepsX87IntermediatesUntilCandidateStore()
|
||||||
|
{
|
||||||
|
float candidate = BuildingDegradeController.CalculateCandidate(14f, 0f);
|
||||||
|
|
||||||
|
Assert.Equal(0x3BB2BC0Au, BitConverter.SingleToUInt32Bits(candidate));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FiveWeightNumeratorPreservesAllFourRetailFloatStores()
|
||||||
|
{
|
||||||
|
float candidate = BuildingDegradeController.CalculateCandidate(16.25f, 0f);
|
||||||
|
|
||||||
|
Assert.Equal(0x3CA3D70Au, BitConverter.SingleToUInt32Bits(candidate));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void StabilityUsesQwordPointZeroOneRatherThanPromotedFloatConstant()
|
||||||
|
{
|
||||||
|
float candidate = BitConverter.UInt32BitsToSingle(0x3C13D70Au);
|
||||||
|
float prior = -BitConverter.UInt32BitsToSingle(0x3A800001u);
|
||||||
|
float[] history = Enumerable.Repeat(prior, 30).ToArray();
|
||||||
|
|
||||||
|
double difference = Math.Abs((double)prior - candidate);
|
||||||
|
Assert.True(difference > (double)0.01f);
|
||||||
|
Assert.True(difference < 0.01);
|
||||||
|
Assert.True(BuildingDegradeController.IsCandidateStable(history, candidate));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AutomaticCommitRequiresAllThirtyPriorSlotsWithinStrictBand()
|
||||||
|
{
|
||||||
|
float[] history = Enumerable.Repeat(0.1f, 30).ToArray();
|
||||||
|
history[15] = 0f;
|
||||||
|
|
||||||
|
float rejected = BuildingDegradeController.AdvanceAutomaticMultiplier(
|
||||||
|
history, automatic: true, fps: 20f, current: 0f);
|
||||||
|
|
||||||
|
Assert.Equal(0f, rejected);
|
||||||
|
Assert.Equal(0f, history[^1]);
|
||||||
|
|
||||||
|
Array.Fill(history, 0.1f);
|
||||||
|
float committed = BuildingDegradeController.AdvanceAutomaticMultiplier(
|
||||||
|
history, automatic: true, fps: 20f, current: 0f);
|
||||||
|
|
||||||
|
Assert.Equal(0.1f, committed, 7);
|
||||||
|
Assert.All(history, value => Assert.Equal(0.1f, value, 7));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AutomaticClampAndManualArmMatchFixedExpectedTransitions()
|
||||||
|
{
|
||||||
|
float[] history = Enumerable.Repeat(1f, 30).ToArray();
|
||||||
|
float clamped = BuildingDegradeController.AdvanceAutomaticMultiplier(
|
||||||
|
history, automatic: true, fps: 25f, current: 0.95f);
|
||||||
|
Assert.Equal(1f, clamped);
|
||||||
|
|
||||||
|
history[10] = -1f;
|
||||||
|
float manual = BuildingDegradeController.AdvanceAutomaticMultiplier(
|
||||||
|
history, automatic: false, fps: 0f, current: 0.35f);
|
||||||
|
|
||||||
|
Assert.Equal(0.35f, manual);
|
||||||
|
Assert.Equal(0.35f, history[^1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using AcDream.App.Rendering;
|
using AcDream.App.Rendering;
|
||||||
|
using AcDream.UI.Abstractions.Panels.Settings;
|
||||||
|
|
||||||
namespace AcDream.App.Tests.Rendering;
|
namespace AcDream.App.Tests.Rendering;
|
||||||
|
|
||||||
|
|
@ -84,6 +85,43 @@ public sealed class RenderFrameOrchestratorTests
|
||||||
Assert.Empty(phases.ObservedInputs);
|
Assert.Empty(phases.ObservedInputs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AcceptedRenderTicksSharedDegradeOwnerExactlyOnceAndZeroAreaDoesNot()
|
||||||
|
{
|
||||||
|
var calls = new List<string>();
|
||||||
|
var phases = new RecordingPhases(calls);
|
||||||
|
var degradation = new RecordingDegradeTick(calls);
|
||||||
|
var orchestrator = new RenderFrameOrchestrator(
|
||||||
|
phases, phases, phases, phases, phases, phases, phases, phases,
|
||||||
|
degradation);
|
||||||
|
|
||||||
|
double[] durations =
|
||||||
|
[
|
||||||
|
1d / 1024d, 2d / 1024d, 3d / 1024d, 4d / 1024d,
|
||||||
|
5d / 1024d, 6d / 1024d, 7d / 1024d, 8d / 1024d,
|
||||||
|
9d / 1024d, 10d / 1024d, 11d / 1024d, 12d / 1024d,
|
||||||
|
13d / 1024d, 14d / 1024d, 15d / 1024d, 16d / 1024d,
|
||||||
|
17d / 1024d, 18d / 1024d, 19d / 1024d, 20d / 1024d,
|
||||||
|
31d / 1024d,
|
||||||
|
];
|
||||||
|
for (int i = 0; i < durations.Length - 1; i++)
|
||||||
|
orchestrator.Render(Input with { DeltaSeconds = durations[i] });
|
||||||
|
orchestrator.Render(Input with { ViewportWidth = 0 });
|
||||||
|
orchestrator.Render(Input with { DeltaSeconds = durations[^1] });
|
||||||
|
|
||||||
|
Assert.Equal(durations, degradation.Durations);
|
||||||
|
Assert.Equal(0x42C30C31u, BitConverter.SingleToUInt32Bits(degradation.Fps));
|
||||||
|
Assert.Equal(durations.Length, calls.Count(static call => call == "degrade-tick"));
|
||||||
|
for (int i = 0; i < calls.Count; i++)
|
||||||
|
{
|
||||||
|
if (calls[i] == "gpu-begin")
|
||||||
|
{
|
||||||
|
Assert.True(i > 0, "degrade tick must precede BeginFrame");
|
||||||
|
Assert.Equal("degrade-tick", calls[i - 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void BeginFailure_DoesNotAttemptAnyPhaseOrClose()
|
public void BeginFailure_DoesNotAttemptAnyPhaseOrClose()
|
||||||
{
|
{
|
||||||
|
|
@ -278,6 +316,7 @@ public sealed class RenderFrameOrchestratorTests
|
||||||
typeof(IRenderFrameDiagnosticsPhase),
|
typeof(IRenderFrameDiagnosticsPhase),
|
||||||
typeof(IRenderFramePostDiagnosticsPhase),
|
typeof(IRenderFramePostDiagnosticsPhase),
|
||||||
typeof(IRenderFrameFailureRecovery),
|
typeof(IRenderFrameFailureRecovery),
|
||||||
|
typeof(IBuildingDegradeFrameTick),
|
||||||
];
|
];
|
||||||
FieldInfo[] fields = typeof(RenderFrameOrchestrator).GetFields(
|
FieldInfo[] fields = typeof(RenderFrameOrchestrator).GetFields(
|
||||||
BindingFlags.Instance | BindingFlags.NonPublic);
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
||||||
|
|
@ -293,7 +332,7 @@ public sealed class RenderFrameOrchestratorTests
|
||||||
Assert.All(
|
Assert.All(
|
||||||
expectedFieldTypes,
|
expectedFieldTypes,
|
||||||
contract => Assert.False(contract.IsAssignableFrom(typeof(GameWindow))));
|
contract => Assert.False(contract.IsAssignableFrom(typeof(GameWindow))));
|
||||||
foreach (Type contract in expectedFieldTypes)
|
foreach (Type contract in expectedFieldTypes.Where(type => type.IsInterface))
|
||||||
{
|
{
|
||||||
foreach (MethodInfo method in contract.GetMethods())
|
foreach (MethodInfo method in contract.GetMethods())
|
||||||
{
|
{
|
||||||
|
|
@ -395,6 +434,25 @@ public sealed class RenderFrameOrchestratorTests
|
||||||
private static RenderFrameOrchestrator Create(RecordingPhases phases) =>
|
private static RenderFrameOrchestrator Create(RecordingPhases phases) =>
|
||||||
new(phases, phases, phases, phases, phases, phases, phases, phases);
|
new(phases, phases, phases, phases, phases, phases, phases, phases);
|
||||||
|
|
||||||
|
private sealed class RecordingDegradeTick : IBuildingDegradeFrameTick
|
||||||
|
{
|
||||||
|
private readonly List<string> _calls;
|
||||||
|
private readonly BuildingDegradeController _inner = new(
|
||||||
|
() => DisplaySettings.Default);
|
||||||
|
|
||||||
|
public RecordingDegradeTick(List<string> calls) => _calls = calls;
|
||||||
|
|
||||||
|
public List<double> Durations { get; } = [];
|
||||||
|
public float Fps => _inner.Fps;
|
||||||
|
|
||||||
|
public void Tick(double elapsedSeconds)
|
||||||
|
{
|
||||||
|
_calls.Add("degrade-tick");
|
||||||
|
Durations.Add(elapsedSeconds);
|
||||||
|
_inner.Tick(elapsedSeconds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private sealed class RecordingPhases :
|
private sealed class RecordingPhases :
|
||||||
IRenderFrameLifetime,
|
IRenderFrameLifetime,
|
||||||
IRenderFrameGpuMeasurement,
|
IRenderFrameGpuMeasurement,
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,14 @@ public sealed class RetailFrameWalkTests
|
||||||
/// them — proves the terrain/building/object-turn ORDER, not just
|
/// them — proves the terrain/building/object-turn ORDER, not just
|
||||||
/// each vocabulary in isolation.</summary>
|
/// each vocabulary in isolation.</summary>
|
||||||
public readonly List<string> Combined = new();
|
public readonly List<string> Combined = new();
|
||||||
|
public int BuildingAlphaTurns;
|
||||||
|
public readonly List<WalkBuildingSelection> ShellSelections = new();
|
||||||
|
|
||||||
|
public void OnBuildingTurn(WalkBuilding building) => BuildingAlphaTurns++;
|
||||||
|
|
||||||
|
public void OnBuildingShellTurn(
|
||||||
|
WalkBuilding building,
|
||||||
|
WalkBuildingSelection selection) => ShellSelections.Add(selection);
|
||||||
|
|
||||||
public void Emit(in WalkEvent walkEvent)
|
public void Emit(in WalkEvent walkEvent)
|
||||||
{
|
{
|
||||||
|
|
@ -66,7 +74,7 @@ public sealed class RetailFrameWalkTests
|
||||||
=> new(screenX, screenY, 100f);
|
=> new(screenX, screenY, 100f);
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class TestContext : IWalkFrameContext, IRetailFrameWalkContext
|
private class TestContext : IWalkFrameContext, IRetailFrameWalkContext
|
||||||
{
|
{
|
||||||
public readonly Dictionary<uint, WalkCell> Cells = new();
|
public readonly Dictionary<uint, WalkCell> Cells = new();
|
||||||
private readonly Matrix4x4 _viewProj;
|
private readonly Matrix4x4 _viewProj;
|
||||||
|
|
@ -88,7 +96,7 @@ public sealed class RetailFrameWalkTests
|
||||||
public float ViewportHeight => 480f;
|
public float ViewportHeight => 480f;
|
||||||
|
|
||||||
public Vector3 ViewpointInBuilding(WalkBuilding building) => Vector3.Zero;
|
public Vector3 ViewpointInBuilding(WalkBuilding building) => Vector3.Zero;
|
||||||
public float ViewerDistanceTo(WalkBuilding building) => 0f;
|
public virtual float ViewerDistanceTo(WalkBuilding building) => 0f;
|
||||||
public IWalkFrameContext CellContext => this;
|
public IWalkFrameContext CellContext => this;
|
||||||
// Permissive near plane: every column wholly inside.
|
// Permissive near plane: every column wholly inside.
|
||||||
// S3 chunk 3 (T2): settable, default unchanged — a permissive
|
// S3 chunk 3 (T2): settable, default unchanged — a permissive
|
||||||
|
|
@ -165,11 +173,154 @@ public sealed class RetailFrameWalkTests
|
||||||
var ctx = new TestContext();
|
var ctx = new TestContext();
|
||||||
var walk = new RetailFrameWalk();
|
var walk = new RetailFrameWalk();
|
||||||
var recorder = new Recorder();
|
var recorder = new Recorder();
|
||||||
var building = new WalkBuilding { PositionCellId = 0xF518002E, HasGeometry = false };
|
var building = new WalkBuilding { PositionCellId = 0xF518002E, GfxObjId = 0 };
|
||||||
|
|
||||||
walk.DrawBuilding(building, new WalkPortalView(), ctx, recorder);
|
walk.DrawBuilding(building, new WalkPortalView(), ctx, recorder);
|
||||||
|
|
||||||
Assert.Equal("BLD:f518002e", recorder.Signature());
|
Assert.Equal("BLD:f518002e", recorder.Signature());
|
||||||
|
Assert.Equal(0, recorder.BuildingAlphaTurns);
|
||||||
|
Assert.Empty(recorder.ShellSelections);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NullBspSelectedGfxStillRunsAlphaAndShellWithoutPortalWalk()
|
||||||
|
{
|
||||||
|
var building = new WalkBuilding
|
||||||
|
{
|
||||||
|
PositionCellId = 0xF518002Eu,
|
||||||
|
GfxObjId = 0x01000001u,
|
||||||
|
DrawingBsp = null,
|
||||||
|
};
|
||||||
|
var recorder = new Recorder();
|
||||||
|
|
||||||
|
new RetailFrameWalk().DrawBuilding(
|
||||||
|
building, new WalkPortalView(), new TestContext(), recorder);
|
||||||
|
|
||||||
|
Assert.Equal("BLD:f518002e", recorder.Signature());
|
||||||
|
Assert.Equal(1, recorder.BuildingAlphaTurns);
|
||||||
|
WalkBuildingSelection selected = Assert.Single(recorder.ShellSelections);
|
||||||
|
Assert.Equal(0x01000001u, selected.GfxObjId);
|
||||||
|
Assert.Null(selected.DrawingBsp);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SelectionUsesStrictThresholdAndFinalZeroIsCompleteBodyFailure()
|
||||||
|
{
|
||||||
|
var firstBsp = new WalkBspNode();
|
||||||
|
var building = new WalkBuilding
|
||||||
|
{
|
||||||
|
GfxObjId = 0x0100FFFFu,
|
||||||
|
DegradeLevels =
|
||||||
|
[
|
||||||
|
new(0x01000001u, 1u, 10f, 20f, 30f, firstBsp),
|
||||||
|
new(0u, 1u, 30f, 40f, 50f, null),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.Equal(0x01000001u, building.Select(29.999f, 0f, 1f).GfxObjId);
|
||||||
|
WalkBuildingSelection equality = building.Select(30f, 0f, 1f);
|
||||||
|
Assert.Equal(0u, equality.GfxObjId);
|
||||||
|
Assert.Equal(1, equality.Level);
|
||||||
|
|
||||||
|
var recorder = new Recorder();
|
||||||
|
var context = new DistanceContext(80f);
|
||||||
|
new RetailFrameWalk().DrawBuilding(
|
||||||
|
building, new WalkPortalView(), context, recorder);
|
||||||
|
Assert.Equal("BLD:00000000", recorder.Signature());
|
||||||
|
Assert.Equal(0, recorder.BuildingAlphaTurns);
|
||||||
|
Assert.Empty(recorder.ShellSelections);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SelectionComparesStoredFloatEffectiveDistanceToWideThreshold()
|
||||||
|
{
|
||||||
|
const float multiplier = 0.0020020019728690386f;
|
||||||
|
const float effective = 24.04804801940918f;
|
||||||
|
Assert.Equal(0x41C06267u, BitConverter.SingleToUInt32Bits(effective));
|
||||||
|
var building = new WalkBuilding
|
||||||
|
{
|
||||||
|
DegradeLevels =
|
||||||
|
[
|
||||||
|
new(0x01000001u, 1u, 0f, 24f, 48f, null),
|
||||||
|
new(0x01000002u, 1u, 48f, 64f, 80f, null),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
WalkBuildingSelection selected = building.Select(
|
||||||
|
effective, degradeDistance: 0f, degradeMultiplier: multiplier);
|
||||||
|
|
||||||
|
Assert.Equal(0, selected.Level);
|
||||||
|
Assert.Equal(0x01000001u, selected.GfxObjId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DirectAndLadderSelectionsCarryExactIdBspLevelAndMode()
|
||||||
|
{
|
||||||
|
var directBsp = new WalkBspNode();
|
||||||
|
var direct = new WalkBuilding
|
||||||
|
{
|
||||||
|
GfxObjId = 0x01000010u,
|
||||||
|
DrawingBsp = directBsp,
|
||||||
|
};
|
||||||
|
Assert.Equal(
|
||||||
|
new WalkBuildingSelection(0x01000010u, directBsp, 0, 1u),
|
||||||
|
direct.Select(float.NaN, 50f, float.NaN));
|
||||||
|
|
||||||
|
var nearBsp = new WalkBspNode();
|
||||||
|
var middleBsp = new WalkBspNode();
|
||||||
|
var ladder = new WalkBuilding
|
||||||
|
{
|
||||||
|
GfxObjId = 0x0100FFFFu,
|
||||||
|
DegradeLevels =
|
||||||
|
[
|
||||||
|
new(0x01000011u, 3u, 10f, 20f, 30f, nearBsp),
|
||||||
|
new(0x01000012u, 5u, 30f, 40f, 50f, middleBsp),
|
||||||
|
new(0u, 7u, 50f, 60f, 70f, null),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
new WalkBuildingSelection(0x01000011u, nearBsp, 0, 3u),
|
||||||
|
ladder.Select(29f, 5f, 0.5f)); // effective 24 < positive threshold 25
|
||||||
|
Assert.Equal(
|
||||||
|
new WalkBuildingSelection(0x01000011u, nearBsp, 0, 3u),
|
||||||
|
ladder.Select(-19f, 5f, -0.5f)); // effective 14 < negative threshold 15
|
||||||
|
Assert.Equal(
|
||||||
|
new WalkBuildingSelection(0x01000012u, middleBsp, 1, 5u),
|
||||||
|
ladder.Select(30f, 5f, 0.5f)); // equality advances
|
||||||
|
Assert.Equal(2, ladder.Select(float.PositiveInfinity, 5f, 0f).Level);
|
||||||
|
Assert.Equal(2, ladder.Select(float.NegativeInfinity, 5f, 0f).Level);
|
||||||
|
Assert.Equal(2, ladder.Select(float.NaN, 5f, 0f).Level);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DisableAndForcedLevelBranchesPreserveRetailPrecedenceAndClamp()
|
||||||
|
{
|
||||||
|
var firstBsp = new WalkBspNode();
|
||||||
|
var building = new WalkBuilding
|
||||||
|
{
|
||||||
|
DegradeLevels =
|
||||||
|
[
|
||||||
|
new(0x01000021u, 11u, 1f, 2f, 3f, firstBsp),
|
||||||
|
new(0x01000022u, 12u, 3f, 4f, 5f, null),
|
||||||
|
new(0u, 13u, 5f, 6f, 7f, null),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
new WalkBuildingSelection(0x01000021u, firstBsp, 0, 11u),
|
||||||
|
building.Select(float.PositiveInfinity, 50f, 1f,
|
||||||
|
degradesDisabled: true, forcedLevel: 2));
|
||||||
|
Assert.Equal(1, building.Select(0f, 50f, 0f, forcedLevel: 1).Level);
|
||||||
|
WalkBuildingSelection clamped = building.Select(0f, 50f, 0f, forcedLevel: 99);
|
||||||
|
Assert.Equal(2, clamped.Level);
|
||||||
|
Assert.Equal(13u, clamped.Mode);
|
||||||
|
Assert.Equal(0u, clamped.GfxObjId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class DistanceContext(float distance) : TestContext
|
||||||
|
{
|
||||||
|
public override float ViewerDistanceTo(WalkBuilding building) => distance;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── T2 (S3 chunk 3 §9.3): the per-cell interleave — DrawLandCell (LC)
|
// ── T2 (S3 chunk 3 §9.3): the per-cell interleave — DrawLandCell (LC)
|
||||||
|
|
@ -224,7 +375,7 @@ public sealed class RetailFrameWalkTests
|
||||||
nearBlock.EnsureCellArrays();
|
nearBlock.EnsureCellArrays();
|
||||||
// A building at an ordinary in-view cell (cx=3, cy=0), away from
|
// A building at an ordinary in-view cell (cx=3, cy=0), away from
|
||||||
// both the excluded column and the closest/farthest cells.
|
// both the excluded column and the closest/farthest cells.
|
||||||
var building = new WalkBuilding { PositionCellId = 0x11110019u, HasGeometry = false };
|
var building = new WalkBuilding { PositionCellId = 0x11110019u, GfxObjId = 0 };
|
||||||
nearBlock.CellBuildings[3 * 8 + 0] = building;
|
nearBlock.CellBuildings[3 * 8 + 0] = building;
|
||||||
|
|
||||||
// The far block (grid slot 3 = (1,1), the diagonal corner — drawn
|
// The far block (grid slot 3 = (1,1), the diagonal corner — drawn
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,104 @@
|
||||||
|
using AcDream.App.Tests.Rendering;
|
||||||
|
using DatReaderWriter;
|
||||||
|
using DatReaderWriter.DBObjs;
|
||||||
|
using DatReaderWriter.Options;
|
||||||
|
using DatReaderWriter.Types;
|
||||||
|
|
||||||
|
namespace AcDream.App.Tests.Rendering.Walk;
|
||||||
|
|
||||||
|
[Trait("Lane", "InstalledDat")]
|
||||||
|
public sealed class WalkBuildingInstalledDatCensusTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void CompleteInstalledBuildingCorpusMatchesBoundedSelectionCensus()
|
||||||
|
{
|
||||||
|
string? datDir = CornerFloodReplayTests.ResolveDatDir();
|
||||||
|
if (datDir is null)
|
||||||
|
Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory.");
|
||||||
|
using var dats = new DatCollection(datDir!, DatAccessType.Read);
|
||||||
|
|
||||||
|
int landblocks = 0;
|
||||||
|
int instances = 0;
|
||||||
|
int ladderInstances = 0;
|
||||||
|
int ladderSlots = 0;
|
||||||
|
int nonzeroSlots = 0;
|
||||||
|
int zeroFinalSlots = 0;
|
||||||
|
var models = new HashSet<uint>();
|
||||||
|
var ladderModels = new HashSet<uint>();
|
||||||
|
var ladderHistogram = new Dictionary<int, int>();
|
||||||
|
|
||||||
|
for (uint blockX = 0; blockX <= 0xFF; blockX++)
|
||||||
|
for (uint blockY = 0; blockY <= 0xFF; blockY++)
|
||||||
|
{
|
||||||
|
uint infoId = (blockX << 24) | (blockY << 16) | 0xFFFEu;
|
||||||
|
LandBlockInfo? info = dats.Get<LandBlockInfo>(infoId);
|
||||||
|
if (info?.Buildings is not { Count: > 0 } buildings)
|
||||||
|
continue;
|
||||||
|
landblocks++;
|
||||||
|
var anchors = new HashSet<uint>();
|
||||||
|
uint landblock = infoId & 0xFFFF0000u;
|
||||||
|
|
||||||
|
foreach (BuildingInfo building in buildings)
|
||||||
|
{
|
||||||
|
instances++;
|
||||||
|
models.Add(building.ModelId);
|
||||||
|
GfxObj? baseGfx = dats.Get<GfxObj>(building.ModelId);
|
||||||
|
Assert.NotNull(baseGfx);
|
||||||
|
|
||||||
|
BuildingPortal? firstInteriorPortal = building.Portals.FirstOrDefault(
|
||||||
|
static portal => portal.OtherCellId != 0xFFFF);
|
||||||
|
int cellX = (int)MathF.Floor(building.Frame.Origin.X / 24f);
|
||||||
|
int cellY = (int)MathF.Floor(building.Frame.Origin.Y / 24f);
|
||||||
|
uint placementCell = landblock | (uint)(cellX * 8 + cellY + 1);
|
||||||
|
uint anchor = firstInteriorPortal is null
|
||||||
|
? placementCell
|
||||||
|
: landblock | firstInteriorPortal.OtherCellId;
|
||||||
|
Assert.True(anchors.Add(anchor),
|
||||||
|
$"duplicate building anchor 0x{anchor:X8}");
|
||||||
|
|
||||||
|
if (baseGfx!.DIDDegrade == 0)
|
||||||
|
continue;
|
||||||
|
GfxObjDegradeInfo? ladder =
|
||||||
|
dats.Get<GfxObjDegradeInfo>(baseGfx.DIDDegrade);
|
||||||
|
Assert.NotNull(ladder);
|
||||||
|
ladderInstances++;
|
||||||
|
ladderModels.Add(building.ModelId);
|
||||||
|
int count = ladder!.Degrades.Count;
|
||||||
|
ladderSlots += count;
|
||||||
|
ladderHistogram[count] = ladderHistogram.GetValueOrDefault(count) + 1;
|
||||||
|
Assert.NotEmpty(ladder.Degrades);
|
||||||
|
Assert.Equal(0u, (uint)ladder.Degrades[^1].Id);
|
||||||
|
zeroFinalSlots++;
|
||||||
|
|
||||||
|
for (int i = 0; i < ladder.Degrades.Count - 1; i++)
|
||||||
|
{
|
||||||
|
GfxObjInfo slot = ladder.Degrades[i];
|
||||||
|
Assert.NotEqual(0u, (uint)slot.Id);
|
||||||
|
GfxObj? selected = dats.Get<GfxObj>((uint)slot.Id);
|
||||||
|
Assert.NotNull(selected);
|
||||||
|
Assert.NotNull(selected!.DrawingBSP?.Root);
|
||||||
|
nonzeroSlots++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.Equal(1_639, landblocks);
|
||||||
|
Assert.Equal(6_979, instances);
|
||||||
|
Assert.Equal(398, models.Count);
|
||||||
|
Assert.Equal(6_760, ladderInstances);
|
||||||
|
Assert.Equal(27_859, ladderSlots);
|
||||||
|
Assert.Equal(350, ladderModels.Count);
|
||||||
|
Assert.Equal(6_760, zeroFinalSlots);
|
||||||
|
Assert.Equal(21_099, nonzeroSlots);
|
||||||
|
Assert.Equal(
|
||||||
|
new Dictionary<int, int>
|
||||||
|
{
|
||||||
|
[2] = 196,
|
||||||
|
[3] = 216,
|
||||||
|
[4] = 4_964,
|
||||||
|
[5] = 1_341,
|
||||||
|
[6] = 43,
|
||||||
|
},
|
||||||
|
ladderHistogram);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1036,6 +1036,7 @@ public sealed partial class WalkFrameDriverTests
|
||||||
var building = new WalkBuilding
|
var building = new WalkBuilding
|
||||||
{
|
{
|
||||||
PositionCellId = 0xA9B4000Fu,
|
PositionCellId = 0xA9B4000Fu,
|
||||||
|
GfxObjId = (uint)shellGfxObj,
|
||||||
Portals =
|
Portals =
|
||||||
[
|
[
|
||||||
new WalkBldPortal
|
new WalkBldPortal
|
||||||
|
|
@ -1139,6 +1140,36 @@ public sealed partial class WalkFrameDriverTests
|
||||||
Assert.Equal(3, mdiCalls.Sum(c => (int)c.DrawCount));
|
Assert.Equal(3, mdiCalls.Sum(c => (int)c.DrawCount));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BuildingShellTurnAllowsEmptyRecordButFailsLoudOnDuplicateAnchorRecords()
|
||||||
|
{
|
||||||
|
using var fx = new DispatcherFixture();
|
||||||
|
var building = new WalkBuilding
|
||||||
|
{
|
||||||
|
PositionCellId = 0xA9B40001u,
|
||||||
|
GfxObjId = 0x01000001u,
|
||||||
|
};
|
||||||
|
var selected = new WalkBuildingSelection(
|
||||||
|
building.GfxObjId, null, 0, 1u);
|
||||||
|
var world = new FakeWorldData();
|
||||||
|
var driver = new WalkFrameDriver(
|
||||||
|
fx.Dispatcher, new RecordingLeafRenderer([]), world);
|
||||||
|
driver.BeginFrame(new TestContext(), Matrix4x4.Identity, Vector3.Zero);
|
||||||
|
|
||||||
|
((IWalkEventSink)driver).OnBuildingShellTurn(building, selected);
|
||||||
|
|
||||||
|
RenderProjectionRecord record = MakeRecord(
|
||||||
|
301, 0, Vector3.Zero,
|
||||||
|
[new MeshRef(building.GfxObjId, Matrix4x4.Identity)],
|
||||||
|
isBuildingShell: true);
|
||||||
|
world.ShellByBuilding[building] = new WalkFrameStaticRecords(
|
||||||
|
new[] { record, record with { Id = RenderProjectionId.FromRaw(302) } },
|
||||||
|
0xA9B4u);
|
||||||
|
InvalidOperationException error = Assert.Throws<InvalidOperationException>(
|
||||||
|
() => ((IWalkEventSink)driver).OnBuildingShellTurn(building, selected));
|
||||||
|
Assert.Contains("expected at most one", error.Message, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
// ── S4-c1 C1/T2 (quantifier corrected at fix round 1 F1): DrawPortalPoly
|
// ── S4-c1 C1/T2 (quantifier corrected at fix round 1 F1): DrawPortalPoly
|
||||||
// Internal's degenerate-input guard, ported at the punch-fan producer
|
// Internal's degenerate-input guard, ported at the punch-fan producer
|
||||||
// (WalkFrameDriver.OnPunchGeometry — the handler that owns the LOCAL
|
// (WalkFrameDriver.OnPunchGeometry — the handler that owns the LOCAL
|
||||||
|
|
|
||||||
|
|
@ -52,9 +52,9 @@ public sealed class WalkPortalGateDumpTests
|
||||||
float dist = ctx.ViewerDistanceTo(building);
|
float dist = ctx.ViewerDistanceTo(building);
|
||||||
Vector3 eyeInBuilding = ctx.ViewpointInBuilding(building);
|
Vector3 eyeInBuilding = ctx.ViewpointInBuilding(building);
|
||||||
dump.AppendLine(
|
dump.AppendLine(
|
||||||
$"building {id:x8}: dist={dist:f1} eff={MathF.Max(0f, dist - 100f):f1} "
|
$"building {id:x8}: dist={dist:f1} eff={MathF.Max(0f, dist - 50f):f1} "
|
||||||
+ $"eyeLocal={eyeInBuilding} portals={building.Portals.Length}");
|
+ $"eyeLocal={eyeInBuilding} portals={building.Portals.Length}");
|
||||||
WalkBspNode? bsp = building.SelectDrawingBsp(dist);
|
WalkBspNode? bsp = building.Select(dist, 50f, 0f).DrawingBsp;
|
||||||
var refs = new List<WalkPortalRef>();
|
var refs = new List<WalkPortalRef>();
|
||||||
Collect(bsp, refs);
|
Collect(bsp, refs);
|
||||||
dump.AppendLine($" level BSP portal refs: {refs.Count}");
|
dump.AppendLine($" level BSP portal refs: {refs.Count}");
|
||||||
|
|
@ -161,9 +161,9 @@ public sealed class WalkPortalGateDumpTests
|
||||||
world.Buildings.Keys.Single(b => b.PositionCellId == id);
|
world.Buildings.Keys.Single(b => b.PositionCellId == id);
|
||||||
float dist = ctx.ViewerDistanceTo(building);
|
float dist = ctx.ViewerDistanceTo(building);
|
||||||
Vector3 eyeInBuilding = ctx.ViewpointInBuilding(building);
|
Vector3 eyeInBuilding = ctx.ViewpointInBuilding(building);
|
||||||
WalkBspNode? bsp = building.SelectDrawingBsp(dist);
|
WalkBspNode? bsp = building.Select(dist, 50f, 0f).DrawingBsp;
|
||||||
dump.AppendLine(
|
dump.AppendLine(
|
||||||
$" building {id:x8}: dist={dist:f1} eff={MathF.Max(0f, dist - 100f):f1} "
|
$" building {id:x8}: dist={dist:f1} eff={MathF.Max(0f, dist - 50f):f1} "
|
||||||
+ $"bsp={(bsp is null ? "NULL" : "selected")} ports={CountPorts(bsp)}");
|
+ $"bsp={(bsp is null ? "NULL" : "selected")} ports={CountPorts(bsp)}");
|
||||||
for (int li = 0; li < building.DegradeLevels.Length; li++)
|
for (int li = 0; li < building.DegradeLevels.Length; li++)
|
||||||
{
|
{
|
||||||
|
|
@ -249,7 +249,8 @@ public sealed class WalkPortalGateDumpTests
|
||||||
WalkBuilding building =
|
WalkBuilding building =
|
||||||
world.Buildings.Keys.Single(b => b.PositionCellId == 0xA9B40036u);
|
world.Buildings.Keys.Single(b => b.PositionCellId == 0xA9B40036u);
|
||||||
Vector3 eye = ctx.ViewpointInBuilding(building);
|
Vector3 eye = ctx.ViewpointInBuilding(building);
|
||||||
WalkBspNode? bsp = building.SelectDrawingBsp(ctx.ViewerDistanceTo(building));
|
WalkBspNode? bsp = building.Select(
|
||||||
|
ctx.ViewerDistanceTo(building), 50f, 0f).DrawingBsp;
|
||||||
var dump = new StringBuilder();
|
var dump = new StringBuilder();
|
||||||
dump.AppendLine($"camera cell={pose.CellId:x8} origin={pose.Origin}");
|
dump.AppendLine($"camera cell={pose.CellId:x8} origin={pose.Origin}");
|
||||||
dump.AppendLine($"eyeInBuilding={eye} dist={ctx.ViewerDistanceTo(building):f2}");
|
dump.AppendLine($"eyeInBuilding={eye} dist={ctx.ViewerDistanceTo(building):f2}");
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,32 @@ public sealed class WalkProductionFrameContextTests
|
||||||
Assert.Equal(13f, ctx.ViewerDistanceTo(building));
|
Assert.Equal(13f, ctx.ViewerDistanceTo(building));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SetupPartZeroTransformIsSharedByViewpointDistanceAndWorldPublication()
|
||||||
|
{
|
||||||
|
var registry = new WalkBuildingRegistry();
|
||||||
|
var building = new WalkBuilding
|
||||||
|
{
|
||||||
|
PositionCellId = 1,
|
||||||
|
SortCenter = new Vector3(0f, 1f, 0f),
|
||||||
|
PartZeroTransform = Matrix4x4.CreateScale(2f)
|
||||||
|
* Matrix4x4.CreateTranslation(0f, 3f, 0f),
|
||||||
|
PartZeroScaleZ = 2f,
|
||||||
|
};
|
||||||
|
Matrix4x4 root = Matrix4x4.CreateTranslation(0f, 10f, 0f);
|
||||||
|
Matrix4x4.Invert(root, out Matrix4x4 inverseRoot);
|
||||||
|
var entry = new WalkBuildingFactory.Entry(building, root, inverseRoot);
|
||||||
|
registry.Publish(0xA9B4FFFFu, [entry]);
|
||||||
|
var ctx = new WalkProductionFrameContext(
|
||||||
|
new CellVisibility(), registry, new Vector3(0f, 19f, 0f), Vector3.UnitY,
|
||||||
|
SimpleViewProjection(), 1024f, 768f);
|
||||||
|
|
||||||
|
Assert.Equal(entry.PartZeroWorldTransform,
|
||||||
|
building.PartZeroTransform * entry.WorldTransform);
|
||||||
|
Assert.Equal(new Vector3(0f, 3f, 0f), ctx.ViewpointInBuilding(building));
|
||||||
|
Assert.Equal(2f, ctx.ViewerDistanceTo(building));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void CyPlane_MatchesTheRetailNearPlaneFormula()
|
public void CyPlane_MatchesTheRetailNearPlaneFormula()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -161,7 +161,7 @@ public sealed class WalkProductionWorldConformanceTests
|
||||||
Dictionary<WalkBuilding, WalkWorldDatAdapter.BuildingEntry> buildings) =
|
Dictionary<WalkBuilding, WalkWorldDatAdapter.BuildingEntry> buildings) =
|
||||||
BuildProductionWorld(adapter, frame.Pose!.CellId, frame.Pose.Origin);
|
BuildProductionWorld(adapter, frame.Pose!.CellId, frame.Pose.Origin);
|
||||||
var ctx = new WalkTraceReplayContext(frame.Pose, cells) { Buildings = buildings };
|
var ctx = new WalkTraceReplayContext(frame.Pose, cells) { Buildings = buildings };
|
||||||
var walk = new RetailFrameWalk();
|
var walk = new RetailFrameWalk(100f, 0.99f);
|
||||||
var recorder = new Recorder();
|
var recorder = new Recorder();
|
||||||
|
|
||||||
walk.WalkFrame(frame.Pose.CellId, null, assembler.Landscape, ctx, recorder);
|
walk.WalkFrame(frame.Pose.CellId, null, assembler.Landscape, ctx, recorder);
|
||||||
|
|
@ -190,7 +190,7 @@ public sealed class WalkProductionWorldConformanceTests
|
||||||
// Matches WalkTraceConformanceTests.Doorway_still_first_frame_diff:
|
// Matches WalkTraceConformanceTests.Doorway_still_first_frame_diff:
|
||||||
// this capture ran under cdb load with Render::deg_mul depressed to
|
// this capture ran under cdb load with Render::deg_mul depressed to
|
||||||
// the portless-arm threshold. Same environment pin, same reason.
|
// the portless-arm threshold. Same environment pin, same reason.
|
||||||
var walk = new RetailFrameWalk { DegradeMultiplier = 0f };
|
var walk = new RetailFrameWalk(100f, 0f);
|
||||||
var recorder = new Recorder();
|
var recorder = new Recorder();
|
||||||
|
|
||||||
walk.WalkFrame(frame.Pose.CellId, camera, assembler.Landscape, ctx, recorder);
|
walk.WalkFrame(frame.Pose.CellId, camera, assembler.Landscape, ctx, recorder);
|
||||||
|
|
@ -221,7 +221,7 @@ public sealed class WalkProductionWorldConformanceTests
|
||||||
WalkCell? camera = (frame.Pose.CellId & 0xFFFFu) >= 0x100
|
WalkCell? camera = (frame.Pose.CellId & 0xFFFFu) >= 0x100
|
||||||
? Assert.Contains(frame.Pose.CellId, cells)
|
? Assert.Contains(frame.Pose.CellId, cells)
|
||||||
: null;
|
: null;
|
||||||
var walk = new RetailFrameWalk();
|
var walk = new RetailFrameWalk(100f, 0.99f);
|
||||||
var recorder = new Recorder();
|
var recorder = new Recorder();
|
||||||
|
|
||||||
walk.WalkFrame(frame.Pose.CellId, camera, assembler.Landscape, ctx, recorder);
|
walk.WalkFrame(frame.Pose.CellId, camera, assembler.Landscape, ctx, recorder);
|
||||||
|
|
@ -302,7 +302,7 @@ public sealed class WalkProductionWorldConformanceTests
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
new RetailFrameWalk().WalkFrame(
|
new RetailFrameWalk(100f, 0.99f).WalkFrame(
|
||||||
cameraCellId,
|
cameraCellId,
|
||||||
camera,
|
camera,
|
||||||
assembler.Landscape,
|
assembler.Landscape,
|
||||||
|
|
@ -348,7 +348,7 @@ public sealed class WalkProductionWorldConformanceTests
|
||||||
(WalkLandscapeAssembler assembler, Dictionary<uint, WalkCell> cells,
|
(WalkLandscapeAssembler assembler, Dictionary<uint, WalkCell> cells,
|
||||||
Dictionary<WalkBuilding, WalkWorldDatAdapter.BuildingEntry> buildings) =
|
Dictionary<WalkBuilding, WalkWorldDatAdapter.BuildingEntry> buildings) =
|
||||||
BuildProductionWorld(adapter, anchor.CellId, anchor.Origin);
|
BuildProductionWorld(adapter, anchor.CellId, anchor.Origin);
|
||||||
var walk = new RetailFrameWalk();
|
var walk = new RetailFrameWalk(100f, 0.99f);
|
||||||
|
|
||||||
for (int n = 1; n < frames.Count - 1; n++)
|
for (int n = 1; n < frames.Count - 1; n++)
|
||||||
{
|
{
|
||||||
|
|
@ -402,7 +402,7 @@ public sealed class WalkProductionWorldConformanceTests
|
||||||
Assert.NotNull(frame.Pose);
|
Assert.NotNull(frame.Pose);
|
||||||
WalkCell camera = Assert.Contains(frame.Pose!.CellId, cells);
|
WalkCell camera = Assert.Contains(frame.Pose!.CellId, cells);
|
||||||
var ctx = new WalkTraceReplayContext(frame.Pose, cells);
|
var ctx = new WalkTraceReplayContext(frame.Pose, cells);
|
||||||
var walk = new RetailFrameWalk();
|
var walk = new RetailFrameWalk(100f, 0.99f);
|
||||||
var recorder = new Recorder();
|
var recorder = new Recorder();
|
||||||
|
|
||||||
walk.WalkFrame(frame.Pose.CellId, camera, landscape, ctx, recorder);
|
walk.WalkFrame(frame.Pose.CellId, camera, landscape, ctx, recorder);
|
||||||
|
|
|
||||||
|
|
@ -542,6 +542,260 @@ public sealed class WalkStaticStreamPopulatorTests
|
||||||
Assert.Equal((uint)leavesGfxObj, selectionParts[1].GfxObjId);
|
Assert.Equal((uint)leavesGfxObj, selectionParts[1].GfxObjId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SelectedBuildingShellUsesExactlySelectedGfxAndPrecomputedPartZeroTransform()
|
||||||
|
{
|
||||||
|
using var fx = new DispatcherFixture();
|
||||||
|
const uint baseGfx = 0x0100_0031u;
|
||||||
|
const uint selectedGfx = 0x0100_0032u;
|
||||||
|
InjectRenderData(fx.Manager, baseGfx, MakeFlatMesh(
|
||||||
|
MakeBatch(0x08000031u, TranslucencyKind.Opaque, 0, 0, 3, 1)));
|
||||||
|
InjectRenderData(fx.Manager, selectedGfx, MakeFlatMesh(
|
||||||
|
MakeBatch(0x08000032u, TranslucencyKind.Opaque, 3, 4, 6, 2)));
|
||||||
|
Matrix4x4 root = Matrix4x4.CreateTranslation(8, 9, 10);
|
||||||
|
Matrix4x4 partZero = Matrix4x4.CreateScale(2f)
|
||||||
|
* Matrix4x4.CreateTranslation(1, 2, 3);
|
||||||
|
var surfaceOverrides = new Dictionary<uint, uint>
|
||||||
|
{
|
||||||
|
[0x08000032u] = 0x05000032u,
|
||||||
|
};
|
||||||
|
RenderProjectionRecord record = MakeRecord(
|
||||||
|
231, 0x7000_0231u, new Vector3(8, 9, 10),
|
||||||
|
[new MeshRef(baseGfx, Matrix4x4.Identity)
|
||||||
|
{
|
||||||
|
SurfaceOverrides = surfaceOverrides,
|
||||||
|
}],
|
||||||
|
isBuildingShell: true);
|
||||||
|
var batches = new List<WbDrawDispatcher.WalkClassifiedBatch>();
|
||||||
|
var selections = new List<WbDrawDispatcher.WalkClassifiedSelectionPart>();
|
||||||
|
var selected = new WalkBuildingSelection(selectedGfx, null, 2, 1u);
|
||||||
|
|
||||||
|
fx.Dispatcher.ClassifyEntityForWalk(
|
||||||
|
in record, 0x8C04u, batches, selections,
|
||||||
|
buildingSelection: selected,
|
||||||
|
buildingPartTransform: partZero);
|
||||||
|
|
||||||
|
WbDrawDispatcher.WalkClassifiedBatch batch = Assert.Single(batches);
|
||||||
|
Assert.Equal(3u, batch.Key.FirstIndex);
|
||||||
|
Assert.True(batch.Key.TextureSlot.IsAssigned);
|
||||||
|
Assert.NotEqual(new GpuTextureSlot(2), batch.Key.TextureSlot);
|
||||||
|
Assert.Equal(partZero * root, batch.Transform);
|
||||||
|
var selection = Assert.Single(selections);
|
||||||
|
Assert.Equal(selectedGfx, selection.GfxObjId);
|
||||||
|
Assert.Equal(partZero * root, selection.LocalToWorld);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SelectedBuildingShellMissDoesNotFallBackToResidentBaseGfx()
|
||||||
|
{
|
||||||
|
using var fx = new DispatcherFixture();
|
||||||
|
const uint baseGfx = 0x0100_0041u;
|
||||||
|
const uint missingSelectedGfx = 0x0100_0042u;
|
||||||
|
InjectRenderData(fx.Manager, baseGfx, MakeFlatMesh(
|
||||||
|
MakeBatch(0x08000041u, TranslucencyKind.Opaque, 0, 0, 3, 1)));
|
||||||
|
RenderProjectionRecord record = MakeRecord(
|
||||||
|
241, 0, Vector3.Zero,
|
||||||
|
[new MeshRef(baseGfx, Matrix4x4.Identity)], isBuildingShell: true);
|
||||||
|
var batches = new List<WbDrawDispatcher.WalkClassifiedBatch>();
|
||||||
|
var selections = new List<WbDrawDispatcher.WalkClassifiedSelectionPart>();
|
||||||
|
var selected = new WalkBuildingSelection(missingSelectedGfx, null, 1, 1u);
|
||||||
|
|
||||||
|
fx.Dispatcher.ClassifyEntityForWalk(
|
||||||
|
in record, 0x8C04u, batches, selections,
|
||||||
|
buildingSelection: selected,
|
||||||
|
buildingPartTransform: Matrix4x4.Identity);
|
||||||
|
|
||||||
|
Assert.Empty(batches);
|
||||||
|
Assert.Empty(selections);
|
||||||
|
|
||||||
|
InjectRenderData(fx.Manager, missingSelectedGfx, MakeFlatMesh(
|
||||||
|
MakeBatch(0x08000042u, TranslucencyKind.Opaque, 3, 4, 6, 2)));
|
||||||
|
fx.Dispatcher.ClassifyEntityForWalk(
|
||||||
|
in record, 0x8C04u, batches, selections,
|
||||||
|
buildingSelection: selected,
|
||||||
|
buildingPartTransform: Matrix4x4.Identity);
|
||||||
|
Assert.Single(batches);
|
||||||
|
Assert.Equal(missingSelectedGfx, Assert.Single(selections).GfxObjId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WalkFrameBoundary_DeduplicatesMissWithinFrameAndRearmsNextAcceptedFrame()
|
||||||
|
{
|
||||||
|
const uint missingGfx = 0x01000049u;
|
||||||
|
var source = new RecordingPreparedAssetSource();
|
||||||
|
using var fx = new DispatcherFixture(preparedAssets: source);
|
||||||
|
RenderProjectionRecord record = MakeRecord(
|
||||||
|
249, 0, Vector3.Zero,
|
||||||
|
[new MeshRef(0x01000048u, Matrix4x4.Identity)],
|
||||||
|
isBuildingShell: true);
|
||||||
|
var selected = new WalkBuildingSelection(missingGfx, null, 1, 1u);
|
||||||
|
var batches = new List<WbDrawDispatcher.WalkClassifiedBatch>();
|
||||||
|
var selections = new List<WbDrawDispatcher.WalkClassifiedSelectionPart>();
|
||||||
|
FieldInfo requestedField = typeof(WbDrawDispatcher).GetField(
|
||||||
|
"_missRequested", BindingFlags.Instance | BindingFlags.NonPublic)!;
|
||||||
|
var requested = (HashSet<ulong>)requestedField.GetValue(fx.Dispatcher)!;
|
||||||
|
|
||||||
|
fx.Dispatcher.BeginWalkPartFrame();
|
||||||
|
fx.Dispatcher.ClassifyEntityForWalk(
|
||||||
|
in record, 0x8C04u, batches, selections,
|
||||||
|
buildingSelection: selected,
|
||||||
|
buildingPartTransform: Matrix4x4.Identity);
|
||||||
|
fx.Dispatcher.ClassifyEntityForWalk(
|
||||||
|
in record, 0x8C04u, batches, selections,
|
||||||
|
buildingSelection: selected,
|
||||||
|
buildingPartTransform: Matrix4x4.Identity);
|
||||||
|
Assert.Equal([missingGfx], requested);
|
||||||
|
Assert.Throws<InvalidOperationException>(() => fx.Dispatcher.BeginWalkPartFrame());
|
||||||
|
Assert.Equal([missingGfx], requested);
|
||||||
|
Assert.True(SpinWait.SpinUntil(() => source.ReadCount >= 1, TimeSpan.FromSeconds(5)));
|
||||||
|
Assert.Equal(1, source.ReadCount);
|
||||||
|
|
||||||
|
fx.Manager.CancelStagedUploads([missingGfx]);
|
||||||
|
fx.Dispatcher.EndWalkPartFrame();
|
||||||
|
fx.Dispatcher.BeginWalkPartFrame();
|
||||||
|
Assert.Empty(requested);
|
||||||
|
fx.Dispatcher.ClassifyEntityForWalk(
|
||||||
|
in record, 0x8C04u, batches, selections,
|
||||||
|
buildingSelection: selected,
|
||||||
|
buildingPartTransform: Matrix4x4.Identity);
|
||||||
|
Assert.True(SpinWait.SpinUntil(() => source.ReadCount >= 2, TimeSpan.FromSeconds(5)));
|
||||||
|
Assert.Equal([missingGfx], requested);
|
||||||
|
fx.Dispatcher.EndWalkPartFrame();
|
||||||
|
|
||||||
|
for (int frame = 0; frame < 32; frame++)
|
||||||
|
{
|
||||||
|
fx.Manager.CancelStagedUploads([missingGfx]);
|
||||||
|
fx.Dispatcher.BeginWalkPartFrame();
|
||||||
|
Assert.Empty(requested);
|
||||||
|
fx.Dispatcher.ClassifyEntityForWalk(
|
||||||
|
in record, 0x8C04u, batches, selections,
|
||||||
|
buildingSelection: selected,
|
||||||
|
buildingPartTransform: Matrix4x4.Identity);
|
||||||
|
Assert.Single(requested);
|
||||||
|
fx.Dispatcher.EndWalkPartFrame();
|
||||||
|
}
|
||||||
|
Assert.Single(requested);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OwnedWalkLadderDependency_PreparesPublishesAndClassifiesWithoutInjection()
|
||||||
|
{
|
||||||
|
const uint selectedGfx = 0x0100004Au;
|
||||||
|
var prepared = PreparedTriangle(selectedGfx, 0x0800004Au);
|
||||||
|
var source = new RecordingPreparedAssetSource(prepared);
|
||||||
|
using var fx = new DispatcherFixture(preparedAssets: source);
|
||||||
|
var ownership = new LandblockSpawnAdapter(fx.Adapter);
|
||||||
|
var building = new WalkBuilding
|
||||||
|
{
|
||||||
|
PositionCellId = 0x8C040001u,
|
||||||
|
DegradeLevels =
|
||||||
|
[
|
||||||
|
new WalkBuildingDegradeLevel(selectedGfx, 1u, 0f, 10f, 20f, null),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
var envCells = new EnvCellLandblockBuild(
|
||||||
|
0x8C04FFFFu,
|
||||||
|
Array.Empty<LoadedCell>(),
|
||||||
|
Array.Empty<EnvCellShellPlacement>(),
|
||||||
|
[new WalkBuildingFactory.Entry(building, Matrix4x4.Identity, Matrix4x4.Identity)]);
|
||||||
|
var landblock = new LoadedLandblock(
|
||||||
|
0x8C04FFFFu, new LandBlock(), Array.Empty<WorldEntity>());
|
||||||
|
|
||||||
|
ownership.OnLandblockLoaded(
|
||||||
|
landblock,
|
||||||
|
additionalOrdinaryIds: envCells.WalkBuildingMeshDependencies);
|
||||||
|
|
||||||
|
Assert.True(fx.Manager.IsOwned(selectedGfx));
|
||||||
|
Assert.True(SpinWait.SpinUntil(() =>
|
||||||
|
{
|
||||||
|
fx.Adapter.Tick();
|
||||||
|
return fx.Adapter.TryGetRenderData(selectedGfx) is not null;
|
||||||
|
}, TimeSpan.FromSeconds(5)));
|
||||||
|
Assert.Equal(1, source.ReadCount);
|
||||||
|
|
||||||
|
RenderProjectionRecord record = MakeRecord(
|
||||||
|
250, 0, Vector3.Zero,
|
||||||
|
[new MeshRef(0x0100004Bu, Matrix4x4.Identity)],
|
||||||
|
isBuildingShell: true);
|
||||||
|
var batches = new List<WbDrawDispatcher.WalkClassifiedBatch>();
|
||||||
|
var selections = new List<WbDrawDispatcher.WalkClassifiedSelectionPart>();
|
||||||
|
fx.Dispatcher.BeginWalkPartFrame();
|
||||||
|
fx.Dispatcher.ClassifyEntityForWalk(
|
||||||
|
in record, 0x8C04u, batches, selections,
|
||||||
|
buildingSelection: building.Select(0f, 1f, 1f),
|
||||||
|
buildingPartTransform: Matrix4x4.Identity);
|
||||||
|
fx.Dispatcher.EndWalkPartFrame();
|
||||||
|
|
||||||
|
Assert.Single(batches);
|
||||||
|
Assert.Equal(selectedGfx, Assert.Single(selections).GfxObjId);
|
||||||
|
ownership.OnLandblockUnloaded(landblock.LandblockId);
|
||||||
|
Assert.False(fx.Manager.IsOwned(selectedGfx));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WarmedBuildingSelectionAndClassificationAllocateZeroAndDoNotMutateRetainedRecord()
|
||||||
|
{
|
||||||
|
using var fx = new DispatcherFixture();
|
||||||
|
const uint baseGfx = 0x0100_0051u;
|
||||||
|
const uint selectedGfx = 0x0100_0052u;
|
||||||
|
InjectRenderData(fx.Manager, selectedGfx, MakeFlatMesh(
|
||||||
|
MakeBatch(0x08000052u, TranslucencyKind.Opaque, 3, 4, 6, 2)));
|
||||||
|
RenderProjectionRecord record = MakeRecord(
|
||||||
|
251, 0x7000_0251u, new Vector3(8, 9, 10),
|
||||||
|
[new MeshRef(baseGfx, Matrix4x4.Identity)],
|
||||||
|
isBuildingShell: true,
|
||||||
|
parentCellId: 0x8C040112u,
|
||||||
|
casterIdentity: RenderCasterIdentityKind.Building) with
|
||||||
|
{
|
||||||
|
Source = new RenderSourceMetadata(
|
||||||
|
LocalEntityId: 251,
|
||||||
|
ServerGuid: 0x7000_0251u,
|
||||||
|
SourceId: 0x02000051u,
|
||||||
|
ParentCellId: 0x8C040112u,
|
||||||
|
EffectCellId: 0x8C040113u,
|
||||||
|
BuildingShellAnchorCellId: 0x8C040001u,
|
||||||
|
TransformFingerprint: new RenderSceneHash128(11, 21),
|
||||||
|
GeometryFingerprint: new RenderSceneHash128(12, 22),
|
||||||
|
AppearanceFingerprint: new RenderSceneHash128(13, 23)),
|
||||||
|
};
|
||||||
|
RenderProjectionRecord original = record;
|
||||||
|
var building = new WalkBuilding
|
||||||
|
{
|
||||||
|
DegradeLevels =
|
||||||
|
[
|
||||||
|
new(selectedGfx, 4u, 0f, 10f, 20f, null),
|
||||||
|
new(0u, 5u, 20f, 30f, 40f, null),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
var batches = new List<WbDrawDispatcher.WalkClassifiedBatch>(1);
|
||||||
|
var selections = new List<WbDrawDispatcher.WalkClassifiedSelectionPart>(1);
|
||||||
|
|
||||||
|
void Classify()
|
||||||
|
{
|
||||||
|
batches.Clear();
|
||||||
|
selections.Clear();
|
||||||
|
WalkBuildingSelection selected = building.Select(0f, 50f, 0f);
|
||||||
|
fx.Dispatcher.ClassifyEntityForWalk(
|
||||||
|
in record, 0x8C04u, batches, selections,
|
||||||
|
buildingSelection: selected,
|
||||||
|
buildingPartTransform: Matrix4x4.Identity);
|
||||||
|
}
|
||||||
|
|
||||||
|
Classify();
|
||||||
|
long allocated = ZeroAllocationProbe.MeasureWarmed(
|
||||||
|
Classify,
|
||||||
|
batchSize: 10_000,
|
||||||
|
warmupBatches: 2,
|
||||||
|
samples: 5);
|
||||||
|
|
||||||
|
Assert.Equal(0, allocated);
|
||||||
|
Assert.Equal(original, record);
|
||||||
|
Assert.Equal(selectedGfx, Assert.Single(selections).GfxObjId);
|
||||||
|
WbDrawDispatcher.WalkClassifiedBatch batch = Assert.Single(batches);
|
||||||
|
Assert.Equal(1u, batch.DetailCategory);
|
||||||
|
Assert.Equal(Matrix4x4.CreateTranslation(8, 9, 10), batch.Transform);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ClassifyEntityForWalk_FrameScopeStampsEachAdmittedSetupPartOncePerRetailPass()
|
public void ClassifyEntityForWalk_FrameScopeStampsEachAdmittedSetupPartOncePerRetailPass()
|
||||||
{
|
{
|
||||||
|
|
@ -1005,6 +1259,31 @@ public sealed class WalkStaticStreamPopulatorTests
|
||||||
return group;
|
return group;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static ObjectMeshData PreparedTriangle(uint objectId, uint surfaceId) => new()
|
||||||
|
{
|
||||||
|
ObjectId = objectId,
|
||||||
|
Vertices =
|
||||||
|
[
|
||||||
|
new VertexPositionNormalTexture { Position = Vector3.Zero },
|
||||||
|
new VertexPositionNormalTexture { Position = Vector3.UnitX },
|
||||||
|
new VertexPositionNormalTexture { Position = Vector3.UnitY },
|
||||||
|
],
|
||||||
|
TextureBatches =
|
||||||
|
{
|
||||||
|
[(2, 2, Chorizite.Core.Render.Enums.TextureFormat.RGBA8)] =
|
||||||
|
[
|
||||||
|
new TextureBatchData
|
||||||
|
{
|
||||||
|
Key = new TextureKey { SurfaceId = surfaceId },
|
||||||
|
TextureData = Enumerable.Repeat((byte)0xFF, 16).ToArray(),
|
||||||
|
Indices = [0, 1, 2],
|
||||||
|
Translucency = TranslucencyKind.Opaque,
|
||||||
|
CullMode = CullMode.Clockwise,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void SubmitWalkAlphaInstance_RejectsAMismatchedViewProjectionInTheSameScope()
|
public void SubmitWalkAlphaInstance_RejectsAMismatchedViewProjectionInTheSameScope()
|
||||||
{
|
{
|
||||||
|
|
@ -1098,7 +1377,8 @@ public sealed class WalkStaticStreamPopulatorTests
|
||||||
bool withAlphaQueue = false,
|
bool withAlphaQueue = false,
|
||||||
IRetailSelectionRenderSink? selectionSink = null,
|
IRetailSelectionRenderSink? selectionSink = null,
|
||||||
bool detailAvailable = false,
|
bool detailAvailable = false,
|
||||||
bool detailEnabled = false)
|
bool detailEnabled = false,
|
||||||
|
IPreparedAssetSource? preparedAssets = null)
|
||||||
{
|
{
|
||||||
Device = new RecordingGpuDevice();
|
Device = new RecordingGpuDevice();
|
||||||
FrameLifetime = new GpuDeviceFrameLifetime(Device);
|
FrameLifetime = new GpuDeviceFrameLifetime(Device);
|
||||||
|
|
@ -1107,7 +1387,7 @@ public sealed class WalkStaticStreamPopulatorTests
|
||||||
_meshAdapter = new WbMeshAdapter(
|
_meshAdapter = new WbMeshAdapter(
|
||||||
Device,
|
Device,
|
||||||
new NoopDatReaderWriter(),
|
new NoopDatReaderWriter(),
|
||||||
new NullPreparedAssetSource(),
|
preparedAssets ?? new NullPreparedAssetSource(),
|
||||||
NullLogger<WbMeshAdapter>.Instance,
|
NullLogger<WbMeshAdapter>.Instance,
|
||||||
Device.Retirement);
|
Device.Retirement);
|
||||||
var entitySpawnAdapter = new EntitySpawnAdapter(
|
var entitySpawnAdapter = new EntitySpawnAdapter(
|
||||||
|
|
@ -1150,6 +1430,8 @@ public sealed class WalkStaticStreamPopulatorTests
|
||||||
|
|
||||||
public ObjectMeshManager Manager => _meshAdapter.MeshManager!;
|
public ObjectMeshManager Manager => _meshAdapter.MeshManager!;
|
||||||
|
|
||||||
|
public WbMeshAdapter Adapter => _meshAdapter;
|
||||||
|
|
||||||
public DrawScope BeginDraw(bool beginAlpha = false)
|
public DrawScope BeginDraw(bool beginAlpha = false)
|
||||||
{
|
{
|
||||||
if (beginAlpha)
|
if (beginAlpha)
|
||||||
|
|
@ -1201,6 +1483,31 @@ public sealed class WalkStaticStreamPopulatorTests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class RecordingPreparedAssetSource(ObjectMeshData? data = null)
|
||||||
|
: IPreparedAssetSource
|
||||||
|
{
|
||||||
|
private int _readCount;
|
||||||
|
|
||||||
|
public int ReadCount => Volatile.Read(ref _readCount);
|
||||||
|
public PreparedAssetSourceStats Stats => default;
|
||||||
|
public CacheStats DecodedTextureCacheStats => default;
|
||||||
|
public PreparedAssetPresence Probe(
|
||||||
|
AcDream.Content.Pak.PakAssetType type,
|
||||||
|
uint sourceFileId) => PreparedAssetPresence.Available;
|
||||||
|
public PreparedAssetReadResult Read(
|
||||||
|
in PreparedAssetRequest request,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref _readCount);
|
||||||
|
return data is not null && data.ObjectId == request.RuntimeObjectId
|
||||||
|
? PreparedAssetReadResult.Loaded(data)
|
||||||
|
: PreparedAssetReadResult.Missing;
|
||||||
|
}
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private sealed class NoopDatReaderWriter : IDatReaderWriter
|
private sealed class NoopDatReaderWriter : IDatReaderWriter
|
||||||
{
|
{
|
||||||
private readonly StubDatabase _portal = new();
|
private readonly StubDatabase _portal = new();
|
||||||
|
|
|
||||||
|
|
@ -130,8 +130,10 @@ public sealed partial class WalkTraceConformanceTests
|
||||||
public void OnBuildingTurn(WalkBuilding building) =>
|
public void OnBuildingTurn(WalkBuilding building) =>
|
||||||
_inner.OnBuildingTurn(building);
|
_inner.OnBuildingTurn(building);
|
||||||
|
|
||||||
public void OnBuildingShellTurn(WalkBuilding building) =>
|
public void OnBuildingShellTurn(
|
||||||
_inner.OnBuildingShellTurn(building);
|
WalkBuilding building,
|
||||||
|
WalkBuildingSelection selection) =>
|
||||||
|
_inner.OnBuildingShellTurn(building, selection);
|
||||||
|
|
||||||
public void OnPunchGeometry(
|
public void OnPunchGeometry(
|
||||||
WalkBuilding building, WalkPolygon polygon, int activeViewIndex)
|
WalkBuilding building, WalkPolygon polygon, int activeViewIndex)
|
||||||
|
|
@ -334,7 +336,7 @@ public sealed partial class WalkTraceConformanceTests
|
||||||
var driver = new WalkFrameDriver(fx.Dispatcher, leaf, worldData);
|
var driver = new WalkFrameDriver(fx.Dispatcher, leaf, worldData);
|
||||||
leaf.Driver = driver;
|
leaf.Driver = driver;
|
||||||
var sink = new AlphaDepthCollectSink(driver);
|
var sink = new AlphaDepthCollectSink(driver);
|
||||||
var walk = new RetailFrameWalk();
|
var walk = new RetailFrameWalk(100f, 0.99f);
|
||||||
|
|
||||||
// R2-2: seed the persistent counter from the fixture's own
|
// R2-2: seed the persistent counter from the fixture's own
|
||||||
// pre-capture value instead of priming a throwaway first pass.
|
// pre-capture value instead of priming a throwaway first pass.
|
||||||
|
|
@ -416,7 +418,7 @@ public sealed partial class WalkTraceConformanceTests
|
||||||
var driver = new WalkFrameDriver(fx.Dispatcher, leaf, worldData);
|
var driver = new WalkFrameDriver(fx.Dispatcher, leaf, worldData);
|
||||||
leaf.Driver = driver;
|
leaf.Driver = driver;
|
||||||
var sink = new AlphaDepthCollectSink(driver);
|
var sink = new AlphaDepthCollectSink(driver);
|
||||||
var walk = new RetailFrameWalk();
|
var walk = new RetailFrameWalk(100f, 0.99f);
|
||||||
driver.PortalsDrawnCount = initialCounter;
|
driver.PortalsDrawnCount = initialCounter;
|
||||||
|
|
||||||
queue.BeginFrame();
|
queue.BeginFrame();
|
||||||
|
|
|
||||||
|
|
@ -139,7 +139,7 @@ public sealed partial class WalkTraceConformanceTests
|
||||||
{
|
{
|
||||||
Buildings = world.Buildings,
|
Buildings = world.Buildings,
|
||||||
};
|
};
|
||||||
var walk = new RetailFrameWalk();
|
var walk = new RetailFrameWalk(100f, 0.99f);
|
||||||
var recorder = new Recorder();
|
var recorder = new Recorder();
|
||||||
|
|
||||||
walk.WalkFrame(frame.Pose.CellId, null, world.Landscape, ctx, recorder);
|
walk.WalkFrame(frame.Pose.CellId, null, world.Landscape, ctx, recorder);
|
||||||
|
|
@ -178,7 +178,7 @@ public sealed partial class WalkTraceConformanceTests
|
||||||
// look-in floods. Every other fixture pins ≈ +0.99 (thresholds at
|
// look-in floods. Every other fixture pins ≈ +0.99 (thresholds at
|
||||||
// max). The recon session's live dump read −0.99 under the same cdb
|
// max). The recon session's live dump read −0.99 under the same cdb
|
||||||
// load. Re-dump deg_mul per capture at the next retail session.
|
// load. Re-dump deg_mul per capture at the next retail session.
|
||||||
var walk = new RetailFrameWalk { DegradeMultiplier = 0f };
|
var walk = new RetailFrameWalk(100f, 0f);
|
||||||
var recorder = new Recorder();
|
var recorder = new Recorder();
|
||||||
|
|
||||||
walk.WalkFrame(frame.Pose.CellId, camera, world.Landscape, ctx, recorder);
|
walk.WalkFrame(frame.Pose.CellId, camera, world.Landscape, ctx, recorder);
|
||||||
|
|
@ -257,7 +257,7 @@ public sealed partial class WalkTraceConformanceTests
|
||||||
Buildings = world.Buildings,
|
Buildings = world.Buildings,
|
||||||
};
|
};
|
||||||
WalkCell camera = Assert.Contains(frame.Pose.CellId, world.Cells);
|
WalkCell camera = Assert.Contains(frame.Pose.CellId, world.Cells);
|
||||||
var walk = new RetailFrameWalk();
|
var walk = new RetailFrameWalk(100f, 0.99f);
|
||||||
var recorder = new Recorder();
|
var recorder = new Recorder();
|
||||||
|
|
||||||
walk.WalkFrame(frame.Pose.CellId, camera, world.Landscape, ctx, recorder);
|
walk.WalkFrame(frame.Pose.CellId, camera, world.Landscape, ctx, recorder);
|
||||||
|
|
@ -304,7 +304,7 @@ public sealed partial class WalkTraceConformanceTests
|
||||||
WalkCell? camera = (frame.Pose.CellId & 0xFFFFu) >= 0x100
|
WalkCell? camera = (frame.Pose.CellId & 0xFFFFu) >= 0x100
|
||||||
? Assert.Contains(frame.Pose.CellId, world.Cells)
|
? Assert.Contains(frame.Pose.CellId, world.Cells)
|
||||||
: null;
|
: null;
|
||||||
var walk = new RetailFrameWalk();
|
var walk = new RetailFrameWalk(100f, 0.99f);
|
||||||
var recorder = new Recorder();
|
var recorder = new Recorder();
|
||||||
|
|
||||||
walk.WalkFrame(frame.Pose.CellId, camera, world.Landscape, ctx, recorder);
|
walk.WalkFrame(frame.Pose.CellId, camera, world.Landscape, ctx, recorder);
|
||||||
|
|
@ -363,7 +363,7 @@ public sealed partial class WalkTraceConformanceTests
|
||||||
WalkOraclePose anchor = frames[1].Pose!;
|
WalkOraclePose anchor = frames[1].Pose!;
|
||||||
WalkLandscapeDatBuilder.BuiltWorld world =
|
WalkLandscapeDatBuilder.BuiltWorld world =
|
||||||
WalkLandscapeDatBuilder.Build(dats, anchor.CellId, anchor.Origin);
|
WalkLandscapeDatBuilder.Build(dats, anchor.CellId, anchor.Origin);
|
||||||
var walk = new RetailFrameWalk();
|
var walk = new RetailFrameWalk(100f, 0.99f);
|
||||||
|
|
||||||
for (int n = 1; n < frames.Count - 1; n++)
|
for (int n = 1; n < frames.Count - 1; n++)
|
||||||
{
|
{
|
||||||
|
|
@ -423,7 +423,7 @@ public sealed partial class WalkTraceConformanceTests
|
||||||
Assert.NotNull(frame.Pose);
|
Assert.NotNull(frame.Pose);
|
||||||
WalkCell camera = Assert.Contains(frame.Pose!.CellId, cells);
|
WalkCell camera = Assert.Contains(frame.Pose!.CellId, cells);
|
||||||
var ctx = new WalkTraceReplayContext(frame.Pose, cells);
|
var ctx = new WalkTraceReplayContext(frame.Pose, cells);
|
||||||
var walk = new RetailFrameWalk();
|
var walk = new RetailFrameWalk(100f, 0.99f);
|
||||||
var recorder = new Recorder();
|
var recorder = new Recorder();
|
||||||
|
|
||||||
walk.WalkFrame(frame.Pose.CellId, camera, landscape, ctx, recorder);
|
walk.WalkFrame(frame.Pose.CellId, camera, landscape, ctx, recorder);
|
||||||
|
|
@ -467,7 +467,7 @@ public sealed partial class WalkTraceConformanceTests
|
||||||
{
|
{
|
||||||
Buildings = world.Buildings,
|
Buildings = world.Buildings,
|
||||||
};
|
};
|
||||||
var walk = new RetailFrameWalk();
|
var walk = new RetailFrameWalk(100f, 0.99f);
|
||||||
var recorder = new Recorder();
|
var recorder = new Recorder();
|
||||||
|
|
||||||
walk.WalkFrame(frame.Pose.CellId, camera, world.Landscape, ctx, recorder);
|
walk.WalkFrame(frame.Pose.CellId, camera, world.Landscape, ctx, recorder);
|
||||||
|
|
|
||||||
|
|
@ -169,7 +169,9 @@ public static class WalkWorldDatAdapter
|
||||||
{
|
{
|
||||||
levelBsp = ConvertDrawingBsp(levelGfx, levelGfx.DrawingBSP?.Root);
|
levelBsp = ConvertDrawingBsp(levelGfx, levelGfx.DrawingBSP?.Root);
|
||||||
}
|
}
|
||||||
degradeLevels.Add(new WalkBuildingDegradeLevel(level.MinDist, level.IdealDist, level.MaxDist, levelBsp));
|
degradeLevels.Add(new WalkBuildingDegradeLevel(
|
||||||
|
(uint)level.Id, level.DegradeMode,
|
||||||
|
level.MinDist, level.IdealDist, level.MaxDist, levelBsp));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -183,6 +185,7 @@ public static class WalkWorldDatAdapter
|
||||||
{
|
{
|
||||||
PositionCellId = positionCellId,
|
PositionCellId = positionCellId,
|
||||||
Portals = portals,
|
Portals = portals,
|
||||||
|
GfxObjId = buildingInfo.ModelId,
|
||||||
DrawingBsp = bsp,
|
DrawingBsp = bsp,
|
||||||
DegradeLevels = degradeLevels.ToArray(),
|
DegradeLevels = degradeLevels.ToArray(),
|
||||||
SortCenter = sortCenter,
|
SortCenter = sortCenter,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ using System.Collections.Immutable;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AcDream.App.Rendering;
|
using AcDream.App.Rendering;
|
||||||
using AcDream.App.Rendering.Wb;
|
using AcDream.App.Rendering.Wb;
|
||||||
|
using AcDream.App.Rendering.Walk;
|
||||||
|
|
||||||
namespace AcDream.App.Tests.Rendering.Wb;
|
namespace AcDream.App.Tests.Rendering.Wb;
|
||||||
|
|
||||||
|
|
@ -85,6 +86,41 @@ public class EnvCellLandblockBuildTests
|
||||||
Assert.Throws<InvalidOperationException>(() => builder.Build());
|
Assert.Throws<InvalidOperationException>(() => builder.Build());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Constructor_PrecomputesUniqueNonzeroWalkBuildingMeshDependencies()
|
||||||
|
{
|
||||||
|
WalkBuildingFactory.Entry[] buildings =
|
||||||
|
[
|
||||||
|
Building(0x01000010u, 0u, 0x01000020u, 0x01000010u),
|
||||||
|
Building(0x01000020u, 0x01000030u),
|
||||||
|
];
|
||||||
|
|
||||||
|
var build = new EnvCellLandblockBuild(
|
||||||
|
LandblockId,
|
||||||
|
Array.Empty<LoadedCell>(),
|
||||||
|
Array.Empty<EnvCellShellPlacement>(),
|
||||||
|
buildings);
|
||||||
|
|
||||||
|
Assert.Equal<ulong>(
|
||||||
|
[0x01000010ul, 0x01000020ul, 0x01000030ul],
|
||||||
|
build.WalkBuildingMeshDependencies);
|
||||||
|
buildings[0].Building.DegradeLevels = [];
|
||||||
|
Assert.Equal<ulong>(
|
||||||
|
[0x01000010ul, 0x01000020ul, 0x01000030ul],
|
||||||
|
build.WalkBuildingMeshDependencies);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static WalkBuildingFactory.Entry Building(params uint[] ids) =>
|
||||||
|
new(
|
||||||
|
new WalkBuilding
|
||||||
|
{
|
||||||
|
PositionCellId = 0x8C040001u,
|
||||||
|
DegradeLevels = ids.Select(id =>
|
||||||
|
new WalkBuildingDegradeLevel(id, 1u, 0f, 10f, 20f, null)).ToArray(),
|
||||||
|
},
|
||||||
|
Matrix4x4.Identity,
|
||||||
|
Matrix4x4.Identity);
|
||||||
|
|
||||||
private static EnvCellShellPlacement Shell(uint cellId, ulong geometryId)
|
private static EnvCellShellPlacement Shell(uint cellId, ulong geometryId)
|
||||||
{
|
{
|
||||||
var min = new Vector3(cellId & 0xFF, 0, 0);
|
var min = new Vector3(cellId & 0xFF, 0, 0);
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,85 @@ public sealed class LandblockSpawnAdapterReadinessTests
|
||||||
Assert.DoesNotContain(sharedGeometryId, meshes.ReferenceCounts);
|
Assert.DoesNotContain(sharedGeometryId, meshes.ReferenceCounts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WalkLadderDependencies_AreOrdinaryOwnedDeduplicatedAndBalancedAcrossRevisit()
|
||||||
|
{
|
||||||
|
const ulong baseAndLadder = 0x01000010ul;
|
||||||
|
const ulong ladderOnly = 0x01000020ul;
|
||||||
|
const ulong prepared = 0x2_0000_1234ul;
|
||||||
|
var meshes = new ReadinessMeshAdapter();
|
||||||
|
var adapter = new LandblockSpawnAdapter(meshes);
|
||||||
|
LoadedLandblock landblock = MakeLandblock(0x1234FFFFu, (uint)baseAndLadder);
|
||||||
|
|
||||||
|
adapter.OnLandblockLoaded(
|
||||||
|
landblock,
|
||||||
|
[prepared],
|
||||||
|
[0ul, baseAndLadder, ladderOnly, ladderOnly]);
|
||||||
|
|
||||||
|
Assert.Equal(1, meshes.ReferenceCounts[baseAndLadder]);
|
||||||
|
Assert.Equal(1, meshes.ReferenceCounts[ladderOnly]);
|
||||||
|
Assert.Equal(1, meshes.ReferenceCounts[prepared]);
|
||||||
|
Assert.Equal([prepared], meshes.PreparedPinCalls);
|
||||||
|
adapter.OnLandblockUnloaded(landblock.LandblockId);
|
||||||
|
Assert.Empty(meshes.ReferenceCounts);
|
||||||
|
|
||||||
|
adapter.OnLandblockLoaded(landblock, [prepared], [baseAndLadder, ladderOnly]);
|
||||||
|
Assert.Equal(1, meshes.ReferenceCounts[baseAndLadder]);
|
||||||
|
Assert.Equal(1, meshes.ReferenceCounts[ladderOnly]);
|
||||||
|
adapter.OnLandblockUnloaded(landblock.LandblockId);
|
||||||
|
Assert.Empty(meshes.ReferenceCounts);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WalkLadderDependency_PartialAcquireFailureCanCancelThenReplaceWithoutLeak()
|
||||||
|
{
|
||||||
|
const ulong first = 0x01000010ul;
|
||||||
|
const ulong failed = 0x01000020ul;
|
||||||
|
const ulong replacement = 0x01000030ul;
|
||||||
|
var meshes = new FaultInjectingMeshAdapter();
|
||||||
|
meshes.FailNext(ReferenceOperation.Increment, failed, committed: false);
|
||||||
|
var adapter = new LandblockSpawnAdapter(meshes);
|
||||||
|
LoadedLandblock landblock = MakeLandblock(0x1234FFFFu);
|
||||||
|
|
||||||
|
Assert.Throws<InvalidOperationException>(() =>
|
||||||
|
adapter.OnLandblockLoaded(landblock, null, [first, failed]));
|
||||||
|
Assert.Equal(1, meshes.ReferenceCounts[first]);
|
||||||
|
|
||||||
|
adapter.OnLandblockUnloaded(landblock.LandblockId);
|
||||||
|
Assert.Empty(meshes.ReferenceCounts);
|
||||||
|
adapter.OnLandblockLoaded(landblock, null, [replacement]);
|
||||||
|
Assert.Equal(1, meshes.ReferenceCounts[replacement]);
|
||||||
|
adapter.OnLandblockLoaded(landblock, null, [replacement]);
|
||||||
|
Assert.Equal(1, meshes.CallCount(ReferenceOperation.Increment, replacement));
|
||||||
|
adapter.OnLandblockUnloaded(landblock.LandblockId);
|
||||||
|
Assert.Empty(meshes.ReferenceCounts);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WalkLadderDependency_ExactReplacementReleasesDisplacedDependencyOnce()
|
||||||
|
{
|
||||||
|
const ulong displaced = 0x01000010ul;
|
||||||
|
const ulong retained = 0x01000020ul;
|
||||||
|
const ulong added = 0x01000030ul;
|
||||||
|
var meshes = new FaultInjectingMeshAdapter();
|
||||||
|
var adapter = new LandblockSpawnAdapter(meshes);
|
||||||
|
LoadedLandblock landblock = MakeLandblock(0x1234FFFFu);
|
||||||
|
adapter.OnLandblockLoaded(landblock, null, [displaced, retained]);
|
||||||
|
|
||||||
|
adapter.OnLandblockLoaded(
|
||||||
|
landblock,
|
||||||
|
additionalOrdinaryIds: [retained, added],
|
||||||
|
replaceExisting: true);
|
||||||
|
|
||||||
|
Assert.DoesNotContain(displaced, meshes.ReferenceCounts);
|
||||||
|
Assert.Equal(1, meshes.ReferenceCounts[retained]);
|
||||||
|
Assert.Equal(1, meshes.ReferenceCounts[added]);
|
||||||
|
Assert.Equal(1, meshes.CallCount(ReferenceOperation.Decrement, displaced));
|
||||||
|
Assert.Equal(1, meshes.CallCount(ReferenceOperation.Increment, retained));
|
||||||
|
adapter.OnLandblockUnloaded(landblock.LandblockId);
|
||||||
|
Assert.Empty(meshes.ReferenceCounts);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Load_BeforeCommitFailure_RetryAcquiresOnlyUnfinishedReferences()
|
public void Load_BeforeCommitFailure_RetryAcquiresOnlyUnfinishedReferences()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -717,6 +717,7 @@ public sealed class WorldSceneRendererTests
|
||||||
var building = new WalkBuilding
|
var building = new WalkBuilding
|
||||||
{
|
{
|
||||||
PositionCellId = landblockId | 1u,
|
PositionCellId = landblockId | 1u,
|
||||||
|
GfxObjId = 0x0100_0001u,
|
||||||
DrawingBsp = new WalkBspNode { InPortals = [] },
|
DrawingBsp = new WalkBspNode { InPortals = [] },
|
||||||
};
|
};
|
||||||
var entry = new WalkBuildingFactory.Entry(
|
var entry = new WalkBuildingFactory.Entry(
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ using AcDream.Core.World;
|
||||||
using DatReaderWriter;
|
using DatReaderWriter;
|
||||||
using DatReaderWriter.DBObjs;
|
using DatReaderWriter.DBObjs;
|
||||||
using DatReaderWriter.Types;
|
using DatReaderWriter.Types;
|
||||||
|
using DatReaderWriter.Enums;
|
||||||
|
using AcDream.App.Rendering.Walk;
|
||||||
|
|
||||||
namespace AcDream.App.Tests.Streaming;
|
namespace AcDream.App.Tests.Streaming;
|
||||||
|
|
||||||
|
|
@ -140,6 +142,52 @@ public sealed class LandblockBuildFactoryTests
|
||||||
Assert.Equal((LandblockId & 0xFFFF0000u) | 1u, buildingEntry.Building.PositionCellId);
|
Assert.Equal((LandblockId & 0xFFFF0000u) | 1u, buildingEntry.Building.PositionCellId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WalkBuildingFactory_SetupModelUsesExactlyRestingPartZeroAndDefaultScale()
|
||||||
|
{
|
||||||
|
const uint setupId = 0x0200_0100u;
|
||||||
|
const uint partZeroId = 0x0100_0101u;
|
||||||
|
var dat = CreateDat(out RecordingDatProxy proxy);
|
||||||
|
var resting = new AnimationFrame(1);
|
||||||
|
resting.Frames.Add(new Frame
|
||||||
|
{
|
||||||
|
Origin = new Vector3(1f, 2f, 3f),
|
||||||
|
Orientation = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, 0.25f),
|
||||||
|
});
|
||||||
|
var setup = new Setup();
|
||||||
|
setup.Parts.Add(partZeroId);
|
||||||
|
setup.DefaultScale.Add(new Vector3(2f, 3f, 4f));
|
||||||
|
setup.PlacementFrames[Placement.Resting] = resting;
|
||||||
|
proxy.Add(setupId, setup);
|
||||||
|
proxy.Add(partZeroId, new GfxObj
|
||||||
|
{
|
||||||
|
Id = partZeroId,
|
||||||
|
SortCenter = new Vector3(4f, 5f, 6f),
|
||||||
|
});
|
||||||
|
var info = new BuildingInfo
|
||||||
|
{
|
||||||
|
ModelId = setupId,
|
||||||
|
Frame = new Frame
|
||||||
|
{
|
||||||
|
Origin = new Vector3(12f, 12f, 0f),
|
||||||
|
Orientation = Quaternion.Identity,
|
||||||
|
},
|
||||||
|
Portals = [],
|
||||||
|
};
|
||||||
|
|
||||||
|
WalkBuildingFactory.Entry entry = Assert.Single(
|
||||||
|
WalkBuildingFactory.Build(dat, LandblockId, [info], Vector3.Zero));
|
||||||
|
|
||||||
|
Assert.Equal(partZeroId, entry.Building.GfxObjId);
|
||||||
|
Assert.Equal(new Vector3(4f, 5f, 6f), entry.Building.SortCenter);
|
||||||
|
Assert.Equal(4f, entry.Building.PartZeroScaleZ);
|
||||||
|
Matrix4x4 expected = Matrix4x4.CreateScale(2f, 3f, 4f)
|
||||||
|
* Matrix4x4.CreateFromQuaternion(resting.Frames[0].Orientation)
|
||||||
|
* Matrix4x4.CreateTranslation(1f, 2f, 3f);
|
||||||
|
Assert.Equal(expected, entry.Building.PartZeroTransform);
|
||||||
|
Assert.Equal(expected * entry.WorldTransform, entry.PartZeroWorldTransform);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void BuildFar_NeverPopulatesWalkDataBecauseEnvCellsIsNull()
|
public void BuildFar_NeverPopulatesWalkDataBecauseEnvCellsIsNull()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,96 @@
|
||||||
using AcDream.App.Streaming;
|
using AcDream.App.Streaming;
|
||||||
using AcDream.App.Rendering;
|
using AcDream.App.Rendering;
|
||||||
using AcDream.App.Rendering.Wb;
|
using AcDream.App.Rendering.Wb;
|
||||||
|
using AcDream.App.Rendering.Walk;
|
||||||
using AcDream.Core.World;
|
using AcDream.Core.World;
|
||||||
using DatReaderWriter.DBObjs;
|
using DatReaderWriter.DBObjs;
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
|
using System.Numerics;
|
||||||
|
|
||||||
namespace AcDream.App.Tests.Streaming;
|
namespace AcDream.App.Tests.Streaming;
|
||||||
|
|
||||||
public sealed class StreamingControllerReadinessTests
|
public sealed class StreamingControllerReadinessTests
|
||||||
{
|
{
|
||||||
|
[Fact]
|
||||||
|
public void NearPublication_OwnsWalkLadderAsOrdinaryReadinessAndReleasesOnRetirement()
|
||||||
|
{
|
||||||
|
const uint landblockId = 0x1236FFFFu;
|
||||||
|
const ulong ladderId = 0x01001234ul;
|
||||||
|
var meshes = new ReadinessMeshAdapter();
|
||||||
|
var state = new GpuWorldState(new LandblockSpawnAdapter(meshes));
|
||||||
|
var outbox = new Queue<LandblockStreamResult>();
|
||||||
|
var building = new WalkBuilding
|
||||||
|
{
|
||||||
|
PositionCellId = 0x12360001u,
|
||||||
|
DegradeLevels =
|
||||||
|
[
|
||||||
|
new WalkBuildingDegradeLevel((uint)ladderId, 1u, 0f, 10f, 20f, null),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
var envCells = new EnvCellLandblockBuild(
|
||||||
|
landblockId,
|
||||||
|
Array.Empty<LoadedCell>(),
|
||||||
|
Array.Empty<EnvCellShellPlacement>(),
|
||||||
|
[new WalkBuildingFactory.Entry(building, Matrix4x4.Identity, Matrix4x4.Identity)]);
|
||||||
|
var build = new LandblockBuild(
|
||||||
|
new LoadedLandblock(landblockId, new LandBlock(), Array.Empty<WorldEntity>()),
|
||||||
|
envCells);
|
||||||
|
outbox.Enqueue(new LandblockStreamResult.Loaded(
|
||||||
|
landblockId,
|
||||||
|
LandblockStreamTier.Near,
|
||||||
|
build,
|
||||||
|
new AcDream.Core.Terrain.LandblockMeshData([], [])));
|
||||||
|
var controller = new StreamingController(
|
||||||
|
enqueueLoad: (_, _) => { },
|
||||||
|
enqueueUnload: _ => { },
|
||||||
|
drainCompletions: maximum =>
|
||||||
|
{
|
||||||
|
var drained = new List<LandblockStreamResult>();
|
||||||
|
while (drained.Count < maximum && outbox.TryDequeue(out var result))
|
||||||
|
drained.Add(result);
|
||||||
|
return drained;
|
||||||
|
},
|
||||||
|
applyTerrain: (_, _) => { },
|
||||||
|
state: state,
|
||||||
|
nearRadius: 0,
|
||||||
|
farRadius: 0);
|
||||||
|
|
||||||
|
for (int frame = 0; frame < 32 && !meshes.ReferenceCounts.ContainsKey(ladderId); frame++)
|
||||||
|
controller.Tick(0x12, 0x36);
|
||||||
|
|
||||||
|
Assert.Equal(1, meshes.ReferenceCounts[ladderId]);
|
||||||
|
Assert.False(controller.IsRenderNeighborhoodResident(landblockId, 0, 0));
|
||||||
|
meshes.ReadyIds.Add(ladderId);
|
||||||
|
Assert.True(controller.IsRenderNeighborhoodResident(landblockId, 0, 0));
|
||||||
|
|
||||||
|
state.RemoveEntitiesFromLandblock(landblockId);
|
||||||
|
Assert.Empty(meshes.ReferenceCounts);
|
||||||
|
GpuLandblockSpatialPublication revisit = state.CommitEntitiesToExistingLandblockSpatial(
|
||||||
|
landblockId,
|
||||||
|
Array.Empty<WorldEntity>(),
|
||||||
|
additionalRenderIds: null,
|
||||||
|
additionalOrdinaryRenderIds: envCells.WalkBuildingMeshDependencies);
|
||||||
|
state.ActivateLandblockPresentation(revisit);
|
||||||
|
Assert.Equal(1, meshes.ReferenceCounts[ladderId]);
|
||||||
|
state.RemoveLandblock(landblockId);
|
||||||
|
Assert.Empty(meshes.ReferenceCounts);
|
||||||
|
|
||||||
|
GpuLandblockSpatialPublication resetPublication =
|
||||||
|
state.CommitLandblockSpatial(
|
||||||
|
build.Landblock,
|
||||||
|
additionalRenderIds: null,
|
||||||
|
tier: LandblockStreamTier.Near,
|
||||||
|
additionalOrdinaryRenderIds:
|
||||||
|
envCells.WalkBuildingMeshDependencies);
|
||||||
|
state.ActivateLandblockPresentation(resetPublication);
|
||||||
|
Assert.Equal(1, meshes.ReferenceCounts[ladderId]);
|
||||||
|
|
||||||
|
GpuWorldRecenterRetirement reset = state.DetachAllForOriginRecenter();
|
||||||
|
Assert.Equal(landblockId, Assert.Single(reset.Landblocks).LandblockId);
|
||||||
|
state.ReleaseLandblockMeshReferences(landblockId);
|
||||||
|
Assert.Empty(meshes.ReferenceCounts);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void RenderNeighborhoodResident_RequiresEveryPublishedLandblockInRing()
|
public void RenderNeighborhoodResident_RequiresEveryPublishedLandblockInRing()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1144,6 +1144,25 @@ public sealed class ConfigOptionsPageControllerTests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AdaptiveDegradeRowsApplyToTheSharedDisplaySettingsSnapshot()
|
||||||
|
{
|
||||||
|
(OptionsPanelController controller, FakeBindings bindings, bool bound) = BindReal();
|
||||||
|
Assert.True(bound);
|
||||||
|
var automatic = Assert.IsType<BoolOptionRow>(controller.ConfigPage.Rows[16]);
|
||||||
|
var bias = Assert.IsType<FloatOptionRow>(controller.ConfigPage.Rows[17]);
|
||||||
|
var distance = Assert.IsType<FloatOptionRow>(controller.ConfigPage.Rows[18]);
|
||||||
|
|
||||||
|
automatic.SetCurrentValue(true);
|
||||||
|
bias.SetCurrentValue(-0.35f);
|
||||||
|
distance.SetCurrentValue(77f);
|
||||||
|
controller.ConfigPage.Apply();
|
||||||
|
|
||||||
|
Assert.True(bindings.Display.AutomaticDegrades);
|
||||||
|
Assert.Equal(-0.35f, bindings.Display.GraphicsPerformance);
|
||||||
|
Assert.Equal(77f, bindings.Display.DegradeDistance);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Bind_ResolvesOnlyTheAuthoredStringKeys_NoInventedOrDroppedKey()
|
public void Bind_ResolvesOnlyTheAuthoredStringKeys_NoInventedOrDroppedKey()
|
||||||
{
|
{
|
||||||
|
|
@ -1412,9 +1431,9 @@ public sealed class ConfigOptionsPageControllerTests
|
||||||
(15, RowKind.Toggle, false, "Full Screen"), // LIVE
|
(15, RowKind.Toggle, false, "Full Screen"), // LIVE
|
||||||
(16, RowKind.Toggle, false, "Sync To Refresh"), // NEXT-LAUNCH
|
(16, RowKind.Toggle, false, "Sync To Refresh"), // NEXT-LAUNCH
|
||||||
(17, RowKind.Slider, true, "Screen Brightness"), // review S2
|
(17, RowKind.Slider, true, "Screen Brightness"), // review S2
|
||||||
(18, RowKind.Toggle, true, "Automatic Degrades"), // AP-198
|
(18, RowKind.Toggle, false, "Automatic Degrades"), // LIVE
|
||||||
(19, RowKind.Slider, true, "Graphics Performance"), // AP-198
|
(19, RowKind.Slider, false, "Graphics Performance"), // LIVE
|
||||||
(20, RowKind.Slider, true, "Degrade Distance"), // AP-198
|
(20, RowKind.Slider, false, "Degrade Distance"), // LIVE
|
||||||
(23, RowKind.Menu, true, "Landscape Texture Detail"), // AP-198
|
(23, RowKind.Menu, true, "Landscape Texture Detail"), // AP-198
|
||||||
(24, RowKind.Menu, true, "Environment Texture Detail"), // AP-198
|
(24, RowKind.Menu, true, "Environment Texture Detail"), // AP-198
|
||||||
(25, RowKind.Menu, true, "Texture Filtering"), // AP-198
|
(25, RowKind.Menu, true, "Texture Filtering"), // AP-198
|
||||||
|
|
@ -1446,6 +1465,7 @@ public sealed class ConfigOptionsPageControllerTests
|
||||||
// at its empty default and hide the very thing this test checks.
|
// at its empty default and hide the very thing this test checks.
|
||||||
(OptionsPanelController controller, _, bool bound) = BindReal(resolveString: (_, _) => "x");
|
(OptionsPanelController controller, _, bool bound) = BindReal(resolveString: (_, _) => "x");
|
||||||
Assert.True(bound);
|
Assert.True(bound);
|
||||||
|
Assert.Equal(16, DimmingExpectations.Count(expectation => expectation.StoreOnly));
|
||||||
|
|
||||||
var configSlot = UiElement.FindDescendant(controller.TabPanel, ConfigPageSlotId)!;
|
var configSlot = UiElement.FindDescendant(controller.TabPanel, ConfigPageSlotId)!;
|
||||||
var listBox = Assert.IsType<UiTemplateListBox>(
|
var listBox = Assert.IsType<UiTemplateListBox>(
|
||||||
|
|
@ -1506,6 +1526,22 @@ public sealed class ConfigOptionsPageControllerTests
|
||||||
Assert.Equal(25, bindings.Display.LandscapeDrawDistance);
|
Assert.Equal(25, bindings.Display.LandscapeDrawDistance);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Ap198TruthNamesFiveResidualsAndLandscapeDrawDistanceAsLive()
|
||||||
|
{
|
||||||
|
string registerPath = Path.Combine(
|
||||||
|
FindRepoRoot(), "docs", "architecture", "retail-divergence-register.md");
|
||||||
|
string row = Assert.Single(File.ReadLines(registerPath), static line =>
|
||||||
|
line.StartsWith("| AP-198 |", StringComparison.Ordinal));
|
||||||
|
|
||||||
|
Assert.Contains("author five rows", row, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("Render_LandscapeDrawDistance` was already live", row, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("ApplyLandscapeDrawDistance", row, StringComparison.Ordinal);
|
||||||
|
Assert.DoesNotContain("author six rows", row, StringComparison.Ordinal);
|
||||||
|
Assert.DoesNotContain("six residual", row, StringComparison.Ordinal);
|
||||||
|
Assert.DoesNotContain("no highlighted selection", row, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
// ── #412-class regression: Config tab content escaping the window frame ──
|
// ── #412-class regression: Config tab content escaping the window frame ──
|
||||||
//
|
//
|
||||||
// 2026-08-16/17 overnight hover/UI round, Batch A bug 2. The user's
|
// 2026-08-16/17 overnight hover/UI round, Batch A bug 2. The user's
|
||||||
|
|
@ -1620,4 +1656,17 @@ public sealed class ConfigOptionsPageControllerTests
|
||||||
ApplyAnchorRecursive(child);
|
ApplyAnchorRecursive(child);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string FindRepoRoot()
|
||||||
|
{
|
||||||
|
var directory = new DirectoryInfo(AppContext.BaseDirectory);
|
||||||
|
while (directory is not null)
|
||||||
|
{
|
||||||
|
if (File.Exists(Path.Combine(directory.FullName, "AcDream.slnx")))
|
||||||
|
return directory.FullName;
|
||||||
|
directory = directory.Parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidOperationException("Could not locate repository root.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue