refactor(render): extract world frame preparation
Move camera/root resolution, live settings and listener preview, sky/lighting/fog preparation, animated classification, and building visibility scratch behind a typed WorldRenderFrameBuilder while preserving retail frame order and borrowed lifetimes. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
parent
bc6f09f987
commit
6d6e5b5fa5
8 changed files with 1463 additions and 583 deletions
|
|
@ -16,8 +16,6 @@ public sealed class GameWindow : IDisposable
|
|||
System.Diagnostics.Stopwatch.GetTimestamp()
|
||||
/ (double)System.Diagnostics.Stopwatch.Frequency;
|
||||
|
||||
private readonly record struct SkyPesKey(int ObjectIndex, uint PesObjectId, bool PostScene);
|
||||
|
||||
private readonly AcDream.App.RuntimeOptions _options;
|
||||
private readonly AnimationPresentationDiagnostics _animationDiagnostics;
|
||||
private readonly string _datDir;
|
||||
|
|
@ -108,6 +106,9 @@ public sealed class GameWindow : IDisposable
|
|||
_renderFrameLivePreparation;
|
||||
private AcDream.App.Rendering.RenderWeatherFrameController?
|
||||
_renderWeatherFrame;
|
||||
private AcDream.App.Rendering.WorldRenderFrameBuilder?
|
||||
_worldRenderFrameBuilder;
|
||||
private AcDream.App.Rendering.SkyPesFrameController? _skyPesFrame;
|
||||
private ResourceShutdownTransaction? _shutdown;
|
||||
private readonly DisplayFramePacingController _displayFramePacing;
|
||||
|
||||
|
|
@ -126,8 +127,18 @@ public sealed class GameWindow : IDisposable
|
|||
private AcDream.App.Streaming.LocalPlayerTeleportController?
|
||||
_localPlayerTeleport;
|
||||
private int _streamingRadius = 2; // default 5×5 (kept for debug overlay getStreamingRadius callback)
|
||||
private int _nearRadius = 4; // Phase A.5 T16: two-tier near ring (default 4 → 9×9)
|
||||
private int _farRadius = 12; // Phase A.5 T16: two-tier far ring (default 12 → 25×25)
|
||||
private readonly AcDream.App.Rendering.WorldRenderRangeState _renderRange =
|
||||
new(nearRadius: 4, farRadius: 12);
|
||||
private int _nearRadius
|
||||
{
|
||||
get => _renderRange.NearRadius;
|
||||
set => _renderRange.NearRadius = value;
|
||||
}
|
||||
private int _farRadius
|
||||
{
|
||||
get => _renderRange.FarRadius;
|
||||
set => _renderRange.FarRadius = value;
|
||||
}
|
||||
// A.5 T22.5: resolved quality settings (preset + env-var overrides).
|
||||
// Set once in OnLoad after LoadAndApplyPersistedSettings(); re-set on
|
||||
// ReapplyQualityPreset(). Default matches QualityPreset.High so the field
|
||||
|
|
@ -226,12 +237,8 @@ public sealed class GameWindow : IDisposable
|
|||
// U.4 replaces the NoClip() frame with one built from the portal-visibility result.
|
||||
private ClipFrame? _clipFrame;
|
||||
|
||||
// Phase 3 (render unification, 2026-06-07): the synthetic outdoor cell node — the outdoor
|
||||
// world as a flood-graph cell (spec 2026-06-07-render-unification-outdoor-as-cell). Rebuilt
|
||||
// each outdoor frame from nearby building-entrance portals. ADDITIVE for now (built but not
|
||||
// yet rooted; the clipRoot flip + OutsideView terrain integration is the cutover step).
|
||||
private LoadedCell? _outdoorNode;
|
||||
private readonly List<LoadedCell> _outdoorNodeBuildingCells = new();
|
||||
// PView pass-local particle classifications move with the pass executor in
|
||||
// Checkpoint E; GameWindow still owns them until that cutover.
|
||||
private readonly HashSet<uint> _outdoorSceneParticleEntityIds = new();
|
||||
private readonly HashSet<uint> _visibleSceneParticleEntityIds = new();
|
||||
private readonly HashSet<uint> _noSceneParticleEntityIds = new();
|
||||
|
|
@ -248,13 +255,6 @@ public sealed class GameWindow : IDisposable
|
|||
private readonly LiveEntityAnimationRuntimeView<LiveEntityAnimationState> _animatedEntities;
|
||||
private readonly AcDream.App.World.LiveEntityRuntimeSlot _liveEntityRuntimeSlot = new();
|
||||
|
||||
// MP-Alloc (2026-07-05): reusable per-frame snapshot of _animatedEntities.Keys.
|
||||
// WbDrawDispatcher.WalkEntitiesInto treats null and an empty set identically
|
||||
// (`animatedEntityIds is null || animatedEntityIds.Count == 0`), so this can
|
||||
// always be a live (possibly empty) HashSet instead of `new`ing one every
|
||||
// frame or passing null.
|
||||
private readonly HashSet<uint> _animatedIdsScratch = new();
|
||||
|
||||
/// <summary>
|
||||
/// Tier 1 cache (#53): per-entity classification results for static
|
||||
/// entities (those NOT in <see cref="_animatedEntities"/>). Conceptually
|
||||
|
|
@ -298,35 +298,6 @@ public sealed class GameWindow : IDisposable
|
|||
private AcDream.App.Rendering.Vfx.EntityEffectController? _entityEffects;
|
||||
private AcDream.App.Rendering.ParticleRenderer? _particleRenderer;
|
||||
private readonly AcDream.App.Rendering.RetailAlphaQueue _retailAlphaQueue = new();
|
||||
// Retail GameSky copies SkyObject.PesObjectId into CelestialPosition but
|
||||
// never consumes it in CreateDeletePhysicsObjects/MakeObject/UseTime.
|
||||
// Keep the experimental path available for DAT archaeology only.
|
||||
// Backed by ACDREAM_ENABLE_SKY_PES via RuntimeOptions.EnableSkyPesDebug.
|
||||
|
||||
// Diagnostic: hide a specific humanoid part (>=10 parts) at render.
|
||||
// Backed by ACDREAM_HIDE_PART via RuntimeOptions.HidePartIndex.
|
||||
|
||||
// Issue #47 — use retail's close-detail GfxObj selection on
|
||||
// humanoid setups. When enabled, every per-part GfxObj id (after
|
||||
// server AnimPartChanges are applied) is replaced with Degrades[0]
|
||||
// from its DIDDegrade table when present. See GfxObjDegradeResolver
|
||||
// for the full retail-decomp citation. Default-on after visual
|
||||
// confirmation; set ACDREAM_RETAIL_CLOSE_DEGRADES=0 only for
|
||||
// diagnostic before/after comparisons.
|
||||
// Backed by ACDREAM_RETAIL_CLOSE_DEGRADES via RuntimeOptions.RetailCloseDegrades.
|
||||
|
||||
// Issue #48 diagnostic — dump per-scenery-spawn placement evidence
|
||||
// (rendered gfx id, sample source physics-vs-bilinear, ground/baseLoc/finalZ,
|
||||
// mesh vertex Z range, DIDDegrade slot 0). One log line per spawn lets
|
||||
// the user identify a floating tree by its world coordinates and tell
|
||||
// whether the cause is BaseLoc.Z addition (H1), bilinear-fallback drift
|
||||
// (H2), or DIDDegrade selection (H3). Diagnostic-first per CLAUDE.md.
|
||||
// Backed by ACDREAM_DUMP_SCENERY_Z via RuntimeOptions.DumpSceneryZ.
|
||||
|
||||
/// <summary>
|
||||
private readonly HashSet<SkyPesKey> _activeSkyPes = new();
|
||||
private readonly HashSet<SkyPesKey> _missingSkyPes = new();
|
||||
|
||||
// Remote-entity motion inference: tracks when each remote entity last
|
||||
// moved meaningfully. Used in TickAnimations to swap to Ready when
|
||||
// position has stalled for >StopIdleMs — retail observer pattern per
|
||||
|
|
@ -467,21 +438,6 @@ public sealed class GameWindow : IDisposable
|
|||
AcDream.Core.World.SkyStateProvider.Default());
|
||||
public readonly AcDream.Core.Lighting.LightManager Lighting = new();
|
||||
|
||||
// A7.L1 (2026-07-09) — last frame's rendered visible-cell set, fed into next
|
||||
// frame's Lighting.BuildPointLightSnapshot scoping. One frame of latency
|
||||
// (~16 ms) instead of a mid-DrawInside callback: DrawableCells only becomes
|
||||
// known partway through DrawInside (after the flood + look-in merge), and the
|
||||
// #176 correction already showed that rebuilding the light pool FROM a
|
||||
// freshly-recomputed frame flood (the deleted RebuildScopedLights callback,
|
||||
// c500912b) is a flicker mechanism. Reusing last frame's already-drawn set
|
||||
// (MP-Alloc: reused HashSet, no per-frame alloc) decouples the light-scoping
|
||||
// change from DrawInside's internals entirely. _lightPoolVisibleCellsValid is
|
||||
// false until the first indoor frame completes, and is cleared on any
|
||||
// outdoor-only frame so scoping fails open (unscoped) for one frame on
|
||||
// re-entry rather than filtering by a stale dungeon's cell ids.
|
||||
private readonly HashSet<uint> _lightPoolVisibleCells = new();
|
||||
private bool _lightPoolVisibleCellsValid;
|
||||
|
||||
public readonly AcDream.Core.World.WeatherSystem Weather = new();
|
||||
// Wired into the hook router in OnLoad so SetLightHook fires
|
||||
// from the animation pipeline flip the matching LightSource.IsLit.
|
||||
|
|
@ -2890,6 +2846,52 @@ public sealed class GameWindow : IDisposable
|
|||
new AcDream.App.Rendering.RenderWeatherFrameController(
|
||||
WorldTime,
|
||||
Weather);
|
||||
_skyPesFrame = new AcDream.App.Rendering.SkyPesFrameController(
|
||||
_scriptRunner!,
|
||||
_particleSink!,
|
||||
_effectPoses,
|
||||
_entityEffects);
|
||||
var worldFrameEnvironment =
|
||||
new AcDream.App.Rendering.RuntimeWorldFrameEnvironmentPreparation(
|
||||
_options,
|
||||
WorldTime,
|
||||
Lighting,
|
||||
_wbDrawDispatcher,
|
||||
_envCellRenderer,
|
||||
_sceneLightingUbo,
|
||||
_renderRange,
|
||||
_skyPesFrame);
|
||||
_worldRenderFrameBuilder =
|
||||
new AcDream.App.Rendering.WorldRenderFrameBuilder(
|
||||
new AcDream.App.Rendering.RuntimeWorldFrameCameraSource(
|
||||
_cameraController!,
|
||||
_localPlayerTeleport),
|
||||
new AcDream.App.Rendering.RuntimeWorldFrameVisibilityPreparation(
|
||||
_retailSelectionScene,
|
||||
_particleVisibility,
|
||||
_terrain,
|
||||
_worldReveal,
|
||||
_envCellFrustum),
|
||||
new AcDream.App.Rendering.RuntimeWorldFrameSettingsPreview(
|
||||
_settingsVm,
|
||||
_audioEngine,
|
||||
_cameraController!,
|
||||
_displayFramePacing),
|
||||
new AcDream.App.Rendering.RuntimeWorldFrameRootSource(
|
||||
_physicsEngine,
|
||||
_cellVisibility,
|
||||
_localPlayerMode,
|
||||
_chaseCameraInput,
|
||||
_playerControllerSlot,
|
||||
_liveWorldOrigin),
|
||||
worldFrameEnvironment,
|
||||
new AcDream.App.Rendering.RuntimeWorldFrameAnimatedEntitySource(
|
||||
_animatedEntities,
|
||||
_staticAnimationScheduler,
|
||||
_equippedChildRenderer),
|
||||
new AcDream.App.Rendering.RuntimeWorldFrameBuildingSource(
|
||||
_landblockPresentationPipeline!,
|
||||
_cellVisibility));
|
||||
var liveFrameCoordinator = new AcDream.App.World.RetailLiveFrameCoordinator(
|
||||
liveObjectFrame,
|
||||
_worldState,
|
||||
|
|
@ -3268,115 +3270,8 @@ public sealed class GameWindow : IDisposable
|
|||
session.SendTurbineChatTo(
|
||||
roomId, chatType, dispatchType, senderGuid, text, cookie),
|
||||
Log: Console.WriteLine);
|
||||
private void UpdateSkyPes(
|
||||
float dayFraction,
|
||||
AcDream.Core.World.DayGroupData? dayGroup,
|
||||
System.Numerics.Vector3 cameraWorldPos,
|
||||
bool suppressSky)
|
||||
{
|
||||
if (_scriptRunner is null || _particleSink is null)
|
||||
return;
|
||||
|
||||
var seen = new HashSet<SkyPesKey>();
|
||||
if (!suppressSky && dayGroup is not null)
|
||||
{
|
||||
for (int i = 0; i < dayGroup.SkyObjects.Count; i++)
|
||||
{
|
||||
var obj = dayGroup.SkyObjects[i];
|
||||
if (obj.PesObjectId == 0 || !obj.IsVisible(dayFraction))
|
||||
continue;
|
||||
|
||||
var key = new SkyPesKey(i, obj.PesObjectId, obj.IsPostScene);
|
||||
seen.Add(key);
|
||||
uint skyEntityId = SkyPesEntityId(key);
|
||||
var renderPass = obj.IsPostScene
|
||||
? AcDream.Core.Vfx.ParticleRenderPass.SkyPostScene
|
||||
: AcDream.Core.Vfx.ParticleRenderPass.SkyPreScene;
|
||||
_particleSink.SetEntityRenderPass(skyEntityId, renderPass);
|
||||
var anchor = SkyPesAnchor(obj, cameraWorldPos);
|
||||
var rotation = SkyPesRotation(obj, dayFraction);
|
||||
_effectPoses.Publish(
|
||||
skyEntityId,
|
||||
System.Numerics.Matrix4x4.CreateFromQuaternion(rotation)
|
||||
* System.Numerics.Matrix4x4.CreateTranslation(anchor),
|
||||
System.Array.Empty<System.Numerics.Matrix4x4>(),
|
||||
cellId: 0u);
|
||||
|
||||
if (_activeSkyPes.Contains(key) || _missingSkyPes.Contains(key))
|
||||
continue;
|
||||
|
||||
_entityEffects?.RegisterSyntheticOwner(skyEntityId);
|
||||
|
||||
if (_scriptRunner.Play(obj.PesObjectId, skyEntityId, anchor))
|
||||
{
|
||||
_activeSkyPes.Add(key);
|
||||
}
|
||||
else
|
||||
{
|
||||
_missingSkyPes.Add(key);
|
||||
_entityEffects?.UnregisterSyntheticOwner(skyEntityId);
|
||||
_particleSink.ClearEntityRenderPass(skyEntityId);
|
||||
_effectPoses.Remove(skyEntityId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var key in _activeSkyPes.ToArray())
|
||||
{
|
||||
if (seen.Contains(key))
|
||||
continue;
|
||||
|
||||
uint skyEntityId = SkyPesEntityId(key);
|
||||
_scriptRunner.StopAllForEntity(skyEntityId);
|
||||
_entityEffects?.UnregisterSyntheticOwner(skyEntityId);
|
||||
_particleSink.StopAllForEntity(skyEntityId, fadeOut: true);
|
||||
_effectPoses.Remove(skyEntityId);
|
||||
_activeSkyPes.Remove(key);
|
||||
}
|
||||
|
||||
foreach (var key in _missingSkyPes.ToArray())
|
||||
{
|
||||
if (!seen.Contains(key))
|
||||
_missingSkyPes.Remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
private static uint SkyPesEntityId(SkyPesKey key)
|
||||
{
|
||||
// 0xF0000000 prefix marks synthetic sky-PES entityIds (no real
|
||||
// server GUID lives in the 0xFxxxxxxx space). Reserve bit
|
||||
// 0x08000000 for the pre/post-scene flag and the lower 27 bits
|
||||
// for the object index — keeps the post-scene flag from sliding
|
||||
// into the index range if a future DayGroup ever ships >65k sky
|
||||
// objects (current Dereth max is 18, but the constraint is free).
|
||||
uint postBit = key.PostScene ? 0x08000000u : 0u;
|
||||
return 0xF0000000u | postBit | ((uint)key.ObjectIndex & 0x07FFFFFFu);
|
||||
}
|
||||
|
||||
internal static uint ParticleEntityKey(AcDream.Core.World.WorldEntity entity) =>
|
||||
entity.Id;
|
||||
|
||||
|
||||
private static System.Numerics.Vector3 SkyPesAnchor(
|
||||
AcDream.Core.World.SkyObjectData obj,
|
||||
System.Numerics.Vector3 cameraWorldPos)
|
||||
{
|
||||
if (obj.IsWeather && (obj.Properties & 0x08u) == 0u)
|
||||
return cameraWorldPos + new System.Numerics.Vector3(0f, 0f, -120f);
|
||||
|
||||
return cameraWorldPos;
|
||||
}
|
||||
|
||||
private static System.Numerics.Quaternion SkyPesRotation(
|
||||
AcDream.Core.World.SkyObjectData obj,
|
||||
float dayFraction)
|
||||
{
|
||||
float rotationRad = obj.CurrentAngle(dayFraction) * (MathF.PI / 180f);
|
||||
return System.Numerics.Quaternion.CreateFromAxisAngle(
|
||||
System.Numerics.Vector3.UnitY,
|
||||
-rotationRad);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Phase 5d — retail <c>AdminEnvirons</c> (0xEA60) dispatcher.
|
||||
/// Routes fog presets into the weather system's sticky override
|
||||
|
|
@ -3522,280 +3417,34 @@ public sealed class GameWindow : IDisposable
|
|||
if (_cameraController is not null && !portalViewportVisible)
|
||||
{
|
||||
_retailAlphaQueue.BeginFrame();
|
||||
var activeCamera = _cameraController.Active;
|
||||
var camera = _localPlayerTeleport?.ApplyViewPlane(activeCamera)
|
||||
?? activeCamera;
|
||||
var worldProjection = camera.Projection;
|
||||
var frustum = AcDream.App.Rendering.FrustumPlanes.FromViewProjection(camera.View * worldProjection);
|
||||
_retailSelectionScene?.SetViewFrustum(frustum);
|
||||
|
||||
// Extract camera world position from the inverse of the view
|
||||
// matrix — needed by the scene-lighting UBO (for fog distance)
|
||||
// and by the sky renderer (for the camera-centered sky dome).
|
||||
System.Numerics.Matrix4x4.Invert(camera.View, out var invView);
|
||||
var camPos = new System.Numerics.Vector3(invView.M41, invView.M42, invView.M43);
|
||||
_particleVisibility.BeginFrame(camPos);
|
||||
_terrain?.BeginVisibilityFrame();
|
||||
if (!IsLiveModeWaitingForLogin)
|
||||
{
|
||||
_particleVisibility.UseWorldView();
|
||||
_worldReveal?.ObserveWorldViewportVisible();
|
||||
_worldReveal?.Complete();
|
||||
}
|
||||
|
||||
// L.0 Audio tab: push the SettingsVM's live AudioDraft into the
|
||||
// engine each frame, so volume sliders preview audibly while
|
||||
// the user drags. Cancel reverts the draft and the engine
|
||||
// catches up on the very next frame; Save persists to
|
||||
// settings.json without changing engine state (already
|
||||
// applied). Cheap enough to run unconditionally on every
|
||||
// tick — four float assignments.
|
||||
if (_audioEngine is not null && _audioEngine.IsAvailable && _settingsVm is not null)
|
||||
{
|
||||
var a = _settingsVm.AudioDraft;
|
||||
_audioEngine.MasterVolume = a.Master;
|
||||
_audioEngine.MusicVolume = a.Music;
|
||||
_audioEngine.SfxVolume = a.Sfx;
|
||||
_audioEngine.AmbientVolume = a.Ambient;
|
||||
}
|
||||
|
||||
// L.0 Display tab: push the live DisplayDraft into the
|
||||
// active rendering surfaces each frame. FOV is the live-
|
||||
// preview slider per the brainstorm — dragging it changes
|
||||
// camera FovY immediately. VSync change-detected to avoid
|
||||
// spamming the window. Resolution + Fullscreen apply on
|
||||
// Save (handled by ApplyDisplayWindowState — too jarring
|
||||
// to live-preview a resize).
|
||||
if (_settingsVm is not null && _cameraController is not null)
|
||||
{
|
||||
var d = _settingsVm.DisplayDraft;
|
||||
float fovYRad = d.FieldOfView * (MathF.PI / 180f);
|
||||
_cameraController.Orbit.FovY = fovYRad;
|
||||
_cameraController.Fly.FovY = fovYRad;
|
||||
if (_cameraController.Chase is not null)
|
||||
_cameraController.Chase.FovY = fovYRad;
|
||||
_displayFramePacing.ApplyPreference(d.VSync);
|
||||
}
|
||||
|
||||
// Phase E.2 audio: update listener pose so 3D sounds pan/attenuate
|
||||
// correctly relative to where we're looking.
|
||||
if (_audioEngine is not null && _audioEngine.IsAvailable)
|
||||
{
|
||||
var fwd = new System.Numerics.Vector3(-invView.M31, -invView.M32, -invView.M33);
|
||||
var up = new System.Numerics.Vector3( invView.M21, invView.M22, invView.M23);
|
||||
_audioEngine.SetListener(
|
||||
camPos.X, camPos.Y, camPos.Z,
|
||||
fwd.X, fwd.Y, fwd.Z,
|
||||
up.X, up.Y, up.Z);
|
||||
}
|
||||
|
||||
// Step 4: portal visibility — compute BEFORE the UBO upload so
|
||||
// the indoor flag drives the sun's intensity to zero for
|
||||
// dungeons (r13 §13.7).
|
||||
// Phase W single-viewpoint V1 (2026-06-03): the render keys on ONE viewpoint — the
|
||||
// collided camera ("viewer") — exactly like retail (RenderNormalMode @ 0x453aa0 →
|
||||
// DrawInside(viewer_cell) pc:92675; InitCell side-test vs viewer.viewpoint pc:432991).
|
||||
// The viewer cell is the camera-collision sweep's swept cell
|
||||
// (RetailChaseCamera.ViewerCellId = retail viewer_cell = sphere_path.curr_cell):
|
||||
// graph-tracked, deterministic, NO AABB / NO grace frames — so the U.4c flap source
|
||||
// (stale FindCameraCell over grace frames) is gone WITHOUT splitting viewpoints.
|
||||
// SEPARATELY, lighting / seen_outside key on the PLAYER cell (CurrCell), matching retail
|
||||
// CellManager::ChangePosition @ 0x4559B0 — the player's cell, not the camera's, decides
|
||||
// whether the sun dies (sealed interior). retail player->cell (physics/lighting) vs
|
||||
// SmartBox->viewer_cell (render); the old per-render player-root + eye-projection split is gone.
|
||||
|
||||
// ── Lighting root: the PLAYER cell (CurrCell). ──
|
||||
LoadedCell? playerRoot = null;
|
||||
if (_physicsEngine.DataCache?.CellGraph.CurrCell is AcDream.Core.World.Cells.EnvCell playerCellObj
|
||||
&& _cellVisibility.TryGetCell(playerCellObj.Id, out var playerRegCell))
|
||||
playerRoot = playerRegCell;
|
||||
bool playerSeenOutside = playerRoot?.SeenOutside ?? true;
|
||||
|
||||
// ── Render root: the VIEWER (collided camera) cell + eye. ──
|
||||
// Default (player mode + retail chase cam): the sweep's viewer cell. Fallback for the
|
||||
// non-default legacy/debug camera paths: the player's registered cell (or none).
|
||||
uint viewerCellId =
|
||||
(_playerMode && _retailChaseCamera is not null
|
||||
&& AcDream.Core.Rendering.CameraDiagnostics.UseRetailChaseCamera)
|
||||
? _retailChaseCamera.ViewerCellId
|
||||
: (playerRoot?.CellId ?? 0u);
|
||||
var viewerEyePos = camPos; // the collided eye drives the projection
|
||||
var playerViewPos = _playerController?.RenderPosition
|
||||
?? _playerController?.Position
|
||||
?? camPos;
|
||||
LoadedCell? viewerRoot = null;
|
||||
if (viewerCellId != 0u && _cellVisibility.TryGetCell(viewerCellId, out var viewerRegCell))
|
||||
viewerRoot = viewerRegCell;
|
||||
// T4 (BR-6): the per-frame ACME BFS (ComputeVisibilityFromRoot) is
|
||||
// DELETED from the frame — it ran a full second visibility
|
||||
// computation whose only production consumer was this boolean,
|
||||
// which is exactly "the viewer root resolved to a loaded interior
|
||||
// cell" (TryGetCell above already proves cells are loaded). The
|
||||
// PView flood is the ONE visibility gate (feedback_render_one_gate).
|
||||
bool cameraInsideCell = viewerRoot is not null;
|
||||
|
||||
// Retail render routing is owned by the collided camera/viewer cell.
|
||||
// The player cell still owns lighting state, but it must not force an
|
||||
// indoor draw while the camera is outside; that drops the outdoor pass
|
||||
// and leaves clear color around a floating doorway slice.
|
||||
bool rootSeenOutside = viewerRoot?.SeenOutside ?? true;
|
||||
|
||||
// Phase U.4 (2026-05-30): the [vis] probe moved DOWN to the unified
|
||||
// gated-draw block (after envCellViewProj exists) where it can report
|
||||
// the real PortalVisibilityFrame — OutsideView polygon/plane counts and
|
||||
// per-cell slot plane counts — via RenderingDiagnostics.EmitVis, instead
|
||||
// of the old camera-state-only spike. See the U.4 ClipFrame assembly
|
||||
// below (gated on ACDREAM_PROBE_VIS=1, cell-change-throttled).
|
||||
|
||||
// Stage 3 (2026-06-02): replace the IsInsideAnyCell AABB scan with the
|
||||
// seen_outside-derived predicate. Retail CellManager::ChangePosition (0x004559B0)
|
||||
// gates sun/lighting off seen_outside on the player's current cell, NOT off an
|
||||
// independent AABB containment scan. playerInsideCell = true (kill sunlight) only
|
||||
// when the player is inside a SEALED interior (seen_outside=false = dungeon).
|
||||
// Building interiors with seen_outside=true keep the sun (sky visible through door).
|
||||
// V1 (2026-06-03): keyed on the PLAYER cell (playerRoot/playerSeenOutside), independent
|
||||
// of the camera's viewer cell — retail kills the sun off the player's cell, not the eye.
|
||||
bool playerInsideCell = playerRoot is not null && !playerSeenOutside;
|
||||
|
||||
// Phase C.1: tick retail PhysicsScript particle hooks. Named
|
||||
// retail decomp confirms SkyObject.PesObjectId is copied by
|
||||
// SkyDesc::GetSky but ignored by GameSky, so the sky-PES path is
|
||||
// debug-only and disabled for normal retail rendering.
|
||||
if (_options.EnableSkyPesDebug)
|
||||
UpdateSkyPes((float)WorldTime.DayFraction, _activeDayGroup, camPos, cameraInsideCell);
|
||||
// Phase G.1/G.2: feed the sun, tick LightManager, build + upload
|
||||
// the scene-lighting UBO once per frame. Every shader that
|
||||
// consumes binding=1 reads the same data for the rest of the
|
||||
// frame — terrain, static mesh, instanced mesh, sky.
|
||||
UpdateSunFromSky(kf, playerInsideCell);
|
||||
// A7 indoor lighting: position retail's viewer fill light at the player
|
||||
// (SmartBox::set_viewer 0x00452c40) before the snapshot is built. It is
|
||||
// the primary interior fill (no sun indoors) and is indoor-only via the
|
||||
// AP-43 gate. playerViewPos is the player render position (or camPos when
|
||||
// there is no player), matching retail's player/viewer branch.
|
||||
Lighting.UpdateViewerLight(playerViewPos);
|
||||
Lighting.Tick(camPos);
|
||||
|
||||
// Fix B (A7 #3): build this frame's point-light snapshot and hand it to
|
||||
// the entity dispatcher for per-OBJECT light selection
|
||||
// (minimize_object_lighting). Replaces the single global nearest-8-to-
|
||||
// camera UBO set for point/spot lights so a wall's torches stay tied to
|
||||
// the wall as the camera moves. The SUN + ambient still flow through the
|
||||
// SceneLighting UBO built below (binding=1) — terrain/sky read those.
|
||||
// #176 root cause: the pool is anchored at the PLAYER (retail
|
||||
// Render::insert_light sorts by Render::player_pos, 0x0054d1b0) and
|
||||
// collected from ALL registered (=resident-cell) lights — it is a
|
||||
// function of player position only, so camera rotation cannot change
|
||||
// it. playerViewPos carries retail's no-player fallback (camPos).
|
||||
// A7.L1: candidacy is additionally scoped to last frame's rendered
|
||||
// visible-cell set (see _lightPoolVisibleCells) when available — the
|
||||
// Town Network starvation fix. Unscoped (null) until the first indoor
|
||||
// frame completes or after any outdoor-only frame.
|
||||
Lighting.BuildPointLightSnapshot(
|
||||
playerViewPos,
|
||||
_lightPoolVisibleCellsValid ? _lightPoolVisibleCells : null);
|
||||
_wbDrawDispatcher?.SetSceneLights(Lighting.PointSnapshot);
|
||||
_envCellRenderer?.SetPointSnapshot(Lighting.PointSnapshot); // A7 Fix D (D-2)
|
||||
|
||||
var ubo = AcDream.Core.Lighting.SceneLightingUbo.Build(
|
||||
Lighting, in atmo, camPos, (float)WorldTime.DayFraction);
|
||||
|
||||
// A.5 T22: override fog ramp with N₁/N₂-derived distances so the
|
||||
// horizon fog masks the N₁ scenery boundary. Sky keyframe fog is
|
||||
// retail-accurate at normal view distances but far too short for
|
||||
// the extended N₂=12 (25×25 LB) streaming window.
|
||||
// FogStart = N₁ × 192m × 0.7 ≈ 538m at defaults (4/12).
|
||||
// FogEnd = N₂ × 192m × 0.95 ≈ 2188m at defaults.
|
||||
// Multipliers exposed as env vars for fast iteration at visual gate.
|
||||
{
|
||||
const float LandblockSize = 192.0f;
|
||||
float startMult = ParseEnvFloat("ACDREAM_FOG_START_MULT", 0.7f);
|
||||
float endMult = ParseEnvFloat("ACDREAM_FOG_END_MULT", 0.95f);
|
||||
float fogStart = _nearRadius * LandblockSize * startMult;
|
||||
float fogEnd = _farRadius * LandblockSize * endMult;
|
||||
// Preserve fog color (xyz), lightning flash (z), and mode (w).
|
||||
ubo.FogParams = new System.Numerics.Vector4(
|
||||
fogStart,
|
||||
fogEnd,
|
||||
ubo.FogParams.Z, // lightning flash — unchanged
|
||||
ubo.FogParams.W); // fog mode — unchanged
|
||||
}
|
||||
|
||||
_sceneLightingUbo?.Upload(ubo);
|
||||
|
||||
// #133 A7 (2026-06-13): objective dungeon-lighting probe. One
|
||||
// rate-limited [light] line — insideCell / ambient / sun /
|
||||
// registered-point-lights / active-slot-count / player cell — so
|
||||
// the dungeon-dim question is self-verifiable from launch.log
|
||||
// without a screenshot. RegisteredCount is point/spot lights only
|
||||
// (the sun lives in LightManager.Sun, never in the _all list);
|
||||
// ubo.CellAmbient.W is the shader active-slot count, which counts
|
||||
// the (zeroed) sun slot indoors. Inert unless ACDREAM_PROBE_LIGHT=1.
|
||||
AcDream.Core.Rendering.RenderingDiagnostics.EmitLight(
|
||||
insideCell: 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: playerRoot?.CellId ?? 0u,
|
||||
lights: Lighting);
|
||||
AcDream.App.Rendering.WorldRenderFrame worldFrame =
|
||||
_worldRenderFrameBuilder!.Build(
|
||||
in foundation,
|
||||
IsLiveModeWaitingForLogin,
|
||||
_activeDayGroup);
|
||||
AcDream.App.Rendering.WorldCameraFrame cameraFrame = worldFrame.Camera;
|
||||
AcDream.App.Rendering.WorldRootFrame rootFrame = worldFrame.Roots;
|
||||
var camera = cameraFrame.Camera;
|
||||
var worldProjection = cameraFrame.Projection;
|
||||
var frustum = cameraFrame.Frustum;
|
||||
var camPos = cameraFrame.Position;
|
||||
// The frame builder resolves retail's player-lighting root and
|
||||
// collided-viewer render root before any draw decision is made.
|
||||
LoadedCell? playerRoot = rootFrame.PlayerRoot;
|
||||
bool playerSeenOutside = rootFrame.PlayerSeenOutside;
|
||||
uint viewerCellId = rootFrame.ViewerCellId;
|
||||
var viewerEyePos = rootFrame.ViewerEyePosition;
|
||||
var playerViewPos = rootFrame.PlayerViewPosition;
|
||||
LoadedCell? viewerRoot = rootFrame.ViewerRoot;
|
||||
bool cameraInsideCell = rootFrame.CameraInsideCell;
|
||||
|
||||
// Never cull the landblock the player is currently on.
|
||||
uint? playerLb = null;
|
||||
if (_playerMode && _playerController is not null)
|
||||
{
|
||||
var pp = _playerController.Position;
|
||||
int plx = _liveCenterX + (int)System.Math.Floor(pp.X / 192f);
|
||||
int ply = _liveCenterY + (int)System.Math.Floor(pp.Y / 192f);
|
||||
playerLb = (uint)((plx << 24) | (ply << 16) | 0xFFFF);
|
||||
}
|
||||
uint? playerLb = rootFrame.PlayerLandblockId;
|
||||
int renderCenterLbX = rootFrame.RenderCenterLandblockX;
|
||||
int renderCenterLbY = rootFrame.RenderCenterLandblockY;
|
||||
|
||||
int renderCenterLbX = _liveCenterX + (int)System.Math.Floor(camPos.X / 192f);
|
||||
int renderCenterLbY = _liveCenterY + (int)System.Math.Floor(camPos.Y / 192f);
|
||||
|
||||
// Phase A8: update EnvCellRenderer's frustum. The per-frame shell snapshot
|
||||
// is prepared after the portal-visible cell filter is known.
|
||||
var envCellViewProj = camera.View * worldProjection;
|
||||
_envCellFrustum?.Update(envCellViewProj);
|
||||
|
||||
// MP-Alloc: reuse _animatedIdsScratch instead of `new`ing a
|
||||
// HashSet<uint> every frame. Downstream consumers (WbDrawDispatcher.
|
||||
// WalkEntitiesInto) treat null and an empty set identically, so an
|
||||
// always-non-null (possibly empty) set is behaviorally the same as
|
||||
// the old null-when-nothing-animated local.
|
||||
//
|
||||
// Every entity in _animatedEntities (i.e. every entity with a
|
||||
// Sequencer) is added UNCONDITIONALLY: membership here is the
|
||||
// "re-classify me every frame, don't use the Tier-1 static cache"
|
||||
// flag, and the Tier-1 cache captures an entity's REST pose +
|
||||
// opacity-1.0 exactly once. Any entity whose rendered state can
|
||||
// depart that rest — a door held at its final OPEN frame, a wall
|
||||
// held FADED-OUT by a TransparentPartHook — MUST stay on the
|
||||
// per-frame path so the live sequencer pose + TranslucencyFadeManager
|
||||
// opacity are read; otherwise it flips back to the stale cached
|
||||
// closed/opaque state the instant its animation settles.
|
||||
//
|
||||
// DO NOT re-narrow this to "only if the current cycle is multi-frame"
|
||||
// (the reverted IsEntityCurrentlyMoving gate, 2026-07-09): a settled
|
||||
// one-shot hold IS pose-stable frame-to-frame, but stable at the
|
||||
// HELD pose the cache does NOT contain — that is the door/fade
|
||||
// flip-back. The per-frame re-classification cost that narrowing
|
||||
// saved was a DEBUG-build artifact; Release is GPU-bound (the CPU
|
||||
// work hides under GPU time), so the unconditional add is free where
|
||||
// it matters. If a real Release CPU cost from static-prop
|
||||
// re-classification is ever measured, gate on "entity is at its
|
||||
// captured rest state" (default motion AND no active fade), never on
|
||||
// "is mid-cycle".
|
||||
_animatedEntities.CopySpatialIdsTo(_animatedIdsScratch);
|
||||
_staticAnimationScheduler?.CopyAnimatedEntityIdsTo(_animatedIdsScratch);
|
||||
if (_equippedChildRenderer is not null)
|
||||
{
|
||||
foreach (uint id in _equippedChildRenderer.AttachedEntityIds)
|
||||
_animatedIdsScratch.Add(id);
|
||||
}
|
||||
HashSet<uint>? animatedIds = _animatedIdsScratch;
|
||||
var envCellViewProj = cameraFrame.ViewProjection;
|
||||
HashSet<uint> animatedIds = worldFrame.AnimatedEntityIds;
|
||||
|
||||
// Phase G.1: sky renderer — draws the far-plane-infinity
|
||||
// celestial meshes FIRST so the rest of the scene z-tests
|
||||
|
|
@ -3814,7 +3463,7 @@ public sealed class GameWindow : IDisposable
|
|||
// Building interior (cameraInsideCell=true, rootSeenOutside=true): render sky — clipped
|
||||
// to the doorway via the OutsideView (Stage 4, below).
|
||||
// Sealed dungeon (cameraInsideCell=true, rootSeenOutside=false): no sky.
|
||||
bool renderSky = viewerRoot is null || rootSeenOutside;
|
||||
bool renderSky = rootFrame.RenderSky;
|
||||
// Phase W Stage 4 (2026-06-02): the sky/weather DRAW moved DOWN to its retail LScape
|
||||
// position — AFTER the portal-visibility ClipFrame is assembled — so it can be clipped to
|
||||
// the doorway (OutsideView) by sky.vert's gl_ClipDistance. See the "[Stage 4] sky
|
||||
|
|
@ -3841,72 +3490,20 @@ public sealed class GameWindow : IDisposable
|
|||
// GPU-fenced frame slot; each renderer re-binds binding=2 defensively.
|
||||
_clipFrame ??= ClipFrame.NoClip();
|
||||
|
||||
// Phase 3 (render unification, additive): build the synthetic outdoor cell node when
|
||||
// the eye is outdoors (no interior viewerRoot). Stored in _outdoorNode but NOT yet
|
||||
// rooted — behaviour is unchanged this commit. The nearby-building enumeration mirrors
|
||||
// the look-in candidate gather in the OUTDOOR branch below (Chebyshev <=1 landblocks);
|
||||
// OutdoorCellNode.Build filters to exit portals internally. The clipRoot flip +
|
||||
// OutsideView terrain integration that consumes this is the next (cutover) step.
|
||||
_outdoorNode = null;
|
||||
_outdoorNodeBuildingCells.Clear();
|
||||
if (viewerRoot is not null || viewerCellId != 0u)
|
||||
{
|
||||
// T2 (BR-4): draw-driven flood gating. Retail floods a building's
|
||||
// interior exactly when its shell DRAWS and an aperture survives
|
||||
// the view (DrawBuilding Ghidra 0x0059f2a0: per-view viewconeCheck
|
||||
// → portal-BSP walk → ConstructView's GetClip; NO distance
|
||||
// constant anywhere on the chain). Port: a per-BUILDING frustum
|
||||
// pre-gate on the aperture bounds (Building.PortalBounds — the
|
||||
// tight equivalent of the shell viewconeCheck for FLOOD purposes),
|
||||
// replacing the old Chebyshev≤1 landblock cell-sweep; the 48 m
|
||||
// seed cap dies with it (RetailPViewRenderer seeds at ∞). The
|
||||
// per-portal admission stays BuildFromExterior's screen clip
|
||||
// (empty clip = no seed) — retail's GetClip-vs-view gate.
|
||||
// Per-building iteration is also the FPS fix the 2026-06-07
|
||||
// Chebyshev hack approximated: dozens of AABB tests instead of an
|
||||
// O(all loaded cells) portal sweep.
|
||||
// #124: the gather now runs for INTERIOR roots too — retail's
|
||||
// look-in executes inside LScape::draw for ANY root with a
|
||||
// non-empty outside view (DrawCells pc:432719). The renderer
|
||||
// routes interior-root look-ins to its landscape-stage sub-pass
|
||||
// (DrawBuildingLookIns); the root's own building self-excludes
|
||||
// via the seed eye-side test.
|
||||
foreach (var registry in _landblockPresentationPipeline?.BuildingRegistries
|
||||
?? Array.Empty<AcDream.App.Rendering.Wb.BuildingRegistry>())
|
||||
{
|
||||
foreach (var b in registry.All())
|
||||
{
|
||||
if (b.HasPortalBounds
|
||||
&& !AcDream.App.Rendering.FrustumCuller.IsAabbVisible(
|
||||
frustum, b.PortalBounds.Min, b.PortalBounds.Max))
|
||||
continue;
|
||||
|
||||
foreach (uint cid in b.EnvCellIds)
|
||||
if (_cellVisibility.TryGetCell(cid, out var bc) && bc is not null)
|
||||
_outdoorNodeBuildingCells.Add(bc);
|
||||
}
|
||||
}
|
||||
if (viewerRoot is null)
|
||||
_outdoorNode = AcDream.App.Rendering.OutdoorCellNode.Build(viewerCellId);
|
||||
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeFlapEnabled)
|
||||
Console.WriteLine(System.FormattableString.Invariant(
|
||||
$"[outdoor-node] cell=0x{viewerCellId:X8} root={(viewerRoot is null ? "OUT" : "IN")} nearbyCells={_outdoorNodeBuildingCells.Count} (T2 frustum-gated per-building floods)"));
|
||||
}
|
||||
|
||||
uint playerCellId = _physicsEngine.DataCache?.CellGraph.CurrCell?.Id ?? 0u;
|
||||
bool playerIndoorGate = AcDream.Core.Rendering.RenderingDiagnostics.ShouldRenderIndoor(
|
||||
playerCellId,
|
||||
playerRoot is not null);
|
||||
IReadOnlyList<LoadedCell> nearbyBuildingCells =
|
||||
worldFrame.Buildings.NearbyBuildingCells;
|
||||
bool playerIndoorGate = rootFrame.PlayerIndoorGate;
|
||||
uint playerCellId = rootFrame.PlayerCellId;
|
||||
// Render unification (outdoor-as-cell, 2026-06-07 cutover): ONE render path rooted at the
|
||||
// VIEWER cell. Eye indoors -> its interior EnvCell (viewerRoot); eye outdoors -> the
|
||||
// synthetic outdoor node (_outdoorNode, built above from nearby building entrances). The
|
||||
// frame builder's synthetic outdoor node, built from nearby building entrances. The
|
||||
// result is null ONLY when neither exists (pre-spawn / login / legacy non-chase camera) ->
|
||||
// the outdoor LScape block below still runs as the safety path (and login still shows the
|
||||
// live sky). There is no inside/outside branch to TOGGLE as the chase eye crosses the
|
||||
// doorway boundary, so the indoor FLAP dies by construction. playerIndoorGate stays
|
||||
// computed for the [render-sig] probe but no longer selects the path (handoff
|
||||
// docs/research/2026-06-07-render-unification-cutover-flip-handoff.md section 4 Step B).
|
||||
var clipRoot = viewerRoot ?? _outdoorNode;
|
||||
var clipRoot = worldFrame.ClipRoot;
|
||||
string renderBranch = clipRoot is null
|
||||
? "OutdoorRoot"
|
||||
: "RetailPViewInside";
|
||||
|
|
@ -4026,7 +3623,7 @@ public sealed class GameWindow : IDisposable
|
|||
// R-A2: outdoor root floods each nearby building per-building (not via the root).
|
||||
// #124: interior roots get the gather too — the renderer routes them to the
|
||||
// landscape-stage look-in sub-pass instead of the merge.
|
||||
NearbyBuildingCells = _outdoorNodeBuildingCells,
|
||||
NearbyBuildingCells = nearbyBuildingCells,
|
||||
ViewerEyePos = viewerEyePos,
|
||||
ViewProjection = envCellViewProj,
|
||||
CellLookup = id => _cellVisibility.TryGetCell(id, out var c) ? c : null,
|
||||
|
|
@ -4141,9 +3738,7 @@ public sealed class GameWindow : IDisposable
|
|||
// pool scoping. DrawableCells is the renderer's own reused scratch set
|
||||
// (cleared/rebuilt every DrawInside call) — copy it, don't hold a
|
||||
// reference to it.
|
||||
_lightPoolVisibleCells.Clear();
|
||||
_lightPoolVisibleCells.UnionWith(pviewResult.DrawableCells);
|
||||
_lightPoolVisibleCellsValid = true;
|
||||
_worldRenderFrameBuilder!.ObserveDrawableCells(pviewResult.DrawableCells);
|
||||
|
||||
// Flap root-cause apparatus (2026-06-07): per-frame, the EXACT Build inputs at 6 dp +
|
||||
// the resulting flood count. The live flap shows flood flipping 2↔6 at an eye/player
|
||||
|
|
@ -4212,7 +3807,7 @@ public sealed class GameWindow : IDisposable
|
|||
|
||||
// A7.L1: no indoor draw this frame — fail open (unscoped) rather than
|
||||
// scoping next frame's light pool by a stale dungeon's cell ids.
|
||||
_lightPoolVisibleCellsValid = false;
|
||||
_worldRenderFrameBuilder!.ClearDrawableCells();
|
||||
}
|
||||
|
||||
// Phase U.3: close the world-geometry clip bracket opened above. From here down the
|
||||
|
|
@ -4993,67 +4588,6 @@ public sealed class GameWindow : IDisposable
|
|||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Derive the current sun (directional light, slot 0 of the UBO)
|
||||
/// from the interpolated <see cref="AcDream.Core.World.SkyKeyframe"/>,
|
||||
/// plus the cell ambient. Indoor cells force the sun intensity to
|
||||
/// zero and substitute a flat 0.2 white ambient — exact retail
|
||||
/// behavior per <c>CellManager::ChangePosition</c> @ 0x004559B0,
|
||||
/// which calls <c>SmartBox::SetWorldAmbientLight(0.2f, 0xFFFFFFFF)</c>
|
||||
/// when the player's <c>CObjCell::seen_outside</c> flag is 0.
|
||||
/// Indoor brightness then comes from per-cell point lights
|
||||
/// (Setup.Lights on the cell's static objects, registered through
|
||||
/// <see cref="AcDream.Core.Lighting.LightingHookSink"/>).
|
||||
/// The trigger is the PLAYER's cell, not the camera's — third-person
|
||||
/// chase camera enters interiors before the player body does, and
|
||||
/// retail keys lighting off the player position.
|
||||
/// </summary>
|
||||
private void UpdateSunFromSky(AcDream.Core.World.SkyKeyframe kf, bool playerInsideCell)
|
||||
{
|
||||
// Sun direction: points FROM the sun TOWARDS the world. Our
|
||||
// shader does dot(N, -forward) so a positive N·L means the
|
||||
// surface faces the sun.
|
||||
var sunToWorld = -AcDream.Core.World.SkyStateProvider.SunDirectionFromKeyframe(kf);
|
||||
|
||||
if (playerInsideCell)
|
||||
{
|
||||
// Indoor default — retail's flat 0.2 neutral ambient, sun
|
||||
// zeroed. See xref to retail decomp in the doc comment above.
|
||||
Lighting.Sun = new AcDream.Core.Lighting.LightSource
|
||||
{
|
||||
Kind = AcDream.Core.Lighting.LightKind.Directional,
|
||||
WorldForward = sunToWorld,
|
||||
ColorLinear = System.Numerics.Vector3.Zero,
|
||||
Intensity = 0f,
|
||||
Range = 1f,
|
||||
};
|
||||
Lighting.CurrentAmbient = new AcDream.Core.Lighting.CellAmbientState(
|
||||
AmbientColor: new System.Numerics.Vector3(0.20f, 0.20f, 0.20f),
|
||||
SunColor: System.Numerics.Vector3.Zero,
|
||||
SunDirection: sunToWorld);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Outdoor: full keyframe sun + ambient. The SkyKeyframe stores
|
||||
// raw DirColor + DirBright (and AmbColor + AmbBright) for
|
||||
// retail-faithful per-channel keyframe interpolation; the
|
||||
// computed `kf.SunColor` / `kf.AmbientColor` properties return
|
||||
// the post-multiplied product the shader expects.
|
||||
Lighting.Sun = new AcDream.Core.Lighting.LightSource
|
||||
{
|
||||
Kind = AcDream.Core.Lighting.LightKind.Directional,
|
||||
WorldForward = sunToWorld,
|
||||
ColorLinear = kf.SunColor,
|
||||
Intensity = 1f,
|
||||
Range = 1f,
|
||||
};
|
||||
Lighting.CurrentAmbient = new AcDream.Core.Lighting.CellAmbientState(
|
||||
AmbientColor: kf.AmbientColor,
|
||||
SunColor: kf.SunColor,
|
||||
SunDirection: sunToWorld);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Phase I.2 — DebugPanel helpers ────────────────────────────────
|
||||
//
|
||||
// The ImGui DebugPanel reads through DebugVM closures that ask
|
||||
|
|
@ -5934,17 +5468,6 @@ public sealed class GameWindow : IDisposable
|
|||
$"resident={_worldState.Entities.Count} walked={walked}");
|
||||
}
|
||||
|
||||
/// <summary>A.5 T22: parse a float environment variable, returning
|
||||
/// <paramref name="defaultValue"/> when the variable is absent or unparseable.</summary>
|
||||
private static float ParseEnvFloat(string name, float defaultValue)
|
||||
{
|
||||
var s = System.Environment.GetEnvironmentVariable(name);
|
||||
if (s is not null && float.TryParse(s, System.Globalization.NumberStyles.Float,
|
||||
System.Globalization.CultureInfo.InvariantCulture, out var v))
|
||||
return v;
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
private void OnClosing()
|
||||
=> CompleteShutdown();
|
||||
|
||||
|
|
@ -5989,6 +5512,17 @@ public sealed class GameWindow : IDisposable
|
|||
_liveSessionController = null;
|
||||
}),
|
||||
]),
|
||||
// Frame composition borrows equipped, effect, audio, and render
|
||||
// owners. Withdraw the graph before the first borrowed owner closes;
|
||||
// the later render-frontend stage still disposes the GL owners.
|
||||
new ResourceShutdownStage("frame borrowers",
|
||||
[
|
||||
new("world frame composition", () =>
|
||||
{
|
||||
_worldRenderFrameBuilder = null;
|
||||
_skyPesFrame = null;
|
||||
}),
|
||||
]),
|
||||
new ResourceShutdownStage("session dependents",
|
||||
[
|
||||
new("mouse capture", () => _gameplayInputFrame?.EndMouseLook()),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue