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 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AcDream.Core.Audio;
|
using AcDream.Core.Audio;
|
||||||
|
|
@ -19,33 +19,33 @@ namespace AcDream.App.Audio;
|
||||||
/// Wiring:
|
/// Wiring:
|
||||||
/// <list type="bullet">
|
/// <list type="bullet">
|
||||||
/// <item><description>
|
/// <item><description>
|
||||||
/// <see cref="SoundHook"/> → direct play of <c>SoundHook.Id</c> (a
|
/// <see cref="SoundHook"/> → direct play of <c>SoundHook.Id</c> (a
|
||||||
/// Wave dat id) at the entity's world position. Used for custom /
|
/// Wave dat id) at the entity's world position. Used for custom /
|
||||||
/// per-animation audio like weapon swoosh or spell chant.
|
/// per-animation audio like weapon swoosh or spell chant.
|
||||||
/// </description></item>
|
/// </description></item>
|
||||||
/// <item><description>
|
/// <item><description>
|
||||||
/// <see cref="SoundTableHook"/> → look up the entity's SoundTable +
|
/// <see cref="SoundTableHook"/> → look up the entity's SoundTable +
|
||||||
/// the hook's <c>SoundType</c>, roll one
|
/// the hook's <c>SoundType</c>, roll one
|
||||||
/// <see cref="DatReaderWriter.Types.SoundEntry"/> via
|
/// <see cref="DatReaderWriter.Types.SoundEntry"/> via
|
||||||
/// <see cref="SoundCookbook"/>, play its wave. Retail's "footstep
|
/// <see cref="SoundCookbook"/>, play its wave. Retail's "footstep
|
||||||
/// that varies slightly" mechanism — also how attack / damage sounds
|
/// that varies slightly" mechanism — also how attack / damage sounds
|
||||||
/// pick a creature-specific variant.
|
/// pick a creature-specific variant.
|
||||||
/// </description></item>
|
/// </description></item>
|
||||||
/// <item><description>
|
/// <item><description>
|
||||||
/// <see cref="SoundTweakedHook"/> → same as SoundHook but with
|
/// <see cref="SoundTweakedHook"/> → same as SoundHook but with
|
||||||
/// pitch / volume overrides baked into the hook.
|
/// pitch / volume overrides baked into the hook.
|
||||||
/// </description></item>
|
/// </description></item>
|
||||||
/// </list>
|
/// </list>
|
||||||
/// </para>
|
/// </para>
|
||||||
///
|
///
|
||||||
/// <para>
|
/// <para>
|
||||||
/// Entity → SoundTable id is resolved via an <see cref="IEntitySoundTable"/>
|
/// Entity → SoundTable id is resolved via an <see cref="IEntitySoundTable"/>
|
||||||
/// callback passed in at construction; the renderer's per-entity state
|
/// callback passed in at construction; the renderer's per-entity state
|
||||||
/// bag knows the PhysicsObj's <c>SoundTableId</c> (retail:
|
/// bag knows the PhysicsObj's <c>SoundTableId</c> (retail:
|
||||||
/// <c>PhysicsObj.soundtable_id</c>).
|
/// <c>PhysicsObj.soundtable_id</c>).
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class AudioHookSink : IAnimationHookSink
|
internal sealed class AudioHookSink : IAnimationHookSink
|
||||||
{
|
{
|
||||||
private readonly OpenAlAudioEngine _engine;
|
private readonly OpenAlAudioEngine _engine;
|
||||||
private readonly DatSoundCache _cache;
|
private readonly DatSoundCache _cache;
|
||||||
|
|
@ -81,7 +81,7 @@ public sealed class AudioHookSink : IAnimationHookSink
|
||||||
case SoundTweakedHook stw:
|
case SoundTweakedHook stw:
|
||||||
// SoundTweakedHook is a direct wave play with volume +
|
// SoundTweakedHook is a direct wave play with volume +
|
||||||
// priority overrides baked into the hook itself (NOT a
|
// priority overrides baked into the hook itself (NOT a
|
||||||
// SoundTable lookup — that's SoundTableHook). Retail uses
|
// SoundTable lookup — that's SoundTableHook). Retail uses
|
||||||
// this for the rare "explicit wave + explicit volume" case.
|
// this for the rare "explicit wave + explicit volume" case.
|
||||||
Play(entityId, entityWorldPosition,
|
Play(entityId, entityWorldPosition,
|
||||||
waveId: (uint)stw.SoundId,
|
waveId: (uint)stw.SoundId,
|
||||||
|
|
@ -90,7 +90,7 @@ public sealed class AudioHookSink : IAnimationHookSink
|
||||||
pitch: 1f);
|
pitch: 1f);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// All the visual-only hooks (Scale, Luminous, Diffuse, …)
|
// All the visual-only hooks (Scale, Luminous, Diffuse, …)
|
||||||
// are for other sinks to handle.
|
// are for other sinks to handle.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -138,7 +138,7 @@ public sealed class AudioHookSink : IAnimationHookSink
|
||||||
/// entity. Retail stores this on <c>PhysicsObj.soundtable_id</c>; our
|
/// entity. Retail stores this on <c>PhysicsObj.soundtable_id</c>; our
|
||||||
/// renderer keeps per-entity state that includes it.
|
/// renderer keeps per-entity state that includes it.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IEntitySoundTable
|
internal interface IEntitySoundTable
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Return the SoundTable dat id (0x20xxxxxx) for <paramref name="entityId"/>,
|
/// Return the SoundTable dat id (0x20xxxxxx) for <paramref name="entityId"/>,
|
||||||
|
|
@ -151,7 +151,7 @@ public interface IEntitySoundTable
|
||||||
/// Simple dictionary-backed <see cref="IEntitySoundTable"/>; the renderer
|
/// Simple dictionary-backed <see cref="IEntitySoundTable"/>; the renderer
|
||||||
/// assigns entries as it hydrates entities.
|
/// assigns entries as it hydrates entities.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class DictionaryEntitySoundTable : IEntitySoundTable
|
internal sealed class DictionaryEntitySoundTable : IEntitySoundTable
|
||||||
{
|
{
|
||||||
private readonly Dictionary<uint, uint> _table = new();
|
private readonly Dictionary<uint, uint> _table = new();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AcDream.Core.Audio;
|
using AcDream.Core.Audio;
|
||||||
|
|
@ -7,8 +7,8 @@ using Silk.NET.OpenAL;
|
||||||
namespace AcDream.App.Audio;
|
namespace AcDream.App.Audio;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// OpenAL-backed audio engine (Phase E.2) — faithful to retail's
|
/// OpenAL-backed audio engine (Phase E.2) — faithful to retail's
|
||||||
/// 16-voice pool and inverse-square falloff behaviour (r05 §5.3).
|
/// 16-voice pool and inverse-square falloff behaviour (r05 §5.3).
|
||||||
///
|
///
|
||||||
/// <para>
|
/// <para>
|
||||||
/// Architecture:
|
/// Architecture:
|
||||||
|
|
@ -16,7 +16,7 @@ namespace AcDream.App.Audio;
|
||||||
/// <item><description>
|
/// <item><description>
|
||||||
/// Single <see cref="ALContext"/> + <see cref="AL"/> bound to the
|
/// Single <see cref="ALContext"/> + <see cref="AL"/> bound to the
|
||||||
/// system default device. Cross-platform (WASAPI / WinMM /
|
/// system default device. Cross-platform (WASAPI / WinMM /
|
||||||
/// PulseAudio / CoreAudio — whichever OpenAL-Soft picks).
|
/// PulseAudio / CoreAudio — whichever OpenAL-Soft picks).
|
||||||
/// </description></item>
|
/// </description></item>
|
||||||
/// <item><description>
|
/// <item><description>
|
||||||
/// Fixed 16-source pool for 3D positional sounds. When all 16 are
|
/// Fixed 16-source pool for 3D positional sounds. When all 16 are
|
||||||
|
|
@ -27,13 +27,13 @@ namespace AcDream.App.Audio;
|
||||||
/// </description></item>
|
/// </description></item>
|
||||||
/// <item><description>
|
/// <item><description>
|
||||||
/// Separate UI source pool (4 sources) for flat 2D UI clicks /
|
/// Separate UI source pool (4 sources) for flat 2D UI clicks /
|
||||||
/// wooshes — not subject to the 3D eviction game.
|
/// wooshes — not subject to the 3D eviction game.
|
||||||
/// </description></item>
|
/// </description></item>
|
||||||
/// <item><description>
|
/// <item><description>
|
||||||
/// PCM buffer cache keyed by Wave dat id so the same footstep isn't
|
/// PCM buffer cache keyed by Wave dat id so the same footstep isn't
|
||||||
/// re-uploaded to the GL-equivalent AL buffers on every hit. Bounded
|
/// re-uploaded to the GL-equivalent AL buffers on every hit. Bounded
|
||||||
/// by a byte budget (<see cref="DefaultBufferByteBudget"/>) enforced
|
/// by a byte budget (<see cref="DefaultBufferByteBudget"/>) enforced
|
||||||
/// with LRU eviction — see <see cref="EvictBuffersOverBudget"/>. A
|
/// with LRU eviction — see <see cref="EvictBuffersOverBudget"/>. A
|
||||||
/// buffer still attached to a live source is never evicted (AL
|
/// buffer still attached to a live source is never evicted (AL
|
||||||
/// rejects deleting a bound buffer); eviction re-queries live AL
|
/// rejects deleting a bound buffer); eviction re-queries live AL
|
||||||
/// source state rather than tracking a second copy of it.
|
/// source state rather than tracking a second copy of it.
|
||||||
|
|
@ -60,15 +60,15 @@ internal interface IWorldAudioQuiescence
|
||||||
void ResumeWorldAudio();
|
void ResumeWorldAudio();
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed unsafe class OpenAlAudioEngine : IAudioEngine, IWorldAudioQuiescence
|
internal sealed unsafe class OpenAlAudioEngine : IAudioEngine, IWorldAudioQuiescence
|
||||||
{
|
{
|
||||||
// ── Backends ─────────────────────────────────────────────────────────────
|
// ── Backends ─────────────────────────────────────────────────────────────
|
||||||
private AL? _al;
|
private AL? _al;
|
||||||
private OpenAlResourceLifetime? _resources;
|
private OpenAlResourceLifetime? _resources;
|
||||||
private bool _available;
|
private bool _available;
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
|
||||||
// ── Pools ────────────────────────────────────────────────────────────────
|
// ── Pools ────────────────────────────────────────────────────────────────
|
||||||
private const int PoolSize3D = 16; // retail 16-slot voice pool
|
private const int PoolSize3D = 16; // retail 16-slot voice pool
|
||||||
private const int PoolSizeUi = 4;
|
private const int PoolSizeUi = 4;
|
||||||
|
|
||||||
|
|
@ -88,7 +88,7 @@ public sealed unsafe class OpenAlAudioEngine : IAudioEngine, IWorldAudioQuiescen
|
||||||
|
|
||||||
private readonly uint[] _poolUi = new uint[PoolSizeUi];
|
private readonly uint[] _poolUi = new uint[PoolSizeUi];
|
||||||
|
|
||||||
// ── Buffer cache (Wave dat id → AL buffer) ───────────────────────────────
|
// ── Buffer cache (Wave dat id → AL buffer) ───────────────────────────────
|
||||||
// Budget rationale: decoded PCM waves run ~100-500 KB each (same sizing
|
// Budget rationale: decoded PCM waves run ~100-500 KB each (same sizing
|
||||||
// as DatSoundCache's payload LRU, which this cache re-uploads from). 48
|
// as DatSoundCache's payload LRU, which this cache re-uploads from). 48
|
||||||
// MiB gives comfortable headroom for the working set of a play session
|
// MiB gives comfortable headroom for the working set of a play session
|
||||||
|
|
@ -99,11 +99,11 @@ public sealed unsafe class OpenAlAudioEngine : IAudioEngine, IWorldAudioQuiescen
|
||||||
private readonly Dictionary<uint, uint> _bufferByWaveId = new();
|
private readonly Dictionary<uint, uint> _bufferByWaveId = new();
|
||||||
private readonly AlBufferBudgetTracker _bufferBudget = new(DefaultBufferByteBudget);
|
private readonly AlBufferBudgetTracker _bufferBudget = new(DefaultBufferByteBudget);
|
||||||
|
|
||||||
// ── Ambient handles (StartAmbient/StopAmbient) ───────────────────────────
|
// ── Ambient handles (StartAmbient/StopAmbient) ───────────────────────────
|
||||||
private readonly Dictionary<int, uint> _ambientSources = new();
|
private readonly Dictionary<int, uint> _ambientSources = new();
|
||||||
private int _nextAmbientHandle = 1;
|
private int _nextAmbientHandle = 1;
|
||||||
|
|
||||||
// ── Public volume knobs ──────────────────────────────────────────────────
|
// ── Public volume knobs ──────────────────────────────────────────────────
|
||||||
public float MasterVolume { get; set; } = 1f;
|
public float MasterVolume { get; set; } = 1f;
|
||||||
public float SfxVolume { get; set; } = 1f;
|
public float SfxVolume { get; set; } = 1f;
|
||||||
public float MusicVolume { get; set; } = 0.7f;
|
public float MusicVolume { get; set; } = 0.7f;
|
||||||
|
|
@ -219,7 +219,7 @@ public sealed unsafe class OpenAlAudioEngine : IAudioEngine, IWorldAudioQuiescen
|
||||||
_al = null;
|
_al = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── IAudioEngine ─────────────────────────────────────────────────────────
|
// ── IAudioEngine ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
public void SetListener(
|
public void SetListener(
|
||||||
float posX, float posY, float posZ,
|
float posX, float posY, float posZ,
|
||||||
|
|
@ -241,7 +241,7 @@ public sealed unsafe class OpenAlAudioEngine : IAudioEngine, IWorldAudioQuiescen
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Not exposed on IAudioEngine but used by the hook sink — play a raw
|
/// Not exposed on IAudioEngine but used by the hook sink — play a raw
|
||||||
/// WaveData blob at a 3D position with full priority/volume controls.
|
/// WaveData blob at a 3D position with full priority/volume controls.
|
||||||
/// Returns true on success, false if the buffer was rejected.
|
/// Returns true on success, false if the buffer was rejected.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -278,7 +278,7 @@ public sealed unsafe class OpenAlAudioEngine : IAudioEngine, IWorldAudioQuiescen
|
||||||
if (_pool3D[idx].PlayingGain < effectiveGain) { slotIdx = idx; break; }
|
if (_pool3D[idx].PlayingGain < effectiveGain) { slotIdx = idx; break; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (slotIdx < 0) return false; // no slot quieter than us — drop
|
if (slotIdx < 0) return false; // no slot quieter than us — drop
|
||||||
|
|
||||||
var slot = _pool3D[slotIdx];
|
var slot = _pool3D[slotIdx];
|
||||||
_al.SourceStop(slot.SourceId);
|
_al.SourceStop(slot.SourceId);
|
||||||
|
|
@ -355,7 +355,7 @@ public sealed unsafe class OpenAlAudioEngine : IAudioEngine, IWorldAudioQuiescen
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// IAudioEngine implementations — the enum-based overloads are less
|
// IAudioEngine implementations — the enum-based overloads are less
|
||||||
// useful than the raw-Wave overloads above, since the hook sink already
|
// useful than the raw-Wave overloads above, since the hook sink already
|
||||||
// has access to decoded WaveData. Left as no-ops for now; R5 defines
|
// has access to decoded WaveData. Left as no-ops for now; R5 defines
|
||||||
// SoundId as a sparse subset of retail enums.
|
// SoundId as a sparse subset of retail enums.
|
||||||
|
|
@ -366,7 +366,7 @@ public sealed unsafe class OpenAlAudioEngine : IAudioEngine, IWorldAudioQuiescen
|
||||||
|
|
||||||
public int StartAmbient(SoundId id, float x, float y, float z)
|
public int StartAmbient(SoundId id, float x, float y, float z)
|
||||||
{
|
{
|
||||||
// Looping ambient — needs a decoded wave + WaveId. The hook sink
|
// Looping ambient — needs a decoded wave + WaveId. The hook sink
|
||||||
// doesn't route ambient; a separate landblock-attached ambient
|
// doesn't route ambient; a separate landblock-attached ambient
|
||||||
// system (outside R5) will drive this. For now: reserve a handle.
|
// system (outside R5) will drive this. For now: reserve a handle.
|
||||||
int handle = _nextAmbientHandle++;
|
int handle = _nextAmbientHandle++;
|
||||||
|
|
@ -383,17 +383,17 @@ public sealed unsafe class OpenAlAudioEngine : IAudioEngine, IWorldAudioQuiescen
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void PlayMusic(string resourceName, bool loop) { /* R5 §6 MIDI — not ported */ }
|
public void PlayMusic(string resourceName, bool loop) { /* R5 §6 MIDI — not ported */ }
|
||||||
public void StopMusic() { /* ditto */ }
|
public void StopMusic() { /* ditto */ }
|
||||||
|
|
||||||
// ── Private helpers ──────────────────────────────────────────────────────
|
// ── Private helpers ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
private uint EnsureBuffer(uint waveId, WaveData wave)
|
private uint EnsureBuffer(uint waveId, WaveData wave)
|
||||||
{
|
{
|
||||||
if (!_available || _al is null) return 0;
|
if (!_available || _al is null) return 0;
|
||||||
if (_bufferByWaveId.TryGetValue(waveId, out var existing))
|
if (_bufferByWaveId.TryGetValue(waveId, out var existing))
|
||||||
{
|
{
|
||||||
// Buffer id 0 is the "unsupported format" negative marker — no
|
// Buffer id 0 is the "unsupported format" negative marker — no
|
||||||
// payload, not tracked by the budget, nothing to touch.
|
// payload, not tracked by the budget, nothing to touch.
|
||||||
if (existing != 0)
|
if (existing != 0)
|
||||||
_bufferBudget.Touch(waveId);
|
_bufferBudget.Touch(waveId);
|
||||||
|
|
@ -425,15 +425,15 @@ public sealed unsafe class OpenAlAudioEngine : IAudioEngine, IWorldAudioQuiescen
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Evict least-recently-used AL buffers until the resident-byte budget
|
/// Evict least-recently-used AL buffers until the resident-byte budget
|
||||||
/// is satisfied again. A buffer still bound to a live source (3D pool,
|
/// is satisfied again. A buffer still bound to a live source (3D pool,
|
||||||
/// UI pool, or an ambient source) is protected — <c>alDeleteBuffers</c>
|
/// UI pool, or an ambient source) is protected — <c>alDeleteBuffers</c>
|
||||||
/// fails on a buffer that's still attached to a source — so eviction
|
/// fails on a buffer that's still attached to a source — so eviction
|
||||||
/// never targets one; nor does it target <paramref name="protectedBufferId"/>,
|
/// never targets one; nor does it target <paramref name="protectedBufferId"/>,
|
||||||
/// the buffer <see cref="EnsureBuffer"/> just created for this call and
|
/// the buffer <see cref="EnsureBuffer"/> just created for this call and
|
||||||
/// hasn't attached to a source yet. If every resident buffer is
|
/// hasn't attached to a source yet. If every resident buffer is
|
||||||
/// protected the budget is temporarily exceeded rather than looping
|
/// protected the budget is temporarily exceeded rather than looping
|
||||||
/// forever; the bounded pool sizes (16 + 4 + ambient) cap how large
|
/// forever; the bounded pool sizes (16 + 4 + ambient) cap how large
|
||||||
/// that overage can get. Evicted waves replay through
|
/// that overage can get. Evicted waves replay through
|
||||||
/// <see cref="EnsureBuffer"/> again on next use — re-upload from
|
/// <see cref="EnsureBuffer"/> again on next use — re-upload from
|
||||||
/// <see cref="DatSoundCache"/>, identical to a first play.
|
/// <see cref="DatSoundCache"/>, identical to a first play.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void EvictBuffersOverBudget(uint protectedBufferId)
|
private void EvictBuffersOverBudget(uint protectedBufferId)
|
||||||
|
|
|
||||||
|
|
@ -263,8 +263,6 @@ internal sealed class FrameRootCompositionPhase
|
||||||
live.DrawDispatcher,
|
live.DrawDispatcher,
|
||||||
live.EnvCellRenderer,
|
live.EnvCellRenderer,
|
||||||
live.PortalDepthMask,
|
live.PortalDepthMask,
|
||||||
foundation.TextRenderer,
|
|
||||||
interaction.RetainedUi?.Host.TextRenderer,
|
|
||||||
live.ClipFrame,
|
live.ClipFrame,
|
||||||
foundation.Terrain,
|
foundation.Terrain,
|
||||||
foundation.SceneLighting),
|
foundation.SceneLighting),
|
||||||
|
|
@ -363,6 +361,7 @@ internal sealed class FrameRootCompositionPhase
|
||||||
d.CellVisibility),
|
d.CellVisibility),
|
||||||
d.WorldSceneDebugState,
|
d.WorldSceneDebugState,
|
||||||
foundation.DebugLines,
|
foundation.DebugLines,
|
||||||
|
host.GpuFrameLifetime,
|
||||||
d.PhysicsEngine,
|
d.PhysicsEngine,
|
||||||
d.PlayerMode,
|
d.PlayerMode,
|
||||||
d.PlayerController,
|
d.PlayerController,
|
||||||
|
|
@ -509,7 +508,7 @@ internal sealed class FrameRootCompositionPhase
|
||||||
: (IRenderFramePostDiagnosticsPhase?)lifecycleAutomation
|
: (IRenderFramePostDiagnosticsPhase?)lifecycleAutomation
|
||||||
?? NullRenderFramePostDiagnosticsPhase.Instance;
|
?? NullRenderFramePostDiagnosticsPhase.Instance;
|
||||||
var renderFrame = new RenderFrameOrchestrator(
|
var renderFrame = new RenderFrameOrchestrator(
|
||||||
host.GpuFrameFlights,
|
host.GpuFrameLifetime,
|
||||||
new FrameProfilerGpuMeasurement(d.FrameProfiler, d.Gl),
|
new FrameProfilerGpuMeasurement(d.FrameProfiler, d.Gl),
|
||||||
framePreparation,
|
framePreparation,
|
||||||
worldSceneRenderer,
|
worldSceneRenderer,
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ internal interface IGameWindowHostInputCameraPublication
|
||||||
{
|
{
|
||||||
void PublishGpuFrameFlights(GpuFrameFlightController value);
|
void PublishGpuFrameFlights(GpuFrameFlightController value);
|
||||||
void PublishGpuDevice(IGpuDevice value);
|
void PublishGpuDevice(IGpuDevice value);
|
||||||
|
void PublishGpuFrameLifetime(GpuDeviceFrameLifetime value);
|
||||||
void PublishKeyboardSource(SilkKeyboardSource value);
|
void PublishKeyboardSource(SilkKeyboardSource value);
|
||||||
void PublishMouseSource(SilkMouseSource value);
|
void PublishMouseSource(SilkMouseSource value);
|
||||||
void PublishMouseLookCursor(IMouseLookCursor value);
|
void PublishMouseLookCursor(IMouseLookCursor value);
|
||||||
|
|
@ -23,6 +24,7 @@ internal interface IGameWindowHostInputCameraPublication
|
||||||
internal sealed record HostInputCameraResult(
|
internal sealed record HostInputCameraResult(
|
||||||
GpuFrameFlightController GpuFrameFlights,
|
GpuFrameFlightController GpuFrameFlights,
|
||||||
IGpuDevice GpuDevice,
|
IGpuDevice GpuDevice,
|
||||||
|
GpuDeviceFrameLifetime GpuFrameLifetime,
|
||||||
WorldRenderDiagnostics WorldRenderDiagnostics,
|
WorldRenderDiagnostics WorldRenderDiagnostics,
|
||||||
SilkKeyboardSource? KeyboardSource,
|
SilkKeyboardSource? KeyboardSource,
|
||||||
SilkMouseSource? MouseSource,
|
SilkMouseSource? MouseSource,
|
||||||
|
|
@ -235,9 +237,10 @@ internal sealed class HostInputCameraCompositionPhase :
|
||||||
// exist — it owns its own BindlessSupport detection (see
|
// exist — it owns its own BindlessSupport detection (see
|
||||||
// GlGpuDevice's class comment), so unlike the legacy WB render path it
|
// GlGpuDevice's class comment), so unlike the legacy WB render path it
|
||||||
// has no dependency on WorldRenderCompositionPhase running first.
|
// has no dependency on WorldRenderCompositionPhase running first.
|
||||||
// Nothing consumes this device yet (Campaign V slice V1); it is
|
// Campaign V slice V4a is the first real consumer (TextRenderer,
|
||||||
|
// BitmapFont, DebugLineRenderer, TextureCache's UI upload path); it is
|
||||||
// proven against the real driver here and torn down with the render
|
// proven against the real driver here and torn down with the render
|
||||||
// stack so later slices (starting at V4a) have somewhere to plug in.
|
// stack.
|
||||||
IGpuDevice gpuDevice = scope.Acquire(
|
IGpuDevice gpuDevice = scope.Acquire(
|
||||||
"GPU device (RHI)",
|
"GPU device (RHI)",
|
||||||
() => _factory.CreateGpuDevice(gl, gpuFrames),
|
() => _factory.CreateGpuDevice(gl, gpuFrames),
|
||||||
|
|
@ -245,6 +248,14 @@ internal sealed class HostInputCameraCompositionPhase :
|
||||||
_publication.PublishGpuDevice);
|
_publication.PublishGpuDevice);
|
||||||
Fault(HostInputCameraCompositionPoint.GpuDevicePublished);
|
Fault(HostInputCameraCompositionPoint.GpuDevicePublished);
|
||||||
|
|
||||||
|
// V4a's structural addition: the shared IGpuFrame lifecycle that
|
||||||
|
// ported renderers allocate rings and open passes against. Owns no
|
||||||
|
// disposable resource of its own — it wraps gpuDevice, whose scope
|
||||||
|
// above already tears it down — so it is published as a plain value,
|
||||||
|
// not a second acquisition.
|
||||||
|
var gpuFrameLifetime = new GpuDeviceFrameLifetime(gpuDevice);
|
||||||
|
_publication.PublishGpuFrameLifetime(gpuFrameLifetime);
|
||||||
|
|
||||||
WorldRenderDiagnostics diagnostics =
|
WorldRenderDiagnostics diagnostics =
|
||||||
_factory.CreateWorldRenderDiagnostics(
|
_factory.CreateWorldRenderDiagnostics(
|
||||||
gl,
|
gl,
|
||||||
|
|
@ -345,6 +356,7 @@ internal sealed class HostInputCameraCompositionPhase :
|
||||||
return new HostInputCameraResult(
|
return new HostInputCameraResult(
|
||||||
gpuFrames,
|
gpuFrames,
|
||||||
gpuDevice,
|
gpuDevice,
|
||||||
|
gpuFrameLifetime,
|
||||||
diagnostics,
|
diagnostics,
|
||||||
keyboard,
|
keyboard,
|
||||||
mouse,
|
mouse,
|
||||||
|
|
|
||||||
|
|
@ -33,9 +33,10 @@ namespace AcDream.App.Composition;
|
||||||
internal sealed record InteractionRetainedUiDependencies(
|
internal sealed record InteractionRetainedUiDependencies(
|
||||||
RuntimeOptions Options,
|
RuntimeOptions Options,
|
||||||
GL Gl,
|
GL Gl,
|
||||||
|
IGpuDevice GpuDevice,
|
||||||
|
Func<IGpuFrame> CurrentGpuFrame,
|
||||||
IView Window,
|
IView Window,
|
||||||
IInputContext Input,
|
IInputContext Input,
|
||||||
string ShadersDirectory,
|
|
||||||
IDatReaderWriter Dats,
|
IDatReaderWriter Dats,
|
||||||
object DatLock,
|
object DatLock,
|
||||||
TextureCache TextureCache,
|
TextureCache TextureCache,
|
||||||
|
|
@ -389,8 +390,8 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
||||||
d.Character.LocalPlayer);
|
d.Character.LocalPlayer);
|
||||||
UiHost host = lease.AcquireHost(
|
UiHost host = lease.AcquireHost(
|
||||||
() => new UiHost(
|
() => new UiHost(
|
||||||
d.Gl,
|
d.GpuDevice,
|
||||||
d.ShadersDirectory,
|
d.CurrentGpuFrame,
|
||||||
d.DebugFont,
|
d.DebugFont,
|
||||||
d.HostQuiescence));
|
d.HostQuiescence));
|
||||||
checkpoint(InteractionRetainedUiCompositionPoint.UiHostAcquired);
|
checkpoint(InteractionRetainedUiCompositionPoint.UiHostAcquired);
|
||||||
|
|
@ -477,9 +478,9 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
||||||
host.WireKeyboard(keyboard);
|
host.WireKeyboard(keyboard);
|
||||||
checkpoint(InteractionRetainedUiCompositionPoint.KeyboardInputWired);
|
checkpoint(InteractionRetainedUiCompositionPoint.KeyboardInputWired);
|
||||||
|
|
||||||
(uint, int, int) ResolveChrome(uint id)
|
(GpuTextureSlot, int, int) ResolveChrome(uint id)
|
||||||
{
|
{
|
||||||
uint texture = d.TextureCache.GetOrUploadRenderSurface(
|
GpuTextureSlot texture = d.TextureCache.GetOrUploadRenderSurface(
|
||||||
id,
|
id,
|
||||||
out int width,
|
out int width,
|
||||||
out int height);
|
out int height);
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ namespace AcDream.App.Composition;
|
||||||
internal sealed record LivePresentationDependencies(
|
internal sealed record LivePresentationDependencies(
|
||||||
RuntimeOptions Options,
|
RuntimeOptions Options,
|
||||||
GL Gl,
|
GL Gl,
|
||||||
|
IGpuDevice GpuDevice,
|
||||||
IWindow Window,
|
IWindow Window,
|
||||||
object DatLock,
|
object DatLock,
|
||||||
RuntimeSettingsController Settings,
|
RuntimeSettingsController Settings,
|
||||||
|
|
@ -795,7 +796,8 @@ internal sealed class LivePresentationCompositionPhase
|
||||||
paperdollLease.Resource,
|
paperdollLease.Resource,
|
||||||
new RetailPaperdollFrameView(
|
new RetailPaperdollFrameView(
|
||||||
viewport,
|
viewport,
|
||||||
new PaperdollInventoryVisibility(inventoryFrame)),
|
new PaperdollInventoryVisibility(inventoryFrame),
|
||||||
|
d.GpuDevice),
|
||||||
new RetailPaperdollDollFactory(
|
new RetailPaperdollDollFactory(
|
||||||
new LivePaperdollEntityLookup(liveEntities),
|
new LivePaperdollEntityLookup(liveEntities),
|
||||||
d.PlayerIdentity,
|
d.PlayerIdentity,
|
||||||
|
|
@ -842,7 +844,8 @@ internal sealed class LivePresentationCompositionPhase
|
||||||
new RetailCreatureAppraisalFrameView(
|
new RetailCreatureAppraisalFrameView(
|
||||||
creatureViewport,
|
creatureViewport,
|
||||||
examinationFrame,
|
examinationFrame,
|
||||||
appraisalController),
|
appraisalController,
|
||||||
|
d.GpuDevice),
|
||||||
new RetailCreatureAppraisalCloneFactory(
|
new RetailCreatureAppraisalCloneFactory(
|
||||||
new LiveCreatureAppraisalEntityLookup(liveEntities)));
|
new LiveCreatureAppraisalEntityLookup(liveEntities)));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ internal sealed record WorldRenderDependencies(
|
||||||
WorldEnvironmentController Environment,
|
WorldEnvironmentController Environment,
|
||||||
IGameRenderResourceLifetime RenderResources,
|
IGameRenderResourceLifetime RenderResources,
|
||||||
IGpuResourceRetirementQueue ResourceRetirement,
|
IGpuResourceRetirementQueue ResourceRetirement,
|
||||||
|
IGpuDevice GpuDevice,
|
||||||
ResidencyBudgetOptions ResidencyBudgets,
|
ResidencyBudgetOptions ResidencyBudgets,
|
||||||
uint InitialCenterLandblockId,
|
uint InitialCenterLandblockId,
|
||||||
string DiagnosticsDirectory,
|
string DiagnosticsDirectory,
|
||||||
|
|
@ -89,10 +90,10 @@ internal interface IWorldRenderCompositionFactory
|
||||||
void SetTerrainAnisotropic(TerrainAtlas atlas, int level);
|
void SetTerrainAnisotropic(TerrainAtlas atlas, int level);
|
||||||
Shader CreateTerrainShader(GL gl, string shadersDirectory);
|
Shader CreateTerrainShader(GL gl, string shadersDirectory);
|
||||||
SceneLightingUboBinding CreateSceneLighting(GL gl);
|
SceneLightingUboBinding CreateSceneLighting(GL gl);
|
||||||
DebugLineRenderer CreateDebugLines(GL gl, string shadersDirectory);
|
DebugLineRenderer CreateDebugLines(IGpuDevice device);
|
||||||
byte[]? TryLoadDebugFont();
|
byte[]? TryLoadDebugFont();
|
||||||
BitmapFont CreateDebugFont(GL gl, byte[] bytes);
|
BitmapFont CreateDebugFont(IGpuDevice device, byte[] bytes);
|
||||||
TextRenderer CreateTextRenderer(GL gl, string shadersDirectory);
|
TextRenderer CreateTextRenderer(IGpuDevice device);
|
||||||
TerrainModernRenderer CreateTerrain(
|
TerrainModernRenderer CreateTerrain(
|
||||||
GL gl,
|
GL gl,
|
||||||
BindlessSupport bindless,
|
BindlessSupport bindless,
|
||||||
|
|
@ -112,6 +113,7 @@ internal interface IWorldRenderCompositionFactory
|
||||||
ResidencyBudgetOptions budgets);
|
ResidencyBudgetOptions budgets);
|
||||||
TextureCache CreateTextureCache(
|
TextureCache CreateTextureCache(
|
||||||
GL gl,
|
GL gl,
|
||||||
|
IGpuDevice device,
|
||||||
IDatReaderWriter dats,
|
IDatReaderWriter dats,
|
||||||
BindlessSupport bindless,
|
BindlessSupport bindless,
|
||||||
IGpuResourceRetirementQueue retirement,
|
IGpuResourceRetirementQueue retirement,
|
||||||
|
|
@ -214,21 +216,15 @@ internal sealed class RetailWorldRenderCompositionFactory
|
||||||
|
|
||||||
public SceneLightingUboBinding CreateSceneLighting(GL gl) => new(gl);
|
public SceneLightingUboBinding CreateSceneLighting(GL gl) => new(gl);
|
||||||
|
|
||||||
public DebugLineRenderer CreateDebugLines(
|
public DebugLineRenderer CreateDebugLines(IGpuDevice device) => new(device);
|
||||||
GL gl,
|
|
||||||
string shadersDirectory) =>
|
|
||||||
new(gl, shadersDirectory);
|
|
||||||
|
|
||||||
public byte[]? TryLoadDebugFont() =>
|
public byte[]? TryLoadDebugFont() =>
|
||||||
BitmapFont.TryLoadSystemMonospaceFont();
|
BitmapFont.TryLoadSystemMonospaceFont();
|
||||||
|
|
||||||
public BitmapFont CreateDebugFont(GL gl, byte[] bytes) =>
|
public BitmapFont CreateDebugFont(IGpuDevice device, byte[] bytes) =>
|
||||||
new(gl, bytes, pixelHeight: 15f, atlasSize: 512);
|
new(device, bytes, pixelHeight: 15f, atlasSize: 512);
|
||||||
|
|
||||||
public TextRenderer CreateTextRenderer(
|
public TextRenderer CreateTextRenderer(IGpuDevice device) => new(device);
|
||||||
GL gl,
|
|
||||||
string shadersDirectory) =>
|
|
||||||
new(gl, shadersDirectory);
|
|
||||||
|
|
||||||
public TerrainModernRenderer CreateTerrain(
|
public TerrainModernRenderer CreateTerrain(
|
||||||
GL gl,
|
GL gl,
|
||||||
|
|
@ -294,6 +290,7 @@ internal sealed class RetailWorldRenderCompositionFactory
|
||||||
|
|
||||||
public TextureCache CreateTextureCache(
|
public TextureCache CreateTextureCache(
|
||||||
GL gl,
|
GL gl,
|
||||||
|
IGpuDevice device,
|
||||||
IDatReaderWriter dats,
|
IDatReaderWriter dats,
|
||||||
BindlessSupport bindless,
|
BindlessSupport bindless,
|
||||||
IGpuResourceRetirementQueue retirement,
|
IGpuResourceRetirementQueue retirement,
|
||||||
|
|
@ -301,6 +298,7 @@ internal sealed class RetailWorldRenderCompositionFactory
|
||||||
ResidencyBudgetOptions budgets) =>
|
ResidencyBudgetOptions budgets) =>
|
||||||
new(
|
new(
|
||||||
gl,
|
gl,
|
||||||
|
device,
|
||||||
dats,
|
dats,
|
||||||
bindless,
|
bindless,
|
||||||
retirement,
|
retirement,
|
||||||
|
|
@ -482,12 +480,12 @@ internal sealed class WorldRenderCompositionPhase
|
||||||
DebugLineRenderer debugLines = AcquireAndPublish(
|
DebugLineRenderer debugLines = AcquireAndPublish(
|
||||||
scope,
|
scope,
|
||||||
"debug lines",
|
"debug lines",
|
||||||
() => _factory.CreateDebugLines(gl, shadersDirectory),
|
() => _factory.CreateDebugLines(_dependencies.GpuDevice),
|
||||||
_publication.PublishDebugLines,
|
_publication.PublishDebugLines,
|
||||||
WorldRenderCompositionPoint.DebugLinesPublished);
|
WorldRenderCompositionPoint.DebugLinesPublished);
|
||||||
|
|
||||||
(BitmapFont? debugFont, TextRenderer? textRenderer) =
|
(BitmapFont? debugFont, TextRenderer? textRenderer) =
|
||||||
ComposeOptionalHudResources(scope, gl, shadersDirectory);
|
ComposeOptionalHudResources(scope);
|
||||||
|
|
||||||
TerrainModernRenderer terrain = AcquireAndPublish(
|
TerrainModernRenderer terrain = AcquireAndPublish(
|
||||||
scope,
|
scope,
|
||||||
|
|
@ -535,6 +533,7 @@ internal sealed class WorldRenderCompositionPhase
|
||||||
"texture cache",
|
"texture cache",
|
||||||
() => _factory.CreateTextureCache(
|
() => _factory.CreateTextureCache(
|
||||||
gl,
|
gl,
|
||||||
|
_dependencies.GpuDevice,
|
||||||
content.Dats,
|
content.Dats,
|
||||||
bindless,
|
bindless,
|
||||||
_dependencies.ResourceRetirement,
|
_dependencies.ResourceRetirement,
|
||||||
|
|
@ -586,9 +585,7 @@ internal sealed class WorldRenderCompositionPhase
|
||||||
}
|
}
|
||||||
|
|
||||||
private (BitmapFont? Font, TextRenderer? Text) ComposeOptionalHudResources(
|
private (BitmapFont? Font, TextRenderer? Text) ComposeOptionalHudResources(
|
||||||
CompositionAcquisitionScope scope,
|
CompositionAcquisitionScope scope)
|
||||||
GL gl,
|
|
||||||
string shadersDirectory)
|
|
||||||
{
|
{
|
||||||
byte[]? fontBytes = _factory.TryLoadDebugFont();
|
byte[]? fontBytes = _factory.TryLoadDebugFont();
|
||||||
if (fontBytes is null)
|
if (fontBytes is null)
|
||||||
|
|
@ -600,13 +597,13 @@ internal sealed class WorldRenderCompositionPhase
|
||||||
|
|
||||||
var fontLease = scope.Acquire(
|
var fontLease = scope.Acquire(
|
||||||
"world HUD font",
|
"world HUD font",
|
||||||
() => _factory.CreateDebugFont(gl, fontBytes),
|
() => _factory.CreateDebugFont(_dependencies.GpuDevice, fontBytes),
|
||||||
_factory.Release);
|
_factory.Release);
|
||||||
BitmapFont font = fontLease.Resource;
|
BitmapFont font = fontLease.Resource;
|
||||||
Fault(WorldRenderCompositionPoint.DebugFontCreated);
|
Fault(WorldRenderCompositionPoint.DebugFontCreated);
|
||||||
var textLease = scope.Acquire(
|
var textLease = scope.Acquire(
|
||||||
"world HUD text renderer",
|
"world HUD text renderer",
|
||||||
() => _factory.CreateTextRenderer(gl, shadersDirectory),
|
() => _factory.CreateTextRenderer(_dependencies.GpuDevice),
|
||||||
_factory.Release);
|
_factory.Release);
|
||||||
TextRenderer text = textLease.Resource;
|
TextRenderer text = textLease.Resource;
|
||||||
Fault(WorldRenderCompositionPoint.TextRendererCreated);
|
Fault(WorldRenderCompositionPoint.TextRendererCreated);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
|
@ -10,11 +10,11 @@ using Silk.NET.OpenGL;
|
||||||
namespace AcDream.App.Diagnostics;
|
namespace AcDream.App.Diagnostics;
|
||||||
|
|
||||||
/// <summary>Stage indices for per-frame CPU attribution.</summary>
|
/// <summary>Stage indices for per-frame CPU attribution.</summary>
|
||||||
public enum FrameStage
|
internal enum FrameStage
|
||||||
{
|
{
|
||||||
/// <summary>Whole OnUpdate body (simulation + streaming apply).</summary>
|
/// <summary>Whole OnUpdate body (simulation + streaming apply).</summary>
|
||||||
Update = 0,
|
Update = 0,
|
||||||
/// <summary>WbMeshAdapter.Tick — staged mesh/texture GPU upload drain.</summary>
|
/// <summary>WbMeshAdapter.Tick — staged mesh/texture GPU upload drain.</summary>
|
||||||
Upload = 1,
|
Upload = 1,
|
||||||
/// <summary>ImGui Render (dev overlay).</summary>
|
/// <summary>ImGui Render (dev overlay).</summary>
|
||||||
ImGui = 2,
|
ImGui = 2,
|
||||||
|
|
@ -23,11 +23,11 @@ public enum FrameStage
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// One <c>ACDREAM_FRAME_HISTORY</c> CSV row — every field the aggregated
|
/// One <c>ACDREAM_FRAME_HISTORY</c> CSV row — every field the aggregated
|
||||||
/// <c>[frame-prof]</c> report discards when its 5-second window resets.
|
/// <c>[frame-prof]</c> report discards when its 5-second window resets.
|
||||||
/// Stage fields mirror <see cref="FrameStage"/> positionally (Update /
|
/// Stage fields mirror <see cref="FrameStage"/> positionally (Update /
|
||||||
/// Upload / ImGui / Pacing, matching <see cref="FrameProfiler.FormatReport"/>'s
|
/// Upload / ImGui / Pacing, matching <see cref="FrameProfiler.FormatReport"/>'s
|
||||||
/// <c>names</c> array) — if <see cref="FrameStage"/> grows, extend this
|
/// <c>names</c> array) — if <see cref="FrameStage"/> grows, extend this
|
||||||
/// record, <see cref="FrameProfiler.WriteHistoryCsv"/>, and the CSV header
|
/// record, <see cref="FrameProfiler.WriteHistoryCsv"/>, and the CSV header
|
||||||
/// together. <c>GpuUs</c> is <c>-1</c> for a frame with no available GPU
|
/// together. <c>GpuUs</c> is <c>-1</c> for a frame with no available GPU
|
||||||
/// sample (warm-up, or <c>ACDREAM_WB_DIAG=1</c> self-disable).
|
/// sample (warm-up, or <c>ACDREAM_WB_DIAG=1</c> self-disable).
|
||||||
|
|
@ -44,7 +44,7 @@ internal readonly record struct FrameHistoryRecord(
|
||||||
long PacingUs);
|
long PacingUs);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// MP0 (2026-07-05) — the permanent honest frame profiler. One
|
/// MP0 (2026-07-05) — the permanent honest frame profiler. One
|
||||||
/// <c>FrameBoundary</c> call at the top of the accepted render transaction
|
/// <c>FrameBoundary</c> call at the top of the accepted render transaction
|
||||||
/// measures CPU frame time as the delta between consecutive boundaries
|
/// measures CPU frame time as the delta between consecutive boundaries
|
||||||
/// (captures the FULL frame including present) and samples per-frame allocated
|
/// (captures the FULL frame including present) and samples per-frame allocated
|
||||||
|
|
@ -56,29 +56,29 @@ internal readonly record struct FrameHistoryRecord(
|
||||||
/// <see cref="RenderingDiagnostics.FrameProfEnabled"/> is true; costs one
|
/// <see cref="RenderingDiagnostics.FrameProfEnabled"/> is true; costs one
|
||||||
/// bool check per frame when off.
|
/// bool check per frame when off.
|
||||||
///
|
///
|
||||||
/// <para>Permanent apparatus — every MP-track gate reads it; do not strip.
|
/// <para>Permanent apparatus — every MP-track gate reads it; do not strip.
|
||||||
/// Whole-frame GPU timing self-disables under <c>ACDREAM_WB_DIAG=1</c>
|
/// Whole-frame GPU timing self-disables under <c>ACDREAM_WB_DIAG=1</c>
|
||||||
/// (nested TimeElapsed is illegal GL; see GpuFrameTimer).</para>
|
/// (nested TimeElapsed is illegal GL; see GpuFrameTimer).</para>
|
||||||
///
|
///
|
||||||
/// <para>2026-07-24 measurement-tooling review — the aggregated report
|
/// <para>2026-07-24 measurement-tooling review — the aggregated report
|
||||||
/// resets its ring buffers every ~5 s (<see cref="FrameStatsBuffer.Reset"/>),
|
/// resets its ring buffers every ~5 s (<see cref="FrameStatsBuffer.Reset"/>),
|
||||||
/// so route-wide p50/p95/p99 distributions across a whole soak cannot be
|
/// so route-wide p50/p95/p99 distributions across a whole soak cannot be
|
||||||
/// reconstructed after the fact. <see cref="RenderingDiagnostics.FrameHistoryPath"/>
|
/// reconstructed after the fact. <see cref="RenderingDiagnostics.FrameHistoryPath"/>
|
||||||
/// (<c>ACDREAM_FRAME_HISTORY=<path></c>) opts into a SEPARATE
|
/// (<c>ACDREAM_FRAME_HISTORY=<path></c>) opts into a SEPARATE
|
||||||
/// per-frame history: one <see cref="FrameHistoryRecord"/> per frame in a
|
/// per-frame history: one <see cref="FrameHistoryRecord"/> per frame in a
|
||||||
/// preallocated, grow-as-needed <see cref="List{T}"/> (1 int + 1 double +
|
/// preallocated, grow-as-needed <see cref="List{T}"/> (1 int + 1 double +
|
||||||
/// 7 longs ≈ 72 bytes/record; a multi-hour capture at 165 fps is roughly
|
/// 7 longs ≈ 72 bytes/record; a multi-hour capture at 165 fps is roughly
|
||||||
/// 165 * 3600 * 72 bytes ≈ 43 MB/hour — fine for a bounded diagnostic run,
|
/// 165 * 3600 * 72 bytes ≈ 43 MB/hour — fine for a bounded diagnostic run,
|
||||||
/// not for unattended day-long capture). Its initial capacity is about 9 MiB
|
/// not for unattended day-long capture). Its initial capacity is about 9 MiB
|
||||||
/// and covers the canonical route above 300 FPS without a frame-thread resize.
|
/// and covers the canonical route above 300 FPS without a frame-thread resize.
|
||||||
/// ZERO frame-thread I/O: the CSV is written once, from
|
/// ZERO frame-thread I/O: the CSV is written once, from
|
||||||
/// <see cref="Dispose"/>, at shutdown. Recording only takes effect while
|
/// <see cref="Dispose"/>, at shutdown. Recording only takes effect while
|
||||||
/// <see cref="RenderingDiagnostics.FrameProfEnabled"/> is ALSO true — it
|
/// <see cref="RenderingDiagnostics.FrameProfEnabled"/> is ALSO true — it
|
||||||
/// reuses that instrumentation rather than duplicating it. Does not
|
/// reuses that instrumentation rather than duplicating it. Does not
|
||||||
/// change the <c>[frame-prof]</c> report format or any existing metric.</para>
|
/// change the <c>[frame-prof]</c> report format or any existing metric.</para>
|
||||||
/// Spec: docs/superpowers/specs/2026-07-05-modern-pipeline-design.md §5.
|
/// Spec: docs/superpowers/specs/2026-07-05-modern-pipeline-design.md §5.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class FrameProfiler : IDisposable
|
internal sealed class FrameProfiler : IDisposable
|
||||||
{
|
{
|
||||||
private const int WindowCapacity = 2048; // ~12 s at 165 fps
|
private const int WindowCapacity = 2048; // ~12 s at 165 fps
|
||||||
private const int HistoryInitialCapacity = 131072;
|
private const int HistoryInitialCapacity = 131072;
|
||||||
|
|
@ -136,7 +136,7 @@ public sealed class FrameProfiler : IDisposable
|
||||||
if (_wasEnabled)
|
if (_wasEnabled)
|
||||||
{
|
{
|
||||||
// Dispose (not just Stop) so a later re-enable rebuilds the
|
// Dispose (not just Stop) so a later re-enable rebuilds the
|
||||||
// query ring fresh — a kept instance would poll slots left
|
// query ring fresh — a kept instance would poll slots left
|
||||||
// pending from BEFORE the pause and report temporally stale
|
// pending from BEFORE the pause and report temporally stale
|
||||||
// GPU samples. Safe here: this runs at the top of OnRender
|
// GPU samples. Safe here: this runs at the top of OnRender
|
||||||
// with the GL context current.
|
// with the GL context current.
|
||||||
|
|
@ -163,7 +163,7 @@ public sealed class FrameProfiler : IDisposable
|
||||||
{
|
{
|
||||||
// First enabled frame (startup or runtime toggle-on): establish
|
// First enabled frame (startup or runtime toggle-on): establish
|
||||||
// baselines, emit nothing. Clear any stage ticks a StageScope
|
// baselines, emit nothing. Clear any stage ticks a StageScope
|
||||||
// disposed after toggle-off may have accumulated mid-pause —
|
// disposed after toggle-off may have accumulated mid-pause —
|
||||||
// EndStage still runs on scopes that were live when the flag
|
// EndStage still runs on scopes that were live when the flag
|
||||||
// flipped, and that partial delta must not leak into the first
|
// flipped, and that partial delta must not leak into the first
|
||||||
// re-enabled frame.
|
// re-enabled frame.
|
||||||
|
|
@ -274,7 +274,7 @@ public sealed class FrameProfiler : IDisposable
|
||||||
internal void EndStage(FrameStage stage, long startTimestamp)
|
internal void EndStage(FrameStage stage, long startTimestamp)
|
||||||
=> _stageAccumTicks[(int)stage] += Stopwatch.GetTimestamp() - startTimestamp;
|
=> _stageAccumTicks[(int)stage] += Stopwatch.GetTimestamp() - startTimestamp;
|
||||||
|
|
||||||
/// <summary>Pure report formatter — unit-tested; invariant culture.</summary>
|
/// <summary>Pure report formatter — unit-tested; invariant culture.</summary>
|
||||||
public static string FormatReport(
|
public static string FormatReport(
|
||||||
int frameCount,
|
int frameCount,
|
||||||
FrameStatsBuffer cpu, FrameStatsBuffer gpu, bool gpuActive,
|
FrameStatsBuffer cpu, FrameStatsBuffer gpu, bool gpuActive,
|
||||||
|
|
@ -301,7 +301,7 @@ public sealed class FrameProfiler : IDisposable
|
||||||
return sb.ToString();
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Pure CSV formatter — unit-tested; invariant culture. One header row plus one row per record.</summary>
|
/// <summary>Pure CSV formatter — unit-tested; invariant culture. One header row plus one row per record.</summary>
|
||||||
internal static void WriteHistoryCsv(
|
internal static void WriteHistoryCsv(
|
||||||
IEnumerable<FrameHistoryRecord> records,
|
IEnumerable<FrameHistoryRecord> records,
|
||||||
TextWriter writer,
|
TextWriter writer,
|
||||||
|
|
@ -330,7 +330,7 @@ public sealed class FrameProfiler : IDisposable
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Shutdown-only write of the accumulated <see cref="_history"/> as CSV
|
/// Shutdown-only write of the accumulated <see cref="_history"/> as CSV
|
||||||
/// (the ONLY I/O this feature performs — never from <see cref="FrameBoundary"/>).
|
/// (the ONLY I/O this feature performs — never from <see cref="FrameBoundary"/>).
|
||||||
/// Failures are logged, not thrown: a history-export problem must never
|
/// Failures are logged, not thrown: a history-export problem must never
|
||||||
/// block the rest of the render-owner shutdown chain
|
/// block the rest of the render-owner shutdown chain
|
||||||
/// (<c>GameWindowLifetime</c>'s <c>Hard("frame profiler", ...)</c> stage).
|
/// (<c>GameWindowLifetime</c>'s <c>Hard("frame profiler", ...)</c> stage).
|
||||||
|
|
@ -355,7 +355,7 @@ public sealed class FrameProfiler : IDisposable
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Disposable stage scope; default instance is a no-op.</summary>
|
/// <summary>Disposable stage scope; default instance is a no-op.</summary>
|
||||||
public readonly struct StageScope : IDisposable
|
internal readonly struct StageScope : IDisposable
|
||||||
{
|
{
|
||||||
private readonly FrameProfiler? _owner;
|
private readonly FrameProfiler? _owner;
|
||||||
private readonly FrameStage _stage;
|
private readonly FrameStage _stage;
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace AcDream.App.Diagnostics;
|
namespace AcDream.App.Diagnostics;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// MP0 (2026-07-05) — fixed-capacity ring buffer of long samples
|
/// MP0 (2026-07-05) — fixed-capacity ring buffer of long samples
|
||||||
/// (microseconds or bytes) with percentile/max over the current window.
|
/// (microseconds or bytes) with percentile/max over the current window.
|
||||||
/// Pure and allocation-free after construction: <see cref="Percentile"/>
|
/// Pure and allocation-free after construction: <see cref="Percentile"/>
|
||||||
/// sorts into a preallocated scratch array, so the 5-second report path
|
/// sorts into a preallocated scratch array, so the 5-second report path
|
||||||
/// allocates nothing. Not thread-safe — owned by the window loop thread.
|
/// allocates nothing. Not thread-safe — owned by the window loop thread.
|
||||||
/// Spec: docs/superpowers/specs/2026-07-05-modern-pipeline-design.md §5.
|
/// Spec: docs/superpowers/specs/2026-07-05-modern-pipeline-design.md §5.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class FrameStatsBuffer
|
internal sealed class FrameStatsBuffer
|
||||||
{
|
{
|
||||||
private readonly long[] _samples;
|
private readonly long[] _samples;
|
||||||
private readonly long[] _scratch;
|
private readonly long[] _scratch;
|
||||||
|
|
@ -41,7 +41,7 @@ public sealed class FrameStatsBuffer
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Nearest-rank percentile over the current window: element at
|
/// Nearest-rank percentile over the current window: element at
|
||||||
/// ceil(q·n) in the ascending sort (1-based), 0 when empty.
|
/// ceil(q·n) in the ascending sort (1-based), 0 when empty.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public long Percentile(double q)
|
public long Percentile(double q)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -2,3 +2,8 @@ global using AcDream.Runtime.Gameplay;
|
||||||
global using AcDream.Runtime.Physics;
|
global using AcDream.Runtime.Physics;
|
||||||
global using ILocalPlayerMotionSource =
|
global using ILocalPlayerMotionSource =
|
||||||
AcDream.Runtime.Gameplay.IRuntimeLocalPlayerMotionSource;
|
AcDream.Runtime.Gameplay.IRuntimeLocalPlayerMotionSource;
|
||||||
|
// Campaign V slice V4a: GpuTextureSlot (and the rest of the RHI contract) is
|
||||||
|
// used pervasively once TextRenderer/TextureCache's UI path stop dealing in
|
||||||
|
// raw GL texture names, so it is imported project-wide rather than adding a
|
||||||
|
// `using AcDream.App.Rendering.Gpu;` to every UI file that resolves a sprite.
|
||||||
|
global using AcDream.App.Rendering.Gpu;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using AcDream.Core.World;
|
using AcDream.Core.World;
|
||||||
using AcDream.App.Physics;
|
using AcDream.App.Physics;
|
||||||
using AcDream.App.World;
|
using AcDream.App.World;
|
||||||
|
|
||||||
|
|
@ -59,7 +59,7 @@ internal sealed class LiveLocalPlayerProjectionRuntime
|
||||||
/// Projects the canonical local physics body into world rendering and spatial
|
/// Projects the canonical local physics body into world rendering and spatial
|
||||||
/// buckets without advancing it.
|
/// buckets without advancing it.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class LocalPlayerProjectionController
|
internal sealed class LocalPlayerProjectionController
|
||||||
{
|
{
|
||||||
private readonly ILocalPlayerProjectionRuntime _runtime;
|
private readonly ILocalPlayerProjectionRuntime _runtime;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using AcDream.App.Net;
|
using AcDream.App.Net;
|
||||||
using AcDream.App.Streaming;
|
using AcDream.App.Streaming;
|
||||||
using AcDream.App.World;
|
using AcDream.App.World;
|
||||||
|
|
@ -6,7 +6,7 @@ using AcDream.App.World;
|
||||||
namespace AcDream.App.Input;
|
namespace AcDream.App.Input;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Phase K.2 — one-shot guard that auto-enters player mode after a
|
/// Phase K.2 — one-shot guard that auto-enters player mode after a
|
||||||
/// successful login once every prerequisite is satisfied. The update-frame
|
/// successful login once every prerequisite is satisfied. The update-frame
|
||||||
/// orchestrator ticks it through a typed production context.
|
/// orchestrator ticks it through a typed production context.
|
||||||
///
|
///
|
||||||
|
|
@ -16,7 +16,7 @@ namespace AcDream.App.Input;
|
||||||
/// entity has been streamed into the world dictionary, the player
|
/// entity has been streamed into the world dictionary, the player
|
||||||
/// movement controller is constructible, and the initial world is fully
|
/// movement controller is constructible, and the initial world is fully
|
||||||
/// drawable and collidable) plus a manual-override path
|
/// drawable and collidable) plus a manual-override path
|
||||||
/// (the user can flip into fly mode before the auto-entry fires —
|
/// (the user can flip into fly mode before the auto-entry fires —
|
||||||
/// their choice wins). All five interact with each other in a way
|
/// their choice wins). All five interact with each other in a way
|
||||||
/// that's painful to test through GameWindow but trivial here against
|
/// that's painful to test through GameWindow but trivial here against
|
||||||
/// fakes.
|
/// fakes.
|
||||||
|
|
@ -25,11 +25,11 @@ namespace AcDream.App.Input;
|
||||||
/// <para>
|
/// <para>
|
||||||
/// The public surface is:
|
/// The public surface is:
|
||||||
/// <list type="bullet">
|
/// <list type="bullet">
|
||||||
/// <item><see cref="Arm"/> — call after <c>EnterWorld</c> succeeds to
|
/// <item><see cref="Arm"/> — call after <c>EnterWorld</c> succeeds to
|
||||||
/// arm the entry trigger.</item>
|
/// arm the entry trigger.</item>
|
||||||
/// <item><see cref="Cancel"/> — call when the user manually enters
|
/// <item><see cref="Cancel"/> — call when the user manually enters
|
||||||
/// fly mode (or any other code path that pre-empts the auto-entry).</item>
|
/// fly mode (or any other code path that pre-empts the auto-entry).</item>
|
||||||
/// <item><see cref="TryEnter"/> — call once per frame; runs the
|
/// <item><see cref="TryEnter"/> — call once per frame; runs the
|
||||||
/// guard and fires the entry callback when armed AND every
|
/// guard and fires the entry callback when armed AND every
|
||||||
/// precondition is satisfied; returns true on the firing tick.</item>
|
/// precondition is satisfied; returns true on the firing tick.</item>
|
||||||
/// </list>
|
/// </list>
|
||||||
|
|
@ -101,7 +101,7 @@ internal sealed class LivePlayerModeAutoEntryContext
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class PlayerModeAutoEntry
|
internal sealed class PlayerModeAutoEntry
|
||||||
{
|
{
|
||||||
private sealed class DelegateContext : IPlayerModeAutoEntryContext
|
private sealed class DelegateContext : IPlayerModeAutoEntryContext
|
||||||
{
|
{
|
||||||
|
|
@ -163,7 +163,7 @@ public sealed class PlayerModeAutoEntry
|
||||||
/// Retail keeps position completion behind one blocking cell-load edge;
|
/// Retail keeps position completion behind one blocking cell-load edge;
|
||||||
/// acdream's asynchronous domains must converge before entry.</param>
|
/// acdream's asynchronous domains must converge before entry.</param>
|
||||||
/// <param name="enterPlayerMode">Action invoked on the firing
|
/// <param name="enterPlayerMode">Action invoked on the firing
|
||||||
/// tick. The same routine the manual Tab handler invokes (fly →
|
/// tick. The same routine the manual Tab handler invokes (fly →
|
||||||
/// player transition). Must construct the controller + chase
|
/// player transition). Must construct the controller + chase
|
||||||
/// camera and switch the active camera; the auto-entry doesn't
|
/// camera and switch the active camera; the auto-entry doesn't
|
||||||
/// reach inside.</param>
|
/// reach inside.</param>
|
||||||
|
|
@ -202,7 +202,7 @@ public sealed class PlayerModeAutoEntry
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Disarm the trigger without firing the callback. Call when the
|
/// Disarm the trigger without firing the callback. Call when the
|
||||||
/// user has manually entered fly mode (or any other code path
|
/// user has manually entered fly mode (or any other code path
|
||||||
/// that pre-empts the auto-entry) — the user's choice wins.
|
/// that pre-empts the auto-entry) — the user's choice wins.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Cancel() => _armed = false;
|
public void Cancel() => _armed = false;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using AcDream.App.Update;
|
using AcDream.App.Update;
|
||||||
using AcDream.Runtime;
|
using AcDream.Runtime;
|
||||||
|
|
||||||
namespace AcDream.App.Input;
|
namespace AcDream.App.Input;
|
||||||
|
|
@ -17,7 +17,7 @@ internal sealed class RetailLocalPlayerFrameController : IPostNetworkCommandFram
|
||||||
{
|
{
|
||||||
private readonly RuntimeLocalPlayerFrameController _runtime;
|
private readonly RuntimeLocalPlayerFrameController _runtime;
|
||||||
|
|
||||||
public readonly record struct PresentationFrame(
|
internal readonly record struct PresentationFrame(
|
||||||
MovementResult Movement,
|
MovementResult Movement,
|
||||||
bool Hidden,
|
bool Hidden,
|
||||||
bool AdvancedBeforeNetwork);
|
bool AdvancedBeforeNetwork);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using AcDream.App.Rendering;
|
using AcDream.App.Rendering;
|
||||||
using AcDream.UI.Abstractions.Input;
|
using AcDream.UI.Abstractions.Input;
|
||||||
using Silk.NET.Input;
|
using Silk.NET.Input;
|
||||||
|
|
||||||
|
|
@ -73,7 +73,7 @@ internal sealed class SilkKeyboardEventSurface : IKeyboardEventSurface
|
||||||
/// subscription. Logical deactivation makes copied delegates inert while
|
/// subscription. Logical deactivation makes copied delegates inert while
|
||||||
/// physical removal is retried.
|
/// physical removal is retried.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class SilkKeyboardSource : IKeyboardSource, IDisposable
|
internal sealed class SilkKeyboardSource : IKeyboardSource, IDisposable
|
||||||
{
|
{
|
||||||
private readonly IKeyboardEventSurface _surface;
|
private readonly IKeyboardEventSurface _surface;
|
||||||
private readonly HostQuiescenceGate _quiescence;
|
private readonly HostQuiescenceGate _quiescence;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AcDream.App.Rendering;
|
using AcDream.App.Rendering;
|
||||||
using AcDream.UI.Abstractions.Input;
|
using AcDream.UI.Abstractions.Input;
|
||||||
using Silk.NET.Input;
|
using Silk.NET.Input;
|
||||||
|
|
@ -100,7 +100,7 @@ internal sealed class SilkMouseEventSurface : IMouseEventSurface
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Reversible Silk mouse bridge with immediate logical cutoff.</summary>
|
/// <summary>Reversible Silk mouse bridge with immediate logical cutoff.</summary>
|
||||||
public sealed class SilkMouseSource : IMouseSource, IDisposable
|
internal sealed class SilkMouseSource : IMouseSource, IDisposable
|
||||||
{
|
{
|
||||||
private readonly IMouseEventSurface _surface;
|
private readonly IMouseEventSurface _surface;
|
||||||
private readonly IInputCaptureSource _capture;
|
private readonly IInputCaptureSource _capture;
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
|
||||||
namespace AcDream.App.Physics;
|
namespace AcDream.App.Physics;
|
||||||
|
|
||||||
/// <summary>Session-scoped cache of the local player's last published shadow pose.</summary>
|
/// <summary>Session-scoped cache of the local player's last published shadow pose.</summary>
|
||||||
internal sealed class LocalPlayerShadowState
|
internal sealed class LocalPlayerShadowState
|
||||||
{
|
{
|
||||||
public readonly record struct Snapshot(
|
internal readonly record struct Snapshot(
|
||||||
Vector3 Position,
|
Vector3 Position,
|
||||||
Quaternion Orientation,
|
Quaternion Orientation,
|
||||||
uint CellId);
|
uint CellId);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
|
|
||||||
namespace AcDream.App.Physics;
|
namespace AcDream.App.Physics;
|
||||||
|
|
||||||
|
|
@ -8,7 +8,7 @@ namespace AcDream.App.Physics;
|
||||||
/// testable while the concrete movement/interpolation/target owners remain
|
/// testable while the concrete movement/interpolation/target owners remain
|
||||||
/// in the composition root.
|
/// in the composition root.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class RemoteTeleportHook
|
internal static class RemoteTeleportHook
|
||||||
{
|
{
|
||||||
private const WeenieError TeleportCancelContext = (WeenieError)0x3Cu;
|
private const WeenieError TeleportCancelContext = (WeenieError)0x3Cu;
|
||||||
|
|
||||||
|
|
@ -46,7 +46,7 @@ public static class RemoteTeleportHook
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed record RemoteTeleportHookActions(
|
internal sealed record RemoteTeleportHookActions(
|
||||||
Action<WeenieError> CancelMoveTo,
|
Action<WeenieError> CancelMoveTo,
|
||||||
Action UnStick,
|
Action UnStick,
|
||||||
Action StopInterpolating,
|
Action StopInterpolating,
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
using AcDream.Plugin.Abstractions;
|
using AcDream.Plugin.Abstractions;
|
||||||
|
|
||||||
namespace AcDream.App.Plugins;
|
namespace AcDream.App.Plugins;
|
||||||
|
|
||||||
public sealed class AppPluginHost : IPluginHost
|
internal sealed class AppPluginHost : IPluginHost
|
||||||
{
|
{
|
||||||
public AppPluginHost(
|
public AppPluginHost(
|
||||||
IPluginLogger log,
|
IPluginLogger log,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using AcDream.Plugin.Abstractions;
|
using AcDream.Plugin.Abstractions;
|
||||||
|
|
||||||
namespace AcDream.App.Plugins;
|
namespace AcDream.App.Plugins;
|
||||||
|
|
@ -8,9 +8,9 @@ namespace AcDream.App.Plugins;
|
||||||
/// Program.cs before the GL window opens) until GameWindow drains them into the
|
/// Program.cs before the GL window opens) until GameWindow drains them into the
|
||||||
/// UiHost tree after construction.
|
/// UiHost tree after construction.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class BufferedUiRegistry : IUiRegistry
|
internal sealed class BufferedUiRegistry : IUiRegistry
|
||||||
{
|
{
|
||||||
public readonly record struct Pending(string MarkupPath, object Binding);
|
internal readonly record struct Pending(string MarkupPath, object Binding);
|
||||||
|
|
||||||
private readonly List<Pending> _pending = new();
|
private readonly List<Pending> _pending = new();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
using AcDream.Plugin.Abstractions;
|
using AcDream.Plugin.Abstractions;
|
||||||
|
|
||||||
namespace AcDream.App.Plugins;
|
namespace AcDream.App.Plugins;
|
||||||
|
|
||||||
public sealed class SerilogAdapter : IPluginLogger
|
internal sealed class SerilogAdapter : IPluginLogger
|
||||||
{
|
{
|
||||||
private readonly Serilog.ILogger _log;
|
private readonly Serilog.ILogger _log;
|
||||||
public SerilogAdapter(Serilog.ILogger log) => _log = log;
|
public SerilogAdapter(Serilog.ILogger log) => _log = log;
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,20 @@
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using Silk.NET.OpenGL;
|
|
||||||
using StbTrueTypeSharp;
|
using StbTrueTypeSharp;
|
||||||
|
|
||||||
namespace AcDream.App.Rendering;
|
namespace AcDream.App.Rendering;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A pixel-font atlas rasterized from a TTF at load time using stb_truetype.
|
/// A pixel-font atlas rasterized from a TTF at load time using stb_truetype.
|
||||||
/// Glyphs are packed into a single-channel (R8) GL texture. Call
|
/// Glyphs are packed into a single-channel (R8) texture registered in the
|
||||||
/// <see cref="TryGetGlyph"/> to resolve an ASCII codepoint to UV + metrics.
|
/// device's global texture table. Call <see cref="TryGetGlyph"/> to resolve an
|
||||||
|
/// ASCII codepoint to UV + metrics.
|
||||||
///
|
///
|
||||||
/// Only printable ASCII (32..127) is supported for the debug overlay.
|
/// Only printable ASCII (32..127) is supported for the debug overlay.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed unsafe class BitmapFont : IDisposable
|
internal sealed unsafe class BitmapFont : IDisposable
|
||||||
{
|
{
|
||||||
public readonly struct Glyph
|
internal readonly struct Glyph
|
||||||
{
|
{
|
||||||
public readonly float UvMinX;
|
public readonly float UvMinX;
|
||||||
public readonly float UvMinY;
|
public readonly float UvMinY;
|
||||||
|
|
@ -34,23 +34,23 @@ public sealed unsafe class BitmapFont : IDisposable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly GL _gl;
|
|
||||||
private readonly Glyph[] _glyphs;
|
private readonly Glyph[] _glyphs;
|
||||||
private readonly int _firstChar;
|
private readonly int _firstChar;
|
||||||
private readonly int _numChars;
|
private readonly int _numChars;
|
||||||
private readonly ResourceCleanupGroup _resources;
|
private readonly IGpuDevice _device;
|
||||||
|
private readonly IGpuTexture _texture;
|
||||||
|
|
||||||
public uint TextureId { get; }
|
public GpuTextureSlot TextureId { get; }
|
||||||
public float PixelHeight { get; }
|
public float PixelHeight { get; }
|
||||||
public float LineHeight { get; }
|
public float LineHeight { get; }
|
||||||
public float Ascent { get; }
|
public float Ascent { get; }
|
||||||
public int AtlasWidth { get; }
|
public int AtlasWidth { get; }
|
||||||
public int AtlasHeight { get; }
|
public int AtlasHeight { get; }
|
||||||
|
|
||||||
public BitmapFont(GL gl, byte[] ttfBytes, float pixelHeight,
|
public BitmapFont(IGpuDevice device, byte[] ttfBytes, float pixelHeight,
|
||||||
int atlasSize = 512, int firstChar = 32, int numChars = 96)
|
int atlasSize = 512, int firstChar = 32, int numChars = 96)
|
||||||
{
|
{
|
||||||
_gl = gl;
|
_device = device ?? throw new ArgumentNullException(nameof(device));
|
||||||
PixelHeight = pixelHeight;
|
PixelHeight = pixelHeight;
|
||||||
AtlasWidth = atlasSize;
|
AtlasWidth = atlasSize;
|
||||||
AtlasHeight = atlasSize;
|
AtlasHeight = atlasSize;
|
||||||
|
|
@ -96,65 +96,31 @@ public sealed unsafe class BitmapFont : IDisposable
|
||||||
adv: bc.xadvance);
|
adv: bc.xadvance);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upload atlas as a single-channel GL texture (R8). Publish the GL
|
// Upload atlas as a single-channel texture (R8) and register it in the
|
||||||
// name into the construction ledger before any later upload/state
|
// device's global texture table. Linear + clamp-to-edge matches the
|
||||||
// command can fail.
|
// GL path's prior fixed sampler state exactly (mip filtering is moot —
|
||||||
var resources = new ResourceCleanupGroup();
|
// the atlas is a single mip level).
|
||||||
uint texture = 0;
|
IGpuTexture texture = _device.CreateTexture(new GpuTextureDescription(
|
||||||
|
"bitmap-font-atlas",
|
||||||
|
GpuTextureKind.Texture2D,
|
||||||
|
GpuTextureFormat.R8Unorm,
|
||||||
|
Width: AtlasWidth,
|
||||||
|
Height: AtlasHeight,
|
||||||
|
LayerCount: 1,
|
||||||
|
MipLevelCount: 1));
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
texture = GlResourceCommand.CreateTexture(_gl, "BitmapFont atlas");
|
texture.Upload(0, 0, pixels);
|
||||||
uint ownedTexture = texture;
|
IGpuSampler sampler = _device.CreateSampler(GpuSamplerDescription.WorldClamp);
|
||||||
resources.Add(
|
TextureId = _device.RegisterTexture(texture, sampler);
|
||||||
"bitmap-font atlas",
|
|
||||||
() => GlResourceCommand.DeleteTexture(
|
|
||||||
_gl,
|
|
||||||
ownedTexture,
|
|
||||||
$"delete BitmapFont atlas {ownedTexture}"));
|
|
||||||
_gl.GetInteger(GetPName.TextureBinding2D, out int previousTexture);
|
|
||||||
_gl.GetInteger(GetPName.UnpackAlignment, out int previousAlignment);
|
|
||||||
GlResourceCommand.Execute(_gl, "initialize BitmapFont atlas", () =>
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_gl.BindTexture(TextureTarget.Texture2D, texture);
|
|
||||||
_gl.PixelStore(PixelStoreParameter.UnpackAlignment, 1);
|
|
||||||
fixed (byte* ptr = pixels)
|
|
||||||
{
|
|
||||||
_gl.TexImage2D(TextureTarget.Texture2D, 0,
|
|
||||||
(int)InternalFormat.R8,
|
|
||||||
(uint)AtlasWidth, (uint)AtlasHeight, 0,
|
|
||||||
PixelFormat.Red, PixelType.UnsignedByte, ptr);
|
|
||||||
}
|
|
||||||
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter,
|
|
||||||
(int)TextureMinFilter.Linear);
|
|
||||||
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter,
|
|
||||||
(int)TextureMagFilter.Linear);
|
|
||||||
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS,
|
|
||||||
(int)TextureWrapMode.ClampToEdge);
|
|
||||||
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT,
|
|
||||||
(int)TextureWrapMode.ClampToEdge);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_gl.PixelStore(
|
|
||||||
PixelStoreParameter.UnpackAlignment,
|
|
||||||
previousAlignment);
|
|
||||||
_gl.BindTexture(
|
|
||||||
TextureTarget.Texture2D,
|
|
||||||
unchecked((uint)previousTexture));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
catch (Exception constructionFailure)
|
catch
|
||||||
{
|
{
|
||||||
resources.RollbackConstructionAndThrow(
|
texture.Dispose();
|
||||||
"BitmapFont construction failed and its GL atlas did not cleanly roll back.",
|
throw;
|
||||||
constructionFailure);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TextureId = texture;
|
_texture = texture;
|
||||||
_resources = resources;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryGetGlyph(char c, out Glyph g)
|
public bool TryGetGlyph(char c, out Glyph g)
|
||||||
|
|
@ -183,7 +149,8 @@ public sealed unsafe class BitmapFont : IDisposable
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_resources.RetryCleanup();
|
_device.ReleaseTextureSlot(TextureId);
|
||||||
|
_texture.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
// src/AcDream.App/Rendering/CameraController.cs
|
// src/AcDream.App/Rendering/CameraController.cs
|
||||||
using AcDream.Core.Rendering;
|
using AcDream.Core.Rendering;
|
||||||
|
|
||||||
namespace AcDream.App.Rendering;
|
namespace AcDream.App.Rendering;
|
||||||
|
|
||||||
public sealed class CameraController
|
internal sealed class CameraController
|
||||||
{
|
{
|
||||||
internal readonly record struct CameraState(
|
internal readonly record struct CameraState(
|
||||||
int ModeCode,
|
int ModeCode,
|
||||||
|
|
@ -19,7 +19,7 @@ public sealed class CameraController
|
||||||
/// The renderer-facing active camera. Both the legacy and retail
|
/// The renderer-facing active camera. Both the legacy and retail
|
||||||
/// chase cameras are held simultaneously so that flipping
|
/// chase cameras are held simultaneously so that flipping
|
||||||
/// <see cref="CameraDiagnostics.UseRetailChaseCamera"/> takes effect
|
/// <see cref="CameraDiagnostics.UseRetailChaseCamera"/> takes effect
|
||||||
/// on the very next access to this property — no re-entry required,
|
/// on the very next access to this property — no re-entry required,
|
||||||
/// no notification mechanism, no stale state.
|
/// no notification mechanism, no stale state.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ICamera Active
|
public ICamera Active
|
||||||
|
|
@ -59,7 +59,7 @@ public sealed class CameraController
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Store both cameras simultaneously; <see cref="Active"/> picks
|
/// Store both cameras simultaneously; <see cref="Active"/> picks
|
||||||
/// between them per-read via the flag — no re-entry needed on flip.
|
/// between them per-read via the flag — no re-entry needed on flip.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void EnterChaseMode(ChaseCamera legacy, RetailChaseCamera retail)
|
public void EnterChaseMode(ChaseCamera legacy, RetailChaseCamera retail)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
// CellVisibility.cs — portal-based interior cell visibility system.
|
// CellVisibility.cs — portal-based interior cell visibility system.
|
||||||
//
|
//
|
||||||
// Stage 3 (2026-06-02): FindCameraCell + grace-frame AABB fallback deleted.
|
// Stage 3 (2026-06-02): FindCameraCell + grace-frame AABB fallback deleted.
|
||||||
// The physics membership answer (CellGraph.CurrCell) is now the mandatory root;
|
// The physics membership answer (CellGraph.CurrCell) is now the mandatory root;
|
||||||
// ComputeVisibilityFromRoot(null, …) returns null (outdoor root) rather than
|
// ComputeVisibilityFromRoot(null, …) returns null (outdoor root) rather than
|
||||||
// falling back to an independent AABB position resolve. This matches retail's
|
// falling back to an independent AABB position resolve. This matches retail's
|
||||||
// CellManager::ChangePosition (0x004559B0) which does not re-derive the cell
|
// CellManager::ChangePosition (0x004559B0) which does not re-derive the cell
|
||||||
// from a static position — it reads the swept transition-owned CurrCell.
|
// from a static position — it reads the swept transition-owned CurrCell.
|
||||||
//
|
//
|
||||||
// This file is intentionally free of GL / rendering types. It depends only on
|
// This file is intentionally free of GL / rendering types. It depends only on
|
||||||
// System.Numerics so it can be unit-tested without a GPU context.
|
// System.Numerics so it can be unit-tested without a GPU context.
|
||||||
|
|
@ -23,7 +23,7 @@ namespace AcDream.App.Rendering;
|
||||||
/// A loaded EnvCell with portal connectivity and spatial data, used by
|
/// A loaded EnvCell with portal connectivity and spatial data, used by
|
||||||
/// <see cref="CellVisibility"/> for portal-traversal visibility decisions.
|
/// <see cref="CellVisibility"/> for portal-traversal visibility decisions.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class LoadedCell
|
internal sealed class LoadedCell
|
||||||
{
|
{
|
||||||
/// <summary>Full 32-bit cell ID, e.g. 0xA9B40105.</summary>
|
/// <summary>Full 32-bit cell ID, e.g. 0xA9B40105.</summary>
|
||||||
public uint CellId;
|
public uint CellId;
|
||||||
|
|
@ -87,7 +87,7 @@ public sealed class LoadedCell
|
||||||
public uint? BuildingId { get; internal set; }
|
public uint? BuildingId { get; internal set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Phase U.4c: the stab_list PVS as full (landblock-prefixed) cell ids — retail
|
/// Phase U.4c: the stab_list PVS as full (landblock-prefixed) cell ids — retail
|
||||||
/// CEnvCell.stab_list (acclient.h ~30925), the stable set of cells potentially
|
/// CEnvCell.stab_list (acclient.h ~30925), the stable set of cells potentially
|
||||||
/// visible from this cell, precomputed by the AC content tools. Refreshed only at
|
/// visible from this cell, precomputed by the AC content tools. Refreshed only at
|
||||||
/// hydration (= retail's per-cell-entry grab_visible_cells, decomp:311878).
|
/// hydration (= retail's per-cell-entry grab_visible_cells, decomp:311878).
|
||||||
|
|
@ -98,7 +98,7 @@ public sealed class LoadedCell
|
||||||
public IReadOnlyList<uint> VisibleCells = System.Array.Empty<uint>();
|
public IReadOnlyList<uint> VisibleCells = System.Array.Empty<uint>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Phase U.4c: retail CEnvCell.seen_outside (acclient.h ~30925) — this cell sees
|
/// Phase U.4c: retail CEnvCell.seen_outside (acclient.h ~30925) — this cell sees
|
||||||
/// the exterior (an exit portal is reachable from it). Retail gates the landscape
|
/// the exterior (an exit portal is reachable from it). Retail gates the landscape
|
||||||
/// data + draw decision on the camera cell's value (RenderNormalMode decomp:92649,
|
/// data + draw decision on the camera cell's value (RenderNormalMode decomp:92649,
|
||||||
/// grab_visible_cells decomp:311878). The stable anchor for the terrain-draw test.
|
/// grab_visible_cells decomp:311878). The stable anchor for the terrain-draw test.
|
||||||
|
|
@ -107,7 +107,7 @@ public sealed class LoadedCell
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Render unification (2026-06-07): true for the synthetic OUTDOOR cell node built by
|
/// Render unification (2026-06-07): true for the synthetic OUTDOOR cell node built by
|
||||||
/// <see cref="OutdoorCellNode.Build"/> — the outdoor world modelled as a flood-graph cell whose
|
/// <see cref="OutdoorCellNode.Build"/> — the outdoor world modelled as a flood-graph cell whose
|
||||||
/// shell is the landscape. <see cref="PortalVisibilityBuilder.Build"/> seeds OutsideView
|
/// shell is the landscape. <see cref="PortalVisibilityBuilder.Build"/> seeds OutsideView
|
||||||
/// full-screen when the root carries this flag (so terrain/sky/scenery draw as the node's shell).
|
/// full-screen when the root carries this flag (so terrain/sky/scenery draw as the node's shell).
|
||||||
/// An explicit flag, not a cell-id heuristic: interior EnvCell ids are >= 0x100 in production but
|
/// An explicit flag, not a cell-id heuristic: interior EnvCell ids are >= 0x100 in production but
|
||||||
|
|
@ -123,19 +123,19 @@ public sealed class LoadedCell
|
||||||
/// <see cref="OtherPortalId"/> is the dat's reciprocal back-link: the index of
|
/// <see cref="OtherPortalId"/> is the dat's reciprocal back-link: the index of
|
||||||
/// the portal WITHIN the neighbour cell's portal list that points back through
|
/// the portal WITHIN the neighbour cell's portal list that points back through
|
||||||
/// this same opening. Retail indexes the reciprocal directly via this field
|
/// this same opening. Retail indexes the reciprocal directly via this field
|
||||||
/// (<c>arg2->other_portal_id</c>, decomp:433557) rather than scanning — which
|
/// (<c>arg2->other_portal_id</c>, decomp:433557) rather than scanning — which
|
||||||
/// is what lets a cell with TWO portals to the same neighbour resolve each
|
/// is what lets a cell with TWO portals to the same neighbour resolve each
|
||||||
/// opening against its OWN reciprocal polygon instead of the first match.
|
/// opening against its OWN reciprocal polygon instead of the first match.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly record struct CellPortalInfo(
|
internal readonly record struct CellPortalInfo(
|
||||||
ushort OtherCellId, ushort PolygonId, ushort Flags, ushort OtherPortalId);
|
ushort OtherCellId, ushort PolygonId, ushort Flags, ushort OtherPortalId);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Clip plane derived from a portal polygon, in cell-local space.
|
/// Clip plane derived from a portal polygon, in cell-local space.
|
||||||
/// Plane equation: Normal.X*x + Normal.Y*y + Normal.Z*z + D = 0.
|
/// Plane equation: Normal.X*x + Normal.Y*y + Normal.Z*z + D = 0.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public struct PortalClipPlane
|
internal struct PortalClipPlane
|
||||||
{
|
{
|
||||||
/// <summary>Plane normal (cell-local space, unit length).</summary>
|
/// <summary>Plane normal (cell-local space, unit length).</summary>
|
||||||
public Vector3 Normal;
|
public Vector3 Normal;
|
||||||
|
|
@ -146,8 +146,8 @@ public struct PortalClipPlane
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Which half-space is "inside" this cell (the side from which you look outward
|
/// Which half-space is "inside" this cell (the side from which you look outward
|
||||||
/// through the portal):
|
/// through the portal):
|
||||||
/// 0 → camera dot-product must be >= 0 (positive half-space is inside)
|
/// 0 → camera dot-product must be >= 0 (positive half-space is inside)
|
||||||
/// 1 → camera dot-product must be <= 0 (negative half-space is inside)
|
/// 1 → camera dot-product must be <= 0 (negative half-space is inside)
|
||||||
/// Determined from cell centroid position relative to the portal plane.
|
/// Determined from cell centroid position relative to the portal plane.
|
||||||
/// Ported from ACME EnvCellManager.cs ~line 404.
|
/// Ported from ACME EnvCellManager.cs ~line 404.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -155,12 +155,12 @@ public struct PortalClipPlane
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Phase U.4c flap probe (diagnostic — OBSOLETE as of Stage 3). Previously tracked
|
/// Phase U.4c flap probe (diagnostic — OBSOLETE as of Stage 3). Previously tracked
|
||||||
/// which branch of FindCameraCell (now deleted) resolved the camera cell. Retained
|
/// which branch of FindCameraCell (now deleted) resolved the camera cell. Retained
|
||||||
/// for binary compatibility with the [flap-cam] probe log site in GameWindow.cs that
|
/// for binary compatibility with the [flap-cam] probe log site in GameWindow.cs that
|
||||||
/// still prints <see cref="LastCameraCellResolution"/> (always None post-Stage 3).
|
/// still prints <see cref="LastCameraCellResolution"/> (always None post-Stage 3).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public enum CameraCellResolution
|
internal enum CameraCellResolution
|
||||||
{
|
{
|
||||||
/// <summary>No cell contains the eye (outdoors), or not yet resolved.</summary>
|
/// <summary>No cell contains the eye (outdoors), or not yet resolved.</summary>
|
||||||
None,
|
None,
|
||||||
|
|
@ -171,14 +171,14 @@ public enum CameraCellResolution
|
||||||
/// <summary>The eye is inside a cell found by the full brute-force scan.</summary>
|
/// <summary>The eye is inside a cell found by the full brute-force scan.</summary>
|
||||||
BruteForce,
|
BruteForce,
|
||||||
/// <summary>The eye is inside NO cell, but the previous cell is kept alive for a
|
/// <summary>The eye is inside NO cell, but the previous cell is kept alive for a
|
||||||
/// few grace frames — the "stale root" case the flap probe watches for.</summary>
|
/// few grace frames — the "stale root" case the flap probe watches for.</summary>
|
||||||
Grace,
|
Grace,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Result of a portal-based visibility BFS from the camera cell.
|
/// Result of a portal-based visibility BFS from the camera cell.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class VisibilityResult
|
internal sealed class VisibilityResult
|
||||||
{
|
{
|
||||||
/// <summary>Full cell IDs (e.g. 0x01D90105) that should be rendered this frame.</summary>
|
/// <summary>Full cell IDs (e.g. 0x01D90105) that should be rendered this frame.</summary>
|
||||||
public HashSet<uint> VisibleCellIds { get; init; } = new();
|
public HashSet<uint> VisibleCellIds { get; init; } = new();
|
||||||
|
|
@ -207,7 +207,7 @@ public sealed class VisibilityResult
|
||||||
/// Ported faithfully from ACME's EnvCellManager.cs portal-visibility region.
|
/// Ported faithfully from ACME's EnvCellManager.cs portal-visibility region.
|
||||||
/// Constants and control flow match the ACME implementation.
|
/// Constants and control flow match the ACME implementation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class CellVisibility
|
internal sealed class CellVisibility
|
||||||
{
|
{
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// Constants (ACME ground-truth values)
|
// Constants (ACME ground-truth values)
|
||||||
|
|
@ -234,7 +234,7 @@ public sealed class CellVisibility
|
||||||
public VisibilityResult? LastVisibilityResult { get; private set; }
|
public VisibilityResult? LastVisibilityResult { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Stage 3 (2026-06-02): always <see cref="CameraCellResolution.None"/> — the FindCameraCell
|
/// Stage 3 (2026-06-02): always <see cref="CameraCellResolution.None"/> — the FindCameraCell
|
||||||
/// AABB grace-frame resolver was deleted; the physics membership answer is the sole root.
|
/// AABB grace-frame resolver was deleted; the physics membership answer is the sole root.
|
||||||
/// Retained for the [flap-cam] probe log line in GameWindow.cs.
|
/// Retained for the [flap-cam] probe log line in GameWindow.cs.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -292,7 +292,7 @@ public sealed class CellVisibility
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Phase A8 (2026-05-28): enumerates the loaded cells that belong to a
|
/// Phase A8 (2026-05-28): enumerates the loaded cells that belong to a
|
||||||
/// landblock prefix. Used by <c>LandblockRenderPublisher</c> when building
|
/// landblock prefix. Used by <c>LandblockRenderPublisher</c> when building
|
||||||
/// the per-landblock <c>BuildingRegistry</c> — the per-frame
|
/// the per-landblock <c>BuildingRegistry</c> — the per-frame
|
||||||
/// <c>drainedCells</c> dict misses cells loaded on prior frames, so the
|
/// <c>drainedCells</c> dict misses cells loaded on prior frames, so the
|
||||||
/// stamping loop in <see cref="Wb.BuildingLoader.Build"/> needs access to
|
/// stamping loop in <see cref="Wb.BuildingLoader.Build"/> needs access to
|
||||||
/// every cell currently in the landblock to ensure <c>BuildingId</c> is set.
|
/// every cell currently in the landblock to ensure <c>BuildingId</c> is set.
|
||||||
|
|
@ -356,15 +356,15 @@ public sealed class CellVisibility
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// UCG W2/Stage 3: compute visibility from a supplied root cell (the physics membership
|
/// UCG W2/Stage 3: compute visibility from a supplied root cell (the physics membership
|
||||||
/// answer). When <paramref name="root"/> is null (pre-spawn, or player outside all indoor
|
/// answer). When <paramref name="root"/> is null (pre-spawn, or player outside all indoor
|
||||||
/// cells), returns <c>null</c> — the caller interprets null as the outdoor root (no portal
|
/// cells), returns <c>null</c> — the caller interprets null as the outdoor root (no portal
|
||||||
/// frame, everything slot 0, terrain ungated). The legacy AABB FindCameraCell fallback is
|
/// frame, everything slot 0, terrain ungated). The legacy AABB FindCameraCell fallback is
|
||||||
/// deleted as of Stage 3; <see cref="CellGraph.CurrCell"/> is the sole authority.
|
/// deleted as of Stage 3; <see cref="CellGraph.CurrCell"/> is the sole authority.
|
||||||
/// Retail anchor: CellManager::ChangePosition @ 0x004559B0 reads the transition-owned
|
/// Retail anchor: CellManager::ChangePosition @ 0x004559B0 reads the transition-owned
|
||||||
/// curr_cell — it does NOT re-derive from a static position.
|
/// curr_cell — it does NOT re-derive from a static position.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="root">
|
/// <param name="root">
|
||||||
/// The render-registered <see cref="LoadedCell"/> that physics determined the player is inside,
|
/// The render-registered <see cref="LoadedCell"/> that physics determined the player is inside,
|
||||||
/// or null when pre-spawn or the player is in an outdoor landcell. Null → outdoor root path.
|
/// or null when pre-spawn or the player is in an outdoor landcell. Null → outdoor root path.
|
||||||
/// </param>
|
/// </param>
|
||||||
/// <param name="fallbackPos">
|
/// <param name="fallbackPos">
|
||||||
/// Used as the viewer position for the portal-side test in the BFS when root is non-null.
|
/// Used as the viewer position for the portal-side test in the BFS when root is non-null.
|
||||||
|
|
@ -389,13 +389,13 @@ public sealed class CellVisibility
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// FindCameraCell — DELETED in Stage 3 (2026-06-02)
|
// FindCameraCell — DELETED in Stage 3 (2026-06-02)
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// The AABB + grace-frame camera-cell resolver was removed. Production code
|
// The AABB + grace-frame camera-cell resolver was removed. Production code
|
||||||
// now exclusively uses ComputeVisibilityFromRoot(root, …) where root is the
|
// now exclusively uses ComputeVisibilityFromRoot(root, …) where root is the
|
||||||
// transition-owned CellGraph.CurrCell (set by ResolveCellId/Stage 2 physics).
|
// transition-owned CellGraph.CurrCell (set by ResolveCellId/Stage 2 physics).
|
||||||
// Retail anchor: CellManager::ChangePosition (0x004559B0) reads curr_cell
|
// Retail anchor: CellManager::ChangePosition (0x004559B0) reads curr_cell
|
||||||
// from the sweep — it never re-derives from a static position.
|
// from the sweep — it never re-derives from a static position.
|
||||||
//
|
//
|
||||||
// GetVisibleCells (used by ComputeVisibility below for test compatibility)
|
// GetVisibleCells (used by ComputeVisibility below for test compatibility)
|
||||||
// still uses the brute-force AABB scan internally.
|
// still uses the brute-force AABB scan internally.
|
||||||
|
|
@ -471,12 +471,12 @@ public sealed class CellVisibility
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// UCG W2: BFS visibility traversal from a pre-resolved root cell.
|
/// UCG W2: BFS visibility traversal from a pre-resolved root cell.
|
||||||
/// The root is the correct membership answer (supplied by the caller —
|
/// The root is the correct membership answer (supplied by the caller —
|
||||||
/// physics CurrCell via <see cref="ComputeVisibilityFromRoot"/>, or AABB
|
/// physics CurrCell via <see cref="ComputeVisibilityFromRoot"/>, or AABB
|
||||||
/// scan via <see cref="GetVisibleCells"/> for test compat).
|
/// scan via <see cref="GetVisibleCells"/> for test compat).
|
||||||
///
|
///
|
||||||
/// The BFS body is byte-identical to the original GetVisibleCells
|
/// The BFS body is byte-identical to the original GetVisibleCells
|
||||||
/// implementation — only root acquisition was extracted out.
|
/// implementation — only root acquisition was extracted out.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private VisibilityResult? GetVisibleCellsFromRoot(LoadedCell cameraCell, Vector3 cameraPos)
|
private VisibilityResult? GetVisibleCellsFromRoot(LoadedCell cameraCell, Vector3 cameraPos)
|
||||||
{
|
{
|
||||||
|
|
@ -499,7 +499,7 @@ public sealed class CellVisibility
|
||||||
{
|
{
|
||||||
var portal = cell.Portals[i];
|
var portal = cell.Portals[i];
|
||||||
|
|
||||||
// Exit portal → outdoor terrain should be visible.
|
// Exit portal → outdoor terrain should be visible.
|
||||||
if (portal.OtherCellId == 0xFFFF)
|
if (portal.OtherCellId == 0xFFFF)
|
||||||
{
|
{
|
||||||
result.HasExitPortalVisible = true;
|
result.HasExitPortalVisible = true;
|
||||||
|
|
@ -522,8 +522,8 @@ public sealed class CellVisibility
|
||||||
var localCam = Vector3.Transform(cameraPos, cell.InverseWorldTransform);
|
var localCam = Vector3.Transform(cameraPos, cell.InverseWorldTransform);
|
||||||
float dot = Vector3.Dot(plane.Normal, localCam) + plane.D;
|
float dot = Vector3.Dot(plane.Normal, localCam) + plane.D;
|
||||||
|
|
||||||
// InsideSide == 0 → inside is positive half-space; reject if dot < -ε.
|
// InsideSide == 0 → inside is positive half-space; reject if dot < -ε.
|
||||||
// InsideSide == 1 → inside is negative half-space; reject if dot > ε.
|
// InsideSide == 1 → inside is negative half-space; reject if dot > ε.
|
||||||
// Source: ACME EnvCellManager.cs lines 1458-1459.
|
// Source: ACME EnvCellManager.cs lines 1458-1459.
|
||||||
if (plane.InsideSide == 0 && dot < -PointInCellEpsilon)
|
if (plane.InsideSide == 0 && dot < -PointInCellEpsilon)
|
||||||
continue;
|
continue;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
|
||||||
namespace AcDream.App.Rendering;
|
namespace AcDream.App.Rendering;
|
||||||
|
|
@ -8,7 +8,7 @@ namespace AcDream.App.Rendering;
|
||||||
/// character. Implements <see cref="ICamera"/> so it plugs into the
|
/// character. Implements <see cref="ICamera"/> so it plugs into the
|
||||||
/// existing renderer pipeline.
|
/// existing renderer pipeline.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class ChaseCamera : ICamera
|
internal sealed class ChaseCamera : ICamera
|
||||||
{
|
{
|
||||||
public Vector3 Position { get; private set; }
|
public Vector3 Position { get; private set; }
|
||||||
public float Aspect { get; set; } = 16f / 9f;
|
public float Aspect { get; set; } = 16f / 9f;
|
||||||
|
|
@ -37,9 +37,9 @@ public sealed class ChaseCamera : ICamera
|
||||||
// (at distance * sin(Pitch)) so the player can be viewed from a low
|
// (at distance * sin(Pitch)) so the player can be viewed from a low
|
||||||
// angle. Clamped to -0.7 to avoid pushing the camera deep underground;
|
// angle. Clamped to -0.7 to avoid pushing the camera deep underground;
|
||||||
// at -0.7 and Distance=8 the camera is ~5m below player-Z which will
|
// at -0.7 and Distance=8 the camera is ~5m below player-Z which will
|
||||||
// clip terrain on hills but is OK on flat ground. 1.4 ≈ looking
|
// clip terrain on hills but is OK on flat ground. 1.4 ≈ looking
|
||||||
// straight down. Wider than the old [0.05, 1.4] so mouse-Y moves the
|
// straight down. Wider than the old [0.05, 1.4] so mouse-Y moves the
|
||||||
// camera in both directions from the neutral [~20°] default.
|
// camera in both directions from the neutral [~20°] default.
|
||||||
private const float PitchMin = -0.7f;
|
private const float PitchMin = -0.7f;
|
||||||
private const float PitchMax = 1.4f;
|
private const float PitchMax = 1.4f;
|
||||||
|
|
||||||
|
|
@ -47,7 +47,7 @@ public sealed class ChaseCamera : ICamera
|
||||||
private Vector3 _lookAt;
|
private Vector3 _lookAt;
|
||||||
|
|
||||||
// K-fix12 (2026-04-26): retail-feel jump camera. The camera Z is
|
// K-fix12 (2026-04-26): retail-feel jump camera. The camera Z is
|
||||||
// pinned to the LAST GROUNDED Z while the player is airborne — the
|
// pinned to the LAST GROUNDED Z while the player is airborne — the
|
||||||
// character rises above the camera on screen, visually matching
|
// character rises above the camera on screen, visually matching
|
||||||
// retail's "you can see yourself jump" feedback. Walking on the
|
// retail's "you can see yourself jump" feedback. Walking on the
|
||||||
// ground tracks Z directly (no lag on hill transitions); falling
|
// ground tracks Z directly (no lag on hill transitions); falling
|
||||||
|
|
@ -90,10 +90,10 @@ public sealed class ChaseCamera : ICamera
|
||||||
{
|
{
|
||||||
_trackedZ = playerPosition.Z; // catch up to falls / drops
|
_trackedZ = playerPosition.Z; // catch up to falls / drops
|
||||||
}
|
}
|
||||||
// else: airborne and rising — keep _trackedZ pinned.
|
// else: airborne and rising — keep _trackedZ pinned.
|
||||||
|
|
||||||
// Look-at uses the actual player Z so the camera always points
|
// Look-at uses the actual player Z so the camera always points
|
||||||
// at the character — when the player rises above the pinned
|
// at the character — when the player rises above the pinned
|
||||||
// camera the look-at tilts up to keep them centered in frame.
|
// camera the look-at tilts up to keep them centered in frame.
|
||||||
_lookAt = playerPosition + new Vector3(0f, 0f, EyeHeight);
|
_lookAt = playerPosition + new Vector3(0f, 0f, EyeHeight);
|
||||||
|
|
||||||
|
|
@ -109,7 +109,7 @@ public sealed class ChaseCamera : ICamera
|
||||||
Position = new Vector3(
|
Position = new Vector3(
|
||||||
playerPosition.X - forwardX * horizontalDist,
|
playerPosition.X - forwardX * horizontalDist,
|
||||||
playerPosition.Y - forwardY * horizontalDist,
|
playerPosition.Y - forwardY * horizontalDist,
|
||||||
_trackedZ + EyeHeight + verticalDist); // ← uses tracked Z (pinned to ground while airborne)
|
_trackedZ + EyeHeight + verticalDist); // ↠uses tracked Z (pinned to ground while airborne)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,20 @@
|
||||||
// ClipFrame.cs
|
// ClipFrame.cs
|
||||||
//
|
//
|
||||||
// Phase U.3: the GPU-side container + uploader for the SHARED per-frame clip
|
// Phase U.3: the GPU-side container + uploader for the SHARED per-frame clip
|
||||||
// data consumed by mesh_modern.vert (SSBO binding=2) and terrain_modern.vert
|
// data consumed by mesh_modern.vert (SSBO binding=2) and terrain_modern.vert
|
||||||
// (UBO binding=2). This is the "shared" half of the U.3 clip mechanism; the
|
// (UBO binding=2). This is the "shared" half of the U.3 clip mechanism; the
|
||||||
// per-instance slot index buffer (SSBO binding=3) is PER-RENDERER and owned by
|
// per-instance slot index buffer (SSBO binding=3) is PER-RENDERER and owned by
|
||||||
// each renderer (WbDrawDispatcher / EnvCellRenderer), parallel to its instance
|
// each renderer (WbDrawDispatcher / EnvCellRenderer), parallel to its instance
|
||||||
// buffer — it is NOT here.
|
// buffer — it is NOT here.
|
||||||
//
|
//
|
||||||
// === The contract (both shader sides obey) ===================================
|
// === The contract (both shader sides obey) ===================================
|
||||||
// binding=2 mesh SSBO holds an array of CellClip, one per "slot":
|
// binding=2 mesh SSBO holds an array of CellClip, one per "slot":
|
||||||
// struct CellClip { uint count; uint _p0; uint _p1; uint _p2; vec4 planes[8]; };
|
// struct CellClip { uint count; uint _p0; uint _p1; uint _p2; vec4 planes[8]; };
|
||||||
// std430 layout: count at byte 0, three pad uints at 4/8/12, planes[8] at 16
|
// std430 layout: count at byte 0, three pad uints at 4/8/12, planes[8] at 16
|
||||||
// (vec4 stride 16) → 144 bytes per slot. Slot 0 is RESERVED = no-clip (count 0).
|
// (vec4 stride 16) → 144 bytes per slot. Slot 0 is RESERVED = no-clip (count 0).
|
||||||
// binding=2 terrain UBO holds the single OutsideView region:
|
// binding=2 terrain UBO holds the single OutsideView region:
|
||||||
// layout(std140) { int uTerrainClipCount; vec4 uTerrainClipPlanes[8]; };
|
// layout(std140) { int uTerrainClipCount; vec4 uTerrainClipPlanes[8]; };
|
||||||
// std140 layout: count at byte 0 (padded to 16), planes[8] at 16 → 144 bytes.
|
// std140 layout: count at byte 0 (padded to 16), planes[8] at 16 → 144 bytes.
|
||||||
//
|
//
|
||||||
// In U.3 a ClipFrame is built via NoClip(): one slot (slot 0, count 0) and a
|
// In U.3 a ClipFrame is built via NoClip(): one slot (slot 0, count 0) and a
|
||||||
// terrain count of 0. Everything renders exactly as before. U.4 populates real
|
// terrain count of 0. Everything renders exactly as before. U.4 populates real
|
||||||
|
|
@ -40,16 +40,16 @@ namespace AcDream.App.Rendering;
|
||||||
/// std430 / std140 byte layout. Per-instance slot buffers (binding=3) are owned by
|
/// std430 / std140 byte layout. Per-instance slot buffers (binding=3) are owned by
|
||||||
/// each renderer, not here.
|
/// each renderer, not here.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class ClipFrame : IDisposable
|
internal sealed class ClipFrame : IDisposable
|
||||||
{
|
{
|
||||||
// ---- Layout constants (mirror mesh_modern.vert + terrain_modern.vert) ----
|
// ---- Layout constants (mirror mesh_modern.vert + terrain_modern.vert) ----
|
||||||
|
|
||||||
/// <summary>Max planes per clip region — matches the shader's <c>planes[8]</c>
|
/// <summary>Max planes per clip region — matches the shader's <c>planes[8]</c>
|
||||||
/// and GL's guaranteed <c>GL_MAX_CLIP_DISTANCES >= 8</c>.</summary>
|
/// and GL's guaranteed <c>GL_MAX_CLIP_DISTANCES >= 8</c>.</summary>
|
||||||
public const int MaxPlanes = 8;
|
public const int MaxPlanes = 8;
|
||||||
|
|
||||||
/// <summary>std430 stride of one <c>CellClip</c>: 16 (count + 3 pad uints) +
|
/// <summary>std430 stride of one <c>CellClip</c>: 16 (count + 3 pad uints) +
|
||||||
/// 8 × 16 (vec4 planes) = 144 bytes.</summary>
|
/// 8 × 16 (vec4 planes) = 144 bytes.</summary>
|
||||||
public const int CellClipStrideBytes = 16 + MaxPlanes * 16; // 144
|
public const int CellClipStrideBytes = 16 + MaxPlanes * 16; // 144
|
||||||
|
|
||||||
/// <summary>Byte offset of <c>planes[0]</c> within a <c>CellClip</c> (after the
|
/// <summary>Byte offset of <c>planes[0]</c> within a <c>CellClip</c> (after the
|
||||||
|
|
@ -57,7 +57,7 @@ public sealed class ClipFrame : IDisposable
|
||||||
public const int CellClipPlanesOffset = 16;
|
public const int CellClipPlanesOffset = 16;
|
||||||
|
|
||||||
/// <summary>std140 size of the terrain UBO block: int count padded to 16, then
|
/// <summary>std140 size of the terrain UBO block: int count padded to 16, then
|
||||||
/// 8 × 16 (vec4 planes) = 144 bytes. Same number as the SSBO stride by
|
/// 8 × 16 (vec4 planes) = 144 bytes. Same number as the SSBO stride by
|
||||||
/// coincidence of the 16-byte vec4 rule, but a DIFFERENT layout family.</summary>
|
/// coincidence of the 16-byte vec4 rule, but a DIFFERENT layout family.</summary>
|
||||||
public const int TerrainUboBytes = 16 + MaxPlanes * 16; // 144
|
public const int TerrainUboBytes = 16 + MaxPlanes * 16; // 144
|
||||||
|
|
||||||
|
|
@ -66,7 +66,7 @@ public sealed class ClipFrame : IDisposable
|
||||||
public const uint MeshClipSsboBinding = 2;
|
public const uint MeshClipSsboBinding = 2;
|
||||||
|
|
||||||
/// <summary>UBO binding index for the terrain OutsideView clip region
|
/// <summary>UBO binding index for the terrain OutsideView clip region
|
||||||
/// (terrain_modern.vert binding=2). UBO namespace — distinct from the SSBO
|
/// (terrain_modern.vert binding=2). UBO namespace — distinct from the SSBO
|
||||||
/// binding=2 above.</summary>
|
/// binding=2 above.</summary>
|
||||||
public const uint TerrainClipUboBinding = 2;
|
public const uint TerrainClipUboBinding = 2;
|
||||||
|
|
||||||
|
|
@ -139,23 +139,23 @@ public sealed class ClipFrame : IDisposable
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The U.3 default frame: exactly slot 0 (no-clip, count 0) and a terrain
|
/// The U.3 default frame: exactly slot 0 (no-clip, count 0) and a terrain
|
||||||
/// count of 0. The whole scene renders ungated — identical to pre-U.3. U.4
|
/// count of 0. The whole scene renders ungated — identical to pre-U.3. U.4
|
||||||
/// replaces this with a frame built from real portal visibility.
|
/// replaces this with a frame built from real portal visibility.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static ClipFrame NoClip()
|
public static ClipFrame NoClip()
|
||||||
{
|
{
|
||||||
// One slot, all zeros: count=0 ⇒ shader passes every plane.
|
// One slot, all zeros: count=0 ⇒ shader passes every plane.
|
||||||
var bytes = new byte[CellClipStrideBytes];
|
var bytes = new byte[CellClipStrideBytes];
|
||||||
return new ClipFrame(bytes, slotCount: 1);
|
return new ClipFrame(bytes, slotCount: 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Number of clip slots currently packed (always >= 1 — slot 0 is
|
/// <summary>Number of clip slots currently packed (always >= 1 — slot 0 is
|
||||||
/// the reserved no-clip slot).</summary>
|
/// the reserved no-clip slot).</summary>
|
||||||
public int SlotCount => _slotCount;
|
public int SlotCount => _slotCount;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Phase U.4: reset this frame back to the NoClip state — exactly slot 0
|
/// Phase U.4: reset this frame back to the NoClip state — exactly slot 0
|
||||||
/// (no-clip, count 0) and a terrain count of 0 — WITHOUT allocating a new
|
/// (no-clip, count 0) and a terrain count of 0 — WITHOUT allocating a new
|
||||||
/// frame or new GL buffers. The single long-lived <c>_clipFrame</c> in
|
/// frame or new GL buffers. The single long-lived <c>_clipFrame</c> in
|
||||||
/// GameWindow is reset + re-packed every frame by <see cref="ClipFrameAssembler"/>,
|
/// GameWindow is reset + re-packed every frame by <see cref="ClipFrameAssembler"/>,
|
||||||
/// then uploaded through one SSBO and one terrain arena per fenced frame slot.
|
/// then uploaded through one SSBO and one terrain arena per fenced frame slot.
|
||||||
|
|
@ -209,7 +209,7 @@ public sealed class ClipFrame : IDisposable
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Append one clip region (becomes the next slot index) from a
|
/// Append one clip region (becomes the next slot index) from a
|
||||||
/// <see cref="ClipPlaneSet"/>. Only the convex-plane case is supported in
|
/// <see cref="ClipPlaneSet"/>. Only the convex-plane case is supported in
|
||||||
/// U.3 — <c>Count > 0</c> packs that many planes; <c>Count == 0</c> packs a
|
/// U.3 — <c>Count > 0</c> packs that many planes; <c>Count == 0</c> packs a
|
||||||
/// no-clip region (pass-all). The scissor / nothing-visible fallbacks that
|
/// no-clip region (pass-all). The scissor / nothing-visible fallbacks that
|
||||||
/// <see cref="ClipPlaneSet"/> can carry are deferred to U.4 (which will draw
|
/// <see cref="ClipPlaneSet"/> can carry are deferred to U.4 (which will draw
|
||||||
/// the AABB box or skip the cell on the CPU side, not via this slot). Returns
|
/// the AABB box or skip the cell on the CPU side, not via this slot). Returns
|
||||||
|
|
@ -259,7 +259,7 @@ public sealed class ClipFrame : IDisposable
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Set the terrain OutsideView clip region (the single region the terrain
|
/// Set the terrain OutsideView clip region (the single region the terrain
|
||||||
/// shader gates against). <paramref name="planes"/> length 0 ungates terrain
|
/// shader gates against). <paramref name="planes"/> length 0 ungates terrain
|
||||||
/// (count 0). U.3 callers never touch this — <see cref="NoClip"/> leaves it
|
/// (count 0). U.3 callers never touch this — <see cref="NoClip"/> leaves it
|
||||||
/// at count 0. U.4 calls it with the OutsideView planes.
|
/// at count 0. U.4 calls it with the OutsideView planes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void SetTerrainClip(ReadOnlySpan<Vector4> planes)
|
public void SetTerrainClip(ReadOnlySpan<Vector4> planes)
|
||||||
|
|
@ -579,7 +579,7 @@ public sealed class ClipFrame : IDisposable
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>A single std140 terrain-clip record within a frame-slot UBO arena.</summary>
|
/// <summary>A single std140 terrain-clip record within a frame-slot UBO arena.</summary>
|
||||||
public readonly record struct TerrainClipBufferBinding(
|
internal readonly record struct TerrainClipBufferBinding(
|
||||||
uint Buffer,
|
uint Buffer,
|
||||||
int OffsetBytes,
|
int OffsetBytes,
|
||||||
int SizeBytes)
|
int SizeBytes)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
// ClipFrameAssembler.cs
|
// ClipFrameAssembler.cs
|
||||||
//
|
//
|
||||||
// Retail PView assembly policy. PortalVisibilityBuilder produces a retail-like
|
// Retail PView assembly policy. PortalVisibilityBuilder produces a retail-like
|
||||||
// view graph: one portal_view list per visible cell plus an outside_view list.
|
// view graph: one portal_view list per visible cell plus an outside_view list.
|
||||||
|
|
@ -21,7 +21,7 @@ namespace AcDream.App.Rendering;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// How the landscape-through-outside_view pass should be interpreted.
|
/// How the landscape-through-outside_view pass should be interpreted.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public enum TerrainClipMode
|
internal enum TerrainClipMode
|
||||||
{
|
{
|
||||||
/// <summary>All outside_view slices have convex plane clips.</summary>
|
/// <summary>All outside_view slices have convex plane clips.</summary>
|
||||||
Planes,
|
Planes,
|
||||||
|
|
@ -37,13 +37,13 @@ public enum TerrainClipMode
|
||||||
/// One retail portal_view slice mapped to a GPU clip slot. The AABB is retained
|
/// One retail portal_view slice mapped to a GPU clip slot. The AABB is retained
|
||||||
/// for passes that cannot write gl_ClipDistance and must use scissor.
|
/// for passes that cannot write gl_ClipDistance and must use scissor.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly record struct ClipViewSlice(int Slot, Vector4 NdcAabb, Vector4[] Planes);
|
internal readonly record struct ClipViewSlice(int Slot, Vector4 NdcAabb, Vector4[] Planes);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Result of <see cref="ClipFrameAssembler.Assemble"/>: populated clip buffers
|
/// Result of <see cref="ClipFrameAssembler.Assemble"/>: populated clip buffers
|
||||||
/// plus routing data consumed by the render orchestration.
|
/// plus routing data consumed by the render orchestration.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class ClipFrameAssembly
|
internal sealed class ClipFrameAssembly
|
||||||
{
|
{
|
||||||
public ClipFrame Frame { get; private set; } = null!;
|
public ClipFrame Frame { get; private set; } = null!;
|
||||||
|
|
||||||
|
|
@ -241,7 +241,7 @@ public sealed class ClipFrameAssembly
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class ClipFrameAssembler
|
internal static class ClipFrameAssembler
|
||||||
{
|
{
|
||||||
public static ClipFrameAssembly Assemble(
|
public static ClipFrameAssembly Assemble(
|
||||||
ClipFrame frame,
|
ClipFrame frame,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
// ClipPlaneSet.cs
|
// ClipPlaneSet.cs
|
||||||
//
|
//
|
||||||
// Phase U.2c: turn a CellView (a cell's accumulated screen-space clip region,
|
// Phase U.2c: turn a CellView (a cell's accumulated screen-space clip region,
|
||||||
// in NDC) into a small set of clip-space half-space planes for the GPU's
|
// in NDC) into a small set of clip-space half-space planes for the GPU's
|
||||||
|
|
@ -6,18 +6,18 @@
|
||||||
// convex plane set.
|
// convex plane set.
|
||||||
//
|
//
|
||||||
// This is the bridge between PortalVisibilityBuilder's 2D NDC view polygons and
|
// This is the bridge between PortalVisibilityBuilder's 2D NDC view polygons and
|
||||||
// the per-vertex clip the mesh/terrain shaders will perform (Phase U.2c → U.2e).
|
// the per-vertex clip the mesh/terrain shaders will perform (Phase U.2c → U.2e).
|
||||||
// Pure System.Numerics math; NO GL. The shader consumes each plane as
|
// Pure System.Numerics math; NO GL. The shader consumes each plane as
|
||||||
// d = nx*clip.x + ny*clip.y + 0*clip.z + dw*clip.w (>= 0 ⇒ keep)
|
// d = nx*clip.x + ny*clip.y + 0*clip.z + dw*clip.w (>= 0 ⇒ keep)
|
||||||
// where (nx, ny, dw) = the plane's (normal.xy, offset). z is always 0 because a
|
// where (nx, ny, dw) = the plane's (normal.xy, offset). z is always 0 because a
|
||||||
// screen-space (NDC) edge is a vertical slab in clip space — independent of depth.
|
// screen-space (NDC) edge is a vertical slab in clip space — independent of depth.
|
||||||
//
|
//
|
||||||
// === The convexity rule (read before touching this file) =====================
|
// === The convexity rule (read before touching this file) =====================
|
||||||
// gl_ClipDistance planes are a CONJUNCTION of half-spaces, i.e. exactly ONE
|
// gl_ClipDistance planes are a CONJUNCTION of half-spaces, i.e. exactly ONE
|
||||||
// convex region (their intersection). A CellView with MORE THAN ONE polygon is a
|
// convex region (their intersection). A CellView with MORE THAN ONE polygon is a
|
||||||
// UNION of convex regions, which is in general NOT convex and CANNOT be
|
// UNION of convex regions, which is in general NOT convex and CANNOT be
|
||||||
// represented by one plane set. Emitting just the first/largest polygon's planes
|
// represented by one plane set. Emitting just the first/largest polygon's planes
|
||||||
// would clip away the others → a real visibility bug (under-inclusion).
|
// would clip away the others → a real visibility bug (under-inclusion).
|
||||||
//
|
//
|
||||||
// Therefore From() NEVER emits a single polygon's planes when the CellView holds
|
// Therefore From() NEVER emits a single polygon's planes when the CellView holds
|
||||||
// several. Multi-polygon (and >8-edge) regions degrade to the UNION AABB scissor:
|
// several. Multi-polygon (and >8-edge) regions degrade to the UNION AABB scissor:
|
||||||
|
|
@ -27,13 +27,13 @@
|
||||||
//
|
//
|
||||||
// === The three Count==0 states (how a consumer tells them apart) =============
|
// === The three Count==0 states (how a consumer tells them apart) =============
|
||||||
// Count == 0 can mean three different things; the consumer MUST distinguish:
|
// Count == 0 can mean three different things; the consumer MUST distinguish:
|
||||||
// (a) Empty — IsNothingVisible == true, UseScissorFallback == false.
|
// (a) Empty — IsNothingVisible == true, UseScissorFallback == false.
|
||||||
// The cell/region isn't visible at all → DRAW NOTHING. The
|
// The cell/region isn't visible at all → DRAW NOTHING. The
|
||||||
// ScissorNdcAabb is a degenerate inverted box (min > max) so that a
|
// ScissorNdcAabb is a degenerate inverted box (min > max) so that a
|
||||||
// consumer which naively scissors on it still draws nothing.
|
// consumer which naively scissors on it still draws nothing.
|
||||||
// (b) Scissor — UseScissorFallback == true, IsNothingVisible == false.
|
// (b) Scissor — UseScissorFallback == true, IsNothingVisible == false.
|
||||||
// The convex-plane budget was exceeded (multi-polygon or >8 edges)
|
// The convex-plane budget was exceeded (multi-polygon or >8 edges)
|
||||||
// → DRAW the ScissorNdcAabb box (a valid min<=max NDC rectangle).
|
// → DRAW the ScissorNdcAabb box (a valid min<=max NDC rectangle).
|
||||||
// There is no third Count==0 state produced by From(). (A separate "no-clip,
|
// There is no third Count==0 state produced by From(). (A separate "no-clip,
|
||||||
// pass-all" slot 0 is constructed by the consumer directly, not via From().)
|
// pass-all" slot 0 is constructed by the consumer directly, not via From().)
|
||||||
// When Count > 0, Planes carries the convex gate and the scissor fields are unused.
|
// When Count > 0, Planes carries the convex gate and the scissor fields are unused.
|
||||||
|
|
@ -45,26 +45,26 @@ using System.Numerics;
|
||||||
namespace AcDream.App.Rendering;
|
namespace AcDream.App.Rendering;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// An NDC convex view region reduced to ≤8 clip-space gl_ClipDistance planes, or a
|
/// An NDC convex view region reduced to ≤8 clip-space gl_ClipDistance planes, or a
|
||||||
/// scissor AABB fallback. See the file header for the convexity rule and the three
|
/// scissor AABB fallback. See the file header for the convexity rule and the three
|
||||||
/// Count==0 states.
|
/// Count==0 states.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly struct ClipPlaneSet
|
internal readonly struct ClipPlaneSet
|
||||||
{
|
{
|
||||||
// Max simultaneous hardware clip planes we target (GL guarantees >= 8).
|
// Max simultaneous hardware clip planes we target (GL guarantees >= 8).
|
||||||
private const int MaxPlanes = 8;
|
private const int MaxPlanes = 8;
|
||||||
|
|
||||||
// Collinear-edge merge threshold. Two consecutive edge directions are treated as
|
// Collinear-edge merge threshold. Two consecutive edge directions are treated as
|
||||||
// the same edge when the turn between them is below ~0.5° (retail copy_view does a
|
// the same edge when the turn between them is below ~0.5° (retail copy_view does a
|
||||||
// ~1px screen-space dedup). |sin θ| for unit dirs = |cross|; sin(0.5°) ≈ 0.0087265.
|
// ~1px screen-space dedup). |sin θ| for unit dirs = |cross|; sin(0.5°) ≈ 0.0087265.
|
||||||
private const float CollinearSinEps = 0.0087265f;
|
private const float CollinearSinEps = 0.0087265f;
|
||||||
|
|
||||||
// Drop a vertex whose two incident edges are shorter than this (NDC) — a duplicate
|
// Drop a vertex whose two incident edges are shorter than this (NDC) — a duplicate
|
||||||
// or near-duplicate point that would otherwise yield a garbage normalized normal.
|
// or near-duplicate point that would otherwise yield a garbage normalized normal.
|
||||||
private const float DegenerateEdgeLen = 1e-6f;
|
private const float DegenerateEdgeLen = 1e-6f;
|
||||||
|
|
||||||
// A polygon whose absolute signed area (full area, not 2x) falls below this is a line
|
// A polygon whose absolute signed area (full area, not 2x) falls below this is a line
|
||||||
// or point — zero screen coverage ⇒ nothing visible. A real portal opening has area far
|
// or point — zero screen coverage ⇒ nothing visible. A real portal opening has area far
|
||||||
// above this (e.g. the sliver-clip test region is 0.4); only an edge-on projection gets here.
|
// above this (e.g. the sliver-clip test region is 0.4); only an edge-on projection gets here.
|
||||||
private const float MinPolygonArea = 1e-7f;
|
private const float MinPolygonArea = 1e-7f;
|
||||||
private readonly Vector4[] _planes;
|
private readonly Vector4[] _planes;
|
||||||
|
|
@ -77,7 +77,7 @@ public readonly struct ClipPlaneSet
|
||||||
ScissorNdcAabb = scissorNdcAabb;
|
ScissorNdcAabb = scissorNdcAabb;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Number of active clip planes, 0..8. 0 ⇒ inspect <see cref="UseScissorFallback"/>
|
/// <summary>Number of active clip planes, 0..8. 0 ⇒ inspect <see cref="UseScissorFallback"/>
|
||||||
/// and <see cref="IsNothingVisible"/> to decide between "draw the AABB" and "draw nothing".</summary>
|
/// and <see cref="IsNothingVisible"/> to decide between "draw the AABB" and "draw nothing".</summary>
|
||||||
public int Count => _planes?.Length ?? 0;
|
public int Count => _planes?.Length ?? 0;
|
||||||
|
|
||||||
|
|
@ -89,11 +89,11 @@ public readonly struct ClipPlaneSet
|
||||||
// its frame-scoped slice instead of cloning every plane payload a second time.
|
// its frame-scoped slice instead of cloning every plane payload a second time.
|
||||||
internal Vector4[] PlaneArray => _planes ?? Array.Empty<Vector4>();
|
internal Vector4[] PlaneArray => _planes ?? Array.Empty<Vector4>();
|
||||||
|
|
||||||
/// <summary>True ⇒ the convex-plane budget was exceeded; gate on <see cref="ScissorNdcAabb"/>
|
/// <summary>True ⇒ the convex-plane budget was exceeded; gate on <see cref="ScissorNdcAabb"/>
|
||||||
/// instead (draw the box). Always false when <see cref="Count"/> > 0 or when the region is empty.</summary>
|
/// instead (draw the box). Always false when <see cref="Count"/> > 0 or when the region is empty.</summary>
|
||||||
public bool UseScissorFallback { get; }
|
public bool UseScissorFallback { get; }
|
||||||
|
|
||||||
/// <summary>True ⇒ the region is not visible at all; the consumer draws NOTHING.
|
/// <summary>True ⇒ the region is not visible at all; the consumer draws NOTHING.
|
||||||
/// Mutually exclusive with <see cref="UseScissorFallback"/>, and only meaningful when Count == 0.</summary>
|
/// Mutually exclusive with <see cref="UseScissorFallback"/>, and only meaningful when Count == 0.</summary>
|
||||||
public bool IsNothingVisible { get; }
|
public bool IsNothingVisible { get; }
|
||||||
|
|
||||||
|
|
@ -106,20 +106,20 @@ public readonly struct ClipPlaneSet
|
||||||
public static ClipPlaneSet Empty { get; } =
|
public static ClipPlaneSet Empty { get; } =
|
||||||
new(Array.Empty<Vector4>(), useScissorFallback: false, isNothingVisible: true, scissorNdcAabb: DegenerateAabb);
|
new(Array.Empty<Vector4>(), useScissorFallback: false, isNothingVisible: true, scissorNdcAabb: DegenerateAabb);
|
||||||
|
|
||||||
// Inverted box (min > max) — any sane AABB intersection against it is empty.
|
// Inverted box (min > max) — any sane AABB intersection against it is empty.
|
||||||
private static Vector4 DegenerateAabb => new(1f, 1f, -1f, -1f);
|
private static Vector4 DegenerateAabb => new(1f, 1f, -1f, -1f);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reduce a CellView's NDC clip region to a ClipPlaneSet. One convex polygon (≤8 edges
|
/// Reduce a CellView's NDC clip region to a ClipPlaneSet. One convex polygon (≤8 edges
|
||||||
/// after collinear-merge) → per-edge planes; multi-polygon or >8 edges → union-AABB scissor;
|
/// after collinear-merge) → per-edge planes; multi-polygon or >8 edges → union-AABB scissor;
|
||||||
/// empty/degenerate → <see cref="Empty"/>. See the file header for the full rule.
|
/// empty/degenerate → <see cref="Empty"/>. See the file header for the full rule.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static ClipPlaneSet From(CellView region)
|
public static ClipPlaneSet From(CellView region)
|
||||||
{
|
{
|
||||||
if (region is null || region.IsEmpty || region.Polygons.Count == 0)
|
if (region is null || region.IsEmpty || region.Polygons.Count == 0)
|
||||||
return Empty;
|
return Empty;
|
||||||
|
|
||||||
// MORE THAN ONE polygon ⇒ union, not convex ⇒ never emit one polygon's planes.
|
// MORE THAN ONE polygon ⇒ union, not convex ⇒ never emit one polygon's planes.
|
||||||
// Over-include via the union AABB (safe). region.Min/Max already track the union.
|
// Over-include via the union AABB (safe). region.Min/Max already track the union.
|
||||||
if (region.Polygons.Count > 1)
|
if (region.Polygons.Count > 1)
|
||||||
return Scissor(region.MinX, region.MinY, region.MaxX, region.MaxY);
|
return Scissor(region.MinX, region.MinY, region.MaxX, region.MaxY);
|
||||||
|
|
@ -152,15 +152,15 @@ public readonly struct ClipPlaneSet
|
||||||
{
|
{
|
||||||
int count = NormalizeAndMerge(input, verts);
|
int count = NormalizeAndMerge(input, verts);
|
||||||
|
|
||||||
// Fewer than 3 distinct edges survive ⇒ a sliver/line with no area. There is no
|
// Fewer than 3 distinct edges survive ⇒ a sliver/line with no area. There is no
|
||||||
// meaningful AABB to over-include (a zero-area region), so treat it as nothing visible.
|
// meaningful AABB to over-include (a zero-area region), so treat it as nothing visible.
|
||||||
if (count < 3)
|
if (count < 3)
|
||||||
return Empty;
|
return Empty;
|
||||||
|
|
||||||
ReadOnlySpan<Vector2> normalized = verts[..count];
|
ReadOnlySpan<Vector2> normalized = verts[..count];
|
||||||
|
|
||||||
// A single convex polygon with too many edges to fit the hardware budget ⇒ scissor
|
// A single convex polygon with too many edges to fit the hardware budget ⇒ scissor
|
||||||
// on ITS own AABB (still a superset of the polygon → over-include, safe).
|
// on ITS own AABB (still a superset of the polygon → over-include, safe).
|
||||||
if (count > MaxPlanes)
|
if (count > MaxPlanes)
|
||||||
return Scissor(normalized);
|
return Scissor(normalized);
|
||||||
|
|
||||||
|
|
@ -173,10 +173,10 @@ public readonly struct ClipPlaneSet
|
||||||
Vector2 q = normalized[(i + 1) % count];
|
Vector2 q = normalized[(i + 1) % count];
|
||||||
Vector2 dir = q - p;
|
Vector2 dir = q - p;
|
||||||
// Inward normal for CCW winding: perp(dir) = (-dir.y, dir.x) points to the polygon's
|
// Inward normal for CCW winding: perp(dir) = (-dir.y, dir.x) points to the polygon's
|
||||||
// interior (the "left" side of the directed edge p→q).
|
// interior (the "left" side of the directed edge p→q).
|
||||||
Vector2 n = Vector2.Normalize(new Vector2(-dir.Y, dir.X));
|
Vector2 n = Vector2.Normalize(new Vector2(-dir.Y, dir.X));
|
||||||
// Plane: n·x + d >= 0 inside, with d = -(n·p). In clip space with NDC x = clip.x/clip.w:
|
// Plane: n·x + d >= 0 inside, with d = -(n·p). In clip space with NDC x = clip.x/clip.w:
|
||||||
// dist = n.x*clip.x + n.y*clip.y + 0*clip.z + (-(n·p))*clip.w (>= 0 ⇒ keep)
|
// dist = n.x*clip.x + n.y*clip.y + 0*clip.z + (-(n·p))*clip.w (>= 0 ⇒ keep)
|
||||||
planes[i] = new Vector4(n.X, n.Y, 0f, -Vector2.Dot(n, p));
|
planes[i] = new Vector4(n.X, n.Y, 0f, -Vector2.Dot(n, p));
|
||||||
}
|
}
|
||||||
return new ClipPlaneSet(planes, useScissorFallback: false, isNothingVisible: false, scissorNdcAabb: DegenerateAabb);
|
return new ClipPlaneSet(planes, useScissorFallback: false, isNothingVisible: false, scissorNdcAabb: DegenerateAabb);
|
||||||
|
|
@ -233,9 +233,9 @@ public readonly struct ClipPlaneSet
|
||||||
if (SignedArea2(points[..count]) < 0f)
|
if (SignedArea2(points[..count]) < 0f)
|
||||||
points[..count].Reverse();
|
points[..count].Reverse();
|
||||||
|
|
||||||
// 3) Merge collinear edges: drop vertex i when edge (i-1→i) and edge (i→i+1) point the same
|
// 3) Merge collinear edges: drop vertex i when edge (i-1→i) and edge (i→i+1) point the same
|
||||||
// way (turn angle < ~0.5°). Iterate until stable — removing one vertex can expose a new
|
// way (turn angle < ~0.5°). Iterate until stable — removing one vertex can expose a new
|
||||||
// collinear triple. |cross(a,b)| of unit dirs = |sin θ|; dot>0 rules out a 180° reversal.
|
// collinear triple. |cross(a,b)| of unit dirs = |sin θ|; dot>0 rules out a 180° reversal.
|
||||||
bool changed = true;
|
bool changed = true;
|
||||||
while (changed && count >= 3)
|
while (changed && count >= 3)
|
||||||
{
|
{
|
||||||
|
|
@ -260,12 +260,12 @@ public readonly struct ClipPlaneSet
|
||||||
|
|
||||||
d0 /= l0;
|
d0 /= l0;
|
||||||
d1 /= l1;
|
d1 /= l1;
|
||||||
float cross = d0.X * d1.Y - d0.Y * d1.X; // sin θ
|
float cross = d0.X * d1.Y - d0.Y * d1.X; // sin θ
|
||||||
float dot = d0.X * d1.X + d0.Y * d1.Y; // cos θ
|
float dot = d0.X * d1.X + d0.Y * d1.Y; // cos θ
|
||||||
if (dot > 0f && MathF.Abs(cross) < CollinearSinEps)
|
if (dot > 0f && MathF.Abs(cross) < CollinearSinEps)
|
||||||
{
|
{
|
||||||
points[(i + 1)..count].CopyTo(points[i..]);
|
points[(i + 1)..count].CopyTo(points[i..]);
|
||||||
count--; // cur lies on the straight line prev→next
|
count--; // cur lies on the straight line prev→next
|
||||||
changed = true;
|
changed = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -276,7 +276,7 @@ public readonly struct ClipPlaneSet
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
// Final degeneracy gate: a polygon with negligible area is a line/point even if it still
|
// Final degeneracy gate: a polygon with negligible area is a line/point even if it still
|
||||||
// has >= 3 distinct vertices (e.g. an edge-on portal, or a near-collinear triple the 0.5°
|
// has >= 3 distinct vertices (e.g. an edge-on portal, or a near-collinear triple the 0.5°
|
||||||
// merge didn't quite collapse). Emitting its planes would yield an empty half-space
|
// merge didn't quite collapse). Emitting its planes would yield an empty half-space
|
||||||
// intersection that silently gates out everything; report it honestly as nothing-visible.
|
// intersection that silently gates out everything; report it honestly as nothing-visible.
|
||||||
if (MathF.Abs(SignedArea2(points[..count])) * 0.5f < MinPolygonArea)
|
if (MathF.Abs(SignedArea2(points[..count])) * 0.5f < MinPolygonArea)
|
||||||
|
|
@ -285,7 +285,7 @@ public readonly struct ClipPlaneSet
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Twice the signed area (the "shoelace" sum). > 0 ⇒ CCW, < 0 ⇒ CW.
|
// Twice the signed area (the "shoelace" sum). > 0 ⇒ CCW, < 0 ⇒ CW.
|
||||||
private static float SignedArea2(ReadOnlySpan<Vector2> poly)
|
private static float SignedArea2(ReadOnlySpan<Vector2> poly)
|
||||||
{
|
{
|
||||||
float a = 0f;
|
float a = 0f;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using AcDream.Core.Textures;
|
using AcDream.Core.Textures;
|
||||||
using AcDream.Core.World;
|
using AcDream.Core.World;
|
||||||
using Silk.NET.OpenGL;
|
using Silk.NET.OpenGL;
|
||||||
|
|
||||||
|
|
@ -8,7 +8,7 @@ namespace AcDream.App.Rendering;
|
||||||
/// Location of one decoded entity-material composite in a resident bindless
|
/// Location of one decoded entity-material composite in a resident bindless
|
||||||
/// texture array. The modern mesh shader consumes this exact pair.
|
/// texture array. The modern mesh shader consumes this exact pair.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly record struct BindlessTextureLocation(ulong Handle, uint Layer);
|
internal readonly record struct BindlessTextureLocation(ulong Handle, uint Layer);
|
||||||
|
|
||||||
internal enum CompositeTextureKind : byte
|
internal enum CompositeTextureKind : byte
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -114,15 +114,18 @@ internal sealed class RetailCreatureAppraisalFrameView :
|
||||||
private readonly UiViewport _viewport;
|
private readonly UiViewport _viewport;
|
||||||
private readonly UiElement _windowFrame;
|
private readonly UiElement _windowFrame;
|
||||||
private readonly AppraisalUiController _controller;
|
private readonly AppraisalUiController _controller;
|
||||||
|
private readonly ExternalViewportTextureBridge _textureBridge;
|
||||||
|
|
||||||
public RetailCreatureAppraisalFrameView(
|
public RetailCreatureAppraisalFrameView(
|
||||||
UiViewport viewport,
|
UiViewport viewport,
|
||||||
UiElement windowFrame,
|
UiElement windowFrame,
|
||||||
AppraisalUiController controller)
|
AppraisalUiController controller,
|
||||||
|
IGpuDevice gpuDevice)
|
||||||
{
|
{
|
||||||
_viewport = viewport ?? throw new ArgumentNullException(nameof(viewport));
|
_viewport = viewport ?? throw new ArgumentNullException(nameof(viewport));
|
||||||
_windowFrame = windowFrame ?? throw new ArgumentNullException(nameof(windowFrame));
|
_windowFrame = windowFrame ?? throw new ArgumentNullException(nameof(windowFrame));
|
||||||
_controller = controller ?? throw new ArgumentNullException(nameof(controller));
|
_controller = controller ?? throw new ArgumentNullException(nameof(controller));
|
||||||
|
_textureBridge = new ExternalViewportTextureBridge(gpuDevice);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryGetVisibleTarget(
|
public bool TryGetVisibleTarget(
|
||||||
|
|
@ -149,7 +152,7 @@ internal sealed class RetailCreatureAppraisalFrameView :
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetTextureHandle(uint textureHandle) =>
|
public void SetTextureHandle(uint textureHandle) =>
|
||||||
_viewport.TextureHandle = textureHandle;
|
_viewport.TextureHandle = _textureBridge.Resolve(textureHandle);
|
||||||
|
|
||||||
private static bool IsEffectivelyVisible(UiElement element)
|
private static bool IsEffectivelyVisible(UiElement element)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,94 +1,56 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using Silk.NET.OpenGL;
|
|
||||||
|
|
||||||
namespace AcDream.App.Rendering;
|
namespace AcDream.App.Rendering;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Minimal GL debug line renderer for visualizing collision shapes,
|
/// Minimal debug line renderer for visualizing collision shapes,
|
||||||
/// bounding boxes, and other debug geometry. Collect lines each frame
|
/// bounding boxes, and other debug geometry. Collect lines each frame
|
||||||
/// via <see cref="AddLine"/> / <see cref="AddCylinder"/>, then call
|
/// via <see cref="AddLine"/> / <see cref="AddCylinder"/>, then call
|
||||||
/// <see cref="Flush"/> to upload + draw them.
|
/// <see cref="Flush"/> to upload + draw them through the current
|
||||||
|
/// <see cref="IGpuFrame"/>.
|
||||||
///
|
///
|
||||||
/// Uses a single shared VBO that's respecialized each frame. Vertex
|
/// Campaign V slice V4a: ported onto <see cref="IGpuDevice"/>. Owns one
|
||||||
/// format is (vec3 pos, vec3 color) = 24 bytes per vertex.
|
/// pipeline (LINE_LIST topology, depth disabled — lines must show through
|
||||||
|
/// geometry, matching the prior explicit <c>DepthTest</c> disable) and draws
|
||||||
|
/// with the frame's ring rather than a persistent respecialized VBO. Vertex
|
||||||
|
/// format is (vec3 pos, vec3 color) = 24 bytes per vertex, unchanged.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed unsafe class DebugLineRenderer : IDisposable
|
internal sealed class DebugLineRenderer : IDisposable
|
||||||
{
|
{
|
||||||
private readonly GL _gl;
|
private readonly IGpuDevice _device;
|
||||||
private readonly Shader _shader;
|
private readonly IGpuPipeline _pipeline;
|
||||||
private readonly uint _vao;
|
|
||||||
private readonly uint _vbo;
|
private static readonly GpuVertexLayout VertexLayout = new(
|
||||||
private readonly ResourceCleanupGroup _resources;
|
StrideBytes: 24,
|
||||||
|
[
|
||||||
|
new GpuVertexAttribute(0, GpuVertexFormat.Float3, 0),
|
||||||
|
new GpuVertexAttribute(1, GpuVertexFormat.Float3, 12),
|
||||||
|
]);
|
||||||
|
|
||||||
private readonly List<float> _buffer = new(4096);
|
private readonly List<float> _buffer = new(4096);
|
||||||
private int _vertexCount;
|
private int _vertexCount;
|
||||||
private int _capacityBytes;
|
|
||||||
|
|
||||||
public DebugLineRenderer(GL gl, string shaderDir)
|
public DebugLineRenderer(IGpuDevice device)
|
||||||
{
|
{
|
||||||
_gl = gl ?? throw new ArgumentNullException(nameof(gl));
|
_device = device ?? throw new ArgumentNullException(nameof(device));
|
||||||
ArgumentException.ThrowIfNullOrWhiteSpace(shaderDir);
|
_pipeline = _device.CreatePipeline(new GpuPipelineDescription
|
||||||
var resources = new ResourceCleanupGroup();
|
|
||||||
Shader? shader = null;
|
|
||||||
uint vao = 0;
|
|
||||||
uint vbo = 0;
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
shader = new Shader(gl,
|
Name = "debug-line",
|
||||||
Path.Combine(shaderDir, "debug_line.vert"),
|
Shaders = new GpuShaderSet("debug_line"),
|
||||||
Path.Combine(shaderDir, "debug_line.frag"));
|
VertexLayout = VertexLayout,
|
||||||
resources.Add("debug-line shader", shader.Dispose);
|
Topology = GpuPrimitiveTopology.LineList,
|
||||||
vao = GlResourceCommand.CreateName(
|
Blend = GpuBlendMode.None,
|
||||||
gl,
|
// Retail debug lines are drawn visible THROUGH geometry — the
|
||||||
"debug-line VAO",
|
// prior GL path captured+disabled DepthTest around the draw and
|
||||||
gl.GenVertexArray,
|
// restored whatever the caller had before. A dedicated pipeline
|
||||||
gl.DeleteVertexArray);
|
// bakes "always visible" directly, which is simpler and exactly
|
||||||
uint ownedVao = vao;
|
// as behaviour-preserving since nothing else shares this pipeline.
|
||||||
resources.Add(
|
Depth = GpuDepthState.Disabled,
|
||||||
"debug-line VAO",
|
Cull = GpuCullMode.None,
|
||||||
() => GlResourceCommand.DeleteVertexArray(
|
ColorWrite = true,
|
||||||
gl,
|
});
|
||||||
ownedVao,
|
|
||||||
$"delete debug-line VAO {ownedVao}"));
|
|
||||||
vbo = GlResourceCommand.CreateName(
|
|
||||||
gl,
|
|
||||||
"debug-line VBO",
|
|
||||||
gl.GenBuffer,
|
|
||||||
gl.DeleteBuffer);
|
|
||||||
uint ownedVbo = vbo;
|
|
||||||
resources.Add(
|
|
||||||
"debug-line VBO",
|
|
||||||
() => GlResourceCommand.DeleteBuffer(
|
|
||||||
gl,
|
|
||||||
ownedVbo,
|
|
||||||
$"delete debug-line VBO {ownedVbo}"));
|
|
||||||
|
|
||||||
GlResourceCommand.Execute(gl, "configure debug-line vertex state", () =>
|
|
||||||
{
|
|
||||||
gl.BindVertexArray(vao);
|
|
||||||
gl.BindBuffer(BufferTargetARB.ArrayBuffer, vbo);
|
|
||||||
// 24-byte stride: vec3 pos + vec3 color
|
|
||||||
gl.EnableVertexAttribArray(0);
|
|
||||||
gl.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, 6 * sizeof(float), (void*)0);
|
|
||||||
gl.EnableVertexAttribArray(1);
|
|
||||||
gl.VertexAttribPointer(1, 3, VertexAttribPointerType.Float, false, 6 * sizeof(float), (void*)(3 * sizeof(float)));
|
|
||||||
gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0);
|
|
||||||
gl.BindVertexArray(0);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
catch (Exception constructionFailure)
|
|
||||||
{
|
|
||||||
resources.RollbackConstructionAndThrow(
|
|
||||||
"DebugLineRenderer construction failed and its GL prefix did not cleanly roll back.",
|
|
||||||
constructionFailure);
|
|
||||||
}
|
|
||||||
|
|
||||||
_resources = resources;
|
|
||||||
_shader = shader!;
|
|
||||||
_vao = vao;
|
|
||||||
_vbo = vbo;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Clear accumulated lines. Call at the start of each frame.</summary>
|
/// <summary>Clear accumulated lines. Call at the start of each frame.</summary>
|
||||||
|
|
@ -169,45 +131,40 @@ public sealed unsafe class DebugLineRenderer : IDisposable
|
||||||
AddLine(c[2], c[6], color); AddLine(c[3], c[7], color);
|
AddLine(c[2], c[6], color); AddLine(c[3], c[7], color);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Upload + draw all accumulated lines.</summary>
|
/// <summary>Upload + draw all accumulated lines against the current frame.</summary>
|
||||||
public void Flush(Matrix4x4 view, Matrix4x4 projection)
|
public void Flush(Matrix4x4 view, Matrix4x4 projection, IGpuFrame frame)
|
||||||
{
|
{
|
||||||
if (_vertexCount == 0) return;
|
if (_vertexCount == 0) return;
|
||||||
|
ArgumentNullException.ThrowIfNull(frame);
|
||||||
|
|
||||||
_shader.Use();
|
int byteCount = _buffer.Count * sizeof(float);
|
||||||
_shader.SetMatrix4("uView", view);
|
GpuRingAllocation allocation = frame.AllocateRing(byteCount, GpuRingUsage.Vertex);
|
||||||
_shader.SetMatrix4("uProjection", projection);
|
CollectionsMarshal.AsSpan(_buffer).CopyTo(allocation.AsSpan<float>());
|
||||||
|
|
||||||
_gl.BindVertexArray(_vao);
|
using IGpuPassEncoder pass = frame.BeginPass(new GpuPassDescription
|
||||||
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, _vbo);
|
|
||||||
|
|
||||||
int neededBytes = _buffer.Count * sizeof(float);
|
|
||||||
if (neededBytes > _capacityBytes)
|
|
||||||
{
|
{
|
||||||
fixed (float* ptr = CollectionsMarshal.AsSpan(_buffer))
|
Name = "debug-lines",
|
||||||
_gl.BufferData(BufferTargetARB.ArrayBuffer, (nuint)neededBytes, ptr, BufferUsageARB.DynamicDraw);
|
Color = new GpuColorAttachment(
|
||||||
_capacityBytes = neededBytes;
|
Target: null,
|
||||||
}
|
Load: GpuLoadOp.Load,
|
||||||
else
|
Store: GpuStoreOp.Store,
|
||||||
{
|
ClearColor: default),
|
||||||
fixed (float* ptr = CollectionsMarshal.AsSpan(_buffer))
|
Depth = null,
|
||||||
_gl.BufferSubData(BufferTargetARB.ArrayBuffer, 0, (nuint)neededBytes, ptr);
|
SampleCount = 1,
|
||||||
}
|
});
|
||||||
|
pass.BindPipeline(_pipeline);
|
||||||
// Depth test on so lines get occluded by geometry (but we want them
|
pass.BindVertexBuffer(allocation.Buffer, allocation.OffsetBytes);
|
||||||
// visible through geometry — disable depth test so everything shows).
|
// Same combined-matrix convention every other ported shader uses
|
||||||
bool wasDepthEnabled = _gl.IsEnabled(EnableCap.DepthTest);
|
// (WbDrawDispatcher, TerrainModernRenderer, ParticleRenderer):
|
||||||
_gl.Disable(EnableCap.DepthTest);
|
// C# multiplies view * projection once and uploads the single
|
||||||
|
// uViewProjection the shader now declares, replacing the separate
|
||||||
_gl.DrawArrays(PrimitiveType.Lines, 0, (uint)_vertexCount);
|
// uView/uProjection uniforms.
|
||||||
|
pass.SetPushConstants(GpuPushConstants.Default with { ViewProjection = view * projection });
|
||||||
if (wasDepthEnabled) _gl.Enable(EnableCap.DepthTest);
|
pass.Draw((uint)_vertexCount, instanceCount: 1, firstVertex: 0, firstInstance: 0);
|
||||||
|
|
||||||
_gl.BindVertexArray(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_resources.RetryCleanup();
|
_pipeline.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,26 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
|
||||||
namespace AcDream.App.Rendering;
|
namespace AcDream.App.Rendering;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fixed camera for the paperdoll mini-scene — retail-exact, ported from the gmPaperDollUI viewport
|
/// Fixed camera for the paperdoll mini-scene — retail-exact, ported from the gmPaperDollUI viewport
|
||||||
/// setup (decomp 0x004a5a39–0x004a5a69). The viewport (element <c>0x100001d5</c>) is configured by
|
/// setup (decomp 0x004a5a39–0x004a5a69). The viewport (element <c>0x100001d5</c>) is configured by
|
||||||
/// <c>UIElement_Viewport::SetCamera(position, direction)</c> with:
|
/// <c>UIElement_Viewport::SetCamera(position, direction)</c> with:
|
||||||
/// <list type="bullet">
|
/// <list type="bullet">
|
||||||
/// <item>position = (0.12, −2.4, 0.88) [hex 0x3df5c28f, 0xc019999a, 0x3f6147ae]</item>
|
/// <item>position = (0.12, −2.4, 0.88) [hex 0x3df5c28f, 0xc019999a, 0x3f6147ae]</item>
|
||||||
/// <item>direction = (0, 0, 0) ⇒ <c>CreatureMode::SetCameraDirection</c> resets the view frame to
|
/// <item>direction = (0, 0, 0) ⇒ <c>CreatureMode::SetCameraDirection</c> resets the view frame to
|
||||||
/// IDENTITY (<c>euler_set_rotate(0,0,0)</c> then <c>rotate(0,0,0)</c>), so the camera looks
|
/// IDENTITY (<c>euler_set_rotate(0,0,0)</c> then <c>rotate(0,0,0)</c>), so the camera looks
|
||||||
/// straight down +Y with +Z up — NO yaw, NO pitch.</item>
|
/// straight down +Y with +Z up — NO yaw, NO pitch.</item>
|
||||||
/// </list>
|
/// </list>
|
||||||
/// The doll is framed purely by camera POSITION + FOV, NOT by aiming at the body: at distance 2.4 with
|
/// The doll is framed purely by camera POSITION + FOV, NOT by aiming at the body: at distance 2.4 with
|
||||||
/// a π/4 (45°) vertical FOV the visible vertical band is z≈[−0.11, 1.87], which covers the whole ~1.6 m
|
/// a π/4 (45°) vertical FOV the visible vertical band is z≈[−0.11, 1.87], which covers the whole ~1.6 m
|
||||||
/// figure even though the model origin sits at the FEET (z=0). FOV π/4 is <c>CreatureMode</c>'s default
|
/// figure even though the model origin sits at the FEET (z=0). FOV π/4 is <c>CreatureMode</c>'s default
|
||||||
/// <c>m_fFOVRadians</c> (ctor 0x004543cf, hex 0x3f490fdb); default ambient is (0.3,0.3,0.3); the
|
/// <c>m_fFOVRadians</c> (ctor 0x004543cf, hex 0x3f490fdb); default ambient is (0.3,0.3,0.3); the
|
||||||
/// paperdoll uses <c>UseSharpMode</c> (not SmartboxFOV) so <c>Render::SetFOVRad(m_fFOVRadians)</c> applies.
|
/// paperdoll uses <c>UseSharpMode</c> (not SmartboxFOV) so <c>Render::SetFOVRad(m_fFOVRadians)</c> applies.
|
||||||
///
|
///
|
||||||
/// <para>An earlier hand-tune aimed the camera at mid-body to "fit the figure", which introduced a
|
/// <para>An earlier hand-tune aimed the camera at mid-body to "fit the figure", which introduced a
|
||||||
/// spurious yaw (Target.x ≠ Eye.x) that rotated the view and turned the doll's face away from the
|
/// spurious yaw (Target.x ≠Eye.x) that rotated the view and turned the doll's face away from the
|
||||||
/// viewer. This restores retail's zero-yaw frame, where full-body framing comes from the eye height +
|
/// viewer. This restores retail's zero-yaw frame, where full-body framing comes from the eye height +
|
||||||
/// FOV, not from aiming.</para>
|
/// FOV, not from aiming.</para>
|
||||||
///
|
///
|
||||||
|
|
@ -28,15 +28,15 @@ namespace AcDream.App.Rendering;
|
||||||
/// convention as <see cref="ChaseCamera"/> so the doll's triangle winding + back-face culling match the
|
/// convention as <see cref="ChaseCamera"/> so the doll's triangle winding + back-face culling match the
|
||||||
/// world render pass. AC up-axis = +Z.
|
/// world render pass. AC up-axis = +Z.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class DollCamera : ICamera
|
internal sealed class DollCamera : ICamera
|
||||||
{
|
{
|
||||||
// Retail paperdoll camera origin (decomp 0x004a5a51–0x004a5a61).
|
// Retail paperdoll camera origin (decomp 0x004a5a51–0x004a5a61).
|
||||||
internal static readonly Vector3 RetailEye = new(0.12f, -2.4f, 0.88f);
|
internal static readonly Vector3 RetailEye = new(0.12f, -2.4f, 0.88f);
|
||||||
// Identity view orientation ⇒ look straight down +Y (no yaw/pitch). Target = Eye + (0,1,0).
|
// Identity view orientation ⇒ look straight down +Y (no yaw/pitch). Target = Eye + (0,1,0).
|
||||||
private static readonly Vector3 Target = new(0.12f, -1.4f, 0.88f);
|
private static readonly Vector3 Target = new(0.12f, -1.4f, 0.88f);
|
||||||
private static readonly Vector3 Up = Vector3.UnitZ; // AC up-axis = +Z, same as ChaseCamera
|
private static readonly Vector3 Up = Vector3.UnitZ; // AC up-axis = +Z, same as ChaseCamera
|
||||||
|
|
||||||
/// <summary>Vertical field of view — retail <c>CreatureMode</c> default <c>m_fFOVRadians</c> = π/4 (45°).</summary>
|
/// <summary>Vertical field of view — retail <c>CreatureMode</c> default <c>m_fFOVRadians</c> = π/4 (45°).</summary>
|
||||||
public float FovRadians { get; set; } = MathF.PI / 4f;
|
public float FovRadians { get; set; } = MathF.PI / 4f;
|
||||||
|
|
||||||
public float Near { get; set; } = 0.1f; // same near plane as ChaseCamera / retail znear
|
public float Near { get; set; } = 0.1f; // same near plane as ChaseCamera / retail znear
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AcDream.Core.World;
|
using AcDream.Core.World;
|
||||||
|
|
@ -6,13 +6,13 @@ using AcDream.Core.World;
|
||||||
namespace AcDream.App.Rendering;
|
namespace AcDream.App.Rendering;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Builds the dedicated paperdoll WorldEntity — retail's <c>makeObject(player)</c>
|
/// Builds the dedicated paperdoll WorldEntity — retail's <c>makeObject(player)</c>
|
||||||
/// clone: the player's Setup id + current ObjDesc (base palette + subpalette overlays
|
/// clone: the player's Setup id + current ObjDesc (base palette + subpalette overlays
|
||||||
/// + part overrides), posed at the scene origin facing the viewer.
|
/// + part overrides), posed at the scene origin facing the viewer.
|
||||||
///
|
///
|
||||||
/// <para>
|
/// <para>
|
||||||
/// The palette / part-override mapping mirrors the inline construction in
|
/// The palette / part-override mapping mirrors the inline construction in
|
||||||
/// <c>GameWindow.cs</c> around lines 3390–3431. Extracted here so it is
|
/// <c>GameWindow.cs</c> around lines 3390–3431. Extracted here so it is
|
||||||
/// unit-testable without dats and so the paperdoll renderer owns a clean
|
/// unit-testable without dats and so the paperdoll renderer owns a clean
|
||||||
/// seam: it calls <see cref="Build"/> with fresh player state each time the
|
/// seam: it calls <see cref="Build"/> with fresh player state each time the
|
||||||
/// ObjDesc changes, and the renderer only has to swap the entity into its
|
/// ObjDesc changes, and the renderer only has to swap the entity into its
|
||||||
|
|
@ -26,7 +26,7 @@ namespace AcDream.App.Rendering;
|
||||||
/// server-assigned guid.
|
/// server-assigned guid.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class DollEntityBuilder
|
internal static class DollEntityBuilder
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reserved synthetic guid for the paperdoll clone. High, deliberately
|
/// Reserved synthetic guid for the paperdoll clone. High, deliberately
|
||||||
|
|
@ -38,18 +38,18 @@ public static class DollEntityBuilder
|
||||||
/// Reserved render-local entity id for the doll. FIXED (the doll is a singleton)
|
/// Reserved render-local entity id for the doll. FIXED (the doll is a singleton)
|
||||||
/// and high enough never to collide with LiveEntityRuntime's local ids. The
|
/// and high enough never to collide with LiveEntityRuntime's local ids. The
|
||||||
/// renderer passes this in <c>animatedEntityIds</c> so the dispatcher treats the
|
/// renderer passes this in <c>animatedEntityIds</c> so the dispatcher treats the
|
||||||
/// doll as animated — which BYPASSES the Tier-1 classification cache
|
/// doll as animated — which BYPASSES the Tier-1 classification cache
|
||||||
/// (WbDrawDispatcher.cs:1142). That matters because a re-dress builds a NEW
|
/// (WbDrawDispatcher.cs:1142). That matters because a re-dress builds a NEW
|
||||||
/// WorldEntity with this SAME id; without the cache bypass the dispatcher would
|
/// WorldEntity with this SAME id; without the cache bypass the dispatcher would
|
||||||
/// serve the previous doll's cached batches and the new gear wouldn't appear.
|
/// serve the previous doll's cached batches and the new gear wouldn't appear.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const uint DollRenderId = 0xDA11_D012u;
|
public const uint DollRenderId = 0xDA11_D012u;
|
||||||
|
|
||||||
// retail RedressCreature: CPhysicsObj::set_heading(191.367905°) (decomp 0x004a3c0a). Frame::set_heading(h)
|
// retail RedressCreature: CPhysicsObj::set_heading(191.367905°) (decomp 0x004a3c0a). Frame::set_heading(h)
|
||||||
// (decomp 0x00535e40) builds the facing vector as (sin h°, cos h°, 0): at h=0 the creature faces +Y (AC
|
// (decomp 0x00535e40) builds the facing vector as (sin h°, cos h°, 0): at h=0 the creature faces +Y (AC
|
||||||
// North), the heading increasing CLOCKWISE toward +X. We rotate the doll's default +Y-forward body about
|
// North), the heading increasing CLOCKWISE toward +X. We rotate the doll's default +Y-forward body about
|
||||||
// +Z; System.Numerics rotates +Y → (−sin θ, cos θ), so to land on retail's (sin h, cos h) the angle is
|
// +Z; System.Numerics rotates +Y → (−sin θ, cos θ), so to land on retail's (sin h, cos h) the angle is
|
||||||
// NEGATED (θ = −h). Using +h mirrors the X-lean (~22° off → the face reads as turned away from the viewer).
|
// NEGATED (θ = −h). Using +h mirrors the X-lean (~22° off → the face reads as turned away from the viewer).
|
||||||
private const float _headingDegrees = 191.367905f;
|
private const float _headingDegrees = 191.367905f;
|
||||||
private static readonly float _headingRad = -_headingDegrees * (MathF.PI / 180f);
|
private static readonly float _headingRad = -_headingDegrees * (MathF.PI / 180f);
|
||||||
private static readonly Quaternion _dollRotation =
|
private static readonly Quaternion _dollRotation =
|
||||||
|
|
@ -62,17 +62,17 @@ public static class DollEntityBuilder
|
||||||
/// <param name="meshRefs">Pre-resolved mesh refs (may be empty; caller fills them in).</param>
|
/// <param name="meshRefs">Pre-resolved mesh refs (may be empty; caller fills them in).</param>
|
||||||
/// <param name="basePaletteId">
|
/// <param name="basePaletteId">
|
||||||
/// ObjDesc base palette id (0x04xxxxxx). Passed only when subpalettes are
|
/// ObjDesc base palette id (0x04xxxxxx). Passed only when subpalettes are
|
||||||
/// also present — mirrors GameWindow which only builds a PaletteOverride
|
/// also present — mirrors GameWindow which only builds a PaletteOverride
|
||||||
/// when <c>SubPalettes.Count > 0</c>.
|
/// when <c>SubPalettes.Count > 0</c>.
|
||||||
/// </param>
|
/// </param>
|
||||||
/// <param name="subPalettes">
|
/// <param name="subPalettes">
|
||||||
/// Subpalette overlays from the server ObjDesc. Each tuple carries the
|
/// Subpalette overlays from the server ObjDesc. Each tuple carries the
|
||||||
/// subpalette dat id, byte offset into the base palette, and byte length.
|
/// subpalette dat id, byte offset into the base palette, and byte length.
|
||||||
/// Null or empty → <c>PaletteOverride</c> on the returned entity is null.
|
/// Null or empty → <c>PaletteOverride</c> on the returned entity is null.
|
||||||
/// </param>
|
/// </param>
|
||||||
/// <param name="partOverrides">
|
/// <param name="partOverrides">
|
||||||
/// AnimPartChange swaps from the server ObjDesc. Each tuple is a
|
/// AnimPartChange swaps from the server ObjDesc. Each tuple is a
|
||||||
/// (PartIndex, replacement GfxObj id) pair. Null or empty → empty array.
|
/// (PartIndex, replacement GfxObj id) pair. Null or empty → empty array.
|
||||||
/// </param>
|
/// </param>
|
||||||
public static WorldEntity Build(
|
public static WorldEntity Build(
|
||||||
uint setupId,
|
uint setupId,
|
||||||
|
|
@ -82,7 +82,7 @@ public static class DollEntityBuilder
|
||||||
IReadOnlyList<(byte PartIndex, uint GfxObjId)>? partOverrides = null)
|
IReadOnlyList<(byte PartIndex, uint GfxObjId)>? partOverrides = null)
|
||||||
{
|
{
|
||||||
// --- palette override (mirrors GameWindow:3395-3405) ---
|
// --- palette override (mirrors GameWindow:3395-3405) ---
|
||||||
// Only build when there are sub-palette overlays — same gate as GameWindow.
|
// Only build when there are sub-palette overlays — same gate as GameWindow.
|
||||||
PaletteOverride? paletteOverride = null;
|
PaletteOverride? paletteOverride = null;
|
||||||
if (subPalettes is { Count: > 0 } spList)
|
if (subPalettes is { Count: > 0 } spList)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AcDream.App.World;
|
using AcDream.App.World;
|
||||||
using AcDream.App.Rendering.Vfx;
|
using AcDream.App.Rendering.Vfx;
|
||||||
using AcDream.Core.Items;
|
using AcDream.Core.Items;
|
||||||
|
|
@ -23,7 +23,7 @@ namespace AcDream.App.Rendering;
|
||||||
/// <see cref="WorldEntity"/> and recomposes it after the parent's animation
|
/// <see cref="WorldEntity"/> and recomposes it after the parent's animation
|
||||||
/// advances each frame.
|
/// advances each frame.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class EquippedChildRenderController : IDisposable
|
internal sealed class EquippedChildRenderController : IDisposable
|
||||||
{
|
{
|
||||||
private readonly IDatReaderWriter _dats;
|
private readonly IDatReaderWriter _dats;
|
||||||
private readonly object _datLock;
|
private readonly object _datLock;
|
||||||
|
|
@ -1674,7 +1674,7 @@ public sealed class EquippedChildRenderController : IDisposable
|
||||||
"has no exact projection key.");
|
"has no exact projection key.");
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum ChildUnparentDisposition
|
internal enum ChildUnparentDisposition
|
||||||
{
|
{
|
||||||
NotAttached,
|
NotAttached,
|
||||||
Completed,
|
Completed,
|
||||||
|
|
|
||||||
73
src/AcDream.App/Rendering/ExternalViewportTextureBridge.cs
Normal file
73
src/AcDream.App/Rendering/ExternalViewportTextureBridge.cs
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
namespace AcDream.App.Rendering;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Campaign V slice V4a bridge: memoizes a <see cref="GpuTextureSlot"/> for the
|
||||||
|
/// most recent raw GL colour-texture name a still-unmigrated private-viewport
|
||||||
|
/// renderer (<see cref="PrivateEntityViewportRenderer"/>,
|
||||||
|
/// <see cref="PaperdollViewportRenderer"/>) produced, so
|
||||||
|
/// <c>UiViewport.TextureHandle</c> — now a <see cref="GpuTextureSlot"/> — has
|
||||||
|
/// something to draw. Those renderers still allocate their FBO colour
|
||||||
|
/// attachment directly on <c>GL</c> (V4g's scope: "PrivateEntityViewportRenderer
|
||||||
|
/// → IGpuRenderTarget"), so this bridge — not their own campaign slice — is
|
||||||
|
/// what lets the RETAINED UI side of the seam move onto the RHI now.
|
||||||
|
///
|
||||||
|
/// Registers lazily and only re-registers when the producer hands back a
|
||||||
|
/// DIFFERENT raw name (the FBO's colour texture is stable across frames at a
|
||||||
|
/// fixed viewport size and only regenerates on resize) — a naive
|
||||||
|
/// register-every-frame would exhaust the 16384-slot table in seconds.
|
||||||
|
/// Deleted at slice V4g once those renderers publish a real
|
||||||
|
/// <see cref="GpuTextureSlot"/> directly.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class ExternalViewportTextureBridge
|
||||||
|
{
|
||||||
|
private readonly IGpuDevice _device;
|
||||||
|
private readonly IGpuSampler _sampler;
|
||||||
|
private uint _lastRawName;
|
||||||
|
private GpuTextureSlot _lastSlot = GpuTextureSlot.Unassigned;
|
||||||
|
|
||||||
|
public ExternalViewportTextureBridge(IGpuDevice device)
|
||||||
|
{
|
||||||
|
_device = device ?? throw new ArgumentNullException(nameof(device));
|
||||||
|
// Matches the FBO colour attachment's own fixed GL_LINEAR / CLAMP_TO_EDGE
|
||||||
|
// parameters (PrivateEntityViewportRenderer.EnsureFramebuffer,
|
||||||
|
// PaperdollViewportRenderer's equivalent): a bindless handle's
|
||||||
|
// filtering comes from the bound SAMPLER object, not the texture's
|
||||||
|
// own — now irrelevant — TEXTURE_MIN_FILTER/TEXTURE_MAG_FILTER.
|
||||||
|
_sampler = device.CreateSampler(GpuSamplerDescription.WorldClamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resolves <paramref name="rawGlColorTextureName"/> (0 = nothing rendered
|
||||||
|
/// this call, matching the producers' existing "0 = no texture" return)
|
||||||
|
/// to a texture-table slot, registering it once per distinct name.
|
||||||
|
/// </summary>
|
||||||
|
public GpuTextureSlot Resolve(uint rawGlColorTextureName)
|
||||||
|
{
|
||||||
|
if (rawGlColorTextureName == 0)
|
||||||
|
{
|
||||||
|
Release();
|
||||||
|
return GpuTextureSlot.Unassigned;
|
||||||
|
}
|
||||||
|
if (rawGlColorTextureName == _lastRawName && _lastSlot.IsAssigned)
|
||||||
|
return _lastSlot;
|
||||||
|
|
||||||
|
Release();
|
||||||
|
if (_device is not Gpu.Gl.GlGpuDevice glDevice)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException(
|
||||||
|
"ExternalViewportTextureBridge only supports the GL backend. " +
|
||||||
|
"Slice V4g removes this bridge before any other backend ships.");
|
||||||
|
}
|
||||||
|
_lastSlot = glDevice.RegisterExternalColorTexture(rawGlColorTextureName, _sampler);
|
||||||
|
_lastRawName = rawGlColorTextureName;
|
||||||
|
return _lastSlot;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Release()
|
||||||
|
{
|
||||||
|
if (_lastSlot.IsAssigned)
|
||||||
|
_device.ReleaseTextureSlot(_lastSlot);
|
||||||
|
_lastSlot = GpuTextureSlot.Unassigned;
|
||||||
|
_lastRawName = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
// src/AcDream.App/Rendering/FlyCamera.cs
|
// src/AcDream.App/Rendering/FlyCamera.cs
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
|
||||||
namespace AcDream.App.Rendering;
|
namespace AcDream.App.Rendering;
|
||||||
|
|
||||||
public sealed class FlyCamera : ICamera
|
internal sealed class FlyCamera : ICamera
|
||||||
{
|
{
|
||||||
public Vector3 Position { get; set; } = new(96, 96, 150);
|
public Vector3 Position { get; set; } = new(96, 96, 150);
|
||||||
public float Yaw { get; set; } = MathF.PI / 2f; // facing +Y
|
public float Yaw { get; set; } = MathF.PI / 2f; // facing +Y
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
|
||||||
namespace AcDream.App.Rendering;
|
namespace AcDream.App.Rendering;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Six normalized view-frustum planes extracted from a View×Projection matrix.
|
/// Six normalized view-frustum planes extracted from a View×Projection matrix.
|
||||||
/// Each plane is represented as (normal.X, normal.Y, normal.Z, distance) where
|
/// Each plane is represented as (normal.X, normal.Y, normal.Z, distance) where
|
||||||
/// dot(normal, point) + distance >= 0 means the point is on the visible side.
|
/// dot(normal, point) + distance >= 0 means the point is on the visible side.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly struct FrustumPlanes
|
internal readonly struct FrustumPlanes
|
||||||
{
|
{
|
||||||
public readonly Vector4 Left;
|
public readonly Vector4 Left;
|
||||||
public readonly Vector4 Right;
|
public readonly Vector4 Right;
|
||||||
|
|
@ -27,7 +27,7 @@ public readonly struct FrustumPlanes
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Extracts the six frustum planes from a combined View×Projection matrix
|
/// Extracts the six frustum planes from a combined View×Projection matrix
|
||||||
/// using the Gribb-Hartmann method. System.Numerics.Matrix4x4 is row-major,
|
/// using the Gribb-Hartmann method. System.Numerics.Matrix4x4 is row-major,
|
||||||
/// so rows are accessed directly via M{row}{col} fields.
|
/// so rows are accessed directly via M{row}{col} fields.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -64,7 +64,7 @@ public readonly struct FrustumPlanes
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Conservative AABB-vs-frustum culling. Zero allocations; suitable for per-frame use.
|
/// Conservative AABB-vs-frustum culling. Zero allocations; suitable for per-frame use.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class FrustumCuller
|
internal static class FrustumCuller
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns true if the axis-aligned bounding box defined by
|
/// Returns true if the axis-aligned bounding box defined by
|
||||||
|
|
@ -74,7 +74,7 @@ public static class FrustumCuller
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static bool IsAabbVisible(FrustumPlanes planes, Vector3 min, Vector3 max)
|
public static bool IsAabbVisible(FrustumPlanes planes, Vector3 min, Vector3 max)
|
||||||
{
|
{
|
||||||
// For each plane, test the AABB's "most-positive vertex" —
|
// For each plane, test the AABB's "most-positive vertex" —
|
||||||
// the corner most in the direction of the plane normal. If
|
// the corner most in the direction of the plane normal. If
|
||||||
// that corner is behind the plane, the entire box is outside.
|
// that corner is behind the plane, the entire box is outside.
|
||||||
return TestPlane(planes.Left, min, max)
|
return TestPlane(planes.Left, min, max)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using AcDream.Core.Plugins;
|
using AcDream.Core.Plugins;
|
||||||
using AcDream.App.Composition;
|
using AcDream.App.Composition;
|
||||||
using AcDream.App.Physics;
|
using AcDream.App.Physics;
|
||||||
using AcDream.App.Rendering.Gpu;
|
using AcDream.App.Rendering.Gpu;
|
||||||
|
|
@ -21,7 +21,7 @@ using Silk.NET.Windowing;
|
||||||
|
|
||||||
namespace AcDream.App.Rendering;
|
namespace AcDream.App.Rendering;
|
||||||
|
|
||||||
public sealed class GameWindow :
|
internal sealed class GameWindow :
|
||||||
IDisposable,
|
IDisposable,
|
||||||
IGameWindowPlatformPublication<GL, IInputContext>,
|
IGameWindowPlatformPublication<GL, IInputContext>,
|
||||||
IGameWindowHostInputCameraPublication,
|
IGameWindowHostInputCameraPublication,
|
||||||
|
|
@ -70,7 +70,7 @@ public sealed class GameWindow :
|
||||||
private AcDream.App.Interaction.WorldSelectionQuery? _worldSelectionQuery;
|
private AcDream.App.Interaction.WorldSelectionQuery? _worldSelectionQuery;
|
||||||
private AcDream.App.Interaction.SelectionInteractionController? _selectionInteractions;
|
private AcDream.App.Interaction.SelectionInteractionController? _selectionInteractions;
|
||||||
/// <summary>Phase N.5: ARB_bindless_texture + ARB_shader_draw_parameters
|
/// <summary>Phase N.5: ARB_bindless_texture + ARB_shader_draw_parameters
|
||||||
/// support. Required at startup — missing bindless throws
|
/// support. Required at startup — missing bindless throws
|
||||||
/// <see cref="NotSupportedException"/> in <c>OnLoad</c>.</summary>
|
/// <see cref="NotSupportedException"/> in <c>OnLoad</c>.</summary>
|
||||||
private AcDream.App.Rendering.Wb.BindlessSupport? _bindlessSupport;
|
private AcDream.App.Rendering.Wb.BindlessSupport? _bindlessSupport;
|
||||||
private SamplerCache? _samplerCache;
|
private SamplerCache? _samplerCache;
|
||||||
|
|
@ -78,14 +78,14 @@ public sealed class GameWindow :
|
||||||
// K-fix4 (2026-04-26): default OFF. The orange BSP / green cylinder
|
// K-fix4 (2026-04-26): default OFF. The orange BSP / green cylinder
|
||||||
// wireframes are noisy outdoors and confuse first-time users into
|
// wireframes are noisy outdoors and confuse first-time users into
|
||||||
// thinking they're a rendering bug. Ctrl+F2 toggles, the DebugPanel
|
// thinking they're a rendering bug. Ctrl+F2 toggles, the DebugPanel
|
||||||
// → Diagnostics → "Toggle collision wires" button toggles too.
|
// → Diagnostics → "Toggle collision wires" button toggles too.
|
||||||
private readonly AcDream.App.Rendering.WorldSceneDebugState
|
private readonly AcDream.App.Rendering.WorldSceneDebugState
|
||||||
_worldSceneDebugState = new();
|
_worldSceneDebugState = new();
|
||||||
|
|
||||||
// Phase I.2: the old StbTrueTypeSharp DebugOverlay was deleted in
|
// Phase I.2: the old StbTrueTypeSharp DebugOverlay was deleted in
|
||||||
// favor of the ImGui-backed DebugPanel (see _debugVm below). The
|
// favor of the ImGui-backed DebugPanel (see _debugVm below). The
|
||||||
// TextRenderer + BitmapFont fields stay alive because they're shared
|
// TextRenderer + BitmapFont fields stay alive because they're shared
|
||||||
// with UiHost and reserved for the future world-space HUD (D.6 —
|
// with UiHost and reserved for the future world-space HUD (D.6 —
|
||||||
// damage floaters, name plates) where ImGui can't reach into the 3D
|
// damage floaters, name plates) where ImGui can't reach into the 3D
|
||||||
// scene. They are no longer used for any debug overlay.
|
// scene. They are no longer used for any debug overlay.
|
||||||
private TextRenderer? _textRenderer;
|
private TextRenderer? _textRenderer;
|
||||||
|
|
@ -98,7 +98,7 @@ public sealed class GameWindow :
|
||||||
"1",
|
"1",
|
||||||
System.StringComparison.Ordinal);
|
System.StringComparison.Ordinal);
|
||||||
|
|
||||||
// MP0 (2026-07-05): permanent frame profiler — one FrameBoundary call
|
// MP0 (2026-07-05): permanent frame profiler — one FrameBoundary call
|
||||||
// per OnRender + three stage scopes. All logic lives in
|
// per OnRender + three stage scopes. All logic lives in
|
||||||
// AcDream.App.Diagnostics.FrameProfiler (structure rule 1).
|
// AcDream.App.Diagnostics.FrameProfiler (structure rule 1).
|
||||||
private readonly AcDream.App.Diagnostics.FrameProfiler _frameProfiler = new();
|
private readonly AcDream.App.Diagnostics.FrameProfiler _frameProfiler = new();
|
||||||
|
|
@ -116,6 +116,7 @@ public sealed class GameWindow :
|
||||||
private IDisposable? _frameGraphPublication;
|
private IDisposable? _frameGraphPublication;
|
||||||
private AcDream.App.Rendering.GpuFrameFlightController? _gpuFrameFlights;
|
private AcDream.App.Rendering.GpuFrameFlightController? _gpuFrameFlights;
|
||||||
private IGpuDevice? _gpuDevice;
|
private IGpuDevice? _gpuDevice;
|
||||||
|
private AcDream.App.Rendering.GpuDeviceFrameLifetime? _gpuFrameLifetime;
|
||||||
private readonly AcDream.App.Rendering.GameFrameGraphSlot _frameGraphs = new();
|
private readonly AcDream.App.Rendering.GameFrameGraphSlot _frameGraphs = new();
|
||||||
private readonly AcDream.App.Rendering.GameRenderResourceLifetime
|
private readonly AcDream.App.Rendering.GameRenderResourceLifetime
|
||||||
_renderResourceLifetime = new();
|
_renderResourceLifetime = new();
|
||||||
|
|
@ -144,11 +145,11 @@ public sealed class GameWindow :
|
||||||
_localPlayerTeleport;
|
_localPlayerTeleport;
|
||||||
private readonly AcDream.App.Rendering.WorldRenderRangeState _renderRange =
|
private readonly AcDream.App.Rendering.WorldRenderRangeState _renderRange =
|
||||||
new(nearRadius: 4, farRadius: 12);
|
new(nearRadius: 4, farRadius: 12);
|
||||||
// Phase B.3: physics engine — populated from the streaming pipeline.
|
// Phase B.3: physics engine — populated from the streaming pipeline.
|
||||||
private AcDream.Core.Physics.PhysicsEngine _physicsEngine =>
|
private AcDream.Core.Physics.PhysicsEngine _physicsEngine =>
|
||||||
_runtimeEntityObjects.Physics.Engine;
|
_runtimeEntityObjects.Physics.Engine;
|
||||||
|
|
||||||
// Task 4: physics data cache — BSP trees + collision shapes extracted from
|
// Task 4: physics data cache — BSP trees + collision shapes extracted from
|
||||||
// GfxObj/Setup dats during streaming. Populated on the worker thread;
|
// GfxObj/Setup dats during streaming. Populated on the worker thread;
|
||||||
// ConcurrentDictionary inside makes cross-thread access safe.
|
// ConcurrentDictionary inside makes cross-thread access safe.
|
||||||
private AcDream.Core.Physics.PhysicsDataCache _physicsDataCache =>
|
private AcDream.Core.Physics.PhysicsDataCache _physicsDataCache =>
|
||||||
|
|
@ -185,7 +186,7 @@ public sealed class GameWindow :
|
||||||
private readonly CellVisibility _cellVisibility = new();
|
private readonly CellVisibility _cellVisibility = new();
|
||||||
|
|
||||||
// Phase A.1 hotfix / Phase A.5 T10: DatCollection is NOT thread-safe.
|
// Phase A.1 hotfix / Phase A.5 T10: DatCollection is NOT thread-safe.
|
||||||
// DatReaderWriter's DatBinReader uses a shared buffer position internally —
|
// DatReaderWriter's DatBinReader uses a shared buffer position internally —
|
||||||
// concurrent _dats.Get<T> calls from the streaming worker thread (T11+) and
|
// concurrent _dats.Get<T> calls from the streaming worker thread (T11+) and
|
||||||
// the render thread (LandblockBuildFactory on the worker; live-spawn
|
// the render thread (LandblockBuildFactory on the worker; live-spawn
|
||||||
// handlers + animation ticks on the render
|
// handlers + animation ticks on the render
|
||||||
|
|
@ -226,7 +227,7 @@ public sealed class GameWindow :
|
||||||
|
|
||||||
// Phase U.3: the shared per-frame clip data (binding=2 mesh SSBO + terrain
|
// Phase U.3: the shared per-frame clip data (binding=2 mesh SSBO + terrain
|
||||||
// UBO). In U.3 a single ClipFrame.NoClip() instance is created lazily (??=) and
|
// UBO). In U.3 a single ClipFrame.NoClip() instance is created lazily (??=) and
|
||||||
// REUSED across frames — its GL buffers persist; only the cheap CPU-side no-clip
|
// REUSED across frames — its GL buffers persist; only the cheap CPU-side no-clip
|
||||||
// state is re-uploaded each frame before terrain/entities draw, so the whole
|
// state is re-uploaded each frame before terrain/entities draw, so the whole
|
||||||
// scene renders ungated (identical to pre-U.3). The buffer ids are handed to the
|
// scene renders ungated (identical to pre-U.3). The buffer ids are handed to the
|
||||||
// three renderers so each re-binds binding=2 immediately before its own draw.
|
// three renderers so each re-binds binding=2 immediately before its own draw.
|
||||||
|
|
@ -248,7 +249,7 @@ public sealed class GameWindow :
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tier 1 cache (#53): per-entity classification results for static
|
/// Tier 1 cache (#53): per-entity classification results for static
|
||||||
/// entities (those NOT in <see cref="_animatedEntities"/>). Conceptually
|
/// entities (those NOT in <see cref="_animatedEntities"/>). Conceptually
|
||||||
/// paired with <see cref="_animatedEntities"/> — that dictionary is the
|
/// paired with <see cref="_animatedEntities"/> — that dictionary is the
|
||||||
/// gating predicate, this cache is the lookup that depends on it.
|
/// gating predicate, this cache is the lookup that depends on it.
|
||||||
/// Passed to <see cref="AcDream.App.Rendering.Wb.WbDrawDispatcher"/> at
|
/// Passed to <see cref="AcDream.App.Rendering.Wb.WbDrawDispatcher"/> at
|
||||||
/// construction time. Tasks 9-10 of the cache plan wire the per-entity
|
/// construction time. Tasks 9-10 of the cache plan wire the per-entity
|
||||||
|
|
@ -283,7 +284,7 @@ public sealed class GameWindow :
|
||||||
private readonly AcDream.App.Rendering.Vfx.ParticleVisibilityController _particleVisibility = new();
|
private readonly AcDream.App.Rendering.Vfx.ParticleVisibilityController _particleVisibility = new();
|
||||||
private readonly AcDream.App.Rendering.Vfx.EntityEffectPoseRegistry _effectPoses = new();
|
private readonly AcDream.App.Rendering.Vfx.EntityEffectPoseRegistry _effectPoses = new();
|
||||||
private AcDream.App.Rendering.Vfx.AnimationHookFrameQueue? _animationHookFrames;
|
private AcDream.App.Rendering.Vfx.AnimationHookFrameQueue? _animationHookFrames;
|
||||||
// Phase 6 — retail PhysicsScript runtime. Receives PlayScript (0xF754)
|
// Phase 6 — retail PhysicsScript runtime. Receives PlayScript (0xF754)
|
||||||
// and typed PlayScriptType (0xF755) events through one effect owner, then
|
// and typed PlayScriptType (0xF755) events through one effect owner, then
|
||||||
// fans every dat-defined hook to particles, audio, lights, translucency,
|
// fans every dat-defined hook to particles, audio, lights, translucency,
|
||||||
// and nested/default-script routing at its StartTime offset.
|
// and nested/default-script routing at its StartTime offset.
|
||||||
|
|
@ -294,7 +295,7 @@ public sealed class GameWindow :
|
||||||
private readonly AcDream.App.Rendering.RetailAlphaQueue _retailAlphaQueue;
|
private readonly AcDream.App.Rendering.RetailAlphaQueue _retailAlphaQueue;
|
||||||
// Remote-entity motion inference: tracks when each remote entity last
|
// Remote-entity motion inference: tracks when each remote entity last
|
||||||
// moved meaningfully. Used in TickAnimations to swap to Ready when
|
// moved meaningfully. Used in TickAnimations to swap to Ready when
|
||||||
// position has stalled for >StopIdleMs — retail observer pattern per
|
// position has stalled for >StopIdleMs — retail observer pattern per
|
||||||
// ACE Player_Tick.cs line 368: the client never sends "released forward"
|
// ACE Player_Tick.cs line 368: the client never sends "released forward"
|
||||||
// MoveToState, so the server never broadcasts an explicit stop. Observer
|
// MoveToState, so the server never broadcasts an explicit stop. Observer
|
||||||
// must infer it from position deltas.
|
// must infer it from position deltas.
|
||||||
|
|
@ -362,21 +363,21 @@ public sealed class GameWindow :
|
||||||
/// <summary>Persisted hotbar shortcuts from the last PlayerDescription (D.5.1 toolbar source).</summary>
|
/// <summary>Persisted hotbar shortcuts from the last PlayerDescription (D.5.1 toolbar source).</summary>
|
||||||
public IReadOnlyList<AcDream.Core.Items.ShortcutEntry> Shortcuts =>
|
public IReadOnlyList<AcDream.Core.Items.ShortcutEntry> Shortcuts =>
|
||||||
_runtimeInventory.Shortcuts.Items;
|
_runtimeInventory.Shortcuts.Items;
|
||||||
// Issue #5 — caches CreatureProfile.{Stamina, Mana, *Max} from
|
// Issue #5 — caches CreatureProfile.{Stamina, Mana, *Max} from
|
||||||
// PlayerDescription so the Vitals HUD can render those bars.
|
// PlayerDescription so the Vitals HUD can render those bars.
|
||||||
// Issue #6 — wired to SpellBook so GetMaxApprox folds enchantment
|
// Issue #6 — wired to SpellBook so GetMaxApprox folds enchantment
|
||||||
// buffs into the max formula via Spellbook.GetVitalMod.
|
// buffs into the max formula via Spellbook.GetVitalMod.
|
||||||
public AcDream.Core.Player.LocalPlayerState LocalPlayer =>
|
public AcDream.Core.Player.LocalPlayerState LocalPlayer =>
|
||||||
_runtimeCharacter.LocalPlayer;
|
_runtimeCharacter.LocalPlayer;
|
||||||
|
|
||||||
// Phase D.2a — ImGui devtools UI overlay. Null unless ACDREAM_DEVTOOLS=1.
|
// Phase D.2a — ImGui devtools UI overlay. Null unless ACDREAM_DEVTOOLS=1.
|
||||||
// See docs/plans/2026-04-24-ui-framework.md for the staged UI strategy.
|
// See docs/plans/2026-04-24-ui-framework.md for the staged UI strategy.
|
||||||
private AcDream.App.Rendering.DevToolsFramePresenter? _devToolsFramePresenter;
|
private AcDream.App.Rendering.DevToolsFramePresenter? _devToolsFramePresenter;
|
||||||
private AcDream.App.Rendering.DevToolsCommandBusSource? _devToolsCommandBus;
|
private AcDream.App.Rendering.DevToolsCommandBusSource? _devToolsCommandBus;
|
||||||
private DevToolsCompositionOwner? _devToolsComposition;
|
private DevToolsCompositionOwner? _devToolsComposition;
|
||||||
private AcDream.UI.Abstractions.Panels.Vitals.VitalsVM? _vitalsVm;
|
private AcDream.UI.Abstractions.Panels.Vitals.VitalsVM? _vitalsVm;
|
||||||
private AcDream.UI.Abstractions.Panels.Chat.ChatVM? _retailChatVm;
|
private AcDream.UI.Abstractions.Panels.Chat.ChatVM? _retailChatVm;
|
||||||
// Phase D.2b — retained host + composition runtime. Null unless ACDREAM_RETAIL_UI=1.
|
// Phase D.2b — retained host + composition runtime. Null unless ACDREAM_RETAIL_UI=1.
|
||||||
private AcDream.App.UI.UiHost? _uiHost;
|
private AcDream.App.UI.UiHost? _uiHost;
|
||||||
private AcDream.App.UI.RetailUiRuntime? _retailUiRuntime;
|
private AcDream.App.UI.RetailUiRuntime? _retailUiRuntime;
|
||||||
private readonly AcDream.App.UI.RetailUiRuntimeLease _retailUiLease = new();
|
private readonly AcDream.App.UI.RetailUiRuntimeLease _retailUiLease = new();
|
||||||
|
|
@ -398,7 +399,7 @@ public sealed class GameWindow :
|
||||||
private AcDream.App.Spells.MagicRuntime? _magicRuntime;
|
private AcDream.App.Spells.MagicRuntime? _magicRuntime;
|
||||||
private MagicCatalog? _magicCatalog;
|
private MagicCatalog? _magicCatalog;
|
||||||
private readonly AcDream.Core.Items.StackSplitQuantityState _stackSplitQuantity = new();
|
private readonly AcDream.Core.Items.StackSplitQuantityState _stackSplitQuantity = new();
|
||||||
// Phase D.2b Sub-phase C Slice 2 — the 3-D doll viewport: an off-screen RTT renderer, the UiViewport
|
// Phase D.2b Sub-phase C Slice 2 — the 3-D doll viewport: an off-screen RTT renderer, the UiViewport
|
||||||
// widget that blits it, the inventory frame (for the open-gate), and a dirty flag (re-dress on 0xF625).
|
// widget that blits it, the inventory frame (for the open-gate), and a dirty flag (re-dress on 0xF625).
|
||||||
private AcDream.App.Rendering.PaperdollViewportRenderer? _paperdollViewportRenderer;
|
private AcDream.App.Rendering.PaperdollViewportRenderer? _paperdollViewportRenderer;
|
||||||
private AcDream.App.Rendering.PaperdollFramePresenter? _paperdollFramePresenter;
|
private AcDream.App.Rendering.PaperdollFramePresenter? _paperdollFramePresenter;
|
||||||
|
|
@ -406,7 +407,7 @@ public sealed class GameWindow :
|
||||||
_creatureAppraisalViewportRenderer;
|
_creatureAppraisalViewportRenderer;
|
||||||
private AcDream.App.Rendering.CreatureAppraisalFramePresenter?
|
private AcDream.App.Rendering.CreatureAppraisalFramePresenter?
|
||||||
_creatureAppraisalFramePresenter;
|
_creatureAppraisalFramePresenter;
|
||||||
// Phase D.2b Task 9 — plugin UI registrations buffered before OnLoad; drained in OnLoad.
|
// Phase D.2b Task 9 — plugin UI registrations buffered before OnLoad; drained in OnLoad.
|
||||||
private readonly AcDream.App.Plugins.BufferedUiRegistry? _uiRegistry;
|
private readonly AcDream.App.Plugins.BufferedUiRegistry? _uiRegistry;
|
||||||
// Phase I.2: ImGui debug panel ViewModel. The devtools presenter owns
|
// Phase I.2: ImGui debug panel ViewModel. The devtools presenter owns
|
||||||
// its panel; the VM remains here because runtime feedback producers bind
|
// its panel; the VM remains here because runtime feedback producers bind
|
||||||
|
|
@ -427,7 +428,7 @@ public sealed class GameWindow :
|
||||||
private AcDream.App.Rendering.Vfx.LiveEntityLightController? _liveEntityLights;
|
private AcDream.App.Rendering.Vfx.LiveEntityLightController? _liveEntityLights;
|
||||||
private AcDream.App.World.LiveEntityPresentationController? _liveEntityPresentation;
|
private AcDream.App.World.LiveEntityPresentationController? _liveEntityPresentation;
|
||||||
|
|
||||||
// #188 — TransparentPartHook fires from the animation pipeline drive
|
// #188 — TransparentPartHook fires from the animation pipeline drive
|
||||||
// a per-(entity,part) translucency ramp; WbDrawDispatcher reads it
|
// a per-(entity,part) translucency ramp; WbDrawDispatcher reads it
|
||||||
// per frame. Wired into the hook router in OnLoad, advanced once per
|
// per frame. Wired into the hook router in OnLoad, advanced once per
|
||||||
// frame in the main loop regardless of _animatedEntities membership
|
// frame in the main loop regardless of _animatedEntities membership
|
||||||
|
|
@ -472,10 +473,10 @@ public sealed class GameWindow :
|
||||||
_localPlayerAnimation;
|
_localPlayerAnimation;
|
||||||
private AcDream.App.Physics.LocalPlayerShadowSynchronizer?
|
private AcDream.App.Physics.LocalPlayerShadowSynchronizer?
|
||||||
_localPlayerShadowSynchronizer;
|
_localPlayerShadowSynchronizer;
|
||||||
// Phase D.2b-C — live character-sheet assembly + raise flow (extracted
|
// Phase D.2b-C — live character-sheet assembly + raise flow (extracted
|
||||||
// feature class; GameWindow only wires it). Null unless ACDREAM_RETAIL_UI=1.
|
// feature class; GameWindow only wires it). Null unless ACDREAM_RETAIL_UI=1.
|
||||||
private AcDream.App.UI.Layout.CharacterSheetProvider? _characterSheetProvider;
|
private AcDream.App.UI.Layout.CharacterSheetProvider? _characterSheetProvider;
|
||||||
// Phase K.2 — auto-enter player mode after a successful login. Armed
|
// Phase K.2 — auto-enter player mode after a successful login. Armed
|
||||||
// by LiveSessionHost's entered-world transition; ticked from
|
// by LiveSessionHost's entered-world transition; ticked from
|
||||||
// OnUpdate; disarmed if the user manually enters fly mode (or any
|
// OnUpdate; disarmed if the user manually enters fly mode (or any
|
||||||
// other path that pre-empts the chase camera). Skipped entirely
|
// other path that pre-empts the chase camera). Skipped entirely
|
||||||
|
|
@ -484,7 +485,7 @@ public sealed class GameWindow :
|
||||||
// the bool here.
|
// the bool here.
|
||||||
private AcDream.App.Input.PlayerModeAutoEntry? _playerModeAutoEntry;
|
private AcDream.App.Input.PlayerModeAutoEntry? _playerModeAutoEntry;
|
||||||
|
|
||||||
// Phase K.1b / Slice 8 F — one semantic input path. Transitional actions
|
// Phase K.1b / Slice 8 F — one semantic input path. Transitional actions
|
||||||
// flow through GameplayInputActionRouter; held movement is polled through
|
// flow through GameplayInputActionRouter; held movement is polled through
|
||||||
// InputDispatcher. Raw axis motion belongs to CameraPointerInputController.
|
// InputDispatcher. Raw axis motion belongs to CameraPointerInputController.
|
||||||
private AcDream.App.Input.SilkKeyboardSource? _kbSource;
|
private AcDream.App.Input.SilkKeyboardSource? _kbSource;
|
||||||
|
|
@ -506,7 +507,7 @@ public sealed class GameWindow :
|
||||||
// configuration path,
|
// configuration path,
|
||||||
// falling back to the retail-faithful defaults if the file is missing
|
// falling back to the retail-faithful defaults if the file is missing
|
||||||
// or corrupt. This is THE single source of truth for the keymap at
|
// or corrupt. This is THE single source of truth for the keymap at
|
||||||
// startup — no other call to RetailDefaults() / AcdreamCurrentDefaults()
|
// startup — no other call to RetailDefaults() / AcdreamCurrentDefaults()
|
||||||
// should land in the GameWindow construction path.
|
// should land in the GameWindow construction path.
|
||||||
private readonly AcDream.UI.Abstractions.Input.KeyBindings _keyBindings;
|
private readonly AcDream.UI.Abstractions.Input.KeyBindings _keyBindings;
|
||||||
private readonly GraphicalHostPlatformServices _platformServices;
|
private readonly GraphicalHostPlatformServices _platformServices;
|
||||||
|
|
@ -522,7 +523,7 @@ public sealed class GameWindow :
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 4.7: optional live connection to an ACE server. Enabled only when
|
// Phase 4.7: optional live connection to an ACE server. Enabled only when
|
||||||
// ACDREAM_LIVE=1 is in the environment — fully backward compatible with
|
// ACDREAM_LIVE=1 is in the environment — fully backward compatible with
|
||||||
// the offline rendering pipeline.
|
// the offline rendering pipeline.
|
||||||
// Runtime owns the canonical session generation and transport lifetime.
|
// Runtime owns the canonical session generation and transport lifetime.
|
||||||
// The window retains only the composition handles needed for startup and
|
// The window retains only the composition handles needed for startup and
|
||||||
|
|
@ -538,7 +539,7 @@ public sealed class GameWindow :
|
||||||
// ACDREAM_LIVE=1 was set when the window came up.
|
// ACDREAM_LIVE=1 was set when the window came up.
|
||||||
// Backed by RuntimeOptions.LiveMode via the _options field.
|
// Backed by RuntimeOptions.LiveMode via the _options field.
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Phase 6.6/6.7: server-guid → local WorldEntity lookup so
|
/// Phase 6.6/6.7: server-guid → local WorldEntity lookup so
|
||||||
/// UpdateMotion and UpdatePosition handlers can find the entity the
|
/// UpdateMotion and UpdatePosition handlers can find the entity the
|
||||||
/// server is talking about. This is the canonical materialized top-level
|
/// server is talking about. This is the canonical materialized top-level
|
||||||
/// projection, including live objects parked in pending landblocks;
|
/// projection, including live objects parked in pending landblocks;
|
||||||
|
|
@ -557,7 +558,7 @@ public sealed class GameWindow :
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private IReadOnlyDictionary<uint, AcDream.Core.Net.WorldSession.EntitySpawn> LastSpawns =>
|
private IReadOnlyDictionary<uint, AcDream.Core.Net.WorldSession.EntitySpawn> LastSpawns =>
|
||||||
_liveEntities?.Snapshots ?? EmptyLiveSpawnMap;
|
_liveEntities?.Snapshots ?? EmptyLiveSpawnMap;
|
||||||
// R5-V2: the local player's CPhysicsObj stand-in — owns the player's
|
// R5-V2: the local player's CPhysicsObj stand-in — owns the player's
|
||||||
// TargetManager voyeur system, stored on the exact live record so remote
|
// TargetManager voyeur system, stored on the exact live record so remote
|
||||||
// entities chasing the player resolve it. Replaces the AP-79
|
// entities chasing the player resolve it. Replaces the AP-79
|
||||||
// _playerMoveToTarget* poll fields.
|
// _playerMoveToTarget* poll fields.
|
||||||
|
|
@ -566,14 +567,14 @@ public sealed class GameWindow :
|
||||||
private EntityPhysicsHost? _playerHost
|
private EntityPhysicsHost? _playerHost
|
||||||
=> _playerHostSlot.Host;
|
=> _playerHostSlot.Host;
|
||||||
|
|
||||||
// R5-V2: guid → per-entity IPhysicsObjHost registry (retail's
|
// R5-V2: guid → per-entity IPhysicsObjHost registry (retail's
|
||||||
// CObjectMaint::GetObjectA lookup). Backs every host's GetObjectA seam,
|
// CObjectMaint::GetObjectA lookup). Backs every host's GetObjectA seam,
|
||||||
// giving the TargetManager voyeur round-trip its cross-entity delivery
|
// giving the TargetManager voyeur round-trip its cross-entity delivery
|
||||||
// path. Populated for remotes plus the PlayerModeController local entry
|
// path. Populated for remotes plus the PlayerModeController local entry
|
||||||
// (player); pruned only by logical LiveEntityRuntime teardown.
|
// (player); pruned only by logical LiveEntityRuntime teardown.
|
||||||
|
|
||||||
// ServerControlledVelocityStaleSeconds moved to RemotePhysicsUpdater (#184
|
// ServerControlledVelocityStaleSeconds moved to RemotePhysicsUpdater (#184
|
||||||
// Slice 2a — the DR tick's stale-velocity anim-stop was its only user).
|
// Slice 2a — the DR tick's stale-velocity anim-stop was its only user).
|
||||||
public GameWindow(
|
public GameWindow(
|
||||||
AcDream.App.RuntimeOptions options,
|
AcDream.App.RuntimeOptions options,
|
||||||
WorldGameState worldGameState,
|
WorldGameState worldGameState,
|
||||||
|
|
@ -684,7 +685,7 @@ public sealed class GameWindow :
|
||||||
var options = WindowOptions.Default with
|
var options = WindowOptions.Default with
|
||||||
{
|
{
|
||||||
Size = new Vector2D<int>(1280, 720),
|
Size = new Vector2D<int>(1280, 720),
|
||||||
Title = "acdream — phase 1",
|
Title = "acdream — phase 1",
|
||||||
API = new GraphicsAPI(
|
API = new GraphicsAPI(
|
||||||
ContextAPI.OpenGL,
|
ContextAPI.OpenGL,
|
||||||
ContextProfile.Core,
|
ContextProfile.Core,
|
||||||
|
|
@ -748,6 +749,10 @@ public sealed class GameWindow :
|
||||||
IGpuDevice value) =>
|
IGpuDevice value) =>
|
||||||
PublishCompositionOwner(ref _gpuDevice, value, "GPU device (RHI)");
|
PublishCompositionOwner(ref _gpuDevice, value, "GPU device (RHI)");
|
||||||
|
|
||||||
|
void IGameWindowHostInputCameraPublication.PublishGpuFrameLifetime(
|
||||||
|
GpuDeviceFrameLifetime value) =>
|
||||||
|
PublishCompositionOwner(ref _gpuFrameLifetime, value, "GPU frame lifetime");
|
||||||
|
|
||||||
void IGameWindowHostInputCameraPublication.PublishKeyboardSource(
|
void IGameWindowHostInputCameraPublication.PublishKeyboardSource(
|
||||||
AcDream.App.Input.SilkKeyboardSource value) =>
|
AcDream.App.Input.SilkKeyboardSource value) =>
|
||||||
PublishCompositionOwner(ref _kbSource, value, "keyboard source");
|
PublishCompositionOwner(ref _kbSource, value, "keyboard source");
|
||||||
|
|
@ -1303,6 +1308,7 @@ public sealed class GameWindow :
|
||||||
_worldEnvironment,
|
_worldEnvironment,
|
||||||
_renderResourceLifetime,
|
_renderResourceLifetime,
|
||||||
_gpuFrameFlights!,
|
_gpuFrameFlights!,
|
||||||
|
_gpuDevice!,
|
||||||
_options.ResidencyBudgets,
|
_options.ResidencyBudgets,
|
||||||
initialCenterLandblockId,
|
initialCenterLandblockId,
|
||||||
_applicationPaths.DiagnosticsDirectory,
|
_applicationPaths.DiagnosticsDirectory,
|
||||||
|
|
@ -1322,9 +1328,10 @@ public sealed class GameWindow :
|
||||||
new InteractionRetainedUiDependencies(
|
new InteractionRetainedUiDependencies(
|
||||||
_options,
|
_options,
|
||||||
platformResult.Graphics,
|
platformResult.Graphics,
|
||||||
|
hostInputCamera.GpuDevice,
|
||||||
|
() => hostInputCamera.GpuFrameLifetime.Current,
|
||||||
_window!,
|
_window!,
|
||||||
platformResult.Input,
|
platformResult.Input,
|
||||||
worldRender.Foundation.ShadersDirectory,
|
|
||||||
contentEffectsAudio.Dats,
|
contentEffectsAudio.Dats,
|
||||||
_datLock,
|
_datLock,
|
||||||
worldRender.Foundation.TextureCache,
|
worldRender.Foundation.TextureCache,
|
||||||
|
|
@ -1374,6 +1381,7 @@ public sealed class GameWindow :
|
||||||
new LivePresentationDependencies(
|
new LivePresentationDependencies(
|
||||||
_options,
|
_options,
|
||||||
platformResult.Graphics,
|
platformResult.Graphics,
|
||||||
|
_gpuDevice!,
|
||||||
_window!,
|
_window!,
|
||||||
_datLock,
|
_datLock,
|
||||||
_runtimeSettings,
|
_runtimeSettings,
|
||||||
|
|
@ -1541,7 +1549,7 @@ public sealed class GameWindow :
|
||||||
_frameGraphs.Tick(new AcDream.App.Update.UpdateFrameInput(dt));
|
_frameGraphs.Tick(new AcDream.App.Update.UpdateFrameInput(dt));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Performance overlay state — updated every ~0.5s and written to the
|
// Performance overlay state — updated every ~0.5s and written to the
|
||||||
// window title so there's zero rendering cost (no font/overlay needed).
|
// window title so there's zero rendering cost (no font/overlay needed).
|
||||||
private void OnRender(double deltaSeconds)
|
private void OnRender(double deltaSeconds)
|
||||||
{
|
{
|
||||||
|
|
@ -1557,20 +1565,20 @@ public sealed class GameWindow :
|
||||||
// IsEntityCurrentlyMoving REMOVED (2026-07-09): it powered a cache-bypass
|
// IsEntityCurrentlyMoving REMOVED (2026-07-09): it powered a cache-bypass
|
||||||
// narrowing that dropped settled-open doors / faded-out walls back onto the
|
// narrowing that dropped settled-open doors / faded-out walls back onto the
|
||||||
// stale Tier-1 rest-pose cache entry (the door/fade "flip-back"). See the
|
// stale Tier-1 rest-pose cache entry (the door/fade "flip-back"). See the
|
||||||
// animatedIds build site — every Sequencer entity is now added
|
// animatedIds build site — every Sequencer entity is now added
|
||||||
// unconditionally, which is the known-good pre-optimization behavior.
|
// unconditionally, which is the known-good pre-optimization behavior.
|
||||||
|
|
||||||
// R3-W6: UpdatePlayerAnimation DELETED — the player's sequencer is
|
// R3-W6: UpdatePlayerAnimation DELETED — the player's sequencer is
|
||||||
// driven through the SAME MotionTableDispatchSink/DefaultSink funnel
|
// driven through the SAME MotionTableDispatchSink/DefaultSink funnel
|
||||||
// remotes use (edge-driven DoMotion/StopMotion/set_hold_run in
|
// remotes use (edge-driven DoMotion/StopMotion/set_hold_run in
|
||||||
// PlayerMovementController; airborne-Falling falls out of
|
// PlayerMovementController; airborne-Falling falls out of
|
||||||
// contact_allows_move + apply_current_movement). The #45 sidestep
|
// contact_allows_move + apply_current_movement). The #45 sidestep
|
||||||
// 1.248x factor + ACDREAM_ANIM_SPEED_SCALE died with it —
|
// 1.248x factor + ACDREAM_ANIM_SPEED_SCALE died with it —
|
||||||
// EXPECTED-DIFF: local sidestep pacing now matches how remotes have
|
// EXPECTED-DIFF: local sidestep pacing now matches how remotes have
|
||||||
// always played (w6-cutover-map.md R3).
|
// always played (w6-cutover-map.md R3).
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// L.0 Display tab: framebuffer-resize handler — update GL viewport
|
/// L.0 Display tab: framebuffer-resize handler — update GL viewport
|
||||||
/// + camera aspect when the window is resized (by the user dragging
|
/// + camera aspect when the window is resized (by the user dragging
|
||||||
/// the corner OR by the runtime display target applying a saved
|
/// the corner OR by the runtime display target applying a saved
|
||||||
/// Resolution). Without this, the viewport stays pinned at the
|
/// Resolution). Without this, the viewport stays pinned at the
|
||||||
|
|
|
||||||
|
|
@ -190,9 +190,26 @@ internal sealed class GlGpuDevice : IGpuDevice
|
||||||
string fragmentPath = Path.Combine(_shadersDirectory, $"{description.Shaders.Name}.frag");
|
string fragmentPath = Path.Combine(_shadersDirectory, $"{description.Shaders.Name}.frag");
|
||||||
string vertexSource = File.ReadAllText(vertexPath);
|
string vertexSource = File.ReadAllText(vertexPath);
|
||||||
string fragmentSource = File.ReadAllText(fragmentPath);
|
string fragmentSource = File.ReadAllText(fragmentPath);
|
||||||
|
|
||||||
|
// Every RHI-created pipeline gets the same common.glsl preamble the
|
||||||
|
// legacy Shader(gl, vert, frag, includeCommonPreamble: true) call
|
||||||
|
// sites already opt into (mesh_modern, terrain_modern): the set-0
|
||||||
|
// binding-9 texture-table buffer + ACDREAM_TEXTURE_HANDLE macro that
|
||||||
|
// slice V2 introduced. A pipeline whose shaders never reference the
|
||||||
|
// macro simply carries an unused SSBO declaration — harmless — so
|
||||||
|
// there is no reason for callers to opt in per pipeline.
|
||||||
|
string commonSource = CommonPreambleSource;
|
||||||
|
vertexSource = Shader.InjectPreamble(vertexSource, commonSource);
|
||||||
|
fragmentSource = Shader.InjectPreamble(fragmentSource, commonSource);
|
||||||
|
|
||||||
return new GlGpuPipeline(_gl, Retirement, description, vertexSource, fragmentSource);
|
return new GlGpuPipeline(_gl, Retirement, description, vertexSource, fragmentSource);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string? _commonPreambleSource;
|
||||||
|
|
||||||
|
private string CommonPreambleSource => _commonPreambleSource ??=
|
||||||
|
File.ReadAllText(Path.Combine(_shadersDirectory, "common.glsl"));
|
||||||
|
|
||||||
public IGpuRenderTarget CreateRenderTarget(in GpuRenderTargetDescription description)
|
public IGpuRenderTarget CreateRenderTarget(in GpuRenderTargetDescription description)
|
||||||
{
|
{
|
||||||
ThrowIfDisposed();
|
ThrowIfDisposed();
|
||||||
|
|
@ -215,6 +232,33 @@ internal sealed class GlGpuDevice : IGpuDevice
|
||||||
return new GpuTextureSlot(slot);
|
return new GpuTextureSlot(slot);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers an externally-created, externally-owned GL texture name
|
||||||
|
/// (a private-viewport FBO colour attachment) into the same texture table
|
||||||
|
/// <see cref="RegisterTexture"/> uses. Not part of <see cref="IGpuDevice"/>
|
||||||
|
/// — <c>PrivateEntityViewportRenderer</c>/<c>PaperdollViewportRenderer</c>
|
||||||
|
/// still allocate their own FBO textures directly on <c>GL</c> pre-V4g, so
|
||||||
|
/// their retained-UI consumers (<see cref="UiViewport"/>) need a bridge
|
||||||
|
/// that does not require the caller to own an <see cref="IGpuTexture"/>.
|
||||||
|
/// The caller keeps deleting the GL name itself; this only ever writes
|
||||||
|
/// the bindless handle into the table and returns the slot. Deleted with
|
||||||
|
/// the GL backend once slice V4g moves those renderers onto
|
||||||
|
/// <see cref="IGpuRenderTarget"/> and they can call
|
||||||
|
/// <see cref="RegisterTexture"/> like every other consumer.
|
||||||
|
/// </summary>
|
||||||
|
internal GpuTextureSlot RegisterExternalColorTexture(uint glTextureName, IGpuSampler sampler)
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
ArgumentNullException.ThrowIfNull(sampler);
|
||||||
|
if (sampler is not GlGpuSampler glSampler)
|
||||||
|
throw new ArgumentException("The GL backend can only register a GL sampler.", nameof(sampler));
|
||||||
|
|
||||||
|
uint slot = _textureSlotAllocator.Allocate();
|
||||||
|
ulong handle = _bindless.GetResidentHandle(glTextureName, glSampler.GlName);
|
||||||
|
WriteHandle(slot, handle);
|
||||||
|
return new GpuTextureSlot(slot);
|
||||||
|
}
|
||||||
|
|
||||||
public void ReleaseTextureSlot(GpuTextureSlot slot)
|
public void ReleaseTextureSlot(GpuTextureSlot slot)
|
||||||
{
|
{
|
||||||
ThrowIfDisposed();
|
ThrowIfDisposed();
|
||||||
|
|
@ -239,6 +283,17 @@ internal sealed class GlGpuDevice : IGpuDevice
|
||||||
long serial = ++_nextSerial;
|
long serial = ++_nextSerial;
|
||||||
int slot = _frameFlights.CurrentSlot;
|
int slot = _frameFlights.CurrentSlot;
|
||||||
_ringStates[slot].Reset();
|
_ringStates[slot].Reset();
|
||||||
|
// Campaign V slice V4a: during the migration, every still-legacy
|
||||||
|
// renderer (WbDrawDispatcher, terrain, particles, EnvCellRenderer, ...)
|
||||||
|
// mutates real GL program/blend/depth/cull state directly, outside this
|
||||||
|
// device's ApplyRenderState. _renderState has no way to observe those
|
||||||
|
// calls, so its cached "current" snapshot goes stale the moment any of
|
||||||
|
// them runs between two RHI binds. Resetting once per frame is the same
|
||||||
|
// defensive move BeginPass already makes after a forced clear (see the
|
||||||
|
// comment there) — it costs one redundant state application on the
|
||||||
|
// frame's first BindPipeline, in exchange for never diffing against a
|
||||||
|
// baseline the driver has since moved past.
|
||||||
|
_renderState.Reset();
|
||||||
return new GlGpuFrame(this, slot, serial);
|
return new GlGpuFrame(this, slot, serial);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -283,6 +338,17 @@ internal sealed class GlGpuDevice : IGpuDevice
|
||||||
_textureHandleTable.AsSpan(tableStart, tableEnd - tableStart));
|
_textureHandleTable.AsSpan(tableStart, tableEnd - tableStart));
|
||||||
_textureTableBuffer.Upload((long)tableStart * sizeof(ulong), bytes);
|
_textureTableBuffer.Upload((long)tableStart * sizeof(ulong), bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bind the device's own table to binding 9 immediately before every
|
||||||
|
// RHI-issued draw. Storage-buffer bindings are global GL context
|
||||||
|
// state, and the still-unmigrated legacy renderers (WbDrawDispatcher,
|
||||||
|
// EnvCellRenderer, TerrainModernRenderer, ParticleRenderer) rebind
|
||||||
|
// their OWN interim per-renderer table to the same binding right
|
||||||
|
// before their own draws (slice V2's documented pattern) — so this
|
||||||
|
// device must re-claim binding 9 before ITS draws too, or an RHI
|
||||||
|
// draw issued after a legacy draw in the same frame would read the
|
||||||
|
// wrong table.
|
||||||
|
_gl.BindBufferBase(GLEnum.ShaderStorageBuffer, GpuBindingModel.StorageTextureTable, _textureTableBuffer.GlName);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void BeginPass(GpuPassDescription description)
|
internal void BeginPass(GpuPassDescription description)
|
||||||
|
|
@ -395,6 +461,24 @@ internal sealed class GlGpuDevice : IGpuDevice
|
||||||
{
|
{
|
||||||
_gl.ColorMask(desired.ColorWrite, desired.ColorWrite, desired.ColorWrite, desired.ColorWrite);
|
_gl.ColorMask(desired.ColorWrite, desired.ColorWrite, desired.ColorWrite, desired.ColorWrite);
|
||||||
}
|
}
|
||||||
|
if (changes.Multisample)
|
||||||
|
{
|
||||||
|
// Vulkan bakes multisample rasterization into the pipeline via its
|
||||||
|
// declared sample count — there is no separate enable bit. The GL
|
||||||
|
// backend mirrors that: GL_MULTISAMPLE tracks
|
||||||
|
// GpuPipelineDescription.SampleCount rather than being left at
|
||||||
|
// whatever the previous pass happened to leave it. This matters
|
||||||
|
// because the default framebuffer can itself be multisampled
|
||||||
|
// (QualitySettings MSAA): a single-sample UI pipeline binding
|
||||||
|
// while GL_MULTISAMPLE is still enabled from an earlier world
|
||||||
|
// pass converts each glyph's soft alpha edge into dithered MSAA
|
||||||
|
// coverage instead of a clean alpha blend (the pre-RHI
|
||||||
|
// TextRenderer explicitly disabled it for the same reason).
|
||||||
|
if (desired.Multisample)
|
||||||
|
_gl.Enable(EnableCap.Multisample);
|
||||||
|
else
|
||||||
|
_gl.Disable(EnableCap.Multisample);
|
||||||
|
}
|
||||||
GLHelpers.ThrowOnResourceError(_gl, "apply GL render state");
|
GLHelpers.ThrowOnResourceError(_gl, "apply GL render state");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,8 @@ internal sealed class GlGpuPassEncoder : IGpuPassEncoder
|
||||||
description.Cull,
|
description.Cull,
|
||||||
description.FrontFace,
|
description.FrontFace,
|
||||||
description.AlphaToCoverage,
|
description.AlphaToCoverage,
|
||||||
description.ColorWrite);
|
description.ColorWrite,
|
||||||
|
Multisample: description.SampleCount > 1);
|
||||||
_device.ApplyRenderState(desired);
|
_device.ApplyRenderState(desired);
|
||||||
|
|
||||||
_gl.BindVertexArray(p.GlVertexArray);
|
_gl.BindVertexArray(p.GlVertexArray);
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,8 @@ internal readonly record struct GlRenderStateSnapshot(
|
||||||
GpuCullMode Cull,
|
GpuCullMode Cull,
|
||||||
GpuFrontFace FrontFace,
|
GpuFrontFace FrontFace,
|
||||||
bool AlphaToCoverage,
|
bool AlphaToCoverage,
|
||||||
bool ColorWrite);
|
bool ColorWrite,
|
||||||
|
bool Multisample);
|
||||||
|
|
||||||
/// <summary>Which GL state calls are needed to move from the previous snapshot to the new one.</summary>
|
/// <summary>Which GL state calls are needed to move from the previous snapshot to the new one.</summary>
|
||||||
internal readonly record struct GlRenderStateChanges(
|
internal readonly record struct GlRenderStateChanges(
|
||||||
|
|
@ -27,14 +28,15 @@ internal readonly record struct GlRenderStateChanges(
|
||||||
bool Cull,
|
bool Cull,
|
||||||
bool FrontFace,
|
bool FrontFace,
|
||||||
bool AlphaToCoverage,
|
bool AlphaToCoverage,
|
||||||
bool ColorWrite)
|
bool ColorWrite,
|
||||||
|
bool Multisample)
|
||||||
{
|
{
|
||||||
public bool AnyChange =>
|
public bool AnyChange =>
|
||||||
Program || Blend || DepthTest || DepthWrite || DepthCompare
|
Program || Blend || DepthTest || DepthWrite || DepthCompare
|
||||||
|| Cull || FrontFace || AlphaToCoverage || ColorWrite;
|
|| Cull || FrontFace || AlphaToCoverage || ColorWrite || Multisample;
|
||||||
|
|
||||||
/// <summary>Every dimension reported changed — used for the first apply after a reset.</summary>
|
/// <summary>Every dimension reported changed — used for the first apply after a reset.</summary>
|
||||||
internal static GlRenderStateChanges All { get; } = new(true, true, true, true, true, true, true, true, true);
|
internal static GlRenderStateChanges All { get; } = new(true, true, true, true, true, true, true, true, true, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -68,7 +70,8 @@ internal sealed class GlRenderStateCache
|
||||||
p.Cull != desired.Cull,
|
p.Cull != desired.Cull,
|
||||||
p.FrontFace != desired.FrontFace,
|
p.FrontFace != desired.FrontFace,
|
||||||
p.AlphaToCoverage != desired.AlphaToCoverage,
|
p.AlphaToCoverage != desired.AlphaToCoverage,
|
||||||
p.ColorWrite != desired.ColorWrite);
|
p.ColorWrite != desired.ColorWrite,
|
||||||
|
p.Multisample != desired.Multisample);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Discards the cached baseline — the next <see cref="Apply"/> reports every dimension changed.</summary>
|
/// <summary>Discards the cached baseline — the next <see cref="Apply"/> reports every dimension changed.</summary>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
|
||||||
namespace AcDream.App.Rendering;
|
namespace AcDream.App.Rendering;
|
||||||
|
|
||||||
public interface ICamera
|
internal interface ICamera
|
||||||
{
|
{
|
||||||
Matrix4x4 View { get; }
|
Matrix4x4 View { get; }
|
||||||
Matrix4x4 Projection { get; }
|
Matrix4x4 Projection { get; }
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
|
||||||
namespace AcDream.App.Rendering;
|
namespace AcDream.App.Rendering;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Result of a camera spring-arm sweep: the collided eye position AND the cell the swept
|
/// Result of a camera spring-arm sweep: the collided eye position AND the cell the swept
|
||||||
/// viewer-sphere ended in (retail <c>viewer_cell = sphere_path.curr_cell</c>, update_viewer
|
/// viewer-sphere ended in (retail <c>viewer_cell = sphere_path.curr_cell</c>, update_viewer
|
||||||
/// pc:92871). The cell is graph-tracked by the transition — no AABB, no grace frames — so it
|
/// pc:92871). The cell is graph-tracked by the transition — no AABB, no grace frames — so it
|
||||||
/// is the robust per-frame "which cell is the camera in?" answer that roots the render.
|
/// is the robust per-frame "which cell is the camera in?" answer that roots the render.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly record struct CameraSweepResult(Vector3 Eye, uint ViewerCellId);
|
internal readonly record struct CameraSweepResult(Vector3 Eye, uint ViewerCellId);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sweeps a small sphere from the camera pivot (player head) toward the
|
/// Sweeps a small sphere from the camera pivot (player head) toward the
|
||||||
|
|
@ -16,7 +16,7 @@ public readonly record struct CameraSweepResult(Vector3 Eye, uint ViewerCellId);
|
||||||
/// lets <see cref="RetailChaseCamera"/> collide its eye without depending on
|
/// lets <see cref="RetailChaseCamera"/> collide its eye without depending on
|
||||||
/// the physics engine directly (and stay unit-testable with a fake).
|
/// the physics engine directly (and stay unit-testable with a fake).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface ICameraCollisionProbe
|
internal interface ICameraCollisionProbe
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Roll a collision sphere from <paramref name="pivot"/> to
|
/// Roll a collision sphere from <paramref name="pivot"/> to
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
// IndoorDrawPlan.cs
|
// IndoorDrawPlan.cs
|
||||||
//
|
//
|
||||||
// Pure (GL-free) port of the membership half of retail PView::DrawCells (0x5a4840):
|
// Pure (GL-free) port of the membership half of retail PView::DrawCells (0x5a4840):
|
||||||
// the reverse cell_draw_list iterated per portal_view slice. EVERY visible cell with a
|
// the reverse cell_draw_list iterated per portal_view slice. EVERY visible cell with a
|
||||||
// non-empty view is included — there is NO "drawable" filter. Dropping cells without a
|
// non-empty view is included — there is NO "drawable" filter. Dropping cells without a
|
||||||
// clip-slot was the grey-walls bug (the cell's sealed shell never drew → clear color showed).
|
// clip-slot was the grey-walls bug (the cell's sealed shell never drew → clear color showed).
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace AcDream.App.Rendering;
|
namespace AcDream.App.Rendering;
|
||||||
|
|
||||||
public readonly record struct CellDrawEntry(uint CellId, IReadOnlyList<ViewPolygon> Slices);
|
internal readonly record struct CellDrawEntry(uint CellId, IReadOnlyList<ViewPolygon> Slices);
|
||||||
|
|
||||||
public static class IndoorDrawPlan
|
internal static class IndoorDrawPlan
|
||||||
{
|
{
|
||||||
/// <summary>Reverse OrderedVisibleCells (far→near), each visible cell with its view
|
/// <summary>Reverse OrderedVisibleCells (far→near), each visible cell with its view
|
||||||
/// slices. Mirrors DrawCells' shell/object loops. Cells whose view is empty are skipped
|
/// slices. Mirrors DrawCells' shell/object loops. Cells whose view is empty are skipped
|
||||||
/// (they are not actually visible); no other cell is ever dropped.</summary>
|
/// (they are not actually visible); no other cell is ever dropped.</summary>
|
||||||
public static List<CellDrawEntry> ShellPass(PortalVisibilityFrame frame)
|
public static List<CellDrawEntry> ShellPass(PortalVisibilityFrame frame)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AcDream.Core.World;
|
using AcDream.Core.World;
|
||||||
|
|
||||||
|
|
@ -8,27 +8,27 @@ namespace AcDream.App.Rendering;
|
||||||
/// Splits a frame's landblock entities into the draw buckets used by the
|
/// Splits a frame's landblock entities into the draw buckets used by the
|
||||||
/// retail-style DrawInside flood.
|
/// retail-style DrawInside flood.
|
||||||
///
|
///
|
||||||
/// <para>T1 (fused BR-2/3, 2026-06-11) — retail draw-order contract: the
|
/// <para>T1 (fused BR-2/3, 2026-06-11) — retail draw-order contract: the
|
||||||
/// frame draws STATIC world first (terrain, building shells, scenery, then
|
/// frame draws STATIC world first (terrain, building shells, scenery, then
|
||||||
/// flooded interior cells + their static object lists), and every DYNAMIC
|
/// flooded interior cells + their static object lists), and every DYNAMIC
|
||||||
/// (server-spawned: player, NPCs, doors, items) draws LAST, depth-tested,
|
/// (server-spawned: player, NPCs, doors, items) draws LAST, depth-tested,
|
||||||
/// never hard-clipped. This is what makes the aperture depth punch safe —
|
/// never hard-clipped. This is what makes the aperture depth punch safe —
|
||||||
/// when the punch erases depth inside a doorway, no dynamic has been drawn
|
/// when the punch erases depth inside a doorway, no dynamic has been drawn
|
||||||
/// yet, so nothing visible is destroyed (retail: objects draw per cell AFTER
|
/// yet, so nothing visible is destroyed (retail: objects draw per cell AFTER
|
||||||
/// cells, PView::DrawCells epilogue Ghidra 0x005a4840; the first BR-2 attempt
|
/// cells, PView::DrawCells epilogue Ghidra 0x005a4840; the first BR-2 attempt
|
||||||
/// punched after dynamics and erased the player, reverted 88be519).</para>
|
/// punched after dynamics and erased the player, reverted 88be519).</para>
|
||||||
///
|
///
|
||||||
/// <list type="bullet">
|
/// <list type="bullet">
|
||||||
/// <item><see cref="Result.ByCell"/> — indoor STATICS (dat-baked, ServerGuid==0)
|
/// <item><see cref="Result.ByCell"/> — indoor STATICS (dat-baked, ServerGuid==0)
|
||||||
/// per visible cell, drawn with their cell.</item>
|
/// per visible cell, drawn with their cell.</item>
|
||||||
/// <item><see cref="Result.OutdoorStatic"/> — outdoor statics (building
|
/// <item><see cref="Result.OutdoorStatic"/> — outdoor statics (building
|
||||||
/// shells, scenery stabs), drawn with the world/landscape pass.</item>
|
/// shells, scenery stabs), drawn with the world/landscape pass.</item>
|
||||||
/// <item><see cref="Result.Dynamics"/> — ALL server-spawned entities
|
/// <item><see cref="Result.Dynamics"/> — ALL server-spawned entities
|
||||||
/// (ServerGuid != 0) regardless of cell, plus unresolved-cell live entities;
|
/// (ServerGuid != 0) regardless of cell, plus unresolved-cell live entities;
|
||||||
/// drawn in the frame's single LAST entity pass.</item>
|
/// drawn in the frame's single LAST entity pass.</item>
|
||||||
/// </list>
|
/// </list>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class InteriorEntityPartition
|
internal static class InteriorEntityPartition
|
||||||
{
|
{
|
||||||
internal enum ProjectionClass : byte
|
internal enum ProjectionClass : byte
|
||||||
{
|
{
|
||||||
|
|
@ -51,13 +51,13 @@ public static class InteriorEntityPartition
|
||||||
void AbortFrame();
|
void AbortFrame();
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class Result
|
internal sealed class Result
|
||||||
{
|
{
|
||||||
public Dictionary<uint, List<WorldEntity>> ByCell { get; } = new();
|
public Dictionary<uint, List<WorldEntity>> ByCell { get; } = new();
|
||||||
public List<WorldEntity> OutdoorStatic { get; } = new();
|
public List<WorldEntity> OutdoorStatic { get; } = new();
|
||||||
public List<WorldEntity> Dynamics { get; } = new();
|
public List<WorldEntity> Dynamics { get; } = new();
|
||||||
|
|
||||||
// MP-Alloc: scratch for PruneEmptyCellBuckets — reused across frames
|
// MP-Alloc: scratch for PruneEmptyCellBuckets — reused across frames
|
||||||
// so pruning itself doesn't allocate.
|
// so pruning itself doesn't allocate.
|
||||||
private readonly List<uint> _emptyCellScratch = new();
|
private readonly List<uint> _emptyCellScratch = new();
|
||||||
|
|
||||||
|
|
@ -82,7 +82,7 @@ public static class InteriorEntityPartition
|
||||||
/// entries (either newly emptied, or a leftover key from a previous
|
/// entries (either newly emptied, or a leftover key from a previous
|
||||||
/// frame's visible-cell set that this frame never touched). Keeps
|
/// frame's visible-cell set that this frame never touched). Keeps
|
||||||
/// ByCell.Count / .Keys bit-identical to the old always-fresh-
|
/// ByCell.Count / .Keys bit-identical to the old always-fresh-
|
||||||
/// Dictionary behavior — callers that inspect key presence/count
|
/// Dictionary behavior — callers that inspect key presence/count
|
||||||
/// directly (not just TryGetValue) must see exactly the cells that
|
/// directly (not just TryGetValue) must see exactly the cells that
|
||||||
/// actually received at least one static this frame.
|
/// actually received at least one static this frame.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -100,7 +100,7 @@ public static class InteriorEntityPartition
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Allocating overload — always returns a brand-new <see cref="Result"/>.
|
/// Allocating overload — always returns a brand-new <see cref="Result"/>.
|
||||||
/// Kept for tests and any one-shot caller; the per-frame render path
|
/// Kept for tests and any one-shot caller; the per-frame render path
|
||||||
/// uses the <see cref="Partition(Result, HashSet{uint}, IEnumerable{ValueTuple})"/>
|
/// uses the <see cref="Partition(Result, HashSet{uint}, IEnumerable{ValueTuple})"/>
|
||||||
/// reuse overload instead (see <see cref="RetailPViewRenderer"/>).
|
/// reuse overload instead (see <see cref="RetailPViewRenderer"/>).
|
||||||
|
|
@ -121,7 +121,7 @@ public static class InteriorEntityPartition
|
||||||
/// in place (see <see cref="Result.ClearForReuse"/>) and refills it,
|
/// in place (see <see cref="Result.ClearForReuse"/>) and refills it,
|
||||||
/// reusing each cell's existing <c>List<WorldEntity></c> when the
|
/// reusing each cell's existing <c>List<WorldEntity></c> when the
|
||||||
/// cell key survives from the previous frame instead of allocating a new
|
/// cell key survives from the previous frame instead of allocating a new
|
||||||
/// one — the per-cell dictionary entries persist across frames (cleared,
|
/// one — the per-cell dictionary entries persist across frames (cleared,
|
||||||
/// never removed) since the visible-cell set is usually stable frame to
|
/// never removed) since the visible-cell set is usually stable frame to
|
||||||
/// frame. Identical partitioning output to the allocating overload; only
|
/// frame. Identical partitioning output to the allocating overload; only
|
||||||
/// the backing storage is reused.
|
/// the backing storage is reused.
|
||||||
|
|
@ -141,7 +141,7 @@ public static class InteriorEntityPartition
|
||||||
if (e.MeshRefs.Count == 0) continue;
|
if (e.MeshRefs.Count == 0) continue;
|
||||||
|
|
||||||
// Retail contract: every server-spawned entity is a DYNAMIC
|
// Retail contract: every server-spawned entity is a DYNAMIC
|
||||||
// and draws in the last pass — indoor, outdoor, or unresolved.
|
// and draws in the last pass — indoor, outdoor, or unresolved.
|
||||||
if (e.ServerGuid != 0)
|
if (e.ServerGuid != 0)
|
||||||
{
|
{
|
||||||
result.Dynamics.Add(e);
|
result.Dynamics.Add(e);
|
||||||
|
|
@ -237,7 +237,7 @@ public static class InteriorEntityPartition
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Shared indoor classification — keep DrawDynamicsLast, the
|
/// <summary>Shared indoor classification — keep DrawDynamicsLast, the
|
||||||
/// outside-stage assignment (#118), and the partition in lockstep.</summary>
|
/// outside-stage assignment (#118), and the partition in lockstep.</summary>
|
||||||
public static bool IsIndoorCellId(uint cellId)
|
public static bool IsIndoorCellId(uint cellId)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,29 @@
|
||||||
// NdcScissorRect.cs
|
// NdcScissorRect.cs
|
||||||
//
|
//
|
||||||
// NDC AABB → framebuffer-pixel scissor box, CONSERVATIVE (outer bound).
|
// NDC AABB → framebuffer-pixel scissor box, CONSERVATIVE (outer bound).
|
||||||
// The scissor that brackets a landscape/doorway slice is a fallback BOUND on
|
// The scissor that brackets a landscape/doorway slice is a fallback BOUND on
|
||||||
// the slice's view region (AD-17 in the divergence register): it must CONTAIN
|
// the slice's view region (AD-17 in the divergence register): it must CONTAIN
|
||||||
// every fragment the per-fragment plane clip would keep. Under-inclusion is
|
// every fragment the per-fragment plane clip would keep. Under-inclusion is
|
||||||
// the bug class — the #130 doorway top-edge background strip was this box
|
// the bug class — the #130 doorway top-edge background strip was this box
|
||||||
// computed as Floor(origin) + Ceiling(size), whose far edge
|
// computed as Floor(origin) + Ceiling(size), whose far edge
|
||||||
// floor(min)+ceil(max−min) lands up to one pixel SHORT of the true max edge
|
// floor(min)+ceil(max−min) lands up to one pixel SHORT of the true max edge
|
||||||
// at unlucky fractional alignments, scissoring away the aperture's top/right
|
// at unlucky fractional alignments, scissoring away the aperture's top/right
|
||||||
// pixel row for the whole slice (sky, terrain, statics, weather) while the
|
// pixel row for the whole slice (sky, terrain, statics, weather) while the
|
||||||
// seal still stamps it — a strip of clear color no later pass can fill.
|
// seal still stamps it — a strip of clear color no later pass can fill.
|
||||||
//
|
//
|
||||||
// Correct outer bound: floor both mins, ceil both maxes, width = difference.
|
// Correct outer bound: floor both mins, ceil both maxes, width = difference.
|
||||||
// A fragment at pixel (i,j) rasterizes iff its CENTER (i+0.5, j+0.5) lies in
|
// A fragment at pixel (i,j) rasterizes iff its CENTER (i+0.5, j+0.5) lies in
|
||||||
// the region ⊆ the NDC box [X0,X1]×[Y0,Y1] (pixel units). Center-inside ⇒
|
// the region ⊆ the NDC box [X0,X1]×[Y0,Y1] (pixel units). Center-inside ⇒
|
||||||
// i ≥ X0−0.5 ⇒ i ≥ floor(X0) and i ≤ X1−0.5 ⇒ i < ceil(X1). So
|
// i ≥ X0−0.5 ⇒ i ≥ floor(X0) and i ≤ X1−0.5 ⇒ i < ceil(X1). So
|
||||||
// [floor(X0), ceil(X1)) admits every center-inside pixel, over-including by
|
// [floor(X0), ceil(X1)) admits every center-inside pixel, over-including by
|
||||||
// at most one pixel per edge — safe per AD-17's doctrine (the wall shell /
|
// at most one pixel per edge — safe per AD-17's doctrine (the wall shell /
|
||||||
// plane clip repaints or kills the surplus).
|
// plane clip repaints or kills the surplus).
|
||||||
using System;
|
using System;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
|
||||||
namespace AcDream.App.Rendering;
|
namespace AcDream.App.Rendering;
|
||||||
|
|
||||||
public static class NdcScissorRect
|
internal static class NdcScissorRect
|
||||||
{
|
{
|
||||||
/// <summary>Convert an NDC AABB (minX, minY, maxX, maxY in [-1,1]) to a
|
/// <summary>Convert an NDC AABB (minX, minY, maxX, maxY in [-1,1]) to a
|
||||||
/// framebuffer-pixel scissor box that CONTAINS it. Inputs are clamped to
|
/// framebuffer-pixel scissor box that CONTAINS it. Inputs are clamped to
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
namespace AcDream.App.Rendering;
|
namespace AcDream.App.Rendering;
|
||||||
|
|
||||||
public sealed class OrbitCamera : ICamera
|
internal sealed class OrbitCamera : ICamera
|
||||||
{
|
{
|
||||||
public Vector3 Target { get; set; } = new(96, 96, 0); // center of a 192x192 landblock
|
public Vector3 Target { get; set; } = new(96, 96, 0); // center of a 192x192 landblock
|
||||||
public float Distance { get; set; } = 300f;
|
public float Distance { get; set; } = 300f;
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,24 @@
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
|
||||||
namespace AcDream.App.Rendering;
|
namespace AcDream.App.Rendering;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Factory for the OUTDOOR render root — the cell the render roots at when the camera eye is outdoors.
|
/// Factory for the OUTDOOR render root — the cell the render roots at when the camera eye is outdoors.
|
||||||
/// Retail roots every in-world frame at <c>viewer_cell</c> (SmartBox::RenderNormalMode →
|
/// Retail roots every in-world frame at <c>viewer_cell</c> (SmartBox::RenderNormalMode →
|
||||||
/// DrawInside(viewer_cell), decomp:92635); when outdoors that is a <c>CLandCell</c>. acdream models it
|
/// DrawInside(viewer_cell), decomp:92635); when outdoors that is a <c>CLandCell</c>. acdream models it
|
||||||
/// as a portal-less <see cref="LoadedCell"/> carrying only <see cref="LoadedCell.IsOutdoorNode"/> (so
|
/// as a portal-less <see cref="LoadedCell"/> carrying only <see cref="LoadedCell.IsOutdoorNode"/> (so
|
||||||
/// <see cref="PortalVisibilityBuilder.Build"/> seeds OutsideView FULL-SCREEN → terrain/sky/scenery draw
|
/// <see cref="PortalVisibilityBuilder.Build"/> seeds OutsideView FULL-SCREEN → terrain/sky/scenery draw
|
||||||
/// as the root's shell) and <see cref="LoadedCell.SeenOutside"/>.
|
/// as the root's shell) and <see cref="LoadedCell.SeenOutside"/>.
|
||||||
///
|
///
|
||||||
/// <para>R-A2 (2026-06-08): the node no longer carries reverse portals into nearby buildings. Retail
|
/// <para>R-A2 (2026-06-08): the node no longer carries reverse portals into nearby buildings. Retail
|
||||||
/// does NOT flood buildings from the land root — buildings flood SEPARATELY, per-building, during the
|
/// does NOT flood buildings from the land root — buildings flood SEPARATELY, per-building, during the
|
||||||
/// landscape draw (terrain BSP → DrawPortal → ConstructView(CBldPortal), decomp:326881/433895/433827).
|
/// landscape draw (terrain BSP → DrawPortal → ConstructView(CBldPortal), decomp:326881/433895/433827).
|
||||||
/// acdream issues those via <see cref="PortalVisibilityBuilder.ConstructViewBuilding"/> per nearby
|
/// acdream issues those via <see cref="PortalVisibilityBuilder.ConstructViewBuilding"/> per nearby
|
||||||
/// building inside <see cref="RetailPViewRenderer.DrawInside"/>. The pre-R-A2 design flooded all
|
/// building inside <see cref="RetailPViewRenderer.DrawInside"/>. The pre-R-A2 design flooded all
|
||||||
/// buildings from one root through reverse portals, coupling their interior membership to a single
|
/// buildings from one root through reverse portals, coupling their interior membership to a single
|
||||||
/// root-level portal-side test that oscillated as the chase eye grazed a doorway — the indoor flap.</para>
|
/// root-level portal-side test that oscillated as the chase eye grazed a doorway — the indoor flap.</para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class OutdoorCellNode
|
internal static class OutdoorCellNode
|
||||||
{
|
{
|
||||||
public static LoadedCell Build(uint outdoorCellId) => new LoadedCell
|
public static LoadedCell Build(uint outdoorCellId) => new LoadedCell
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -191,13 +191,16 @@ internal sealed class RetailPaperdollFrameView : IPaperdollFrameView
|
||||||
{
|
{
|
||||||
private readonly UiViewport _viewport;
|
private readonly UiViewport _viewport;
|
||||||
private readonly IPaperdollInventoryVisibility _inventory;
|
private readonly IPaperdollInventoryVisibility _inventory;
|
||||||
|
private readonly ExternalViewportTextureBridge _textureBridge;
|
||||||
|
|
||||||
public RetailPaperdollFrameView(
|
public RetailPaperdollFrameView(
|
||||||
UiViewport viewport,
|
UiViewport viewport,
|
||||||
IPaperdollInventoryVisibility inventory)
|
IPaperdollInventoryVisibility inventory,
|
||||||
|
IGpuDevice gpuDevice)
|
||||||
{
|
{
|
||||||
_viewport = viewport ?? throw new ArgumentNullException(nameof(viewport));
|
_viewport = viewport ?? throw new ArgumentNullException(nameof(viewport));
|
||||||
_inventory = inventory ?? throw new ArgumentNullException(nameof(inventory));
|
_inventory = inventory ?? throw new ArgumentNullException(nameof(inventory));
|
||||||
|
_textureBridge = new ExternalViewportTextureBridge(gpuDevice);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryGetVisibleSize(out int width, out int height)
|
public bool TryGetVisibleSize(out int width, out int height)
|
||||||
|
|
@ -215,7 +218,7 @@ internal sealed class RetailPaperdollFrameView : IPaperdollFrameView
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetTextureHandle(uint textureHandle) =>
|
public void SetTextureHandle(uint textureHandle) =>
|
||||||
_viewport.TextureHandle = textureHandle;
|
_viewport.TextureHandle = _textureBridge.Resolve(textureHandle);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Narrow visibility adapter for the paperdoll's inventory host.</summary>
|
/// <summary>Narrow visibility adapter for the paperdoll's inventory host.</summary>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using AcDream.App.Rendering.Wb;
|
using AcDream.App.Rendering.Wb;
|
||||||
using AcDream.App.UI;
|
using AcDream.App.UI;
|
||||||
using AcDream.Core.Lighting;
|
using AcDream.Core.Lighting;
|
||||||
using AcDream.Core.World;
|
using AcDream.Core.World;
|
||||||
|
|
@ -10,7 +10,7 @@ namespace AcDream.App.Rendering;
|
||||||
/// Paperdoll-specific facade over the shared private creature viewport. The
|
/// Paperdoll-specific facade over the shared private creature viewport. The
|
||||||
/// fixed camera remains the verbatim retail <c>gmPaperDollUI</c> camera.
|
/// fixed camera remains the verbatim retail <c>gmPaperDollUI</c> camera.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class PaperdollViewportRenderer :
|
internal sealed class PaperdollViewportRenderer :
|
||||||
IUiViewportRenderer,
|
IUiViewportRenderer,
|
||||||
IPaperdollDollRenderer,
|
IPaperdollDollRenderer,
|
||||||
IDisposable
|
IDisposable
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
@ -22,7 +22,7 @@ namespace AcDream.App.Rendering;
|
||||||
/// compositing order is shared with ordinary translucent GfxObj parts. Sky and
|
/// compositing order is shared with ordinary translucent GfxObj parts. Sky and
|
||||||
/// sealed off-screen passes retain their independent immediate path.
|
/// sealed off-screen passes retain their independent immediate path.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed unsafe class ParticleRenderer : IDisposable
|
internal sealed unsafe class ParticleRenderer : IDisposable
|
||||||
{
|
{
|
||||||
// The texture is per instance through GL_ARB_bindless_texture. Only blend
|
// The texture is per instance through GL_ARB_bindless_texture. Only blend
|
||||||
// state remains a draw-call boundary, so stable retail distance order no
|
// state remains a draw-call boundary, so stable retail distance order no
|
||||||
|
|
@ -69,8 +69,8 @@ public sealed unsafe class ParticleRenderer : IDisposable
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Vertex-instance ABI shared with particle.vert. Campaign V slice V2c
|
/// Vertex-instance ABI shared with particle.vert. Campaign V slice V2c
|
||||||
/// (2026-07-27): TextureHandleLow/High (the split halves of a raw 64-bit
|
/// (2026-07-27): TextureHandleLow/High (the split halves of a raw 64-bit
|
||||||
/// ARB_bindless_texture handle) became one TextureIndex — a slot into the
|
/// ARB_bindless_texture handle) became one TextureIndex — a slot into the
|
||||||
/// binding=9 handle table — so ordered particles using different textures
|
/// binding=9 handle table — so ordered particles using different textures
|
||||||
/// still remain one instanced draw when their blend mode matches.
|
/// still remain one instanced draw when their blend mode matches.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[StructLayout(LayoutKind.Sequential)]
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
|
@ -121,8 +121,8 @@ public sealed unsafe class ParticleRenderer : IDisposable
|
||||||
|
|
||||||
// Campaign V slice V2c (2026-07-27): GL-only emulation of the eventual
|
// Campaign V slice V2c (2026-07-27): GL-only emulation of the eventual
|
||||||
// Vulkan global texture descriptor array (binding=9,
|
// Vulkan global texture descriptor array (binding=9,
|
||||||
// GpuBindingModel.StorageTextureTable). Owns its own table — see
|
// GpuBindingModel.StorageTextureTable). Owns its own table — see
|
||||||
// GlBindlessHandleTable's doc comment and the campaign doc's §5.2 for why
|
// GlBindlessHandleTable's doc comment and the campaign doc's §5.2 for why
|
||||||
// particles don't share WbDrawDispatcher's/EnvCellRenderer's/
|
// particles don't share WbDrawDispatcher's/EnvCellRenderer's/
|
||||||
// TerrainModernRenderer's tables. There is no automated pixel-gate
|
// TerrainModernRenderer's tables. There is no automated pixel-gate
|
||||||
// coverage for particles (the offline gate's fixed outdoor view has none
|
// coverage for particles (the offline gate's fixed outdoor view has none
|
||||||
|
|
@ -1157,7 +1157,7 @@ public sealed unsafe class ParticleRenderer : IDisposable
|
||||||
_gl.VertexAttribPointer(5, 4, VertexAttribPointerType.Float, false, instanceStride, (void*)(12 * sizeof(float)));
|
_gl.VertexAttribPointer(5, 4, VertexAttribPointerType.Float, false, instanceStride, (void*)(12 * sizeof(float)));
|
||||||
_gl.VertexAttribDivisor(5, 1);
|
_gl.VertexAttribDivisor(5, 1);
|
||||||
// Campaign V slice V2c: one uint table slot (was uvec2 low/high
|
// Campaign V slice V2c: one uint table slot (was uvec2 low/high
|
||||||
// handle halves) — BillboardGpuInstance shrank by 4 bytes.
|
// handle halves) — BillboardGpuInstance shrank by 4 bytes.
|
||||||
_gl.EnableVertexAttribArray(6);
|
_gl.EnableVertexAttribArray(6);
|
||||||
_gl.VertexAttribIPointer(
|
_gl.VertexAttribIPointer(
|
||||||
6,
|
6,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
|
|
||||||
namespace AcDream.App.Rendering;
|
namespace AcDream.App.Rendering;
|
||||||
|
|
@ -12,7 +12,7 @@ namespace AcDream.App.Rendering;
|
||||||
/// cell walls (<c>FindEnvCollisions</c>) AND outdoor/baked GfxObj shells
|
/// cell walls (<c>FindEnvCollisions</c>) AND outdoor/baked GfxObj shells
|
||||||
/// (<c>FindObjCollisions</c>) in one faithful path.
|
/// (<c>FindObjCollisions</c>) in one faithful path.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class PhysicsCameraCollisionProbe : ICameraCollisionProbe
|
internal sealed class PhysicsCameraCollisionProbe : ICameraCollisionProbe
|
||||||
{
|
{
|
||||||
/// <summary>Retail <c>viewer_sphere</c> radius (acclient :93314).</summary>
|
/// <summary>Retail <c>viewer_sphere</c> radius (acclient :93314).</summary>
|
||||||
public const float ViewerSphereRadius = 0.3f;
|
public const float ViewerSphereRadius = 0.3f;
|
||||||
|
|
@ -23,13 +23,13 @@ public sealed class PhysicsCameraCollisionProbe : ICameraCollisionProbe
|
||||||
|
|
||||||
public CameraSweepResult SweepEye(Vector3 pivot, Vector3 desiredEye, uint cellId, uint selfEntityId, Vector3 playerPos)
|
public CameraSweepResult SweepEye(Vector3 pivot, Vector3 desiredEye, uint cellId, uint selfEntityId, Vector3 playerPos)
|
||||||
{
|
{
|
||||||
// update_viewer: player->cell == 0 → set_viewer(player_pos, 1), viewer_cell = null
|
// update_viewer: player->cell == 0 → set_viewer(player_pos, 1), viewer_cell = null
|
||||||
// (acclient_2013_pseudo_c.txt:92775). No cell to sweep against → snap to the player.
|
// (acclient_2013_pseudo_c.txt:92775). No cell to sweep against → snap to the player.
|
||||||
if (cellId == 0) return new CameraSweepResult(playerPos, 0u);
|
if (cellId == 0) return new CameraSweepResult(playerPos, 0u);
|
||||||
|
|
||||||
// === Start cell (pc:92824-92844) ===
|
// === Start cell (pc:92824-92844) ===
|
||||||
// Indoor (objcell_id >= 0x100): seat the sweep's start cell at the head-PIVOT via
|
// Indoor (objcell_id >= 0x100): seat the sweep's start cell at the head-PIVOT via
|
||||||
// CPhysicsObj::AdjustPosition (pc:92832) — the head can sit in a different cell than
|
// CPhysicsObj::AdjustPosition (pc:92832) — the head can sit in a different cell than
|
||||||
// the feet (the cellar lip: feet in the low connector, head up at floor level). On
|
// the feet (the cellar lip: feet in the low connector, head up at floor level). On
|
||||||
// failure retail falls back to player->cell. Outdoor: cell = player->cell (no AdjustPosition).
|
// failure retail falls back to player->cell. Outdoor: cell = player->cell (no AdjustPosition).
|
||||||
uint startCell = cellId;
|
uint startCell = cellId;
|
||||||
|
|
@ -39,10 +39,10 @@ public sealed class PhysicsCameraCollisionProbe : ICameraCollisionProbe
|
||||||
if (found) startCell = pivotCell;
|
if (found) startCell = pivotCell;
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Sweep the viewer_sphere pivot → sought-eye from the start cell (pc:92860-92868) ===
|
// === Sweep the viewer_sphere pivot → sought-eye from the start cell (pc:92860-92868) ===
|
||||||
// SpherePath.InitPath puts sphere0's center at pathPos + (0,0,radius) (the player
|
// SpherePath.InitPath puts sphere0's center at pathPos + (0,0,radius) (the player
|
||||||
// foot-capsule convention). Retail's viewer_sphere center is (0,0,0), so shift the
|
// foot-capsule convention). Retail's viewer_sphere center is (0,0,0), so shift the
|
||||||
// path DOWN by the radius to make the SPHERE CENTER travel pivot→eye, then add it back.
|
// path DOWN by the radius to make the SPHERE CENTER travel pivot→eye, then add it back.
|
||||||
Vector3 begin = ToSpherePath(pivot, ViewerSphereRadius);
|
Vector3 begin = ToSpherePath(pivot, ViewerSphereRadius);
|
||||||
Vector3 end = ToSpherePath(desiredEye, ViewerSphereRadius);
|
Vector3 end = ToSpherePath(desiredEye, ViewerSphereRadius);
|
||||||
|
|
||||||
|
|
@ -59,7 +59,7 @@ public sealed class PhysicsCameraCollisionProbe : ICameraCollisionProbe
|
||||||
// Retail SmartBox::update_viewer calls init_object(player, 0x5c) =
|
// Retail SmartBox::update_viewer calls init_object(player, 0x5c) =
|
||||||
// IsViewer | PathClipped | FreeRotate | PerfectClip (acclient
|
// IsViewer | PathClipped | FreeRotate | PerfectClip (acclient
|
||||||
// pseudo-C :92864; enum TransitionTypes.cs:24-33). PathClipped makes
|
// pseudo-C :92864; enum TransitionTypes.cs:24-33). PathClipped makes
|
||||||
// the sweep HARD-STOP at first contact (TransitionTypes.cs:811) — the
|
// the sweep HARD-STOP at first contact (TransitionTypes.cs:811) — the
|
||||||
// spring-arm pull-in, not the player's edge-slide. IsViewer lets the
|
// spring-arm pull-in, not the player's edge-slide. IsViewer lets the
|
||||||
// eye pass through creatures, colliding only with world geometry
|
// eye pass through creatures, colliding only with world geometry
|
||||||
// (CollisionExemption.cs:83-85). FreeRotate/PerfectClip are no-ops in
|
// (CollisionExemption.cs:83-85). FreeRotate/PerfectClip are no-ops in
|
||||||
|
|
@ -95,7 +95,7 @@ public sealed class PhysicsCameraCollisionProbe : ICameraCollisionProbe
|
||||||
|
|
||||||
// === Fallback 1 (pc:92878-92883): AdjustPosition at the sought eye ===
|
// === Fallback 1 (pc:92878-92883): AdjustPosition at the sought eye ===
|
||||||
// The sweep found no valid position; try to seat the eye at its own cell.
|
// The sweep found no valid position; try to seat the eye at its own cell.
|
||||||
// (Seed with the player cell — acdream's camera doesn't track the sought-eye's
|
// (Seed with the player cell — acdream's camera doesn't track the sought-eye's
|
||||||
// cell separately; the eye is near the player so its stab-list is the right one.)
|
// cell separately; the eye is near the player so its stab-list is the right one.)
|
||||||
var (eyeCell, eyeFound) = _physics.AdjustPosition(cellId, desiredEye);
|
var (eyeCell, eyeFound) = _physics.AdjustPosition(cellId, desiredEye);
|
||||||
if (eyeFound) return new CameraSweepResult(desiredEye, eyeCell);
|
if (eyeFound) return new CameraSweepResult(desiredEye, eyeCell);
|
||||||
|
|
@ -104,11 +104,11 @@ public sealed class PhysicsCameraCollisionProbe : ICameraCollisionProbe
|
||||||
return new CameraSweepResult(playerPos, 0u);
|
return new CameraSweepResult(playerPos, 0u);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Eye/pivot point → InitPath path point (subtract the sphere-center offset).</summary>
|
/// <summary>Eye/pivot point → InitPath path point (subtract the sphere-center offset).</summary>
|
||||||
internal static Vector3 ToSpherePath(Vector3 spherePoint, float radius)
|
internal static Vector3 ToSpherePath(Vector3 spherePoint, float radius)
|
||||||
=> spherePoint - new Vector3(0f, 0f, radius);
|
=> spherePoint - new Vector3(0f, 0f, radius);
|
||||||
|
|
||||||
/// <summary>InitPath path point → eye point (add the sphere-center offset back).</summary>
|
/// <summary>InitPath path point → eye point (add the sphere-center offset back).</summary>
|
||||||
internal static Vector3 FromSpherePath(Vector3 pathPoint, float radius)
|
internal static Vector3 FromSpherePath(Vector3 pathPoint, float radius)
|
||||||
=> pathPoint + new Vector3(0f, 0f, radius);
|
=> pathPoint + new Vector3(0f, 0f, radius);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AcDream.App.Rendering.Wb;
|
using AcDream.App.Rendering.Wb;
|
||||||
|
|
@ -8,15 +8,15 @@ namespace AcDream.App.Rendering;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// BR-2 (holistic building-render port): retail's invisible portal depth
|
/// BR-2 (holistic building-render port): retail's invisible portal depth
|
||||||
/// writes — the port of <c>D3DPolyRender::DrawPortalPolyInternal</c>
|
/// writes — the port of <c>D3DPolyRender::DrawPortalPolyInternal</c>
|
||||||
/// (Ghidra 0x0059bc90, pc:424490).
|
/// (Ghidra 0x0059bc90, pc:424490).
|
||||||
///
|
///
|
||||||
/// <para><b>Wired by T1 (BR-3, `579c8b0`):</b> seal on interior roots, punch
|
/// <para><b>Wired by T1 (BR-3, `579c8b0`):</b> seal on interior roots, punch
|
||||||
/// on outdoor / look-in roots, via <c>RetailPViewPassExecutor.DrawPortalDepthWrite</c>
|
/// on outdoor / look-in roots, via <c>RetailPViewPassExecutor.DrawPortalDepthWrite</c>
|
||||||
/// (the <c>DrawExitPortalMasks</c> slice callback) — safe alongside the
|
/// (the <c>DrawExitPortalMasks</c> slice callback) — safe alongside the
|
||||||
/// dynamics-drawn-LAST frame order (the first BR-2 attempt punched after
|
/// dynamics-drawn-LAST frame order (the first BR-2 attempt punched after
|
||||||
/// dynamics and erased the player; reverted 88be519). #117 (2026-06-11)
|
/// dynamics and erased the player; reverted 88be519). #117 (2026-06-11)
|
||||||
/// added the two-pass stencil depth gate on the punch side — see
|
/// added the two-pass stencil depth gate on the punch side — see
|
||||||
/// <see cref="DrawDepthFan"/>.</para>
|
/// <see cref="DrawDepthFan"/>.</para>
|
||||||
///
|
///
|
||||||
/// <para>Retail projects a portal polygon, software-clips it against the
|
/// <para>Retail projects a portal polygon, software-clips it against the
|
||||||
|
|
@ -25,12 +25,12 @@ namespace AcDream.App.Rendering;
|
||||||
/// <list type="bullet">
|
/// <list type="bullet">
|
||||||
/// <item><b>Seal</b> (retail <c>maxZ2=6</c>, bit0 clear, data 0x00820e14):
|
/// <item><b>Seal</b> (retail <c>maxZ2=6</c>, bit0 clear, data 0x00820e14):
|
||||||
/// z = the polygon's true projected depth. Drawn on portals leading OUTSIDE
|
/// z = the polygon's true projected depth. Drawn on portals leading OUTSIDE
|
||||||
/// (<c>other_cell_id==0xFFFF</c>) after the landscape pass — terrain seen
|
/// (<c>other_cell_id==0xFFFF</c>) after the landscape pass — terrain seen
|
||||||
/// through a doorway keeps its pixels because farther interior geometry
|
/// through a doorway keeps its pixels because farther interior geometry
|
||||||
/// z-fails inside the aperture (PView::DrawCells loop 1, Ghidra 0x005a4840,
|
/// z-fails inside the aperture (PView::DrawCells loop 1, Ghidra 0x005a4840,
|
||||||
/// pc:432783-432786).</item>
|
/// pc:432783-432786).</item>
|
||||||
/// <item><b>Punch</b> (retail <c>maxZ1=7</c>, bit0 set, data 0x00820e18):
|
/// <item><b>Punch</b> (retail <c>maxZ1=7</c>, bit0 set, data 0x00820e18):
|
||||||
/// z forced to the far plane (0.99999988) — erases depth inside a building
|
/// z forced to the far plane (0.99999988) — erases depth inside a building
|
||||||
/// aperture so the interior cells drawn next land cleanly
|
/// aperture so the interior cells drawn next land cleanly
|
||||||
/// (ConstructView(CBldPortal) mode-1, pc:433827). BR-2 commit 2 wires this
|
/// (ConstructView(CBldPortal) mode-1, pc:433827). BR-2 commit 2 wires this
|
||||||
/// side.</item>
|
/// side.</item>
|
||||||
|
|
@ -38,7 +38,7 @@ namespace AcDream.App.Rendering;
|
||||||
///
|
///
|
||||||
/// <para>Where retail clips the polygon on the CPU against the view, we apply
|
/// <para>Where retail clips the polygon on the CPU against the view, we apply
|
||||||
/// the SAME view region via <c>gl_ClipDistance</c> from the slice's clip-space
|
/// the SAME view region via <c>gl_ClipDistance</c> from the slice's clip-space
|
||||||
/// half-planes (≤8, the validated <see cref="ClipPlaneSet"/> output) — the
|
/// half-planes (≤8, the validated <see cref="ClipPlaneSet"/> output) — the
|
||||||
/// depth write lands only inside the slice region, matching retail's clipped
|
/// depth write lands only inside the slice region, matching retail's clipped
|
||||||
/// fan.</para>
|
/// fan.</para>
|
||||||
///
|
///
|
||||||
|
|
@ -46,7 +46,7 @@ namespace AcDream.App.Rendering;
|
||||||
/// sets everything it depends on, restores the frame-global convention on
|
/// sets everything it depends on, restores the frame-global convention on
|
||||||
/// exit, no early-outs between set and restore.</para>
|
/// exit, no early-outs between set and restore.</para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class PortalDepthMaskRenderer : IDisposable
|
internal sealed class PortalDepthMaskRenderer : IDisposable
|
||||||
{
|
{
|
||||||
private const string VertSrc = @"#version 430 core
|
private const string VertSrc = @"#version 430 core
|
||||||
layout(location = 0) in vec3 aPos;
|
layout(location = 0) in vec3 aPos;
|
||||||
|
|
@ -215,28 +215,28 @@ void main() { } // depth-only: color writes are masked off by the caller state
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// #117 (2026-06-11): the mark-pass depth bias, in NDC, toward the
|
/// #117 (2026-06-11): the mark-pass depth bias, in NDC, toward the
|
||||||
/// viewer. Retail's punch is DEPTHTEST_ALWAYS and is safe only because
|
/// viewer. Retail's punch is DEPTHTEST_ALWAYS and is safe only because
|
||||||
/// retail's outdoor pass is painter's-ordered far→near (anything nearer
|
/// retail's outdoor pass is painter's-ordered far→near (anything nearer
|
||||||
/// redraws AFTER the punch and re-covers it). Our z-buffered MDI frame
|
/// redraws AFTER the punch and re-covers it). Our z-buffered MDI frame
|
||||||
/// has no such order, so an unconditional far-Z punch erased the depth
|
/// has no such order, so an unconditional far-Z punch erased the depth
|
||||||
/// of NEARER occluders (terrain hills, closer buildings) at aperture
|
/// of NEARER occluders (terrain hills, closer buildings) at aperture
|
||||||
/// pixels — doors/interiors painted through them (the T5 #117 report).
|
/// pixels — doors/interiors painted through them (the T5 #117 report).
|
||||||
/// The z-buffer-correct equivalent: punch ONLY where the aperture
|
/// The z-buffer-correct equivalent: punch ONLY where the aperture
|
||||||
/// polygon itself wins a depth test at its true depth (two-pass
|
/// polygon itself wins a depth test at its true depth (two-pass
|
||||||
/// stencil below). The bias keeps the #108 case covered — terrain
|
/// stencil below). The bias keeps the #108 case covered — terrain
|
||||||
/// hugging the door plane (centimeters in front of the aperture) must
|
/// hugging the door plane (centimeters in front of the aperture) must
|
||||||
/// still be punched; a hill or another house meters nearer must not.
|
/// still be punched; a hill or another house meters nearer must not.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private const float PunchMarkDepthBias = 0.0005f;
|
private const float PunchMarkDepthBias = 0.0005f;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// #129 (2026-06-12): NDC depth is non-linear — a constant NDC bias b
|
/// #129 (2026-06-12): NDC depth is non-linear — a constant NDC bias b
|
||||||
/// spans ≈ b·d²/near meters of eye depth at eye distance d. With
|
/// spans ≈ b·d²/near meters of eye depth at eye distance d. With
|
||||||
/// znear = 0.1, the 0.0005 constant alone spanned 0.125 m at 5 m but
|
/// znear = 0.1, the 0.0005 constant alone spanned 0.125 m at 5 m but
|
||||||
/// ~190 m at a landblock away: every hill/house in front of a distant
|
/// ~190 m at a landblock away: every hill/house in front of a distant
|
||||||
/// aperture passed the mark and got far-Z punched — door-shaped leaks
|
/// aperture passed the mark and got far-Z punched — door-shaped leaks
|
||||||
/// through occluders. Fix: cap the bias's EYE-SPACE span at
|
/// through occluders. Fix: cap the bias's EYE-SPACE span at
|
||||||
/// <see cref="PunchMarkBiasEyeCapMeters"/>. Below the ~10 m crossover
|
/// <see cref="PunchMarkBiasEyeCapMeters"/>. Below the ~10 m crossover
|
||||||
/// (sqrt(cap·near/0.0005)) the constant-NDC term is smaller and wins —
|
/// (sqrt(cap·near/0.0005)) the constant-NDC term is smaller and wins —
|
||||||
/// bit-identical to the T5-validated close-range behavior (#108 grass
|
/// bit-identical to the T5-validated close-range behavior (#108 grass
|
||||||
/// coverage untouched); beyond it the punch can never reach an occluder
|
/// coverage untouched); beyond it the punch can never reach an occluder
|
||||||
/// more than the cap in front of the aperture plane.
|
/// more than the cap in front of the aperture plane.
|
||||||
|
|
@ -245,7 +245,7 @@ void main() { } // depth-only: color writes are masked off by the caller state
|
||||||
|
|
||||||
/// <summary>Retail <c>Render::znear</c> = 0.1 (decomp :342173, re-landed
|
/// <summary>Retail <c>Render::znear</c> = 0.1 (decomp :342173, re-landed
|
||||||
/// d4b5c71). The cap conversion below assumes the production camera near
|
/// d4b5c71). The cap conversion below assumes the production camera near
|
||||||
/// plane; the small f/(f−n) factor (~1.00002 at far 5000) is ignored.</summary>
|
/// plane; the small f/(f−n) factor (~1.00002 at far 5000) is ignored.</summary>
|
||||||
public const float CameraNearPlaneMeters = 0.1f;
|
public const float CameraNearPlaneMeters = 0.1f;
|
||||||
|
|
||||||
/// <summary>CPU mirror of the vertex-shader mark-bias expression (keep in
|
/// <summary>CPU mirror of the vertex-shader mark-bias expression (keep in
|
||||||
|
|
@ -261,13 +261,13 @@ void main() { } // depth-only: color writes are masked off by the caller state
|
||||||
/// slice's clip-space half-planes. <paramref name="forceFarZ"/> selects
|
/// slice's clip-space half-planes. <paramref name="forceFarZ"/> selects
|
||||||
/// punch (true, retail maxZ1) vs seal (false, retail maxZ2 true depth).
|
/// punch (true, retail maxZ1) vs seal (false, retail maxZ2 true depth).
|
||||||
///
|
///
|
||||||
/// <para><b>Seal</b> (interior root): one pass, retail-verbatim —
|
/// <para><b>Seal</b> (interior root): one pass, retail-verbatim —
|
||||||
/// depth ALWAYS + true projected depth. It runs immediately after the
|
/// depth ALWAYS + true projected depth. It runs immediately after the
|
||||||
/// gated full depth clear, so there is no nearer content to stomp.</para>
|
/// gated full depth clear, so there is no nearer content to stomp.</para>
|
||||||
///
|
///
|
||||||
/// <para><b>Punch</b> (outdoor root / look-in): two passes (#117).
|
/// <para><b>Punch</b> (outdoor root / look-in): two passes (#117).
|
||||||
/// Pass A marks stencil where the aperture fan passes a LEQUAL depth
|
/// Pass A marks stencil where the aperture fan passes a LEQUAL depth
|
||||||
/// test at its (biased) true depth — i.e. where the aperture is
|
/// test at its (biased) true depth — i.e. where the aperture is
|
||||||
/// actually visible against everything drawn so far. Pass B writes the
|
/// actually visible against everything drawn so far. Pass B writes the
|
||||||
/// far-Z punch with depth ALWAYS but stencil-gated to the marked
|
/// far-Z punch with depth ALWAYS but stencil-gated to the marked
|
||||||
/// pixels, and zeroes the stencil as it goes (self-cleaning). This is
|
/// pixels, and zeroes the stencil as it goes (self-cleaning). This is
|
||||||
|
|
@ -301,7 +301,7 @@ void main() { } // depth-only: color writes are masked off by the caller state
|
||||||
_gl.Disable(EnableCap.CullFace); // portal fans face either way
|
_gl.Disable(EnableCap.CullFace); // portal fans face either way
|
||||||
_gl.Disable(EnableCap.ScissorTest);
|
_gl.Disable(EnableCap.ScissorTest);
|
||||||
_gl.Enable(EnableCap.DepthTest);
|
_gl.Enable(EnableCap.DepthTest);
|
||||||
_gl.ColorMask(false, false, false, false); // alpha-0 fan ≙ no color
|
_gl.ColorMask(false, false, false, false); // alpha-0 fan ≙ no color
|
||||||
for (int i = 0; i < planeCount; i++)
|
for (int i = 0; i < planeCount; i++)
|
||||||
_gl.Enable(EnableCap.ClipDistance0 + i);
|
_gl.Enable(EnableCap.ClipDistance0 + i);
|
||||||
|
|
||||||
|
|
@ -351,7 +351,7 @@ void main() { } // depth-only: color writes are masked off by the caller state
|
||||||
|
|
||||||
if (!forceFarZ)
|
if (!forceFarZ)
|
||||||
{
|
{
|
||||||
// ── SEAL: retail-verbatim single pass ──
|
// ── SEAL: retail-verbatim single pass ──
|
||||||
_gl.DepthFunc(DepthFunction.Always);
|
_gl.DepthFunc(DepthFunction.Always);
|
||||||
_gl.DepthMask(true);
|
_gl.DepthMask(true);
|
||||||
_gl.Uniform1(_locForceFarZ, 0);
|
_gl.Uniform1(_locForceFarZ, 0);
|
||||||
|
|
@ -360,7 +360,7 @@ void main() { } // depth-only: color writes are masked off by the caller state
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// ── PUNCH pass A: stencil-mark visible aperture pixels ──
|
// ── PUNCH pass A: stencil-mark visible aperture pixels ──
|
||||||
_gl.Enable(EnableCap.StencilTest);
|
_gl.Enable(EnableCap.StencilTest);
|
||||||
_gl.StencilFunc(StencilFunction.Always, 1, 0xFF);
|
_gl.StencilFunc(StencilFunction.Always, 1, 0xFF);
|
||||||
_gl.StencilOp(StencilOp.Keep, StencilOp.Keep, StencilOp.Replace);
|
_gl.StencilOp(StencilOp.Keep, StencilOp.Keep, StencilOp.Replace);
|
||||||
|
|
@ -373,8 +373,8 @@ void main() { } // depth-only: color writes are masked off by the caller state
|
||||||
PunchMarkBiasEyeCapMeters * CameraNearPlaneMeters);
|
PunchMarkBiasEyeCapMeters * CameraNearPlaneMeters);
|
||||||
_gl.DrawArrays(PrimitiveType.TriangleFan, firstVertex, (uint)n);
|
_gl.DrawArrays(PrimitiveType.TriangleFan, firstVertex, (uint)n);
|
||||||
|
|
||||||
// ── PUNCH pass B: far-Z write on marked pixels only;
|
// ── PUNCH pass B: far-Z write on marked pixels only;
|
||||||
// zero the stencil as we go (self-cleaning) ──
|
// zero the stencil as we go (self-cleaning) ──
|
||||||
_gl.StencilFunc(StencilFunction.Equal, 1, 0xFF);
|
_gl.StencilFunc(StencilFunction.Equal, 1, 0xFF);
|
||||||
_gl.StencilOp(StencilOp.Keep, StencilOp.Keep, StencilOp.Zero);
|
_gl.StencilOp(StencilOp.Keep, StencilOp.Keep, StencilOp.Zero);
|
||||||
_gl.DepthFunc(DepthFunction.Always);
|
_gl.DepthFunc(DepthFunction.Always);
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
// PortalProjection.cs
|
// PortalProjection.cs
|
||||||
//
|
//
|
||||||
// Phase A8.F: project a cell-local portal polygon to NDC screen space. Homogeneous frustum clip
|
// Phase A8.F: project a cell-local portal polygon to NDC screen space. Homogeneous frustum clip
|
||||||
// in CLIP SPACE (before the perspective divide): first the IN-FRONT-OF-EYE half-space (keep where
|
// in CLIP SPACE (before the perspective divide): first the IN-FRONT-OF-EYE half-space (keep where
|
||||||
// w > MinW) so a portal straddling the camera does not invert under the divide and the divide
|
// w > MinW) so a portal straddling the camera does not invert under the divide and the divide
|
||||||
// stays bounded away from the w=0 eye singularity, then the 4 SIDE planes (x,y within ±w) so every
|
// stays bounded away from the w=0 eye singularity, then the 4 SIDE planes (x,y within ±w) so every
|
||||||
// surviving vertex lands on the screen [-1,1] by construction. The side-plane clip is the R1
|
// surviving vertex lands on the screen [-1,1] by construction. The side-plane clip is the R1
|
||||||
// void-flap fix (2026-06-05) — see ProjectToNdc.
|
// void-flap fix (2026-06-05) — see ProjectToNdc.
|
||||||
//
|
//
|
||||||
// The clip is NEAR-INDEPENDENT on purpose. We only use the projected x/y for the visibility clip
|
// The clip is NEAR-INDEPENDENT on purpose. We only use the projected x/y for the visibility clip
|
||||||
// REGION, so a vertex in front of the eye is meaningful even if it is closer than the projection's
|
// REGION, so a vertex in front of the eye is meaningful even if it is closer than the projection's
|
||||||
|
|
@ -23,7 +23,7 @@ using System.Numerics;
|
||||||
|
|
||||||
namespace AcDream.App.Rendering;
|
namespace AcDream.App.Rendering;
|
||||||
|
|
||||||
public static class PortalProjection
|
internal static class PortalProjection
|
||||||
{
|
{
|
||||||
internal ref struct ClipPolygonLease
|
internal ref struct ClipPolygonLease
|
||||||
{
|
{
|
||||||
|
|
@ -118,17 +118,17 @@ public static class PortalProjection
|
||||||
Vector4[]? second = null;
|
Vector4[]? second = null;
|
||||||
|
|
||||||
// Homogeneous frustum clip in CLIP SPACE, before the perspective divide. First the
|
// Homogeneous frustum clip in CLIP SPACE, before the perspective divide. First the
|
||||||
// in-front-of-eye half-space (w > MinW) — near-INDEPENDENT, so a portal the camera is
|
// in-front-of-eye half-space (w > MinW) — near-INDEPENDENT, so a portal the camera is
|
||||||
// standing in still projects (see header); then the 4 SIDE planes (x,y within ±w). The
|
// standing in still projects (see header); then the 4 SIDE planes (x,y within ±w). The
|
||||||
// side clip is the R1 void-flap fix (2026-06-05): without it, a portal WITHIN the near
|
// side clip is the R1 void-flap fix (2026-06-05): without it, a portal WITHIN the near
|
||||||
// plane projected small-w verts to wildly off-screen NDC (the probe saw (10.2,-67.4)),
|
// plane projected small-w verts to wildly off-screen NDC (the probe saw (10.2,-67.4)),
|
||||||
// which corrupted the downstream 2D ScreenPolygonClip into an EMPTY region -> OutsideView
|
// which corrupted the downstream 2D ScreenPolygonClip into an EMPTY region -> OutsideView
|
||||||
// empty -> terrain Skip -> the bluish doorway "void". Clipping the side planes here bounds
|
// empty -> terrain Skip -> the bluish doorway "void". Clipping the side planes here bounds
|
||||||
// every surviving vertex to the screen [-1,1] by construction, so a screen-covering doorway
|
// every surviving vertex to the screen [-1,1] by construction, so a screen-covering doorway
|
||||||
// clips to the screen (non-empty) instead of collapsing. The eye plane is clipped FIRST so
|
// clips to the screen (non-empty) instead of collapsing. The eye plane is clipped FIRST so
|
||||||
// all survivors have w > 0, making the side-plane functionals (w ± x, w ± y) well defined.
|
// all survivors have w > 0, making the side-plane functionals (w ± x, w ± y) well defined.
|
||||||
// Near/far are intentionally NOT clipped (near-independence). Retail PView::GetClip
|
// Near/far are intentionally NOT clipped (near-independence). Retail PView::GetClip
|
||||||
// (decomp:0x005a4320) projects + frustum-clips the portal poly likewise (research doc A §3.5).
|
// (decomp:0x005a4320) projects + frustum-clips the portal poly likewise (research doc A §3.5).
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
second = vectorPool.Rent(capacity);
|
second = vectorPool.Rent(capacity);
|
||||||
|
|
@ -155,7 +155,7 @@ public static class PortalProjection
|
||||||
(current, output) = (output, current);
|
(current, output) = (output, current);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perspective divide → NDC xy. This is the only result allocation.
|
// Perspective divide → NDC xy. This is the only result allocation.
|
||||||
var ndc = new Vector2[currentCount];
|
var ndc = new Vector2[currentCount];
|
||||||
for (int i = 0; i < currentCount; i++)
|
for (int i = 0; i < currentCount; i++)
|
||||||
{
|
{
|
||||||
|
|
@ -174,21 +174,21 @@ public static class PortalProjection
|
||||||
|
|
||||||
/// <summary>Faithful homogeneous projection (retail PrimD3DRender::xformStart + the W=0 clip of
|
/// <summary>Faithful homogeneous projection (retail PrimD3DRender::xformStart + the W=0 clip of
|
||||||
/// ACRender::polyClipFinish, decomp 424310 / 702749): transform the portal to clip space and clip
|
/// ACRender::polyClipFinish, decomp 424310 / 702749): transform the portal to clip space and clip
|
||||||
/// ONLY the eye plane (w >= 0, EXACT), keeping homogeneous coords — NO perspective divide, NO
|
/// ONLY the eye plane (w >= 0, EXACT), keeping homogeneous coords — NO perspective divide, NO
|
||||||
/// frustum side-plane clamp. The screen bound is applied later by <see cref="ClipToRegion"/>
|
/// frustum side-plane clamp. The screen bound is applied later by <see cref="ClipToRegion"/>
|
||||||
/// against the view region (the root region is the full screen), exactly as retail clips the portal
|
/// against the view region (the root region is the full screen), exactly as retail clips the portal
|
||||||
/// against the accumulated portal_view rather than fixed side planes.
|
/// against the accumulated portal_view rather than fixed side planes.
|
||||||
///
|
///
|
||||||
/// <para>The W=0 clip is exact on purpose (the knife-edge port, 2026-06-11; pseudocode at
|
/// <para>The W=0 clip is exact on purpose (the knife-edge port, 2026-06-11; pseudocode at
|
||||||
/// docs/research/2026-06-11-polyclipfinish-w0-clip-pseudocode.md): boundary intersections land
|
/// docs/research/2026-06-11-polyclipfinish-w0-clip-pseudocode.md): boundary intersections land
|
||||||
/// at w == 0 — homogeneous DIRECTIONS — so a portal the eye is crossing (stair openings, decks)
|
/// at w == 0 — homogeneous DIRECTIONS — so a portal the eye is crossing (stair openings, decks)
|
||||||
/// yields the correct UNBOUNDED half-region, which the bounded view-region clip then cuts to the
|
/// yields the correct UNBOUNDED half-region, which the bounded view-region clip then cuts to the
|
||||||
/// screen. The previous EyePlaneW = 1e-4 produced finite ~1e4-NDC boundary verts whose region
|
/// screen. The previous EyePlaneW = 1e-4 produced finite ~1e4-NDC boundary verts whose region
|
||||||
/// intersections sat at the dedup/merge degeneracy threshold — the climb-strobe class. A w=0
|
/// intersections sat at the dedup/merge degeneracy threshold — the climb-strobe class. A w=0
|
||||||
/// vertex can never survive ClipToRegion into its divide (a nonzero direction fails at least one
|
/// vertex can never survive ClipToRegion into its divide (a nonzero direction fails at least one
|
||||||
/// edge test of any BOUNDED convex region), so no divide-by-zero path exists; the measure-zero
|
/// edge test of any BOUNDED convex region), so no divide-by-zero path exists; the measure-zero
|
||||||
/// corner case is guarded in ClipToRegion. Matches polyClipFinish part 1: clip pass runs only
|
/// corner case is guarded in ClipToRegion. Matches polyClipFinish part 1: clip pass runs only
|
||||||
/// when some vertex has w < 0; <3 survivors → reject (empty).</para></summary>
|
/// when some vertex has w < 0; <3 survivors → reject (empty).</para></summary>
|
||||||
public static Vector4[] ProjectToClip(IReadOnlyList<Vector3> localPoly, Matrix4x4 cellToWorld, Matrix4x4 viewProj)
|
public static Vector4[] ProjectToClip(IReadOnlyList<Vector3> localPoly, Matrix4x4 cellToWorld, Matrix4x4 viewProj)
|
||||||
{
|
{
|
||||||
using ClipPolygonLease lease = ProjectToClipLease(localPoly, cellToWorld, viewProj);
|
using ClipPolygonLease lease = ProjectToClipLease(localPoly, cellToWorld, viewProj);
|
||||||
|
|
@ -274,7 +274,7 @@ public static class PortalProjection
|
||||||
/// (CCW convex) with w-aware Sutherland-Hodgman edge tests, then divide the survivors to NDC and
|
/// (CCW convex) with w-aware Sutherland-Hodgman edge tests, then divide the survivors to NDC and
|
||||||
/// normalize to CCW. Ports retail ACRender::polyClipFinish's view-region clip (decomp 702749): the
|
/// normalize to CCW. Ports retail ACRender::polyClipFinish's view-region clip (decomp 702749): the
|
||||||
/// edge test multiplies through w (which is > 0 after the eye-plane clip) so it never divides a
|
/// edge test multiplies through w (which is > 0 after the eye-plane clip) so it never divides a
|
||||||
/// near-eye vertex, and the final divide runs only on survivors already bounded to the region —
|
/// near-eye vertex, and the final divide runs only on survivors already bounded to the region —
|
||||||
/// stable by construction. Returns <3 verts when the portal does not intersect the region.</summary>
|
/// stable by construction. Returns <3 verts when the portal does not intersect the region.</summary>
|
||||||
public static Vector2[] ClipToRegion(IReadOnlyList<Vector4> subjectClip, IReadOnlyList<Vector2> regionCcwNdc)
|
public static Vector2[] ClipToRegion(IReadOnlyList<Vector4> subjectClip, IReadOnlyList<Vector2> regionCcwNdc)
|
||||||
{
|
{
|
||||||
|
|
@ -339,7 +339,7 @@ public static class PortalProjection
|
||||||
|
|
||||||
// Homogeneous Sutherland-Hodgman: clip the (w > 0) subject against each CCW edge of the NDC
|
// Homogeneous Sutherland-Hodgman: clip the (w > 0) subject against each CCW edge of the NDC
|
||||||
// region. f(P) below is the NDC inside test cross(edge, P_ndc - a) multiplied through P.W,
|
// region. f(P) below is the NDC inside test cross(edge, P_ndc - a) multiplied through P.W,
|
||||||
// which is > 0 after the eye-plane clip — so the sign is the NDC sign yet no near-eye vertex
|
// which is > 0 after the eye-plane clip — so the sign is the NDC sign yet no near-eye vertex
|
||||||
// is ever divided (retail polyClipFinish, decomp 702749).
|
// is ever divided (retail polyClipFinish, decomp 702749).
|
||||||
int regionCount = regionCcwNdc.Count;
|
int regionCount = regionCcwNdc.Count;
|
||||||
int capacity = checked(subjectClip.Length + regionCount);
|
int capacity = checked(subjectClip.Length + regionCount);
|
||||||
|
|
@ -370,7 +370,7 @@ public static class PortalProjection
|
||||||
if (currentCount < 3)
|
if (currentCount < 3)
|
||||||
return System.Array.Empty<Vector2>();
|
return System.Array.Empty<Vector2>();
|
||||||
|
|
||||||
// Divide survivors → NDC. They are already inside the bounded region. A w=0
|
// Divide survivors → NDC. They are already inside the bounded region. A w=0
|
||||||
// measure-zero corner remains the same empty knife-edge result as the prior path.
|
// measure-zero corner remains the same empty knife-edge result as the prior path.
|
||||||
ndcScratch = vector2Pool.Rent(currentCount);
|
ndcScratch = vector2Pool.Rent(currentCount);
|
||||||
Span<Vector2> ndc = ndcScratch.AsSpan(0, currentCount);
|
Span<Vector2> ndc = ndcScratch.AsSpan(0, currentCount);
|
||||||
|
|
@ -408,8 +408,8 @@ public static class PortalProjection
|
||||||
// Retail copy_view's ~1-pixel vertex merge (see ClipToRegion). Collapses
|
// Retail copy_view's ~1-pixel vertex merge (see ClipToRegion). Collapses
|
||||||
// runs of consecutive near-identical vertices, including across the
|
// runs of consecutive near-identical vertices, including across the
|
||||||
// wrap-around. A polygon that collapses below 3 distinct vertices is
|
// wrap-around. A polygon that collapses below 3 distinct vertices is
|
||||||
// degenerate (sub-pixel sliver) and returns empty — exactly retail's
|
// degenerate (sub-pixel sliver) and returns empty — exactly retail's
|
||||||
// "<3 surviving verts → output count 0".
|
// "<3 surviving verts → output count 0".
|
||||||
private const float VertexMergeEpsilonNdc = 2f / 1080f;
|
private const float VertexMergeEpsilonNdc = 2f / 1080f;
|
||||||
|
|
||||||
private static int MergeSubPixelVertices(Span<Vector2> poly)
|
private static int MergeSubPixelVertices(Span<Vector2> poly)
|
||||||
|
|
@ -428,7 +428,7 @@ public static class PortalProjection
|
||||||
}
|
}
|
||||||
poly[kept++] = vertex;
|
poly[kept++] = vertex;
|
||||||
}
|
}
|
||||||
// Wrap-around: last ≈ first.
|
// Wrap-around: last ≈ first.
|
||||||
while (kept >= 2)
|
while (kept >= 2)
|
||||||
{
|
{
|
||||||
Vector2 first = poly[0];
|
Vector2 first = poly[0];
|
||||||
|
|
@ -442,9 +442,9 @@ public static class PortalProjection
|
||||||
return kept;
|
return kept;
|
||||||
}
|
}
|
||||||
|
|
||||||
// One Sutherland-Hodgman half-plane against the directed NDC edge a→b, keeping the CCW-inside
|
// One Sutherland-Hodgman half-plane against the directed NDC edge a→b, keeping the CCW-inside
|
||||||
// (left) part of a HOMOGENEOUS polygon. Inside test for vertex P (clip space): the NDC cross
|
// (left) part of a HOMOGENEOUS polygon. Inside test for vertex P (clip space): the NDC cross
|
||||||
// product cross(b-a, P/P.W - a) scaled by P.W (> 0): ex·(P.Y - P.W·a.Y) - ey·(P.X - P.W·a.X) ≥ 0.
|
// product cross(b-a, P/P.W - a) scaled by P.W (> 0): ex·(P.Y - P.W·a.Y) - ey·(P.X - P.W·a.X) ≥ 0.
|
||||||
// Crossings interpolate in homogeneous coords (perspective-correct), via the shared Lerp.
|
// Crossings interpolate in homogeneous coords (perspective-correct), via the shared Lerp.
|
||||||
private static int ClipHomogeneousEdge(
|
private static int ClipHomogeneousEdge(
|
||||||
ReadOnlySpan<Vector4> polygon,
|
ReadOnlySpan<Vector4> polygon,
|
||||||
|
|
@ -489,7 +489,7 @@ public static class PortalProjection
|
||||||
if (area2 < 0f) System.Array.Reverse(poly);
|
if (area2 < 0f) System.Array.Reverse(poly);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Minimum clip-space w (≈ metres in front of the eye) to keep a vertex. Excludes the eye
|
// Minimum clip-space w (≈ metres in front of the eye) to keep a vertex. Excludes the eye
|
||||||
// (w=0) singularity and the ~5 cm right at it (bounding the perspective divide), but is
|
// (w=0) singularity and the ~5 cm right at it (bounding the perspective divide), but is
|
||||||
// INTENTIONALLY far closer than the projection's 1.0 m near plane so a doorway the camera is
|
// INTENTIONALLY far closer than the projection's 1.0 m near plane so a doorway the camera is
|
||||||
// standing in still projects and the cell behind it stays visible. See the file header.
|
// standing in still projects and the cell behind it stays visible. See the file header.
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
|
||||||
namespace AcDream.App.Rendering;
|
namespace AcDream.App.Rendering;
|
||||||
|
|
||||||
|
|
@ -8,7 +8,7 @@ namespace AcDream.App.Rendering;
|
||||||
/// (<c>0x00453760</c>). Identity looks along AC +Y with +Z up; the animated
|
/// (<c>0x00453760</c>). Identity looks along AC +Y with +Z up; the animated
|
||||||
/// angle rolls that view around its own +Y forward axis.
|
/// angle rolls that view around its own +Y forward axis.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class PortalTunnelCamera : ICamera
|
internal sealed class PortalTunnelCamera : ICamera
|
||||||
{
|
{
|
||||||
public static readonly Vector3 RetailEye = new(0.24f, -2.7f, 0.88f);
|
public static readonly Vector3 RetailEye = new(0.24f, -2.7f, 0.88f);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AcDream.App.Rendering.Wb;
|
using AcDream.App.Rendering.Wb;
|
||||||
using AcDream.App.UI;
|
using AcDream.App.UI;
|
||||||
using AcDream.Content.Vfx;
|
using AcDream.Content.Vfx;
|
||||||
|
|
@ -22,7 +22,7 @@ namespace AcDream.App.Rendering;
|
||||||
/// 40 frames/second and drawn as a replacement 3-D viewport beneath the
|
/// 40 frames/second and drawn as a replacement 3-D viewport beneath the
|
||||||
/// retained gameplay UI.
|
/// retained gameplay UI.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class PortalTunnelPresentation : IDisposable
|
internal sealed class PortalTunnelPresentation : IDisposable
|
||||||
{
|
{
|
||||||
public const uint SetupClientEnum = 0x10000001u;
|
public const uint SetupClientEnum = 0x10000001u;
|
||||||
public const uint AnimationClientEnum = 0x10000002u;
|
public const uint AnimationClientEnum = 0x10000002u;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
// PortalView.cs
|
// PortalView.cs
|
||||||
//
|
//
|
||||||
// Phase A8.F: GL-free 2D screen-space (NDC) clip-region data model.
|
// Phase A8.F: GL-free 2D screen-space (NDC) clip-region data model.
|
||||||
// Mirrors retail view_poly (acclient.h:32465) and view_type (acclient.h:32338):
|
// Mirrors retail view_poly (acclient.h:32465) and view_type (acclient.h:32338):
|
||||||
|
|
@ -10,7 +10,7 @@ using System.Numerics;
|
||||||
namespace AcDream.App.Rendering;
|
namespace AcDream.App.Rendering;
|
||||||
|
|
||||||
/// <summary>One convex polygon in NDC screen space (xy in [-1,1]), plus its bounding rect.</summary>
|
/// <summary>One convex polygon in NDC screen space (xy in [-1,1]), plus its bounding rect.</summary>
|
||||||
public readonly struct ViewPolygon
|
internal readonly struct ViewPolygon
|
||||||
{
|
{
|
||||||
public readonly Vector2[] Vertices;
|
public readonly Vector2[] Vertices;
|
||||||
public readonly float MinX, MinY, MaxX, MaxY;
|
public readonly float MinX, MinY, MaxX, MaxY;
|
||||||
|
|
@ -101,7 +101,7 @@ internal sealed class PortalPolygonVertexStore
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>A cell's accumulated clip region: a set of convex view polygons + the union bounding rect.</summary>
|
/// <summary>A cell's accumulated clip region: a set of convex view polygons + the union bounding rect.</summary>
|
||||||
public sealed class CellView
|
internal sealed class CellView
|
||||||
{
|
{
|
||||||
// ViewPolygon exposes its vertex array for the renderer, so this seed must
|
// ViewPolygon exposes its vertex array for the renderer, so this seed must
|
||||||
// be owned by the CellView rather than shared globally. Pooling the
|
// be owned by the CellView rather than shared globally. Pooling the
|
||||||
|
|
@ -170,7 +170,7 @@ public sealed class CellView
|
||||||
MaxY = float.MinValue;
|
MaxY = float.MinValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>A region covering the entire NDC viewport — the camera cell's seed region
|
/// <summary>A region covering the entire NDC viewport — the camera cell's seed region
|
||||||
/// (mirrors retail PView::DrawInside copy_view(..., 4) at decomp:433814).</summary>
|
/// (mirrors retail PView::DrawInside copy_view(..., 4) at decomp:433814).</summary>
|
||||||
public static CellView FullScreen()
|
public static CellView FullScreen()
|
||||||
{
|
{
|
||||||
|
|
@ -192,7 +192,7 @@ public sealed class CellView
|
||||||
// Drift-tolerant, rotation-invariant dedup (2026-06-06 hang fix). PortalVisibilityBuilder.Build
|
// Drift-tolerant, rotation-invariant dedup (2026-06-06 hang fix). PortalVisibilityBuilder.Build
|
||||||
// re-queues a cell every time its CellView GROWS, so the flood only terminates when Add
|
// re-queues a cell every time its CellView GROWS, so the flood only terminates when Add
|
||||||
// recognises a re-clipped region as a duplicate. Across BFS rounds the SAME region returns
|
// recognises a re-clipped region as a duplicate. Across BFS rounds the SAME region returns
|
||||||
// float-drifted, vertex-rotated, and/or with a ±1 vertex count (homogeneous Sutherland-Hodgman +
|
// float-drifted, vertex-rotated, and/or with a ±1 vertex count (homogeneous Sutherland-Hodgman +
|
||||||
// EnsureCcw); the old exact index-by-index match (eps 1e-4) caught none of those, so the region
|
// EnsureCcw); the old exact index-by-index match (eps 1e-4) caught none of those, so the region
|
||||||
// grew without bound -> O(n^2) CPU-spin hang in this method. We instead key each polygon by its
|
// grew without bound -> O(n^2) CPU-spin hang in this method. We instead key each polygon by its
|
||||||
// vertices SNAPPED to a small NDC grid, consecutive snap-duplicates removed, rotated to a
|
// vertices SNAPPED to a small NDC grid, consecutive snap-duplicates removed, rotated to a
|
||||||
|
|
@ -206,14 +206,14 @@ public sealed class CellView
|
||||||
|
|
||||||
// #120 convergence (2026-06-11): reject a polygon CONTAINED in one already
|
// #120 convergence (2026-06-11): reject a polygon CONTAINED in one already
|
||||||
// stored. The reciprocal ping-pong (eye within PortalSideEpsilon of a
|
// stored. The reciprocal ping-pong (eye within PortalSideEpsilon of a
|
||||||
// portal plane → BOTH side tests pass → views lap A→B→A…) re-emits, each
|
// portal plane → BOTH side tests pass → views lap A→B→A…) re-emits, each
|
||||||
// lap, a region that is — in exact arithmetic — a SUBSET of the polygon
|
// lap, a region that is — in exact arithmetic — a SUBSET of the polygon
|
||||||
// that originated it; near-edge-on apertures make the re-clip wobble by
|
// that originated it; near-edge-on apertures make the re-clip wobble by
|
||||||
// more than the 1e-3 key grid, so every lap keyed as "new" and the
|
// more than the 1e-3 key grid, so every lap keyed as "new" and the
|
||||||
// in-place growth recursed to the depth-128 tripwire (chain dumps:
|
// in-place growth recursed to the depth-128 tripwire (chain dumps:
|
||||||
// 0xA9B4015C↔0x0162, 0xA9B30103↔0x010F; Issue120ReciprocalPingPongTests
|
// 0xA9B4015C↔0x0162, 0xA9B30103↔0x010F; Issue120ReciprocalPingPongTests
|
||||||
// reproduces deterministically). Containment rejection makes growth
|
// reproduces deterministically). Containment rejection makes growth
|
||||||
// strictly area-increasing — no new visible area, no propagation. The
|
// strictly area-increasing — no new visible area, no propagation. The
|
||||||
// key stays recorded so the exact emission also short-circuits later.
|
// key stays recorded so the exact emission also short-circuits later.
|
||||||
// Bonus: back-emission into a full-screen view (the root cell) is now
|
// Bonus: back-emission into a full-screen view (the root cell) is now
|
||||||
// always rejected outright.
|
// always rejected outright.
|
||||||
|
|
@ -228,7 +228,7 @@ public sealed class CellView
|
||||||
}
|
}
|
||||||
|
|
||||||
// #120: is polygon p entirely inside ONE stored polygon (with DedupGridNdc
|
// #120: is polygon p entirely inside ONE stored polygon (with DedupGridNdc
|
||||||
// slack)? Single-polygon containment is sufficient for the ping-pong class —
|
// slack)? Single-polygon containment is sufficient for the ping-pong class —
|
||||||
// a round-trip re-emission descends from exactly one originator. Stored
|
// a round-trip re-emission descends from exactly one originator. Stored
|
||||||
// polygons are convex (Sutherland-Hodgman / full-screen seed outputs); the
|
// polygons are convex (Sutherland-Hodgman / full-screen seed outputs); the
|
||||||
// edge test adapts to either winding via the polygon's signed area.
|
// edge test adapts to either winding via the polygon's signed area.
|
||||||
|
|
@ -252,7 +252,7 @@ public sealed class CellView
|
||||||
{
|
{
|
||||||
if (convex.Length < 3) return false;
|
if (convex.Length < 3) return false;
|
||||||
|
|
||||||
// signed area → winding (CCW positive); inside = left of every CCW edge.
|
// signed area → winding (CCW positive); inside = left of every CCW edge.
|
||||||
float area2 = 0f;
|
float area2 = 0f;
|
||||||
for (int i = 0; i < convex.Length; i++)
|
for (int i = 0; i < convex.Length; i++)
|
||||||
{
|
{
|
||||||
|
|
@ -268,10 +268,10 @@ public sealed class CellView
|
||||||
var b = convex[(i + 1) % convex.Length];
|
var b = convex[(i + 1) % convex.Length];
|
||||||
var ab = b - a;
|
var ab = b - a;
|
||||||
float len = ab.Length();
|
float len = ab.Length();
|
||||||
if (len < 1e-9f) continue; // degenerate edge — no constraint
|
if (len < 1e-9f) continue; // degenerate edge — no constraint
|
||||||
foreach (var pt in pts)
|
foreach (var pt in pts)
|
||||||
{
|
{
|
||||||
// signed perpendicular distance of pt from edge a→b (positive = inside for CCW)
|
// signed perpendicular distance of pt from edge a→b (positive = inside for CCW)
|
||||||
float cross = sign * (ab.X * (pt.Y - a.Y) - ab.Y * (pt.X - a.X));
|
float cross = sign * (ab.X * (pt.Y - a.Y) - ab.Y * (pt.X - a.X));
|
||||||
if (cross < -eps * len)
|
if (cross < -eps * len)
|
||||||
return false; // a vertex lies outside this edge by more than eps
|
return false; // a vertex lies outside this edge by more than eps
|
||||||
|
|
@ -280,7 +280,7 @@ public sealed class CellView
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// NDC dedup grid. 1e-3 is ~0.5 px at 1080p — finer than the gap between distinct portal openings
|
// NDC dedup grid. 1e-3 is ~0.5 px at 1080p — finer than the gap between distinct portal openings
|
||||||
// (so real regions stay distinct) yet far coarser than the per-round float drift of a re-clipped
|
// (so real regions stay distinct) yet far coarser than the per-round float drift of a re-clipped
|
||||||
// region (so a drifted duplicate snaps onto its predecessor). The finite grid is what bounds growth.
|
// region (so a drifted duplicate snaps onto its predecessor). The finite grid is what bounds growth.
|
||||||
private const float DedupGridNdc = 1e-3f;
|
private const float DedupGridNdc = 1e-3f;
|
||||||
|
|
@ -293,24 +293,24 @@ public sealed class CellView
|
||||||
//
|
//
|
||||||
// W=0 port (2026-06-11): an ALL-COLLINEAR polygon (zero area) keys as its snapped segment
|
// W=0 port (2026-06-11): an ALL-COLLINEAR polygon (zero area) keys as its snapped segment
|
||||||
// ("L:" + extreme points) instead of null. A portal whose plane contains the eye projects to
|
// ("L:" + extreme points) instead of null. A portal whose plane contains the eye projects to
|
||||||
// exactly this — and retail PROPAGATES it: PView::ClipPortals (decomp:433651-433711) forwards
|
// exactly this — and retail PROPAGATES it: PView::ClipPortals (decomp:433651-433711) forwards
|
||||||
// any GetClip output with count != 0 to copy_view/OtherPortalClip with no area gate anywhere,
|
// any GetClip output with count != 0 to copy_view/OtherPortalClip with no area gate anywhere,
|
||||||
// so the neighbour cell stays in the draw list (cells draw whole; onward floods die naturally
|
// so the neighbour cell stays in the draw list (cells draw whole; onward floods die naturally
|
||||||
// against the zero-area region). Rejecting these views dropped the whole chain behind an
|
// against the zero-area region). Rejecting these views dropped the whole chain behind an
|
||||||
// exactly-in-plane portal for the frame — the parked-eye knife-edge band (tower deck, spiral
|
// exactly-in-plane portal for the frame — the parked-eye knife-edge band (tower deck, spiral
|
||||||
// landings). The segment key space is finite like the area-key space, so dedup + the strict
|
// landings). The segment key space is finite like the area-key space, so dedup + the strict
|
||||||
// growth convergence invariant are unchanged. Degenerate is returned only when fewer than 2
|
// growth convergence invariant are unchanged. Degenerate is returned only when fewer than 2
|
||||||
// distinct snapped points survive (a true sub-grid point — not a real region OR segment).
|
// distinct snapped points survive (a true sub-grid point — not a real region OR segment).
|
||||||
//
|
//
|
||||||
// §4 corner/doorway fix (2026-06-10) — the collinear pass: the homogeneous region clipper
|
// §4 corner/doorway fix (2026-06-10) — the collinear pass: the homogeneous region clipper
|
||||||
// (PortalProjection.ClipToRegion, used by the forward AND — as of today — the reciprocal hop)
|
// (PortalProjection.ClipToRegion, used by the forward AND — as of today — the reciprocal hop)
|
||||||
// legitimately inserts intersection vertices ON a subject edge when a region edge grazes it, so
|
// legitimately inserts intersection vertices ON a subject edge when a region edge grazes it, so
|
||||||
// BFS re-clip rounds re-emit the SAME geometric region with 1-2 extra collinear edge vertices.
|
// BFS re-clip rounds re-emit the SAME geometric region with 1-2 extra collinear edge vertices.
|
||||||
// Without collinear canonicalization those re-emissions key as distinct, defeating the dedup and
|
// Without collinear canonicalization those re-emissions key as distinct, defeating the dedup and
|
||||||
// accumulating duplicate polygons (the pre-2026-06-06 unbounded-growth hang in miniature, and the
|
// accumulating duplicate polygons (the pre-2026-06-06 unbounded-growth hang in miniature, and the
|
||||||
// exact reason the reciprocal clip was previously parked on the unstable divide-first path).
|
// exact reason the reciprocal clip was previously parked on the unstable divide-first path).
|
||||||
// Dropping collinear snapped points makes the key purely a function of the region's CORNERS, so
|
// Dropping collinear snapped points makes the key purely a function of the region's CORNERS, so
|
||||||
// any re-emission of the same shape — drifted, rotated, vertex-count-inflated — deduplicates.
|
// any re-emission of the same shape — drifted, rotated, vertex-count-inflated — deduplicates.
|
||||||
private CanonicalKeyResult TryAddCanonicalKey(Vector2[]? verts)
|
private CanonicalKeyResult TryAddCanonicalKey(Vector2[]? verts)
|
||||||
{
|
{
|
||||||
if (verts is null || verts.Length < 3)
|
if (verts is null || verts.Length < 3)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
// PortalVisibilityBuilder.cs
|
// PortalVisibilityBuilder.cs
|
||||||
//
|
//
|
||||||
// Phase A8.F: recursive portal-clip visibility (the builder). Port of retail
|
// Phase A8.F: recursive portal-clip visibility (the builder). Port of retail
|
||||||
// PView::ConstructView (decomp:433750) -> ClipPortals (433572) -> AddViewToPortals
|
// PView::ConstructView (decomp:433750) -> ClipPortals (433572) -> AddViewToPortals
|
||||||
|
|
@ -12,7 +12,7 @@ using System.Numerics;
|
||||||
namespace AcDream.App.Rendering;
|
namespace AcDream.App.Rendering;
|
||||||
|
|
||||||
/// <summary>Per-frame output of the portal-frame BFS.</summary>
|
/// <summary>Per-frame output of the portal-frame BFS.</summary>
|
||||||
public sealed class PortalVisibilityFrame
|
internal sealed class PortalVisibilityFrame
|
||||||
{
|
{
|
||||||
private const int MaxRetainedCellViews = 512;
|
private const int MaxRetainedCellViews = 512;
|
||||||
internal const int MaxRetainedBuildCollectionCapacity = 512;
|
internal const int MaxRetainedBuildCollectionCapacity = 512;
|
||||||
|
|
@ -31,7 +31,7 @@ public sealed class PortalVisibilityFrame
|
||||||
internal int PolygonVertexAllocationCount => _polygonVertices.AllocationCount;
|
internal int PolygonVertexAllocationCount => _polygonVertices.AllocationCount;
|
||||||
internal int RetainedPolygonVertexArrayCount => _polygonVertices.RetainedArrayCount;
|
internal int RetainedPolygonVertexArrayCount => _polygonVertices.RetainedArrayCount;
|
||||||
|
|
||||||
/// <summary>Screen region (NDC) where outdoor terrain/scenery may draw — exit portals
|
/// <summary>Screen region (NDC) where outdoor terrain/scenery may draw — exit portals
|
||||||
/// recursively clipped to their portal chain. The cellar-flap fix.</summary>
|
/// recursively clipped to their portal chain. The cellar-flap fix.</summary>
|
||||||
public CellView OutsideView { get; private set; } = new();
|
public CellView OutsideView { get; private set; } = new();
|
||||||
|
|
||||||
|
|
@ -39,7 +39,7 @@ public sealed class PortalVisibilityFrame
|
||||||
public Dictionary<uint, CellView> CellViews { get; } = new();
|
public Dictionary<uint, CellView> CellViews { get; } = new();
|
||||||
|
|
||||||
/// <summary>Visible interior cells in the exact order they were first dequeued from the
|
/// <summary>Visible interior cells in the exact order they were first dequeued from the
|
||||||
/// distance-priority work list — closest-first (Phase U.2a). Mirrors retail's
|
/// distance-priority work list — closest-first (Phase U.2a). Mirrors retail's
|
||||||
/// PView::cell_draw_list, appended in PView::ConstructView (decomp:433783) as each cell pops
|
/// PView::cell_draw_list, appended in PView::ConstructView (decomp:433783) as each cell pops
|
||||||
/// off the nearest-vertex-sorted cell_todo_list (InsCellTodoList 433183). Deduplicated: a cell
|
/// off the nearest-vertex-sorted cell_todo_list (InsCellTodoList 433183). Deduplicated: a cell
|
||||||
/// appears exactly once, on its first dequeue. The camera cell is always first.</summary>
|
/// appears exactly once, on its first dequeue. The camera cell is always first.</summary>
|
||||||
|
|
@ -243,7 +243,7 @@ public sealed class PortalVisibilityFrame
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class PortalVisibilityBuilder
|
internal static class PortalVisibilityBuilder
|
||||||
{
|
{
|
||||||
// Side-classification epsilon. Retail's is F_EPSILON = 0.000199999995
|
// Side-classification epsilon. Retail's is F_EPSILON = 0.000199999995
|
||||||
// (const @0x007c8c70; PView::InitCell Ghidra 0x005a4b70). T2 (BR-4)
|
// (const @0x007c8c70; PView::InitCell Ghidra 0x005a4b70). T2 (BR-4)
|
||||||
|
|
@ -253,20 +253,20 @@ public static class PortalVisibilityBuilder
|
||||||
// side test never sees a stale root more than F_EPSILON past a plane. Our
|
// side test never sees a stale root more than F_EPSILON past a plane. Our
|
||||||
// root can lag the eye by up to ~1 cm at pressed corners (the harness's
|
// root can lag the eye by up to ~1 cm at pressed corners (the harness's
|
||||||
// fixed-root sweep models this), and 0.01 is that documented root-lag
|
// fixed-root sweep models this), and 0.01 is that documented root-lag
|
||||||
// tolerance — NOT a retail constant. Tighten to F_EPSILON only together
|
// tolerance — NOT a retail constant. Tighten to F_EPSILON only together
|
||||||
// with eye-exact viewer-cell tracking verification (the #108-membership
|
// with eye-exact viewer-cell tracking verification (the #108-membership
|
||||||
// family) + the cdstW near-clip pin.
|
// family) + the cdstW near-clip pin.
|
||||||
private const float PortalSideEpsilon = 0.01f;
|
private const float PortalSideEpsilon = 0.01f;
|
||||||
|
|
||||||
// Retail F_EPSILON proper — used where the semantic is knife-edge
|
// Retail F_EPSILON proper — used where the semantic is knife-edge
|
||||||
// REJECTION (ConstructView(CBldPortal) Sidedness IN_PLANE → return 0,
|
// REJECTION (ConstructView(CBldPortal) Sidedness IN_PLANE → return 0,
|
||||||
// Ghidra 0x005a59a0), which must NOT inherit the root-lag tolerance above
|
// Ghidra 0x005a59a0), which must NOT inherit the root-lag tolerance above
|
||||||
// (a 1 cm-wide in-plane band would reject look-in seeds whenever the eye
|
// (a 1 cm-wide in-plane band would reject look-in seeds whenever the eye
|
||||||
// stands near a doorway plane).
|
// stands near a doorway plane).
|
||||||
private const float SeedInPlaneEpsilon = 0.0002f;
|
private const float SeedInPlaneEpsilon = 0.0002f;
|
||||||
|
|
||||||
// TEMP diagnostic (Phase A8.F visual-gate triage; strip after): ACDREAM_A8_DUMP_PV=1 dumps the
|
// TEMP diagnostic (Phase A8.F visual-gate triage; strip after): ACDREAM_A8_DUMP_PV=1 dumps the
|
||||||
// local→NDC→clipped portal geometry for the first 2 Build calls per distinct camera cell.
|
// local→NDC→clipped portal geometry for the first 2 Build calls per distinct camera cell.
|
||||||
private static readonly bool s_pvDump =
|
private static readonly bool s_pvDump =
|
||||||
Environment.GetEnvironmentVariable("ACDREAM_A8_DUMP_PV") == "1";
|
Environment.GetEnvironmentVariable("ACDREAM_A8_DUMP_PV") == "1";
|
||||||
private static readonly Dictionary<uint, int> s_pvDumpCount = new();
|
private static readonly Dictionary<uint, int> s_pvDumpCount = new();
|
||||||
|
|
@ -275,14 +275,14 @@ public static class PortalVisibilityBuilder
|
||||||
/// #120 observable: total convergence-tripwire firings across both the
|
/// #120 observable: total convergence-tripwire firings across both the
|
||||||
/// interior <see cref="Build"/> and the exterior look-in propagation.
|
/// interior <see cref="Build"/> and the exterior look-in propagation.
|
||||||
/// The tripwire firing means the in-place growth's fixpoint invariant
|
/// The tripwire firing means the in-place growth's fixpoint invariant
|
||||||
/// broke (T2/BR-4) — tests reset this and assert it stays 0.
|
/// broke (T2/BR-4) — tests reset this and assert it stays 0.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static int ConvergenceTripwireCount;
|
public static int ConvergenceTripwireCount;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// #120 self-attribution dump: the growth-recursion path that exceeded
|
/// #120 self-attribution dump: the growth-recursion path that exceeded
|
||||||
/// the tripwire, as a per-cell frequency summary plus the chain tail —
|
/// the tripwire, as a per-cell frequency summary plus the chain tail —
|
||||||
/// the cycle's structure (e.g. 0174↔0175 ping-pong vs a 3-cycle lap)
|
/// the cycle's structure (e.g. 0174↔0175 ping-pong vs a 3-cycle lap)
|
||||||
/// reads directly off the output.
|
/// reads directly off the output.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static void DumpPropagationChain(uint[] chain, int depth, uint rootCellId, Vector3 eye)
|
private static void DumpPropagationChain(uint[] chain, int depth, uint rootCellId, Vector3 eye)
|
||||||
|
|
@ -309,11 +309,11 @@ public static class PortalVisibilityBuilder
|
||||||
|
|
||||||
/// <summary>The +Z world lift applied to DRAWN cell shells (z-fighting vs
|
/// <summary>The +Z world lift applied to DRAWN cell shells (z-fighting vs
|
||||||
/// terrain; applied in GameWindow's cell registration). The visibility
|
/// terrain; applied in GameWindow's cell registration). The visibility
|
||||||
/// graph stays in PHYSICS (unlifted) space — feeding the lift into portal
|
/// graph stays in PHYSICS (unlifted) space — feeding the lift into portal
|
||||||
/// planes broke horizontal-portal side tests (#119-residual, f35cb8b).
|
/// planes broke horizontal-portal side tests (#119-residual, f35cb8b).
|
||||||
/// Draw-space consumers of portal polygons (the OutsideView color gate
|
/// Draw-space consumers of portal polygons (the OutsideView color gate
|
||||||
/// here, the seal/punch depth fans in GameWindow) must apply this lift so
|
/// here, the seal/punch depth fans in GameWindow) must apply this lift so
|
||||||
/// they meet the drawn shell's aperture edge — the unlifted gate left a
|
/// they meet the drawn shell's aperture edge — the unlifted gate left a
|
||||||
/// 2 cm background strip under the drawn lintel (#130).</summary>
|
/// 2 cm background strip under the drawn lintel (#130).</summary>
|
||||||
public const float ShellDrawLiftZ = 0.02f;
|
public const float ShellDrawLiftZ = 0.02f;
|
||||||
|
|
||||||
|
|
@ -347,7 +347,7 @@ public static class PortalVisibilityBuilder
|
||||||
// Render unification (outdoor-as-cell, 2026-06-07): when the root IS the synthetic outdoor
|
// Render unification (outdoor-as-cell, 2026-06-07): when the root IS the synthetic outdoor
|
||||||
// node, the landscape is visible FULL-SCREEN, so seed OutsideView with the full-screen NDC
|
// node, the landscape is visible FULL-SCREEN, so seed OutsideView with the full-screen NDC
|
||||||
// quad. ClipFrameAssembler turns that into a full-screen OutsideView slice, so DrawInside's
|
// quad. ClipFrameAssembler turns that into a full-screen OutsideView slice, so DrawInside's
|
||||||
// DrawLandscapeThroughOutsideView draws terrain/sky/scenery/weather as the node's "shell" —
|
// DrawLandscapeThroughOutsideView draws terrain/sky/scenery/weather as the node's "shell" —
|
||||||
// the very same callback that already draws the doorway slice when an INTERIOR root reaches
|
// the very same callback that already draws the doorway slice when an INTERIOR root reaches
|
||||||
// outdoors. Keyed on the explicit IsOutdoorNode flag (set by OutdoorCellNode.Build), NOT a
|
// outdoors. Keyed on the explicit IsOutdoorNode flag (set by OutdoorCellNode.Build), NOT a
|
||||||
// cell-id heuristic: production EnvCell ids are >= 0x100 but test fixtures use low interior
|
// cell-id heuristic: production EnvCell ids are >= 0x100 but test fixtures use low interior
|
||||||
|
|
@ -357,7 +357,7 @@ public static class PortalVisibilityBuilder
|
||||||
frame.OutsideView.Add(frame.CopyPolygon(FullScreenQuad));
|
frame.OutsideView.Add(frame.CopyPolygon(FullScreenQuad));
|
||||||
|
|
||||||
// Distance-priority work list (retail PView::cell_todo_list). Cells pop closest-first;
|
// Distance-priority work list (retail PView::cell_todo_list). Cells pop closest-first;
|
||||||
// each cell carries the camera→nearest-portal-vertex distance that put it on the list
|
// each cell carries the camera→nearest-portal-vertex distance that put it on the list
|
||||||
// (retail keys on InitCell's per-portal min-vertex distance, decomp 432988-433004). The
|
// (retail keys on InitCell's per-portal min-vertex distance, decomp 432988-433004). The
|
||||||
// camera cell seeds at distance 0 (retail InsCellTodoList(this, arg2, 0f) at 433758) so it
|
// camera cell seeds at distance 0 (retail InsCellTodoList(this, arg2, 0f) at 433758) so it
|
||||||
// always pops first.
|
// always pops first.
|
||||||
|
|
@ -367,10 +367,10 @@ public static class PortalVisibilityBuilder
|
||||||
// Fixpoint termination replacing the old MaxReprocessPerCell hard cap. This mirrors the
|
// Fixpoint termination replacing the old MaxReprocessPerCell hard cap. This mirrors the
|
||||||
// retail portal_view slice offset 0x44 (last-incorporated view-poly watermark) vs 0x38
|
// retail portal_view slice offset 0x44 (last-incorporated view-poly watermark) vs 0x38
|
||||||
// (current view_count) decision in AddViewToPortals (433446): a cell is INSERTED into the
|
// (current view_count) decision in AddViewToPortals (433446): a cell is INSERTED into the
|
||||||
// todo list exactly once — on first discovery (retail's ecx_5==0 branch calls
|
// todo list exactly once — on first discovery (retail's ecx_5==0 branch calls
|
||||||
// InsCellTodoList; the ecx_5!=eax_2 growth branch calls AddToCell IN PLACE and never
|
// InsCellTodoList; the ecx_5!=eax_2 growth branch calls AddToCell IN PLACE and never
|
||||||
// re-enqueues). Later growth into an already-discovered cell is unioned into its CellView but
|
// re-enqueues). Later growth into an already-discovered cell is unioned into its CellView but
|
||||||
// does NOT re-enqueue it — the `cell_view_done` guarantee (ConstructView sets it at 433784
|
// does NOT re-enqueue it — the `cell_view_done` guarantee (ConstructView sets it at 433784
|
||||||
// the instant a cell is popped). Enqueue-once across the cell set is the hard termination
|
// the instant a cell is popped). Enqueue-once across the cell set is the hard termination
|
||||||
// guarantee for cyclic / hub / diamond graphs: at most N cells are ever processed. The
|
// guarantee for cyclic / hub / diamond graphs: at most N cells are ever processed. The
|
||||||
// camera cell is pre-marked so a portal looping back to it can never re-enqueue it.
|
// camera cell is pre-marked so a portal looping back to it can never re-enqueue it.
|
||||||
|
|
@ -398,11 +398,11 @@ public static class PortalVisibilityBuilder
|
||||||
{
|
{
|
||||||
Console.WriteLine($"[pv-dump] camCell=0x{cameraCell.CellId:X8} portals={cameraCell.Portals.Count} polyLists={cameraCell.PortalPolygons.Count} vp[M11={viewProj.M11:F3} M22={viewProj.M22:F3} M33={viewProj.M33:F3} M34={viewProj.M34:F3} M43={viewProj.M43:F3} M44={viewProj.M44:F3}]");
|
Console.WriteLine($"[pv-dump] camCell=0x{cameraCell.CellId:X8} portals={cameraCell.Portals.Count} polyLists={cameraCell.PortalPolygons.Count} vp[M11={viewProj.M11:F3} M22={viewProj.M22:F3} M33={viewProj.M33:F3} M34={viewProj.M34:F3} M43={viewProj.M43:F3} M44={viewProj.M44:F3}]");
|
||||||
// Camera-cell portal census (A8.F triage 2026-05-29): report, for EVERY
|
// Camera-cell portal census (A8.F triage 2026-05-29): report, for EVERY
|
||||||
// portal, the exact inputs the BFS guards read — BEFORE the guards run, so
|
// portal, the exact inputs the BFS guards read — BEFORE the guards run, so
|
||||||
// a portal the loop silently `continue`s past is still visible here. An
|
// a portal the loop silently `continue`s past is still visible here. An
|
||||||
// empty OUTSIDEVIEW can then be traced to the precise gate: polyLen<3 (empty
|
// empty OUTSIDEVIEW can then be traced to the precise gate: polyLen<3 (empty
|
||||||
// polygon from EnvCellLandblockBuildBuilder), interiorSide=false (camera back-facing the
|
// polygon from EnvCellLandblockBuildBuilder), interiorSide=false (camera back-facing the
|
||||||
// portal — a legitimately-empty result, not a bug), or (if both OK) a
|
// portal — a legitimately-empty result, not a bug), or (if both OK) a
|
||||||
// downstream projection/clip failure shown by the EXIT-PROJ/EXIT-CLIP lines.
|
// downstream projection/clip failure shown by the EXIT-PROJ/EXIT-CLIP lines.
|
||||||
for (int ci = 0; ci < cameraCell.Portals.Count; ci++)
|
for (int ci = 0; ci < cameraCell.Portals.Count; ci++)
|
||||||
{
|
{
|
||||||
|
|
@ -417,7 +417,7 @@ public static class PortalVisibilityBuilder
|
||||||
}
|
}
|
||||||
|
|
||||||
// T2 (BR-4): retail's growth propagation is IN PLACE, never by re-enqueue
|
// T2 (BR-4): retail's growth propagation is IN PLACE, never by re-enqueue
|
||||||
// — PView::AddViewToPortals (Ghidra 0x005a52d0, pc:433446): first
|
// — PView::AddViewToPortals (Ghidra 0x005a52d0, pc:433446): first
|
||||||
// discovery enqueues via InsCellTodoList; growth into a cell whose
|
// discovery enqueues via InsCellTodoList; growth into a cell whose
|
||||||
// cell_view_done is set calls AdjustCellView (pc:433741-433745), which
|
// cell_view_done is set calls AdjustCellView (pc:433741-433745), which
|
||||||
// re-clips ONLY the new views (the update_count watermark) through that
|
// re-clips ONLY the new views (the update_count watermark) through that
|
||||||
|
|
@ -426,7 +426,7 @@ public static class PortalVisibilityBuilder
|
||||||
// neighbour; it processes exactly the new tail and recurses further
|
// neighbour; it processes exactly the new tail and recurses further
|
||||||
// growth. Termination is physical: recursion fires only when AddRegion
|
// growth. Termination is physical: recursion fires only when AddRegion
|
||||||
// added a DISTINCT polygon (CanonicalKey dedup) that survived the 1-px
|
// added a DISTINCT polygon (CanonicalKey dedup) that survived the 1-px
|
||||||
// vertex merge — the finite fixpoint floor that replaced the old
|
// vertex merge — the finite fixpoint floor that replaced the old
|
||||||
// MaxReprocessPerCell=16 drift cap (deleted). The depth tripwire below
|
// MaxReprocessPerCell=16 drift cap (deleted). The depth tripwire below
|
||||||
// is a loud failsafe, not control flow: it firing means the convergence
|
// is a loud failsafe, not control flow: it firing means the convergence
|
||||||
// invariant broke and must be fixed, not tuned.
|
// invariant broke and must be fixed, not tuned.
|
||||||
|
|
@ -434,7 +434,7 @@ public static class PortalVisibilityBuilder
|
||||||
// #120 self-attribution: the recursion path (cell id per depth), so a
|
// #120 self-attribution: the recursion path (cell id per depth), so a
|
||||||
// tripwire firing names the growth CYCLE instead of just the tip.
|
// tripwire firing names the growth CYCLE instead of just the tip.
|
||||||
// Harness sweeps (CornerFloodReplayTests *Converges tests) could not
|
// Harness sweeps (CornerFloodReplayTests *Converges tests) could not
|
||||||
// reproduce the T5 firing — production-only ingredients (full lookup
|
// reproduce the T5 firing — production-only ingredients (full lookup
|
||||||
// graph / real camera path) are suspected; this dump pins them on the
|
// graph / real camera path) are suspected; this dump pins them on the
|
||||||
// next natural occurrence.
|
// next natural occurrence.
|
||||||
uint[] propagationChain = frame.PropagationChainScratch;
|
uint[] propagationChain = frame.PropagationChainScratch;
|
||||||
|
|
@ -444,7 +444,7 @@ public static class PortalVisibilityBuilder
|
||||||
if (depth >= RecursionTripwire)
|
if (depth >= RecursionTripwire)
|
||||||
{
|
{
|
||||||
System.Threading.Interlocked.Increment(ref ConvergenceTripwireCount);
|
System.Threading.Interlocked.Increment(ref ConvergenceTripwireCount);
|
||||||
Console.WriteLine($"[pv-ERROR] in-place propagation tripwire at depth {depth} on cell=0x{cell.CellId:X8} — convergence invariant broken, investigate");
|
Console.WriteLine($"[pv-ERROR] in-place propagation tripwire at depth {depth} on cell=0x{cell.CellId:X8} — convergence invariant broken, investigate");
|
||||||
DumpPropagationChain(propagationChain, depth, cameraCell.CellId, cameraPos);
|
DumpPropagationChain(propagationChain, depth, cameraCell.CellId, cameraPos);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -497,7 +497,7 @@ public static class PortalVisibilityBuilder
|
||||||
|
|
||||||
// Portal-side test (retail PView::InitCell side test, decomp:432962): only traverse a portal
|
// Portal-side test (retail PView::InitCell side test, decomp:432962): only traverse a portal
|
||||||
// the camera is on the INTERIOR side of. Retail culls the back-facing portal (the doorway just
|
// the camera is on the INTERIOR side of. Retail culls the back-facing portal (the doorway just
|
||||||
// flooded through) by this test ALONE — there is NO eye-in-opening bypass. R-A2b: the old
|
// flooded through) by this test ALONE — there is NO eye-in-opening bypass. R-A2b: the old
|
||||||
// `&& !eyeInsideOpening` bypass let a back portal within 1.75 m through, forming the
|
// `&& !eyeInsideOpening` bypass let a back portal within 1.75 m through, forming the
|
||||||
// 0171<->0173 flood cycle -> re-enqueue churn -> the doorway flap (pinned in flap-sidechk.log:
|
// 0171<->0173 flood cycle -> re-enqueue churn -> the doorway flap (pinned in flap-sidechk.log:
|
||||||
// back portals show camInterior=False eyeIn=True).
|
// back portals show camInterior=False eyeIn=True).
|
||||||
|
|
@ -529,8 +529,8 @@ public static class PortalVisibilityBuilder
|
||||||
if (dx) Console.WriteLine($"[pv-dump] EXIT-PROJ cell=0x{cell.CellId:X8} p{i} localN={poly.Length} clipN={clipVerts} local0=({poly[0].X:F2},{poly[0].Y:F2},{poly[0].Z:F2})");
|
if (dx) Console.WriteLine($"[pv-dump] EXIT-PROJ cell=0x{cell.CellId:X8} p{i} localN={poly.Length} clipN={clipVerts} local0=({poly[0].X:F2},{poly[0].Y:F2},{poly[0].Z:F2})");
|
||||||
if (dx) Console.WriteLine($"[pv-dump] EXIT-CLIP cell=0x{cell.CellId:X8} p{i} currentViewPolys={currentView.Polygons.Count} clipResult={clippedRegion.Count}");
|
if (dx) Console.WriteLine($"[pv-dump] EXIT-CLIP cell=0x{cell.CellId:X8} p{i} currentViewPolys={currentView.Polygons.Count} clipResult={clippedRegion.Count}");
|
||||||
|
|
||||||
// Empty clip = no flood through this portal, period — retail's empty-GetClip rule
|
// Empty clip = no flood through this portal, period — retail's empty-GetClip rule
|
||||||
// (polyClipFinish <3 survivors → reject; ClipPortals adds no view). The
|
// (polyClipFinish <3 survivors → reject; ClipPortals adds no view). The
|
||||||
// EyeInsidePortalOpening rescue that used to substitute the current view here was
|
// EyeInsidePortalOpening rescue that used to substitute the current view here was
|
||||||
// the documented compensation for ProjectToClip's old EyePlaneW=1e-4 divergence
|
// the documented compensation for ProjectToClip's old EyePlaneW=1e-4 divergence
|
||||||
// from polyClipFinish's exact W=0 clip; with the W=0 port (2026-06-11, pseudocode
|
// from polyClipFinish's exact W=0 clip; with the W=0 port (2026-06-11, pseudocode
|
||||||
|
|
@ -554,7 +554,7 @@ public static class PortalVisibilityBuilder
|
||||||
// Exit portal -> outdoors visible through this (clipped) opening.
|
// Exit portal -> outdoors visible through this (clipped) opening.
|
||||||
// OutsideView gates DRAWN color (terrain/sky/scissor), and the
|
// OutsideView gates DRAWN color (terrain/sky/scissor), and the
|
||||||
// shell that rasterizes this aperture draws +drawLiftZ above
|
// shell that rasterizes this aperture draws +drawLiftZ above
|
||||||
// the physics transform — project the region in the SAME
|
// the physics transform — project the region in the SAME
|
||||||
// lifted space or terrain stops a lift-height short of the
|
// lifted space or terrain stops a lift-height short of the
|
||||||
// drawn lintel (#130 strip). Flood semantics keep the
|
// drawn lintel (#130 strip). Flood semantics keep the
|
||||||
// unlifted clippedRegion path above.
|
// unlifted clippedRegion path above.
|
||||||
|
|
@ -607,20 +607,20 @@ public static class PortalVisibilityBuilder
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase U.2b — neighbour-side OtherPortalClip (retail PView::OtherPortalClip
|
// Phase U.2b — neighbour-side OtherPortalClip (retail PView::OtherPortalClip
|
||||||
// decomp:433524). The portal opening seen from THIS cell may be wider than the
|
// decomp:433524). The portal opening seen from THIS cell may be wider than the
|
||||||
// SAME opening seen from the neighbour (skewed/oblique apertures), so retail
|
// SAME opening seen from the neighbour (skewed/oblique apertures), so retail
|
||||||
// re-clips the already-near-side-clipped region against the neighbour's matching
|
// re-clips the already-near-side-clipped region against the neighbour's matching
|
||||||
// (reciprocal) portal polygon — the propagated region is the intersection of the
|
// (reciprocal) portal polygon — the propagated region is the intersection of the
|
||||||
// opening "seen from A" AND "seen from B". This can only TIGHTEN, never widen, and
|
// opening "seen from A" AND "seen from B". This can only TIGHTEN, never widen, and
|
||||||
// degrades to the prior near-side-only region when the reciprocal is unresolvable
|
// degrades to the prior near-side-only region when the reciprocal is unresolvable
|
||||||
// (over-include is the safe default). The reciprocal is the portal at index
|
// (over-include is the safe default). The reciprocal is the portal at index
|
||||||
// `portal.OtherPortalId` in the NEIGHBOUR's portal list — retail's direct back-link
|
// `portal.OtherPortalId` in the NEIGHBOUR's portal list — retail's direct back-link
|
||||||
// (arg2->other_portal_id, 433557), NOT a scan for the first OtherCellId match. The
|
// (arg2->other_portal_id, 433557), NOT a scan for the first OtherCellId match. The
|
||||||
// direct index is what lets a cell with TWO portals to the same neighbour clip each
|
// direct index is what lets a cell with TWO portals to the same neighbour clip each
|
||||||
// opening against its OWN reciprocal instead of the first one. Mutates clippedRegion
|
// opening against its OWN reciprocal instead of the first one. Mutates clippedRegion
|
||||||
// in place before the union below.
|
// in place before the union below.
|
||||||
// T2 (BR-4): reciprocal-empty culls — retail OtherPortalClip
|
// T2 (BR-4): reciprocal-empty culls — retail OtherPortalClip
|
||||||
// returning nothing means the opening is invisible from the
|
// returning nothing means the opening is invisible from the
|
||||||
// neighbour's side; the old eye-in-opening restore was part of
|
// neighbour's side; the old eye-in-opening restore was part of
|
||||||
// the deleted rescue.
|
// the deleted rescue.
|
||||||
|
|
@ -645,8 +645,8 @@ public static class PortalVisibilityBuilder
|
||||||
|
|
||||||
if (grew)
|
if (grew)
|
||||||
{
|
{
|
||||||
// First discovery → enqueue once (retail InsCellTodoList in
|
// First discovery → enqueue once (retail InsCellTodoList in
|
||||||
// the ecx_5==0 branch). Distance = camera→nearest portal-
|
// the ecx_5==0 branch). Distance = camera→nearest portal-
|
||||||
// opening vertex (retail InitCell min-vertex distance,
|
// opening vertex (retail InitCell min-vertex distance,
|
||||||
// pc:432988-433004).
|
// pc:432988-433004).
|
||||||
if (queued.Add(neighbourId))
|
if (queued.Add(neighbourId))
|
||||||
|
|
@ -655,10 +655,10 @@ public static class PortalVisibilityBuilder
|
||||||
InsertTodo(todo, neighbour, dist);
|
InsertTodo(todo, neighbour, dist);
|
||||||
inserted = true;
|
inserted = true;
|
||||||
}
|
}
|
||||||
// Growth into an already-POPPED cell → retail AdjustCellView:
|
// Growth into an already-POPPED cell → retail AdjustCellView:
|
||||||
// process only the new views, immediately, in place. A cell
|
// process only the new views, immediately, in place. A cell
|
||||||
// discovered but still pending in the todo list needs nothing
|
// discovered but still pending in the todo list needs nothing
|
||||||
// — its pop processes everything to date via the watermark.
|
// — its pop processes everything to date via the watermark.
|
||||||
else if (drawListed.Contains(neighbourId))
|
else if (drawListed.Contains(neighbourId))
|
||||||
{
|
{
|
||||||
inPlace = true;
|
inPlace = true;
|
||||||
|
|
@ -678,8 +678,8 @@ public static class PortalVisibilityBuilder
|
||||||
// draw position (retail appends to cell_draw_list once per pop,
|
// draw position (retail appends to cell_draw_list once per pop,
|
||||||
// pc:433783). Note: retail also RE-SORTS the draw list when a
|
// pc:433783). Note: retail also RE-SORTS the draw list when a
|
||||||
// late-grown cell's dependency order changes (AdjustCellPlace,
|
// late-grown cell's dependency order changes (AdjustCellPlace,
|
||||||
// pc:433247); we keep first-pop order — under T1's whole-cell
|
// pc:433247); we keep first-pop order — under T1's whole-cell
|
||||||
// far→near draws + depth testing, order affects only transparent-
|
// far→near draws + depth testing, order affects only transparent-
|
||||||
// pass compositing in exotic chains (documented residual for T5).
|
// pass compositing in exotic chains (documented residual for T5).
|
||||||
if (drawListed.Add(cell.CellId))
|
if (drawListed.Add(cell.CellId))
|
||||||
frame.OrderedVisibleCells.Add(cell.CellId);
|
frame.OrderedVisibleCells.Add(cell.CellId);
|
||||||
|
|
@ -691,7 +691,7 @@ public static class PortalVisibilityBuilder
|
||||||
if (pvDump)
|
if (pvDump)
|
||||||
Console.WriteLine($"[pv-dump] OUTSIDEVIEW polys={frame.OutsideView.Polygons.Count} bfsCellViews={frame.CellViews.Count} crossBldg={frame.CrossBuildingViews.Count}");
|
Console.WriteLine($"[pv-dump] OUTSIDEVIEW polys={frame.OutsideView.Polygons.Count} bfsCellViews={frame.CellViews.Count} crossBldg={frame.CrossBuildingViews.Count}");
|
||||||
|
|
||||||
// Phase U.4c flap probe (ACDREAM_PROBE_FLAP) — read-only per-frame snapshot of the
|
// Phase U.4c flap probe (ACDREAM_PROBE_FLAP) — read-only per-frame snapshot of the
|
||||||
// root cell's per-portal side-test + projection + the frame's exit/visible counts.
|
// root cell's per-portal side-test + projection + the frame's exit/visible counts.
|
||||||
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeFlapEnabled)
|
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeFlapEnabled)
|
||||||
EmitFlapProbe(cameraCell, cameraPos, viewProj, frame);
|
EmitFlapProbe(cameraCell, cameraPos, viewProj, frame);
|
||||||
|
|
@ -715,7 +715,7 @@ public static class PortalVisibilityBuilder
|
||||||
/// camera cell. It keeps the same retail distance-priority traversal and
|
/// camera cell. It keeps the same retail distance-priority traversal and
|
||||||
/// neighbour reciprocal clipping once inside the building.
|
/// neighbour reciprocal clipping once inside the building.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="seedRegion">Optional NDC region the seed apertures clip against —
|
/// <param name="seedRegion">Optional NDC region the seed apertures clip against —
|
||||||
/// retail's GetClip runs under the CURRENTLY INSTALLED view (PView::GetClip
|
/// retail's GetClip runs under the CURRENTLY INSTALLED view (PView::GetClip
|
||||||
/// 0x005a4320): full screen when the viewer is outdoors, the accumulated
|
/// 0x005a4320): full screen when the viewer is outdoors, the accumulated
|
||||||
/// outside (doorway) view when a building is looked into from an interior
|
/// outside (doorway) view when a building is looked into from an interior
|
||||||
|
|
@ -756,7 +756,7 @@ public static class PortalVisibilityBuilder
|
||||||
// If the camera is on the cell-interior side, the normal indoor
|
// If the camera is on the cell-interior side, the normal indoor
|
||||||
// DrawInside path owns this portal instead. T2 (BR-4): a seed
|
// DrawInside path owns this portal instead. T2 (BR-4): a seed
|
||||||
// portal the eye is IN-PLANE with (|dist| <= F_EPSILON) rejects
|
// portal the eye is IN-PLANE with (|dist| <= F_EPSILON) rejects
|
||||||
// OUTRIGHT — retail ConstructView(CBldPortal) returns 0 on
|
// OUTRIGHT — retail ConstructView(CBldPortal) returns 0 on
|
||||||
// Sidedness IN_PLANE (Ghidra 0x005a59a0); no degenerate view is
|
// Sidedness IN_PLANE (Ghidra 0x005a59a0); no degenerate view is
|
||||||
// ever built from a knife-edge aperture.
|
// ever built from a knife-edge aperture.
|
||||||
if (i < cell.ClipPlanes.Count)
|
if (i < cell.ClipPlanes.Count)
|
||||||
|
|
@ -788,7 +788,7 @@ public static class PortalVisibilityBuilder
|
||||||
|
|
||||||
// T2 (BR-4): empty clip = no seed, no exceptions (retail's
|
// T2 (BR-4): empty clip = no seed, no exceptions (retail's
|
||||||
// empty-GetClip rule; the full-screen substitute rescue is
|
// empty-GetClip rule; the full-screen substitute rescue is
|
||||||
// deleted — see Build()).
|
// deleted — see Build()).
|
||||||
if (clippedRegion.Count == 0)
|
if (clippedRegion.Count == 0)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
|
@ -800,19 +800,19 @@ public static class PortalVisibilityBuilder
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// T2 (BR-4): in-place growth propagation — mirrors Build()'s
|
// T2 (BR-4): in-place growth propagation — mirrors Build()'s
|
||||||
// ProcessCellPortals (retail AdjustCellView via the watermark); the
|
// ProcessCellPortals (retail AdjustCellView via the watermark); the
|
||||||
// re-enqueue + MaxReprocessPerCell cap and the eye-in-opening rescues
|
// re-enqueue + MaxReprocessPerCell cap and the eye-in-opening rescues
|
||||||
// are deleted (empty clip culls, period).
|
// are deleted (empty clip culls, period).
|
||||||
const int RecursionTripwire = 128;
|
const int RecursionTripwire = 128;
|
||||||
uint[] propagationChain = frame.PropagationChainScratch; // #120 self-attribution — see Build()
|
uint[] propagationChain = frame.PropagationChainScratch; // #120 self-attribution — see Build()
|
||||||
|
|
||||||
void ProcessCellPortals(LoadedCell cell, int depth)
|
void ProcessCellPortals(LoadedCell cell, int depth)
|
||||||
{
|
{
|
||||||
if (depth >= RecursionTripwire)
|
if (depth >= RecursionTripwire)
|
||||||
{
|
{
|
||||||
System.Threading.Interlocked.Increment(ref ConvergenceTripwireCount);
|
System.Threading.Interlocked.Increment(ref ConvergenceTripwireCount);
|
||||||
Console.WriteLine($"[pv-ERROR] look-in in-place propagation tripwire at depth {depth} on cell=0x{cell.CellId:X8} — convergence invariant broken, investigate");
|
Console.WriteLine($"[pv-ERROR] look-in in-place propagation tripwire at depth {depth} on cell=0x{cell.CellId:X8} — convergence invariant broken, investigate");
|
||||||
DumpPropagationChain(propagationChain, depth, 0u, cameraPos);
|
DumpPropagationChain(propagationChain, depth, 0u, cameraPos);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -841,7 +841,7 @@ public static class PortalVisibilityBuilder
|
||||||
if (portal.OtherCellId == 0xFFFF)
|
if (portal.OtherCellId == 0xFFFF)
|
||||||
continue; // already outdoors; exterior terrain was drawn by the caller.
|
continue; // already outdoors; exterior terrain was drawn by the caller.
|
||||||
|
|
||||||
// R-A2b: cull back portals by the side test alone — see Build().
|
// R-A2b: cull back portals by the side test alone — see Build().
|
||||||
if (i < cell.ClipPlanes.Count
|
if (i < cell.ClipPlanes.Count
|
||||||
&& !CameraOnInteriorSide(cell, i, cameraPos))
|
&& !CameraOnInteriorSide(cell, i, cameraPos))
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -905,15 +905,15 @@ public static class PortalVisibilityBuilder
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retail per-building flood — <c>PView::ConstructView(CBldPortal*, …)</c> (decomp:433827),
|
/// Retail per-building flood — <c>PView::ConstructView(CBldPortal*, …)</c> (decomp:433827),
|
||||||
/// reached from <c>BSPPORTAL::portal_draw_portals_only</c> (0x53d870) → <c>DrawPortal</c>
|
/// reached from <c>BSPPORTAL::portal_draw_portals_only</c> (0x53d870) → <c>DrawPortal</c>
|
||||||
/// (0x5a5ab0) during the terrain BSP walk. Floods ONE building's cells from its outside-facing
|
/// (0x5a5ab0) during the terrain BSP walk. Floods ONE building's cells from its outside-facing
|
||||||
/// entrance portal(s). Identical machinery to <see cref="BuildFromExterior"/>, but the CONTRACT is
|
/// entrance portal(s). Identical machinery to <see cref="BuildFromExterior"/>, but the CONTRACT is
|
||||||
/// per-building: the caller passes exactly one building's cells, so the seed is that building's
|
/// per-building: the caller passes exactly one building's cells, so the seed is that building's
|
||||||
/// FINITE entrance opening (bounded flood depth → the stable ~2-cell view retail draws per visible
|
/// FINITE entrance opening (bounded flood depth → the stable ~2-cell view retail draws per visible
|
||||||
/// building, measured live §3.4). This differs from the synthetic outdoor node's single unified
|
/// building, measured live §3.4). This differs from the synthetic outdoor node's single unified
|
||||||
/// flood whose full-screen-ish seed reaches variable depth into a building as the eye moves — the
|
/// flood whose full-screen-ish seed reaches variable depth into a building as the eye moves — the
|
||||||
/// 2↔6 oscillation. Robustness is validated by the conformance test, not assumed.
|
/// 2↔6 oscillation. Robustness is validated by the conformance test, not assumed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static PortalVisibilityFrame ConstructViewBuilding(
|
public static PortalVisibilityFrame ConstructViewBuilding(
|
||||||
IEnumerable<LoadedCell> buildingCells,
|
IEnumerable<LoadedCell> buildingCells,
|
||||||
|
|
@ -1053,12 +1053,12 @@ public static class PortalVisibilityBuilder
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase U.4c flap probe. One [flap] line per Build: the root cell's per-portal
|
// Phase U.4c flap probe. One [flap] line per Build: the root cell's per-portal
|
||||||
// signed distance D (eye→portal plane), traverse/cull decision, and NDC projection
|
// signed distance D (eye→portal plane), traverse/cull decision, and NDC projection
|
||||||
// vertex count, plus the frame's OutsideView polygon count + visible-cell count.
|
// vertex count, plus the frame's OutsideView polygon count + visible-cell count.
|
||||||
// `localEye` is the eye in root-local space — its component along an interior portal
|
// `localEye` is the eye in root-local space — its component along an interior portal
|
||||||
// plane reveals when the eye has crossed past that plane (the stale-root region that
|
// plane reveals when the eye has crossed past that plane (the stale-root region that
|
||||||
// makes the side test cull a still-needed portal). Read-only recompute; no effect on
|
// makes the side test cull a still-needed portal). Read-only recompute; no effect on
|
||||||
// the returned frame. Throwaway apparatus — strip with the probe.
|
// the returned frame. Throwaway apparatus — strip with the probe.
|
||||||
private static void EmitFlapProbe(
|
private static void EmitFlapProbe(
|
||||||
LoadedCell cameraCell, Vector3 cameraPos, Matrix4x4 viewProj, PortalVisibilityFrame frame)
|
LoadedCell cameraCell, Vector3 cameraPos, Matrix4x4 viewProj, PortalVisibilityFrame frame)
|
||||||
{
|
{
|
||||||
|
|
@ -1080,9 +1080,9 @@ public static class PortalVisibilityBuilder
|
||||||
d = Vector3.Dot(pl.Normal, localEye) + pl.D;
|
d = Vector3.Dot(pl.Normal, localEye) + pl.D;
|
||||||
side = CameraOnInteriorSide(cameraCell, i, cameraPos);
|
side = CameraOnInteriorSide(cameraCell, i, cameraPos);
|
||||||
}
|
}
|
||||||
// Replicate the walk's faithful path exactly (ProjectToClip → ClipToRegion(FullScreen)) so
|
// Replicate the walk's faithful path exactly (ProjectToClip → ClipToRegion(FullScreen)) so
|
||||||
// proj/clip mean the same as production: proj = clip-space verts in front of the eye,
|
// proj/clip mean the same as production: proj = clip-space verts in front of the eye,
|
||||||
// clip = verts surviving the screen-region clip. clip=0 with proj>=3 ⇒ the portal is
|
// clip = verts surviving the screen-region clip. clip=0 with proj>=3 ⇒ the portal is
|
||||||
// genuinely off-screen; the ndc coords (post-clip, bounded) show where on screen it lands.
|
// genuinely off-screen; the ndc coords (post-clip, bounded) show where on screen it lands.
|
||||||
int projN = -1, clipN = -1;
|
int projN = -1, clipN = -1;
|
||||||
string ndcText = "";
|
string ndcText = "";
|
||||||
|
|
@ -1115,21 +1115,21 @@ public static class PortalVisibilityBuilder
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mirrors CellVisibility's portal-side test (InsideSide convention).
|
// Mirrors CellVisibility's portal-side test (InsideSide convention).
|
||||||
// In-plane (|dot| <= PortalSideEpsilon) counts as interior-side — retail
|
// In-plane (|dot| <= PortalSideEpsilon) counts as interior-side — retail
|
||||||
// InitCell leaves the in-plane case a CANDIDATE for cell portals (Ghidra
|
// InitCell leaves the in-plane case a CANDIDATE for cell portals (Ghidra
|
||||||
// 0x005a4b70); building/exterior SEED portals additionally reject in-plane
|
// 0x005a4b70); building/exterior SEED portals additionally reject in-plane
|
||||||
// via EyeInPlaneOfPortal (retail ConstructView(CBldPortal) IN_PLANE → 0).
|
// via EyeInPlaneOfPortal (retail ConstructView(CBldPortal) IN_PLANE → 0).
|
||||||
private static bool CameraOnInteriorSide(LoadedCell cell, int portalIndex, Vector3 cameraPos)
|
private static bool CameraOnInteriorSide(LoadedCell cell, int portalIndex, Vector3 cameraPos)
|
||||||
{
|
{
|
||||||
var plane = cell.ClipPlanes[portalIndex];
|
var plane = cell.ClipPlanes[portalIndex];
|
||||||
if (plane.Normal.LengthSquared() < 1e-8f) return true; // no usable plane → allow
|
if (plane.Normal.LengthSquared() < 1e-8f) return true; // no usable plane → allow
|
||||||
var localCam = Vector3.Transform(cameraPos, cell.InverseWorldTransform);
|
var localCam = Vector3.Transform(cameraPos, cell.InverseWorldTransform);
|
||||||
float dot = Vector3.Dot(plane.Normal, localCam) + plane.D;
|
float dot = Vector3.Dot(plane.Normal, localCam) + plane.D;
|
||||||
return plane.InsideSide == 0 ? dot >= -PortalSideEpsilon : dot <= PortalSideEpsilon;
|
return plane.InsideSide == 0 ? dot >= -PortalSideEpsilon : dot <= PortalSideEpsilon;
|
||||||
}
|
}
|
||||||
|
|
||||||
// T2 (BR-4): retail ConstructView(CBldPortal)'s Sidedness IN_PLANE reject
|
// T2 (BR-4): retail ConstructView(CBldPortal)'s Sidedness IN_PLANE reject
|
||||||
// (Ghidra 0x005a59a0): |eye·N + d| <= F_EPSILON → the building/exterior
|
// (Ghidra 0x005a59a0): |eye·N + d| <= F_EPSILON → the building/exterior
|
||||||
// portal contributes nothing this frame (knife-edge aperture). Uses the
|
// portal contributes nothing this frame (knife-edge aperture). Uses the
|
||||||
// true retail epsilon, NOT the side test's root-lag tolerance.
|
// true retail epsilon, NOT the side test's root-lag tolerance.
|
||||||
private static bool EyeInPlaneOfPortal(LoadedCell cell, int portalIndex, Vector3 cameraPos)
|
private static bool EyeInPlaneOfPortal(LoadedCell cell, int portalIndex, Vector3 cameraPos)
|
||||||
|
|
@ -1153,22 +1153,22 @@ public static class PortalVisibilityBuilder
|
||||||
if (area2 < 0f) Array.Reverse(poly);
|
if (area2 < 0f) Array.Reverse(poly);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase U.2b — reciprocal OtherPortalClip (retail PView::OtherPortalClip decomp:433524).
|
// Phase U.2b — reciprocal OtherPortalClip (retail PView::OtherPortalClip decomp:433524).
|
||||||
// Resolves the neighbour's reciprocal back-portal by DIRECT INDEX (`otherPortalId`), projects
|
// Resolves the neighbour's reciprocal back-portal by DIRECT INDEX (`otherPortalId`), projects
|
||||||
// that reciprocal polygon through the NEIGHBOUR's world transform to NDC, and intersects it into
|
// that reciprocal polygon through the NEIGHBOUR's world transform to NDC, and intersects it into
|
||||||
// every polygon of `clippedRegion` (already clipped against the near-side opening + current
|
// every polygon of `clippedRegion` (already clipped against the near-side opening + current
|
||||||
// view). The net region is "opening seen from the near cell" ∩ "opening seen from the
|
// view). The net region is "opening seen from the near cell" ∩ "opening seen from the
|
||||||
// neighbour" — a strict tightening that prevents over-inclusion through skewed apertures.
|
// neighbour" — a strict tightening that prevents over-inclusion through skewed apertures.
|
||||||
//
|
//
|
||||||
// `otherPortalId` is the near-side portal's reciprocal back-link, straight from the dat's
|
// `otherPortalId` is the near-side portal's reciprocal back-link, straight from the dat's
|
||||||
// CellPortal.OtherPortalId. Retail indexes the neighbour's portal array with it directly —
|
// CellPortal.OtherPortalId. Retail indexes the neighbour's portal array with it directly —
|
||||||
// `portals->portal[arg2->other_portal_id ...]` at 005a54b2/005a54f6 — rather than scanning for
|
// `portals->portal[arg2->other_portal_id ...]` at 005a54b2/005a54f6 — rather than scanning for
|
||||||
// the first OtherCellId match. A scan picks the FIRST back-portal for EVERY near-side portal to
|
// the first OtherCellId match. A scan picks the FIRST back-portal for EVERY near-side portal to
|
||||||
// the same neighbour, so a cell with two openings into one neighbour clips both against the same
|
// the same neighbour, so a cell with two openings into one neighbour clips both against the same
|
||||||
// (first) reciprocal — hiding the second opening when the apertures are disjoint (under-inclusion
|
// (first) reciprocal — hiding the second opening when the apertures are disjoint (under-inclusion
|
||||||
// bug #102 M-4). The direct index gives each opening its own reciprocal.
|
// bug #102 M-4). The direct index gives each opening its own reciprocal.
|
||||||
//
|
//
|
||||||
// GUARDS — degrade to over-include (leave `clippedRegion` untouched), NEVER clip against a
|
// GUARDS — degrade to over-include (leave `clippedRegion` untouched), NEVER clip against a
|
||||||
// guessed polygon: the index is out of range, OR the indexed polygon is missing/degenerate
|
// guessed polygon: the index is out of range, OR the indexed polygon is missing/degenerate
|
||||||
// (< 3 verts), OR it projects entirely behind the camera. Over-inclusion is the safe default;
|
// (< 3 verts), OR it projects entirely behind the camera. Over-inclusion is the safe default;
|
||||||
// mis-resolution is the bug this method exists to remove. PortalPolygons is in lockstep with
|
// mis-resolution is the bug this method exists to remove. PortalPolygons is in lockstep with
|
||||||
|
|
@ -1184,38 +1184,38 @@ public static class PortalVisibilityBuilder
|
||||||
{
|
{
|
||||||
if (clippedRegion.Count == 0) return;
|
if (clippedRegion.Count == 0) return;
|
||||||
|
|
||||||
// Retail skips OtherPortalClip entirely for exact-match portals — both cells share
|
// Retail skips OtherPortalClip entirely for exact-match portals — both cells share
|
||||||
// the SAME opening polygon, so re-clipping against the reciprocal can only re-derive
|
// the SAME opening polygon, so re-clipping against the reciprocal can only re-derive
|
||||||
// the near-side clip: PView::ClipPortals decomp:433689
|
// the near-side clip: PView::ClipPortals decomp:433689
|
||||||
// `if (exact_match != 0 || other_portal_id < 0) goto propagate-without-reciprocal`.
|
// `if (exact_match != 0 || other_portal_id < 0) goto propagate-without-reciprocal`.
|
||||||
if ((portalFlags & PortalFlagExactMatch) != 0) return;
|
if ((portalFlags & PortalFlagExactMatch) != 0) return;
|
||||||
|
|
||||||
// Direct back-link index (retail arg2->other_portal_id). Out-of-range → over-include.
|
// Direct back-link index (retail arg2->other_portal_id). Out-of-range → over-include.
|
||||||
if (otherPortalId >= neighbour.PortalPolygons.Count) return;
|
if (otherPortalId >= neighbour.PortalPolygons.Count) return;
|
||||||
Vector3[]? reciprocalPoly = neighbour.PortalPolygons[otherPortalId];
|
Vector3[]? reciprocalPoly = neighbour.PortalPolygons[otherPortalId];
|
||||||
if (reciprocalPoly == null || reciprocalPoly.Length < 3) return; // missing/degenerate → over-include
|
if (reciprocalPoly == null || reciprocalPoly.Length < 3) return; // missing/degenerate → over-include
|
||||||
|
|
||||||
// §4 corner/doorway fix (2026-06-10): the reciprocal clip now runs the SAME homogeneous
|
// §4 corner/doorway fix (2026-06-10): the reciprocal clip now runs the SAME homogeneous
|
||||||
// pipeline as the forward clip — retail PView::OtherPortalClip (decomp:433524-433563) routes
|
// pipeline as the forward clip — retail PView::OtherPortalClip (decomp:433524-433563) routes
|
||||||
// the reciprocal polygon through the very same GetClip(finish=1) → ACRender::polyClipFinish
|
// the reciprocal polygon through the very same GetClip(finish=1) → ACRender::polyClipFinish
|
||||||
// homogeneous clipper as the near-side portal; there is no divide-first special case.
|
// homogeneous clipper as the near-side portal; there is no divide-first special case.
|
||||||
//
|
//
|
||||||
// HISTORY: this used to be ProjectToNdc + 2D ScreenPolygonClip.Intersect, justified by "the
|
// HISTORY: this used to be ProjectToNdc + 2D ScreenPolygonClip.Intersect, justified by "the
|
||||||
// reciprocal is a back-portal one hop away — never near the eye". That assumption is FALSE
|
// reciprocal is a back-portal one hop away — never near the eye". That assumption is FALSE
|
||||||
// exactly at doorways/corners: the reciprocal IS the same opening whose plane the eye presses
|
// exactly at doorways/corners: the reciprocal IS the same opening whose plane the eye presses
|
||||||
// against (2-60 cm). ProjectToNdc's MinW=0.05 eye-clip + side-plane clip + divide is knife-edge
|
// against (2-60 cm). ProjectToNdc's MinW=0.05 eye-clip + side-plane clip + divide is knife-edge
|
||||||
// there — 2 cm eye moves flipped its output between "covers the region" and a duplicated-vertex
|
// there — 2 cm eye moves flipped its output between "covers the region" and a duplicated-vertex
|
||||||
// hairline, which CellView.Add's snap-dedup then rejected → the neighbour room dropped from the
|
// hairline, which CellView.Add's snap-dedup then rejected → the neighbour room dropped from the
|
||||||
// flood for isolated frames → the corner/transition background strobe (CornerFloodReplayTests
|
// flood for isolated frames → the corner/transition background strobe (CornerFloodReplayTests
|
||||||
// pins this deterministically; the glitch steps die with this change). The old path's other
|
// pins this deterministically; the glitch steps die with this change). The old path's other
|
||||||
// rationale — per-round float drift defeating the exact-match CellView dedup — is obsolete:
|
// rationale — per-round float drift defeating the exact-match CellView dedup — is obsolete:
|
||||||
// CanonicalKey's 1e-3-grid snap dedup (2026-06-06) absorbs re-clip drift by construction.
|
// CanonicalKey's 1e-3-grid snap dedup (2026-06-06) absorbs re-clip drift by construction.
|
||||||
using PortalProjection.ClipPolygonLease reciprocalClip =
|
using PortalProjection.ClipPolygonLease reciprocalClip =
|
||||||
PortalProjection.ProjectToClipLease(
|
PortalProjection.ProjectToClipLease(
|
||||||
reciprocalPoly,
|
reciprocalPoly,
|
||||||
neighbour.WorldTransform,
|
neighbour.WorldTransform,
|
||||||
viewProj);
|
viewProj);
|
||||||
if (reciprocalClip.Count < 3) return; // reciprocal entirely behind the eye → no constraint (over-include)
|
if (reciprocalClip.Count < 3) return; // reciprocal entirely behind the eye → no constraint (over-include)
|
||||||
|
|
||||||
// Intersect the reciprocal opening into each near-side polygon; drop any that fall away.
|
// Intersect the reciprocal opening into each near-side polygon; drop any that fall away.
|
||||||
// ClipToRegion(subject=homogeneous reciprocal, region=near-side NDC polygon) = the same
|
// ClipToRegion(subject=homogeneous reciprocal, region=near-side NDC polygon) = the same
|
||||||
|
|
@ -1252,7 +1252,7 @@ public static class PortalVisibilityBuilder
|
||||||
return grew;
|
return grew;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Camera→nearest-vertex distance for a portal polygon, in world space. Mirrors the per-portal
|
// Camera→nearest-vertex distance for a portal polygon, in world space. Mirrors the per-portal
|
||||||
// min-distance loop retail runs in PView::InitCell (decomp:432988-433004) to key the todo list:
|
// min-distance loop retail runs in PView::InitCell (decomp:432988-433004) to key the todo list:
|
||||||
// it walks the portal's vertices, transforms each to world space, and keeps the smallest
|
// it walks the portal's vertices, transforms each to world space, and keeps the smallest
|
||||||
// straight-line distance to the camera viewpoint. Keying on the portal opening (not the cell
|
// straight-line distance to the camera viewpoint. Keying on the portal opening (not the cell
|
||||||
|
|
@ -1272,10 +1272,10 @@ public static class PortalVisibilityBuilder
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Distance-sorted work list for the portal BFS, ported from retail PView::cell_todo_list +
|
/// Distance-sorted work list for the portal BFS, ported from retail PView::cell_todo_list +
|
||||||
/// InsCellTodoList (decomp:433183). Insertion keeps the list ordered so the NEAREST cell sits at
|
/// InsCellTodoList (decomp:433183). Insertion keeps the list ordered so the NEAREST cell sits at
|
||||||
/// the tail; <see cref="PopNearest"/> removes the tail — giving closest-first traversal exactly
|
/// the tail; <see cref="PopNearest"/> removes the tail — giving closest-first traversal exactly
|
||||||
/// as ConstructView's pop-from-(cell_todo_num-1) does (433767-433769). The insertion only shifts
|
/// as ConstructView's pop-from-(cell_todo_num-1) does (433767-433769). The insertion only shifts
|
||||||
/// entries strictly farther than the newcomer (retail's flag test breaks on the first
|
/// entries strictly farther than the newcomer (retail's flag test breaks on the first
|
||||||
/// not-greater entry), so an equal-distance newcomer lands at the tail and pops FIRST —
|
/// not-greater entry), so an equal-distance newcomer lands at the tail and pops FIRST —
|
||||||
/// LIFO on ties, matching retail's break-on-first-not-greater + pop-from-tail.
|
/// LIFO on ties, matching retail's break-on-first-not-greater + pop-from-tail.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static void InsertTodo(
|
private static void InsertTodo(
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using AcDream.Content;
|
using AcDream.Content;
|
||||||
using DatReaderWriter;
|
using DatReaderWriter;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
|
@ -12,7 +12,7 @@ namespace AcDream.App.Rendering;
|
||||||
/// classes and the same order as <see cref="GameWindow.OnLoad"/>, minus
|
/// classes and the same order as <see cref="GameWindow.OnLoad"/>, minus
|
||||||
/// terrain / sky / physics / streaming.
|
/// terrain / sky / physics / streaming.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed record RenderStack(
|
internal sealed record RenderStack(
|
||||||
GL Gl,
|
GL Gl,
|
||||||
IDatReaderWriter Dats,
|
IDatReaderWriter Dats,
|
||||||
string ShaderDir,
|
string ShaderDir,
|
||||||
|
|
@ -28,15 +28,13 @@ public sealed record RenderStack(
|
||||||
AcDream.App.UI.UiDatFont? LargeDatFont) : System.IDisposable
|
AcDream.App.UI.UiDatFont? LargeDatFont) : System.IDisposable
|
||||||
{
|
{
|
||||||
internal GpuFrameFlightController FrameFlights { get; init; } = null!;
|
internal GpuFrameFlightController FrameFlights { get; init; } = null!;
|
||||||
|
internal IGpuDevice GpuDevice { get; init; } = null!;
|
||||||
|
internal GpuDeviceFrameLifetime FrameLifetime { get; init; } = null!;
|
||||||
private ResourceShutdownTransaction? _shutdown;
|
private ResourceShutdownTransaction? _shutdown;
|
||||||
|
|
||||||
internal void BeginFrame()
|
internal void BeginFrame() => FrameLifetime.BeginFrame();
|
||||||
{
|
|
||||||
FrameFlights.BeginFrame();
|
|
||||||
UiHost.TextRenderer.BeginFrame(FrameFlights.CurrentSlot);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void EndFrame() => FrameFlights.EndFrame();
|
internal void EndFrame() => FrameLifetime.EndFrame();
|
||||||
|
|
||||||
/// <summary>Dispose the GL pieces this stack OWNS (everything created in
|
/// <summary>Dispose the GL pieces this stack OWNS (everything created in
|
||||||
/// <see cref="RenderBootstrap.Create"/>). <see cref="Dats"/> + <see cref="Gl"/> are caller-owned
|
/// <see cref="RenderBootstrap.Create"/>). <see cref="Dats"/> + <see cref="Gl"/> are caller-owned
|
||||||
|
|
@ -63,6 +61,10 @@ public sealed record RenderStack(
|
||||||
new("lighting UBO", LightingUbo.Dispose),
|
new("lighting UBO", LightingUbo.Dispose),
|
||||||
new("UI host", UiHost.Dispose),
|
new("UI host", UiHost.Dispose),
|
||||||
]),
|
]),
|
||||||
|
new ResourceShutdownStage("GPU device (RHI)",
|
||||||
|
[
|
||||||
|
new("GPU device", GpuDevice.Dispose),
|
||||||
|
]),
|
||||||
new ResourceShutdownStage("frame flight owner",
|
new ResourceShutdownStage("frame flight owner",
|
||||||
[
|
[
|
||||||
new("frame flights", FrameFlights.Dispose),
|
new("frame flights", FrameFlights.Dispose),
|
||||||
|
|
@ -71,17 +73,17 @@ public sealed record RenderStack(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resolves a sprite id (0x06xxxxxx) to a (GL handle, width, height) triple.
|
/// Resolves a sprite id (0x06xxxxxx) to a (texture-table slot, width, height) triple.
|
||||||
/// Copied verbatim from GameWindow's ResolveChrome closure — it calls
|
/// Copied verbatim from GameWindow's ResolveChrome closure — it calls
|
||||||
/// TextureCache.GetOrUploadRenderSurface(id, out w, out h).
|
/// TextureCache.GetOrUploadRenderSurface(id, out w, out h).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public (uint handle, int width, int height) ResolveChrome(uint spriteId)
|
public (GpuTextureSlot handle, int width, int height) ResolveChrome(uint spriteId)
|
||||||
{
|
{
|
||||||
uint t = TextureCache.GetOrUploadRenderSurface(spriteId, out int w, out int h);
|
GpuTextureSlot t = TextureCache.GetOrUploadRenderSurface(spriteId, out int w, out int h);
|
||||||
return (t, w, h);
|
return (t, w, h);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Font cache (per-stack, keyed by FontDid) ─────────────────────────────
|
// ── Font cache (per-stack, keyed by FontDid) ─────────────────────────────
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Cache of loaded dat fonts keyed by FontDid (0x40000000-range).
|
/// Cache of loaded dat fonts keyed by FontDid (0x40000000-range).
|
||||||
|
|
@ -93,7 +95,7 @@ public sealed record RenderStack(
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Lazily load and cache a dat font by its FontDid. Returns null (and
|
/// Lazily load and cache a dat font by its FontDid. Returns null (and
|
||||||
/// caches null) when the Font DBObj is absent or has no foreground surface —
|
/// caches null) when the Font DBObj is absent or has no foreground surface —
|
||||||
/// callers fall back to the global font in that case.
|
/// callers fall back to the global font in that case.
|
||||||
///
|
///
|
||||||
/// <para>Pre-seeds <see cref="VitalsDatFont"/> (0x40000000) and
|
/// <para>Pre-seeds <see cref="VitalsDatFont"/> (0x40000000) and
|
||||||
|
|
@ -123,7 +125,7 @@ public sealed record RenderStack(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Options for <see cref="RenderBootstrap.Create"/>.</summary>
|
/// <summary>Options for <see cref="RenderBootstrap.Create"/>.</summary>
|
||||||
public sealed record RenderBootstrapOptions(
|
internal sealed record RenderBootstrapOptions(
|
||||||
AcDream.UI.Abstractions.Settings.QualitySettings Quality,
|
AcDream.UI.Abstractions.Settings.QualitySettings Quality,
|
||||||
string DiagnosticsDirectory);
|
string DiagnosticsDirectory);
|
||||||
|
|
||||||
|
|
@ -131,12 +133,12 @@ public sealed record RenderBootstrapOptions(
|
||||||
/// Constructs the UI Studio's render stack from the production classes,
|
/// Constructs the UI Studio's render stack from the production classes,
|
||||||
/// in the same order as <see cref="GameWindow.OnLoad"/>.
|
/// in the same order as <see cref="GameWindow.OnLoad"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class RenderBootstrap
|
internal static class RenderBootstrap
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Build the studio's render stack. Throws <see cref="NotSupportedException"/>
|
/// Build the studio's render stack. Throws <see cref="NotSupportedException"/>
|
||||||
/// (same message as GameWindow) if GL_ARB_bindless_texture or
|
/// (same message as GameWindow) if GL_ARB_bindless_texture or
|
||||||
/// GL_ARB_shader_draw_parameters are absent — the modern path is mandatory.
|
/// GL_ARB_shader_draw_parameters are absent — the modern path is mandatory.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static RenderStack Create(
|
public static RenderStack Create(
|
||||||
GL gl,
|
GL gl,
|
||||||
|
|
@ -165,8 +167,15 @@ public static class RenderBootstrap
|
||||||
|
|
||||||
// --- TextureCache (GameWindow ~1774) ---
|
// --- TextureCache (GameWindow ~1774) ---
|
||||||
var frameFlights = new GpuFrameFlightController(gl);
|
var frameFlights = new GpuFrameFlightController(gl);
|
||||||
|
// Campaign V slice V4a: the RHI device UI Studio's TextureCache/UiHost
|
||||||
|
// now need for their retained-UI texture/pipeline path, constructed
|
||||||
|
// the same way HostInputCameraCompositionPhase does for the main
|
||||||
|
// GameWindow (V1).
|
||||||
|
var gpuDevice = new AcDream.App.Rendering.Gpu.Gl.GlGpuDevice(gl, frameFlights, shaderDir);
|
||||||
|
var frameLifetime = new GpuDeviceFrameLifetime(gpuDevice);
|
||||||
var textureCache = new TextureCache(
|
var textureCache = new TextureCache(
|
||||||
gl,
|
gl,
|
||||||
|
gpuDevice,
|
||||||
dats,
|
dats,
|
||||||
bindless,
|
bindless,
|
||||||
frameFlights,
|
frameFlights,
|
||||||
|
|
@ -201,7 +210,7 @@ public static class RenderBootstrap
|
||||||
return new AcDream.Core.Physics.AnimationSequencer(
|
return new AcDream.Core.Physics.AnimationSequencer(
|
||||||
setup, mtable, capturedAnimLoader);
|
setup, mtable, capturedAnimLoader);
|
||||||
}
|
}
|
||||||
// Setup exists but no motion table — no-op sequencer.
|
// Setup exists but no motion table — no-op sequencer.
|
||||||
return new AcDream.Core.Physics.AnimationSequencer(
|
return new AcDream.Core.Physics.AnimationSequencer(
|
||||||
setup,
|
setup,
|
||||||
new DatReaderWriter.DBObjs.MotionTable(),
|
new DatReaderWriter.DBObjs.MotionTable(),
|
||||||
|
|
@ -219,10 +228,10 @@ public static class RenderBootstrap
|
||||||
var entitySpawnAdapter = new Wb.EntitySpawnAdapter(
|
var entitySpawnAdapter = new Wb.EntitySpawnAdapter(
|
||||||
textureCache, SequencerFactory, meshAdapter);
|
textureCache, SequencerFactory, meshAdapter);
|
||||||
|
|
||||||
// --- EntityClassificationCache (GameWindow ~217 — field initializer, new()) ---
|
// --- EntityClassificationCache (GameWindow ~217 — field initializer, new()) ---
|
||||||
var classificationCache = new Wb.EntityClassificationCache();
|
var classificationCache = new Wb.EntityClassificationCache();
|
||||||
|
|
||||||
// --- TranslucencyFadeManager (GameWindow — field initializer, new()) ---
|
// --- TranslucencyFadeManager (GameWindow — field initializer, new()) ---
|
||||||
var translucencyFades = new AcDream.Core.Rendering.TranslucencyFadeManager();
|
var translucencyFades = new AcDream.Core.Rendering.TranslucencyFadeManager();
|
||||||
|
|
||||||
// --- WbDrawDispatcher (GameWindow ~2377-2381) ---
|
// --- WbDrawDispatcher (GameWindow ~2377-2381) ---
|
||||||
|
|
@ -237,13 +246,13 @@ public static class RenderBootstrap
|
||||||
// --- Larger retail font (0x40000001, MaxCharHeight=18) for attribute row text.
|
// --- Larger retail font (0x40000001, MaxCharHeight=18) for attribute row text.
|
||||||
// The default font (0x40000000, 16px) renders the row names too small; the 18px
|
// The default font (0x40000000, 16px) renders the row names too small; the 18px
|
||||||
// variant (confirmed in client_portal.dat 2026-06-26) matches the retail character
|
// variant (confirmed in client_portal.dat 2026-06-26) matches the retail character
|
||||||
// window list more closely (≈ icon height ≈ 24px target, 18px is best available).
|
// window list more closely (≈ icon height ≈ 24px target, 18px is best available).
|
||||||
var largeDatFont = AcDream.App.UI.UiDatFont.Load(dats, textureCache, 0x40000001u);
|
var largeDatFont = AcDream.App.UI.UiDatFont.Load(dats, textureCache, 0x40000001u);
|
||||||
|
|
||||||
// --- UiHost (GameWindow ~1790); pass null for debugFont (only used as
|
// --- UiHost (GameWindow ~1790); pass null for debugFont (only used as
|
||||||
// a fallback BitmapFont for the world-space HUD — not needed for the
|
// a fallback BitmapFont for the world-space HUD — not needed for the
|
||||||
// UI Studio, and BitmapFont requires a system font byte array) ---
|
// UI Studio, and BitmapFont requires a system font byte array) ---
|
||||||
var uiHost = new AcDream.App.UI.UiHost(gl, shaderDir, defaultFont: null);
|
var uiHost = new AcDream.App.UI.UiHost(gpuDevice, () => frameLifetime.Current, defaultFont: null);
|
||||||
|
|
||||||
var stack = new RenderStack(
|
var stack = new RenderStack(
|
||||||
Gl: gl,
|
Gl: gl,
|
||||||
|
|
@ -261,6 +270,8 @@ public static class RenderBootstrap
|
||||||
LargeDatFont: largeDatFont)
|
LargeDatFont: largeDatFont)
|
||||||
{
|
{
|
||||||
FrameFlights = frameFlights,
|
FrameFlights = frameFlights,
|
||||||
|
GpuDevice = gpuDevice,
|
||||||
|
FrameLifetime = frameLifetime,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Pre-seed the font cache with the two already-uploaded atlas instances
|
// Pre-seed the font cache with the two already-uploaded atlas instances
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,52 @@ internal interface IRenderFrameLifetime
|
||||||
void EndFrame();
|
void EndFrame();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Campaign V slice V4a: the current frame's <see cref="IGpuFrame"/>, readable
|
||||||
|
/// by any ported renderer that needs to allocate a ring or open a pass — the
|
||||||
|
/// structural piece this slice adds so <c>TextRenderer</c>/<c>DebugLineRenderer</c>
|
||||||
|
/// have somewhere to reach the frame lifecycle already bracketing every render
|
||||||
|
/// callback (<see cref="RenderFrameOrchestrator"/>'s <see cref="IRenderFrameLifetime"/>).
|
||||||
|
/// </summary>
|
||||||
|
internal interface ICurrentGpuFrameSource
|
||||||
|
{
|
||||||
|
/// <summary>The frame opened by the most recent <see cref="IRenderFrameLifetime.BeginFrame"/>. Throws if none is open.</summary>
|
||||||
|
IGpuFrame Current { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Wires <see cref="IGpuDevice.BeginFrame"/>/<see cref="IGpuFrame.End"/> into
|
||||||
|
/// the same <see cref="IRenderFrameLifetime"/> bracket <see cref="GpuFrameFlightController"/>
|
||||||
|
/// occupied before this slice — additive, not a frame-graph restructuring:
|
||||||
|
/// <see cref="Gpu.Gl.GlGpuDevice.BeginFrame"/> already calls the frame-flight
|
||||||
|
/// controller's <c>BeginFrame</c> internally, so this type OWNS the
|
||||||
|
/// <see cref="IRenderFrameLifetime"/> slot rather than running alongside the
|
||||||
|
/// controller (which would double-begin the same flight fence). Clears,
|
||||||
|
/// framebuffer management, and every non-ported renderer's own per-frame
|
||||||
|
/// bracket are untouched — slice V4h formalizes the rest of the frame spine.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class GpuDeviceFrameLifetime : IRenderFrameLifetime, ICurrentGpuFrameSource
|
||||||
|
{
|
||||||
|
private readonly IGpuDevice _device;
|
||||||
|
private IGpuFrame? _current;
|
||||||
|
|
||||||
|
public GpuDeviceFrameLifetime(IGpuDevice device) =>
|
||||||
|
_device = device ?? throw new ArgumentNullException(nameof(device));
|
||||||
|
|
||||||
|
public IGpuFrame Current => _current
|
||||||
|
?? throw new InvalidOperationException(
|
||||||
|
"No GPU frame is open — BeginFrame must run before Current is read.");
|
||||||
|
|
||||||
|
public void BeginFrame() => _current = _device.BeginFrame();
|
||||||
|
|
||||||
|
public void EndFrame()
|
||||||
|
{
|
||||||
|
IGpuFrame frame = Current;
|
||||||
|
_current = null;
|
||||||
|
frame.End();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal interface IRenderFrameResourcePhase
|
internal interface IRenderFrameResourcePhase
|
||||||
{
|
{
|
||||||
void Prepare(RenderFrameInput input);
|
void Prepare(RenderFrameInput input);
|
||||||
|
|
|
||||||
|
|
@ -88,8 +88,6 @@ internal sealed class RuntimeRenderFrameBeginResources : IRenderFrameBeginResour
|
||||||
private readonly WbDrawDispatcher? _dispatcher;
|
private readonly WbDrawDispatcher? _dispatcher;
|
||||||
private readonly EnvCellRenderer? _environmentCells;
|
private readonly EnvCellRenderer? _environmentCells;
|
||||||
private readonly PortalDepthMaskRenderer? _portalDepth;
|
private readonly PortalDepthMaskRenderer? _portalDepth;
|
||||||
private readonly TextRenderer? _worldText;
|
|
||||||
private readonly TextRenderer? _uiText;
|
|
||||||
private readonly ClipFrame? _clip;
|
private readonly ClipFrame? _clip;
|
||||||
private readonly TerrainModernRenderer? _terrain;
|
private readonly TerrainModernRenderer? _terrain;
|
||||||
private readonly SceneLightingUboBinding? _lighting;
|
private readonly SceneLightingUboBinding? _lighting;
|
||||||
|
|
@ -98,8 +96,6 @@ internal sealed class RuntimeRenderFrameBeginResources : IRenderFrameBeginResour
|
||||||
WbDrawDispatcher? dispatcher,
|
WbDrawDispatcher? dispatcher,
|
||||||
EnvCellRenderer? environmentCells,
|
EnvCellRenderer? environmentCells,
|
||||||
PortalDepthMaskRenderer? portalDepth,
|
PortalDepthMaskRenderer? portalDepth,
|
||||||
TextRenderer? worldText,
|
|
||||||
TextRenderer? uiText,
|
|
||||||
ClipFrame? clip,
|
ClipFrame? clip,
|
||||||
TerrainModernRenderer? terrain,
|
TerrainModernRenderer? terrain,
|
||||||
SceneLightingUboBinding? lighting)
|
SceneLightingUboBinding? lighting)
|
||||||
|
|
@ -108,8 +104,6 @@ internal sealed class RuntimeRenderFrameBeginResources : IRenderFrameBeginResour
|
||||||
_dispatcher = dispatcher;
|
_dispatcher = dispatcher;
|
||||||
_environmentCells = environmentCells;
|
_environmentCells = environmentCells;
|
||||||
_portalDepth = portalDepth;
|
_portalDepth = portalDepth;
|
||||||
_worldText = worldText;
|
|
||||||
_uiText = uiText;
|
|
||||||
_clip = clip;
|
_clip = clip;
|
||||||
_terrain = terrain;
|
_terrain = terrain;
|
||||||
_lighting = lighting;
|
_lighting = lighting;
|
||||||
|
|
@ -122,8 +116,11 @@ internal sealed class RuntimeRenderFrameBeginResources : IRenderFrameBeginResour
|
||||||
_dispatcher?.BeginFrame(gpuSlot);
|
_dispatcher?.BeginFrame(gpuSlot);
|
||||||
_environmentCells?.BeginFrame(gpuSlot);
|
_environmentCells?.BeginFrame(gpuSlot);
|
||||||
_portalDepth?.BeginFrame(gpuSlot);
|
_portalDepth?.BeginFrame(gpuSlot);
|
||||||
_worldText?.BeginFrame(gpuSlot);
|
// TextRenderer (world-hud + retained UI) is off this per-slot int
|
||||||
_uiText?.BeginFrame(gpuSlot);
|
// pattern as of Campaign V slice V4a — it allocates rings directly
|
||||||
|
// from the current IGpuFrame at Flush time, with no separate begin
|
||||||
|
// step (GpuDeviceFrameLifetime resets the device's ring watermark
|
||||||
|
// when IGpuDevice.BeginFrame runs).
|
||||||
_clip?.BeginFrame(gpuSlot);
|
_clip?.BeginFrame(gpuSlot);
|
||||||
_terrain?.BeginFrame(gpuSlot);
|
_terrain?.BeginFrame(gpuSlot);
|
||||||
_lighting?.BeginFrame(gpuSlot);
|
_lighting?.BeginFrame(gpuSlot);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
|
||||||
namespace AcDream.App.Rendering.Residency;
|
namespace AcDream.App.Rendering.Residency;
|
||||||
|
|
||||||
|
|
@ -6,7 +6,7 @@ namespace AcDream.App.Rendering.Residency;
|
||||||
/// Startup-time residency ceilings. Values preserve the pre-Slice-D cache
|
/// Startup-time residency ceilings. Values preserve the pre-Slice-D cache
|
||||||
/// behavior by default and are changed atomically as one profile.
|
/// behavior by default and are changed atomically as one profile.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed record ResidencyBudgetOptions(
|
internal sealed record ResidencyBudgetOptions(
|
||||||
long ObjectMeshGpuBytes,
|
long ObjectMeshGpuBytes,
|
||||||
int ObjectMeshUnownedEntries,
|
int ObjectMeshUnownedEntries,
|
||||||
long PreparedMeshCpuBytes,
|
long PreparedMeshCpuBytes,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AcDream.Core.Rendering;
|
using AcDream.Core.Rendering;
|
||||||
|
|
||||||
|
|
@ -10,7 +10,7 @@ namespace AcDream.App.Rendering;
|
||||||
/// <c>docs/research/named-retail/acclient_2013_pseudo_c.txt:95505</c>):
|
/// <c>docs/research/named-retail/acclient_2013_pseudo_c.txt:95505</c>):
|
||||||
/// a STATEFUL sought position that converges from the current swept
|
/// a STATEFUL sought position that converges from the current swept
|
||||||
/// viewer toward the desired boom pose (<c>CameraManager::UpdateCamera</c>
|
/// viewer toward the desired boom pose (<c>CameraManager::UpdateCamera</c>
|
||||||
/// 0x00456660 → <c>viewer_sought_position</c>, the #180 fix), 5-frame
|
/// 0x00456660 → <c>viewer_sought_position</c>, the #180 fix), 5-frame
|
||||||
/// velocity-averaged slope-aligned heading frame, mouse-input low-pass
|
/// velocity-averaged slope-aligned heading frame, mouse-input low-pass
|
||||||
/// filter. Pseudocode:
|
/// filter. Pseudocode:
|
||||||
/// <c>docs/research/2026-07-06-camera-sought-position-pseudocode.md</c>.
|
/// <c>docs/research/2026-07-06-camera-sought-position-pseudocode.md</c>.
|
||||||
|
|
@ -27,7 +27,7 @@ namespace AcDream.App.Rendering;
|
||||||
/// Spec: <c>docs/superpowers/specs/2026-05-18-retail-chase-camera-design.md</c>.
|
/// Spec: <c>docs/superpowers/specs/2026-05-18-retail-chase-camera-design.md</c>.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class RetailChaseCamera : ICamera
|
internal sealed class RetailChaseCamera : ICamera
|
||||||
{
|
{
|
||||||
// ICamera surface.
|
// ICamera surface.
|
||||||
public Vector3 Position { get; private set; }
|
public Vector3 Position { get; private set; }
|
||||||
|
|
@ -35,19 +35,19 @@ public sealed class RetailChaseCamera : ICamera
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The cell the collided viewer-sphere ended in (retail <c>viewer_cell =
|
/// The cell the collided viewer-sphere ended in (retail <c>viewer_cell =
|
||||||
/// sphere_path.curr_cell</c>). Roots the render mode + indoor visibility + the portal
|
/// sphere_path.curr_cell</c>). Roots the render mode + indoor visibility + the portal
|
||||||
/// side-test in <see cref="GameWindow"/> (Phase W single-viewpoint V1) — the ONE viewpoint.
|
/// side-test in <see cref="GameWindow"/> (Phase W single-viewpoint V1) — the ONE viewpoint.
|
||||||
/// Equals the passed player cell when camera collision is off / the probe is null.
|
/// Equals the passed player cell when camera collision is off / the probe is null.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public uint ViewerCellId { get; private set; }
|
public uint ViewerCellId { get; private set; }
|
||||||
public float Aspect { get; set; } = 16f / 9f;
|
public float Aspect { get; set; } = 16f / 9f;
|
||||||
public float FovY { get; set; } = MathF.PI / 3f;
|
public float FovY { get; set; } = MathF.PI / 3f;
|
||||||
public Matrix4x4 View { get; private set; } = Matrix4x4.Identity;
|
public Matrix4x4 View { get; private set; } = Matrix4x4.Identity;
|
||||||
// Near plane = retail Render::znear = 0.1 m (decomp :342130/:342173/:1101867 —
|
// Near plane = retail Render::znear = 0.1 m (decomp :342130/:342173/:1101867 —
|
||||||
// Render::SetFOVRad sets 0.1 flat; the legacy set_vdst variant is max(0.1, vdst·0.25)).
|
// Render::SetFOVRad sets 0.1 flat; the legacy set_vdst variant is max(0.1, vdst·0.25)).
|
||||||
// MUST be smaller than the 0.3 m camera-collision sphere (PhysicsCameraCollisionProbe.
|
// MUST be smaller than the 0.3 m camera-collision sphere (PhysicsCameraCollisionProbe.
|
||||||
// ViewerSphereRadius): with a 1.0 m near, a wall the collided eye sits 0.3 m from
|
// ViewerSphereRadius): with a 1.0 m near, a wall the collided eye sits 0.3 m from
|
||||||
// falls INSIDE the near plane and is clipped away — pressing the camera into a corner
|
// falls INSIDE the near plane and is clipped away — pressing the camera into a corner
|
||||||
// let you see straight through the wall (§4 corner residual). History: 0.1 landed
|
// let you see straight through the wall (§4 corner residual). History: 0.1 landed
|
||||||
// (137b4f2), was reverted (8bd3492) after correlating with missing indoor textures,
|
// (137b4f2), was reverted (8bd3492) after correlating with missing indoor textures,
|
||||||
// and re-landed once #110 resolved: the textures were the pre-existing #105
|
// and re-landed once #110 resolved: the textures were the pre-existing #105
|
||||||
// staged-texture-flush drop (WbMeshAdapter.Tick), and 0.1 merely raised its trigger
|
// staged-texture-flush drop (WbMeshAdapter.Tick), and 0.1 merely raised its trigger
|
||||||
|
|
@ -55,12 +55,12 @@ public sealed class RetailChaseCamera : ICamera
|
||||||
public Matrix4x4 Projection =>
|
public Matrix4x4 Projection =>
|
||||||
Matrix4x4.CreatePerspectiveFieldOfView(FovY, Aspect, 0.1f, 5000f);
|
Matrix4x4.CreatePerspectiveFieldOfView(FovY, Aspect, 0.1f, 5000f);
|
||||||
|
|
||||||
// ── Public tunables (per-instance) ──────────────────────────────
|
// ── Public tunables (per-instance) ──────────────────────────────
|
||||||
|
|
||||||
/// <summary>Length of the viewer_offset vector. Retail default ≈ 2.61.</summary>
|
/// <summary>Length of the viewer_offset vector. Retail default ≈ 2.61.</summary>
|
||||||
public float Distance { get; set; } = 2.61f;
|
public float Distance { get; set; } = 2.61f;
|
||||||
|
|
||||||
/// <summary>Angle of the camera above the heading-frame XY plane. Retail default ≈ 0.291 rad (16.7°).</summary>
|
/// <summary>Angle of the camera above the heading-frame XY plane. Retail default ≈ 0.291 rad (16.7°).</summary>
|
||||||
public float Pitch { get; set; } = 0.291f;
|
public float Pitch { get; set; } = 0.291f;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -92,27 +92,27 @@ public sealed class RetailChaseCamera : ICamera
|
||||||
public const float PitchMax = 1.4f;
|
public const float PitchMax = 1.4f;
|
||||||
|
|
||||||
// Retail CameraManager::UpdateCamera convergence-snap thresholds (decomp
|
// Retail CameraManager::UpdateCamera convergence-snap thresholds (decomp
|
||||||
// acclient_2013_pseudo_c.txt, 0x00456fcd–0x00457035). SnapEpsilon = 2 ×
|
// acclient_2013_pseudo_c.txt, 0x00456fcd–0x00457035). SnapEpsilon = 2 ×
|
||||||
// 0.000199999995 m ≈ 0.0004 m — the per-frame translation step below which retail
|
// 0.000199999995 m ≈ 0.0004 m — the per-frame translation step below which retail
|
||||||
// freezes the boom at an exact fixed point (0x00456fe1). RotCloseEpsilon =
|
// freezes the boom at an exact fixed point (0x00456fe1). RotCloseEpsilon =
|
||||||
// 0.000199999995 — the Frame::close_rotation tolerance (0x00456fdd). Without the
|
// 0.000199999995 — the Frame::close_rotation tolerance (0x00456fdd). Without the
|
||||||
// snap, Vector3.Lerp asymptotes forever and the boom drifts at rest, walking the eye
|
// snap, Vector3.Lerp asymptotes forever and the boom drifts at rest, walking the eye
|
||||||
// across a portal plane and flipping the viewer cell → the indoor flicker.
|
// across a portal plane and flipping the viewer cell → the indoor flicker.
|
||||||
private const float SnapEpsilon = 0.000199999995f * 2f;
|
private const float SnapEpsilon = 0.000199999995f * 2f;
|
||||||
private const float RotCloseEpsilon = 0.000199999995f;
|
private const float RotCloseEpsilon = 0.000199999995f;
|
||||||
|
|
||||||
// ── Stateful camera state (retail SmartBox's two Positions) ─────
|
// ── Stateful camera state (retail SmartBox's two Positions) ─────
|
||||||
//
|
//
|
||||||
// _soughtEye = retail viewer_sought_position — the persisted sweep
|
// _soughtEye = retail viewer_sought_position — the persisted sweep
|
||||||
// TARGET, re-derived each frame from the current swept
|
// TARGET, re-derived each frame from the current swept
|
||||||
// viewer (NOT from itself).
|
// viewer (NOT from itself).
|
||||||
// _publishedEye = retail viewer — the swept, published eye; the base
|
// _publishedEye = retail viewer — the swept, published eye; the base
|
||||||
// of next frame's interpolation (SmartBox::
|
// of next frame's interpolation (SmartBox::
|
||||||
// PlayerPhysicsUpdatedCallback passes &this->viewer
|
// PlayerPhysicsUpdatedCallback passes &this->viewer
|
||||||
// into UpdateCamera, 0x00452d75).
|
// into UpdateCamera, 0x00452d75).
|
||||||
// _dampedForward = the sought's look direction. Sweeps translate but
|
// _dampedForward = the sought's look direction. Sweeps translate but
|
||||||
// never rotate, so the viewer's rotation is always the
|
// never rotate, so the viewer's rotation is always the
|
||||||
// previous sought rotation — one field serves both.
|
// previous sought rotation — one field serves both.
|
||||||
|
|
||||||
private readonly Vector3[] _velocityRing = new Vector3[5];
|
private readonly Vector3[] _velocityRing = new Vector3[5];
|
||||||
private int _velocityCount;
|
private int _velocityCount;
|
||||||
|
|
@ -121,12 +121,12 @@ public sealed class RetailChaseCamera : ICamera
|
||||||
private Vector3 _dampedForward = new(1f, 0f, 0f);
|
private Vector3 _dampedForward = new(1f, 0f, 0f);
|
||||||
private bool _initialised;
|
private bool _initialised;
|
||||||
|
|
||||||
// Mouse-filter state — shared by FilterMouseDelta entrypoint.
|
// Mouse-filter state — shared by FilterMouseDelta entrypoint.
|
||||||
private float _lastMouseDeltaX;
|
private float _lastMouseDeltaX;
|
||||||
private float _lastMouseDeltaY;
|
private float _lastMouseDeltaY;
|
||||||
private float _lastFilterTimeSec;
|
private float _lastFilterTimeSec;
|
||||||
|
|
||||||
// ── Per-frame entry point ────────────────────────────────────────
|
// ── Per-frame entry point ────────────────────────────────────────
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Advance the camera one frame. Caller passes the player's current
|
/// Advance the camera one frame. Caller passes the player's current
|
||||||
|
|
@ -177,7 +177,7 @@ public sealed class RetailChaseCamera : ICamera
|
||||||
// 5. Stateful sought position (#180). Retail CameraManager::UpdateCamera
|
// 5. Stateful sought position (#180). Retail CameraManager::UpdateCamera
|
||||||
// (0x00456660) interpolates FROM THE CURRENT SWEPT VIEWER toward the
|
// (0x00456660) interpolates FROM THE CURRENT SWEPT VIEWER toward the
|
||||||
// desired pose and assigns the result to viewer_sought_position
|
// desired pose and assigns the result to viewer_sought_position
|
||||||
// (SmartBox::PlayerPhysicsUpdatedCallback 0x00452d60) — the sweep
|
// (SmartBox::PlayerPhysicsUpdatedCallback 0x00452d60) — the sweep
|
||||||
// target converges onto whatever the collision produced last frame
|
// target converges onto whatever the collision produced last frame
|
||||||
// and re-extends gradually. The full-length ideal boom is never swept
|
// and re-extends gradually. The full-length ideal boom is never swept
|
||||||
// directly. Pseudocode:
|
// directly. Pseudocode:
|
||||||
|
|
@ -193,15 +193,15 @@ public sealed class RetailChaseCamera : ICamera
|
||||||
{
|
{
|
||||||
float tAlpha = ComputeDampingAlpha(CameraDiagnostics.TranslationStiffness, dt);
|
float tAlpha = ComputeDampingAlpha(CameraDiagnostics.TranslationStiffness, dt);
|
||||||
float rAlpha = ComputeDampingAlpha(CameraDiagnostics.RotationStiffness, dt);
|
float rAlpha = ComputeDampingAlpha(CameraDiagnostics.RotationStiffness, dt);
|
||||||
// interpolate_origin(viewer.frame → desired, t) — the lerp base is the
|
// interpolate_origin(viewer.frame → desired, t) — the lerp base is the
|
||||||
// VIEWER (0x00456fae), not the previous sought. The forward base is the
|
// VIEWER (0x00456fae), not the previous sought. The forward base is the
|
||||||
// viewer's rotation ≡ the previous sought forward (sweeps never rotate).
|
// viewer's rotation ≡ the previous sought forward (sweeps never rotate).
|
||||||
Vector3 candidateEye = Vector3.Lerp(_publishedEye, targetEye, tAlpha);
|
Vector3 candidateEye = Vector3.Lerp(_publishedEye, targetEye, tAlpha);
|
||||||
Vector3 candidateForward = Vector3.Normalize(Vector3.Lerp(_dampedForward, targetForward, rAlpha));
|
Vector3 candidateForward = Vector3.Normalize(Vector3.Lerp(_dampedForward, targetForward, rAlpha));
|
||||||
|
|
||||||
// Retail UpdateCamera dead-band (0x00456fcd–0x00457035): once the step
|
// Retail UpdateCamera dead-band (0x00456fcd–0x00457035): once the step
|
||||||
// off the viewer is sub-epsilon in translation AND rotation, the sought
|
// off the viewer is sub-epsilon in translation AND rotation, the sought
|
||||||
// parks EXACTLY ON the viewer — an exact fixed point instead of an
|
// parks EXACTLY ON the viewer — an exact fixed point instead of an
|
||||||
// asymptote. Kills the at-rest drift AND the residual micro-jitter when
|
// asymptote. Kills the at-rest drift AND the residual micro-jitter when
|
||||||
// pressed against a wall. See ApplyConvergenceSnap + SnapEpsilon.
|
// pressed against a wall. See ApplyConvergenceSnap + SnapEpsilon.
|
||||||
(_soughtEye, _dampedForward, _) =
|
(_soughtEye, _dampedForward, _) =
|
||||||
|
|
@ -209,13 +209,13 @@ public sealed class RetailChaseCamera : ICamera
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5b. Spring-arm collision (A8.F / #180). Retail SmartBox::update_viewer
|
// 5b. Spring-arm collision (A8.F / #180). Retail SmartBox::update_viewer
|
||||||
// (0x00453ce0) sweeps the viewer_sphere pivot → viewer_sought_position
|
// (0x00453ce0) sweeps the viewer_sphere pivot → viewer_sought_position
|
||||||
// and publishes the swept result as the viewer (set_viewer(curr_pos, 0)
|
// and publishes the swept result as the viewer (set_viewer(curr_pos, 0)
|
||||||
// — the sought is NOT reset on success). Pressed against a wall, the
|
// — the sought is NOT reset on success). Pressed against a wall, the
|
||||||
// sweep ray extends only one interpolation step past the contact, so a
|
// sweep ray extends only one interpolation step past the contact, so a
|
||||||
// knife-edge r±ε graze can move the eye by at most that step (sub-mm at
|
// knife-edge r±ε graze can move the eye by at most that step (sub-mm at
|
||||||
// high fps) instead of re-solving the full-length boom with its 0.27 m
|
// high fps) instead of re-solving the full-length boom with its 0.27 m
|
||||||
// bistable contact pair — the #180 strobe fix.
|
// bistable contact pair — the #180 strobe fix.
|
||||||
Vector3 publishedEye = _soughtEye;
|
Vector3 publishedEye = _soughtEye;
|
||||||
// The viewer cell defaults to the player cell (collision off / null probe); the sweep
|
// The viewer cell defaults to the player cell (collision off / null probe); the sweep
|
||||||
// overwrites it with the swept cell (retail viewer_cell). Always set so GameWindow has a
|
// overwrites it with the swept cell (retail viewer_cell). Always set so GameWindow has a
|
||||||
|
|
@ -227,13 +227,13 @@ public sealed class RetailChaseCamera : ICamera
|
||||||
publishedEye = swept.Eye;
|
publishedEye = swept.Eye;
|
||||||
ViewerCellId = swept.ViewerCellId;
|
ViewerCellId = swept.ViewerCellId;
|
||||||
// Total-failure fallback = retail set_viewer(player_pos, reset_sought=1)
|
// Total-failure fallback = retail set_viewer(player_pos, reset_sought=1)
|
||||||
// (update_viewer :92886 and the cell==0 bail :92775 — both surface here as
|
// (update_viewer :92886 and the cell==0 bail :92775 — both surface here as
|
||||||
// ViewerCellId == 0): the sought resets to the returned position and
|
// ViewerCellId == 0): the sought resets to the returned position and
|
||||||
// re-extends from there.
|
// re-extends from there.
|
||||||
if (swept.ViewerCellId == 0)
|
if (swept.ViewerCellId == 0)
|
||||||
_soughtEye = swept.Eye;
|
_soughtEye = swept.Eye;
|
||||||
}
|
}
|
||||||
// Retail viewer — the base of next frame's interpolation (step 5).
|
// Retail viewer — the base of next frame's interpolation (step 5).
|
||||||
_publishedEye = publishedEye;
|
_publishedEye = publishedEye;
|
||||||
|
|
||||||
// 6. Publish renderer surface (from the collided eye; rotation stays the
|
// 6. Publish renderer surface (from the collided eye; rotation stays the
|
||||||
|
|
@ -241,7 +241,7 @@ public sealed class RetailChaseCamera : ICamera
|
||||||
Position = publishedEye;
|
Position = publishedEye;
|
||||||
View = Matrix4x4.CreateLookAt(publishedEye, publishedEye + _dampedForward, new Vector3(0f, 0f, 1f));
|
View = Matrix4x4.CreateLookAt(publishedEye, publishedEye + _dampedForward, new Vector3(0f, 0f, 1f));
|
||||||
|
|
||||||
// 7. Auto-fade translucency — uses the published (collided) eye so the
|
// 7. Auto-fade translucency — uses the published (collided) eye so the
|
||||||
// player fades once the eye is pulled in close.
|
// player fades once the eye is pulled in close.
|
||||||
float d = Vector3.Distance(publishedEye, pivotWorld);
|
float d = Vector3.Distance(publishedEye, pivotWorld);
|
||||||
PlayerTranslucency = ComputeTranslucency(d);
|
PlayerTranslucency = ComputeTranslucency(d);
|
||||||
|
|
@ -295,12 +295,12 @@ public sealed class RetailChaseCamera : ICamera
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public (float outX, float outY) FilterMouseDelta(float rawX, float rawY, float weight, float nowSec)
|
public (float outX, float outY) FilterMouseDelta(float rawX, float rawY, float weight, float nowSec)
|
||||||
{
|
{
|
||||||
// X first — advances the shared timestamp.
|
// X first — advances the shared timestamp.
|
||||||
float x = FilterMouseAxis(rawX, weight, nowSec,
|
float x = FilterMouseAxis(rawX, weight, nowSec,
|
||||||
ref _lastMouseDeltaX, ref _lastFilterTimeSec, CameraDiagnostics.MouseLowPassWindowSec);
|
ref _lastMouseDeltaX, ref _lastFilterTimeSec, CameraDiagnostics.MouseLowPassWindowSec);
|
||||||
// Y uses a throwaway timestamp so the within-window check still uses the original delta
|
// Y uses a throwaway timestamp so the within-window check still uses the original delta
|
||||||
// (X already advanced _lastFilterTimeSec to nowSec; if Y reused it, the within-window
|
// (X already advanced _lastFilterTimeSec to nowSec; if Y reused it, the within-window
|
||||||
// check would be 0 < windowSec which is always true — which is what we want here, since
|
// check would be 0 < windowSec which is always true — which is what we want here, since
|
||||||
// both axes are sampled simultaneously and should both blend.).
|
// both axes are sampled simultaneously and should both blend.).
|
||||||
float yTimeShadow = _lastFilterTimeSec - 1f; // force within-window path for the Y axis
|
float yTimeShadow = _lastFilterTimeSec - 1f; // force within-window path for the Y axis
|
||||||
float y = FilterMouseAxis(rawY, weight, nowSec,
|
float y = FilterMouseAxis(rawY, weight, nowSec,
|
||||||
|
|
@ -308,7 +308,7 @@ public sealed class RetailChaseCamera : ICamera
|
||||||
return (x, y);
|
return (x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Math primitives — pure, internal-static for unit-testability.
|
// Math primitives — pure, internal-static for unit-testability.
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Pick the heading vector that drives the camera basis. Mirrors
|
/// Pick the heading vector that drives the camera basis. Mirrors
|
||||||
|
|
@ -316,8 +316,8 @@ public sealed class RetailChaseCamera : ICamera
|
||||||
/// path (decomp <c>acclient_2013_pseudo_c.txt:95644-95795</c>):
|
/// path (decomp <c>acclient_2013_pseudo_c.txt:95644-95795</c>):
|
||||||
/// <list type="number">
|
/// <list type="number">
|
||||||
/// <item><description>Base heading is the player's facing
|
/// <item><description>Base heading is the player's facing
|
||||||
/// direction in world space — <c>(cos yaw, sin yaw, 0)</c>
|
/// direction in world space — <c>(cos yaw, sin yaw, 0)</c>
|
||||||
/// — not the velocity vector. Velocity only gates whether
|
/// — not the velocity vector. Velocity only gates whether
|
||||||
/// slope-alignment fires.</description></item>
|
/// slope-alignment fires.</description></item>
|
||||||
/// <item><description>If <paramref name="alignToSlope"/> is off
|
/// <item><description>If <paramref name="alignToSlope"/> is off
|
||||||
/// OR the player's horizontal velocity is below epsilon (i.e.
|
/// OR the player's horizontal velocity is below epsilon (i.e.
|
||||||
|
|
@ -337,7 +337,7 @@ public sealed class RetailChaseCamera : ICamera
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="avgVelocity">5-frame averaged player velocity in world space.</param>
|
/// <param name="avgVelocity">5-frame averaged player velocity in world space.</param>
|
||||||
/// <param name="yaw">Player facing yaw + any orbit offset, radians.</param>
|
/// <param name="yaw">Player facing yaw + any orbit offset, radians.</param>
|
||||||
/// <param name="isOnGround">Player's <c>transient_state & 1</c> — does <paramref name="contactPlaneNormal"/> describe a valid contact plane?</param>
|
/// <param name="isOnGround">Player's <c>transient_state & 1</c> — does <paramref name="contactPlaneNormal"/> describe a valid contact plane?</param>
|
||||||
/// <param name="contactPlaneNormal">Player's current contact plane normal in world space; ignored when <paramref name="isOnGround"/> is false.</param>
|
/// <param name="contactPlaneNormal">Player's current contact plane normal in world space; ignored when <paramref name="isOnGround"/> is false.</param>
|
||||||
/// <param name="alignToSlope">User-tunable; when false skips the projection and returns the flat facing direction.</param>
|
/// <param name="alignToSlope">User-tunable; when false skips the projection and returns the flat facing direction.</param>
|
||||||
internal static Vector3 ComputeHeading(
|
internal static Vector3 ComputeHeading(
|
||||||
|
|
@ -356,15 +356,15 @@ public sealed class RetailChaseCamera : ICamera
|
||||||
// |vx| > 0.0002 AND |vy| > 0.0002 (decomp :95704, :95713). The
|
// |vx| > 0.0002 AND |vy| > 0.0002 (decomp :95704, :95713). The
|
||||||
// horizontal-magnitude-squared form is a cleaner equivalent.
|
// horizontal-magnitude-squared form is a cleaner equivalent.
|
||||||
// Without this, the airborne path would still project against
|
// Without this, the airborne path would still project against
|
||||||
// world up (no-op) which is fine — but the standing-jump case
|
// world up (no-op) which is fine — but the standing-jump case
|
||||||
// wants the historical `direction` fallback that retail uses.
|
// wants the historical `direction` fallback that retail uses.
|
||||||
float hMagSq = avgVelocity.X * avgVelocity.X + avgVelocity.Y * avgVelocity.Y;
|
float hMagSq = avgVelocity.X * avgVelocity.X + avgVelocity.Y * avgVelocity.Y;
|
||||||
if (hMagSq < 1e-4f) return baseHeading;
|
if (hMagSq < 1e-4f) return baseHeading;
|
||||||
|
|
||||||
// Pick the projection plane normal:
|
// Pick the projection plane normal:
|
||||||
// grounded → contact_plane.N (slope-aligned camera basis)
|
// grounded → contact_plane.N (slope-aligned camera basis)
|
||||||
// airborne → world up (projection becomes a no-op because
|
// airborne → world up (projection becomes a no-op because
|
||||||
// baseHeading is already in the XY plane — but
|
// baseHeading is already in the XY plane — but
|
||||||
// keeping the code path uniform makes the airborne
|
// keeping the code path uniform makes the airborne
|
||||||
// case impossible to swing vertically).
|
// case impossible to swing vertically).
|
||||||
Vector3 normal;
|
Vector3 normal;
|
||||||
|
|
@ -375,13 +375,13 @@ public sealed class RetailChaseCamera : ICamera
|
||||||
|
|
||||||
// Project baseHeading onto plane perpendicular to normal:
|
// Project baseHeading onto plane perpendicular to normal:
|
||||||
// projected = forward - normal * dot(forward, normal)
|
// projected = forward - normal * dot(forward, normal)
|
||||||
// On flat ground this is a no-op (dot ≈ 0). On a slope the
|
// On flat ground this is a no-op (dot ≈ 0). On a slope the
|
||||||
// projected vector gains a Z component matching the slope angle,
|
// projected vector gains a Z component matching the slope angle,
|
||||||
// which tilts the camera basis with the terrain.
|
// which tilts the camera basis with the terrain.
|
||||||
float dot = Vector3.Dot(baseHeading, normal);
|
float dot = Vector3.Dot(baseHeading, normal);
|
||||||
Vector3 projected = baseHeading - normal * dot;
|
Vector3 projected = baseHeading - normal * dot;
|
||||||
|
|
||||||
// Degenerate: facing nearly parallel to normal (rare — would
|
// Degenerate: facing nearly parallel to normal (rare — would
|
||||||
// require player rotated to face into the ground). Fall back to
|
// require player rotated to face into the ground). Fall back to
|
||||||
// the unprojected base heading.
|
// the unprojected base heading.
|
||||||
if (projected.LengthSquared() < 1e-4f) return baseHeading;
|
if (projected.LengthSquared() < 1e-4f) return baseHeading;
|
||||||
|
|
@ -449,7 +449,7 @@ public sealed class RetailChaseCamera : ICamera
|
||||||
Vector3 right;
|
Vector3 right;
|
||||||
if (MathF.Abs(forward.Z) > 0.99f)
|
if (MathF.Abs(forward.Z) > 0.99f)
|
||||||
{
|
{
|
||||||
// Near-vertical forward — use world +X as the secondary axis.
|
// Near-vertical forward — use world +X as the secondary axis.
|
||||||
right = Vector3.Normalize(Vector3.Cross(forward, new Vector3(1f, 0f, 0f)));
|
right = Vector3.Normalize(Vector3.Cross(forward, new Vector3(1f, 0f, 0f)));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
@ -495,7 +495,7 @@ public sealed class RetailChaseCamera : ICamera
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Exponential-damping rate per frame.
|
/// Exponential-damping rate per frame.
|
||||||
/// <c>alpha = clamp(stiffness * dt * 10, 0, 1)</c>. At
|
/// <c>alpha = clamp(stiffness * dt * 10, 0, 1)</c>. At
|
||||||
/// <c>stiffness=0.45</c>, <c>dt=1/60</c> → <c>~0.075</c>
|
/// <c>stiffness=0.45</c>, <c>dt=1/60</c> → <c>~0.075</c>
|
||||||
/// (~150 ms half-life). Matches retail's
|
/// (~150 ms half-life). Matches retail's
|
||||||
/// <c>x_1 = stiffness * dt * 10</c> formulation.
|
/// <c>x_1 = stiffness * dt * 10</c> formulation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -508,11 +508,11 @@ public sealed class RetailChaseCamera : ICamera
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retail <c>CameraManager::UpdateCamera</c> dead-band (decomp 0x00456fcd–0x00457035).
|
/// Retail <c>CameraManager::UpdateCamera</c> dead-band (decomp 0x00456fcd–0x00457035).
|
||||||
/// After the per-frame lerp, if the translation step from <paramref name="viewerEye"/>
|
/// After the per-frame lerp, if the translation step from <paramref name="viewerEye"/>
|
||||||
/// (the interpolation base = the current swept viewer) to <paramref name="candidateEye"/>
|
/// (the interpolation base = the current swept viewer) to <paramref name="candidateEye"/>
|
||||||
/// is below <see cref="SnapEpsilon"/> AND the rotation step is below
|
/// is below <see cref="SnapEpsilon"/> AND the rotation step is below
|
||||||
/// <see cref="RotCloseEpsilon"/>, retail returns the VIEWER unchanged — the sought
|
/// <see cref="RotCloseEpsilon"/>, retail returns the VIEWER unchanged — the sought
|
||||||
/// parks exactly on it (<c>return viewer</c>, 0x00457025). Returns <c>frozen=true</c>
|
/// parks exactly on it (<c>return viewer</c>, 0x00457025). Returns <c>frozen=true</c>
|
||||||
/// with the viewer state in that case; otherwise <c>frozen=false</c> with the candidate.
|
/// with the viewer state in that case; otherwise <c>frozen=false</c> with the candidate.
|
||||||
/// Both conditions are required (retail couples origin + rotation in the test),
|
/// Both conditions are required (retail couples origin + rotation in the test),
|
||||||
|
|
@ -562,7 +562,7 @@ public sealed class RetailChaseCamera : ICamera
|
||||||
/// distance. <c>0</c> = fully opaque, <c>1</c> = fully transparent.
|
/// distance. <c>0</c> = fully opaque, <c>1</c> = fully transparent.
|
||||||
/// Opaque at and beyond 0.45 m; fully transparent at and within
|
/// Opaque at and beyond 0.45 m; fully transparent at and within
|
||||||
/// 0.20 m; linear ramp between. Matches retail's <c>CameraSet::
|
/// 0.20 m; linear ramp between. Matches retail's <c>CameraSet::
|
||||||
/// UpdateCamera</c> distance check (decomp :97703–97725).
|
/// UpdateCamera</c> distance check (decomp :97703–97725).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static float ComputeTranslucency(float distance)
|
internal static float ComputeTranslucency(float distance)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using AcDream.App.UI;
|
using AcDream.App.UI;
|
||||||
using AcDream.Core.Textures;
|
using AcDream.Core.Textures;
|
||||||
using DatReaderWriter;
|
using DatReaderWriter;
|
||||||
using AcDream.Content;
|
using AcDream.Content;
|
||||||
|
|
@ -9,7 +9,7 @@ using Silk.NET.Input;
|
||||||
namespace AcDream.App.Rendering;
|
namespace AcDream.App.Rendering;
|
||||||
|
|
||||||
/// <summary>Applies retail cursor feedback to Silk using dat MediaDescCursor art when available.</summary>
|
/// <summary>Applies retail cursor feedback to Silk using dat MediaDescCursor art when available.</summary>
|
||||||
public sealed class RetailCursorManager
|
internal sealed class RetailCursorManager
|
||||||
{
|
{
|
||||||
private readonly IDatReaderWriter _dats;
|
private readonly IDatReaderWriter _dats;
|
||||||
private readonly object _datLock;
|
private readonly object _datLock;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AcDream.App.Rendering.Scene;
|
using AcDream.App.Rendering.Scene;
|
||||||
|
|
@ -11,7 +11,7 @@ namespace AcDream.App.Rendering;
|
||||||
/// SmartBox::RenderNormalMode -> RenderDeviceD3D::DrawInside ->
|
/// SmartBox::RenderNormalMode -> RenderDeviceD3D::DrawInside ->
|
||||||
/// PView::DrawInside -> ConstructView -> DrawCells.
|
/// PView::DrawInside -> ConstructView -> DrawCells.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class RetailPViewRenderer
|
internal sealed class RetailPViewRenderer
|
||||||
{
|
{
|
||||||
private readonly InteriorEntityPartition.IObserver? _partitionObserver;
|
private readonly InteriorEntityPartition.IObserver? _partitionObserver;
|
||||||
private readonly ICurrentRenderPViewObserver? _candidateObserver;
|
private readonly ICurrentRenderPViewObserver? _candidateObserver;
|
||||||
|
|
@ -44,7 +44,7 @@ public sealed class RetailPViewRenderer
|
||||||
private readonly PortalVisibilityFrame _outdoorBuildingFrameScratch = new();
|
private readonly PortalVisibilityFrame _outdoorBuildingFrameScratch = new();
|
||||||
|
|
||||||
// #124: per-building look-in frames under an INTERIOR root, drawn as a
|
// #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.
|
// main frame (see DrawInside). Rebuilt each interior-root frame.
|
||||||
private readonly List<PortalVisibilityFrame> _lookInFrames = new();
|
private readonly List<PortalVisibilityFrame> _lookInFrames = new();
|
||||||
private readonly Stack<PortalVisibilityFrame> _lookInFramePool = new();
|
private readonly Stack<PortalVisibilityFrame> _lookInFramePool = new();
|
||||||
|
|
@ -59,7 +59,7 @@ public sealed class RetailPViewRenderer
|
||||||
// MP-Alloc (2026-07-05): the frame's entity partition (ByCell/OutdoorStatic/
|
// MP-Alloc (2026-07-05): the frame's entity partition (ByCell/OutdoorStatic/
|
||||||
// Dynamics), reused across frames instead of `new`ing a Result (a Dictionary
|
// Dynamics), reused across frames instead of `new`ing a Result (a Dictionary
|
||||||
// + 2 Lists, plus one List<WorldEntity> per visible cell) every DrawInside
|
// + 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
|
// place and reuses each cell's list across frames when the cell stays
|
||||||
// visible.
|
// visible.
|
||||||
// Slice G4: this is now a diagnostic/fallback oracle only. Normal
|
// Slice G4: this is now a diagnostic/fallback oracle only. Normal
|
||||||
|
|
@ -88,7 +88,7 @@ public sealed class RetailPViewRenderer
|
||||||
}
|
}
|
||||||
|
|
||||||
// T2 (BR-4): retail has NO distance constant on the flood-admission chain
|
// 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
|
// 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
|
// caller's per-building frustum pre-gate on aperture bounds (GameWindow's
|
||||||
// gather); seeds themselves are unbounded.
|
// gather); seeds themselves are unbounded.
|
||||||
|
|
@ -113,28 +113,28 @@ public sealed class RetailPViewRenderer
|
||||||
drawLiftZ: PortalVisibilityBuilder.ShellDrawLiftZ,
|
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
|
||||||
// the small (~2-cell) per-building views into the frame. Retail reaches building interiors via
|
// 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
|
// 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
|
// 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).
|
// oscillated as the chase eye grazed a doorway (the indoor flap).
|
||||||
if (ctx.RootCell.IsOutdoorNode && ctx.NearbyBuildingCells is not null)
|
if (ctx.RootCell.IsOutdoorNode && ctx.NearbyBuildingCells is not null)
|
||||||
MergeNearbyBuildingFloods(ctx, pvFrame);
|
MergeNearbyBuildingFloods(ctx, pvFrame);
|
||||||
|
|
||||||
// #124: interior-root building look-ins. Retail runs the look-in INSIDE
|
// #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
|
// DrawCells' outside-view branch (pc:432719), strictly BEFORE the depth
|
||||||
// clear (pc:432732) and the exit-portal seals (pc:432785); a far
|
// clear (pc:432732) and the exit-portal seals (pc:432785); a far
|
||||||
// building seen through our doorway floods clipped to the INSTALLED
|
// building seen through our doorway floods clipped to the INSTALLED
|
||||||
// outside view (GetClip vs current view, ConstructView(CBldPortal)
|
// outside view (GetClip vs current view, ConstructView(CBldPortal)
|
||||||
// 0x005a59a0). These frames therefore draw in DrawBuildingLookIns
|
// 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
|
// 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
|
// (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
|
// self-excludes the root's own building (the eye is on its interior
|
||||||
// side). Outdoor roots keep the MergeNearbyBuildingFloods path above
|
// 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).
|
// there).
|
||||||
if (!ctx.RootCell.IsOutdoorNode
|
if (!ctx.RootCell.IsOutdoorNode
|
||||||
&& ctx.NearbyBuildingCells is not null
|
&& ctx.NearbyBuildingCells is not null
|
||||||
|
|
@ -149,7 +149,7 @@ public sealed class RetailPViewRenderer
|
||||||
|
|
||||||
// R1: draw EVERY visible cell (retail cell_draw_list), not only the cells the
|
// 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,
|
// 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).
|
// (the old clipAssembly.CellIdToSlot.Keys filter silently dropped slot-less cells).
|
||||||
// Per-slice trim still applies in DrawEnvCellShells (Task 4 makes it self-contained).
|
// Per-slice trim still applies in DrawEnvCellShells (Task 4 makes it self-contained).
|
||||||
_drawableCellsScratch.Clear();
|
_drawableCellsScratch.Clear();
|
||||||
|
|
@ -158,7 +158,7 @@ public sealed class RetailPViewRenderer
|
||||||
passes.UseIndoorMembershipOnlyRouting();
|
passes.UseIndoorMembershipOnlyRouting();
|
||||||
|
|
||||||
// #124: look-in cells need prepared shell batches + their statics routed
|
// #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
|
// cell-object pass iterates pvFrame.OrderedVisibleCells, which never
|
||||||
// contains them). drawableCells itself stays the MAIN flood: it feeds the
|
// contains them). drawableCells itself stays the MAIN flood: it feeds the
|
||||||
// seals, the outside-stage predicate, and the frame result.
|
// seals, the outside-stage predicate, and the frame result.
|
||||||
|
|
@ -174,22 +174,22 @@ public sealed class RetailPViewRenderer
|
||||||
}
|
}
|
||||||
|
|
||||||
// (#176 correction, 2026-07-06: the flood-scoped light-pool rebuild that ran
|
// (#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
|
// 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
|
// the RESIDENT-cell registry, not the frame flood — and is deleted. The pool
|
||||||
// is built once per frame in GameWindow, player-anchored.)
|
// is built once per frame in GameWindow, player-anchored.)
|
||||||
|
|
||||||
passes.PrepareCellBatches(ctx, prepareCells);
|
passes.PrepareCellBatches(ctx, prepareCells);
|
||||||
|
|
||||||
// T1 (fused BR-2/3): retail's frame order — static world, then the
|
// T1 (fused BR-2/3): retail's frame order — static world, then the
|
||||||
// aperture depth writes, then interior cells WHOLE far→near, then
|
// aperture depth writes, then interior cells WHOLE far→near, then
|
||||||
// per-cell statics, then ALL dynamics last (retail draws objects after
|
// per-cell statics, then ALL dynamics last (retail draws objects after
|
||||||
// cells: PView::DrawCells Ghidra 0x005a4840; DrawBuilding 0x0059f2a0).
|
// cells: PView::DrawCells Ghidra 0x005a4840; DrawBuilding 0x0059f2a0).
|
||||||
// The geometric shell chop (gl_ClipDistance crop, 927fd8f/9ce335e) is
|
// 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-
|
// 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
|
// last order is what makes the punch safe (the first BR-2 attempt
|
||||||
// punched after dynamics and erased the player, reverted 88be519).
|
// 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
|
// never clipped (Ghidra 0x0054c250). Built once per frame from the
|
||||||
// assembled slices + this frame's view-projection.
|
// assembled slices + this frame's view-projection.
|
||||||
var viewcone = ViewconeCuller.Build(
|
var viewcone = ViewconeCuller.Build(
|
||||||
|
|
@ -256,19 +256,19 @@ public sealed class RetailPViewRenderer
|
||||||
passes.EmitDiagnostics(ctx, result);
|
passes.EmitDiagnostics(ctx, result);
|
||||||
|
|
||||||
// #118: stage assignment for dynamics under an INTERIOR root. Retail
|
// #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
|
// PView::DrawCells runs LScape::draw FIRST (pc:432719), then the gated
|
||||||
// full depth clear (pc:432731-432732) and the exit-portal SEALS
|
// full depth clear (pc:432731-432732) and the exit-portal SEALS
|
||||||
// (pc:432785-432786); DrawBlock draws every landcell's objects via
|
// (pc:432785-432786); DrawBlock draws every landcell's objects via
|
||||||
// DrawSortCell (0x005a17c0, pc:430124). A dynamic deferred to our
|
// DrawSortCell (0x005a17c0, pc:430124). A dynamic deferred to our
|
||||||
// single last pass instead z-fails against the seal's true-depth stamp
|
// 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
|
// clip+vanish (pinned by HouseExitWalkReplayTests). So under an
|
||||||
// interior root: outdoor-classified dynamics draw in the outside
|
// interior root: outdoor-classified dynamics draw in the outside
|
||||||
// stage; an indoor dynamic whose sphere STRADDLES an exit portal
|
// stage; an indoor dynamic whose sphere STRADDLES an exit portal
|
||||||
// draws in BOTH stages (retail's per-overlapped-cell shadow-part
|
// draws in BOTH stages (retail's per-overlapped-cell shadow-part
|
||||||
// draw, DrawBlock pc:430056-430064) so neither body half clips at the
|
// 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
|
// z-buffered equivalent of retail's painter-ordered outdoor pass (the
|
||||||
// BR-2 punch-after-dynamics lesson, reverted 88be519).
|
// BR-2 punch-after-dynamics lesson, reverted 88be519).
|
||||||
_outsideStageDynamics.Clear();
|
_outsideStageDynamics.Clear();
|
||||||
|
|
@ -362,11 +362,11 @@ public sealed class RetailPViewRenderer
|
||||||
// on the cell (Render::copy_view appends + view_count++, Ghidra 0x0054dfc0;
|
// on the cell (Render::copy_view appends + view_count++, Ghidra 0x0054dfc0;
|
||||||
// a cell visible through two apertures holds two views, all consumed
|
// a cell visible through two apertures holds two views, all consumed
|
||||||
// downstream). The old first-wins (`ContainsKey -> continue`) dropped the
|
// 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
|
// the multiview-loss-first-wins divergence (a named #109 suspect: per-frame
|
||||||
// winner flips between apertures). CellView.Add dedups exact/collinear
|
// winner flips between apertures). CellView.Add dedups exact/collinear
|
||||||
// re-emissions (the dac8f6a CanonicalKey), so unioning is convergent.
|
// 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
|
// terrain, and ConstructViewBuilding (BuildFromExterior) leaves OutsideView
|
||||||
// empty (it stops at exit portals once inside the building).
|
// empty (it stops at exit portals once inside the building).
|
||||||
private static void MergeBuildingFrame(PortalVisibilityFrame target, PortalVisibilityFrame src)
|
private static void MergeBuildingFrame(PortalVisibilityFrame target, PortalVisibilityFrame src)
|
||||||
|
|
@ -393,8 +393,8 @@ public sealed class RetailPViewRenderer
|
||||||
}
|
}
|
||||||
|
|
||||||
// #124: per-building look-in floods for an INTERIOR root, seeded clipped
|
// #124: per-building look-in floods for an INTERIOR root, seeded clipped
|
||||||
// against the OutsideView (retail: GetClip runs under the INSTALLED view —
|
// against the OutsideView (retail: GetClip runs under the INSTALLED view —
|
||||||
// the accumulated doorway region — so a far building floods only within the
|
// the accumulated doorway region — so a far building floods only within the
|
||||||
// doorway, ConstructView(CBldPortal) 0x005a59a0 via PView::GetClip
|
// doorway, ConstructView(CBldPortal) 0x005a59a0 via PView::GetClip
|
||||||
// 0x005a4320). Same grouping as MergeNearbyBuildingFloods; the root's own
|
// 0x005a4320). Same grouping as MergeNearbyBuildingFloods; the root's own
|
||||||
// building self-excludes via the seed eye-side test.
|
// building self-excludes via the seed eye-side test.
|
||||||
|
|
@ -445,15 +445,15 @@ public sealed class RetailPViewRenderer
|
||||||
private void ResetBuildingGroups()
|
private void ResetBuildingGroups()
|
||||||
=> _buildingGroups.Reset();
|
=> _buildingGroups.Reset();
|
||||||
|
|
||||||
// #124: draw the interior-root look-ins INSIDE the landscape stage —
|
// #124: draw the interior-root look-ins INSIDE the landscape stage —
|
||||||
// retail's placement (LScape::draw → DrawBlock → DrawSortCell →
|
// retail's placement (LScape::draw → DrawBlock → DrawSortCell →
|
||||||
// DrawBuilding runs as the FIRST call of DrawCells' outside-view branch,
|
// DrawBuilding runs as the FIRST call of DrawCells' outside-view branch,
|
||||||
// pc:432719, before the depth clear + seals). Per building: punch ALL
|
// pc:432719, before the depth clear + seals). Per building: punch ALL
|
||||||
// apertures first (retail finishes build_draw_portals_only pass 1 — the
|
// apertures first (retail finishes build_draw_portals_only pass 1 — the
|
||||||
// far-Z maxZ1 punch — across the whole building BSP before pass 2 floods),
|
// far-Z maxZ1 punch — across the whole building BSP before pass 2 floods),
|
||||||
// then draw the flooded cells' shells + statics far→near (the nested
|
// then draw the flooded cells' shells + statics far→near (the nested
|
||||||
// DrawCells' DrawEnvCell + DrawObjCellForDummies; its outside_view is
|
// 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
|
// landscape/clear/seal). Anything rasterized outside an aperture is
|
||||||
// repainted by the root's own shells after the depth clear, so over-draw
|
// 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
|
// here is color-safe; statics draw whole (the main viewcone has no entry
|
||||||
|
|
@ -491,12 +491,12 @@ public sealed class RetailPViewRenderer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pass 2: shells + statics, far→near.
|
// Pass 2: shells + statics, far→near.
|
||||||
passes.UseIndoorMembershipOnlyRouting();
|
passes.UseIndoorMembershipOnlyRouting();
|
||||||
|
|
||||||
// Opaque shells batched per building into ONE Render (this building's
|
// Opaque shells batched per building into ONE Render (this building's
|
||||||
// aperture punches above already ran; z-buffer handles order and
|
// 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.
|
// Render per cell. Per-cell entity/particle work stays in the loop.
|
||||||
_shellBatch.Clear();
|
_shellBatch.Clear();
|
||||||
foreach (uint cid in frame.OrderedVisibleCells)
|
foreach (uint cid in frame.OrderedVisibleCells)
|
||||||
|
|
@ -509,7 +509,7 @@ public sealed class RetailPViewRenderer
|
||||||
uint cellId = frame.OrderedVisibleCells[i];
|
uint cellId = frame.OrderedVisibleCells[i];
|
||||||
_oneCell.Clear();
|
_oneCell.Clear();
|
||||||
_oneCell.Add(cellId);
|
_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.
|
// for correct compositing; skipped for opaque-only cells.
|
||||||
if (passes.CellHasTransparentShell(cellId))
|
if (passes.CellHasTransparentShell(cellId))
|
||||||
passes.DrawTransparentCellShells(_oneCell);
|
passes.DrawTransparentCellShells(_oneCell);
|
||||||
|
|
@ -523,7 +523,7 @@ public sealed class RetailPViewRenderer
|
||||||
|
|
||||||
// #131 ROOT CAUSE: DYNAMICS living in a look-in cell (the
|
// #131 ROOT CAUSE: DYNAMICS living in a look-in cell (the
|
||||||
// Holtburg hall-porch PORTAL, pCell 0xA9B4017A) draw NOWHERE
|
// 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
|
// them (the main cone has no entries for look-in cells), and
|
||||||
// post-clear they would z-fail against the root's seal anyway
|
// post-clear they would z-fail against the root's seal anyway
|
||||||
// (the #118 lesson). Retail draws a look-in cell's objects
|
// (the #118 lesson). Retail draws a look-in cell's objects
|
||||||
|
|
@ -576,7 +576,7 @@ public sealed class RetailPViewRenderer
|
||||||
_cellStaticScratch,
|
_cellStaticScratch,
|
||||||
_oneCell);
|
_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.
|
// nested DrawCells draws objects WITH their emitters.
|
||||||
foreach (var slice in GetCellSlicesOrNoClip(clipAssembly, cellId))
|
foreach (var slice in GetCellSlicesOrNoClip(clipAssembly, cellId))
|
||||||
passes.DrawCellParticles(ctx, new RetailPViewCellSliceContext(
|
passes.DrawCellParticles(ctx, new RetailPViewCellSliceContext(
|
||||||
|
|
@ -600,7 +600,7 @@ public sealed class RetailPViewRenderer
|
||||||
|
|
||||||
// #131/#132 (the FlushAlphaList deferral): retail collects ALL alpha
|
// #131/#132 (the FlushAlphaList deferral): retail collects ALL alpha
|
||||||
// draws of the landscape stage and flushes them ONCE after LScape::draw
|
// 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
|
// landscape content (portal swirl meshes, flame particles) composites
|
||||||
// AFTER the building look-ins. Our dispatcher draws translucency inside
|
// AFTER the building look-ins. Our dispatcher draws translucency inside
|
||||||
// each Draw call, so the stage is split in TWO phases instead: EARLY =
|
// each Draw call, so the stage is split in TWO phases instead: EARLY =
|
||||||
|
|
@ -609,12 +609,12 @@ public sealed class RetailPViewRenderer
|
||||||
// LATE = outside-stage dynamics' meshes + ALL scene particles +
|
// LATE = outside-stage dynamics' meshes + ALL scene particles +
|
||||||
// weather. Content drawn early and overlapped by a look-in aperture
|
// weather. Content drawn early and overlapped by a look-in aperture
|
||||||
// was otherwise overpainted by the far interior (translucents write no
|
// 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;
|
int probeSliceIndex = 0;
|
||||||
foreach (var slice in clipAssembly.OutsideViewSlices)
|
foreach (var slice in clipAssembly.OutsideViewSlices)
|
||||||
{
|
{
|
||||||
passes.SetTerrainClip(slice.Planes);
|
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)
|
// CHECKS each mesh's sphere against the view (Ghidra 0x0054c250)
|
||||||
// and draws it whole. The old per-slice entity clip routing
|
// and draws it whole. The old per-slice entity clip routing
|
||||||
// (gl_ClipDistance via SetClipRouting) is replaced by the sphere
|
// (gl_ClipDistance via SetClipRouting) is replaced by the sphere
|
||||||
|
|
@ -663,7 +663,7 @@ public 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
|
// stage (their punches mark against the terrain/exterior depth just
|
||||||
// drawn), strictly BEFORE the depth clear + seals below, matching
|
// drawn), strictly BEFORE the depth clear + seals below, matching
|
||||||
// retail's LScape::draw placement (DrawCells pc:432719 vs 432732/432785).
|
// retail's LScape::draw placement (DrawCells pc:432719 vs 432732/432785).
|
||||||
|
|
@ -675,11 +675,11 @@ public sealed class RetailPViewRenderer
|
||||||
frameEntityPasses,
|
frameEntityPasses,
|
||||||
in frameView);
|
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
|
// pre-clear so the seal protects their aperture pixels; AFTER the
|
||||||
// look-ins so a translucent portal mesh blends over a far interior
|
// look-ins so a translucent portal mesh blends over a far interior
|
||||||
// instead of being overpainted) + the scene-particle owners (statics +
|
// 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;
|
probeSliceIndex = 0;
|
||||||
foreach (var slice in clipAssembly.OutsideViewSlices)
|
foreach (var slice in clipAssembly.OutsideViewSlices)
|
||||||
{
|
{
|
||||||
|
|
@ -700,7 +700,7 @@ public sealed class RetailPViewRenderer
|
||||||
if (ownerPass)
|
if (ownerPass)
|
||||||
_lateParticleOwnerScratch.Add(e.Id);
|
_lateParticleOwnerScratch.Add(e.Id);
|
||||||
// #131 owner watchlist (throwaway): ACDREAM_DUMP_ENTITY ids
|
// #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.
|
// outdoor-static owner per CHANGE of its cone verdict.
|
||||||
passes.EmitOutStageOwner(
|
passes.EmitOutStageOwner(
|
||||||
e,
|
e,
|
||||||
|
|
@ -764,11 +764,11 @@ public 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
|
// campfires, ground effects anchored at a position) have no owner id
|
||||||
// to ride any of the id-filtered particle passes. The outdoor root
|
// 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
|
// 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
|
// 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.
|
// stage: after the clear they would z-fail against the doorway seal.
|
||||||
if (!ctx.RootCell.IsOutdoorNode)
|
if (!ctx.RootCell.IsOutdoorNode)
|
||||||
|
|
@ -780,7 +780,7 @@ public sealed class RetailPViewRenderer
|
||||||
passes.FlushLandscapeAlpha();
|
passes.FlushLandscapeAlpha();
|
||||||
|
|
||||||
// T1: retail clears the FULL depth buffer ONCE between the outside
|
// 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
|
// Clear gated on portalsDrawnCount; exact gate semantics is a plan
|
||||||
// open question, staged as "any outside slice drawn"), then re-stamps
|
// open question, staged as "any outside slice drawn"), then re-stamps
|
||||||
// every outside-leading portal's TRUE depth (the seals,
|
// every outside-leading portal's TRUE depth (the seals,
|
||||||
|
|
@ -819,20 +819,20 @@ public sealed class RetailPViewRenderer
|
||||||
IRetailPViewPassExecutor passes,
|
IRetailPViewPassExecutor passes,
|
||||||
PortalVisibilityFrame pvFrame)
|
PortalVisibilityFrame pvFrame)
|
||||||
{
|
{
|
||||||
// T1 (fused BR-2/3): retail DrawCells Loop 2 — every visible cell's
|
// T1 (fused BR-2/3): retail DrawCells Loop 2 — every visible cell's
|
||||||
// shell drawn WHOLE, reverse cell_draw_list (far→near), drawn once.
|
// shell drawn WHOLE, reverse cell_draw_list (far→near), drawn once.
|
||||||
// Retail NEVER clips cell geometry: the production path is the
|
// Retail NEVER clips cell geometry: the production path is the
|
||||||
// prebuilt mesh (DrawEnvCell use_built_mesh, pc:427905; the
|
// prebuilt mesh (DrawEnvCell use_built_mesh, pc:427905; the
|
||||||
// planeMask=0xffffffff legacy submit means skip-all-edges), and
|
// planeMask=0xffffffff legacy submit means skip-all-edges), and
|
||||||
// aperture exactness comes from the punch/seal depth writes + the
|
// aperture exactness comes from the punch/seal depth writes + the
|
||||||
// z-buffer + this order. The former gl_ClipDistance chop
|
// z-buffer + this order. The former gl_ClipDistance chop
|
||||||
// (927fd8f/9ce335e, #114) is deleted with this rewrite.
|
// (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.
|
// compositing the per-cell loop already provided.
|
||||||
passes.UseIndoorMembershipOnlyRouting();
|
passes.UseIndoorMembershipOnlyRouting();
|
||||||
|
|
||||||
// Opaque: ONE batched Render for all shell cells (was one heavy per-frame
|
// 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
|
// Arwic). Opaque needs no draw order (z-buffer), and lighting is
|
||||||
// per-instance (CellId-keyed light SSBO in EnvCellRenderer.RenderModernMDI-
|
// per-instance (CellId-keyed light SSBO in EnvCellRenderer.RenderModernMDI-
|
||||||
// Internal), so cross-cell batching is visually identical. The filtered
|
// Internal), so cross-cell batching is visually identical. The filtered
|
||||||
|
|
@ -856,16 +856,16 @@ public sealed class RetailPViewRenderer
|
||||||
passes.DrawTransparentCellShellsOrdered(_orderedTransparentShellCells);
|
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
|
// (player, NPCs, doors, items), indoor or out, drawn after the static
|
||||||
// world + punches + interior cells. Depth-tested, never hard-clipped
|
// 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
|
// PView::DrawCells epilogue Ghidra 0x005a4840; the sphere-vs-view cull is
|
||||||
// T3). Drawing dynamics last is what makes the aperture punch safe.
|
// 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
|
// 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
|
// 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
|
// the draw list; the partition keeps routing it so the CULL (not the
|
||||||
// visibility set) drops it, exactly retail's shape.
|
// visibility set) drops it, exactly retail's shape.
|
||||||
private void DrawDynamicsLast(
|
private void DrawDynamicsLast(
|
||||||
|
|
@ -928,12 +928,12 @@ public sealed class RetailPViewRenderer
|
||||||
&& AcDream.App.Streaming.EntityVanishProbe.PlayerGuid != 0
|
&& AcDream.App.Streaming.EntityVanishProbe.PlayerGuid != 0
|
||||||
&& e.ServerGuid == AcDream.App.Streaming.EntityVanishProbe.PlayerGuid;
|
&& e.ServerGuid == AcDream.App.Streaming.EntityVanishProbe.PlayerGuid;
|
||||||
// #118: under an interior root, outdoor-classified dynamics drew in
|
// #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
|
// via LScape::draw's per-landcell DrawSortCell, never in the
|
||||||
// post-seal cell-object epilogue (PView::DrawCells pc:432719 vs
|
// post-seal cell-object epilogue (PView::DrawCells pc:432719 vs
|
||||||
// pc:432878). Drawing them here instead z-fails them against the
|
// pc:432878). Drawing them here instead z-fails them against the
|
||||||
// seal. Indoor dynamics (incl. exit-portal straddlers, which drew
|
// 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 (!rootIsOutdoor && !indoor)
|
||||||
{
|
{
|
||||||
if (isProbePlayer)
|
if (isProbePlayer)
|
||||||
|
|
@ -976,12 +976,12 @@ public sealed class RetailPViewRenderer
|
||||||
visibleCellIds: null);
|
visibleCellIds: null);
|
||||||
|
|
||||||
// #121: dynamics' attached emitters (portal swirls, creature effects)
|
// #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,
|
// retail draws emitters with the owner object. Before this callback,
|
||||||
// dynamics' emitters fell through EVERY particle filter under the pview
|
// dynamics' emitters fell through EVERY particle filter under the pview
|
||||||
// path (the landscape slice carries outdoor statics + #118 outside-
|
// path (the landscape slice carries outdoor statics + #118 outside-
|
||||||
// stage dynamics; the cell callback carries cell statics; T4 deleted
|
// 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:
|
// portals went invisible. Outside-stage dynamics are excluded here:
|
||||||
// their emitters already drew in the landscape slice (alpha-blended
|
// their emitters already drew in the landscape slice (alpha-blended
|
||||||
// particles must not double-draw, unlike the depth-idempotent meshes).
|
// particles must not double-draw, unlike the depth-idempotent meshes).
|
||||||
|
|
@ -1049,29 +1049,29 @@ public sealed class RetailPViewRenderer
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// T1: per-cell STATIC object lists only (dat-baked 0x40 statics) —
|
// T1: per-cell STATIC object lists only (dat-baked 0x40 statics) —
|
||||||
// dynamics moved to DrawDynamicsLast. Far→near with the cells, after
|
// dynamics moved to DrawDynamicsLast. Far→near with the cells, after
|
||||||
// the shells (retail DrawCells epilogue: PortalList = cell's views →
|
// the shells (retail DrawCells epilogue: PortalList = cell's views →
|
||||||
// DrawObjCell, Ghidra 0x005a4840). T3 (BR-5): each static's sphere is
|
// 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
|
// statics-through-walls fix: a static whose sphere is outside every
|
||||||
// view of its cell no longer paints through the wall (the cottage
|
// view of its cell no longer paints through the wall (the cottage
|
||||||
// phantom staircase's draw path).
|
// phantom staircase's draw path).
|
||||||
// Dense-town FPS iteration-1 (spec 2026-06-23-cellobject-draw-batching):
|
// 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
|
// the per-cell DrawEntityBucket calls below were the top CPU sink at Arwic
|
||||||
// (cellobjects ~3.5 ms/frame; each WbDrawDispatcher.Draw orphans 6 SSBOs +
|
// (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
|
// shipped cells-shell batching pattern applied to cell OBJECTS. Two loops
|
||||||
// preserve the statics-before-particles depth order: loop 1 culls +
|
// preserve the statics-before-particles depth order: loop 1 culls +
|
||||||
// accumulates every cell's survivors and draws them once; loop 2 runs the
|
// 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
|
// 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
|
// depth-test but write no depth). The dispatcher sorts opaque front-to-back
|
||||||
// and transparent back-to-front by group distance (WbDrawDispatcher.cs:
|
// 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,
|
// than the old per-cell-bucketed order. visibleCellIds = the union of cells,
|
||||||
// so the dispatcher admits exactly the same survivor set.
|
// 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();
|
_allCellStatics.Clear();
|
||||||
_cellObjCells.Clear();
|
_cellObjCells.Clear();
|
||||||
for (int i = pvFrame.OrderedVisibleCells.Count - 1; i >= 0; i--)
|
for (int i = pvFrame.OrderedVisibleCells.Count - 1; i >= 0; i--)
|
||||||
|
|
@ -1099,7 +1099,7 @@ public sealed class RetailPViewRenderer
|
||||||
}
|
}
|
||||||
|
|
||||||
// ONE batched static-object draw for every visible cell (was N per-cell
|
// 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
|
// 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
|
// ITS cell's views above (the statics-through-walls fix is preserved by the
|
||||||
// cull; only the draw is batched).
|
// cull; only the draw is batched).
|
||||||
|
|
@ -1124,20 +1124,20 @@ public sealed class RetailPViewRenderer
|
||||||
_cellObjCells);
|
_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
|
// draw. Was per-cell, and each call re-walked the ENTIRE live particle set
|
||||||
// (RetailPViewPassExecutor.DrawCellParticles → ParticleRenderer.Draw enumerates every
|
// (RetailPViewPassExecutor.DrawCellParticles → ParticleRenderer.Draw enumerates every
|
||||||
// live emitter), i.e. O(cells × particles) — the dense-town cellobjects
|
// 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
|
// sink (~5 ms at Arwic). Static owners are disjoint per cell, so the UNION
|
||||||
// (= _allCellStatics, already accumulated above for the batched draw) draws
|
// (= _allCellStatics, already accumulated above for the batched draw) draws
|
||||||
// EXACTLY the same emitters: the callback gates on owner id (the cone-
|
// EXACTLY the same emitters: the callback gates on owner id (the cone-
|
||||||
// surviving set), the renderer sorts globally back-to-front, and the per-
|
// 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
|
// 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 batched static draw so emitters depth-test against the statics now in
|
||||||
// the buffer (the statics-before-particles order). cellId/slice are unused
|
// the buffer (the statics-before-particles order). cellId/slice are unused
|
||||||
// by the particle pass — pass NoClipSlice + the union owner list. This also
|
// by the particle pass — pass NoClipSlice + the union owner list. This also
|
||||||
// drops the per-cell BuildDrawList allocations (N → 1).
|
// drops the per-cell BuildDrawList allocations (N → 1).
|
||||||
if (frameEntityPasses is not null
|
if (frameEntityPasses is not null
|
||||||
|| _allCellStatics.Count > 0)
|
|| _allCellStatics.Count > 0)
|
||||||
{
|
{
|
||||||
|
|
@ -1199,7 +1199,7 @@ public sealed class RetailPViewRenderer
|
||||||
private readonly List<WorldEntity> _cellStaticScratch = new();
|
private readonly List<WorldEntity> _cellStaticScratch = new();
|
||||||
private readonly List<WorldEntity> _dynamicsScratch = new();
|
private readonly List<WorldEntity> _dynamicsScratch = new();
|
||||||
// #118: dynamics assigned to the OUTSIDE stage this frame (interior roots
|
// #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();
|
private readonly List<WorldEntity> _outsideStageDynamics = new();
|
||||||
// Dense-town FPS iteration-1 (cellobject batching): all visible cells'
|
// Dense-town FPS iteration-1 (cellobject batching): all visible cells'
|
||||||
// viewcone-surviving statics accumulated for ONE batched DrawEntityBucket,
|
// viewcone-surviving statics accumulated for ONE batched DrawEntityBucket,
|
||||||
|
|
@ -1274,17 +1274,17 @@ public sealed class RetailPViewRenderer
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// #118 stage assignment for a dynamic under an INTERIOR root: does it draw
|
/// #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
|
/// in the OUTSIDE (landscape) stage — before the gated depth clear and the
|
||||||
/// exit-portal seals — like retail's per-landcell object draw
|
/// exit-portal seals — like retail's per-landcell object draw
|
||||||
/// (LScape::draw → DrawBlock 0x005a17c0 → DrawSortCell pc:430124, run at
|
/// (LScape::draw → DrawBlock 0x005a17c0 → DrawSortCell pc:430124, run at
|
||||||
/// the top of PView::DrawCells pc:432719)?
|
/// the top of PView::DrawCells pc:432719)?
|
||||||
///
|
///
|
||||||
/// True for outdoor-classified dynamics (their fragments lie beyond the
|
/// 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
|
/// 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-
|
/// 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
|
/// (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.
|
/// headlessly by HouseExitWalkReplayTests as the ordering contract.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static bool DynamicDrawsInOutsideStage(
|
public static bool DynamicDrawsInOutsideStage(
|
||||||
|
|
@ -1299,7 +1299,7 @@ public sealed class RetailPViewRenderer
|
||||||
|
|
||||||
uint cellId = parentCellId!.Value;
|
uint cellId = parentCellId!.Value;
|
||||||
if (!drawableCells.Contains(cellId))
|
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);
|
var cell = cells.Find(cellId);
|
||||||
if (cell is null)
|
if (cell is null)
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -1320,7 +1320,7 @@ public sealed class RetailPViewRenderer
|
||||||
return false;
|
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.
|
// bounds source the dispatcher's frustum cull uses.
|
||||||
private static void EntitySphere(WorldEntity e, out Vector3 center, out float radius)
|
private static void EntitySphere(WorldEntity e, out Vector3 center, out float radius)
|
||||||
{
|
{
|
||||||
|
|
@ -1343,7 +1343,7 @@ public sealed class RetailPViewRenderer
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IRetailPViewCellSource
|
internal interface IRetailPViewCellSource
|
||||||
{
|
{
|
||||||
LoadedCell? Find(uint cellId);
|
LoadedCell? Find(uint cellId);
|
||||||
}
|
}
|
||||||
|
|
@ -1354,7 +1354,7 @@ public interface IRetailPViewCellSource
|
||||||
/// pass only; visibility construction and draw ordering remain renderer-owned.
|
/// pass only; visibility construction and draw ordering remain renderer-owned.
|
||||||
/// All frame inputs and results are borrowed for the duration of the call.
|
/// All frame inputs and results are borrowed for the duration of the call.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IRetailPViewPassExecutor
|
internal interface IRetailPViewPassExecutor
|
||||||
{
|
{
|
||||||
void AbortFrame();
|
void AbortFrame();
|
||||||
void BeginFrame();
|
void BeginFrame();
|
||||||
|
|
@ -1565,7 +1565,7 @@ internal sealed class BuildingGroupScratch
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class RetailPViewFrameInput
|
internal sealed class RetailPViewFrameInput
|
||||||
{
|
{
|
||||||
public LoadedCell RootCell { get; private set; } = null!;
|
public LoadedCell RootCell { get; private set; } = null!;
|
||||||
|
|
||||||
|
|
@ -1669,7 +1669,7 @@ public sealed class RetailPViewFrameInput
|
||||||
/// frame objects are deliberately reused to keep the render loop allocation
|
/// frame objects are deliberately reused to keep the render loop allocation
|
||||||
/// free; consumers must copy any state they need to retain asynchronously.
|
/// free; consumers must copy any state they need to retain asynchronously.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class RetailPViewFrameResult
|
internal sealed class RetailPViewFrameResult
|
||||||
{
|
{
|
||||||
public PortalVisibilityFrame PortalFrame { get; private set; } = null!;
|
public PortalVisibilityFrame PortalFrame { get; private set; } = null!;
|
||||||
public ClipFrameAssembly ClipAssembly { get; private set; } = null!;
|
public ClipFrameAssembly ClipAssembly { get; private set; } = null!;
|
||||||
|
|
@ -1712,17 +1712,17 @@ public sealed class RetailPViewFrameResult
|
||||||
diagnosticPartition);
|
diagnosticPartition);
|
||||||
}
|
}
|
||||||
|
|
||||||
public readonly record struct RetailPViewLandscapeSliceContext(
|
internal readonly record struct RetailPViewLandscapeSliceContext(
|
||||||
ClipViewSlice Slice,
|
ClipViewSlice Slice,
|
||||||
IReadOnlyList<WorldEntity> OutdoorEntities)
|
IReadOnlyList<WorldEntity> OutdoorEntities)
|
||||||
{
|
{
|
||||||
internal RenderFrameEntityDrawRequest? EntityDraw { get; init; }
|
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
|
/// outside-stage dynamics to mesh-draw, plus the full scene-particle owner
|
||||||
/// set (statics + dynamics cone survivors) the attached-emitter filter keys on.</summary>
|
/// set (statics + dynamics cone survivors) the attached-emitter filter keys on.</summary>
|
||||||
public readonly record struct RetailPViewLandscapeLateSliceContext(
|
internal readonly record struct RetailPViewLandscapeLateSliceContext(
|
||||||
ClipViewSlice Slice,
|
ClipViewSlice Slice,
|
||||||
IReadOnlyList<WorldEntity> Dynamics,
|
IReadOnlyList<WorldEntity> Dynamics,
|
||||||
IReadOnlySet<uint> ParticleOwnerIds)
|
IReadOnlySet<uint> ParticleOwnerIds)
|
||||||
|
|
@ -1730,7 +1730,7 @@ public readonly record struct RetailPViewLandscapeLateSliceContext(
|
||||||
internal RenderFrameEntityDrawRequest? EntityDraw { get; init; }
|
internal RenderFrameEntityDrawRequest? EntityDraw { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public readonly record struct RetailPViewCellSliceContext(
|
internal readonly record struct RetailPViewCellSliceContext(
|
||||||
uint CellId,
|
uint CellId,
|
||||||
ClipViewSlice Slice,
|
ClipViewSlice Slice,
|
||||||
IReadOnlySet<uint> ParticleOwnerIds);
|
IReadOnlySet<uint> ParticleOwnerIds);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using AcDream.Core.Meshing;
|
using AcDream.Core.Meshing;
|
||||||
using DatReaderWriter.DBObjs;
|
using DatReaderWriter.DBObjs;
|
||||||
|
|
||||||
|
|
@ -10,7 +10,7 @@ namespace AcDream.App.Rendering;
|
||||||
/// degrade has mode 1, is drawn as its authored 3D mesh. Every other first
|
/// degrade has mode 1, is drawn as its authored 3D mesh. Every other first
|
||||||
/// degrade mode uses the camera-facing 2D presentation.
|
/// degrade mode uses the camera-facing 2D presentation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class RetailParticleGeometryClassifier
|
internal static class RetailParticleGeometryClassifier
|
||||||
{
|
{
|
||||||
public static RetailParticleGeometryKind Classify(uint? firstDegradeMode)
|
public static RetailParticleGeometryKind Classify(uint? firstDegradeMode)
|
||||||
=> firstDegradeMode is uint mode && mode != 1u
|
=> firstDegradeMode is uint mode && mode != 1u
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using Silk.NET.OpenGL;
|
using Silk.NET.OpenGL;
|
||||||
|
|
||||||
namespace AcDream.App.Rendering;
|
namespace AcDream.App.Rendering;
|
||||||
|
|
@ -7,7 +7,7 @@ namespace AcDream.App.Rendering;
|
||||||
/// Two persistent GL sampler objects (Repeat + ClampToEdge) created once
|
/// Two persistent GL sampler objects (Repeat + ClampToEdge) created once
|
||||||
/// per GL context. Renderers <see cref="GL.BindSampler"/> the appropriate
|
/// per GL context. Renderers <see cref="GL.BindSampler"/> the appropriate
|
||||||
/// one to a texture unit instead of mutating per-texture
|
/// one to a texture unit instead of mutating per-texture
|
||||||
/// <c>GL_TEXTURE_WRAP_S/T</c> state — sampler state overrides the
|
/// <c>GL_TEXTURE_WRAP_S/T</c> state — sampler state overrides the
|
||||||
/// texture's own wrap parameters, so two renderers can share the same
|
/// texture's own wrap parameters, so two renderers can share the same
|
||||||
/// texture handle but sample it with different wrap modes safely.
|
/// texture handle but sample it with different wrap modes safely.
|
||||||
///
|
///
|
||||||
|
|
@ -16,7 +16,7 @@ namespace AcDream.App.Rendering;
|
||||||
/// <c>references/WorldBuilder/Chorizite.OpenGLSDLBackend/OpenGLGraphicsDevice.cs:115-132</c>.
|
/// <c>references/WorldBuilder/Chorizite.OpenGLSDLBackend/OpenGLGraphicsDevice.cs:115-132</c>.
|
||||||
/// Filter modes match <see cref="TextureCache"/>'s upload defaults
|
/// Filter modes match <see cref="TextureCache"/>'s upload defaults
|
||||||
/// (Linear / Linear, no mipmaps) so binding either sampler doesn't
|
/// (Linear / Linear, no mipmaps) so binding either sampler doesn't
|
||||||
/// change the visual filtering behavior — only the wrap behavior at
|
/// change the visual filtering behavior — only the wrap behavior at
|
||||||
/// UVs outside [0, 1].
|
/// UVs outside [0, 1].
|
||||||
/// </para>
|
/// </para>
|
||||||
///
|
///
|
||||||
|
|
@ -28,7 +28,7 @@ namespace AcDream.App.Rendering;
|
||||||
/// per-texture wrap state.
|
/// per-texture wrap state.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class SamplerCache : IDisposable
|
internal sealed class SamplerCache : IDisposable
|
||||||
{
|
{
|
||||||
private readonly GL _gl;
|
private readonly GL _gl;
|
||||||
private readonly ResourceCleanupGroup _resources;
|
private readonly ResourceCleanupGroup _resources;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using AcDream.App.Rendering.Wb;
|
using AcDream.App.Rendering.Wb;
|
||||||
using AcDream.Core.Lighting;
|
using AcDream.Core.Lighting;
|
||||||
|
|
@ -13,7 +13,7 @@ namespace AcDream.App.Rendering;
|
||||||
/// consistent data without per-shader re-upload.
|
/// consistent data without per-shader re-upload.
|
||||||
///
|
///
|
||||||
/// <para>
|
/// <para>
|
||||||
/// Usage (r12 §13.2 + r13 §12.3):
|
/// Usage (r12 §13.2 + r13 §12.3):
|
||||||
/// <list type="number">
|
/// <list type="number">
|
||||||
/// <item><description>Instantiate once at startup, after the GL context exists.</description></item>
|
/// <item><description>Instantiate once at startup, after the GL context exists.</description></item>
|
||||||
/// <item><description>Each frame, after <see cref="LightManager.Tick"/>, call <see cref="Upload"/> with a freshly-built <see cref="SceneLightingUbo"/>.</description></item>
|
/// <item><description>Each frame, after <see cref="LightManager.Tick"/>, call <see cref="Upload"/> with a freshly-built <see cref="SceneLightingUbo"/>.</description></item>
|
||||||
|
|
@ -21,7 +21,7 @@ namespace AcDream.App.Rendering;
|
||||||
/// </list>
|
/// </list>
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed unsafe class SceneLightingUboBinding : IDisposable
|
internal sealed unsafe class SceneLightingUboBinding : IDisposable
|
||||||
{
|
{
|
||||||
private readonly GL _gl;
|
private readonly GL _gl;
|
||||||
private uint _ubo;
|
private uint _ubo;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
// ScreenPolygonClip.cs
|
// ScreenPolygonClip.cs
|
||||||
//
|
//
|
||||||
// Phase A8.F: 2D convex-polygon intersection (Sutherland-Hodgman).
|
// Phase A8.F: 2D convex-polygon intersection (Sutherland-Hodgman).
|
||||||
// Ports the BEHAVIOR of retail ACRender::polyClipFinish (the screen-space
|
// Ports the BEHAVIOR of retail ACRender::polyClipFinish (the screen-space
|
||||||
|
|
@ -11,7 +11,7 @@ using System.Numerics;
|
||||||
|
|
||||||
namespace AcDream.App.Rendering;
|
namespace AcDream.App.Rendering;
|
||||||
|
|
||||||
public static class ScreenPolygonClip
|
internal static class ScreenPolygonClip
|
||||||
{
|
{
|
||||||
private const float Eps = 1e-7f;
|
private const float Eps = 1e-7f;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using Silk.NET.OpenGL;
|
using Silk.NET.OpenGL;
|
||||||
|
|
||||||
namespace AcDream.App.Rendering;
|
namespace AcDream.App.Rendering;
|
||||||
|
|
||||||
public sealed class Shader : IDisposable
|
internal sealed class Shader : IDisposable
|
||||||
{
|
{
|
||||||
private readonly GL _gl;
|
private readonly GL _gl;
|
||||||
private readonly Dictionary<string, int> _uniformLocations = new(StringComparer.Ordinal);
|
private readonly Dictionary<string, int> _uniformLocations = new(StringComparer.Ordinal);
|
||||||
|
|
@ -15,10 +15,10 @@ public sealed class Shader : IDisposable
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Campaign V slice V2 (docs/plans/2026-07-27-vulkan-campaign.md §3.4): when
|
/// Campaign V slice V2 (docs/plans/2026-07-27-vulkan-campaign.md §3.4): when
|
||||||
/// <paramref name="includeCommonPreamble"/> is true, the text of
|
/// <paramref name="includeCommonPreamble"/> is true, the text of
|
||||||
/// <c>Shaders/common.glsl</c> — sitting alongside <paramref name="vertexPath"/>
|
/// <c>Shaders/common.glsl</c> — sitting alongside <paramref name="vertexPath"/>
|
||||||
/// — is spliced into both sources right after their leading
|
/// — is spliced into both sources right after their leading
|
||||||
/// <c>#version</c>/<c>#extension</c> block. GL has no <c>#include</c>, so this
|
/// <c>#version</c>/<c>#extension</c> block. GL has no <c>#include</c>, so this
|
||||||
/// is plain string concatenation at load time rather than a GLSL-level
|
/// is plain string concatenation at load time rather than a GLSL-level
|
||||||
/// mechanism. Every existing two-argument-path caller is unaffected: the
|
/// mechanism. Every existing two-argument-path caller is unaffected: the
|
||||||
|
|
@ -49,10 +49,14 @@ public sealed class Shader : IDisposable
|
||||||
/// Inserts <paramref name="preamble"/> right after the shader's leading
|
/// Inserts <paramref name="preamble"/> right after the shader's leading
|
||||||
/// <c>#version</c>/<c>#extension</c>/blank-line block. GLSL requires
|
/// <c>#version</c>/<c>#extension</c>/blank-line block. GLSL requires
|
||||||
/// <c>#version</c> to be the very first statement in the source, so the
|
/// <c>#version</c> to be the very first statement in the source, so the
|
||||||
/// preamble cannot simply be prepended — it has to land after that block,
|
/// preamble cannot simply be prepended — it has to land after that block,
|
||||||
/// before the first real declaration.
|
/// before the first real declaration.
|
||||||
|
///
|
||||||
|
/// Internal (not private) so <see cref="Gpu.Gl.GlGpuDevice.CreatePipeline"/>
|
||||||
|
/// can reuse the exact same splice for RHI-created pipelines rather than
|
||||||
|
/// a second copy of this parsing.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static string InjectPreamble(string source, string preamble)
|
internal static string InjectPreamble(string source, string preamble)
|
||||||
{
|
{
|
||||||
int insertAt = 0;
|
int insertAt = 0;
|
||||||
int lineStart = 0;
|
int lineStart = 0;
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,16 @@
|
||||||
layout(location = 0) in vec3 aPos;
|
layout(location = 0) in vec3 aPos;
|
||||||
layout(location = 1) in vec3 aColor;
|
layout(location = 1) in vec3 aColor;
|
||||||
|
|
||||||
uniform mat4 uView;
|
// Campaign V slice V4a: the shared GpuPushConstants block carries ONE
|
||||||
uniform mat4 uProjection;
|
// combined view-projection matrix (uViewProjection), the same convention
|
||||||
|
// every other ported shader uses (mesh_modern.vert, terrain_modern.vert,
|
||||||
|
// particle.vert) rather than the separate uView/uProjection this shader used
|
||||||
|
// pre-migration.
|
||||||
|
uniform mat4 uViewProjection;
|
||||||
|
|
||||||
out vec3 vColor;
|
out vec3 vColor;
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
vColor = aColor;
|
vColor = aColor;
|
||||||
gl_Position = uProjection * uView * vec4(aPos, 1.0);
|
gl_Position = uViewProjection * vec4(aPos, 1.0);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,36 @@
|
||||||
#version 430 core
|
#version 430 core
|
||||||
|
#extension GL_ARB_bindless_texture : require
|
||||||
in vec2 vUv;
|
in vec2 vUv;
|
||||||
in vec4 vColor;
|
in vec4 vColor;
|
||||||
out vec4 FragColor;
|
out vec4 FragColor;
|
||||||
|
|
||||||
uniform sampler2D uTex;
|
// Campaign V slice V4a: the bound sprite/glyph texture arrives as a
|
||||||
uniform int uUseTexture;
|
// texture-table slot index (GpuTextureSlot.Index) via the shared
|
||||||
|
// push-constant block, rather than a raw sampler2D bound to a fixed texture
|
||||||
|
// unit. common.glsl's ACDREAM_TEXTURE_HANDLE macro (spliced in below this
|
||||||
|
// shader's leading #version/#extension block) resolves the slot to the GL
|
||||||
|
// bindless handle; the Vulkan backend will index its set-2 sampled-texture
|
||||||
|
// descriptor array with the same integer.
|
||||||
|
uniform uint uTextureIndexA;
|
||||||
|
|
||||||
|
// uUseTexture reuses the shared block's uRenderPass scalar: the block has no
|
||||||
|
// other spare per-draw int, and "shaders declare only the fields they read,
|
||||||
|
// interpreted as they need" is the documented design
|
||||||
|
// (docs/plans/2026-07-27-vulkan-campaign.md §3.4) — mesh_modern's
|
||||||
|
// "0=opaque,1=translucent" meaning for this field does not apply here.
|
||||||
|
uniform int uRenderPass;
|
||||||
|
#define uUseTexture uRenderPass
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
if (uUseTexture == 1) {
|
if (uUseTexture == 1) {
|
||||||
// Font atlas is a single-channel R8 texture; red = coverage alpha.
|
// Font atlas is a single-channel R8 texture; red = coverage alpha.
|
||||||
float coverage = texture(uTex, vUv).r;
|
sampler2D tex = sampler2D(ACDREAM_TEXTURE_HANDLE(uTextureIndexA));
|
||||||
|
float coverage = texture(tex, vUv).r;
|
||||||
FragColor = vec4(vColor.rgb, vColor.a * coverage);
|
FragColor = vec4(vColor.rgb, vColor.a * coverage);
|
||||||
} else if (uUseTexture == 2) {
|
} else if (uUseTexture == 2) {
|
||||||
// RGBA dat sprite (decoded to RGBA8); modulate by tint/alpha.
|
// RGBA dat sprite (decoded to RGBA8); modulate by tint/alpha.
|
||||||
FragColor = texture(uTex, vUv) * vColor;
|
sampler2D tex = sampler2D(ACDREAM_TEXTURE_HANDLE(uTextureIndexA));
|
||||||
|
FragColor = texture(tex, vUv) * vColor;
|
||||||
} else {
|
} else {
|
||||||
FragColor = vColor;
|
FragColor = vColor;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,19 @@ layout(location = 0) in vec2 aPos; // screen pixels, origin top-left
|
||||||
layout(location = 1) in vec2 aUv;
|
layout(location = 1) in vec2 aUv;
|
||||||
layout(location = 2) in vec4 aColor;
|
layout(location = 2) in vec4 aColor;
|
||||||
|
|
||||||
uniform vec2 uScreenSize;
|
// Campaign V slice V4a: this vertex shader has no projection matrix to carry
|
||||||
|
// (V3 audit, docs/plans/2026-07-27-vulkan-campaign.md §4.10: ui_text converts
|
||||||
|
// pixel coordinates straight to NDC with a constant z=0), so the screen size
|
||||||
|
// travels through the shared push-constant block's two spare scalars instead
|
||||||
|
// of a dedicated uScreenSize uniform.
|
||||||
|
uniform float uParamA; // screen width in pixels
|
||||||
|
uniform float uParamB; // screen height in pixels
|
||||||
|
|
||||||
out vec2 vUv;
|
out vec2 vUv;
|
||||||
out vec4 vColor;
|
out vec4 vColor;
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
vec2 uScreenSize = vec2(uParamA, uParamB);
|
||||||
// Convert pixel coords (origin top-left, +Y down) to NDC (origin center, +Y up).
|
// Convert pixel coords (origin top-left, +Y down) to NDC (origin center, +Y up).
|
||||||
vec2 ndc = vec2(
|
vec2 ndc = vec2(
|
||||||
aPos.x / uScreenSize.x * 2.0 - 1.0,
|
aPos.x / uScreenSize.x * 2.0 - 1.0,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
|
@ -16,7 +16,7 @@ namespace AcDream.App.Rendering.Sky;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Port of <c>references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/SkyboxRenderManager.cs</c>.
|
/// Port of <c>references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/SkyboxRenderManager.cs</c>.
|
||||||
/// Draws the retail sky as a stack of independent celestial meshes (the
|
/// Draws the retail sky as a stack of independent celestial meshes (the
|
||||||
/// "it's not a dome" insight from r12 §2) rather than a cube/sphere
|
/// "it's not a dome" insight from r12 §2) rather than a cube/sphere
|
||||||
/// with a gradient texture. Each <see cref="SkyObjectData"/> is
|
/// with a gradient texture. Each <see cref="SkyObjectData"/> is
|
||||||
/// visible in a window of day-fraction space, sweeps from
|
/// visible in a window of day-fraction space, sweeps from
|
||||||
/// <c>BeginAngle</c> to <c>EndAngle</c> across the sky, and samples its
|
/// <c>BeginAngle</c> to <c>EndAngle</c> across the sky, and samples its
|
||||||
|
|
@ -25,11 +25,11 @@ namespace AcDream.App.Rendering.Sky;
|
||||||
/// <para>
|
/// <para>
|
||||||
/// GL state delta per frame:
|
/// GL state delta per frame:
|
||||||
/// <list type="bullet">
|
/// <list type="bullet">
|
||||||
/// <item><description>Depth mask OFF, depth test OFF, cull OFF — the sky
|
/// <item><description>Depth mask OFF, depth test OFF, cull OFF — the sky
|
||||||
/// should never occlude scene geometry.</description></item>
|
/// should never occlude scene geometry.</description></item>
|
||||||
/// <item><description>Separate projection matrix with a 0.1–1e6 near/far
|
/// <item><description>Separate projection matrix with a 0.1–1e6 near/far
|
||||||
/// so mesh vertices at large distance don't clip.</description></item>
|
/// so mesh vertices at large distance don't clip.</description></item>
|
||||||
/// <item><description>View matrix with translation zeroed — sky is
|
/// <item><description>View matrix with translation zeroed — sky is
|
||||||
/// always camera-centred; moving doesn't get you closer to the
|
/// always camera-centred; moving doesn't get you closer to the
|
||||||
/// sun.</description></item>
|
/// sun.</description></item>
|
||||||
/// </list>
|
/// </list>
|
||||||
|
|
@ -38,12 +38,12 @@ namespace AcDream.App.Rendering.Sky;
|
||||||
/// <para>
|
/// <para>
|
||||||
/// Meshes are built lazily per GfxObj id on first reference. The
|
/// Meshes are built lazily per GfxObj id on first reference. The
|
||||||
/// per-object arc transform matches WorldBuilder's composition:
|
/// per-object arc transform matches WorldBuilder's composition:
|
||||||
/// <c>scale × RotZ(-heading) × RotY(-rotation)</c> — the negative signs
|
/// <c>scale × RotZ(-heading) × RotY(-rotation)</c> — the negative signs
|
||||||
/// come from AC's Z-up right-handed convention where heading is
|
/// come from AC's Z-up right-handed convention where heading is
|
||||||
/// measured clockwise from north.
|
/// measured clockwise from north.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed unsafe class SkyRenderer : IDisposable
|
internal sealed unsafe class SkyRenderer : IDisposable
|
||||||
{
|
{
|
||||||
private readonly GL _gl;
|
private readonly GL _gl;
|
||||||
private readonly IDatReaderWriter _dats;
|
private readonly IDatReaderWriter _dats;
|
||||||
|
|
@ -54,11 +54,11 @@ public sealed unsafe class SkyRenderer : IDisposable
|
||||||
// Lazily-built GPU resources per sky-GfxObj.
|
// Lazily-built GPU resources per sky-GfxObj.
|
||||||
private readonly Dictionary<uint, List<SubMeshGpu>> _gpuByGfxObj = new();
|
private readonly Dictionary<uint, List<SubMeshGpu>> _gpuByGfxObj = new();
|
||||||
|
|
||||||
// When did we start running — used to accumulate TexVelocityX/Y over
|
// When did we start running — used to accumulate TexVelocityX/Y over
|
||||||
// real time (independent of the day-fraction clock).
|
// real time (independent of the day-fraction clock).
|
||||||
private readonly DateTime _startedAt = DateTime.UtcNow;
|
private readonly DateTime _startedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
// Configurable render distance — retail uses ~1e6; anything larger
|
// Configurable render distance — retail uses ~1e6; anything larger
|
||||||
// than the scene far plane works.
|
// than the scene far plane works.
|
||||||
public float Near { get; set; } = 0.1f;
|
public float Near { get; set; } = 0.1f;
|
||||||
public float Far { get; set; } = 1_000_000f;
|
public float Far { get; set; } = 1_000_000f;
|
||||||
|
|
@ -73,7 +73,7 @@ public sealed unsafe class SkyRenderer : IDisposable
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Draw all NON-WEATHER sky objects (dome, sun, moon, stars, clouds —
|
/// Draw all NON-WEATHER sky objects (dome, sun, moon, stars, clouds —
|
||||||
/// every <c>SkyObject</c> with <c>Properties & 0x04 == 0</c>).
|
/// every <c>SkyObject</c> with <c>Properties & 0x04 == 0</c>).
|
||||||
/// Called BEFORE the scene; terrain / meshes / debug lines / overlay
|
/// Called BEFORE the scene; terrain / meshes / debug lines / overlay
|
||||||
/// land on top via depth-test.
|
/// land on top via depth-test.
|
||||||
|
|
@ -82,7 +82,7 @@ public sealed unsafe class SkyRenderer : IDisposable
|
||||||
/// Mirrors the first half of retail's <c>LScape::draw</c> at
|
/// Mirrors the first half of retail's <c>LScape::draw</c> at
|
||||||
/// <c>0x00506330</c>: that function calls <c>GameSky::Draw(0)</c>
|
/// <c>0x00506330</c>: that function calls <c>GameSky::Draw(0)</c>
|
||||||
/// (sky pass) before the landblock loop, then <c>GameSky::Draw(1)</c>
|
/// (sky pass) before the landblock loop, then <c>GameSky::Draw(1)</c>
|
||||||
/// (weather pass) after. acdream splits the same way — see
|
/// (weather pass) after. acdream splits the same way — see
|
||||||
/// <see cref="RenderWeather"/> for the post-scene companion.
|
/// <see cref="RenderWeather"/> for the post-scene companion.
|
||||||
/// </para>
|
/// </para>
|
||||||
///
|
///
|
||||||
|
|
@ -90,17 +90,17 @@ public sealed unsafe class SkyRenderer : IDisposable
|
||||||
/// Each submesh renders with retail's per-vertex lighting formula:
|
/// Each submesh renders with retail's per-vertex lighting formula:
|
||||||
/// <c>tint = clamp(emissive + ambient + max(dot(N, -sunDir), 0) * sunColor, 0, 1)</c>
|
/// <c>tint = clamp(emissive + ambient + max(dot(N, -sunDir), 0) * sunColor, 0, 1)</c>
|
||||||
/// where <c>emissive</c> is the submesh's <c>Surface.Luminosity</c>
|
/// where <c>emissive</c> is the submesh's <c>Surface.Luminosity</c>
|
||||||
/// float (1.0 for dome + sun + moon → texture passthrough via
|
/// float (1.0 for dome + sun + moon → texture passthrough via
|
||||||
/// saturation; 0.0 for clouds → get the full time-of-day tint).
|
/// saturation; 0.0 for clouds → get the full time-of-day tint).
|
||||||
/// <paramref name="keyframe"/> supplies the AmbientColor and SunColor
|
/// <paramref name="keyframe"/> supplies the AmbientColor and SunColor
|
||||||
/// already pre-multiplied by AmbBright / DirBright (loader-side).
|
/// already pre-multiplied by AmbBright / DirBright (loader-side).
|
||||||
/// </para>
|
/// </para>
|
||||||
/// <para>
|
/// <para>
|
||||||
/// See <c>docs/research/2026-04-23-sky-retail-verbatim.md</c> §6 for
|
/// See <c>docs/research/2026-04-23-sky-retail-verbatim.md</c> §6 for
|
||||||
/// the full decompile citation. The empirical Dereth dump (
|
/// the full decompile citation. The empirical Dereth dump (
|
||||||
/// <c>ACDREAM_DUMP_SKY=1</c>, logged 2026-04-23) confirmed the
|
/// <c>ACDREAM_DUMP_SKY=1</c>, logged 2026-04-23) confirmed the
|
||||||
/// <c>SurfaceType.Luminous</c> flag bit is NOT set on any Dereth sky
|
/// <c>SurfaceType.Luminous</c> flag bit is NOT set on any Dereth sky
|
||||||
/// mesh — the differentiator is the <c>Surface.Luminosity</c> FLOAT
|
/// mesh — the differentiator is the <c>Surface.Luminosity</c> FLOAT
|
||||||
/// field.
|
/// field.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -118,13 +118,13 @@ public sealed unsafe class SkyRenderer : IDisposable
|
||||||
/// Draw the POST-SCENE sky objects (the foreground rain mesh
|
/// Draw the POST-SCENE sky objects (the foreground rain mesh
|
||||||
/// <c>0x01004C44</c> on Rainy DayGroups, plus any other SkyObject with
|
/// <c>0x01004C44</c> on Rainy DayGroups, plus any other SkyObject with
|
||||||
/// <c>Properties & 0x01 != 0</c>). Called AFTER the scene so these
|
/// <c>Properties & 0x01 != 0</c>). Called AFTER the scene so these
|
||||||
/// meshes paint on top of terrain and entities — retail-faithful order
|
/// meshes paint on top of terrain and entities — retail-faithful order
|
||||||
/// from <c>LScape::draw</c> at <c>0x00506330</c>, where
|
/// from <c>LScape::draw</c> at <c>0x00506330</c>, where
|
||||||
/// <c>GameSky::Draw(1)</c> fires after the <c>DrawBlock</c> loop and
|
/// <c>GameSky::Draw(1)</c> fires after the <c>DrawBlock</c> loop and
|
||||||
/// renders the <c>after_sky_cell</c> contents. With depth-test
|
/// renders the <c>after_sky_cell</c> contents. With depth-test
|
||||||
/// disabled and additive blend (the rain Surface flag includes
|
/// disabled and additive blend (the rain Surface flag includes
|
||||||
/// Additive), the 815m-tall rain cylinder's bright streak texels add
|
/// Additive), the 815m-tall rain cylinder's bright streak texels add
|
||||||
/// over the scene — making rain appear in the air between camera and
|
/// over the scene — making rain appear in the air between camera and
|
||||||
/// character instead of only at the horizon.
|
/// character instead of only at the horizon.
|
||||||
/// <para>
|
/// <para>
|
||||||
/// Method name kept as <c>RenderWeather</c> for API stability; the
|
/// Method name kept as <c>RenderWeather</c> for API stability; the
|
||||||
|
|
@ -149,7 +149,7 @@ public sealed unsafe class SkyRenderer : IDisposable
|
||||||
/// Sets up the same GL state for both (depth-test off, additive +
|
/// Sets up the same GL state for both (depth-test off, additive +
|
||||||
/// alpha-blend per submesh, camera-anchored translation) and iterates
|
/// alpha-blend per submesh, camera-anchored translation) and iterates
|
||||||
/// only the SkyObjects matching the requested partition by
|
/// only the SkyObjects matching the requested partition by
|
||||||
/// <see cref="SkyObjectData.IsPostScene"/> — bit <c>0x01</c> per the
|
/// <see cref="SkyObjectData.IsPostScene"/> — bit <c>0x01</c> per the
|
||||||
/// retail decomp at <c>GameSky::MakeObject</c> (<c>0x00506ee0</c>).
|
/// retail decomp at <c>GameSky::MakeObject</c> (<c>0x00506ee0</c>).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void RenderPass(
|
private void RenderPass(
|
||||||
|
|
@ -171,7 +171,7 @@ public sealed unsafe class SkyRenderer : IDisposable
|
||||||
// that FOV here, including the near-180-degree teleport transition.
|
// that FOV here, including the near-180-degree teleport transition.
|
||||||
var skyProj = SkyProjection.WithDepthRange(camera.Projection, Near, Far);
|
var skyProj = SkyProjection.WithDepthRange(camera.Projection, Near, Far);
|
||||||
|
|
||||||
// View with translation zeroed — keeps the sky at camera origin
|
// View with translation zeroed — keeps the sky at camera origin
|
||||||
// regardless of camera position in the world.
|
// regardless of camera position in the world.
|
||||||
var skyView = camera.View;
|
var skyView = camera.View;
|
||||||
skyView.M41 = 0f;
|
skyView.M41 = 0f;
|
||||||
|
|
@ -183,7 +183,7 @@ public sealed unsafe class SkyRenderer : IDisposable
|
||||||
_shader.SetMatrix4("uSkyProjection", skyProj);
|
_shader.SetMatrix4("uSkyProjection", skyProj);
|
||||||
|
|
||||||
// Retail per-vertex lighting inputs (AdjustPlanes formula).
|
// Retail per-vertex lighting inputs (AdjustPlanes formula).
|
||||||
// AmbColor/SunColor are already × AmbBright/DirBright from
|
// AmbColor/SunColor are already × AmbBright/DirBright from
|
||||||
// SkyDescLoader. SunDir is the unit vector FROM surface TO sun
|
// SkyDescLoader. SunDir is the unit vector FROM surface TO sun
|
||||||
// derived from the keyframe's DirHeading/DirPitch.
|
// derived from the keyframe's DirHeading/DirPitch.
|
||||||
_shader.SetVec3("uAmbientColor", keyframe.AmbientColor);
|
_shader.SetVec3("uAmbientColor", keyframe.AmbientColor);
|
||||||
|
|
@ -204,14 +204,14 @@ public sealed unsafe class SkyRenderer : IDisposable
|
||||||
bool wasCullFace = _gl.IsEnabled(EnableCap.CullFace);
|
bool wasCullFace = _gl.IsEnabled(EnableCap.CullFace);
|
||||||
_gl.Disable(EnableCap.CullFace);
|
_gl.Disable(EnableCap.CullFace);
|
||||||
_gl.Enable(EnableCap.Blend);
|
_gl.Enable(EnableCap.Blend);
|
||||||
// Default blend — overridden per-submesh inside the inner loop.
|
// Default blend — overridden per-submesh inside the inner loop.
|
||||||
// Additive surfaces (sun/moon/stars via SurfaceType.Additive =
|
// Additive surfaces (sun/moon/stars via SurfaceType.Additive =
|
||||||
// 0x10000) get GL_SRC_ALPHA / GL_ONE; alpha-blended (clouds, dome
|
// 0x10000) get GL_SRC_ALPHA / GL_ONE; alpha-blended (clouds, dome
|
||||||
// with Alpha flag) get GL_SRC_ALPHA / GL_ONE_MINUS_SRC_ALPHA.
|
// with Alpha flag) get GL_SRC_ALPHA / GL_ONE_MINUS_SRC_ALPHA.
|
||||||
_gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
|
_gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
|
||||||
|
|
||||||
// Look up the keyframe's override list so we can apply
|
// Look up the keyframe's override list so we can apply
|
||||||
// SkyObjReplace (r12 §2.3): per-keyframe GfxObj swaps + rotation
|
// SkyObjReplace (r12 §2.3): per-keyframe GfxObj swaps + rotation
|
||||||
// override + transparency fade + luminosity cap.
|
// override + transparency fade + luminosity cap.
|
||||||
var replaces = PickReplaces(group, dayFraction);
|
var replaces = PickReplaces(group, dayFraction);
|
||||||
|
|
||||||
|
|
@ -220,19 +220,19 @@ public sealed unsafe class SkyRenderer : IDisposable
|
||||||
for (int i = 0; i < group.SkyObjects.Count; i++)
|
for (int i = 0; i < group.SkyObjects.Count; i++)
|
||||||
{
|
{
|
||||||
var obj = group.SkyObjects[i];
|
var obj = group.SkyObjects[i];
|
||||||
// Partition by post-scene flag (Properties bit 0x01) — the
|
// Partition by post-scene flag (Properties bit 0x01) — the
|
||||||
// caller chose either the pre-scene sky pass (bit clear) or
|
// caller chose either the pre-scene sky pass (bit clear) or
|
||||||
// the post-scene pass (bit set). Mirrors retail
|
// the post-scene pass (bit set). Mirrors retail
|
||||||
// GameSky::CreateDeletePhysicsObjects at 0x005073c0 / decomp
|
// GameSky::CreateDeletePhysicsObjects at 0x005073c0 / decomp
|
||||||
// line 269036 which routes (Properties & 1) into
|
// line 269036 which routes (Properties & 1) into
|
||||||
// before_sky_cell vs after_sky_cell, and GameSky::Draw at
|
// before_sky_cell vs after_sky_cell, and GameSky::Draw at
|
||||||
// 0x00506ff0 which renders those cells in the two passes.
|
// 0x00506ff0 which renders those cells in the two passes.
|
||||||
// NOTE: bit 0x04 (IsWeather) is independent — it gates whether
|
// NOTE: bit 0x04 (IsWeather) is independent — it gates whether
|
||||||
// the object is instantiated when weather_enabled is false.
|
// the object is instantiated when weather_enabled is false.
|
||||||
// Earlier acdream incorrectly used IsWeather for this
|
// Earlier acdream incorrectly used IsWeather for this
|
||||||
// partition, putting the outer rain cylinder 0x01004C42
|
// partition, putting the outer rain cylinder 0x01004C42
|
||||||
// (Props=0x04, NO bit 0x01) into the post-scene pass with the
|
// (Props=0x04, NO bit 0x01) into the post-scene pass with the
|
||||||
// foreground rain — double-thick rain not matching retail.
|
// foreground rain — double-thick rain not matching retail.
|
||||||
if (obj.IsPostScene != postScenePass) continue;
|
if (obj.IsPostScene != postScenePass) continue;
|
||||||
if (!obj.IsVisible(dayFraction)) continue;
|
if (!obj.IsVisible(dayFraction)) continue;
|
||||||
// Retail GameSky::Draw (0x00506ff0) skips Properties bit 0x02
|
// Retail GameSky::Draw (0x00506ff0) skips Properties bit 0x02
|
||||||
|
|
@ -254,7 +254,7 @@ public sealed unsafe class SkyRenderer : IDisposable
|
||||||
// fallback at the inner loop never fired (1f is always > 0).
|
// fallback at the inner loop never fired (1f is always > 0).
|
||||||
// RainMeshProbe (committed b8e0857) confirmed empirically that
|
// RainMeshProbe (committed b8e0857) confirmed empirically that
|
||||||
// NO Dereth sky surface carries the SurfaceType.Luminous flag
|
// NO Dereth sky surface carries the SurfaceType.Luminous flag
|
||||||
// bit (0x40) — the differentiator is purely the float field.
|
// bit (0x40) — the differentiator is purely the float field.
|
||||||
float replaceLuminosity = float.NaN;
|
float replaceLuminosity = float.NaN;
|
||||||
float replaceDiffuse = float.NaN;
|
float replaceDiffuse = float.NaN;
|
||||||
if (replaces.TryGetValue((uint)i, out var rep))
|
if (replaces.TryGetValue((uint)i, out var rep))
|
||||||
|
|
@ -289,7 +289,7 @@ public sealed unsafe class SkyRenderer : IDisposable
|
||||||
// int32_t var_4_1 = 0xc2f00000; // 0xc2f00000 == -120.0f
|
// int32_t var_4_1 = 0xc2f00000; // 0xc2f00000 == -120.0f
|
||||||
//
|
//
|
||||||
// Gate: bit 0x04 (weather) set AND bit 0x08 unset. NOT every
|
// Gate: bit 0x04 (weather) set AND bit 0x08 unset. NOT every
|
||||||
// post-scene SkyObject — bit 0x01 (post-scene) is independent
|
// post-scene SkyObject — bit 0x01 (post-scene) is independent
|
||||||
// of bit 0x04 (weather). Today's Dereth ships every post-scene
|
// of bit 0x04 (weather). Today's Dereth ships every post-scene
|
||||||
// entry as also weather-flagged so the previous unconditional
|
// entry as also weather-flagged so the previous unconditional
|
||||||
// offset was a no-op divergence, but a future DayGroup with a
|
// offset was a no-op divergence, but a future DayGroup with a
|
||||||
|
|
@ -302,15 +302,15 @@ public sealed unsafe class SkyRenderer : IDisposable
|
||||||
// cylinder bottom sits at z=0.11 ABOVE the camera (skyView
|
// cylinder bottom sits at z=0.11 ABOVE the camera (skyView
|
||||||
// translation is zeroed so model-origin == camera); looking
|
// translation is zeroed so model-origin == camera); looking
|
||||||
// horizontally shows nothing. With -120m the cylinder spans z
|
// horizontally shows nothing. With -120m the cylinder spans z
|
||||||
// = (camera-119.89)..(camera+694.90) — camera is inside,
|
// = (camera-119.89)..(camera+694.90) — camera is inside,
|
||||||
// looking in any direction shows surrounding walls — the
|
// looking in any direction shows surrounding walls — the
|
||||||
// volumetric foreground-rain look retail has.
|
// volumetric foreground-rain look retail has.
|
||||||
if (postScenePass && obj.IsWeather && (obj.Properties & 0x08u) == 0u)
|
if (postScenePass && obj.IsWeather && (obj.Properties & 0x08u) == 0u)
|
||||||
model = model * Matrix4x4.CreateTranslation(0f, 0f, -120f);
|
model = model * Matrix4x4.CreateTranslation(0f, 0f, -120f);
|
||||||
|
|
||||||
_shader.SetMatrix4("uModel", model);
|
_shader.SetMatrix4("uModel", model);
|
||||||
|
|
||||||
// UV scroll accumulates real-time × velocity. Wrap to [0, 1]
|
// UV scroll accumulates real-time × velocity. Wrap to [0, 1]
|
||||||
// so long-running sessions don't accumulate float precision
|
// so long-running sessions don't accumulate float precision
|
||||||
// loss in the fragment UV.
|
// loss in the fragment UV.
|
||||||
float uOffset = (obj.TexVelocityX * secondsSinceStart) % 1f;
|
float uOffset = (obj.TexVelocityX * secondsSinceStart) % 1f;
|
||||||
|
|
@ -328,7 +328,7 @@ public sealed unsafe class SkyRenderer : IDisposable
|
||||||
// sky dome is Base1Image (Opaque, mapped to
|
// sky dome is Base1Image (Opaque, mapped to
|
||||||
// SrcAlpha/InvSrcAlpha for a no-op blend at alpha=1).
|
// SrcAlpha/InvSrcAlpha for a no-op blend at alpha=1).
|
||||||
// See FUN_00508010 (chunk_00500000.c:7535) for the retail
|
// See FUN_00508010 (chunk_00500000.c:7535) for the retail
|
||||||
// pattern — retail routes sky meshes through the normal
|
// pattern — retail routes sky meshes through the normal
|
||||||
// mesh pipeline where Surface flags dictate state.
|
// mesh pipeline where Surface flags dictate state.
|
||||||
if (sub.IsAdditive)
|
if (sub.IsAdditive)
|
||||||
_gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.One);
|
_gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.One);
|
||||||
|
|
@ -338,24 +338,24 @@ public sealed unsafe class SkyRenderer : IDisposable
|
||||||
// Emissive source picks the surface's authored Luminosity by
|
// Emissive source picks the surface's authored Luminosity by
|
||||||
// default; the per-keyframe replace data can OVERRIDE
|
// default; the per-keyframe replace data can OVERRIDE
|
||||||
// (rep.Luminosity > 0) or CAP (rep.MaxBright). This matches
|
// (rep.Luminosity > 0) or CAP (rep.MaxBright). This matches
|
||||||
// retail's FUN_0059da60: surface.Luminosity → D3DMATERIAL.Emissive
|
// retail's FUN_0059da60: surface.Luminosity → D3DMATERIAL.Emissive
|
||||||
// (via material cache +0x3c), with the keyframe replace
|
// (via material cache +0x3c), with the keyframe replace
|
||||||
// promoting bright-keyframe clouds when the keyframe asks.
|
// promoting bright-keyframe clouds when the keyframe asks.
|
||||||
//
|
//
|
||||||
// Empirical Dereth sky surfaces (RainMeshProbe, b8e0857):
|
// Empirical Dereth sky surfaces (RainMeshProbe, b8e0857):
|
||||||
// dome/sun/moon → Lum=1.0 → vTint saturates → texture
|
// dome/sun/moon → Lum=1.0 → vTint saturates → texture
|
||||||
// passthrough (correct retail look);
|
// passthrough (correct retail look);
|
||||||
// stars/clouds → Lum=0.0 → vTint = ambient + diffuse →
|
// stars/clouds → Lum=0.0 → vTint = ambient + diffuse →
|
||||||
// picks up the time-of-day tint;
|
// picks up the time-of-day tint;
|
||||||
// rain → Lum=0.1484 → faint emissive baseline,
|
// rain → Lum=0.1484 → faint emissive baseline,
|
||||||
// ambient+diffuse adds atmospheric tint.
|
// ambient+diffuse adds atmospheric tint.
|
||||||
//
|
//
|
||||||
// Pre-fix: the replace-override variable defaulted to 1f and
|
// Pre-fix: the replace-override variable defaulted to 1f and
|
||||||
// the fallback `(luminosity > 0) ? luminosity : sub.SurfLuminosity`
|
// the fallback `(luminosity > 0) ? luminosity : sub.SurfLuminosity`
|
||||||
// never fired — every sky mesh got effEmissive=1.0,
|
// never fired — every sky mesh got effEmissive=1.0,
|
||||||
// saturating vTint. That made stars/clouds look full-bright
|
// saturating vTint. That made stars/clouds look full-bright
|
||||||
// instead of time-of-day-tinted, and made rain streaks
|
// instead of time-of-day-tinted, and made rain streaks
|
||||||
// 6.7× too bright (one of two factors compounding the
|
// 6.7× too bright (one of two factors compounding the
|
||||||
// foreground-rim visibility bug).
|
// foreground-rim visibility bug).
|
||||||
float effEmissive = float.IsNaN(replaceLuminosity)
|
float effEmissive = float.IsNaN(replaceLuminosity)
|
||||||
? sub.SurfLuminosity
|
? sub.SurfLuminosity
|
||||||
|
|
@ -374,7 +374,7 @@ public sealed unsafe class SkyRenderer : IDisposable
|
||||||
|
|
||||||
// Retail D3DPolyRender::SetSurface at 0x59c882 calls
|
// Retail D3DPolyRender::SetSurface at 0x59c882 calls
|
||||||
// SetFFFogAlphaDisabled(1) when the Additive flag (0x10000)
|
// SetFFFogAlphaDisabled(1) when the Additive flag (0x10000)
|
||||||
// is set on the Surface — so the sun, moon, stars, and any
|
// is set on the Surface — so the sun, moon, stars, and any
|
||||||
// additive cloud sheet are drawn WITHOUT fog. Skipping fog
|
// additive cloud sheet are drawn WITHOUT fog. Skipping fog
|
||||||
// on additive surfaces keeps the sun bright at horizon
|
// on additive surfaces keeps the sun bright at horizon
|
||||||
// dusk/dawn (where fog would otherwise dim it to fog color).
|
// dusk/dawn (where fog would otherwise dim it to fog color).
|
||||||
|
|
@ -477,13 +477,13 @@ public sealed unsafe class SkyRenderer : IDisposable
|
||||||
/// Lazy mesh build for a sky object. Handles two cases:
|
/// Lazy mesh build for a sky object. Handles two cases:
|
||||||
/// <list type="bullet">
|
/// <list type="bullet">
|
||||||
/// <item><description>
|
/// <item><description>
|
||||||
/// <c>0x010xxxxx</c> — direct <see cref="GfxObj"/>. Reuses
|
/// <c>0x010xxxxx</c> — direct <see cref="GfxObj"/>. Reuses
|
||||||
/// <see cref="GfxObjMesh.Build"/> so the pos/neg polygon
|
/// <see cref="GfxObjMesh.Build"/> so the pos/neg polygon
|
||||||
/// splitting logic stays consistent with the main static-mesh
|
/// splitting logic stays consistent with the main static-mesh
|
||||||
/// pipeline. Most sky meshes are single-surface.
|
/// pipeline. Most sky meshes are single-surface.
|
||||||
/// </description></item>
|
/// </description></item>
|
||||||
/// <item><description>
|
/// <item><description>
|
||||||
/// <c>0x020xxxxx</c> — <see cref="Setup"/>. The agent at
|
/// <c>0x020xxxxx</c> — <see cref="Setup"/>. The agent at
|
||||||
/// 2026-04-27 found these Setup-backed sky objects (e.g.
|
/// 2026-04-27 found these Setup-backed sky objects (e.g.
|
||||||
/// <c>0x02000588</c>, <c>0x02000589</c>, <c>0x02000714</c>,
|
/// <c>0x02000588</c>, <c>0x02000589</c>, <c>0x02000714</c>,
|
||||||
/// <c>0x02000BA6</c>) were silently dropped: every cache miss
|
/// <c>0x02000BA6</c>) were silently dropped: every cache miss
|
||||||
|
|
@ -496,7 +496,7 @@ public sealed unsafe class SkyRenderer : IDisposable
|
||||||
/// <c>Setup.Parts</c> at the default placement frame and
|
/// <c>Setup.Parts</c> at the default placement frame and
|
||||||
/// <see cref="GfxObjMesh.Build"/> produces submeshes for each
|
/// <see cref="GfxObjMesh.Build"/> produces submeshes for each
|
||||||
/// part. Per-part transforms are baked into vertex positions
|
/// part. Per-part transforms are baked into vertex positions
|
||||||
/// (sky setups are static — no animation needed for the static
|
/// (sky setups are static — no animation needed for the static
|
||||||
/// mesh half of the visual).
|
/// mesh half of the visual).
|
||||||
/// </description></item>
|
/// </description></item>
|
||||||
/// </list>
|
/// </list>
|
||||||
|
|
@ -504,7 +504,7 @@ public sealed unsafe class SkyRenderer : IDisposable
|
||||||
/// Even with this fix the visible aurora-style sheen most retail
|
/// Even with this fix the visible aurora-style sheen most retail
|
||||||
/// rainy/cloudy setups produce comes from the <c>pes_id</c> field
|
/// rainy/cloudy setups produce comes from the <c>pes_id</c> field
|
||||||
/// on each <see cref="DatReaderWriter.Types.SkyObject"/> (a Particle
|
/// on each <see cref="DatReaderWriter.Types.SkyObject"/> (a Particle
|
||||||
/// Effect Schedule) — that's a separate Phase-level feature.
|
/// Effect Schedule) — that's a separate Phase-level feature.
|
||||||
/// Rendering the Setup's static parts here is the geometry half;
|
/// Rendering the Setup's static parts here is the geometry half;
|
||||||
/// the dynamic particle half is deferred.
|
/// the dynamic particle half is deferred.
|
||||||
/// </para>
|
/// </para>
|
||||||
|
|
@ -550,8 +550,8 @@ public sealed unsafe class SkyRenderer : IDisposable
|
||||||
// Phase 1 diagnostic: dump Surface.Type flags on every sky GfxObj
|
// Phase 1 diagnostic: dump Surface.Type flags on every sky GfxObj
|
||||||
// once, so we can determine which submeshes carry Luminous (0x40)
|
// once, so we can determine which submeshes carry Luminous (0x40)
|
||||||
// vs plain-lit. This settles the retail "cloud tint = per-vertex
|
// vs plain-lit. This settles the retail "cloud tint = per-vertex
|
||||||
// lighting on non-Luminous meshes" hypothesis — see
|
// lighting on non-Luminous meshes" hypothesis — see
|
||||||
// docs/research/2026-04-23-sky-retail-verbatim.md §6.
|
// docs/research/2026-04-23-sky-retail-verbatim.md §6.
|
||||||
if (System.Environment.GetEnvironmentVariable("ACDREAM_DUMP_SKY") == "1")
|
if (System.Environment.GetEnvironmentVariable("ACDREAM_DUMP_SKY") == "1")
|
||||||
DumpGfxObjSurfaces(gfxObjId, gfx, subMeshes);
|
DumpGfxObjSurfaces(gfxObjId, gfx, subMeshes);
|
||||||
|
|
||||||
|
|
@ -565,14 +565,14 @@ public sealed unsafe class SkyRenderer : IDisposable
|
||||||
/// Setup-backed sky object loader. Walks <see cref="Setup.Parts"/> at
|
/// Setup-backed sky object loader. Walks <see cref="Setup.Parts"/> at
|
||||||
/// the default placement frame, builds submeshes via
|
/// the default placement frame, builds submeshes via
|
||||||
/// <see cref="GfxObjMesh.Build"/>, and bakes the per-part transform
|
/// <see cref="GfxObjMesh.Build"/>, and bakes the per-part transform
|
||||||
/// into the vertex positions before upload. Static-pose only — sky
|
/// into the vertex positions before upload. Static-pose only — sky
|
||||||
/// setups don't animate in any meaningful way for the visual we care
|
/// setups don't animate in any meaningful way for the visual we care
|
||||||
/// about (the dynamic look comes from <c>pes_id</c> particles, not
|
/// about (the dynamic look comes from <c>pes_id</c> particles, not
|
||||||
/// the underlying mesh).
|
/// the underlying mesh).
|
||||||
/// <para>
|
/// <para>
|
||||||
/// Mirrors retail's <see cref="CPhysicsObj.InitPartArrayObject"/> at
|
/// Mirrors retail's <see cref="CPhysicsObj.InitPartArrayObject"/> at
|
||||||
/// decomp <c>280484</c> dispatching type 7 → <c>CPartArray::CreateSetup</c>
|
/// decomp <c>280484</c> dispatching type 7 → <c>CPartArray::CreateSetup</c>
|
||||||
/// → <c>CSetup::SetSetupID</c>, which loads the setup and instantiates
|
/// → <c>CSetup::SetSetupID</c>, which loads the setup and instantiates
|
||||||
/// each part as a separate <c>CPhysicsObj</c> child. We collapse the
|
/// each part as a separate <c>CPhysicsObj</c> child. We collapse the
|
||||||
/// children into a flat submesh list because the sky pass renders
|
/// children into a flat submesh list because the sky pass renders
|
||||||
/// without per-part transforms anyway.
|
/// without per-part transforms anyway.
|
||||||
|
|
@ -654,13 +654,13 @@ public sealed unsafe class SkyRenderer : IDisposable
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// SurfaceType is a flag enum — `ToString()` gives the
|
// SurfaceType is a flag enum — `ToString()` gives the
|
||||||
// comma-joined names (e.g. "Base1Image, Additive").
|
// comma-joined names (e.g. "Base1Image, Additive").
|
||||||
uint rawType = (uint)surface.Type;
|
uint rawType = (uint)surface.Type;
|
||||||
string names = surface.Type.ToString();
|
string names = surface.Type.ToString();
|
||||||
uint origTex = surface.OrigTextureId?.DataId ?? 0u;
|
uint origTex = surface.OrigTextureId?.DataId ?? 0u;
|
||||||
var trans = TranslucencyKindExtensions.FromSurfaceType(surface.Type);
|
var trans = TranslucencyKindExtensions.FromSurfaceType(surface.Type);
|
||||||
// Surface's own Luminosity (0..1 fraction per test fixture —
|
// Surface's own Luminosity (0..1 fraction per test fixture —
|
||||||
// different from SkyObjectReplace.Luminosity which lives in the keyframe).
|
// different from SkyObjectReplace.Luminosity which lives in the keyframe).
|
||||||
Console.WriteLine(
|
Console.WriteLine(
|
||||||
$"[sky-dump] Surface[{i}] 0x{surfaceId:X8} Type=0x{rawType:X8} ({names}) " +
|
$"[sky-dump] Surface[{i}] 0x{surfaceId:X8} Type=0x{rawType:X8} ({names}) " +
|
||||||
|
|
@ -703,7 +703,7 @@ public sealed unsafe class SkyRenderer : IDisposable
|
||||||
//
|
//
|
||||||
// NOTE: earlier revision also treated `SurfaceType.Luminous = 0x40`
|
// NOTE: earlier revision also treated `SurfaceType.Luminous = 0x40`
|
||||||
// as additive, but that flag is present on the sky DOME itself and
|
// as additive, but that flag is present on the sky DOME itself and
|
||||||
// on cloud sheets — turning those additive blew the whole sky to
|
// on cloud sheets — turning those additive blew the whole sky to
|
||||||
// white. `Luminous` means "self-illuminated / unshaded" in retail's
|
// white. `Luminous` means "self-illuminated / unshaded" in retail's
|
||||||
// render pipeline, not "additive blend". Only the Additive bit
|
// render pipeline, not "additive blend". Only the Additive bit
|
||||||
// toggles the blend mode.
|
// toggles the blend mode.
|
||||||
|
|
@ -753,18 +753,18 @@ public sealed unsafe class SkyRenderer : IDisposable
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsAdditive;
|
public bool IsAdditive;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// <c>Surface.Luminosity</c> float (0..1 — NOT the SurfaceType.Luminous
|
/// <c>Surface.Luminosity</c> float (0..1 — NOT the SurfaceType.Luminous
|
||||||
/// flag bit). Passed to the sky fragment shader as <c>uEmissive</c>;
|
/// flag bit). Passed to the sky fragment shader as <c>uEmissive</c>;
|
||||||
/// when 1.0 it saturates the lighting math so the mesh renders at
|
/// when 1.0 it saturates the lighting math so the mesh renders at
|
||||||
/// full texture brightness (dome, sun). When 0.0 the mesh picks up
|
/// full texture brightness (dome, sun). When 0.0 the mesh picks up
|
||||||
/// the time-of-day ambient+diffuse tint (clouds). See
|
/// the time-of-day ambient+diffuse tint (clouds). See
|
||||||
/// <c>docs/research/2026-04-23-sky-retail-verbatim.md</c> §6.
|
/// <c>docs/research/2026-04-23-sky-retail-verbatim.md</c> §6.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public float SurfLuminosity;
|
public float SurfLuminosity;
|
||||||
public float SurfDiffuse;
|
public float SurfDiffuse;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// True when the source mesh's authored UVs exceed [0,1] (e.g.
|
/// True when the source mesh's authored UVs exceed [0,1] (e.g.
|
||||||
/// the inner sky/star layer 0x010015EF and the cloud meshes —
|
/// the inner sky/star layer 0x010015EF and the cloud meshes —
|
||||||
/// they tile their texture across the geometry). The renderer
|
/// they tile their texture across the geometry). The renderer
|
||||||
/// must use <c>GL_REPEAT</c> for these or only the small region
|
/// must use <c>GL_REPEAT</c> for these or only the small region
|
||||||
/// where UVs fall in [0,1] samples the actual texture; the rest
|
/// where UVs fall in [0,1] samples the actual texture; the rest
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AcDream.Core.World;
|
using AcDream.Core.World;
|
||||||
|
|
||||||
namespace AcDream.App.Rendering;
|
namespace AcDream.App.Rendering;
|
||||||
|
|
@ -12,7 +12,7 @@ namespace AcDream.App.Rendering;
|
||||||
/// from portal space to the destination world at the transition projection;
|
/// from portal space to the destination world at the transition projection;
|
||||||
/// there is no black-alpha compositor between them.
|
/// there is no black-alpha compositor between them.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class TeleportViewPlaneController
|
internal sealed class TeleportViewPlaneController
|
||||||
{
|
{
|
||||||
public const float TransitionViewPlaneDistance = 0.001f;
|
public const float TransitionViewPlaneDistance = 0.001f;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using AcDream.Core.Textures;
|
using AcDream.Core.Textures;
|
||||||
using DatReaderWriter;
|
using DatReaderWriter;
|
||||||
using AcDream.Content;
|
using AcDream.Content;
|
||||||
using DatReaderWriter.DBObjs;
|
using DatReaderWriter.DBObjs;
|
||||||
|
|
@ -13,21 +13,21 @@ namespace AcDream.App.Rendering;
|
||||||
/// Holds both texture arrays the terrain renderer samples from:
|
/// Holds both texture arrays the terrain renderer samples from:
|
||||||
/// <list type="bullet">
|
/// <list type="bullet">
|
||||||
/// <item><description>
|
/// <item><description>
|
||||||
/// <b>Terrain atlas</b> — one GL_TEXTURE_2D_ARRAY layer per terrain type
|
/// <b>Terrain atlas</b> — one GL_TEXTURE_2D_ARRAY layer per terrain type
|
||||||
/// (grass, dirt, sand, forest...), sourced from
|
/// (grass, dirt, sand, forest...), sourced from
|
||||||
/// Region.TerrainInfo.LandSurfaces.TexMerge.TerrainDesc.
|
/// Region.TerrainInfo.LandSurfaces.TexMerge.TerrainDesc.
|
||||||
/// </description></item>
|
/// </description></item>
|
||||||
/// <item><description>
|
/// <item><description>
|
||||||
/// <b>Alpha atlas</b> — one GL_TEXTURE_2D_ARRAY layer per blend mask,
|
/// <b>Alpha atlas</b> — one GL_TEXTURE_2D_ARRAY layer per blend mask,
|
||||||
/// sourced from CornerTerrainMaps / SideTerrainMaps / RoadMaps in the
|
/// sourced from CornerTerrainMaps / SideTerrainMaps / RoadMaps in the
|
||||||
/// same TexMerge. Used by the fragment shader to blend up to three
|
/// same TexMerge. Used by the fragment shader to blend up to three
|
||||||
/// terrain overlays and two roads on top of a base cell texture.
|
/// terrain overlays and two roads on top of a base cell texture.
|
||||||
/// </description></item>
|
/// </description></item>
|
||||||
/// </list>
|
/// </list>
|
||||||
/// The alpha atlas is built but not yet sampled by any shader — that wiring
|
/// The alpha atlas is built but not yet sampled by any shader — that wiring
|
||||||
/// lands in Phase 3c.4 along with the shader rewrite.
|
/// lands in Phase 3c.4 along with the shader rewrite.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed unsafe class TerrainAtlas : IDisposable
|
internal sealed unsafe class TerrainAtlas : IDisposable
|
||||||
{
|
{
|
||||||
private readonly GL _gl;
|
private readonly GL _gl;
|
||||||
|
|
||||||
|
|
@ -276,7 +276,7 @@ public sealed unsafe class TerrainAtlas : IDisposable
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Load corner, side, and road alpha maps from the TexMerge into a second
|
/// Load corner, side, and road alpha maps from the TexMerge into a second
|
||||||
/// GL_TEXTURE_2D_ARRAY. AC ships these as 512×512 PFID_A8 textures;
|
/// GL_TEXTURE_2D_ARRAY. AC ships these as 512×512 PFID_A8 textures;
|
||||||
/// <see cref="SurfaceDecoder.DecodeRenderSurface"/> expands each alpha byte
|
/// <see cref="SurfaceDecoder.DecodeRenderSurface"/> expands each alpha byte
|
||||||
/// into all four RGBA channels so the shader can sample from any channel.
|
/// into all four RGBA channels so the shader can sample from any channel.
|
||||||
///
|
///
|
||||||
|
|
@ -368,7 +368,7 @@ public sealed unsafe class TerrainAtlas : IDisposable
|
||||||
cornerTCodes, sideTCodes, roadRCodes);
|
cornerTCodes, sideTCodes, roadRCodes);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Alpha maps should all be uniform size (WorldBuilder asserts 512×512).
|
// Alpha maps should all be uniform size (WorldBuilder asserts 512×512).
|
||||||
// Fall back to the max observed so a stray mismatch doesn't crash us.
|
// Fall back to the max observed so a stray mismatch doesn't crash us.
|
||||||
int aMaxW = 1, aMaxH = 1;
|
int aMaxW = 1, aMaxH = 1;
|
||||||
foreach (var d in decoded)
|
foreach (var d in decoded)
|
||||||
|
|
@ -433,7 +433,7 @@ public sealed unsafe class TerrainAtlas : IDisposable
|
||||||
|
|
||||||
// Alpha maps ship as PFID_CUSTOM_LSCAPE_ALPHA (AC's landscape-alpha
|
// Alpha maps ship as PFID_CUSTOM_LSCAPE_ALPHA (AC's landscape-alpha
|
||||||
// format) or the more generic PFID_A8; terrain blending alpha masks
|
// format) or the more generic PFID_A8; terrain blending alpha masks
|
||||||
// MUST use isAdditive=true so R=G=B=A=val — the terrain fragment shader
|
// MUST use isAdditive=true so R=G=B=A=val — the terrain fragment shader
|
||||||
// reads .r for the blend weight. Palette is not used.
|
// reads .r for the blend weight. Palette is not used.
|
||||||
var d = SurfaceDecoder.DecodeRenderSurface(rs, palette: null, isClipMap: false, isAdditive: true);
|
var d = SurfaceDecoder.DecodeRenderSurface(rs, palette: null, isClipMap: false, isAdditive: true);
|
||||||
if (ReferenceEquals(d, DecodedTexture.Magenta))
|
if (ReferenceEquals(d, DecodedTexture.Magenta))
|
||||||
|
|
@ -515,7 +515,7 @@ public sealed unsafe class TerrainAtlas : IDisposable
|
||||||
/// A.5 T22.5: update GL_TEXTURE_MAX_ANISOTROPY on the terrain atlas at
|
/// A.5 T22.5: update GL_TEXTURE_MAX_ANISOTROPY on the terrain atlas at
|
||||||
/// runtime (called by
|
/// runtime (called by
|
||||||
/// <see cref="AcDream.App.Settings.RuntimeSettingsController.ReapplyQualityPreset"/> when
|
/// <see cref="AcDream.App.Settings.RuntimeSettingsController.ReapplyQualityPreset"/> when
|
||||||
/// the user changes Quality preset mid-session). Idempotent — calling with
|
/// the user changes Quality preset mid-session). Idempotent — calling with
|
||||||
/// the same level as the current setting is safe and produces no visual
|
/// the same level as the current setting is safe and produces no visual
|
||||||
/// change. The texture must not be resident-bindless when its parameters
|
/// change. The texture must not be resident-bindless when its parameters
|
||||||
/// are mutated; we temporarily make it non-resident if needed.
|
/// are mutated; we temporarily make it non-resident if needed.
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AcDream.App.Rendering.Gpu;
|
using AcDream.App.Rendering.Gpu;
|
||||||
using AcDream.App.Rendering.Wb;
|
using AcDream.App.Rendering.Wb;
|
||||||
using AcDream.Core.Terrain;
|
using AcDream.Core.Terrain;
|
||||||
|
|
@ -8,7 +8,7 @@ namespace AcDream.App.Rendering;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Phase N.5b modern terrain dispatcher. Single global VBO/EBO with a slot
|
/// Phase N.5b modern terrain dispatcher. Single global VBO/EBO with a slot
|
||||||
/// allocator (one slot per landblock, 384 verts × 40 bytes = 15,360 bytes
|
/// allocator (one slot per landblock, 384 verts × 40 bytes = 15,360 bytes
|
||||||
/// per slot). Per-frame: build a DrawElementsIndirectCommand array from
|
/// per slot). Per-frame: build a DrawElementsIndirectCommand array from
|
||||||
/// visible slots, upload, dispatch via glMultiDrawElementsIndirect. Atlas
|
/// visible slots, upload, dispatch via glMultiDrawElementsIndirect. Atlas
|
||||||
/// textures bound via bindless handles set per-frame as sampler uniforms.
|
/// textures bound via bindless handles set per-frame as sampler uniforms.
|
||||||
|
|
@ -16,9 +16,9 @@ namespace AcDream.App.Rendering;
|
||||||
/// Total ~6-8 GL calls per frame for terrain regardless of visible
|
/// Total ~6-8 GL calls per frame for terrain regardless of visible
|
||||||
/// landblock count.
|
/// landblock count.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed unsafe class TerrainModernRenderer : IDisposable
|
internal sealed unsafe class TerrainModernRenderer : IDisposable
|
||||||
{
|
{
|
||||||
// VertsPerLandblock MUST stay divisible by 6 — terrain_modern.vert uses
|
// VertsPerLandblock MUST stay divisible by 6 — terrain_modern.vert uses
|
||||||
// `gl_VertexID % 6` to pick the cell-corner index (BL/BR/TR/TL), and
|
// `gl_VertexID % 6` to pick the cell-corner index (BL/BR/TR/TL), and
|
||||||
// because we bake `slot * VertsPerLandblock` into indices CPU-side and
|
// because we bake `slot * VertsPerLandblock` into indices CPU-side and
|
||||||
// pass BaseVertex=0 to MultiDrawElementsIndirect, gl_VertexID becomes
|
// pass BaseVertex=0 to MultiDrawElementsIndirect, gl_VertexID becomes
|
||||||
|
|
@ -83,7 +83,7 @@ public sealed unsafe class TerrainModernRenderer : IDisposable
|
||||||
private uint _fallbackClipUbo;
|
private uint _fallbackClipUbo;
|
||||||
|
|
||||||
// Campaign V slice V2b (2026-07-27): uTerrainHandle/uAlphaHandle (uvec2)
|
// Campaign V slice V2b (2026-07-27): uTerrainHandle/uAlphaHandle (uvec2)
|
||||||
// became uTextureIndexA/uTextureIndexB (uint table slots) — cached
|
// became uTextureIndexA/uTextureIndexB (uint table slots) — cached
|
||||||
// uniform locations (matrix uniforms are set by name via Shader.SetMatrix4).
|
// uniform locations (matrix uniforms are set by name via Shader.SetMatrix4).
|
||||||
private int _uTextureIndexALoc;
|
private int _uTextureIndexALoc;
|
||||||
private int _uTextureIndexBLoc;
|
private int _uTextureIndexBLoc;
|
||||||
|
|
@ -91,8 +91,8 @@ public sealed unsafe class TerrainModernRenderer : IDisposable
|
||||||
private bool _textureTilingUploaded;
|
private bool _textureTilingUploaded;
|
||||||
|
|
||||||
// GL-only emulation of the eventual Vulkan global texture descriptor array
|
// GL-only emulation of the eventual Vulkan global texture descriptor array
|
||||||
// (binding=9, GpuBindingModel.StorageTextureTable). Owns its own table —
|
// (binding=9, GpuBindingModel.StorageTextureTable). Owns its own table —
|
||||||
// see GlBindlessHandleTable's doc comment and the campaign doc's §5.2 for
|
// see GlBindlessHandleTable's doc comment and the campaign doc's §5.2 for
|
||||||
// why terrain doesn't share WbDrawDispatcher's/EnvCellRenderer's tables.
|
// why terrain doesn't share WbDrawDispatcher's/EnvCellRenderer's tables.
|
||||||
private readonly GlBindlessHandleTable _textureTable = new();
|
private readonly GlBindlessHandleTable _textureTable = new();
|
||||||
private uint _textureTableSsbo;
|
private uint _textureTableSsbo;
|
||||||
|
|
@ -495,7 +495,7 @@ public sealed unsafe class TerrainModernRenderer : IDisposable
|
||||||
// Bind shader + uniforms + atlas handles.
|
// Bind shader + uniforms + atlas handles.
|
||||||
// Verified Phase W Stage 4 (T4.2): terrain projects from the camera view-proj;
|
// Verified Phase W Stage 4 (T4.2): terrain projects from the camera view-proj;
|
||||||
// no separate landscape viewpoint to sync. Both uView and uProjection derive
|
// no separate landscape viewpoint to sync. Both uView and uProjection derive
|
||||||
// from the ICamera passed into this method — the same camera used for all other
|
// from the ICamera passed into this method — the same camera used for all other
|
||||||
// renderers in the unified pipeline. Retail's LScape::update_viewpoint
|
// renderers in the unified pipeline. Retail's LScape::update_viewpoint
|
||||||
// pre-positions terrain to the outdoor landcell, but acdream uses the
|
// pre-positions terrain to the outdoor landcell, but acdream uses the
|
||||||
// unified camera matrix everywhere, so no separate viewpoint divergence can occur.
|
// unified camera matrix everywhere, so no separate viewpoint divergence can occur.
|
||||||
|
|
@ -508,7 +508,7 @@ public sealed unsafe class TerrainModernRenderer : IDisposable
|
||||||
// Campaign V slice V2b: pass each handle's binding=9 table slot
|
// Campaign V slice V2b: pass each handle's binding=9 table slot
|
||||||
// instead of the raw uvec2 handle. GLSL reconstructs
|
// instead of the raw uvec2 handle. GLSL reconstructs
|
||||||
// sampler2DArray(ACDREAM_TEXTURE_HANDLE(uTextureIndexA)) at the use
|
// sampler2DArray(ACDREAM_TEXTURE_HANDLE(uTextureIndexA)) at the use
|
||||||
// site — see terrain_modern.frag.
|
// site — see terrain_modern.frag.
|
||||||
uint terrainSlot = _textureTable.GetOrAdd(terrainHandle);
|
uint terrainSlot = _textureTable.GetOrAdd(terrainHandle);
|
||||||
uint alphaSlot = _textureTable.GetOrAdd(alphaHandle);
|
uint alphaSlot = _textureTable.GetOrAdd(alphaHandle);
|
||||||
FlushAndBindTextureTable();
|
FlushAndBindTextureTable();
|
||||||
|
|
@ -519,7 +519,7 @@ public sealed unsafe class TerrainModernRenderer : IDisposable
|
||||||
// when wired, else the no-clip fallback (count 0 = ungated terrain).
|
// when wired, else the no-clip fallback (count 0 = ungated terrain).
|
||||||
BindClipUboBinding2();
|
BindClipUboBinding2();
|
||||||
|
|
||||||
// #108-residual: retail terrain is SINGLE-SIDED — ACRender::landPolysDraw
|
// #108-residual: retail terrain is SINGLE-SIDED — ACRender::landPolysDraw
|
||||||
// (0x006b7040) draws each land triangle ONLY when the camera is on the
|
// (0x006b7040) draws each land triangle ONLY when the camera is on the
|
||||||
// POSITIVE (upper) side of its plane (Plane::which_side2 vs
|
// POSITIVE (upper) side of its plane (Plane::which_side2 vs
|
||||||
// Render::FrameCurrent, zFightTerrainAdjust bias). GL backface culling
|
// Render::FrameCurrent, zFightTerrainAdjust bias). GL backface culling
|
||||||
|
|
@ -527,13 +527,13 @@ public sealed unsafe class TerrainModernRenderer : IDisposable
|
||||||
// LandblockMesh emits every triangle CCW in world XY seen from above
|
// LandblockMesh emits every triangle CCW in world XY seen from above
|
||||||
// (LandblockMeshTests winding pin), which the unified camera chain
|
// (LandblockMeshTests winding pin), which the unified camera chain
|
||||||
// (CreateLookAt up=+Z + Numerics perspective) maps to CCW window
|
// (CreateLookAt up=+Z + Numerics perspective) maps to CCW window
|
||||||
// winding from above / CW from below (TerrainCullOrientationTests) —
|
// winding from above / CW from below (TerrainCullOrientationTests) —
|
||||||
// so FrontFace(Ccw)+Cull(Back) keeps the top side and culls the
|
// so FrontFace(Ccw)+Cull(Back) keeps the top side and culls the
|
||||||
// underside. WB drew the whole world with culling DISABLED
|
// underside. WB drew the whole world with culling DISABLED
|
||||||
// frame-globally (WB GameScene.cs:841 — an editor camera goes
|
// frame-globally (WB GameScene.cs:841 — an editor camera goes
|
||||||
// underground); inheriting that drew terrain DOUBLE-SIDED, and a
|
// underground); inheriting that drew terrain DOUBLE-SIDED, and a
|
||||||
// below-grade eye (cellar ascent) saw the UNDERSIDE of the grade
|
// below-grade eye (cellar ascent) saw the UNDERSIDE of the grade
|
||||||
// sheet through the exit-door aperture — the #108 grass window.
|
// sheet through the exit-door aperture — the #108 grass window.
|
||||||
// Self-contained state per feedback_render_self_contained_gl_state;
|
// Self-contained state per feedback_render_self_contained_gl_state;
|
||||||
// the frame-global CW + cull-off baseline is restored after the draw.
|
// the frame-global CW + cull-off baseline is restored after the draw.
|
||||||
_gl.Enable(EnableCap.CullFace);
|
_gl.Enable(EnableCap.CullFace);
|
||||||
|
|
|
||||||
|
|
@ -1,156 +0,0 @@
|
||||||
using Silk.NET.OpenGL;
|
|
||||||
|
|
||||||
namespace AcDream.App.Rendering;
|
|
||||||
|
|
||||||
internal interface ITextRenderGlStateApi
|
|
||||||
{
|
|
||||||
bool IsEnabled(EnableCap capability);
|
|
||||||
|
|
||||||
int GetInteger(GetPName parameter);
|
|
||||||
|
|
||||||
bool GetBoolean(GetPName parameter);
|
|
||||||
|
|
||||||
void SetCapability(EnableCap capability, bool enabled);
|
|
||||||
|
|
||||||
void DepthMask(bool enabled);
|
|
||||||
|
|
||||||
void BlendFuncSeparate(
|
|
||||||
BlendingFactor sourceRgb,
|
|
||||||
BlendingFactor destinationRgb,
|
|
||||||
BlendingFactor sourceAlpha,
|
|
||||||
BlendingFactor destinationAlpha);
|
|
||||||
|
|
||||||
void UseProgram(uint program);
|
|
||||||
|
|
||||||
void BindVertexArray(uint vertexArray);
|
|
||||||
|
|
||||||
void BindBuffer(BufferTargetARB target, uint buffer);
|
|
||||||
|
|
||||||
void ActiveTexture(TextureUnit unit);
|
|
||||||
|
|
||||||
void BindTexture(TextureTarget target, uint texture);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal sealed class SilkTextRenderGlStateApi : ITextRenderGlStateApi
|
|
||||||
{
|
|
||||||
private readonly GL _gl;
|
|
||||||
|
|
||||||
public SilkTextRenderGlStateApi(GL gl) =>
|
|
||||||
_gl = gl ?? throw new ArgumentNullException(nameof(gl));
|
|
||||||
|
|
||||||
public bool IsEnabled(EnableCap capability) => _gl.IsEnabled(capability);
|
|
||||||
|
|
||||||
public int GetInteger(GetPName parameter) => _gl.GetInteger(parameter);
|
|
||||||
|
|
||||||
public bool GetBoolean(GetPName parameter) => _gl.GetBoolean(parameter);
|
|
||||||
|
|
||||||
public void SetCapability(EnableCap capability, bool enabled)
|
|
||||||
{
|
|
||||||
if (enabled)
|
|
||||||
_gl.Enable(capability);
|
|
||||||
else
|
|
||||||
_gl.Disable(capability);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void DepthMask(bool enabled) => _gl.DepthMask(enabled);
|
|
||||||
|
|
||||||
public void BlendFuncSeparate(
|
|
||||||
BlendingFactor sourceRgb,
|
|
||||||
BlendingFactor destinationRgb,
|
|
||||||
BlendingFactor sourceAlpha,
|
|
||||||
BlendingFactor destinationAlpha) =>
|
|
||||||
_gl.BlendFuncSeparate(
|
|
||||||
sourceRgb,
|
|
||||||
destinationRgb,
|
|
||||||
sourceAlpha,
|
|
||||||
destinationAlpha);
|
|
||||||
|
|
||||||
public void UseProgram(uint program) => _gl.UseProgram(program);
|
|
||||||
|
|
||||||
public void BindVertexArray(uint vertexArray) => _gl.BindVertexArray(vertexArray);
|
|
||||||
|
|
||||||
public void BindBuffer(BufferTargetARB target, uint buffer) =>
|
|
||||||
_gl.BindBuffer(target, buffer);
|
|
||||||
|
|
||||||
public void ActiveTexture(TextureUnit unit) => _gl.ActiveTexture(unit);
|
|
||||||
|
|
||||||
public void BindTexture(TextureTarget target, uint texture) =>
|
|
||||||
_gl.BindTexture(target, texture);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Exact, focused state transaction for <see cref="TextRenderer.Flush"/>. It
|
|
||||||
/// captures every GL value that Flush or DrawLayer mutates, while avoiding the
|
|
||||||
/// dozens of unrelated synchronous reads made by the broad diagnostic scope.
|
|
||||||
/// </summary>
|
|
||||||
internal readonly struct TextRenderGlStateScope : IDisposable
|
|
||||||
{
|
|
||||||
private readonly ITextRenderGlStateApi _gl;
|
|
||||||
private readonly bool _depthTest;
|
|
||||||
private readonly bool _blend;
|
|
||||||
private readonly bool _cullFace;
|
|
||||||
private readonly bool _alphaToCoverage;
|
|
||||||
private readonly bool _multisample;
|
|
||||||
private readonly bool _depthWrite;
|
|
||||||
private readonly int _blendSourceRgb;
|
|
||||||
private readonly int _blendDestinationRgb;
|
|
||||||
private readonly int _blendSourceAlpha;
|
|
||||||
private readonly int _blendDestinationAlpha;
|
|
||||||
private readonly int _program;
|
|
||||||
private readonly int _vertexArray;
|
|
||||||
private readonly int _arrayBuffer;
|
|
||||||
private readonly int _activeTexture;
|
|
||||||
private readonly int _texture0Binding2D;
|
|
||||||
|
|
||||||
public TextRenderGlStateScope(ITextRenderGlStateApi gl)
|
|
||||||
{
|
|
||||||
_gl = gl ?? throw new ArgumentNullException(nameof(gl));
|
|
||||||
_depthTest = gl.IsEnabled(EnableCap.DepthTest);
|
|
||||||
_blend = gl.IsEnabled(EnableCap.Blend);
|
|
||||||
_cullFace = gl.IsEnabled(EnableCap.CullFace);
|
|
||||||
_alphaToCoverage = gl.IsEnabled(EnableCap.SampleAlphaToCoverage);
|
|
||||||
_multisample = gl.IsEnabled(EnableCap.Multisample);
|
|
||||||
_depthWrite = gl.GetBoolean(GetPName.DepthWritemask);
|
|
||||||
_blendSourceRgb = gl.GetInteger(GetPName.BlendSrcRgb);
|
|
||||||
_blendDestinationRgb = gl.GetInteger(GetPName.BlendDstRgb);
|
|
||||||
_blendSourceAlpha = gl.GetInteger(GetPName.BlendSrcAlpha);
|
|
||||||
_blendDestinationAlpha = gl.GetInteger(GetPName.BlendDstAlpha);
|
|
||||||
_program = gl.GetInteger(GetPName.CurrentProgram);
|
|
||||||
_vertexArray = gl.GetInteger(GetPName.VertexArrayBinding);
|
|
||||||
_arrayBuffer = gl.GetInteger(GetPName.ArrayBufferBinding);
|
|
||||||
_activeTexture = gl.GetInteger(GetPName.ActiveTexture);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
gl.ActiveTexture(TextureUnit.Texture0);
|
|
||||||
_texture0Binding2D = gl.GetInteger(GetPName.TextureBinding2D);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
gl.ActiveTexture((TextureUnit)_activeTexture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
_gl.UseProgram((uint)_program);
|
|
||||||
_gl.BindVertexArray((uint)_vertexArray);
|
|
||||||
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, (uint)_arrayBuffer);
|
|
||||||
|
|
||||||
_gl.ActiveTexture(TextureUnit.Texture0);
|
|
||||||
_gl.BindTexture(TextureTarget.Texture2D, (uint)_texture0Binding2D);
|
|
||||||
_gl.ActiveTexture((TextureUnit)_activeTexture);
|
|
||||||
|
|
||||||
_gl.DepthMask(_depthWrite);
|
|
||||||
_gl.BlendFuncSeparate(
|
|
||||||
(BlendingFactor)_blendSourceRgb,
|
|
||||||
(BlendingFactor)_blendDestinationRgb,
|
|
||||||
(BlendingFactor)_blendSourceAlpha,
|
|
||||||
(BlendingFactor)_blendDestinationAlpha);
|
|
||||||
_gl.SetCapability(EnableCap.DepthTest, _depthTest);
|
|
||||||
_gl.SetCapability(EnableCap.Blend, _blend);
|
|
||||||
_gl.SetCapability(EnableCap.CullFace, _cullFace);
|
|
||||||
_gl.SetCapability(EnableCap.SampleAlphaToCoverage, _alphaToCoverage);
|
|
||||||
_gl.SetCapability(EnableCap.Multisample, _multisample);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using AcDream.App.Rendering.Wb;
|
|
||||||
using Silk.NET.OpenGL;
|
|
||||||
|
|
||||||
namespace AcDream.App.Rendering;
|
namespace AcDream.App.Rendering;
|
||||||
|
|
||||||
|
|
@ -15,36 +11,39 @@ namespace AcDream.App.Rendering;
|
||||||
/// <see cref="Begin"/> at the start of a HUD pass, queue geometry via
|
/// <see cref="Begin"/> at the start of a HUD pass, queue geometry via
|
||||||
/// <see cref="DrawString"/> / <see cref="DrawRect"/>, then <see cref="Flush"/>.
|
/// <see cref="DrawString"/> / <see cref="DrawRect"/>, then <see cref="Flush"/>.
|
||||||
///
|
///
|
||||||
/// Uses two internal vertex buffers (text and rect) flushed in two draw calls
|
/// Campaign V slice V4a: ported onto <see cref="IGpuDevice"/>. One pipeline
|
||||||
/// to avoid a per-vertex "use texture" flag. Rects are drawn first so text
|
/// (blend, depth-disable, and the MSAA/alpha-to-coverage isolation the prior
|
||||||
/// sits on top of background panels.
|
/// <c>TextRenderGlStateScope</c> restored by hand are now baked into the
|
||||||
|
/// pipeline description) and one pass per <see cref="Flush"/>, with each
|
||||||
|
/// bucket/segment getting its own per-frame ring allocation instead of a
|
||||||
|
/// shared, growable VBO. Rects are drawn first so text sits on top of
|
||||||
|
/// background panels — bucket ORDER is unchanged, only how each bucket
|
||||||
|
/// reaches the GPU.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed unsafe class TextRenderer : IDisposable
|
internal sealed class TextRenderer : IDisposable
|
||||||
{
|
{
|
||||||
private const int FloatsPerVertex = 8; // pos(2) + uv(2) + color(4)
|
private const int FloatsPerVertex = 8; // pos(2) + uv(2) + color(4)
|
||||||
|
private const int VertexStrideBytes = FloatsPerVertex * sizeof(float);
|
||||||
|
|
||||||
private readonly GL _gl;
|
private static readonly GpuVertexLayout VertexLayout = new(
|
||||||
private readonly ITextRenderGlStateApi _glState;
|
StrideBytes: VertexStrideBytes,
|
||||||
private readonly Shader _shader;
|
[
|
||||||
private readonly ResourceCleanupGroup _resources;
|
new GpuVertexAttribute(0, GpuVertexFormat.Float2, 0),
|
||||||
private uint _vao;
|
new GpuVertexAttribute(1, GpuVertexFormat.Float2, 8),
|
||||||
private uint _vbo;
|
new GpuVertexAttribute(2, GpuVertexFormat.Float4, 16),
|
||||||
private readonly uint _whiteTex; // 1×1 white, for solid fills routed through the sprite bucket
|
]);
|
||||||
private int _vboCapacityBytes;
|
|
||||||
|
|
||||||
private sealed class FrameBufferSet
|
// uUseTexture values the ui_text.frag shader branches on (reusing the
|
||||||
{
|
// shared push-constant block's uRenderPass scalar — see the shader's own
|
||||||
public uint Vao;
|
// comment for why there is no dedicated field).
|
||||||
public uint Vbo;
|
private const int UseTextureNone = 0;
|
||||||
public int CapacityBytes;
|
private const int UseTextureFont = 1;
|
||||||
public int UsedBytes;
|
private const int UseTextureSprite = 2;
|
||||||
}
|
|
||||||
|
|
||||||
private readonly FrameBufferSet[] _frameBuffers;
|
private readonly IGpuDevice _device;
|
||||||
private FrameBufferSet? _activeFrameBuffer;
|
private readonly IGpuPipeline _pipeline;
|
||||||
|
|
||||||
internal long DynamicBufferCapacityBytes =>
|
private sealed class SpriteSeg { public GpuTextureSlot TextureSlot; public readonly List<float> Verts = new(256); }
|
||||||
_frameBuffers.Sum(set => (long)set.CapacityBytes);
|
|
||||||
|
|
||||||
private readonly List<float> _textBuf = new(8192);
|
private readonly List<float> _textBuf = new(8192);
|
||||||
private readonly List<float> _rectBuf = new(1024);
|
private readonly List<float> _rectBuf = new(1024);
|
||||||
|
|
@ -53,16 +52,14 @@ public sealed unsafe class TextRenderer : IDisposable
|
||||||
// Drawing segments in submission order preserves painter z-order for
|
// Drawing segments in submission order preserves painter z-order for
|
||||||
// sprite-on-sprite UI. (The old per-texture dictionary drew a REUSED texture
|
// sprite-on-sprite UI. (The old per-texture dictionary drew a REUSED texture
|
||||||
// at its FIRST-insertion point, so later bar sprites covered glyphs emitted
|
// at its FIRST-insertion point, so later bar sprites covered glyphs emitted
|
||||||
// earlier via the shared dat-font atlas — the stamina/mana numbers vanished.)
|
// earlier via the shared dat-font atlas — the stamina/mana numbers vanished.)
|
||||||
private sealed class SpriteSeg { public uint Texture; public readonly List<float> Verts = new(256); }
|
|
||||||
|
|
||||||
private readonly List<SpriteSeg> _spriteSegs = new();
|
private readonly List<SpriteSeg> _spriteSegs = new();
|
||||||
private int _segUsed;
|
private int _segUsed;
|
||||||
private int _textVerts;
|
private int _textVerts;
|
||||||
private int _rectVerts;
|
private int _rectVerts;
|
||||||
private Vector2 _screenSize;
|
private Vector2 _screenSize;
|
||||||
|
|
||||||
// Overlay layer — a parallel set of buckets drawn AFTER the normal sprite/rect/text
|
// Overlay layer — a parallel set of buckets drawn AFTER the normal sprite/rect/text
|
||||||
// buckets, so open popups/menus composite on top of EVERYTHING, including translucent
|
// buckets, so open popups/menus composite on top of EVERYTHING, including translucent
|
||||||
// rect panel backgrounds (which otherwise always win because rects flush after
|
// rect panel backgrounds (which otherwise always win because rects flush after
|
||||||
// sprites). Routed by OverlayMode; the UI root sets it for the popup traversal.
|
// sprites). Routed by OverlayMode; the UI root sets it for the popup traversal.
|
||||||
|
|
@ -77,142 +74,38 @@ public sealed unsafe class TextRenderer : IDisposable
|
||||||
/// of all normal-layer geometry). Set by the UI root around the popup/overlay pass.</summary>
|
/// of all normal-layer geometry). Set by the UI root around the popup/overlay pass.</summary>
|
||||||
public bool OverlayMode { get; set; }
|
public bool OverlayMode { get; set; }
|
||||||
|
|
||||||
public TextRenderer(GL gl, string shaderDir)
|
|
||||||
{
|
|
||||||
_gl = gl;
|
|
||||||
_glState = new SilkTextRenderGlStateApi(gl);
|
|
||||||
var resources = new ResourceCleanupGroup();
|
|
||||||
Shader? shader = null;
|
|
||||||
var frameBuffers = new FrameBufferSet[3];
|
|
||||||
uint whiteTexture = 0;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
shader = new Shader(gl,
|
|
||||||
Path.Combine(shaderDir, "ui_text.vert"),
|
|
||||||
Path.Combine(shaderDir, "ui_text.frag"));
|
|
||||||
resources.Add("text shader", shader.Dispose);
|
|
||||||
|
|
||||||
for (int i = 0; i < frameBuffers.Length; i++)
|
|
||||||
frameBuffers[i] = CreateFrameBufferSet(resources);
|
|
||||||
|
|
||||||
// 1×1 white texture so DrawFill can route solid-colour quads through the SPRITE
|
|
||||||
// bucket (the shader multiplies texel×color → white×color = color). Lets a panel
|
|
||||||
// background draw UNDER its text in painter order, which DrawRect's separate
|
|
||||||
// bucket cannot (it always composites after all sprites).
|
|
||||||
whiteTexture = GlResourceCommand.CreateTexture(
|
|
||||||
_gl,
|
|
||||||
"TextRenderer white texture");
|
|
||||||
uint ownedWhiteTexture = whiteTexture;
|
|
||||||
resources.Add(
|
|
||||||
"white texture",
|
|
||||||
() => GlResourceCommand.DeleteTexture(
|
|
||||||
_gl,
|
|
||||||
ownedWhiteTexture,
|
|
||||||
$"delete TextRenderer white texture {ownedWhiteTexture}"));
|
|
||||||
GlResourceCommand.Execute(
|
|
||||||
_gl,
|
|
||||||
"initialize TextRenderer white texture",
|
|
||||||
() =>
|
|
||||||
{
|
|
||||||
_gl.BindTexture(TextureTarget.Texture2D, whiteTexture);
|
|
||||||
Span<byte> whitePixel = stackalloc byte[] { 255, 255, 255, 255 };
|
|
||||||
fixed (byte* wp = whitePixel)
|
|
||||||
_gl.TexImage2D(TextureTarget.Texture2D, 0, (int)InternalFormat.Rgba8, 1, 1, 0,
|
|
||||||
PixelFormat.Rgba, PixelType.UnsignedByte, wp);
|
|
||||||
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Nearest);
|
|
||||||
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMinFilter.Nearest);
|
|
||||||
_gl.BindTexture(TextureTarget.Texture2D, 0);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
catch (Exception constructionFailure)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
resources.RetryCleanup();
|
|
||||||
}
|
|
||||||
catch (Exception cleanupFailure)
|
|
||||||
{
|
|
||||||
throw new GlResourceConstructionException(
|
|
||||||
"TextRenderer construction failed and its published GL resources did not cleanly roll back.",
|
|
||||||
resources,
|
|
||||||
[constructionFailure, cleanupFailure]);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
|
|
||||||
_resources = resources;
|
|
||||||
_shader = shader;
|
|
||||||
_frameBuffers = frameBuffers;
|
|
||||||
_whiteTex = whiteTexture;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Selects the GPU-fenced frame slot and resets its append cursor. Every
|
/// No longer meaningful post-V4a: per-frame vertex data comes from the
|
||||||
/// UI segment rendered during the frame receives a distinct byte range;
|
/// device's shared ring rather than a VBO this class owns. Kept (returning
|
||||||
/// later text or sprite batches cannot overwrite an earlier in-flight draw.
|
/// 0) so <c>RenderFrameDiagnosticSources</c>'s telemetry read still compiles;
|
||||||
|
/// the dynamic-buffer dimension it reported is now a device-wide, not a
|
||||||
|
/// per-renderer, concern.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void BeginFrame(int frameSlot)
|
internal long DynamicBufferCapacityBytes => 0;
|
||||||
|
|
||||||
|
public TextRenderer(IGpuDevice device)
|
||||||
{
|
{
|
||||||
if ((uint)frameSlot >= (uint)_frameBuffers.Length)
|
_device = device ?? throw new ArgumentNullException(nameof(device));
|
||||||
throw new ArgumentOutOfRangeException(nameof(frameSlot));
|
_pipeline = _device.CreatePipeline(new GpuPipelineDescription
|
||||||
|
{
|
||||||
FrameBufferSet set = _frameBuffers[frameSlot];
|
Name = "ui-text",
|
||||||
set.UsedBytes = 0;
|
Shaders = new GpuShaderSet("ui_text"),
|
||||||
_activeFrameBuffer = set;
|
VertexLayout = VertexLayout,
|
||||||
_vao = set.Vao;
|
Topology = GpuPrimitiveTopology.TriangleList,
|
||||||
_vbo = set.Vbo;
|
Blend = GpuBlendMode.StraightAlpha,
|
||||||
_vboCapacityBytes = set.CapacityBytes;
|
// The retained UI is a self-contained 2-D pass: depth is
|
||||||
}
|
// irrelevant (feedback_render_self_contained_gl_state) and the
|
||||||
|
// world pass's alpha-to-coverage/multisample state must not leak
|
||||||
private FrameBufferSet CreateFrameBufferSet(ResourceCleanupGroup resources)
|
// in — SampleCount=1 drives the GL backend's GL_MULTISAMPLE
|
||||||
{
|
// toggle off when this pipeline binds (GlGpuDevice.ApplyRenderState),
|
||||||
uint vao = TrackedGlResource.CreateVertexArray(
|
// which is what the deleted TextRenderGlStateScope used to restore
|
||||||
_gl,
|
// by hand around every Flush.
|
||||||
"TextRenderer frame VAO creation");
|
Depth = GpuDepthState.Disabled,
|
||||||
RetryableGpuResourceRelease vaoRelease =
|
Cull = GpuCullMode.None,
|
||||||
TrackedGlResource.CreateRetryableVertexArrayDeletion(
|
AlphaToCoverage = false,
|
||||||
_gl,
|
ColorWrite = true,
|
||||||
vao,
|
SampleCount = 1,
|
||||||
"TextRenderer frame VAO disposal");
|
});
|
||||||
resources.Add("frame VAO", vaoRelease.Run);
|
|
||||||
var set = new FrameBufferSet { Vao = vao };
|
|
||||||
|
|
||||||
uint vbo = TrackedGlResource.CreateBuffer(
|
|
||||||
_gl,
|
|
||||||
"TextRenderer frame VBO creation");
|
|
||||||
set.Vbo = vbo;
|
|
||||||
RetryableGpuResourceRelease? vboRelease = null;
|
|
||||||
resources.Add(
|
|
||||||
"frame VBO",
|
|
||||||
() =>
|
|
||||||
{
|
|
||||||
vboRelease ??= TrackedGlResource.CreateRetryableBufferDeletion(
|
|
||||||
_gl,
|
|
||||||
vbo,
|
|
||||||
set.CapacityBytes,
|
|
||||||
"TextRenderer frame VBO disposal");
|
|
||||||
vboRelease.Run();
|
|
||||||
});
|
|
||||||
|
|
||||||
GlResourceCommand.Execute(
|
|
||||||
_gl,
|
|
||||||
"initialize TextRenderer frame VAO and VBO",
|
|
||||||
() =>
|
|
||||||
{
|
|
||||||
_gl.BindVertexArray(set.Vao);
|
|
||||||
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, set.Vbo);
|
|
||||||
uint stride = FloatsPerVertex * sizeof(float);
|
|
||||||
_gl.EnableVertexAttribArray(0);
|
|
||||||
_gl.VertexAttribPointer(0, 2, VertexAttribPointerType.Float, false, stride, (void*)0);
|
|
||||||
_gl.EnableVertexAttribArray(1);
|
|
||||||
_gl.VertexAttribPointer(1, 2, VertexAttribPointerType.Float, false, stride, (void*)(2 * sizeof(float)));
|
|
||||||
_gl.EnableVertexAttribArray(2);
|
|
||||||
_gl.VertexAttribPointer(2, 4, VertexAttribPointerType.Float, false, stride, (void*)(4 * sizeof(float)));
|
|
||||||
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0);
|
|
||||||
_gl.BindVertexArray(0);
|
|
||||||
});
|
|
||||||
return set;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Begin a HUD pass. Call once per frame before any Draw* calls.</summary>
|
/// <summary>Begin a HUD pass. Call once per frame before any Draw* calls.</summary>
|
||||||
|
|
@ -241,11 +134,11 @@ public sealed unsafe class TextRenderer : IDisposable
|
||||||
|
|
||||||
/// <summary>Draw a solid-colour quad through the SPRITE bucket (and the overlay layer
|
/// <summary>Draw a solid-colour quad through the SPRITE bucket (and the overlay layer
|
||||||
/// when active), so it composites in painter order with sprites + dat-font text. Use
|
/// when active), so it composites in painter order with sprites + dat-font text. Use
|
||||||
/// this — not <see cref="DrawRect"/> — for a panel BACKGROUND that text draws on top of:
|
/// this — not <see cref="DrawRect"/> — for a panel BACKGROUND that text draws on top of:
|
||||||
/// DrawRect's bucket always flushes after all sprites, so a rect background would cover
|
/// DrawRect's bucket always flushes after all sprites, so a rect background would cover
|
||||||
/// the text instead.</summary>
|
/// the text instead.</summary>
|
||||||
public void DrawFill(float x, float y, float w, float h, Vector4 color)
|
public void DrawFill(float x, float y, float w, float h, Vector4 color)
|
||||||
=> DrawSprite(_whiteTex, x, y, w, h, 0f, 0f, 1f, 1f, color);
|
=> DrawSprite(_device.DefaultTextureSlot, x, y, w, h, 0f, 0f, 1f, 1f, color);
|
||||||
|
|
||||||
/// <summary>Draw a 1-pixel-thick outline rect.</summary>
|
/// <summary>Draw a 1-pixel-thick outline rect.</summary>
|
||||||
public void DrawRectOutline(float x, float y, float w, float h, Vector4 color, float thickness = 1f)
|
public void DrawRectOutline(float x, float y, float w, float h, Vector4 color, float thickness = 1f)
|
||||||
|
|
@ -313,7 +206,7 @@ public sealed unsafe class TextRenderer : IDisposable
|
||||||
}
|
}
|
||||||
if (!font.TryGetGlyph(c, out var g))
|
if (!font.TryGetGlyph(c, out var g))
|
||||||
{
|
{
|
||||||
// Unknown glyph — skip its advance width if '?' exists.
|
// Unknown glyph — skip its advance width if '?' exists.
|
||||||
if (font.TryGetGlyph('?', out var q))
|
if (font.TryGetGlyph('?', out var q))
|
||||||
cursorX += q.Advance;
|
cursorX += q.Advance;
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -344,9 +237,9 @@ public sealed unsafe class TextRenderer : IDisposable
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Draw a textured sprite quad in screen pixel space with an explicit
|
/// Draw a textured sprite quad in screen pixel space with an explicit
|
||||||
/// source-UV rectangle (for 9-slice / atlas sub-regions). Batched per
|
/// source-UV rectangle (for 9-slice / atlas sub-regions). Batched per
|
||||||
/// GL texture handle; flushed with uUseTexture=2 (RGBA modulate).
|
/// texture-table slot, flushed with uUseTexture=2 (RGBA modulate).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void DrawSprite(uint texture, float x, float y, float w, float h,
|
public void DrawSprite(GpuTextureSlot texture, float x, float y, float w, float h,
|
||||||
float u0, float v0, float u1, float v1, Vector4 tint)
|
float u0, float v0, float u1, float v1, Vector4 tint)
|
||||||
{
|
{
|
||||||
SpriteSeg seg = OverlayMode
|
SpriteSeg seg = OverlayMode
|
||||||
|
|
@ -358,18 +251,18 @@ public sealed unsafe class TextRenderer : IDisposable
|
||||||
/// <summary>Pick the sprite segment for <paramref name="texture"/>: extend the current
|
/// <summary>Pick the sprite segment for <paramref name="texture"/>: extend the current
|
||||||
/// same-texture run, else reuse a pooled segment, else allocate. Submission order is
|
/// same-texture run, else reuse a pooled segment, else allocate. Submission order is
|
||||||
/// preserved (painter z-order for sprite-on-sprite UI).</summary>
|
/// preserved (painter z-order for sprite-on-sprite UI).</summary>
|
||||||
private static SpriteSeg NextSpriteSeg(List<SpriteSeg> segs, ref int used, uint texture)
|
private static SpriteSeg NextSpriteSeg(List<SpriteSeg> segs, ref int used, GpuTextureSlot texture)
|
||||||
{
|
{
|
||||||
if (used > 0 && segs[used - 1].Texture == texture)
|
if (used > 0 && segs[used - 1].TextureSlot == texture)
|
||||||
return segs[used - 1];
|
return segs[used - 1];
|
||||||
if (used < segs.Count)
|
if (used < segs.Count)
|
||||||
{
|
{
|
||||||
var s = segs[used++];
|
var s = segs[used++];
|
||||||
s.Texture = texture;
|
s.TextureSlot = texture;
|
||||||
s.Verts.Clear();
|
s.Verts.Clear();
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
var ns = new SpriteSeg { Texture = texture };
|
var ns = new SpriteSeg { TextureSlot = texture };
|
||||||
segs.Add(ns);
|
segs.Add(ns);
|
||||||
used++;
|
used++;
|
||||||
return ns;
|
return ns;
|
||||||
|
|
@ -381,10 +274,10 @@ public sealed unsafe class TextRenderer : IDisposable
|
||||||
{
|
{
|
||||||
// Two triangles (6 verts). CCW in pixel space is clockwise in NDC
|
// Two triangles (6 verts). CCW in pixel space is clockwise in NDC
|
||||||
// because the vertex shader flips Y, so OpenGL's default front-face
|
// because the vertex shader flips Y, so OpenGL's default front-face
|
||||||
// is GL_CCW — we rely on cull-face being disabled during HUD pass.
|
// is GL_CCW — we rely on cull-face being disabled during HUD pass.
|
||||||
// (x, y) ─ (x+w, y)
|
// (x, y) ─ (x+w, y)
|
||||||
// │ │
|
// │ │
|
||||||
// (x, y+h) ─ (x+w, y+h)
|
// (x, y+h) ─ (x+w, y+h)
|
||||||
//
|
//
|
||||||
// Triangle 1: (x,y) (x+w,y+h) (x+w,y)
|
// Triangle 1: (x,y) (x+w,y+h) (x+w,y)
|
||||||
// Triangle 2: (x,y) (x,y+h) (x+w,y+h)
|
// Triangle 2: (x,y) (x,y+h) (x+w,y+h)
|
||||||
|
|
@ -402,131 +295,105 @@ public sealed unsafe class TextRenderer : IDisposable
|
||||||
V(x + w, y + h, u1, v1);
|
V(x + w, y + h, u1, v1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Upload + draw accumulated rects + text. font may be null if only DrawRect was used.</summary>
|
/// <summary>Upload + draw accumulated rects + text against the current frame. font may
|
||||||
public void Flush(BitmapFont? font)
|
/// be null if only DrawRect was used.</summary>
|
||||||
|
public void Flush(BitmapFont? font, IGpuFrame frame)
|
||||||
{
|
{
|
||||||
bool anyNormal = _segUsed > 0 || _textVerts > 0 || _rectVerts > 0;
|
bool anyNormal = _segUsed > 0 || _textVerts > 0 || _rectVerts > 0;
|
||||||
bool anyOverlay = _overlaySegUsed > 0 || _overlayTextVerts > 0 || _overlayRectVerts > 0;
|
bool anyOverlay = _overlaySegUsed > 0 || _overlayTextVerts > 0 || _overlayRectVerts > 0;
|
||||||
if (!anyNormal && !anyOverlay) return;
|
if (!anyNormal && !anyOverlay) return;
|
||||||
|
ArgumentNullException.ThrowIfNull(frame);
|
||||||
|
|
||||||
// Retained UI is a private render pass: an upload or draw failure must
|
using IGpuPassEncoder pass = frame.BeginPass(new GpuPassDescription
|
||||||
// not leak its depth/cull/blend/MSAA state into a later recoverable
|
{
|
||||||
// frame. The focused scope restores from Flush's generated finally,
|
Name = "ui-text",
|
||||||
// including when either DrawLayer call throws.
|
// GL's BeginPass deliberately does not touch viewport/scissor for
|
||||||
using var stateScope = new TextRenderGlStateScope(_glState);
|
// a Target:null pass, and Load/Store against the backbuffer
|
||||||
|
// reproduces exactly what this pass did before the RHI existed —
|
||||||
|
// the frame spine still owns clears until slice V4h.
|
||||||
|
Color = new GpuColorAttachment(
|
||||||
|
Target: null,
|
||||||
|
Load: GpuLoadOp.Load,
|
||||||
|
Store: GpuStoreOp.Store,
|
||||||
|
ClearColor: default),
|
||||||
|
Depth = null,
|
||||||
|
SampleCount = 1,
|
||||||
|
});
|
||||||
|
pass.BindPipeline(_pipeline);
|
||||||
|
|
||||||
_shader.Use();
|
GpuPushConstants baseConstants = GpuPushConstants.Default;
|
||||||
_shader.SetVec2("uScreenSize", _screenSize);
|
baseConstants.ParamA = _screenSize.X;
|
||||||
|
baseConstants.ParamB = _screenSize.Y;
|
||||||
|
|
||||||
_gl.BindVertexArray(_vao);
|
// LAYERED compositing for the UI (background → fill → text):
|
||||||
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, _vbo);
|
// 1. RGBA dat sprites — window chrome / panel backgrounds (behind)
|
||||||
|
// 2. Untextured rects — widget fills (e.g. vital bars) on the chrome
|
||||||
// Establish the self-contained UI pass state.
|
// 3. Text glyphs — on top
|
||||||
// The world pass leaves alpha-to-coverage + multisample enabled (WbDrawDispatcher,
|
|
||||||
// QualitySettings MSAA). If they bleed into the UI pass, each glyph's soft alpha
|
|
||||||
// EDGE is converted to dithered MSAA coverage instead of a clean alpha blend —
|
|
||||||
// the "text not sharp / fuzzy" artifact. The UI composites with straight alpha
|
|
||||||
// blending and must own this state (feedback_render_self_contained_gl_state).
|
|
||||||
_gl.Disable(EnableCap.SampleAlphaToCoverage);
|
|
||||||
_gl.Disable(EnableCap.Multisample);
|
|
||||||
_gl.Disable(EnableCap.DepthTest);
|
|
||||||
_gl.Disable(EnableCap.CullFace);
|
|
||||||
_gl.DepthMask(false);
|
|
||||||
_gl.Enable(EnableCap.Blend);
|
|
||||||
_gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
|
|
||||||
|
|
||||||
// LAYERED compositing for the UI (background → fill → text):
|
|
||||||
// 1. RGBA dat sprites — window chrome / panel backgrounds (behind)
|
|
||||||
// 2. Untextured rects — widget fills (e.g. vital bars) on the chrome
|
|
||||||
// 3. Text glyphs — on top
|
|
||||||
// Bucket 1 (sprites) draws in SUBMISSION (painter) order via _spriteSegs,
|
// Bucket 1 (sprites) draws in SUBMISSION (painter) order via _spriteSegs,
|
||||||
// so sprite-on-sprite z is preserved. Buckets 2 (rects) + 3 (debug text)
|
// so sprite-on-sprite z is preserved. Buckets 2 (rects) + 3 (debug text)
|
||||||
// composite on top, in that order. The OVERLAY layer repeats all three
|
// composite on top, in that order. The OVERLAY layer repeats all three
|
||||||
// AFTER the normal layer, so open popups beat even the rect backgrounds.
|
// AFTER the normal layer, so open popups beat even the rect backgrounds.
|
||||||
DrawLayer(_spriteSegs, _segUsed, _rectBuf, _rectVerts, _textBuf, _textVerts, font);
|
DrawLayer(pass, frame, in baseConstants, _spriteSegs, _segUsed, _rectBuf, _rectVerts, _textBuf, _textVerts, font);
|
||||||
DrawLayer(_overlaySpriteSegs, _overlaySegUsed, _overlayRectBuf, _overlayRectVerts, _overlayTextBuf, _overlayTextVerts, font);
|
DrawLayer(pass, frame, in baseConstants, _overlaySpriteSegs, _overlaySegUsed, _overlayRectBuf, _overlayRectVerts, _overlayTextBuf, _overlayTextVerts, font);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Draw one compositing layer: sprites (submission order, one call per
|
/// <summary>Draw one compositing layer: sprites (submission order, one draw per
|
||||||
/// texture) → untextured rects → debug-font text. Shared by the normal and overlay
|
/// segment) → untextured rects → debug-font text. Shared by the normal and overlay
|
||||||
/// layers; GL state + shader are set up by <see cref="Flush"/>.</summary>
|
/// layers; pipeline + pass are already bound by <see cref="Flush"/>.</summary>
|
||||||
private void DrawLayer(
|
private void DrawLayer(
|
||||||
|
IGpuPassEncoder pass,
|
||||||
|
IGpuFrame frame,
|
||||||
|
in GpuPushConstants baseConstants,
|
||||||
List<SpriteSeg> spriteSegs, int segUsed,
|
List<SpriteSeg> spriteSegs, int segUsed,
|
||||||
List<float> rectBuf, int rectVerts,
|
List<float> rectBuf, int rectVerts,
|
||||||
List<float> textBuf, int textVerts, BitmapFont? font)
|
List<float> textBuf, int textVerts, BitmapFont? font)
|
||||||
{
|
{
|
||||||
// 1. RGBA dat sprites — one draw call per distinct GL texture.
|
// 1. RGBA dat sprites — one draw per distinct texture-table slot.
|
||||||
if (segUsed > 0)
|
for (int i = 0; i < segUsed; i++)
|
||||||
{
|
{
|
||||||
_shader.SetInt("uUseTexture", 2);
|
SpriteSeg seg = spriteSegs[i];
|
||||||
_gl.ActiveTexture(TextureUnit.Texture0);
|
if (seg.Verts.Count == 0) continue;
|
||||||
_shader.SetInt("uTex", 0);
|
DrawBucket(pass, frame, in baseConstants, seg.Verts, UseTextureSprite, seg.TextureSlot);
|
||||||
for (int i = 0; i < segUsed; i++)
|
|
||||||
{
|
|
||||||
var seg = spriteSegs[i];
|
|
||||||
if (seg.Verts.Count == 0) continue;
|
|
||||||
_gl.BindTexture(TextureTarget.Texture2D, seg.Texture);
|
|
||||||
int firstVertex = UploadBuffer(seg.Verts);
|
|
||||||
_gl.DrawArrays(PrimitiveType.Triangles, firstVertex, (uint)(seg.Verts.Count / FloatsPerVertex));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Untextured rects — widget fills on top of the chrome.
|
// 2. Untextured rects — widget fills on top of the chrome.
|
||||||
if (rectVerts > 0)
|
if (rectVerts > 0)
|
||||||
{
|
DrawBucket(pass, frame, in baseConstants, rectBuf, UseTextureNone, GpuTextureSlot.Unassigned);
|
||||||
_shader.SetInt("uUseTexture", 0);
|
|
||||||
int firstVertex = UploadBuffer(rectBuf);
|
|
||||||
_gl.DrawArrays(PrimitiveType.Triangles, firstVertex, (uint)rectVerts);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Textured debug-font text glyphs on top.
|
// 3. Textured debug-font text glyphs on top.
|
||||||
if (textVerts > 0 && font is not null)
|
if (textVerts > 0 && font is not null)
|
||||||
{
|
DrawBucket(pass, frame, in baseConstants, textBuf, UseTextureFont, font.TextureId);
|
||||||
_shader.SetInt("uUseTexture", 1);
|
|
||||||
_gl.ActiveTexture(TextureUnit.Texture0);
|
|
||||||
_gl.BindTexture(TextureTarget.Texture2D, font.TextureId);
|
|
||||||
_shader.SetInt("uTex", 0);
|
|
||||||
int firstVertex = UploadBuffer(textBuf);
|
|
||||||
_gl.DrawArrays(PrimitiveType.Triangles, firstVertex, (uint)textVerts);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private int UploadBuffer(List<float> buf)
|
private static void DrawBucket(
|
||||||
|
IGpuPassEncoder pass,
|
||||||
|
IGpuFrame frame,
|
||||||
|
in GpuPushConstants baseConstants,
|
||||||
|
List<float> verts,
|
||||||
|
int useTexture,
|
||||||
|
GpuTextureSlot textureSlot)
|
||||||
{
|
{
|
||||||
int bytes = buf.Count * sizeof(float);
|
int byteCount = verts.Count * sizeof(float);
|
||||||
if (bytes == 0) return 0;
|
if (byteCount == 0) return;
|
||||||
FrameBufferSet set = _activeFrameBuffer
|
|
||||||
?? throw new InvalidOperationException("BeginFrame must be called before rendering text.");
|
|
||||||
int byteOffset = set.UsedBytes;
|
|
||||||
int requiredBytes = checked(byteOffset + bytes);
|
|
||||||
|
|
||||||
if (requiredBytes > _vboCapacityBytes)
|
GpuRingAllocation allocation = frame.AllocateRing(byteCount, GpuRingUsage.Vertex);
|
||||||
{
|
CollectionsMarshal.AsSpan(verts).CopyTo(allocation.AsSpan<float>());
|
||||||
int newCapacity = DynamicBufferCapacity.Grow(
|
|
||||||
_vboCapacityBytes,
|
|
||||||
requiredBytes);
|
|
||||||
TrackedGlResource.AllocateBufferStorage(
|
|
||||||
_gl,
|
|
||||||
GLEnum.ArrayBuffer,
|
|
||||||
_vbo,
|
|
||||||
_vboCapacityBytes,
|
|
||||||
newCapacity,
|
|
||||||
GLEnum.DynamicDraw,
|
|
||||||
"TextRenderer frame VBO growth");
|
|
||||||
_vboCapacityBytes = newCapacity;
|
|
||||||
}
|
|
||||||
|
|
||||||
fixed (float* p = CollectionsMarshal.AsSpan(buf))
|
GpuPushConstants constants = baseConstants;
|
||||||
_gl.BufferSubData(BufferTargetARB.ArrayBuffer, (nint)byteOffset, (nuint)bytes, p);
|
constants.RenderPass = useTexture;
|
||||||
|
// Unassigned (the untextured-rect bucket) never reaches the shader as
|
||||||
|
// a slot index — uUseTexture==0 skips the sample entirely — but a
|
||||||
|
// defined value is still written so no stale slot lingers in the
|
||||||
|
// uniform between draws.
|
||||||
|
constants.TextureIndexA = textureSlot.IsAssigned ? textureSlot.Index : 0u;
|
||||||
|
|
||||||
set.UsedBytes = requiredBytes;
|
pass.BindVertexBuffer(allocation.Buffer, allocation.OffsetBytes);
|
||||||
set.CapacityBytes = _vboCapacityBytes;
|
pass.SetPushConstants(in constants);
|
||||||
return byteOffset / (FloatsPerVertex * sizeof(float));
|
pass.Draw((uint)(verts.Count / FloatsPerVertex), instanceCount: 1, firstVertex: 0, firstInstance: 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_resources.RetryCleanup();
|
_pipeline.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
// src/AcDream.App/Rendering/TextureCache.cs
|
// src/AcDream.App/Rendering/TextureCache.cs
|
||||||
using AcDream.Core.Textures;
|
using AcDream.Core.Textures;
|
||||||
using AcDream.Core.World;
|
using AcDream.Core.World;
|
||||||
using AcDream.Content;
|
using AcDream.Content;
|
||||||
|
|
@ -12,11 +12,12 @@ using AcDream.App.Rendering.Residency;
|
||||||
|
|
||||||
namespace AcDream.App.Rendering;
|
namespace AcDream.App.Rendering;
|
||||||
|
|
||||||
public sealed unsafe class TextureCache
|
internal sealed unsafe class TextureCache
|
||||||
: Wb.IEntityTextureLifetime,
|
: Wb.IEntityTextureLifetime,
|
||||||
IDisposable
|
IDisposable
|
||||||
{
|
{
|
||||||
private readonly GL _gl;
|
private readonly GL _gl;
|
||||||
|
private readonly IGpuDevice _device;
|
||||||
private readonly IDatReaderWriter _dats;
|
private readonly IDatReaderWriter _dats;
|
||||||
private readonly string _diagnosticsDirectory;
|
private readonly string _diagnosticsDirectory;
|
||||||
// Handle and decoded dimensions are one atomic cache entry. Keeping them
|
// Handle and decoded dimensions are one atomic cache entry. Keeping them
|
||||||
|
|
@ -28,17 +29,40 @@ public sealed unsafe class TextureCache
|
||||||
_decodedDimensionsByTexture = new();
|
_decodedDimensionsByTexture = new();
|
||||||
private uint _magentaHandle;
|
private uint _magentaHandle;
|
||||||
|
|
||||||
// Direct-RenderSurface caches for UI sprites: 0x06xxxxxx RenderSurface ids
|
/// <summary>
|
||||||
// decoded directly (Portal/HighRes → DecodeRenderSurface), bypassing the
|
/// Campaign V slice V4a: one registered <see cref="IGpuTexture"/> plus its
|
||||||
// Surface→SurfaceTexture chain that GetOrUpload uses for world materials.
|
/// device texture-table <see cref="GpuTextureSlot"/> and decoded pixel
|
||||||
private readonly Dictionary<uint, uint> _handlesByRenderSurfaceId = new();
|
/// size. Direct-RenderSurface caches for UI sprites: 0x06xxxxxx
|
||||||
private readonly Dictionary<uint, (int w, int h)> _rsSizeById = new();
|
/// RenderSurface ids decoded directly (Portal/HighRes →
|
||||||
|
/// DecodeRenderSurface), bypassing the Surface→SurfaceTexture chain that
|
||||||
|
/// GetOrUpload uses for world materials — that world path stays on raw GL
|
||||||
|
/// (<see cref="_surfacesById"/>) until its own campaign slice.
|
||||||
|
/// </summary>
|
||||||
|
private readonly record struct GpuUiTextureEntry(
|
||||||
|
GpuTextureSlot Slot,
|
||||||
|
IGpuTexture Texture,
|
||||||
|
int Width,
|
||||||
|
int Height);
|
||||||
|
|
||||||
// Ad-hoc handles produced by the public UploadRgba8(byte[],int,int,bool) wrapper
|
private readonly Dictionary<uint, GpuUiTextureEntry> _renderSurfaceGpuTextures = new();
|
||||||
// (used by IconComposer for composited item icons). These are NOT stored in any
|
|
||||||
// of the keyed caches above, so Dispose must sweep this list to avoid leaking
|
// Ad-hoc GPU textures produced by the public UploadRgba8(byte[],int,int,bool)
|
||||||
// GL texture objects until process exit.
|
// wrapper (used by IconComposer for composited item icons). These are NOT
|
||||||
private readonly List<uint> _adhocHandles = new();
|
// stored in the keyed cache above, so Dispose must sweep this list to avoid
|
||||||
|
// leaking GPU texture-table slots until process exit.
|
||||||
|
private readonly List<GpuUiTextureEntry> _adhocGpuTextures = new();
|
||||||
|
|
||||||
|
// Every UI-path upload uses REPEAT addressing (existing behaviour: panel
|
||||||
|
// fills and tiled chrome sample UVs > 1) with the caller-selected filter.
|
||||||
|
// Nearest+Repeat has no predefined GpuSamplerDescription (UiNearest clamps),
|
||||||
|
// so it is constructed once here; CreateSampler de-duplicates by value.
|
||||||
|
private static readonly GpuSamplerDescription UiSpriteNearestRepeat = new(
|
||||||
|
GpuFilter.Nearest,
|
||||||
|
GpuFilter.Nearest,
|
||||||
|
GpuMipFilter.None,
|
||||||
|
GpuAddressMode.Repeat,
|
||||||
|
GpuAddressMode.Repeat,
|
||||||
|
MaxAnisotropy: 1f);
|
||||||
|
|
||||||
private readonly Wb.BindlessSupport? _bindless;
|
private readonly Wb.BindlessSupport? _bindless;
|
||||||
private readonly CompositeTextureArrayCache? _compositeTextures;
|
private readonly CompositeTextureArrayCache? _compositeTextures;
|
||||||
|
|
@ -82,13 +106,14 @@ public sealed unsafe class TextureCache
|
||||||
|
|
||||||
// Frame counter for the one-shot ACDREAM_DUMP_SURFACES=1 trigger.
|
// Frame counter for the one-shot ACDREAM_DUMP_SURFACES=1 trigger.
|
||||||
// Increments per Tick call; fires the dump once at frame index 600
|
// Increments per Tick call; fires the dump once at frame index 600
|
||||||
// and never again for the session. See spec §5.
|
// and never again for the session. See spec §5.
|
||||||
private int _dumpFrameCounter;
|
private int _dumpFrameCounter;
|
||||||
private bool _surfaceHistogramAlreadyDumped;
|
private bool _surfaceHistogramAlreadyDumped;
|
||||||
|
|
||||||
public TextureCache(GL gl, IDatReaderWriter dats, Wb.BindlessSupport? bindless = null)
|
public TextureCache(GL gl, IGpuDevice device, IDatReaderWriter dats, Wb.BindlessSupport? bindless = null)
|
||||||
: this(
|
: this(
|
||||||
gl,
|
gl,
|
||||||
|
device,
|
||||||
dats,
|
dats,
|
||||||
bindless,
|
bindless,
|
||||||
ImmediateGpuResourceRetirementQueue.Instance,
|
ImmediateGpuResourceRetirementQueue.Instance,
|
||||||
|
|
@ -101,6 +126,7 @@ public sealed unsafe class TextureCache
|
||||||
|
|
||||||
internal TextureCache(
|
internal TextureCache(
|
||||||
GL gl,
|
GL gl,
|
||||||
|
IGpuDevice device,
|
||||||
IDatReaderWriter dats,
|
IDatReaderWriter dats,
|
||||||
Wb.BindlessSupport? bindless,
|
Wb.BindlessSupport? bindless,
|
||||||
IGpuResourceRetirementQueue retirementQueue,
|
IGpuResourceRetirementQueue retirementQueue,
|
||||||
|
|
@ -109,6 +135,7 @@ public sealed unsafe class TextureCache
|
||||||
{
|
{
|
||||||
budgets ??= ResidencyBudgetOptions.Default;
|
budgets ??= ResidencyBudgetOptions.Default;
|
||||||
_gl = gl;
|
_gl = gl;
|
||||||
|
_device = device ?? throw new ArgumentNullException(nameof(device));
|
||||||
_dats = dats;
|
_dats = dats;
|
||||||
_bindless = bindless;
|
_bindless = bindless;
|
||||||
ArgumentException.ThrowIfNullOrWhiteSpace(diagnosticsDirectory);
|
ArgumentException.ThrowIfNullOrWhiteSpace(diagnosticsDirectory);
|
||||||
|
|
@ -219,23 +246,22 @@ public sealed unsafe class TextureCache
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Upload a UI sprite by its RenderSurface DataId (0x06xxxxxx), decoded
|
/// Upload a UI sprite by its RenderSurface DataId (0x06xxxxxx), decoded
|
||||||
/// DIRECTLY (Portal/HighRes → DecodeRenderSurface) rather than through the
|
/// DIRECTLY (Portal/HighRes → DecodeRenderSurface) rather than through the
|
||||||
/// Surface→SurfaceTexture chain that <see cref="GetOrUpload(uint)"/> uses
|
/// Surface→SurfaceTexture chain that <see cref="GetOrUpload(uint)"/> uses
|
||||||
/// for world-geometry materials. This is the correct path for retail UI
|
/// for world-geometry materials. This is the correct path for retail UI
|
||||||
/// chrome + font glyph sheets, which reference RenderSurface directly.
|
/// chrome + font glyph sheets, which reference RenderSurface directly.
|
||||||
/// Paletted (PFID_P8 / PFID_INDEX16) UI sprites — e.g. the selected-object
|
/// Paletted (PFID_P8 / PFID_INDEX16) UI sprites — e.g. the selected-object
|
||||||
/// health-bar track 0x0600193E — are decoded against the RenderSurface's own
|
/// health-bar track 0x0600193E — are decoded against the RenderSurface's own
|
||||||
/// <c>DefaultPaletteId</c> (same starting palette <see cref="DecodeFromDats"/>
|
/// <c>DefaultPaletteId</c> (same starting palette <see cref="DecodeFromDats"/>
|
||||||
/// uses); non-paletted formats have DefaultPaletteId==0 → palette null. Returns
|
/// uses); non-paletted formats have DefaultPaletteId==0 → palette null. Returns
|
||||||
/// a 1x1 magenta handle on miss.
|
/// a 1x1 magenta handle on miss.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public uint GetOrUploadRenderSurface(uint renderSurfaceId, out int width, out int height, bool nearest = false)
|
public GpuTextureSlot GetOrUploadRenderSurface(uint renderSurfaceId, out int width, out int height, bool nearest = false)
|
||||||
{
|
{
|
||||||
if (_handlesByRenderSurfaceId.TryGetValue(renderSurfaceId, out var existing)
|
if (_renderSurfaceGpuTextures.TryGetValue(renderSurfaceId, out GpuUiTextureEntry existing))
|
||||||
&& _rsSizeById.TryGetValue(renderSurfaceId, out var sz))
|
|
||||||
{
|
{
|
||||||
width = sz.w; height = sz.h;
|
width = existing.Width; height = existing.Height;
|
||||||
return existing;
|
return existing.Slot;
|
||||||
}
|
}
|
||||||
|
|
||||||
DecodedTexture decoded;
|
DecodedTexture decoded;
|
||||||
|
|
@ -256,16 +282,43 @@ public sealed unsafe class TextureCache
|
||||||
decoded = DecodedTexture.Magenta;
|
decoded = DecodedTexture.Magenta;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint h = UploadRgba8(decoded, nearest);
|
GpuUiTextureEntry entry = UploadUiTexture(decoded, nearest, $"ui-rendersurface-0x{renderSurfaceId:X8}");
|
||||||
_handlesByRenderSurfaceId[renderSurfaceId] = h;
|
_renderSurfaceGpuTextures[renderSurfaceId] = entry;
|
||||||
_rsSizeById[renderSurfaceId] = (decoded.Width, decoded.Height);
|
|
||||||
width = decoded.Width; height = decoded.Height;
|
width = decoded.Width; height = decoded.Height;
|
||||||
return h;
|
return entry.Slot;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Campaign V slice V4a: creates an <see cref="IGpuTexture"/> for one
|
||||||
|
/// decoded UI sprite/atlas and registers it into the device's global
|
||||||
|
/// texture table. Every UI-path texture uses REPEAT addressing (existing
|
||||||
|
/// behaviour — panel fills and tiled chrome sample UVs greater than 1) and
|
||||||
|
/// a single mip level (UI sprites never mip). <paramref name="nearest"/>
|
||||||
|
/// selects point sampling for pixel-crisp glyphs/icons versus bilinear for
|
||||||
|
/// everything else, matching the GL path's prior per-call choice.
|
||||||
|
/// </summary>
|
||||||
|
private GpuUiTextureEntry UploadUiTexture(DecodedTexture decoded, bool nearest, string debugName)
|
||||||
|
{
|
||||||
|
int width = Math.Max(1, decoded.Width);
|
||||||
|
int height = Math.Max(1, decoded.Height);
|
||||||
|
IGpuTexture texture = _device.CreateTexture(new GpuTextureDescription(
|
||||||
|
debugName,
|
||||||
|
GpuTextureKind.Texture2D,
|
||||||
|
GpuTextureFormat.Rgba8Unorm,
|
||||||
|
Width: width,
|
||||||
|
Height: height,
|
||||||
|
LayerCount: 1,
|
||||||
|
MipLevelCount: 1));
|
||||||
|
texture.Upload(0, 0, decoded.Rgba8);
|
||||||
|
IGpuSampler sampler = _device.CreateSampler(
|
||||||
|
nearest ? UiSpriteNearestRepeat : GpuSamplerDescription.WorldRepeat);
|
||||||
|
GpuTextureSlot slot = _device.RegisterTexture(texture, sampler);
|
||||||
|
return new GpuUiTextureEntry(slot, texture, decoded.Width, decoded.Height);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Alpha-channel histogram for one decoded texture. Used to diagnose
|
/// Alpha-channel histogram for one decoded texture. Used to diagnose
|
||||||
/// "why are clouds not transparent" — if cloud textures come out with
|
/// "why are clouds not transparent" — if cloud textures come out with
|
||||||
/// alpha = 1.0 everywhere we know the decode path strips the alpha
|
/// alpha = 1.0 everywhere we know the decode path strips the alpha
|
||||||
/// channel somewhere. Printed once per unique surfaceId under
|
/// channel somewhere. Printed once per unique surfaceId under
|
||||||
/// <c>ACDREAM_DUMP_SKY=1</c>. Adds ~2ms per texture upload, negligible.
|
/// <c>ACDREAM_DUMP_SKY=1</c>. Adds ~2ms per texture upload, negligible.
|
||||||
|
|
@ -530,7 +583,7 @@ public sealed unsafe class TextureCache
|
||||||
{
|
{
|
||||||
if (_bindless is null)
|
if (_bindless is null)
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
"TextureCache constructed without BindlessSupport — cannot generate bindless handles. " +
|
"TextureCache constructed without BindlessSupport — cannot generate bindless handles. " +
|
||||||
"WbDrawDispatcher requires the bindless-aware ctor overload (pass non-null BindlessSupport).");
|
"WbDrawDispatcher requires the bindless-aware ctor overload (pass non-null BindlessSupport).");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -585,7 +638,7 @@ public sealed unsafe class TextureCache
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static ulong HashPaletteOverride(PaletteOverride p)
|
internal static ulong HashPaletteOverride(PaletteOverride p)
|
||||||
{
|
{
|
||||||
// Not cryptographic — just needs to distinguish override setups
|
// Not cryptographic — just needs to distinguish override setups
|
||||||
// for caching. Start with base palette id, fold in each entry.
|
// for caching. Start with base palette id, fold in each entry.
|
||||||
ulong h = 0xCBF29CE484222325UL; // FNV-1a offset basis
|
ulong h = 0xCBF29CE484222325UL; // FNV-1a offset basis
|
||||||
const ulong prime = 0x100000001B3UL;
|
const ulong prime = 0x100000001B3UL;
|
||||||
|
|
@ -606,17 +659,17 @@ public sealed unsafe class TextureCache
|
||||||
/// Phase N.6 slice 1: one-shot surface-format histogram dump for the
|
/// Phase N.6 slice 1: one-shot surface-format histogram dump for the
|
||||||
/// atlas-opportunity audit. Activated by ACDREAM_DUMP_SURFACES=1; fires
|
/// atlas-opportunity audit. Activated by ACDREAM_DUMP_SURFACES=1; fires
|
||||||
/// once after BOTH gates pass:
|
/// once after BOTH gates pass:
|
||||||
/// 1. <c>_dumpFrameCounter >= 600</c> — at least 600 OnRender ticks
|
/// 1. <c>_dumpFrameCounter >= 600</c> — at least 600 OnRender ticks
|
||||||
/// have elapsed (catches the "we're already past startup boilerplate"
|
/// have elapsed (catches the "we're already past startup boilerplate"
|
||||||
/// bound; ~10s at 60fps, ~3s at 200fps).
|
/// bound; ~10s at 60fps, ~3s at 200fps).
|
||||||
/// 2. <c>_uploadMetadata.Count >= 100</c> — the cache contains at
|
/// 2. <c>_uploadMetadata.Count >= 100</c> — the cache contains at
|
||||||
/// least 100 uploaded textures, indicating streaming has actually
|
/// least 100 uploaded textures, indicating streaming has actually
|
||||||
/// pulled in world content (not just sky/UI/font). The original
|
/// pulled in world content (not just sky/UI/font). The original
|
||||||
/// frame-only gate fired during the login/handshake phase where
|
/// frame-only gate fired during the login/handshake phase where
|
||||||
/// OnRender ticks at GUI rates but no world has streamed in.
|
/// OnRender ticks at GUI rates but no world has streamed in.
|
||||||
/// Output goes to the host-provided portable diagnostics directory.
|
/// Output goes to the host-provided portable diagnostics directory.
|
||||||
/// Zero cost
|
/// Zero cost
|
||||||
/// when off. See spec §5 in
|
/// when off. See spec §5 in
|
||||||
/// docs/superpowers/specs/2026-05-11-phase-n6-slice1-design.md.
|
/// docs/superpowers/specs/2026-05-11-phase-n6-slice1-design.md.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void TickSurfaceHistogramDumpIfEnabled()
|
public void TickSurfaceHistogramDumpIfEnabled()
|
||||||
|
|
@ -641,7 +694,7 @@ public sealed unsafe class TextureCache
|
||||||
{
|
{
|
||||||
// Diagnostic-only path. If the dump file can't be written
|
// Diagnostic-only path. If the dump file can't be written
|
||||||
// (disk full, permission denied, antivirus lock, path too
|
// (disk full, permission denied, antivirus lock, path too
|
||||||
// long) we must NOT crash OnRender — that would invalidate
|
// long) we must NOT crash OnRender — that would invalidate
|
||||||
// the very measurement pass this diagnostic is meant to
|
// the very measurement pass this diagnostic is meant to
|
||||||
// support. Log to stderr and let the caller mark the dump
|
// support. Log to stderr and let the caller mark the dump
|
||||||
// as "already done" so it doesn't retry every frame.
|
// as "already done" so it doesn't retry every frame.
|
||||||
|
|
@ -657,7 +710,7 @@ public sealed unsafe class TextureCache
|
||||||
"n6-surfaces.txt");
|
"n6-surfaces.txt");
|
||||||
|
|
||||||
var sb = new System.Text.StringBuilder();
|
var sb = new System.Text.StringBuilder();
|
||||||
sb.AppendLine($"# acdream surface-format histogram — generated {DateTime.UtcNow:yyyy-MM-ddTHH:mm:ssZ}");
|
sb.AppendLine($"# acdream surface-format histogram — generated {DateTime.UtcNow:yyyy-MM-ddTHH:mm:ssZ}");
|
||||||
sb.AppendLine("# Per-entry: surfaceId(hex), width, height, format, byteCount");
|
sb.AppendLine("# Per-entry: surfaceId(hex), width, height, format, byteCount");
|
||||||
sb.AppendLine();
|
sb.AppendLine();
|
||||||
|
|
||||||
|
|
@ -710,7 +763,7 @@ public sealed unsafe class TextureCache
|
||||||
foreach (var kv in bucketsByFormat.OrderByDescending(kv => kv.Value))
|
foreach (var kv in bucketsByFormat.OrderByDescending(kv => kv.Value))
|
||||||
sb.AppendLine($"# {kv.Key}: {kv.Value}");
|
sb.AppendLine($"# {kv.Key}: {kv.Value}");
|
||||||
|
|
||||||
sb.AppendLine("# Top 10 (W,H,format) triples — atlas-opportunity input:");
|
sb.AppendLine("# Top 10 (W,H,format) triples — atlas-opportunity input:");
|
||||||
foreach (var kv in bucketsByTriple.OrderByDescending(kv => kv.Value).Take(10))
|
foreach (var kv in bucketsByTriple.OrderByDescending(kv => kv.Value).Take(10))
|
||||||
sb.AppendLine($"# {kv.Key.W}x{kv.Key.H} {kv.Key.F}: {kv.Value}");
|
sb.AppendLine($"# {kv.Key.W}x{kv.Key.H} {kv.Key.F}: {kv.Value}");
|
||||||
|
|
||||||
|
|
@ -729,8 +782,8 @@ public sealed unsafe class TextureCache
|
||||||
}
|
}
|
||||||
|
|
||||||
// Base1Solid surfaces (and any with OrigTextureId==0) carry a ColorValue
|
// Base1Solid surfaces (and any with OrigTextureId==0) carry a ColorValue
|
||||||
// instead of a texture chain. Overrides are irrelevant here — there's
|
// instead of a texture chain. Overrides are irrelevant here — there's
|
||||||
// no texture chain to swap — so the override is ignored for solid-color
|
// no texture chain to swap — so the override is ignored for solid-color
|
||||||
// surfaces. Translucency is honored so Base1Solid|Translucent surfaces
|
// surfaces. Translucency is honored so Base1Solid|Translucent surfaces
|
||||||
// with Translucency=1.0 become alpha=0, which the mesh shader's discard
|
// with Translucency=1.0 become alpha=0, which the mesh shader's discard
|
||||||
// cutout makes invisible.
|
// cutout makes invisible.
|
||||||
|
|
@ -759,7 +812,7 @@ public sealed unsafe class TextureCache
|
||||||
|
|
||||||
// Start with the texture's default palette, then apply overlays.
|
// Start with the texture's default palette, then apply overlays.
|
||||||
// ACViewer's Render/TextureCache.IndexToColor does the same and never
|
// ACViewer's Render/TextureCache.IndexToColor does the same and never
|
||||||
// consults ObjDesc.BasePaletteId for palette-indexed textures — the
|
// consults ObjDesc.BasePaletteId for palette-indexed textures — the
|
||||||
// RenderSurface's own default palette is the starting point.
|
// RenderSurface's own default palette is the starting point.
|
||||||
Palette? basePalette = rs.DefaultPaletteId != 0
|
Palette? basePalette = rs.DefaultPaletteId != 0
|
||||||
? _dats.Get<Palette>(rs.DefaultPaletteId)
|
? _dats.Get<Palette>(rs.DefaultPaletteId)
|
||||||
|
|
@ -817,12 +870,13 @@ public sealed unsafe class TextureCache
|
||||||
/// <see cref="AcDream.App.UI.IconComposer"/> to upload CPU-composited icon layers.
|
/// <see cref="AcDream.App.UI.IconComposer"/> to upload CPU-composited icon layers.
|
||||||
/// The returned handle is tracked in <see cref="_adhocHandles"/> and deleted by
|
/// The returned handle is tracked in <see cref="_adhocHandles"/> and deleted by
|
||||||
/// <see cref="Dispose"/>. Callers must NOT also store the handle in any of the
|
/// <see cref="Dispose"/>. Callers must NOT also store the handle in any of the
|
||||||
/// keyed caches — that would cause a double-delete on Dispose.</summary>
|
/// keyed caches — that would cause a double-delete on Dispose.</summary>
|
||||||
public uint UploadRgba8(byte[] rgba, int width, int height, bool nearest = false)
|
public GpuTextureSlot UploadRgba8(byte[] rgba, int width, int height, bool nearest = false)
|
||||||
{
|
{
|
||||||
uint h = UploadRgba8(new DecodedTexture(rgba, width, height), nearest);
|
GpuUiTextureEntry entry = UploadUiTexture(
|
||||||
_adhocHandles.Add(h);
|
new DecodedTexture(rgba, width, height), nearest, "ui-adhoc-icon");
|
||||||
return h;
|
_adhocGpuTextures.Add(entry);
|
||||||
|
return entry.Slot;
|
||||||
}
|
}
|
||||||
|
|
||||||
private uint UploadRgba8(DecodedTexture decoded, bool nearest = false)
|
private uint UploadRgba8(DecodedTexture decoded, bool nearest = false)
|
||||||
|
|
@ -846,7 +900,7 @@ public sealed unsafe class TextureCache
|
||||||
PixelType.UnsignedByte,
|
PixelType.UnsignedByte,
|
||||||
p);
|
p);
|
||||||
|
|
||||||
// Point (nearest) sampling for pixel-exact UI text — bilinear softens the dat
|
// Point (nearest) sampling for pixel-exact UI text — bilinear softens the dat
|
||||||
// font's small glyphs. Other surfaces use bilinear.
|
// font's small glyphs. Other surfaces use bilinear.
|
||||||
int filter = nearest ? (int)TextureMinFilter.Nearest : (int)TextureMinFilter.Linear;
|
int filter = nearest ? (int)TextureMinFilter.Nearest : (int)TextureMinFilter.Linear;
|
||||||
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, filter);
|
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, filter);
|
||||||
|
|
@ -962,16 +1016,28 @@ public sealed unsafe class TextureCache
|
||||||
_magentaHandle = 0;
|
_magentaHandle = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenderSurface (UI sprite) handles — pre-existing gap: this dict was populated
|
// RenderSurface (UI sprite) GPU textures — pre-existing gap: this dict was
|
||||||
// by GetOrUploadRenderSurface but was not swept here before this fix.
|
// populated by GetOrUploadRenderSurface but was not swept here before that fix.
|
||||||
foreach (var h in _handlesByRenderSurfaceId.Values)
|
foreach (GpuUiTextureEntry entry in _renderSurfaceGpuTextures.Values)
|
||||||
DeleteUploadedTexture(h);
|
DisposeUiTexture(entry);
|
||||||
_handlesByRenderSurfaceId.Clear();
|
_renderSurfaceGpuTextures.Clear();
|
||||||
|
|
||||||
// Ad-hoc handles from the public UploadRgba8(byte[],int,int,bool) wrapper
|
// Ad-hoc GPU textures from the public UploadRgba8(byte[],int,int,bool) wrapper
|
||||||
// (IconComposer composited icons). Not stored in any keyed cache.
|
// (IconComposer composited icons). Not stored in any keyed cache.
|
||||||
foreach (var h in _adhocHandles)
|
foreach (GpuUiTextureEntry entry in _adhocGpuTextures)
|
||||||
DeleteUploadedTexture(h);
|
DisposeUiTexture(entry);
|
||||||
_adhocHandles.Clear();
|
_adhocGpuTextures.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Releases a UI-path texture's table slot before disposing the backing
|
||||||
|
/// <see cref="IGpuTexture"/>. Both route through the device's retirement
|
||||||
|
/// queue, so releasing the slot first is purely bookkeeping order, not a
|
||||||
|
/// use-after-free concern.
|
||||||
|
/// </summary>
|
||||||
|
private void DisposeUiTexture(GpuUiTextureEntry entry)
|
||||||
|
{
|
||||||
|
_device.ReleaseTextureSlot(entry.Slot);
|
||||||
|
entry.Texture.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
using AcDream.Core.Vfx;
|
using AcDream.Core.Vfx;
|
||||||
using DatReaderWriter.Types;
|
using DatReaderWriter.Types;
|
||||||
|
|
@ -16,7 +16,7 @@ namespace AcDream.App.Rendering.Vfx;
|
||||||
/// here preserves that observable order and avoids firing a hand or weapon
|
/// here preserves that observable order and avoids firing a hand or weapon
|
||||||
/// effect against the previous animation frame.
|
/// effect against the previous animation frame.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public sealed class AnimationHookFrameQueue
|
internal sealed class AnimationHookFrameQueue
|
||||||
{
|
{
|
||||||
private readonly AnimationHookRouter _router;
|
private readonly AnimationHookRouter _router;
|
||||||
private readonly IEntityEffectPoseSource _poses;
|
private readonly IEntityEffectPoseSource _poses;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AcDream.App.World;
|
using AcDream.App.World;
|
||||||
using AcDream.Core.Net.Messages;
|
using AcDream.Core.Net.Messages;
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
|
|
@ -23,7 +23,7 @@ namespace AcDream.App.Rendering.Vfx;
|
||||||
/// (<c>0x00513260</c>) and both <c>play_default_script</c> overloads
|
/// (<c>0x00513260</c>) and both <c>play_default_script</c> overloads
|
||||||
/// (<c>0x005132B0</c>, <c>0x00513300</c>).
|
/// (<c>0x005132B0</c>, <c>0x00513300</c>).
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public sealed class EntityEffectController : IAnimationHookSink,
|
internal sealed class EntityEffectController : IAnimationHookSink,
|
||||||
IEntityEffectAdvanceSource
|
IEntityEffectAdvanceSource
|
||||||
{
|
{
|
||||||
private readonly LiveEntityRuntime _liveEntities;
|
private readonly LiveEntityRuntime _liveEntities;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AcDream.Core.Vfx;
|
using AcDream.Core.Vfx;
|
||||||
using AcDream.Core.World;
|
using AcDream.Core.World;
|
||||||
|
|
||||||
|
|
@ -15,7 +15,7 @@ namespace AcDream.App.Rendering.Vfx;
|
||||||
/// (<c>0x0051D180</c>). This registry is the modern, read-only seam exposing
|
/// (<c>0x0051D180</c>). This registry is the modern, read-only seam exposing
|
||||||
/// those same final frames without coupling Core effects to the renderer.
|
/// those same final frames without coupling Core effects to the renderer.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public sealed class EntityEffectPoseRegistry :
|
internal sealed class EntityEffectPoseRegistry :
|
||||||
IEntityEffectPoseSource,
|
IEntityEffectPoseSource,
|
||||||
IEntityEffectCellSource,
|
IEntityEffectCellSource,
|
||||||
IEntityEffectPoseChangeSource,
|
IEntityEffectPoseChangeSource,
|
||||||
|
|
@ -314,7 +314,7 @@ public sealed class EntityEffectPoseRegistry :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IEntityEffectPoseLifetimeSource
|
internal interface IEntityEffectPoseLifetimeSource
|
||||||
{
|
{
|
||||||
ulong GetPoseOwnerLifetimeVersion(uint localEntityId);
|
ulong GetPoseOwnerLifetimeVersion(uint localEntityId);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using AcDream.App.World;
|
using AcDream.App.World;
|
||||||
using AcDream.Core.Net.Messages;
|
using AcDream.Core.Net.Messages;
|
||||||
using AcDream.Core.Vfx;
|
using AcDream.Core.Vfx;
|
||||||
using DatReaderWriter.DBObjs;
|
using DatReaderWriter.DBObjs;
|
||||||
|
|
@ -16,7 +16,7 @@ namespace AcDream.App.Rendering.Vfx;
|
||||||
/// a network PhysicsDesc then unconditionally replaces the typed table, even
|
/// a network PhysicsDesc then unconditionally replaces the typed table, even
|
||||||
/// when PeTable was absent or zero.
|
/// when PeTable was absent or zero.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public sealed class EntityEffectProfile : ILiveEntityEffectProfile
|
internal sealed class EntityEffectProfile : ILiveEntityEffectProfile
|
||||||
{
|
{
|
||||||
private EntityEffectProfile(Setup setup)
|
private EntityEffectProfile(Setup setup)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AcDream.Core.Vfx;
|
using AcDream.Core.Vfx;
|
||||||
using AcDream.Core.World;
|
using AcDream.Core.World;
|
||||||
|
|
||||||
|
|
@ -8,7 +8,7 @@ namespace AcDream.App.Rendering.Vfx;
|
||||||
/// Setup script/profile data resolved once when a logical effect owner is
|
/// Setup script/profile data resolved once when a logical effect owner is
|
||||||
/// registered. Part transforms are indexed and root-local.
|
/// registered. Part transforms are indexed and root-local.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed record ScriptActivationInfo(
|
internal sealed record ScriptActivationInfo(
|
||||||
uint ScriptId,
|
uint ScriptId,
|
||||||
IReadOnlyList<Matrix4x4> PartTransforms,
|
IReadOnlyList<Matrix4x4> PartTransforms,
|
||||||
EntityEffectProfile? EffectProfile = null,
|
EntityEffectProfile? EffectProfile = null,
|
||||||
|
|
@ -26,7 +26,7 @@ public sealed record ScriptActivationInfo(
|
||||||
/// initialization. Live registration invokes this class once per logical
|
/// initialization. Live registration invokes this class once per logical
|
||||||
/// generation; spatial rebucketing never replays it.
|
/// generation; spatial rebucketing never replays it.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public sealed class EntityScriptActivator
|
internal sealed class EntityScriptActivator
|
||||||
{
|
{
|
||||||
private sealed class StaticOwnerState
|
private sealed class StaticOwnerState
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using AcDream.App.World;
|
using AcDream.App.World;
|
||||||
using AcDream.Core.Lighting;
|
using AcDream.Core.Lighting;
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
using AcDream.Runtime.Entities;
|
using AcDream.Runtime.Entities;
|
||||||
|
|
@ -17,7 +17,7 @@ namespace AcDream.App.Rendering.Vfx;
|
||||||
/// <see cref="LiveEntityRuntime"/>; leaving the world removes only this
|
/// <see cref="LiveEntityRuntime"/>; leaving the world removes only this
|
||||||
/// cell-scoped presentation and re-entry registers it again.
|
/// cell-scoped presentation and re-entry registers it again.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public sealed class LiveEntityLightController : IDisposable
|
internal sealed class LiveEntityLightController : IDisposable
|
||||||
{
|
{
|
||||||
private readonly LiveEntityRuntime _liveEntities;
|
private readonly LiveEntityRuntime _liveEntities;
|
||||||
private readonly EntityEffectPoseRegistry _poses;
|
private readonly EntityEffectPoseRegistry _poses;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AcDream.Core.Vfx;
|
using AcDream.Core.Vfx;
|
||||||
|
|
||||||
namespace AcDream.App.Rendering.Vfx;
|
namespace AcDream.App.Rendering.Vfx;
|
||||||
|
|
@ -18,7 +18,7 @@ internal interface IWorldSceneParticleVisibility
|
||||||
/// frame meaning: one completed viewer position plus the AC cells admitted by
|
/// frame meaning: one completed viewer position plus the AC cells admitted by
|
||||||
/// that completed view. It neither creates emitters nor performs rendering.
|
/// that completed view. It neither creates emitters nor performs rendering.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class ParticleVisibilityController : IWorldSceneParticleVisibility
|
internal sealed class ParticleVisibilityController : IWorldSceneParticleVisibility
|
||||||
{
|
{
|
||||||
public const float ExtendedRangeMultiplier = 2f;
|
public const float ExtendedRangeMultiplier = 2f;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
|
||||||
|
|
@ -6,31 +6,31 @@ namespace AcDream.App.Rendering;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// T3 (BR-5): the port of retail's <c>Render::viewconeCheck</c> (Ghidra
|
/// T3 (BR-5): the port of retail's <c>Render::viewconeCheck</c> (Ghidra
|
||||||
/// 0x0054c250) — meshes (characters, statics, emitters) are CULLED per portal
|
/// 0x0054c250) — meshes (characters, statics, emitters) are CULLED per portal
|
||||||
/// view by a bounding-sphere test against the view's edge planes, never
|
/// view by a bounding-sphere test against the view's edge planes, never
|
||||||
/// clipped. Retail stores each view vertex with its 3D eye-edge plane
|
/// clipped. Retail stores each view vertex with its 3D eye-edge plane
|
||||||
/// (<c>view_vertex { Vec2D pt; Plane plane }</c>, acclient.h:32483) and tests
|
/// (<c>view_vertex { Vec2D pt; Plane plane }</c>, acclient.h:32483) and tests
|
||||||
/// the object's drawing sphere against the installed view's plane set;
|
/// the object's drawing sphere against the installed view's plane set;
|
||||||
/// OUTSIDE → skipped (RenderDeviceD3D::DrawMesh per-view loop pc:429290-429310,
|
/// OUTSIDE → skipped (RenderDeviceD3D::DrawMesh per-view loop pc:429290-429310,
|
||||||
/// and the DrawCells per-cell object epilogue, Ghidra 0x005a4840).
|
/// and the DrawCells per-cell object epilogue, Ghidra 0x005a4840).
|
||||||
///
|
///
|
||||||
/// <para>Our views are clip-space half-planes (≤8 per slice,
|
/// <para>Our views are clip-space half-planes (≤8 per slice,
|
||||||
/// <see cref="ClipPlaneSet"/> output: (nx,ny,0,d) satisfied when
|
/// <see cref="ClipPlaneSet"/> output: (nx,ny,0,d) satisfied when
|
||||||
/// nx·Cx + ny·Cy + d·Cw ≥ 0 for clip-space C). Lifting one to world space —
|
/// nx·Cx + ny·Cy + d·Cw ≥ 0 for clip-space C). Lifting one to world space —
|
||||||
/// the view_vertex.plane analog, a plane through the EYE and the view edge —
|
/// the view_vertex.plane analog, a plane through the EYE and the view edge —
|
||||||
/// is one matrix fold: with row-vector convention (System.Numerics),
|
/// is one matrix fold: with row-vector convention (System.Numerics),
|
||||||
/// C = world·VP, so C·P = world·(VP·P); L = VP·P (rows of VP dotted with P)
|
/// C = world·VP, so C·P = world·(VP·P); L = VP·P (rows of VP dotted with P)
|
||||||
/// is the world-space homogeneous half-plane. Sphere-vs-half-plane keeps the
|
/// is the world-space homogeneous half-plane. Sphere-vs-half-plane keeps the
|
||||||
/// sphere when L.xyz·c + L.w ≥ −r·|L.xyz| (not entirely outside).</para>
|
/// sphere when L.xyz·c + L.w ≥ −r·|L.xyz| (not entirely outside).</para>
|
||||||
///
|
///
|
||||||
/// <para>A sphere is visible through a SLICE when it is not entirely outside
|
/// <para>A sphere is visible through a SLICE when it is not entirely outside
|
||||||
/// any of the slice's planes (convex region); visible for a CELL when any of
|
/// any of the slice's planes (convex region); visible for a CELL when any of
|
||||||
/// the cell's slices passes. A slice with zero planes is pass-all (the
|
/// the cell's slices passes. A slice with zero planes is pass-all (the
|
||||||
/// NoClipSlice / full-screen outdoor case). A cell with no views culls — in
|
/// NoClipSlice / full-screen outdoor case). A cell with no views culls — in
|
||||||
/// retail an object whose cell is not in the draw list is simply never
|
/// retail an object whose cell is not in the draw list is simply never
|
||||||
/// reached.</para>
|
/// reached.</para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class ViewconeCuller
|
internal sealed class ViewconeCuller
|
||||||
{
|
{
|
||||||
private const int MaxRetainedCellPlaneSets = 512;
|
private const int MaxRetainedCellPlaneSets = 512;
|
||||||
private const int MaxRetainedPlanesPerCell = 256;
|
private const int MaxRetainedPlanesPerCell = 256;
|
||||||
|
|
@ -65,7 +65,7 @@ public sealed class ViewconeCuller
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>True when the outside view is a full-screen pass-all (the
|
/// <summary>True when the outside view is a full-screen pass-all (the
|
||||||
/// synthetic outdoor root) — every outside-test passes.</summary>
|
/// synthetic outdoor root) — every outside-test passes.</summary>
|
||||||
public bool OutsideIsFullScreen { get; private set; }
|
public bool OutsideIsFullScreen { get; private set; }
|
||||||
|
|
||||||
public static ViewconeCuller Build(
|
public static ViewconeCuller Build(
|
||||||
|
|
@ -158,7 +158,7 @@ public sealed class ViewconeCuller
|
||||||
Vector4 l = plane.Equation;
|
Vector4 l = plane.Equation;
|
||||||
float nLen = plane.NormalLength;
|
float nLen = plane.NormalLength;
|
||||||
if (nLen < 1e-12f)
|
if (nLen < 1e-12f)
|
||||||
continue; // degenerate plane — no constraint
|
continue; // degenerate plane — no constraint
|
||||||
float dist = l.X * center.X + l.Y * center.Y + l.Z * center.Z + l.W;
|
float dist = l.X * center.X + l.Y * center.Y + l.Z * center.Z + l.W;
|
||||||
if (dist < -radius * nLen)
|
if (dist < -radius * nLen)
|
||||||
return false; // entirely outside this edge plane
|
return false; // entirely outside this edge plane
|
||||||
|
|
@ -167,7 +167,7 @@ public sealed class ViewconeCuller
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Sphere-vs-the-cell's-views: visible when any slice passes.
|
/// <summary>Sphere-vs-the-cell's-views: visible when any slice passes.
|
||||||
/// A cell with no views culls (not in the draw list ⇒ never reached in
|
/// A cell with no views culls (not in the draw list ⇒ never reached in
|
||||||
/// retail). A zero-plane slice is pass-all.</summary>
|
/// retail). A zero-plane slice is pass-all.</summary>
|
||||||
public bool SphereVisibleInCell(uint cellId, in Vector3 center, float radius)
|
public bool SphereVisibleInCell(uint cellId, in Vector3 center, float radius)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
|
|
||||||
namespace AcDream.App.Rendering.Wb;
|
namespace AcDream.App.Rendering.Wb;
|
||||||
|
|
@ -8,7 +8,7 @@ namespace AcDream.App.Rendering.Wb;
|
||||||
/// equipped items). Holds AC-specific per-instance customizations the WB
|
/// equipped items). Holds AC-specific per-instance customizations the WB
|
||||||
/// atlas cache doesn't carry: <c>AnimPartChange</c> override map +
|
/// atlas cache doesn't carry: <c>AnimPartChange</c> override map +
|
||||||
/// <c>HiddenParts</c> bitmask. Also holds a reference to acdream's existing
|
/// <c>HiddenParts</c> bitmask. Also holds a reference to acdream's existing
|
||||||
/// <see cref="AnimationSequencer"/> — Phase N.4 explicitly does not touch
|
/// <see cref="AnimationSequencer"/> — Phase N.4 explicitly does not touch
|
||||||
/// the sequencer; we just route through it at draw time.
|
/// the sequencer; we just route through it at draw time.
|
||||||
///
|
///
|
||||||
/// <para>
|
/// <para>
|
||||||
|
|
@ -16,11 +16,11 @@ namespace AcDream.App.Rendering.Wb;
|
||||||
/// a server <c>CreateObject</c> is processed; destroyed by
|
/// a server <c>CreateObject</c> is processed; destroyed by
|
||||||
/// <c>EntitySpawnAdapter.OnRemove</c> on <c>RemoveObject</c>. The mesh
|
/// <c>EntitySpawnAdapter.OnRemove</c> on <c>RemoveObject</c>. The mesh
|
||||||
/// data backing each part is cached in WB's <c>ObjectMeshManager</c>;
|
/// data backing each part is cached in WB's <c>ObjectMeshManager</c>;
|
||||||
/// per-instance customizations don't go through the atlas — they overlay
|
/// per-instance customizations don't go through the atlas — they overlay
|
||||||
/// at draw time.
|
/// at draw time.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class AnimatedEntityState
|
internal sealed class AnimatedEntityState
|
||||||
{
|
{
|
||||||
private readonly Dictionary<int, ulong> _partGfxObjOverrides = new();
|
private readonly Dictionary<int, ulong> _partGfxObjOverrides = new();
|
||||||
private ulong _hiddenMask = 0;
|
private ulong _hiddenMask = 0;
|
||||||
|
|
@ -49,7 +49,7 @@ public sealed class AnimatedEntityState
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Override the GfxObj id for a Setup part. Used for
|
/// <summary>Override the GfxObj id for a Setup part. Used for
|
||||||
/// AnimPartChange — e.g. wielding a weapon swaps the hand-part's
|
/// AnimPartChange — e.g. wielding a weapon swaps the hand-part's
|
||||||
/// GfxObj.</summary>
|
/// GfxObj.</summary>
|
||||||
public void SetPartOverride(int partIdx, ulong gfxObjId)
|
public void SetPartOverride(int partIdx, ulong gfxObjId)
|
||||||
=> _partGfxObjOverrides[partIdx] = gfxObjId;
|
=> _partGfxObjOverrides[partIdx] = gfxObjId;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using Silk.NET.OpenGL;
|
using Silk.NET.OpenGL;
|
||||||
using Silk.NET.OpenGL.Extensions.ARB;
|
using Silk.NET.OpenGL.Extensions.ARB;
|
||||||
using AcDream.App.Rendering;
|
using AcDream.App.Rendering;
|
||||||
|
|
||||||
|
|
@ -9,7 +9,7 @@ namespace AcDream.App.Rendering.Wb;
|
||||||
/// for the modern rendering path. Constructed once at startup via
|
/// for the modern rendering path. Constructed once at startup via
|
||||||
/// <see cref="TryCreate"/>, which returns false if the extension isn't present.
|
/// <see cref="TryCreate"/>, which returns false if the extension isn't present.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class BindlessSupport
|
internal sealed class BindlessSupport
|
||||||
{
|
{
|
||||||
private readonly GL _gl;
|
private readonly GL _gl;
|
||||||
private readonly ArbBindlessTexture _ext;
|
private readonly ArbBindlessTexture _ext;
|
||||||
|
|
@ -63,7 +63,7 @@ public sealed class BindlessSupport
|
||||||
/// make it resident. Idempotent per (texture, sampler) pair.
|
/// make it resident. Idempotent per (texture, sampler) pair.
|
||||||
///
|
///
|
||||||
/// Added for Campaign V slice V1's <c>GlGpuDevice.RegisterTexture</c>,
|
/// Added for Campaign V slice V1's <c>GlGpuDevice.RegisterTexture</c>,
|
||||||
/// which registers a (texture, sampler) pair per the RHI contract — "the
|
/// which registers a (texture, sampler) pair per the RHI contract — "the
|
||||||
/// same texture registered with two samplers occupies two slots." The
|
/// same texture registered with two samplers occupies two slots." The
|
||||||
/// texture-only <see cref="GetResidentHandle(uint)"/> above cannot express
|
/// texture-only <see cref="GetResidentHandle(uint)"/> above cannot express
|
||||||
/// that; <c>ManagedGLTextureArray</c> already calls the equivalent
|
/// that; <c>ManagedGLTextureArray</c> already calls the equivalent
|
||||||
|
|
@ -117,7 +117,7 @@ public sealed class BindlessSupport
|
||||||
// and removed when terrain rendering surfaced GL_INVALID_OPERATION on
|
// and removed when terrain rendering surfaced GL_INVALID_OPERATION on
|
||||||
// NVIDIA Windows for the `uniform sampler2DArray` + glProgramUniformHandleARB
|
// NVIDIA Windows for the `uniform sampler2DArray` + glProgramUniformHandleARB
|
||||||
// combination. The replacement pattern (uvec2 handle uniform + GLSL
|
// combination. The replacement pattern (uvec2 handle uniform + GLSL
|
||||||
// sampler-from-handle constructor — see terrain_modern.frag) lives at the
|
// sampler-from-handle constructor — see terrain_modern.frag) lives at the
|
||||||
// call site via plain `_gl.ProgramUniform2(program, loc, low, high)`. If
|
// call site via plain `_gl.ProgramUniform2(program, loc, low, high)`. If
|
||||||
// you re-introduce a sampler-handle helper, restrict it to drivers known
|
// you re-introduce a sampler-handle helper, restrict it to drivers known
|
||||||
// to accept the direct sampler-uniform path.
|
// to accept the direct sampler-uniform path.
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using Chorizite.Core.Render.Enums;
|
using Chorizite.Core.Render.Enums;
|
||||||
using Silk.NET.OpenGL;
|
using Silk.NET.OpenGL;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
@ -7,7 +7,7 @@ using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace AcDream.App.Rendering.Wb {
|
namespace AcDream.App.Rendering.Wb {
|
||||||
public static class BufferUsageExtensions {
|
internal static class BufferUsageExtensions {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Converts a BufferUsage to a GL BufferUsageARB
|
/// Converts a BufferUsage to a GL BufferUsageARB
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
|
||||||
namespace AcDream.App.Rendering.Wb;
|
namespace AcDream.App.Rendering.Wb;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Phase A8 (2026-05-26): a logical building — one or more EnvCells linked
|
/// Phase A8 (2026-05-26): a logical building — one or more EnvCells linked
|
||||||
/// via the dat-level <c>LandBlockInfo.Buildings</c> entry. Building shells (cottage
|
/// via the dat-level <c>LandBlockInfo.Buildings</c> entry. Building shells (cottage
|
||||||
/// walls, inn walls — <c>IsBuildingShell=true</c> entities) are scoped to this
|
/// walls, inn walls — <c>IsBuildingShell=true</c> entities) are scoped to this
|
||||||
/// building's cells via their dat-derived anchor. The exit portal polygons are
|
/// building's cells via their dat-derived anchor. The exit portal polygons are
|
||||||
/// stencil-marked so outdoor visibility leaks through portal silhouettes only.
|
/// stencil-marked so outdoor visibility leaks through portal silhouettes only.
|
||||||
///
|
///
|
||||||
|
|
@ -19,7 +19,7 @@ namespace AcDream.App.Rendering.Wb;
|
||||||
/// Retail reference: <c>docs/research/named-retail/acclient.h:32035</c>
|
/// Retail reference: <c>docs/research/named-retail/acclient.h:32035</c>
|
||||||
/// (<c>BuildInfo</c>) + <c>32094</c> (<c>CBldPortal</c>).</para>
|
/// (<c>BuildInfo</c>) + <c>32094</c> (<c>CBldPortal</c>).</para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class Building
|
internal sealed class Building
|
||||||
{
|
{
|
||||||
/// <summary>Unique within a landblock; allocated sequentially by <see cref="BuildingLoader"/>
|
/// <summary>Unique within a landblock; allocated sequentially by <see cref="BuildingLoader"/>
|
||||||
/// starting at 1 (0 is reserved for "no building" semantics on <c>LoadedCell</c>).</summary>
|
/// starting at 1 (0 is reserved for "no building" semantics on <c>LoadedCell</c>).</summary>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AcDream.App.Rendering;
|
using AcDream.App.Rendering;
|
||||||
using DatReaderWriter.DBObjs;
|
using DatReaderWriter.DBObjs;
|
||||||
|
|
@ -14,11 +14,11 @@ namespace AcDream.App.Rendering.Wb;
|
||||||
/// <para>Algorithm (mirrors WB's <c>PortalService.GetPortalsByBuilding</c> at
|
/// <para>Algorithm (mirrors WB's <c>PortalService.GetPortalsByBuilding</c> at
|
||||||
/// <c>WorldBuilder.Shared/Services/PortalService.cs:43-97</c>):</para>
|
/// <c>WorldBuilder.Shared/Services/PortalService.cs:43-97</c>):</para>
|
||||||
/// <list type="bullet">
|
/// <list type="bullet">
|
||||||
/// <item>Step A — seed the cell set from <c>BuildingInfo.Portals</c> entry portals.</item>
|
/// <item>Step A — seed the cell set from <c>BuildingInfo.Portals</c> entry portals.</item>
|
||||||
/// <item>Step B — BFS through <see cref="LoadedCell.Portals"/> to discover all
|
/// <item>Step B — BFS through <see cref="LoadedCell.Portals"/> to discover all
|
||||||
/// interior cells reachable from the entry portals (interior portals only;
|
/// interior cells reachable from the entry portals (interior portals only;
|
||||||
/// exit portals — <c>OtherCellId == 0xFFFF</c> — terminate each BFS branch).</item>
|
/// exit portals — <c>OtherCellId == 0xFFFF</c> — terminate each BFS branch).</item>
|
||||||
/// <item>Step C — collect exit portal polygons in world space for the stencil
|
/// <item>Step C — collect exit portal polygons in world space for the stencil
|
||||||
/// pipeline (Phase A8 Steps 1+2, RR7 scope).</item>
|
/// pipeline (Phase A8 Steps 1+2, RR7 scope).</item>
|
||||||
/// </list>
|
/// </list>
|
||||||
///
|
///
|
||||||
|
|
@ -57,7 +57,7 @@ internal sealed class BuildingRegistryPublication
|
||||||
internal bool PublicationCommitted { get; set; }
|
internal bool PublicationCommitted { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class BuildingLoader
|
internal static class BuildingLoader
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Builds a <see cref="BuildingRegistry"/> from the supplied landblock data.
|
/// Builds a <see cref="BuildingRegistry"/> from the supplied landblock data.
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace AcDream.App.Rendering.Wb;
|
namespace AcDream.App.Rendering.Wb;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Phase A8 (2026-05-26): per-landblock registry of <see cref="Building"/>s.
|
/// Phase A8 (2026-05-26): per-landblock registry of <see cref="Building"/>s.
|
||||||
/// Two-way indexed for O(1) cell→building and building-id→building lookups.
|
/// Two-way indexed for O(1) cell→building and building-id→building lookups.
|
||||||
/// Built once per landblock at load time by <see cref="BuildingLoader"/>;
|
/// Built once per landblock at load time by <see cref="BuildingLoader"/>;
|
||||||
/// no mutations occur after initial population.
|
/// no mutations occur after initial population.
|
||||||
///
|
///
|
||||||
/// <para>The cell→building index uses a <c>List<Building></c> value type
|
/// <para>The cell→building index uses a <c>List<Building></c> value type
|
||||||
/// to handle the (rare but valid) case where two buildings share an EnvCell —
|
/// to handle the (rare but valid) case where two buildings share an EnvCell —
|
||||||
/// each building performs its own BFS so a shared boundary cell ends up in both
|
/// each building performs its own BFS so a shared boundary cell ends up in both
|
||||||
/// <c>EnvCellIds</c> sets. <see cref="GetBuildingsContainingCell"/> returns all
|
/// <c>EnvCellIds</c> sets. <see cref="GetBuildingsContainingCell"/> returns all
|
||||||
/// owners so RR7's render path can pick the correct one.</para>
|
/// owners so RR7's render path can pick the correct one.</para>
|
||||||
|
|
@ -19,13 +19,13 @@ namespace AcDream.App.Rendering.Wb;
|
||||||
/// (<c>BuildingPortalGroup</c>). Design:
|
/// (<c>BuildingPortalGroup</c>). Design:
|
||||||
/// <c>docs/superpowers/specs/2026-05-26-phase-a8-wb-full-port-design.md</c>.</para>
|
/// <c>docs/superpowers/specs/2026-05-26-phase-a8-wb-full-port-design.md</c>.</para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class BuildingRegistry
|
internal sealed class BuildingRegistry
|
||||||
{
|
{
|
||||||
// Index 1: cell-id → list of buildings containing that cell.
|
// Index 1: cell-id → list of buildings containing that cell.
|
||||||
// Cells may belong to multiple buildings (rare; handled via List<Building>).
|
// Cells may belong to multiple buildings (rare; handled via List<Building>).
|
||||||
private readonly Dictionary<uint, List<Building>> _byCellId = new();
|
private readonly Dictionary<uint, List<Building>> _byCellId = new();
|
||||||
|
|
||||||
// Index 2: building-id → Building.
|
// Index 2: building-id → Building.
|
||||||
private readonly Dictionary<uint, Building> _byBuildingId = new();
|
private readonly Dictionary<uint, Building> _byBuildingId = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
|
||||||
namespace AcDream.App.Rendering.Wb {
|
namespace AcDream.App.Rendering.Wb {
|
||||||
// Extracted verbatim from WorldBuilder.Shared/Models/DebugRenderSettings.cs.
|
// Extracted verbatim from WorldBuilder.Shared/Models/DebugRenderSettings.cs.
|
||||||
// LandscapeColorsSettings dependency (editor-only, CommunityToolkit.Mvvm) stripped;
|
// LandscapeColorsSettings dependency (editor-only, CommunityToolkit.Mvvm) stripped;
|
||||||
// default color values inlined from LandscapeColorsSettings field initializers.
|
// default color values inlined from LandscapeColorsSettings field initializers.
|
||||||
public class DebugRenderSettings {
|
internal class DebugRenderSettings {
|
||||||
public bool ShowBoundingBoxes { get; set; } = false;
|
public bool ShowBoundingBoxes { get; set; } = false;
|
||||||
public bool SelectVertices { get; set; } = true;
|
public bool SelectVertices { get; set; } = true;
|
||||||
public bool SelectBuildings { get; set; } = true;
|
public bool SelectBuildings { get; set; } = true;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace AcDream.App.Rendering.Wb;
|
namespace AcDream.App.Rendering.Wb;
|
||||||
|
|
||||||
|
|
@ -7,7 +7,7 @@ namespace AcDream.App.Rendering.Wb;
|
||||||
/// Total size 20 bytes; arrays are typically uploaded with stride = sizeof(this).
|
/// Total size 20 bytes; arrays are typically uploaded with stride = sizeof(this).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[StructLayout(LayoutKind.Sequential, Pack = 4)]
|
[StructLayout(LayoutKind.Sequential, Pack = 4)]
|
||||||
public struct DrawElementsIndirectCommand
|
internal struct DrawElementsIndirectCommand
|
||||||
{
|
{
|
||||||
public uint Count; // index count for this draw
|
public uint Count; // index count for this draw
|
||||||
public uint InstanceCount; // number of instances
|
public uint InstanceCount; // number of instances
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
using AcDream.Core.World;
|
using AcDream.Core.World;
|
||||||
|
|
@ -11,7 +11,7 @@ namespace AcDream.App.Rendering.Wb;
|
||||||
/// logical removal has been queued and must be retried by the live-entity
|
/// logical removal has been queued and must be retried by the live-entity
|
||||||
/// teardown owner after the transition unwinds.
|
/// teardown owner after the transition unwinds.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class EntityPresentationRemovalDeferredException(uint serverGuid)
|
internal sealed class EntityPresentationRemovalDeferredException(uint serverGuid)
|
||||||
: InvalidOperationException(
|
: InvalidOperationException(
|
||||||
$"Live entity 0x{serverGuid:X8} presentation removal is deferred until its active reference transition completes.");
|
$"Live entity 0x{serverGuid:X8} presentation removal is deferred until its active reference transition completes.");
|
||||||
|
|
||||||
|
|
@ -43,7 +43,7 @@ public sealed class EntityPresentationRemovalDeferredException(uint serverGuid)
|
||||||
/// both to the created <see cref="AnimatedEntityState"/>.
|
/// both to the created <see cref="AnimatedEntityState"/>.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class EntitySpawnAdapter
|
internal sealed class EntitySpawnAdapter
|
||||||
{
|
{
|
||||||
private readonly IEntityTextureLifetime _textureLifetime;
|
private readonly IEntityTextureLifetime _textureLifetime;
|
||||||
private readonly Func<WorldEntity, AnimationSequencer> _sequencerFactory;
|
private readonly Func<WorldEntity, AnimationSequencer> _sequencerFactory;
|
||||||
|
|
@ -161,14 +161,14 @@ public sealed class EntitySpawnAdapter
|
||||||
}
|
}
|
||||||
|
|
||||||
// A.5 T18: populate cached AABB so WalkEntities reads from the cache
|
// A.5 T18: populate cached AABB so WalkEntities reads from the cache
|
||||||
// rather than recomputing Position±5 per frame. Called here because
|
// rather than recomputing Position±5 per frame. Called here because
|
||||||
// all entity-state initialization (position, rotation) is complete
|
// all entity-state initialization (position, rotation) is complete
|
||||||
// by this point via the WorldEntity passed in.
|
// by this point via the WorldEntity passed in.
|
||||||
entity.RefreshAabb();
|
entity.RefreshAabb();
|
||||||
|
|
||||||
// Build the per-entity AnimatedEntityState. The sequencer factory
|
// Build the per-entity AnimatedEntityState. The sequencer factory
|
||||||
// may return a stub (in tests) or a fully-constructed sequencer from
|
// may return a stub (in tests) or a fully-constructed sequencer from
|
||||||
// the MotionTable (in production). Factory must not return null —
|
// the MotionTable (in production). Factory must not return null —
|
||||||
// if the entity has no motion table the factory should construct a
|
// if the entity has no motion table the factory should construct a
|
||||||
// no-op sequencer (Setup + empty MotionTable + NullAnimationLoader).
|
// no-op sequencer (Setup + empty MotionTable + NullAnimationLoader).
|
||||||
var sequencer = _sequencerFactory(entity);
|
var sequencer = _sequencerFactory(entity);
|
||||||
|
|
@ -185,7 +185,7 @@ public sealed class EntitySpawnAdapter
|
||||||
|
|
||||||
// Snapshot each unique GfxObj id for the shorter presentation lifetime.
|
// Snapshot each unique GfxObj id for the shorter presentation lifetime.
|
||||||
// Includes both the entity's natural MeshRefs AND any server-sent
|
// Includes both the entity's natural MeshRefs AND any server-sent
|
||||||
// PartOverride GfxObjs (weapons, clothing, helmets) — those replace the
|
// PartOverride GfxObjs (weapons, clothing, helmets) — those replace the
|
||||||
// Setup default and need their own mesh data uploaded.
|
// Setup default and need their own mesh data uploaded.
|
||||||
// Construct the replacement completely before displacing a live owner.
|
// Construct the replacement completely before displacing a live owner.
|
||||||
// Sequencer/appearance construction is allowed to fail; in that case
|
// Sequencer/appearance construction is allowed to fail; in that case
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AcDream.Core.Rendering.Wb;
|
using AcDream.Core.Rendering.Wb;
|
||||||
using DatReaderWriter.DBObjs;
|
using DatReaderWriter.DBObjs;
|
||||||
|
|
@ -12,7 +12,7 @@ namespace AcDream.App.Rendering.Wb;
|
||||||
/// placement data; the render thread schedules mesh preparation when it commits
|
/// placement data; the render thread schedules mesh preparation when it commits
|
||||||
/// the containing <see cref="EnvCellLandblockBuild"/>.
|
/// the containing <see cref="EnvCellLandblockBuild"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed record EnvCellShellPlacement(
|
internal sealed record EnvCellShellPlacement(
|
||||||
uint CellId,
|
uint CellId,
|
||||||
ulong GeometryId,
|
ulong GeometryId,
|
||||||
uint EnvironmentId,
|
uint EnvironmentId,
|
||||||
|
|
@ -29,7 +29,7 @@ public sealed record EnvCellShellPlacement(
|
||||||
/// both portal-visibility cells and drawable shell placements so neither can be
|
/// both portal-visibility cells and drawable shell placements so neither can be
|
||||||
/// drained by, or mixed with, another streaming completion.
|
/// drained by, or mixed with, another streaming completion.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class EnvCellLandblockBuild
|
internal sealed class EnvCellLandblockBuild
|
||||||
{
|
{
|
||||||
public EnvCellLandblockBuild(
|
public EnvCellLandblockBuild(
|
||||||
uint landblockId,
|
uint landblockId,
|
||||||
|
|
@ -57,7 +57,7 @@ public sealed class EnvCellLandblockBuild
|
||||||
/// global pending bags, instances of this class are never shared between jobs or
|
/// global pending bags, instances of this class are never shared between jobs or
|
||||||
/// observed by the render thread before <see cref="Build"/> returns.
|
/// observed by the render thread before <see cref="Build"/> returns.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class EnvCellLandblockBuildBuilder
|
internal sealed class EnvCellLandblockBuildBuilder
|
||||||
{
|
{
|
||||||
private readonly uint _landblockId;
|
private readonly uint _landblockId;
|
||||||
private readonly List<LoadedCell> _visibilityCells = new();
|
private readonly List<LoadedCell> _visibilityCells = new();
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
|
||||||
namespace AcDream.App.Rendering.Wb;
|
namespace AcDream.App.Rendering.Wb;
|
||||||
|
|
||||||
|
|
@ -7,7 +7,7 @@ namespace AcDream.App.Rendering.Wb;
|
||||||
/// one private shell at a time; commit publishes the completed immutable
|
/// one private shell at a time; commit publishes the completed immutable
|
||||||
/// landblock snapshot with one owner dictionary replacement.
|
/// landblock snapshot with one owner dictionary replacement.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IEnvCellLandblockPublisher
|
internal interface IEnvCellLandblockPublisher
|
||||||
{
|
{
|
||||||
EnvCellLandblockPublication PreparePublication(
|
EnvCellLandblockPublication PreparePublication(
|
||||||
EnvCellLandblockBuild build);
|
EnvCellLandblockBuild build);
|
||||||
|
|
@ -17,7 +17,7 @@ public interface IEnvCellLandblockPublisher
|
||||||
void CommitPublication(EnvCellLandblockPublication publication);
|
void CommitPublication(EnvCellLandblockPublication publication);
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class EnvCellLandblockPublication
|
internal sealed class EnvCellLandblockPublication
|
||||||
{
|
{
|
||||||
internal EnvCellLandblockPublication(
|
internal EnvCellLandblockPublication(
|
||||||
object owner,
|
object owner,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
namespace AcDream.App.Rendering.Wb;
|
namespace AcDream.App.Rendering.Wb;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Starts CPU mesh extraction after the completed EnvCell build has been
|
/// Starts CPU mesh extraction after the completed EnvCell build has been
|
||||||
|
|
@ -7,7 +7,7 @@ namespace AcDream.App.Rendering.Wb;
|
||||||
/// current streaming generation; stale portal destinations never keep decoder
|
/// current streaming generation; stale portal destinations never keep decoder
|
||||||
/// jobs or surface lists alive.
|
/// jobs or surface lists alive.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class EnvCellMeshPreparationScheduler
|
internal static class EnvCellMeshPreparationScheduler
|
||||||
{
|
{
|
||||||
public static void Schedule(
|
public static void Schedule(
|
||||||
EnvCellLandblockBuild build,
|
EnvCellLandblockBuild build,
|
||||||
|
|
|
||||||
|
|
@ -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
|
// production cell-rendering pipeline for indoor visibility, replacing the
|
||||||
// broken "cell as WorldEntity with MeshRef(envCellId)" approach that the
|
// broken "cell as WorldEntity with MeshRef(envCellId)" approach that the
|
||||||
// four reverted RR7 variants couldn't fix.
|
// four reverted RR7 variants couldn't fix.
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
//
|
//
|
||||||
// Note: we do NOT inherit from WB's ObjectRenderManagerBase. That base
|
// Note: we do NOT inherit from WB's ObjectRenderManagerBase. That base
|
||||||
// class owns the landblock-streaming loop (Update, _pendingGeneration,
|
// 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
|
// running a parallel loop would compete for dat I/O. Instead, streaming builds
|
||||||
// a private EnvCellLandblockBuild and CommitLandblock publishes the completed
|
// a private EnvCellLandblockBuild and CommitLandblock publishes the completed
|
||||||
// snapshot on the render thread.
|
// snapshot on the render thread.
|
||||||
|
|
@ -29,7 +29,7 @@ using Silk.NET.OpenGL;
|
||||||
|
|
||||||
namespace AcDream.App.Rendering.Wb;
|
namespace AcDream.App.Rendering.Wb;
|
||||||
|
|
||||||
public sealed unsafe class EnvCellRenderer :
|
internal sealed unsafe class EnvCellRenderer :
|
||||||
IDisposable,
|
IDisposable,
|
||||||
IEnvCellLandblockPublisher
|
IEnvCellLandblockPublisher
|
||||||
{
|
{
|
||||||
|
|
@ -39,7 +39,7 @@ public sealed unsafe class EnvCellRenderer :
|
||||||
private readonly WbFrustum _frustum;
|
private readonly WbFrustum _frustum;
|
||||||
|
|
||||||
// Per-landblock storage. Key = full 32-bit landblock dat id (e.g. 0xA9B4FFFF).
|
// 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.
|
// we use uint (full LB id) because acdream uses 32-bit landblock keys throughout.
|
||||||
private readonly ConcurrentDictionary<uint, EnvCellLandblock> _landblocks = new();
|
private readonly ConcurrentDictionary<uint, EnvCellLandblock> _landblocks = new();
|
||||||
|
|
||||||
|
|
@ -64,7 +64,7 @@ public sealed unsafe class EnvCellRenderer :
|
||||||
private Matrix4x4 _lastViewProjection = Matrix4x4.Identity;
|
private Matrix4x4 _lastViewProjection = Matrix4x4.Identity;
|
||||||
private bool _initialized;
|
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;
|
// WB ObjectRenderManagerBase.cs:83-86: protected readonly List<List<InstanceData>> _listPool = new(); protected int _poolIndex = 0;
|
||||||
private readonly List<List<InstanceData>> _listPool = new();
|
private readonly List<List<InstanceData>> _listPool = new();
|
||||||
private int _poolIndex = 0;
|
private int _poolIndex = 0;
|
||||||
|
|
@ -78,7 +78,7 @@ public sealed unsafe class EnvCellRenderer :
|
||||||
private readonly ThreadLocal<PrepareScratch> _prepareScratch =
|
private readonly ThreadLocal<PrepareScratch> _prepareScratch =
|
||||||
new(() => new PrepareScratch(), trackAllValues: true);
|
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
|
// 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.
|
// We collapse the ring-of-3 to a single slot since we have no persistent/consolidated draws.
|
||||||
private uint _mdiCommandBuffer;
|
private uint _mdiCommandBuffer;
|
||||||
|
|
@ -95,7 +95,7 @@ public sealed unsafe class EnvCellRenderer :
|
||||||
// Phase U.3: per-instance clip-slot SSBO (binding=3), parallel to
|
// Phase U.3: per-instance clip-slot SSBO (binding=3), parallel to
|
||||||
// _modernInstanceBuffer. One uint per instance selecting its CellClip slot,
|
// _modernInstanceBuffer. One uint per instance selecting its CellClip slot,
|
||||||
// indexed by the same BaseInstance + gl_InstanceID the shader uses for
|
// 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 uint _clipSlotBuffer;
|
||||||
private int _clipSlotCapacity;
|
private int _clipSlotCapacity;
|
||||||
private uint[] _clipSlotData = Array.Empty<uint>();
|
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
|
// Campaign V slice V2 (2026-07-27): GL-only emulation of the eventual
|
||||||
// Vulkan global texture descriptor array (binding=9,
|
// Vulkan global texture descriptor array (binding=9,
|
||||||
// GpuBindingModel.StorageTextureTable). Owns its own table rather than
|
// 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
|
// dependency and nothing requires index agreement between renderers (each
|
||||||
// rebinds its own buffer to binding=9 immediately before its own draw
|
// rebinds its own buffer to binding=9 immediately before its own draw
|
||||||
// call). See GlBindlessHandleTable's doc comment and the campaign doc's
|
// call). See GlBindlessHandleTable's doc comment and the campaign doc's
|
||||||
// §5.2. Lazily created; grown/uploaded only when a genuinely new handle
|
// §5.2. Lazily created; grown/uploaded only when a genuinely new handle
|
||||||
// appears (rare — see FlushAndBindTextureTable).
|
// appears (rare — see FlushAndBindTextureTable).
|
||||||
private readonly GlBindlessHandleTable _textureTable = new();
|
private readonly GlBindlessHandleTable _textureTable = new();
|
||||||
private uint _textureTableSsbo;
|
private uint _textureTableSsbo;
|
||||||
private int _textureTableSsboCapacityBytes;
|
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<...>()
|
// WB BaseObjectRenderManager.cs:58-59: private DrawElementsIndirectCommand[] _commands = Array.Empty<...>()
|
||||||
private DrawElementsIndirectCommand[] _commands = Array.Empty<DrawElementsIndirectCommand>();
|
private DrawElementsIndirectCommand[] _commands = Array.Empty<DrawElementsIndirectCommand>();
|
||||||
private ModernBatchData[] _modernBatches = Array.Empty<ModernBatchData>();
|
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 Dictionary<ulong, List<InstanceData>> _activeSnapshotGlobalGroups = new();
|
||||||
private readonly List<ulong> _activeSnapshotGlobalGfxObjIds = 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.
|
// Shared across all manager instances on the same GL context.
|
||||||
private static uint _currentVao;
|
private static uint _currentVao;
|
||||||
private static CullMode? _currentCullMode;
|
private static CullMode? _currentCullMode;
|
||||||
|
|
@ -204,8 +204,8 @@ public sealed unsafe class EnvCellRenderer :
|
||||||
// inputs changed: landblock commits/removals (NeedsPrepare), the visible-cell
|
// inputs changed: landblock commits/removals (NeedsPrepare), the visible-cell
|
||||||
// filter, the trim window, mesh render-data availability (the snapshot bakes
|
// filter, the trim window, mesh render-data availability (the snapshot bakes
|
||||||
// per-cell transparency from TryGetRenderData), or the view-projection.
|
// per-cell transparency from TryGetRenderData), or the view-projection.
|
||||||
// NeedsPrepare existed since A8 but was never read — this wires it. The VP
|
// NeedsPrepare existed since A8 but was never read — this wires it. The VP
|
||||||
// tolerance must swallow the ~36 µm eye rest jitter (RetailPViewRenderer
|
// tolerance must swallow the ~36 µm eye rest jitter (RetailPViewRenderer
|
||||||
// R-A2 note) while any real camera motion crosses it in the same frame.
|
// R-A2 note) while any real camera motion crosses it in the same frame.
|
||||||
private Matrix4x4 _preparedViewProjection;
|
private Matrix4x4 _preparedViewProjection;
|
||||||
private Vector3 _preparedCameraPosition;
|
private Vector3 _preparedCameraPosition;
|
||||||
|
|
@ -223,14 +223,14 @@ public sealed unsafe class EnvCellRenderer :
|
||||||
public bool IsDisposed { get; private set; }
|
public bool IsDisposed { get; private set; }
|
||||||
|
|
||||||
public LastFrameStats Stats => _lastFrameStats;
|
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;
|
private LastFrameStats _lastFrameStats;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Diagnostic accessor for the [envcells] probe (Phase A8 apparatus 2026-05-28).
|
/// Diagnostic accessor for the [envcells] probe (Phase A8 apparatus 2026-05-28).
|
||||||
/// Returns (pool-list count total, snapshot's PostPreparePoolIndex high-water).
|
/// Returns (pool-list count total, snapshot's PostPreparePoolIndex high-water).
|
||||||
/// A divergence between expected and actual values would indicate a pool-
|
/// 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>
|
/// </summary>
|
||||||
public (int PoolTotal, int SnapshotPoolHwm) GetPoolDiagnostics()
|
public (int PoolTotal, int SnapshotPoolHwm) GetPoolDiagnostics()
|
||||||
{
|
{
|
||||||
|
|
@ -338,20 +338,20 @@ public sealed unsafe class EnvCellRenderer :
|
||||||
public void SetClipRegionSsbo(uint sharedClipRegionSsbo)
|
public void SetClipRegionSsbo(uint sharedClipRegionSsbo)
|
||||||
=> _sharedClipRegionSsbo = 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] =
|
// non-null, RenderModernMDIInternal writes instanceClipSlot[i] =
|
||||||
// _cellIdToSlot[allInstances[i].CellId] so each cell's shell instances are
|
// _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
|
// 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
|
// 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.
|
// map's keys, so that fallback should not fire in practice.
|
||||||
private IReadOnlyDictionary<uint, int>? _cellIdToSlot;
|
private IReadOnlyDictionary<uint, int>? _cellIdToSlot;
|
||||||
|
|
||||||
/// <summary>
|
/// <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
|
/// to their portal-clip regions. Call once per frame BEFORE
|
||||||
/// <see cref="Render(WbRenderPass, HashSet{uint}?)"/>. Pass null to revert to
|
/// <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>
|
/// </summary>
|
||||||
public void SetClipRouting(IReadOnlyDictionary<uint, int>? cellIdToSlot)
|
public void SetClipRouting(IReadOnlyDictionary<uint, int>? cellIdToSlot)
|
||||||
=> _cellIdToSlot = cellIdToSlot;
|
=> _cellIdToSlot = cellIdToSlot;
|
||||||
|
|
@ -386,7 +386,7 @@ public sealed unsafe class EnvCellRenderer :
|
||||||
surfaces);
|
surfaces);
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// CommitLandblock — render-thread transaction boundary
|
// CommitLandblock — render-thread transaction boundary
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -586,7 +586,7 @@ public sealed unsafe class EnvCellRenderer :
|
||||||
int? renderRadius = null)
|
int? renderRadius = null)
|
||||||
{
|
{
|
||||||
// Phase U.4 fix: stash the view-projection so Render() can upload it itself.
|
// 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).
|
// project with the CURRENT frame's matrix (the U.4 stale-matrix root cause).
|
||||||
_lastViewProjection = viewProjection;
|
_lastViewProjection = viewProjection;
|
||||||
|
|
||||||
|
|
@ -614,7 +614,7 @@ public sealed unsafe class EnvCellRenderer :
|
||||||
return;
|
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
|
// (Same-thread discipline makes the version sample exact: publish, release
|
||||||
// tickets, and this method all run on the render thread.)
|
// tickets, and this method all run on the render thread.)
|
||||||
if (_hasPreparedSnapshot
|
if (_hasPreparedSnapshot
|
||||||
|
|
@ -633,7 +633,7 @@ public sealed unsafe class EnvCellRenderer :
|
||||||
lock (_renderLock) { _poolIndex = 0; }
|
lock (_renderLock) { _poolIndex = 0; }
|
||||||
|
|
||||||
// WB skips _cameraLbX/Y update (from LandscapeDoc.Region) here in our variant
|
// 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:
|
// WB EnvCellRenderManager.cs:262:
|
||||||
// Filter loaded landblocks by GpuReady + Instances non-empty.
|
// Filter loaded landblocks by GpuReady + Instances non-empty.
|
||||||
|
|
@ -674,7 +674,7 @@ public sealed unsafe class EnvCellRenderer :
|
||||||
|
|
||||||
PrepareScratch scratch = _prepareScratch.Value!;
|
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)
|
if (testResult == FrustumTestResult.Inside)
|
||||||
{
|
{
|
||||||
foreach (var (gfxObjId, instances) in lb.BuildingPartGroups)
|
foreach (var (gfxObjId, instances) in lb.BuildingPartGroups)
|
||||||
|
|
@ -692,7 +692,7 @@ public sealed unsafe class EnvCellRenderer :
|
||||||
return;
|
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;
|
HashSet<uint> visibleCells = scratch.VisibleCells;
|
||||||
visibleCells.Clear();
|
visibleCells.Clear();
|
||||||
foreach (var kvp in lb.EnvCellBounds)
|
foreach (var kvp in lb.EnvCellBounds)
|
||||||
|
|
@ -804,13 +804,13 @@ public sealed unsafe class EnvCellRenderer :
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Pure half of the prepare gate's camera test (regression-tested without a
|
/// Pure half of the prepare gate's camera test (regression-tested without a
|
||||||
/// GL context, same pattern as <see cref="CreateCommittedSnapshot"/>).
|
/// 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).
|
/// 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
|
/// translation row scales with world coordinates (~5e4 in AC), where a
|
||||||
/// relative tolerance would mask sub-meter motion. Rows 1–3 of
|
/// relative tolerance would mask sub-meter motion. Rows 1–3 of
|
||||||
/// view × projection are position-independent (rotation × projection), so a
|
/// view × projection are position-independent (rotation × projection), so a
|
||||||
/// relative 1e-5 there dirties at ≈0.001° of rotation and on any
|
/// relative 1e-5 there dirties at ≈0.001° of rotation and on any
|
||||||
/// projection (FOV/aspect/near/far) change.
|
/// projection (FOV/aspect/near/far) change.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static bool CameraApproximatelyEqual(
|
internal static bool CameraApproximatelyEqual(
|
||||||
|
|
@ -926,7 +926,7 @@ public sealed unsafe class EnvCellRenderer :
|
||||||
// Verbatim port of WB EnvCellRenderManager.cs:395-511.
|
// Verbatim port of WB EnvCellRenderManager.cs:395-511.
|
||||||
// Deviations from WB (all documented):
|
// Deviations from WB (all documented):
|
||||||
// - Drop the _useModernRendering branch (our codebase asserts modern at startup per Phase N.5).
|
// - 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.
|
// - Replace RenderModernMDI(base) with private RenderModernMDIInternal.
|
||||||
// - shader.Bind() / SetUniform API: mapped to acdream's legacy Shader
|
// - shader.Bind() / SetUniform API: mapped to acdream's legacy Shader
|
||||||
// class (Use() + SetInt/SetVec4/SetMatrix4) to match the existing
|
// 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
|
/// 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
|
/// 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
|
/// 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).
|
/// Source: WB EnvCellRenderManager.cs:399-511 (verbatim minus selection highlights).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Render(WbRenderPass renderPass, HashSet<uint>? filter)
|
public void Render(WbRenderPass renderPass, HashSet<uint>? filter)
|
||||||
|
|
@ -979,7 +979,7 @@ public sealed unsafe class EnvCellRenderer :
|
||||||
// WB EnvCellRenderManager.cs:403-404:
|
// WB EnvCellRenderManager.cs:403-404:
|
||||||
_shader.Use();
|
_shader.Use();
|
||||||
// FIX 2026-05-28 (pool aliasing root cause): mirror WB
|
// 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
|
// high-water mark Prepare's merge phase reached, so any
|
||||||
// GetPooledList calls below return lists past the snapshot's
|
// GetPooledList calls below return lists past the snapshot's
|
||||||
// owned region. Original code used `snapshot.BatchedByCell.Count`
|
// owned region. Original code used `snapshot.BatchedByCell.Count`
|
||||||
|
|
@ -999,7 +999,7 @@ public sealed unsafe class EnvCellRenderer :
|
||||||
// RenderInsideOutAcdream stencil pipeline) change the actual GL
|
// RenderInsideOutAcdream stencil pipeline) change the actual GL
|
||||||
// state without updating these caches. The cache then lies, and
|
// state without updating these caches. The cache then lies, and
|
||||||
// the per-batch SetCullMode in RenderModernMDIInternal skips its
|
// 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
|
// consumer. For a cottage with mixed CullMode batches, half the
|
||||||
// walls end up culled and the user sees "missing walls".
|
// 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("uRenderPass", (int)renderPass);
|
||||||
_shader.SetInt("uFilterByCell", 0);
|
_shader.SetInt("uFilterByCell", 0);
|
||||||
_shader.SetInt("uLightingMode", 1); // A7 Fix D D-3/D-4: EnvCell bake (wrap points, no sun)
|
_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);
|
_shader.SetInt("uLightDebug", AcDream.Core.Rendering.RenderingDiagnostics.LightDebugMode);
|
||||||
|
|
||||||
// Phase U.4 ROOT-CAUSE FIX (cell-shell flicker / "transparent walls when
|
// Phase U.4 ROOT-CAUSE FIX (cell-shell flicker / "transparent walls when
|
||||||
// moving"): upload uViewProjection HERE rather than inheriting it from
|
// moving"): upload uViewProjection HERE rather than inheriting it from
|
||||||
// WbDrawDispatcher. The opaque shell pass runs BEFORE the dispatcher's
|
// WbDrawDispatcher. The opaque shell pass runs BEFORE the dispatcher's
|
||||||
// Draw (GameWindow ~7411 vs ~7418, the only other setter), so without
|
// Draw (GameWindow ~7411 vs ~7418, the only other setter), so without
|
||||||
// this the opaque shells used the PREVIOUS frame's matrix — a stale
|
// this the opaque shells used the PREVIOUS frame's matrix — a stale
|
||||||
// gl_Position against this frame's clip planes → pose-dependent clipping,
|
// gl_Position against this frame's clip planes → pose-dependent clipping,
|
||||||
// worst while moving. Same self-contained-GL-state precedent as the
|
// worst while moving. Same self-contained-GL-state precedent as the
|
||||||
// 2026-05-28 cull-state cache fix above.
|
// 2026-05-28 cull-state cache fix above.
|
||||||
_shader.SetMatrix4("uViewProjection", _lastViewProjection);
|
_shader.SetMatrix4("uViewProjection", _lastViewProjection);
|
||||||
|
|
@ -1059,7 +1059,7 @@ public sealed unsafe class EnvCellRenderer :
|
||||||
else if (filter is null)
|
else if (filter is null)
|
||||||
{
|
{
|
||||||
RebuildUnfilteredGroups(snapshot);
|
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)
|
foreach (var gfxObjId in _activeSnapshotGlobalGfxObjIds)
|
||||||
{
|
{
|
||||||
if (_activeSnapshotGlobalGroups.TryGetValue(gfxObjId, out var transforms))
|
if (_activeSnapshotGlobalGroups.TryGetValue(gfxObjId, out var transforms))
|
||||||
|
|
@ -1144,7 +1144,7 @@ public sealed unsafe class EnvCellRenderer :
|
||||||
renderPass);
|
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.
|
// WB EnvCellRenderManager.cs:506-509: cleanup.
|
||||||
_shader.SetVec4("uHighlightColor", new System.Numerics.Vector4(0, 0, 0, 0));
|
_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
|
? dc.renderData.Batches[0].IndexCount / 3
|
||||||
: 0) * dc.count;
|
: 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
|
// Per opaque-pass call: totals + per visible (filtered) cell whether it is
|
||||||
// present in the prepared snapshot, and its geometry/flags. Answers why the
|
// present in the prepared snapshot, and its geometry/flags. Answers why the
|
||||||
// interior walls/ceiling don't appear: NOSNAP / gfx=0 ⇒ no shell geometry
|
// 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
|
// 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/
|
// (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).
|
// occlusion or the geometry isn't the wall). Opaque pass only (halves noise).
|
||||||
if (renderPass == WbRenderPass.Opaque
|
if (renderPass == WbRenderPass.Opaque
|
||||||
&& AcDream.Core.Rendering.RenderingDiagnostics.ProbeShellEnabled)
|
&& AcDream.Core.Rendering.RenderingDiagnostics.ProbeShellEnabled)
|
||||||
|
|
@ -1217,7 +1217,7 @@ public sealed unsafe class EnvCellRenderer :
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// True if the cell's prepared snapshot has any transparent render batch.
|
/// 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
|
/// 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
|
/// opaque walls/floors/ceilings, so this removes the bulk of the per-cell
|
||||||
/// transparent draws. Read-only; mirrors the [shell] probe's batch scan.
|
/// transparent draws. Read-only; mirrors the [shell] probe's batch scan.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -1227,7 +1227,7 @@ public sealed unsafe class EnvCellRenderer :
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// GetCellLightSet (A7 Fix D D-2 helper)
|
// GetCellLightSet (A7 Fix D D-2 helper)
|
||||||
// Per-cell up-to-8 point lights, cached per frame. Camera-independent, like
|
// 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
|
// 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;
|
var snap = _pointSnapshot;
|
||||||
// Landblocks are keyed by the streaming landblock id 0xXXYYFFFF
|
// 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
|
// key is (cellId & 0xFFFF0000) | 0xFFFF. The old `cellId & 0xFFFF0000` key
|
||||||
// (0xXXYY0000) NEVER matched a registered landblock, so this lookup always
|
// (0xXXYY0000) NEVER matched a registered landblock, so this lookup always
|
||||||
// missed: SelectForObject never ran and every EnvCell wall received ZERO
|
// missed: SelectForObject never ran and every EnvCell wall received ZERO
|
||||||
// point lights (the entire "indoor torches/lanterns don't light the room"
|
// 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 } &&
|
if (snap is { Count: > 0 } &&
|
||||||
_landblocks.TryGetValue((cellId & 0xFFFF0000u) | 0xFFFFu, out var lb) &&
|
_landblocks.TryGetValue((cellId & 0xFFFF0000u) | 0xFFFFu, out var lb) &&
|
||||||
lb.EnvCellBounds.TryGetValue(cellId, out var b))
|
lb.EnvCellBounds.TryGetValue(cellId, out var b))
|
||||||
{
|
{
|
||||||
Vector3 center = (b.Min + b.Max) * 0.5f;
|
Vector3 center = (b.Min + b.Max) * 0.5f;
|
||||||
float radius = (b.Max - b.Min).Length() * 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
|
// 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);
|
AcDream.Core.Lighting.LightManager.SelectForCell(snap, center, radius, set);
|
||||||
}
|
}
|
||||||
cached.FrameGeneration = _lightFrameGeneration;
|
cached.FrameGeneration = _lightFrameGeneration;
|
||||||
|
|
@ -1414,9 +1414,9 @@ public sealed unsafe class EnvCellRenderer :
|
||||||
int passIdx = (int)renderPass;
|
int passIdx = (int)renderPass;
|
||||||
if (passIdx < 0 || passIdx > 2) return;
|
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
|
// 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).
|
// see the comment on the state-establish block below).
|
||||||
var globalVao = _meshManager.GlobalBuffer?.VAO ?? 0u;
|
var globalVao = _meshManager.GlobalBuffer?.VAO ?? 0u;
|
||||||
if (globalVao == 0) return;
|
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). Restored to opaque defaults at the end of the draw loop so a
|
||||||
// Transparent pass can't leak into later draws.
|
// 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
|
// 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
|
// 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
|
// 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
|
// 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
|
// glClear(DEPTH) silently no-oped (depth clears honor glDepthMask), every world
|
||||||
// fragment failed GL_LESS against its own previous-frame depth ghost, and the
|
// 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
|
// 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
|
// until camera rotation dropped the cell from the flood. From here down every
|
||||||
// path reaches the end-of-pass restore.
|
// 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
|
// Phase U.4: upload the per-instance clip-slot buffer (binding=3). When
|
||||||
// _cellIdToSlot is set (indoor routing), each cell shell instance is gated
|
// _cellIdToSlot is set (indoor routing), each cell shell instance is gated
|
||||||
// to its cell's CellClip slot via allInstances[i].CellId; cells absent from
|
// 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
|
// 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
|
// SAME order as the binding=0 transforms (_gpuInstanceTransforms below), so
|
||||||
// instanceClipSlot[i] tracks Instances[i] through the MDI BaseInstance.
|
// instanceClipSlot[i] tracks Instances[i] through the MDI BaseInstance.
|
||||||
if (_clipSlotData.Length < uniqueInstanceCount)
|
if (_clipSlotData.Length < uniqueInstanceCount)
|
||||||
_clipSlotData = new uint[Math.Max(_clipSlotData.Length * 2, uniqueInstanceCount)];
|
_clipSlotData = new uint[Math.Max(_clipSlotData.Length * 2, uniqueInstanceCount)];
|
||||||
// #176 stripe-hunt isolation (ACDREAM_CLIP_DEBUG=1): force every shell
|
// #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
|
if (_cellIdToSlot is null
|
||||||
|| AcDream.Core.Rendering.RenderingDiagnostics.ClipDebugNoShellTrim)
|
|| 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
|
// #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
|
// 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
|
if (renderPass == WbRenderPass.Opaque
|
||||||
&& AcDream.Core.Rendering.RenderingDiagnostics.ProbeSeamDrawEnabled)
|
&& AcDream.Core.Rendering.RenderingDiagnostics.ProbeSeamDrawEnabled)
|
||||||
EmitSeamDrawProbe(drawCalls, allInstances, _seamProbeFilter);
|
EmitSeamDrawProbe(drawCalls, allInstances, _seamProbeFilter);
|
||||||
|
|
@ -1766,7 +1766,7 @@ public sealed unsafe class EnvCellRenderer :
|
||||||
PersistActiveDynamicBufferCapacities();
|
PersistActiveDynamicBufferCapacities();
|
||||||
|
|
||||||
// WB BaseObjectRenderManager.cs:807-818: bind VAO + SSBOs + barrier.
|
// 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.)
|
// pass state established above.)
|
||||||
if (_currentVao != globalVao)
|
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
|
// The in-engine replacement for the RenderDoc pixel-history the pipeline
|
||||||
// can't have (RenderDoc hides GL_ARB_bindless_texture → our mandatory-modern
|
// can't have (RenderDoc hides GL_ARB_bindless_texture → our mandatory-modern
|
||||||
// startup gate throws). Per opaque pass: for each target cell — flood
|
// startup gate throws). Per opaque pass: for each target cell — flood
|
||||||
// membership, every shell instance (count + translation, F3 z shows the
|
// 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 +
|
// cell's 8-light set resolved to stable IDENTITIES (owner-cell low16 +
|
||||||
// intensity; raw indices shuffle when the pool rebuilds). Plus the
|
// intensity; raw indices shuffle when the pool rebuilds). Plus the
|
||||||
// snapshot's HOT lights (intensity ≥ 50 — the portal purples; fixtures are
|
// snapshot's HOT lights (intensity ≥ 50 — the portal purples; fixtures are
|
||||||
// ~1–2). Change-deduped block with a 2 s heartbeat: a purple identity
|
// ~1–2). Change-deduped block with a 2 s heartbeat: a purple identity
|
||||||
// flipping with flood membership = the snapshot-scope mechanism; two
|
// flipping with flood membership = the snapshot-scope mechanism; two
|
||||||
// coincident instances = the z-fight. See RenderingDiagnostics.
|
// 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"/>
|
/// 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
|
/// when a new one was registered since the last flush, then (re)binds it at
|
||||||
/// <see cref="AcDream.App.Rendering.Gpu.GpuBindingModel.StorageTextureTable"/>.
|
/// <see cref="AcDream.App.Rendering.Gpu.GpuBindingModel.StorageTextureTable"/>.
|
||||||
/// A genuinely new handle is rare — new dat surfaces/atlases, not every
|
/// 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;
|
/// frame — so this is not part of the ring-buffered per-frame SSBO set;
|
||||||
/// see GlBindlessHandleTable's doc comment.
|
/// see GlBindlessHandleTable's doc comment.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void FlushAndBindTextureTable()
|
private void FlushAndBindTextureTable()
|
||||||
|
|
@ -2068,7 +2068,7 @@ public sealed unsafe class EnvCellRenderer :
|
||||||
GLEnum.DynamicDraw,
|
GLEnum.DynamicDraw,
|
||||||
"allocating EnvCell fallback clip SSBO");
|
"allocating EnvCell fallback clip SSBO");
|
||||||
allocated = true;
|
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];
|
Span<byte> zero = stackalloc byte[AcDream.App.Rendering.ClipFrame.CellClipStrideBytes];
|
||||||
zero.Clear();
|
zero.Clear();
|
||||||
fixed (byte* p = zero)
|
fixed (byte* p = zero)
|
||||||
|
|
@ -2103,12 +2103,12 @@ public sealed unsafe class EnvCellRenderer :
|
||||||
|
|
||||||
private List<InstanceData> GetPooledList()
|
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'
|
// branch MUST clear the list before returning. PrepareRenderBatches'
|
||||||
// merge phase pattern is `gfxDict[k] = list; list.AddRange(...)`,
|
// merge phase pattern is `gfxDict[k] = list; list.AddRange(...)`,
|
||||||
// which assumes the list is empty. Without the clear, lists grow
|
// which assumes the list is empty. Without the clear, lists grow
|
||||||
// unbounded across frames and each frame's draw includes all prior
|
// 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
|
// cause of post-Wave-5 visual chaos (FIX 2026-05-28). See
|
||||||
// docs/research/2026-05-28-a8-env-cell-renderer-audit-findings.md.
|
// docs/research/2026-05-28-a8-env-cell-renderer-audit-findings.md.
|
||||||
lock (_listPool)
|
lock (_listPool)
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue