acdream/src/AcDream.App/Rendering/WorldRenderFrameBuilder.cs
Erik 28e1cf8029 refactor(render): extract world scene frame owner
Move fallback and PView world drawing, shared alpha, particles, visibility, selection, and diagnostics behind focused frame owners. Make exceptional frames failure-atomic by restoring the shared GL baseline and preserving primary alpha failures through cleanup.

Co-authored-by: Codex <codex@openai.com>
2026-07-22 07:22:09 +02:00

657 lines
23 KiB
C#

using System.Numerics;
using AcDream.App.Audio;
using AcDream.App.Input;
using AcDream.App.Rendering.Selection;
using AcDream.App.Rendering.Vfx;
using AcDream.App.Rendering.Wb;
using AcDream.App.Streaming;
using AcDream.App.World;
using AcDream.Core.Lighting;
using AcDream.Core.Physics;
using AcDream.Core.Rendering;
using AcDream.Core.World;
using AcDream.UI.Abstractions.Panels.Settings;
namespace AcDream.App.Rendering;
/// <summary>Camera facts borrowed for one world render preparation.</summary>
internal readonly record struct WorldCameraFrame(
ICamera Camera,
Matrix4x4 Projection,
Matrix4x4 ViewProjection,
FrustumPlanes Frustum,
Matrix4x4 InverseView,
Vector3 Position);
/// <summary>Player-lighting and collided-viewer roots for one frame.</summary>
internal readonly record struct WorldRootFrame(
LoadedCell? PlayerRoot,
bool PlayerSeenOutside,
uint ViewerCellId,
Vector3 ViewerEyePosition,
Vector3 PlayerViewPosition,
LoadedCell? ViewerRoot,
bool CameraInsideCell,
bool RootSeenOutside,
bool PlayerInsideCell,
uint? PlayerLandblockId,
int RenderCenterLandblockX,
int RenderCenterLandblockY,
uint PlayerCellId,
bool PlayerIndoorGate)
{
public bool RenderSky => ViewerRoot is null || RootSeenOutside;
}
/// <summary>Borrowed building scratch, valid only until the next build.</summary>
internal readonly record struct WorldBuildingFrame(
LoadedCell? OutdoorNode,
IReadOnlyList<LoadedCell> NearbyBuildingCells);
/// <summary>
/// Immutable one-frame world facts. Collection members borrow reusable owner
/// scratch and must not be retained beyond the next <see cref="WorldRenderFrameBuilder.Build"/>.
/// </summary>
internal readonly record struct WorldRenderFrame(
WorldCameraFrame Camera,
WorldRootFrame Roots,
WorldBuildingFrame Buildings,
HashSet<uint> AnimatedEntityIds)
{
public LoadedCell? ClipRoot => Roots.ViewerRoot ?? Buildings.OutdoorNode;
}
internal interface IWorldRenderFrameBuilder
{
WorldRenderFrame Build(
in RenderFrameFoundation foundation,
bool waitingForLogin,
DayGroupData? activeDayGroup);
void ObserveDrawableCells(IReadOnlySet<uint> drawableCells);
void ClearDrawableCells();
}
internal interface IWorldFrameCameraSource
{
WorldCameraFrame Resolve();
}
internal interface IWorldFrameRootSource
{
WorldRootFrame Resolve(in WorldCameraFrame camera);
}
internal interface IWorldFrameVisibilityPreparation
{
void Begin(in WorldCameraFrame camera, bool waitingForLogin);
void PublishViewProjection(in WorldCameraFrame camera);
}
internal interface IWorldFrameSettingsPreview
{
void Apply(in WorldCameraFrame camera);
}
internal interface IWorldFrameEnvironmentPreparation
{
void Prepare(
in WorldCameraFrame camera,
in WorldRootFrame roots,
in RenderFrameFoundation foundation,
DayGroupData? activeDayGroup);
void ObserveDrawableCells(IReadOnlySet<uint> drawableCells);
void ClearDrawableCells();
}
internal interface IWorldFrameAnimatedEntitySource
{
HashSet<uint> Capture();
}
internal interface IWorldFrameBuildingSource
{
WorldBuildingFrame Gather(
LoadedCell? viewerRoot,
uint viewerCellId,
in FrustumPlanes frustum);
}
/// <summary>
/// Orders typed world-frame fact sources without owning their borrowed render
/// resources. It makes no draw decision and retains no result from a prior call.
/// </summary>
internal sealed class WorldRenderFrameBuilder : IWorldRenderFrameBuilder
{
private readonly IWorldFrameCameraSource _camera;
private readonly IWorldFrameVisibilityPreparation _visibility;
private readonly IWorldFrameSettingsPreview _settings;
private readonly IWorldFrameRootSource _roots;
private readonly IWorldFrameEnvironmentPreparation _environment;
private readonly IWorldFrameAnimatedEntitySource _animated;
private readonly IWorldFrameBuildingSource _buildings;
public WorldRenderFrameBuilder(
IWorldFrameCameraSource camera,
IWorldFrameVisibilityPreparation visibility,
IWorldFrameSettingsPreview settings,
IWorldFrameRootSource roots,
IWorldFrameEnvironmentPreparation environment,
IWorldFrameAnimatedEntitySource animated,
IWorldFrameBuildingSource buildings)
{
_camera = camera ?? throw new ArgumentNullException(nameof(camera));
_visibility = visibility ?? throw new ArgumentNullException(nameof(visibility));
_settings = settings ?? throw new ArgumentNullException(nameof(settings));
_roots = roots ?? throw new ArgumentNullException(nameof(roots));
_environment = environment ?? throw new ArgumentNullException(nameof(environment));
_animated = animated ?? throw new ArgumentNullException(nameof(animated));
_buildings = buildings ?? throw new ArgumentNullException(nameof(buildings));
}
public WorldRenderFrame Build(
in RenderFrameFoundation foundation,
bool waitingForLogin,
DayGroupData? activeDayGroup)
{
WorldCameraFrame camera = _camera.Resolve();
_visibility.Begin(in camera, waitingForLogin);
_settings.Apply(in camera);
WorldRootFrame roots = _roots.Resolve(in camera);
_environment.Prepare(in camera, in roots, in foundation, activeDayGroup);
_visibility.PublishViewProjection(in camera);
HashSet<uint> animated = _animated.Capture();
FrustumPlanes frustum = camera.Frustum;
WorldBuildingFrame buildings = _buildings.Gather(
roots.ViewerRoot,
roots.ViewerCellId,
in frustum);
return new WorldRenderFrame(camera, roots, buildings, animated);
}
public void ObserveDrawableCells(IReadOnlySet<uint> drawableCells) =>
_environment.ObserveDrawableCells(drawableCells);
public void ClearDrawableCells() => _environment.ClearDrawableCells();
}
internal sealed class RuntimeWorldFrameCameraSource : IWorldFrameCameraSource
{
private readonly CameraController _cameras;
private readonly LocalPlayerTeleportController _teleport;
public RuntimeWorldFrameCameraSource(
CameraController cameras,
LocalPlayerTeleportController teleport)
{
_cameras = cameras ?? throw new ArgumentNullException(nameof(cameras));
_teleport = teleport ?? throw new ArgumentNullException(nameof(teleport));
}
public WorldCameraFrame Resolve()
{
ICamera camera = _teleport.ApplyViewPlane(_cameras.Active);
Matrix4x4 projection = camera.Projection;
Matrix4x4 viewProjection = camera.View * projection;
FrustumPlanes frustum = FrustumPlanes.FromViewProjection(viewProjection);
Matrix4x4.Invert(camera.View, out Matrix4x4 inverseView);
var position = new Vector3(inverseView.M41, inverseView.M42, inverseView.M43);
return new WorldCameraFrame(
camera,
projection,
viewProjection,
frustum,
inverseView,
position);
}
}
internal sealed class RuntimeWorldFrameRootSource : IWorldFrameRootSource
{
private const float LandblockSize = 192f;
private readonly PhysicsEngine _physics;
private readonly CellVisibility _cells;
private readonly ILocalPlayerModeSource _mode;
private readonly IChaseCameraSource _chase;
private readonly ILocalPlayerControllerSource _player;
private readonly LiveWorldOriginState _origin;
public RuntimeWorldFrameRootSource(
PhysicsEngine physics,
CellVisibility cells,
ILocalPlayerModeSource mode,
IChaseCameraSource chase,
ILocalPlayerControllerSource player,
LiveWorldOriginState origin)
{
_physics = physics ?? throw new ArgumentNullException(nameof(physics));
_cells = cells ?? throw new ArgumentNullException(nameof(cells));
_mode = mode ?? throw new ArgumentNullException(nameof(mode));
_chase = chase ?? throw new ArgumentNullException(nameof(chase));
_player = player ?? throw new ArgumentNullException(nameof(player));
_origin = origin ?? throw new ArgumentNullException(nameof(origin));
}
public WorldRootFrame Resolve(in WorldCameraFrame camera)
{
// Retail keeps these as two related but distinct positions:
// CellManager::ChangePosition @ 0x004559B0 uses the player's current
// cell for seen_outside/lighting, while RenderNormalMode @ 0x00453AA0
// passes the collided camera's viewer_cell to DrawInside.
LoadedCell? playerRoot = null;
if (_physics.DataCache?.CellGraph.CurrCell is AcDream.Core.World.Cells.EnvCell playerCell
&& _cells.TryGetCell(playerCell.Id, out LoadedCell? registeredPlayer))
{
playerRoot = registeredPlayer;
}
bool playerSeenOutside = playerRoot?.SeenOutside ?? true;
uint viewerCellId = _mode.IsPlayerMode
&& _chase.Retail is { } retailChase
&& CameraDiagnostics.UseRetailChaseCamera
? retailChase.ViewerCellId
: playerRoot?.CellId ?? 0u;
LoadedCell? viewerRoot = null;
if (viewerCellId != 0u
&& _cells.TryGetCell(viewerCellId, out LoadedCell? registeredViewer))
{
viewerRoot = registeredViewer;
}
var player = _player.Controller;
Vector3 playerViewPosition = player?.RenderPosition
?? player?.Position
?? camera.Position;
bool cameraInsideCell = viewerRoot is not null;
bool rootSeenOutside = viewerRoot?.SeenOutside ?? true;
bool playerInsideCell = playerRoot is not null && !playerSeenOutside;
uint? playerLandblockId = null;
if (_mode.IsPlayerMode && player is not null)
{
int playerX = _origin.CenterX + (int)Math.Floor(player.Position.X / LandblockSize);
int playerY = _origin.CenterY + (int)Math.Floor(player.Position.Y / LandblockSize);
playerLandblockId = (uint)((playerX << 24) | (playerY << 16) | 0xFFFF);
}
int renderCenterX = _origin.CenterX
+ (int)Math.Floor(camera.Position.X / LandblockSize);
int renderCenterY = _origin.CenterY
+ (int)Math.Floor(camera.Position.Y / LandblockSize);
uint playerCellId = _physics.DataCache?.CellGraph.CurrCell?.Id ?? 0u;
bool playerIndoorGate = RenderingDiagnostics.ShouldRenderIndoor(
playerCellId,
playerRoot is not null);
return new WorldRootFrame(
playerRoot,
playerSeenOutside,
viewerCellId,
camera.Position,
playerViewPosition,
viewerRoot,
cameraInsideCell,
rootSeenOutside,
playerInsideCell,
playerLandblockId,
renderCenterX,
renderCenterY,
playerCellId,
playerIndoorGate);
}
}
internal sealed class RuntimeWorldFrameVisibilityPreparation
: IWorldFrameVisibilityPreparation
{
private readonly RetailSelectionScene? _selection;
private readonly ParticleVisibilityController _particles;
private readonly TerrainModernRenderer? _terrain;
private readonly WorldRevealCoordinator? _reveal;
private readonly WbFrustum? _environmentFrustum;
public RuntimeWorldFrameVisibilityPreparation(
RetailSelectionScene? selection,
ParticleVisibilityController particles,
TerrainModernRenderer? terrain,
WorldRevealCoordinator? reveal,
WbFrustum? environmentFrustum)
{
_selection = selection;
_particles = particles ?? throw new ArgumentNullException(nameof(particles));
_terrain = terrain;
_reveal = reveal;
_environmentFrustum = environmentFrustum;
}
public void Begin(in WorldCameraFrame camera, bool waitingForLogin)
{
_selection?.SetViewFrustum(camera.Frustum);
_particles.BeginFrame(camera.Position);
_terrain?.BeginVisibilityFrame();
if (waitingForLogin)
return;
_particles.UseWorldView();
_reveal?.ObserveWorldViewportVisible();
_reveal?.Complete();
}
public void PublishViewProjection(in WorldCameraFrame camera) =>
_environmentFrustum?.Update(camera.ViewProjection);
}
internal sealed class RuntimeWorldFrameSettingsPreview : IWorldFrameSettingsPreview
{
private readonly SettingsVM? _settings;
private readonly OpenAlAudioEngine? _audio;
private readonly CameraController _cameras;
private readonly DisplayFramePacingController _pacing;
public RuntimeWorldFrameSettingsPreview(
SettingsVM? settings,
OpenAlAudioEngine? audio,
CameraController cameras,
DisplayFramePacingController pacing)
{
_settings = settings;
_audio = audio;
_cameras = cameras ?? throw new ArgumentNullException(nameof(cameras));
_pacing = pacing ?? throw new ArgumentNullException(nameof(pacing));
}
public void Apply(in WorldCameraFrame camera)
{
if (_audio is { IsAvailable: true } && _settings is not null)
{
var audio = _settings.AudioDraft;
_audio.MasterVolume = audio.Master;
_audio.MusicVolume = audio.Music;
_audio.SfxVolume = audio.Sfx;
_audio.AmbientVolume = audio.Ambient;
}
if (_settings is not null)
{
var display = _settings.DisplayDraft;
float fieldOfView = display.FieldOfView * (MathF.PI / 180f);
_cameras.Orbit.FovY = fieldOfView;
_cameras.Fly.FovY = fieldOfView;
if (_cameras.Chase is not null)
_cameras.Chase.FovY = fieldOfView;
_pacing.ApplyPreference(display.VSync);
}
if (_audio is not { IsAvailable: true })
return;
Matrix4x4 inverse = camera.InverseView;
var forward = new Vector3(-inverse.M31, -inverse.M32, -inverse.M33);
var up = new Vector3(inverse.M21, inverse.M22, inverse.M23);
Vector3 position = camera.Position;
_audio.SetListener(
position.X, position.Y, position.Z,
forward.X, forward.Y, forward.Z,
up.X, up.Y, up.Z);
}
}
internal interface IWorldRenderRangeSource
{
int NearRadius { get; }
int FarRadius { get; }
}
internal sealed class WorldRenderRangeState : IWorldRenderRangeSource
{
public WorldRenderRangeState(int nearRadius, int farRadius)
{
NearRadius = nearRadius;
FarRadius = farRadius;
}
public int NearRadius { get; set; }
public int FarRadius { get; set; }
}
internal sealed class RuntimeWorldFrameEnvironmentPreparation
: IWorldFrameEnvironmentPreparation
{
private const float LandblockSize = 192f;
private readonly RuntimeOptions _options;
private readonly WorldTimeService _worldTime;
private readonly LightManager _lighting;
private readonly WbDrawDispatcher? _dispatcher;
private readonly EnvCellRenderer? _environmentCells;
private readonly SceneLightingUboBinding? _lightingUbo;
private readonly IWorldRenderRangeSource _ranges;
private readonly SkyPesFrameController? _skyPes;
private readonly HashSet<uint> _visibleCells = [];
private bool _visibleCellsValid;
public RuntimeWorldFrameEnvironmentPreparation(
RuntimeOptions options,
WorldTimeService worldTime,
LightManager lighting,
WbDrawDispatcher? dispatcher,
EnvCellRenderer? environmentCells,
SceneLightingUboBinding? lightingUbo,
IWorldRenderRangeSource ranges,
SkyPesFrameController? skyPes)
{
_options = options ?? throw new ArgumentNullException(nameof(options));
_worldTime = worldTime ?? throw new ArgumentNullException(nameof(worldTime));
_lighting = lighting ?? throw new ArgumentNullException(nameof(lighting));
_dispatcher = dispatcher;
_environmentCells = environmentCells;
_lightingUbo = lightingUbo;
_ranges = ranges ?? throw new ArgumentNullException(nameof(ranges));
_skyPes = skyPes;
}
public void Prepare(
in WorldCameraFrame camera,
in WorldRootFrame roots,
in RenderFrameFoundation foundation,
DayGroupData? activeDayGroup)
{
if (_options.EnableSkyPesDebug)
{
_skyPes?.Update(
(float)_worldTime.DayFraction,
activeDayGroup,
camera.Position,
roots.CameraInsideCell);
}
UpdateSunFromSky(foundation.Sky, roots.PlayerInsideCell);
_lighting.UpdateViewerLight(roots.PlayerViewPosition);
_lighting.Tick(camera.Position);
_lighting.BuildPointLightSnapshot(
roots.PlayerViewPosition,
_visibleCellsValid ? _visibleCells : null);
_dispatcher?.SetSceneLights(_lighting.PointSnapshot);
_environmentCells?.SetPointSnapshot(_lighting.PointSnapshot);
AtmosphereSnapshot atmosphere = foundation.Atmosphere;
SceneLightingUbo ubo = SceneLightingUbo.Build(
_lighting,
in atmosphere,
camera.Position,
(float)_worldTime.DayFraction);
float fogStart = _ranges.NearRadius
* LandblockSize
* _options.FogStartMultiplier;
float fogEnd = _ranges.FarRadius
* LandblockSize
* _options.FogEndMultiplier;
ubo.FogParams = new Vector4(
fogStart,
fogEnd,
ubo.FogParams.Z,
ubo.FogParams.W);
_lightingUbo?.Upload(ubo);
RenderingDiagnostics.EmitLight(
insideCell: roots.PlayerInsideCell,
ambientR: _lighting.CurrentAmbient.AmbientColor.X,
ambientG: _lighting.CurrentAmbient.AmbientColor.Y,
ambientB: _lighting.CurrentAmbient.AmbientColor.Z,
sunIntensity: _lighting.Sun?.Intensity ?? 0f,
registeredLights: _lighting.RegisteredCount,
activeLights: (int)ubo.CellAmbient.W,
playerCellId: roots.PlayerRoot?.CellId ?? 0u,
lights: _lighting);
}
public void ObserveDrawableCells(IReadOnlySet<uint> drawableCells)
{
ArgumentNullException.ThrowIfNull(drawableCells);
_visibleCells.Clear();
_visibleCells.UnionWith(drawableCells);
_visibleCellsValid = true;
}
public void ClearDrawableCells() => _visibleCellsValid = false;
private void UpdateSunFromSky(SkyKeyframe keyframe, bool playerInsideCell)
{
// CellManager::ChangePosition @ 0x004559B0 keys this on the player
// cell's seen_outside flag and installs neutral 0.2 ambient indoors.
Vector3 sunToWorld = -SkyStateProvider.SunDirectionFromKeyframe(keyframe);
if (playerInsideCell)
{
_lighting.Sun = new LightSource
{
Kind = LightKind.Directional,
WorldForward = sunToWorld,
ColorLinear = Vector3.Zero,
Intensity = 0f,
Range = 1f,
};
_lighting.CurrentAmbient = new CellAmbientState(
new Vector3(0.20f, 0.20f, 0.20f),
Vector3.Zero,
sunToWorld);
return;
}
_lighting.Sun = new LightSource
{
Kind = LightKind.Directional,
WorldForward = sunToWorld,
ColorLinear = keyframe.SunColor,
Intensity = 1f,
Range = 1f,
};
_lighting.CurrentAmbient = new CellAmbientState(
keyframe.AmbientColor,
keyframe.SunColor,
sunToWorld);
}
}
internal sealed class RuntimeWorldFrameAnimatedEntitySource
: IWorldFrameAnimatedEntitySource
{
private readonly LiveEntityAnimationRuntimeView<LiveEntityAnimationState> _live;
private readonly RetailStaticAnimatingObjectScheduler? _statics;
private readonly EquippedChildRenderController? _equipped;
private readonly HashSet<uint> _scratch = [];
public RuntimeWorldFrameAnimatedEntitySource(
LiveEntityAnimationRuntimeView<LiveEntityAnimationState> live,
RetailStaticAnimatingObjectScheduler? statics,
EquippedChildRenderController? equipped)
{
_live = live ?? throw new ArgumentNullException(nameof(live));
_statics = statics;
_equipped = equipped;
}
public HashSet<uint> Capture()
{
// Membership means "classify from the current pose this frame," not
// merely "animation is advancing." A settled door or translucency
// hook can differ permanently from its cached rest pose, so every
// sequenced/attached entity remains on the live path.
_live.CopySpatialIdsTo(_scratch);
_statics?.CopyAnimatedEntityIdsTo(_scratch);
if (_equipped is not null)
{
foreach (uint entityId in _equipped.AttachedEntityIds)
_scratch.Add(entityId);
}
return _scratch;
}
}
internal sealed class RuntimeWorldFrameBuildingSource : IWorldFrameBuildingSource
{
private readonly LandblockPresentationPipeline _presentation;
private readonly CellVisibility _cells;
private readonly List<LoadedCell> _scratch = [];
public RuntimeWorldFrameBuildingSource(
LandblockPresentationPipeline presentation,
CellVisibility cells)
{
_presentation = presentation
?? throw new ArgumentNullException(nameof(presentation));
_cells = cells ?? throw new ArgumentNullException(nameof(cells));
}
public WorldBuildingFrame Gather(
LoadedCell? viewerRoot,
uint viewerCellId,
in FrustumPlanes frustum)
{
_scratch.Clear();
LoadedCell? outdoorNode = null;
if (viewerRoot is null && viewerCellId == 0u)
return new WorldBuildingFrame(null, _scratch);
// DrawBuilding @ 0x0059F2A0 gates each building by its visible aperture
// before walking its portal BSP. The portal-bounds frustum test is the
// corresponding coarse admission gate; there is no distance cutoff.
foreach (BuildingRegistry registry in _presentation.BuildingRegistries)
{
foreach (Building building in registry.All())
{
if (building.HasPortalBounds
&& !FrustumCuller.IsAabbVisible(
frustum,
building.PortalBounds.Min,
building.PortalBounds.Max))
{
continue;
}
foreach (uint cellId in building.EnvCellIds)
{
if (_cells.TryGetCell(cellId, out LoadedCell? cell) && cell is not null)
_scratch.Add(cell);
}
}
}
if (viewerRoot is null)
outdoorNode = OutdoorCellNode.Build(viewerCellId);
if (RenderingDiagnostics.ProbeFlapEnabled)
{
Console.WriteLine(FormattableString.Invariant(
$"[outdoor-node] cell=0x{viewerCellId:X8} root={(viewerRoot is null ? "OUT" : "IN")} nearbyCells={_scratch.Count} (T2 frustum-gated per-building floods)"));
}
return new WorldBuildingFrame(outdoorNode, _scratch);
}
}