Revert "Campaign V slice V4a" - it lost world multisampling
This reverts ceec3bc4. Two independent reasons, either sufficient.
The rendering regression. The slice deleted TextRenderGlStateScope, which
saved GL_MULTISAMPLE and GL_SAMPLE_ALPHA_TO_COVERAGE on entry, disabled them
for the text pass, and restored them on exit (TextRenderGlStateScope.cs:111-112
and 153-154 at the parent commit). Its replacement bakes that state into the
text pipeline but nothing restores it, and GlGpuPassEncoder.Dispose does not
either. Every world renderer is still raw GL at this point in the campaign, so
from the first UI frame onward the world drew with multisampling disabled.
The offline pixel gate caught it: 1,791 of 563,200 compared pixels differed,
0.318% against a 0.001 threshold. The commit message attributed this to
wall-clock-driven ambient animation shifting phase, and committed through the
failure. That explanation does not survive its own control: capturing twice at
the reverted-to commit differs by 19 pixels and twice at the slice's own commit
by 8, while base-versus-head differs by 1,791 - a 224x gap that no shared-noise
source explains. An amplified difference image settles it visually: the changed
pixels are the silhouette edges of every tree, building and rock, with terrain
interiors, water and the entire UI untouched. That is the signature of losing
edge antialiasing, not of animated sprites.
This is the exact failure mode two existing memory notes already warn about -
a mid-frame renderer must set every GL state it uses rather than inherit it,
and issue #52's lesson that a rendering migration must audit per-pass GL state
before declaring itself done.
The scope. The brief was three small leaf renderers plus additive frame-
lifecycle wiring, roughly ten files. The commit changed 334 files with 3,665
insertions and 3,845 deletions, including 323 public-to-internal visibility
conversions across the App assembly, 55 test files, two retired conformance
tests, and a self-described temporary escape hatch for bridging raw-GL viewport
textures. Even without the regression, that is not separable into the part
worth keeping and the part worth dropping.
Reverting rather than patching because the good work here - the RHI frame
lifecycle wiring and a genuine render-state-cache staleness fix - is small
enough to redo cleanly against a tightened spec, while untangling it from 300+
files of unrelated churn is not.
Post-revert: Release build clean, App suite back to 3,843 passed / 3 skipped,
offline pixel gate passing at 19 differing pixels.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
ceec3bc440
commit
9aaf97e785
334 changed files with 3841 additions and 3661 deletions
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using AcDream.App.Rendering.Scene;
|
||||
|
|
@ -11,7 +11,7 @@ namespace AcDream.App.Rendering;
|
|||
/// SmartBox::RenderNormalMode -> RenderDeviceD3D::DrawInside ->
|
||||
/// PView::DrawInside -> ConstructView -> DrawCells.
|
||||
/// </summary>
|
||||
internal sealed class RetailPViewRenderer
|
||||
public sealed class RetailPViewRenderer
|
||||
{
|
||||
private readonly InteriorEntityPartition.IObserver? _partitionObserver;
|
||||
private readonly ICurrentRenderPViewObserver? _candidateObserver;
|
||||
|
|
@ -44,7 +44,7 @@ internal sealed class RetailPViewRenderer
|
|||
private readonly PortalVisibilityFrame _outdoorBuildingFrameScratch = new();
|
||||
|
||||
// #124: per-building look-in frames under an INTERIOR root, drawn as a
|
||||
// landscape-stage sub-pass (DrawBuildingLookIns) — never merged into the
|
||||
// landscape-stage sub-pass (DrawBuildingLookIns) — never merged into the
|
||||
// main frame (see DrawInside). Rebuilt each interior-root frame.
|
||||
private readonly List<PortalVisibilityFrame> _lookInFrames = new();
|
||||
private readonly Stack<PortalVisibilityFrame> _lookInFramePool = new();
|
||||
|
|
@ -59,7 +59,7 @@ internal sealed class RetailPViewRenderer
|
|||
// MP-Alloc (2026-07-05): the frame's entity partition (ByCell/OutdoorStatic/
|
||||
// Dynamics), reused across frames instead of `new`ing a Result (a Dictionary
|
||||
// + 2 Lists, plus one List<WorldEntity> per visible cell) every DrawInside
|
||||
// call. See InteriorEntityPartition.Partition(Result, ...) — clears in
|
||||
// call. See InteriorEntityPartition.Partition(Result, ...) — clears in
|
||||
// place and reuses each cell's list across frames when the cell stays
|
||||
// visible.
|
||||
// Slice G4: this is now a diagnostic/fallback oracle only. Normal
|
||||
|
|
@ -88,7 +88,7 @@ internal sealed class RetailPViewRenderer
|
|||
}
|
||||
|
||||
// T2 (BR-4): retail has NO distance constant on the flood-admission chain
|
||||
// (DrawBuilding → portal walk → ConstructView: viewconeCheck + side test +
|
||||
// (DrawBuilding → portal walk → ConstructView: viewconeCheck + side test +
|
||||
// GetClip + GetVisible only). The old 48 m seed cap is replaced by the
|
||||
// caller's per-building frustum pre-gate on aperture bounds (GameWindow's
|
||||
// gather); seeds themselves are unbounded.
|
||||
|
|
@ -113,28 +113,28 @@ internal sealed class RetailPViewRenderer
|
|||
drawLiftZ: PortalVisibilityBuilder.ShellDrawLiftZ,
|
||||
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
|
||||
// the small (~2-cell) per-building views into the frame. Retail reaches building interiors via
|
||||
// the terrain BSP -> DrawPortal -> ConstructView(CBldPortal) (decomp:326881/433895/433827); the
|
||||
// land root itself has no portals (it floods nothing into buildings). Per-building seeding is
|
||||
// robust to the eye's ~36 µm rest jitter where the pre-R-A2 single reverse-portal flood
|
||||
// robust to the eye's ~36 µm rest jitter where the pre-R-A2 single reverse-portal flood
|
||||
// oscillated as the chase eye grazed a doorway (the indoor flap).
|
||||
if (ctx.RootCell.IsOutdoorNode && ctx.NearbyBuildingCells is not null)
|
||||
MergeNearbyBuildingFloods(ctx, pvFrame);
|
||||
|
||||
// #124: interior-root building look-ins. Retail runs the look-in INSIDE
|
||||
// the landscape stage for ANY root — LScape::draw is the FIRST call of
|
||||
// the landscape stage for ANY root — LScape::draw is the FIRST call of
|
||||
// DrawCells' outside-view branch (pc:432719), strictly BEFORE the depth
|
||||
// clear (pc:432732) and the exit-portal seals (pc:432785); a far
|
||||
// building seen through our doorway floods clipped to the INSTALLED
|
||||
// outside view (GetClip vs current view, ConstructView(CBldPortal)
|
||||
// 0x005a59a0). These frames therefore draw in DrawBuildingLookIns
|
||||
// (inside the landscape stage), NEVER merged into the main frame — a
|
||||
// (inside the landscape stage), NEVER merged into the main frame — a
|
||||
// merged cell would draw post-clear and z-fail against the root's seal
|
||||
// (its geometry is beyond the door plane). The eye-side seed test
|
||||
// self-excludes the root's own building (the eye is on its interior
|
||||
// side). Outdoor roots keep the MergeNearbyBuildingFloods path above
|
||||
// (no depth clear under outdoor roots — the merged form is equivalent
|
||||
// (no depth clear under outdoor roots — the merged form is equivalent
|
||||
// there).
|
||||
if (!ctx.RootCell.IsOutdoorNode
|
||||
&& ctx.NearbyBuildingCells is not null
|
||||
|
|
@ -149,7 +149,7 @@ internal sealed class RetailPViewRenderer
|
|||
|
||||
// R1: draw EVERY visible cell (retail cell_draw_list), not only the cells the
|
||||
// assembler handed a clip-slot. This feeds the Prepare filter + entity partition,
|
||||
// so every visible cell's shell has a prepared batch and seals — killing the grey
|
||||
// so every visible cell's shell has a prepared batch and seals — killing the grey
|
||||
// (the old clipAssembly.CellIdToSlot.Keys filter silently dropped slot-less cells).
|
||||
// Per-slice trim still applies in DrawEnvCellShells (Task 4 makes it self-contained).
|
||||
_drawableCellsScratch.Clear();
|
||||
|
|
@ -158,7 +158,7 @@ internal sealed class RetailPViewRenderer
|
|||
passes.UseIndoorMembershipOnlyRouting();
|
||||
|
||||
// #124: look-in cells need prepared shell batches + their statics routed
|
||||
// into partition.ByCell (consumed ONLY by DrawBuildingLookIns — the main
|
||||
// into partition.ByCell (consumed ONLY by DrawBuildingLookIns — the main
|
||||
// cell-object pass iterates pvFrame.OrderedVisibleCells, which never
|
||||
// contains them). drawableCells itself stays the MAIN flood: it feeds the
|
||||
// seals, the outside-stage predicate, and the frame result.
|
||||
|
|
@ -174,22 +174,22 @@ internal sealed class RetailPViewRenderer
|
|||
}
|
||||
|
||||
// (#176 correction, 2026-07-06: the flood-scoped light-pool rebuild that ran
|
||||
// here was the seam-floor flicker mechanism — retail's visible_cell_table is
|
||||
// the RESIDENT-cell registry, not the frame flood — and is deleted. The pool
|
||||
// here was the seam-floor flicker mechanism — retail's visible_cell_table is
|
||||
// the RESIDENT-cell registry, not the frame flood — and is deleted. The pool
|
||||
// is built once per frame in GameWindow, player-anchored.)
|
||||
|
||||
passes.PrepareCellBatches(ctx, prepareCells);
|
||||
|
||||
// T1 (fused BR-2/3): retail's frame order — static world, then the
|
||||
// aperture depth writes, then interior cells WHOLE far→near, then
|
||||
// T1 (fused BR-2/3): retail's frame order — static world, then the
|
||||
// aperture depth writes, then interior cells WHOLE far→near, then
|
||||
// per-cell statics, then ALL dynamics last (retail draws objects after
|
||||
// cells: PView::DrawCells Ghidra 0x005a4840; DrawBuilding 0x0059f2a0).
|
||||
// The geometric shell chop (gl_ClipDistance crop, 927fd8f/9ce335e) is
|
||||
// DELETED — retail never clips cell geometry; aperture exactness comes
|
||||
// DELETED — retail never clips cell geometry; aperture exactness comes
|
||||
// from the punch/seal depth writes + the z-buffer, and the dynamics-
|
||||
// last order is what makes the punch safe (the first BR-2 attempt
|
||||
// punched after dynamics and erased the player, reverted 88be519).
|
||||
// T3 (BR-5): retail viewconeCheck — meshes are sphere-CULLED per view,
|
||||
// T3 (BR-5): retail viewconeCheck — meshes are sphere-CULLED per view,
|
||||
// never clipped (Ghidra 0x0054c250). Built once per frame from the
|
||||
// assembled slices + this frame's view-projection.
|
||||
var viewcone = ViewconeCuller.Build(
|
||||
|
|
@ -256,19 +256,19 @@ internal sealed class RetailPViewRenderer
|
|||
passes.EmitDiagnostics(ctx, result);
|
||||
|
||||
// #118: stage assignment for dynamics under an INTERIOR root. Retail
|
||||
// draws the OUTSIDE world's objects inside the landscape stage —
|
||||
// draws the OUTSIDE world's objects inside the landscape stage —
|
||||
// PView::DrawCells runs LScape::draw FIRST (pc:432719), then the gated
|
||||
// full depth clear (pc:432731-432732) and the exit-portal SEALS
|
||||
// (pc:432785-432786); DrawBlock draws every landcell's objects via
|
||||
// DrawSortCell (0x005a17c0, pc:430124). A dynamic deferred to our
|
||||
// single last pass instead z-fails against the seal's true-depth stamp
|
||||
// the moment it stands beyond the door plane — the house-exit
|
||||
// the moment it stands beyond the door plane — the house-exit
|
||||
// clip+vanish (pinned by HouseExitWalkReplayTests). So under an
|
||||
// interior root: outdoor-classified dynamics draw in the outside
|
||||
// stage; an indoor dynamic whose sphere STRADDLES an exit portal
|
||||
// draws in BOTH stages (retail's per-overlapped-cell shadow-part
|
||||
// draw, DrawBlock pc:430056-430064) so neither body half clips at the
|
||||
// plane. Outdoor roots keep ALL dynamics in the last pass — our
|
||||
// plane. Outdoor roots keep ALL dynamics in the last pass — our
|
||||
// z-buffered equivalent of retail's painter-ordered outdoor pass (the
|
||||
// BR-2 punch-after-dynamics lesson, reverted 88be519).
|
||||
_outsideStageDynamics.Clear();
|
||||
|
|
@ -362,11 +362,11 @@ internal sealed class RetailPViewRenderer
|
|||
// on the cell (Render::copy_view appends + view_count++, Ghidra 0x0054dfc0;
|
||||
// a cell visible through two apertures holds two views, all consumed
|
||||
// downstream). The old first-wins (`ContainsKey -> continue`) dropped the
|
||||
// second building flood's views whenever a cell was already in the frame —
|
||||
// second building flood's views whenever a cell was already in the frame —
|
||||
// the multiview-loss-first-wins divergence (a named #109 suspect: per-frame
|
||||
// winner flips between apertures). CellView.Add dedups exact/collinear
|
||||
// re-emissions (the dac8f6a CanonicalKey), so unioning is convergent.
|
||||
// OutsideView is NOT merged — the outdoor root already seeds full-screen
|
||||
// OutsideView is NOT merged — the outdoor root already seeds full-screen
|
||||
// terrain, and ConstructViewBuilding (BuildFromExterior) leaves OutsideView
|
||||
// empty (it stops at exit portals once inside the building).
|
||||
private static void MergeBuildingFrame(PortalVisibilityFrame target, PortalVisibilityFrame src)
|
||||
|
|
@ -393,8 +393,8 @@ internal sealed class RetailPViewRenderer
|
|||
}
|
||||
|
||||
// #124: per-building look-in floods for an INTERIOR root, seeded clipped
|
||||
// against the OutsideView (retail: GetClip runs under the INSTALLED view —
|
||||
// the accumulated doorway region — so a far building floods only within the
|
||||
// against the OutsideView (retail: GetClip runs under the INSTALLED view —
|
||||
// the accumulated doorway region — so a far building floods only within the
|
||||
// doorway, ConstructView(CBldPortal) 0x005a59a0 via PView::GetClip
|
||||
// 0x005a4320). Same grouping as MergeNearbyBuildingFloods; the root's own
|
||||
// building self-excludes via the seed eye-side test.
|
||||
|
|
@ -445,15 +445,15 @@ internal sealed class RetailPViewRenderer
|
|||
private void ResetBuildingGroups()
|
||||
=> _buildingGroups.Reset();
|
||||
|
||||
// #124: draw the interior-root look-ins INSIDE the landscape stage —
|
||||
// retail's placement (LScape::draw → DrawBlock → DrawSortCell →
|
||||
// #124: draw the interior-root look-ins INSIDE the landscape stage —
|
||||
// retail's placement (LScape::draw → DrawBlock → DrawSortCell →
|
||||
// DrawBuilding runs as the FIRST call of DrawCells' outside-view branch,
|
||||
// pc:432719, before the depth clear + seals). Per building: punch ALL
|
||||
// apertures first (retail finishes build_draw_portals_only pass 1 — the
|
||||
// far-Z maxZ1 punch — across the whole building BSP before pass 2 floods),
|
||||
// then draw the flooded cells' shells + statics far→near (the nested
|
||||
// apertures first (retail finishes build_draw_portals_only pass 1 — the
|
||||
// far-Z maxZ1 punch — across the whole building BSP before pass 2 floods),
|
||||
// then draw the flooded cells' shells + statics far→near (the nested
|
||||
// DrawCells' DrawEnvCell + DrawObjCellForDummies; its outside_view is
|
||||
// empty by construction — PView ctor draw_landscape=0 — so no recursive
|
||||
// empty by construction — PView ctor draw_landscape=0 — so no recursive
|
||||
// landscape/clear/seal). Anything rasterized outside an aperture is
|
||||
// repainted by the root's own shells after the depth clear, so over-draw
|
||||
// here is color-safe; statics draw whole (the main viewcone has no entry
|
||||
|
|
@ -491,12 +491,12 @@ internal sealed class RetailPViewRenderer
|
|||
}
|
||||
}
|
||||
|
||||
// Pass 2: shells + statics, far→near.
|
||||
// Pass 2: shells + statics, far→near.
|
||||
passes.UseIndoorMembershipOnlyRouting();
|
||||
|
||||
// Opaque shells batched per building into ONE Render (this building's
|
||||
// aperture punches above already ran; z-buffer handles order and
|
||||
// lighting is per-instance CellId-keyed) — was one heavy per-frame
|
||||
// lighting is per-instance CellId-keyed) — was one heavy per-frame
|
||||
// Render per cell. Per-cell entity/particle work stays in the loop.
|
||||
_shellBatch.Clear();
|
||||
foreach (uint cid in frame.OrderedVisibleCells)
|
||||
|
|
@ -509,7 +509,7 @@ internal sealed class RetailPViewRenderer
|
|||
uint cellId = frame.OrderedVisibleCells[i];
|
||||
_oneCell.Clear();
|
||||
_oneCell.Add(cellId);
|
||||
// Opaque shell batched above. Transparent stays per-cell (far→near)
|
||||
// Opaque shell batched above. Transparent stays per-cell (far→near)
|
||||
// for correct compositing; skipped for opaque-only cells.
|
||||
if (passes.CellHasTransparentShell(cellId))
|
||||
passes.DrawTransparentCellShells(_oneCell);
|
||||
|
|
@ -523,7 +523,7 @@ internal sealed class RetailPViewRenderer
|
|||
|
||||
// #131 ROOT CAUSE: DYNAMICS living in a look-in cell (the
|
||||
// Holtburg hall-porch PORTAL, pCell 0xA9B4017A) draw NOWHERE
|
||||
// under an interior root — DrawDynamicsLast viewcone-culls
|
||||
// under an interior root — DrawDynamicsLast viewcone-culls
|
||||
// them (the main cone has no entries for look-in cells), and
|
||||
// post-clear they would z-fail against the root's seal anyway
|
||||
// (the #118 lesson). Retail draws a look-in cell's objects
|
||||
|
|
@ -576,7 +576,7 @@ internal sealed class RetailPViewRenderer
|
|||
_cellStaticScratch,
|
||||
_oneCell);
|
||||
|
||||
// The cell-particles pass for look-in cells — retail's
|
||||
// The cell-particles pass for look-in cells — retail's
|
||||
// nested DrawCells draws objects WITH their emitters.
|
||||
foreach (var slice in GetCellSlicesOrNoClip(clipAssembly, cellId))
|
||||
passes.DrawCellParticles(ctx, new RetailPViewCellSliceContext(
|
||||
|
|
@ -600,7 +600,7 @@ internal sealed class RetailPViewRenderer
|
|||
|
||||
// #131/#132 (the FlushAlphaList deferral): retail collects ALL alpha
|
||||
// draws of the landscape stage and flushes them ONCE after LScape::draw
|
||||
// (D3DPolyRender::FlushAlphaList, DrawCells pc:432722) — so translucent
|
||||
// (D3DPolyRender::FlushAlphaList, DrawCells pc:432722) — so translucent
|
||||
// landscape content (portal swirl meshes, flame particles) composites
|
||||
// AFTER the building look-ins. Our dispatcher draws translucency inside
|
||||
// each Draw call, so the stage is split in TWO phases instead: EARLY =
|
||||
|
|
@ -609,12 +609,12 @@ internal sealed class RetailPViewRenderer
|
|||
// LATE = outside-stage dynamics' meshes + ALL scene particles +
|
||||
// weather. Content drawn early and overlapped by a look-in aperture
|
||||
// was otherwise overpainted by the far interior (translucents write no
|
||||
// depth to protect themselves) — the portal-swirl/candle-flame class.
|
||||
// depth to protect themselves) — the portal-swirl/candle-flame class.
|
||||
int probeSliceIndex = 0;
|
||||
foreach (var slice in clipAssembly.OutsideViewSlices)
|
||||
{
|
||||
passes.SetTerrainClip(slice.Planes);
|
||||
// T3 (BR-5): entities are never hard-clipped — retail viewcone-
|
||||
// T3 (BR-5): entities are never hard-clipped — retail viewcone-
|
||||
// CHECKS each mesh's sphere against the view (Ghidra 0x0054c250)
|
||||
// and draws it whole. The old per-slice entity clip routing
|
||||
// (gl_ClipDistance via SetClipRouting) is replaced by the sphere
|
||||
|
|
@ -663,7 +663,7 @@ internal sealed class RetailPViewRenderer
|
|||
});
|
||||
}
|
||||
|
||||
// #124: far-building look-ins draw HERE — still inside the landscape
|
||||
// #124: far-building look-ins draw HERE — still inside the landscape
|
||||
// stage (their punches mark against the terrain/exterior depth just
|
||||
// drawn), strictly BEFORE the depth clear + seals below, matching
|
||||
// retail's LScape::draw placement (DrawCells pc:432719 vs 432732/432785).
|
||||
|
|
@ -675,11 +675,11 @@ internal sealed class RetailPViewRenderer
|
|||
frameEntityPasses,
|
||||
in frameView);
|
||||
|
||||
// LATE phase (per slice): outside-stage dynamics' meshes (#118 — drawn
|
||||
// LATE phase (per slice): outside-stage dynamics' meshes (#118 — drawn
|
||||
// pre-clear so the seal protects their aperture pixels; AFTER the
|
||||
// look-ins so a translucent portal mesh blends over a far interior
|
||||
// instead of being overpainted) + the scene-particle owners (statics +
|
||||
// dynamics cone survivors — flames ride here for the same reason).
|
||||
// dynamics cone survivors — flames ride here for the same reason).
|
||||
probeSliceIndex = 0;
|
||||
foreach (var slice in clipAssembly.OutsideViewSlices)
|
||||
{
|
||||
|
|
@ -700,7 +700,7 @@ internal sealed class RetailPViewRenderer
|
|||
if (ownerPass)
|
||||
_lateParticleOwnerScratch.Add(e.Id);
|
||||
// #131 owner watchlist (throwaway): ACDREAM_DUMP_ENTITY ids
|
||||
// double as an ENTITY-id watchlist here — one line per watched
|
||||
// double as an ENTITY-id watchlist here — one line per watched
|
||||
// outdoor-static owner per CHANGE of its cone verdict.
|
||||
passes.EmitOutStageOwner(
|
||||
e,
|
||||
|
|
@ -764,11 +764,11 @@ internal sealed class RetailPViewRenderer
|
|||
});
|
||||
}
|
||||
|
||||
// #131: UNATTACHED emitters (AttachedObjectId == 0 — portal swirls,
|
||||
// #131: UNATTACHED emitters (AttachedObjectId == 0 — portal swirls,
|
||||
// campfires, ground effects anchored at a position) have no owner id
|
||||
// to ride any of the id-filtered particle passes. The outdoor root
|
||||
// has the dedicated T3 pass for them; an INTERIOR root had NO pass
|
||||
// at all. Draw them ONCE per frame (not per slice — alpha particles
|
||||
// at all. Draw them ONCE per frame (not per slice — alpha particles
|
||||
// must not double-draw, the #121 lesson), at the END of the landscape
|
||||
// stage: after the clear they would z-fail against the doorway seal.
|
||||
if (!ctx.RootCell.IsOutdoorNode)
|
||||
|
|
@ -780,7 +780,7 @@ internal sealed class RetailPViewRenderer
|
|||
passes.FlushLandscapeAlpha();
|
||||
|
||||
// T1: retail clears the FULL depth buffer ONCE between the outside
|
||||
// stage and the interior stage (PView::DrawCells, Ghidra 0x005a4840 —
|
||||
// stage and the interior stage (PView::DrawCells, Ghidra 0x005a4840 —
|
||||
// Clear gated on portalsDrawnCount; exact gate semantics is a plan
|
||||
// open question, staged as "any outside slice drawn"), then re-stamps
|
||||
// every outside-leading portal's TRUE depth (the seals,
|
||||
|
|
@ -819,20 +819,20 @@ internal sealed class RetailPViewRenderer
|
|||
IRetailPViewPassExecutor passes,
|
||||
PortalVisibilityFrame pvFrame)
|
||||
{
|
||||
// T1 (fused BR-2/3): retail DrawCells Loop 2 — every visible cell's
|
||||
// shell drawn WHOLE, reverse cell_draw_list (far→near), drawn once.
|
||||
// T1 (fused BR-2/3): retail DrawCells Loop 2 — every visible cell's
|
||||
// shell drawn WHOLE, reverse cell_draw_list (far→near), drawn once.
|
||||
// Retail NEVER clips cell geometry: the production path is the
|
||||
// prebuilt mesh (DrawEnvCell use_built_mesh, pc:427905; the
|
||||
// planeMask=0xffffffff legacy submit means skip-all-edges), and
|
||||
// aperture exactness comes from the punch/seal depth writes + the
|
||||
// z-buffer + this order. The former gl_ClipDistance chop
|
||||
// (927fd8f/9ce335e, #114) is deleted with this rewrite.
|
||||
// Per-cell opaque+transparent keeps the far→near transparent
|
||||
// Per-cell opaque+transparent keeps the far→near transparent
|
||||
// compositing the per-cell loop already provided.
|
||||
passes.UseIndoorMembershipOnlyRouting();
|
||||
|
||||
// Opaque: ONE batched Render for all shell cells (was one heavy per-frame
|
||||
// Render call PER cell — the dense-town FPS sink, ~94 calls/24.75ms at
|
||||
// Render call PER cell — the dense-town FPS sink, ~94 calls/24.75ms at
|
||||
// Arwic). Opaque needs no draw order (z-buffer), and lighting is
|
||||
// per-instance (CellId-keyed light SSBO in EnvCellRenderer.RenderModernMDI-
|
||||
// Internal), so cross-cell batching is visually identical. The filtered
|
||||
|
|
@ -856,16 +856,16 @@ internal sealed class RetailPViewRenderer
|
|||
passes.DrawTransparentCellShellsOrdered(_orderedTransparentShellCells);
|
||||
}
|
||||
|
||||
// T1: the frame's single LAST entity pass — ALL server-spawned dynamics
|
||||
// T1: the frame's single LAST entity pass — ALL server-spawned dynamics
|
||||
// (player, NPCs, doors, items), indoor or out, drawn after the static
|
||||
// world + punches + interior cells. Depth-tested, never hard-clipped
|
||||
// (retail draws objects per cell AFTER cells and viewcone-culls them —
|
||||
// (retail draws objects per cell AFTER cells and viewcone-culls them —
|
||||
// PView::DrawCells epilogue Ghidra 0x005a4840; the sphere-vs-view cull is
|
||||
// T3). Drawing dynamics last is what makes the aperture punch safe.
|
||||
// T3 (BR-5): each dynamic is viewcone-culled like retail — sphere vs its
|
||||
// T3 (BR-5): each dynamic is viewcone-culled like retail — sphere vs its
|
||||
// cell's views; outdoor/unresolved vs the outside views (pass-all under
|
||||
// the outdoor root's full-screen outside view). A dynamic in a NON-flooded
|
||||
// room culls HERE — retail never reaches an object whose cell is not in
|
||||
// room culls HERE — retail never reaches an object whose cell is not in
|
||||
// the draw list; the partition keeps routing it so the CULL (not the
|
||||
// visibility set) drops it, exactly retail's shape.
|
||||
private void DrawDynamicsLast(
|
||||
|
|
@ -928,12 +928,12 @@ internal sealed class RetailPViewRenderer
|
|||
&& AcDream.App.Streaming.EntityVanishProbe.PlayerGuid != 0
|
||||
&& e.ServerGuid == AcDream.App.Streaming.EntityVanishProbe.PlayerGuid;
|
||||
// #118: under an interior root, outdoor-classified dynamics drew in
|
||||
// the outside stage (pre-clear, seal-protected) — retail draws them
|
||||
// the outside stage (pre-clear, seal-protected) — retail draws them
|
||||
// via LScape::draw's per-landcell DrawSortCell, never in the
|
||||
// post-seal cell-object epilogue (PView::DrawCells pc:432719 vs
|
||||
// pc:432878). Drawing them here instead z-fails them against the
|
||||
// seal. Indoor dynamics (incl. exit-portal straddlers, which drew
|
||||
// in BOTH stages) stay — this pass is retail's loop C.
|
||||
// in BOTH stages) stay — this pass is retail's loop C.
|
||||
if (!rootIsOutdoor && !indoor)
|
||||
{
|
||||
if (isProbePlayer)
|
||||
|
|
@ -976,12 +976,12 @@ internal sealed class RetailPViewRenderer
|
|||
visibleCellIds: null);
|
||||
|
||||
// #121: dynamics' attached emitters (portal swirls, creature effects)
|
||||
// gate through the SAME cone-surviving owner set as their meshes —
|
||||
// gate through the SAME cone-surviving owner set as their meshes —
|
||||
// retail draws emitters with the owner object. Before this callback,
|
||||
// dynamics' emitters fell through EVERY particle filter under the pview
|
||||
// path (the landscape slice carries outdoor statics + #118 outside-
|
||||
// stage dynamics; the cell callback carries cell statics; T4 deleted
|
||||
// the old clipRoot==null global pass from normal frames) — all world
|
||||
// the old clipRoot==null global pass from normal frames) — all world
|
||||
// portals went invisible. Outside-stage dynamics are excluded here:
|
||||
// their emitters already drew in the landscape slice (alpha-blended
|
||||
// particles must not double-draw, unlike the depth-idempotent meshes).
|
||||
|
|
@ -1049,29 +1049,29 @@ internal sealed class RetailPViewRenderer
|
|||
return;
|
||||
}
|
||||
|
||||
// T1: per-cell STATIC object lists only (dat-baked 0x40 statics) —
|
||||
// dynamics moved to DrawDynamicsLast. Far→near with the cells, after
|
||||
// the shells (retail DrawCells epilogue: PortalList = cell's views →
|
||||
// T1: per-cell STATIC object lists only (dat-baked 0x40 statics) —
|
||||
// dynamics moved to DrawDynamicsLast. Far→near with the cells, after
|
||||
// the shells (retail DrawCells epilogue: PortalList = cell's views →
|
||||
// DrawObjCell, Ghidra 0x005a4840). T3 (BR-5): each static's sphere is
|
||||
// tested against ITS CELL's views (retail viewconeCheck) — the
|
||||
// tested against ITS CELL's views (retail viewconeCheck) — the
|
||||
// statics-through-walls fix: a static whose sphere is outside every
|
||||
// view of its cell no longer paints through the wall (the cottage
|
||||
// phantom staircase's draw path).
|
||||
// Dense-town FPS iteration-1 (spec 2026-06-23-cellobject-draw-batching):
|
||||
// the per-cell DrawEntityBucket calls below were the top CPU sink at Arwic
|
||||
// (cellobjects ~3.5 ms/frame; each WbDrawDispatcher.Draw orphans 6 SSBOs +
|
||||
// full state setup). Collapse them into ONE cross-cell batched draw — the
|
||||
// full state setup). Collapse them into ONE cross-cell batched draw — the
|
||||
// shipped cells-shell batching pattern applied to cell OBJECTS. Two loops
|
||||
// preserve the statics-before-particles depth order: loop 1 culls +
|
||||
// accumulates every cell's survivors and draws them once; loop 2 runs the
|
||||
// per-cell particle passes AFTER the statics own the depth buffer (particles
|
||||
// depth-test but write no depth). The dispatcher sorts opaque front-to-back
|
||||
// and transparent back-to-front by group distance (WbDrawDispatcher.cs:
|
||||
// 1469-1470), so cross-cell batching composites correctly — equal-or-better
|
||||
// 1469-1470), so cross-cell batching composites correctly — equal-or-better
|
||||
// than the old per-cell-bucketed order. visibleCellIds = the union of cells,
|
||||
// so the dispatcher admits exactly the same survivor set.
|
||||
|
||||
// Loop 1: per-cell viewcone cull → accumulate survivors + the union of cells.
|
||||
// Loop 1: per-cell viewcone cull → accumulate survivors + the union of cells.
|
||||
_allCellStatics.Clear();
|
||||
_cellObjCells.Clear();
|
||||
for (int i = pvFrame.OrderedVisibleCells.Count - 1; i >= 0; i--)
|
||||
|
|
@ -1099,7 +1099,7 @@ internal sealed class RetailPViewRenderer
|
|||
}
|
||||
|
||||
// ONE batched static-object draw for every visible cell (was N per-cell
|
||||
// WbDrawDispatcher.Draw calls). T1: per-cell STATIC lists only — dynamics
|
||||
// WbDrawDispatcher.Draw calls). T1: per-cell STATIC lists only — dynamics
|
||||
// draw in DrawDynamicsLast. T3 (BR-5): each static was sphere-tested against
|
||||
// ITS cell's views above (the statics-through-walls fix is preserved by the
|
||||
// cull; only the draw is batched).
|
||||
|
|
@ -1124,20 +1124,20 @@ internal sealed class RetailPViewRenderer
|
|||
_cellObjCells);
|
||||
}
|
||||
|
||||
// Cell-particle pass — consolidated across ALL visible cells into ONE
|
||||
// Cell-particle pass — consolidated across ALL visible cells into ONE
|
||||
// draw. Was per-cell, and each call re-walked the ENTIRE live particle set
|
||||
// (RetailPViewPassExecutor.DrawCellParticles → ParticleRenderer.Draw enumerates every
|
||||
// live emitter), i.e. O(cells × particles) — the dense-town cellobjects
|
||||
// (RetailPViewPassExecutor.DrawCellParticles → ParticleRenderer.Draw enumerates every
|
||||
// live emitter), i.e. O(cells × particles) — the dense-town cellobjects
|
||||
// sink (~5 ms at Arwic). Static owners are disjoint per cell, so the UNION
|
||||
// (= _allCellStatics, already accumulated above for the batched draw) draws
|
||||
// EXACTLY the same emitters: the callback gates on owner id (the cone-
|
||||
// surviving set), the renderer sorts globally back-to-front, and the per-
|
||||
// cell slice was never used for clipping (the scissor gate was deleted in
|
||||
// T3 — RetailPViewPassExecutor.DrawCellParticles disables clip distances). Runs after
|
||||
// T3 — RetailPViewPassExecutor.DrawCellParticles disables clip distances). Runs after
|
||||
// the batched static draw so emitters depth-test against the statics now in
|
||||
// the buffer (the statics-before-particles order). cellId/slice are unused
|
||||
// by the particle pass — pass NoClipSlice + the union owner list. This also
|
||||
// drops the per-cell BuildDrawList allocations (N → 1).
|
||||
// by the particle pass — pass NoClipSlice + the union owner list. This also
|
||||
// drops the per-cell BuildDrawList allocations (N → 1).
|
||||
if (frameEntityPasses is not null
|
||||
|| _allCellStatics.Count > 0)
|
||||
{
|
||||
|
|
@ -1199,7 +1199,7 @@ internal sealed class RetailPViewRenderer
|
|||
private readonly List<WorldEntity> _cellStaticScratch = new();
|
||||
private readonly List<WorldEntity> _dynamicsScratch = new();
|
||||
// #118: dynamics assigned to the OUTSIDE stage this frame (interior roots
|
||||
// only) — outdoor-classified + exit-portal straddlers. Cleared per frame.
|
||||
// only) — outdoor-classified + exit-portal straddlers. Cleared per frame.
|
||||
private readonly List<WorldEntity> _outsideStageDynamics = new();
|
||||
// Dense-town FPS iteration-1 (cellobject batching): all visible cells'
|
||||
// viewcone-surviving statics accumulated for ONE batched DrawEntityBucket,
|
||||
|
|
@ -1274,17 +1274,17 @@ internal sealed class RetailPViewRenderer
|
|||
|
||||
/// <summary>
|
||||
/// #118 stage assignment for a dynamic under an INTERIOR root: does it draw
|
||||
/// in the OUTSIDE (landscape) stage — before the gated depth clear and the
|
||||
/// exit-portal seals — like retail's per-landcell object draw
|
||||
/// (LScape::draw → DrawBlock 0x005a17c0 → DrawSortCell pc:430124, run at
|
||||
/// in the OUTSIDE (landscape) stage — before the gated depth clear and the
|
||||
/// exit-portal seals — like retail's per-landcell object draw
|
||||
/// (LScape::draw → DrawBlock 0x005a17c0 → DrawSortCell pc:430124, run at
|
||||
/// the top of PView::DrawCells pc:432719)?
|
||||
///
|
||||
/// True for outdoor-classified dynamics (their fragments lie beyond the
|
||||
/// door plane and would z-fail the seal in the last pass), and for INDOOR
|
||||
/// dynamics whose sphere straddles an exit-portal plane of their flood-
|
||||
/// visible cell — retail draws an object once per overlapped shadow cell
|
||||
/// visible cell — retail draws an object once per overlapped shadow cell
|
||||
/// (DrawBlock pc:430056-430064), so a threshold-straddling body draws in
|
||||
/// both stages and neither half clips at the plane. Pure — also driven
|
||||
/// both stages and neither half clips at the plane. Pure — also driven
|
||||
/// headlessly by HouseExitWalkReplayTests as the ordering contract.
|
||||
/// </summary>
|
||||
public static bool DynamicDrawsInOutsideStage(
|
||||
|
|
@ -1299,7 +1299,7 @@ internal sealed class RetailPViewRenderer
|
|||
|
||||
uint cellId = parentCellId!.Value;
|
||||
if (!drawableCells.Contains(cellId))
|
||||
return false; // not in the flood — the last-pass cone cull owns it
|
||||
return false; // not in the flood — the last-pass cone cull owns it
|
||||
var cell = cells.Find(cellId);
|
||||
if (cell is null)
|
||||
return false;
|
||||
|
|
@ -1320,7 +1320,7 @@ internal sealed class RetailPViewRenderer
|
|||
return false;
|
||||
}
|
||||
|
||||
// Conservative bounding sphere from the entity's cached AABB — the same
|
||||
// Conservative bounding sphere from the entity's cached AABB — the same
|
||||
// bounds source the dispatcher's frustum cull uses.
|
||||
private static void EntitySphere(WorldEntity e, out Vector3 center, out float radius)
|
||||
{
|
||||
|
|
@ -1343,7 +1343,7 @@ internal sealed class RetailPViewRenderer
|
|||
|
||||
}
|
||||
|
||||
internal interface IRetailPViewCellSource
|
||||
public interface IRetailPViewCellSource
|
||||
{
|
||||
LoadedCell? Find(uint cellId);
|
||||
}
|
||||
|
|
@ -1354,7 +1354,7 @@ internal interface IRetailPViewCellSource
|
|||
/// pass only; visibility construction and draw ordering remain renderer-owned.
|
||||
/// All frame inputs and results are borrowed for the duration of the call.
|
||||
/// </summary>
|
||||
internal interface IRetailPViewPassExecutor
|
||||
public interface IRetailPViewPassExecutor
|
||||
{
|
||||
void AbortFrame();
|
||||
void BeginFrame();
|
||||
|
|
@ -1565,7 +1565,7 @@ internal sealed class BuildingGroupScratch
|
|||
}
|
||||
}
|
||||
|
||||
internal sealed class RetailPViewFrameInput
|
||||
public sealed class RetailPViewFrameInput
|
||||
{
|
||||
public LoadedCell RootCell { get; private set; } = null!;
|
||||
|
||||
|
|
@ -1669,7 +1669,7 @@ internal sealed class RetailPViewFrameInput
|
|||
/// frame objects are deliberately reused to keep the render loop allocation
|
||||
/// free; consumers must copy any state they need to retain asynchronously.
|
||||
/// </summary>
|
||||
internal sealed class RetailPViewFrameResult
|
||||
public sealed class RetailPViewFrameResult
|
||||
{
|
||||
public PortalVisibilityFrame PortalFrame { get; private set; } = null!;
|
||||
public ClipFrameAssembly ClipAssembly { get; private set; } = null!;
|
||||
|
|
@ -1712,17 +1712,17 @@ internal sealed class RetailPViewFrameResult
|
|||
diagnosticPartition);
|
||||
}
|
||||
|
||||
internal readonly record struct RetailPViewLandscapeSliceContext(
|
||||
public readonly record struct RetailPViewLandscapeSliceContext(
|
||||
ClipViewSlice Slice,
|
||||
IReadOnlyList<WorldEntity> OutdoorEntities)
|
||||
{
|
||||
internal RenderFrameEntityDrawRequest? EntityDraw { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>#131/#132: the late landscape phase's per-slice payload —
|
||||
/// <summary>#131/#132: the late landscape phase's per-slice payload —
|
||||
/// outside-stage dynamics to mesh-draw, plus the full scene-particle owner
|
||||
/// set (statics + dynamics cone survivors) the attached-emitter filter keys on.</summary>
|
||||
internal readonly record struct RetailPViewLandscapeLateSliceContext(
|
||||
public readonly record struct RetailPViewLandscapeLateSliceContext(
|
||||
ClipViewSlice Slice,
|
||||
IReadOnlyList<WorldEntity> Dynamics,
|
||||
IReadOnlySet<uint> ParticleOwnerIds)
|
||||
|
|
@ -1730,7 +1730,7 @@ internal readonly record struct RetailPViewLandscapeLateSliceContext(
|
|||
internal RenderFrameEntityDrawRequest? EntityDraw { get; init; }
|
||||
}
|
||||
|
||||
internal readonly record struct RetailPViewCellSliceContext(
|
||||
public readonly record struct RetailPViewCellSliceContext(
|
||||
uint CellId,
|
||||
ClipViewSlice Slice,
|
||||
IReadOnlySet<uint> ParticleOwnerIds);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue