feat(render) Campaign FW3.3: ShellDrawLiftZ is RETIRED - cells draw at the dat origin
Retail draws cell geometry at the dat EnvCell origin verbatim; the 0.02 m lift was our z-fight stand-in (register row AP-32, deleted in this commit). With the walk owning retail draw ORDER under WorldDepthContract Less (first-drawn-wins: DrawBlock terrain-then- objects per cell, DrawCells shells-then-contents), the coplanar tie-breaks the lift compensated for are now resolved the way retail resolves them. Deleted at every site: the PortalVisibilityBuilder const + the drawLiftZ Build parameter and its lifted exit-portal projection branch (gate and drawn geometry now share ONE space); the seal/punch fan lifts (DrawPortalDepthWrite + the walk's DrawWalkPunchFan); the LandblockBuildFactory drawn-cell-transform lift (render and physics share the one verbatim transform). The #130 proof flipped exactly as its own doc predicted: UnliftedGate_LeavesTheStripAtTheDrawnTopEdge is deleted (its premise - gate space != drawn space - no longer exists), and the renamed ExitDoorTopEdge_GateCoversTheDrawnApertureWithinPixelTolerance sweep (147 eye/gaze combos at the Holtburg corner door) passes with both in the same unlifted space (worst plane gap <= 1.2 px, scissor <= 0.15 px - unchanged tolerances). Ten more replay-test call sites swept to the new Build signature. Suites: full Release build 0 warnings; hermetic 6,750/0; the 21 affected InstalledDat replay tests green; Walk conformance 40/1 untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
1955a0d5f1
commit
05240d2cab
12 changed files with 55 additions and 196 deletions
|
|
@ -325,7 +325,6 @@ research and is no longer active.
|
||||||
| AP-27 | PlayerDescription trailer: GameplayOptions skipped by a 4-byte-aligned heuristic scan for a valid inventory parse; options blob captured opaque, never decoded (retail decodes + applies UI options) | `src/AcDream.Core.Net/Messages/PlayerDescriptionParser.cs:69` | Variable-length opaque blobs; mirrors holtburger's heuristics; follow-up issue extends when panels consume those sections | An options blob that coincidentally parses as a valid inventory (or inventory not landing at EOF) yields wrong/empty inventory+equipped at login; retail-persisted UI options silently ignored | ACE GameEventPlayerDescription.WriteEventBody; holtburger events.rs:195-218 |
|
| AP-27 | PlayerDescription trailer: GameplayOptions skipped by a 4-byte-aligned heuristic scan for a valid inventory parse; options blob captured opaque, never decoded (retail decodes + applies UI options) | `src/AcDream.Core.Net/Messages/PlayerDescriptionParser.cs:69` | Variable-length opaque blobs; mirrors holtburger's heuristics; follow-up issue extends when panels consume those sections | An options blob that coincidentally parses as a valid inventory (or inventory not landing at EOF) yields wrong/empty inventory+equipped at login; retail-persisted UI options silently ignored | ACE GameEventPlayerDescription.WriteEventBody; holtburger events.rs:195-218 |
|
||||||
| ~~AP-28~~ | **RETIRED 2026-08-08 (Campaign A slice A2).** The three picked AL parameters and the gain-driven eviction are both gone. `RetailSoundMixer` now carries the byte-decoded retail curve — `g = dist < 5 ? vol : 25·vol/dist²`, clamped to 1, ONE master multiply, `db = ceil(20·log10 g)`, and a hard −50 dB no-allocate floor (audible radius ≈94.2 m at unity) — with pan as retail's `−15·sin(Δbearing)` in whole decibels and a 5-metre integer deadzone. Every AL source is source-relative with `AL_ROLLOFF_FACTOR = 0` and the global distance model is `None`, so AL contributes no attenuation of its own; the old `InverseDistanceClamped` ref-2 m curve was inverse FIRST power (`2/d`), quieter than retail up close and far louder at range with no cutoff at all. Voice eviction now compares the DAT-authored float priority strictly-less in ring order per `SoundManager::PlaySoundInternal` @ `0x0054FEC0` (the row's old `FUN_00550ad0` citation was wrong — that address is inside an `IntrusiveHashTable` constructor). The residual pan-LAW approximation is AP-173; retail's own `s_bPlaySoundOnlyWhenActive` gate is TS-64. | retired | — | — | `SoundManager::GetAttenuation @ 0x00550020`; `SoundManager::PlaySoundInternal @ 0x00550170` and `@ 0x0054FEC0`; `docs/research/2026-08-08-audio-retail-soundmanager-core.md` |
|
| ~~AP-28~~ | **RETIRED 2026-08-08 (Campaign A slice A2).** The three picked AL parameters and the gain-driven eviction are both gone. `RetailSoundMixer` now carries the byte-decoded retail curve — `g = dist < 5 ? vol : 25·vol/dist²`, clamped to 1, ONE master multiply, `db = ceil(20·log10 g)`, and a hard −50 dB no-allocate floor (audible radius ≈94.2 m at unity) — with pan as retail's `−15·sin(Δbearing)` in whole decibels and a 5-metre integer deadzone. Every AL source is source-relative with `AL_ROLLOFF_FACTOR = 0` and the global distance model is `None`, so AL contributes no attenuation of its own; the old `InverseDistanceClamped` ref-2 m curve was inverse FIRST power (`2/d`), quieter than retail up close and far louder at range with no cutoff at all. Voice eviction now compares the DAT-authored float priority strictly-less in ring order per `SoundManager::PlaySoundInternal` @ `0x0054FEC0` (the row's old `FUN_00550ad0` citation was wrong — that address is inside an `IntrusiveHashTable` constructor). The residual pan-LAW approximation is AP-173; retail's own `s_bPlaySoundOnlyWhenActive` gate is TS-64. | retired | — | — | `SoundManager::GetAttenuation @ 0x00550020`; `SoundManager::PlaySoundInternal @ 0x00550170` and `@ 0x0054FEC0`; `docs/research/2026-08-08-audio-retail-soundmanager-core.md` |
|
||||||
| AP-31 | Scenery placement drift + the 0xA9B1 road-edge tree — WB-upstream divergences from retail, ACCEPTED (**#49/#50**, 2026-05-11) | `src/AcDream.Core/World/SceneryGenerator.cs` (via `WbSceneryAdapter`) | Piecemeal patching against WB upstream is net-negative (the `e279c46` road-check attempt over-suppressed scenery elsewhere, reverted `677a726`); visible impact = a handful of trees a few meters off | The same WB-upstream class could hide a *larger* placement divergence elsewhere; revisit only via a coherent ACME-style per-vertex filter port | `CLandBlock::get_land_scenes`; ACME GameScene.cs:1074 per-vertex road filter |
|
| AP-31 | Scenery placement drift + the 0xA9B1 road-edge tree — WB-upstream divergences from retail, ACCEPTED (**#49/#50**, 2026-05-11) | `src/AcDream.Core/World/SceneryGenerator.cs` (via `WbSceneryAdapter`) | Piecemeal patching against WB upstream is net-negative (the `e279c46` road-check attempt over-suppressed scenery elsewhere, reverted `677a726`); visible impact = a handful of trees a few meters off | The same WB-upstream class could hide a *larger* placement divergence elsewhere; revisit only via a coherent ACME-style per-vertex filter port | `CLandBlock::get_land_scenes`; ACME GameScene.cs:1074 per-vertex road filter |
|
||||||
| AP-32 | Cell shells DRAW +0.02 m above the dat EnvCell origin (`ShellDrawLiftZ`, z-fight vs coplanar terrain); retail draws at the origin verbatim. Split invariant: PHYSICS + visibility graph UNLIFTED (f35cb8b, **#119**-residual), every DRAW-space consumer of portal/cell geometry LIFTED (OutsideView color gate via `Build(drawLiftZ)`, seal/punch fans — **#130**) | `src/AcDream.App/Rendering/PortalVisibilityBuilder.cs` (`ShellDrawLiftZ`); `src/AcDream.App/Rendering/RetailPViewPassExecutor.cs` (`DrawPortalDepthWrite`) | Shell floors coplanar with terrain z-fight in our z-buffered frame; the 2 cm lift is the documented stand-in | A new draw-space consumer of portal/cell polygons that forgets the lift re-opens a 2 cm seam at horizontal aperture edges (the #130 top-edge strip, ~7 px at 2.4 m); a visibility consumer that picks up the LIFTED transform re-opens the #119-residual horizontal-portal side-cull | retail draws cell geometry at the dat EnvCell origin (no lift) |
|
|
||||||
| AP-33 | Interior-root look-in cells (**#124** sub-pass) draw their statics + DYNAMICS + emitters WHOLE — no per-part/per-object viewcone check; retail viewconeCheck's each vs the installed view (the **#131** portal closure: a server object in a look-in cell drew nowhere — dynamics-last culls cells absent from the main cone, and post-seal it z-fails anyway) | `src/AcDream.App/Rendering/RetailPViewRenderer.cs` (`DrawBuildingLookIns`) | The main viewcone has no entries for look-in cells; over-include is the safe direction (z-correct, repainted outside apertures by the root's shells); look-in cell counts are small (~1-3 cells) | A few wasted draws on content outside the doorway region (repainted); no under-draw direction remains | `viewconeCheck` 0x0054c250; nested `DrawCells` objects pc:432878 |
|
| AP-33 | Interior-root look-in cells (**#124** sub-pass) draw their statics + DYNAMICS + emitters WHOLE — no per-part/per-object viewcone check; retail viewconeCheck's each vs the installed view (the **#131** portal closure: a server object in a look-in cell drew nowhere — dynamics-last culls cells absent from the main cone, and post-seal it z-fails anyway) | `src/AcDream.App/Rendering/RetailPViewRenderer.cs` (`DrawBuildingLookIns`) | The main viewcone has no entries for look-in cells; over-include is the safe direction (z-correct, repainted outside apertures by the root's shells); look-in cell counts are small (~1-3 cells) | A few wasted draws on content outside the doorway region (repainted); no under-draw direction remains | `viewconeCheck` 0x0054c250; nested `DrawCells` objects pc:432878 |
|
||||||
| AP-34 | The world now shares one delayed alpha queue across Wb GfxObj/Setup entities and scene particles and drains it at retail's landscape/final boundaries. Residual: the modern reconstruction uses one stable scope-global CYpt sort rather than retail's per-`CPartCell` `CShadowPart` sort followed by cell traversal; `EnvCellRenderer` transparent shell batches also remain immediate and outside this queue | `src/AcDream.App/Rendering/RetailAlphaQueue.cs`; `RetailPViewPassExecutor.cs` (`FlushLandscapeAlpha`); `Rendering/Wb/WbDrawDispatcher.cs`; `ParticleRenderer.cs` | The mandatory modern renderer no longer owns retail `CPartCell` shadow lists. The shared queue restores the material consequence that motivated the port—particles and ordinary translucent parts can interleave—without rebuilding a second scene graph; stable sequence retains authored order on equal CYpt | Transparent objects from different cells can exchange order at a narrow overlap compared with retail cell traversal; an alpha-blended EnvCell shell cannot interleave with a particle or Wb entity, so those rare overlaps can still overpaint differently | `RenderDeviceD3D::DrawObjCellForDummies` 0x005A0760; `CShadowPart::insertion_sort` 0x006B5130; `D3DPolyRender::FlushAlphaList` 0x0059D2E0; `PView::DrawCells` 0x005A4840 |
|
| AP-34 | The world now shares one delayed alpha queue across Wb GfxObj/Setup entities and scene particles and drains it at retail's landscape/final boundaries. Residual: the modern reconstruction uses one stable scope-global CYpt sort rather than retail's per-`CPartCell` `CShadowPart` sort followed by cell traversal; `EnvCellRenderer` transparent shell batches also remain immediate and outside this queue | `src/AcDream.App/Rendering/RetailAlphaQueue.cs`; `RetailPViewPassExecutor.cs` (`FlushLandscapeAlpha`); `Rendering/Wb/WbDrawDispatcher.cs`; `ParticleRenderer.cs` | The mandatory modern renderer no longer owns retail `CPartCell` shadow lists. The shared queue restores the material consequence that motivated the port—particles and ordinary translucent parts can interleave—without rebuilding a second scene graph; stable sequence retains authored order on equal CYpt | Transparent objects from different cells can exchange order at a narrow overlap compared with retail cell traversal; an alpha-blended EnvCell shell cannot interleave with a particle or Wb entity, so those rare overlaps can still overpaint differently | `RenderDeviceD3D::DrawObjCellForDummies` 0x005A0760; `CShadowPart::insertion_sort` 0x006B5130; `D3DPolyRender::FlushAlphaList` 0x0059D2E0; `PView::DrawCells` 0x005A4840 |
|
||||||
| AP-36 | Dungeon streaming gate triggers on the player's CURRENT cell being a sealed EnvCell (`CurrCell.IsEnv && !SeenOutside`), an approximation of ACE's full landblock `IsDungeon` (all-heights-zero + NumCells>0 + Buildings.Count==0). The retail BEHAVIOR (a dungeon loads no adjacent landblocks) is faithful — only the runtime TRIGGER is the cheap cell predicate instead of classifying the center landblock. **#135 pre-collapse:** at login/teleport the same collapse is triggered EARLY (the instant the streaming center is recentered onto the spawn/dest cell) via `IsSealedDungeonCell` reading the EnvCell **dat** `SeenOutside` flag — because the physics `CurrCell` is null until placement, which waits for hydration; without the early trigger the full 25×25 ocean-grid window loads then unloads (the ~30 s login FPS ramp). **#215 cell identity:** the pre-collapse/recenter decision compares the player's current `Position.objcell_id` landblock with the received destination `objcell_id`; it never reconstructs the source from XYZ because dungeon frame origins may be negative. **#145/#138 teleport-hold suppression:** during a teleport arrival HOLD the player is unplaced, so `CurrCell` is the frozen SOURCE cell, not the destination; the gate is suppressed for the hold (`DungeonStreamingGate.Compute(isTeleportHold:true)` → not-inside-dungeon) so a teleport OUT of a dungeon follows the destination (the PortalSpace observer pin) and `ExitDungeonExpand`s, instead of re-pinning streaming onto the source dungeon (which left the outdoor destination un-hydrated → 600-frame readiness timeout → force-snap to ocean — the #145 "second teleport does nothing" + #138 incomplete-world) | `src/AcDream.App/Streaming/TeleportLandblockTransition.cs` (source/destination cell-ID classification) + `src/AcDream.App/Streaming/DungeonStreamingGate.cs` (`Compute` — per-frame predicate + teleport-hold suppression) + `src/AcDream.App/World/LiveEntityHydrationPorts.cs` (`LiveEntityWorldOriginCoordinator.TryInitialize` — login pre-collapse) + `src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs` (`OnPosition` — first accepted canonical Position) + `GameWindow:AimTeleportDestination`/`IsSealedDungeonCell` (teleport pre-collapse and DAT predicate) + `src/AcDream.App/Streaming/StreamingController.cs` (collapse/expand/`PreCollapseToDungeon`) | The predicate is already computed for sun/sky gating (playerInsideCell) and exactly matches for sealed dungeons vs windowed building interiors (SeenOutside=true → not gated); no landblock re-classification needed. The dat-flag read is the same `EnvCellFlags.SeenOutside` the hydrated `ObjCell.SeenOutside` is built from (`EnvCell.cs:72`/`PhysicsDataCache.cs:224`), so the pre-collapse decision matches the eventual per-frame gate exactly. The cell-ID comparison matches retail's complete `Position` flow. | A dungeon cell that reports SeenOutside (an entrance cell open to the surface) briefly un-collapses and re-streams the window; a hypothetical windowless building back-room (IsEnv && !SeenOutside but HasBuildings) would wrongly collapse its outdoor neighbors; a sealed-dungeon entrance cell that is itself SeenOutside is simply MISSED by the early trigger and falls back to the existing late collapse (no worse than before #135) | ACE `LandblockManager.GetAdjacentIDs` (dungeons→empty) Landblock.cs:577-582; `IsDungeon` Landblock.cs:1264-1277; retail `SmartBox::TeleportPlayer` 0x00453910 |
|
| AP-36 | Dungeon streaming gate triggers on the player's CURRENT cell being a sealed EnvCell (`CurrCell.IsEnv && !SeenOutside`), an approximation of ACE's full landblock `IsDungeon` (all-heights-zero + NumCells>0 + Buildings.Count==0). The retail BEHAVIOR (a dungeon loads no adjacent landblocks) is faithful — only the runtime TRIGGER is the cheap cell predicate instead of classifying the center landblock. **#135 pre-collapse:** at login/teleport the same collapse is triggered EARLY (the instant the streaming center is recentered onto the spawn/dest cell) via `IsSealedDungeonCell` reading the EnvCell **dat** `SeenOutside` flag — because the physics `CurrCell` is null until placement, which waits for hydration; without the early trigger the full 25×25 ocean-grid window loads then unloads (the ~30 s login FPS ramp). **#215 cell identity:** the pre-collapse/recenter decision compares the player's current `Position.objcell_id` landblock with the received destination `objcell_id`; it never reconstructs the source from XYZ because dungeon frame origins may be negative. **#145/#138 teleport-hold suppression:** during a teleport arrival HOLD the player is unplaced, so `CurrCell` is the frozen SOURCE cell, not the destination; the gate is suppressed for the hold (`DungeonStreamingGate.Compute(isTeleportHold:true)` → not-inside-dungeon) so a teleport OUT of a dungeon follows the destination (the PortalSpace observer pin) and `ExitDungeonExpand`s, instead of re-pinning streaming onto the source dungeon (which left the outdoor destination un-hydrated → 600-frame readiness timeout → force-snap to ocean — the #145 "second teleport does nothing" + #138 incomplete-world) | `src/AcDream.App/Streaming/TeleportLandblockTransition.cs` (source/destination cell-ID classification) + `src/AcDream.App/Streaming/DungeonStreamingGate.cs` (`Compute` — per-frame predicate + teleport-hold suppression) + `src/AcDream.App/World/LiveEntityHydrationPorts.cs` (`LiveEntityWorldOriginCoordinator.TryInitialize` — login pre-collapse) + `src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs` (`OnPosition` — first accepted canonical Position) + `GameWindow:AimTeleportDestination`/`IsSealedDungeonCell` (teleport pre-collapse and DAT predicate) + `src/AcDream.App/Streaming/StreamingController.cs` (collapse/expand/`PreCollapseToDungeon`) | The predicate is already computed for sun/sky gating (playerInsideCell) and exactly matches for sealed dungeons vs windowed building interiors (SeenOutside=true → not gated); no landblock re-classification needed. The dat-flag read is the same `EnvCellFlags.SeenOutside` the hydrated `ObjCell.SeenOutside` is built from (`EnvCell.cs:72`/`PhysicsDataCache.cs:224`), so the pre-collapse decision matches the eventual per-frame gate exactly. The cell-ID comparison matches retail's complete `Position` flow. | A dungeon cell that reports SeenOutside (an entrance cell open to the surface) briefly un-collapses and re-streams the window; a hypothetical windowless building back-room (IsEnv && !SeenOutside but HasBuildings) would wrongly collapse its outdoor neighbors; a sealed-dungeon entrance cell that is itself SeenOutside is simply MISSED by the early trigger and falls back to the existing late collapse (no worse than before #135) | ACE `LandblockManager.GetAdjacentIDs` (dungeons→empty) Landblock.cs:577-582; `IsDungeon` Landblock.cs:1264-1277; retail `SmartBox::TeleportPlayer` 0x00453910 |
|
||||||
|
|
|
||||||
|
|
@ -342,31 +342,26 @@ public static class PortalVisibilityBuilder
|
||||||
Console.WriteLine($"[pv-ERROR] chain tail(24):{tail}");
|
Console.WriteLine($"[pv-ERROR] chain tail(24):{tail}");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>The +Z world lift applied to DRAWN cell shells (z-fighting vs
|
// Campaign FW3.3 (2026-08-30): ShellDrawLiftZ (the 0.02 m drawn-shell
|
||||||
/// terrain; applied in GameWindow's cell registration). The visibility
|
// lift, register row AP-32) is RETIRED. Retail draws cell geometry at
|
||||||
/// graph stays in PHYSICS (unlifted) space — feeding the lift into portal
|
// the dat EnvCell origin verbatim; the coplanar tie-breaks the lift
|
||||||
/// planes broke horizontal-portal side tests (#119-residual, f35cb8b).
|
// stood in for are now owned by the walk's retail draw ORDER under
|
||||||
/// Draw-space consumers of portal polygons (the OutsideView color gate
|
// WorldDepthContract's Less (first-drawn-wins — RenderDeviceD3D::
|
||||||
/// here, the seal/punch depth fans in GameWindow) must apply this lift so
|
// DrawBlock terrain-then-objects per cell, PView::DrawCells shells-then-
|
||||||
/// they meet the drawn shell's aperture edge — the unlifted gate left a
|
// contents). With the lift gone, the visibility gate and the drawn
|
||||||
/// 2 cm background strip under the drawn lintel (#130).</summary>
|
// geometry share ONE space, so the #130 strip mechanism (gate space ≠
|
||||||
public const float ShellDrawLiftZ = 0.02f;
|
// drawn space) no longer exists.
|
||||||
|
|
||||||
/// <param name="lookup">Resolve a full cell id to its LoadedCell, or null if not loaded.</param>
|
/// <param name="lookup">Resolve a full cell id to its LoadedCell, or null if not loaded.</param>
|
||||||
/// <param name="buildingMembership">Optional: true if a cell id is in the camera building's cell
|
/// <param name="buildingMembership">Optional: true if a cell id is in the camera building's cell
|
||||||
/// set. When provided, a neighbour OUTSIDE the set routes to CrossBuildingViews instead of
|
/// set. When provided, a neighbour OUTSIDE the set routes to CrossBuildingViews instead of
|
||||||
/// continuing the in-building BFS. Pass null to treat all reachable cells as in-building.</param>
|
/// continuing the in-building BFS. Pass null to treat all reachable cells as in-building.</param>
|
||||||
/// <param name="drawLiftZ">World +Z applied ONLY to the exit-portal projection feeding
|
|
||||||
/// <see cref="PortalVisibilityFrame.OutsideView"/> (a draw-space region; see
|
|
||||||
/// <see cref="ShellDrawLiftZ"/>). Flood admission, side tests, and CellViews are unaffected.
|
|
||||||
/// Production passes <see cref="ShellDrawLiftZ"/>; tests replaying visibility semantics pass 0.</param>
|
|
||||||
public static PortalVisibilityFrame Build(
|
public static PortalVisibilityFrame Build(
|
||||||
LoadedCell cameraCell,
|
LoadedCell cameraCell,
|
||||||
Vector3 cameraPos,
|
Vector3 cameraPos,
|
||||||
Func<uint, LoadedCell?> lookup,
|
Func<uint, LoadedCell?> lookup,
|
||||||
Matrix4x4 viewProj,
|
Matrix4x4 viewProj,
|
||||||
Func<uint, bool>? buildingMembership = null,
|
Func<uint, bool>? buildingMembership = null,
|
||||||
float drawLiftZ = 0f,
|
|
||||||
PortalVisibilityFrame? reuseFrame = null)
|
PortalVisibilityFrame? reuseFrame = null)
|
||||||
{
|
{
|
||||||
var frame = reuseFrame ?? new PortalVisibilityFrame();
|
var frame = reuseFrame ?? new PortalVisibilityFrame();
|
||||||
|
|
@ -544,38 +539,13 @@ public static class PortalVisibilityBuilder
|
||||||
|
|
||||||
if (portal.OtherCellId == 0xFFFF)
|
if (portal.OtherCellId == 0xFFFF)
|
||||||
{
|
{
|
||||||
// Exit portal -> outdoors visible through this (clipped) opening.
|
// Exit portal -> outdoors visible through this (clipped)
|
||||||
// OutsideView gates DRAWN color (terrain/sky/scissor), and the
|
// opening. FW3.3: shells draw at the dat origin (the
|
||||||
// shell that rasterizes this aperture draws +drawLiftZ above
|
// ShellDrawLiftZ retirement), so the gate and the drawn
|
||||||
// the physics transform — project the region in the SAME
|
// aperture share one space — the unlifted clippedRegion
|
||||||
// lifted space or terrain stops a lift-height short of the
|
// IS the draw-space region.
|
||||||
// drawn lintel (#130 strip). Flood semantics keep the
|
AddRegion(frame.OutsideView, clippedRegion);
|
||||||
// unlifted clippedRegion path above.
|
trace?.Add($"portal cell=0x{cell.CellId:X8} p{i}->EXIT addOutside={clippedRegion.Count} clipVerts={clipVerts}");
|
||||||
int outsideCount;
|
|
||||||
if (drawLiftZ == 0f)
|
|
||||||
{
|
|
||||||
AddRegion(frame.OutsideView, clippedRegion);
|
|
||||||
outsideCount = clippedRegion.Count;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
using PortalVisibilityFrame.ClipRegionScratchLease outsideRegionLease =
|
|
||||||
frame.RentClipRegionScratch();
|
|
||||||
List<ViewPolygon> outsideRegion = outsideRegionLease.Region;
|
|
||||||
ClipPortalAgainstView(
|
|
||||||
frame,
|
|
||||||
poly,
|
|
||||||
cell.WorldTransform * Matrix4x4.CreateTranslation(0f, 0f, drawLiftZ),
|
|
||||||
viewProj,
|
|
||||||
currentView.Polygons,
|
|
||||||
processedCount,
|
|
||||||
endCount - processedCount,
|
|
||||||
outsideRegion,
|
|
||||||
out _);
|
|
||||||
AddRegion(frame.OutsideView, outsideRegion);
|
|
||||||
outsideCount = outsideRegion.Count;
|
|
||||||
}
|
|
||||||
trace?.Add($"portal cell=0x{cell.CellId:X8} p{i}->EXIT addOutside={outsideCount} clipVerts={clipVerts}");
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -78,9 +78,8 @@ internal sealed partial class RetailPViewPassExecutor
|
||||||
/// <summary>The walk's punch-fan turn — <c>DrawPortalPolyInternal</c>
|
/// <summary>The walk's punch-fan turn — <c>DrawPortalPolyInternal</c>
|
||||||
/// @0x0059bc90's far-Z punch through <c>PortalDepthMaskRenderer</c>,
|
/// @0x0059bc90's far-Z punch through <c>PortalDepthMaskRenderer</c>,
|
||||||
/// clipped by the pinned view's slice planes (retail
|
/// clipped by the pinned view's slice planes (retail
|
||||||
/// <c>building_view</c> @0x0059f3bf). The +<c>ShellDrawLiftZ</c>
|
/// <c>building_view</c> @0x0059f3bf). FW3.3: fans draw at the dat
|
||||||
/// matches today's <c>DrawPortalDepthWrite</c> and retires with
|
/// aperture verbatim (the ShellDrawLiftZ retirement).</summary>
|
||||||
/// FW3.3.</summary>
|
|
||||||
internal void DrawWalkPunchFan(
|
internal void DrawWalkPunchFan(
|
||||||
RetailPViewFrameInput frame,
|
RetailPViewFrameInput frame,
|
||||||
ClipFrameAssembly clipAssembly,
|
ClipFrameAssembly clipAssembly,
|
||||||
|
|
@ -100,10 +99,7 @@ internal sealed partial class RetailPViewPassExecutor
|
||||||
Span<Vector3> world = stackalloc Vector3[32];
|
Span<Vector3> world = stackalloc Vector3[32];
|
||||||
int count = Math.Min(vertices.Length, world.Length);
|
int count = Math.Min(vertices.Length, world.Length);
|
||||||
for (int vertex = 0; vertex < count; vertex++)
|
for (int vertex = 0; vertex < count; vertex++)
|
||||||
{
|
|
||||||
world[vertex] = vertices[vertex];
|
world[vertex] = vertices[vertex];
|
||||||
world[vertex].Z += PortalVisibilityBuilder.ShellDrawLiftZ;
|
|
||||||
}
|
|
||||||
_portalDepthMask.DrawDepthFan(
|
_portalDepthMask.DrawDepthFan(
|
||||||
world[..count],
|
world[..count],
|
||||||
frame.ViewProjection,
|
frame.ViewProjection,
|
||||||
|
|
@ -134,9 +130,8 @@ internal sealed partial class RetailPViewPassExecutor
|
||||||
/// @0x0059f170 draws per cell at its flood turn).</item>
|
/// @0x0059f170 draws per cell at its flood turn).</item>
|
||||||
/// <item><see cref="DrawPunchFan"/> → <c>PortalDepthMaskRenderer.DrawDepthFan</c>
|
/// <item><see cref="DrawPunchFan"/> → <c>PortalDepthMaskRenderer.DrawDepthFan</c>
|
||||||
/// with <c>forceFarZ</c>, clipped by the pinned view's slice planes
|
/// with <c>forceFarZ</c>, clipped by the pinned view's slice planes
|
||||||
/// (retail <c>building_view</c> @0x0059f3bf). The +<c>ShellDrawLiftZ</c>
|
/// (retail <c>building_view</c> @0x0059f3bf); FW3.3 draws fans at the dat
|
||||||
/// on the fan verts matches today's <c>DrawPortalDepthWrite</c> and
|
/// aperture verbatim (the ShellDrawLiftZ retirement).</item>
|
||||||
/// retires with FW3.3.</item>
|
|
||||||
/// <item><see cref="ClearInteriorDepth"/>/<see cref="DrawExitSeals"/> →
|
/// <item><see cref="ClearInteriorDepth"/>/<see cref="DrawExitSeals"/> →
|
||||||
/// caller-supplied actions (the renderer owns the pass scope and the
|
/// caller-supplied actions (the renderer owns the pass scope and the
|
||||||
/// root-flood seal iteration; the adapter only provides the turns).</item>
|
/// root-flood seal iteration; the adapter only provides the turns).</item>
|
||||||
|
|
|
||||||
|
|
@ -711,10 +711,11 @@ internal sealed partial class RetailPViewPassExecutor :
|
||||||
int count = Math.Min(localVertices.Length, world.Length);
|
int count = Math.Min(localVertices.Length, world.Length);
|
||||||
for (int vertex = 0; vertex < count; vertex++)
|
for (int vertex = 0; vertex < count; vertex++)
|
||||||
{
|
{
|
||||||
|
// FW3.3: fans draw at the dat aperture verbatim (the
|
||||||
|
// ShellDrawLiftZ retirement — shells draw unlifted too).
|
||||||
world[vertex] = Vector3.Transform(
|
world[vertex] = Vector3.Transform(
|
||||||
localVertices[vertex],
|
localVertices[vertex],
|
||||||
cell.WorldTransform);
|
cell.WorldTransform);
|
||||||
world[vertex].Z += PortalVisibilityBuilder.ShellDrawLiftZ;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_diagnostics.EmitSeamMask(
|
_diagnostics.EmitSeamMask(
|
||||||
|
|
|
||||||
|
|
@ -148,7 +148,6 @@ public sealed class RetailPViewRenderer
|
||||||
ctx.Cells.Find,
|
ctx.Cells.Find,
|
||||||
ctx.ViewProjection,
|
ctx.ViewProjection,
|
||||||
buildingMembership: null,
|
buildingMembership: null,
|
||||||
drawLiftZ: PortalVisibilityBuilder.ShellDrawLiftZ,
|
|
||||||
reuseFrame: _mainPortalFrameScratch);
|
reuseFrame: _mainPortalFrameScratch);
|
||||||
|
|
||||||
// R-A2: outdoor root — flood each nearby building SEPARATELY from its own entrance and merge
|
// R-A2: outdoor root — flood each nearby building SEPARATELY from its own entrance and merge
|
||||||
|
|
|
||||||
|
|
@ -353,12 +353,10 @@ public sealed class EnvCellLandblockBuildBuilder
|
||||||
// two consumers decode the same dat fields for different needs)
|
// two consumers decode the same dat fields for different needs)
|
||||||
// so it stays a direct, auditable port of the FW1 test adapter
|
// so it stays a direct, auditable port of the FW1 test adapter
|
||||||
// rather than reshaping LoadedCell's own list layout around it.
|
// rather than reshaping LoadedCell's own list layout around it.
|
||||||
// cellTransform/inverse here are UNLIFTED (this method's
|
// cellTransform/inverse are the dat EnvCell origin verbatim —
|
||||||
// cellOrigin/cellTransform params are the caller's
|
// since FW3.3 retired ShellDrawLiftZ, render and physics share
|
||||||
// physicsCellOrigin/physicsCellTransform — see
|
// this one transform (retail draws at the origin; the walk's
|
||||||
// LandblockBuildFactory.BuildInteriorEntitiesForStreaming, which
|
// retail draw order owns the coplanar tie-breaks).
|
||||||
// keeps the +0.02 m ShellDrawLiftZ out of this transform) —
|
|
||||||
// exactly what the walk wants.
|
|
||||||
Walk = WalkCellFactory.FromParsed(envCellId, envCell, cellStruct, cellTransform, inverse),
|
Walk = WalkCellFactory.FromParsed(envCellId, envCell, cellStruct, cellTransform, inverse),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -532,21 +532,20 @@ public sealed class LandblockBuildFactory
|
||||||
// Static objects inside the cell continue to flow through the dispatcher
|
// Static objects inside the cell continue to flow through the dispatcher
|
||||||
// as WorldEntity records below — they have real GfxObj MeshRefs that work
|
// as WorldEntity records below — they have real GfxObj MeshRefs that work
|
||||||
// fine; EnvCellRenderer receives only the completed shell transaction.
|
// fine; EnvCellRenderer receives only the completed shell transaction.
|
||||||
// Transforms — needed by the portal-visibility cell (unlifted) AND the
|
// Transforms — needed by the portal-visibility cell AND the
|
||||||
// render/physics path. Computed for EVERY cell with a valid cellStruct,
|
// render/physics path. Computed for EVERY cell with a valid cellStruct,
|
||||||
// not just drawable ones. Keep the small render lift out of physics; retail
|
// not just drawable ones. Campaign FW3.3 (2026-08-30): the +0.02 m
|
||||||
// BSP contact planes use the EnvCell origin verbatim. The lift constant is
|
// ShellDrawLiftZ render lift is RETIRED (register row AP-32 deleted) —
|
||||||
// shared with every draw-space consumer of portal polygons (OutsideView
|
// retail draws cell geometry at the dat EnvCell origin verbatim, and the
|
||||||
// gate, seal/punch fans) — PortalVisibilityBuilder.ShellDrawLiftZ (#130).
|
// coplanar tie-breaks the lift stood in for are owned by the walk's
|
||||||
|
// retail draw order under depth Less. Render and physics now share the
|
||||||
|
// one verbatim transform.
|
||||||
var physicsCellOrigin = envCell.Position.Origin + lbOffset;
|
var physicsCellOrigin = envCell.Position.Origin + lbOffset;
|
||||||
var cellOrigin = physicsCellOrigin + new System.Numerics.Vector3(
|
var cellOrigin = physicsCellOrigin;
|
||||||
0f, 0f, AcDream.App.Rendering.PortalVisibilityBuilder.ShellDrawLiftZ);
|
|
||||||
var cellTransform =
|
var cellTransform =
|
||||||
System.Numerics.Matrix4x4.CreateFromQuaternion(envCell.Position.Orientation) *
|
System.Numerics.Matrix4x4.CreateFromQuaternion(envCell.Position.Orientation) *
|
||||||
System.Numerics.Matrix4x4.CreateTranslation(cellOrigin);
|
System.Numerics.Matrix4x4.CreateTranslation(cellOrigin);
|
||||||
var physicsCellTransform =
|
var physicsCellTransform = cellTransform;
|
||||||
System.Numerics.Matrix4x4.CreateFromQuaternion(envCell.Position.Orientation) *
|
|
||||||
System.Numerics.Matrix4x4.CreateTranslation(physicsCellOrigin);
|
|
||||||
|
|
||||||
// PORTAL VISIBILITY: register EVERY cell with a valid cellStruct, regardless
|
// PORTAL VISIBILITY: register EVERY cell with a valid cellStruct, regardless
|
||||||
// of whether CellMesh.Build produced drawable sub-meshes. A portals-only
|
// of whether CellMesh.Build produced drawable sub-meshes. A portals-only
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ public class Issue130DoorwayStripTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ExitDoorTopEdge_GateCoversLiftedApertureWithinPixelTolerance()
|
public void ExitDoorTopEdge_GateCoversTheDrawnApertureWithinPixelTolerance()
|
||||||
{
|
{
|
||||||
var datDir = CornerFloodReplayTests.ResolveDatDir();
|
var datDir = CornerFloodReplayTests.ResolveDatDir();
|
||||||
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
|
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
|
||||||
|
|
@ -78,16 +78,15 @@ public class Issue130DoorwayStripTests
|
||||||
Assert.True(exitIdx >= 0, "0x0170 has no exit portal polygon");
|
Assert.True(exitIdx >= 0, "0x0170 has no exit portal polygon");
|
||||||
|
|
||||||
var localPoly = root.PortalPolygons[exitIdx];
|
var localPoly = root.PortalPolygons[exitIdx];
|
||||||
// DRAWN space: the shell that rasterizes the aperture (and the seal fan)
|
// Campaign FW3.3: ShellDrawLiftZ is retired — shells, seal fans, and
|
||||||
// draws +ShellDrawLiftZ above the physics transform — the gate must be
|
// the gate all live in the ONE dat space, so the drawn aperture IS
|
||||||
// compared against the drawn hole, not the physics polygon (#130: the
|
// the physics polygon. This test survives as the coverage proof
|
||||||
// unlifted gate left a 2 cm background strip under the drawn lintel).
|
// (gate covers the drawn hole within tolerance across the sweep);
|
||||||
|
// the historical lifted-vs-unlifted strip test's premise (two
|
||||||
|
// spaces) no longer exists and that test is deleted.
|
||||||
var worldPoly = new Vector3[localPoly.Length];
|
var worldPoly = new Vector3[localPoly.Length];
|
||||||
for (int i = 0; i < localPoly.Length; i++)
|
for (int i = 0; i < localPoly.Length; i++)
|
||||||
{
|
|
||||||
worldPoly[i] = Vector3.Transform(localPoly[i], root.WorldTransform);
|
worldPoly[i] = Vector3.Transform(localPoly[i], root.WorldTransform);
|
||||||
worldPoly[i].Z += PortalVisibilityBuilder.ShellDrawLiftZ;
|
|
||||||
}
|
|
||||||
|
|
||||||
Vector3 centroid = Vector3.Zero;
|
Vector3 centroid = Vector3.Zero;
|
||||||
foreach (var w in worldPoly) centroid += w;
|
foreach (var w in worldPoly) centroid += w;
|
||||||
|
|
@ -146,7 +145,7 @@ public class Issue130DoorwayStripTests
|
||||||
aperture[i] = new Vector2(clip[i].X / clip[i].W, clip[i].Y / clip[i].W);
|
aperture[i] = new Vector2(clip[i].X / clip[i].W, clip[i].Y / clip[i].W);
|
||||||
|
|
||||||
var pv = PortalVisibilityBuilder.Build(root, eye, Lookup, viewProj,
|
var pv = PortalVisibilityBuilder.Build(root, eye, Lookup, viewProj,
|
||||||
buildingMembership: null, drawLiftZ: PortalVisibilityBuilder.ShellDrawLiftZ);
|
buildingMembership: null);
|
||||||
var asm = ClipFrameAssembler.Assemble(ClipFrame.NoClip(), pv);
|
var asm = ClipFrameAssembler.Assemble(ClipFrame.NoClip(), pv);
|
||||||
if (asm.OutsideViewSlices.Length == 0)
|
if (asm.OutsideViewSlices.Length == 0)
|
||||||
{
|
{
|
||||||
|
|
@ -203,103 +202,6 @@ public class Issue130DoorwayStripTests
|
||||||
$"plane gate under-covers the aperture top edge by {worstPlaneGapPx:F2}px @ {worstDesc}"));
|
$"plane gate under-covers the aperture top edge by {worstPlaneGapPx:F2}px @ {worstDesc}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Sensitivity proof + regression documentation: a gate built in
|
|
||||||
/// PHYSICS space (drawLiftZ 0) against the DRAWN (lifted) aperture shows a
|
|
||||||
/// multi-pixel strip at a close doorway — the user-visible #130 strip
|
|
||||||
/// (f35cb8b split the lift out of the visibility transform; the OutsideView
|
|
||||||
/// kept gating drawn color in unlifted space). If this stops failing-by-gap,
|
|
||||||
/// the lift is gone and the production drawLiftZ plumbing can go too.</summary>
|
|
||||||
[Fact]
|
|
||||||
public void UnliftedGate_LeavesTheStripAtTheDrawnTopEdge()
|
|
||||||
{
|
|
||||||
var datDir = CornerFloodReplayTests.ResolveDatDir();
|
|
||||||
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
|
|
||||||
|
|
||||||
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
|
||||||
var cells = CornerFloodReplayTests.LoadBuilding(dats);
|
|
||||||
var root = cells[ExitCellId];
|
|
||||||
LoadedCell? Lookup(uint id) => cells.TryGetValue(id, out var c) ? c : null;
|
|
||||||
|
|
||||||
int exitIdx = -1;
|
|
||||||
for (int i = 0; i < root.Portals.Count; i++)
|
|
||||||
{
|
|
||||||
if (root.Portals[i].OtherCellId == 0xFFFF && i < root.PortalPolygons.Count
|
|
||||||
&& root.PortalPolygons[i].Length >= 3)
|
|
||||||
{ exitIdx = i; break; }
|
|
||||||
}
|
|
||||||
Assert.True(exitIdx >= 0);
|
|
||||||
|
|
||||||
var localPoly = root.PortalPolygons[exitIdx];
|
|
||||||
var worldPoly = new Vector3[localPoly.Length];
|
|
||||||
Vector3 centroid = Vector3.Zero;
|
|
||||||
for (int i = 0; i < localPoly.Length; i++)
|
|
||||||
{
|
|
||||||
worldPoly[i] = Vector3.Transform(localPoly[i], root.WorldTransform);
|
|
||||||
worldPoly[i].Z += PortalVisibilityBuilder.ShellDrawLiftZ; // drawn space
|
|
||||||
centroid += worldPoly[i];
|
|
||||||
}
|
|
||||||
centroid /= worldPoly.Length;
|
|
||||||
|
|
||||||
var plane = root.ClipPlanes[exitIdx];
|
|
||||||
var worldNormal = Vector3.TransformNormal(plane.Normal, root.WorldTransform);
|
|
||||||
var cellCenterWorld = Vector3.Transform(
|
|
||||||
(root.LocalBoundsMin + root.LocalBoundsMax) * 0.5f, root.WorldTransform);
|
|
||||||
if (Vector3.Dot(worldNormal, cellCenterWorld - centroid) < 0)
|
|
||||||
worldNormal = -worldNormal;
|
|
||||||
worldNormal = Vector3.Normalize(worldNormal);
|
|
||||||
|
|
||||||
// d=2.4 m, eye low (0.9 m above the opening's base), gaze at the
|
|
||||||
// centroid — the main sweep's clean case, where the aperture top edge
|
|
||||||
// projects ON SCREEN (y≈0.79; a closer/higher eye pushes the lintel
|
|
||||||
// past the screen top and the seam becomes unmeasurable).
|
|
||||||
var eye = centroid + worldNormal * 2.4f;
|
|
||||||
eye.Z = centroid.Z - 1.0f + 0.9f;
|
|
||||||
var viewProj = ViewProjFor(eye, centroid);
|
|
||||||
|
|
||||||
var clip = new Vector4[worldPoly.Length];
|
|
||||||
for (int i = 0; i < worldPoly.Length; i++)
|
|
||||||
clip[i] = Vector4.Transform(new Vector4(worldPoly[i], 1f), viewProj);
|
|
||||||
var aperture = new Vector2[clip.Length];
|
|
||||||
for (int i = 0; i < clip.Length; i++)
|
|
||||||
aperture[i] = new Vector2(clip[i].X / clip[i].W, clip[i].Y / clip[i].W);
|
|
||||||
|
|
||||||
var pvUnlifted = PortalVisibilityBuilder.Build(root, eye, Lookup, viewProj); // drawLiftZ 0
|
|
||||||
var asmUnlifted = ClipFrameAssembler.Assemble(ClipFrame.NoClip(), pvUnlifted);
|
|
||||||
Assert.True(asmUnlifted.OutsideViewSlices.Length > 0);
|
|
||||||
(float unliftedGapPx, _, _) = MeasureTopEdgeGap(aperture, asmUnlifted.OutsideViewSlices, 1920, 1080);
|
|
||||||
|
|
||||||
var pvLifted = PortalVisibilityBuilder.Build(root, eye, Lookup, viewProj,
|
|
||||||
buildingMembership: null, drawLiftZ: PortalVisibilityBuilder.ShellDrawLiftZ);
|
|
||||||
var asmLifted = ClipFrameAssembler.Assemble(ClipFrame.NoClip(), pvLifted);
|
|
||||||
Assert.True(asmLifted.OutsideViewSlices.Length > 0);
|
|
||||||
(float liftedGapPx, _, _) = MeasureTopEdgeGap(aperture, asmLifted.OutsideViewSlices, 1920, 1080);
|
|
||||||
|
|
||||||
_out.WriteLine(FormattableString.Invariant(
|
|
||||||
$"top-edge gap vs the DRAWN aperture at d=2.4 m: unliftedGate={unliftedGapPx:F2}px liftedGate={liftedGapPx:F2}px"));
|
|
||||||
var dbg = new System.Text.StringBuilder(" aperture(LIFTED):");
|
|
||||||
foreach (var v in aperture) dbg.Append(FormattableString.Invariant($" ({v.X:F4},{v.Y:F4})"));
|
|
||||||
_out.WriteLine(dbg.ToString());
|
|
||||||
foreach (var poly in pvUnlifted.OutsideView.Polygons)
|
|
||||||
{
|
|
||||||
var sb = new System.Text.StringBuilder(" unliftedGatePoly:");
|
|
||||||
foreach (var v in poly.Vertices) sb.Append(FormattableString.Invariant($" ({v.X:F4},{v.Y:F4})"));
|
|
||||||
_out.WriteLine(sb.ToString());
|
|
||||||
}
|
|
||||||
foreach (var poly in pvLifted.OutsideView.Polygons)
|
|
||||||
{
|
|
||||||
var sb = new System.Text.StringBuilder(" liftedGatePoly:");
|
|
||||||
foreach (var v in poly.Vertices) sb.Append(FormattableString.Invariant($" ({v.X:F4},{v.Y:F4})"));
|
|
||||||
_out.WriteLine(sb.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
// The strip the user saw: physics-space gate vs drawn hole, several px.
|
|
||||||
Assert.True(unliftedGapPx > 2.0f, FormattableString.Invariant(
|
|
||||||
$"expected the unlifted gate to show the strip (>2px), got {unliftedGapPx:F2}px"));
|
|
||||||
// The fix: a gate in drawn space covers the drawn hole.
|
|
||||||
Assert.True(liftedGapPx <= 1.2f, FormattableString.Invariant(
|
|
||||||
$"lifted gate still under-covers by {liftedGapPx:F2}px"));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string DescribePolys(CellView view)
|
private static string DescribePolys(CellView view)
|
||||||
{
|
{
|
||||||
var parts = new List<string>();
|
var parts = new List<string>();
|
||||||
|
|
|
||||||
|
|
@ -48,8 +48,7 @@ public class Issue131SetupProbeTests
|
||||||
var root = cells[0xA9B40171u];
|
var root = cells[0xA9B40171u];
|
||||||
var pv = AcDream.App.Rendering.PortalVisibilityBuilder.Build(
|
var pv = AcDream.App.Rendering.PortalVisibilityBuilder.Build(
|
||||||
root, eye, Lookup, viewProj,
|
root, eye, Lookup, viewProj,
|
||||||
buildingMembership: null,
|
buildingMembership: null);
|
||||||
drawLiftZ: AcDream.App.Rendering.PortalVisibilityBuilder.ShellDrawLiftZ);
|
|
||||||
_out.WriteLine(FormattableString.Invariant(
|
_out.WriteLine(FormattableString.Invariant(
|
||||||
$"main flood={pv.OrderedVisibleCells.Count} outPolys={pv.OutsideView.Polygons.Count}"));
|
$"main flood={pv.OrderedVisibleCells.Count} outPolys={pv.OutsideView.Polygons.Count}"));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ namespace AcDream.App.Tests.Rendering;
|
||||||
/// where its geometry was; stair cells failing admission = the pop).
|
/// where its geometry was; stair cells failing admission = the pop).
|
||||||
///
|
///
|
||||||
/// Production-matched inputs: Build(root, eye, lookup, viewProj,
|
/// Production-matched inputs: Build(root, eye, lookup, viewProj,
|
||||||
/// buildingMembership: null, drawLiftZ: ShellDrawLiftZ) — the drawLiftZ
|
/// buildingMembership: null) — the production call shape (FW3.3 retired drawLiftZ)
|
||||||
/// mirrors RetailPViewRenderer.DrawInside.
|
/// mirrors RetailPViewRenderer.DrawInside.
|
||||||
///
|
///
|
||||||
/// Scenarios:
|
/// Scenarios:
|
||||||
|
|
@ -54,8 +54,7 @@ public class Issue176177FacilityHubFloodReplayTests
|
||||||
Func<uint, LoadedCell?> lookup = id => cells.TryGetValue(id, out var c) ? c : null;
|
Func<uint, LoadedCell?> lookup = id => cells.TryGetValue(id, out var c) ? c : null;
|
||||||
var frame = PortalVisibilityBuilder.Build(
|
var frame = PortalVisibilityBuilder.Build(
|
||||||
cells[rootId], eye, lookup, ViewProjFor(eye, gazeDir),
|
cells[rootId], eye, lookup, ViewProjFor(eye, gazeDir),
|
||||||
buildingMembership: null,
|
buildingMembership: null);
|
||||||
drawLiftZ: PortalVisibilityBuilder.ShellDrawLiftZ);
|
|
||||||
var result = new List<uint>(frame.OrderedVisibleCells);
|
var result = new List<uint>(frame.OrderedVisibleCells);
|
||||||
result.Sort();
|
result.Sort();
|
||||||
return result;
|
return result;
|
||||||
|
|
|
||||||
|
|
@ -207,7 +207,7 @@ public class Issue177StairDescentCameraFloodTests
|
||||||
string admit = "?";
|
string admit = "?";
|
||||||
if (viewer != 0u && renderCells.TryGetValue(viewer, out var rc))
|
if (viewer != 0u && renderCells.TryGetValue(viewer, out var rc))
|
||||||
{
|
{
|
||||||
var f = PortalVisibilityBuilder.Build(rc, eye, lookup, cam.View * cam.Projection, null, PortalVisibilityBuilder.ShellDrawLiftZ);
|
var f = PortalVisibilityBuilder.Build(rc, eye, lookup, cam.View * cam.Projection, null);
|
||||||
var s = f.OrderedVisibleCells;
|
var s = f.OrderedVisibleCells;
|
||||||
admit = string.Concat(stair.Select(low => (s.Contains(FacilityHub | low) ? "1" : "0")));
|
admit = string.Concat(stair.Select(low => (s.Contains(FacilityHub | low) ? "1" : "0")));
|
||||||
}
|
}
|
||||||
|
|
@ -259,7 +259,7 @@ public class Issue177StairDescentCameraFloodTests
|
||||||
for (int i = 0; i < 140; i++) cam.Update(playerPos, yaw, Vector3.Zero, true, Vector3.UnitZ, 1f / 60f, pcell, 0x5000000A);
|
for (int i = 0; i < 140; i++) cam.Update(playerPos, yaw, Vector3.Zero, true, Vector3.UnitZ, 1f / 60f, pcell, 0x5000000A);
|
||||||
uint viewer = cam.ViewerCellId;
|
uint viewer = cam.ViewerCellId;
|
||||||
if (viewer == 0u || !renderCells.TryGetValue(viewer, out var rc)) continue;
|
if (viewer == 0u || !renderCells.TryGetValue(viewer, out var rc)) continue;
|
||||||
var f = PortalVisibilityBuilder.Build(rc, cam.Position, lookup, cam.View * cam.Projection, null, PortalVisibilityBuilder.ShellDrawLiftZ);
|
var f = PortalVisibilityBuilder.Build(rc, cam.Position, lookup, cam.View * cam.Projection, null);
|
||||||
int c = f.OrderedVisibleCells.Count; sum += c; n++;
|
int c = f.OrderedVisibleCells.Count; sum += c; n++;
|
||||||
if (c > maxCells) { maxCells = c; bestViewer = viewer; bestSet = CellSet(f.OrderedVisibleCells.OrderBy(v => v)); }
|
if (c > maxCells) { maxCells = c; bestViewer = viewer; bestSet = CellSet(f.OrderedVisibleCells.OrderBy(v => v)); }
|
||||||
}
|
}
|
||||||
|
|
@ -326,7 +326,7 @@ public class Issue177StairDescentCameraFloodTests
|
||||||
float d = PortalPlaneDistance(root, upIdx, eye);
|
float d = PortalPlaneDistance(root, upIdx, eye);
|
||||||
var frame = PortalVisibilityBuilder.Build(
|
var frame = PortalVisibilityBuilder.Build(
|
||||||
root, eye, lookup, ViewProj(eye, gaze),
|
root, eye, lookup, ViewProj(eye, gaze),
|
||||||
buildingMembership: null, drawLiftZ: PortalVisibilityBuilder.ShellDrawLiftZ);
|
buildingMembership: null);
|
||||||
var vis = frame.OrderedVisibleCells.OrderBy(v => v).ToList();
|
var vis = frame.OrderedVisibleCells.OrderBy(v => v).ToList();
|
||||||
bool up = vis.Contains(upperId);
|
bool up = vis.Contains(upperId);
|
||||||
string admit = up ? "ADMITTED" : "DROPPED";
|
string admit = up ? "ADMITTED" : "DROPPED";
|
||||||
|
|
@ -339,8 +339,8 @@ public class Issue177StairDescentCameraFloodTests
|
||||||
// spiral, root 0x01C8, portals off-screen/sliver, camera NOT collision-jammed).
|
// spiral, root 0x01C8, portals off-screen/sliver, camera NOT collision-jammed).
|
||||||
// Kept as a mechanism pin: an eye exactly in the ceiling-portal plane drops the upper
|
// Kept as a mechanism pin: an eye exactly in the ceiling-portal plane drops the upper
|
||||||
// cell (edge-on → <3 clip), an eye 0.4 m off admits it via the normal clip.
|
// cell (edge-on → <3 clip), an eye 0.4 m off admits it via the normal clip.
|
||||||
var fEdge = PortalVisibilityBuilder.Build(root, eyeEdgeOn, lookup, ViewProj(eyeEdgeOn, gaze), null, PortalVisibilityBuilder.ShellDrawLiftZ);
|
var fEdge = PortalVisibilityBuilder.Build(root, eyeEdgeOn, lookup, ViewProj(eyeEdgeOn, gaze), null);
|
||||||
var fOff = PortalVisibilityBuilder.Build(root, eyeOffPlane, lookup, ViewProj(eyeOffPlane, gaze), null, PortalVisibilityBuilder.ShellDrawLiftZ);
|
var fOff = PortalVisibilityBuilder.Build(root, eyeOffPlane, lookup, ViewProj(eyeOffPlane, gaze), null);
|
||||||
Assert.DoesNotContain(upperId, fEdge.OrderedVisibleCells); // edge-on drops (mechanism)
|
Assert.DoesNotContain(upperId, fEdge.OrderedVisibleCells); // edge-on drops (mechanism)
|
||||||
Assert.Contains(upperId, fOff.OrderedVisibleCells); // off-plane admits (control)
|
Assert.Contains(upperId, fOff.OrderedVisibleCells); // off-plane admits (control)
|
||||||
}
|
}
|
||||||
|
|
@ -438,7 +438,7 @@ public class Issue177StairDescentCameraFloodTests
|
||||||
var vp = cam.View * cam.Projection;
|
var vp = cam.View * cam.Projection;
|
||||||
if (viewer != 0u && renderCells.TryGetValue(viewer, out var rootCell))
|
if (viewer != 0u && renderCells.TryGetValue(viewer, out var rootCell))
|
||||||
{
|
{
|
||||||
var f = PortalVisibilityBuilder.Build(rootCell, eye, lookup, vp, null, PortalVisibilityBuilder.ShellDrawLiftZ);
|
var f = PortalVisibilityBuilder.Build(rootCell, eye, lookup, vp, null);
|
||||||
var vis = f.OrderedVisibleCells.OrderBy(v => v).ToList();
|
var vis = f.OrderedVisibleCells.OrderBy(v => v).ToList();
|
||||||
string flags = $"0178={(vis.Contains(FacilityHub | 0x0178u) ? "Y" : "-")} 0182={(vis.Contains(FacilityHub | 0x0182u) ? "Y" : "-")} 0183={(vis.Contains(FacilityHub | 0x0183u) ? "Y" : "-")} 0181={(vis.Contains(FacilityHub | 0x0181u) ? "Y" : "-")}";
|
string flags = $"0178={(vis.Contains(FacilityHub | 0x0178u) ? "Y" : "-")} 0182={(vis.Contains(FacilityHub | 0x0182u) ? "Y" : "-")} 0183={(vis.Contains(FacilityHub | 0x0183u) ? "Y" : "-")} 0181={(vis.Contains(FacilityHub | 0x0181u) ? "Y" : "-")}";
|
||||||
_out.WriteLine(FormattableString.Invariant(
|
_out.WriteLine(FormattableString.Invariant(
|
||||||
|
|
@ -519,7 +519,7 @@ public class Issue177StairDescentCameraFloodTests
|
||||||
{
|
{
|
||||||
var frame = PortalVisibilityBuilder.Build(
|
var frame = PortalVisibilityBuilder.Build(
|
||||||
rootCell, eye, lookup, viewProj,
|
rootCell, eye, lookup, viewProj,
|
||||||
buildingMembership: null, drawLiftZ: PortalVisibilityBuilder.ShellDrawLiftZ);
|
buildingMembership: null);
|
||||||
var vis = frame.OrderedVisibleCells.OrderBy(v => v).ToList();
|
var vis = frame.OrderedVisibleCells.OrderBy(v => v).ToList();
|
||||||
floodStr = $"{vis.Count,2} [{CellSet(vis)}]";
|
floodStr = $"{vis.Count,2} [{CellSet(vis)}]";
|
||||||
bool ramp = vis.Contains(FacilityHub | 0x0182u);
|
bool ramp = vis.Contains(FacilityHub | 0x0182u);
|
||||||
|
|
|
||||||
|
|
@ -46,8 +46,7 @@ public class Issue181VisFlapReplayTests
|
||||||
Func<uint, LoadedCell?> lookup = id => cells.TryGetValue(id, out var c) ? c : null;
|
Func<uint, LoadedCell?> lookup = id => cells.TryGetValue(id, out var c) ? c : null;
|
||||||
var frame = PortalVisibilityBuilder.Build(
|
var frame = PortalVisibilityBuilder.Build(
|
||||||
cells[rootId], eye, lookup, ViewProjFor(eye, gazeDir),
|
cells[rootId], eye, lookup, ViewProjFor(eye, gazeDir),
|
||||||
buildingMembership: null,
|
buildingMembership: null);
|
||||||
drawLiftZ: PortalVisibilityBuilder.ShellDrawLiftZ);
|
|
||||||
var result = new List<uint>(frame.OrderedVisibleCells);
|
var result = new List<uint>(frame.OrderedVisibleCells);
|
||||||
result.Sort();
|
result.Sort();
|
||||||
return result;
|
return result;
|
||||||
|
|
@ -88,8 +87,7 @@ public class Issue181VisFlapReplayTests
|
||||||
{
|
{
|
||||||
var frame = PortalVisibilityBuilder.Build(
|
var frame = PortalVisibilityBuilder.Build(
|
||||||
cells[LiveRoot], eye, lookup, ViewProjFor(eye, gaze),
|
cells[LiveRoot], eye, lookup, ViewProjFor(eye, gaze),
|
||||||
buildingMembership: null,
|
buildingMembership: null);
|
||||||
drawLiftZ: PortalVisibilityBuilder.ShellDrawLiftZ);
|
|
||||||
|
|
||||||
if (!frame.CellViews.TryGetValue(flapper, out var view) || view.IsEmpty)
|
if (!frame.CellViews.TryGetValue(flapper, out var view) || view.IsEmpty)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue