feat(render): Campaign V slice V4a - port TextRenderer/BitmapFont/DebugLineRenderer/TextureCache onto IGpuDevice
TextRenderer, BitmapFont, DebugLineRenderer, and TextureCache's UI-texture
upload path (GetOrUploadRenderSurface/UploadRgba8) now issue every draw and
resource creation through the pinned IGpuDevice/IGpuFrame/IGpuPassEncoder
RHI contract instead of raw GL. This is the RHI's first real production
consumer - V0-V3 only established the contract, GL backend skeleton, and a
shader-dialect migration with no live GL exercise. TextRenderer owns one
IGpuPipeline (ui_text shader, straight-alpha blend, depth disabled) and
allocates a per-bucket ring each Flush; BitmapFont's atlas texture is
created and uploaded via device.CreateTexture/.Upload; DebugLineRenderer
mirrors the same one-pipeline-per-Flush shape for its line-list draws.
World-path TextureCache methods (GetOrUpload, the raw-GL layer-array
upload) are untouched - still legacy GL, still out of scope.
Frame lifecycle: GpuDeviceFrameLifetime (RenderFrameOrchestrator.cs) wraps
IGpuDevice.BeginFrame()/IGpuFrame.End() inside the existing
IRenderFrameLifetime bracket HostInputCameraCompositionPhase already opens
per callback, additively - no frame-graph restructuring. Ported renderers
reach the frame via ICurrentGpuFrameSource, a plain interface (not a
delegate field) so WorldSceneDiagnosticsController keeps passing its
existing "no stored window/delegate" architectural-conformance test.
Two real bugs surfaced by actually exercising the RHI against a live GL
context (nothing here was previously reachable before this slice):
- GlGpuDevice.BeginFrame() now resets the render-state cache every frame.
The cache assumes it is the sole writer of GL program/blend/depth/cull
state, which was true while it had zero real consumers, but every
still-legacy renderer (WbDrawDispatcher, terrain, particles, EnvCells)
mutates that same GL state directly and never informs the cache. Once a
legacy renderer ran between two RHI binds, the cache's belief about the
current GL program went stale, so a later BindPipeline(text shader)
skipped re-issuing glUseProgram and the following push-constant upload
threw GL_INVALID_OPERATION against whatever program was actually bound.
Reset() at the frame boundary is the same defensive move BeginPass
already makes after a forced clear (see its comment); it costs one
redundant state application on the frame's first bind.
- GL_MULTISAMPLE has no representation in the pinned contract. Added a
GL-backend-internal Multisample field to GlRenderStateSnapshot/Changes,
computed from GpuPipelineDescription.SampleCount at BindPipeline time -
mirrors how Vulkan bakes MSAA into the pipeline instead of a separate
toggle.
Collateral, scoped to keep the port real rather than a stub:
- GpuTextureSlot (Unassigned = uint.MaxValue, NOT 0) now flows through
every consumer of TextureCache.GetOrUploadRenderSurface/UploadRgba8 and
TextRenderer.DrawSprite - the entire retained UI layer, since a pervasive
Func<uint,(uint,int,int)> sprite-resolve delegate threads through nearly
every UI element/controller. Every prior `== 0` / `!= 0` "no texture"
check became `.IsAssigned` / `!.IsAssigned`; slot 0 is a real assigned
slot (the device's default white texture), so the old sentinel would
have produced live visual regressions if left in place.
- GpuTextureSlot/IGpuDevice/IGpuFrame are internal, so ~270 previously
public AcDream.App types that touched them (directly or transitively)
are now internal too - safe, since AcDream.App is an exe with no
external project references; only the two test projects consume it, via
InternalsVisibleTo. A handful of unrelated types the sweep caught
(ElementInfo/ImportedLayout's property-bag hierarchy, several enums used
as public [Theory] parameters, CursorFeedbackSnapshot's DragAcceptState)
were reverted back to public where making them internal would have
either cascaded into unrelated files or broken xUnit's public-member
discovery.
- ExternalViewportTextureBridge (new) registers the still-raw-GL FBO
color textures PrivateEntityViewportRenderer/PaperdollViewportRenderer
produce (V4g's scope) into the device's texture table for
UiViewport.TextureHandle, via a temporary
GlGpuDevice.RegisterExternalColorTexture escape hatch (internal, not
part of IGpuDevice) deleted when V4g ports those viewports.
- TextRenderGlStateScope.cs and its test deleted: the pipeline description
now bakes what it used to restore by hand.
- ResourceCleanupGroupTests/GlTextureOwnershipTests: the two source-text
conformance tests keyed to TextRenderer's old multi-resource
construction shape (Shader + per-flight FrameBufferSet array + white
texture + tracked VAO/VBO, all via ResourceCleanupGroup) no longer apply
- that shape is gone, replaced by one IGpuPipeline created through
IGpuDevice. The construction-order test is deleted; the checked-commit
texture-creation check now targets GlGpuTexture (which already used
the same GlResourceCommand.CreateName primitive before this slice).
Gates:
- dotnet build -c Release: 0 warnings, 0 errors (AcDream.App has
TreatWarningsAsErrors).
- dotnet test tests/AcDream.App.Tests -c Release: 3,840 passed / 3
skipped (was 3,843/3 entering this slice - net 3 fewer tests:
TextRendererFailureSafetyTests.cs deleted (2, tested the now-deleted
TextRenderGlStateScope) plus the one retired ResourceCleanupGroupTests
method). Full solution: 8,908 passed / 5 skipped across all nine test
projects.
- Offline pixel gate (tools/run-offline-pixel-gate.ps1, parent ec414d60
vs this commit): differing fraction 0.318% (1,791/563,200 compared
pixels), above the 0.001 threshold. Investigated pixel-by-pixel rather
than waved through: a diff heatmap plus 4x crops at the differing
clusters show zero differences anywhere in the retained UI, terrain,
scenery, or static meshes - every differing pixel sits on continuously-
animated ambient content (flying-insect sprites over the swamp, foliage
sparkle/dew glints) whose exact phase depends on elapsed wall-clock
time, the same category the gate's own sky-masking rationale already
documents and the campaign doc's coverage table explicitly excludes
("Not covered - particles"). Confirming evidence: two same-commit
captures at HEAD compare clean against each other (0.0025%), and two
same-commit captures at the parent compare clean against each other
(0.0044%) - only base-vs-head is consistently elevated, which is what
frame-pacing drift from genuinely new per-frame RHI work (BeginFrame,
ring resets, the render-state reset above) would produce against a
fixed wall-clock capture deadline, not a rendering defect. Recommend a
quick user visual check of this capture pair alongside the automated
result, matching how V2c's particle work was already handled in this
campaign (flagged for user visual confirmation rather than blocked on
an automated gate that cannot cover animated content).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
ec414d60cd
commit
ceec3bc440
334 changed files with 3660 additions and 3840 deletions
|
|
@ -1,4 +1,4 @@
|
|||
// Phase A8 (2026-05-28): port of WB's EnvCellRenderManager. This is the
|
||||
// Phase A8 (2026-05-28): port of WB's EnvCellRenderManager. This is the
|
||||
// production cell-rendering pipeline for indoor visibility, replacing the
|
||||
// broken "cell as WorldEntity with MeshRef(envCellId)" approach that the
|
||||
// four reverted RR7 variants couldn't fix.
|
||||
|
|
@ -12,7 +12,7 @@
|
|||
//
|
||||
// Note: we do NOT inherit from WB's ObjectRenderManagerBase. That base
|
||||
// class owns the landblock-streaming loop (Update, _pendingGeneration,
|
||||
// _uploadQueue). acdream's StreamingController already does that work —
|
||||
// _uploadQueue). acdream's StreamingController already does that work —
|
||||
// running a parallel loop would compete for dat I/O. Instead, streaming builds
|
||||
// a private EnvCellLandblockBuild and CommitLandblock publishes the completed
|
||||
// snapshot on the render thread.
|
||||
|
|
@ -29,7 +29,7 @@ using Silk.NET.OpenGL;
|
|||
|
||||
namespace AcDream.App.Rendering.Wb;
|
||||
|
||||
public sealed unsafe class EnvCellRenderer :
|
||||
internal sealed unsafe class EnvCellRenderer :
|
||||
IDisposable,
|
||||
IEnvCellLandblockPublisher
|
||||
{
|
||||
|
|
@ -39,7 +39,7 @@ public sealed unsafe class EnvCellRenderer :
|
|||
private readonly WbFrustum _frustum;
|
||||
|
||||
// Per-landblock storage. Key = full 32-bit landblock dat id (e.g. 0xA9B4FFFF).
|
||||
// WB EnvCellRenderManager.cs:75 uses ConcurrentDictionary<ushort, ObjectLandblock> _landblocks —
|
||||
// WB EnvCellRenderManager.cs:75 uses ConcurrentDictionary<ushort, ObjectLandblock> _landblocks —
|
||||
// we use uint (full LB id) because acdream uses 32-bit landblock keys throughout.
|
||||
private readonly ConcurrentDictionary<uint, EnvCellLandblock> _landblocks = new();
|
||||
|
||||
|
|
@ -64,7 +64,7 @@ public sealed unsafe class EnvCellRenderer :
|
|||
private Matrix4x4 _lastViewProjection = Matrix4x4.Identity;
|
||||
private bool _initialized;
|
||||
|
||||
// List pool — copied from WB ObjectRenderManagerBase.
|
||||
// List pool — copied from WB ObjectRenderManagerBase.
|
||||
// WB ObjectRenderManagerBase.cs:83-86: protected readonly List<List<InstanceData>> _listPool = new(); protected int _poolIndex = 0;
|
||||
private readonly List<List<InstanceData>> _listPool = new();
|
||||
private int _poolIndex = 0;
|
||||
|
|
@ -78,7 +78,7 @@ public sealed unsafe class EnvCellRenderer :
|
|||
private readonly ThreadLocal<PrepareScratch> _prepareScratch =
|
||||
new(() => new PrepareScratch(), trackAllValues: true);
|
||||
|
||||
// Modern-MDI scratch buffers (single slot — we re-upload every frame).
|
||||
// Modern-MDI scratch buffers (single slot — we re-upload every frame).
|
||||
// WB BaseObjectRenderManager.cs:43-48: _scratchMdiCommandBuffers, _scratchModernBatchBuffers, _modernInstanceBuffers
|
||||
// We collapse the ring-of-3 to a single slot since we have no persistent/consolidated draws.
|
||||
private uint _mdiCommandBuffer;
|
||||
|
|
@ -95,7 +95,7 @@ public sealed unsafe class EnvCellRenderer :
|
|||
// Phase U.3: per-instance clip-slot SSBO (binding=3), parallel to
|
||||
// _modernInstanceBuffer. One uint per instance selecting its CellClip slot,
|
||||
// indexed by the same BaseInstance + gl_InstanceID the shader uses for
|
||||
// binding=0. ALL ZEROS in U.3 ⇒ slot 0 ⇒ no-clip. U.4 populates real slots.
|
||||
// binding=0. ALL ZEROS in U.3 ⇒ slot 0 ⇒ no-clip. U.4 populates real slots.
|
||||
private uint _clipSlotBuffer;
|
||||
private int _clipSlotCapacity;
|
||||
private uint[] _clipSlotData = Array.Empty<uint>();
|
||||
|
|
@ -156,17 +156,17 @@ public sealed unsafe class EnvCellRenderer :
|
|||
// Campaign V slice V2 (2026-07-27): GL-only emulation of the eventual
|
||||
// Vulkan global texture descriptor array (binding=9,
|
||||
// GpuBindingModel.StorageTextureTable). Owns its own table rather than
|
||||
// sharing WbDrawDispatcher's — EnvCellRenderer never had a TextureCache
|
||||
// sharing WbDrawDispatcher's — EnvCellRenderer never had a TextureCache
|
||||
// dependency and nothing requires index agreement between renderers (each
|
||||
// rebinds its own buffer to binding=9 immediately before its own draw
|
||||
// call). See GlBindlessHandleTable's doc comment and the campaign doc's
|
||||
// §5.2. Lazily created; grown/uploaded only when a genuinely new handle
|
||||
// appears (rare — see FlushAndBindTextureTable).
|
||||
// §5.2. Lazily created; grown/uploaded only when a genuinely new handle
|
||||
// appears (rare — see FlushAndBindTextureTable).
|
||||
private readonly GlBindlessHandleTable _textureTable = new();
|
||||
private uint _textureTableSsbo;
|
||||
private int _textureTableSsboCapacityBytes;
|
||||
|
||||
// Reusable scratch arrays — avoid per-frame allocation.
|
||||
// Reusable scratch arrays — avoid per-frame allocation.
|
||||
// WB BaseObjectRenderManager.cs:58-59: private DrawElementsIndirectCommand[] _commands = Array.Empty<...>()
|
||||
private DrawElementsIndirectCommand[] _commands = Array.Empty<DrawElementsIndirectCommand>();
|
||||
private ModernBatchData[] _modernBatches = Array.Empty<ModernBatchData>();
|
||||
|
|
@ -192,7 +192,7 @@ public sealed unsafe class EnvCellRenderer :
|
|||
private readonly Dictionary<ulong, List<InstanceData>> _activeSnapshotGlobalGroups = new();
|
||||
private readonly List<ulong> _activeSnapshotGlobalGfxObjIds = new();
|
||||
|
||||
// Static render-state tracking — matches WB BaseObjectRenderManager.cs:24-28.
|
||||
// Static render-state tracking — matches WB BaseObjectRenderManager.cs:24-28.
|
||||
// Shared across all manager instances on the same GL context.
|
||||
private static uint _currentVao;
|
||||
private static CullMode? _currentCullMode;
|
||||
|
|
@ -204,8 +204,8 @@ public sealed unsafe class EnvCellRenderer :
|
|||
// inputs changed: landblock commits/removals (NeedsPrepare), the visible-cell
|
||||
// filter, the trim window, mesh render-data availability (the snapshot bakes
|
||||
// per-cell transparency from TryGetRenderData), or the view-projection.
|
||||
// NeedsPrepare existed since A8 but was never read — this wires it. The VP
|
||||
// tolerance must swallow the ~36 µm eye rest jitter (RetailPViewRenderer
|
||||
// NeedsPrepare existed since A8 but was never read — this wires it. The VP
|
||||
// tolerance must swallow the ~36 µm eye rest jitter (RetailPViewRenderer
|
||||
// R-A2 note) while any real camera motion crosses it in the same frame.
|
||||
private Matrix4x4 _preparedViewProjection;
|
||||
private Vector3 _preparedCameraPosition;
|
||||
|
|
@ -223,14 +223,14 @@ public sealed unsafe class EnvCellRenderer :
|
|||
public bool IsDisposed { get; private set; }
|
||||
|
||||
public LastFrameStats Stats => _lastFrameStats;
|
||||
public struct LastFrameStats { public int CellsRendered; public int TrianglesDrawn; }
|
||||
internal struct LastFrameStats { public int CellsRendered; public int TrianglesDrawn; }
|
||||
private LastFrameStats _lastFrameStats;
|
||||
|
||||
/// <summary>
|
||||
/// Diagnostic accessor for the [envcells] probe (Phase A8 apparatus 2026-05-28).
|
||||
/// Returns (pool-list count total, snapshot's PostPreparePoolIndex high-water).
|
||||
/// A divergence between expected and actual values would indicate a pool-
|
||||
/// management regression — exactly the bug class the 2026-05-28 audit caught.
|
||||
/// management regression — exactly the bug class the 2026-05-28 audit caught.
|
||||
/// </summary>
|
||||
public (int PoolTotal, int SnapshotPoolHwm) GetPoolDiagnostics()
|
||||
{
|
||||
|
|
@ -338,20 +338,20 @@ public sealed unsafe class EnvCellRenderer :
|
|||
public void SetClipRegionSsbo(uint sharedClipRegionSsbo)
|
||||
=> _sharedClipRegionSsbo = sharedClipRegionSsbo;
|
||||
|
||||
// Phase U.4: per-frame cellId→CellClip-slot map for the cell shells. When
|
||||
// Phase U.4: per-frame cellId→CellClip-slot map for the cell shells. When
|
||||
// non-null, RenderModernMDIInternal writes instanceClipSlot[i] =
|
||||
// _cellIdToSlot[allInstances[i].CellId] so each cell's shell instances are
|
||||
// gated to that cell's portal-clip region. When null (U.3 path), every
|
||||
// instance maps to slot 0 (no-clip). A cell absent from the map writes slot 0
|
||||
// (no-clip) — but the caller's Render filter already restricts the draw to the
|
||||
// (no-clip) — but the caller's Render filter already restricts the draw to the
|
||||
// map's keys, so that fallback should not fire in practice.
|
||||
private IReadOnlyDictionary<uint, int>? _cellIdToSlot;
|
||||
|
||||
/// <summary>
|
||||
/// Phase U.4: install the per-frame cellId→slot map used to gate cell shells
|
||||
/// Phase U.4: install the per-frame cellId→slot map used to gate cell shells
|
||||
/// to their portal-clip regions. Call once per frame BEFORE
|
||||
/// <see cref="Render(WbRenderPass, HashSet{uint}?)"/>. Pass null to revert to
|
||||
/// the U.3 no-clip behavior (every shell instance → slot 0).
|
||||
/// the U.3 no-clip behavior (every shell instance → slot 0).
|
||||
/// </summary>
|
||||
public void SetClipRouting(IReadOnlyDictionary<uint, int>? cellIdToSlot)
|
||||
=> _cellIdToSlot = cellIdToSlot;
|
||||
|
|
@ -386,7 +386,7 @@ public sealed unsafe class EnvCellRenderer :
|
|||
surfaces);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CommitLandblock — render-thread transaction boundary
|
||||
// CommitLandblock — render-thread transaction boundary
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -586,7 +586,7 @@ public sealed unsafe class EnvCellRenderer :
|
|||
int? renderRadius = null)
|
||||
{
|
||||
// Phase U.4 fix: stash the view-projection so Render() can upload it itself.
|
||||
// Stashed even when the gate below skips the rebuild — Render must always
|
||||
// Stashed even when the gate below skips the rebuild — Render must always
|
||||
// project with the CURRENT frame's matrix (the U.4 stale-matrix root cause).
|
||||
_lastViewProjection = viewProjection;
|
||||
|
||||
|
|
@ -614,7 +614,7 @@ public sealed unsafe class EnvCellRenderer :
|
|||
return;
|
||||
}
|
||||
|
||||
// Prepare gate: every snapshot input unchanged → keep the active snapshot.
|
||||
// Prepare gate: every snapshot input unchanged → keep the active snapshot.
|
||||
// (Same-thread discipline makes the version sample exact: publish, release
|
||||
// tickets, and this method all run on the render thread.)
|
||||
if (_hasPreparedSnapshot
|
||||
|
|
@ -633,7 +633,7 @@ public sealed unsafe class EnvCellRenderer :
|
|||
lock (_renderLock) { _poolIndex = 0; }
|
||||
|
||||
// WB skips _cameraLbX/Y update (from LandscapeDoc.Region) here in our variant
|
||||
// because we don't need camera-LB tracking for the snapshot — just frustum tests.
|
||||
// because we don't need camera-LB tracking for the snapshot — just frustum tests.
|
||||
|
||||
// WB EnvCellRenderManager.cs:262:
|
||||
// Filter loaded landblocks by GpuReady + Instances non-empty.
|
||||
|
|
@ -674,7 +674,7 @@ public sealed unsafe class EnvCellRenderer :
|
|||
|
||||
PrepareScratch scratch = _prepareScratch.Value!;
|
||||
|
||||
// WB EnvCellRenderManager.cs:279-295: fast path — LB fully inside.
|
||||
// WB EnvCellRenderManager.cs:279-295: fast path — LB fully inside.
|
||||
if (testResult == FrustumTestResult.Inside)
|
||||
{
|
||||
foreach (var (gfxObjId, instances) in lb.BuildingPartGroups)
|
||||
|
|
@ -692,7 +692,7 @@ public sealed unsafe class EnvCellRenderer :
|
|||
return;
|
||||
}
|
||||
|
||||
// WB EnvCellRenderManager.cs:298-324: slow path — per-cell frustum test.
|
||||
// WB EnvCellRenderManager.cs:298-324: slow path — per-cell frustum test.
|
||||
HashSet<uint> visibleCells = scratch.VisibleCells;
|
||||
visibleCells.Clear();
|
||||
foreach (var kvp in lb.EnvCellBounds)
|
||||
|
|
@ -804,13 +804,13 @@ public sealed unsafe class EnvCellRenderer :
|
|||
/// <summary>
|
||||
/// Pure half of the prepare gate's camera test (regression-tested without a
|
||||
/// GL context, same pattern as <see cref="CreateCommittedSnapshot"/>).
|
||||
/// Eye position uses a 1 mm ABSOLUTE epsilon: it swallows the ~36 µm rest
|
||||
/// Eye position uses a 1 mm ABSOLUTE epsilon: it swallows the ~36 µm rest
|
||||
/// jitter but dirties on any real movement (a slow walk moves 20+ mm/frame).
|
||||
/// Position must not be tested through the matrix — the view-projection's
|
||||
/// Position must not be tested through the matrix — the view-projection's
|
||||
/// translation row scales with world coordinates (~5e4 in AC), where a
|
||||
/// relative tolerance would mask sub-meter motion. Rows 1–3 of
|
||||
/// view × projection are position-independent (rotation × projection), so a
|
||||
/// relative 1e-5 there dirties at ≈0.001° of rotation and on any
|
||||
/// relative tolerance would mask sub-meter motion. Rows 1–3 of
|
||||
/// view × projection are position-independent (rotation × projection), so a
|
||||
/// relative 1e-5 there dirties at ≈0.001° of rotation and on any
|
||||
/// projection (FOV/aspect/near/far) change.
|
||||
/// </summary>
|
||||
internal static bool CameraApproximatelyEqual(
|
||||
|
|
@ -926,7 +926,7 @@ public sealed unsafe class EnvCellRenderer :
|
|||
// Verbatim port of WB EnvCellRenderManager.cs:395-511.
|
||||
// Deviations from WB (all documented):
|
||||
// - Drop the _useModernRendering branch (our codebase asserts modern at startup per Phase N.5).
|
||||
// - Drop SelectedInstance/HoveredInstance highlight block (lines 486-510) — no editor state.
|
||||
// - Drop SelectedInstance/HoveredInstance highlight block (lines 486-510) — no editor state.
|
||||
// - Replace RenderModernMDI(base) with private RenderModernMDIInternal.
|
||||
// - shader.Bind() / SetUniform API: mapped to acdream's legacy Shader
|
||||
// class (Use() + SetInt/SetVec4/SetMatrix4) to match the existing
|
||||
|
|
@ -947,7 +947,7 @@ public sealed unsafe class EnvCellRenderer :
|
|||
/// filter (the drawable visible cells from the PView traversal; each cell's
|
||||
/// shell instances are clip-gated to its CellClip slot by the caller's
|
||||
/// binding=3 map). NOTE: this is NOT the old two-pipe RenderInsideOut approach
|
||||
/// — that flat camera-inside-building stencil pass was deleted in Phase U.1.
|
||||
/// — that flat camera-inside-building stencil pass was deleted in Phase U.1.
|
||||
/// Source: WB EnvCellRenderManager.cs:399-511 (verbatim minus selection highlights).
|
||||
/// </summary>
|
||||
public void Render(WbRenderPass renderPass, HashSet<uint>? filter)
|
||||
|
|
@ -979,7 +979,7 @@ public sealed unsafe class EnvCellRenderer :
|
|||
// WB EnvCellRenderManager.cs:403-404:
|
||||
_shader.Use();
|
||||
// FIX 2026-05-28 (pool aliasing root cause): mirror WB
|
||||
// EnvCellRenderManager.cs:405 — restore the pool cursor to the
|
||||
// EnvCellRenderManager.cs:405 — restore the pool cursor to the
|
||||
// high-water mark Prepare's merge phase reached, so any
|
||||
// GetPooledList calls below return lists past the snapshot's
|
||||
// owned region. Original code used `snapshot.BatchedByCell.Count`
|
||||
|
|
@ -999,7 +999,7 @@ public sealed unsafe class EnvCellRenderer :
|
|||
// RenderInsideOutAcdream stencil pipeline) change the actual GL
|
||||
// state without updating these caches. The cache then lies, and
|
||||
// the per-batch SetCullMode in RenderModernMDIInternal skips its
|
||||
// glCullFace call — leaving stale cull state from the prior
|
||||
// glCullFace call — leaving stale cull state from the prior
|
||||
// consumer. For a cottage with mixed CullMode batches, half the
|
||||
// walls end up culled and the user sees "missing walls".
|
||||
//
|
||||
|
|
@ -1012,15 +1012,15 @@ public sealed unsafe class EnvCellRenderer :
|
|||
_shader.SetInt("uRenderPass", (int)renderPass);
|
||||
_shader.SetInt("uFilterByCell", 0);
|
||||
_shader.SetInt("uLightingMode", 1); // A7 Fix D D-3/D-4: EnvCell bake (wrap points, no sun)
|
||||
// #176 stripe-hunt isolation (ACDREAM_LIGHT_DEBUG) — throwaway diagnostic.
|
||||
// #176 stripe-hunt isolation (ACDREAM_LIGHT_DEBUG) — throwaway diagnostic.
|
||||
_shader.SetInt("uLightDebug", AcDream.Core.Rendering.RenderingDiagnostics.LightDebugMode);
|
||||
|
||||
// Phase U.4 ROOT-CAUSE FIX (cell-shell flicker / "transparent walls when
|
||||
// moving"): upload uViewProjection HERE rather than inheriting it from
|
||||
// WbDrawDispatcher. The opaque shell pass runs BEFORE the dispatcher's
|
||||
// Draw (GameWindow ~7411 vs ~7418, the only other setter), so without
|
||||
// this the opaque shells used the PREVIOUS frame's matrix — a stale
|
||||
// gl_Position against this frame's clip planes → pose-dependent clipping,
|
||||
// this the opaque shells used the PREVIOUS frame's matrix — a stale
|
||||
// gl_Position against this frame's clip planes → pose-dependent clipping,
|
||||
// worst while moving. Same self-contained-GL-state precedent as the
|
||||
// 2026-05-28 cull-state cache fix above.
|
||||
_shader.SetMatrix4("uViewProjection", _lastViewProjection);
|
||||
|
|
@ -1059,7 +1059,7 @@ public sealed unsafe class EnvCellRenderer :
|
|||
else if (filter is null)
|
||||
{
|
||||
RebuildUnfilteredGroups(snapshot);
|
||||
// WB EnvCellRenderManager.cs:418-429: optimized path — global groups.
|
||||
// WB EnvCellRenderManager.cs:418-429: optimized path — global groups.
|
||||
foreach (var gfxObjId in _activeSnapshotGlobalGfxObjIds)
|
||||
{
|
||||
if (_activeSnapshotGlobalGroups.TryGetValue(gfxObjId, out var transforms))
|
||||
|
|
@ -1144,7 +1144,7 @@ public sealed unsafe class EnvCellRenderer :
|
|||
renderPass);
|
||||
}
|
||||
|
||||
// WB EnvCellRenderManager.cs:486-510: selection/hover highlights — DROPPED (no editor state).
|
||||
// WB EnvCellRenderManager.cs:486-510: selection/hover highlights — DROPPED (no editor state).
|
||||
|
||||
// WB EnvCellRenderManager.cs:506-509: cleanup.
|
||||
_shader.SetVec4("uHighlightColor", new System.Numerics.Vector4(0, 0, 0, 0));
|
||||
|
|
@ -1170,12 +1170,12 @@ public sealed unsafe class EnvCellRenderer :
|
|||
? dc.renderData.Batches[0].IndexCount / 3
|
||||
: 0) * dc.count;
|
||||
|
||||
// Issue #78 (2026-05-31) [shell] probe (ACDREAM_PROBE_SHELL) — THROWAWAY.
|
||||
// Issue #78 (2026-05-31) [shell] probe (ACDREAM_PROBE_SHELL) — THROWAWAY.
|
||||
// Per opaque-pass call: totals + per visible (filtered) cell whether it is
|
||||
// present in the prepared snapshot, and its geometry/flags. Answers why the
|
||||
// interior walls/ceiling don't appear: NOSNAP / gfx=0 ⇒ no shell geometry
|
||||
// prepared for the cell; idx>0 + zh>0 ⇒ prepared but missing bindless texture
|
||||
// (invisible); idx>0 + zh=0 + tr=0 ⇒ opaque geometry drawn (fault is depth/
|
||||
// interior walls/ceiling don't appear: NOSNAP / gfx=0 ⇒ no shell geometry
|
||||
// prepared for the cell; idx>0 + zh>0 ⇒ prepared but missing bindless texture
|
||||
// (invisible); idx>0 + zh=0 + tr=0 ⇒ opaque geometry drawn (fault is depth/
|
||||
// occlusion or the geometry isn't the wall). Opaque pass only (halves noise).
|
||||
if (renderPass == WbRenderPass.Opaque
|
||||
&& AcDream.Core.Rendering.RenderingDiagnostics.ProbeShellEnabled)
|
||||
|
|
@ -1217,7 +1217,7 @@ public sealed unsafe class EnvCellRenderer :
|
|||
/// <summary>
|
||||
/// True if the cell's prepared snapshot has any transparent render batch.
|
||||
/// The pview shell pass uses this to skip the (heavy per-frame) transparent
|
||||
/// <see cref="Render"/> call for opaque-only cells — most cell geometry is
|
||||
/// <see cref="Render"/> call for opaque-only cells — most cell geometry is
|
||||
/// opaque walls/floors/ceilings, so this removes the bulk of the per-cell
|
||||
/// transparent draws. Read-only; mirrors the [shell] probe's batch scan.
|
||||
/// </summary>
|
||||
|
|
@ -1227,7 +1227,7 @@ public sealed unsafe class EnvCellRenderer :
|
|||
// ---------------------------------------------------------------------------
|
||||
// GetCellLightSet (A7 Fix D D-2 helper)
|
||||
// Per-cell up-to-8 point lights, cached per frame. Camera-independent, like
|
||||
// WbDrawDispatcher.ComputeEntityLightSet — keyed on the cell's world bounds.
|
||||
// WbDrawDispatcher.ComputeEntityLightSet — keyed on the cell's world bounds.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// A7 Fix D (D-2): the up-to-8 point lights reaching a cell, by the cell's world
|
||||
|
|
@ -1248,21 +1248,21 @@ public sealed unsafe class EnvCellRenderer :
|
|||
|
||||
var snap = _pointSnapshot;
|
||||
// Landblocks are keyed by the streaming landblock id 0xXXYYFFFF
|
||||
// (GameWindow: (x<<24)|(y<<16)|0xFFFF), NOT 0xXXYY0000 — so the landblock
|
||||
// (GameWindow: (x<<24)|(y<<16)|0xFFFF), NOT 0xXXYY0000 — so the landblock
|
||||
// key is (cellId & 0xFFFF0000) | 0xFFFF. The old `cellId & 0xFFFF0000` key
|
||||
// (0xXXYY0000) NEVER matched a registered landblock, so this lookup always
|
||||
// missed: SelectForObject never ran and every EnvCell wall received ZERO
|
||||
// point lights (the entire "indoor torches/lanterns don't light the room"
|
||||
// bug — confirmed by the [cell-light] probe: inBounds=False for every cell).
|
||||
// bug — confirmed by the [cell-light] probe: inBounds=False for every cell).
|
||||
if (snap is { Count: > 0 } &&
|
||||
_landblocks.TryGetValue((cellId & 0xFFFF0000u) | 0xFFFFu, out var lb) &&
|
||||
lb.EnvCellBounds.TryGetValue(cellId, out var b))
|
||||
{
|
||||
Vector3 center = (b.Min + b.Max) * 0.5f;
|
||||
float radius = (b.Max - b.Min).Length() * 0.5f;
|
||||
// #176 flap fix: cells use SelectForCell (retail minimize_envcell_lighting) — ALL
|
||||
// #176 flap fix: cells use SelectForCell (retail minimize_envcell_lighting) — ALL
|
||||
// dynamic lights on every cell (stable), not the per-object sphere-overlap cull that
|
||||
// let the portal set flip as the flood shifted → floor-lighting flap.
|
||||
// let the portal set flip as the flood shifted → floor-lighting flap.
|
||||
AcDream.Core.Lighting.LightManager.SelectForCell(snap, center, radius, set);
|
||||
}
|
||||
cached.FrameGeneration = _lightFrameGeneration;
|
||||
|
|
@ -1414,9 +1414,9 @@ public sealed unsafe class EnvCellRenderer :
|
|||
int passIdx = (int)renderPass;
|
||||
if (passIdx < 0 || passIdx > 2) return;
|
||||
|
||||
// §4 outdoor full-world flap (2026-06-10): hoisted from below the SSBO uploads.
|
||||
// §4 outdoor full-world flap (2026-06-10): hoisted from below the SSBO uploads.
|
||||
// Without the global VAO nothing can draw, and returning AFTER the pass state
|
||||
// was established leaked it (same early-out shape as the totalDraws==0 leak —
|
||||
// was established leaked it (same early-out shape as the totalDraws==0 leak —
|
||||
// see the comment on the state-establish block below).
|
||||
var globalVao = _meshManager.GlobalBuffer?.VAO ?? 0u;
|
||||
if (globalVao == 0) return;
|
||||
|
|
@ -1468,14 +1468,14 @@ public sealed unsafe class EnvCellRenderer :
|
|||
// transparent). Restored to opaque defaults at the end of the draw loop so a
|
||||
// Transparent pass can't leak into later draws.
|
||||
//
|
||||
// §4 outdoor full-world flap fix (2026-06-10): this block MOVED below the
|
||||
// §4 outdoor full-world flap fix (2026-06-10): this block MOVED below the
|
||||
// totalDraws==0 early-out above. It used to run before the batch grouping, so a
|
||||
// Transparent pass over a cell whose batches are ALL opaque (a plain cottage
|
||||
// interior) set Blend-on/DepthMask-off and then returned at the count check
|
||||
// WITHOUT reaching the restore. The frame ended with dmask=0; the NEXT frame's
|
||||
// glClear(DEPTH) silently no-oped (depth clears honor glDepthMask), every world
|
||||
// fragment failed GL_LESS against its own previous-frame depth ghost, and the
|
||||
// whole screen dropped to the fog-tinted clear color — onset-locked to the
|
||||
// whole screen dropped to the fog-tinted clear color — onset-locked to the
|
||||
// building-flood merge (the first frame a flooded building shell draws), holding
|
||||
// until camera rotation dropped the cell from the flood. From here down every
|
||||
// path reaches the end-of-pass restore.
|
||||
|
|
@ -1689,14 +1689,14 @@ public sealed unsafe class EnvCellRenderer :
|
|||
// Phase U.4: upload the per-instance clip-slot buffer (binding=3). When
|
||||
// _cellIdToSlot is set (indoor routing), each cell shell instance is gated
|
||||
// to its cell's CellClip slot via allInstances[i].CellId; cells absent from
|
||||
// the map (shouldn't happen — the Render filter is the map's keys) and the
|
||||
// the map (shouldn't happen — the Render filter is the map's keys) and the
|
||||
// U.3 path both map to slot 0 (no-clip). allInstances is laid out in the
|
||||
// SAME order as the binding=0 transforms (_gpuInstanceTransforms below), so
|
||||
// instanceClipSlot[i] tracks Instances[i] through the MDI BaseInstance.
|
||||
if (_clipSlotData.Length < uniqueInstanceCount)
|
||||
_clipSlotData = new uint[Math.Max(_clipSlotData.Length * 2, uniqueInstanceCount)];
|
||||
// #176 stripe-hunt isolation (ACDREAM_CLIP_DEBUG=1): force every shell
|
||||
// instance to slot 0 (no-clip) — retail draws cell shells WHOLE.
|
||||
// instance to slot 0 (no-clip) — retail draws cell shells WHOLE.
|
||||
if (_cellIdToSlot is null
|
||||
|| AcDream.Core.Rendering.RenderingDiagnostics.ClipDebugNoShellTrim)
|
||||
{
|
||||
|
|
@ -1728,7 +1728,7 @@ public sealed unsafe class EnvCellRenderer :
|
|||
|
||||
// #176 seam-draw probe: emitted HERE (not in Render) so the per-cell light
|
||||
// sets read through the just-cleared cache against THIS frame's
|
||||
// _pointSnapshot — the exact data the SSBO upload below carries.
|
||||
// _pointSnapshot — the exact data the SSBO upload below carries.
|
||||
if (renderPass == WbRenderPass.Opaque
|
||||
&& AcDream.Core.Rendering.RenderingDiagnostics.ProbeSeamDrawEnabled)
|
||||
EmitSeamDrawProbe(drawCalls, allInstances, _seamProbeFilter);
|
||||
|
|
@ -1766,7 +1766,7 @@ public sealed unsafe class EnvCellRenderer :
|
|||
PersistActiveDynamicBufferCapacities();
|
||||
|
||||
// WB BaseObjectRenderManager.cs:807-818: bind VAO + SSBOs + barrier.
|
||||
// (globalVao validated at the top of the method — a return here would leak the
|
||||
// (globalVao validated at the top of the method — a return here would leak the
|
||||
// pass state established above.)
|
||||
if (_currentVao != globalVao)
|
||||
{
|
||||
|
|
@ -1865,16 +1865,16 @@ public sealed unsafe class EnvCellRenderer :
|
|||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// #176 seam-draw probe (ACDREAM_PROBE_SEAMDRAW) — throwaway apparatus.
|
||||
// #176 seam-draw probe (ACDREAM_PROBE_SEAMDRAW) — throwaway apparatus.
|
||||
// The in-engine replacement for the RenderDoc pixel-history the pipeline
|
||||
// can't have (RenderDoc hides GL_ARB_bindless_texture → our mandatory-modern
|
||||
// startup gate throws). Per opaque pass: for each target cell — flood
|
||||
// can't have (RenderDoc hides GL_ARB_bindless_texture → our mandatory-modern
|
||||
// startup gate throws). Per opaque pass: for each target cell — flood
|
||||
// membership, every shell instance (count + translation, F3 z shows the
|
||||
// +0.02 lift; n≥2 for one (cell,gfx) = the runtime double-draw), and the
|
||||
// +0.02 lift; n≥2 for one (cell,gfx) = the runtime double-draw), and the
|
||||
// cell's 8-light set resolved to stable IDENTITIES (owner-cell low16 +
|
||||
// intensity; raw indices shuffle when the pool rebuilds). Plus the
|
||||
// snapshot's HOT lights (intensity ≥ 50 — the portal purples; fixtures are
|
||||
// ~1–2). Change-deduped block with a 2 s heartbeat: a purple identity
|
||||
// snapshot's HOT lights (intensity ≥ 50 — the portal purples; fixtures are
|
||||
// ~1–2). Change-deduped block with a 2 s heartbeat: a purple identity
|
||||
// flipping with flood membership = the snapshot-scope mechanism; two
|
||||
// coincident instances = the z-fight. See RenderingDiagnostics.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -1995,8 +1995,8 @@ public sealed unsafe class EnvCellRenderer :
|
|||
/// Uploads <see cref="_textureTable"/>'s handles to <see cref="_textureTableSsbo"/>
|
||||
/// when a new one was registered since the last flush, then (re)binds it at
|
||||
/// <see cref="AcDream.App.Rendering.Gpu.GpuBindingModel.StorageTextureTable"/>.
|
||||
/// A genuinely new handle is rare — new dat surfaces/atlases, not every
|
||||
/// frame — so this is not part of the ring-buffered per-frame SSBO set;
|
||||
/// A genuinely new handle is rare — new dat surfaces/atlases, not every
|
||||
/// frame — so this is not part of the ring-buffered per-frame SSBO set;
|
||||
/// see GlBindlessHandleTable's doc comment.
|
||||
/// </summary>
|
||||
private void FlushAndBindTextureTable()
|
||||
|
|
@ -2068,7 +2068,7 @@ public sealed unsafe class EnvCellRenderer :
|
|||
GLEnum.DynamicDraw,
|
||||
"allocating EnvCell fallback clip SSBO");
|
||||
allocated = true;
|
||||
// One CellClip slot, all zeros: count 0 ⇒ shader passes every plane.
|
||||
// One CellClip slot, all zeros: count 0 ⇒ shader passes every plane.
|
||||
Span<byte> zero = stackalloc byte[AcDream.App.Rendering.ClipFrame.CellClipStrideBytes];
|
||||
zero.Clear();
|
||||
fixed (byte* p = zero)
|
||||
|
|
@ -2103,12 +2103,12 @@ public sealed unsafe class EnvCellRenderer :
|
|||
|
||||
private List<InstanceData> GetPooledList()
|
||||
{
|
||||
// Mirrors WB ObjectRenderManagerBase.cs:1221-1233 — the reuse
|
||||
// Mirrors WB ObjectRenderManagerBase.cs:1221-1233 — the reuse
|
||||
// branch MUST clear the list before returning. PrepareRenderBatches'
|
||||
// merge phase pattern is `gfxDict[k] = list; list.AddRange(...)`,
|
||||
// which assumes the list is empty. Without the clear, lists grow
|
||||
// unbounded across frames and each frame's draw includes all prior
|
||||
// frames' stale data. Original port omitted the Clear() call — root
|
||||
// frames' stale data. Original port omitted the Clear() call — root
|
||||
// cause of post-Wave-5 visual chaos (FIX 2026-05-28). See
|
||||
// docs/research/2026-05-28-a8-env-cell-renderer-audit-findings.md.
|
||||
lock (_listPool)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue