diff --git a/src/AcDream.App/Audio/AudioHookSink.cs b/src/AcDream.App/Audio/AudioHookSink.cs
index 9f91b8cc..a687fafc 100644
--- a/src/AcDream.App/Audio/AudioHookSink.cs
+++ b/src/AcDream.App/Audio/AudioHookSink.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Numerics;
using AcDream.Core.Audio;
@@ -19,33 +19,33 @@ namespace AcDream.App.Audio;
/// Wiring:
///
/// -
-/// → direct play of SoundHook.Id (a
+/// → direct play of SoundHook.Id (a
/// Wave dat id) at the entity's world position. Used for custom /
/// per-animation audio like weapon swoosh or spell chant.
///
/// -
-/// → look up the entity's SoundTable +
+/// → look up the entity's SoundTable +
/// the hook's SoundType, roll one
/// via
/// , 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.
///
/// -
-/// → same as SoundHook but with
+/// → same as SoundHook but with
/// pitch / volume overrides baked into the hook.
///
///
///
///
///
-/// Entity → SoundTable id is resolved via an
+/// Entity → SoundTable id is resolved via an
/// callback passed in at construction; the renderer's per-entity state
/// bag knows the PhysicsObj's SoundTableId (retail:
/// PhysicsObj.soundtable_id).
///
///
-internal sealed class AudioHookSink : IAnimationHookSink
+public sealed class AudioHookSink : IAnimationHookSink
{
private readonly OpenAlAudioEngine _engine;
private readonly DatSoundCache _cache;
@@ -81,7 +81,7 @@ internal sealed class AudioHookSink : IAnimationHookSink
case SoundTweakedHook stw:
// SoundTweakedHook is a direct wave play with volume +
// 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.
Play(entityId, entityWorldPosition,
waveId: (uint)stw.SoundId,
@@ -90,7 +90,7 @@ internal sealed class AudioHookSink : IAnimationHookSink
pitch: 1f);
break;
- // All the visual-only hooks (Scale, Luminous, Diffuse, …)
+ // All the visual-only hooks (Scale, Luminous, Diffuse, …)
// are for other sinks to handle.
}
}
@@ -138,7 +138,7 @@ internal sealed class AudioHookSink : IAnimationHookSink
/// entity. Retail stores this on PhysicsObj.soundtable_id; our
/// renderer keeps per-entity state that includes it.
///
-internal interface IEntitySoundTable
+public interface IEntitySoundTable
{
///
/// Return the SoundTable dat id (0x20xxxxxx) for ,
@@ -151,7 +151,7 @@ internal interface IEntitySoundTable
/// Simple dictionary-backed ; the renderer
/// assigns entries as it hydrates entities.
///
-internal sealed class DictionaryEntitySoundTable : IEntitySoundTable
+public sealed class DictionaryEntitySoundTable : IEntitySoundTable
{
private readonly Dictionary _table = new();
diff --git a/src/AcDream.App/Audio/OpenAlAudioEngine.cs b/src/AcDream.App/Audio/OpenAlAudioEngine.cs
index e94409e2..959415ea 100644
--- a/src/AcDream.App/Audio/OpenAlAudioEngine.cs
+++ b/src/AcDream.App/Audio/OpenAlAudioEngine.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Numerics;
using AcDream.Core.Audio;
@@ -7,8 +7,8 @@ using Silk.NET.OpenAL;
namespace AcDream.App.Audio;
///
-/// OpenAL-backed audio engine (Phase E.2) — faithful to retail's
-/// 16-voice pool and inverse-square falloff behaviour (r05 §5.3).
+/// OpenAL-backed audio engine (Phase E.2) — faithful to retail's
+/// 16-voice pool and inverse-square falloff behaviour (r05 §5.3).
///
///
/// Architecture:
@@ -16,7 +16,7 @@ namespace AcDream.App.Audio;
/// -
/// Single + bound to the
/// system default device. Cross-platform (WASAPI / WinMM /
-/// PulseAudio / CoreAudio — whichever OpenAL-Soft picks).
+/// PulseAudio / CoreAudio — whichever OpenAL-Soft picks).
///
/// -
/// Fixed 16-source pool for 3D positional sounds. When all 16 are
@@ -27,13 +27,13 @@ namespace AcDream.App.Audio;
///
/// -
/// 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.
///
/// -
/// 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
/// by a byte budget () enforced
-/// with LRU eviction — see . A
+/// with LRU eviction — see . A
/// buffer still attached to a live source is never evicted (AL
/// rejects deleting a bound buffer); eviction re-queries live AL
/// source state rather than tracking a second copy of it.
@@ -60,15 +60,15 @@ internal interface IWorldAudioQuiescence
void ResumeWorldAudio();
}
-internal sealed unsafe class OpenAlAudioEngine : IAudioEngine, IWorldAudioQuiescence
+public sealed unsafe class OpenAlAudioEngine : IAudioEngine, IWorldAudioQuiescence
{
- // ── Backends ─────────────────────────────────────────────────────────────
+ // ── Backends ─────────────────────────────────────────────────────────────
private AL? _al;
private OpenAlResourceLifetime? _resources;
private bool _available;
private bool _disposed;
- // ── Pools ────────────────────────────────────────────────────────────────
+ // ── Pools ────────────────────────────────────────────────────────────────
private const int PoolSize3D = 16; // retail 16-slot voice pool
private const int PoolSizeUi = 4;
@@ -88,7 +88,7 @@ internal sealed unsafe class OpenAlAudioEngine : IAudioEngine, IWorldAudioQuiesc
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
// as DatSoundCache's payload LRU, which this cache re-uploads from). 48
// MiB gives comfortable headroom for the working set of a play session
@@ -99,11 +99,11 @@ internal sealed unsafe class OpenAlAudioEngine : IAudioEngine, IWorldAudioQuiesc
private readonly Dictionary _bufferByWaveId = new();
private readonly AlBufferBudgetTracker _bufferBudget = new(DefaultBufferByteBudget);
- // ── Ambient handles (StartAmbient/StopAmbient) ───────────────────────────
+ // ── Ambient handles (StartAmbient/StopAmbient) ───────────────────────────
private readonly Dictionary _ambientSources = new();
private int _nextAmbientHandle = 1;
- // ── Public volume knobs ──────────────────────────────────────────────────
+ // ── Public volume knobs ──────────────────────────────────────────────────
public float MasterVolume { get; set; } = 1f;
public float SfxVolume { get; set; } = 1f;
public float MusicVolume { get; set; } = 0.7f;
@@ -219,7 +219,7 @@ internal sealed unsafe class OpenAlAudioEngine : IAudioEngine, IWorldAudioQuiesc
_al = null;
}
- // ── IAudioEngine ─────────────────────────────────────────────────────────
+ // ── IAudioEngine ─────────────────────────────────────────────────────────
public void SetListener(
float posX, float posY, float posZ,
@@ -241,7 +241,7 @@ internal sealed unsafe class OpenAlAudioEngine : IAudioEngine, IWorldAudioQuiesc
}
///
- /// 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.
/// Returns true on success, false if the buffer was rejected.
///
@@ -278,7 +278,7 @@ internal sealed unsafe class OpenAlAudioEngine : IAudioEngine, IWorldAudioQuiesc
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];
_al.SourceStop(slot.SourceId);
@@ -355,7 +355,7 @@ internal sealed unsafe class OpenAlAudioEngine : IAudioEngine, IWorldAudioQuiesc
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
// has access to decoded WaveData. Left as no-ops for now; R5 defines
// SoundId as a sparse subset of retail enums.
@@ -366,7 +366,7 @@ internal sealed unsafe class OpenAlAudioEngine : IAudioEngine, IWorldAudioQuiesc
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
// system (outside R5) will drive this. For now: reserve a handle.
int handle = _nextAmbientHandle++;
@@ -383,17 +383,17 @@ internal sealed unsafe class OpenAlAudioEngine : IAudioEngine, IWorldAudioQuiesc
}
}
- 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 */ }
- // ── Private helpers ──────────────────────────────────────────────────────
+ // ── Private helpers ──────────────────────────────────────────────────────
private uint EnsureBuffer(uint waveId, WaveData wave)
{
if (!_available || _al is null) return 0;
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.
if (existing != 0)
_bufferBudget.Touch(waveId);
@@ -425,15 +425,15 @@ internal sealed unsafe class OpenAlAudioEngine : IAudioEngine, IWorldAudioQuiesc
///
/// Evict least-recently-used AL buffers until the resident-byte budget
/// is satisfied again. A buffer still bound to a live source (3D pool,
- /// UI pool, or an ambient source) is protected — alDeleteBuffers
- /// fails on a buffer that's still attached to a source — so eviction
+ /// UI pool, or an ambient source) is protected — alDeleteBuffers
+ /// fails on a buffer that's still attached to a source — so eviction
/// never targets one; nor does it target ,
/// the buffer just created for this call and
/// hasn't attached to a source yet. If every resident buffer is
/// protected the budget is temporarily exceeded rather than looping
/// forever; the bounded pool sizes (16 + 4 + ambient) cap how large
/// that overage can get. Evicted waves replay through
- /// again on next use — re-upload from
+ /// again on next use — re-upload from
/// , identical to a first play.
///
private void EvictBuffersOverBudget(uint protectedBufferId)
diff --git a/src/AcDream.App/Composition/FrameRootComposition.cs b/src/AcDream.App/Composition/FrameRootComposition.cs
index 4caa35fa..0319914f 100644
--- a/src/AcDream.App/Composition/FrameRootComposition.cs
+++ b/src/AcDream.App/Composition/FrameRootComposition.cs
@@ -263,6 +263,8 @@ internal sealed class FrameRootCompositionPhase
live.DrawDispatcher,
live.EnvCellRenderer,
live.PortalDepthMask,
+ foundation.TextRenderer,
+ interaction.RetainedUi?.Host.TextRenderer,
live.ClipFrame,
foundation.Terrain,
foundation.SceneLighting),
@@ -361,7 +363,6 @@ internal sealed class FrameRootCompositionPhase
d.CellVisibility),
d.WorldSceneDebugState,
foundation.DebugLines,
- host.GpuFrameLifetime,
d.PhysicsEngine,
d.PlayerMode,
d.PlayerController,
@@ -508,7 +509,7 @@ internal sealed class FrameRootCompositionPhase
: (IRenderFramePostDiagnosticsPhase?)lifecycleAutomation
?? NullRenderFramePostDiagnosticsPhase.Instance;
var renderFrame = new RenderFrameOrchestrator(
- host.GpuFrameLifetime,
+ host.GpuFrameFlights,
new FrameProfilerGpuMeasurement(d.FrameProfiler, d.Gl),
framePreparation,
worldSceneRenderer,
diff --git a/src/AcDream.App/Composition/HostInputCameraComposition.cs b/src/AcDream.App/Composition/HostInputCameraComposition.cs
index b3f0ca91..52e0592b 100644
--- a/src/AcDream.App/Composition/HostInputCameraComposition.cs
+++ b/src/AcDream.App/Composition/HostInputCameraComposition.cs
@@ -12,7 +12,6 @@ internal interface IGameWindowHostInputCameraPublication
{
void PublishGpuFrameFlights(GpuFrameFlightController value);
void PublishGpuDevice(IGpuDevice value);
- void PublishGpuFrameLifetime(GpuDeviceFrameLifetime value);
void PublishKeyboardSource(SilkKeyboardSource value);
void PublishMouseSource(SilkMouseSource value);
void PublishMouseLookCursor(IMouseLookCursor value);
@@ -24,7 +23,6 @@ internal interface IGameWindowHostInputCameraPublication
internal sealed record HostInputCameraResult(
GpuFrameFlightController GpuFrameFlights,
IGpuDevice GpuDevice,
- GpuDeviceFrameLifetime GpuFrameLifetime,
WorldRenderDiagnostics WorldRenderDiagnostics,
SilkKeyboardSource? KeyboardSource,
SilkMouseSource? MouseSource,
@@ -237,10 +235,9 @@ internal sealed class HostInputCameraCompositionPhase :
// exist — it owns its own BindlessSupport detection (see
// GlGpuDevice's class comment), so unlike the legacy WB render path it
// has no dependency on WorldRenderCompositionPhase running first.
- // Campaign V slice V4a is the first real consumer (TextRenderer,
- // BitmapFont, DebugLineRenderer, TextureCache's UI upload path); it is
+ // Nothing consumes this device yet (Campaign V slice V1); it is
// proven against the real driver here and torn down with the render
- // stack.
+ // stack so later slices (starting at V4a) have somewhere to plug in.
IGpuDevice gpuDevice = scope.Acquire(
"GPU device (RHI)",
() => _factory.CreateGpuDevice(gl, gpuFrames),
@@ -248,14 +245,6 @@ internal sealed class HostInputCameraCompositionPhase :
_publication.PublishGpuDevice);
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 =
_factory.CreateWorldRenderDiagnostics(
gl,
@@ -356,7 +345,6 @@ internal sealed class HostInputCameraCompositionPhase :
return new HostInputCameraResult(
gpuFrames,
gpuDevice,
- gpuFrameLifetime,
diagnostics,
keyboard,
mouse,
diff --git a/src/AcDream.App/Composition/InteractionRetainedUiComposition.cs b/src/AcDream.App/Composition/InteractionRetainedUiComposition.cs
index e2b9e7e8..7f189414 100644
--- a/src/AcDream.App/Composition/InteractionRetainedUiComposition.cs
+++ b/src/AcDream.App/Composition/InteractionRetainedUiComposition.cs
@@ -33,10 +33,9 @@ namespace AcDream.App.Composition;
internal sealed record InteractionRetainedUiDependencies(
RuntimeOptions Options,
GL Gl,
- IGpuDevice GpuDevice,
- Func CurrentGpuFrame,
IView Window,
IInputContext Input,
+ string ShadersDirectory,
IDatReaderWriter Dats,
object DatLock,
TextureCache TextureCache,
@@ -390,8 +389,8 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
d.Character.LocalPlayer);
UiHost host = lease.AcquireHost(
() => new UiHost(
- d.GpuDevice,
- d.CurrentGpuFrame,
+ d.Gl,
+ d.ShadersDirectory,
d.DebugFont,
d.HostQuiescence));
checkpoint(InteractionRetainedUiCompositionPoint.UiHostAcquired);
@@ -478,9 +477,9 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
host.WireKeyboard(keyboard);
checkpoint(InteractionRetainedUiCompositionPoint.KeyboardInputWired);
- (GpuTextureSlot, int, int) ResolveChrome(uint id)
+ (uint, int, int) ResolveChrome(uint id)
{
- GpuTextureSlot texture = d.TextureCache.GetOrUploadRenderSurface(
+ uint texture = d.TextureCache.GetOrUploadRenderSurface(
id,
out int width,
out int height);
diff --git a/src/AcDream.App/Composition/LivePresentationComposition.cs b/src/AcDream.App/Composition/LivePresentationComposition.cs
index ead0ec98..0e3cfe73 100644
--- a/src/AcDream.App/Composition/LivePresentationComposition.cs
+++ b/src/AcDream.App/Composition/LivePresentationComposition.cs
@@ -38,7 +38,6 @@ namespace AcDream.App.Composition;
internal sealed record LivePresentationDependencies(
RuntimeOptions Options,
GL Gl,
- IGpuDevice GpuDevice,
IWindow Window,
object DatLock,
RuntimeSettingsController Settings,
@@ -796,8 +795,7 @@ internal sealed class LivePresentationCompositionPhase
paperdollLease.Resource,
new RetailPaperdollFrameView(
viewport,
- new PaperdollInventoryVisibility(inventoryFrame),
- d.GpuDevice),
+ new PaperdollInventoryVisibility(inventoryFrame)),
new RetailPaperdollDollFactory(
new LivePaperdollEntityLookup(liveEntities),
d.PlayerIdentity,
@@ -844,8 +842,7 @@ internal sealed class LivePresentationCompositionPhase
new RetailCreatureAppraisalFrameView(
creatureViewport,
examinationFrame,
- appraisalController,
- d.GpuDevice),
+ appraisalController),
new RetailCreatureAppraisalCloneFactory(
new LiveCreatureAppraisalEntityLookup(liveEntities)));
}
diff --git a/src/AcDream.App/Composition/WorldRenderComposition.cs b/src/AcDream.App/Composition/WorldRenderComposition.cs
index 4decbc81..bdbc10f6 100644
--- a/src/AcDream.App/Composition/WorldRenderComposition.cs
+++ b/src/AcDream.App/Composition/WorldRenderComposition.cs
@@ -52,7 +52,6 @@ internal sealed record WorldRenderDependencies(
WorldEnvironmentController Environment,
IGameRenderResourceLifetime RenderResources,
IGpuResourceRetirementQueue ResourceRetirement,
- IGpuDevice GpuDevice,
ResidencyBudgetOptions ResidencyBudgets,
uint InitialCenterLandblockId,
string DiagnosticsDirectory,
@@ -90,10 +89,10 @@ internal interface IWorldRenderCompositionFactory
void SetTerrainAnisotropic(TerrainAtlas atlas, int level);
Shader CreateTerrainShader(GL gl, string shadersDirectory);
SceneLightingUboBinding CreateSceneLighting(GL gl);
- DebugLineRenderer CreateDebugLines(IGpuDevice device);
+ DebugLineRenderer CreateDebugLines(GL gl, string shadersDirectory);
byte[]? TryLoadDebugFont();
- BitmapFont CreateDebugFont(IGpuDevice device, byte[] bytes);
- TextRenderer CreateTextRenderer(IGpuDevice device);
+ BitmapFont CreateDebugFont(GL gl, byte[] bytes);
+ TextRenderer CreateTextRenderer(GL gl, string shadersDirectory);
TerrainModernRenderer CreateTerrain(
GL gl,
BindlessSupport bindless,
@@ -113,7 +112,6 @@ internal interface IWorldRenderCompositionFactory
ResidencyBudgetOptions budgets);
TextureCache CreateTextureCache(
GL gl,
- IGpuDevice device,
IDatReaderWriter dats,
BindlessSupport bindless,
IGpuResourceRetirementQueue retirement,
@@ -216,15 +214,21 @@ internal sealed class RetailWorldRenderCompositionFactory
public SceneLightingUboBinding CreateSceneLighting(GL gl) => new(gl);
- public DebugLineRenderer CreateDebugLines(IGpuDevice device) => new(device);
+ public DebugLineRenderer CreateDebugLines(
+ GL gl,
+ string shadersDirectory) =>
+ new(gl, shadersDirectory);
public byte[]? TryLoadDebugFont() =>
BitmapFont.TryLoadSystemMonospaceFont();
- public BitmapFont CreateDebugFont(IGpuDevice device, byte[] bytes) =>
- new(device, bytes, pixelHeight: 15f, atlasSize: 512);
+ public BitmapFont CreateDebugFont(GL gl, byte[] bytes) =>
+ new(gl, bytes, pixelHeight: 15f, atlasSize: 512);
- public TextRenderer CreateTextRenderer(IGpuDevice device) => new(device);
+ public TextRenderer CreateTextRenderer(
+ GL gl,
+ string shadersDirectory) =>
+ new(gl, shadersDirectory);
public TerrainModernRenderer CreateTerrain(
GL gl,
@@ -290,7 +294,6 @@ internal sealed class RetailWorldRenderCompositionFactory
public TextureCache CreateTextureCache(
GL gl,
- IGpuDevice device,
IDatReaderWriter dats,
BindlessSupport bindless,
IGpuResourceRetirementQueue retirement,
@@ -298,7 +301,6 @@ internal sealed class RetailWorldRenderCompositionFactory
ResidencyBudgetOptions budgets) =>
new(
gl,
- device,
dats,
bindless,
retirement,
@@ -480,12 +482,12 @@ internal sealed class WorldRenderCompositionPhase
DebugLineRenderer debugLines = AcquireAndPublish(
scope,
"debug lines",
- () => _factory.CreateDebugLines(_dependencies.GpuDevice),
+ () => _factory.CreateDebugLines(gl, shadersDirectory),
_publication.PublishDebugLines,
WorldRenderCompositionPoint.DebugLinesPublished);
(BitmapFont? debugFont, TextRenderer? textRenderer) =
- ComposeOptionalHudResources(scope);
+ ComposeOptionalHudResources(scope, gl, shadersDirectory);
TerrainModernRenderer terrain = AcquireAndPublish(
scope,
@@ -533,7 +535,6 @@ internal sealed class WorldRenderCompositionPhase
"texture cache",
() => _factory.CreateTextureCache(
gl,
- _dependencies.GpuDevice,
content.Dats,
bindless,
_dependencies.ResourceRetirement,
@@ -585,7 +586,9 @@ internal sealed class WorldRenderCompositionPhase
}
private (BitmapFont? Font, TextRenderer? Text) ComposeOptionalHudResources(
- CompositionAcquisitionScope scope)
+ CompositionAcquisitionScope scope,
+ GL gl,
+ string shadersDirectory)
{
byte[]? fontBytes = _factory.TryLoadDebugFont();
if (fontBytes is null)
@@ -597,13 +600,13 @@ internal sealed class WorldRenderCompositionPhase
var fontLease = scope.Acquire(
"world HUD font",
- () => _factory.CreateDebugFont(_dependencies.GpuDevice, fontBytes),
+ () => _factory.CreateDebugFont(gl, fontBytes),
_factory.Release);
BitmapFont font = fontLease.Resource;
Fault(WorldRenderCompositionPoint.DebugFontCreated);
var textLease = scope.Acquire(
"world HUD text renderer",
- () => _factory.CreateTextRenderer(_dependencies.GpuDevice),
+ () => _factory.CreateTextRenderer(gl, shadersDirectory),
_factory.Release);
TextRenderer text = textLease.Resource;
Fault(WorldRenderCompositionPoint.TextRendererCreated);
diff --git a/src/AcDream.App/Diagnostics/FrameProfiler.cs b/src/AcDream.App/Diagnostics/FrameProfiler.cs
index f49c1810..69d02add 100644
--- a/src/AcDream.App/Diagnostics/FrameProfiler.cs
+++ b/src/AcDream.App/Diagnostics/FrameProfiler.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
@@ -10,11 +10,11 @@ using Silk.NET.OpenGL;
namespace AcDream.App.Diagnostics;
/// Stage indices for per-frame CPU attribution.
-internal enum FrameStage
+public enum FrameStage
{
/// Whole OnUpdate body (simulation + streaming apply).
Update = 0,
- /// WbMeshAdapter.Tick — staged mesh/texture GPU upload drain.
+ /// WbMeshAdapter.Tick — staged mesh/texture GPU upload drain.
Upload = 1,
/// ImGui Render (dev overlay).
ImGui = 2,
@@ -23,11 +23,11 @@ internal enum FrameStage
}
///
-/// One ACDREAM_FRAME_HISTORY CSV row — every field the aggregated
+/// One ACDREAM_FRAME_HISTORY CSV row — every field the aggregated
/// [frame-prof] report discards when its 5-second window resets.
/// Stage fields mirror positionally (Update /
/// Upload / ImGui / Pacing, matching 's
-/// names array) — if grows, extend this
+/// names array) — if grows, extend this
/// record, , and the CSV header
/// together. GpuUs is -1 for a frame with no available GPU
/// sample (warm-up, or ACDREAM_WB_DIAG=1 self-disable).
@@ -44,7 +44,7 @@ internal readonly record struct FrameHistoryRecord(
long PacingUs);
///
-/// MP0 (2026-07-05) — the permanent honest frame profiler. One
+/// MP0 (2026-07-05) — the permanent honest frame profiler. One
/// FrameBoundary call at the top of the accepted render transaction
/// measures CPU frame time as the delta between consecutive boundaries
/// (captures the FULL frame including present) and samples per-frame allocated
@@ -56,29 +56,29 @@ internal readonly record struct FrameHistoryRecord(
/// is true; costs one
/// bool check per frame when off.
///
-/// Permanent apparatus — every MP-track gate reads it; do not strip.
+/// Permanent apparatus — every MP-track gate reads it; do not strip.
/// Whole-frame GPU timing self-disables under ACDREAM_WB_DIAG=1
/// (nested TimeElapsed is illegal GL; see GpuFrameTimer).
///
-/// 2026-07-24 measurement-tooling review — the aggregated report
+/// 2026-07-24 measurement-tooling review — the aggregated report
/// resets its ring buffers every ~5 s (),
/// so route-wide p50/p95/p99 distributions across a whole soak cannot be
/// reconstructed after the fact.
/// (ACDREAM_FRAME_HISTORY=<path>) opts into a SEPARATE
/// per-frame history: one per frame in a
/// preallocated, grow-as-needed (1 int + 1 double +
-/// 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,
+/// 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,
/// 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.
/// ZERO frame-thread I/O: the CSV is written once, from
/// , at shutdown. Recording only takes effect while
-/// is ALSO true — it
+/// is ALSO true — it
/// reuses that instrumentation rather than duplicating it. Does not
/// change the [frame-prof] report format or any existing metric.
-/// Spec: docs/superpowers/specs/2026-07-05-modern-pipeline-design.md §5.
+/// Spec: docs/superpowers/specs/2026-07-05-modern-pipeline-design.md §5.
///
-internal sealed class FrameProfiler : IDisposable
+public sealed class FrameProfiler : IDisposable
{
private const int WindowCapacity = 2048; // ~12 s at 165 fps
private const int HistoryInitialCapacity = 131072;
@@ -136,7 +136,7 @@ internal sealed class FrameProfiler : IDisposable
if (_wasEnabled)
{
// 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
// GPU samples. Safe here: this runs at the top of OnRender
// with the GL context current.
@@ -163,7 +163,7 @@ internal sealed class FrameProfiler : IDisposable
{
// First enabled frame (startup or runtime toggle-on): establish
// 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
// flipped, and that partial delta must not leak into the first
// re-enabled frame.
@@ -274,7 +274,7 @@ internal sealed class FrameProfiler : IDisposable
internal void EndStage(FrameStage stage, long startTimestamp)
=> _stageAccumTicks[(int)stage] += Stopwatch.GetTimestamp() - startTimestamp;
- /// Pure report formatter — unit-tested; invariant culture.
+ /// Pure report formatter — unit-tested; invariant culture.
public static string FormatReport(
int frameCount,
FrameStatsBuffer cpu, FrameStatsBuffer gpu, bool gpuActive,
@@ -301,7 +301,7 @@ internal sealed class FrameProfiler : IDisposable
return sb.ToString();
}
- /// Pure CSV formatter — unit-tested; invariant culture. One header row plus one row per record.
+ /// Pure CSV formatter — unit-tested; invariant culture. One header row plus one row per record.
internal static void WriteHistoryCsv(
IEnumerable records,
TextWriter writer,
@@ -330,7 +330,7 @@ internal sealed class FrameProfiler : IDisposable
///
/// Shutdown-only write of the accumulated as CSV
- /// (the ONLY I/O this feature performs — never from ).
+ /// (the ONLY I/O this feature performs — never from ).
/// Failures are logged, not thrown: a history-export problem must never
/// block the rest of the render-owner shutdown chain
/// (GameWindowLifetime's Hard("frame profiler", ...) stage).
@@ -355,7 +355,7 @@ internal sealed class FrameProfiler : IDisposable
}
/// Disposable stage scope; default instance is a no-op.
-internal readonly struct StageScope : IDisposable
+public readonly struct StageScope : IDisposable
{
private readonly FrameProfiler? _owner;
private readonly FrameStage _stage;
diff --git a/src/AcDream.App/Diagnostics/FrameStatsBuffer.cs b/src/AcDream.App/Diagnostics/FrameStatsBuffer.cs
index d94936ce..60773235 100644
--- a/src/AcDream.App/Diagnostics/FrameStatsBuffer.cs
+++ b/src/AcDream.App/Diagnostics/FrameStatsBuffer.cs
@@ -1,16 +1,16 @@
-using System;
+using System;
namespace AcDream.App.Diagnostics;
///
-/// 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.
/// Pure and allocation-free after construction:
/// sorts into a preallocated scratch array, so the 5-second report path
-/// allocates nothing. Not thread-safe — owned by the window loop thread.
-/// Spec: docs/superpowers/specs/2026-07-05-modern-pipeline-design.md §5.
+/// allocates nothing. Not thread-safe — owned by the window loop thread.
+/// Spec: docs/superpowers/specs/2026-07-05-modern-pipeline-design.md §5.
///
-internal sealed class FrameStatsBuffer
+public sealed class FrameStatsBuffer
{
private readonly long[] _samples;
private readonly long[] _scratch;
@@ -41,7 +41,7 @@ internal sealed class FrameStatsBuffer
///
/// 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.
///
public long Percentile(double q)
{
diff --git a/src/AcDream.App/GlobalUsings.cs b/src/AcDream.App/GlobalUsings.cs
index 7e8a6845..5213b6cc 100644
--- a/src/AcDream.App/GlobalUsings.cs
+++ b/src/AcDream.App/GlobalUsings.cs
@@ -2,8 +2,3 @@ global using AcDream.Runtime.Gameplay;
global using AcDream.Runtime.Physics;
global using ILocalPlayerMotionSource =
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;
diff --git a/src/AcDream.App/Input/LocalPlayerProjectionController.cs b/src/AcDream.App/Input/LocalPlayerProjectionController.cs
index 0e6cf13f..8a32c4d7 100644
--- a/src/AcDream.App/Input/LocalPlayerProjectionController.cs
+++ b/src/AcDream.App/Input/LocalPlayerProjectionController.cs
@@ -1,4 +1,4 @@
-using AcDream.Core.World;
+using AcDream.Core.World;
using AcDream.App.Physics;
using AcDream.App.World;
@@ -59,7 +59,7 @@ internal sealed class LiveLocalPlayerProjectionRuntime
/// Projects the canonical local physics body into world rendering and spatial
/// buckets without advancing it.
///
-internal sealed class LocalPlayerProjectionController
+public sealed class LocalPlayerProjectionController
{
private readonly ILocalPlayerProjectionRuntime _runtime;
diff --git a/src/AcDream.App/Input/PlayerModeAutoEntry.cs b/src/AcDream.App/Input/PlayerModeAutoEntry.cs
index 2c629a11..cc158601 100644
--- a/src/AcDream.App/Input/PlayerModeAutoEntry.cs
+++ b/src/AcDream.App/Input/PlayerModeAutoEntry.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using AcDream.App.Net;
using AcDream.App.Streaming;
using AcDream.App.World;
@@ -6,7 +6,7 @@ using AcDream.App.World;
namespace AcDream.App.Input;
///
-/// 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
/// 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
/// movement controller is constructible, and the initial world is fully
/// 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
/// that's painful to test through GameWindow but trivial here against
/// fakes.
@@ -25,11 +25,11 @@ namespace AcDream.App.Input;
///
/// The public surface is:
///
-/// - — call after EnterWorld succeeds to
+///
- — call after EnterWorld succeeds to
/// arm the entry trigger.
-/// - — call when the user manually enters
+///
- — call when the user manually enters
/// fly mode (or any other code path that pre-empts the auto-entry).
-/// - — call once per frame; runs the
+///
- — call once per frame; runs the
/// guard and fires the entry callback when armed AND every
/// precondition is satisfied; returns true on the firing tick.
///
@@ -101,7 +101,7 @@ internal sealed class LivePlayerModeAutoEntryContext
}
}
-internal sealed class PlayerModeAutoEntry
+public sealed class PlayerModeAutoEntry
{
private sealed class DelegateContext : IPlayerModeAutoEntryContext
{
@@ -163,7 +163,7 @@ internal sealed class PlayerModeAutoEntry
/// Retail keeps position completion behind one blocking cell-load edge;
/// acdream's asynchronous domains must converge before entry.
/// 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
/// camera and switch the active camera; the auto-entry doesn't
/// reach inside.
@@ -202,7 +202,7 @@ internal sealed class PlayerModeAutoEntry
///
/// Disarm the trigger without firing the callback. Call when the
/// 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.
///
public void Cancel() => _armed = false;
diff --git a/src/AcDream.App/Input/RetailLocalPlayerFrameController.cs b/src/AcDream.App/Input/RetailLocalPlayerFrameController.cs
index f66bee33..ca4ee1b9 100644
--- a/src/AcDream.App/Input/RetailLocalPlayerFrameController.cs
+++ b/src/AcDream.App/Input/RetailLocalPlayerFrameController.cs
@@ -1,4 +1,4 @@
-using AcDream.App.Update;
+using AcDream.App.Update;
using AcDream.Runtime;
namespace AcDream.App.Input;
@@ -17,7 +17,7 @@ internal sealed class RetailLocalPlayerFrameController : IPostNetworkCommandFram
{
private readonly RuntimeLocalPlayerFrameController _runtime;
- internal readonly record struct PresentationFrame(
+ public readonly record struct PresentationFrame(
MovementResult Movement,
bool Hidden,
bool AdvancedBeforeNetwork);
diff --git a/src/AcDream.App/Input/SilkKeyboardSource.cs b/src/AcDream.App/Input/SilkKeyboardSource.cs
index 061e258e..bf1c17d1 100644
--- a/src/AcDream.App/Input/SilkKeyboardSource.cs
+++ b/src/AcDream.App/Input/SilkKeyboardSource.cs
@@ -1,4 +1,4 @@
-using AcDream.App.Rendering;
+using AcDream.App.Rendering;
using AcDream.UI.Abstractions.Input;
using Silk.NET.Input;
@@ -73,7 +73,7 @@ internal sealed class SilkKeyboardEventSurface : IKeyboardEventSurface
/// subscription. Logical deactivation makes copied delegates inert while
/// physical removal is retried.
///
-internal sealed class SilkKeyboardSource : IKeyboardSource, IDisposable
+public sealed class SilkKeyboardSource : IKeyboardSource, IDisposable
{
private readonly IKeyboardEventSurface _surface;
private readonly HostQuiescenceGate _quiescence;
diff --git a/src/AcDream.App/Input/SilkMouseSource.cs b/src/AcDream.App/Input/SilkMouseSource.cs
index d0df94cb..a8530a64 100644
--- a/src/AcDream.App/Input/SilkMouseSource.cs
+++ b/src/AcDream.App/Input/SilkMouseSource.cs
@@ -1,4 +1,4 @@
-using System.Numerics;
+using System.Numerics;
using AcDream.App.Rendering;
using AcDream.UI.Abstractions.Input;
using Silk.NET.Input;
@@ -100,7 +100,7 @@ internal sealed class SilkMouseEventSurface : IMouseEventSurface
}
/// Reversible Silk mouse bridge with immediate logical cutoff.
-internal sealed class SilkMouseSource : IMouseSource, IDisposable
+public sealed class SilkMouseSource : IMouseSource, IDisposable
{
private readonly IMouseEventSurface _surface;
private readonly IInputCaptureSource _capture;
diff --git a/src/AcDream.App/Physics/LocalPlayerShadowState.cs b/src/AcDream.App/Physics/LocalPlayerShadowState.cs
index db419aee..777384da 100644
--- a/src/AcDream.App/Physics/LocalPlayerShadowState.cs
+++ b/src/AcDream.App/Physics/LocalPlayerShadowState.cs
@@ -1,11 +1,11 @@
-using System.Numerics;
+using System.Numerics;
namespace AcDream.App.Physics;
/// Session-scoped cache of the local player's last published shadow pose.
internal sealed class LocalPlayerShadowState
{
- internal readonly record struct Snapshot(
+ public readonly record struct Snapshot(
Vector3 Position,
Quaternion Orientation,
uint CellId);
diff --git a/src/AcDream.App/Physics/RemoteTeleportHook.cs b/src/AcDream.App/Physics/RemoteTeleportHook.cs
index ed689267..a0b54769 100644
--- a/src/AcDream.App/Physics/RemoteTeleportHook.cs
+++ b/src/AcDream.App/Physics/RemoteTeleportHook.cs
@@ -1,4 +1,4 @@
-using AcDream.Core.Physics;
+using AcDream.Core.Physics;
namespace AcDream.App.Physics;
@@ -8,7 +8,7 @@ namespace AcDream.App.Physics;
/// testable while the concrete movement/interpolation/target owners remain
/// in the composition root.
///
-internal static class RemoteTeleportHook
+public static class RemoteTeleportHook
{
private const WeenieError TeleportCancelContext = (WeenieError)0x3Cu;
@@ -46,7 +46,7 @@ internal static class RemoteTeleportHook
}
}
-internal sealed record RemoteTeleportHookActions(
+public sealed record RemoteTeleportHookActions(
Action CancelMoveTo,
Action UnStick,
Action StopInterpolating,
diff --git a/src/AcDream.App/Plugins/AppPluginHost.cs b/src/AcDream.App/Plugins/AppPluginHost.cs
index c8b99ed2..bfabab86 100644
--- a/src/AcDream.App/Plugins/AppPluginHost.cs
+++ b/src/AcDream.App/Plugins/AppPluginHost.cs
@@ -1,8 +1,8 @@
-using AcDream.Plugin.Abstractions;
+using AcDream.Plugin.Abstractions;
namespace AcDream.App.Plugins;
-internal sealed class AppPluginHost : IPluginHost
+public sealed class AppPluginHost : IPluginHost
{
public AppPluginHost(
IPluginLogger log,
diff --git a/src/AcDream.App/Plugins/BufferedUiRegistry.cs b/src/AcDream.App/Plugins/BufferedUiRegistry.cs
index b8c431f4..bcab04fb 100644
--- a/src/AcDream.App/Plugins/BufferedUiRegistry.cs
+++ b/src/AcDream.App/Plugins/BufferedUiRegistry.cs
@@ -1,4 +1,4 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
using AcDream.Plugin.Abstractions;
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
/// UiHost tree after construction.
///
-internal sealed class BufferedUiRegistry : IUiRegistry
+public sealed class BufferedUiRegistry : IUiRegistry
{
- internal readonly record struct Pending(string MarkupPath, object Binding);
+ public readonly record struct Pending(string MarkupPath, object Binding);
private readonly List _pending = new();
diff --git a/src/AcDream.App/Plugins/SerilogAdapter.cs b/src/AcDream.App/Plugins/SerilogAdapter.cs
index d336b019..9f0b4f09 100644
--- a/src/AcDream.App/Plugins/SerilogAdapter.cs
+++ b/src/AcDream.App/Plugins/SerilogAdapter.cs
@@ -1,8 +1,8 @@
-using AcDream.Plugin.Abstractions;
+using AcDream.Plugin.Abstractions;
namespace AcDream.App.Plugins;
-internal sealed class SerilogAdapter : IPluginLogger
+public sealed class SerilogAdapter : IPluginLogger
{
private readonly Serilog.ILogger _log;
public SerilogAdapter(Serilog.ILogger log) => _log = log;
diff --git a/src/AcDream.App/Rendering/BitmapFont.cs b/src/AcDream.App/Rendering/BitmapFont.cs
index 73ecb214..9306b4bd 100644
--- a/src/AcDream.App/Rendering/BitmapFont.cs
+++ b/src/AcDream.App/Rendering/BitmapFont.cs
@@ -1,20 +1,20 @@
-using System;
+using System;
using System.IO;
+using Silk.NET.OpenGL;
using StbTrueTypeSharp;
namespace AcDream.App.Rendering;
///
/// A pixel-font atlas rasterized from a TTF at load time using stb_truetype.
-/// Glyphs are packed into a single-channel (R8) texture registered in the
-/// device's global texture table. Call to resolve an
-/// ASCII codepoint to UV + metrics.
+/// Glyphs are packed into a single-channel (R8) GL texture. Call
+/// to resolve an ASCII codepoint to UV + metrics.
///
/// Only printable ASCII (32..127) is supported for the debug overlay.
///
-internal sealed unsafe class BitmapFont : IDisposable
+public sealed unsafe class BitmapFont : IDisposable
{
- internal readonly struct Glyph
+ public readonly struct Glyph
{
public readonly float UvMinX;
public readonly float UvMinY;
@@ -34,23 +34,23 @@ internal sealed unsafe class BitmapFont : IDisposable
}
}
+ private readonly GL _gl;
private readonly Glyph[] _glyphs;
private readonly int _firstChar;
private readonly int _numChars;
- private readonly IGpuDevice _device;
- private readonly IGpuTexture _texture;
+ private readonly ResourceCleanupGroup _resources;
- public GpuTextureSlot TextureId { get; }
+ public uint TextureId { get; }
public float PixelHeight { get; }
public float LineHeight { get; }
public float Ascent { get; }
public int AtlasWidth { get; }
public int AtlasHeight { get; }
- public BitmapFont(IGpuDevice device, byte[] ttfBytes, float pixelHeight,
+ public BitmapFont(GL gl, byte[] ttfBytes, float pixelHeight,
int atlasSize = 512, int firstChar = 32, int numChars = 96)
{
- _device = device ?? throw new ArgumentNullException(nameof(device));
+ _gl = gl;
PixelHeight = pixelHeight;
AtlasWidth = atlasSize;
AtlasHeight = atlasSize;
@@ -96,31 +96,65 @@ internal sealed unsafe class BitmapFont : IDisposable
adv: bc.xadvance);
}
- // Upload atlas as a single-channel texture (R8) and register it in the
- // device's global texture table. Linear + clamp-to-edge matches the
- // GL path's prior fixed sampler state exactly (mip filtering is moot —
- // the atlas is a single mip level).
- IGpuTexture texture = _device.CreateTexture(new GpuTextureDescription(
- "bitmap-font-atlas",
- GpuTextureKind.Texture2D,
- GpuTextureFormat.R8Unorm,
- Width: AtlasWidth,
- Height: AtlasHeight,
- LayerCount: 1,
- MipLevelCount: 1));
+ // Upload atlas as a single-channel GL texture (R8). Publish the GL
+ // name into the construction ledger before any later upload/state
+ // command can fail.
+ var resources = new ResourceCleanupGroup();
+ uint texture = 0;
try
{
- texture.Upload(0, 0, pixels);
- IGpuSampler sampler = _device.CreateSampler(GpuSamplerDescription.WorldClamp);
- TextureId = _device.RegisterTexture(texture, sampler);
+ texture = GlResourceCommand.CreateTexture(_gl, "BitmapFont atlas");
+ uint ownedTexture = texture;
+ resources.Add(
+ "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
+ catch (Exception constructionFailure)
{
- texture.Dispose();
- throw;
+ resources.RollbackConstructionAndThrow(
+ "BitmapFont construction failed and its GL atlas did not cleanly roll back.",
+ constructionFailure);
}
- _texture = texture;
+ TextureId = texture;
+ _resources = resources;
}
public bool TryGetGlyph(char c, out Glyph g)
@@ -149,8 +183,7 @@ internal sealed unsafe class BitmapFont : IDisposable
public void Dispose()
{
- _device.ReleaseTextureSlot(TextureId);
- _texture.Dispose();
+ _resources.RetryCleanup();
}
///
diff --git a/src/AcDream.App/Rendering/CameraController.cs b/src/AcDream.App/Rendering/CameraController.cs
index 0944e344..60b84ac5 100644
--- a/src/AcDream.App/Rendering/CameraController.cs
+++ b/src/AcDream.App/Rendering/CameraController.cs
@@ -1,9 +1,9 @@
-// src/AcDream.App/Rendering/CameraController.cs
+// src/AcDream.App/Rendering/CameraController.cs
using AcDream.Core.Rendering;
namespace AcDream.App.Rendering;
-internal sealed class CameraController
+public sealed class CameraController
{
internal readonly record struct CameraState(
int ModeCode,
@@ -19,7 +19,7 @@ internal sealed class CameraController
/// The renderer-facing active camera. Both the legacy and retail
/// chase cameras are held simultaneously so that flipping
/// 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.
///
public ICamera Active
@@ -59,7 +59,7 @@ internal sealed class CameraController
///
/// Store both cameras simultaneously; 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.
///
public void EnterChaseMode(ChaseCamera legacy, RetailChaseCamera retail)
{
diff --git a/src/AcDream.App/Rendering/CellVisibility.cs b/src/AcDream.App/Rendering/CellVisibility.cs
index 544d60cd..31b9ebca 100644
--- a/src/AcDream.App/Rendering/CellVisibility.cs
+++ b/src/AcDream.App/Rendering/CellVisibility.cs
@@ -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.
// 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
// 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
// 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
/// for portal-traversal visibility decisions.
///
-internal sealed class LoadedCell
+public sealed class LoadedCell
{
/// Full 32-bit cell ID, e.g. 0xA9B40105.
public uint CellId;
@@ -87,7 +87,7 @@ internal sealed class LoadedCell
public uint? BuildingId { get; internal set; }
///
- /// 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
/// visible from this cell, precomputed by the AC content tools. Refreshed only at
/// hydration (= retail's per-cell-entry grab_visible_cells, decomp:311878).
@@ -98,7 +98,7 @@ internal sealed class LoadedCell
public IReadOnlyList VisibleCells = System.Array.Empty();
///
- /// 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
/// 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.
@@ -107,7 +107,7 @@ internal sealed class LoadedCell
///
/// Render unification (2026-06-07): true for the synthetic OUTDOOR cell node built by
- /// — the outdoor world modelled as a flood-graph cell whose
+ /// — the outdoor world modelled as a flood-graph cell whose
/// shell is the landscape. seeds OutsideView
/// 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
@@ -123,19 +123,19 @@ internal sealed class LoadedCell
/// is the dat's reciprocal back-link: the index of
/// the portal WITHIN the neighbour cell's portal list that points back through
/// this same opening. Retail indexes the reciprocal directly via this field
-/// (arg2->other_portal_id, decomp:433557) rather than scanning — which
+/// (arg2->other_portal_id, decomp:433557) rather than scanning — which
/// 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.
///
///
-internal readonly record struct CellPortalInfo(
+public readonly record struct CellPortalInfo(
ushort OtherCellId, ushort PolygonId, ushort Flags, ushort OtherPortalId);
///
/// Clip plane derived from a portal polygon, in cell-local space.
/// Plane equation: Normal.X*x + Normal.Y*y + Normal.Z*z + D = 0.
///
-internal struct PortalClipPlane
+public struct PortalClipPlane
{
/// Plane normal (cell-local space, unit length).
public Vector3 Normal;
@@ -146,8 +146,8 @@ internal struct PortalClipPlane
///
/// Which half-space is "inside" this cell (the side from which you look outward
/// through the portal):
- /// 0 → camera dot-product must be >= 0 (positive half-space is inside)
- /// 1 → camera dot-product must be <= 0 (negative 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)
/// Determined from cell centroid position relative to the portal plane.
/// Ported from ACME EnvCellManager.cs ~line 404.
///
@@ -155,12 +155,12 @@ internal struct PortalClipPlane
}
///
-/// 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
/// for binary compatibility with the [flap-cam] probe log site in GameWindow.cs that
/// still prints (always None post-Stage 3).
///
-internal enum CameraCellResolution
+public enum CameraCellResolution
{
/// No cell contains the eye (outdoors), or not yet resolved.
None,
@@ -171,14 +171,14 @@ internal enum CameraCellResolution
/// The eye is inside a cell found by the full brute-force scan.
BruteForce,
/// 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.
+ /// few grace frames — the "stale root" case the flap probe watches for.
Grace,
}
///
/// Result of a portal-based visibility BFS from the camera cell.
///
-internal sealed class VisibilityResult
+public sealed class VisibilityResult
{
/// Full cell IDs (e.g. 0x01D90105) that should be rendered this frame.
public HashSet VisibleCellIds { get; init; } = new();
@@ -207,7 +207,7 @@ internal sealed class VisibilityResult
/// Ported faithfully from ACME's EnvCellManager.cs portal-visibility region.
/// Constants and control flow match the ACME implementation.
///
-internal sealed class CellVisibility
+public sealed class CellVisibility
{
// ------------------------------------------------------------------
// Constants (ACME ground-truth values)
@@ -234,7 +234,7 @@ internal sealed class CellVisibility
public VisibilityResult? LastVisibilityResult { get; private set; }
///
- /// Stage 3 (2026-06-02): always — the FindCameraCell
+ /// Stage 3 (2026-06-02): always — the FindCameraCell
/// 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.
///
@@ -292,7 +292,7 @@ internal sealed class CellVisibility
///
/// Phase A8 (2026-05-28): enumerates the loaded cells that belong to a
/// landblock prefix. Used by LandblockRenderPublisher when building
- /// the per-landblock BuildingRegistry — the per-frame
+ /// the per-landblock BuildingRegistry — the per-frame
/// drainedCells dict misses cells loaded on prior frames, so the
/// stamping loop in needs access to
/// every cell currently in the landblock to ensure BuildingId is set.
@@ -356,15 +356,15 @@ internal sealed class CellVisibility
///
/// UCG W2/Stage 3: compute visibility from a supplied root cell (the physics membership
/// answer). When is null (pre-spawn, or player outside all indoor
- /// cells), returns null — the caller interprets null as the outdoor root (no portal
+ /// cells), returns null — the caller interprets null as the outdoor root (no portal
/// frame, everything slot 0, terrain ungated). The legacy AABB FindCameraCell fallback is
/// deleted as of Stage 3; is the sole authority.
/// 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.
///
///
/// The render-registered 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.
///
///
/// Used as the viewer position for the portal-side test in the BFS when root is non-null.
@@ -389,13 +389,13 @@ internal 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
- // 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).
// 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)
// still uses the brute-force AABB scan internally.
@@ -471,12 +471,12 @@ internal sealed class CellVisibility
///
/// 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 , or AABB
/// scan via for test compat).
///
/// The BFS body is byte-identical to the original GetVisibleCells
- /// implementation — only root acquisition was extracted out.
+ /// implementation — only root acquisition was extracted out.
///
private VisibilityResult? GetVisibleCellsFromRoot(LoadedCell cameraCell, Vector3 cameraPos)
{
@@ -499,7 +499,7 @@ internal sealed class CellVisibility
{
var portal = cell.Portals[i];
- // Exit portal → outdoor terrain should be visible.
+ // Exit portal → outdoor terrain should be visible.
if (portal.OtherCellId == 0xFFFF)
{
result.HasExitPortalVisible = true;
@@ -522,8 +522,8 @@ internal sealed class CellVisibility
var localCam = Vector3.Transform(cameraPos, cell.InverseWorldTransform);
float dot = Vector3.Dot(plane.Normal, localCam) + plane.D;
- // InsideSide == 0 → inside is positive half-space; reject if dot < -ε.
- // InsideSide == 1 → inside is negative 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 > ε.
// Source: ACME EnvCellManager.cs lines 1458-1459.
if (plane.InsideSide == 0 && dot < -PointInCellEpsilon)
continue;
diff --git a/src/AcDream.App/Rendering/ChaseCamera.cs b/src/AcDream.App/Rendering/ChaseCamera.cs
index b6b33c28..f58476f2 100644
--- a/src/AcDream.App/Rendering/ChaseCamera.cs
+++ b/src/AcDream.App/Rendering/ChaseCamera.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Numerics;
namespace AcDream.App.Rendering;
@@ -8,7 +8,7 @@ namespace AcDream.App.Rendering;
/// character. Implements so it plugs into the
/// existing renderer pipeline.
///
-internal sealed class ChaseCamera : ICamera
+public sealed class ChaseCamera : ICamera
{
public Vector3 Position { get; private set; }
public float Aspect { get; set; } = 16f / 9f;
@@ -37,9 +37,9 @@ internal sealed class ChaseCamera : ICamera
// (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;
// 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
- // 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 PitchMax = 1.4f;
@@ -47,7 +47,7 @@ internal sealed class ChaseCamera : ICamera
private Vector3 _lookAt;
// 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
// retail's "you can see yourself jump" feedback. Walking on the
// ground tracks Z directly (no lag on hill transitions); falling
@@ -90,10 +90,10 @@ internal sealed class ChaseCamera : ICamera
{
_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
- // 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.
_lookAt = playerPosition + new Vector3(0f, 0f, EyeHeight);
@@ -109,7 +109,7 @@ internal sealed class ChaseCamera : ICamera
Position = new Vector3(
playerPosition.X - forwardX * 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)
}
///
diff --git a/src/AcDream.App/Rendering/ClipFrame.cs b/src/AcDream.App/Rendering/ClipFrame.cs
index cd02c440..76174b22 100644
--- a/src/AcDream.App/Rendering/ClipFrame.cs
+++ b/src/AcDream.App/Rendering/ClipFrame.cs
@@ -1,20 +1,20 @@
-// ClipFrame.cs
+// ClipFrame.cs
//
// 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
// (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
// each renderer (WbDrawDispatcher / EnvCellRenderer), parallel to its instance
-// buffer — it is NOT here.
+// buffer — it is NOT here.
//
// === The contract (both shader sides obey) ===================================
// 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]; };
// 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:
// 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
// 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
/// each renderer, not here.
///
-internal sealed class ClipFrame : IDisposable
+public sealed class ClipFrame : IDisposable
{
// ---- Layout constants (mirror mesh_modern.vert + terrain_modern.vert) ----
- /// Max planes per clip region — matches the shader's planes[8]
+ /// Max planes per clip region — matches the shader's planes[8]
/// and GL's guaranteed GL_MAX_CLIP_DISTANCES >= 8.
public const int MaxPlanes = 8;
/// std430 stride of one CellClip: 16 (count + 3 pad uints) +
- /// 8 × 16 (vec4 planes) = 144 bytes.
+ /// 8 × 16 (vec4 planes) = 144 bytes.
public const int CellClipStrideBytes = 16 + MaxPlanes * 16; // 144
/// Byte offset of planes[0] within a CellClip (after the
@@ -57,7 +57,7 @@ internal sealed class ClipFrame : IDisposable
public const int CellClipPlanesOffset = 16;
/// 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.
public const int TerrainUboBytes = 16 + MaxPlanes * 16; // 144
@@ -66,7 +66,7 @@ internal sealed class ClipFrame : IDisposable
public const uint MeshClipSsboBinding = 2;
/// 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.
public const uint TerrainClipUboBinding = 2;
@@ -139,23 +139,23 @@ internal sealed class ClipFrame : IDisposable
///
/// 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.
///
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];
return new ClipFrame(bytes, slotCount: 1);
}
- /// Number of clip slots currently packed (always >= 1 — slot 0 is
+ /// Number of clip slots currently packed (always >= 1 — slot 0 is
/// the reserved no-clip slot).
public int SlotCount => _slotCount;
///
- /// 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
+ /// 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
/// frame or new GL buffers. The single long-lived _clipFrame in
/// GameWindow is reset + re-packed every frame by ,
/// then uploaded through one SSBO and one terrain arena per fenced frame slot.
@@ -209,7 +209,7 @@ internal sealed class ClipFrame : IDisposable
///
/// Append one clip region (becomes the next slot index) from a
/// . Only the convex-plane case is supported in
- /// U.3 — Count > 0 packs that many planes; Count == 0 packs a
+ /// U.3 — Count > 0 packs that many planes; Count == 0 packs a
/// no-clip region (pass-all). The scissor / nothing-visible fallbacks that
/// 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
@@ -259,7 +259,7 @@ internal sealed class ClipFrame : IDisposable
///
/// Set the terrain OutsideView clip region (the single region the terrain
/// shader gates against). length 0 ungates terrain
- /// (count 0). U.3 callers never touch this — leaves it
+ /// (count 0). U.3 callers never touch this — leaves it
/// at count 0. U.4 calls it with the OutsideView planes.
///
public void SetTerrainClip(ReadOnlySpan planes)
@@ -579,7 +579,7 @@ internal sealed class ClipFrame : IDisposable
}
/// A single std140 terrain-clip record within a frame-slot UBO arena.
-internal readonly record struct TerrainClipBufferBinding(
+public readonly record struct TerrainClipBufferBinding(
uint Buffer,
int OffsetBytes,
int SizeBytes)
diff --git a/src/AcDream.App/Rendering/ClipFrameAssembler.cs b/src/AcDream.App/Rendering/ClipFrameAssembler.cs
index 2a38bff1..38b72df4 100644
--- a/src/AcDream.App/Rendering/ClipFrameAssembler.cs
+++ b/src/AcDream.App/Rendering/ClipFrameAssembler.cs
@@ -1,4 +1,4 @@
-// ClipFrameAssembler.cs
+// ClipFrameAssembler.cs
//
// Retail PView assembly policy. PortalVisibilityBuilder produces a retail-like
// view graph: one portal_view list per visible cell plus an outside_view list.
@@ -21,7 +21,7 @@ namespace AcDream.App.Rendering;
///
/// How the landscape-through-outside_view pass should be interpreted.
///
-internal enum TerrainClipMode
+public enum TerrainClipMode
{
/// All outside_view slices have convex plane clips.
Planes,
@@ -37,13 +37,13 @@ internal enum TerrainClipMode
/// 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.
///
-internal readonly record struct ClipViewSlice(int Slot, Vector4 NdcAabb, Vector4[] Planes);
+public readonly record struct ClipViewSlice(int Slot, Vector4 NdcAabb, Vector4[] Planes);
///
/// Result of : populated clip buffers
/// plus routing data consumed by the render orchestration.
///
-internal sealed class ClipFrameAssembly
+public sealed class ClipFrameAssembly
{
public ClipFrame Frame { get; private set; } = null!;
@@ -241,7 +241,7 @@ internal sealed class ClipFrameAssembly
}
}
-internal static class ClipFrameAssembler
+public static class ClipFrameAssembler
{
public static ClipFrameAssembly Assemble(
ClipFrame frame,
diff --git a/src/AcDream.App/Rendering/ClipPlaneSet.cs b/src/AcDream.App/Rendering/ClipPlaneSet.cs
index 9a468fd0..dc4f0bd8 100644
--- a/src/AcDream.App/Rendering/ClipPlaneSet.cs
+++ b/src/AcDream.App/Rendering/ClipPlaneSet.cs
@@ -1,4 +1,4 @@
-// ClipPlaneSet.cs
+// ClipPlaneSet.cs
//
// 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
@@ -6,18 +6,18 @@
// convex plane set.
//
// 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
-// 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
-// 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) =====================
// 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
// 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
-// 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
// 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) =============
// Count == 0 can mean three different things; the consumer MUST distinguish:
-// (a) Empty — IsNothingVisible == true, UseScissorFallback == false.
-// The cell/region isn't visible at all → DRAW NOTHING. The
+// (a) Empty — IsNothingVisible == true, UseScissorFallback == false.
+// The cell/region isn't visible at all → DRAW NOTHING. The
// ScissorNdcAabb is a degenerate inverted box (min > max) so that a
// 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)
-// → 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,
// 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.
@@ -45,26 +45,26 @@ using System.Numerics;
namespace AcDream.App.Rendering;
///
-/// 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
/// Count==0 states.
///
-internal readonly struct ClipPlaneSet
+public readonly struct ClipPlaneSet
{
// Max simultaneous hardware clip planes we target (GL guarantees >= 8).
private const int MaxPlanes = 8;
// 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
- // ~1px screen-space dedup). |sin θ| for unit dirs = |cross|; sin(0.5°) ≈ 0.0087265.
+ // 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.
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.
private const float DegenerateEdgeLen = 1e-6f;
// 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.
private const float MinPolygonArea = 1e-7f;
private readonly Vector4[] _planes;
@@ -77,7 +77,7 @@ internal readonly struct ClipPlaneSet
ScissorNdcAabb = scissorNdcAabb;
}
- /// Number of active clip planes, 0..8. 0 ⇒ inspect
+ /// Number of active clip planes, 0..8. 0 ⇒ inspect
/// and to decide between "draw the AABB" and "draw nothing".
public int Count => _planes?.Length ?? 0;
@@ -89,11 +89,11 @@ internal readonly struct ClipPlaneSet
// its frame-scoped slice instead of cloning every plane payload a second time.
internal Vector4[] PlaneArray => _planes ?? Array.Empty();
- /// True ⇒ the convex-plane budget was exceeded; gate on
+ /// True ⇒ the convex-plane budget was exceeded; gate on
/// instead (draw the box). Always false when > 0 or when the region is empty.
public bool UseScissorFallback { get; }
- /// True ⇒ the region is not visible at all; the consumer draws NOTHING.
+ /// True ⇒ the region is not visible at all; the consumer draws NOTHING.
/// Mutually exclusive with , and only meaningful when Count == 0.
public bool IsNothingVisible { get; }
@@ -106,20 +106,20 @@ internal readonly struct ClipPlaneSet
public static ClipPlaneSet Empty { get; } =
new(Array.Empty(), 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);
///
- /// 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;
- /// empty/degenerate → . See the file header for the full rule.
+ /// 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;
+ /// empty/degenerate → . See the file header for the full rule.
///
public static ClipPlaneSet From(CellView region)
{
if (region is null || region.IsEmpty || region.Polygons.Count == 0)
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.
if (region.Polygons.Count > 1)
return Scissor(region.MinX, region.MinY, region.MaxX, region.MaxY);
@@ -152,15 +152,15 @@ internal readonly struct ClipPlaneSet
{
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.
if (count < 3)
return Empty;
ReadOnlySpan normalized = verts[..count];
- // 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).
+ // 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).
if (count > MaxPlanes)
return Scissor(normalized);
@@ -173,10 +173,10 @@ internal readonly struct ClipPlaneSet
Vector2 q = normalized[(i + 1) % count];
Vector2 dir = q - p;
// 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));
- // 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)
+ // 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)
planes[i] = new Vector4(n.X, n.Y, 0f, -Vector2.Dot(n, p));
}
return new ClipPlaneSet(planes, useScissorFallback: false, isNothingVisible: false, scissorNdcAabb: DegenerateAabb);
@@ -233,9 +233,9 @@ internal readonly struct ClipPlaneSet
if (SignedArea2(points[..count]) < 0f)
points[..count].Reverse();
- // 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
- // collinear triple. |cross(a,b)| of unit dirs = |sin θ|; dot>0 rules out a 180° reversal.
+ // 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
+ // collinear triple. |cross(a,b)| of unit dirs = |sin θ|; dot>0 rules out a 180° reversal.
bool changed = true;
while (changed && count >= 3)
{
@@ -260,12 +260,12 @@ internal readonly struct ClipPlaneSet
d0 /= l0;
d1 /= l1;
- float cross = d0.X * d1.Y - d0.Y * d1.X; // sin θ
- float dot = d0.X * d1.X + d0.Y * d1.Y; // cos θ
+ float cross = d0.X * d1.Y - d0.Y * d1.X; // sin θ
+ float dot = d0.X * d1.X + d0.Y * d1.Y; // cos θ
if (dot > 0f && MathF.Abs(cross) < CollinearSinEps)
{
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;
break;
}
@@ -276,7 +276,7 @@ internal readonly struct ClipPlaneSet
return 0;
// 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
// intersection that silently gates out everything; report it honestly as nothing-visible.
if (MathF.Abs(SignedArea2(points[..count])) * 0.5f < MinPolygonArea)
@@ -285,7 +285,7 @@ internal readonly struct ClipPlaneSet
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 poly)
{
float a = 0f;
diff --git a/src/AcDream.App/Rendering/CompositeTextureArrayCache.cs b/src/AcDream.App/Rendering/CompositeTextureArrayCache.cs
index 713577ac..cb9899a8 100644
--- a/src/AcDream.App/Rendering/CompositeTextureArrayCache.cs
+++ b/src/AcDream.App/Rendering/CompositeTextureArrayCache.cs
@@ -1,4 +1,4 @@
-using AcDream.Core.Textures;
+using AcDream.Core.Textures;
using AcDream.Core.World;
using Silk.NET.OpenGL;
@@ -8,7 +8,7 @@ namespace AcDream.App.Rendering;
/// Location of one decoded entity-material composite in a resident bindless
/// texture array. The modern mesh shader consumes this exact pair.
///
-internal readonly record struct BindlessTextureLocation(ulong Handle, uint Layer);
+public readonly record struct BindlessTextureLocation(ulong Handle, uint Layer);
internal enum CompositeTextureKind : byte
{
diff --git a/src/AcDream.App/Rendering/CreatureAppraisalPresentation.cs b/src/AcDream.App/Rendering/CreatureAppraisalPresentation.cs
index 7828a97f..1cecd9ef 100644
--- a/src/AcDream.App/Rendering/CreatureAppraisalPresentation.cs
+++ b/src/AcDream.App/Rendering/CreatureAppraisalPresentation.cs
@@ -114,18 +114,15 @@ internal sealed class RetailCreatureAppraisalFrameView :
private readonly UiViewport _viewport;
private readonly UiElement _windowFrame;
private readonly AppraisalUiController _controller;
- private readonly ExternalViewportTextureBridge _textureBridge;
public RetailCreatureAppraisalFrameView(
UiViewport viewport,
UiElement windowFrame,
- AppraisalUiController controller,
- IGpuDevice gpuDevice)
+ AppraisalUiController controller)
{
_viewport = viewport ?? throw new ArgumentNullException(nameof(viewport));
_windowFrame = windowFrame ?? throw new ArgumentNullException(nameof(windowFrame));
_controller = controller ?? throw new ArgumentNullException(nameof(controller));
- _textureBridge = new ExternalViewportTextureBridge(gpuDevice);
}
public bool TryGetVisibleTarget(
@@ -152,7 +149,7 @@ internal sealed class RetailCreatureAppraisalFrameView :
}
public void SetTextureHandle(uint textureHandle) =>
- _viewport.TextureHandle = _textureBridge.Resolve(textureHandle);
+ _viewport.TextureHandle = textureHandle;
private static bool IsEffectivelyVisible(UiElement element)
{
diff --git a/src/AcDream.App/Rendering/DebugLineRenderer.cs b/src/AcDream.App/Rendering/DebugLineRenderer.cs
index d84bb6e6..7f883a78 100644
--- a/src/AcDream.App/Rendering/DebugLineRenderer.cs
+++ b/src/AcDream.App/Rendering/DebugLineRenderer.cs
@@ -1,56 +1,94 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.Numerics;
using System.Runtime.InteropServices;
+using Silk.NET.OpenGL;
namespace AcDream.App.Rendering;
///
-/// Minimal debug line renderer for visualizing collision shapes,
+/// Minimal GL debug line renderer for visualizing collision shapes,
/// bounding boxes, and other debug geometry. Collect lines each frame
/// via / , then call
-/// to upload + draw them through the current
-/// .
+/// to upload + draw them.
///
-/// Campaign V slice V4a: ported onto . Owns one
-/// pipeline (LINE_LIST topology, depth disabled — lines must show through
-/// geometry, matching the prior explicit DepthTest 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.
+/// Uses a single shared VBO that's respecialized each frame. Vertex
+/// format is (vec3 pos, vec3 color) = 24 bytes per vertex.
///
-internal sealed class DebugLineRenderer : IDisposable
+public sealed unsafe class DebugLineRenderer : IDisposable
{
- private readonly IGpuDevice _device;
- private readonly IGpuPipeline _pipeline;
-
- private static readonly GpuVertexLayout VertexLayout = new(
- StrideBytes: 24,
- [
- new GpuVertexAttribute(0, GpuVertexFormat.Float3, 0),
- new GpuVertexAttribute(1, GpuVertexFormat.Float3, 12),
- ]);
+ private readonly GL _gl;
+ private readonly Shader _shader;
+ private readonly uint _vao;
+ private readonly uint _vbo;
+ private readonly ResourceCleanupGroup _resources;
private readonly List _buffer = new(4096);
private int _vertexCount;
+ private int _capacityBytes;
- public DebugLineRenderer(IGpuDevice device)
+ public DebugLineRenderer(GL gl, string shaderDir)
{
- _device = device ?? throw new ArgumentNullException(nameof(device));
- _pipeline = _device.CreatePipeline(new GpuPipelineDescription
+ _gl = gl ?? throw new ArgumentNullException(nameof(gl));
+ ArgumentException.ThrowIfNullOrWhiteSpace(shaderDir);
+ var resources = new ResourceCleanupGroup();
+ Shader? shader = null;
+ uint vao = 0;
+ uint vbo = 0;
+ try
{
- Name = "debug-line",
- Shaders = new GpuShaderSet("debug_line"),
- VertexLayout = VertexLayout,
- Topology = GpuPrimitiveTopology.LineList,
- Blend = GpuBlendMode.None,
- // Retail debug lines are drawn visible THROUGH geometry — the
- // prior GL path captured+disabled DepthTest around the draw and
- // restored whatever the caller had before. A dedicated pipeline
- // bakes "always visible" directly, which is simpler and exactly
- // as behaviour-preserving since nothing else shares this pipeline.
- Depth = GpuDepthState.Disabled,
- Cull = GpuCullMode.None,
- ColorWrite = true,
- });
+ shader = new Shader(gl,
+ Path.Combine(shaderDir, "debug_line.vert"),
+ Path.Combine(shaderDir, "debug_line.frag"));
+ resources.Add("debug-line shader", shader.Dispose);
+ vao = GlResourceCommand.CreateName(
+ gl,
+ "debug-line VAO",
+ gl.GenVertexArray,
+ gl.DeleteVertexArray);
+ uint ownedVao = vao;
+ resources.Add(
+ "debug-line VAO",
+ () => GlResourceCommand.DeleteVertexArray(
+ 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;
}
/// Clear accumulated lines. Call at the start of each frame.
@@ -131,40 +169,45 @@ internal sealed class DebugLineRenderer : IDisposable
AddLine(c[2], c[6], color); AddLine(c[3], c[7], color);
}
- /// Upload + draw all accumulated lines against the current frame.
- public void Flush(Matrix4x4 view, Matrix4x4 projection, IGpuFrame frame)
+ /// Upload + draw all accumulated lines.
+ public void Flush(Matrix4x4 view, Matrix4x4 projection)
{
if (_vertexCount == 0) return;
- ArgumentNullException.ThrowIfNull(frame);
- int byteCount = _buffer.Count * sizeof(float);
- GpuRingAllocation allocation = frame.AllocateRing(byteCount, GpuRingUsage.Vertex);
- CollectionsMarshal.AsSpan(_buffer).CopyTo(allocation.AsSpan());
+ _shader.Use();
+ _shader.SetMatrix4("uView", view);
+ _shader.SetMatrix4("uProjection", projection);
- using IGpuPassEncoder pass = frame.BeginPass(new GpuPassDescription
+ _gl.BindVertexArray(_vao);
+ _gl.BindBuffer(BufferTargetARB.ArrayBuffer, _vbo);
+
+ int neededBytes = _buffer.Count * sizeof(float);
+ if (neededBytes > _capacityBytes)
{
- Name = "debug-lines",
- Color = new GpuColorAttachment(
- Target: null,
- Load: GpuLoadOp.Load,
- Store: GpuStoreOp.Store,
- ClearColor: default),
- Depth = null,
- SampleCount = 1,
- });
- pass.BindPipeline(_pipeline);
- pass.BindVertexBuffer(allocation.Buffer, allocation.OffsetBytes);
- // Same combined-matrix convention every other ported shader uses
- // (WbDrawDispatcher, TerrainModernRenderer, ParticleRenderer):
- // C# multiplies view * projection once and uploads the single
- // uViewProjection the shader now declares, replacing the separate
- // uView/uProjection uniforms.
- pass.SetPushConstants(GpuPushConstants.Default with { ViewProjection = view * projection });
- pass.Draw((uint)_vertexCount, instanceCount: 1, firstVertex: 0, firstInstance: 0);
+ fixed (float* ptr = CollectionsMarshal.AsSpan(_buffer))
+ _gl.BufferData(BufferTargetARB.ArrayBuffer, (nuint)neededBytes, ptr, BufferUsageARB.DynamicDraw);
+ _capacityBytes = neededBytes;
+ }
+ else
+ {
+ fixed (float* ptr = CollectionsMarshal.AsSpan(_buffer))
+ _gl.BufferSubData(BufferTargetARB.ArrayBuffer, 0, (nuint)neededBytes, ptr);
+ }
+
+ // Depth test on so lines get occluded by geometry (but we want them
+ // visible through geometry — disable depth test so everything shows).
+ bool wasDepthEnabled = _gl.IsEnabled(EnableCap.DepthTest);
+ _gl.Disable(EnableCap.DepthTest);
+
+ _gl.DrawArrays(PrimitiveType.Lines, 0, (uint)_vertexCount);
+
+ if (wasDepthEnabled) _gl.Enable(EnableCap.DepthTest);
+
+ _gl.BindVertexArray(0);
}
public void Dispose()
{
- _pipeline.Dispose();
+ _resources.RetryCleanup();
}
}
diff --git a/src/AcDream.App/Rendering/DollCamera.cs b/src/AcDream.App/Rendering/DollCamera.cs
index 6c13f234..3a37cdb1 100644
--- a/src/AcDream.App/Rendering/DollCamera.cs
+++ b/src/AcDream.App/Rendering/DollCamera.cs
@@ -1,26 +1,26 @@
-using System;
+using System;
using System.Numerics;
namespace AcDream.App.Rendering;
///
-/// Fixed camera for the paperdoll mini-scene — retail-exact, ported from the gmPaperDollUI viewport
-/// setup (decomp 0x004a5a39–0x004a5a69). The viewport (element 0x100001d5) is configured by
+/// Fixed camera for the paperdoll mini-scene — retail-exact, ported from the gmPaperDollUI viewport
+/// setup (decomp 0x004a5a39–0x004a5a69). The viewport (element 0x100001d5) is configured by
/// UIElement_Viewport::SetCamera(position, direction) with:
///
-/// - position = (0.12, −2.4, 0.88) [hex 0x3df5c28f, 0xc019999a, 0x3f6147ae]
-/// - direction = (0, 0, 0) ⇒ CreatureMode::SetCameraDirection resets the view frame to
+///
- position = (0.12, −2.4, 0.88) [hex 0x3df5c28f, 0xc019999a, 0x3f6147ae]
+/// - direction = (0, 0, 0) ⇒ CreatureMode::SetCameraDirection resets the view frame to
/// IDENTITY (euler_set_rotate(0,0,0) then rotate(0,0,0)), so the camera looks
-/// straight down +Y with +Z up — NO yaw, NO pitch.
+/// straight down +Y with +Z up — NO yaw, NO pitch.
///
/// 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
-/// figure even though the model origin sits at the FEET (z=0). FOV π/4 is CreatureMode's default
+/// 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 CreatureMode's default
/// m_fFOVRadians (ctor 0x004543cf, hex 0x3f490fdb); default ambient is (0.3,0.3,0.3); the
/// paperdoll uses UseSharpMode (not SmartboxFOV) so Render::SetFOVRad(m_fFOVRadians) applies.
///
/// 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 +
/// FOV, not from aiming.
///
@@ -28,15 +28,15 @@ namespace AcDream.App.Rendering;
/// convention as so the doll's triangle winding + back-face culling match the
/// world render pass. AC up-axis = +Z.
///
-internal sealed class DollCamera : ICamera
+public 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);
- // 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 Up = Vector3.UnitZ; // AC up-axis = +Z, same as ChaseCamera
- /// Vertical field of view — retail CreatureMode default m_fFOVRadians = π/4 (45°).
+ /// Vertical field of view — retail CreatureMode default m_fFOVRadians = π/4 (45°).
public float FovRadians { get; set; } = MathF.PI / 4f;
public float Near { get; set; } = 0.1f; // same near plane as ChaseCamera / retail znear
diff --git a/src/AcDream.App/Rendering/DollEntityBuilder.cs b/src/AcDream.App/Rendering/DollEntityBuilder.cs
index eb562ba7..3d64ccb3 100644
--- a/src/AcDream.App/Rendering/DollEntityBuilder.cs
+++ b/src/AcDream.App/Rendering/DollEntityBuilder.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Numerics;
using AcDream.Core.World;
@@ -6,13 +6,13 @@ using AcDream.Core.World;
namespace AcDream.App.Rendering;
///
-/// Builds the dedicated paperdoll WorldEntity — retail's makeObject(player)
+/// Builds the dedicated paperdoll WorldEntity — retail's makeObject(player)
/// clone: the player's Setup id + current ObjDesc (base palette + subpalette overlays
/// + part overrides), posed at the scene origin facing the viewer.
///
///
/// The palette / part-override mapping mirrors the inline construction in
-/// GameWindow.cs around lines 3390–3431. Extracted here so it is
+/// GameWindow.cs around lines 3390–3431. Extracted here so it is
/// unit-testable without dats and so the paperdoll renderer owns a clean
/// seam: it calls with fresh player state each time the
/// ObjDesc changes, and the renderer only has to swap the entity into its
@@ -26,7 +26,7 @@ namespace AcDream.App.Rendering;
/// server-assigned guid.
///
///
-internal static class DollEntityBuilder
+public static class DollEntityBuilder
{
///
/// Reserved synthetic guid for the paperdoll clone. High, deliberately
@@ -38,18 +38,18 @@ internal static class DollEntityBuilder
/// 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
/// renderer passes this in animatedEntityIds 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
/// 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.
///
public const uint DollRenderId = 0xDA11_D012u;
- // 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
+ // 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
// 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
- // NEGATED (θ = −h). Using +h mirrors the X-lean (~22° off → the face reads as turned away from the viewer).
+ // +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).
private const float _headingDegrees = 191.367905f;
private static readonly float _headingRad = -_headingDegrees * (MathF.PI / 180f);
private static readonly Quaternion _dollRotation =
@@ -62,17 +62,17 @@ internal static class DollEntityBuilder
/// Pre-resolved mesh refs (may be empty; caller fills them in).
///
/// 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 SubPalettes.Count > 0.
///
///
/// Subpalette overlays from the server ObjDesc. Each tuple carries the
/// subpalette dat id, byte offset into the base palette, and byte length.
- /// Null or empty → PaletteOverride on the returned entity is null.
+ /// Null or empty → PaletteOverride on the returned entity is null.
///
///
/// 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.
///
public static WorldEntity Build(
uint setupId,
@@ -82,7 +82,7 @@ internal static class DollEntityBuilder
IReadOnlyList<(byte PartIndex, uint GfxObjId)>? partOverrides = null)
{
// --- 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;
if (subPalettes is { Count: > 0 } spList)
{
diff --git a/src/AcDream.App/Rendering/EquippedChildRenderController.cs b/src/AcDream.App/Rendering/EquippedChildRenderController.cs
index efb1b568..e408afa8 100644
--- a/src/AcDream.App/Rendering/EquippedChildRenderController.cs
+++ b/src/AcDream.App/Rendering/EquippedChildRenderController.cs
@@ -1,4 +1,4 @@
-using System.Numerics;
+using System.Numerics;
using AcDream.App.World;
using AcDream.App.Rendering.Vfx;
using AcDream.Core.Items;
@@ -23,7 +23,7 @@ namespace AcDream.App.Rendering;
/// and recomposes it after the parent's animation
/// advances each frame.
///
-internal sealed class EquippedChildRenderController : IDisposable
+public sealed class EquippedChildRenderController : IDisposable
{
private readonly IDatReaderWriter _dats;
private readonly object _datLock;
@@ -1674,7 +1674,7 @@ internal sealed class EquippedChildRenderController : IDisposable
"has no exact projection key.");
}
-internal enum ChildUnparentDisposition
+public enum ChildUnparentDisposition
{
NotAttached,
Completed,
diff --git a/src/AcDream.App/Rendering/ExternalViewportTextureBridge.cs b/src/AcDream.App/Rendering/ExternalViewportTextureBridge.cs
deleted file mode 100644
index 1be7e29c..00000000
--- a/src/AcDream.App/Rendering/ExternalViewportTextureBridge.cs
+++ /dev/null
@@ -1,73 +0,0 @@
-namespace AcDream.App.Rendering;
-
-///
-/// Campaign V slice V4a bridge: memoizes a for the
-/// most recent raw GL colour-texture name a still-unmigrated private-viewport
-/// renderer (,
-/// ) produced, so
-/// UiViewport.TextureHandle — now a — has
-/// something to draw. Those renderers still allocate their FBO colour
-/// attachment directly on GL (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
-/// directly.
-///
-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);
- }
-
- ///
- /// Resolves (0 = nothing rendered
- /// this call, matching the producers' existing "0 = no texture" return)
- /// to a texture-table slot, registering it once per distinct name.
- ///
- 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;
- }
-}
diff --git a/src/AcDream.App/Rendering/FlyCamera.cs b/src/AcDream.App/Rendering/FlyCamera.cs
index 39cb0f76..49a5341b 100644
--- a/src/AcDream.App/Rendering/FlyCamera.cs
+++ b/src/AcDream.App/Rendering/FlyCamera.cs
@@ -1,9 +1,9 @@
-// src/AcDream.App/Rendering/FlyCamera.cs
+// src/AcDream.App/Rendering/FlyCamera.cs
using System.Numerics;
namespace AcDream.App.Rendering;
-internal sealed class FlyCamera : ICamera
+public sealed class FlyCamera : ICamera
{
public Vector3 Position { get; set; } = new(96, 96, 150);
public float Yaw { get; set; } = MathF.PI / 2f; // facing +Y
diff --git a/src/AcDream.App/Rendering/FrustumCuller.cs b/src/AcDream.App/Rendering/FrustumCuller.cs
index 39cbef0b..3c792bbb 100644
--- a/src/AcDream.App/Rendering/FrustumCuller.cs
+++ b/src/AcDream.App/Rendering/FrustumCuller.cs
@@ -1,13 +1,13 @@
-using System.Numerics;
+using System.Numerics;
namespace AcDream.App.Rendering;
///
-/// 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
/// dot(normal, point) + distance >= 0 means the point is on the visible side.
///
-internal readonly struct FrustumPlanes
+public readonly struct FrustumPlanes
{
public readonly Vector4 Left;
public readonly Vector4 Right;
@@ -27,7 +27,7 @@ internal readonly struct FrustumPlanes
}
///
- /// 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,
/// so rows are accessed directly via M{row}{col} fields.
///
@@ -64,7 +64,7 @@ internal readonly struct FrustumPlanes
///
/// Conservative AABB-vs-frustum culling. Zero allocations; suitable for per-frame use.
///
-internal static class FrustumCuller
+public static class FrustumCuller
{
///
/// Returns true if the axis-aligned bounding box defined by
@@ -74,7 +74,7 @@ internal static class FrustumCuller
///
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
// that corner is behind the plane, the entire box is outside.
return TestPlane(planes.Left, min, max)
diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs
index a27f71fe..a638ba48 100644
--- a/src/AcDream.App/Rendering/GameWindow.cs
+++ b/src/AcDream.App/Rendering/GameWindow.cs
@@ -1,4 +1,4 @@
-using AcDream.Core.Plugins;
+using AcDream.Core.Plugins;
using AcDream.App.Composition;
using AcDream.App.Physics;
using AcDream.App.Rendering.Gpu;
@@ -21,7 +21,7 @@ using Silk.NET.Windowing;
namespace AcDream.App.Rendering;
-internal sealed class GameWindow :
+public sealed class GameWindow :
IDisposable,
IGameWindowPlatformPublication,
IGameWindowHostInputCameraPublication,
@@ -70,7 +70,7 @@ internal sealed class GameWindow :
private AcDream.App.Interaction.WorldSelectionQuery? _worldSelectionQuery;
private AcDream.App.Interaction.SelectionInteractionController? _selectionInteractions;
/// Phase N.5: ARB_bindless_texture + ARB_shader_draw_parameters
- /// support. Required at startup — missing bindless throws
+ /// support. Required at startup — missing bindless throws
/// in OnLoad.
private AcDream.App.Rendering.Wb.BindlessSupport? _bindlessSupport;
private SamplerCache? _samplerCache;
@@ -78,14 +78,14 @@ internal sealed class GameWindow :
// K-fix4 (2026-04-26): default OFF. The orange BSP / green cylinder
// wireframes are noisy outdoors and confuse first-time users into
// 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
_worldSceneDebugState = new();
// Phase I.2: the old StbTrueTypeSharp DebugOverlay was deleted in
// favor of the ImGui-backed DebugPanel (see _debugVm below). The
// 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
// scene. They are no longer used for any debug overlay.
private TextRenderer? _textRenderer;
@@ -98,7 +98,7 @@ internal sealed class GameWindow :
"1",
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
// AcDream.App.Diagnostics.FrameProfiler (structure rule 1).
private readonly AcDream.App.Diagnostics.FrameProfiler _frameProfiler = new();
@@ -116,7 +116,6 @@ internal sealed class GameWindow :
private IDisposable? _frameGraphPublication;
private AcDream.App.Rendering.GpuFrameFlightController? _gpuFrameFlights;
private IGpuDevice? _gpuDevice;
- private AcDream.App.Rendering.GpuDeviceFrameLifetime? _gpuFrameLifetime;
private readonly AcDream.App.Rendering.GameFrameGraphSlot _frameGraphs = new();
private readonly AcDream.App.Rendering.GameRenderResourceLifetime
_renderResourceLifetime = new();
@@ -145,11 +144,11 @@ internal sealed class GameWindow :
_localPlayerTeleport;
private readonly AcDream.App.Rendering.WorldRenderRangeState _renderRange =
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 =>
_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;
// ConcurrentDictionary inside makes cross-thread access safe.
private AcDream.Core.Physics.PhysicsDataCache _physicsDataCache =>
@@ -186,7 +185,7 @@ internal sealed class GameWindow :
private readonly CellVisibility _cellVisibility = new();
// 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 calls from the streaming worker thread (T11+) and
// the render thread (LandblockBuildFactory on the worker; live-spawn
// handlers + animation ticks on the render
@@ -227,7 +226,7 @@ internal sealed class GameWindow :
// 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
- // 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
// 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.
@@ -249,7 +248,7 @@ internal sealed class GameWindow :
///
/// Tier 1 cache (#53): per-entity classification results for static
/// entities (those NOT in ). Conceptually
- /// paired with — that dictionary is the
+ /// paired with — that dictionary is the
/// gating predicate, this cache is the lookup that depends on it.
/// Passed to at
/// construction time. Tasks 9-10 of the cache plan wire the per-entity
@@ -284,7 +283,7 @@ internal sealed class GameWindow :
private readonly AcDream.App.Rendering.Vfx.ParticleVisibilityController _particleVisibility = new();
private readonly AcDream.App.Rendering.Vfx.EntityEffectPoseRegistry _effectPoses = new();
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
// fans every dat-defined hook to particles, audio, lights, translucency,
// and nested/default-script routing at its StartTime offset.
@@ -295,7 +294,7 @@ internal sealed class GameWindow :
private readonly AcDream.App.Rendering.RetailAlphaQueue _retailAlphaQueue;
// Remote-entity motion inference: tracks when each remote entity last
// moved meaningfully. Used in TickAnimations to swap to Ready when
- // position has stalled for >StopIdleMs — retail observer pattern per
+ // position has stalled for >StopIdleMs — retail observer pattern per
// ACE Player_Tick.cs line 368: the client never sends "released forward"
// MoveToState, so the server never broadcasts an explicit stop. Observer
// must infer it from position deltas.
@@ -363,21 +362,21 @@ internal sealed class GameWindow :
/// Persisted hotbar shortcuts from the last PlayerDescription (D.5.1 toolbar source).
public IReadOnlyList Shortcuts =>
_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.
- // 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.
public AcDream.Core.Player.LocalPlayerState 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.
private AcDream.App.Rendering.DevToolsFramePresenter? _devToolsFramePresenter;
private AcDream.App.Rendering.DevToolsCommandBusSource? _devToolsCommandBus;
private DevToolsCompositionOwner? _devToolsComposition;
private AcDream.UI.Abstractions.Panels.Vitals.VitalsVM? _vitalsVm;
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.RetailUiRuntime? _retailUiRuntime;
private readonly AcDream.App.UI.RetailUiRuntimeLease _retailUiLease = new();
@@ -399,7 +398,7 @@ internal sealed class GameWindow :
private AcDream.App.Spells.MagicRuntime? _magicRuntime;
private MagicCatalog? _magicCatalog;
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).
private AcDream.App.Rendering.PaperdollViewportRenderer? _paperdollViewportRenderer;
private AcDream.App.Rendering.PaperdollFramePresenter? _paperdollFramePresenter;
@@ -407,7 +406,7 @@ internal sealed class GameWindow :
_creatureAppraisalViewportRenderer;
private AcDream.App.Rendering.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;
// Phase I.2: ImGui debug panel ViewModel. The devtools presenter owns
// its panel; the VM remains here because runtime feedback producers bind
@@ -428,7 +427,7 @@ internal sealed class GameWindow :
private AcDream.App.Rendering.Vfx.LiveEntityLightController? _liveEntityLights;
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
// per frame. Wired into the hook router in OnLoad, advanced once per
// frame in the main loop regardless of _animatedEntities membership
@@ -473,10 +472,10 @@ internal sealed class GameWindow :
_localPlayerAnimation;
private AcDream.App.Physics.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.
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
// OnUpdate; disarmed if the user manually enters fly mode (or any
// other path that pre-empts the chase camera). Skipped entirely
@@ -485,7 +484,7 @@ internal sealed class GameWindow :
// the bool here.
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
// InputDispatcher. Raw axis motion belongs to CameraPointerInputController.
private AcDream.App.Input.SilkKeyboardSource? _kbSource;
@@ -507,7 +506,7 @@ internal sealed class GameWindow :
// configuration path,
// 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
- // startup — no other call to RetailDefaults() / AcdreamCurrentDefaults()
+ // startup — no other call to RetailDefaults() / AcdreamCurrentDefaults()
// should land in the GameWindow construction path.
private readonly AcDream.UI.Abstractions.Input.KeyBindings _keyBindings;
private readonly GraphicalHostPlatformServices _platformServices;
@@ -523,7 +522,7 @@ internal sealed class GameWindow :
}
// 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.
// Runtime owns the canonical session generation and transport lifetime.
// The window retains only the composition handles needed for startup and
@@ -539,7 +538,7 @@ internal sealed class GameWindow :
// ACDREAM_LIVE=1 was set when the window came up.
// Backed by RuntimeOptions.LiveMode via the _options field.
///
- /// 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
/// server is talking about. This is the canonical materialized top-level
/// projection, including live objects parked in pending landblocks;
@@ -558,7 +557,7 @@ internal sealed class GameWindow :
///
private IReadOnlyDictionary LastSpawns =>
_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
// entities chasing the player resolve it. Replaces the AP-79
// _playerMoveToTarget* poll fields.
@@ -567,14 +566,14 @@ internal sealed class GameWindow :
private EntityPhysicsHost? _playerHost
=> _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,
// giving the TargetManager voyeur round-trip its cross-entity delivery
// path. Populated for remotes plus the PlayerModeController local entry
// (player); pruned only by logical LiveEntityRuntime teardown.
// 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(
AcDream.App.RuntimeOptions options,
WorldGameState worldGameState,
@@ -685,7 +684,7 @@ internal sealed class GameWindow :
var options = WindowOptions.Default with
{
Size = new Vector2D(1280, 720),
- Title = "acdream — phase 1",
+ Title = "acdream — phase 1",
API = new GraphicsAPI(
ContextAPI.OpenGL,
ContextProfile.Core,
@@ -749,10 +748,6 @@ internal sealed class GameWindow :
IGpuDevice value) =>
PublishCompositionOwner(ref _gpuDevice, value, "GPU device (RHI)");
- void IGameWindowHostInputCameraPublication.PublishGpuFrameLifetime(
- GpuDeviceFrameLifetime value) =>
- PublishCompositionOwner(ref _gpuFrameLifetime, value, "GPU frame lifetime");
-
void IGameWindowHostInputCameraPublication.PublishKeyboardSource(
AcDream.App.Input.SilkKeyboardSource value) =>
PublishCompositionOwner(ref _kbSource, value, "keyboard source");
@@ -1308,7 +1303,6 @@ internal sealed class GameWindow :
_worldEnvironment,
_renderResourceLifetime,
_gpuFrameFlights!,
- _gpuDevice!,
_options.ResidencyBudgets,
initialCenterLandblockId,
_applicationPaths.DiagnosticsDirectory,
@@ -1328,10 +1322,9 @@ internal sealed class GameWindow :
new InteractionRetainedUiDependencies(
_options,
platformResult.Graphics,
- hostInputCamera.GpuDevice,
- () => hostInputCamera.GpuFrameLifetime.Current,
_window!,
platformResult.Input,
+ worldRender.Foundation.ShadersDirectory,
contentEffectsAudio.Dats,
_datLock,
worldRender.Foundation.TextureCache,
@@ -1381,7 +1374,6 @@ internal sealed class GameWindow :
new LivePresentationDependencies(
_options,
platformResult.Graphics,
- _gpuDevice!,
_window!,
_datLock,
_runtimeSettings,
@@ -1549,7 +1541,7 @@ internal sealed class GameWindow :
_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).
private void OnRender(double deltaSeconds)
{
@@ -1565,20 +1557,20 @@ internal sealed class GameWindow :
// IsEntityCurrentlyMoving REMOVED (2026-07-09): it powered a cache-bypass
// 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
- // 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.
- // 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
// remotes use (edge-driven DoMotion/StopMotion/set_hold_run in
// PlayerMovementController; airborne-Falling falls out of
// 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
// always played (w6-cutover-map.md R3).
///
- /// 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
/// the corner OR by the runtime display target applying a saved
/// Resolution). Without this, the viewport stays pinned at the
diff --git a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuDevice.cs b/src/AcDream.App/Rendering/Gpu/Gl/GlGpuDevice.cs
index 3a681b41..b416b597 100644
--- a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuDevice.cs
+++ b/src/AcDream.App/Rendering/Gpu/Gl/GlGpuDevice.cs
@@ -190,26 +190,9 @@ internal sealed class GlGpuDevice : IGpuDevice
string fragmentPath = Path.Combine(_shadersDirectory, $"{description.Shaders.Name}.frag");
string vertexSource = File.ReadAllText(vertexPath);
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);
}
- private string? _commonPreambleSource;
-
- private string CommonPreambleSource => _commonPreambleSource ??=
- File.ReadAllText(Path.Combine(_shadersDirectory, "common.glsl"));
-
public IGpuRenderTarget CreateRenderTarget(in GpuRenderTargetDescription description)
{
ThrowIfDisposed();
@@ -232,33 +215,6 @@ internal sealed class GlGpuDevice : IGpuDevice
return new GpuTextureSlot(slot);
}
- ///
- /// Registers an externally-created, externally-owned GL texture name
- /// (a private-viewport FBO colour attachment) into the same texture table
- /// uses. Not part of
- /// — PrivateEntityViewportRenderer/PaperdollViewportRenderer
- /// still allocate their own FBO textures directly on GL pre-V4g, so
- /// their retained-UI consumers () need a bridge
- /// that does not require the caller to own an .
- /// 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
- /// and they can call
- /// like every other consumer.
- ///
- 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)
{
ThrowIfDisposed();
@@ -283,17 +239,6 @@ internal sealed class GlGpuDevice : IGpuDevice
long serial = ++_nextSerial;
int slot = _frameFlights.CurrentSlot;
_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);
}
@@ -338,17 +283,6 @@ internal sealed class GlGpuDevice : IGpuDevice
_textureHandleTable.AsSpan(tableStart, tableEnd - tableStart));
_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)
@@ -461,24 +395,6 @@ internal sealed class GlGpuDevice : IGpuDevice
{
_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");
}
diff --git a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuPassEncoder.cs b/src/AcDream.App/Rendering/Gpu/Gl/GlGpuPassEncoder.cs
index 819f8e6d..f3ac77c8 100644
--- a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuPassEncoder.cs
+++ b/src/AcDream.App/Rendering/Gpu/Gl/GlGpuPassEncoder.cs
@@ -51,8 +51,7 @@ internal sealed class GlGpuPassEncoder : IGpuPassEncoder
description.Cull,
description.FrontFace,
description.AlphaToCoverage,
- description.ColorWrite,
- Multisample: description.SampleCount > 1);
+ description.ColorWrite);
_device.ApplyRenderState(desired);
_gl.BindVertexArray(p.GlVertexArray);
diff --git a/src/AcDream.App/Rendering/Gpu/Gl/GlRenderStateCache.cs b/src/AcDream.App/Rendering/Gpu/Gl/GlRenderStateCache.cs
index 5414a9a5..846f8d35 100644
--- a/src/AcDream.App/Rendering/Gpu/Gl/GlRenderStateCache.cs
+++ b/src/AcDream.App/Rendering/Gpu/Gl/GlRenderStateCache.cs
@@ -15,8 +15,7 @@ internal readonly record struct GlRenderStateSnapshot(
GpuCullMode Cull,
GpuFrontFace FrontFace,
bool AlphaToCoverage,
- bool ColorWrite,
- bool Multisample);
+ bool ColorWrite);
/// Which GL state calls are needed to move from the previous snapshot to the new one.
internal readonly record struct GlRenderStateChanges(
@@ -28,15 +27,14 @@ internal readonly record struct GlRenderStateChanges(
bool Cull,
bool FrontFace,
bool AlphaToCoverage,
- bool ColorWrite,
- bool Multisample)
+ bool ColorWrite)
{
public bool AnyChange =>
Program || Blend || DepthTest || DepthWrite || DepthCompare
- || Cull || FrontFace || AlphaToCoverage || ColorWrite || Multisample;
+ || Cull || FrontFace || AlphaToCoverage || ColorWrite;
/// Every dimension reported changed — used for the first apply after a reset.
- internal static GlRenderStateChanges All { get; } = new(true, true, true, true, true, true, true, true, true, true);
+ internal static GlRenderStateChanges All { get; } = new(true, true, true, true, true, true, true, true, true);
}
///
@@ -70,8 +68,7 @@ internal sealed class GlRenderStateCache
p.Cull != desired.Cull,
p.FrontFace != desired.FrontFace,
p.AlphaToCoverage != desired.AlphaToCoverage,
- p.ColorWrite != desired.ColorWrite,
- p.Multisample != desired.Multisample);
+ p.ColorWrite != desired.ColorWrite);
}
/// Discards the cached baseline — the next reports every dimension changed.
diff --git a/src/AcDream.App/Rendering/ICamera.cs b/src/AcDream.App/Rendering/ICamera.cs
index 16c3a237..3aeaf987 100644
--- a/src/AcDream.App/Rendering/ICamera.cs
+++ b/src/AcDream.App/Rendering/ICamera.cs
@@ -1,8 +1,8 @@
-using System.Numerics;
+using System.Numerics;
namespace AcDream.App.Rendering;
-internal interface ICamera
+public interface ICamera
{
Matrix4x4 View { get; }
Matrix4x4 Projection { get; }
diff --git a/src/AcDream.App/Rendering/ICameraCollisionProbe.cs b/src/AcDream.App/Rendering/ICameraCollisionProbe.cs
index de609d66..2ad67136 100644
--- a/src/AcDream.App/Rendering/ICameraCollisionProbe.cs
+++ b/src/AcDream.App/Rendering/ICameraCollisionProbe.cs
@@ -1,14 +1,14 @@
-using System.Numerics;
+using System.Numerics;
namespace AcDream.App.Rendering;
///
/// Result of a camera spring-arm sweep: the collided eye position AND the cell the swept
/// viewer-sphere ended in (retail viewer_cell = sphere_path.curr_cell, 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.
///
-internal readonly record struct CameraSweepResult(Vector3 Eye, uint ViewerCellId);
+public readonly record struct CameraSweepResult(Vector3 Eye, uint ViewerCellId);
///
/// Sweeps a small sphere from the camera pivot (player head) toward the
@@ -16,7 +16,7 @@ internal readonly record struct CameraSweepResult(Vector3 Eye, uint ViewerCellId
/// lets collide its eye without depending on
/// the physics engine directly (and stay unit-testable with a fake).
///
-internal interface ICameraCollisionProbe
+public interface ICameraCollisionProbe
{
///
/// Roll a collision sphere from to
diff --git a/src/AcDream.App/Rendering/IndoorDrawPlan.cs b/src/AcDream.App/Rendering/IndoorDrawPlan.cs
index 6696f09a..a3aab2a2 100644
--- a/src/AcDream.App/Rendering/IndoorDrawPlan.cs
+++ b/src/AcDream.App/Rendering/IndoorDrawPlan.cs
@@ -1,18 +1,18 @@
-// IndoorDrawPlan.cs
+// IndoorDrawPlan.cs
//
// 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
-// 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).
+// 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).
using System.Collections.Generic;
namespace AcDream.App.Rendering;
-internal readonly record struct CellDrawEntry(uint CellId, IReadOnlyList Slices);
+public readonly record struct CellDrawEntry(uint CellId, IReadOnlyList Slices);
-internal static class IndoorDrawPlan
+public static class IndoorDrawPlan
{
- /// Reverse OrderedVisibleCells (far→near), each visible cell with its view
+ /// Reverse OrderedVisibleCells (far→near), each visible cell with its view
/// slices. Mirrors DrawCells' shell/object loops. Cells whose view is empty are skipped
/// (they are not actually visible); no other cell is ever dropped.
public static List ShellPass(PortalVisibilityFrame frame)
diff --git a/src/AcDream.App/Rendering/InteriorEntityPartition.cs b/src/AcDream.App/Rendering/InteriorEntityPartition.cs
index ad57505e..08da6b15 100644
--- a/src/AcDream.App/Rendering/InteriorEntityPartition.cs
+++ b/src/AcDream.App/Rendering/InteriorEntityPartition.cs
@@ -1,4 +1,4 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.Numerics;
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
/// retail-style DrawInside flood.
///
-/// T1 (fused BR-2/3, 2026-06-11) — retail draw-order contract: the
+/// T1 (fused BR-2/3, 2026-06-11) — retail draw-order contract: the
/// frame draws STATIC world first (terrain, building shells, scenery, then
/// flooded interior cells + their static object lists), and every DYNAMIC
/// (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
/// yet, so nothing visible is destroyed (retail: objects draw per cell AFTER
/// cells, PView::DrawCells epilogue Ghidra 0x005a4840; the first BR-2 attempt
/// punched after dynamics and erased the player, reverted 88be519).
///
///
-/// - — indoor STATICS (dat-baked, ServerGuid==0)
+///
- — indoor STATICS (dat-baked, ServerGuid==0)
/// per visible cell, drawn with their cell.
-/// - — outdoor statics (building
+///
- — outdoor statics (building
/// shells, scenery stabs), drawn with the world/landscape pass.
-/// - — ALL server-spawned entities
+///
- — ALL server-spawned entities
/// (ServerGuid != 0) regardless of cell, plus unresolved-cell live entities;
/// drawn in the frame's single LAST entity pass.
///
///
-internal static class InteriorEntityPartition
+public static class InteriorEntityPartition
{
internal enum ProjectionClass : byte
{
@@ -51,13 +51,13 @@ internal static class InteriorEntityPartition
void AbortFrame();
}
- internal sealed class Result
+ public sealed class Result
{
public Dictionary> ByCell { get; } = new();
public List OutdoorStatic { get; } = new();
public List 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.
private readonly List _emptyCellScratch = new();
@@ -82,7 +82,7 @@ internal static class InteriorEntityPartition
/// entries (either newly emptied, or a leftover key from a previous
/// frame's visible-cell set that this frame never touched). Keeps
/// 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
/// actually received at least one static this frame.
///
@@ -100,7 +100,7 @@ internal static class InteriorEntityPartition
}
///
- /// Allocating overload — always returns a brand-new .
+ /// Allocating overload — always returns a brand-new .
/// Kept for tests and any one-shot caller; the per-frame render path
/// uses the
/// reuse overload instead (see ).
@@ -121,7 +121,7 @@ internal static class InteriorEntityPartition
/// in place (see ) and refills it,
/// reusing each cell's existing List<WorldEntity> when the
/// 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
/// frame. Identical partitioning output to the allocating overload; only
/// the backing storage is reused.
@@ -141,7 +141,7 @@ internal static class InteriorEntityPartition
if (e.MeshRefs.Count == 0) continue;
// 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)
{
result.Dynamics.Add(e);
@@ -237,7 +237,7 @@ internal static class InteriorEntityPartition
}
}
- /// Shared indoor classification — keep DrawDynamicsLast, the
+ /// Shared indoor classification — keep DrawDynamicsLast, the
/// outside-stage assignment (#118), and the partition in lockstep.
public static bool IsIndoorCellId(uint cellId)
{
diff --git a/src/AcDream.App/Rendering/NdcScissorRect.cs b/src/AcDream.App/Rendering/NdcScissorRect.cs
index f764335a..f26eb0c6 100644
--- a/src/AcDream.App/Rendering/NdcScissorRect.cs
+++ b/src/AcDream.App/Rendering/NdcScissorRect.cs
@@ -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 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
-// 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
-// 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
// 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.
// 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 ⇒
-// i ≥ X0−0.5 ⇒ i ≥ floor(X0) and i ≤ X1−0.5 ⇒ i < ceil(X1). So
+// 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
// [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).
using System;
using System.Numerics;
namespace AcDream.App.Rendering;
-internal static class NdcScissorRect
+public static class NdcScissorRect
{
/// Convert an NDC AABB (minX, minY, maxX, maxY in [-1,1]) to a
/// framebuffer-pixel scissor box that CONTAINS it. Inputs are clamped to
diff --git a/src/AcDream.App/Rendering/OrbitCamera.cs b/src/AcDream.App/Rendering/OrbitCamera.cs
index 1316222b..358dc9c2 100644
--- a/src/AcDream.App/Rendering/OrbitCamera.cs
+++ b/src/AcDream.App/Rendering/OrbitCamera.cs
@@ -2,7 +2,7 @@
namespace AcDream.App.Rendering;
-internal sealed class OrbitCamera : ICamera
+public sealed class OrbitCamera : ICamera
{
public Vector3 Target { get; set; } = new(96, 96, 0); // center of a 192x192 landblock
public float Distance { get; set; } = 300f;
diff --git a/src/AcDream.App/Rendering/OutdoorCellNode.cs b/src/AcDream.App/Rendering/OutdoorCellNode.cs
index 7bbec56c..13ae4d0d 100644
--- a/src/AcDream.App/Rendering/OutdoorCellNode.cs
+++ b/src/AcDream.App/Rendering/OutdoorCellNode.cs
@@ -1,24 +1,24 @@
-using System.Numerics;
+using System.Numerics;
namespace AcDream.App.Rendering;
///
-/// 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 viewer_cell (SmartBox::RenderNormalMode →
+/// 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 viewer_cell (SmartBox::RenderNormalMode →
/// DrawInside(viewer_cell), decomp:92635); when outdoors that is a CLandCell. acdream models it
/// as a portal-less carrying only (so
-/// seeds OutsideView FULL-SCREEN → terrain/sky/scenery draw
+/// seeds OutsideView FULL-SCREEN → terrain/sky/scenery draw
/// as the root's shell) and .
///
/// 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
-/// landscape draw (terrain BSP → DrawPortal → ConstructView(CBldPortal), decomp:326881/433895/433827).
+/// 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).
/// acdream issues those via per nearby
/// building inside . The pre-R-A2 design flooded all
/// 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.
+/// root-level portal-side test that oscillated as the chase eye grazed a doorway — the indoor flap.
///
-internal static class OutdoorCellNode
+public static class OutdoorCellNode
{
public static LoadedCell Build(uint outdoorCellId) => new LoadedCell
{
diff --git a/src/AcDream.App/Rendering/PaperdollFramePresenter.cs b/src/AcDream.App/Rendering/PaperdollFramePresenter.cs
index 4ddfcb0e..33883803 100644
--- a/src/AcDream.App/Rendering/PaperdollFramePresenter.cs
+++ b/src/AcDream.App/Rendering/PaperdollFramePresenter.cs
@@ -191,16 +191,13 @@ internal sealed class RetailPaperdollFrameView : IPaperdollFrameView
{
private readonly UiViewport _viewport;
private readonly IPaperdollInventoryVisibility _inventory;
- private readonly ExternalViewportTextureBridge _textureBridge;
public RetailPaperdollFrameView(
UiViewport viewport,
- IPaperdollInventoryVisibility inventory,
- IGpuDevice gpuDevice)
+ IPaperdollInventoryVisibility inventory)
{
_viewport = viewport ?? throw new ArgumentNullException(nameof(viewport));
_inventory = inventory ?? throw new ArgumentNullException(nameof(inventory));
- _textureBridge = new ExternalViewportTextureBridge(gpuDevice);
}
public bool TryGetVisibleSize(out int width, out int height)
@@ -218,7 +215,7 @@ internal sealed class RetailPaperdollFrameView : IPaperdollFrameView
}
public void SetTextureHandle(uint textureHandle) =>
- _viewport.TextureHandle = _textureBridge.Resolve(textureHandle);
+ _viewport.TextureHandle = textureHandle;
}
/// Narrow visibility adapter for the paperdoll's inventory host.
diff --git a/src/AcDream.App/Rendering/PaperdollViewportRenderer.cs b/src/AcDream.App/Rendering/PaperdollViewportRenderer.cs
index f0f168e3..b01395d3 100644
--- a/src/AcDream.App/Rendering/PaperdollViewportRenderer.cs
+++ b/src/AcDream.App/Rendering/PaperdollViewportRenderer.cs
@@ -1,4 +1,4 @@
-using AcDream.App.Rendering.Wb;
+using AcDream.App.Rendering.Wb;
using AcDream.App.UI;
using AcDream.Core.Lighting;
using AcDream.Core.World;
@@ -10,7 +10,7 @@ namespace AcDream.App.Rendering;
/// Paperdoll-specific facade over the shared private creature viewport. The
/// fixed camera remains the verbatim retail gmPaperDollUI camera.
///
-internal sealed class PaperdollViewportRenderer :
+public sealed class PaperdollViewportRenderer :
IUiViewportRenderer,
IPaperdollDollRenderer,
IDisposable
diff --git a/src/AcDream.App/Rendering/ParticleRenderer.cs b/src/AcDream.App/Rendering/ParticleRenderer.cs
index ecfa0ca8..81657d9a 100644
--- a/src/AcDream.App/Rendering/ParticleRenderer.cs
+++ b/src/AcDream.App/Rendering/ParticleRenderer.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.CompilerServices;
@@ -22,7 +22,7 @@ namespace AcDream.App.Rendering;
/// compositing order is shared with ordinary translucent GfxObj parts. Sky and
/// sealed off-screen passes retain their independent immediate path.
///
-internal sealed unsafe class ParticleRenderer : IDisposable
+public sealed unsafe class ParticleRenderer : IDisposable
{
// The texture is per instance through GL_ARB_bindless_texture. Only blend
// state remains a draw-call boundary, so stable retail distance order no
@@ -69,8 +69,8 @@ internal sealed unsafe class ParticleRenderer : IDisposable
///
/// Vertex-instance ABI shared with particle.vert. Campaign V slice V2c
/// (2026-07-27): TextureHandleLow/High (the split halves of a raw 64-bit
- /// ARB_bindless_texture handle) became one TextureIndex — a slot into the
- /// binding=9 handle table — so ordered particles using different textures
+ /// ARB_bindless_texture handle) became one TextureIndex — a slot into the
+ /// binding=9 handle table — so ordered particles using different textures
/// still remain one instanced draw when their blend mode matches.
///
[StructLayout(LayoutKind.Sequential)]
@@ -121,8 +121,8 @@ internal sealed unsafe class ParticleRenderer : IDisposable
// Campaign V slice V2c (2026-07-27): GL-only emulation of the eventual
// Vulkan global texture descriptor array (binding=9,
- // GpuBindingModel.StorageTextureTable). Owns its own table — see
- // GlBindlessHandleTable's doc comment and the campaign doc's §5.2 for why
+ // GpuBindingModel.StorageTextureTable). Owns its own table — see
+ // GlBindlessHandleTable's doc comment and the campaign doc's §5.2 for why
// particles don't share WbDrawDispatcher's/EnvCellRenderer's/
// TerrainModernRenderer's tables. There is no automated pixel-gate
// coverage for particles (the offline gate's fixed outdoor view has none
@@ -1157,7 +1157,7 @@ internal sealed unsafe class ParticleRenderer : IDisposable
_gl.VertexAttribPointer(5, 4, VertexAttribPointerType.Float, false, instanceStride, (void*)(12 * sizeof(float)));
_gl.VertexAttribDivisor(5, 1);
// 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.VertexAttribIPointer(
6,
diff --git a/src/AcDream.App/Rendering/PhysicsCameraCollisionProbe.cs b/src/AcDream.App/Rendering/PhysicsCameraCollisionProbe.cs
index c2fb411c..23662168 100644
--- a/src/AcDream.App/Rendering/PhysicsCameraCollisionProbe.cs
+++ b/src/AcDream.App/Rendering/PhysicsCameraCollisionProbe.cs
@@ -1,4 +1,4 @@
-using System.Numerics;
+using System.Numerics;
using AcDream.Core.Physics;
namespace AcDream.App.Rendering;
@@ -12,7 +12,7 @@ namespace AcDream.App.Rendering;
/// cell walls (FindEnvCollisions) AND outdoor/baked GfxObj shells
/// (FindObjCollisions) in one faithful path.
///
-internal sealed class PhysicsCameraCollisionProbe : ICameraCollisionProbe
+public sealed class PhysicsCameraCollisionProbe : ICameraCollisionProbe
{
/// Retail viewer_sphere radius (acclient :93314).
public const float ViewerSphereRadius = 0.3f;
@@ -23,13 +23,13 @@ internal sealed class PhysicsCameraCollisionProbe : ICameraCollisionProbe
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
- // (acclient_2013_pseudo_c.txt:92775). No cell to sweep against → snap to the player.
+ // 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.
if (cellId == 0) return new CameraSweepResult(playerPos, 0u);
// === Start cell (pc:92824-92844) ===
// 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
// failure retail falls back to player->cell. Outdoor: cell = player->cell (no AdjustPosition).
uint startCell = cellId;
@@ -39,10 +39,10 @@ internal sealed class PhysicsCameraCollisionProbe : ICameraCollisionProbe
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
// 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 end = ToSpherePath(desiredEye, ViewerSphereRadius);
@@ -59,7 +59,7 @@ internal sealed class PhysicsCameraCollisionProbe : ICameraCollisionProbe
// Retail SmartBox::update_viewer calls init_object(player, 0x5c) =
// IsViewer | PathClipped | FreeRotate | PerfectClip (acclient
// 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
// eye pass through creatures, colliding only with world geometry
// (CollisionExemption.cs:83-85). FreeRotate/PerfectClip are no-ops in
@@ -95,7 +95,7 @@ internal sealed class PhysicsCameraCollisionProbe : ICameraCollisionProbe
// === 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.
- // (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.)
var (eyeCell, eyeFound) = _physics.AdjustPosition(cellId, desiredEye);
if (eyeFound) return new CameraSweepResult(desiredEye, eyeCell);
@@ -104,11 +104,11 @@ internal sealed class PhysicsCameraCollisionProbe : ICameraCollisionProbe
return new CameraSweepResult(playerPos, 0u);
}
- /// Eye/pivot point → InitPath path point (subtract the sphere-center offset).
+ /// Eye/pivot point → InitPath path point (subtract the sphere-center offset).
internal static Vector3 ToSpherePath(Vector3 spherePoint, float radius)
=> spherePoint - new Vector3(0f, 0f, radius);
- /// InitPath path point → eye point (add the sphere-center offset back).
+ /// InitPath path point → eye point (add the sphere-center offset back).
internal static Vector3 FromSpherePath(Vector3 pathPoint, float radius)
=> pathPoint + new Vector3(0f, 0f, radius);
}
diff --git a/src/AcDream.App/Rendering/PortalDepthMaskRenderer.cs b/src/AcDream.App/Rendering/PortalDepthMaskRenderer.cs
index 9565044e..8de3bdaa 100644
--- a/src/AcDream.App/Rendering/PortalDepthMaskRenderer.cs
+++ b/src/AcDream.App/Rendering/PortalDepthMaskRenderer.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Linq;
using System.Numerics;
using AcDream.App.Rendering.Wb;
@@ -8,15 +8,15 @@ namespace AcDream.App.Rendering;
///
/// BR-2 (holistic building-render port): retail's invisible portal depth
-/// writes — the port of D3DPolyRender::DrawPortalPolyInternal
+/// writes — the port of D3DPolyRender::DrawPortalPolyInternal
/// (Ghidra 0x0059bc90, pc:424490).
///
/// Wired by T1 (BR-3, `579c8b0`): seal on interior roots, punch
/// on outdoor / look-in roots, via RetailPViewPassExecutor.DrawPortalDepthWrite
-/// (the DrawExitPortalMasks slice callback) — safe alongside the
+/// (the DrawExitPortalMasks slice callback) — safe alongside the
/// dynamics-drawn-LAST frame order (the first BR-2 attempt punched after
/// 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
/// .
///
/// Retail projects a portal polygon, software-clips it against the
@@ -25,12 +25,12 @@ namespace AcDream.App.Rendering;
///
/// - Seal (retail maxZ2=6, bit0 clear, data 0x00820e14):
/// z = the polygon's true projected depth. Drawn on portals leading OUTSIDE
-/// (other_cell_id==0xFFFF) after the landscape pass — terrain seen
+/// (other_cell_id==0xFFFF) after the landscape pass — terrain seen
/// through a doorway keeps its pixels because farther interior geometry
/// z-fails inside the aperture (PView::DrawCells loop 1, Ghidra 0x005a4840,
/// pc:432783-432786).
/// - Punch (retail maxZ1=7, 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
/// (ConstructView(CBldPortal) mode-1, pc:433827). BR-2 commit 2 wires this
/// side.
@@ -38,7 +38,7 @@ namespace AcDream.App.Rendering;
///
/// Where retail clips the polygon on the CPU against the view, we apply
/// the SAME view region via gl_ClipDistance from the slice's clip-space
-/// half-planes (≤8, the validated output) — the
+/// half-planes (≤8, the validated output) — the
/// depth write lands only inside the slice region, matching retail's clipped
/// fan.
///
@@ -46,7 +46,7 @@ namespace AcDream.App.Rendering;
/// sets everything it depends on, restores the frame-global convention on
/// exit, no early-outs between set and restore.
///
-internal sealed class PortalDepthMaskRenderer : IDisposable
+public sealed class PortalDepthMaskRenderer : IDisposable
{
private const string VertSrc = @"#version 430 core
layout(location = 0) in vec3 aPos;
@@ -215,28 +215,28 @@ void main() { } // depth-only: color writes are masked off by the caller state
///
/// #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
- /// 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
/// has no such order, so an unconditional far-Z punch erased the depth
/// 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
/// 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
/// still be punched; a hill or another house meters nearer must not.
///
private const float PunchMarkDepthBias = 0.0005f;
///
- /// #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
+ /// #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
/// 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
- /// 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
/// . 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
/// coverage untouched); beyond it the punch can never reach an occluder
/// 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
/// Retail Render::znear = 0.1 (decomp :342173, re-landed
/// d4b5c71). The cap conversion below assumes the production camera near
- /// plane; the small f/(f−n) factor (~1.00002 at far 5000) is ignored.
+ /// plane; the small f/(f−n) factor (~1.00002 at far 5000) is ignored.
public const float CameraNearPlaneMeters = 0.1f;
/// 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. selects
/// punch (true, retail maxZ1) vs seal (false, retail maxZ2 true depth).
///
- /// Seal (interior root): one pass, retail-verbatim —
+ /// Seal (interior root): one pass, retail-verbatim —
/// depth ALWAYS + true projected depth. It runs immediately after the
/// gated full depth clear, so there is no nearer content to stomp.
///
/// Punch (outdoor root / look-in): two passes (#117).
/// 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
/// far-Z punch with depth ALWAYS but stencil-gated to the marked
/// 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.ScissorTest);
_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++)
_gl.Enable(EnableCap.ClipDistance0 + i);
@@ -351,7 +351,7 @@ void main() { } // depth-only: color writes are masked off by the caller state
if (!forceFarZ)
{
- // ── SEAL: retail-verbatim single pass ──
+ // ── SEAL: retail-verbatim single pass ──
_gl.DepthFunc(DepthFunction.Always);
_gl.DepthMask(true);
_gl.Uniform1(_locForceFarZ, 0);
@@ -360,7 +360,7 @@ void main() { } // depth-only: color writes are masked off by the caller state
}
else
{
- // ── PUNCH pass A: stencil-mark visible aperture pixels ──
+ // ── PUNCH pass A: stencil-mark visible aperture pixels ──
_gl.Enable(EnableCap.StencilTest);
_gl.StencilFunc(StencilFunction.Always, 1, 0xFF);
_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);
_gl.DrawArrays(PrimitiveType.TriangleFan, firstVertex, (uint)n);
- // ── PUNCH pass B: far-Z write on marked pixels only;
- // zero the stencil as we go (self-cleaning) ──
+ // ── PUNCH pass B: far-Z write on marked pixels only;
+ // zero the stencil as we go (self-cleaning) ──
_gl.StencilFunc(StencilFunction.Equal, 1, 0xFF);
_gl.StencilOp(StencilOp.Keep, StencilOp.Keep, StencilOp.Zero);
_gl.DepthFunc(DepthFunction.Always);
diff --git a/src/AcDream.App/Rendering/PortalProjection.cs b/src/AcDream.App/Rendering/PortalProjection.cs
index b9163def..d6092ded 100644
--- a/src/AcDream.App/Rendering/PortalProjection.cs
+++ b/src/AcDream.App/Rendering/PortalProjection.cs
@@ -1,11 +1,11 @@
-// PortalProjection.cs
+// PortalProjection.cs
//
// 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
// 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
-// 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
// 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;
-internal static class PortalProjection
+public static class PortalProjection
{
internal ref struct ClipPolygonLease
{
@@ -118,17 +118,17 @@ internal static class PortalProjection
Vector4[]? second = null;
// 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
- // standing in still projects (see header); then the 4 SIDE planes (x,y within ±w). The
+ // 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
// 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)),
// which corrupted the downstream 2D ScreenPolygonClip into an EMPTY region -> OutsideView
// 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
// 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
- // (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
{
second = vectorPool.Rent(capacity);
@@ -155,7 +155,7 @@ internal static class PortalProjection
(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];
for (int i = 0; i < currentCount; i++)
{
@@ -174,21 +174,21 @@ internal static class PortalProjection
/// Faithful homogeneous projection (retail PrimD3DRender::xformStart + the W=0 clip of
/// 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
/// 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.
///
/// 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
- /// 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
/// 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
/// 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
- /// when some vertex has w < 0; <3 survivors → reject (empty).
+ /// when some vertex has w < 0; <3 survivors → reject (empty).
public static Vector4[] ProjectToClip(IReadOnlyList localPoly, Matrix4x4 cellToWorld, Matrix4x4 viewProj)
{
using ClipPolygonLease lease = ProjectToClipLease(localPoly, cellToWorld, viewProj);
@@ -274,7 +274,7 @@ internal static class PortalProjection
/// (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
/// 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.
public static Vector2[] ClipToRegion(IReadOnlyList subjectClip, IReadOnlyList regionCcwNdc)
{
@@ -339,7 +339,7 @@ internal static class PortalProjection
// 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,
- // 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).
int regionCount = regionCcwNdc.Count;
int capacity = checked(subjectClip.Length + regionCount);
@@ -370,7 +370,7 @@ internal static class PortalProjection
if (currentCount < 3)
return System.Array.Empty();
- // 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.
ndcScratch = vector2Pool.Rent(currentCount);
Span ndc = ndcScratch.AsSpan(0, currentCount);
@@ -408,8 +408,8 @@ internal static class PortalProjection
// Retail copy_view's ~1-pixel vertex merge (see ClipToRegion). Collapses
// runs of consecutive near-identical vertices, including across the
// wrap-around. A polygon that collapses below 3 distinct vertices is
- // degenerate (sub-pixel sliver) and returns empty — exactly retail's
- // "<3 surviving verts → output count 0".
+ // degenerate (sub-pixel sliver) and returns empty — exactly retail's
+ // "<3 surviving verts → output count 0".
private const float VertexMergeEpsilonNdc = 2f / 1080f;
private static int MergeSubPixelVertices(Span poly)
@@ -428,7 +428,7 @@ internal static class PortalProjection
}
poly[kept++] = vertex;
}
- // Wrap-around: last ≈ first.
+ // Wrap-around: last ≈ first.
while (kept >= 2)
{
Vector2 first = poly[0];
@@ -442,9 +442,9 @@ internal static class PortalProjection
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
- // 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.
private static int ClipHomogeneousEdge(
ReadOnlySpan polygon,
@@ -489,7 +489,7 @@ internal static class PortalProjection
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
// 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.
diff --git a/src/AcDream.App/Rendering/PortalTunnelCamera.cs b/src/AcDream.App/Rendering/PortalTunnelCamera.cs
index 07aa34ab..7126b4a9 100644
--- a/src/AcDream.App/Rendering/PortalTunnelCamera.cs
+++ b/src/AcDream.App/Rendering/PortalTunnelCamera.cs
@@ -1,4 +1,4 @@
-using System.Numerics;
+using System.Numerics;
namespace AcDream.App.Rendering;
@@ -8,7 +8,7 @@ namespace AcDream.App.Rendering;
/// (0x00453760). Identity looks along AC +Y with +Z up; the animated
/// angle rolls that view around its own +Y forward axis.
///
-internal sealed class PortalTunnelCamera : ICamera
+public sealed class PortalTunnelCamera : ICamera
{
public static readonly Vector3 RetailEye = new(0.24f, -2.7f, 0.88f);
diff --git a/src/AcDream.App/Rendering/PortalTunnelPresentation.cs b/src/AcDream.App/Rendering/PortalTunnelPresentation.cs
index e9781784..8b10eb24 100644
--- a/src/AcDream.App/Rendering/PortalTunnelPresentation.cs
+++ b/src/AcDream.App/Rendering/PortalTunnelPresentation.cs
@@ -1,4 +1,4 @@
-using System.Numerics;
+using System.Numerics;
using AcDream.App.Rendering.Wb;
using AcDream.App.UI;
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
/// retained gameplay UI.
///
-internal sealed class PortalTunnelPresentation : IDisposable
+public sealed class PortalTunnelPresentation : IDisposable
{
public const uint SetupClientEnum = 0x10000001u;
public const uint AnimationClientEnum = 0x10000002u;
diff --git a/src/AcDream.App/Rendering/PortalView.cs b/src/AcDream.App/Rendering/PortalView.cs
index 8915296e..9dd082cd 100644
--- a/src/AcDream.App/Rendering/PortalView.cs
+++ b/src/AcDream.App/Rendering/PortalView.cs
@@ -1,4 +1,4 @@
-// PortalView.cs
+// PortalView.cs
//
// 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):
@@ -10,7 +10,7 @@ using System.Numerics;
namespace AcDream.App.Rendering;
/// One convex polygon in NDC screen space (xy in [-1,1]), plus its bounding rect.
-internal readonly struct ViewPolygon
+public readonly struct ViewPolygon
{
public readonly Vector2[] Vertices;
public readonly float MinX, MinY, MaxX, MaxY;
@@ -101,7 +101,7 @@ internal sealed class PortalPolygonVertexStore
}
/// A cell's accumulated clip region: a set of convex view polygons + the union bounding rect.
-internal sealed class CellView
+public sealed class CellView
{
// ViewPolygon exposes its vertex array for the renderer, so this seed must
// be owned by the CellView rather than shared globally. Pooling the
@@ -170,7 +170,7 @@ internal sealed class CellView
MaxY = float.MinValue;
}
- /// A region covering the entire NDC viewport — the camera cell's seed region
+ /// A region covering the entire NDC viewport — the camera cell's seed region
/// (mirrors retail PView::DrawInside copy_view(..., 4) at decomp:433814).
public static CellView FullScreen()
{
@@ -192,7 +192,7 @@ internal sealed class CellView
// 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
// 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
// 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
@@ -206,14 +206,14 @@ internal sealed class CellView
// #120 convergence (2026-06-11): reject a polygon CONTAINED in one already
// stored. The reciprocal ping-pong (eye within PortalSideEpsilon of a
- // 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
+ // 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
// 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
// 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
- // 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.
// Bonus: back-emission into a full-screen view (the root cell) is now
// always rejected outright.
@@ -228,7 +228,7 @@ internal sealed class CellView
}
// #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
// polygons are convex (Sutherland-Hodgman / full-screen seed outputs); the
// edge test adapts to either winding via the polygon's signed area.
@@ -252,7 +252,7 @@ internal sealed class CellView
{
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;
for (int i = 0; i < convex.Length; i++)
{
@@ -268,10 +268,10 @@ internal sealed class CellView
var b = convex[(i + 1) % convex.Length];
var ab = b - a;
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)
{
- // 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));
if (cross < -eps * len)
return false; // a vertex lies outside this edge by more than eps
@@ -280,7 +280,7 @@ internal sealed class CellView
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
// region (so a drifted duplicate snaps onto its predecessor). The finite grid is what bounds growth.
private const float DedupGridNdc = 1e-3f;
@@ -293,24 +293,24 @@ internal sealed class CellView
//
// 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
- // 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,
// 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
- // 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
// 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
- // (PortalProjection.ClipToRegion, used by the forward AND — as of today — the reciprocal hop)
+ // §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)
// 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.
// 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
// 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
- // 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)
{
if (verts is null || verts.Length < 3)
diff --git a/src/AcDream.App/Rendering/PortalVisibilityBuilder.cs b/src/AcDream.App/Rendering/PortalVisibilityBuilder.cs
index 2f37a238..2edaaf2a 100644
--- a/src/AcDream.App/Rendering/PortalVisibilityBuilder.cs
+++ b/src/AcDream.App/Rendering/PortalVisibilityBuilder.cs
@@ -1,4 +1,4 @@
-// PortalVisibilityBuilder.cs
+// PortalVisibilityBuilder.cs
//
// Phase A8.F: recursive portal-clip visibility (the builder). Port of retail
// PView::ConstructView (decomp:433750) -> ClipPortals (433572) -> AddViewToPortals
@@ -12,7 +12,7 @@ using System.Numerics;
namespace AcDream.App.Rendering;
/// Per-frame output of the portal-frame BFS.
-internal sealed class PortalVisibilityFrame
+public sealed class PortalVisibilityFrame
{
private const int MaxRetainedCellViews = 512;
internal const int MaxRetainedBuildCollectionCapacity = 512;
@@ -31,7 +31,7 @@ internal sealed class PortalVisibilityFrame
internal int PolygonVertexAllocationCount => _polygonVertices.AllocationCount;
internal int RetainedPolygonVertexArrayCount => _polygonVertices.RetainedArrayCount;
- /// Screen region (NDC) where outdoor terrain/scenery may draw — exit portals
+ /// Screen region (NDC) where outdoor terrain/scenery may draw — exit portals
/// recursively clipped to their portal chain. The cellar-flap fix.
public CellView OutsideView { get; private set; } = new();
@@ -39,7 +39,7 @@ internal sealed class PortalVisibilityFrame
public Dictionary CellViews { get; } = new();
/// 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
/// 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.
@@ -243,7 +243,7 @@ internal sealed class PortalVisibilityFrame
}
}
-internal static class PortalVisibilityBuilder
+public static class PortalVisibilityBuilder
{
// Side-classification epsilon. Retail's is F_EPSILON = 0.000199999995
// (const @0x007c8c70; PView::InitCell Ghidra 0x005a4b70). T2 (BR-4)
@@ -253,20 +253,20 @@ internal static class PortalVisibilityBuilder
// 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
// 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
// family) + the cdstW near-clip pin.
private const float PortalSideEpsilon = 0.01f;
- // Retail F_EPSILON proper — used where the semantic is knife-edge
- // REJECTION (ConstructView(CBldPortal) Sidedness IN_PLANE → return 0,
+ // Retail F_EPSILON proper — used where the semantic is knife-edge
+ // REJECTION (ConstructView(CBldPortal) Sidedness IN_PLANE → return 0,
// 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
// stands near a doorway plane).
private const float SeedInPlaneEpsilon = 0.0002f;
// 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 =
Environment.GetEnvironmentVariable("ACDREAM_A8_DUMP_PV") == "1";
private static readonly Dictionary s_pvDumpCount = new();
@@ -275,14 +275,14 @@ internal static class PortalVisibilityBuilder
/// #120 observable: total convergence-tripwire firings across both the
/// interior and the exterior look-in propagation.
/// 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.
///
public static int ConvergenceTripwireCount;
///
/// #120 self-attribution dump: the growth-recursion path that exceeded
- /// 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 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)
/// reads directly off the output.
///
private static void DumpPropagationChain(uint[] chain, int depth, uint rootCellId, Vector3 eye)
@@ -309,11 +309,11 @@ internal static class PortalVisibilityBuilder
/// The +Z world lift applied to DRAWN cell shells (z-fighting vs
/// 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).
/// Draw-space consumers of portal polygons (the OutsideView color gate
/// 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).
public const float ShellDrawLiftZ = 0.02f;
@@ -347,7 +347,7 @@ internal static class PortalVisibilityBuilder
// 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
// 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
// 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
@@ -357,7 +357,7 @@ internal static class PortalVisibilityBuilder
frame.OutsideView.Add(frame.CopyPolygon(FullScreenQuad));
// 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
// camera cell seeds at distance 0 (retail InsCellTodoList(this, arg2, 0f) at 433758) so it
// always pops first.
@@ -367,10 +367,10 @@ internal static class PortalVisibilityBuilder
// Fixpoint termination replacing the old MaxReprocessPerCell hard cap. This mirrors the
// 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
- // 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
// 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
// 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.
@@ -398,11 +398,11 @@ internal 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}]");
// 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
// empty OUTSIDEVIEW can then be traced to the precise gate: polyLen<3 (empty
// 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.
for (int ci = 0; ci < cameraCell.Portals.Count; ci++)
{
@@ -417,7 +417,7 @@ internal static class PortalVisibilityBuilder
}
// 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
// cell_view_done is set calls AdjustCellView (pc:433741-433745), which
// re-clips ONLY the new views (the update_count watermark) through that
@@ -426,7 +426,7 @@ internal static class PortalVisibilityBuilder
// neighbour; it processes exactly the new tail and recurses further
// growth. Termination is physical: recursion fires only when AddRegion
// 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
// is a loud failsafe, not control flow: it firing means the convergence
// invariant broke and must be fixed, not tuned.
@@ -434,7 +434,7 @@ internal static class PortalVisibilityBuilder
// #120 self-attribution: the recursion path (cell id per depth), so a
// tripwire firing names the growth CYCLE instead of just the tip.
// 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
// next natural occurrence.
uint[] propagationChain = frame.PropagationChainScratch;
@@ -444,7 +444,7 @@ internal static class PortalVisibilityBuilder
if (depth >= RecursionTripwire)
{
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);
return;
}
@@ -497,7 +497,7 @@ internal static class PortalVisibilityBuilder
// 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
- // 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
// 0171<->0173 flood cycle -> re-enqueue churn -> the doorway flap (pinned in flap-sidechk.log:
// back portals show camInterior=False eyeIn=True).
@@ -529,8 +529,8 @@ internal 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-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
- // (polyClipFinish <3 survivors → reject; ClipPortals adds no view). The
+ // Empty clip = no flood through this portal, period — retail's empty-GetClip rule
+ // (polyClipFinish <3 survivors → reject; ClipPortals adds no view). The
// EyeInsidePortalOpening rescue that used to substitute the current view here was
// 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
@@ -554,7 +554,7 @@ internal static class PortalVisibilityBuilder
// Exit portal -> outdoors visible through this (clipped) opening.
// OutsideView gates DRAWN color (terrain/sky/scissor), and the
// 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
// drawn lintel (#130 strip). Flood semantics keep the
// unlifted clippedRegion path above.
@@ -607,20 +607,20 @@ internal static class PortalVisibilityBuilder
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
// SAME opening seen from the neighbour (skewed/oblique apertures), so retail
// 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
// 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
- // `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
// 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
// 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
// neighbour's side; the old eye-in-opening restore was part of
// the deleted rescue.
@@ -645,8 +645,8 @@ internal static class PortalVisibilityBuilder
if (grew)
{
- // First discovery → enqueue once (retail InsCellTodoList in
- // the ecx_5==0 branch). Distance = camera→nearest portal-
+ // First discovery → enqueue once (retail InsCellTodoList in
+ // the ecx_5==0 branch). Distance = camera→nearest portal-
// opening vertex (retail InitCell min-vertex distance,
// pc:432988-433004).
if (queued.Add(neighbourId))
@@ -655,10 +655,10 @@ internal static class PortalVisibilityBuilder
InsertTodo(todo, neighbour, dist);
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
// 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))
{
inPlace = true;
@@ -678,8 +678,8 @@ internal static class PortalVisibilityBuilder
// draw position (retail appends to cell_draw_list once per pop,
// pc:433783). Note: retail also RE-SORTS the draw list when a
// late-grown cell's dependency order changes (AdjustCellPlace,
- // pc:433247); we keep first-pop order — under T1's whole-cell
- // far→near draws + depth testing, order affects only transparent-
+ // pc:433247); we keep first-pop order — under T1's whole-cell
+ // far→near draws + depth testing, order affects only transparent-
// pass compositing in exotic chains (documented residual for T5).
if (drawListed.Add(cell.CellId))
frame.OrderedVisibleCells.Add(cell.CellId);
@@ -691,7 +691,7 @@ internal static class PortalVisibilityBuilder
if (pvDump)
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.
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeFlapEnabled)
EmitFlapProbe(cameraCell, cameraPos, viewProj, frame);
@@ -715,7 +715,7 @@ internal static class PortalVisibilityBuilder
/// camera cell. It keeps the same retail distance-priority traversal and
/// neighbour reciprocal clipping once inside the building.
///
- /// Optional NDC region the seed apertures clip against —
+ /// Optional NDC region the seed apertures clip against —
/// retail's GetClip runs under the CURRENTLY INSTALLED view (PView::GetClip
/// 0x005a4320): full screen when the viewer is outdoors, the accumulated
/// outside (doorway) view when a building is looked into from an interior
@@ -756,7 +756,7 @@ internal static class PortalVisibilityBuilder
// If the camera is on the cell-interior side, the normal indoor
// DrawInside path owns this portal instead. T2 (BR-4): a seed
// 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
// ever built from a knife-edge aperture.
if (i < cell.ClipPlanes.Count)
@@ -788,7 +788,7 @@ internal static class PortalVisibilityBuilder
// T2 (BR-4): empty clip = no seed, no exceptions (retail's
// empty-GetClip rule; the full-screen substitute rescue is
- // deleted — see Build()).
+ // deleted — see Build()).
if (clippedRegion.Count == 0)
continue;
@@ -800,19 +800,19 @@ internal 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
// re-enqueue + MaxReprocessPerCell cap and the eye-in-opening rescues
// are deleted (empty clip culls, period).
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)
{
if (depth >= RecursionTripwire)
{
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);
return;
}
@@ -841,7 +841,7 @@ internal static class PortalVisibilityBuilder
if (portal.OtherCellId == 0xFFFF)
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
&& !CameraOnInteriorSide(cell, i, cameraPos))
continue;
@@ -905,15 +905,15 @@ internal static class PortalVisibilityBuilder
}
///
- /// Retail per-building flood — PView::ConstructView(CBldPortal*, …) (decomp:433827),
- /// reached from BSPPORTAL::portal_draw_portals_only (0x53d870) → DrawPortal
+ /// Retail per-building flood — PView::ConstructView(CBldPortal*, …) (decomp:433827),
+ /// reached from BSPPORTAL::portal_draw_portals_only (0x53d870) → DrawPortal
/// (0x5a5ab0) during the terrain BSP walk. Floods ONE building's cells from its outside-facing
/// entrance portal(s). Identical machinery to , but the CONTRACT is
/// 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
- /// 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
- /// 2↔6 oscillation. Robustness is validated by the conformance test, not assumed.
+ /// 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
+ /// 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.
///
public static PortalVisibilityFrame ConstructViewBuilding(
IEnumerable buildingCells,
@@ -1053,12 +1053,12 @@ internal static class PortalVisibilityBuilder
}
// 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.
- // `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
// 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(
LoadedCell cameraCell, Vector3 cameraPos, Matrix4x4 viewProj, PortalVisibilityFrame frame)
{
@@ -1080,9 +1080,9 @@ internal static class PortalVisibilityBuilder
d = Vector3.Dot(pl.Normal, localEye) + pl.D;
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,
- // 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.
int projN = -1, clipN = -1;
string ndcText = "";
@@ -1115,21 +1115,21 @@ internal static class PortalVisibilityBuilder
}
// 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
// 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)
{
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);
float dot = Vector3.Dot(plane.Normal, localCam) + plane.D;
return plane.InsideSide == 0 ? dot >= -PortalSideEpsilon : dot <= PortalSideEpsilon;
}
// 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
// true retail epsilon, NOT the side test's root-lag tolerance.
private static bool EyeInPlaneOfPortal(LoadedCell cell, int portalIndex, Vector3 cameraPos)
@@ -1153,22 +1153,22 @@ internal static class PortalVisibilityBuilder
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
// 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
- // 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.
+ // 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.
//
// `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 —
- // `portals->portal[arg2->other_portal_id ...]` at 005a54b2/005a54f6 — rather than scanning for
+ // 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
// 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
- // (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.
//
- // 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
// (< 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
@@ -1184,38 +1184,38 @@ internal static class PortalVisibilityBuilder
{
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 near-side clip: PView::ClipPortals decomp:433689
// `if (exact_match != 0 || other_portal_id < 0) goto propagate-without-reciprocal`.
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;
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
- // pipeline as the forward clip — retail PView::OtherPortalClip (decomp:433524-433563) routes
- // the reciprocal polygon through the very same GetClip(finish=1) → ACRender::polyClipFinish
+ // §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
+ // 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.
//
// 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
// 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
- // 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
+ // 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
+ // 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
- // 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.
using PortalProjection.ClipPolygonLease reciprocalClip =
PortalProjection.ProjectToClipLease(
reciprocalPoly,
neighbour.WorldTransform,
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.
// ClipToRegion(subject=homogeneous reciprocal, region=near-side NDC polygon) = the same
@@ -1252,7 +1252,7 @@ internal static class PortalVisibilityBuilder
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:
// 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
@@ -1272,10 +1272,10 @@ internal static class PortalVisibilityBuilder
///
/// 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
- /// the tail; removes the tail — giving closest-first traversal exactly
+ /// the tail; removes the tail — giving closest-first traversal exactly
/// 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
- /// 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.
///
private static void InsertTodo(
diff --git a/src/AcDream.App/Rendering/RenderBootstrap.cs b/src/AcDream.App/Rendering/RenderBootstrap.cs
index 39817ab0..1b3949b2 100644
--- a/src/AcDream.App/Rendering/RenderBootstrap.cs
+++ b/src/AcDream.App/Rendering/RenderBootstrap.cs
@@ -1,4 +1,4 @@
-using System.Collections.Concurrent;
+using System.Collections.Concurrent;
using AcDream.Content;
using DatReaderWriter;
using Microsoft.Extensions.Logging.Abstractions;
@@ -12,7 +12,7 @@ namespace AcDream.App.Rendering;
/// classes and the same order as , minus
/// terrain / sky / physics / streaming.
///
-internal sealed record RenderStack(
+public sealed record RenderStack(
GL Gl,
IDatReaderWriter Dats,
string ShaderDir,
@@ -28,13 +28,15 @@ internal sealed record RenderStack(
AcDream.App.UI.UiDatFont? LargeDatFont) : System.IDisposable
{
internal GpuFrameFlightController FrameFlights { get; init; } = null!;
- internal IGpuDevice GpuDevice { get; init; } = null!;
- internal GpuDeviceFrameLifetime FrameLifetime { get; init; } = null!;
private ResourceShutdownTransaction? _shutdown;
- internal void BeginFrame() => FrameLifetime.BeginFrame();
+ internal void BeginFrame()
+ {
+ FrameFlights.BeginFrame();
+ UiHost.TextRenderer.BeginFrame(FrameFlights.CurrentSlot);
+ }
- internal void EndFrame() => FrameLifetime.EndFrame();
+ internal void EndFrame() => FrameFlights.EndFrame();
/// Dispose the GL pieces this stack OWNS (everything created in
/// ). + are caller-owned
@@ -61,10 +63,6 @@ internal sealed record RenderStack(
new("lighting UBO", LightingUbo.Dispose),
new("UI host", UiHost.Dispose),
]),
- new ResourceShutdownStage("GPU device (RHI)",
- [
- new("GPU device", GpuDevice.Dispose),
- ]),
new ResourceShutdownStage("frame flight owner",
[
new("frame flights", FrameFlights.Dispose),
@@ -73,17 +71,17 @@ internal sealed record RenderStack(
}
///
- /// Resolves a sprite id (0x06xxxxxx) to a (texture-table slot, width, height) triple.
- /// Copied verbatim from GameWindow's ResolveChrome closure — it calls
+ /// Resolves a sprite id (0x06xxxxxx) to a (GL handle, width, height) triple.
+ /// Copied verbatim from GameWindow's ResolveChrome closure — it calls
/// TextureCache.GetOrUploadRenderSurface(id, out w, out h).
///
- public (GpuTextureSlot handle, int width, int height) ResolveChrome(uint spriteId)
+ public (uint handle, int width, int height) ResolveChrome(uint spriteId)
{
- GpuTextureSlot t = TextureCache.GetOrUploadRenderSurface(spriteId, out int w, out int h);
+ uint t = TextureCache.GetOrUploadRenderSurface(spriteId, out int w, out int h);
return (t, w, h);
}
- // ── Font cache (per-stack, keyed by FontDid) ─────────────────────────────
+ // ── Font cache (per-stack, keyed by FontDid) ─────────────────────────────
///
/// Cache of loaded dat fonts keyed by FontDid (0x40000000-range).
@@ -95,7 +93,7 @@ internal sealed record RenderStack(
///
/// 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.
///
/// Pre-seeds (0x40000000) and
@@ -125,7 +123,7 @@ internal sealed record RenderStack(
}
/// Options for .
-internal sealed record RenderBootstrapOptions(
+public sealed record RenderBootstrapOptions(
AcDream.UI.Abstractions.Settings.QualitySettings Quality,
string DiagnosticsDirectory);
@@ -133,12 +131,12 @@ internal sealed record RenderBootstrapOptions(
/// Constructs the UI Studio's render stack from the production classes,
/// in the same order as .
///
-internal static class RenderBootstrap
+public static class RenderBootstrap
{
///
/// Build the studio's render stack. Throws
/// (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.
///
public static RenderStack Create(
GL gl,
@@ -167,15 +165,8 @@ internal static class RenderBootstrap
// --- TextureCache (GameWindow ~1774) ---
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(
gl,
- gpuDevice,
dats,
bindless,
frameFlights,
@@ -210,7 +201,7 @@ internal static class RenderBootstrap
return new AcDream.Core.Physics.AnimationSequencer(
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(
setup,
new DatReaderWriter.DBObjs.MotionTable(),
@@ -228,10 +219,10 @@ internal static class RenderBootstrap
var entitySpawnAdapter = new Wb.EntitySpawnAdapter(
textureCache, SequencerFactory, meshAdapter);
- // --- EntityClassificationCache (GameWindow ~217 — field initializer, new()) ---
+ // --- EntityClassificationCache (GameWindow ~217 — field initializer, new()) ---
var classificationCache = new Wb.EntityClassificationCache();
- // --- TranslucencyFadeManager (GameWindow — field initializer, new()) ---
+ // --- TranslucencyFadeManager (GameWindow — field initializer, new()) ---
var translucencyFades = new AcDream.Core.Rendering.TranslucencyFadeManager();
// --- WbDrawDispatcher (GameWindow ~2377-2381) ---
@@ -246,13 +237,13 @@ internal static class RenderBootstrap
// --- Larger retail font (0x40000001, MaxCharHeight=18) for attribute row text.
// 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
- // 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);
// --- 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) ---
- var uiHost = new AcDream.App.UI.UiHost(gpuDevice, () => frameLifetime.Current, defaultFont: null);
+ var uiHost = new AcDream.App.UI.UiHost(gl, shaderDir, defaultFont: null);
var stack = new RenderStack(
Gl: gl,
@@ -270,8 +261,6 @@ internal static class RenderBootstrap
LargeDatFont: largeDatFont)
{
FrameFlights = frameFlights,
- GpuDevice = gpuDevice,
- FrameLifetime = frameLifetime,
};
// Pre-seed the font cache with the two already-uploaded atlas instances
diff --git a/src/AcDream.App/Rendering/RenderFrameOrchestrator.cs b/src/AcDream.App/Rendering/RenderFrameOrchestrator.cs
index a3531334..df5f0e0a 100644
--- a/src/AcDream.App/Rendering/RenderFrameOrchestrator.cs
+++ b/src/AcDream.App/Rendering/RenderFrameOrchestrator.cs
@@ -38,52 +38,6 @@ internal interface IRenderFrameLifetime
void EndFrame();
}
-///
-/// Campaign V slice V4a: the current frame's , readable
-/// by any ported renderer that needs to allocate a ring or open a pass — the
-/// structural piece this slice adds so TextRenderer/DebugLineRenderer
-/// have somewhere to reach the frame lifecycle already bracketing every render
-/// callback ('s ).
-///
-internal interface ICurrentGpuFrameSource
-{
- /// The frame opened by the most recent . Throws if none is open.
- IGpuFrame Current { get; }
-}
-
-///
-/// Wires / into
-/// the same bracket
-/// occupied before this slice — additive, not a frame-graph restructuring:
-/// already calls the frame-flight
-/// controller's BeginFrame internally, so this type OWNS the
-/// 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.
-///
-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
{
void Prepare(RenderFrameInput input);
diff --git a/src/AcDream.App/Rendering/RenderFrameResourceController.cs b/src/AcDream.App/Rendering/RenderFrameResourceController.cs
index db714e99..03d4792d 100644
--- a/src/AcDream.App/Rendering/RenderFrameResourceController.cs
+++ b/src/AcDream.App/Rendering/RenderFrameResourceController.cs
@@ -88,6 +88,8 @@ internal sealed class RuntimeRenderFrameBeginResources : IRenderFrameBeginResour
private readonly WbDrawDispatcher? _dispatcher;
private readonly EnvCellRenderer? _environmentCells;
private readonly PortalDepthMaskRenderer? _portalDepth;
+ private readonly TextRenderer? _worldText;
+ private readonly TextRenderer? _uiText;
private readonly ClipFrame? _clip;
private readonly TerrainModernRenderer? _terrain;
private readonly SceneLightingUboBinding? _lighting;
@@ -96,6 +98,8 @@ internal sealed class RuntimeRenderFrameBeginResources : IRenderFrameBeginResour
WbDrawDispatcher? dispatcher,
EnvCellRenderer? environmentCells,
PortalDepthMaskRenderer? portalDepth,
+ TextRenderer? worldText,
+ TextRenderer? uiText,
ClipFrame? clip,
TerrainModernRenderer? terrain,
SceneLightingUboBinding? lighting)
@@ -104,6 +108,8 @@ internal sealed class RuntimeRenderFrameBeginResources : IRenderFrameBeginResour
_dispatcher = dispatcher;
_environmentCells = environmentCells;
_portalDepth = portalDepth;
+ _worldText = worldText;
+ _uiText = uiText;
_clip = clip;
_terrain = terrain;
_lighting = lighting;
@@ -116,11 +122,8 @@ internal sealed class RuntimeRenderFrameBeginResources : IRenderFrameBeginResour
_dispatcher?.BeginFrame(gpuSlot);
_environmentCells?.BeginFrame(gpuSlot);
_portalDepth?.BeginFrame(gpuSlot);
- // TextRenderer (world-hud + retained UI) is off this per-slot int
- // 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).
+ _worldText?.BeginFrame(gpuSlot);
+ _uiText?.BeginFrame(gpuSlot);
_clip?.BeginFrame(gpuSlot);
_terrain?.BeginFrame(gpuSlot);
_lighting?.BeginFrame(gpuSlot);
diff --git a/src/AcDream.App/Rendering/Residency/ResidencyBudgetOptions.cs b/src/AcDream.App/Rendering/Residency/ResidencyBudgetOptions.cs
index 4acb4474..121802d6 100644
--- a/src/AcDream.App/Rendering/Residency/ResidencyBudgetOptions.cs
+++ b/src/AcDream.App/Rendering/Residency/ResidencyBudgetOptions.cs
@@ -1,4 +1,4 @@
-using System.Globalization;
+using System.Globalization;
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
/// behavior by default and are changed atomically as one profile.
///
-internal sealed record ResidencyBudgetOptions(
+public sealed record ResidencyBudgetOptions(
long ObjectMeshGpuBytes,
int ObjectMeshUnownedEntries,
long PreparedMeshCpuBytes,
diff --git a/src/AcDream.App/Rendering/RetailChaseCamera.cs b/src/AcDream.App/Rendering/RetailChaseCamera.cs
index af2115f5..c2eefc8b 100644
--- a/src/AcDream.App/Rendering/RetailChaseCamera.cs
+++ b/src/AcDream.App/Rendering/RetailChaseCamera.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Numerics;
using AcDream.Core.Rendering;
@@ -10,7 +10,7 @@ namespace AcDream.App.Rendering;
/// docs/research/named-retail/acclient_2013_pseudo_c.txt:95505):
/// a STATEFUL sought position that converges from the current swept
/// viewer toward the desired boom pose (CameraManager::UpdateCamera
-/// 0x00456660 → viewer_sought_position, the #180 fix), 5-frame
+/// 0x00456660 → viewer_sought_position, the #180 fix), 5-frame
/// velocity-averaged slope-aligned heading frame, mouse-input low-pass
/// filter. Pseudocode:
/// docs/research/2026-07-06-camera-sought-position-pseudocode.md.
@@ -27,7 +27,7 @@ namespace AcDream.App.Rendering;
/// Spec: docs/superpowers/specs/2026-05-18-retail-chase-camera-design.md.
///
///
-internal sealed class RetailChaseCamera : ICamera
+public sealed class RetailChaseCamera : ICamera
{
// ICamera surface.
public Vector3 Position { get; private set; }
@@ -35,19 +35,19 @@ internal sealed class RetailChaseCamera : ICamera
///
/// The cell the collided viewer-sphere ended in (retail viewer_cell =
/// sphere_path.curr_cell). Roots the render mode + indoor visibility + the portal
- /// side-test in (Phase W single-viewpoint V1) — the ONE viewpoint.
+ /// side-test in (Phase W single-viewpoint V1) — the ONE viewpoint.
/// Equals the passed player cell when camera collision is off / the probe is null.
///
public uint ViewerCellId { get; private set; }
public float Aspect { get; set; } = 16f / 9f;
public float FovY { get; set; } = MathF.PI / 3f;
public Matrix4x4 View { get; private set; } = Matrix4x4.Identity;
- // 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)).
+ // 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)).
// 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
- // 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
+ // 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
// (137b4f2), was reverted (8bd3492) after correlating with missing indoor textures,
// 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
@@ -55,12 +55,12 @@ internal sealed class RetailChaseCamera : ICamera
public Matrix4x4 Projection =>
Matrix4x4.CreatePerspectiveFieldOfView(FovY, Aspect, 0.1f, 5000f);
- // ── Public tunables (per-instance) ──────────────────────────────
+ // ── Public tunables (per-instance) ──────────────────────────────
- /// Length of the viewer_offset vector. Retail default ≈ 2.61.
+ /// Length of the viewer_offset vector. Retail default ≈ 2.61.
public float Distance { get; set; } = 2.61f;
- /// Angle of the camera above the heading-frame XY plane. Retail default ≈ 0.291 rad (16.7°).
+ /// Angle of the camera above the heading-frame XY plane. Retail default ≈ 0.291 rad (16.7°).
public float Pitch { get; set; } = 0.291f;
///
@@ -92,27 +92,27 @@ internal sealed class RetailChaseCamera : ICamera
public const float PitchMax = 1.4f;
// Retail CameraManager::UpdateCamera convergence-snap thresholds (decomp
- // acclient_2013_pseudo_c.txt, 0x00456fcd–0x00457035). SnapEpsilon = 2 ×
- // 0.000199999995 m ≈ 0.0004 m — the per-frame translation step below which retail
+ // acclient_2013_pseudo_c.txt, 0x00456fcd–0x00457035). SnapEpsilon = 2 ×
+ // 0.000199999995 m ≈ 0.0004 m — the per-frame translation step below which retail
// 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
- // 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 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
// 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::
// PlayerPhysicsUpdatedCallback passes &this->viewer
// into UpdateCamera, 0x00452d75).
// _dampedForward = the sought's look direction. Sweeps translate but
// 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 int _velocityCount;
@@ -121,12 +121,12 @@ internal sealed class RetailChaseCamera : ICamera
private Vector3 _dampedForward = new(1f, 0f, 0f);
private bool _initialised;
- // Mouse-filter state — shared by FilterMouseDelta entrypoint.
+ // Mouse-filter state — shared by FilterMouseDelta entrypoint.
private float _lastMouseDeltaX;
private float _lastMouseDeltaY;
private float _lastFilterTimeSec;
- // ── Per-frame entry point ────────────────────────────────────────
+ // ── Per-frame entry point ────────────────────────────────────────
///
/// Advance the camera one frame. Caller passes the player's current
@@ -177,7 +177,7 @@ internal sealed class RetailChaseCamera : ICamera
// 5. Stateful sought position (#180). Retail CameraManager::UpdateCamera
// (0x00456660) interpolates FROM THE CURRENT SWEPT VIEWER toward the
// 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
// and re-extends gradually. The full-length ideal boom is never swept
// directly. Pseudocode:
@@ -193,15 +193,15 @@ internal sealed class RetailChaseCamera : ICamera
{
float tAlpha = ComputeDampingAlpha(CameraDiagnostics.TranslationStiffness, 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'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 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
- // 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
// pressed against a wall. See ApplyConvergenceSnap + SnapEpsilon.
(_soughtEye, _dampedForward, _) =
@@ -209,13 +209,13 @@ internal sealed class RetailChaseCamera : ICamera
}
// 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)
- // — 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
- // 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
- // bistable contact pair — the #180 strobe fix.
+ // bistable contact pair — the #180 strobe fix.
Vector3 publishedEye = _soughtEye;
// 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
@@ -227,13 +227,13 @@ internal sealed class RetailChaseCamera : ICamera
publishedEye = swept.Eye;
ViewerCellId = swept.ViewerCellId;
// 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
// re-extends from there.
if (swept.ViewerCellId == 0)
_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;
// 6. Publish renderer surface (from the collided eye; rotation stays the
@@ -241,7 +241,7 @@ internal sealed class RetailChaseCamera : ICamera
Position = publishedEye;
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.
float d = Vector3.Distance(publishedEye, pivotWorld);
PlayerTranslucency = ComputeTranslucency(d);
@@ -295,12 +295,12 @@ internal sealed class RetailChaseCamera : ICamera
///
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,
ref _lastMouseDeltaX, ref _lastFilterTimeSec, CameraDiagnostics.MouseLowPassWindowSec);
// 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
- // 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.).
float yTimeShadow = _lastFilterTimeSec - 1f; // force within-window path for the Y axis
float y = FilterMouseAxis(rawY, weight, nowSec,
@@ -308,7 +308,7 @@ internal sealed class RetailChaseCamera : ICamera
return (x, y);
}
- // Math primitives — pure, internal-static for unit-testability.
+ // Math primitives — pure, internal-static for unit-testability.
///
/// Pick the heading vector that drives the camera basis. Mirrors
@@ -316,8 +316,8 @@ internal sealed class RetailChaseCamera : ICamera
/// path (decomp acclient_2013_pseudo_c.txt:95644-95795):
///
/// - Base heading is the player's facing
- /// direction in world space — (cos yaw, sin yaw, 0)
- /// — not the velocity vector. Velocity only gates whether
+ /// direction in world space — (cos yaw, sin yaw, 0)
+ /// — not the velocity vector. Velocity only gates whether
/// slope-alignment fires.
/// - If is off
/// OR the player's horizontal velocity is below epsilon (i.e.
@@ -337,7 +337,7 @@ internal sealed class RetailChaseCamera : ICamera
///
/// 5-frame averaged player velocity in world space.
/// Player facing yaw + any orbit offset, radians.
- /// Player's transient_state & 1 — does describe a valid contact plane?
+ /// Player's transient_state & 1 — does describe a valid contact plane?
/// Player's current contact plane normal in world space; ignored when is false.
/// User-tunable; when false skips the projection and returns the flat facing direction.
internal static Vector3 ComputeHeading(
@@ -356,15 +356,15 @@ internal sealed class RetailChaseCamera : ICamera
// |vx| > 0.0002 AND |vy| > 0.0002 (decomp :95704, :95713). The
// horizontal-magnitude-squared form is a cleaner equivalent.
// 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.
float hMagSq = avgVelocity.X * avgVelocity.X + avgVelocity.Y * avgVelocity.Y;
if (hMagSq < 1e-4f) return baseHeading;
// Pick the projection plane normal:
- // grounded → contact_plane.N (slope-aligned camera basis)
- // airborne → world up (projection becomes a no-op because
- // baseHeading is already in the XY plane — but
+ // grounded → contact_plane.N (slope-aligned camera basis)
+ // airborne → world up (projection becomes a no-op because
+ // baseHeading is already in the XY plane — but
// keeping the code path uniform makes the airborne
// case impossible to swing vertically).
Vector3 normal;
@@ -375,13 +375,13 @@ internal sealed class RetailChaseCamera : ICamera
// Project baseHeading onto plane perpendicular to 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,
// which tilts the camera basis with the terrain.
float dot = Vector3.Dot(baseHeading, normal);
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
// the unprojected base heading.
if (projected.LengthSquared() < 1e-4f) return baseHeading;
@@ -449,7 +449,7 @@ internal sealed class RetailChaseCamera : ICamera
Vector3 right;
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)));
}
else
@@ -495,7 +495,7 @@ internal sealed class RetailChaseCamera : ICamera
///
/// Exponential-damping rate per frame.
/// alpha = clamp(stiffness * dt * 10, 0, 1). At
- /// stiffness=0.45, dt=1/60 → ~0.075
+ /// stiffness=0.45, dt=1/60 → ~0.075
/// (~150 ms half-life). Matches retail's
/// x_1 = stiffness * dt * 10 formulation.
///
@@ -508,11 +508,11 @@ internal sealed class RetailChaseCamera : ICamera
}
///
- /// Retail CameraManager::UpdateCamera dead-band (decomp 0x00456fcd–0x00457035).
+ /// Retail CameraManager::UpdateCamera dead-band (decomp 0x00456fcd–0x00457035).
/// After the per-frame lerp, if the translation step from
/// (the interpolation base = the current swept viewer) to
/// is below AND the rotation step is below
- /// , retail returns the VIEWER unchanged — the sought
+ /// , retail returns the VIEWER unchanged — the sought
/// parks exactly on it (return viewer, 0x00457025). Returns frozen=true
/// with the viewer state in that case; otherwise frozen=false with the candidate.
/// Both conditions are required (retail couples origin + rotation in the test),
@@ -562,7 +562,7 @@ internal sealed class RetailChaseCamera : ICamera
/// distance. 0 = fully opaque, 1 = fully transparent.
/// Opaque at and beyond 0.45 m; fully transparent at and within
/// 0.20 m; linear ramp between. Matches retail's CameraSet::
- /// UpdateCamera distance check (decomp :97703–97725).
+ /// UpdateCamera distance check (decomp :97703–97725).
///
internal static float ComputeTranslucency(float distance)
{
diff --git a/src/AcDream.App/Rendering/RetailCursorManager.cs b/src/AcDream.App/Rendering/RetailCursorManager.cs
index 1c49d6f9..1a73b39d 100644
--- a/src/AcDream.App/Rendering/RetailCursorManager.cs
+++ b/src/AcDream.App/Rendering/RetailCursorManager.cs
@@ -1,4 +1,4 @@
-using AcDream.App.UI;
+using AcDream.App.UI;
using AcDream.Core.Textures;
using DatReaderWriter;
using AcDream.Content;
@@ -9,7 +9,7 @@ using Silk.NET.Input;
namespace AcDream.App.Rendering;
/// Applies retail cursor feedback to Silk using dat MediaDescCursor art when available.
-internal sealed class RetailCursorManager
+public sealed class RetailCursorManager
{
private readonly IDatReaderWriter _dats;
private readonly object _datLock;
diff --git a/src/AcDream.App/Rendering/RetailPViewRenderer.cs b/src/AcDream.App/Rendering/RetailPViewRenderer.cs
index e0d37eab..9b515852 100644
--- a/src/AcDream.App/Rendering/RetailPViewRenderer.cs
+++ b/src/AcDream.App/Rendering/RetailPViewRenderer.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Numerics;
using AcDream.App.Rendering.Scene;
@@ -11,7 +11,7 @@ namespace AcDream.App.Rendering;
/// SmartBox::RenderNormalMode -> RenderDeviceD3D::DrawInside ->
/// PView::DrawInside -> ConstructView -> DrawCells.
///
-internal sealed class RetailPViewRenderer
+public sealed class RetailPViewRenderer
{
private readonly InteriorEntityPartition.IObserver? _partitionObserver;
private readonly ICurrentRenderPViewObserver? _candidateObserver;
@@ -44,7 +44,7 @@ internal sealed class RetailPViewRenderer
private readonly PortalVisibilityFrame _outdoorBuildingFrameScratch = new();
// #124: per-building look-in frames under an INTERIOR root, drawn as a
- // landscape-stage sub-pass (DrawBuildingLookIns) — never merged into the
+ // landscape-stage sub-pass (DrawBuildingLookIns) — never merged into the
// main frame (see DrawInside). Rebuilt each interior-root frame.
private readonly List _lookInFrames = new();
private readonly Stack _lookInFramePool = new();
@@ -59,7 +59,7 @@ internal sealed class RetailPViewRenderer
// MP-Alloc (2026-07-05): the frame's entity partition (ByCell/OutdoorStatic/
// Dynamics), reused across frames instead of `new`ing a Result (a Dictionary
// + 2 Lists, plus one List per visible cell) every DrawInside
- // call. See InteriorEntityPartition.Partition(Result, ...) — clears in
+ // call. See InteriorEntityPartition.Partition(Result, ...) — clears in
// place and reuses each cell's list across frames when the cell stays
// visible.
// Slice G4: this is now a diagnostic/fallback oracle only. Normal
@@ -88,7 +88,7 @@ internal sealed class RetailPViewRenderer
}
// T2 (BR-4): retail has NO distance constant on the flood-admission chain
- // (DrawBuilding → portal walk → ConstructView: viewconeCheck + side test +
+ // (DrawBuilding → portal walk → ConstructView: viewconeCheck + side test +
// GetClip + GetVisible only). The old 48 m seed cap is replaced by the
// caller's per-building frustum pre-gate on aperture bounds (GameWindow's
// gather); seeds themselves are unbounded.
@@ -113,28 +113,28 @@ internal sealed class RetailPViewRenderer
drawLiftZ: PortalVisibilityBuilder.ShellDrawLiftZ,
reuseFrame: _mainPortalFrameScratch);
- // R-A2: outdoor root — flood each nearby building SEPARATELY from its own entrance and merge
+ // R-A2: outdoor root — flood each nearby building SEPARATELY from its own entrance and merge
// the small (~2-cell) per-building views into the frame. Retail reaches building interiors via
// the terrain BSP -> DrawPortal -> ConstructView(CBldPortal) (decomp:326881/433895/433827); the
// land root itself has no portals (it floods nothing into buildings). Per-building seeding is
- // robust to the eye's ~36 µm rest jitter where the pre-R-A2 single reverse-portal flood
+ // robust to the eye's ~36 µm rest jitter where the pre-R-A2 single reverse-portal flood
// oscillated as the chase eye grazed a doorway (the indoor flap).
if (ctx.RootCell.IsOutdoorNode && ctx.NearbyBuildingCells is not null)
MergeNearbyBuildingFloods(ctx, pvFrame);
// #124: interior-root building look-ins. Retail runs the look-in INSIDE
- // the landscape stage for ANY root — LScape::draw is the FIRST call of
+ // the landscape stage for ANY root — LScape::draw is the FIRST call of
// DrawCells' outside-view branch (pc:432719), strictly BEFORE the depth
// clear (pc:432732) and the exit-portal seals (pc:432785); a far
// building seen through our doorway floods clipped to the INSTALLED
// outside view (GetClip vs current view, ConstructView(CBldPortal)
// 0x005a59a0). These frames therefore draw in DrawBuildingLookIns
- // (inside the landscape stage), NEVER merged into the main frame — a
+ // (inside the landscape stage), NEVER merged into the main frame — a
// merged cell would draw post-clear and z-fail against the root's seal
// (its geometry is beyond the door plane). The eye-side seed test
// self-excludes the root's own building (the eye is on its interior
// side). Outdoor roots keep the MergeNearbyBuildingFloods path above
- // (no depth clear under outdoor roots — the merged form is equivalent
+ // (no depth clear under outdoor roots — the merged form is equivalent
// there).
if (!ctx.RootCell.IsOutdoorNode
&& ctx.NearbyBuildingCells is not null
@@ -149,7 +149,7 @@ internal sealed class RetailPViewRenderer
// R1: draw EVERY visible cell (retail cell_draw_list), not only the cells the
// assembler handed a clip-slot. This feeds the Prepare filter + entity partition,
- // so every visible cell's shell has a prepared batch and seals — killing the grey
+ // so every visible cell's shell has a prepared batch and seals — killing the grey
// (the old clipAssembly.CellIdToSlot.Keys filter silently dropped slot-less cells).
// Per-slice trim still applies in DrawEnvCellShells (Task 4 makes it self-contained).
_drawableCellsScratch.Clear();
@@ -158,7 +158,7 @@ internal sealed class RetailPViewRenderer
passes.UseIndoorMembershipOnlyRouting();
// #124: look-in cells need prepared shell batches + their statics routed
- // into partition.ByCell (consumed ONLY by DrawBuildingLookIns — the main
+ // into partition.ByCell (consumed ONLY by DrawBuildingLookIns — the main
// cell-object pass iterates pvFrame.OrderedVisibleCells, which never
// contains them). drawableCells itself stays the MAIN flood: it feeds the
// seals, the outside-stage predicate, and the frame result.
@@ -174,22 +174,22 @@ internal sealed class RetailPViewRenderer
}
// (#176 correction, 2026-07-06: the flood-scoped light-pool rebuild that ran
- // here was the seam-floor flicker mechanism — retail's visible_cell_table is
- // the RESIDENT-cell registry, not the frame flood — and is deleted. The pool
+ // here was the seam-floor flicker mechanism — retail's visible_cell_table is
+ // the RESIDENT-cell registry, not the frame flood — and is deleted. The pool
// is built once per frame in GameWindow, player-anchored.)
passes.PrepareCellBatches(ctx, prepareCells);
- // T1 (fused BR-2/3): retail's frame order — static world, then the
- // aperture depth writes, then interior cells WHOLE far→near, then
+ // T1 (fused BR-2/3): retail's frame order — static world, then the
+ // aperture depth writes, then interior cells WHOLE far→near, then
// per-cell statics, then ALL dynamics last (retail draws objects after
// cells: PView::DrawCells Ghidra 0x005a4840; DrawBuilding 0x0059f2a0).
// The geometric shell chop (gl_ClipDistance crop, 927fd8f/9ce335e) is
- // DELETED — retail never clips cell geometry; aperture exactness comes
+ // DELETED — retail never clips cell geometry; aperture exactness comes
// from the punch/seal depth writes + the z-buffer, and the dynamics-
// last order is what makes the punch safe (the first BR-2 attempt
// punched after dynamics and erased the player, reverted 88be519).
- // T3 (BR-5): retail viewconeCheck — meshes are sphere-CULLED per view,
+ // T3 (BR-5): retail viewconeCheck — meshes are sphere-CULLED per view,
// never clipped (Ghidra 0x0054c250). Built once per frame from the
// assembled slices + this frame's view-projection.
var viewcone = ViewconeCuller.Build(
@@ -256,19 +256,19 @@ internal sealed class RetailPViewRenderer
passes.EmitDiagnostics(ctx, result);
// #118: stage assignment for dynamics under an INTERIOR root. Retail
- // draws the OUTSIDE world's objects inside the landscape stage —
+ // draws the OUTSIDE world's objects inside the landscape stage —
// PView::DrawCells runs LScape::draw FIRST (pc:432719), then the gated
// full depth clear (pc:432731-432732) and the exit-portal SEALS
// (pc:432785-432786); DrawBlock draws every landcell's objects via
// DrawSortCell (0x005a17c0, pc:430124). A dynamic deferred to our
// single last pass instead z-fails against the seal's true-depth stamp
- // the moment it stands beyond the door plane — the house-exit
+ // the moment it stands beyond the door plane — the house-exit
// clip+vanish (pinned by HouseExitWalkReplayTests). So under an
// interior root: outdoor-classified dynamics draw in the outside
// stage; an indoor dynamic whose sphere STRADDLES an exit portal
// draws in BOTH stages (retail's per-overlapped-cell shadow-part
// draw, DrawBlock pc:430056-430064) so neither body half clips at the
- // plane. Outdoor roots keep ALL dynamics in the last pass — our
+ // plane. Outdoor roots keep ALL dynamics in the last pass — our
// z-buffered equivalent of retail's painter-ordered outdoor pass (the
// BR-2 punch-after-dynamics lesson, reverted 88be519).
_outsideStageDynamics.Clear();
@@ -362,11 +362,11 @@ internal sealed class RetailPViewRenderer
// on the cell (Render::copy_view appends + view_count++, Ghidra 0x0054dfc0;
// a cell visible through two apertures holds two views, all consumed
// downstream). The old first-wins (`ContainsKey -> continue`) dropped the
- // second building flood's views whenever a cell was already in the frame —
+ // second building flood's views whenever a cell was already in the frame —
// the multiview-loss-first-wins divergence (a named #109 suspect: per-frame
// winner flips between apertures). CellView.Add dedups exact/collinear
// re-emissions (the dac8f6a CanonicalKey), so unioning is convergent.
- // OutsideView is NOT merged — the outdoor root already seeds full-screen
+ // OutsideView is NOT merged — the outdoor root already seeds full-screen
// terrain, and ConstructViewBuilding (BuildFromExterior) leaves OutsideView
// empty (it stops at exit portals once inside the building).
private static void MergeBuildingFrame(PortalVisibilityFrame target, PortalVisibilityFrame src)
@@ -393,8 +393,8 @@ internal sealed class RetailPViewRenderer
}
// #124: per-building look-in floods for an INTERIOR root, seeded clipped
- // against the OutsideView (retail: GetClip runs under the INSTALLED view —
- // the accumulated doorway region — so a far building floods only within the
+ // against the OutsideView (retail: GetClip runs under the INSTALLED view —
+ // the accumulated doorway region — so a far building floods only within the
// doorway, ConstructView(CBldPortal) 0x005a59a0 via PView::GetClip
// 0x005a4320). Same grouping as MergeNearbyBuildingFloods; the root's own
// building self-excludes via the seed eye-side test.
@@ -445,15 +445,15 @@ internal sealed class RetailPViewRenderer
private void ResetBuildingGroups()
=> _buildingGroups.Reset();
- // #124: draw the interior-root look-ins INSIDE the landscape stage —
- // retail's placement (LScape::draw → DrawBlock → DrawSortCell →
+ // #124: draw the interior-root look-ins INSIDE the landscape stage —
+ // retail's placement (LScape::draw → DrawBlock → DrawSortCell →
// DrawBuilding runs as the FIRST call of DrawCells' outside-view branch,
// pc:432719, before the depth clear + seals). Per building: punch ALL
- // apertures first (retail finishes build_draw_portals_only pass 1 — the
- // far-Z maxZ1 punch — across the whole building BSP before pass 2 floods),
- // then draw the flooded cells' shells + statics far→near (the nested
+ // apertures first (retail finishes build_draw_portals_only pass 1 — the
+ // far-Z maxZ1 punch — across the whole building BSP before pass 2 floods),
+ // then draw the flooded cells' shells + statics far→near (the nested
// DrawCells' DrawEnvCell + DrawObjCellForDummies; its outside_view is
- // empty by construction — PView ctor draw_landscape=0 — so no recursive
+ // empty by construction — PView ctor draw_landscape=0 — so no recursive
// landscape/clear/seal). Anything rasterized outside an aperture is
// repainted by the root's own shells after the depth clear, so over-draw
// here is color-safe; statics draw whole (the main viewcone has no entry
@@ -491,12 +491,12 @@ internal sealed class RetailPViewRenderer
}
}
- // Pass 2: shells + statics, far→near.
+ // Pass 2: shells + statics, far→near.
passes.UseIndoorMembershipOnlyRouting();
// Opaque shells batched per building into ONE Render (this building's
// aperture punches above already ran; z-buffer handles order and
- // lighting is per-instance CellId-keyed) — was one heavy per-frame
+ // lighting is per-instance CellId-keyed) — was one heavy per-frame
// Render per cell. Per-cell entity/particle work stays in the loop.
_shellBatch.Clear();
foreach (uint cid in frame.OrderedVisibleCells)
@@ -509,7 +509,7 @@ internal sealed class RetailPViewRenderer
uint cellId = frame.OrderedVisibleCells[i];
_oneCell.Clear();
_oneCell.Add(cellId);
- // Opaque shell batched above. Transparent stays per-cell (far→near)
+ // Opaque shell batched above. Transparent stays per-cell (far→near)
// for correct compositing; skipped for opaque-only cells.
if (passes.CellHasTransparentShell(cellId))
passes.DrawTransparentCellShells(_oneCell);
@@ -523,7 +523,7 @@ internal sealed class RetailPViewRenderer
// #131 ROOT CAUSE: DYNAMICS living in a look-in cell (the
// Holtburg hall-porch PORTAL, pCell 0xA9B4017A) draw NOWHERE
- // under an interior root — DrawDynamicsLast viewcone-culls
+ // under an interior root — DrawDynamicsLast viewcone-culls
// them (the main cone has no entries for look-in cells), and
// post-clear they would z-fail against the root's seal anyway
// (the #118 lesson). Retail draws a look-in cell's objects
@@ -576,7 +576,7 @@ internal sealed class RetailPViewRenderer
_cellStaticScratch,
_oneCell);
- // The cell-particles pass for look-in cells — retail's
+ // The cell-particles pass for look-in cells — retail's
// nested DrawCells draws objects WITH their emitters.
foreach (var slice in GetCellSlicesOrNoClip(clipAssembly, cellId))
passes.DrawCellParticles(ctx, new RetailPViewCellSliceContext(
@@ -600,7 +600,7 @@ internal sealed class RetailPViewRenderer
// #131/#132 (the FlushAlphaList deferral): retail collects ALL alpha
// draws of the landscape stage and flushes them ONCE after LScape::draw
- // (D3DPolyRender::FlushAlphaList, DrawCells pc:432722) — so translucent
+ // (D3DPolyRender::FlushAlphaList, DrawCells pc:432722) — so translucent
// landscape content (portal swirl meshes, flame particles) composites
// AFTER the building look-ins. Our dispatcher draws translucency inside
// each Draw call, so the stage is split in TWO phases instead: EARLY =
@@ -609,12 +609,12 @@ internal sealed class RetailPViewRenderer
// LATE = outside-stage dynamics' meshes + ALL scene particles +
// weather. Content drawn early and overlapped by a look-in aperture
// was otherwise overpainted by the far interior (translucents write no
- // depth to protect themselves) — the portal-swirl/candle-flame class.
+ // depth to protect themselves) — the portal-swirl/candle-flame class.
int probeSliceIndex = 0;
foreach (var slice in clipAssembly.OutsideViewSlices)
{
passes.SetTerrainClip(slice.Planes);
- // T3 (BR-5): entities are never hard-clipped — retail viewcone-
+ // T3 (BR-5): entities are never hard-clipped — retail viewcone-
// CHECKS each mesh's sphere against the view (Ghidra 0x0054c250)
// and draws it whole. The old per-slice entity clip routing
// (gl_ClipDistance via SetClipRouting) is replaced by the sphere
@@ -663,7 +663,7 @@ internal sealed class RetailPViewRenderer
});
}
- // #124: far-building look-ins draw HERE — still inside the landscape
+ // #124: far-building look-ins draw HERE — still inside the landscape
// stage (their punches mark against the terrain/exterior depth just
// drawn), strictly BEFORE the depth clear + seals below, matching
// retail's LScape::draw placement (DrawCells pc:432719 vs 432732/432785).
@@ -675,11 +675,11 @@ internal sealed class RetailPViewRenderer
frameEntityPasses,
in frameView);
- // LATE phase (per slice): outside-stage dynamics' meshes (#118 — drawn
+ // LATE phase (per slice): outside-stage dynamics' meshes (#118 — drawn
// pre-clear so the seal protects their aperture pixels; AFTER the
// look-ins so a translucent portal mesh blends over a far interior
// instead of being overpainted) + the scene-particle owners (statics +
- // dynamics cone survivors — flames ride here for the same reason).
+ // dynamics cone survivors — flames ride here for the same reason).
probeSliceIndex = 0;
foreach (var slice in clipAssembly.OutsideViewSlices)
{
@@ -700,7 +700,7 @@ internal sealed class RetailPViewRenderer
if (ownerPass)
_lateParticleOwnerScratch.Add(e.Id);
// #131 owner watchlist (throwaway): ACDREAM_DUMP_ENTITY ids
- // double as an ENTITY-id watchlist here — one line per watched
+ // double as an ENTITY-id watchlist here — one line per watched
// outdoor-static owner per CHANGE of its cone verdict.
passes.EmitOutStageOwner(
e,
@@ -764,11 +764,11 @@ internal sealed class RetailPViewRenderer
});
}
- // #131: UNATTACHED emitters (AttachedObjectId == 0 — portal swirls,
+ // #131: UNATTACHED emitters (AttachedObjectId == 0 — portal swirls,
// campfires, ground effects anchored at a position) have no owner id
// to ride any of the id-filtered particle passes. The outdoor root
// has the dedicated T3 pass for them; an INTERIOR root had NO pass
- // at all. Draw them ONCE per frame (not per slice — alpha particles
+ // at all. Draw them ONCE per frame (not per slice — alpha particles
// must not double-draw, the #121 lesson), at the END of the landscape
// stage: after the clear they would z-fail against the doorway seal.
if (!ctx.RootCell.IsOutdoorNode)
@@ -780,7 +780,7 @@ internal sealed class RetailPViewRenderer
passes.FlushLandscapeAlpha();
// T1: retail clears the FULL depth buffer ONCE between the outside
- // stage and the interior stage (PView::DrawCells, Ghidra 0x005a4840 —
+ // stage and the interior stage (PView::DrawCells, Ghidra 0x005a4840 —
// Clear gated on portalsDrawnCount; exact gate semantics is a plan
// open question, staged as "any outside slice drawn"), then re-stamps
// every outside-leading portal's TRUE depth (the seals,
@@ -819,20 +819,20 @@ internal sealed class RetailPViewRenderer
IRetailPViewPassExecutor passes,
PortalVisibilityFrame pvFrame)
{
- // T1 (fused BR-2/3): retail DrawCells Loop 2 — every visible cell's
- // shell drawn WHOLE, reverse cell_draw_list (far→near), drawn once.
+ // T1 (fused BR-2/3): retail DrawCells Loop 2 — every visible cell's
+ // shell drawn WHOLE, reverse cell_draw_list (far→near), drawn once.
// Retail NEVER clips cell geometry: the production path is the
// prebuilt mesh (DrawEnvCell use_built_mesh, pc:427905; the
// planeMask=0xffffffff legacy submit means skip-all-edges), and
// aperture exactness comes from the punch/seal depth writes + the
// z-buffer + this order. The former gl_ClipDistance chop
// (927fd8f/9ce335e, #114) is deleted with this rewrite.
- // Per-cell opaque+transparent keeps the far→near transparent
+ // Per-cell opaque+transparent keeps the far→near transparent
// compositing the per-cell loop already provided.
passes.UseIndoorMembershipOnlyRouting();
// Opaque: ONE batched Render for all shell cells (was one heavy per-frame
- // Render call PER cell — the dense-town FPS sink, ~94 calls/24.75ms at
+ // Render call PER cell — the dense-town FPS sink, ~94 calls/24.75ms at
// Arwic). Opaque needs no draw order (z-buffer), and lighting is
// per-instance (CellId-keyed light SSBO in EnvCellRenderer.RenderModernMDI-
// Internal), so cross-cell batching is visually identical. The filtered
@@ -856,16 +856,16 @@ internal sealed class RetailPViewRenderer
passes.DrawTransparentCellShellsOrdered(_orderedTransparentShellCells);
}
- // T1: the frame's single LAST entity pass — ALL server-spawned dynamics
+ // T1: the frame's single LAST entity pass — ALL server-spawned dynamics
// (player, NPCs, doors, items), indoor or out, drawn after the static
// world + punches + interior cells. Depth-tested, never hard-clipped
- // (retail draws objects per cell AFTER cells and viewcone-culls them —
+ // (retail draws objects per cell AFTER cells and viewcone-culls them —
// PView::DrawCells epilogue Ghidra 0x005a4840; the sphere-vs-view cull is
// T3). Drawing dynamics last is what makes the aperture punch safe.
- // T3 (BR-5): each dynamic is viewcone-culled like retail — sphere vs its
+ // T3 (BR-5): each dynamic is viewcone-culled like retail — sphere vs its
// cell's views; outdoor/unresolved vs the outside views (pass-all under
// the outdoor root's full-screen outside view). A dynamic in a NON-flooded
- // room culls HERE — retail never reaches an object whose cell is not in
+ // room culls HERE — retail never reaches an object whose cell is not in
// the draw list; the partition keeps routing it so the CULL (not the
// visibility set) drops it, exactly retail's shape.
private void DrawDynamicsLast(
@@ -928,12 +928,12 @@ internal sealed class RetailPViewRenderer
&& AcDream.App.Streaming.EntityVanishProbe.PlayerGuid != 0
&& e.ServerGuid == AcDream.App.Streaming.EntityVanishProbe.PlayerGuid;
// #118: under an interior root, outdoor-classified dynamics drew in
- // the outside stage (pre-clear, seal-protected) — retail draws them
+ // the outside stage (pre-clear, seal-protected) — retail draws them
// via LScape::draw's per-landcell DrawSortCell, never in the
// post-seal cell-object epilogue (PView::DrawCells pc:432719 vs
// pc:432878). Drawing them here instead z-fails them against the
// seal. Indoor dynamics (incl. exit-portal straddlers, which drew
- // in BOTH stages) stay — this pass is retail's loop C.
+ // in BOTH stages) stay — this pass is retail's loop C.
if (!rootIsOutdoor && !indoor)
{
if (isProbePlayer)
@@ -976,12 +976,12 @@ internal sealed class RetailPViewRenderer
visibleCellIds: null);
// #121: dynamics' attached emitters (portal swirls, creature effects)
- // gate through the SAME cone-surviving owner set as their meshes —
+ // gate through the SAME cone-surviving owner set as their meshes —
// retail draws emitters with the owner object. Before this callback,
// dynamics' emitters fell through EVERY particle filter under the pview
// path (the landscape slice carries outdoor statics + #118 outside-
// stage dynamics; the cell callback carries cell statics; T4 deleted
- // the old clipRoot==null global pass from normal frames) — all world
+ // the old clipRoot==null global pass from normal frames) — all world
// portals went invisible. Outside-stage dynamics are excluded here:
// their emitters already drew in the landscape slice (alpha-blended
// particles must not double-draw, unlike the depth-idempotent meshes).
@@ -1049,29 +1049,29 @@ internal sealed class RetailPViewRenderer
return;
}
- // T1: per-cell STATIC object lists only (dat-baked 0x40 statics) —
- // dynamics moved to DrawDynamicsLast. Far→near with the cells, after
- // the shells (retail DrawCells epilogue: PortalList = cell's views →
+ // T1: per-cell STATIC object lists only (dat-baked 0x40 statics) —
+ // dynamics moved to DrawDynamicsLast. Far→near with the cells, after
+ // the shells (retail DrawCells epilogue: PortalList = cell's views →
// DrawObjCell, Ghidra 0x005a4840). T3 (BR-5): each static's sphere is
- // tested against ITS CELL's views (retail viewconeCheck) — the
+ // tested against ITS CELL's views (retail viewconeCheck) — the
// statics-through-walls fix: a static whose sphere is outside every
// view of its cell no longer paints through the wall (the cottage
// phantom staircase's draw path).
// Dense-town FPS iteration-1 (spec 2026-06-23-cellobject-draw-batching):
// the per-cell DrawEntityBucket calls below were the top CPU sink at Arwic
// (cellobjects ~3.5 ms/frame; each WbDrawDispatcher.Draw orphans 6 SSBOs +
- // full state setup). Collapse them into ONE cross-cell batched draw — the
+ // full state setup). Collapse them into ONE cross-cell batched draw — the
// shipped cells-shell batching pattern applied to cell OBJECTS. Two loops
// preserve the statics-before-particles depth order: loop 1 culls +
// accumulates every cell's survivors and draws them once; loop 2 runs the
// per-cell particle passes AFTER the statics own the depth buffer (particles
// depth-test but write no depth). The dispatcher sorts opaque front-to-back
// and transparent back-to-front by group distance (WbDrawDispatcher.cs:
- // 1469-1470), so cross-cell batching composites correctly — equal-or-better
+ // 1469-1470), so cross-cell batching composites correctly — equal-or-better
// than the old per-cell-bucketed order. visibleCellIds = the union of cells,
// so the dispatcher admits exactly the same survivor set.
- // Loop 1: per-cell viewcone cull → accumulate survivors + the union of cells.
+ // Loop 1: per-cell viewcone cull → accumulate survivors + the union of cells.
_allCellStatics.Clear();
_cellObjCells.Clear();
for (int i = pvFrame.OrderedVisibleCells.Count - 1; i >= 0; i--)
@@ -1099,7 +1099,7 @@ internal sealed class RetailPViewRenderer
}
// ONE batched static-object draw for every visible cell (was N per-cell
- // WbDrawDispatcher.Draw calls). T1: per-cell STATIC lists only — dynamics
+ // WbDrawDispatcher.Draw calls). T1: per-cell STATIC lists only — dynamics
// draw in DrawDynamicsLast. T3 (BR-5): each static was sphere-tested against
// ITS cell's views above (the statics-through-walls fix is preserved by the
// cull; only the draw is batched).
@@ -1124,20 +1124,20 @@ internal sealed class RetailPViewRenderer
_cellObjCells);
}
- // Cell-particle pass — consolidated across ALL visible cells into ONE
+ // Cell-particle pass — consolidated across ALL visible cells into ONE
// draw. Was per-cell, and each call re-walked the ENTIRE live particle set
- // (RetailPViewPassExecutor.DrawCellParticles → ParticleRenderer.Draw enumerates every
- // live emitter), i.e. O(cells × particles) — the dense-town cellobjects
+ // (RetailPViewPassExecutor.DrawCellParticles → ParticleRenderer.Draw enumerates every
+ // live emitter), i.e. O(cells × particles) — the dense-town cellobjects
// sink (~5 ms at Arwic). Static owners are disjoint per cell, so the UNION
// (= _allCellStatics, already accumulated above for the batched draw) draws
// EXACTLY the same emitters: the callback gates on owner id (the cone-
// surviving set), the renderer sorts globally back-to-front, and the per-
// cell slice was never used for clipping (the scissor gate was deleted in
- // T3 — RetailPViewPassExecutor.DrawCellParticles disables clip distances). Runs after
+ // T3 — RetailPViewPassExecutor.DrawCellParticles disables clip distances). Runs after
// the batched static draw so emitters depth-test against the statics now in
// the buffer (the statics-before-particles order). cellId/slice are unused
- // by the particle pass — pass NoClipSlice + the union owner list. This also
- // drops the per-cell BuildDrawList allocations (N → 1).
+ // by the particle pass — pass NoClipSlice + the union owner list. This also
+ // drops the per-cell BuildDrawList allocations (N → 1).
if (frameEntityPasses is not null
|| _allCellStatics.Count > 0)
{
@@ -1199,7 +1199,7 @@ internal sealed class RetailPViewRenderer
private readonly List _cellStaticScratch = new();
private readonly List _dynamicsScratch = new();
// #118: dynamics assigned to the OUTSIDE stage this frame (interior roots
- // only) — outdoor-classified + exit-portal straddlers. Cleared per frame.
+ // only) — outdoor-classified + exit-portal straddlers. Cleared per frame.
private readonly List _outsideStageDynamics = new();
// Dense-town FPS iteration-1 (cellobject batching): all visible cells'
// viewcone-surviving statics accumulated for ONE batched DrawEntityBucket,
@@ -1274,17 +1274,17 @@ internal sealed class RetailPViewRenderer
///
/// #118 stage assignment for a dynamic under an INTERIOR root: does it draw
- /// in the OUTSIDE (landscape) stage — before the gated depth clear and the
- /// exit-portal seals — like retail's per-landcell object draw
- /// (LScape::draw → DrawBlock 0x005a17c0 → DrawSortCell pc:430124, run at
+ /// in the OUTSIDE (landscape) stage — before the gated depth clear and the
+ /// exit-portal seals — like retail's per-landcell object draw
+ /// (LScape::draw → DrawBlock 0x005a17c0 → DrawSortCell pc:430124, run at
/// the top of PView::DrawCells pc:432719)?
///
/// True for outdoor-classified dynamics (their fragments lie beyond the
/// door plane and would z-fail the seal in the last pass), and for INDOOR
/// dynamics whose sphere straddles an exit-portal plane of their flood-
- /// visible cell — retail draws an object once per overlapped shadow cell
+ /// visible cell — retail draws an object once per overlapped shadow cell
/// (DrawBlock pc:430056-430064), so a threshold-straddling body draws in
- /// both stages and neither half clips at the plane. Pure — also driven
+ /// both stages and neither half clips at the plane. Pure — also driven
/// headlessly by HouseExitWalkReplayTests as the ordering contract.
///
public static bool DynamicDrawsInOutsideStage(
@@ -1299,7 +1299,7 @@ internal sealed class RetailPViewRenderer
uint cellId = parentCellId!.Value;
if (!drawableCells.Contains(cellId))
- return false; // not in the flood — the last-pass cone cull owns it
+ return false; // not in the flood — the last-pass cone cull owns it
var cell = cells.Find(cellId);
if (cell is null)
return false;
@@ -1320,7 +1320,7 @@ internal sealed class RetailPViewRenderer
return false;
}
- // Conservative bounding sphere from the entity's cached AABB — the same
+ // Conservative bounding sphere from the entity's cached AABB — the same
// bounds source the dispatcher's frustum cull uses.
private static void EntitySphere(WorldEntity e, out Vector3 center, out float radius)
{
@@ -1343,7 +1343,7 @@ internal sealed class RetailPViewRenderer
}
-internal interface IRetailPViewCellSource
+public interface IRetailPViewCellSource
{
LoadedCell? Find(uint cellId);
}
@@ -1354,7 +1354,7 @@ internal interface IRetailPViewCellSource
/// pass only; visibility construction and draw ordering remain renderer-owned.
/// All frame inputs and results are borrowed for the duration of the call.
///
-internal interface IRetailPViewPassExecutor
+public interface IRetailPViewPassExecutor
{
void AbortFrame();
void BeginFrame();
@@ -1565,7 +1565,7 @@ internal sealed class BuildingGroupScratch
}
}
-internal sealed class RetailPViewFrameInput
+public sealed class RetailPViewFrameInput
{
public LoadedCell RootCell { get; private set; } = null!;
@@ -1669,7 +1669,7 @@ internal sealed class RetailPViewFrameInput
/// frame objects are deliberately reused to keep the render loop allocation
/// free; consumers must copy any state they need to retain asynchronously.
///
-internal sealed class RetailPViewFrameResult
+public sealed class RetailPViewFrameResult
{
public PortalVisibilityFrame PortalFrame { get; private set; } = null!;
public ClipFrameAssembly ClipAssembly { get; private set; } = null!;
@@ -1712,17 +1712,17 @@ internal sealed class RetailPViewFrameResult
diagnosticPartition);
}
-internal readonly record struct RetailPViewLandscapeSliceContext(
+public readonly record struct RetailPViewLandscapeSliceContext(
ClipViewSlice Slice,
IReadOnlyList OutdoorEntities)
{
internal RenderFrameEntityDrawRequest? EntityDraw { get; init; }
}
-/// #131/#132: the late landscape phase's per-slice payload —
+/// #131/#132: the late landscape phase's per-slice payload —
/// outside-stage dynamics to mesh-draw, plus the full scene-particle owner
/// set (statics + dynamics cone survivors) the attached-emitter filter keys on.
-internal readonly record struct RetailPViewLandscapeLateSliceContext(
+public readonly record struct RetailPViewLandscapeLateSliceContext(
ClipViewSlice Slice,
IReadOnlyList Dynamics,
IReadOnlySet ParticleOwnerIds)
@@ -1730,7 +1730,7 @@ internal readonly record struct RetailPViewLandscapeLateSliceContext(
internal RenderFrameEntityDrawRequest? EntityDraw { get; init; }
}
-internal readonly record struct RetailPViewCellSliceContext(
+public readonly record struct RetailPViewCellSliceContext(
uint CellId,
ClipViewSlice Slice,
IReadOnlySet ParticleOwnerIds);
diff --git a/src/AcDream.App/Rendering/RetailParticleGeometryClassifier.cs b/src/AcDream.App/Rendering/RetailParticleGeometryClassifier.cs
index 484fef9e..2d221c27 100644
--- a/src/AcDream.App/Rendering/RetailParticleGeometryClassifier.cs
+++ b/src/AcDream.App/Rendering/RetailParticleGeometryClassifier.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using AcDream.Core.Meshing;
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 mode uses the camera-facing 2D presentation.
///
-internal static class RetailParticleGeometryClassifier
+public static class RetailParticleGeometryClassifier
{
public static RetailParticleGeometryKind Classify(uint? firstDegradeMode)
=> firstDegradeMode is uint mode && mode != 1u
diff --git a/src/AcDream.App/Rendering/SamplerCache.cs b/src/AcDream.App/Rendering/SamplerCache.cs
index fc874764..91647bab 100644
--- a/src/AcDream.App/Rendering/SamplerCache.cs
+++ b/src/AcDream.App/Rendering/SamplerCache.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using Silk.NET.OpenGL;
namespace AcDream.App.Rendering;
@@ -7,7 +7,7 @@ namespace AcDream.App.Rendering;
/// Two persistent GL sampler objects (Repeat + ClampToEdge) created once
/// per GL context. Renderers the appropriate
/// one to a texture unit instead of mutating per-texture
-/// GL_TEXTURE_WRAP_S/T state — sampler state overrides the
+/// GL_TEXTURE_WRAP_S/T state — sampler state overrides the
/// texture's own wrap parameters, so two renderers can share the same
/// texture handle but sample it with different wrap modes safely.
///
@@ -16,7 +16,7 @@ namespace AcDream.App.Rendering;
/// references/WorldBuilder/Chorizite.OpenGLSDLBackend/OpenGLGraphicsDevice.cs:115-132.
/// Filter modes match 's upload defaults
/// (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].
///
///
@@ -28,7 +28,7 @@ namespace AcDream.App.Rendering;
/// per-texture wrap state.
///
///
-internal sealed class SamplerCache : IDisposable
+public sealed class SamplerCache : IDisposable
{
private readonly GL _gl;
private readonly ResourceCleanupGroup _resources;
diff --git a/src/AcDream.App/Rendering/SceneLightingUboBinding.cs b/src/AcDream.App/Rendering/SceneLightingUboBinding.cs
index d5ae01e6..72b02277 100644
--- a/src/AcDream.App/Rendering/SceneLightingUboBinding.cs
+++ b/src/AcDream.App/Rendering/SceneLightingUboBinding.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Runtime.InteropServices;
using AcDream.App.Rendering.Wb;
using AcDream.Core.Lighting;
@@ -13,7 +13,7 @@ namespace AcDream.App.Rendering;
/// consistent data without per-shader re-upload.
///
///
-/// Usage (r12 §13.2 + r13 §12.3):
+/// Usage (r12 §13.2 + r13 §12.3):
///
/// - Instantiate once at startup, after the GL context exists.
/// - Each frame, after , call with a freshly-built .
@@ -21,7 +21,7 @@ namespace AcDream.App.Rendering;
///
///
///
-internal sealed unsafe class SceneLightingUboBinding : IDisposable
+public sealed unsafe class SceneLightingUboBinding : IDisposable
{
private readonly GL _gl;
private uint _ubo;
diff --git a/src/AcDream.App/Rendering/ScreenPolygonClip.cs b/src/AcDream.App/Rendering/ScreenPolygonClip.cs
index 16617645..666b5fff 100644
--- a/src/AcDream.App/Rendering/ScreenPolygonClip.cs
+++ b/src/AcDream.App/Rendering/ScreenPolygonClip.cs
@@ -1,4 +1,4 @@
-// ScreenPolygonClip.cs
+// ScreenPolygonClip.cs
//
// Phase A8.F: 2D convex-polygon intersection (Sutherland-Hodgman).
// Ports the BEHAVIOR of retail ACRender::polyClipFinish (the screen-space
@@ -11,7 +11,7 @@ using System.Numerics;
namespace AcDream.App.Rendering;
-internal static class ScreenPolygonClip
+public static class ScreenPolygonClip
{
private const float Eps = 1e-7f;
diff --git a/src/AcDream.App/Rendering/Shader.cs b/src/AcDream.App/Rendering/Shader.cs
index 1a431ce4..b74960ae 100644
--- a/src/AcDream.App/Rendering/Shader.cs
+++ b/src/AcDream.App/Rendering/Shader.cs
@@ -1,9 +1,9 @@
-using System.Numerics;
+using System.Numerics;
using Silk.NET.OpenGL;
namespace AcDream.App.Rendering;
-internal sealed class Shader : IDisposable
+public sealed class Shader : IDisposable
{
private readonly GL _gl;
private readonly Dictionary _uniformLocations = new(StringComparer.Ordinal);
@@ -15,10 +15,10 @@ internal sealed class Shader : IDisposable
}
///
- /// 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
/// is true, the text of
- /// Shaders/common.glsl — sitting alongside
- /// — is spliced into both sources right after their leading
+ /// Shaders/common.glsl — sitting alongside
+ /// — is spliced into both sources right after their leading
/// #version/#extension block. GL has no #include, so this
/// is plain string concatenation at load time rather than a GLSL-level
/// mechanism. Every existing two-argument-path caller is unaffected: the
@@ -49,14 +49,10 @@ internal sealed class Shader : IDisposable
/// Inserts right after the shader's leading
/// #version/#extension/blank-line block. GLSL requires
/// #version 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.
- ///
- /// Internal (not private) so
- /// can reuse the exact same splice for RHI-created pipelines rather than
- /// a second copy of this parsing.
///
- internal static string InjectPreamble(string source, string preamble)
+ private static string InjectPreamble(string source, string preamble)
{
int insertAt = 0;
int lineStart = 0;
diff --git a/src/AcDream.App/Rendering/Shaders/debug_line.vert b/src/AcDream.App/Rendering/Shaders/debug_line.vert
index 6c7eb398..f6340133 100644
--- a/src/AcDream.App/Rendering/Shaders/debug_line.vert
+++ b/src/AcDream.App/Rendering/Shaders/debug_line.vert
@@ -2,16 +2,12 @@
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec3 aColor;
-// Campaign V slice V4a: the shared GpuPushConstants block carries ONE
-// 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;
+uniform mat4 uView;
+uniform mat4 uProjection;
out vec3 vColor;
void main() {
vColor = aColor;
- gl_Position = uViewProjection * vec4(aPos, 1.0);
+ gl_Position = uProjection * uView * vec4(aPos, 1.0);
}
diff --git a/src/AcDream.App/Rendering/Shaders/ui_text.frag b/src/AcDream.App/Rendering/Shaders/ui_text.frag
index 09a94ec1..75c9cd3d 100644
--- a/src/AcDream.App/Rendering/Shaders/ui_text.frag
+++ b/src/AcDream.App/Rendering/Shaders/ui_text.frag
@@ -1,36 +1,19 @@
#version 430 core
-#extension GL_ARB_bindless_texture : require
in vec2 vUv;
in vec4 vColor;
out vec4 FragColor;
-// Campaign V slice V4a: the bound sprite/glyph texture arrives as a
-// 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
+uniform sampler2D uTex;
+uniform int uUseTexture;
void main() {
if (uUseTexture == 1) {
// Font atlas is a single-channel R8 texture; red = coverage alpha.
- sampler2D tex = sampler2D(ACDREAM_TEXTURE_HANDLE(uTextureIndexA));
- float coverage = texture(tex, vUv).r;
+ float coverage = texture(uTex, vUv).r;
FragColor = vec4(vColor.rgb, vColor.a * coverage);
} else if (uUseTexture == 2) {
// RGBA dat sprite (decoded to RGBA8); modulate by tint/alpha.
- sampler2D tex = sampler2D(ACDREAM_TEXTURE_HANDLE(uTextureIndexA));
- FragColor = texture(tex, vUv) * vColor;
+ FragColor = texture(uTex, vUv) * vColor;
} else {
FragColor = vColor;
}
diff --git a/src/AcDream.App/Rendering/Shaders/ui_text.vert b/src/AcDream.App/Rendering/Shaders/ui_text.vert
index 037b2809..0cc6c932 100644
--- a/src/AcDream.App/Rendering/Shaders/ui_text.vert
+++ b/src/AcDream.App/Rendering/Shaders/ui_text.vert
@@ -3,19 +3,12 @@ layout(location = 0) in vec2 aPos; // screen pixels, origin top-left
layout(location = 1) in vec2 aUv;
layout(location = 2) in vec4 aColor;
-// 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
+uniform vec2 uScreenSize;
out vec2 vUv;
out vec4 vColor;
void main() {
- vec2 uScreenSize = vec2(uParamA, uParamB);
// Convert pixel coords (origin top-left, +Y down) to NDC (origin center, +Y up).
vec2 ndc = vec2(
aPos.x / uScreenSize.x * 2.0 - 1.0,
diff --git a/src/AcDream.App/Rendering/Sky/SkyRenderer.cs b/src/AcDream.App/Rendering/Sky/SkyRenderer.cs
index 5173ad04..26520bb8 100644
--- a/src/AcDream.App/Rendering/Sky/SkyRenderer.cs
+++ b/src/AcDream.App/Rendering/Sky/SkyRenderer.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
@@ -16,7 +16,7 @@ namespace AcDream.App.Rendering.Sky;
///
/// Port of references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/SkyboxRenderManager.cs.
/// 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 is
/// visible in a window of day-fraction space, sweeps from
/// BeginAngle to EndAngle across the sky, and samples its
@@ -25,11 +25,11 @@ namespace AcDream.App.Rendering.Sky;
///
/// GL state delta per frame:
///
-/// - Depth mask OFF, depth test OFF, cull OFF — the sky
+///
- Depth mask OFF, depth test OFF, cull OFF — the sky
/// should never occlude scene geometry.
-/// - Separate projection matrix with a 0.1–1e6 near/far
+///
- Separate projection matrix with a 0.1–1e6 near/far
/// so mesh vertices at large distance don't clip.
-/// - View matrix with translation zeroed — sky is
+///
- View matrix with translation zeroed — sky is
/// always camera-centred; moving doesn't get you closer to the
/// sun.
///
@@ -38,12 +38,12 @@ namespace AcDream.App.Rendering.Sky;
///
/// Meshes are built lazily per GfxObj id on first reference. The
/// per-object arc transform matches WorldBuilder's composition:
-/// scale × RotZ(-heading) × RotY(-rotation) — the negative signs
+/// scale × RotZ(-heading) × RotY(-rotation) — the negative signs
/// come from AC's Z-up right-handed convention where heading is
/// measured clockwise from north.
///
///
-internal sealed unsafe class SkyRenderer : IDisposable
+public sealed unsafe class SkyRenderer : IDisposable
{
private readonly GL _gl;
private readonly IDatReaderWriter _dats;
@@ -54,11 +54,11 @@ internal sealed unsafe class SkyRenderer : IDisposable
// Lazily-built GPU resources per sky-GfxObj.
private readonly Dictionary> _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).
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.
public float Near { get; set; } = 0.1f;
public float Far { get; set; } = 1_000_000f;
@@ -73,7 +73,7 @@ internal sealed unsafe class SkyRenderer : IDisposable
}
///
- /// Draw all NON-WEATHER sky objects (dome, sun, moon, stars, clouds —
+ /// Draw all NON-WEATHER sky objects (dome, sun, moon, stars, clouds —
/// every SkyObject with Properties & 0x04 == 0).
/// Called BEFORE the scene; terrain / meshes / debug lines / overlay
/// land on top via depth-test.
@@ -82,7 +82,7 @@ internal sealed unsafe class SkyRenderer : IDisposable
/// Mirrors the first half of retail's LScape::draw at
/// 0x00506330: that function calls GameSky::Draw(0)
/// (sky pass) before the landblock loop, then GameSky::Draw(1)
- /// (weather pass) after. acdream splits the same way — see
+ /// (weather pass) after. acdream splits the same way — see
/// for the post-scene companion.
///
///
@@ -90,17 +90,17 @@ internal sealed unsafe class SkyRenderer : IDisposable
/// Each submesh renders with retail's per-vertex lighting formula:
/// tint = clamp(emissive + ambient + max(dot(N, -sunDir), 0) * sunColor, 0, 1)
/// where emissive is the submesh's Surface.Luminosity
- /// float (1.0 for dome + sun + moon → texture passthrough via
- /// saturation; 0.0 for clouds → get the full time-of-day tint).
+ /// float (1.0 for dome + sun + moon → texture passthrough via
+ /// saturation; 0.0 for clouds → get the full time-of-day tint).
/// supplies the AmbientColor and SunColor
/// already pre-multiplied by AmbBright / DirBright (loader-side).
///
///
- /// See docs/research/2026-04-23-sky-retail-verbatim.md §6 for
+ /// See docs/research/2026-04-23-sky-retail-verbatim.md §6 for
/// the full decompile citation. The empirical Dereth dump (
/// ACDREAM_DUMP_SKY=1, logged 2026-04-23) confirmed the
/// SurfaceType.Luminous flag bit is NOT set on any Dereth sky
- /// mesh — the differentiator is the Surface.Luminosity FLOAT
+ /// mesh — the differentiator is the Surface.Luminosity FLOAT
/// field.
///
///
@@ -118,13 +118,13 @@ internal sealed unsafe class SkyRenderer : IDisposable
/// Draw the POST-SCENE sky objects (the foreground rain mesh
/// 0x01004C44 on Rainy DayGroups, plus any other SkyObject with
/// Properties & 0x01 != 0). 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 LScape::draw at 0x00506330, where
/// GameSky::Draw(1) fires after the DrawBlock loop and
/// renders the after_sky_cell contents. With depth-test
/// disabled and additive blend (the rain Surface flag includes
/// 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.
///
/// Method name kept as RenderWeather for API stability; the
@@ -149,7 +149,7 @@ internal sealed unsafe class SkyRenderer : IDisposable
/// Sets up the same GL state for both (depth-test off, additive +
/// alpha-blend per submesh, camera-anchored translation) and iterates
/// only the SkyObjects matching the requested partition by
- /// — bit 0x01 per the
+ /// — bit 0x01 per the
/// retail decomp at GameSky::MakeObject (0x00506ee0).
///
private void RenderPass(
@@ -171,7 +171,7 @@ internal sealed unsafe class SkyRenderer : IDisposable
// that FOV here, including the near-180-degree teleport transition.
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.
var skyView = camera.View;
skyView.M41 = 0f;
@@ -183,7 +183,7 @@ internal sealed unsafe class SkyRenderer : IDisposable
_shader.SetMatrix4("uSkyProjection", skyProj);
// 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
// derived from the keyframe's DirHeading/DirPitch.
_shader.SetVec3("uAmbientColor", keyframe.AmbientColor);
@@ -204,14 +204,14 @@ internal sealed unsafe class SkyRenderer : IDisposable
bool wasCullFace = _gl.IsEnabled(EnableCap.CullFace);
_gl.Disable(EnableCap.CullFace);
_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 =
// 0x10000) get GL_SRC_ALPHA / GL_ONE; alpha-blended (clouds, dome
// with Alpha flag) get GL_SRC_ALPHA / GL_ONE_MINUS_SRC_ALPHA.
_gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
// 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.
var replaces = PickReplaces(group, dayFraction);
@@ -220,19 +220,19 @@ internal sealed unsafe class SkyRenderer : IDisposable
for (int i = 0; i < group.SkyObjects.Count; 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
// the post-scene pass (bit set). Mirrors retail
// GameSky::CreateDeletePhysicsObjects at 0x005073c0 / decomp
// line 269036 which routes (Properties & 1) into
// before_sky_cell vs after_sky_cell, and GameSky::Draw at
// 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.
// Earlier acdream incorrectly used IsWeather for this
// partition, putting the outer rain cylinder 0x01004C42
// (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.IsVisible(dayFraction)) continue;
// Retail GameSky::Draw (0x00506ff0) skips Properties bit 0x02
@@ -254,7 +254,7 @@ internal sealed unsafe class SkyRenderer : IDisposable
// fallback at the inner loop never fired (1f is always > 0).
// RainMeshProbe (committed b8e0857) confirmed empirically that
// 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 replaceDiffuse = float.NaN;
if (replaces.TryGetValue((uint)i, out var rep))
@@ -289,7 +289,7 @@ internal sealed unsafe class SkyRenderer : IDisposable
// int32_t var_4_1 = 0xc2f00000; // 0xc2f00000 == -120.0f
//
// 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
// entry as also weather-flagged so the previous unconditional
// offset was a no-op divergence, but a future DayGroup with a
@@ -302,15 +302,15 @@ internal sealed unsafe class SkyRenderer : IDisposable
// cylinder bottom sits at z=0.11 ABOVE the camera (skyView
// translation is zeroed so model-origin == camera); looking
// horizontally shows nothing. With -120m the cylinder spans z
- // = (camera-119.89)..(camera+694.90) — camera is inside,
- // looking in any direction shows surrounding walls — the
+ // = (camera-119.89)..(camera+694.90) — camera is inside,
+ // looking in any direction shows surrounding walls — the
// volumetric foreground-rain look retail has.
if (postScenePass && obj.IsWeather && (obj.Properties & 0x08u) == 0u)
model = model * Matrix4x4.CreateTranslation(0f, 0f, -120f);
_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
// loss in the fragment UV.
float uOffset = (obj.TexVelocityX * secondsSinceStart) % 1f;
@@ -328,7 +328,7 @@ internal sealed unsafe class SkyRenderer : IDisposable
// sky dome is Base1Image (Opaque, mapped to
// SrcAlpha/InvSrcAlpha for a no-op blend at alpha=1).
// 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.
if (sub.IsAdditive)
_gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.One);
@@ -338,24 +338,24 @@ internal sealed unsafe class SkyRenderer : IDisposable
// Emissive source picks the surface's authored Luminosity by
// default; the per-keyframe replace data can OVERRIDE
// (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
// promoting bright-keyframe clouds when the keyframe asks.
//
// 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);
- // stars/clouds → Lum=0.0 → vTint = ambient + diffuse →
+ // stars/clouds → Lum=0.0 → vTint = ambient + diffuse →
// 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.
//
// Pre-fix: the replace-override variable defaulted to 1f and
// 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
// 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).
float effEmissive = float.IsNaN(replaceLuminosity)
? sub.SurfLuminosity
@@ -374,7 +374,7 @@ internal sealed unsafe class SkyRenderer : IDisposable
// Retail D3DPolyRender::SetSurface at 0x59c882 calls
// 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
// on additive surfaces keeps the sun bright at horizon
// dusk/dawn (where fog would otherwise dim it to fog color).
@@ -477,13 +477,13 @@ internal sealed unsafe class SkyRenderer : IDisposable
/// Lazy mesh build for a sky object. Handles two cases:
///
/// -
- /// 0x010xxxxx — direct . Reuses
+ /// 0x010xxxxx — direct . Reuses
/// so the pos/neg polygon
/// splitting logic stays consistent with the main static-mesh
/// pipeline. Most sky meshes are single-surface.
///
/// -
- /// 0x020xxxxx — . The agent at
+ /// 0x020xxxxx — . The agent at
/// 2026-04-27 found these Setup-backed sky objects (e.g.
/// 0x02000588, 0x02000589, 0x02000714,
/// 0x02000BA6) were silently dropped: every cache miss
@@ -496,7 +496,7 @@ internal sealed unsafe class SkyRenderer : IDisposable
/// Setup.Parts at the default placement frame and
/// produces submeshes for each
/// 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).
///
///
@@ -504,7 +504,7 @@ internal sealed unsafe class SkyRenderer : IDisposable
/// Even with this fix the visible aurora-style sheen most retail
/// rainy/cloudy setups produce comes from the pes_id field
/// on each (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;
/// the dynamic particle half is deferred.
///
@@ -550,8 +550,8 @@ internal sealed unsafe class SkyRenderer : IDisposable
// Phase 1 diagnostic: dump Surface.Type flags on every sky GfxObj
// once, so we can determine which submeshes carry Luminous (0x40)
// vs plain-lit. This settles the retail "cloud tint = per-vertex
- // lighting on non-Luminous meshes" hypothesis — see
- // docs/research/2026-04-23-sky-retail-verbatim.md §6.
+ // lighting on non-Luminous meshes" hypothesis — see
+ // docs/research/2026-04-23-sky-retail-verbatim.md §6.
if (System.Environment.GetEnvironmentVariable("ACDREAM_DUMP_SKY") == "1")
DumpGfxObjSurfaces(gfxObjId, gfx, subMeshes);
@@ -565,14 +565,14 @@ internal sealed unsafe class SkyRenderer : IDisposable
/// Setup-backed sky object loader. Walks at
/// the default placement frame, builds submeshes via
/// , 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
/// about (the dynamic look comes from pes_id particles, not
/// the underlying mesh).
///
/// Mirrors retail's at
- /// decomp 280484 dispatching type 7 → CPartArray::CreateSetup
- /// → CSetup::SetSetupID, which loads the setup and instantiates
+ /// decomp 280484 dispatching type 7 → CPartArray::CreateSetup
+ /// → CSetup::SetSetupID, which loads the setup and instantiates
/// each part as a separate CPhysicsObj child. We collapse the
/// children into a flat submesh list because the sky pass renders
/// without per-part transforms anyway.
@@ -654,13 +654,13 @@ internal sealed unsafe class SkyRenderer : IDisposable
continue;
}
- // SurfaceType is a flag enum — `ToString()` gives the
+ // SurfaceType is a flag enum — `ToString()` gives the
// comma-joined names (e.g. "Base1Image, Additive").
uint rawType = (uint)surface.Type;
string names = surface.Type.ToString();
uint origTex = surface.OrigTextureId?.DataId ?? 0u;
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).
Console.WriteLine(
$"[sky-dump] Surface[{i}] 0x{surfaceId:X8} Type=0x{rawType:X8} ({names}) " +
@@ -703,7 +703,7 @@ internal sealed unsafe class SkyRenderer : IDisposable
//
// NOTE: earlier revision also treated `SurfaceType.Luminous = 0x40`
// 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
// render pipeline, not "additive blend". Only the Additive bit
// toggles the blend mode.
@@ -753,18 +753,18 @@ internal sealed unsafe class SkyRenderer : IDisposable
///
public bool IsAdditive;
///
- /// Surface.Luminosity float (0..1 — NOT the SurfaceType.Luminous
+ /// Surface.Luminosity float (0..1 — NOT the SurfaceType.Luminous
/// flag bit). Passed to the sky fragment shader as uEmissive;
/// 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
/// the time-of-day ambient+diffuse tint (clouds). See
- /// docs/research/2026-04-23-sky-retail-verbatim.md §6.
+ /// docs/research/2026-04-23-sky-retail-verbatim.md §6.
///
public float SurfLuminosity;
public float SurfDiffuse;
///
/// 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
/// must use GL_REPEAT for these or only the small region
/// where UVs fall in [0,1] samples the actual texture; the rest
diff --git a/src/AcDream.App/Rendering/TeleportViewPlaneController.cs b/src/AcDream.App/Rendering/TeleportViewPlaneController.cs
index 1af9fabc..01c50711 100644
--- a/src/AcDream.App/Rendering/TeleportViewPlaneController.cs
+++ b/src/AcDream.App/Rendering/TeleportViewPlaneController.cs
@@ -1,4 +1,4 @@
-using System.Numerics;
+using System.Numerics;
using AcDream.Core.World;
namespace AcDream.App.Rendering;
@@ -12,7 +12,7 @@ namespace AcDream.App.Rendering;
/// from portal space to the destination world at the transition projection;
/// there is no black-alpha compositor between them.
///
-internal sealed class TeleportViewPlaneController
+public sealed class TeleportViewPlaneController
{
public const float TransitionViewPlaneDistance = 0.001f;
diff --git a/src/AcDream.App/Rendering/TerrainAtlas.cs b/src/AcDream.App/Rendering/TerrainAtlas.cs
index 4d8860d8..e4c7ad59 100644
--- a/src/AcDream.App/Rendering/TerrainAtlas.cs
+++ b/src/AcDream.App/Rendering/TerrainAtlas.cs
@@ -1,4 +1,4 @@
-using AcDream.Core.Textures;
+using AcDream.Core.Textures;
using DatReaderWriter;
using AcDream.Content;
using DatReaderWriter.DBObjs;
@@ -13,21 +13,21 @@ namespace AcDream.App.Rendering;
/// Holds both texture arrays the terrain renderer samples from:
///
/// -
-/// Terrain atlas — one GL_TEXTURE_2D_ARRAY layer per terrain type
+/// Terrain atlas — one GL_TEXTURE_2D_ARRAY layer per terrain type
/// (grass, dirt, sand, forest...), sourced from
/// Region.TerrainInfo.LandSurfaces.TexMerge.TerrainDesc.
///
/// -
-/// Alpha atlas — one GL_TEXTURE_2D_ARRAY layer per blend mask,
+/// Alpha atlas — one GL_TEXTURE_2D_ARRAY layer per blend mask,
/// sourced from CornerTerrainMaps / SideTerrainMaps / RoadMaps in the
/// same TexMerge. Used by the fragment shader to blend up to three
/// terrain overlays and two roads on top of a base cell texture.
///
///
-/// 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.
///
-internal sealed unsafe class TerrainAtlas : IDisposable
+public sealed unsafe class TerrainAtlas : IDisposable
{
private readonly GL _gl;
@@ -276,7 +276,7 @@ internal sealed unsafe class TerrainAtlas : IDisposable
///
/// 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;
/// expands each alpha byte
/// into all four RGBA channels so the shader can sample from any channel.
///
@@ -368,7 +368,7 @@ internal sealed unsafe class TerrainAtlas : IDisposable
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.
int aMaxW = 1, aMaxH = 1;
foreach (var d in decoded)
@@ -433,7 +433,7 @@ internal sealed unsafe class TerrainAtlas : IDisposable
// Alpha maps ship as PFID_CUSTOM_LSCAPE_ALPHA (AC's landscape-alpha
// 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.
var d = SurfaceDecoder.DecodeRenderSurface(rs, palette: null, isClipMap: false, isAdditive: true);
if (ReferenceEquals(d, DecodedTexture.Magenta))
@@ -515,7 +515,7 @@ internal sealed unsafe class TerrainAtlas : IDisposable
/// A.5 T22.5: update GL_TEXTURE_MAX_ANISOTROPY on the terrain atlas at
/// runtime (called by
/// 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
/// change. The texture must not be resident-bindless when its parameters
/// are mutated; we temporarily make it non-resident if needed.
diff --git a/src/AcDream.App/Rendering/TerrainModernRenderer.cs b/src/AcDream.App/Rendering/TerrainModernRenderer.cs
index d5e1f6e4..643a2d1a 100644
--- a/src/AcDream.App/Rendering/TerrainModernRenderer.cs
+++ b/src/AcDream.App/Rendering/TerrainModernRenderer.cs
@@ -1,4 +1,4 @@
-using System.Numerics;
+using System.Numerics;
using AcDream.App.Rendering.Gpu;
using AcDream.App.Rendering.Wb;
using AcDream.Core.Terrain;
@@ -8,7 +8,7 @@ namespace AcDream.App.Rendering;
///
/// 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
/// visible slots, upload, dispatch via glMultiDrawElementsIndirect. Atlas
/// 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
/// landblock count.
///
-internal sealed unsafe class TerrainModernRenderer : IDisposable
+public 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
// because we bake `slot * VertsPerLandblock` into indices CPU-side and
// pass BaseVertex=0 to MultiDrawElementsIndirect, gl_VertexID becomes
@@ -83,7 +83,7 @@ internal sealed unsafe class TerrainModernRenderer : IDisposable
private uint _fallbackClipUbo;
// 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).
private int _uTextureIndexALoc;
private int _uTextureIndexBLoc;
@@ -91,8 +91,8 @@ internal sealed unsafe class TerrainModernRenderer : IDisposable
private bool _textureTilingUploaded;
// GL-only emulation of the eventual Vulkan global texture descriptor array
- // (binding=9, GpuBindingModel.StorageTextureTable). Owns its own table —
- // see GlBindlessHandleTable's doc comment and the campaign doc's §5.2 for
+ // (binding=9, GpuBindingModel.StorageTextureTable). Owns its own table —
+ // see GlBindlessHandleTable's doc comment and the campaign doc's §5.2 for
// why terrain doesn't share WbDrawDispatcher's/EnvCellRenderer's tables.
private readonly GlBindlessHandleTable _textureTable = new();
private uint _textureTableSsbo;
@@ -495,7 +495,7 @@ internal sealed unsafe class TerrainModernRenderer : IDisposable
// Bind shader + uniforms + atlas handles.
// 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
- // 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
// pre-positions terrain to the outdoor landcell, but acdream uses the
// unified camera matrix everywhere, so no separate viewpoint divergence can occur.
@@ -508,7 +508,7 @@ internal sealed unsafe class TerrainModernRenderer : IDisposable
// Campaign V slice V2b: pass each handle's binding=9 table slot
// instead of the raw uvec2 handle. GLSL reconstructs
// sampler2DArray(ACDREAM_TEXTURE_HANDLE(uTextureIndexA)) at the use
- // site — see terrain_modern.frag.
+ // site — see terrain_modern.frag.
uint terrainSlot = _textureTable.GetOrAdd(terrainHandle);
uint alphaSlot = _textureTable.GetOrAdd(alphaHandle);
FlushAndBindTextureTable();
@@ -519,7 +519,7 @@ internal sealed unsafe class TerrainModernRenderer : IDisposable
// when wired, else the no-clip fallback (count 0 = ungated terrain).
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
// POSITIVE (upper) side of its plane (Plane::which_side2 vs
// Render::FrameCurrent, zFightTerrainAdjust bias). GL backface culling
@@ -527,13 +527,13 @@ internal sealed unsafe class TerrainModernRenderer : IDisposable
// LandblockMesh emits every triangle CCW in world XY seen from above
// (LandblockMeshTests winding pin), which the unified camera chain
// (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
// 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
// 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;
// the frame-global CW + cull-off baseline is restored after the draw.
_gl.Enable(EnableCap.CullFace);
diff --git a/src/AcDream.App/Rendering/TextRenderGlStateScope.cs b/src/AcDream.App/Rendering/TextRenderGlStateScope.cs
new file mode 100644
index 00000000..b65123b7
--- /dev/null
+++ b/src/AcDream.App/Rendering/TextRenderGlStateScope.cs
@@ -0,0 +1,156 @@
+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);
+}
+
+///
+/// Exact, focused state transaction for . It
+/// captures every GL value that Flush or DrawLayer mutates, while avoiding the
+/// dozens of unrelated synchronous reads made by the broad diagnostic scope.
+///
+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);
+ }
+}
diff --git a/src/AcDream.App/Rendering/TextRenderer.cs b/src/AcDream.App/Rendering/TextRenderer.cs
index 7358a76d..18f8862b 100644
--- a/src/AcDream.App/Rendering/TextRenderer.cs
+++ b/src/AcDream.App/Rendering/TextRenderer.cs
@@ -1,7 +1,11 @@
-using System;
+using System;
using System.Collections.Generic;
+using System.IO;
+using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices;
+using AcDream.App.Rendering.Wb;
+using Silk.NET.OpenGL;
namespace AcDream.App.Rendering;
@@ -11,39 +15,36 @@ namespace AcDream.App.Rendering;
/// at the start of a HUD pass, queue geometry via
/// / , then .
///
-/// Campaign V slice V4a: ported onto . One pipeline
-/// (blend, depth-disable, and the MSAA/alpha-to-coverage isolation the prior
-/// TextRenderGlStateScope restored by hand are now baked into the
-/// pipeline description) and one pass per , 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.
+/// Uses two internal vertex buffers (text and rect) flushed in two draw calls
+/// to avoid a per-vertex "use texture" flag. Rects are drawn first so text
+/// sits on top of background panels.
///
-internal sealed class TextRenderer : IDisposable
+public sealed unsafe class TextRenderer : IDisposable
{
private const int FloatsPerVertex = 8; // pos(2) + uv(2) + color(4)
- private const int VertexStrideBytes = FloatsPerVertex * sizeof(float);
- private static readonly GpuVertexLayout VertexLayout = new(
- StrideBytes: VertexStrideBytes,
- [
- new GpuVertexAttribute(0, GpuVertexFormat.Float2, 0),
- new GpuVertexAttribute(1, GpuVertexFormat.Float2, 8),
- new GpuVertexAttribute(2, GpuVertexFormat.Float4, 16),
- ]);
+ private readonly GL _gl;
+ private readonly ITextRenderGlStateApi _glState;
+ private readonly Shader _shader;
+ private readonly ResourceCleanupGroup _resources;
+ private uint _vao;
+ private uint _vbo;
+ private readonly uint _whiteTex; // 1×1 white, for solid fills routed through the sprite bucket
+ private int _vboCapacityBytes;
- // uUseTexture values the ui_text.frag shader branches on (reusing the
- // shared push-constant block's uRenderPass scalar — see the shader's own
- // comment for why there is no dedicated field).
- private const int UseTextureNone = 0;
- private const int UseTextureFont = 1;
- private const int UseTextureSprite = 2;
+ private sealed class FrameBufferSet
+ {
+ public uint Vao;
+ public uint Vbo;
+ public int CapacityBytes;
+ public int UsedBytes;
+ }
- private readonly IGpuDevice _device;
- private readonly IGpuPipeline _pipeline;
+ private readonly FrameBufferSet[] _frameBuffers;
+ private FrameBufferSet? _activeFrameBuffer;
- private sealed class SpriteSeg { public GpuTextureSlot TextureSlot; public readonly List Verts = new(256); }
+ internal long DynamicBufferCapacityBytes =>
+ _frameBuffers.Sum(set => (long)set.CapacityBytes);
private readonly List _textBuf = new(8192);
private readonly List _rectBuf = new(1024);
@@ -52,14 +53,16 @@ internal sealed class TextRenderer : IDisposable
// Drawing segments in submission order preserves painter z-order for
// 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
- // 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 Verts = new(256); }
+
private readonly List _spriteSegs = new();
private int _segUsed;
private int _textVerts;
private int _rectVerts;
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
// rect panel backgrounds (which otherwise always win because rects flush after
// sprites). Routed by OverlayMode; the UI root sets it for the popup traversal.
@@ -74,38 +77,142 @@ internal sealed class TextRenderer : IDisposable
/// of all normal-layer geometry). Set by the UI root around the popup/overlay pass.
public bool OverlayMode { get; set; }
- ///
- /// No longer meaningful post-V4a: per-frame vertex data comes from the
- /// device's shared ring rather than a VBO this class owns. Kept (returning
- /// 0) so RenderFrameDiagnosticSources's telemetry read still compiles;
- /// the dynamic-buffer dimension it reported is now a device-wide, not a
- /// per-renderer, concern.
- ///
- internal long DynamicBufferCapacityBytes => 0;
-
- public TextRenderer(IGpuDevice device)
+ public TextRenderer(GL gl, string shaderDir)
{
- _device = device ?? throw new ArgumentNullException(nameof(device));
- _pipeline = _device.CreatePipeline(new GpuPipelineDescription
+ _gl = gl;
+ _glState = new SilkTextRenderGlStateApi(gl);
+ var resources = new ResourceCleanupGroup();
+ Shader? shader = null;
+ var frameBuffers = new FrameBufferSet[3];
+ uint whiteTexture = 0;
+ try
{
- Name = "ui-text",
- Shaders = new GpuShaderSet("ui_text"),
- VertexLayout = VertexLayout,
- Topology = GpuPrimitiveTopology.TriangleList,
- Blend = GpuBlendMode.StraightAlpha,
- // 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
- // in — SampleCount=1 drives the GL backend's GL_MULTISAMPLE
- // toggle off when this pipeline binds (GlGpuDevice.ApplyRenderState),
- // which is what the deleted TextRenderGlStateScope used to restore
- // by hand around every Flush.
- Depth = GpuDepthState.Disabled,
- Cull = GpuCullMode.None,
- AlphaToCoverage = false,
- ColorWrite = true,
- SampleCount = 1,
- });
+ 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 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;
+ }
+
+ ///
+ /// Selects the GPU-fenced frame slot and resets its append cursor. Every
+ /// UI segment rendered during the frame receives a distinct byte range;
+ /// later text or sprite batches cannot overwrite an earlier in-flight draw.
+ ///
+ public void BeginFrame(int frameSlot)
+ {
+ if ((uint)frameSlot >= (uint)_frameBuffers.Length)
+ throw new ArgumentOutOfRangeException(nameof(frameSlot));
+
+ FrameBufferSet set = _frameBuffers[frameSlot];
+ set.UsedBytes = 0;
+ _activeFrameBuffer = set;
+ _vao = set.Vao;
+ _vbo = set.Vbo;
+ _vboCapacityBytes = set.CapacityBytes;
+ }
+
+ private FrameBufferSet CreateFrameBufferSet(ResourceCleanupGroup resources)
+ {
+ uint vao = TrackedGlResource.CreateVertexArray(
+ _gl,
+ "TextRenderer frame VAO creation");
+ RetryableGpuResourceRelease vaoRelease =
+ TrackedGlResource.CreateRetryableVertexArrayDeletion(
+ _gl,
+ vao,
+ "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;
}
/// Begin a HUD pass. Call once per frame before any Draw* calls.
@@ -134,11 +241,11 @@ internal sealed class TextRenderer : IDisposable
/// 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
- /// this — not — for a panel BACKGROUND that text draws on top of:
+ /// this — not — for a panel BACKGROUND that text draws on top of:
/// DrawRect's bucket always flushes after all sprites, so a rect background would cover
/// the text instead.
public void DrawFill(float x, float y, float w, float h, Vector4 color)
- => DrawSprite(_device.DefaultTextureSlot, x, y, w, h, 0f, 0f, 1f, 1f, color);
+ => DrawSprite(_whiteTex, x, y, w, h, 0f, 0f, 1f, 1f, color);
/// Draw a 1-pixel-thick outline rect.
public void DrawRectOutline(float x, float y, float w, float h, Vector4 color, float thickness = 1f)
@@ -206,7 +313,7 @@ internal sealed class TextRenderer : IDisposable
}
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))
cursorX += q.Advance;
continue;
@@ -237,9 +344,9 @@ internal sealed class TextRenderer : IDisposable
///
/// Draw a textured sprite quad in screen pixel space with an explicit
/// source-UV rectangle (for 9-slice / atlas sub-regions). Batched per
- /// texture-table slot, flushed with uUseTexture=2 (RGBA modulate).
+ /// GL texture handle; flushed with uUseTexture=2 (RGBA modulate).
///
- public void DrawSprite(GpuTextureSlot texture, float x, float y, float w, float h,
+ public void DrawSprite(uint texture, float x, float y, float w, float h,
float u0, float v0, float u1, float v1, Vector4 tint)
{
SpriteSeg seg = OverlayMode
@@ -251,18 +358,18 @@ internal sealed class TextRenderer : IDisposable
/// Pick the sprite segment for : extend the current
/// same-texture run, else reuse a pooled segment, else allocate. Submission order is
/// preserved (painter z-order for sprite-on-sprite UI).
- private static SpriteSeg NextSpriteSeg(List segs, ref int used, GpuTextureSlot texture)
+ private static SpriteSeg NextSpriteSeg(List segs, ref int used, uint texture)
{
- if (used > 0 && segs[used - 1].TextureSlot == texture)
+ if (used > 0 && segs[used - 1].Texture == texture)
return segs[used - 1];
if (used < segs.Count)
{
var s = segs[used++];
- s.TextureSlot = texture;
+ s.Texture = texture;
s.Verts.Clear();
return s;
}
- var ns = new SpriteSeg { TextureSlot = texture };
+ var ns = new SpriteSeg { Texture = texture };
segs.Add(ns);
used++;
return ns;
@@ -274,10 +381,10 @@ internal sealed class TextRenderer : IDisposable
{
// Two triangles (6 verts). CCW in pixel space is clockwise in NDC
// 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.
- // (x, y) ─ (x+w, y)
- // │ │
- // (x, y+h) ─ (x+w, y+h)
+ // is GL_CCW — we rely on cull-face being disabled during HUD pass.
+ // (x, y) ─ (x+w, y)
+ // │ │
+ // (x, y+h) ─ (x+w, y+h)
//
// Triangle 1: (x,y) (x+w,y+h) (x+w,y)
// Triangle 2: (x,y) (x,y+h) (x+w,y+h)
@@ -295,105 +402,131 @@ internal sealed class TextRenderer : IDisposable
V(x + w, y + h, u1, v1);
}
- /// Upload + draw accumulated rects + text against the current frame. font may
- /// be null if only DrawRect was used.
- public void Flush(BitmapFont? font, IGpuFrame frame)
+ /// Upload + draw accumulated rects + text. font may be null if only DrawRect was used.
+ public void Flush(BitmapFont? font)
{
bool anyNormal = _segUsed > 0 || _textVerts > 0 || _rectVerts > 0;
bool anyOverlay = _overlaySegUsed > 0 || _overlayTextVerts > 0 || _overlayRectVerts > 0;
if (!anyNormal && !anyOverlay) return;
- ArgumentNullException.ThrowIfNull(frame);
- using IGpuPassEncoder pass = frame.BeginPass(new GpuPassDescription
- {
- Name = "ui-text",
- // GL's BeginPass deliberately does not touch viewport/scissor for
- // 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);
+ // Retained UI is a private render pass: an upload or draw failure must
+ // not leak its depth/cull/blend/MSAA state into a later recoverable
+ // frame. The focused scope restores from Flush's generated finally,
+ // including when either DrawLayer call throws.
+ using var stateScope = new TextRenderGlStateScope(_glState);
- GpuPushConstants baseConstants = GpuPushConstants.Default;
- baseConstants.ParamA = _screenSize.X;
- baseConstants.ParamB = _screenSize.Y;
+ _shader.Use();
+ _shader.SetVec2("uScreenSize", _screenSize);
- // 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
+ _gl.BindVertexArray(_vao);
+ _gl.BindBuffer(BufferTargetARB.ArrayBuffer, _vbo);
+
+ // Establish the self-contained UI pass state.
+ // 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,
// 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
// AFTER the normal layer, so open popups beat even the rect backgrounds.
- DrawLayer(pass, frame, in baseConstants, _spriteSegs, _segUsed, _rectBuf, _rectVerts, _textBuf, _textVerts, font);
- DrawLayer(pass, frame, in baseConstants, _overlaySpriteSegs, _overlaySegUsed, _overlayRectBuf, _overlayRectVerts, _overlayTextBuf, _overlayTextVerts, font);
+ DrawLayer(_spriteSegs, _segUsed, _rectBuf, _rectVerts, _textBuf, _textVerts, font);
+ DrawLayer(_overlaySpriteSegs, _overlaySegUsed, _overlayRectBuf, _overlayRectVerts, _overlayTextBuf, _overlayTextVerts, font);
+
}
- /// Draw one compositing layer: sprites (submission order, one draw per
- /// segment) → untextured rects → debug-font text. Shared by the normal and overlay
- /// layers; pipeline + pass are already bound by .
+ /// Draw one compositing layer: sprites (submission order, one call per
+ /// texture) → untextured rects → debug-font text. Shared by the normal and overlay
+ /// layers; GL state + shader are set up by .
private void DrawLayer(
- IGpuPassEncoder pass,
- IGpuFrame frame,
- in GpuPushConstants baseConstants,
List spriteSegs, int segUsed,
List rectBuf, int rectVerts,
List textBuf, int textVerts, BitmapFont? font)
{
- // 1. RGBA dat sprites — one draw per distinct texture-table slot.
- for (int i = 0; i < segUsed; i++)
+ // 1. RGBA dat sprites — one draw call per distinct GL texture.
+ if (segUsed > 0)
{
- SpriteSeg seg = spriteSegs[i];
- if (seg.Verts.Count == 0) continue;
- DrawBucket(pass, frame, in baseConstants, seg.Verts, UseTextureSprite, seg.TextureSlot);
+ _shader.SetInt("uUseTexture", 2);
+ _gl.ActiveTexture(TextureUnit.Texture0);
+ _shader.SetInt("uTex", 0);
+ 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)
- 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.
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 static void DrawBucket(
- IGpuPassEncoder pass,
- IGpuFrame frame,
- in GpuPushConstants baseConstants,
- List verts,
- int useTexture,
- GpuTextureSlot textureSlot)
+ private int UploadBuffer(List buf)
{
- int byteCount = verts.Count * sizeof(float);
- if (byteCount == 0) return;
+ int bytes = buf.Count * sizeof(float);
+ if (bytes == 0) return 0;
+ FrameBufferSet set = _activeFrameBuffer
+ ?? throw new InvalidOperationException("BeginFrame must be called before rendering text.");
+ int byteOffset = set.UsedBytes;
+ int requiredBytes = checked(byteOffset + bytes);
- GpuRingAllocation allocation = frame.AllocateRing(byteCount, GpuRingUsage.Vertex);
- CollectionsMarshal.AsSpan(verts).CopyTo(allocation.AsSpan());
+ if (requiredBytes > _vboCapacityBytes)
+ {
+ int newCapacity = DynamicBufferCapacity.Grow(
+ _vboCapacityBytes,
+ requiredBytes);
+ TrackedGlResource.AllocateBufferStorage(
+ _gl,
+ GLEnum.ArrayBuffer,
+ _vbo,
+ _vboCapacityBytes,
+ newCapacity,
+ GLEnum.DynamicDraw,
+ "TextRenderer frame VBO growth");
+ _vboCapacityBytes = newCapacity;
+ }
- GpuPushConstants constants = baseConstants;
- 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;
+ fixed (float* p = CollectionsMarshal.AsSpan(buf))
+ _gl.BufferSubData(BufferTargetARB.ArrayBuffer, (nint)byteOffset, (nuint)bytes, p);
- pass.BindVertexBuffer(allocation.Buffer, allocation.OffsetBytes);
- pass.SetPushConstants(in constants);
- pass.Draw((uint)(verts.Count / FloatsPerVertex), instanceCount: 1, firstVertex: 0, firstInstance: 0);
+ set.UsedBytes = requiredBytes;
+ set.CapacityBytes = _vboCapacityBytes;
+ return byteOffset / (FloatsPerVertex * sizeof(float));
}
public void Dispose()
{
- _pipeline.Dispose();
+ _resources.RetryCleanup();
}
}
diff --git a/src/AcDream.App/Rendering/TextureCache.cs b/src/AcDream.App/Rendering/TextureCache.cs
index adab5810..c4bfb1fd 100644
--- a/src/AcDream.App/Rendering/TextureCache.cs
+++ b/src/AcDream.App/Rendering/TextureCache.cs
@@ -1,4 +1,4 @@
-// src/AcDream.App/Rendering/TextureCache.cs
+// src/AcDream.App/Rendering/TextureCache.cs
using AcDream.Core.Textures;
using AcDream.Core.World;
using AcDream.Content;
@@ -12,12 +12,11 @@ using AcDream.App.Rendering.Residency;
namespace AcDream.App.Rendering;
-internal sealed unsafe class TextureCache
+public sealed unsafe class TextureCache
: Wb.IEntityTextureLifetime,
IDisposable
{
private readonly GL _gl;
- private readonly IGpuDevice _device;
private readonly IDatReaderWriter _dats;
private readonly string _diagnosticsDirectory;
// Handle and decoded dimensions are one atomic cache entry. Keeping them
@@ -29,40 +28,17 @@ internal sealed unsafe class TextureCache
_decodedDimensionsByTexture = new();
private uint _magentaHandle;
- ///
- /// Campaign V slice V4a: one registered plus its
- /// device texture-table and decoded pixel
- /// size. Direct-RenderSurface caches for UI sprites: 0x06xxxxxx
- /// 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
- /// () until its own campaign slice.
- ///
- private readonly record struct GpuUiTextureEntry(
- GpuTextureSlot Slot,
- IGpuTexture Texture,
- int Width,
- int Height);
+ // Direct-RenderSurface caches for UI sprites: 0x06xxxxxx RenderSurface ids
+ // decoded directly (Portal/HighRes → DecodeRenderSurface), bypassing the
+ // Surface→SurfaceTexture chain that GetOrUpload uses for world materials.
+ private readonly Dictionary _handlesByRenderSurfaceId = new();
+ private readonly Dictionary _rsSizeById = new();
- private readonly Dictionary _renderSurfaceGpuTextures = new();
-
- // Ad-hoc GPU textures produced by the public UploadRgba8(byte[],int,int,bool)
- // wrapper (used by IconComposer for composited item icons). These are NOT
- // 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 _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);
+ // Ad-hoc handles produced by the public UploadRgba8(byte[],int,int,bool) wrapper
+ // (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
+ // GL texture objects until process exit.
+ private readonly List _adhocHandles = new();
private readonly Wb.BindlessSupport? _bindless;
private readonly CompositeTextureArrayCache? _compositeTextures;
@@ -106,14 +82,13 @@ internal sealed unsafe class TextureCache
// Frame counter for the one-shot ACDREAM_DUMP_SURFACES=1 trigger.
// 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 bool _surfaceHistogramAlreadyDumped;
- public TextureCache(GL gl, IGpuDevice device, IDatReaderWriter dats, Wb.BindlessSupport? bindless = null)
+ public TextureCache(GL gl, IDatReaderWriter dats, Wb.BindlessSupport? bindless = null)
: this(
gl,
- device,
dats,
bindless,
ImmediateGpuResourceRetirementQueue.Instance,
@@ -126,7 +101,6 @@ internal sealed unsafe class TextureCache
internal TextureCache(
GL gl,
- IGpuDevice device,
IDatReaderWriter dats,
Wb.BindlessSupport? bindless,
IGpuResourceRetirementQueue retirementQueue,
@@ -135,7 +109,6 @@ internal sealed unsafe class TextureCache
{
budgets ??= ResidencyBudgetOptions.Default;
_gl = gl;
- _device = device ?? throw new ArgumentNullException(nameof(device));
_dats = dats;
_bindless = bindless;
ArgumentException.ThrowIfNullOrWhiteSpace(diagnosticsDirectory);
@@ -246,22 +219,23 @@ internal sealed unsafe class TextureCache
///
/// Upload a UI sprite by its RenderSurface DataId (0x06xxxxxx), decoded
- /// DIRECTLY (Portal/HighRes → DecodeRenderSurface) rather than through the
- /// Surface→SurfaceTexture chain that uses
+ /// DIRECTLY (Portal/HighRes → DecodeRenderSurface) rather than through the
+ /// Surface→SurfaceTexture chain that uses
/// for world-geometry materials. This is the correct path for retail UI
/// chrome + font glyph sheets, which reference RenderSurface directly.
- /// Paletted (PFID_P8 / PFID_INDEX16) UI sprites — e.g. the selected-object
- /// health-bar track 0x0600193E — are decoded against the RenderSurface's own
+ /// Paletted (PFID_P8 / PFID_INDEX16) UI sprites — e.g. the selected-object
+ /// health-bar track 0x0600193E — are decoded against the RenderSurface's own
/// DefaultPaletteId (same starting palette
- /// 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.
///
- public GpuTextureSlot GetOrUploadRenderSurface(uint renderSurfaceId, out int width, out int height, bool nearest = false)
+ public uint GetOrUploadRenderSurface(uint renderSurfaceId, out int width, out int height, bool nearest = false)
{
- if (_renderSurfaceGpuTextures.TryGetValue(renderSurfaceId, out GpuUiTextureEntry existing))
+ if (_handlesByRenderSurfaceId.TryGetValue(renderSurfaceId, out var existing)
+ && _rsSizeById.TryGetValue(renderSurfaceId, out var sz))
{
- width = existing.Width; height = existing.Height;
- return existing.Slot;
+ width = sz.w; height = sz.h;
+ return existing;
}
DecodedTexture decoded;
@@ -282,43 +256,16 @@ internal sealed unsafe class TextureCache
decoded = DecodedTexture.Magenta;
}
- GpuUiTextureEntry entry = UploadUiTexture(decoded, nearest, $"ui-rendersurface-0x{renderSurfaceId:X8}");
- _renderSurfaceGpuTextures[renderSurfaceId] = entry;
+ uint h = UploadRgba8(decoded, nearest);
+ _handlesByRenderSurfaceId[renderSurfaceId] = h;
+ _rsSizeById[renderSurfaceId] = (decoded.Width, decoded.Height);
width = decoded.Width; height = decoded.Height;
- return entry.Slot;
- }
-
- ///
- /// Campaign V slice V4a: creates an 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).
- /// selects point sampling for pixel-crisp glyphs/icons versus bilinear for
- /// everything else, matching the GL path's prior per-call choice.
- ///
- 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);
+ return h;
}
///
/// 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
/// channel somewhere. Printed once per unique surfaceId under
/// ACDREAM_DUMP_SKY=1. Adds ~2ms per texture upload, negligible.
@@ -583,7 +530,7 @@ internal sealed unsafe class TextureCache
{
if (_bindless is null)
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).");
}
@@ -638,7 +585,7 @@ internal sealed unsafe class TextureCache
///
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.
ulong h = 0xCBF29CE484222325UL; // FNV-1a offset basis
const ulong prime = 0x100000001B3UL;
@@ -659,17 +606,17 @@ internal sealed unsafe class TextureCache
/// Phase N.6 slice 1: one-shot surface-format histogram dump for the
/// atlas-opportunity audit. Activated by ACDREAM_DUMP_SURFACES=1; fires
/// once after BOTH gates pass:
- /// 1. _dumpFrameCounter >= 600 — at least 600 OnRender ticks
+ /// 1. _dumpFrameCounter >= 600 — at least 600 OnRender ticks
/// have elapsed (catches the "we're already past startup boilerplate"
/// bound; ~10s at 60fps, ~3s at 200fps).
- /// 2. _uploadMetadata.Count >= 100 — the cache contains at
+ /// 2. _uploadMetadata.Count >= 100 — the cache contains at
/// least 100 uploaded textures, indicating streaming has actually
/// pulled in world content (not just sky/UI/font). The original
/// frame-only gate fired during the login/handshake phase where
/// OnRender ticks at GUI rates but no world has streamed in.
/// Output goes to the host-provided portable diagnostics directory.
/// 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.
///
public void TickSurfaceHistogramDumpIfEnabled()
@@ -694,7 +641,7 @@ internal sealed unsafe class TextureCache
{
// Diagnostic-only path. If the dump file can't be written
// (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
// support. Log to stderr and let the caller mark the dump
// as "already done" so it doesn't retry every frame.
@@ -710,7 +657,7 @@ internal sealed unsafe class TextureCache
"n6-surfaces.txt");
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();
@@ -763,7 +710,7 @@ internal sealed unsafe class TextureCache
foreach (var kv in bucketsByFormat.OrderByDescending(kv => 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))
sb.AppendLine($"# {kv.Key.W}x{kv.Key.H} {kv.Key.F}: {kv.Value}");
@@ -782,8 +729,8 @@ internal sealed unsafe class TextureCache
}
// Base1Solid surfaces (and any with OrigTextureId==0) carry a ColorValue
- // instead of a texture chain. Overrides are irrelevant here — there's
- // no texture chain to swap — so the override is ignored for solid-color
+ // instead of a texture chain. Overrides are irrelevant here — there's
+ // no texture chain to swap — so the override is ignored for solid-color
// surfaces. Translucency is honored so Base1Solid|Translucent surfaces
// with Translucency=1.0 become alpha=0, which the mesh shader's discard
// cutout makes invisible.
@@ -812,7 +759,7 @@ internal sealed unsafe class TextureCache
// Start with the texture's default palette, then apply overlays.
// 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.
Palette? basePalette = rs.DefaultPaletteId != 0
? _dats.Get(rs.DefaultPaletteId)
@@ -870,13 +817,12 @@ internal sealed unsafe class TextureCache
/// to upload CPU-composited icon layers.
/// The returned handle is tracked in and deleted by
/// . Callers must NOT also store the handle in any of the
- /// keyed caches — that would cause a double-delete on Dispose.
- public GpuTextureSlot UploadRgba8(byte[] rgba, int width, int height, bool nearest = false)
+ /// keyed caches — that would cause a double-delete on Dispose.
+ public uint UploadRgba8(byte[] rgba, int width, int height, bool nearest = false)
{
- GpuUiTextureEntry entry = UploadUiTexture(
- new DecodedTexture(rgba, width, height), nearest, "ui-adhoc-icon");
- _adhocGpuTextures.Add(entry);
- return entry.Slot;
+ uint h = UploadRgba8(new DecodedTexture(rgba, width, height), nearest);
+ _adhocHandles.Add(h);
+ return h;
}
private uint UploadRgba8(DecodedTexture decoded, bool nearest = false)
@@ -900,7 +846,7 @@ internal sealed unsafe class TextureCache
PixelType.UnsignedByte,
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.
int filter = nearest ? (int)TextureMinFilter.Nearest : (int)TextureMinFilter.Linear;
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, filter);
@@ -1016,28 +962,16 @@ internal sealed unsafe class TextureCache
_magentaHandle = 0;
}
- // RenderSurface (UI sprite) GPU textures — pre-existing gap: this dict was
- // populated by GetOrUploadRenderSurface but was not swept here before that fix.
- foreach (GpuUiTextureEntry entry in _renderSurfaceGpuTextures.Values)
- DisposeUiTexture(entry);
- _renderSurfaceGpuTextures.Clear();
+ // RenderSurface (UI sprite) handles — pre-existing gap: this dict was populated
+ // by GetOrUploadRenderSurface but was not swept here before this fix.
+ foreach (var h in _handlesByRenderSurfaceId.Values)
+ DeleteUploadedTexture(h);
+ _handlesByRenderSurfaceId.Clear();
- // Ad-hoc GPU textures from the public UploadRgba8(byte[],int,int,bool) wrapper
+ // Ad-hoc handles from the public UploadRgba8(byte[],int,int,bool) wrapper
// (IconComposer composited icons). Not stored in any keyed cache.
- foreach (GpuUiTextureEntry entry in _adhocGpuTextures)
- DisposeUiTexture(entry);
- _adhocGpuTextures.Clear();
- }
-
- ///
- /// Releases a UI-path texture's table slot before disposing the backing
- /// . Both route through the device's retirement
- /// queue, so releasing the slot first is purely bookkeeping order, not a
- /// use-after-free concern.
- ///
- private void DisposeUiTexture(GpuUiTextureEntry entry)
- {
- _device.ReleaseTextureSlot(entry.Slot);
- entry.Texture.Dispose();
+ foreach (var h in _adhocHandles)
+ DeleteUploadedTexture(h);
+ _adhocHandles.Clear();
}
}
diff --git a/src/AcDream.App/Rendering/Vfx/AnimationHookFrameQueue.cs b/src/AcDream.App/Rendering/Vfx/AnimationHookFrameQueue.cs
index 18f31c44..578a3b65 100644
--- a/src/AcDream.App/Rendering/Vfx/AnimationHookFrameQueue.cs
+++ b/src/AcDream.App/Rendering/Vfx/AnimationHookFrameQueue.cs
@@ -1,4 +1,4 @@
-using System.Numerics;
+using System.Numerics;
using AcDream.Core.Physics;
using AcDream.Core.Vfx;
using DatReaderWriter.Types;
@@ -16,7 +16,7 @@ namespace AcDream.App.Rendering.Vfx;
/// here preserves that observable order and avoids firing a hand or weapon
/// effect against the previous animation frame.
///
-internal sealed class AnimationHookFrameQueue
+public sealed class AnimationHookFrameQueue
{
private readonly AnimationHookRouter _router;
private readonly IEntityEffectPoseSource _poses;
diff --git a/src/AcDream.App/Rendering/Vfx/EntityEffectController.cs b/src/AcDream.App/Rendering/Vfx/EntityEffectController.cs
index f11df828..1d42cb5d 100644
--- a/src/AcDream.App/Rendering/Vfx/EntityEffectController.cs
+++ b/src/AcDream.App/Rendering/Vfx/EntityEffectController.cs
@@ -1,4 +1,4 @@
-using System.Numerics;
+using System.Numerics;
using AcDream.App.World;
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
@@ -23,7 +23,7 @@ namespace AcDream.App.Rendering.Vfx;
/// (0x00513260) and both play_default_script overloads
/// (0x005132B0, 0x00513300).
///
-internal sealed class EntityEffectController : IAnimationHookSink,
+public sealed class EntityEffectController : IAnimationHookSink,
IEntityEffectAdvanceSource
{
private readonly LiveEntityRuntime _liveEntities;
diff --git a/src/AcDream.App/Rendering/Vfx/EntityEffectPoseRegistry.cs b/src/AcDream.App/Rendering/Vfx/EntityEffectPoseRegistry.cs
index 88677b61..0e9a6362 100644
--- a/src/AcDream.App/Rendering/Vfx/EntityEffectPoseRegistry.cs
+++ b/src/AcDream.App/Rendering/Vfx/EntityEffectPoseRegistry.cs
@@ -1,4 +1,4 @@
-using System.Numerics;
+using System.Numerics;
using AcDream.Core.Vfx;
using AcDream.Core.World;
@@ -15,7 +15,7 @@ namespace AcDream.App.Rendering.Vfx;
/// (0x0051D180). This registry is the modern, read-only seam exposing
/// those same final frames without coupling Core effects to the renderer.
///
-internal sealed class EntityEffectPoseRegistry :
+public sealed class EntityEffectPoseRegistry :
IEntityEffectPoseSource,
IEntityEffectCellSource,
IEntityEffectPoseChangeSource,
@@ -314,7 +314,7 @@ internal sealed class EntityEffectPoseRegistry :
}
}
-internal interface IEntityEffectPoseLifetimeSource
+public interface IEntityEffectPoseLifetimeSource
{
ulong GetPoseOwnerLifetimeVersion(uint localEntityId);
}
diff --git a/src/AcDream.App/Rendering/Vfx/EntityEffectProfile.cs b/src/AcDream.App/Rendering/Vfx/EntityEffectProfile.cs
index 4696558e..304b44bf 100644
--- a/src/AcDream.App/Rendering/Vfx/EntityEffectProfile.cs
+++ b/src/AcDream.App/Rendering/Vfx/EntityEffectProfile.cs
@@ -1,4 +1,4 @@
-using AcDream.App.World;
+using AcDream.App.World;
using AcDream.Core.Net.Messages;
using AcDream.Core.Vfx;
using DatReaderWriter.DBObjs;
@@ -16,7 +16,7 @@ namespace AcDream.App.Rendering.Vfx;
/// a network PhysicsDesc then unconditionally replaces the typed table, even
/// when PeTable was absent or zero.
///
-internal sealed class EntityEffectProfile : ILiveEntityEffectProfile
+public sealed class EntityEffectProfile : ILiveEntityEffectProfile
{
private EntityEffectProfile(Setup setup)
{
diff --git a/src/AcDream.App/Rendering/Vfx/EntityScriptActivator.cs b/src/AcDream.App/Rendering/Vfx/EntityScriptActivator.cs
index 97b23c27..d998be06 100644
--- a/src/AcDream.App/Rendering/Vfx/EntityScriptActivator.cs
+++ b/src/AcDream.App/Rendering/Vfx/EntityScriptActivator.cs
@@ -1,4 +1,4 @@
-using System.Numerics;
+using System.Numerics;
using AcDream.Core.Vfx;
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
/// registered. Part transforms are indexed and root-local.
///
-internal sealed record ScriptActivationInfo(
+public sealed record ScriptActivationInfo(
uint ScriptId,
IReadOnlyList PartTransforms,
EntityEffectProfile? EffectProfile = null,
@@ -26,7 +26,7 @@ internal sealed record ScriptActivationInfo(
/// initialization. Live registration invokes this class once per logical
/// generation; spatial rebucketing never replays it.
///
-internal sealed class EntityScriptActivator
+public sealed class EntityScriptActivator
{
private sealed class StaticOwnerState
{
diff --git a/src/AcDream.App/Rendering/Vfx/LiveEntityLightController.cs b/src/AcDream.App/Rendering/Vfx/LiveEntityLightController.cs
index 65e6582f..97a74b7e 100644
--- a/src/AcDream.App/Rendering/Vfx/LiveEntityLightController.cs
+++ b/src/AcDream.App/Rendering/Vfx/LiveEntityLightController.cs
@@ -1,4 +1,4 @@
-using AcDream.App.World;
+using AcDream.App.World;
using AcDream.Core.Lighting;
using AcDream.Core.Physics;
using AcDream.Runtime.Entities;
@@ -17,7 +17,7 @@ namespace AcDream.App.Rendering.Vfx;
/// ; leaving the world removes only this
/// cell-scoped presentation and re-entry registers it again.
///
-internal sealed class LiveEntityLightController : IDisposable
+public sealed class LiveEntityLightController : IDisposable
{
private readonly LiveEntityRuntime _liveEntities;
private readonly EntityEffectPoseRegistry _poses;
diff --git a/src/AcDream.App/Rendering/Vfx/ParticleVisibilityController.cs b/src/AcDream.App/Rendering/Vfx/ParticleVisibilityController.cs
index db95c41f..f922dd29 100644
--- a/src/AcDream.App/Rendering/Vfx/ParticleVisibilityController.cs
+++ b/src/AcDream.App/Rendering/Vfx/ParticleVisibilityController.cs
@@ -1,4 +1,4 @@
-using System.Numerics;
+using System.Numerics;
using AcDream.Core.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
/// that completed view. It neither creates emitters nor performs rendering.
///
-internal sealed class ParticleVisibilityController : IWorldSceneParticleVisibility
+public sealed class ParticleVisibilityController : IWorldSceneParticleVisibility
{
public const float ExtendedRangeMultiplier = 2f;
diff --git a/src/AcDream.App/Rendering/ViewconeCuller.cs b/src/AcDream.App/Rendering/ViewconeCuller.cs
index 2bbbbc2d..d686deeb 100644
--- a/src/AcDream.App/Rendering/ViewconeCuller.cs
+++ b/src/AcDream.App/Rendering/ViewconeCuller.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Numerics;
@@ -6,31 +6,31 @@ namespace AcDream.App.Rendering;
///
/// T3 (BR-5): the port of retail's Render::viewconeCheck (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
/// clipped. Retail stores each view vertex with its 3D eye-edge plane
/// (view_vertex { Vec2D pt; Plane plane }, acclient.h:32483) and tests
/// 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).
///
-/// Our views are clip-space half-planes (≤8 per slice,
+/// Our views are clip-space half-planes (≤8 per slice,
/// output: (nx,ny,0,d) satisfied when
-/// 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 —
+/// 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 —
/// 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
-/// sphere when L.xyz·c + L.w ≥ −r·|L.xyz| (not entirely outside).
+/// sphere when L.xyz·c + L.w ≥ −r·|L.xyz| (not entirely outside).
///
/// 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
/// 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
/// reached.
///
-internal sealed class ViewconeCuller
+public sealed class ViewconeCuller
{
private const int MaxRetainedCellPlaneSets = 512;
private const int MaxRetainedPlanesPerCell = 256;
@@ -65,7 +65,7 @@ internal sealed class ViewconeCuller
}
/// True when the outside view is a full-screen pass-all (the
- /// synthetic outdoor root) — every outside-test passes.
+ /// synthetic outdoor root) — every outside-test passes.
public bool OutsideIsFullScreen { get; private set; }
public static ViewconeCuller Build(
@@ -158,7 +158,7 @@ internal sealed class ViewconeCuller
Vector4 l = plane.Equation;
float nLen = plane.NormalLength;
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;
if (dist < -radius * nLen)
return false; // entirely outside this edge plane
@@ -167,7 +167,7 @@ internal sealed class ViewconeCuller
}
/// 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.
public bool SphereVisibleInCell(uint cellId, in Vector3 center, float radius)
{
diff --git a/src/AcDream.App/Rendering/Wb/AnimatedEntityState.cs b/src/AcDream.App/Rendering/Wb/AnimatedEntityState.cs
index 168c7612..913b7bf1 100644
--- a/src/AcDream.App/Rendering/Wb/AnimatedEntityState.cs
+++ b/src/AcDream.App/Rendering/Wb/AnimatedEntityState.cs
@@ -1,4 +1,4 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
using AcDream.Core.Physics;
namespace AcDream.App.Rendering.Wb;
@@ -8,7 +8,7 @@ namespace AcDream.App.Rendering.Wb;
/// equipped items). Holds AC-specific per-instance customizations the WB
/// atlas cache doesn't carry: AnimPartChange override map +
/// HiddenParts bitmask. Also holds a reference to acdream's existing
-/// — Phase N.4 explicitly does not touch
+/// — Phase N.4 explicitly does not touch
/// the sequencer; we just route through it at draw time.
///
///
@@ -16,11 +16,11 @@ namespace AcDream.App.Rendering.Wb;
/// a server CreateObject is processed; destroyed by
/// EntitySpawnAdapter.OnRemove on RemoveObject. The mesh
/// data backing each part is cached in WB's ObjectMeshManager;
-/// 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.
///
///
-internal sealed class AnimatedEntityState
+public sealed class AnimatedEntityState
{
private readonly Dictionary _partGfxObjOverrides = new();
private ulong _hiddenMask = 0;
@@ -49,7 +49,7 @@ internal sealed class AnimatedEntityState
}
/// 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.
public void SetPartOverride(int partIdx, ulong gfxObjId)
=> _partGfxObjOverrides[partIdx] = gfxObjId;
diff --git a/src/AcDream.App/Rendering/Wb/BindlessSupport.cs b/src/AcDream.App/Rendering/Wb/BindlessSupport.cs
index 299e0350..cde447c2 100644
--- a/src/AcDream.App/Rendering/Wb/BindlessSupport.cs
+++ b/src/AcDream.App/Rendering/Wb/BindlessSupport.cs
@@ -1,4 +1,4 @@
-using Silk.NET.OpenGL;
+using Silk.NET.OpenGL;
using Silk.NET.OpenGL.Extensions.ARB;
using AcDream.App.Rendering;
@@ -9,7 +9,7 @@ namespace AcDream.App.Rendering.Wb;
/// for the modern rendering path. Constructed once at startup via
/// , which returns false if the extension isn't present.
///
-internal sealed class BindlessSupport
+public sealed class BindlessSupport
{
private readonly GL _gl;
private readonly ArbBindlessTexture _ext;
@@ -63,7 +63,7 @@ internal sealed class BindlessSupport
/// make it resident. Idempotent per (texture, sampler) pair.
///
/// Added for Campaign V slice V1's GlGpuDevice.RegisterTexture,
- /// 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
/// texture-only above cannot express
/// that; ManagedGLTextureArray already calls the equivalent
@@ -117,7 +117,7 @@ internal sealed class BindlessSupport
// and removed when terrain rendering surfaced GL_INVALID_OPERATION on
// NVIDIA Windows for the `uniform sampler2DArray` + glProgramUniformHandleARB
// 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
// you re-introduce a sampler-handle helper, restrict it to drivers known
// to accept the direct sampler-uniform path.
diff --git a/src/AcDream.App/Rendering/Wb/BufferUsageExtensions.cs b/src/AcDream.App/Rendering/Wb/BufferUsageExtensions.cs
index 47d08f38..19788818 100644
--- a/src/AcDream.App/Rendering/Wb/BufferUsageExtensions.cs
+++ b/src/AcDream.App/Rendering/Wb/BufferUsageExtensions.cs
@@ -1,4 +1,4 @@
-using Chorizite.Core.Render.Enums;
+using Chorizite.Core.Render.Enums;
using Silk.NET.OpenGL;
using System;
using System.Collections.Generic;
@@ -7,7 +7,7 @@ using System.Text;
using System.Threading.Tasks;
namespace AcDream.App.Rendering.Wb {
- internal static class BufferUsageExtensions {
+ public static class BufferUsageExtensions {
///
/// Converts a BufferUsage to a GL BufferUsageARB
///
diff --git a/src/AcDream.App/Rendering/Wb/Building.cs b/src/AcDream.App/Rendering/Wb/Building.cs
index e43d5134..aaff1b96 100644
--- a/src/AcDream.App/Rendering/Wb/Building.cs
+++ b/src/AcDream.App/Rendering/Wb/Building.cs
@@ -1,12 +1,12 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.Numerics;
namespace AcDream.App.Rendering.Wb;
///
-/// 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 LandBlockInfo.Buildings entry. Building shells (cottage
-/// walls, inn walls — IsBuildingShell=true entities) are scoped to this
+/// walls, inn walls — IsBuildingShell=true entities) are scoped to this
/// building's cells via their dat-derived anchor. The exit portal polygons are
/// stencil-marked so outdoor visibility leaks through portal silhouettes only.
///
@@ -19,7 +19,7 @@ namespace AcDream.App.Rendering.Wb;
/// Retail reference: docs/research/named-retail/acclient.h:32035
/// (BuildInfo) + 32094 (CBldPortal).
///
-internal sealed class Building
+public sealed class Building
{
/// Unique within a landblock; allocated sequentially by
/// starting at 1 (0 is reserved for "no building" semantics on LoadedCell).
diff --git a/src/AcDream.App/Rendering/Wb/BuildingLoader.cs b/src/AcDream.App/Rendering/Wb/BuildingLoader.cs
index b5e6c978..bcb6c586 100644
--- a/src/AcDream.App/Rendering/Wb/BuildingLoader.cs
+++ b/src/AcDream.App/Rendering/Wb/BuildingLoader.cs
@@ -1,4 +1,4 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.Numerics;
using AcDream.App.Rendering;
using DatReaderWriter.DBObjs;
@@ -14,11 +14,11 @@ namespace AcDream.App.Rendering.Wb;
/// Algorithm (mirrors WB's PortalService.GetPortalsByBuilding at
/// WorldBuilder.Shared/Services/PortalService.cs:43-97):
///
-/// - Step A — seed the cell set from BuildingInfo.Portals entry portals.
-/// - Step B — BFS through to discover all
+///
- Step A — seed the cell set from BuildingInfo.Portals entry portals.
+/// - Step B — BFS through to discover all
/// interior cells reachable from the entry portals (interior portals only;
-/// exit portals — OtherCellId == 0xFFFF — terminate each BFS branch).
-/// - Step C — collect exit portal polygons in world space for the stencil
+/// exit portals — OtherCellId == 0xFFFF — terminate each BFS branch).
+/// - Step C — collect exit portal polygons in world space for the stencil
/// pipeline (Phase A8 Steps 1+2, RR7 scope).
///
///
@@ -57,7 +57,7 @@ internal sealed class BuildingRegistryPublication
internal bool PublicationCommitted { get; set; }
}
-internal static class BuildingLoader
+public static class BuildingLoader
{
///
/// Builds a from the supplied landblock data.
diff --git a/src/AcDream.App/Rendering/Wb/BuildingRegistry.cs b/src/AcDream.App/Rendering/Wb/BuildingRegistry.cs
index f2826008..998f64bd 100644
--- a/src/AcDream.App/Rendering/Wb/BuildingRegistry.cs
+++ b/src/AcDream.App/Rendering/Wb/BuildingRegistry.cs
@@ -1,16 +1,16 @@
-using System;
+using System;
using System.Collections.Generic;
namespace AcDream.App.Rendering.Wb;
///
/// Phase A8 (2026-05-26): per-landblock registry of 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 ;
/// no mutations occur after initial population.
///
-/// The cell→building index uses a List<Building> value type
-/// to handle the (rare but valid) case where two buildings share an EnvCell —
+/// The cell→building index uses a List<Building> value type
+/// 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
/// EnvCellIds sets. returns all
/// owners so RR7's render path can pick the correct one.
@@ -19,13 +19,13 @@ namespace AcDream.App.Rendering.Wb;
/// (BuildingPortalGroup). Design:
/// docs/superpowers/specs/2026-05-26-phase-a8-wb-full-port-design.md.
///
-internal sealed class BuildingRegistry
+public 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).
private readonly Dictionary> _byCellId = new();
- // Index 2: building-id → Building.
+ // Index 2: building-id → Building.
private readonly Dictionary _byBuildingId = new();
///
diff --git a/src/AcDream.App/Rendering/Wb/DebugRenderSettings.cs b/src/AcDream.App/Rendering/Wb/DebugRenderSettings.cs
index a8b28c68..66bdd06a 100644
--- a/src/AcDream.App/Rendering/Wb/DebugRenderSettings.cs
+++ b/src/AcDream.App/Rendering/Wb/DebugRenderSettings.cs
@@ -1,10 +1,10 @@
-using System.Numerics;
+using System.Numerics;
namespace AcDream.App.Rendering.Wb {
// Extracted verbatim from WorldBuilder.Shared/Models/DebugRenderSettings.cs.
// LandscapeColorsSettings dependency (editor-only, CommunityToolkit.Mvvm) stripped;
// default color values inlined from LandscapeColorsSettings field initializers.
- internal class DebugRenderSettings {
+ public class DebugRenderSettings {
public bool ShowBoundingBoxes { get; set; } = false;
public bool SelectVertices { get; set; } = true;
public bool SelectBuildings { get; set; } = true;
diff --git a/src/AcDream.App/Rendering/Wb/DrawElementsIndirectCommand.cs b/src/AcDream.App/Rendering/Wb/DrawElementsIndirectCommand.cs
index ee4a4d4f..80d1119d 100644
--- a/src/AcDream.App/Rendering/Wb/DrawElementsIndirectCommand.cs
+++ b/src/AcDream.App/Rendering/Wb/DrawElementsIndirectCommand.cs
@@ -1,4 +1,4 @@
-using System.Runtime.InteropServices;
+using System.Runtime.InteropServices;
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).
///
[StructLayout(LayoutKind.Sequential, Pack = 4)]
-internal struct DrawElementsIndirectCommand
+public struct DrawElementsIndirectCommand
{
public uint Count; // index count for this draw
public uint InstanceCount; // number of instances
diff --git a/src/AcDream.App/Rendering/Wb/EntitySpawnAdapter.cs b/src/AcDream.App/Rendering/Wb/EntitySpawnAdapter.cs
index 35d75023..1caaa343 100644
--- a/src/AcDream.App/Rendering/Wb/EntitySpawnAdapter.cs
+++ b/src/AcDream.App/Rendering/Wb/EntitySpawnAdapter.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using AcDream.Core.Physics;
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
/// teardown owner after the transition unwinds.
///
-internal sealed class EntityPresentationRemovalDeferredException(uint serverGuid)
+public sealed class EntityPresentationRemovalDeferredException(uint serverGuid)
: InvalidOperationException(
$"Live entity 0x{serverGuid:X8} presentation removal is deferred until its active reference transition completes.");
@@ -43,7 +43,7 @@ internal sealed class EntityPresentationRemovalDeferredException(uint serverGuid
/// both to the created .
///
///
-internal sealed class EntitySpawnAdapter
+public sealed class EntitySpawnAdapter
{
private readonly IEntityTextureLifetime _textureLifetime;
private readonly Func _sequencerFactory;
@@ -161,14 +161,14 @@ internal sealed class EntitySpawnAdapter
}
// 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
// by this point via the WorldEntity passed in.
entity.RefreshAabb();
// Build the per-entity AnimatedEntityState. The sequencer factory
// 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
// no-op sequencer (Setup + empty MotionTable + NullAnimationLoader).
var sequencer = _sequencerFactory(entity);
@@ -185,7 +185,7 @@ internal sealed class EntitySpawnAdapter
// Snapshot each unique GfxObj id for the shorter presentation lifetime.
// 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.
// Construct the replacement completely before displacing a live owner.
// Sequencer/appearance construction is allowed to fail; in that case
diff --git a/src/AcDream.App/Rendering/Wb/EnvCellLandblockBuild.cs b/src/AcDream.App/Rendering/Wb/EnvCellLandblockBuild.cs
index 9fbbfe81..9a1c02a0 100644
--- a/src/AcDream.App/Rendering/Wb/EnvCellLandblockBuild.cs
+++ b/src/AcDream.App/Rendering/Wb/EnvCellLandblockBuild.cs
@@ -1,4 +1,4 @@
-using System.Collections.Immutable;
+using System.Collections.Immutable;
using System.Numerics;
using AcDream.Core.Rendering.Wb;
using DatReaderWriter.DBObjs;
@@ -12,7 +12,7 @@ namespace AcDream.App.Rendering.Wb;
/// placement data; the render thread schedules mesh preparation when it commits
/// the containing .
///
-internal sealed record EnvCellShellPlacement(
+public sealed record EnvCellShellPlacement(
uint CellId,
ulong GeometryId,
uint EnvironmentId,
@@ -29,7 +29,7 @@ internal sealed record EnvCellShellPlacement(
/// both portal-visibility cells and drawable shell placements so neither can be
/// drained by, or mixed with, another streaming completion.
///
-internal sealed class EnvCellLandblockBuild
+public sealed class EnvCellLandblockBuild
{
public EnvCellLandblockBuild(
uint landblockId,
@@ -57,7 +57,7 @@ internal sealed class EnvCellLandblockBuild
/// global pending bags, instances of this class are never shared between jobs or
/// observed by the render thread before returns.
///
-internal sealed class EnvCellLandblockBuildBuilder
+public sealed class EnvCellLandblockBuildBuilder
{
private readonly uint _landblockId;
private readonly List _visibilityCells = new();
diff --git a/src/AcDream.App/Rendering/Wb/EnvCellLandblockPublication.cs b/src/AcDream.App/Rendering/Wb/EnvCellLandblockPublication.cs
index a40d625b..1b39656f 100644
--- a/src/AcDream.App/Rendering/Wb/EnvCellLandblockPublication.cs
+++ b/src/AcDream.App/Rendering/Wb/EnvCellLandblockPublication.cs
@@ -1,4 +1,4 @@
-using System.Numerics;
+using System.Numerics;
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
/// landblock snapshot with one owner dictionary replacement.
///
-internal interface IEnvCellLandblockPublisher
+public interface IEnvCellLandblockPublisher
{
EnvCellLandblockPublication PreparePublication(
EnvCellLandblockBuild build);
@@ -17,7 +17,7 @@ internal interface IEnvCellLandblockPublisher
void CommitPublication(EnvCellLandblockPublication publication);
}
-internal sealed class EnvCellLandblockPublication
+public sealed class EnvCellLandblockPublication
{
internal EnvCellLandblockPublication(
object owner,
diff --git a/src/AcDream.App/Rendering/Wb/EnvCellMeshPreparationScheduler.cs b/src/AcDream.App/Rendering/Wb/EnvCellMeshPreparationScheduler.cs
index 9f8d2f6c..725c0137 100644
--- a/src/AcDream.App/Rendering/Wb/EnvCellMeshPreparationScheduler.cs
+++ b/src/AcDream.App/Rendering/Wb/EnvCellMeshPreparationScheduler.cs
@@ -1,4 +1,4 @@
-namespace AcDream.App.Rendering.Wb;
+namespace AcDream.App.Rendering.Wb;
///
/// Starts CPU mesh extraction after the completed EnvCell build has been
@@ -7,7 +7,7 @@
/// current streaming generation; stale portal destinations never keep decoder
/// jobs or surface lists alive.
///
-internal static class EnvCellMeshPreparationScheduler
+public static class EnvCellMeshPreparationScheduler
{
public static void Schedule(
EnvCellLandblockBuild build,
diff --git a/src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs b/src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs
index e4983b56..4c4054a1 100644
--- a/src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs
+++ b/src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs
@@ -1,4 +1,4 @@
-// Phase A8 (2026-05-28): port of WB's EnvCellRenderManager. This is the
+// Phase A8 (2026-05-28): port of WB's EnvCellRenderManager. This is the
// production cell-rendering pipeline for indoor visibility, replacing the
// broken "cell as WorldEntity with MeshRef(envCellId)" approach that the
// four reverted RR7 variants couldn't fix.
@@ -12,7 +12,7 @@
//
// Note: we do NOT inherit from WB's ObjectRenderManagerBase. That base
// class owns the landblock-streaming loop (Update, _pendingGeneration,
-// _uploadQueue). acdream's StreamingController already does that work —
+// _uploadQueue). acdream's StreamingController already does that work —
// running a parallel loop would compete for dat I/O. Instead, streaming builds
// a private EnvCellLandblockBuild and CommitLandblock publishes the completed
// snapshot on the render thread.
@@ -29,7 +29,7 @@ using Silk.NET.OpenGL;
namespace AcDream.App.Rendering.Wb;
-internal sealed unsafe class EnvCellRenderer :
+public sealed unsafe class EnvCellRenderer :
IDisposable,
IEnvCellLandblockPublisher
{
@@ -39,7 +39,7 @@ internal sealed unsafe class EnvCellRenderer :
private readonly WbFrustum _frustum;
// Per-landblock storage. Key = full 32-bit landblock dat id (e.g. 0xA9B4FFFF).
- // WB EnvCellRenderManager.cs:75 uses ConcurrentDictionary _landblocks —
+ // WB EnvCellRenderManager.cs:75 uses ConcurrentDictionary _landblocks —
// we use uint (full LB id) because acdream uses 32-bit landblock keys throughout.
private readonly ConcurrentDictionary _landblocks = new();
@@ -64,7 +64,7 @@ internal sealed unsafe class EnvCellRenderer :
private Matrix4x4 _lastViewProjection = Matrix4x4.Identity;
private bool _initialized;
- // List pool — copied from WB ObjectRenderManagerBase.
+ // List pool — copied from WB ObjectRenderManagerBase.
// WB ObjectRenderManagerBase.cs:83-86: protected readonly List> _listPool = new(); protected int _poolIndex = 0;
private readonly List> _listPool = new();
private int _poolIndex = 0;
@@ -78,7 +78,7 @@ internal sealed unsafe class EnvCellRenderer :
private readonly ThreadLocal _prepareScratch =
new(() => new PrepareScratch(), trackAllValues: true);
- // Modern-MDI scratch buffers (single slot — we re-upload every frame).
+ // Modern-MDI scratch buffers (single slot — we re-upload every frame).
// WB BaseObjectRenderManager.cs:43-48: _scratchMdiCommandBuffers, _scratchModernBatchBuffers, _modernInstanceBuffers
// We collapse the ring-of-3 to a single slot since we have no persistent/consolidated draws.
private uint _mdiCommandBuffer;
@@ -95,7 +95,7 @@ internal sealed unsafe class EnvCellRenderer :
// Phase U.3: per-instance clip-slot SSBO (binding=3), parallel to
// _modernInstanceBuffer. One uint per instance selecting its CellClip slot,
// indexed by the same BaseInstance + gl_InstanceID the shader uses for
- // binding=0. ALL ZEROS in U.3 ⇒ slot 0 ⇒ no-clip. U.4 populates real slots.
+ // binding=0. ALL ZEROS in U.3 ⇒ slot 0 ⇒ no-clip. U.4 populates real slots.
private uint _clipSlotBuffer;
private int _clipSlotCapacity;
private uint[] _clipSlotData = Array.Empty();
@@ -156,17 +156,17 @@ internal sealed unsafe class EnvCellRenderer :
// Campaign V slice V2 (2026-07-27): GL-only emulation of the eventual
// Vulkan global texture descriptor array (binding=9,
// GpuBindingModel.StorageTextureTable). Owns its own table rather than
- // sharing WbDrawDispatcher's — EnvCellRenderer never had a TextureCache
+ // sharing WbDrawDispatcher's — EnvCellRenderer never had a TextureCache
// dependency and nothing requires index agreement between renderers (each
// rebinds its own buffer to binding=9 immediately before its own draw
// call). See GlBindlessHandleTable's doc comment and the campaign doc's
- // §5.2. Lazily created; grown/uploaded only when a genuinely new handle
- // appears (rare — see FlushAndBindTextureTable).
+ // §5.2. Lazily created; grown/uploaded only when a genuinely new handle
+ // appears (rare — see FlushAndBindTextureTable).
private readonly GlBindlessHandleTable _textureTable = new();
private uint _textureTableSsbo;
private int _textureTableSsboCapacityBytes;
- // Reusable scratch arrays — avoid per-frame allocation.
+ // Reusable scratch arrays — avoid per-frame allocation.
// WB BaseObjectRenderManager.cs:58-59: private DrawElementsIndirectCommand[] _commands = Array.Empty<...>()
private DrawElementsIndirectCommand[] _commands = Array.Empty();
private ModernBatchData[] _modernBatches = Array.Empty();
@@ -192,7 +192,7 @@ internal sealed unsafe class EnvCellRenderer :
private readonly Dictionary> _activeSnapshotGlobalGroups = new();
private readonly List _activeSnapshotGlobalGfxObjIds = new();
- // Static render-state tracking — matches WB BaseObjectRenderManager.cs:24-28.
+ // Static render-state tracking — matches WB BaseObjectRenderManager.cs:24-28.
// Shared across all manager instances on the same GL context.
private static uint _currentVao;
private static CullMode? _currentCullMode;
@@ -204,8 +204,8 @@ internal sealed unsafe class EnvCellRenderer :
// inputs changed: landblock commits/removals (NeedsPrepare), the visible-cell
// filter, the trim window, mesh render-data availability (the snapshot bakes
// per-cell transparency from TryGetRenderData), or the view-projection.
- // NeedsPrepare existed since A8 but was never read — this wires it. The VP
- // tolerance must swallow the ~36 µm eye rest jitter (RetailPViewRenderer
+ // NeedsPrepare existed since A8 but was never read — this wires it. The VP
+ // tolerance must swallow the ~36 µm eye rest jitter (RetailPViewRenderer
// R-A2 note) while any real camera motion crosses it in the same frame.
private Matrix4x4 _preparedViewProjection;
private Vector3 _preparedCameraPosition;
@@ -223,14 +223,14 @@ internal sealed unsafe class EnvCellRenderer :
public bool IsDisposed { get; private set; }
public LastFrameStats Stats => _lastFrameStats;
- internal struct LastFrameStats { public int CellsRendered; public int TrianglesDrawn; }
+ public struct LastFrameStats { public int CellsRendered; public int TrianglesDrawn; }
private LastFrameStats _lastFrameStats;
///
/// Diagnostic accessor for the [envcells] probe (Phase A8 apparatus 2026-05-28).
/// Returns (pool-list count total, snapshot's PostPreparePoolIndex high-water).
/// A divergence between expected and actual values would indicate a pool-
- /// management regression — exactly the bug class the 2026-05-28 audit caught.
+ /// management regression — exactly the bug class the 2026-05-28 audit caught.
///
public (int PoolTotal, int SnapshotPoolHwm) GetPoolDiagnostics()
{
@@ -338,20 +338,20 @@ internal sealed unsafe class EnvCellRenderer :
public void SetClipRegionSsbo(uint sharedClipRegionSsbo)
=> _sharedClipRegionSsbo = sharedClipRegionSsbo;
- // Phase U.4: per-frame cellId→CellClip-slot map for the cell shells. When
+ // Phase U.4: per-frame cellId→CellClip-slot map for the cell shells. When
// non-null, RenderModernMDIInternal writes instanceClipSlot[i] =
// _cellIdToSlot[allInstances[i].CellId] so each cell's shell instances are
// gated to that cell's portal-clip region. When null (U.3 path), every
// instance maps to slot 0 (no-clip). A cell absent from the map writes slot 0
- // (no-clip) — but the caller's Render filter already restricts the draw to the
+ // (no-clip) — but the caller's Render filter already restricts the draw to the
// map's keys, so that fallback should not fire in practice.
private IReadOnlyDictionary? _cellIdToSlot;
///
- /// 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
/// . 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).
///
public void SetClipRouting(IReadOnlyDictionary? cellIdToSlot)
=> _cellIdToSlot = cellIdToSlot;
@@ -386,7 +386,7 @@ internal sealed unsafe class EnvCellRenderer :
surfaces);
// ---------------------------------------------------------------------------
- // CommitLandblock — render-thread transaction boundary
+ // CommitLandblock — render-thread transaction boundary
// ---------------------------------------------------------------------------
///
@@ -586,7 +586,7 @@ internal sealed unsafe class EnvCellRenderer :
int? renderRadius = null)
{
// Phase U.4 fix: stash the view-projection so Render() can upload it itself.
- // Stashed even when the gate below skips the rebuild — Render must always
+ // Stashed even when the gate below skips the rebuild — Render must always
// project with the CURRENT frame's matrix (the U.4 stale-matrix root cause).
_lastViewProjection = viewProjection;
@@ -614,7 +614,7 @@ internal sealed unsafe class EnvCellRenderer :
return;
}
- // Prepare gate: every snapshot input unchanged → keep the active snapshot.
+ // Prepare gate: every snapshot input unchanged → keep the active snapshot.
// (Same-thread discipline makes the version sample exact: publish, release
// tickets, and this method all run on the render thread.)
if (_hasPreparedSnapshot
@@ -633,7 +633,7 @@ internal sealed unsafe class EnvCellRenderer :
lock (_renderLock) { _poolIndex = 0; }
// WB skips _cameraLbX/Y update (from LandscapeDoc.Region) here in our variant
- // because we don't need camera-LB tracking for the snapshot — just frustum tests.
+ // because we don't need camera-LB tracking for the snapshot — just frustum tests.
// WB EnvCellRenderManager.cs:262:
// Filter loaded landblocks by GpuReady + Instances non-empty.
@@ -674,7 +674,7 @@ internal sealed unsafe class EnvCellRenderer :
PrepareScratch scratch = _prepareScratch.Value!;
- // WB EnvCellRenderManager.cs:279-295: fast path — LB fully inside.
+ // WB EnvCellRenderManager.cs:279-295: fast path — LB fully inside.
if (testResult == FrustumTestResult.Inside)
{
foreach (var (gfxObjId, instances) in lb.BuildingPartGroups)
@@ -692,7 +692,7 @@ internal sealed unsafe class EnvCellRenderer :
return;
}
- // WB EnvCellRenderManager.cs:298-324: slow path — per-cell frustum test.
+ // WB EnvCellRenderManager.cs:298-324: slow path — per-cell frustum test.
HashSet visibleCells = scratch.VisibleCells;
visibleCells.Clear();
foreach (var kvp in lb.EnvCellBounds)
@@ -804,13 +804,13 @@ internal sealed unsafe class EnvCellRenderer :
///
/// Pure half of the prepare gate's camera test (regression-tested without a
/// GL context, same pattern as ).
- /// Eye position uses a 1 mm ABSOLUTE epsilon: it swallows the ~36 µm rest
+ /// Eye position uses a 1 mm ABSOLUTE epsilon: it swallows the ~36 µm rest
/// jitter but dirties on any real movement (a slow walk moves 20+ mm/frame).
- /// Position must not be tested through the matrix — the view-projection's
+ /// Position must not be tested through the matrix — the view-projection's
/// translation row scales with world coordinates (~5e4 in AC), where a
- /// relative tolerance would mask sub-meter motion. Rows 1–3 of
- /// view × projection are position-independent (rotation × projection), so a
- /// relative 1e-5 there dirties at ≈0.001° of rotation and on any
+ /// relative tolerance would mask sub-meter motion. Rows 1–3 of
+ /// view × projection are position-independent (rotation × projection), so a
+ /// relative 1e-5 there dirties at ≈0.001° of rotation and on any
/// projection (FOV/aspect/near/far) change.
///
internal static bool CameraApproximatelyEqual(
@@ -926,7 +926,7 @@ internal sealed unsafe class EnvCellRenderer :
// Verbatim port of WB EnvCellRenderManager.cs:395-511.
// Deviations from WB (all documented):
// - Drop the _useModernRendering branch (our codebase asserts modern at startup per Phase N.5).
- // - Drop SelectedInstance/HoveredInstance highlight block (lines 486-510) — no editor state.
+ // - Drop SelectedInstance/HoveredInstance highlight block (lines 486-510) — no editor state.
// - Replace RenderModernMDI(base) with private RenderModernMDIInternal.
// - shader.Bind() / SetUniform API: mapped to acdream's legacy Shader
// class (Use() + SetInt/SetVec4/SetMatrix4) to match the existing
@@ -947,7 +947,7 @@ internal sealed unsafe class EnvCellRenderer :
/// filter (the drawable visible cells from the PView traversal; each cell's
/// shell instances are clip-gated to its CellClip slot by the caller's
/// binding=3 map). NOTE: this is NOT the old two-pipe RenderInsideOut approach
- /// — that flat camera-inside-building stencil pass was deleted in Phase U.1.
+ /// — that flat camera-inside-building stencil pass was deleted in Phase U.1.
/// Source: WB EnvCellRenderManager.cs:399-511 (verbatim minus selection highlights).
///
public void Render(WbRenderPass renderPass, HashSet? filter)
@@ -979,7 +979,7 @@ internal sealed unsafe class EnvCellRenderer :
// WB EnvCellRenderManager.cs:403-404:
_shader.Use();
// FIX 2026-05-28 (pool aliasing root cause): mirror WB
- // EnvCellRenderManager.cs:405 — restore the pool cursor to the
+ // EnvCellRenderManager.cs:405 — restore the pool cursor to the
// high-water mark Prepare's merge phase reached, so any
// GetPooledList calls below return lists past the snapshot's
// owned region. Original code used `snapshot.BatchedByCell.Count`
@@ -999,7 +999,7 @@ internal sealed unsafe class EnvCellRenderer :
// RenderInsideOutAcdream stencil pipeline) change the actual GL
// state without updating these caches. The cache then lies, and
// the per-batch SetCullMode in RenderModernMDIInternal skips its
- // glCullFace call — leaving stale cull state from the prior
+ // glCullFace call — leaving stale cull state from the prior
// consumer. For a cottage with mixed CullMode batches, half the
// walls end up culled and the user sees "missing walls".
//
@@ -1012,15 +1012,15 @@ internal sealed unsafe class EnvCellRenderer :
_shader.SetInt("uRenderPass", (int)renderPass);
_shader.SetInt("uFilterByCell", 0);
_shader.SetInt("uLightingMode", 1); // A7 Fix D D-3/D-4: EnvCell bake (wrap points, no sun)
- // #176 stripe-hunt isolation (ACDREAM_LIGHT_DEBUG) — throwaway diagnostic.
+ // #176 stripe-hunt isolation (ACDREAM_LIGHT_DEBUG) — throwaway diagnostic.
_shader.SetInt("uLightDebug", AcDream.Core.Rendering.RenderingDiagnostics.LightDebugMode);
// Phase U.4 ROOT-CAUSE FIX (cell-shell flicker / "transparent walls when
// moving"): upload uViewProjection HERE rather than inheriting it from
// WbDrawDispatcher. The opaque shell pass runs BEFORE the dispatcher's
// Draw (GameWindow ~7411 vs ~7418, the only other setter), so without
- // this the opaque shells used the PREVIOUS frame's matrix — a stale
- // gl_Position against this frame's clip planes → pose-dependent clipping,
+ // this the opaque shells used the PREVIOUS frame's matrix — a stale
+ // gl_Position against this frame's clip planes → pose-dependent clipping,
// worst while moving. Same self-contained-GL-state precedent as the
// 2026-05-28 cull-state cache fix above.
_shader.SetMatrix4("uViewProjection", _lastViewProjection);
@@ -1059,7 +1059,7 @@ internal sealed unsafe class EnvCellRenderer :
else if (filter is null)
{
RebuildUnfilteredGroups(snapshot);
- // WB EnvCellRenderManager.cs:418-429: optimized path — global groups.
+ // WB EnvCellRenderManager.cs:418-429: optimized path — global groups.
foreach (var gfxObjId in _activeSnapshotGlobalGfxObjIds)
{
if (_activeSnapshotGlobalGroups.TryGetValue(gfxObjId, out var transforms))
@@ -1144,7 +1144,7 @@ internal sealed unsafe class EnvCellRenderer :
renderPass);
}
- // WB EnvCellRenderManager.cs:486-510: selection/hover highlights — DROPPED (no editor state).
+ // WB EnvCellRenderManager.cs:486-510: selection/hover highlights — DROPPED (no editor state).
// WB EnvCellRenderManager.cs:506-509: cleanup.
_shader.SetVec4("uHighlightColor", new System.Numerics.Vector4(0, 0, 0, 0));
@@ -1170,12 +1170,12 @@ internal sealed unsafe class EnvCellRenderer :
? dc.renderData.Batches[0].IndexCount / 3
: 0) * dc.count;
- // Issue #78 (2026-05-31) [shell] probe (ACDREAM_PROBE_SHELL) — THROWAWAY.
+ // Issue #78 (2026-05-31) [shell] probe (ACDREAM_PROBE_SHELL) — THROWAWAY.
// Per opaque-pass call: totals + per visible (filtered) cell whether it is
// present in the prepared snapshot, and its geometry/flags. Answers why the
- // interior walls/ceiling don't appear: NOSNAP / gfx=0 ⇒ no shell geometry
- // prepared for the cell; idx>0 + zh>0 ⇒ prepared but missing bindless texture
- // (invisible); idx>0 + zh=0 + tr=0 ⇒ opaque geometry drawn (fault is depth/
+ // interior walls/ceiling don't appear: NOSNAP / gfx=0 ⇒ no shell geometry
+ // prepared for the cell; idx>0 + zh>0 ⇒ prepared but missing bindless texture
+ // (invisible); idx>0 + zh=0 + tr=0 ⇒ opaque geometry drawn (fault is depth/
// occlusion or the geometry isn't the wall). Opaque pass only (halves noise).
if (renderPass == WbRenderPass.Opaque
&& AcDream.Core.Rendering.RenderingDiagnostics.ProbeShellEnabled)
@@ -1217,7 +1217,7 @@ internal sealed unsafe class EnvCellRenderer :
///
/// 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
- /// call for opaque-only cells — most cell geometry is
+ /// call for opaque-only cells — most cell geometry is
/// opaque walls/floors/ceilings, so this removes the bulk of the per-cell
/// transparent draws. Read-only; mirrors the [shell] probe's batch scan.
///
@@ -1227,7 +1227,7 @@ internal sealed unsafe class EnvCellRenderer :
// ---------------------------------------------------------------------------
// GetCellLightSet (A7 Fix D D-2 helper)
// Per-cell up-to-8 point lights, cached per frame. Camera-independent, like
- // WbDrawDispatcher.ComputeEntityLightSet — keyed on the cell's world bounds.
+ // WbDrawDispatcher.ComputeEntityLightSet — keyed on the cell's world bounds.
// ---------------------------------------------------------------------------
// A7 Fix D (D-2): the up-to-8 point lights reaching a cell, by the cell's world
@@ -1248,21 +1248,21 @@ internal sealed unsafe class EnvCellRenderer :
var snap = _pointSnapshot;
// Landblocks are keyed by the streaming landblock id 0xXXYYFFFF
- // (GameWindow: (x<<24)|(y<<16)|0xFFFF), NOT 0xXXYY0000 — so the landblock
+ // (GameWindow: (x<<24)|(y<<16)|0xFFFF), NOT 0xXXYY0000 — so the landblock
// key is (cellId & 0xFFFF0000) | 0xFFFF. The old `cellId & 0xFFFF0000` key
// (0xXXYY0000) NEVER matched a registered landblock, so this lookup always
// missed: SelectForObject never ran and every EnvCell wall received ZERO
// point lights (the entire "indoor torches/lanterns don't light the room"
- // bug — confirmed by the [cell-light] probe: inBounds=False for every cell).
+ // bug — confirmed by the [cell-light] probe: inBounds=False for every cell).
if (snap is { Count: > 0 } &&
_landblocks.TryGetValue((cellId & 0xFFFF0000u) | 0xFFFFu, out var lb) &&
lb.EnvCellBounds.TryGetValue(cellId, out var b))
{
Vector3 center = (b.Min + b.Max) * 0.5f;
float radius = (b.Max - b.Min).Length() * 0.5f;
- // #176 flap fix: cells use SelectForCell (retail minimize_envcell_lighting) — ALL
+ // #176 flap fix: cells use SelectForCell (retail minimize_envcell_lighting) — ALL
// dynamic lights on every cell (stable), not the per-object sphere-overlap cull that
- // let the portal set flip as the flood shifted → floor-lighting flap.
+ // let the portal set flip as the flood shifted → floor-lighting flap.
AcDream.Core.Lighting.LightManager.SelectForCell(snap, center, radius, set);
}
cached.FrameGeneration = _lightFrameGeneration;
@@ -1414,9 +1414,9 @@ internal sealed unsafe class EnvCellRenderer :
int passIdx = (int)renderPass;
if (passIdx < 0 || passIdx > 2) return;
- // §4 outdoor full-world flap (2026-06-10): hoisted from below the SSBO uploads.
+ // §4 outdoor full-world flap (2026-06-10): hoisted from below the SSBO uploads.
// Without the global VAO nothing can draw, and returning AFTER the pass state
- // was established leaked it (same early-out shape as the totalDraws==0 leak —
+ // was established leaked it (same early-out shape as the totalDraws==0 leak —
// see the comment on the state-establish block below).
var globalVao = _meshManager.GlobalBuffer?.VAO ?? 0u;
if (globalVao == 0) return;
@@ -1468,14 +1468,14 @@ internal sealed unsafe class EnvCellRenderer :
// transparent). Restored to opaque defaults at the end of the draw loop so a
// Transparent pass can't leak into later draws.
//
- // §4 outdoor full-world flap fix (2026-06-10): this block MOVED below the
+ // §4 outdoor full-world flap fix (2026-06-10): this block MOVED below the
// totalDraws==0 early-out above. It used to run before the batch grouping, so a
// Transparent pass over a cell whose batches are ALL opaque (a plain cottage
// interior) set Blend-on/DepthMask-off and then returned at the count check
// WITHOUT reaching the restore. The frame ended with dmask=0; the NEXT frame's
// glClear(DEPTH) silently no-oped (depth clears honor glDepthMask), every world
// fragment failed GL_LESS against its own previous-frame depth ghost, and the
- // whole screen dropped to the fog-tinted clear color — onset-locked to the
+ // whole screen dropped to the fog-tinted clear color — onset-locked to the
// building-flood merge (the first frame a flooded building shell draws), holding
// until camera rotation dropped the cell from the flood. From here down every
// path reaches the end-of-pass restore.
@@ -1689,14 +1689,14 @@ internal sealed unsafe class EnvCellRenderer :
// Phase U.4: upload the per-instance clip-slot buffer (binding=3). When
// _cellIdToSlot is set (indoor routing), each cell shell instance is gated
// to its cell's CellClip slot via allInstances[i].CellId; cells absent from
- // the map (shouldn't happen — the Render filter is the map's keys) and the
+ // the map (shouldn't happen — the Render filter is the map's keys) and the
// U.3 path both map to slot 0 (no-clip). allInstances is laid out in the
// SAME order as the binding=0 transforms (_gpuInstanceTransforms below), so
// instanceClipSlot[i] tracks Instances[i] through the MDI BaseInstance.
if (_clipSlotData.Length < uniqueInstanceCount)
_clipSlotData = new uint[Math.Max(_clipSlotData.Length * 2, uniqueInstanceCount)];
// #176 stripe-hunt isolation (ACDREAM_CLIP_DEBUG=1): force every shell
- // instance to slot 0 (no-clip) — retail draws cell shells WHOLE.
+ // instance to slot 0 (no-clip) — retail draws cell shells WHOLE.
if (_cellIdToSlot is null
|| AcDream.Core.Rendering.RenderingDiagnostics.ClipDebugNoShellTrim)
{
@@ -1728,7 +1728,7 @@ internal sealed unsafe class EnvCellRenderer :
// #176 seam-draw probe: emitted HERE (not in Render) so the per-cell light
// sets read through the just-cleared cache against THIS frame's
- // _pointSnapshot — the exact data the SSBO upload below carries.
+ // _pointSnapshot — the exact data the SSBO upload below carries.
if (renderPass == WbRenderPass.Opaque
&& AcDream.Core.Rendering.RenderingDiagnostics.ProbeSeamDrawEnabled)
EmitSeamDrawProbe(drawCalls, allInstances, _seamProbeFilter);
@@ -1766,7 +1766,7 @@ internal sealed unsafe class EnvCellRenderer :
PersistActiveDynamicBufferCapacities();
// WB BaseObjectRenderManager.cs:807-818: bind VAO + SSBOs + barrier.
- // (globalVao validated at the top of the method — a return here would leak the
+ // (globalVao validated at the top of the method — a return here would leak the
// pass state established above.)
if (_currentVao != globalVao)
{
@@ -1865,16 +1865,16 @@ internal sealed unsafe class EnvCellRenderer :
}
// ---------------------------------------------------------------------------
- // #176 seam-draw probe (ACDREAM_PROBE_SEAMDRAW) — throwaway apparatus.
+ // #176 seam-draw probe (ACDREAM_PROBE_SEAMDRAW) — throwaway apparatus.
// The in-engine replacement for the RenderDoc pixel-history the pipeline
- // can't have (RenderDoc hides GL_ARB_bindless_texture → our mandatory-modern
- // startup gate throws). Per opaque pass: for each target cell — flood
+ // can't have (RenderDoc hides GL_ARB_bindless_texture → our mandatory-modern
+ // startup gate throws). Per opaque pass: for each target cell — flood
// membership, every shell instance (count + translation, F3 z shows the
- // +0.02 lift; n≥2 for one (cell,gfx) = the runtime double-draw), and the
+ // +0.02 lift; n≥2 for one (cell,gfx) = the runtime double-draw), and the
// cell's 8-light set resolved to stable IDENTITIES (owner-cell low16 +
// intensity; raw indices shuffle when the pool rebuilds). Plus the
- // snapshot's HOT lights (intensity ≥ 50 — the portal purples; fixtures are
- // ~1–2). Change-deduped block with a 2 s heartbeat: a purple identity
+ // snapshot's HOT lights (intensity ≥ 50 — the portal purples; fixtures are
+ // ~1–2). Change-deduped block with a 2 s heartbeat: a purple identity
// flipping with flood membership = the snapshot-scope mechanism; two
// coincident instances = the z-fight. See RenderingDiagnostics.
// ---------------------------------------------------------------------------
@@ -1995,8 +1995,8 @@ internal sealed unsafe class EnvCellRenderer :
/// Uploads 's handles to
/// when a new one was registered since the last flush, then (re)binds it at
/// .
- /// A genuinely new handle is rare — new dat surfaces/atlases, not every
- /// frame — so this is not part of the ring-buffered per-frame SSBO set;
+ /// A genuinely new handle is rare — new dat surfaces/atlases, not every
+ /// frame — so this is not part of the ring-buffered per-frame SSBO set;
/// see GlBindlessHandleTable's doc comment.
///
private void FlushAndBindTextureTable()
@@ -2068,7 +2068,7 @@ internal sealed unsafe class EnvCellRenderer :
GLEnum.DynamicDraw,
"allocating EnvCell fallback clip SSBO");
allocated = true;
- // One CellClip slot, all zeros: count 0 ⇒ shader passes every plane.
+ // One CellClip slot, all zeros: count 0 ⇒ shader passes every plane.
Span zero = stackalloc byte[AcDream.App.Rendering.ClipFrame.CellClipStrideBytes];
zero.Clear();
fixed (byte* p = zero)
@@ -2103,12 +2103,12 @@ internal sealed unsafe class EnvCellRenderer :
private List GetPooledList()
{
- // Mirrors WB ObjectRenderManagerBase.cs:1221-1233 — the reuse
+ // Mirrors WB ObjectRenderManagerBase.cs:1221-1233 — the reuse
// branch MUST clear the list before returning. PrepareRenderBatches'
// merge phase pattern is `gfxDict[k] = list; list.AddRange(...)`,
// which assumes the list is empty. Without the clear, lists grow
// unbounded across frames and each frame's draw includes all prior
- // frames' stale data. Original port omitted the Clear() call — root
+ // frames' stale data. Original port omitted the Clear() call — root
// cause of post-Wave-5 visual chaos (FIX 2026-05-28). See
// docs/research/2026-05-28-a8-env-cell-renderer-audit-findings.md.
lock (_listPool)
diff --git a/src/AcDream.App/Rendering/Wb/EnvCellSceneryInstance.cs b/src/AcDream.App/Rendering/Wb/EnvCellSceneryInstance.cs
index aec47b79..55f0b34f 100644
--- a/src/AcDream.App/Rendering/Wb/EnvCellSceneryInstance.cs
+++ b/src/AcDream.App/Rendering/Wb/EnvCellSceneryInstance.cs
@@ -1,4 +1,4 @@
-// Ported from references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/SceneryInstance.cs
+// Ported from references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/SceneryInstance.cs
// Phase A8 extraction (2026-05-28). Verbatim port; adaptations:
// - SceneryInstance -> EnvCellSceneryInstance (scope-narrow to env-cell rendering)
// - ObjectLandblock -> EnvCellLandblock
@@ -17,7 +17,7 @@ namespace AcDream.App.Rendering.Wb;
/// Lightweight data for a single placed env-cell scenery object.
/// Source: references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/SceneryInstance.cs (lines 11-56)
///
-internal struct EnvCellSceneryInstance
+public struct EnvCellSceneryInstance
{
/// GfxObj or Setup ID from DAT.
public ulong ObjectId;
@@ -67,7 +67,7 @@ internal struct EnvCellSceneryInstance
/// Shared by both scenery and static object render managers.
/// Source: references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/SceneryInstance.cs (lines 62-160)
///
-internal class EnvCellLandblock
+public class EnvCellLandblock
{
/// Grid X coordinate of this landblock.
public int GridX { get; set; }
diff --git a/src/AcDream.App/Rendering/Wb/EnvCellVisibilitySnapshot.cs b/src/AcDream.App/Rendering/Wb/EnvCellVisibilitySnapshot.cs
index e3eeeaa1..77120b39 100644
--- a/src/AcDream.App/Rendering/Wb/EnvCellVisibilitySnapshot.cs
+++ b/src/AcDream.App/Rendering/Wb/EnvCellVisibilitySnapshot.cs
@@ -1,4 +1,4 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
namespace AcDream.App.Rendering.Wb;
@@ -9,14 +9,14 @@ namespace AcDream.App.Rendering.Wb;
/// narrowed to the fields actually consumes
/// (BatchedByCell + VisibleLandblocks + PostPreparePoolIndex).
/// The scenery-side VisibleGroups / VisibleGfxObjIds /
-/// IntersectingLandblocks are dropped — we render scenery through
+/// IntersectingLandblocks are dropped — we render scenery through
/// , not through this snapshot.
///
/// Used as an immutable snapshot atomically swapped under the
/// renderer's render lock so PrepareRenderBatches (worker-driven) and
/// Render (render-thread-driven) can't race on a half-populated dict.
///
-internal sealed class EnvCellVisibilitySnapshot
+public sealed class EnvCellVisibilitySnapshot
{
/// Landblocks fully or partially inside the frustum at prepare time.
public List VisibleLandblocks { get; init; } = new();
@@ -38,7 +38,7 @@ internal sealed class EnvCellVisibilitySnapshot
/// cursor to a safe region past the snapshot's owned lists, so any
/// GetPooledList calls inside Render don't trample data the
/// snapshot still references. Dropping this field caused the post-Wave-5
- /// visual chaos — see
+ /// visual chaos — see
/// docs/research/2026-05-28-a8-env-cell-renderer-audit-findings.md.
///
public int PostPreparePoolIndex { get; init; }
diff --git a/src/AcDream.App/Rendering/Wb/GLHelpers.cs b/src/AcDream.App/Rendering/Wb/GLHelpers.cs
index 7b85e242..8e0640d6 100644
--- a/src/AcDream.App/Rendering/Wb/GLHelpers.cs
+++ b/src/AcDream.App/Rendering/Wb/GLHelpers.cs
@@ -1,4 +1,4 @@
-using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging;
using Silk.NET.Core.Native;
using Silk.NET.OpenGL;
using System;
@@ -6,7 +6,7 @@ using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace AcDream.App.Rendering.Wb {
- internal static class GLHelpers {
+ public static class GLHelpers {
public static OpenGLGraphicsDevice? Device { get; set; }
public static ILogger? Logger { get; set; }
diff --git a/src/AcDream.App/Rendering/Wb/GLSLShader.cs b/src/AcDream.App/Rendering/Wb/GLSLShader.cs
index fa610dd7..f4eabecf 100644
--- a/src/AcDream.App/Rendering/Wb/GLSLShader.cs
+++ b/src/AcDream.App/Rendering/Wb/GLSLShader.cs
@@ -1,4 +1,4 @@
-using Chorizite.Core.Render;
+using Chorizite.Core.Render;
using AcDream.App.Rendering;
using Microsoft.Extensions.Logging;
using Silk.NET.OpenGL;
@@ -14,7 +14,7 @@ using System.Xml.Linq;
namespace AcDream.App.Rendering.Wb {
- internal unsafe class GLSLShader : BaseShader, IDisposable {
+ public unsafe class GLSLShader : BaseShader, IDisposable {
private OpenGLGraphicsDevice _device;
private Dictionary _uniformLocations = [];
private Dictionary _uniformValues = [];
diff --git a/src/AcDream.App/Rendering/Wb/GeometryUtils.cs b/src/AcDream.App/Rendering/Wb/GeometryUtils.cs
index 09369907..5b5ea099 100644
--- a/src/AcDream.App/Rendering/Wb/GeometryUtils.cs
+++ b/src/AcDream.App/Rendering/Wb/GeometryUtils.cs
@@ -1,4 +1,4 @@
-using System.Numerics;
+using System.Numerics;
// Phase O-T7: verbatim copy of WorldBuilder.Shared.Lib.GeometryUtils into
// the AcDream.App.Rendering.Wb namespace so the WorldBuilder.Shared project
@@ -8,7 +8,7 @@
namespace AcDream.App.Rendering.Wb;
-internal static class GeometryUtils {
+public static class GeometryUtils {
public static bool RayIntersectsBox(Vector3 rayOrigin, Vector3 rayDirection, Vector3 min, Vector3 max, out float distance) {
distance = 0;
diff --git a/src/AcDream.App/Rendering/Wb/GlobalMeshBuffer.cs b/src/AcDream.App/Rendering/Wb/GlobalMeshBuffer.cs
index 0510761c..ba074462 100644
--- a/src/AcDream.App/Rendering/Wb/GlobalMeshBuffer.cs
+++ b/src/AcDream.App/Rendering/Wb/GlobalMeshBuffer.cs
@@ -1,4 +1,4 @@
-using AcDream.Content;
+using AcDream.Content;
using Chorizite.Core.Render.Enums;
using Silk.NET.OpenGL;
using AcDream.App.Rendering;
@@ -73,7 +73,7 @@ internal enum GlobalMeshCapacityResult
/// ObjectMeshManager owns allocation lifetime and releases a mesh's ranges
/// when its zero-reference LRU entry is evicted.
///
-internal sealed class GlobalMeshBuffer : IDisposable
+public sealed class GlobalMeshBuffer : IDisposable
{
internal const int InitialVertexCapacity = 1024 * 1024;
internal const int InitialIndexCapacity = 3 * 1024 * 1024;
diff --git a/src/AcDream.App/Rendering/Wb/GpuMemoryTracker.cs b/src/AcDream.App/Rendering/Wb/GpuMemoryTracker.cs
index 8151e0c9..2b4862c0 100644
--- a/src/AcDream.App/Rendering/Wb/GpuMemoryTracker.cs
+++ b/src/AcDream.App/Rendering/Wb/GpuMemoryTracker.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
@@ -8,7 +8,7 @@ namespace AcDream.App.Rendering.Wb {
///
/// Resource types for GPU memory tracking.
///
- internal enum GpuResourceType {
+ public enum GpuResourceType {
Texture,
Buffer,
Shader,
@@ -21,17 +21,17 @@ namespace AcDream.App.Rendering.Wb {
///
/// Details about a GPU resource type.
///
- internal record GpuResourceDetails(GpuResourceType Type, int Count, long Bytes);
+ public record GpuResourceDetails(GpuResourceType Type, int Count, long Bytes);
///
/// Details about a specific named buffer.
///
- internal record NamedBufferDetails(string Name, long CapacityBytes, long UsedBytes);
+ public record NamedBufferDetails(string Name, long CapacityBytes, long UsedBytes);
///
/// Tracks manual VRAM allocations for buffers and textures.
///
- internal static class GpuMemoryTracker {
+ public static class GpuMemoryTracker {
private static long _allocatedBytes;
private static readonly long[] _allocatedBytesByType = new long[Enum.GetValues().Length];
private static readonly int[] _resourceCountsByType = new int[Enum.GetValues().Length];
diff --git a/src/AcDream.App/Rendering/Wb/IEntityTextureLifetime.cs b/src/AcDream.App/Rendering/Wb/IEntityTextureLifetime.cs
index 8faf54d9..dc6c3745 100644
--- a/src/AcDream.App/Rendering/Wb/IEntityTextureLifetime.cs
+++ b/src/AcDream.App/Rendering/Wb/IEntityTextureLifetime.cs
@@ -1,11 +1,11 @@
-namespace AcDream.App.Rendering.Wb;
+namespace AcDream.App.Rendering.Wb;
///
/// Logical-owner lifetime seam for per-entity texture composites. Retail
/// CSurface::Destroy (0x005361F0) releases its current ImgTex;
/// live-object teardown must do the same for modern bindless composites.
///
-internal interface IEntityTextureLifetime
+public interface IEntityTextureLifetime
{
/// Release every composite acquired by one local entity id.
void ReleaseOwner(uint localEntityId);
diff --git a/src/AcDream.App/Rendering/Wb/IWbMeshAdapter.cs b/src/AcDream.App/Rendering/Wb/IWbMeshAdapter.cs
index f20a7e33..3ade216b 100644
--- a/src/AcDream.App/Rendering/Wb/IWbMeshAdapter.cs
+++ b/src/AcDream.App/Rendering/Wb/IWbMeshAdapter.cs
@@ -1,4 +1,4 @@
-namespace AcDream.App.Rendering.Wb;
+namespace AcDream.App.Rendering.Wb;
///
/// Reports the physical outcome when a mesh-reference callback cannot provide
@@ -6,7 +6,7 @@
/// a transactional owner reconcile its marker without guessing whether a
/// throwing backend already changed the reference count.
///
-internal sealed class MeshReferenceMutationException : Exception
+public sealed class MeshReferenceMutationException : Exception
{
public MeshReferenceMutationException(
string message,
@@ -25,7 +25,7 @@ internal sealed class MeshReferenceMutationException : Exception
/// drive ref-count lifecycle (e.g. LandblockSpawnAdapter, EntitySpawnAdapter)
/// can be unit-tested without a real WB pipeline behind them.
///
-internal interface IWbMeshAdapter
+public interface IWbMeshAdapter
{
///
/// Acquires one logical reference. A normal exception guarantees that no
diff --git a/src/AcDream.App/Rendering/Wb/InstanceData.cs b/src/AcDream.App/Rendering/Wb/InstanceData.cs
index 993403bf..bf911ecd 100644
--- a/src/AcDream.App/Rendering/Wb/InstanceData.cs
+++ b/src/AcDream.App/Rendering/Wb/InstanceData.cs
@@ -1,9 +1,9 @@
-using System.Numerics;
+using System.Numerics;
using System.Runtime.InteropServices;
namespace AcDream.App.Rendering.Wb {
[StructLayout(LayoutKind.Sequential, Pack = 16)]
- internal struct InstanceData {
+ public struct InstanceData {
public const uint INSTANCE_FLAG_DISQUALIFIED = 1u;
public Matrix4x4 Transform; // 64 bytes
diff --git a/src/AcDream.App/Rendering/Wb/LandblockSpawnAdapter.cs b/src/AcDream.App/Rendering/Wb/LandblockSpawnAdapter.cs
index 242824c3..2e5949a4 100644
--- a/src/AcDream.App/Rendering/Wb/LandblockSpawnAdapter.cs
+++ b/src/AcDream.App/Rendering/Wb/LandblockSpawnAdapter.cs
@@ -1,4 +1,4 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
using AcDream.Core.World;
namespace AcDream.App.Rendering.Wb;
@@ -8,9 +8,9 @@ namespace AcDream.App.Rendering.Wb;
/// reference-count lifecycle. Tier-aware by design: only atlas-tier
/// entities (procedural / dat-hydrated, identified by
/// ServerGuid == 0) drive ref counts. Server-spawned entities
-/// (per-instance tier) are skipped — those go through
+/// (per-instance tier) are skipped — those go through
/// EntitySpawnAdapter and the owner-scoped texture path
-/// (see Phase N.4 spec, Architecture → Two-tier rendering split).
+/// (see Phase N.4 spec, Architecture → Two-tier rendering split).
///
///
/// On load: walks the landblock's atlas-tier entities, collects unique
@@ -44,7 +44,7 @@ namespace AcDream.App.Rendering.Wb;
/// on the owning render/update thread.
///
///
-internal sealed class LandblockSpawnAdapter
+public sealed class LandblockSpawnAdapter
{
private sealed class ReferenceRegistration
{
diff --git a/src/AcDream.App/Rendering/Wb/ManagedGLFrameBuffer.cs b/src/AcDream.App/Rendering/Wb/ManagedGLFrameBuffer.cs
index 107dcab3..7a3b190a 100644
--- a/src/AcDream.App/Rendering/Wb/ManagedGLFrameBuffer.cs
+++ b/src/AcDream.App/Rendering/Wb/ManagedGLFrameBuffer.cs
@@ -1,4 +1,4 @@
-using Chorizite.Core.Render;
+using Chorizite.Core.Render;
using Silk.NET.OpenGL;
using System;
using System.Collections.Generic;
@@ -10,7 +10,7 @@ namespace AcDream.App.Rendering.Wb {
///
/// Implementation of a framebuffer for OpenGL ES 3.0 using Silk.NET.
///
- internal class ManagedGLFramebuffer : IFramebuffer {
+ public class ManagedGLFramebuffer : IFramebuffer {
private readonly OpenGLGraphicsDevice _device;
private GL _gl => _device.GL;
private readonly uint _fboId;
diff --git a/src/AcDream.App/Rendering/Wb/ManagedGLIndexBuffer.cs b/src/AcDream.App/Rendering/Wb/ManagedGLIndexBuffer.cs
index 95a6aae3..7443fc58 100644
--- a/src/AcDream.App/Rendering/Wb/ManagedGLIndexBuffer.cs
+++ b/src/AcDream.App/Rendering/Wb/ManagedGLIndexBuffer.cs
@@ -1,4 +1,4 @@
-using Chorizite.Core.Render.Enums;
+using Chorizite.Core.Render.Enums;
using Chorizite.Core.Render.Vertex;
using Silk.NET.OpenGL;
using BufferUsage = Chorizite.Core.Render.Enums.BufferUsage;
@@ -7,7 +7,7 @@ namespace AcDream.App.Rendering.Wb {
///
/// OpenGL index buffer
///
- internal unsafe class ManagedGLIndexBuffer : IIndexBuffer {
+ public unsafe class ManagedGLIndexBuffer : IIndexBuffer {
private uint bufferId;
private readonly OpenGLGraphicsDevice _device;
private void* _mappedPtr;
diff --git a/src/AcDream.App/Rendering/Wb/ManagedGLTexture.cs b/src/AcDream.App/Rendering/Wb/ManagedGLTexture.cs
index a3ff538a..31e252e4 100644
--- a/src/AcDream.App/Rendering/Wb/ManagedGLTexture.cs
+++ b/src/AcDream.App/Rendering/Wb/ManagedGLTexture.cs
@@ -1,9 +1,9 @@
-using Chorizite.Core.Render;
+using Chorizite.Core.Render;
using Chorizite.Core.Render.Enums;
using Silk.NET.OpenGL;
namespace AcDream.App.Rendering.Wb {
- internal unsafe class ManagedGLTexture : ITexture {
+ public unsafe class ManagedGLTexture : ITexture {
private uint _texture;
private readonly OpenGLGraphicsDevice _device;
diff --git a/src/AcDream.App/Rendering/Wb/ManagedGLTextureArray.cs b/src/AcDream.App/Rendering/Wb/ManagedGLTextureArray.cs
index 1520645a..1115cede 100644
--- a/src/AcDream.App/Rendering/Wb/ManagedGLTextureArray.cs
+++ b/src/AcDream.App/Rendering/Wb/ManagedGLTextureArray.cs
@@ -1,7 +1,7 @@
-using AcDream.Core.Rendering.Wb;
+using AcDream.Core.Rendering.Wb;
using Chorizite.Core.Render;
using Chorizite.Core.Render.Enums;
-// Use our extracted TextureHelpers (T3), not the WB original — disambiguate explicitly
+// Use our extracted TextureHelpers (T3), not the WB original — disambiguate explicitly
using TextureHelpers = AcDream.Core.Rendering.Wb.TextureHelpers;
using Microsoft.Extensions.Logging;
using Silk.NET.OpenGL;
@@ -9,7 +9,7 @@ using System.Runtime.InteropServices;
using AcDream.App.Rendering;
namespace AcDream.App.Rendering.Wb {
- internal class ManagedGLTextureArray : ITextureArray {
+ public class ManagedGLTextureArray : ITextureArray {
private readonly bool[] _usedLayers;
private readonly GL GL;
private readonly OpenGLGraphicsDevice _device;
@@ -46,7 +46,7 @@ namespace AcDream.App.Rendering.Wb {
/// #105 diagnostic: staged layer updates (retained decoded payloads) not yet
/// applied to the GL texture by . Layers with
/// a pending update sample UNDEFINED content (TexStorage3D contents) until the
- /// flush runs — a stuck non-zero count at standstill is the white-walls mechanism.
+ /// flush runs — a stuck non-zero count at standstill is the white-walls mechanism.
///
public int PendingUpdateCount {
get { lock (_mipmapLock) { return _pendingUpdates.Count; } }
diff --git a/src/AcDream.App/Rendering/Wb/ManagedGLUniformBuffer.cs b/src/AcDream.App/Rendering/Wb/ManagedGLUniformBuffer.cs
index 4ab11f4c..ebcc56d2 100644
--- a/src/AcDream.App/Rendering/Wb/ManagedGLUniformBuffer.cs
+++ b/src/AcDream.App/Rendering/Wb/ManagedGLUniformBuffer.cs
@@ -1,4 +1,4 @@
-using Chorizite.Core.Render;
+using Chorizite.Core.Render;
using Chorizite.Core.Render.Enums;
using Silk.NET.OpenGL;
using System.Runtime.InteropServices;
@@ -10,7 +10,7 @@ namespace AcDream.App.Rendering.Wb {
///
/// OpenGL uniform buffer
///
- internal unsafe class ManagedGLUniformBuffer : IUniformBuffer {
+ public unsafe class ManagedGLUniformBuffer : IUniformBuffer {
private uint bufferId;
private readonly OpenGLGraphicsDevice _device;
private GL GL => _device.GL;
diff --git a/src/AcDream.App/Rendering/Wb/ManagedGLVertexArray.cs b/src/AcDream.App/Rendering/Wb/ManagedGLVertexArray.cs
index 27abfc4d..583e82d9 100644
--- a/src/AcDream.App/Rendering/Wb/ManagedGLVertexArray.cs
+++ b/src/AcDream.App/Rendering/Wb/ManagedGLVertexArray.cs
@@ -1,4 +1,4 @@
-using Chorizite.Core.Render.Enums;
+using Chorizite.Core.Render.Enums;
using Chorizite.Core.Render.Vertex;
using Silk.NET.OpenGL;
using System;
@@ -9,7 +9,7 @@ using System.Threading.Tasks;
using VertexAttribType = Silk.NET.OpenGL.VertexAttribType;
namespace AcDream.App.Rendering.Wb {
- internal unsafe class ManagedGLVertexArray : IVertexArray {
+ public unsafe class ManagedGLVertexArray : IVertexArray {
private readonly OpenGLGraphicsDevice _device;
private GL GL => _device.GL;
private uint _vaoId = 0;
diff --git a/src/AcDream.App/Rendering/Wb/ManagedGLVertexBuffer.cs b/src/AcDream.App/Rendering/Wb/ManagedGLVertexBuffer.cs
index 92b09777..d9a3752a 100644
--- a/src/AcDream.App/Rendering/Wb/ManagedGLVertexBuffer.cs
+++ b/src/AcDream.App/Rendering/Wb/ManagedGLVertexBuffer.cs
@@ -1,4 +1,4 @@
-using Chorizite.Core.Render.Enums;
+using Chorizite.Core.Render.Enums;
using Chorizite.Core.Render.Vertex;
using Microsoft.Extensions.Logging;
using Silk.NET.OpenGL;
@@ -10,7 +10,7 @@ namespace AcDream.App.Rendering.Wb {
///
/// OpenGL vertex buffer
///
- internal unsafe class ManagedGLVertexBuffer : IVertexBuffer {
+ public unsafe class ManagedGLVertexBuffer : IVertexBuffer {
private uint bufferId;
private readonly OpenGLGraphicsDevice _device;
private void* _mappedPtr;
diff --git a/src/AcDream.App/Rendering/Wb/ModernRenderData.cs b/src/AcDream.App/Rendering/Wb/ModernRenderData.cs
index 4d8192aa..a3b497eb 100644
--- a/src/AcDream.App/Rendering/Wb/ModernRenderData.cs
+++ b/src/AcDream.App/Rendering/Wb/ModernRenderData.cs
@@ -1,4 +1,4 @@
-using System.Runtime.InteropServices;
+using System.Runtime.InteropServices;
using DatReaderWriter.Enums;
using Chorizite.Core.Render;
@@ -9,17 +9,17 @@ namespace AcDream.App.Rendering.Wb {
/// bindless handle) and the layer index within the shared/pooled array.
/// Indexed by gl_DrawIDARB in the vertex shader. Same 16-byte std430 shape
/// as mesh_modern.vert's BatchData: TextureTableIndex/Reserved/TextureIndex/
- /// Flags at offsets 0/4/8/12 — see ModernBatchDataLayoutTests.
+ /// Flags at offsets 0/4/8/12 — see ModernBatchDataLayoutTests.
///
[StructLayout(LayoutKind.Sequential, Pack = 4)]
- internal struct ModernBatchData {
- public uint TextureTableIndex; // 4 bytes — slot into the binding=9 handle table
- public uint Reserved; // 4 bytes — pad, keeps TextureIndex/Flags at offsets 8/12
- public uint TextureIndex; // 4 bytes — layer within the texture array
- public uint Flags; // 4 bytes — reserved, matches mesh_modern.vert's BatchData.flags
+ public struct ModernBatchData {
+ public uint TextureTableIndex; // 4 bytes — slot into the binding=9 handle table
+ public uint Reserved; // 4 bytes — pad, keeps TextureIndex/Flags at offsets 8/12
+ public uint TextureIndex; // 4 bytes — layer within the texture array
+ public uint Flags; // 4 bytes — reserved, matches mesh_modern.vert's BatchData.flags
}
- internal struct LandblockMdiCommand {
+ public struct LandblockMdiCommand {
public ulong SortKey;
public ulong ObjectId;
public DrawElementsIndirectCommand Command;
diff --git a/src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs b/src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs
index 6ecc75b9..d3d97c4f 100644
--- a/src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs
+++ b/src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs
@@ -1,4 +1,4 @@
-using Chorizite.Core.Lib;
+using Chorizite.Core.Lib;
using Chorizite.Core.Render;
using Chorizite.Core.Render.Enums;
using DatReaderWriter.DBObjs;
@@ -26,7 +26,7 @@ namespace AcDream.App.Rendering.Wb
///
/// GPU-side render data created on the main thread.
///
- internal class ObjectRenderData
+ public class ObjectRenderData
{
public uint VAO { get; set; }
public uint VBO { get; set; }
@@ -74,7 +74,7 @@ namespace AcDream.App.Rendering.Wb
///
/// A single GPU draw batch: IBO + texture array layer.
///
- internal class ObjectRenderBatch
+ public class ObjectRenderBatch
{
public uint IBO { get; set; }
public int IndexCount { get; set; }
@@ -101,7 +101,7 @@ namespace AcDream.App.Rendering.Wb
/// Key design: mesh data is prepared on background threads via PrepareMeshData(),
/// then GPU resources are created on the main thread via UploadMeshData().
///
- internal class ObjectMeshManager : IDisposable
+ public class ObjectMeshManager : IDisposable
{
private readonly OpenGLGraphicsDevice _graphicsDevice;
private readonly IPreparedAssetSource _preparedAssets;
@@ -171,7 +171,7 @@ namespace AcDream.App.Rendering.Wb
private volatile bool _arenaBackpressured;
/// #125: how many times a failed GL upload is re-staged before
- /// giving up loudly. Small — a transient GL error clears on the next
+ /// giving up loudly. Small — a transient GL error clears on the next
/// frame; anything that fails this many times is a genuine defect to
/// surface, not retry forever. See .
public const int MaxUploadRetries = 3;
@@ -179,8 +179,8 @@ namespace AcDream.App.Rendering.Wb
///
/// #125: drain one staged upload, returning whether it should be
/// re-staged for a later frame. The caller (the per-frame Tick drain)
- /// collects the re-stages and re-enqueues them AFTER the drain loop —
- /// never inside it — so a deterministic failure can't spin the queue in
+ /// collects the re-stages and re-enqueues them AFTER the drain loop —
+ /// never inside it — so a deterministic failure can't spin the queue in
/// a single frame. increments the mesh
/// data's own counter only when new upload work actually starts (not
/// while a prior rollback waits); this drain gives up loudly past
@@ -197,7 +197,7 @@ namespace AcDream.App.Rendering.Wb
if (UploadMeshData(meshData) is not null)
{
_stagedMeshData.Complete(item);
- return false; // success (incl. legitimate 0-vertex → empty render data)
+ return false; // success (incl. legitimate 0-vertex → empty render data)
}
if (HasRenderData(meshData.ObjectId))
{
@@ -215,7 +215,7 @@ namespace AcDream.App.Rendering.Wb
if (meshData.UploadAttempts < MaxUploadRetries)
return true; // re-stage for next frame
_stagedMeshData.Complete(item);
- Console.WriteLine($"[up-retry] 0x{meshData.ObjectId:X10} upload failed {meshData.UploadAttempts}x — giving up (was the #125 silent sticky drop; a GL error is being surfaced, not hidden)");
+ Console.WriteLine($"[up-retry] 0x{meshData.ObjectId:X10} upload failed {meshData.UploadAttempts}x — giving up (was the #125 silent sticky drop; a GL error is being surfaced, not hidden)");
return false;
}
@@ -570,7 +570,7 @@ namespace AcDream.App.Rendering.Wb
///
/// #105 diagnostic: counts staged-but-unflushed texture layer updates across all
/// shared atlases (see ).
- /// Render thread only — _globalAtlases is render-thread-owned.
+ /// Render thread only — _globalAtlases is render-thread-owned.
///
public (int PendingUpdates, int ArraysWithPending, int TotalArrays) GetPendingTextureUpdateStats()
{
@@ -838,7 +838,7 @@ namespace AcDream.App.Rendering.Wb
_cpuMeshCache.Clear();
}
- internal struct EnvCellGeomRequest
+ public struct EnvCellGeomRequest
{
public uint SourceCellId;
public uint EnvironmentId;
@@ -1520,14 +1520,14 @@ namespace AcDream.App.Rendering.Wb
{
// 0-vertex mesh: every polygon was gated out at extraction. #119
// (2026-06-11) dat-verified this is LEGITIMATE for all-no-draw
- // models (all polys NoPos + Base1Solid surfaces — retail's
+ // models (all polys NoPos + Base1Solid surfaces — retail's
// skipNoTexture never draws them either; 0x010002B4/0x010008A8
// are this class, Issue119UpNullGfxObjDumpTests). The empty
// cache is the correct terminal state for those. The line stays
// as a tripwire for the OTHER way to get here (extraction
- // dropped textured polys — a real defect; dat-verify with the
+ // dropped textured polys — a real defect; dat-verify with the
// dump test before treating as one).
- Console.WriteLine($"[up-null] 0x{meshData.ObjectId:X10} produced a 0-vertex mesh — caching empty render data (legitimate for all-no-draw models; dat-verify via Issue119UpNullGfxObjDumpTests)");
+ Console.WriteLine($"[up-null] 0x{meshData.ObjectId:X10} produced a 0-vertex mesh — caching empty render data (legitimate for all-no-draw models; dat-verify via Issue119UpNullGfxObjDumpTests)");
renderData = new ObjectRenderData();
}
@@ -1611,7 +1611,7 @@ namespace AcDream.App.Rendering.Wb
///
/// Plans the actual GL work the next object would trigger against the
/// current atlas inventory. This includes array storage, global-buffer
- /// growth/copies, and one full mip generation per newly-dirtied array—not merely the
+ /// growth/copies, and one full mip generation per newly-dirtied array—not merely the
/// source byte arrays held by ObjectMeshData.
///
internal MeshUploadCost PlanUploadCost(
@@ -1852,7 +1852,7 @@ namespace AcDream.App.Rendering.Wb
#region Private: Background Preparation
///
- /// #113: the set of polygon ids referenced by the GfxObj's drawing BSP —
+ /// #113: the set of polygon ids referenced by the GfxObj's drawing BSP —
/// the polys retail actually renders (D3DPolyRender traverses the BSP;
/// dictionary-orphaned polys are physics/no-draw geometry). Returns null
/// when the model has no drawing BSP (caller draws everything).
@@ -1985,7 +1985,7 @@ namespace AcDream.App.Rendering.Wb
}
atlasManager.LastUseSequence = ++_atlasUseSequence;
- // MP1a: AcDream.Content is Silk.NET-free — the extraction records
+ // MP1a: AcDream.Content is Silk.NET-free — the extraction records
// carry Content-owned UploadPixelFormat/UploadPixelType enums whose
// underlying values are the GL ABI constants (numerically identical
// to Silk.NET.OpenGL.PixelFormat/PixelType), so this lifted nullable
@@ -2591,7 +2591,7 @@ namespace AcDream.App.Rendering.Wb
// disposes the DatCollection right after this adapter chain, which
// unmaps the dats' memory-mapped views. A worker still inside
// MemoryMappedBlockAllocator.ReadBlock at that point dereferences the
- // dead view pointer — an uncatchable, process-fatal AccessViolation
+ // dead view pointer — an uncatchable, process-fatal AccessViolation
// (dat-race investigation 2026-06-09). Setting IsDisposed under the
// queue lock publishes it to workers, which re-check it before every
// dequeue; draining the queue means each worker exits after at most
diff --git a/src/AcDream.App/Rendering/Wb/OpenGLGraphicsDevice.cs b/src/AcDream.App/Rendering/Wb/OpenGLGraphicsDevice.cs
index 997c39f4..38bf7e1d 100644
--- a/src/AcDream.App/Rendering/Wb/OpenGLGraphicsDevice.cs
+++ b/src/AcDream.App/Rendering/Wb/OpenGLGraphicsDevice.cs
@@ -1,4 +1,4 @@
-using Chorizite.Core.Render;
+using Chorizite.Core.Render;
using Chorizite.Core.Render.Enums;
using Chorizite.Core.Render.Vertex;
using AcDream.App.Rendering;
@@ -20,7 +20,7 @@ namespace AcDream.App.Rendering.Wb {
///
/// OpenGL graphics device
///
- internal unsafe class OpenGLGraphicsDevice : BaseGraphicsDevice {
+ public unsafe class OpenGLGraphicsDevice : BaseGraphicsDevice {
private readonly ILogger _log;
private readonly DebugRenderSettings _renderSettings;
private readonly AcDream.App.Rendering.IGpuResourceRetirementQueue _resourceRetirement;
diff --git a/src/AcDream.App/Rendering/Wb/RenderStateCache.cs b/src/AcDream.App/Rendering/Wb/RenderStateCache.cs
index fb62cd2c..6453ed7e 100644
--- a/src/AcDream.App/Rendering/Wb/RenderStateCache.cs
+++ b/src/AcDream.App/Rendering/Wb/RenderStateCache.cs
@@ -1,4 +1,4 @@
-namespace AcDream.App.Rendering.Wb;
+namespace AcDream.App.Rendering.Wb;
///
/// Tracks currently-bound GL state to skip redundant rebinds across the
@@ -7,9 +7,9 @@
/// here in Phase O-T7 to eliminate the WorldBuilder project reference.
///
/// Semantics are identical to the WB originals:
-/// CurrentAtlas — slot index of the currently bound texture atlas.
-/// CurrentVAO — OpenGL name of the currently bound vertex array object.
-/// CurrentIBO — OpenGL name of the currently bound index buffer object.
+/// CurrentAtlas — slot index of the currently bound texture atlas.
+/// CurrentVAO — OpenGL name of the currently bound vertex array object.
+/// CurrentIBO — OpenGL name of the currently bound index buffer object.
/// Sentinel value 0 means "no valid binding cached."
///
///
@@ -18,7 +18,7 @@
/// corresponding glBind* call and read by the next dispatch on the
/// same thread.
///
-internal static class RenderStateCache
+public static class RenderStateCache
{
public static uint CurrentAtlas = 0;
public static uint CurrentVAO = 0;
diff --git a/src/AcDream.App/Rendering/Wb/SceneData.cs b/src/AcDream.App/Rendering/Wb/SceneData.cs
index 76ba5a19..2923404f 100644
--- a/src/AcDream.App/Rendering/Wb/SceneData.cs
+++ b/src/AcDream.App/Rendering/Wb/SceneData.cs
@@ -1,4 +1,4 @@
-using System.Numerics;
+using System.Numerics;
using System.Runtime.InteropServices;
namespace AcDream.App.Rendering.Wb {
@@ -6,7 +6,7 @@ namespace AcDream.App.Rendering.Wb {
/// Global scene data for Uniform Buffer Object (UBO)
///
[StructLayout(LayoutKind.Sequential, Pack = 16)]
- internal struct SceneData {
+ public struct SceneData {
public Matrix4x4 View; // 64 bytes
public Matrix4x4 Projection; // 64 bytes
public Matrix4x4 ViewProjection; // 64 bytes
diff --git a/src/AcDream.App/Rendering/Wb/TextureAtlasManager.cs b/src/AcDream.App/Rendering/Wb/TextureAtlasManager.cs
index 66bda875..ee9ed87f 100644
--- a/src/AcDream.App/Rendering/Wb/TextureAtlasManager.cs
+++ b/src/AcDream.App/Rendering/Wb/TextureAtlasManager.cs
@@ -1,4 +1,4 @@
-using AcDream.Content;
+using AcDream.Content;
using Chorizite.Core.Render;
using Chorizite.Core.Render.Enums;
using DatReaderWriter.Enums;
@@ -42,7 +42,7 @@ namespace AcDream.App.Rendering.Wb {
/// Manages texture arrays grouped by (Width, Height, Format).
/// Deduplicates textures by a TextureKey and supports reference counting.
///
- internal class TextureAtlasManager : IDisposable {
+ public class TextureAtlasManager : IDisposable {
private static uint _nextSlot = 1;
private readonly OpenGLGraphicsDevice _graphicsDevice;
private readonly int _textureWidth;
diff --git a/src/AcDream.App/Rendering/Wb/TextureFormatExtensions.cs b/src/AcDream.App/Rendering/Wb/TextureFormatExtensions.cs
index 32d7b008..d3b00bb3 100644
--- a/src/AcDream.App/Rendering/Wb/TextureFormatExtensions.cs
+++ b/src/AcDream.App/Rendering/Wb/TextureFormatExtensions.cs
@@ -1,9 +1,9 @@
-using Chorizite.Core.Render.Enums;
+using Chorizite.Core.Render.Enums;
using Silk.NET.OpenGL;
using System;
namespace AcDream.App.Rendering.Wb {
- internal static class TextureFormatExtensions {
+ public static class TextureFormatExtensions {
public static SizedInternalFormat ToGL(this Chorizite.Core.Render.Enums.TextureFormat format) {
return format switch {
TextureFormat.RGBA8 => SizedInternalFormat.Rgba8,
diff --git a/src/AcDream.App/Rendering/Wb/TextureParameters.cs b/src/AcDream.App/Rendering/Wb/TextureParameters.cs
index f7c49af4..30a7be14 100644
--- a/src/AcDream.App/Rendering/Wb/TextureParameters.cs
+++ b/src/AcDream.App/Rendering/Wb/TextureParameters.cs
@@ -1,10 +1,10 @@
-using Silk.NET.OpenGL;
+using Silk.NET.OpenGL;
namespace AcDream.App.Rendering.Wb {
///
/// Configurable OpenGL texture parameters for wrap mode, filtering, mipmaps, and anisotropic filtering.
///
- internal struct TextureParameters {
+ public struct TextureParameters {
public TextureWrapMode WrapS;
public TextureWrapMode WrapT;
public TextureMinFilter MinFilter;
@@ -12,7 +12,7 @@ namespace AcDream.App.Rendering.Wb {
public bool EnableMipmaps;
public bool EnableAnisotropicFiltering;
- /// Standard tiling textures — Repeat + trilinear + aniso.
+ /// Standard tiling textures — Repeat + trilinear + aniso.
public static readonly TextureParameters Default = new() {
WrapS = TextureWrapMode.Repeat,
WrapT = TextureWrapMode.Repeat,
@@ -22,7 +22,7 @@ namespace AcDream.App.Rendering.Wb {
EnableAnisotropicFiltering = true,
};
- /// Non-tiling textures (alpha maps, fonts, UI, object atlases) — ClampToEdge + trilinear + aniso.
+ /// Non-tiling textures (alpha maps, fonts, UI, object atlases) — ClampToEdge + trilinear + aniso.
public static readonly TextureParameters ClampToEdge = new() {
WrapS = TextureWrapMode.ClampToEdge,
WrapT = TextureWrapMode.ClampToEdge,
diff --git a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.PackedOracle.cs b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.PackedOracle.cs
index 81a2325a..b04df854 100644
--- a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.PackedOracle.cs
+++ b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.PackedOracle.cs
@@ -1,4 +1,4 @@
-using System.Numerics;
+using System.Numerics;
using AcDream.Core.Rendering;
using AcDream.App.Rendering.Scene;
using AcDream.App.Rendering.Selection;
@@ -16,7 +16,7 @@ namespace AcDream.App.Rendering.Wb;
/// routes reuse the dispatcher's exact mesh, texture, light, translucency,
/// selection, upload, and draw owners.
///
-internal sealed unsafe partial class WbDrawDispatcher
+public sealed unsafe partial class WbDrawDispatcher
{
private readonly Dictionary
_packedEntityById = [];
diff --git a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs
index 0484da78..bb82c7e1 100644
--- a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs
+++ b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.CompilerServices;
@@ -34,12 +34,12 @@ namespace AcDream.App.Rendering.Wb;
/// WB. Native surfaces still reuse the WB atlas; only actual indexed-palette
/// and original-texture replacements resolve through owner-scoped
/// composites. is currently
-/// unused at draw time — GameWindow's spawn path already bakes AnimPartChanges +
+/// unused at draw time — GameWindow's spawn path already bakes AnimPartChanges +
/// GfxObjDegradeResolver (Issue #47 close-detail mesh) into MeshRefs.
///
///
///
-/// GL strategy (N.5 — mandatory): glMultiDrawElementsIndirect with SSBOs
+/// GL strategy (N.5 — mandatory): glMultiDrawElementsIndirect with SSBOs
/// and GL_ARB_bindless_texture + GL_ARB_shader_draw_parameters.
/// All visible (entity, batch) pairs are bucketed by ;
/// each group becomes one DrawElementsIndirectCommand. Three GPU buffers
@@ -54,7 +54,7 @@ namespace AcDream.App.Rendering.Wb;
///
/// Shader: mesh_modern (bindless + gl_DrawIDARB /
/// gl_BaseInstanceARB). Missing bindless/draw-parameters throws
-/// at startup — there is no legacy fallback.
+/// at startup — there is no legacy fallback.
///
///
///
@@ -65,7 +65,7 @@ namespace AcDream.App.Rendering.Wb;
/// glMultiDrawElementsIndirect.
///
///
-internal sealed unsafe partial class WbDrawDispatcher : IDisposable
+public sealed unsafe partial class WbDrawDispatcher : IDisposable
{
///
/// Which subset of entities to walk in a single Draw call.
@@ -78,10 +78,10 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
/// the unified pass can re-introduce partitioning later without re-threading
/// the call sites.
///
- internal enum EntitySet
+ public enum EntitySet
{
/// Every entity walked, gated only by the existing
- /// ParentCellId ∈ visibleCellIds filter.
+ /// ParentCellId ∈ visibleCellIds filter.
All,
}
@@ -100,7 +100,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
private readonly BindlessSupport _bindless;
private ICurrentRenderDispatcherObserver? _currentRenderSceneObserver;
- internal readonly record struct DrawStats(
+ public readonly record struct DrawStats(
EntitySet Set,
int EntitiesWalked,
int MeshRefs,
@@ -419,16 +419,16 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
// Tier 1 cache (#53): per-entity classification results for static
// entities (those NOT in GameWindow._animatedEntities). Wired here in
- // Task 7 for plumbing only — Tasks 9-10 wire the per-entity
+ // Task 7 for plumbing only — Tasks 9-10 wire the per-entity
// miss-populate / hit-fast-path through the loop.
private readonly EntityClassificationCache _cache;
- // #188 — per-(entity, Setup-part) translucency ramp state (fading doors /
+ // #188 — per-(entity, Setup-part) translucency ramp state (fading doors /
// secret-passage walls). ClassifyBatches reads this per part to compute
// the instance's opacity multiplier; never mutated here.
private readonly AcDream.Core.Rendering.TranslucencyFadeManager _translucencyFades;
- // ACDREAM_DISABLE_TIER1_CACHE=1 A/B diagnostic — forces every static
+ // ACDREAM_DISABLE_TIER1_CACHE=1 A/B diagnostic — forces every static
// entity through the slow path. Read once in ctor.
private readonly bool _tier1CacheDisabled =
string.Equals(Environment.GetEnvironmentVariable("ACDREAM_DISABLE_TIER1_CACHE"), "1", StringComparison.Ordinal);
@@ -453,7 +453,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
// Phase U.3: per-instance clip-slot SSBO (binding=3), parallel to
// _instanceSsbo. One uint per instance selecting its CellClip slot. In U.3
- // this is ALL ZEROS (every instance → slot 0 → no-clip), so the render is
+ // this is ALL ZEROS (every instance → slot 0 → no-clip), so the render is
// identical to pre-U.3. U.4 populates real slot indices.
private uint _clipSlotSsbo;
private int _clipSlotSsboCapacityBytes;
@@ -461,7 +461,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
// Fix B (A7 #3): per-OBJECT light selection (minimize_object_lighting). Two
// SSBOs replace the single global nearest-8-to-CAMERA UBO set for point/spot
- // lights — see mesh_modern.vert binding=4/5. _globalLightsSsbo (binding=4)
+ // lights — see mesh_modern.vert binding=4/5. _globalLightsSsbo (binding=4)
// holds the per-frame point-light snapshot (LightManager.PointSnapshot);
// _instLightSetSsbo (binding=5) holds MaxLightsPerObject int indices per
// instance INTO it (-1 = unused), laid out parallel to _instanceSsbo.
@@ -495,10 +495,10 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
// Campaign V slice V2 (2026-07-27): GL-only emulation of the eventual
// Vulkan global texture descriptor array (binding=9,
- // GpuBindingModel.StorageTextureTable). A genuinely new handle is rare —
- // new dat surfaces/atlases, not every frame — so this single buffer is
+ // GpuBindingModel.StorageTextureTable). A genuinely new handle is rare —
+ // new dat surfaces/atlases, not every frame — so this single buffer is
// NOT part of the ring-buffered DynamicBufferSet below; 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
// this table is owned here rather than by GlGpuDevice. Lazily created.
private readonly GlBindlessHandleTable _textureTable = new();
private uint _textureTableSsbo;
@@ -537,15 +537,15 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
_dynamicBufferSetsByFrame.Sum(frameSets => frameSets.Count);
private Vector2[] _selectionLightingData = new Vector2[256];
// This frame's point-light snapshot, handed in by GameWindow before Draw via
- // SetSceneLights. Null/empty ⇒ only ambient + sun render (all instance sets -1).
+ // SetSceneLights. Null/empty ⇒ only ambient + sun render (all instance sets -1).
private IReadOnlyList? _pointSnapshot;
- // This entity's selected point/spot light set — computed ONCE per entity at
+ // This entity's selected point/spot light set — computed ONCE per entity at
// the isNewEntity site (constant across the entity's parts/tuples), exactly
// like _currentEntitySlot. -1 = unused slot.
private readonly int[] _currentEntityLightSetScratch = new int[LightManager.MaxLightsPerObject];
private InstanceLightSet _currentEntityLightSet = InstanceLightSet.Disabled;
- // #142: per-entity "indoor" flag — set once per entity in ComputeEntityLightSet,
+ // #142: per-entity "indoor" flag — set once per entity in ComputeEntityLightSet,
// parallel to _currentEntityLightSet. True when IndoorObjectReceivesTorches fires
// (ParentCellId is an EnvCell). Appended to InstanceGroup.IndoorFlags in
// AppendCurrentLightSet; uploaded as binding=6 instanceIndoor[].
@@ -563,7 +563,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
// Phase U.4: per-frame clip-slot routing handed in via SetClipRouting before
// each Draw. When _clipRoutingActive is false (the U.3 path / outdoor root /
// no portal frame), every instance maps to slot 0 (no-clip) and no instance is
- // culled — identical to U.3. When active, each instance's slot is resolved by
+ // culled — identical to U.3. When active, each instance's slot is resolved by
// ResolveEntitySlot per the U.4 policy (cell-owned entities to their cell slot;
// outdoor-owned entities to OutsideView; non-visible/unresolved indoors culled).
private bool _clipRoutingActive;
@@ -583,7 +583,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
// across the entity's tuples; the per-tuple body skips all instance emission.
private bool _currentEntityCulled;
- // Per-frame scratch arrays — Tasks 9-10 fully wire these.
+ // Per-frame scratch arrays — Tasks 9-10 fully wire these.
private float[] _instanceData = new float[256 * 16]; // mat4 floats per instance
private BatchData[] _batchData = new BatchData[256];
private DrawElementsIndirectCommand[] _indirectCommands = new DrawElementsIndirectCommand[256];
@@ -600,7 +600,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
// Campaign V slice V2 (2026-07-27): std430 layout: uint TextureIndex at
// offset 0, uint Reserved (pad) at offset 4, uint TextureLayer at offset 8,
- // uint Flags at offset 12. Total 16 bytes — unchanged from before V2, so
+ // uint Flags at offset 12. Total 16 bytes — unchanged from before V2, so
// every existing CPU writer's offsets are unchanged (see
// GpuBindingModel.GpuBatchDataStrideBytes). TextureIndex used to be a
// 64-bit ulong TextureHandle (an ARB_bindless_texture handle, uvec2 in
@@ -611,7 +611,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
private struct BatchData
{
public uint TextureIndex; // slot into the binding=9 handle table
- public uint Reserved; // pad — keeps TextureLayer/Flags at offsets 8/12
+ public uint Reserved; // pad — keeps TextureLayer/Flags at offsets 8/12
public uint TextureLayer;
public uint Flags;
}
@@ -674,7 +674,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
=> owner.ResetDeferredAlphaSubmissions();
}
- // Per-frame scratch — reused across frames to avoid per-frame allocation.
+ // Per-frame scratch — reused across frames to avoid per-frame allocation.
private readonly Dictionary _groups = new();
private readonly List _opaqueDraws = new();
private readonly List _translucentDraws = new();
@@ -704,7 +704,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
* Unsafe.SizeOf());
// A.5 T26 follow-up (Bug B): WalkEntities populates this scratch list
// instead of allocating a fresh List<(WorldEntity, int)> per frame. At
- // ~10K entities × ~3 mesh refs = ~30K tuples × 16 bytes = ~480 KB / frame
+ // ~10K entities × ~3 mesh refs = ~30K tuples × 16 bytes = ~480 KB / frame
// of GC pressure on the render thread under the original T17 shape.
private readonly List<(WorldEntity Entity, int MeshRefIndex, uint LandblockId)> _walkScratch = new();
// G2: the dispatcher consumes this acdream-owned value boundary, never
@@ -712,16 +712,16 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
// retained list; G4 will replace only that producer with RenderFrameView.
private readonly List _candidateTupleScratch = new();
- // Tier 1 cache (#53) — per-entity classification collector. Reused across
+ // Tier 1 cache (#53) — per-entity classification collector. Reused across
// frames; cleared at flush time when the per-entity loop crosses an entity
// boundary in _walkScratch (and once more at end-of-loop for the last
// entity). _walkScratch is in entity-order, so all MeshRefs of one entity
- // are contiguous — accumulate them all before flushing one Populate call.
+ // are contiguous — accumulate them all before flushing one Populate call.
// Animated entities skip this scratch entirely (collector = null).
private readonly List _populateScratch = new();
private readonly List _populateSelectionScratch = new();
- // Per-entity-cull AABB radius. Conservative — covers most entities; large
+ // Per-entity-cull AABB radius. Conservative — covers most entities; large
// outliers (long banners, tall columns) are still landblock-culled.
private const float PerEntityCullRadius = 5.0f;
@@ -763,7 +763,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
private long _lastLogTick;
// #128 self-heal: per-Draw dedup of point-of-use load re-requests
- // (PrepareMeshDataAsync is idempotent while pending — the dedup just
+ // (PrepareMeshDataAsync is idempotent while pending — the dedup just
// avoids redundant dictionary probes within one pass) + the once-per-id
// [mesh-miss] diagnostic set (never cleared; diag-gated emission).
private readonly HashSet _missRequested = new();
@@ -772,7 +772,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
// #119 decisive probe (2026-06-11): ACDREAM_DUMP_ENTITY one-shot entity
// dump. Keyed by entity Id; the stored signature re-emits the header line
// whenever (MeshRefs count, cache batch count, zero-translation count,
- // culled) changes — e.g. the Tier-1 populate landing one frame after the
+ // culled) changes — e.g. the Tier-1 populate landing one frame after the
// first slow-path draw. The full per-part listing prints only on first
// sight. Inert (one Count==0 check per new entity) when the env var is
// unset. Render-thread only.
@@ -798,12 +798,12 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
private readonly uint[] _gpuQueryOpaque = new uint[GpuQueryRingDepth];
private readonly uint[] _gpuQueryTransparent = new uint[GpuQueryRingDepth];
// #125: a glGenQueries name does not become a QUERY OBJECT until its first
- // glBeginQuery — GetQueryObject on a never-begun name is GL_INVALID_OPERATION.
+ // glBeginQuery — GetQueryObject on a never-begun name is GL_INVALID_OPERATION.
// The N.6 ring assumed ONE Draw per frame with both passes always non-empty;
// the pview pipeline issues MANY small Draws per frame (landscape slices,
// per-cell buckets, dynamics), where zero-draw passes routinely skip
// BeginQuery. Under ACDREAM_WB_DIAG=1 the slot read then queued an
- // InvalidOperation EVERY frame — silently, until WB's diligent texture-path
+ // InvalidOperation EVERY frame — silently, until WB's diligent texture-path
// glGetError checks ate the stale errors and treated their own successful
// uploads as failures ([wb-error] + sticky drop) and ProcessDirtyUpdates'
// check threw (process death; tower-wbdiag3.log). Track which slots were
@@ -816,7 +816,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
private bool _gpuQueriesInitialized;
// Constructor accessibility is internal because EntityClassificationCache
- // is internal — a public ctor with an internal-typed parameter would be
+ // is internal — a public ctor with an internal-typed parameter would be
// an inconsistent-accessibility error. The dispatcher is constructed
// exclusively from GameWindow (same assembly), so internal is fine.
internal WbDrawDispatcher(
@@ -898,7 +898,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
/// (). Call once per frame BEFORE
/// . The dispatcher uploads it to binding=4 and selects each
/// object's up-to-8 lights from it ()
- /// by the object's bounding sphere — camera-independent. Pass null/empty to
+ /// by the object's bounding sphere — camera-independent. Pass null/empty to
/// disable per-object point lights (only ambient + sun render).
///
public void SetSceneLights(IReadOnlyList? pointSnapshot)
@@ -923,11 +923,11 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
/// Pair with on outdoor-root frames so the
/// dispatcher reverts to the U.3 no-clip-everything behavior.
///
- /// cellId → CellClip slot. A cell absent from the map
- /// is NOT visible → its cell-static instances are culled.
+ /// cellId → CellClip slot. A cell absent from the map
+ /// is NOT visible → its cell-static instances are culled.
/// Slot for outdoor scenery / building shells while
/// indoors (the OutsideView slot, or 0 for no-clip over-include).
- /// False ⇒ cull outdoor scenery / shells this frame
+ /// False ⇒ cull outdoor scenery / shells this frame
/// (the OutsideView is empty).
public void SetClipRouting(IReadOnlyDictionary cellIdToSlot, int outdoorSlot, bool outdoorVisible)
{
@@ -939,7 +939,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
}
///
- /// Phase U.4: revert to U.3 behavior — every instance maps to slot 0 (no-clip),
+ /// Phase U.4: revert to U.3 behavior — every instance maps to slot 0 (no-clip),
/// nothing is culled by clip routing. Call on outdoor-root frames (camera
/// outdoors) and any frame without a portal-visibility result.
///
@@ -951,16 +951,16 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
_outdoorVisible = false;
}
- // §4 flap [clip-route-disp] probe state (2026-06-10, throwaway): print-on-change
+ // §4 flap [clip-route-disp] probe state (2026-06-10, throwaway): print-on-change
// signature + monotonic sequence + reusable histogram. See RenderingDiagnostics
// .ProbeClipRouteEnabled for the full probe contract.
private string? _lastClipRouteDispSig;
private long _clipRouteDispSeq;
private readonly SortedDictionary _clipRouteHist = new();
- // §4 flap apparatus (2026-06-10): per-slot instance histogram as staged for binding=3.
+ // §4 flap apparatus (2026-06-10): per-slot instance histogram as staged for binding=3.
// grp.Slots is laid out 1:1 with grp.Matrices (binding=0), so this IS the slot content
- // the GPU reads per instance — if outdoor instances land on the wrong slot (or vanish
+ // the GPU reads per instance — if outdoor instances land on the wrong slot (or vanish
// into cullEnt) when the building flood merges, this line shows it directly.
private void EmitClipRouteDispatchProbe(int culledEntities)
{
@@ -1001,7 +1001,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
// are dropped entirely (not emitted into the binding=0 instance buffer NOR the
// binding=3 slot buffer), matching the existing frustum / visible-cell cull.
// Internal (not private) so the clip-slot unit tests can assert against it
- // directly — see WbDrawDispatcherClipSlotTests.
+ // directly — see WbDrawDispatcherClipSlotTests.
internal const int ClipSlotCull = -1;
///
@@ -1014,14 +1014,14 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
/// - ServerGuid != 0 with ParentCellId == null: CULL while routing is active.
///
/// Only called when _clipRoutingActive (indoor root). On the U.3 / outdoor
- /// path every instance is slot 0 and nothing is culled — see
+ /// path every instance is slot 0 and nothing is culled — see
/// , which gates on that flag.
///
/// INVARIANT: and the keys of
/// MUST live in the same FULL cell-id space
/// (lbMask | OtherCellId, e.g. 0xA9B40164). A bare-low-byte
/// ParentCellId (e.g. 0x64) would never match a full-id key and would
- /// silently CULL every indoor stab — cf. the L.2e bare-low-byte finding in
+ /// silently CULL every indoor stab — cf. the L.2e bare-low-byte finding in
/// CLAUDE.md where player CellId was tracked without its landblock prefix.
///
///
@@ -1073,12 +1073,12 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
/// (Slot, Culled) pair the per-entity loop body consumes. Wraps
/// with the
/// gate: when routing is INACTIVE (outdoor root / no portal frame), every entity
- /// is slot 0 and nothing is clip-culled — the bit-identical-to-U.3 property, so
+ /// is slot 0 and nothing is clip-culled — the bit-identical-to-U.3 property, so
/// the resolver (and ) is bypassed entirely.
- /// When active, a CULL sentinel maps to (0, culled=true) — the slot value
+ /// When active, a CULL sentinel maps to (0, culled=true) — the slot value
/// is never emitted for a culled entity.
/// internal static + pure so the whole policy (including the routing-
- /// inactive branch) is unit-testable — see WbDrawDispatcherClipSlotTests.
+ /// inactive branch) is unit-testable — see WbDrawDispatcherClipSlotTests.
///
internal static (uint Slot, bool Culled) ResolveSlotForFrame(
bool clipRoutingActive,
@@ -1106,7 +1106,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
/// Entry for per-landblock iteration.
/// Mirrors the shape yielded by GpuWorldState.LandblockEntries.
///
- internal readonly record struct LandblockEntry(
+ public readonly record struct LandblockEntry(
uint LandblockId,
Vector3 AabbMin,
Vector3 AabbMax,
@@ -1114,10 +1114,10 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
IReadOnlyDictionary? AnimatedById);
///
- /// Result of — the list of (entity, meshRef index)
+ /// Result of — the list of (entity, meshRef index)
/// pairs that passed all visibility filters, plus a diagnostic walk count.
///
- internal struct WalkResult
+ public struct WalkResult
{
public int EntitiesWalked;
public int BuildingShellAnchorPass;
@@ -1142,7 +1142,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
/// A.5 T18 Change #2: per-entity AABB cull reads from the cached
/// /
/// (refreshed lazily if ), instead of
- /// recomputing Position±5 each frame.
+ /// recomputing Position±5 each frame.
///
///
///
@@ -1177,7 +1177,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
/// visibleCellIds or frustum filters, and [indoor-walk] lines for
/// cell entities that pass all filters. Rate-limited by
/// . Pass (the default)
- /// to disable all probe emission — used by the test-friendly
+ /// to disable all probe emission — used by the test-friendly
/// overload.
///
///
@@ -1238,8 +1238,8 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
if (!EntityMatchesSet(entity, set)) continue;
if (entity.MeshRefs.Count == 0) continue;
- // Detect cell entity for indoor probes — first MeshRef.GfxObjId
- // is an EnvCell id (low 16 bits ≥ 0x0100). Cheap to compute;
+ // Detect cell entity for indoor probes — first MeshRef.GfxObjId
+ // is an EnvCell id (low 16 bits ≥ 0x0100). Cheap to compute;
// result reused for all probe checks below.
ulong cellProbeId = (ulong)entity.MeshRefs[0].GfxObjId;
bool isCellEntity = indoorProbeState is not null
@@ -1265,7 +1265,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
}
if (shellScoped) result.BuildingShellAnchorPass++;
- // Per-entity AABB frustum cull (perf #3). Animated entities bypass —
+ // Per-entity AABB frustum cull (perf #3). Animated entities bypass —
// they're tracked at landblock level + need per-frame work regardless.
// A.5 T18 Change #2: read cached AABB, refresh lazily on AabbDirty.
bool isAnimated = animatedEntityIds?.Contains(entity.Id) == true;
@@ -1291,7 +1291,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
continue;
}
- // Passed all filters — emit walk probe.
+ // Passed all filters — emit walk probe.
if (isCellEntity && RenderingDiagnostics.ProbeIndoorWalkEnabled
&& indoorProbeState!.ShouldEmit(cellProbeId))
{
@@ -1315,16 +1315,16 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
/// #119 ROOT-CAUSE FIX (2026-06-11): the Tier-1 cache hint must identify the
/// entity's OWNING landblock, not the Draw call's tuple landblock.
/// RetailPViewRenderer.DrawEntityBucket fabricates its tuple with the
- /// PLAYER's landblock id, so every bucket entity that frame shared one hint —
+ /// PLAYER's landblock id, so every bucket entity that frame shared one hint —
/// and colliding entity ids from different landblocks (the pre-fix
/// 0x40YYFF00 interior namespace discarded the landblock X byte) mapped
/// to the SAME cache key and served each other's batches: the AAB3 tower's
/// 43-part staircase drew a 1-part entity's 3 zero-RestPose batches
- /// (captured live, tower-dump-launch1.log) — the session-sticky "broken
+ /// (captured live, tower-dump-launch1.log) — the session-sticky "broken
/// stairs + water barrel". Interior statics carry their owning cell; derive
/// the hint from it, canonicalized to the same 0xXXYYFFFF key format
/// the streaming entries and
- /// use — which also makes owner-unload invalidation actually hit these
+ /// use — which also makes owner-unload invalidation actually hit these
/// entries (bucket-hinted entries were previously orphaned forever).
/// Entities without a ParentCellId (outdoor stabs / scenery / building
/// shells via GpuWorldState entries) keep the tuple id, which IS their
@@ -1404,7 +1404,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
/// (meshRefs, cacheBatches, zeroTranslations, culled) signature changes.
/// Discriminates H-A (hydration-time MeshRef corruption: translations
/// collapsed to ~zero / missing parts) from H-B (Tier-1 cache holding a
- /// partial or stale batch set) from H-C (both healthy ⇒ draw-side compose).
+ /// partial or stale batch set) from H-C (both healthy ⇒ draw-side compose).
///
private void MaybeEmitEntityDump(
in RenderInstanceCandidate entity,
@@ -1503,7 +1503,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
out Matrix4x4 vp,
out Vector3 camPos);
- // ── Phase 1: clear groups, walk entities, build groups ──────────────
+ // ── Phase 1: clear groups, walk entities, build groups ──────────────
// Draw is invoked several times per frame (landscape slices, late
// dynamics, paperdoll). Per-dispatch payloads reset here, while group
// retirement happens once in BeginFrame from whole-frame liveness.
@@ -1534,7 +1534,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
if (RenderingDiagnostics.ProbeIndoorCullEnabled || RenderingDiagnostics.ProbeIndoorWalkEnabled)
{
// _currentFrame is snapped at construction time. Construct
- // once per Draw() call only — a second construction within
+ // once per Draw() call only — a second construction within
// the same frame would stamp the dictionary with the
// (already-advanced) counter value, suppressing the second
// pass's emissions for IndoorProbeRateLimitFrames frames.
@@ -1572,35 +1572,35 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
// end of the loop for the last entity. Flushing per-tuple would
// overwrite earlier MeshRefs (the cache is keyed by entity.Id), so
// multi-part Setup-backed entities would only retain their LAST
- // MeshRef's batches — bug fixed in commit after 2f489a8.
+ // MeshRef's batches — bug fixed in commit after 2f489a8.
uint? populateEntityId = null;
uint populateLandblockId = 0;
- // §4 flap [clip-route-disp] probe (2026-06-10, throwaway): entities dropped by
- // ResolveSlotForFrame's CULL sentinel this Draw. One increment per culled entity —
+ // §4 flap [clip-route-disp] probe (2026-06-10, throwaway): entities dropped by
+ // ResolveSlotForFrame's CULL sentinel this Draw. One increment per culled entity —
// cheap enough to count unconditionally; emission below is probe-gated.
int probeCulledEntities = 0;
- // Tier 1 cache (#53) — fast-path one-shot tracker. The cache stores a
+ // Tier 1 cache (#53) — fast-path one-shot tracker. The cache stores a
// FLAT list of batches across all MeshRefs of an entity, so a single
// ApplyCacheHit call already drew every batch. _walkScratch yields
// one tuple per (entity, MeshRefIndex), so without this guard a
// 3-MeshRef static entity on a frame-2 cache hit would call
- // ApplyCacheHit 3 times — appending all 6 batches × 3 = 18 instances
- // to _groups instead of 6. Result: severe Z-fighting + 3× perf hit
+ // ApplyCacheHit 3 times — appending all 6 batches × 3 = 18 instances
+ // to _groups instead of 6. Result: severe Z-fighting + 3× perf hit
// on every multi-part static entity (buildings, statues, multi-MeshRef
// NPCs). The fast path must fire only on the FIRST tuple of each
// entity; subsequent tuples skip via this tracker.
uint? lastHitEntityId = null;
- // Tier 1 cache (#53) — incomplete-entity guard. When any MeshRef of
+ // Tier 1 cache (#53) — incomplete-entity guard. When any MeshRef of
// the current entity has _meshAdapter.TryGetRenderData return null
// (mesh still async-decoding via ObjectMeshManager.PrepareMeshDataAsync),
// we mark the entity incomplete and DROP the accumulated populate
// scratch at entity boundary instead of writing it to the cache.
// Otherwise the cache would hold a partial classification (some parts
// missing), and frame-2 cache hits would persist that partial render
- // even after the missing mesh loads — every subsequent frame sees the
+ // even after the missing mesh loads — every subsequent frame sees the
// cache hit and skips re-classification, so the missing parts never
// recover. User-visible symptom: the drudge statue on top of the
// Foundry (multi-part Setup entity with AnimPartChange) renders with
@@ -1642,9 +1642,9 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
lastHitEntityId = null;
}
- // Tier 1 cache (#53) — drop the previous entity's accumulated
+ // Tier 1 cache (#53) — drop the previous entity's accumulated
// populate scratch BEFORE MaybeFlushOnEntityChange runs. If the
- // previous entity ended incomplete (≥1 null renderData), we MUST
+ // previous entity ended incomplete (≥1 null renderData), we MUST
// NOT cache its partial classification: clear scratch and null
// the tracker so MaybeFlushOnEntityChange sees the cleaned state
// and no-ops for this entity. Reset the incomplete flag for the
@@ -1653,11 +1653,11 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
// CRITICAL: the flag reset must fire ONLY on entity change, not
// every tuple. Resetting per-tuple within the same entity would
// undo a null-renderData flag set by a previous tuple of the same
- // entity → if the missing MeshRef sits in the MIDDLE of the
+ // entity → if the missing MeshRef sits in the MIDDLE of the
// entity's MeshRefs list, a later valid tuple's reset would
// re-mark the entity "complete" and let partial data populate
// the cache. Trees with [trunk valid, branches null, leaves
- // valid] hit this exactly — branches never recover.
+ // valid] hit this exactly — branches never recover.
// #119 root-cause fix: cache operations key on the entity's OWNING
// landblock, never the Draw call's tuple landblock (which is the
// PLAYER's landblock on the bucket path). See ResolveCacheLandblockHint.
@@ -1687,7 +1687,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
// Fix B: select this entity's up-to-8 point/spot lights ONCE (the set
// is constant across the entity's parts/tuples), by the entity's
- // bounding sphere — camera-INDEPENDENT (minimize_object_lighting).
+ // bounding sphere — camera-INDEPENDENT (minimize_object_lighting).
ComputeEntityLightSet(entity);
_currentEntitySelectionLighting =
_selectionLighting?.TryGetLighting(
@@ -1749,7 +1749,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
// Placed AFTER the entity-change flush above so that, on a
// hit, this iteration also finishes flushing any pending
// populate state from a previous entity. Animated entities never
- // enter this branch — the !isAnimated guard makes that explicit.
+ // enter this branch — the !isAnimated guard makes that explicit.
//
// Fires ONCE per entity: the first tuple reaches here, runs
// ApplyCacheHit, sets lastHitEntityId, and continues. Subsequent
@@ -1769,7 +1769,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
// anyVao recovery: when the first visible entity in the frame
// takes the fast path, no slow-path lookup has populated
// anyVao yet. Look up THIS entity's first MeshRef once via
- // the mesh adapter — cheap dict lookup, not a re-classify.
+ // the mesh adapter — cheap dict lookup, not a re-classify.
if (anyVao == 0)
{
MeshRef firstMeshRef = tuple.MeshRef;
@@ -1783,7 +1783,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
#if DEBUG
// Cross-check guard: assert the membership predicate held at hit time.
// The full re-classification cross-check (spec section 6.5) is a stretch
- // goal; this simpler assert catches the prior Tier 1 bug class — a
+ // goal; this simpler assert catches the prior Tier 1 bug class — a
// static entity that turns out to actually be animated would fire here.
//
// Structurally redundant with the `if (!isAnimated && ...)` branch
@@ -1793,7 +1793,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
// entities into the fast path; the assert catches that immediately.
System.Diagnostics.Debug.Assert(
!isAnimated,
- $"EntityClassificationCache hit on animated entity {entity.Id} — invariant violated");
+ $"EntityClassificationCache hit on animated entity {entity.Id} — invariant violated");
#endif
continue;
@@ -1806,7 +1806,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
paletteIdentity = TextureCache.GetPaletteIdentity(entity.PaletteOverride);
// Note: GameWindow's spawn path already applies
- // AnimPartChanges + GfxObjDegradeResolver (Issue #47 fix —
+ // AnimPartChanges + GfxObjDegradeResolver (Issue #47 fix —
// close-detail mesh swap for humanoids) to MeshRefs. We
// trust MeshRefs as the source of truth here. AnimatedEntityState's
// overrides become relevant only for hot-swap (0xF625
@@ -1816,7 +1816,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
var renderData = _meshAdapter.TryGetRenderData(gfxObjId);
- // [indoor-lookup] probe — emit once per cell entity per sec.
+ // [indoor-lookup] probe — emit once per cell entity per sec.
// Fires BEFORE the null-renderData early-continue so a miss still
// emits hit=false, distinguishing H2 (empty batches) from H6
// (dispatcher fails to traverse Setup).
@@ -1863,7 +1863,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
currentEntityIncomplete = true;
if (diag) _meshesMissing++;
// #128 self-heal: a missing-but-referenced mesh re-requests
- // its load HERE — the one site that touches it every frame —
+ // its load HERE — the one site that touches it every frame —
// so a preparation lost to landblock churn (cancelled after
// the last registration event) can never stay lost. Deduped
// per Draw; PrepareMeshDataAsync is idempotent while pending.
@@ -1888,11 +1888,11 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
if (renderData.IsSetup && renderData.SetupParts.Count > 0)
{
// #188: setupPartIndex is the SAME index space
- // TransparentPartHook.PartIndex addresses — retail's CPartArray
+ // TransparentPartHook.PartIndex addresses — retail's CPartArray
// numbers parts by their ordinal position in the Setup's own
// part list (SetupPartTransforms.Compute is the other verified
// consumer of this exact indexing: one rigid pose per
- // Setup.Parts[i]). NOT the outer per-MeshRef loop index — a
+ // Setup.Parts[i]). NOT the outer per-MeshRef loop index — a
// MeshRef is acdream's own decomposition of top-level
// attachments (weapon/shield/etc), a different concept.
for (int setupPartIndex = 0; setupPartIndex < renderData.SetupParts.Count; setupPartIndex++)
@@ -1903,7 +1903,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
{
// #128 self-heal + #53: a missing Setup PART must mark
// the entity incomplete (else a partial batch set
- // caches permanently — the same bug class one level
+ // caches permanently — the same bug class one level
// deeper) and re-request its load like the MeshRef
// path above.
currentEntityIncomplete = true;
@@ -1920,11 +1920,11 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
var model = ComposePartWorldMatrix(
entityWorld, meshRef.PartTransform, partTransform);
- // [indoor-xform] probe — only for the cell's synthetic
+ // [indoor-xform] probe — only for the cell's synthetic
// geometry part (bit 32 set, per WB's PrepareEnvCellMeshData
// cellGeomId convention). One line per part per sec.
- // Disambiguates hypothesis H5 (transform double-apply —
- // composedT lands at 2 × cellOrigin).
+ // Disambiguates hypothesis H5 (transform double-apply —
+ // composedT lands at 2 × cellOrigin).
if ((partGfxObjId & 0x1_0000_0000UL) != 0
&& RenderingDiagnostics.ProbeIndoorXformEnabled
&& ShouldEmitIndoorProbe(partGfxObjId))
@@ -1941,7 +1941,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
// #188 retail CPhysicsPart::Draw (0x0050d7a0) early-out: once a
// part's translucency hits EXACTLY 1.0 (fully invisible), retail
- // sets draw_state|=1 and skips the whole part outright — not a
+ // sets draw_state|=1 and skips the whole part outright — not a
// blend to nothing. TranslucencyFadeManager.AdvanceAll guarantees
// t=1 commits the bitwise-exact value so this check is safe.
float opacityMultiplier = 1.0f;
@@ -1968,7 +1968,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
}
else
{
- // #188: a bare (non-Setup) GfxObj entity has exactly one part —
+ // #188: a bare (non-Setup) GfxObj entity has exactly one part —
// retail's CPartArray for such an object is a single-entry array,
// so TransparentPartHook.PartIndex for it is always 0.
float opacityMultiplier = 1.0f;
@@ -2013,8 +2013,8 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
if (diag && drewAny) _entitiesDrawn++;
}
- // Tier 1 cache (#53) — drop the accumulated populate scratch if the
- // LAST entity in the loop ended incomplete (had ≥1 null renderData).
+ // Tier 1 cache (#53) — drop the accumulated populate scratch if the
+ // LAST entity in the loop ended incomplete (had ≥1 null renderData).
// Same reason as the entity-boundary handling above: avoid caching a
// partial classification. The slow path will retry on the next frame
// and populate correctly once all meshes have loaded.
@@ -2033,7 +2033,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
populateEntityId, populateLandblockId, _cache,
_populateScratch, _populateSelectionScratch);
- // §4 flap [clip-route-disp] probe (2026-06-10, throwaway): the per-slot instance
+ // §4 flap [clip-route-disp] probe (2026-06-10, throwaway): the per-slot instance
// histogram exactly as it will be uploaded to binding=3 (grp.Slots) plus the
// culled-entity count. Routed draws only (the landscape pass under DrawInside) so the
// unrouted per-cell bucket draws don't oscillate the print-on-change signature.
@@ -2102,7 +2102,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
bool diag,
bool observeCurrentPath)
{
- // Nothing visible — skip the GL pass entirely.
+ // Nothing visible — skip the GL pass entirely.
if (anyVao == 0)
{
LastDrawStats = new DrawStats(set, entitiesWalked, tupleCount, 0, 0, 0, 0, 0, 0);
@@ -2116,7 +2116,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
return;
}
- // ── Phase 3: assign FirstInstance per group, lay matrices contiguously, sort opaque ──
+ // ── Phase 3: assign FirstInstance per group, lay matrices contiguously, sort opaque ──
bool deferTransparent = _alphaQueue?.IsCollecting == true;
var instanceCounts = PartitionInstanceGroups(
groups,
@@ -2152,7 +2152,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
// Phase U.4: size the per-instance clip-slot buffer to match the instance
// count and lay it out in the SAME group order / cursor as _instanceData,
// so instanceClipSlot[i] (binding=3) tracks Instances[i] (binding=0). On
- // the U.3 / outdoor path every Slots entry is 0 ⇒ identical to U.3.
+ // the U.3 / outdoor path every Slots entry is 0 ⇒ identical to U.3.
if (_clipSlotData.Length < immediateInstances)
_clipSlotData = new uint[immediateInstances + 256];
@@ -2198,8 +2198,8 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
// submission so the shared queue can interleave it with particles.
// Immediate mode remains for sealed off-screen consumers such as the
// paperdoll and UI Studio render stack.
- // ── Phase 4: build IndirectGroupInput list (opaque sorted, then translucent),
- // fill via BuildIndirectArrays ──────────────────────────────────
+ // ── Phase 4: build IndirectGroupInput list (opaque sorted, then translucent),
+ // fill via BuildIndirectArrays ──────────────────────────────────
int immediateTransparentCount = deferTransparent ? 0 : _translucentDraws.Count;
int totalDraws = _opaqueDraws.Count + immediateTransparentCount;
TrackScratchDemand(Math.Max(totalInstances, totalDraws));
@@ -2260,7 +2260,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
deferTransparent,
camPos);
- // ── Phase 5: upload four buffers ────────────────────────────────────
+ // ── Phase 5: upload four buffers ────────────────────────────────────
ActivateNextDynamicBufferSet();
fixed (float* ip = _instanceData)
UploadSsbo(_instanceSsbo, 0, ref _instanceSsboCapacityBytes,
@@ -2273,9 +2273,9 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
// Phase U.4: per-instance clip-slot buffer (binding=3), one uint per
// instance, laid out parallel to _instanceData in Phase 3's group loop so
// instanceClipSlot[instanceIndex] tracks Instances[instanceIndex]. On the
- // U.3 / outdoor path every entry is 0 ⇒ slot 0 ⇒ no-clip (identical to
+ // U.3 / outdoor path every entry is 0 ⇒ slot 0 ⇒ no-clip (identical to
// U.3); under indoor routing it holds the per-instance slot from
- // ResolveEntitySlot. No clear here — Phase 3 wrote exactly immediateInstances
+ // ResolveEntitySlot. No clear here — Phase 3 wrote exactly immediateInstances
// entries; only [0..immediateInstances) is uploaded, so any stale tail is
// never read by the shader.
fixed (uint* sp = _clipSlotData)
@@ -2284,14 +2284,14 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
// #142: per-instance indoor flag buffer (binding=6), one uint per instance,
// laid out parallel to _instanceData in Phase 3. Only [0..immediateInstances)
- // is uploaded — stale tail never read (same guarantee as clip-slot above).
+ // is uploaded — stale tail never read (same guarantee as clip-slot above).
fixed (uint* dp = _indoorData)
UploadSsbo(_instIndoorSsbo, 6, ref _instIndoorSsboCapacityBytes,
dp, immediateInstances * sizeof(uint));
// #188: per-instance opacity buffer (binding=7), one float per instance,
// laid out parallel to _instanceData in Phase 3. Only [0..immediateInstances)
- // is uploaded — stale tail never read (same guarantee as clip-slot above).
+ // is uploaded — stale tail never read (same guarantee as clip-slot above).
fixed (float* ap = _alphaData)
UploadSsbo(_instAlphaSsbo, 7, ref _instAlphaSsboCapacityBytes,
ap, immediateInstances * sizeof(float));
@@ -2306,7 +2306,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
// Fix B: global point-light buffer (binding=4) + per-instance light-set
// buffer (binding=5). The global buffer is this frame's PointSnapshot; the
// per-instance buffer holds 8 int indices into it per instance, laid out
- // parallel to _instanceData in Phase 3. Both bound with ≥1 element so the
+ // parallel to _instanceData in Phase 3. Both bound with ≥1 element so the
// shader never reads an unbound SSBO on a no-lights frame.
UploadGlobalLights();
fixed (int* lp = _lightSetData)
@@ -2336,7 +2336,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
// shader never reads an unbound SSBO at binding=2.
BindClipRegionBinding2();
- // ── Phase 6: bind global VAO once ───────────────────────────────────
+ // ── Phase 6: bind global VAO once ───────────────────────────────────
_gl.BindVertexArray(anyVao);
if (string.Equals(Environment.GetEnvironmentVariable("ACDREAM_NO_CULL"), "1", StringComparison.Ordinal))
@@ -2346,11 +2346,11 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
// result (the oldest data in the ring) before overwriting it with
// frame N's queries. Hoisted to function scope so both the opaque
// and transparent passes below can reference gpuQuerySlot. See spec
- // §3 Q1/Q2 + §4 in
+ // §3 Q1/Q2 + §4 in
// docs/superpowers/specs/2026-05-11-phase-n6-slice1-design.md.
int gpuQuerySlot = _gpuQueryFrameIndex % GpuQueryRingDepth;
// diag is part of the gate so the read/issue/increment trio stays
- // symmetric — without it, toggling ACDREAM_WB_DIAG mid-session would
+ // symmetric — without it, toggling ACDREAM_WB_DIAG mid-session would
// freeze the frame counter (gated by diag below) while the read kept
// re-reading the same slot, producing duplicate stale samples.
if (diag && _gpuQueriesInitialized && _gpuQueryFrameIndex >= GpuQueryRingDepth)
@@ -2391,14 +2391,14 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
}
}
- // ── Phase 7: opaque pass ─────────────────────────────────────────────
+ // ── Phase 7: opaque pass ─────────────────────────────────────────────
if (_opaqueDrawCount > 0)
{
_gl.Disable(EnableCap.Blend);
_gl.DepthMask(true);
- // A.5 T20: enable A2C for ClipMap foliage — GPU derives sample mask
+ // A.5 T20: enable A2C for ClipMap foliage — GPU derives sample mask
// from the alpha written by mesh_modern.frag so foliage edges are
- // smooth under MSAA 4x. A no-op for fully-opaque (α=1) batches.
+ // smooth under MSAA 4x. A no-op for fully-opaque (α=1) batches.
// A.5 T22.5: gated by AlphaToCoverage property so Low/Medium presets
// (no MSAA) skip the unnecessary GL state change.
if (AlphaToCoverage) _gl.Enable(EnableCap.SampleAlphaToCoverage);
@@ -2418,7 +2418,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
if (AlphaToCoverage) _gl.Disable(EnableCap.SampleAlphaToCoverage);
}
- // ── Phase 8: transparent pass ────────────────────────────────────────
+ // ── Phase 8: transparent pass ────────────────────────────────────────
if (_transparentDrawCount > 0)
{
_gl.Enable(EnableCap.Blend);
@@ -2426,8 +2426,8 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
_gl.DepthMask(false);
// Phase Post-A.5 (ISSUE #52, 2026-05-10): transparent section of
// Batches[] starts at index _opaqueDrawCount. Without this offset,
- // each transparent draw reads BatchData[0..transparentCount) — the
- // OPAQUE section — and the lifestone crystal's apparent texture
+ // each transparent draw reads BatchData[0..transparentCount) — the
+ // OPAQUE section — and the lifestone crystal's apparent texture
// flickers to whatever opaque batch sorted first that frame. See
// uDrawIDOffset comment in mesh_modern.vert.
_shader.SetInt("uDrawIDOffset", _opaqueDrawCount);
@@ -2481,7 +2481,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
/// explicit cell list (not the BFS-derived visibility set). The semantic
/// difference is at the caller: cellIds = the camera-buildings' EnvCellIds,
/// not the portal BFS result. The dispatcher's internal logic is identical
- /// — it filters indoor entities by membership in the provided set.
+ /// — it filters indoor entities by membership in the provided set.
///
public void Draw(
ICamera camera,
@@ -2494,7 +2494,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
HashSet? animatedEntityIds = null,
EntitySet set = EntitySet.All)
{
- // Adapt IReadOnlyCollection → HashSet for the existing path.
+ // Adapt IReadOnlyCollection → HashSet for the existing path.
// If the caller already passed a HashSet, avoid re-wrapping.
HashSet cellIdSet = cellIds is HashSet hs ? hs : new HashSet(cellIds);
Draw(camera, landblockEntries,
@@ -3442,8 +3442,8 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
///
/// Fix B: pack into the binding=4 global light
/// buffer (one GlobalLight = 4 vec4 = 16 floats, std430 stride 64 bytes,
- /// matching mesh_modern.vert's GlobalLight). Always uploads ≥1 element
- /// so the shader never reads an unbound SSBO — on a no-lights frame index 0 is
+ /// matching mesh_modern.vert's GlobalLight). Always uploads ≥1 element
+ /// so the shader never reads an unbound SSBO — on a no-lights frame index 0 is
/// a zeroed dummy that no instance set references (all sets are -1).
///
private unsafe void UploadGlobalLights()
@@ -3461,8 +3461,8 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
/// when a new one was registered since the
/// last flush (by or ),
/// then (re)binds it at .
- /// A genuinely new handle is rare — new dat surfaces/composite overrides,
- /// not every frame — so unlike the SSBOs above this is not part of the
+ /// A genuinely new handle is rare — new dat surfaces/composite overrides,
+ /// not every frame — so unlike the SSBOs above this is not part of the
/// ring-buffered ; see
/// 's doc comment.
///
@@ -3511,7 +3511,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
if (_fallbackClipRegionSsbo == 0)
{
_fallbackClipRegionSsbo = _gl.GenBuffer();
- // One CellClip slot, all zeros: count 0 ⇒ shader passes every plane.
+ // One CellClip slot, all zeros: count 0 ⇒ shader passes every plane.
var zero = stackalloc byte[ClipFrame.CellClipStrideBytes];
for (int i = 0; i < ClipFrame.CellClipStrideBytes; i++) zero[i] = 0;
_gl.BindBuffer(BufferTargetARB.ShaderStorageBuffer, _fallbackClipRegionSsbo);
@@ -3532,7 +3532,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
long gpuMed = MedianMicros(_gpuSamples);
long gpuP95 = Percentile95Micros(_gpuSamples);
// A.5 T23: flag when entity dispatcher median exceeds 2.0ms budget
- // (Phase A.5 spec §2 acceptance criterion 6). Grep-friendly prefix.
+ // (Phase A.5 spec §2 acceptance criterion 6). Grep-friendly prefix.
const long BudgetUs = 2000;
string budgetFlag = cpuMed > BudgetUs ? " BUDGET_OVER" : "";
Console.WriteLine(
@@ -3540,7 +3540,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
$"cpu_us={cpuMed}m/{cpuP95}p95 gpu_us={gpuMed}m/{gpuP95}p95");
_entitiesSeen = _entitiesDrawn = _meshesMissing = _drawsIssued = _instancesIssued = 0;
_lastLogTick = now;
- // Don't reset the sample buffers — they're a moving window of the
+ // Don't reset the sample buffers — they're a moving window of the
// last 256 frames; clearing per 5s flush would lose recent history.
}
}
@@ -3553,7 +3553,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
foreach (var v in copy) if (v > 0) nz++;
if (nz == 0) return 0;
// Sorted ascending: zero-padding front, samples at the back. (nz - 1) / 2
- // from the end keeps the offset >= 0 for all nz >= 1 — the original
+ // from the end keeps the offset >= 0 for all nz >= 1 — the original
// nz / 2 form indexed copy[copy.Length] (crash) on the first diag flush
// when exactly 1 sample was recorded. Same fix as GameWindow's
// TerrainDiagMedianMicros twin.
@@ -3571,7 +3571,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
return copy[idx];
}
- // ── Tier 1 cache (#53) helpers extracted for testability ─────────────────
+ // ── Tier 1 cache (#53) helpers extracted for testability ─────────────────
//
// Three pure-CPU static helpers carved out of Draw's per-entity loop so
// unit tests can exercise the populate/flush algorithm + cache-hit fast
@@ -3669,7 +3669,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
/// AND differs from , the previous
/// entity's accumulated batches are committed to
/// and is cleared. Returns the
- /// updated tracker tuple — pass these back into the field locals in the
+ /// updated tracker tuple — pass these back into the field locals in the
/// caller's loop.
///
///
@@ -3711,7 +3711,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
/// so commit its accumulated batches here. No-op when no populate is
/// pending (the last entity was animated, or the scratch is empty).
///
- /// End-of-loop only — does NOT reset the caller's tracker locals
+ /// End-of-loop only — does NOT reset the caller's tracker locals
/// (intentional, since they go out of scope immediately after).
///
///
@@ -3784,11 +3784,11 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
grp.Matrices.Add(model);
grp.LocalSortCenters.Add(localSortCenter);
grp.SubmissionOrders.Add(_nextInstanceSubmissionOrder++);
- grp.Slots.Add(_currentEntitySlot); // Phase U.4 — parallel to Matrices
- AppendCurrentLightSet(grp); // Fix B — 8 ints per instance, parallel to Matrices
+ grp.Slots.Add(_currentEntitySlot); // Phase U.4 — parallel to Matrices
+ AppendCurrentLightSet(grp); // Fix B — 8 ints per instance, parallel to Matrices
// #188: cache-hit entities are always non-animated (the Tier-1 cache
// gates on !isAnimated), and TranslucencyFadeManager only ever holds
- // state for entities whose animation hooks fired — so a cached
+ // state for entities whose animation hooks fired — so a cached
// instance can never be mid-fade. Always unmodified opacity.
grp.Opacities.Add(1.0f);
grp.SelectionLighting.Add(_currentEntitySelectionLighting);
@@ -3800,11 +3800,11 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
/// sphere. Camera-independent (), so
/// a static building's torches stay constant as the viewer moves. Fills
/// ; unused slots are -1. On the no-lights
- /// path (no snapshot handed in) every slot is -1 ⇒ shader adds no point light.
+ /// path (no snapshot handed in) every slot is -1 ⇒ shader adds no point light.
///
///
/// A7 Fix D round 2 (2026-06-19): retail lights OUTDOOR objects with the SUN +
- /// ambient ONLY — never the static wall torches. The per-object torch step
+ /// ambient ONLY — never the static wall torches. The per-object torch step
/// (minimize_object_lighting, 0x0054d480) runs ONLY in the indoor stage:
/// RenderDeviceD3D::DrawMeshInternal (0x0059f398) calls it under
/// if (Render::useSunlight == 0), and the outdoor landscape stage runs
@@ -3815,16 +3815,16 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
/// creatures get the sun, not torches. We mirror that: only objects parented to
/// an EnvCell (indoor) select torches; outdoor objects keep the all-(-1) set so
/// the sun path alone lights them. This is what made the Holtburg meeting-hall
- /// facade wash out warm — the dat's intensity-100 wall torches (range
- /// Falloff×1.3) were flooding the exterior shell that retail never torch-lights.
+ /// facade wash out warm — the dat's intensity-100 wall torches (range
+ /// Falloff×1.3) were flooding the exterior shell that retail never torch-lights.
/// The indoor "no sun" half is already handled by the global sun kill when the
/// player is inside a cell (UpdateSunFromSky). See the divergence register
/// (AP-43) and docs/research/2026-06-19-lighting-a7-fixD-round2-*.
///
///
- // #176 seam-draw probe (ACDREAM_PROBE_SEAMDRAW) — throwaway apparatus. One
+ // #176 seam-draw probe (ACDREAM_PROBE_SEAMDRAW) — throwaway apparatus. One
// [seam-ent] line per target-cell entity, re-emitted on state change: world
- // position (F3 z — entities do NOT get the +0.02 shell lift), cull/slot,
+ // position (F3 z — entities do NOT get the +0.02 shell lift), cull/slot,
// and the SelectForObject light set resolved to identities (owner-cell
// low16 + intensity). Sig dict is bounded by the handful of entities that
// ever live in the target cells.
@@ -3865,7 +3865,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
{
// #142: set the indoor flag first so it's available even when the early-return
// fires below. Both the torch selection and the sun gate use the same predicate,
- // so they can't disagree — one call, one truth.
+ // so they can't disagree — one call, one truth.
_currentEntityIndoor =
IndoorObjectReceivesTorches(entity.ParentCell);
@@ -3889,7 +3889,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
/// Retail's useSunlight gate for per-object torch lighting, as a pure
/// predicate. An object receives the static wall torches (the indoor
/// minimize_object_lighting pass) ONLY when it is parented to an EnvCell
- /// — an interior cell, by the AC convention (cellId & 0xFFFF) >= 0x0100.
+ /// — an interior cell, by the AC convention (cellId & 0xFFFF) >= 0x0100.
/// Outdoor objects (building shells with null ,
/// outdoor scenery in a land sub-cell 0x0001..0x00FF, outdoor creatures)
/// are sun-lit only and return false. Mirrors
@@ -3899,7 +3899,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
internal static bool IndoorObjectReceivesTorches(uint? parentCellId)
=> parentCellId.HasValue
&& (parentCellId.Value & 0xFFFFu) >= 0x0100u
- && (parentCellId.Value & 0xFFFFu) != 0xFFFFu; // 0xFFFF = landblock marker, not an EnvCell → outdoor
+ && (parentCellId.Value & 0xFFFFu) != 0xFFFFu; // 0xFFFF = landblock marker, not an EnvCell → outdoor
///
/// Fix B: append the current entity's 8-slot light set to a group's
@@ -3931,7 +3931,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
// #188: a mid-fade instance whose surface is otherwise Opaque/ClipMap
// must route through the alpha-blend pass so mesh_modern.frag's
- // (blend-enabled) shader actually composites the reduced alpha —
+ // (blend-enabled) shader actually composites the reduced alpha —
// the no-blend opaque pass would ignore it.
if (opacityMultiplier < 1.0f && IsOpaque(translucency))
translucency = TranslucencyKind.AlphaBlend;
@@ -3956,9 +3956,9 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
grp.Matrices.Add(model);
grp.LocalSortCenters.Add(renderData.SortCenter);
grp.SubmissionOrders.Add(_nextInstanceSubmissionOrder++);
- grp.Slots.Add(_currentEntitySlot); // Phase U.4 — parallel to Matrices
- AppendCurrentLightSet(grp); // Fix B — 8 ints per instance, parallel to Matrices
- grp.Opacities.Add(opacityMultiplier); // #188 — parallel to Matrices
+ grp.Slots.Add(_currentEntitySlot); // Phase U.4 — parallel to Matrices
+ AppendCurrentLightSet(grp); // Fix B — 8 ints per instance, parallel to Matrices
+ grp.Opacities.Add(opacityMultiplier); // #188 — parallel to Matrices
grp.SelectionLighting.Add(_currentEntitySelectionLighting);
collector?.Add(new CachedBatch(
key,
@@ -4085,7 +4085,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
EntitySet set)
{
// No cell filter (outdoor root, or a bucket drawn unfiltered like live-dynamics / outdoor
- // scenery) ⇒ every entity passes; clip-slot routing (ResolveEntitySlot) does the gating.
+ // scenery) ⇒ every entity passes; clip-slot routing (ResolveEntitySlot) does the gating.
if (visibleCellIds is null)
return true;
@@ -4094,7 +4094,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
return visibleCellIds.Contains(entity.ParentCellId.Value);
// ParentCellId == null (outdoor scenery / building shell): NOT a member of any interior cell,
- // so it does NOT pass a cell-membership filter (R1: the bleed fix — was an unconditional
+ // so it does NOT pass a cell-membership filter (R1: the bleed fix — was an unconditional
// `return true`). When such entities must draw (through the doorway), the caller passes
// visibleCellIds: null and relies on ResolveEntitySlot's OutsideView routing instead.
return false;
@@ -4272,24 +4272,24 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
_gpuQueriesInitialized = false;
}
- // ── Public types + helpers for BuildIndirectArrays (Task 9) ─────────────
+ // ── Public types + helpers for BuildIndirectArrays (Task 9) ─────────────
//
// These are public so the pure-CPU unit tests in AcDream.Core.Tests can
// exercise BuildIndirectArrays without needing a GL context.
///
/// Stride in bytes of DrawElementsIndirectCommand in the indirect buffer.
- /// 5 × uint = 20 bytes. Tests and callers reference this symbolically
+ /// 5 × uint = 20 bytes. Tests and callers reference this symbolically
/// rather than hard-coding 20 so a layout change produces a compile error.
///
- public const int DrawCommandStride = 20; // sizeof(DrawElementsIndirectCommand): 5 × uint
+ public const int DrawCommandStride = 20; // sizeof(DrawElementsIndirectCommand): 5 × uint
///
- /// Public view of the per-group inputs to — used in tests.
+ /// Public view of the per-group inputs to — used in tests.
/// Campaign V slice V2: TextureIndex is a slot into the binding=9
/// handle table (was a raw 64-bit bindless TextureHandle).
///
- internal readonly record struct IndirectGroupInput(
+ public readonly record struct IndirectGroupInput(
int IndexCount,
uint FirstIndex,
int BaseVertex,
@@ -4305,7 +4305,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
/// Tests verify the layout. Same field shape as the private BatchData.
///
[StructLayout(LayoutKind.Sequential, Pack = 4)]
- internal struct BatchDataPublic
+ public struct BatchDataPublic
{
public uint TextureIndex;
public uint Reserved;
@@ -4314,7 +4314,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
}
/// Result of .
- internal readonly record struct IndirectLayoutResult(
+ public readonly record struct IndirectLayoutResult(
int OpaqueCount,
int TransparentCount,
int TransparentByteOffset);
@@ -4325,8 +4325,8 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
/// Pure CPU, no GL state. Caller passes pre-sized scratch arrays.
///
///
- /// Classification: Opaque + ClipMap → opaque pass (ClipMap uses discard, not
- /// blending). Everything else (AlphaBlend, Additive, InvAlpha) → transparent pass.
+ /// Classification: Opaque + ClipMap → opaque pass (ClipMap uses discard, not
+ /// blending). Everything else (AlphaBlend, Additive, InvAlpha) → transparent pass.
///
public static IndirectLayoutResult BuildIndirectArrays(
IReadOnlyList groups,
@@ -4385,15 +4385,15 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
///
/// Public test shim for . Locks in the N.5 Decision 2
- /// translucency partition: Opaque + ClipMap → opaque indirect; AlphaBlend +
- /// Additive + InvAlpha → transparent indirect.
+ /// translucency partition: Opaque + ClipMap → opaque indirect; AlphaBlend +
+ /// Additive + InvAlpha → transparent indirect.
///
public static bool IsOpaquePublic(TranslucencyKind t) => IsOpaque(t);
private static bool IsOpaque(TranslucencyKind t)
=> t == TranslucencyKind.Opaque || t == TranslucencyKind.ClipMap;
- // ────────────────────────────────────────────────────────────────────────
+ // ────────────────────────────────────────────────────────────────────────
///
/// Thin wrapper around an instance's rate-limit dictionary + frame
@@ -4496,7 +4496,7 @@ internal sealed unsafe partial class WbDrawDispatcher : IDisposable
/// appended in lockstep (one entry per drawn instance) during group build, so
/// they MUST all be cleared together each frame. Keeping the reset in one
/// method stops a newly-added parallel list from silently drifting out of the
- /// frame lifecycle — which is exactly the #193 OOM: #188 added
+ /// frame lifecycle — which is exactly the #193 OOM: #188 added
/// alongside the others but left it out of the old
/// inline clear loop, so it grew one float per instance per frame forever and
/// leaked ~1 GB/min of LOH float[] as its backing array doubled.
diff --git a/src/AcDream.App/Rendering/Wb/WbFrustum.cs b/src/AcDream.App/Rendering/Wb/WbFrustum.cs
index 7e22c5cb..96a254e0 100644
--- a/src/AcDream.App/Rendering/Wb/WbFrustum.cs
+++ b/src/AcDream.App/Rendering/Wb/WbFrustum.cs
@@ -1,4 +1,4 @@
-// Ported from references/WorldBuilder/Chorizite.OpenGLSDLBackend/Frustum.cs
+// Ported from references/WorldBuilder/Chorizite.OpenGLSDLBackend/Frustum.cs
// Phase A8 extraction (2026-05-28). Verbatim algorithm; adaptations:
// - Namespace: AcDream.App.Rendering.Wb
// - Class renamed Frustum -> WbFrustum
@@ -10,7 +10,7 @@ using System.Numerics;
namespace AcDream.App.Rendering.Wb;
-internal struct WbBoundingBox
+public struct WbBoundingBox
{
public Vector3 Min;
public Vector3 Max;
@@ -27,7 +27,7 @@ internal struct WbBoundingBox
Vector3.Max(a.Max, b.Max));
}
-internal enum FrustumTestResult
+public enum FrustumTestResult
{
Outside,
Inside,
@@ -39,7 +39,7 @@ internal enum FrustumTestResult
/// Source: references/WorldBuilder/Chorizite.OpenGLSDLBackend/Frustum.cs
/// Phase A8 extraction (2026-05-28).
///
-internal sealed class WbFrustum
+public sealed class WbFrustum
{
private struct Plane
{
diff --git a/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs b/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs
index 7f59775b..1bc0fd88 100644
--- a/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs
+++ b/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using AcDream.Content;
using AcDream.App.Rendering.Residency;
@@ -17,11 +17,11 @@ namespace AcDream.App.Rendering.Wb;
///
///
/// As of Phase O-T7, all DAT I/O routes through the runtime-owned shared
-/// facade — the separate
+/// facade — the separate
/// DefaultDatReaderWriter file-handle set has been removed.
///
///
-internal sealed class WbMeshAdapter
+public sealed class WbMeshAdapter
: IDisposable,
IWbMeshAdapter
{
@@ -291,12 +291,12 @@ internal sealed class WbMeshAdapter
private WbMeshAdapter()
{
- // Uninitialized constructor — only for tests / flag-off cases where
+ // Uninitialized constructor — only for tests / flag-off cases where
// the caller wants a Dispose-safe no-op instance.
_isUninitialized = true;
}
- /// Test/init helper — produces a Dispose-safe instance with no
+ /// Test/init helper — produces a Dispose-safe instance with no
/// underlying mesh manager. Public methods are all no-ops.
public static WbMeshAdapter CreateUninitialized() => new();
@@ -310,7 +310,7 @@ internal sealed class WbMeshAdapter
///
/// Returns the WB render data for , or null if not
/// yet uploaded or if this adapter is uninitialized. Increments WB's
- /// internal usage counter — use for
+ /// internal usage counter — use for
/// render-loop lookups that should not affect lifecycle.
///
public ObjectRenderData? GetRenderData(ulong id)
@@ -353,15 +353,15 @@ internal sealed class WbMeshAdapter
// auto-enqueues into _stagedMeshData (ObjectMeshManager line 510),
// which Tick() drains onto the GPU. Until that completes,
// TryGetRenderData(id) returns null and the dispatcher silently
- // skips the entity — standard streaming flicker.
+ // skips the entity — standard streaming flicker.
//
// #128 (2026-06-11): Prepare must RE-ARM whenever the id has no render
- // data — NOT only on the first-ever registration. A first-only gate
+ // data — NOT only on the first-ever registration. A first-only gate
// permanently lost any id whose initial read was cancelled before completing
- // (landblock unload → CancelStagedUploads during login/teleport
+ // (landblock unload → CancelStagedUploads during login/teleport
// churn) or whose upload was later LRU-evicted: every subsequent
// registration skipped Prepare, so the mesh stayed invisible for the
- // session with zero log output — the dispatcher's slow path just
+ // session with zero log output — the dispatcher's slow path just
// counted meshMissing forever (issue #55's 1.45M/5s mountain was this
// bug's heartbeat). User-visible: the AAB3 tower staircase rendering
// partially or not at all depending on the session's landblock
@@ -370,7 +370,7 @@ internal sealed class WbMeshAdapter
// on existing render data, returns the in-flight task when already
// pending, and dedups via _preparationTasks.
//
- // isSetup: false — acdream's MeshRefs already carry expanded
+ // isSetup: false — acdream's MeshRefs already carry expanded
// per-part GfxObj ids (0x01XXXXXX). WB's Setup-expansion path is
// unused.
if (_meshManager.TryGetRenderData(id) is null)
@@ -419,15 +419,15 @@ internal sealed class WbMeshAdapter
///
/// #128 self-heal (2026-06-11): re-request a mesh load at the POINT OF
- /// USE. Registration-time re-arming was insufficient — a preparation
+ /// USE. Registration-time re-arming was insufficient — a preparation
/// cancelled by landblock churn AFTER the last registration event
/// (running across blocks loads/unloads them repeatedly) left the mesh
/// permanently unloadable with no later event to re-fire it. The draw
/// dispatcher touches every missing-but-referenced mesh every frame (the
- /// meshMissing slow path) — that is the one place a retry can never be
+ /// meshMissing slow path) — that is the one place a retry can never be
/// missed. Cheap and idempotent: PrepareMeshDataAsync early-outs on
/// existing render data and returns the in-flight task when pending.
- /// Retail-equivalence: retail loads content synchronously — geometry is
+ /// Retail-equivalence: retail loads content synchronously — geometry is
/// never permanently absent; this converges our async pipeline to the
/// same guarantee.
///
@@ -466,7 +466,7 @@ internal sealed class WbMeshAdapter
_graphicsDevice!.ProcessGLQueue();
// #125: drain staged uploads; a FAILED upload (UploadMeshData returned
// null from its catch) is re-staged for a LATER frame, not dropped. The
- // re-stages are collected and re-enqueued AFTER the loop — re-enqueuing
+ // re-stages are collected and re-enqueued AFTER the loop — re-enqueuing
// inside the while would let a deterministic failure spin the queue in a
// single frame. UploadOrRequeue bounds the retries (MaxUploadRetries) so
// a genuine defect surfaces loudly instead of the old silent sticky drop.
@@ -594,14 +594,14 @@ internal sealed class WbMeshAdapter
: default;
// #105 root cause (2026-06-10): TextureAtlasManager.AddTexture only STAGES
- // immutable decoded payloads in ManagedGLTextureArray._pendingUpdates — the
+ // immutable decoded payloads in ManagedGLTextureArray._pendingUpdates — the
// actual TexSubImage3D copies + mipmap regeneration happen in
// ProcessDirtyUpdates, which WB drives ONCE PER FRAME from its render loop
// (WB GameScene.cs:975 `_meshManager?.GenerateMipmaps()`, just before the
// opaque pass). That call site lived in the GameScene file the N.4/O-T4
// extraction replaced with GameWindow, so the driver was silently dropped:
// staged updates never reached TexSubImage3D, leaving undefined
- // TexStorage3D content behind valid resident bindless handles — the
+ // TexStorage3D content behind valid resident bindless handles — the
// intermittent white indoor walls (#105). Pre-fix evidence: 126 updates
// stuck across 34/34 arrays at standstill (texflush-prefix.log). Tick()
// runs before all draw passes (GameWindow OnRender), so this is the exact
@@ -662,7 +662,7 @@ internal sealed class WbMeshAdapter
meshManager.RejectUnsupportedStagedUpload(rejected, error);
}
- // #105 apparatus state — see RenderingDiagnostics.ProbeTexFlushEnabled.
+ // #105 apparatus state — see RenderingDiagnostics.ProbeTexFlushEnabled.
private int _lastTexFlushBefore = -1;
private int _texFlushHeartbeat;
diff --git a/src/AcDream.App/Rendering/Wb/WbRenderPass.cs b/src/AcDream.App/Rendering/Wb/WbRenderPass.cs
index 753bffff..746050f3 100644
--- a/src/AcDream.App/Rendering/Wb/WbRenderPass.cs
+++ b/src/AcDream.App/Rendering/Wb/WbRenderPass.cs
@@ -1,4 +1,4 @@
-namespace AcDream.App.Rendering.Wb;
+namespace AcDream.App.Rendering.Wb;
///
/// Phase A8 (2026-05-28): WB's RenderPass enum, extracted verbatim from
@@ -9,7 +9,7 @@
/// Consumed by and matches the
/// uRenderPass uniform in the modern mesh shaders.
///
-internal enum WbRenderPass
+public enum WbRenderPass
{
///
/// The opaque pass. Only non-transparent objects are rendered.
diff --git a/src/AcDream.App/Rendering/WorldSceneDiagnosticsController.cs b/src/AcDream.App/Rendering/WorldSceneDiagnosticsController.cs
index cc5eeb81..6a6d31c2 100644
--- a/src/AcDream.App/Rendering/WorldSceneDiagnosticsController.cs
+++ b/src/AcDream.App/Rendering/WorldSceneDiagnosticsController.cs
@@ -58,7 +58,6 @@ internal sealed class WorldSceneDiagnosticsController : IWorldSceneDiagnostics
private readonly IWorldScenePViewDiagnosticSource _pview;
private readonly IWorldSceneDebugStateSource _state;
private readonly DebugLineRenderer? _lines;
- private readonly ICurrentGpuFrameSource _currentFrame;
private readonly PhysicsEngine _physics;
private readonly ILocalPlayerModeSource _mode;
private readonly IRuntimeLocalPlayerControllerSource _player;
@@ -71,7 +70,6 @@ internal sealed class WorldSceneDiagnosticsController : IWorldSceneDiagnostics
IWorldScenePViewDiagnosticSource pview,
IWorldSceneDebugStateSource state,
DebugLineRenderer? lines,
- ICurrentGpuFrameSource currentFrame,
PhysicsEngine physics,
ILocalPlayerModeSource mode,
IRuntimeLocalPlayerControllerSource player,
@@ -82,7 +80,6 @@ internal sealed class WorldSceneDiagnosticsController : IWorldSceneDiagnostics
_pview = pview ?? throw new ArgumentNullException(nameof(pview));
_state = state ?? throw new ArgumentNullException(nameof(state));
_lines = lines;
- _currentFrame = currentFrame ?? throw new ArgumentNullException(nameof(currentFrame));
_physics = physics ?? throw new ArgumentNullException(nameof(physics));
_mode = mode ?? throw new ArgumentNullException(nameof(mode));
_player = player ?? throw new ArgumentNullException(nameof(player));
@@ -250,7 +247,7 @@ internal sealed class WorldSceneDiagnosticsController : IWorldSceneDiagnostics
LogNearbyCollisionObjects(position, drawn);
}
- _lines.Flush(camera.Camera.View, camera.Projection, _currentFrame.Current);
+ _lines.Flush(camera.Camera.View, camera.Projection);
}
private void LogNearbyCollisionObjects(Vector3 playerPosition, int drawn)
diff --git a/src/AcDream.App/RuntimeOptions.cs b/src/AcDream.App/RuntimeOptions.cs
index 4beeda4b..8324c504 100644
--- a/src/AcDream.App/RuntimeOptions.cs
+++ b/src/AcDream.App/RuntimeOptions.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Globalization;
using System.IO;
using AcDream.App.Rendering.Residency;
@@ -15,19 +15,19 @@ namespace AcDream.App;
///
///
///
-/// Scope: startup-time only — values that don't change
+/// Scope: startup-time only — values that don't change
/// once the window is up. Runtime diagnostic toggles
/// (e.g. ACDREAM_DUMP_MOTION, ACDREAM_PROBE_*) belong in
/// diagnostic owner classes (see AcDream.Core.Physics.PhysicsDiagnostics
/// for the template), not here.
///
///
-/// See docs/architecture/code-structure.md §2 Rule 4 for the
-/// rule that drove this extraction, and §4 Step 1 for the broader
+/// See docs/architecture/code-structure.md §2 Rule 4 for the
+/// rule that drove this extraction, and §4 Step 1 for the broader
/// extraction sequence this is the first cut of.
///
///
-internal sealed record RuntimeOptions(
+public sealed record RuntimeOptions(
string DatDir,
string PreparedAssetPath,
bool LiveMode,
diff --git a/src/AcDream.App/Spells/MagicRuntime.cs b/src/AcDream.App/Spells/MagicRuntime.cs
index 14fd5163..100b8fd5 100644
--- a/src/AcDream.App/Spells/MagicRuntime.cs
+++ b/src/AcDream.App/Spells/MagicRuntime.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using AcDream.Core.Items;
using AcDream.Core.Spells;
@@ -8,7 +8,7 @@ using AcDream.Content;
namespace AcDream.App.Spells;
/// One resolved formula cell in retail's spell examination subview.
-internal readonly record struct SpellExamineComponent(
+public readonly record struct SpellExamineComponent(
uint SpellComponentId,
SpellComponentDescriptor Descriptor,
bool Owned);
@@ -18,7 +18,7 @@ internal readonly record struct SpellExamineComponent(
/// owner. The server owns turning, motion, fizzle, mana/component consumption,
/// effects, and results.
///
-internal sealed class MagicRuntime : IDisposable
+public sealed class MagicRuntime : IDisposable
{
private readonly SpellComponentRequirementService _requirements;
private IDisposable? _castOperationsBinding;
diff --git a/src/AcDream.App/Streaming/DungeonStreamingGate.cs b/src/AcDream.App/Streaming/DungeonStreamingGate.cs
index 048a0158..86aaeb5d 100644
--- a/src/AcDream.App/Streaming/DungeonStreamingGate.cs
+++ b/src/AcDream.App/Streaming/DungeonStreamingGate.cs
@@ -1,16 +1,16 @@
-namespace AcDream.App.Streaming;
+namespace AcDream.App.Streaming;
/// Result of the per-frame dungeon streaming-gate decision.
-/// Passed to — collapse
+/// Passed to — collapse
/// streaming to the single dungeon landblock.
/// When non-null, override the streaming observer to this
/// landblock key (the cell id's high 16 bits, 0xXXYY). Null leaves the caller's observer as-is.
-internal readonly record struct DungeonGateResult(bool InsideDungeon, uint? ObserverLandblockKey);
+public readonly record struct DungeonGateResult(bool InsideDungeon, uint? ObserverLandblockKey);
///
/// AP-36: the dungeon streaming gate (#133 FPS). When the player stands in a SEALED
/// EnvCell (an indoor cell that doesn't see outside), streaming collapses to the single
-/// dungeon landblock — AC dungeons have no adjacent landblocks, so the normal 25×25
+/// dungeon landblock — AC dungeons have no adjacent landblocks, so the normal 25×25
/// window would pull in ~129 unrelated ocean-grid dungeons. The trigger is the player's
/// CURRENT cell (CellGraph.CurrCell, set the moment the player is placed), and the
/// observer is pinned to that cell's OWN landblock (the cell id high 16 bits) because a
@@ -19,7 +19,7 @@ internal readonly record struct DungeonGateResult(bool InsideDungeon, uint? Obse
/// Extracted from GameWindow.OnUpdate as a pure function so the
/// teleport-hold rule (below) is unit-testable without the GL/dat/network stack.
///
-internal static class DungeonStreamingGate
+public static class DungeonStreamingGate
{
///
/// Decide the streaming gate from the player's current cell.
@@ -33,11 +33,11 @@ internal static class DungeonStreamingGate
bool isTeleportHold, bool currCellIsSealedDungeon, uint currCellId)
{
// #145/#138: during a teleport hold the player is NOT yet placed, so CurrCell is
- // the frozen SOURCE cell — where the player IS, not where they're going. Streaming
+ // the frozen SOURCE cell — where the player IS, not where they're going. Streaming
// must follow the DESTINATION, which the PortalSpace observer pin already does, so
// the source-cell gate is suppressed. Otherwise a teleport OUT of a dungeon keeps
- // streaming collapsed on the source dungeon (CurrCell still sealed) → the outdoor
- // destination never hydrates → the TAS transit holds 600 frames → force-snap to
+ // streaming collapsed on the source dungeon (CurrCell still sealed) → the outdoor
+ // destination never hydrates → the TAS transit holds 600 frames → force-snap to
// ocean. A teleport INTO a dungeon is handled explicitly upstream by
// StreamingController.PreCollapseToDungeon (and the controller's _collapsed latch
// holds it through the hold), so suppressing the gate here doesn't regress it.
diff --git a/src/AcDream.App/Streaming/GpuLandblockRetirement.cs b/src/AcDream.App/Streaming/GpuLandblockRetirement.cs
index 26c1f92c..0cfee190 100644
--- a/src/AcDream.App/Streaming/GpuLandblockRetirement.cs
+++ b/src/AcDream.App/Streaming/GpuLandblockRetirement.cs
@@ -1,4 +1,4 @@
-using AcDream.Core.World;
+using AcDream.Core.World;
namespace AcDream.App.Streaming;
@@ -8,7 +8,7 @@ namespace AcDream.App.Streaming;
/// asynchronously from the state transition, so a failed renderer cleanup
/// cannot keep the old landblock logically resident.
///
-internal sealed record GpuLandblockRetirement(
+public sealed record GpuLandblockRetirement(
uint LandblockId,
LandblockRetirementKind Kind,
IReadOnlyList Entities);
@@ -24,7 +24,7 @@ internal sealed record GpuWorldRecenterRetirement(
int SpatialOperationCount,
Exception? ObserverFailure);
-internal enum LandblockRetirementKind
+public enum LandblockRetirementKind
{
Full,
NearLayer,
diff --git a/src/AcDream.App/Streaming/GpuWorldState.cs b/src/AcDream.App/Streaming/GpuWorldState.cs
index bcbc857d..5b2a32cd 100644
--- a/src/AcDream.App/Streaming/GpuWorldState.cs
+++ b/src/AcDream.App/Streaming/GpuWorldState.cs
@@ -1,4 +1,4 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Numerics;
@@ -56,7 +56,7 @@ internal sealed record GpuLandblockSpatialPublication(
/// Threading: not thread-safe. All calls must happen on the render thread.
///
///
-internal sealed class GpuWorldState : ILiveEntitySpatialQuery
+public sealed class GpuWorldState : ILiveEntitySpatialQuery
{
private readonly LandblockSpawnAdapter? _wbSpawnAdapter;
private readonly EntityScriptActivator? _entityScriptActivator;
@@ -194,7 +194,7 @@ internal sealed class GpuWorldState : ILiveEntitySpatialQuery
&& IsLiveEntityProjectionResident(key);
///
- /// Try to grab the loaded record for a landblock — useful for callers
+ /// Try to grab the loaded record for a landblock — useful for callers
/// that need to enumerate entities before the landblock is dropped
/// (e.g. unregistering dynamic lights on a RemoveLandblock).
///
@@ -1053,7 +1053,7 @@ internal sealed class GpuWorldState : ILiveEntitySpatialQuery
}
///
- /// Mark a server-GUID as persistent — this entity survives landblock unloads
+ /// Mark a server-GUID as persistent — this entity survives landblock unloads
/// and gets re-parked as pending for its current canonical landblock.
///
public void MarkPersistent(uint serverGuid)
@@ -1093,7 +1093,7 @@ internal sealed class GpuWorldState : ILiveEntitySpatialQuery
uint canonical = (newCanonicalLb & 0xFFFF0000u) | 0xFFFFu;
- // Fast path: already drawn in the correct loaded bucket → nothing to do
+ // Fast path: already drawn in the correct loaded bucket → nothing to do
// (avoids per-frame list churn for a settled, stationary entity).
bool hasCurrent = _projectionLocations.TryGetValue(
entity,
@@ -1110,17 +1110,17 @@ internal sealed class GpuWorldState : ILiveEntitySpatialQuery
return;
}
- // Remove the entity from wherever it currently lives — a loaded bucket
- // OR a pending bucket — then re-append to its current landblock.
+ // Remove the entity from wherever it currently lives — a loaded bucket
+ // OR a pending bucket — then re-append to its current landblock.
//
// The projection-location index is the 2026-07-03 fix for the cold-spawn /
// run-out "invisible player" bug: a persistent (server-spawned) entity
// that spawned into a not-yet-loaded landblock sits in _pendingByLandblock,
- // and the old code scanned ONLY _loaded — so it silently no-op'd and left
+ // and the old code scanned ONLY _loaded — so it silently no-op'd and left
// the player stranded, hidden, even after its landblock finished loading
// (the AddLandblock pending-drain had already run empty before the churn
// re-parked the player, and the player is excluded from the server-object
- // re-hydrate — so RebucketLiveEntity was the ONLY path that could recover it,
+ // re-hydrate — so RebucketLiveEntity was the ONLY path that could recover it,
// and it couldn't reach a pending entity). The index now reaches either
// residency class in O(1). Re-appending routes the entity
// to _loaded (drawn) when its landblock is loaded, or back to pending to
@@ -1274,8 +1274,8 @@ internal sealed class GpuWorldState : ILiveEntitySpatialQuery
// landblock often hasn't streamed in yet, so the player lands in
// _pendingByLandblock. If that same landblock is then unloaded (a
// streaming churn / re-teleport before it finishes loading), the
- // pending entry was silently dropped here — violating the
- // "persistent ⇒ survives unload" contract and making the avatar
+ // pending entry was silently dropped here — violating the
+ // "persistent ⇒ survives unload" contract and making the avatar
// vanish after a couple round-trips. Rescue them so DrainRescued
// re-parks them at the next valid landblock.
if (_pendingByLandblock.TryGetValue(canonical, out var pendingForLb))
@@ -1580,7 +1580,7 @@ internal sealed class GpuWorldState : ILiveEntitySpatialQuery
/// world withdrawal, or logical teardown. It owns no renderer/script
/// lifecycle and is safe for a still-live incarnation.
///
- /// Safe to call with a server guid that's not currently present — no-op.
+ /// Safe to call with a server guid that's not currently present — no-op.
///
public void RemoveLiveEntityProjection(uint serverGuid)
{
@@ -1707,7 +1707,7 @@ internal sealed class GpuWorldState : ILiveEntitySpatialQuery
if (_loaded.ContainsKey(canonicalLandblockId))
{
- // Hot path — append directly to the render-thread-owned mutable
+ // Hot path — append directly to the render-thread-owned mutable
// resident bucket and flat view.
AddLoadedProjection(canonicalLandblockId, entity, key);
if (probePersistent)
@@ -1715,7 +1715,7 @@ internal sealed class GpuWorldState : ILiveEntitySpatialQuery
return;
}
- // Cold path — landblock not yet loaded. Park the entity in the
+ // Cold path — landblock not yet loaded. Park the entity in the
// pending bucket; AddLandblock will pick it up when the streamer
// delivers the matching landblock.
if (!_pendingByLandblock.TryGetValue(canonicalLandblockId, out var bucket))
@@ -1737,8 +1737,8 @@ internal sealed class GpuWorldState : ILiveEntitySpatialQuery
///
/// Drop all entities from a landblock without removing the terrain. Used
- /// by two-tier streaming when a landblock crosses Near→Far hysteresis.
- /// Per Phase A.5 spec §4.4.
+ /// by two-tier streaming when a landblock crosses Near→Far hysteresis.
+ /// Per Phase A.5 spec §4.4.
///
///
/// Only dat-static entity layers demote. Live server projections retain
@@ -1798,7 +1798,7 @@ internal sealed class GpuWorldState : ILiveEntitySpatialQuery
// for this landblock BEFORE we drop the entity list. The cache stores
// canonical landblock ids (the dispatcher's _walkScratch carries
// entry.LandblockId from GpuWorldState.LandblockEntries, whose keys are
- // canonicalized). Null when the cache isn't wired (tests). Per spec §5.3 W3b.
+ // canonicalized). Null when the cache isn't wired (tests). Per spec §5.3 W3b.
// C.1.5b: stop DefaultScript for each dat-hydrated entity about to
// be dropped. Demote-tier entities are always atlas-tier (ServerGuid==0
@@ -1852,14 +1852,14 @@ internal sealed class GpuWorldState : ILiveEntitySpatialQuery
///
/// Merge entities into an existing-loaded landblock. Used by two-tier
- /// streaming for the Far→Near promotion case (terrain already loaded;
+ /// streaming for the Far→Near promotion case (terrain already loaded;
/// entity layer streaming in). Falls back to the pending bucket if the
/// landblock isn't loaded yet (handles the rare "promote arrives before
/// far load completes" race).
- /// Per Phase A.5 spec §4.4.
+ /// Per Phase A.5 spec §4.4.
///
///
- /// Landblock id is canonicalized (low 16 bits forced to 0xFFFF) —
+ /// Landblock id is canonicalized (low 16 bits forced to 0xFFFF) —
/// callers may pass cell-resolved ids and they will key correctly.
///
///
@@ -1913,7 +1913,7 @@ internal sealed class GpuWorldState : ILiveEntitySpatialQuery
if (!parkIfMissing)
return null;
- // Park as pending — same pattern as live projections for not-yet-loaded LBs.
+ // Park as pending — same pattern as live projections for not-yet-loaded LBs.
if (!_pendingByLandblock.TryGetValue(canonical, out var bucket))
{
bucket = new List();
@@ -2023,7 +2023,7 @@ internal sealed class GpuWorldState : ILiveEntitySpatialQuery
private readonly HashSet _persistentInFlatProbe = new();
// TEMP (#138-B): log when a persistent (player) entity enters/leaves the
- // drawn flat view. Transition-gated → low volume (fires at teleport
+ // drawn flat view. Transition-gated → low volume (fires at teleport
// boundaries, not every rebuild). Strip with EntityVanishProbe.
private void ProbeFlatViewTransitions()
{
diff --git a/src/AcDream.App/Streaming/LandblockBuild.cs b/src/AcDream.App/Streaming/LandblockBuild.cs
index 7f66c554..4a04929d 100644
--- a/src/AcDream.App/Streaming/LandblockBuild.cs
+++ b/src/AcDream.App/Streaming/LandblockBuild.cs
@@ -1,4 +1,4 @@
-using AcDream.App.Rendering.Wb;
+using AcDream.App.Rendering.Wb;
using AcDream.Core.World;
namespace AcDream.App.Streaming;
@@ -8,7 +8,7 @@ namespace AcDream.App.Streaming;
/// until it posts the corresponding completion; the render thread then applies
/// the landblock and its cell transaction together.
///
-internal sealed record LandblockBuild(
+public sealed record LandblockBuild(
LoadedLandblock Landblock,
EnvCellLandblockBuild? EnvCells = null,
LandblockBuildOrigin Origin = default,
diff --git a/src/AcDream.App/Streaming/LandblockBuildFactory.cs b/src/AcDream.App/Streaming/LandblockBuildFactory.cs
index 25a13fc1..5e1e04cc 100644
--- a/src/AcDream.App/Streaming/LandblockBuildFactory.cs
+++ b/src/AcDream.App/Streaming/LandblockBuildFactory.cs
@@ -1,4 +1,4 @@
-using DatReaderWriter;
+using DatReaderWriter;
using AcDream.Content;
namespace AcDream.App.Streaming;
@@ -14,7 +14,7 @@ namespace AcDream.App.Streaming;
/// CObjCell::init_objects (0x0052B420).
/// See docs/architecture/worldbuilder-inventory.md.
///
-internal sealed class LandblockBuildFactory
+public sealed class LandblockBuildFactory
{
private readonly IDatReaderWriter _dats;
private readonly IPreparedCollisionSource _preparedCollisions;
@@ -51,7 +51,7 @@ internal sealed class LandblockBuildFactory
///
/// ISSUE #54 (post-A.5): far-tier loads (kind == LoadFar) skip
/// LandBlockInfo + scenery + interior hydration. They return only the
- /// LandBlock heightmap dat record + an empty entity list — enough for
+ /// LandBlock heightmap dat record + an empty entity list — enough for
/// terrain-mesh build on the next phase. Near-tier loads run the full
/// path. This replaces Bug A's post-load entity strip in
/// with an
@@ -70,7 +70,7 @@ internal sealed class LandblockBuildFactory
// gate through mesh/bounds hydration prevents another consumer from
// interleaving reader cursor/cache state with this build.
// tp-probe (2026-06-22, REMOVABLE): measure lock-WAIT (the _datLock
- // contention signal — large only when the render thread is hammering the
+ // contention signal — large only when the render thread is hammering the
// lock during a CreateObject flood) AND lock-HOLD (the intrinsic build
// cost). Identical work in both branches; the probe branch only adds the
// stopwatch + log. No behavior change when ProbeTeleportEnabled is false.
@@ -123,7 +123,7 @@ internal sealed class LandblockBuildFactory
{
uint landblockId = request.LandblockId;
- // ISSUE #54: far-tier early-out — heightmap only, empty entities.
+ // ISSUE #54: far-tier early-out — heightmap only, empty entities.
// Skips the LandBlockInfo dat read AND all entity hydration (stabs
// + buildings) AND the SceneryGenerator AND interior cells. Cuts
// worker-thread cost per far-tier LB from ~tens of ms to a single
@@ -203,7 +203,7 @@ internal sealed class LandblockBuildFactory
}
///
/// Phase A.1 Task 8: generate scenery (trees, rocks, bushes) for a single
- /// landblock on the worker thread. Pure CPU — no GL calls.
+ /// landblock on the worker thread. Pure CPU — no GL calls.
///
/// Ported from the pre-streaming preload loop in GameWindow.OnLoad
/// (pre-Task-7 version, lines 329-405). Adapted to operate on a single
@@ -305,7 +305,7 @@ internal sealed class LandblockBuildFactory
float localX = spawn.LocalPosition.X;
float localY = spawn.LocalPosition.Y;
// Prefer the physics engine's terrain sampler (TerrainSurface.SampleZ)
- // — it uses the same AC2D render split-direction formula the
+ // — it uses the same AC2D render split-direction formula the
// TerrainModernRenderer uses for the visible terrain mesh. This
// guarantees trees are placed on the SAME Z height the player
// walks on. If physics hasn't registered this landblock yet,
@@ -313,14 +313,14 @@ internal sealed class LandblockBuildFactory
var worldPx = localX + lbOffset.X;
var worldPy = localY + lbOffset.Y;
// FIX (trees-in-sky, 2026-06-22): scenery ground-Z comes from THIS
- // landblock's OWN heightmap — the same triangle-aware Z the player walks on
+ // landblock's OWN heightmap — the same triangle-aware Z the player walks on
// (TerrainSurface.SampleZFromHeightmap, lock-step with physics per #48),
// scoped to the landblock being built. The former global
// _physicsEngine.SampleTerrainZ(worldPx) query was structurally racy: at
// build time this landblock is NOT registered in physics yet, so that query
- // could only return null (→ this same own-heightmap) or a STALE neighbor's
- // height — the previous location's terrain before the full old-window
- // recenter retirement converges — planting scenery at the old location's
+ // could only return null (→ this same own-heightmap) or a STALE neighbor's
+ // height — the previous location's terrain before the full old-window
+ // recenter retirement converges — planting scenery at the old location's
// altitude (trees-in-sky, deltaZ up to +500m; confirmed via the
// [scenery-z-stale] probe 2026-06-22). Own-heightmap is correct in every
// case, so the global query is removed (also drops its per-spawn cost).
@@ -335,7 +335,7 @@ internal sealed class LandblockBuildFactory
if (_dumpSceneryZ)
{
// groundZ now always comes from THIS landblock's own heightmap (the
- // global physics query was removed — see the trees-in-sky fix above).
+ // global physics query was removed — see the trees-in-sky fix above).
string source = "heightmap";
foreach (var mr in meshRefs)
{
@@ -422,7 +422,7 @@ internal sealed class LandblockBuildFactory
///
/// Phase A.1 Task 8: walk a landblock's EnvCells and produce (a) the cell
/// room-mesh entity (Phase 7.1) for each EnvCell with an EnvironmentId, and
- /// (b) a WorldEntity per StaticObject in each cell. Pure CPU — no GL calls.
+ /// (b) a WorldEntity per StaticObject in each cell. Pure CPU — no GL calls.
///
/// Portal cells and drawable shell placements are accumulated in the
/// transaction-local . The render thread
@@ -448,28 +448,28 @@ internal sealed class LandblockBuildFactory
(lbY - origin.CenterY) * 192f,
0f);
- // Per-landblock id namespace — see AcDream.Core.World.InteriorEntityIdAllocator
+ // Per-landblock id namespace — see AcDream.Core.World.InteriorEntityIdAllocator
// for the full bit layout + history. Distinct from scenery (0x80000000+) and
// landblock stabs (0xC0000000+, ids from LandblockLoader).
//
// #119 ROOT-CAUSE FIX (2026-06-11): this used to be
// `0x40000000 | (landblockId & 0x00FFFF00)`, which for landblock keys 0xXXYYFFFF
- // resolves to 0x40YYFF00 — the landblock X byte DISCARDED. Every landblock in a
+ // resolves to 0x40YYFF00 — the landblock X byte DISCARDED. Every landblock in a
// map Y-row produced the same id base, so interior statics collided across
// landblocks (Holtburg town A9B3's 9th stab == the AAB3 tower's 43-part spiral
// staircase, both 0x40B3FF09). The Tier-1 classification cache then served one
// entity's batches to the other (the cache hint at bucket-draw time was the
- // player's landblock, identical for both) — the session-sticky "broken stairs +
+ // player's landblock, identical for both) — the session-sticky "broken stairs +
// water barrel".
//
- // #190 (2026-07-09): the fix above LEFT a documented residual — "counter overflow
+ // #190 (2026-07-09): the fix above LEFT a documented residual — "counter overflow
// past 0xFF still bleeds into the lbY byte." That residual manifested for real:
// the Town Network hub (205 cells, one landblock) reached 277 interior entities
// after the #79/#93 A7.L1 light-carrier hydration fix, aliasing into the NEXT
// landblock's Y-byte (entity script/particle tracking is keyed on entity.Id
- // directly — EntityScriptActivator — with no landblock-hint disambiguation, so
+ // directly — EntityScriptActivator — with no landblock-hint disambiguation, so
// the fountain's water-spray script silently stopped firing). Widened the
- // counter budget 8→12 bits (256→4096); see InteriorEntityIdAllocator's doc for
+ // counter budget 8→12 bits (256→4096); see InteriorEntityIdAllocator's doc for
// why this is safe (nothing decodes X/Y back out of an entity id).
uint interiorLbX = (landblockId >> 24) & 0xFFu;
uint interiorLbY = (landblockId >> 16) & 0xFFu;
@@ -484,7 +484,7 @@ internal sealed class LandblockBuildFactory
{
// TEMP diagnostic (dat-race investigation 2026-06-09, strip with fix):
// every id in [0x0100, 0x0100+NumCells) is derived from LandBlockInfo and
- // MUST exist in the cell dat — a null here is always a read anomaly.
+ // MUST exist in the cell dat — a null here is always a read anomaly.
Console.WriteLine($"[cell-miss] EnvCell 0x{envCellId:X8} null during interior hydration (NumCells={lbInfo.NumCells})");
continue;
}
@@ -498,7 +498,7 @@ internal sealed class LandblockBuildFactory
{
// TEMP diagnostic (dat-race investigation 2026-06-09, strip with fix):
// a null Environment means this cell's WALLS are silently never
- // registered while its static objects still draw — the exact
+ // registered while its static objects still draw — the exact
// white-walls geometry signature.
Console.WriteLine($"[cell-miss] Environment 0x{0x0D000000u | envCell.EnvironmentId:X8} null for EnvCell 0x{envCellId:X8} -> walls not registered");
}
@@ -510,14 +510,14 @@ internal sealed class LandblockBuildFactory
// drawable-geometry predicate; the actual shell placement is now owned
// by this streaming job's EnvCellLandblockBuild transaction.
// Static objects inside the cell continue to flow through the dispatcher
- // as WorldEntity records below — they have real GfxObj MeshRefs that work
+ // as WorldEntity records below — they have real GfxObj MeshRefs that work
// fine; EnvCellRenderer receives only the completed shell transaction.
- // Transforms — needed by the portal-visibility cell (unlifted) AND the
+ // Transforms — needed by the portal-visibility cell (unlifted) AND the
// render/physics path. Computed for EVERY cell with a valid cellStruct,
// not just drawable ones. Keep the small render lift out of physics; retail
// BSP contact planes use the EnvCell origin verbatim. The lift constant is
// shared with every draw-space consumer of portal polygons (OutsideView
- // gate, seal/punch fans) — PortalVisibilityBuilder.ShellDrawLiftZ (#130).
+ // gate, seal/punch fans) — PortalVisibilityBuilder.ShellDrawLiftZ (#130).
var physicsCellOrigin = envCell.Position.Origin + lbOffset;
var cellOrigin = physicsCellOrigin + new System.Numerics.Vector3(
0f, 0f, AcDream.App.Rendering.PortalVisibilityBuilder.ShellDrawLiftZ);
@@ -532,9 +532,9 @@ internal sealed class LandblockBuildFactory
// of whether CellMesh.Build produced drawable sub-meshes. A portals-only
// pass-through connector (a ramp / stair / cellar mouth) yields 0 render
// sub-meshes but MUST be in the visibility graph so the flood can traverse it
- // to the cells beyond — otherwise the flood lookup-misses the unregistered
+ // to the cells beyond — otherwise the flood lookup-misses the unregistered
// neighbour and the grey clear shows through the opening (#133: ramp
- // neighbour 0x0007014D had 0 sub-meshes → unregistered → vis=1 grey barrier
+ // neighbour 0x0007014D had 0 sub-meshes → unregistered → vis=1 grey barrier
// at the ramp; confirmed via [cellreg] registered=204/205 + [pv-trace]
// skip=lookup-miss). Retail keeps the whole landblock cell array resident
// before the flood runs; the cell-build transaction reads portals, NOT
@@ -558,7 +558,7 @@ internal sealed class LandblockBuildFactory
foreach (var stab in envCell.StaticObjects)
{
// #119 decisive probe: HYDRATE-side dump for ACDREAM_DUMP_ENTITY-
- // targeted stabs. This is the MOMENT MeshRefs are constructed —
+ // targeted stabs. This is the MOMENT MeshRefs are constructed —
// a degraded dat read here (setup null / placement frames short /
// part GfxObj null) permanently corrupts the entity (H-A), and
// nothing downstream ever rebuilds it. Inert when the set is empty.
@@ -568,7 +568,7 @@ internal sealed class LandblockBuildFactory
// #136: skip an EDITOR-ONLY placement marker. Such a dat object degrades to
// nothing (GfxObj id 0) at any runtime distance, so retail's distance-based
- // degrade (CPhysicsPart::UpdateViewerDistance) never draws it — only the
+ // degrade (CPhysicsPart::UpdateViewerDistance) never draws it — only the
// WorldBuilder editor shows it at the origin. acdream's render path came from
// WB (no distance LOD), so without this skip it draws the marker forever (the
// red/green dungeon "cone"). Bare-GfxObj stabs are checked here; Setup stabs
@@ -581,7 +581,7 @@ internal sealed class LandblockBuildFactory
var interiorBounds = new AcDream.Core.Meshing.LocalBoundsAccumulator();
// #79/#93 (2026-07-09): a Setup-sourced stab whose sole visual part is a
// runtime-hidden marker (#136) flattens to zero mesh refs even though its
- // Setup carries real Lights — a "light attach point" fixture (e.g. the Town
+ // Setup carries real Lights — a "light attach point" fixture (e.g. the Town
// Network fountain room's ceiling light, Setup 0x02000365). Track the dat
// Setup's Lights.Count here so the meshRefs==0 gate below doesn't also drop
// the entity that otherwise carries those lights to the static
@@ -618,7 +618,7 @@ internal sealed class LandblockBuildFactory
{
// #136: skip an editor-only marker PART (retail hides it at runtime
// distance). The #136 dungeon "cone" is Setup 0x02000C39 whose sole
- // part GfxObj 0x010028CA is such a marker — skipping it empties
+ // part GfxObj 0x010028CA is such a marker — skipping it empties
// meshRefs and the whole stab drops below.
if (AcDream.Core.Meshing.GfxObjDegradeResolver.IsRuntimeHiddenMarker(_dats, mr.GfxObjId))
continue;
@@ -652,7 +652,7 @@ internal sealed class LandblockBuildFactory
// Stabs inside EnvCells are already in landblock-local coordinates
// (same space as LandBlockInfo.Objects stabs). Adding cellOrigin would
- // be wrong — see Phase 2d comment in the pre-streaming preload.
+ // be wrong — see Phase 2d comment in the pre-streaming preload.
var worldPos = stab.Frame.Origin + lbOffset;
var worldRot = stab.Frame.Orientation;
diff --git a/src/AcDream.App/Streaming/LandblockBuildRequest.cs b/src/AcDream.App/Streaming/LandblockBuildRequest.cs
index 16d14e69..315753f4 100644
--- a/src/AcDream.App/Streaming/LandblockBuildRequest.cs
+++ b/src/AcDream.App/Streaming/LandblockBuildRequest.cs
@@ -1,11 +1,11 @@
-namespace AcDream.App.Streaming;
+namespace AcDream.App.Streaming;
///
/// Immutable world-origin center captured by the update thread when a
/// landblock load is admitted. Worker construction and render publication use
/// this same value, so a later recenter cannot combine two coordinate frames.
///
-internal readonly record struct LandblockBuildOrigin
+public readonly record struct LandblockBuildOrigin
{
public LandblockBuildOrigin(int centerX, int centerY)
{
@@ -29,7 +29,7 @@ internal readonly record struct LandblockBuildOrigin
/// is carried for matching and diagnostics only;
/// residency policy remains owned by .
///
-internal readonly record struct LandblockBuildRequest(
+public readonly record struct LandblockBuildRequest(
uint LandblockId,
LandblockStreamJobKind Kind,
ulong Generation,
diff --git a/src/AcDream.App/Streaming/LandblockPhysicsPublisher.cs b/src/AcDream.App/Streaming/LandblockPhysicsPublisher.cs
index c4163f52..169d912c 100644
--- a/src/AcDream.App/Streaming/LandblockPhysicsPublisher.cs
+++ b/src/AcDream.App/Streaming/LandblockPhysicsPublisher.cs
@@ -1,4 +1,4 @@
-using System.Diagnostics;
+using System.Diagnostics;
using System.Numerics;
using AcDream.Core.Physics;
using AcDream.Core.World;
@@ -12,7 +12,7 @@ namespace AcDream.App.Streaming;
/// publication. The render publisher commits buildings and EnvCells between
/// these stages without recomputing the captured origin.
///
-internal sealed class LandblockPhysicsPublication
+public sealed class LandblockPhysicsPublication
{
internal LandblockPhysicsPublication(
object owner,
@@ -72,7 +72,7 @@ internal sealed class LandblockPhysicsPublication
/// Cumulative update-thread diagnostics for physics publication and removal.
/// Durations are ticks.
///
-internal readonly record struct LandblockPhysicsPublisherDiagnostics(
+public readonly record struct LandblockPhysicsPublisherDiagnostics(
long BeginCount,
long CompleteCount,
long BasePublishTicks,
@@ -103,7 +103,7 @@ internal readonly record struct LandblockPhysicsPublisherDiagnostics(
/// use one multipart shadow owner or the mutually exclusive Setup
/// cylinder/sphere fallback.
///
-internal sealed class LandblockPhysicsPublisher
+public sealed class LandblockPhysicsPublisher
{
private readonly object _receiptOwner = new();
private readonly RuntimePhysicsState _physics;
@@ -953,7 +953,7 @@ internal sealed class LandblockPhysicsPublisher
string sampleText = string.Join(",", samples.Select(
value => $"0x{value:X8}"));
Console.WriteLine(
- $" → {missingCount} scenery entities had no visual bounds cached. " +
+ $" → {missingCount} scenery entities had no visual bounds cached. " +
$"Samples: {sampleText}");
}
}
diff --git a/src/AcDream.App/Streaming/LandblockPresentationPipeline.cs b/src/AcDream.App/Streaming/LandblockPresentationPipeline.cs
index cef987b0..f5ae4f9d 100644
--- a/src/AcDream.App/Streaming/LandblockPresentationPipeline.cs
+++ b/src/AcDream.App/Streaming/LandblockPresentationPipeline.cs
@@ -1,4 +1,4 @@
-using AcDream.App.Rendering.Wb;
+using AcDream.App.Rendering.Wb;
using AcDream.App.Rendering.Scene;
using AcDream.Core.Terrain;
using AcDream.Core.World;
@@ -10,7 +10,7 @@ namespace AcDream.App.Streaming;
/// Keeping this projection on the pipeline prevents the composition root from
/// retaining each concrete publication owner independently.
///
-internal readonly record struct LandblockPresentationDiagnostics(
+public readonly record struct LandblockPresentationDiagnostics(
LandblockRenderPublisherDiagnostics Render,
LandblockPhysicsPublisherDiagnostics Physics,
LandblockStaticPresentationDiagnostics Statics);
@@ -37,7 +37,7 @@ internal readonly record struct LandblockPublicationAdvance(
/// graph; the internal delegate constructor exists only for hermetic policy and
/// retry characterization tests.
///
-internal sealed class LandblockPresentationPipeline
+public sealed class LandblockPresentationPipeline
{
private enum PublicationKind : byte
{
diff --git a/src/AcDream.App/Streaming/LandblockPresentationRetirementOwner.cs b/src/AcDream.App/Streaming/LandblockPresentationRetirementOwner.cs
index 7536fb1f..f9e0e937 100644
--- a/src/AcDream.App/Streaming/LandblockPresentationRetirementOwner.cs
+++ b/src/AcDream.App/Streaming/LandblockPresentationRetirementOwner.cs
@@ -1,4 +1,4 @@
-using AcDream.Core.Lighting;
+using AcDream.Core.Lighting;
using AcDream.Core.Rendering;
using AcDream.Core.World;
@@ -16,7 +16,7 @@ namespace AcDream.App.Streaming;
/// detaches spatial reachability first, then advances these concrete owners
/// from the retained ticket without replaying successful stages or entities.
///
-internal sealed class LandblockPresentationRetirementOwner
+public sealed class LandblockPresentationRetirementOwner
{
private readonly LandblockRenderPublisher _render;
private readonly LandblockPhysicsPublisher _physics;
diff --git a/src/AcDream.App/Streaming/LandblockRenderPublisher.cs b/src/AcDream.App/Streaming/LandblockRenderPublisher.cs
index aa54bb89..5578bc1a 100644
--- a/src/AcDream.App/Streaming/LandblockRenderPublisher.cs
+++ b/src/AcDream.App/Streaming/LandblockRenderPublisher.cs
@@ -1,4 +1,4 @@
-using System.Collections.ObjectModel;
+using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Numerics;
using AcDream.App.Rendering;
@@ -13,7 +13,7 @@ namespace AcDream.App.Streaming;
/// origin between and
/// .
///
-internal sealed class LandblockRenderPublication
+public sealed class LandblockRenderPublication
{
private readonly IReadOnlyDictionary _visibilityCells;
@@ -64,7 +64,7 @@ internal sealed class LandblockRenderPublication
/// Times use ticks so callers can aggregate without
/// losing sub-millisecond precision.
///
-internal readonly record struct LandblockRenderPublisherDiagnostics(
+public readonly record struct LandblockRenderPublisherDiagnostics(
long BeginCount,
long CompleteCount,
long TerrainPublishTicks,
@@ -90,7 +90,7 @@ internal readonly record struct LandblockRenderPublisherDiagnostics(
/// WorldBuilder one-job/one-EnvCell-transaction seam documented in
/// docs/architecture/worldbuilder-inventory.md.
///
-internal sealed class LandblockRenderPublisher
+public sealed class LandblockRenderPublisher
{
private readonly object _receiptOwner = new();
private readonly Action _publishTerrain;
diff --git a/src/AcDream.App/Streaming/LandblockRetirementCoordinator.cs b/src/AcDream.App/Streaming/LandblockRetirementCoordinator.cs
index 460f63cd..82046fa1 100644
--- a/src/AcDream.App/Streaming/LandblockRetirementCoordinator.cs
+++ b/src/AcDream.App/Streaming/LandblockRetirementCoordinator.cs
@@ -1,9 +1,9 @@
-using AcDream.Core.World;
+using AcDream.Core.World;
namespace AcDream.App.Streaming;
[Flags]
-internal enum LandblockRetirementStage : ushort
+public enum LandblockRetirementStage : ushort
{
None = 0,
MeshReferences = 1 << 0,
@@ -31,7 +31,7 @@ internal enum LandblockRetirementOperationResult : byte
/// One retryable landblock-retirement ledger. Successful owner operations are
/// committed once; a later attempt resumes at the exact failed owner/entity.
///
-internal sealed class LandblockRetirementTicket
+public sealed class LandblockRetirementTicket
{
private readonly Dictionary _entityCursors = new();
private readonly Dictionary _failures = new();
@@ -240,7 +240,7 @@ internal sealed class LandblockRetirementTicket
/// owner callback cannot recursively replay its active stage or mutate the
/// pending-ticket map while it is being enumerated.
///
-internal sealed class LandblockRetirementCoordinator
+public sealed class LandblockRetirementCoordinator
{
private enum BudgetedAdvanceResult : byte
{
diff --git a/src/AcDream.App/Streaming/LandblockStaticPresentationPublisher.cs b/src/AcDream.App/Streaming/LandblockStaticPresentationPublisher.cs
index 8ba7c031..ac9e0121 100644
--- a/src/AcDream.App/Streaming/LandblockStaticPresentationPublisher.cs
+++ b/src/AcDream.App/Streaming/LandblockStaticPresentationPublisher.cs
@@ -1,4 +1,4 @@
-using AcDream.Core.Lighting;
+using AcDream.Core.Lighting;
using AcDream.Core.Plugins;
using AcDream.Core.Rendering;
using AcDream.Core.World;
@@ -11,7 +11,7 @@ namespace AcDream.App.Streaming;
/// publication. The physics publisher calls the light/translucency stage at
/// the existing per-object point immediately before ordinary collision.
///
-internal sealed class LandblockStaticPresentationPublication
+public sealed class LandblockStaticPresentationPublication
{
private readonly Dictionary _entities;
private readonly Dictionary _snapshots;
@@ -63,7 +63,7 @@ internal sealed class LandblockStaticPresentationPublication
public IReadOnlyDictionary Snapshots => _snapshots;
}
-internal readonly record struct LandblockStaticPresentationDiagnostics(
+public readonly record struct LandblockStaticPresentationDiagnostics(
long BeginCount,
long CompleteCount,
long LightReplacementCount,
@@ -87,7 +87,7 @@ internal readonly record struct LandblockStaticPresentationDiagnostics(
/// logical spawn per retained static ID, while reapply only refreshes current
/// state.
///
-internal sealed class LandblockStaticPresentationPublisher
+public sealed class LandblockStaticPresentationPublisher
{
private readonly object _receiptOwner = new();
private readonly LightingHookSink _lighting;
diff --git a/src/AcDream.App/Streaming/LandblockStreamJob.cs b/src/AcDream.App/Streaming/LandblockStreamJob.cs
index d69bd421..17017cd2 100644
--- a/src/AcDream.App/Streaming/LandblockStreamJob.cs
+++ b/src/AcDream.App/Streaming/LandblockStreamJob.cs
@@ -1,4 +1,4 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
using AcDream.Core.Terrain;
using AcDream.Core.World;
@@ -12,7 +12,7 @@ namespace AcDream.App.Streaming;
///
public abstract record LandblockStreamJob(uint LandblockId)
{
- internal sealed record Load(
+ public sealed record Load(
uint LandblockId,
LandblockStreamJobKind Kind,
ulong Generation = 0,
@@ -21,7 +21,7 @@ public abstract record LandblockStreamJob(uint LandblockId)
public LandblockBuildRequest Request =>
new(LandblockId, Kind, Generation, Origin);
}
- internal sealed record Unload(
+ public sealed record Unload(
uint LandblockId,
ulong Generation = 0) : LandblockStreamJob(LandblockId);
@@ -30,10 +30,10 @@ public abstract record LandblockStreamJob(uint LandblockId)
/// priority queues, keeping Unloads. Posted by
/// when the player enters a
/// dungeon and the in-flight outdoor/neighbor window load must be cancelled
- /// (#133 FPS — dungeons have no adjacent landblocks). LandblockId is 0 by
+ /// (#133 FPS — dungeons have no adjacent landblocks). LandblockId is 0 by
/// convention; readers pattern-match on the type.
///
- internal sealed record ClearLoads() : LandblockStreamJob(0);
+ public sealed record ClearLoads() : LandblockStreamJob(0);
}
///
@@ -49,7 +49,7 @@ public abstract record LandblockStreamResult(uint LandblockId, ulong Generation)
/// (terrain only) from Near (terrain + entities).
/// is built off the render thread on the streaming worker.
///
- internal sealed record Loaded(
+ public sealed record Loaded(
uint LandblockId,
LandblockStreamTier Tier,
LandblockBuild Build,
@@ -78,7 +78,7 @@ public abstract record LandblockStreamResult(uint LandblockId, ulong Generation)
/// GpuWorldState still merges only the entity layer so live entities already
/// attached to the landblock are preserved.
///
- internal sealed record Promoted(
+ public sealed record Promoted(
uint LandblockId,
LandblockBuild Build,
LandblockMeshData MeshData,
@@ -98,20 +98,20 @@ public abstract record LandblockStreamResult(uint LandblockId, ulong Generation)
public IReadOnlyList Entities => Landblock.Entities;
}
- internal sealed record Failed(
+ public sealed record Failed(
uint LandblockId,
string Error,
ulong Generation = 0) : LandblockStreamResult(LandblockId, Generation);
- internal sealed record Unloaded(
+ public sealed record Unloaded(
uint LandblockId,
ulong Generation = 0) : LandblockStreamResult(LandblockId, Generation);
///
/// The worker loop itself crashed with an unhandled exception. Not tied
- /// to a specific landblock — distinguished from
+ /// to a specific landblock — distinguished from
/// because consumers typically route this to a fatal-log path rather
/// than retrying a single landblock later. LandblockId is 0 by
/// convention; readers should pattern-match on the type, not the id.
///
- internal sealed record WorkerCrashed(string Error) : LandblockStreamResult(0, 0);
+ public sealed record WorkerCrashed(string Error) : LandblockStreamResult(0, 0);
}
diff --git a/src/AcDream.App/Streaming/LandblockStreamResultCost.cs b/src/AcDream.App/Streaming/LandblockStreamResultCost.cs
index 4d6b9dc0..b0f01426 100644
--- a/src/AcDream.App/Streaming/LandblockStreamResultCost.cs
+++ b/src/AcDream.App/Streaming/LandblockStreamResultCost.cs
@@ -1,4 +1,4 @@
-using System.Runtime.CompilerServices;
+using System.Runtime.CompilerServices;
using AcDream.App.Rendering.Wb;
using AcDream.Core.Terrain;
using AcDream.Core.World;
@@ -12,7 +12,7 @@ namespace AcDream.App.Streaming;
/// the managed heap. Exact array payloads and logical retained entries are
/// charged consistently so admission has a deterministic unit.
///
-internal readonly record struct LandblockStreamCostEstimate(
+public readonly record struct LandblockStreamCostEstimate(
StreamingWorkCost Work,
long TerrainPayloadBytes,
int Entities,
@@ -26,7 +26,7 @@ internal readonly record struct LandblockStreamCostEstimate(
int PhysicsSetups,
int PhysicsGfxObjects);
-internal static class LandblockStreamResultCost
+public static class LandblockStreamResultCost
{
private const int ReferenceChargeBytes = 8;
private const int DictionaryEntryChargeBytes = 16;
diff --git a/src/AcDream.App/Streaming/LandblockStreamTier.cs b/src/AcDream.App/Streaming/LandblockStreamTier.cs
index 5daf4393..c4a9e5d7 100644
--- a/src/AcDream.App/Streaming/LandblockStreamTier.cs
+++ b/src/AcDream.App/Streaming/LandblockStreamTier.cs
@@ -1,11 +1,11 @@
-namespace AcDream.App.Streaming;
+namespace AcDream.App.Streaming;
///
/// Streaming-tier classification for a landblock. means
/// terrain mesh only; means terrain + scenery + EnvCells +
-/// entity registration with the WB dispatcher. Per Phase A.5 spec §3.
+/// entity registration with the WB dispatcher. Per Phase A.5 spec §3.
///
-internal enum LandblockStreamTier
+public enum LandblockStreamTier
{
Far,
Near,
@@ -15,7 +15,7 @@ internal enum LandblockStreamTier
/// What work the streaming worker should perform for a given job. Distinct
/// from because
/// reads only the entity layer (terrain mesh already loaded), while
-/// reads everything from scratch. Per Phase A.5 spec §4.3.
+/// reads everything from scratch. Per Phase A.5 spec §4.3.
///
public enum LandblockStreamJobKind
{
@@ -23,6 +23,6 @@ public enum LandblockStreamJobKind
LoadFar,
/// Read LandBlock + LandBlockInfo, generate scenery, build mesh, full entity layer.
LoadNear,
- /// Read LandBlockInfo + scenery only — terrain already loaded for this LB.
+ /// Read LandBlockInfo + scenery only — terrain already loaded for this LB.
PromoteToNear,
}
diff --git a/src/AcDream.App/Streaming/LandblockStreamer.cs b/src/AcDream.App/Streaming/LandblockStreamer.cs
index ed6fa5f8..234b90e1 100644
--- a/src/AcDream.App/Streaming/LandblockStreamer.cs
+++ b/src/AcDream.App/Streaming/LandblockStreamer.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Channels;
@@ -28,12 +28,12 @@ namespace AcDream.App.Streaming;
/// GameWindow's _datLock (Phase A.5 T10) serialises all
/// DatCollection.Get<T> calls. Both factory closures passed at
/// construction acquire that lock before reading dats. The worker never
-/// touches DatCollection directly — it only calls the factories.
+/// touches DatCollection directly — it only calls the factories.
///
///
///
/// Unloads pass through the outbox as
-/// records so the render thread can release GPU state on the next drain —
+/// records so the render thread can release GPU state on the next drain —
/// the streamer never touches GPU resources directly.
///
///
@@ -43,7 +43,7 @@ namespace AcDream.App.Streaming;
/// methods are thread-safe.
///
///
-internal sealed class LandblockStreamer : IDisposable, ILandblockCompletionSource
+public sealed class LandblockStreamer : IDisposable, ILandblockCompletionSource
{
///
/// Default drain batch size. Tuned to cap GPU upload work the render
@@ -78,7 +78,7 @@ internal sealed class LandblockStreamer : IDisposable, ILandblockCompletionSourc
{
_loadLandblock = loadLandblock;
_supportsRequestOrigin = supportsRequestOrigin;
- // Default: no mesh build (returns null → Failed result). Production
+ // Default: no mesh build (returns null → Failed result). Production
// wires in LandblockMesh.Build via the T12 construction site.
_buildMeshOrNull = buildMeshOrNull ?? ((_, _) => null);
_inbox = Channel.CreateUnbounded(
@@ -131,7 +131,7 @@ internal sealed class LandblockStreamer : IDisposable, ILandblockCompletionSourc
}
///
- /// Back-compat overload — wraps a kind-agnostic factory so existing test code
+ /// Back-compat overload — wraps a kind-agnostic factory so existing test code
/// that doesn't care about the JobKind branch keeps compiling. The wrapper
/// ignores the kind and calls the factory once per LB regardless of tier.
/// New production code should use .
@@ -239,7 +239,7 @@ internal sealed class LandblockStreamer : IDisposable, ILandblockCompletionSourc
/// control job which the worker
/// honours at read time, dropping all pending Loads from both priority
/// queues (Unloads survive). Used on the dungeon-entry edge to abort the
- /// in-flight 25×25 neighbor window so the ~129 ocean-grid dungeons never
+ /// in-flight 25×25 neighbor window so the ~129 ocean-grid dungeons never
/// finish loading (#133 FPS). Loads the worker has ALREADY dequeued still
/// complete; the StreamingController's collapsed-sweep unloads those few.
///
diff --git a/src/AcDream.App/Streaming/StreamingCompletionQueue.cs b/src/AcDream.App/Streaming/StreamingCompletionQueue.cs
index eb440650..147767aa 100644
--- a/src/AcDream.App/Streaming/StreamingCompletionQueue.cs
+++ b/src/AcDream.App/Streaming/StreamingCompletionQueue.cs
@@ -1,4 +1,4 @@
-using System.Diagnostics;
+using System.Diagnostics;
namespace AcDream.App.Streaming;
@@ -7,7 +7,7 @@ namespace AcDream.App.Streaming;
/// channel. Peek plus read lets the update thread price a payload before
/// adopting it into the scheduler.
///
-internal interface ILandblockCompletionSource
+public interface ILandblockCompletionSource
{
int BacklogCount { get; }
bool TryPeek(out LandblockStreamResult? result);
diff --git a/src/AcDream.App/Streaming/StreamingController.cs b/src/AcDream.App/Streaming/StreamingController.cs
index 01ae537d..08f20cc8 100644
--- a/src/AcDream.App/Streaming/StreamingController.cs
+++ b/src/AcDream.App/Streaming/StreamingController.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Diagnostics;
using AcDream.App.Rendering.Wb;
@@ -17,7 +17,7 @@ namespace AcDream.App.Streaming;
/// Threading: not thread-safe. All calls must happen on the render thread.
///
///
-internal sealed class StreamingController
+public sealed class StreamingController
: IStreamingFrameBackend,
IWorldRevealStreamingScheduler
{
@@ -88,14 +88,14 @@ internal sealed class StreamingController
// True while streaming is collapsed to the single dungeon landblock the
// player stands in (the dungeon gate, #133 FPS). AC dungeons have NO
- // adjacent landblocks — neighbors are unrelated ocean-grid dungeons that
- // are never visible, so we stop loading the 25×25 window entirely.
+ // adjacent landblocks — neighbors are unrelated ocean-grid dungeons that
+ // are never visible, so we stop loading the 25×25 window entirely.
private bool _collapsed;
// The dungeon landblock id we collapsed onto. Once collapsed we key the
// gate on this STABLE landblock, not the per-frame insideDungeon signal:
// CurrCell can momentarily resolve to null/outdoor mid-frame, and gating
- // expand on that flicker thrashes collapse↔expand (reload storms + a light
+ // expand on that flicker thrashes collapse↔expand (reload storms + a light
// leak). We only expand when the observer actually moves to a different
// landblock (teleport/portal out).
private uint _collapsedCenter;
@@ -610,16 +610,16 @@ internal sealed class StreamingController
///
/// Advance one frame. /
- /// are landblock coordinates (0..255) of the current viewer — the camera
+ /// are landblock coordinates (0..255) of the current viewer — the camera
/// in offline mode, the server-sent player position in live.
///
/// Two-tier model (Phase A.5 T13):
///
- /// - → enqueue LoadFar (terrain only, no entities)
- /// - → enqueue LoadNear (terrain + entities)
- /// - → enqueue PromoteToNear (entity layer for already-loaded terrain)
- /// - → drop entities on render thread immediately (terrain stays)
- /// - → enqueue full unload
+ /// - → enqueue LoadFar (terrain only, no entities)
+ /// - → enqueue LoadNear (terrain + entities)
+ /// - → enqueue PromoteToNear (entity layer for already-loaded terrain)
+ /// - → drop entities on render thread immediately (terrain stays)
+ /// - → enqueue full unload
///
///
public void Tick(int observerCx, int observerCy, bool insideDungeon = false)
@@ -699,15 +699,15 @@ internal sealed class StreamingController
if (_collapsed)
{
// Hysteresis. Cases:
- // - Still in the SAME dungeon landblock → hold (sweep stragglers).
+ // - Still in the SAME dungeon landblock → hold (sweep stragglers).
// - In a DIFFERENT dungeon cell (multi-landblock dungeon / new dungeon)
- // → re-collapse onto it.
+ // → re-collapse onto it.
// - CurrCell flickered null but the player hasn't gone anywhere: the
// observer landblock reverts to the position-derived value, which for a
// dungeon is only ever the ADJACENT off-by-one landblock (negative cell-
- // local Y). Hold — never expand on an adjacent flicker.
+ // local Y). Hold — never expand on an adjacent flicker.
// - Genuinely left to a DISTANT landblock (portal/teleport out, always far
- // from the ocean-grid dungeon block) → expand.
+ // from the ocean-grid dungeon block) → expand.
if (insideDungeon && centerId != _collapsedCenter)
EnterDungeonCollapse(observerCx, observerCy, centerId);
else if (!insideDungeon && ChebyshevLandblocks(centerId, _collapsedCenter) > 1)
@@ -910,23 +910,23 @@ internal sealed class StreamingController
///
/// #135: collapse to a single dungeon landblock IMMEDIATELY, before the first
- /// has a chance to bootstrap the full 25×25 window. Called
+ /// has a chance to bootstrap the full 25×25 window. Called
/// from the login / teleport spawn path the instant the streaming center is
/// recentered onto a SEALED dungeon landblock.
///
/// The per-frame insideDungeon gate keys on the physics
- /// CurrCell, which is only set once the player is PLACED — and placement
+ /// CurrCell, which is only set once the player is PLACED — and placement
/// waits for the dungeon landblock to hydrate. So for the whole hydration window
/// (tens of seconds for a ~200-cell dungeon) the gate reads false and
/// would enqueue the ~24 unrelated ocean-grid neighbor
/// dungeons (+ ~19k entities each); the collapse then only mops them up after
- /// placement. That mop-up is the 10→high FPS ramp users see at a dungeon login.
+ /// placement. That mop-up is the 10→high FPS ramp users see at a dungeon login.
///
/// Pre-collapsing means the EXPENSIVE dungeon-neighbour window is never
/// enqueued. On teleport nothing is enqueued at all (this fires before the next
/// Tick recenters). On login a brief Holtburg outdoor window may be enqueued by the
/// frame-1 NormalTick (before the player's spawn arrives) and is immediately
- /// cancelled by _clearPendingLoads here — cheap outdoor terrain, not the
+ /// cancelled by _clearPendingLoads here — cheap outdoor terrain, not the
/// ocean-grid dungeons, and a handful of already-dequeued loads get swept next
/// frame. Idempotent: a no-op when already collapsed onto this same landblock, so a
/// re-sent spawn or a same-frame double call costs nothing. Render-thread only,
@@ -954,7 +954,7 @@ internal sealed class StreamingController
}
///
- /// Outdoor / building-interior streaming — the original two-tier model.
+ /// Outdoor / building-interior streaming — the original two-tier model.
///
private void NormalTick(int observerCx, int observerCy)
{
@@ -1012,11 +1012,11 @@ internal sealed class StreamingController
///
/// Dungeon-entry edge: cancel the in-flight window load, unload every
/// resident neighbor, and pin streaming to the player's single dungeon
- /// landblock. Retail-faithful — AC dungeons have no adjacent landblocks
+ /// landblock. Retail-faithful — AC dungeons have no adjacent landblocks
/// (ACE LandblockManager.GetAdjacentIDs returns empty for a dungeon);
- /// the 25×25 window was pulling in ~129 unrelated ocean-grid dungeons and
+ /// the 25×25 window was pulling in ~129 unrelated ocean-grid dungeons and
/// their thousands of emitters (#133 FPS). Unloading them also tears down
- /// their lights, shrinking the static-light set toward retail's ≤40.
+ /// their lights, shrinking the static-light set toward retail's ≤40.
///
private void EnterDungeonCollapse(int cx, int cy, uint centerId)
{
@@ -1049,7 +1049,7 @@ internal sealed class StreamingController
///
/// While collapsed, unload any landblock that finished loading after the
- /// collapse edge — a Load the worker had already dequeued before the
+ /// collapse edge — a Load the worker had already dequeued before the
/// control job took
/// effect. At steady state only the dungeon landblock is resident, so this
/// is a no-op.
@@ -1057,7 +1057,7 @@ internal sealed class StreamingController
private void SweepCollapsed()
{
// Always preserve the true dungeon landblock (_collapsedCenter), never the
- // per-frame observer landblock — a CurrCell flicker must not unload the dungeon.
+ // per-frame observer landblock — a CurrCell flicker must not unload the dungeon.
foreach (var id in _state.LoadedLandblockIds)
if (id != _collapsedCenter) EnqueueUnload(id);
}
diff --git a/src/AcDream.App/Streaming/StreamingMutationException.cs b/src/AcDream.App/Streaming/StreamingMutationException.cs
index 70424ab1..5c74dc88 100644
--- a/src/AcDream.App/Streaming/StreamingMutationException.cs
+++ b/src/AcDream.App/Streaming/StreamingMutationException.cs
@@ -1,11 +1,11 @@
-namespace AcDream.App.Streaming;
+namespace AcDream.App.Streaming;
///
/// Reports whether a streaming queue/retirement mutation crossed its commit
/// point before a callback failed. Retry ledgers use this to avoid submitting
/// the same generation operation twice after a post-commit diagnostic error.
///
-internal sealed class StreamingMutationException : Exception
+public sealed class StreamingMutationException : Exception
{
public StreamingMutationException(
string message,
diff --git a/src/AcDream.App/Streaming/StreamingRegion.cs b/src/AcDream.App/Streaming/StreamingRegion.cs
index 66264d9b..1118489f 100644
--- a/src/AcDream.App/Streaming/StreamingRegion.cs
+++ b/src/AcDream.App/Streaming/StreamingRegion.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Linq;
@@ -7,9 +7,9 @@ namespace AcDream.App.Streaming;
///
/// Pure data type describing the set of landblocks currently considered
/// "visible" by the streaming system. Given a center landblock (x, y) and
-/// a radius, builds the set of landblock IDs in the (2r+1)×(2r+1) window.
+/// a radius, builds the set of landblock IDs in the (2r+1)×(2r+1) window.
///
-internal sealed class StreamingRegion
+public sealed class StreamingRegion
{
public int CenterX { get; private set; }
public int CenterY { get; private set; }
@@ -17,7 +17,7 @@ internal sealed class StreamingRegion
public int NearRadius { get; }
public int FarRadius { get; }
- // Strictly the (2r+1)×(2r+1) window (clamped to world bounds).
+ // Strictly the (2r+1)×(2r+1) window (clamped to world bounds).
private readonly HashSet _visible = new();
// Everything currently loaded: window + hysteresis-retained landblocks.
@@ -42,7 +42,7 @@ internal sealed class StreamingRegion
/// LandblockLoader.
///
///
- /// This set is strictly the (2r+1)×(2r+1) window; it does NOT include
+ /// This set is strictly the (2r+1)×(2r+1) window; it does NOT include
/// hysteresis-retained landblocks outside the window. Use
/// to enumerate everything actually loaded.
///
@@ -89,7 +89,7 @@ internal sealed class StreamingRegion
///
/// Encode a landblock at (lbX, lbY) into the AC dat id form. Always uses
/// the 0xFFFF terminator (LandBlock = terrain). The earlier
- /// version of this method used 0xFFFE by mistake — that's the
+ /// version of this method used 0xFFFE by mistake — that's the
/// LandBlockInfo id, and asking LandblockLoader.Load to read a
/// LandBlock at the LandBlockInfo coords corrupts the dat reader's
/// buffer position, returning a half-populated LandBlock.Height[]
@@ -135,7 +135,7 @@ internal sealed class StreamingRegion
///
/// Call once after to seed
/// _tierResidence with the initial window. Every LB in the inner
- /// ring (Chebyshev ≤ NearRadius) is marked Near; everything else Far.
+ /// ring (Chebyshev ≤ NearRadius) is marked Near; everything else Far.
///
public void MarkResidentFromBootstrap()
{
@@ -191,8 +191,8 @@ internal sealed class StreamingRegion
}
///
- /// Two-tier recenter: computes the 5-list diff per Phase A.5 spec §4.2.
- /// Hysteresis: NearRadius+2 for Near→Far demote; FarRadius+2 for Far→null
+ /// Two-tier recenter: computes the 5-list diff per Phase A.5 spec §4.2.
+ /// Hysteresis: NearRadius+2 for Near→Far demote; FarRadius+2 for Far→null
/// unload. Requires (or a prior
/// call to this method) to have seeded _tierResidence.
///
@@ -214,7 +214,7 @@ internal sealed class StreamingRegion
var toDemote = new List();
var toUnload = new List();
- // Pass 1: walk new far window — emit ToLoadFar / ToLoadNear / ToPromote.
+ // Pass 1: walk new far window — emit ToLoadFar / ToLoadNear / ToPromote.
var newCenterIds = new HashSet();
for (int dx = -FarRadius; dx <= FarRadius; dx++)
{
@@ -231,18 +231,18 @@ internal sealed class StreamingRegion
if (!_tierResidence.TryGetValue(id, out var current))
{
- // Not resident at all — fresh load.
+ // Not resident at all — fresh load.
if (inNear) toLoadNear.Add(id);
else toLoadFar.Add(id);
_tierResidence[id] = inNear ? TierResidence.Near : TierResidence.Far;
}
else if (current == TierResidence.Far && inNear)
{
- // Was Far, now inside Near ring — promote.
+ // Was Far, now inside Near ring — promote.
toPromote.Add(id);
_tierResidence[id] = TierResidence.Near;
}
- // Near→Near and Far→Far are no-ops.
+ // Near→Near and Far→Far are no-ops.
}
}
@@ -259,7 +259,7 @@ internal sealed class StreamingRegion
if (newCenterIds.Contains(id))
{
- // Still in the far window — only Near→Far demote possible here.
+ // Still in the far window — only Near→Far demote possible here.
if (current == TierResidence.Near && (absDx > NearRadius || absDy > NearRadius))
{
if (distance > nearUnloadThreshold)
@@ -271,7 +271,7 @@ internal sealed class StreamingRegion
continue;
}
- // Outside the new window — demote / unload by threshold.
+ // Outside the new window — demote / unload by threshold.
if (current == TierResidence.Near)
{
if (distance > nearUnloadThreshold)
@@ -336,7 +336,7 @@ internal sealed class StreamingRegion
toUnload.Add(id);
}
- // Update resident: (oldResident ∪ newVisible) ∖ toUnload.
+ // Update resident: (oldResident ∪ newVisible) ∖ toUnload.
_resident.UnionWith(_visible);
foreach (var id in toUnload)
_resident.Remove(id);
@@ -352,7 +352,7 @@ internal sealed class StreamingRegion
/// Both lists are disjoint from the current
/// set; the caller hands them to LandblockStreamer as jobs.
///
-internal readonly record struct RegionDiff(
+public readonly record struct RegionDiff(
IReadOnlyList ToLoad,
IReadOnlyList ToUnload);
diff --git a/src/AcDream.App/Streaming/StreamingWorkBudget.cs b/src/AcDream.App/Streaming/StreamingWorkBudget.cs
index 68618945..1d04f352 100644
--- a/src/AcDream.App/Streaming/StreamingWorkBudget.cs
+++ b/src/AcDream.App/Streaming/StreamingWorkBudget.cs
@@ -1,11 +1,11 @@
-using System.Diagnostics;
+using System.Diagnostics;
namespace AcDream.App.Streaming;
///
/// Immutable, validated per-frame streaming work profile.
///
-internal readonly record struct StreamingWorkBudget
+public readonly record struct StreamingWorkBudget
{
public StreamingWorkBudget(
TimeSpan maxUpdateTime,
@@ -75,7 +75,7 @@ internal readonly record struct StreamingWorkBudget
///
/// Conservative, known cost of one atomic streaming operation.
///
-internal readonly record struct StreamingWorkCost(
+public readonly record struct StreamingWorkCost(
int CompletionAdmissions = 0,
long AdoptedCpuBytes = 0,
int EntityOperations = 0,
@@ -117,14 +117,14 @@ internal readonly record struct StreamingWorkCost(
left > long.MaxValue - right ? long.MaxValue : left + right;
}
-internal enum StreamingWorkAdmission : byte
+public enum StreamingWorkAdmission : byte
{
Admitted,
OversizedProgress,
Yielded,
}
-internal enum StreamingWorkLimit : byte
+public enum StreamingWorkLimit : byte
{
None,
Time,
@@ -144,7 +144,7 @@ internal enum StreamingWorkLane : byte
///
/// Immutable observation of one frame's streaming work.
///
-internal readonly record struct StreamingWorkMeterSnapshot(
+public readonly record struct StreamingWorkMeterSnapshot(
StreamingWorkCost Used,
StreamingWorkCost DestinationUsed,
StreamingWorkCost NonDestinationUsed,
@@ -163,7 +163,7 @@ internal readonly record struct StreamingWorkMeterSnapshot(
///
/// Read-only streaming scheduler facts published to lifecycle artifacts.
///
-internal readonly record struct StreamingWorkDiagnostics(
+public readonly record struct StreamingWorkDiagnostics(
StreamingWorkMeterSnapshot LastFrame,
long LifetimeFrameOverrunCount,
long LifetimeOversizedProgressCount,
@@ -187,7 +187,7 @@ internal readonly record struct StreamingWorkDiagnostics(
/// Single-thread, frame-scoped admission meter. Callers reserve a known cost
/// before an atomic operation and complete or fail that reservation afterward.
///
-internal sealed class StreamingWorkMeter
+public sealed class StreamingWorkMeter
{
private readonly StreamingWorkBudget _budget;
private readonly Func _timestamp;
diff --git a/src/AcDream.App/Streaming/StreamingWorkBudgetOptions.cs b/src/AcDream.App/Streaming/StreamingWorkBudgetOptions.cs
index d61532ce..edc4800c 100644
--- a/src/AcDream.App/Streaming/StreamingWorkBudgetOptions.cs
+++ b/src/AcDream.App/Streaming/StreamingWorkBudgetOptions.cs
@@ -1,4 +1,4 @@
-using System.Globalization;
+using System.Globalization;
namespace AcDream.App.Streaming;
@@ -7,7 +7,7 @@ namespace AcDream.App.Streaming;
/// These values limit work per frame; they never reduce the amount or
/// quality of content that eventually becomes resident.
///
-internal sealed record StreamingWorkBudgetOptions(
+public sealed record StreamingWorkBudgetOptions(
double MaxUpdateMilliseconds,
int MaxCompletionAdmissions,
long MaxAdoptedCpuBytes,
diff --git a/src/AcDream.App/Streaming/TwoTierDiff.cs b/src/AcDream.App/Streaming/TwoTierDiff.cs
index 7d08c9b9..2a24dab9 100644
--- a/src/AcDream.App/Streaming/TwoTierDiff.cs
+++ b/src/AcDream.App/Streaming/TwoTierDiff.cs
@@ -1,15 +1,15 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
namespace AcDream.App.Streaming;
///
/// Output of for the two-tier model.
/// Five disjoint lists describe what changed since the previous Tick. Per
-/// Phase A.5 spec §4.2.
+/// Phase A.5 spec §4.2.
///
-internal readonly record struct TwoTierDiff(
+public readonly record struct TwoTierDiff(
IReadOnlyList ToLoadFar, // entered far window from null (terrain only)
- IReadOnlyList ToLoadNear, // entered near window from null (terrain + entities — first-tick or teleport)
+ IReadOnlyList ToLoadNear, // entered near window from null (terrain + entities — first-tick or teleport)
IReadOnlyList ToPromote, // entered near window from far-resident (entities only)
IReadOnlyList ToDemote, // exited near window past hysteresis (drop entities)
IReadOnlyList ToUnload); // exited far window past hysteresis (drop terrain)
diff --git a/src/AcDream.App/Streaming/WorldGenerationQuiescence.cs b/src/AcDream.App/Streaming/WorldGenerationQuiescence.cs
index 41fa676c..5ebfd3e1 100644
--- a/src/AcDream.App/Streaming/WorldGenerationQuiescence.cs
+++ b/src/AcDream.App/Streaming/WorldGenerationQuiescence.cs
@@ -1,4 +1,4 @@
-using AcDream.App.Audio;
+using AcDream.App.Audio;
using AcDream.Core.Selection;
using AcDream.Runtime.Entities;
using AcDream.Runtime.World;
@@ -10,7 +10,7 @@ namespace AcDream.App.Streaming;
/// A hard login/portal reveal boundary makes the prior world unavailable
/// immediately while its physical owners retire over later frames.
///
-internal interface IWorldGenerationAvailability
+public interface IWorldGenerationAvailability
{
bool IsWorldAvailable { get; }
long QuiescedGeneration { get; }
diff --git a/src/AcDream.App/Studio/DumpLayout.cs b/src/AcDream.App/Studio/DumpLayout.cs
index 17279d2f..1f90f724 100644
--- a/src/AcDream.App/Studio/DumpLayout.cs
+++ b/src/AcDream.App/Studio/DumpLayout.cs
@@ -1,27 +1,27 @@
-using System.Numerics;
+using System.Numerics;
using AcDream.App.UI;
namespace AcDream.App.Studio;
-// ─────────────────────────────────────────────────────────────────────────────
-// DumpLayout — load a panel from the retail UI layout dump
+// ─────────────────────────────────────────────────────────────────────────────
+// DumpLayout — load a panel from the retail UI layout dump
//
// The dump stores every node's rect in ABSOLUTE screen coordinates (the
// panel's design position in the retail UI, not relative to its parent).
// Evidence: for the "inventory" panel, the root node is at x=500,y=138 and
-// its direct children are also at x=500,y=161 — the child y=161 is only
+// its direct children are also at x=500,y=161 — the child y=161 is only
// 23 pixels below the parent y=138, which makes sense as a child offset
// (the header row), not as the raw rect. If the rects were parent-relative,
// (500,161) would place the child way off the window.
//
-// DumpLayout converts absolute → parent-relative by computing:
+// DumpLayout converts absolute → parent-relative by computing:
// child.Left = child.Rect.X - parent.Rect.X
// child.Top = child.Rect.Y - parent.Rect.Y
//
// The root node (ParentTraversalIndex == null) is placed at (0,0) so the
// whole tree sits at the UiHost origin rather than at the panel's retail
// screen position.
-// ─────────────────────────────────────────────────────────────────────────────
+// ─────────────────────────────────────────────────────────────────────────────
///
/// Builds a static tree from the retail UI layout dump
@@ -30,11 +30,11 @@ namespace AcDream.App.Studio;
/// node's set to the dump's element_id and
/// set to the widget_kind string.
///
-/// This source is STATIC — no controllers, no FixtureProvider, no live
+/// This source is STATIC — no controllers, no FixtureProvider, no live
/// game data. It is a build reference for the UI Studio showing any of the 26
/// retail windows without needing the production panel wired up.
///
-internal static class DumpLayout
+public static class DumpLayout
{
///
/// Parse the dump at , find the panel whose slug
@@ -42,7 +42,7 @@ internal static class DumpLayout
///
///
/// maps a RenderSurface id (0x06xxxxxx) to a
- /// (GL texture handle, native width, native height) triple — pass
+ /// (GL texture handle, native width, native height) triple — pass
/// RenderStack.ResolveChrome from the studio, or a stub returning
/// (1,1,1) for tests.
///
@@ -52,10 +52,10 @@ internal static class DumpLayout
public static UiElement? Load(
string dumpPath,
string slug,
- Func resolve,
+ Func resolve,
out string? error)
{
- // ── 1. Parse the dump JSON ────────────────────────────────────────
+ // ── 1. Parse the dump JSON ────────────────────────────────────────
var dump = UiDumpModel.Parse(dumpPath);
if (dump is null)
{
@@ -63,7 +63,7 @@ internal static class DumpLayout
return null;
}
- // ── 2. Find the requested panel ───────────────────────────────────
+ // ── 2. Find the requested panel ───────────────────────────────────
var panel = dump.Panels.FirstOrDefault(
p => string.Equals(p.Slug, slug, StringComparison.OrdinalIgnoreCase));
if (panel is null)
@@ -79,12 +79,12 @@ internal static class DumpLayout
return null;
}
- // ── 3. Build a traversal-index → node lookup ──────────────────────
+ // ── 3. Build a traversal-index → node lookup ──────────────────────
var byIndex = new Dictionary(panel.Nodes.Count);
foreach (var n in panel.Nodes)
byIndex[n.TraversalIndex] = n;
- // ── 4. Create UiElement objects for every node ────────────────────
+ // ── 4. Create UiElement objects for every node ────────────────────
var elements = new Dictionary(panel.Nodes.Count);
foreach (var node in panel.Nodes)
{
@@ -92,7 +92,7 @@ internal static class DumpLayout
elements[node.TraversalIndex] = el;
}
- // ── 5. Wire parent–child relationships + set parent-relative coords ─
+ // ── 5. Wire parent–child relationships + set parent-relative coords ─
UiElement? root = null;
foreach (var node in panel.Nodes)
{
@@ -100,7 +100,7 @@ internal static class DumpLayout
if (node.ParentTraversalIndex is null)
{
- // Root node — place at (0,0) so the tree sits at the UiHost origin.
+ // Root node — place at (0,0) so the tree sits at the UiHost origin.
// The panel's absolute rect offset is discarded here (it was the
// retail design position inside the retail screen, which we don't need).
el.Left = 0f;
@@ -109,7 +109,7 @@ internal static class DumpLayout
}
else
{
- // Non-root: convert absolute → parent-relative by subtracting parent rect.
+ // Non-root: convert absolute → parent-relative by subtracting parent rect.
// child.Left = child.Rect.X - parent.Rect.X
// child.Top = child.Rect.Y - parent.Rect.Y
// This preserves the visual layout inside each group without placing the
@@ -137,11 +137,11 @@ internal static class DumpLayout
return root;
}
- // ── Private helpers ───────────────────────────────────────────────────────
+ // ── Private helpers ───────────────────────────────────────────────────────
private static UiElement BuildElement(
DumpNode node,
- Func resolve)
+ Func resolve)
{
uint imageId = UiDumpModel.PickImageId(node);
var kind = node.WidgetKind ?? "Group";
@@ -149,7 +149,7 @@ internal static class DumpLayout
UiElement el;
if (imageId != 0 && !string.Equals(kind, "Group", StringComparison.OrdinalIgnoreCase))
{
- // Sprite/Button/Scrollbar/Slider — create a sprite-drawing element.
+ // Sprite/Button/Scrollbar/Slider — create a sprite-drawing element.
el = new DumpSpriteElement(imageId, resolve)
{
Name = kind,
@@ -159,7 +159,7 @@ internal static class DumpLayout
}
else
{
- // Group (or sprite without an image) — plain container, no own draw.
+ // Group (or sprite without an image) — plain container, no own draw.
el = new DumpGroupElement()
{
Name = kind,
@@ -168,7 +168,7 @@ internal static class DumpLayout
};
}
- // EventId is set from the dump's element_id (cast to uint — the decimal
+ // EventId is set from the dump's element_id (cast to uint — the decimal
// values in the JSON represent the same dat handle used at runtime).
el.EventId = (uint)node.ElementId;
el.Left = node.Rect.X; // overwritten by caller per root/child logic
@@ -180,27 +180,27 @@ internal static class DumpLayout
}
}
-// ─────────────────────────────────────────────────────────────────────────────
-// DumpSpriteElement — minimal element that draws a single sprite
-// ─────────────────────────────────────────────────────────────────────────────
+// ─────────────────────────────────────────────────────────────────────────────
+// DumpSpriteElement — minimal element that draws a single sprite
+// ─────────────────────────────────────────────────────────────────────────────
///
/// Draws a single sprite at its native size tiled to fill
-/// × . Used for Sprite/Button/Scrollbar/Slider nodes from
+/// × . Used for Sprite/Button/Scrollbar/Slider nodes from
/// the retail UI dump.
///
/// We do NOT reuse here because
/// that class requires an ElementInfo with a populated StateMedia
-/// dictionary — the dat-import plumbing — which is not needed for a static dump
+/// dictionary — the dat-import plumbing — which is not needed for a static dump
/// preview. A minimal subclass keeps the code simpler and the dependency surface
/// smaller.
///
internal sealed class DumpSpriteElement : UiElement
{
private readonly uint _imageId;
- private readonly Func _resolve;
+ private readonly Func _resolve;
- public DumpSpriteElement(uint imageId, Func resolve)
+ public DumpSpriteElement(uint imageId, Func resolve)
{
_imageId = imageId;
_resolve = resolve;
@@ -211,25 +211,25 @@ internal sealed class DumpSpriteElement : UiElement
if (_imageId == 0) return;
var (tex, tw, th) = _resolve(_imageId);
- if (!tex.IsAssigned || tw == 0 || th == 0) return;
+ if (tex == 0 || tw == 0 || th == 0) return;
- // Tile at native resolution (same as UiDatElement.OnDraw — UV-repeat on both
+ // Tile at native resolution (same as UiDatElement.OnDraw — UV-repeat on both
// axes via GL_REPEAT, Width/tw and Height/th tile the texture).
ctx.DrawSprite(tex, 0, 0, Width, Height,
0, 0, Width / tw, Height / th, Vector4.One);
}
}
-// ─────────────────────────────────────────────────────────────────────────────
-// DumpGroupElement — pure container (Group nodes from the dump)
-// ─────────────────────────────────────────────────────────────────────────────
+// ─────────────────────────────────────────────────────────────────────────────
+// DumpGroupElement — pure container (Group nodes from the dump)
+// ─────────────────────────────────────────────────────────────────────────────
///
-/// Container element for dump Group nodes — no own draw, just hosts children.
+/// Container element for dump Group nodes — no own draw, just hosts children.
/// Extending UiElement directly (no OnDraw override) gives transparent groups,
/// which matches Group nodes in the retail layout that have no background sprite.
///
internal sealed class DumpGroupElement : UiElement
{
- // No OnDraw — completely transparent container.
+ // No OnDraw — completely transparent container.
}
diff --git a/src/AcDream.App/Studio/FixtureProvider.cs b/src/AcDream.App/Studio/FixtureProvider.cs
index 8489e688..eb375b40 100644
--- a/src/AcDream.App/Studio/FixtureProvider.cs
+++ b/src/AcDream.App/Studio/FixtureProvider.cs
@@ -1,4 +1,4 @@
-using AcDream.App.Rendering;
+using AcDream.App.Rendering;
using AcDream.App.UI;
using AcDream.App.UI.Layout;
using AcDream.Core.Combat;
@@ -15,7 +15,7 @@ namespace AcDream.App.Studio;
/// controller Bind methods against .
///
///
-/// The studio is intentionally thin — there is no live game session, no
+/// The studio is intentionally thin — there is no live game session, no
/// server connection, and no network. FixtureProvider bridges that gap by
/// feeding static fixtures (vitals percentages, a fake inventory, empty
/// shortcut lists) so the bound widgets show plausible state instead of
@@ -23,7 +23,7 @@ namespace AcDream.App.Studio;
///
///
///
-/// IconIds approach: raw-resolve stub — resolve the base iconId
+/// IconIds approach: raw-resolve stub — resolve the base iconId
/// via and return the GL handle
/// directly. This is intentionally simpler than GameWindow's full
/// (5-layer composite). The raw icon is enough
@@ -31,7 +31,7 @@ namespace AcDream.App.Studio;
/// compositor is the live-game concern, not the layout preview concern.
///
///
-internal static class FixtureProvider
+public static class FixtureProvider
{
///
/// Populate with sample data appropriate for
@@ -131,7 +131,7 @@ internal static class FixtureProvider
{
// Resolve the per-list empty-slot art from the dat cell template, matching the
// exact lookup GameWindow.OnLoad performs (UIElement_ItemList::InternalCreateItem
- // 0x004e3570 → attr 0x1000000e → catalog 0x21000037 → ItemSlot_Empty).
+ // 0x004e3570 → attr 0x1000000e → catalog 0x21000037 → ItemSlot_Empty).
uint contentsEmpty = ItemListCellTemplate.ResolveEmptySprite(dats, 0x21000021u, 0x100001C6u);
uint sideBagEmpty = ItemListCellTemplate.ResolveEmptySprite(dats, 0x21000022u, 0x100001CAu);
uint mainPackEmpty = ItemListCellTemplate.ResolveEmptySprite(dats, 0x21000022u, 0x100001C9u);
@@ -177,10 +177,10 @@ internal static class FixtureProvider
return new RetainedPanelControllerGroup(inventory, paperdoll);
}
- case 0x2100002Eu: // gmStatManagementUI — Attributes/Skills/Titles window (LayoutDesc 0x2100002E)
+ case 0x2100002Eu: // gmStatManagementUI — Attributes/Skills/Titles window (LayoutDesc 0x2100002E)
// Bind the REAL importer-mounted header + list elements (name/heritage/PK/level/
// total-XP/XP-meter + the 9-row attribute list + footer State-A). NOT the text-report
- // sub-panel (that is gmCharacterInfoUI 0x2100001A → CharacterController).
+ // sub-panel (that is gmCharacterInfoUI 0x2100001A → CharacterController).
// LargeDatFont (0x40000001, MaxCharHeight=18) is used for the attribute row text;
// fallback to VitalsDatFont (0x40000000, 16px) if unavailable.
CharacterStatController.Bind(
@@ -192,12 +192,12 @@ internal static class FixtureProvider
return null;
default:
- // Unknown layout — no-op; the panel renders structurally.
+ // Unknown layout — no-op; the panel renders structurally.
return null;
}
}
- // ── Helpers ─────────────────────────────────────────────────────────────
+ // ── Helpers ─────────────────────────────────────────────────────────────
///
/// Build the iconIds delegate for toolbar / inventory controllers.
@@ -206,15 +206,15 @@ internal static class FixtureProvider
/// Raw-resolve stub: resolve the base (arg 2)
/// via and return its GL handle.
/// The remaining args (type, underlayId, overlayId, effects) are ignored
- /// for the studio — a single-layer icon is sufficient for layout preview.
+ /// for the studio — a single-layer icon is sufficient for layout preview.
///
///
/// This is what the task spec calls "v1 raw-resolve stub".
///
- private static Func MakeIconIds(RenderStack stack)
+ private static Func MakeIconIds(RenderStack stack)
=> (_, iconId, _, _, _) =>
{
- if (iconId == 0u) return GpuTextureSlot.Unassigned;
+ if (iconId == 0u) return 0u;
var (handle, _, _) = stack.ResolveChrome(iconId);
return handle;
};
diff --git a/src/AcDream.App/Studio/LayoutSource.cs b/src/AcDream.App/Studio/LayoutSource.cs
index ba61f2b8..78e53383 100644
--- a/src/AcDream.App/Studio/LayoutSource.cs
+++ b/src/AcDream.App/Studio/LayoutSource.cs
@@ -1,4 +1,4 @@
-using AcDream.App.UI;
+using AcDream.App.UI;
using AcDream.App.UI.Layout;
using DatReaderWriter;
using AcDream.Content;
@@ -6,21 +6,21 @@ using AcDream.Content;
namespace AcDream.App.Studio;
/// Which kind of source the studio is currently previewing.
-internal enum LayoutSourceKind { DatLayout, Markup }
+public enum LayoutSourceKind { DatLayout, Markup }
///
/// Wraps the two ways the UI Studio can load a panel to preview:
-/// a LayoutDesc dat id, or a KSML markup file path (Task 6 — unsupported now).
+/// a LayoutDesc dat id, or a KSML markup file path (Task 6 — unsupported now).
///
/// Call with the current to
/// import the layout and get the root . The result is also
/// cached in so can re-run the same
/// source without re-reading the options.
///
-internal sealed class LayoutSource
+public sealed class LayoutSource
{
private readonly IDatReaderWriter _dats;
- private readonly Func _resolve;
+ private readonly Func _resolve;
private readonly UiDatFont? _datFont;
private readonly Func? _fontResolve;
@@ -33,17 +33,17 @@ internal sealed class LayoutSource
///
/// Create a LayoutSource.
///
- /// Optional per-element font resolver: FontDid →
+ /// Optional per-element font resolver: FontDid →
/// (null when the font isn't in the dats). When supplied,
/// elements with a non-zero FontDid receive their own dat font at build time
/// instead of the shared global. Controllers that
/// explicitly set after
/// still override the build-time value.
- /// Pass null (default) for the original single-font behavior — the live
+ /// Pass null (default) for the original single-font behavior — the live
/// path passes null so it is provably unchanged.
public LayoutSource(
IDatReaderWriter dats,
- Func resolve,
+ Func resolve,
UiDatFont? datFont,
Func? fontResolve = null)
{
@@ -107,7 +107,7 @@ internal sealed class LayoutSource
return LoadDat(LayoutId.Value);
}
- // ── Private ──────────────────────────────────────────────────────────────────
+ // ── Private ──────────────────────────────────────────────────────────────────
private UiElement? LoadDat(uint layoutId)
{
diff --git a/src/AcDream.App/Studio/PanelFbo.cs b/src/AcDream.App/Studio/PanelFbo.cs
index d8295aa1..4f61ec55 100644
--- a/src/AcDream.App/Studio/PanelFbo.cs
+++ b/src/AcDream.App/Studio/PanelFbo.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Numerics;
using AcDream.App.Rendering.Wb;
using AcDream.App.UI;
@@ -19,11 +19,11 @@ namespace AcDream.App.Studio;
/// displaying the texture in ImGui (pass uv0=(0,1), uv1=(1,0) to ImGui.Image)
/// so the image appears right-side-up in ImGui's top-left coordinate system.
///
-internal sealed unsafe class PanelFbo : IDisposable
+public sealed unsafe class PanelFbo : IDisposable
{
private readonly GL _gl;
- // Off-screen target — lazily (re)created when the requested size changes.
+ // Off-screen target — lazily (re)created when the requested size changes.
private uint _fbo;
private uint _colorTex;
private uint _depthRbo;
@@ -37,7 +37,7 @@ internal sealed unsafe class PanelFbo : IDisposable
///
/// Render (a full draw pass) into a
- /// private FBO at × pixels.
+ /// private FBO at × pixels.
/// Returns the GL color texture handle (0 on failure). The texture is valid until
/// the next call to with a different size, or until .
///
@@ -50,7 +50,7 @@ internal sealed unsafe class PanelFbo : IDisposable
// Seal the entire pass: GLStateScope saves + restores every GL state the
// UI draw touches (viewport, blend, FBO binding, etc.) so ImGui's own state
- // — set up by BeginFrame and expected intact by Render — is untouched.
+ // — set up by BeginFrame and expected intact by Render — is untouched.
using var scope = new GLStateScope(_gl);
_gl.BindFramebuffer(FramebufferTarget.Framebuffer, _fbo);
@@ -95,7 +95,7 @@ internal sealed unsafe class PanelFbo : IDisposable
return buf;
}
- // ── FBO lifecycle (mirrors PaperdollViewportRenderer.EnsureFramebuffer) ──────
+ // ── FBO lifecycle (mirrors PaperdollViewportRenderer.EnsureFramebuffer) ──────
private void EnsureFramebuffer(int width, int height)
{
diff --git a/src/AcDream.App/Studio/SampleData.cs b/src/AcDream.App/Studio/SampleData.cs
index 2422ba59..e523fc8e 100644
--- a/src/AcDream.App/Studio/SampleData.cs
+++ b/src/AcDream.App/Studio/SampleData.cs
@@ -1,4 +1,4 @@
-using AcDream.App.UI.Layout;
+using AcDream.App.UI.Layout;
using AcDream.Core.Items;
namespace AcDream.App.Studio;
@@ -25,18 +25,18 @@ namespace AcDream.App.Studio;
/// Equipped chest armor : 0x060011CFu
/// Equipped melee weapon : 0x060011CBu
///
-/// These are the icon *base* RenderSurface ids — the same ids GameWindow passes
+/// These are the icon *base* RenderSurface ids — the same ids GameWindow passes
/// as `iconId` into the iconIds lambda. FixtureProvider resolves them via
/// and returns the raw GL handle.
///
-internal static class SampleData
+public static class SampleData
{
- // ── Guids ────────────────────────────────────────────────────────────────
+ // ── Guids ────────────────────────────────────────────────────────────────
/// Fake server guid for the studio's synthetic player.
public const uint PlayerGuid = 0x50000001u;
- // Items in main pack (slots 0–5).
+ // Items in main pack (slots 0–5).
private const uint SwordGuid = 0x50000010u;
private const uint ChestGuid = 0x50000011u;
private const uint GlovesGuid = 0x50000012u;
@@ -53,7 +53,7 @@ internal static class SampleData
private const uint ChestEqGuid = 0x50000031u;
private const uint WeaponEqGuid = 0x50000032u;
- // ── Icon ids (0x06xxxxxx RenderSurface dat ids) ───────────────────────────
+ // ── Icon ids (0x06xxxxxx RenderSurface dat ids) ───────────────────────────
// These are the same underlay/fallback icon ids the IconComposer tests pin.
private const uint IconWeapon = 0x060011CBu; // weapon underlay
@@ -62,7 +62,7 @@ internal static class SampleData
private const uint IconJewelry = 0x060011D5u; // jewelry underlay
private const uint IconMisc = 0x060011D4u; // misc / fallback underlay
- // ── Public API ──────────────────────────────────────────────────────────
+ // ── Public API ──────────────────────────────────────────────────────────
///
/// Build a fresh populated with the
@@ -74,7 +74,7 @@ internal static class SampleData
{
var t = new ClientObjectTable();
- // ── Player object ─────────────────────────────────────────────────
+ // ── Player object ─────────────────────────────────────────────────
t.AddOrUpdate(new ClientObject
{
ObjectId = PlayerGuid,
@@ -89,7 +89,7 @@ internal static class SampleData
AetheriaUnlocks.PropertyId,
(int)AetheriaUnlockState.All);
- // ── Loose items in main pack (slots 0–5) ──────────────────────────
+ // ── Loose items in main pack (slots 0–5) ──────────────────────────
AddItem(t, SwordGuid, ItemType.MeleeWeapon, IconWeapon, "Iron Sword", PlayerGuid, 0, burden: 60);
AddItem(t, ChestGuid, ItemType.Armor, IconArmor, "Leather Breastplate", PlayerGuid, 1, burden: 200);
@@ -98,14 +98,14 @@ internal static class SampleData
AddItem(t, HealKitGuid, ItemType.Misc, IconMisc, "Healing Kit", PlayerGuid, 4, burden: 30);
AddItem(t, CompGuid, ItemType.SpellComponents, IconMisc, "Spell Comps", PlayerGuid, 5, burden: 25, stackSize: 50, stackMax: 100);
- // ── Side bags (Container items in main pack, slots 6 & 7) ─────────
+ // ── Side bags (Container items in main pack, slots 6 & 7) ─────────
AddItem(t, Bag1Guid, ItemType.Container, IconMisc, "Small Pack 1",
containerId: PlayerGuid, slot: 6, burden: 20, itemsCapacity: 24);
AddItem(t, Bag2Guid, ItemType.Container, IconMisc, "Small Pack 2",
containerId: PlayerGuid, slot: 7, burden: 20, itemsCapacity: 24);
- // ── Equipped items (ContainerId = PlayerGuid, CurrentlyEquippedLocation set) ──
+ // ── Equipped items (ContainerId = PlayerGuid, CurrentlyEquippedLocation set) ──
AddEquipped(t, HelmGuid, ItemType.Armor, IconClothing, "Tin Helm", EquipMask.HeadWear);
AddEquipped(t, ChestEqGuid, ItemType.Armor, IconArmor, "Chain Coat", EquipMask.ChestArmor);
@@ -114,13 +114,13 @@ internal static class SampleData
return t;
}
- // ── Sample vital constants (used by FixtureProvider) ────────────────────
+ // ── Sample vital constants (used by FixtureProvider) ────────────────────
public const float HealthPct = 0.8f;
public const float StaminaPct = 0.6f;
public const float ManaPct = 0.9f;
- // ── Sample character sheet (used by CharacterController in the Studio) ───
+ // ── Sample character sheet (used by CharacterController in the Studio) ───
///
/// Returns a representative for the studio's
@@ -145,12 +145,12 @@ internal static class SampleData
XpToNextLevel = 42_000_000,
XpFraction = 0.63f,
- // Vitals: retail screenshot spec (Pass 1 acceptance criteria §Goal).
+ // Vitals: retail screenshot spec (Pass 1 acceptance criteria §Goal).
HealthCurrent = 5, HealthMax = 5,
StaminaCurrent = 10, StaminaMax = 10,
ManaCurrent = 10, ManaMax = 10,
- // Attributes: Strength + Quickness = 200; all others = 10 (retail screenshot spec §Goal).
+ // Attributes: Strength + Quickness = 200; all others = 10 (retail screenshot spec §Goal).
Strength = 200,
Endurance = 10,
Quickness = 200,
@@ -171,9 +171,9 @@ internal static class SampleData
// Raise costs in retail display order (Strength, Endurance, Coordination, Quickness,
// Focus, Self, Health, Stamina, Mana).
- // Str@200 = maxed → 0 (disabled). Quickness@200 = maxed → 0. Others @10 → affordable.
- // Focus@10 → 110 matches the authoritative retail screenshot (spec §4).
- // Formula bracket at value=10: ExperienceToAttributeLevel(11) − ExperienceToAttributeLevel(10).
+ // Str@200 = maxed → 0 (disabled). Quickness@200 = maxed → 0. Others @10 → affordable.
+ // Focus@10 → 110 matches the authoritative retail screenshot (spec §4).
+ // Formula bracket at value=10: ExperienceToAttributeLevel(11) − ExperienceToAttributeLevel(10).
AttributeRaiseCosts = new long[] { 0L, 95L, 100L, 0L, 110L, 105L, 90L, 88L, 112L },
AttributeRaise10Costs = new long[] { 0L, 950L, 1_000L, 0L, 1_100L, 1_050L, 900L, 880L, 1_120L },
@@ -207,7 +207,7 @@ internal static class SampleData
BurdenMax = 4500,
};
- // ── Helpers ─────────────────────────────────────────────────────────────
+ // ── Helpers ─────────────────────────────────────────────────────────────
private static void AddItem(
ClientObjectTable t,
diff --git a/src/AcDream.App/Studio/StudioInspector.cs b/src/AcDream.App/Studio/StudioInspector.cs
index 76f2f9a9..26e6dc0d 100644
--- a/src/AcDream.App/Studio/StudioInspector.cs
+++ b/src/AcDream.App/Studio/StudioInspector.cs
@@ -1,4 +1,4 @@
-using System.Numerics;
+using System.Numerics;
using AcDream.App.UI;
using ImGuiNET;
@@ -8,7 +8,7 @@ namespace AcDream.App.Studio;
/// All canvas mouse events gathered by in one frame.
/// All coordinates are already mapped to panel-local pixels (origin top-left, same as UiRoot).
///
-internal readonly struct CanvasInputEvent
+public readonly struct CanvasInputEvent
{
/// Mouse is currently hovering the canvas image. When false all other fields are 0 / false.
public readonly bool IsHovered;
@@ -37,13 +37,13 @@ internal readonly struct CanvasInputEvent
///
/// Four-pane ImGui IDE for the acdream UI Studio:
///
-/// - Toolbar — panel picker (slug combo) across the top.
-/// - Canvas — shows the panel FBO texture; in Interact mode mouse events
+///
- Toolbar — panel picker (slug combo) across the top.
+/// - Canvas — shows the panel FBO texture; in Interact mode mouse events
/// are forwarded to the panel UiHost (buttons/tabs respond); in Inspect mode a
/// left-click hit-tests and selects the element under the cursor.
-/// - Tree — recursive ImGui tree of the element hierarchy; clicking a node
+///
- Tree — recursive ImGui tree of the element hierarchy; clicking a node
/// sets .
-/// - Properties — shows the element's geometry,
+///
- Properties — shows the element's geometry,
/// anchors, and z-order.
///
///
@@ -52,20 +52,20 @@ internal readonly struct CanvasInputEvent
/// sub-window. After ImGui.Image we call ImGui.GetItemRectMin() to get the
/// screen-space top-left of the drawn image (accounting for the sub-window's title bar,
/// padding, and any scrolling). Subtracting that from the raw mouse screen position gives
-/// panel-local pixels directly — no additional scale factor is needed because the image is
+/// panel-local pixels directly — no additional scale factor is needed because the image is
/// drawn 1:1.
///
-/// V-flip — no extra Y inversion needed:
+/// V-flip — no extra Y inversion needed:
/// The FBO origin is bottom-left (GL convention), so we pass uv0=(0,1), uv1=(1,0) to
/// ImGui.Image to flip V. After this flip, displayed row 0 (top of the image on
/// screen) corresponds to panel Y=0 (the top of the UI panel), matching UiRoot's
/// top-left origin. Therefore the panel-local Y computed above maps directly into UiRoot
-/// without further inversion — do NOT flip Y again.
+/// without further inversion — do NOT flip Y again.
///
/// Layout: the four panes call SetNextWindowPos + SetNextWindowSize
/// with ImGuiCond.FirstUseEver so they start docked but can be freely dragged.
///
-internal sealed class StudioInspector
+public sealed class StudioInspector
{
/// Currently selected element (set by tree-click or canvas-click in Inspect mode).
public UiElement? Selected { get; set; }
@@ -77,7 +77,7 @@ internal sealed class StudioInspector
///
public bool InteractMode { get; set; } = true;
- // ── Toolbar ───────────────────────────────────────────────────────────────────
+ // ── Toolbar ───────────────────────────────────────────────────────────────────
///
/// Draw the "Studio" toolbar window (top strip) containing a slug combo-box and
@@ -124,7 +124,7 @@ internal sealed class StudioInspector
return result;
}
- // ── Canvas ────────────────────────────────────────────────────────────────────
+ // ── Canvas ────────────────────────────────────────────────────────────────────
///
/// Draw the "Canvas" ImGui window containing the panel FBO texture and return all
@@ -133,13 +133,13 @@ internal sealed class StudioInspector
/// Coordinate mapping: After ImGui.Image, GetItemRectMin()
/// returns the actual screen-space top-left of the drawn image (accounting for the
/// sub-window title bar, padding, and scrolling). Subtracting that from the raw ImGui
- /// mouse position gives panel-local pixels directly — no scale factor because the
+ /// mouse position gives panel-local pixels directly — no scale factor because the
/// image is drawn 1:1.
///
- /// V-flip — no extra Y inversion: we pass uv0=(0,1) / uv1=(1,0) so the
+ /// V-flip — no extra Y inversion: we pass uv0=(0,1) / uv1=(1,0) so the
/// GL bottom-left origin is flipped to top-left on screen. After the flip, screen
/// row 0 = panel Y 0 (top of the UI), so the computed Y already matches UiRoot's
- /// top-left origin — do NOT flip Y again.
+ /// top-left origin — do NOT flip Y again.
///
/// If is non-null a bright-green 2-pixel outline is
/// drawn over it using the window draw list.
@@ -163,7 +163,7 @@ internal sealed class StudioInspector
// This is what lets us translate raw mouse screen coords into panel-local pixels.
var rectMin = ImGui.GetItemRectMin();
- // ── Selection highlight ───────────────────────────────────────────────
+ // ── Selection highlight ───────────────────────────────────────────────
var el = Selected;
if (el is not null && el.Width > 0f && el.Height > 0f)
{
@@ -176,7 +176,7 @@ internal sealed class StudioInspector
0f, ImDrawFlags.None, 2f);
}
- // ── Gather canvas mouse events ────────────────────────────────────────
+ // ── Gather canvas mouse events ────────────────────────────────────────
// IsItemHovered is true when the mouse is over the Image item (not just the window).
bool hovered = ImGui.IsItemHovered();
int mx = 0, my = 0;
@@ -187,7 +187,7 @@ internal sealed class StudioInspector
{
var mousePos = ImGui.GetMousePos();
// Panel-local pixel = mouse offset from the image's screen-space top-left.
- // Scale is 1:1 (image drawn at full FBO size). Y needs no extra flip — see summary.
+ // Scale is 1:1 (image drawn at full FBO size). Y needs no extra flip — see summary.
int ix = (int)(mousePos.X - rectMin.X);
int iy = (int)(mousePos.Y - rectMin.Y);
// Clamp to image bounds (mouse can be on the image edge pixel).
@@ -202,7 +202,7 @@ internal sealed class StudioInspector
}
else
{
- // Mouse is over ImGui chrome (title bar, padding) adjacent to image — not over the panel.
+ // Mouse is over ImGui chrome (title bar, padding) adjacent to image — not over the panel.
hovered = false;
}
}
@@ -211,7 +211,7 @@ internal sealed class StudioInspector
return new CanvasInputEvent(hovered, mx, my, leftDown, leftUp, scroll);
}
- // ── Tree ──────────────────────────────────────────────────────────────────────
+ // ── Tree ──────────────────────────────────────────────────────────────────────
/// Draw the "Tree" ImGui window. Clicking a node sets .
public void DrawTree(UiElement root, int windowX, int windowY, int windowW, int windowH)
@@ -252,7 +252,7 @@ internal sealed class StudioInspector
}
}
- // ── Properties ───────────────────────────────────────────────────────────────
+ // ── Properties ───────────────────────────────────────────────────────────────
/// Draw the "Properties" ImGui window for .
public void DrawProperties(int windowX, int windowY, int windowW, int windowH)
diff --git a/src/AcDream.App/Studio/StudioOptions.cs b/src/AcDream.App/Studio/StudioOptions.cs
index 1af8c075..8e2a3c2e 100644
--- a/src/AcDream.App/Studio/StudioOptions.cs
+++ b/src/AcDream.App/Studio/StudioOptions.cs
@@ -1,11 +1,11 @@
-namespace AcDream.App.Studio;
+namespace AcDream.App.Studio;
///
/// Parsed options for the acdream UI Studio standalone tool.
/// Constructed by from the command-line tokens that follow
/// the ui-studio dispatch token.
///
-internal sealed record StudioOptions(
+public sealed record StudioOptions(
string DatDir,
uint? LayoutId,
string? MarkupPath,
@@ -24,11 +24,11 @@ internal sealed record StudioOptions(
/// --layout 0xNNNN: hex LayoutDesc dat id to preview.
/// --markup <path>: path to a KSML markup file (Task 6, unsupported for now).
/// --dump <slug>: load a panel from the retail UI dump JSON by slug
- /// (e.g. inventory, radar, toolbar). Static mockup — no controllers.
+ /// (e.g. inventory, radar, toolbar). Static mockup — no controllers.
/// --dump-file <path>: override the default dump file path
/// (docs/research/2026-06-25-retail-ui-layout-dump.json from the solution root).
/// Only meaningful when --dump is also given.
- /// --screenshot <path>: headless mode — render the loaded panel to a PNG
+ /// --screenshot <path>: headless mode — render the loaded panel to a PNG
/// at and exit without showing an interactive window.
/// Combines with --dump or --layout.
/// When neither --layout, --markup, nor --dump is given the
@@ -96,7 +96,7 @@ internal sealed record StudioOptions(
if (string.IsNullOrWhiteSpace(datDir))
throw new InvalidOperationException(
- "ui-studio: dat directory required — pass as first arg or set ACDREAM_DAT_DIR.");
+ "ui-studio: dat directory required — pass as first arg or set ACDREAM_DAT_DIR.");
// Default layout: vitals (0x2100006C), unless a dump slug or markup is requested.
if (!mockup && layoutId is null && markupPath is null && dumpSlug is null)
diff --git a/src/AcDream.App/Studio/StudioWindow.cs b/src/AcDream.App/Studio/StudioWindow.cs
index 4817f943..be43d853 100644
--- a/src/AcDream.App/Studio/StudioWindow.cs
+++ b/src/AcDream.App/Studio/StudioWindow.cs
@@ -1,4 +1,4 @@
-using System.Numerics;
+using System.Numerics;
using AcDream.Content;
using AcDream.App.Platform;
using AcDream.App.Audio;
@@ -26,18 +26,18 @@ namespace AcDream.App.Studio;
///
/// Task 3 adds an ImGui IDE on top of the panel FBO:
///
-/// - Canvas pane — the panel rendered off-screen via .
-/// - Tree pane — the element hierarchy; click-to-select.
-/// - Properties pane — geometry/anchors/flags of the selected element.
-/// - Click-to-inspect — a left-click in the canvas selects the topmost
+///
- Canvas pane — the panel rendered off-screen via .
+/// - Tree pane — the element hierarchy; click-to-select.
+/// - Properties pane — geometry/anchors/flags of the selected element.
+/// - Click-to-inspect — a left-click in the canvas selects the topmost
/// element under the cursor via .
///
///
-/// The window is intentionally thin: no game world, no physics, no streaming —
+/// The window is intentionally thin: no game world, no physics, no streaming —
/// just GL + UiHost + the layout under test, identical to how the panel
/// appears inside GameWindow.
///
-internal sealed class StudioWindow : IDisposable
+public sealed class StudioWindow : IDisposable
{
private readonly StudioOptions _opts;
private readonly ApplicationPathSet _applicationPaths;
@@ -63,7 +63,7 @@ internal sealed class StudioWindow : IDisposable
private string? _dumpFile; // resolved dump file path (once, in OnLoad)
private IReadOnlyList _dumpSlugs = Array.Empty(); // all slugs from the dump
- // Task 4: sample data table — built once in OnLoad and kept alive for the window's lifetime
+ // Task 4: sample data table — built once in OnLoad and kept alive for the window's lifetime
// so the controller subscriptions (ObjectAdded/ObjectMoved etc.) fire correctly.
private AcDream.Core.Items.ClientObjectTable? _objects;
private IRetainedPanelController? _fixtureController;
@@ -114,7 +114,7 @@ internal sealed class StudioWindow : IDisposable
{
_platformServices.ConfigureWindowBackend();
// Resolve quality settings the same way GameWindow.Run() does
- // (SettingsStore → QualitySettings.From → WithEnvOverrides).
+ // (SettingsStore → QualitySettings.From → WithEnvOverrides).
var startupStore = new AcDream.UI.Abstractions.Panels.Settings.SettingsStore(
_applicationPaths.SettingsFile);
var startupDisplay = startupStore.LoadDisplay();
@@ -131,7 +131,7 @@ internal sealed class StudioWindow : IDisposable
ContextFlags.ForwardCompatible,
new APIVersion(4, 3)),
VSync = false,
- // MSAA from quality preset — must be baked into the GL context at creation.
+ // MSAA from quality preset — must be baked into the GL context at creation.
Samples = startupQuality.MsaaSamples,
PreferredStencilBufferBits = 8,
// Headless screenshot mode: hide the window so no desktop flash occurs.
@@ -195,7 +195,7 @@ internal sealed class StudioWindow : IDisposable
_dats = RuntimeDatCollectionFactory.OpenReadOnly(_opts.DatDir);
- // Build QualitySettings for RenderBootstrap (same as Run() above — re-read
+ // Build QualitySettings for RenderBootstrap (same as Run() above — re-read
// after the GL context is confirmed, mirroring GameWindow.OnLoad).
var store = new AcDream.UI.Abstractions.Panels.Settings.SettingsStore(
_applicationPaths.SettingsFile);
@@ -265,7 +265,7 @@ internal sealed class StudioWindow : IDisposable
// Task 4: populate the panel with sample data via production controllers,
// so inventory / vitals / toolbar panels render with plausible content.
- // Dump source is static — no FixtureProvider needed.
+ // Dump source is static — no FixtureProvider needed.
if (_opts.DumpSlug is null && _source.CurrentLayout is not null)
{
uint layoutId = _opts.LayoutId ?? 0x2100006Cu;
@@ -275,7 +275,7 @@ internal sealed class StudioWindow : IDisposable
}
}
- // Task 3: ImGui IDE — interactive mode only.
+ // Task 3: ImGui IDE — interactive mode only.
// Headless screenshot mode needs only PanelFbo; ImGui/inspector/input are skipped.
_panelFbo = new PanelFbo(_gl);
if (_opts.ScreenshotPath is null)
@@ -337,14 +337,14 @@ internal sealed class StudioWindow : IDisposable
try
{
- // ── HEADLESS SCREENSHOT PATH ──────────────────────────────────────────────
+ // ── HEADLESS SCREENSHOT PATH ──────────────────────────────────────────────
if (_opts.ScreenshotPath is not null)
{
if (_screenshotDone) return; // fire exactly once
_screenshotDone = true;
// Pick render size from the loaded root's bounds (clamped to sane limits).
- // Fall back to 1280×720 when the root has no explicit size.
+ // Fall back to 1280×720 when the root has no explicit size.
int w = 1280, h = 720;
if (!_opts.Mockup && _panelRoot is not null)
{
@@ -372,7 +372,7 @@ internal sealed class StudioWindow : IDisposable
return;
}
- // Flip rows vertically: FBO bottom-left → PNG top-left.
+ // Flip rows vertically: FBO bottom-left → PNG top-left.
int stride = w * 4;
byte[] flipped = new byte[pixels.Length];
for (int row = 0; row < h; row++)
@@ -394,7 +394,7 @@ internal sealed class StudioWindow : IDisposable
return;
}
- // ── INTERACTIVE PATH ──────────────────────────────────────────────────────
+ // ── INTERACTIVE PATH ──────────────────────────────────────────────────────
if (_opts.Mockup)
{
var mockupGl = _stack.Gl;
@@ -415,12 +415,12 @@ internal sealed class StudioWindow : IDisposable
int iw = _window!.Size.X;
int ih = _window!.Size.Y;
- // 1. Tick the UI widgets (OnRender's own dt — Update + Render fire with the same delta).
+ // 1. Tick the UI widgets (OnRender's own dt — Update + Render fire with the same delta).
_stack.UiHost.Tick(dt);
// 2. Render the panel into the off-screen FBO; get the color texture.
// The FBO is the same logical size as the window, so element rects map 1:1 to
- // FBO pixels — no scale factor needed when displaying the canvas at full size.
+ // FBO pixels — no scale factor needed when displaying the canvas at full size.
uint panelTex = _panelFbo.Render(iw, ih, _stack.UiHost);
// 3. Clear the window back-buffer (the dark ImGui background shows behind panes).
@@ -430,8 +430,8 @@ internal sealed class StudioWindow : IDisposable
// 4. Begin the ImGui frame.
_imgui.BeginFrame((float)dt);
- // ── Layout constants (fixed pane arrangement, FirstUseEver) ──────────────
- // MenuBar: always-on-top main menu bar (~22px) — panel picker lives here so it
+ // ── Layout constants (fixed pane arrangement, FirstUseEver) ──────────────
+ // MenuBar: always-on-top main menu bar (~22px) — panel picker lives here so it
// is never covered by the floating panes (replaces the old 40px toolbar).
// Tree: 280px wide on the left, below menu bar.
// Canvas: centre strip between tree and properties.
@@ -445,7 +445,7 @@ internal sealed class StudioWindow : IDisposable
int paneY = kMenuBarH;
int paneH = Math.Max(1, ih - kMenuBarH);
- // 5. Main menu bar — panel picker combo pinned to the window top.
+ // 5. Main menu bar — panel picker combo pinned to the window top.
// BeginMainMenuBar returns true when the bar is visible (always is); the combo
// inside it is always-on-top and is never occluded by Tree/Canvas/Props panes.
string? pickedSlug = null;
@@ -471,7 +471,7 @@ internal sealed class StudioWindow : IDisposable
if (pickedSlug is not null)
LoadDumpPanel(pickedSlug);
- // 6. Canvas pane — show the FBO texture; gather canvas mouse events.
+ // 6. Canvas pane — show the FBO texture; gather canvas mouse events.
var canvasEvt = default(CanvasInputEvent);
if (panelTex != 0)
canvasEvt = _inspector.DrawCanvas(
@@ -501,10 +501,10 @@ internal sealed class StudioWindow : IDisposable
if (_inspector.InteractMode)
{
- // ── Interact: live panel interaction ──────────────────────────────
+ // ── Interact: live panel interaction ──────────────────────────────
if (canvasEvt.LeftDown)
{
- Console.WriteLine($"[studio] canvas click → panel ({mx}, {my})");
+ Console.WriteLine($"[studio] canvas click → panel ({mx}, {my})");
root.OnMouseDown(UiMouseButton.Left, mx, my);
}
if (canvasEvt.LeftUp)
@@ -514,7 +514,7 @@ internal sealed class StudioWindow : IDisposable
}
else
{
- // ── Inspect: click selects an element in the tree ─────────────────
+ // ── Inspect: click selects an element in the tree ─────────────────
if (canvasEvt.LeftDown)
{
var hit = root.Pick(mx, my);
@@ -634,7 +634,7 @@ internal sealed class StudioWindow : IDisposable
_imgui = null;
_panelFbo = null;
// If OnClosing wasn't called (e.g. an exception before Run() completed), dispose the FULL
- // stack anyway — the review flagged that disposing only UiHost here leaked the rest.
+ // stack anyway — the review flagged that disposing only UiHost here leaked the rest.
_stack?.Dispose();
_dats?.Dispose();
_audio?.Dispose();
diff --git a/src/AcDream.App/Studio/UiDumpModel.cs b/src/AcDream.App/Studio/UiDumpModel.cs
index 174e34a5..c33e4b9e 100644
--- a/src/AcDream.App/Studio/UiDumpModel.cs
+++ b/src/AcDream.App/Studio/UiDumpModel.cs
@@ -1,10 +1,10 @@
-using System.Text.Json;
+using System.Text.Json;
using System.Text.Json.Serialization;
namespace AcDream.App.Studio;
-// ─────────────────────────────────────────────────────────────────────────────
-// UiDumpModel — POCOs for docs/research/2026-06-25-retail-ui-layout-dump.json
+// ─────────────────────────────────────────────────────────────────────────────
+// UiDumpModel — POCOs for docs/research/2026-06-25-retail-ui-layout-dump.json
//
// Schema (v1):
// { "version":1, "panels":[ { "id":int, "slug":string, "title":string,
@@ -25,10 +25,10 @@ namespace AcDream.App.Studio;
// Rect coordinates are ABSOLUTE (screen-space origin = panel's design position
// in retail layout, NOT relative to the parent). DumpLayout.Load converts them
// to parent-relative when building the UiElement tree.
-// ─────────────────────────────────────────────────────────────────────────────
+// ─────────────────────────────────────────────────────────────────────────────
/// Top-level container for the retail UI layout dump.
-internal sealed class UiDump
+public sealed class UiDump
{
[JsonPropertyName("version")]
public int Version { get; set; }
@@ -38,7 +38,7 @@ internal sealed class UiDump
}
/// One panel (window) exported from the retail UI.
-internal sealed class DumpPanel
+public sealed class DumpPanel
{
[JsonPropertyName("id")]
public long Id { get; set; }
@@ -66,7 +66,7 @@ internal sealed class DumpPanel
}
/// One element node within a panel's traversal list.
-internal sealed class DumpNode
+public sealed class DumpNode
{
[JsonPropertyName("traversal_index")]
public int TraversalIndex { get; set; }
@@ -96,8 +96,8 @@ internal sealed class DumpNode
public DumpStateSet StateSet { get; set; } = new();
}
-/// Absolute screen-space rect (see comment above — must subtract parent rect for UiElement).
-internal sealed class DumpRect
+/// Absolute screen-space rect (see comment above — must subtract parent rect for UiElement).
+public sealed class DumpRect
{
[JsonPropertyName("x")]
public float X { get; set; }
@@ -112,8 +112,8 @@ internal sealed class DumpRect
public float Height { get; set; }
}
-/// State set for a node — default image plus per-state overrides.
-internal sealed class DumpStateSet
+/// State set for a node — default image plus per-state overrides.
+public sealed class DumpStateSet
{
[JsonPropertyName("default_image")]
public DumpImage? DefaultImage { get; set; }
@@ -123,7 +123,7 @@ internal sealed class DumpStateSet
}
/// Image reference (RenderSurface dat id + optional separate alpha surface).
-internal sealed class DumpImage
+public sealed class DumpImage
{
[JsonPropertyName("image_id")]
public long ImageId { get; set; }
@@ -133,7 +133,7 @@ internal sealed class DumpImage
}
/// A named state override.
-internal sealed class DumpState
+public sealed class DumpState
{
[JsonPropertyName("state_id")]
public int StateId { get; set; }
@@ -142,14 +142,14 @@ internal sealed class DumpState
public DumpImage Image { get; set; } = new();
}
-// ─────────────────────────────────────────────────────────────────────────────
+// ─────────────────────────────────────────────────────────────────────────────
// Helper statics
-// ─────────────────────────────────────────────────────────────────────────────
+// ─────────────────────────────────────────────────────────────────────────────
///
/// Parsing helpers for the retail UI dump JSON.
///
-internal static class UiDumpModel
+public static class UiDumpModel
{
private static readonly JsonSerializerOptions _opts = new()
{
diff --git a/src/AcDream.App/UI/ClientCommandController.cs b/src/AcDream.App/UI/ClientCommandController.cs
index 2e1510ac..08eef64d 100644
--- a/src/AcDream.App/UI/ClientCommandController.cs
+++ b/src/AcDream.App/UI/ClientCommandController.cs
@@ -1,4 +1,4 @@
-using AcDream.Core.Physics;
+using AcDream.Core.Physics;
using AcDream.Core.Ui;
using AcDream.Core.Social;
using AcDream.UI.Abstractions;
@@ -10,9 +10,9 @@ namespace AcDream.App.UI;
/// remain backend- and network-agnostic; this controller owns the boundary
/// between verified command behavior and live session/UI services.
///
-internal sealed class ClientCommandController
+public sealed class ClientCommandController
{
- internal sealed record Bindings(
+ public sealed record Bindings(
Action TeleportToLifestone,
Action TeleportToMarketplace,
Action TeleportToPkArena,
diff --git a/src/AcDream.App/UI/ControlsIni.cs b/src/AcDream.App/UI/ControlsIni.cs
index 42e03e74..2812d696 100644
--- a/src/AcDream.App/UI/ControlsIni.cs
+++ b/src/AcDream.App/UI/ControlsIni.cs
@@ -1,16 +1,16 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.Globalization;
using System.Numerics;
namespace AcDream.App.UI;
///
-/// Minimal reader for retail's controls.ini — a flat INI with one
+/// Minimal reader for retail's controls.ini — a flat INI with one
/// [section] per element type. Colors are #AARRGGBB (alpha
/// first). Optional: a missing file yields an empty sheet (callers fall back
-/// to hardcoded defaults). See the D.2b spec §7.
+/// to hardcoded defaults). See the D.2b spec §7.
///
-internal sealed class ControlsIni
+public sealed class ControlsIni
{
private readonly Dictionary> _sections;
diff --git a/src/AcDream.App/UI/CursorFeedbackController.cs b/src/AcDream.App/UI/CursorFeedbackController.cs
index fbfa3597..babab5d6 100644
--- a/src/AcDream.App/UI/CursorFeedbackController.cs
+++ b/src/AcDream.App/UI/CursorFeedbackController.cs
@@ -1,4 +1,4 @@
-using AcDream.Core.Combat;
+using AcDream.Core.Combat;
using AcDream.Runtime.Gameplay;
namespace AcDream.App.UI;
@@ -62,7 +62,7 @@ public enum RetailGlobalCursorKind
TargetInvalid,
}
-internal readonly record struct CursorFeedback(
+public readonly record struct CursorFeedback(
CursorFeedbackKind Kind,
UiCursorMedia Cursor = default,
RetailGlobalCursorKind GlobalKind = RetailGlobalCursorKind.Default)
@@ -72,7 +72,7 @@ internal readonly record struct CursorFeedback(
public readonly record struct CursorFeedbackSnapshot(
object? DragPayload = null,
- DragAcceptState DragAccept = DragAcceptState.None,
+ UiItemSlot.DragAcceptState DragAccept = UiItemSlot.DragAcceptState.None,
ResizeEdges ActiveResizeEdges = ResizeEdges.None,
ResizeEdges HoverResizeEdges = ResizeEdges.None,
bool WindowMoveActive = false,
@@ -85,7 +85,7 @@ public readonly record struct CursorFeedbackSnapshot(
RetailCursorTargetMode TargetMode = RetailCursorTargetMode.None,
CombatMode CombatMode = CombatMode.NonCombat);
-internal sealed class CursorFeedbackController
+public sealed class CursorFeedbackController
{
private readonly ItemInteractionController? _itemInteraction;
private readonly Func? _worldTargetProvider;
@@ -110,8 +110,8 @@ internal sealed class CursorFeedbackController
UiElement? hover = root.Pick(root.MouseX, root.MouseY);
// Retail UpdateCursorState (0x00564630) keys the target-mode cursor off
- // the SmartBox found object — the WORLD entity under the cursor. A UI
- // window occludes the world (no found object → pending). The one
+ // the SmartBox found object — the WORLD entity under the cursor. A UI
+ // window occludes the world (no found object → pending). The one
// UI-side source retail-style cells contribute is an occupied item
// slot's own item.
RetailCursorTargetMode targetMode = ModeFromInteraction(_itemInteraction);
@@ -127,7 +127,7 @@ internal sealed class CursorFeedbackController
var snapshot = new CursorFeedbackSnapshot(
DragPayload: root.DragPayload,
- DragAccept: FindHoveredItemSlot(hover)?.DragAcceptVisual ?? DragAcceptState.None,
+ DragAccept: FindHoveredItemSlot(hover)?.DragAcceptVisual ?? UiItemSlot.DragAcceptState.None,
ActiveResizeEdges: root.ActiveResizeEdges,
HoverResizeEdges: root.HoverResizeEdges,
WindowMoveActive: root.IsWindowMoveActive,
@@ -168,8 +168,8 @@ internal sealed class CursorFeedbackController
{
return snapshot.DragAccept switch
{
- DragAcceptState.Accept => CursorFeedbackKind.DragAccept,
- DragAcceptState.Reject => CursorFeedbackKind.DragReject,
+ UiItemSlot.DragAcceptState.Accept => CursorFeedbackKind.DragAccept,
+ UiItemSlot.DragAcceptState.Reject => CursorFeedbackKind.DragReject,
_ => CursorFeedbackKind.Drag,
};
}
@@ -197,10 +197,10 @@ internal sealed class CursorFeedbackController
}
// Retail UpdateCursorState (0x00564630), TARGET_MODE 3: no found
- // object → the 0x27 four-arrows pending cursor — INCLUDING over
+ // object → the 0x27 four-arrows pending cursor — INCLUDING over
// UI chrome. Valid/invalid exist only with a target under the
- // cursor. (The earlier HoverUi → Invalid arm was a non-retail
- // invention — 2026-07-03 visual gate.)
+ // cursor. (The earlier HoverUi → Invalid arm was a non-retail
+ // invention — 2026-07-03 visual gate.)
return CursorFeedbackKind.TargetPending;
}
diff --git a/src/AcDream.App/UI/GameplayConfirmationController.cs b/src/AcDream.App/UI/GameplayConfirmationController.cs
index 2ef0ebf7..b23686d0 100644
--- a/src/AcDream.App/UI/GameplayConfirmationController.cs
+++ b/src/AcDream.App/UI/GameplayConfirmationController.cs
@@ -1,4 +1,4 @@
-using AcDream.App.UI.Layout;
+using AcDream.App.UI.Layout;
using AcDream.Core.Net.Messages;
namespace AcDream.App.UI;
@@ -9,7 +9,7 @@ namespace AcDream.App.UI;
/// completion; this semantic owner retains the server type/context and sends the
/// matching confirmation response.
///
-internal sealed class GameplayConfirmationController : IDisposable
+public sealed class GameplayConfirmationController : IDisposable
{
private readonly RetailDialogFactory _dialogs;
private readonly Action _sendResponse;
diff --git a/src/AcDream.App/UI/IItemListDragHandler.cs b/src/AcDream.App/UI/IItemListDragHandler.cs
index 64543527..3c149142 100644
--- a/src/AcDream.App/UI/IItemListDragHandler.cs
+++ b/src/AcDream.App/UI/IItemListDragHandler.cs
@@ -1,11 +1,11 @@
-namespace AcDream.App.UI;
+namespace AcDream.App.UI;
///
/// Visual result of a drag-over query. Retail handlers can consume a drag-over
/// message without setting either accept or reject art; shortcut aliases over a
/// physical item list use that neutral path.
///
-internal enum ItemDragAcceptance
+public enum ItemDragAcceptance
{
None,
Accept,
@@ -18,13 +18,13 @@ internal enum ItemDragAcceptance
/// (RegisterItemListDragHandler, decomp 230461; confirmed acclient
/// 0x004a539e + the gmToolbarUI block 0x004bdd89).
/// decides the neutral/accept/reject overlay only (advisory).
-/// is authoritative — it performs the action, or
+/// is authoritative — it performs the action, or
/// no-ops to reject.
///
-internal interface IItemListDragHandler
+public interface IItemListDragHandler
{
- /// The drag STARTED from a cell in this list — retail's RecvNotice_ItemListBeginDrag
- /// → RemoveShortcut (decomp 0x004bd930/0x004bd450): the handler removes the lifted item from its
+ /// The drag STARTED from a cell in this list — retail's RecvNotice_ItemListBeginDrag
+ /// → RemoveShortcut (decomp 0x004bd930/0x004bd450): the handler removes the lifted item from its
/// model + wire so the source slot empties immediately. The item is "in hand" until
/// HandleDropRelease (place) or the drag ends off-target (stays removed). No restore on cancel.
void OnDragLift(UiItemList sourceList, UiItemSlot sourceCell, ItemDragPayload payload);
diff --git a/src/AcDream.App/UI/IRetainedPanelController.cs b/src/AcDream.App/UI/IRetainedPanelController.cs
index d75d0d33..4ea2ae40 100644
--- a/src/AcDream.App/UI/IRetainedPanelController.cs
+++ b/src/AcDream.App/UI/IRetainedPanelController.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
namespace AcDream.App.UI;
@@ -7,7 +7,7 @@ namespace AcDream.App.UI;
/// Implementations own panel-specific subscriptions and reactions; the window
/// manager owns visibility, focus, capture, geometry, and teardown ordering.
///
-internal interface IRetainedPanelController : IDisposable
+public interface IRetainedPanelController : IDisposable
{
/// Called once for each hidden-to-shown transition.
void OnShown() { }
diff --git a/src/AcDream.App/UI/IRetainedWindowStateController.cs b/src/AcDream.App/UI/IRetainedWindowStateController.cs
index 6ed1d086..cdb129bc 100644
--- a/src/AcDream.App/UI/IRetainedWindowStateController.cs
+++ b/src/AcDream.App/UI/IRetainedWindowStateController.cs
@@ -1,7 +1,7 @@
-namespace AcDream.App.UI;
+namespace AcDream.App.UI;
/// Panel state that is not completely described by outer-frame bounds.
-internal readonly record struct RetainedWindowState(
+public readonly record struct RetainedWindowState(
bool Collapsed = false,
bool Maximized = false,
float? PersistedTop = null,
@@ -11,7 +11,7 @@ internal readonly record struct RetainedWindowState(
/// Optional state seam used by retained-window persistence. Bounds are restored
/// first; the controller then reapplies collapsed/maximized presentation.
///
-internal interface IRetainedWindowStateController
+public interface IRetainedWindowStateController
{
RetainedWindowState CaptureWindowState();
void RestoreWindowState(RetainedWindowState state);
diff --git a/src/AcDream.App/UI/IUiDatStateful.cs b/src/AcDream.App/UI/IUiDatStateful.cs
index 6fb9768a..7a86e9b8 100644
--- a/src/AcDream.App/UI/IUiDatStateful.cs
+++ b/src/AcDream.App/UI/IUiDatStateful.cs
@@ -1,16 +1,16 @@
-namespace AcDream.App.UI;
+namespace AcDream.App.UI;
///
/// Narrow numeric-state bridge for widgets imported from retail LayoutDesc data.
/// Controllers use retail state ids without knowing how names/media are stored.
///
-internal interface IUiDatStateful
+public interface IUiDatStateful
{
uint ActiveRetailStateId { get; }
bool TrySetRetailState(uint stateId);
}
-internal static class RetailUiStateIds
+public static class RetailUiStateIds
{
public const uint Closed = 11u;
public const uint Open = 12u;
diff --git a/src/AcDream.App/UI/IUiGlobalTimeListener.cs b/src/AcDream.App/UI/IUiGlobalTimeListener.cs
index 54d5a3f1..36680887 100644
--- a/src/AcDream.App/UI/IUiGlobalTimeListener.cs
+++ b/src/AcDream.App/UI/IUiGlobalTimeListener.cs
@@ -1,10 +1,10 @@
-namespace AcDream.App.UI;
+namespace AcDream.App.UI;
///
/// Opt-in recipient of retail UI global message 3, broadcast once per UI frame
/// after tooltip deadline processing.
///
-internal interface IUiGlobalTimeListener
+public interface IUiGlobalTimeListener
{
void OnGlobalUiTime(double nowSeconds);
}
diff --git a/src/AcDream.App/UI/IUiViewportRenderer.cs b/src/AcDream.App/UI/IUiViewportRenderer.cs
index 0a2a182b..13023078 100644
--- a/src/AcDream.App/UI/IUiViewportRenderer.cs
+++ b/src/AcDream.App/UI/IUiViewportRenderer.cs
@@ -1,10 +1,10 @@
-namespace AcDream.App.UI;
+namespace AcDream.App.UI;
/// Renders a 3-D mini-scene into an off-screen buffer and returns the GL color-texture
/// handle. Called by the per-frame pre-UI hook (GameWindow), NOT from UiViewport.OnDraw. Implemented
/// by PaperdollViewportRenderer in AcDream.App.Rendering. Intra-App decoupling so the UI widget
/// doesn't depend on WbDrawDispatcher/GameWindow.
-internal interface IUiViewportRenderer
+public interface IUiViewportRenderer
{
/// Render at (width,height); return the color-texture GL handle, or 0 if nothing rendered.
uint Render(int width, int height);
diff --git a/src/AcDream.App/UI/IconComposer.cs b/src/AcDream.App/UI/IconComposer.cs
index 196678da..77dacf2b 100644
--- a/src/AcDream.App/UI/IconComposer.cs
+++ b/src/AcDream.App/UI/IconComposer.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Numerics;
using AcDream.App.Rendering;
@@ -17,9 +17,9 @@ namespace AcDream.App.UI;
/// DBCache::GetDIDFromEnum (0x413940). Each layer is a 0x06 RenderSurface decoded
/// DIRECTLY (the D.2b RenderSurface-vs-Surface rule).
///
-/// Layer order (bottom → top), matching retail:
+/// Layer order (bottom → top), matching retail:
/// 1. type-default underlay (OPAQUE backing; resolved via EnumIDMap 0x10000004 from
-/// the portal MasterMap) —
+/// the portal MasterMap) —
/// 2. item custom underlay (e.g. "magic" tint strip)
/// 3. base icon
/// 4. item custom overlay (e.g. "enchanted" sparkle)
@@ -31,28 +31,28 @@ namespace AcDream.App.UI;
///
/// Composited textures are cached by their (typeUnderlay, underlay, base, overlay) tuple.
///
-internal sealed class IconComposer
+public sealed class IconComposer
{
private readonly IDatReaderWriter _dats;
private readonly TextureCache _cache;
- private readonly Dictionary<(uint, uint, uint, uint, uint), GpuTextureSlot> _byTuple = new();
+ private readonly Dictionary<(uint, uint, uint, uint, uint), uint> _byTuple = new();
private readonly Dictionary<(uint, uint, uint), ComposedIcon> _dragByTuple = new();
- private readonly Dictionary _spellIcons = new();
- private readonly Dictionary _componentIcons = new();
+ private readonly Dictionary _spellIcons = new();
+ private readonly Dictionary _componentIcons = new();
- private sealed record ComposedIcon(byte[] Rgba, int Width, int Height, GpuTextureSlot Texture);
+ private sealed record ComposedIcon(byte[] Rgba, int Width, int Height, uint Texture);
- // ── type-default underlay resolve (EnumIDMap 0x10000004) ─────────────────
- // Portal MasterMap (0x25000000) maps enum 0x10000004 → submap DID (0x25000008).
- // Submap maps index → 0x06 RenderSurface DID. index = LSB(itemType)+1, or 0x21.
- // Refs: IconData::RenderIcons 0058d214–0058d22c; DBCache::GetDIDFromEnum 0x413940.
+ // ── type-default underlay resolve (EnumIDMap 0x10000004) ─────────────────
+ // Portal MasterMap (0x25000000) maps enum 0x10000004 → submap DID (0x25000008).
+ // Submap maps index → 0x06 RenderSurface DID. index = LSB(itemType)+1, or 0x21.
+ // Refs: IconData::RenderIcons 0058d214–0058d22c; DBCache::GetDIDFromEnum 0x413940.
private EnumIDMap? _underlaySubMap;
private bool _underlayResolveTried;
private readonly Dictionary _underlayDidByIndex = new();
- // ── effect overlay resolve (EnumIDMap 0x10000005) ────────────────────────
- // Portal MasterMap (0x25000000) maps enum 0x10000005 → submap DID (0x25000009).
- // Submap maps index → 0x06 RenderSurface DID. index = LSB(effects)+1, fallback 0x21.
+ // ── effect overlay resolve (EnumIDMap 0x10000005) ────────────────────────
+ // Portal MasterMap (0x25000000) maps enum 0x10000005 → submap DID (0x25000009).
+ // Submap maps index → 0x06 RenderSurface DID. index = LSB(effects)+1, fallback 0x21.
// Refs: IconData::RenderIcons 0x0058d180 (effect path); the effect tile is a
// ReplaceColor tint SOURCE, not a blit layer (see RESOLVED doc, divergence DR-1).
private EnumIDMap? _effectSubMap;
@@ -68,13 +68,13 @@ internal sealed class IconComposer
///
/// Resolve the type-default underlay DID for via the
- /// two-level EnumIDMap chain (retail: IconData::RenderIcons 0058d214–0058d22c +
+ /// two-level EnumIDMap chain (retail: IconData::RenderIcons 0058d214–0058d22c +
/// DBCache::GetDIDFromEnum 0x413940).
///
/// index = LowestSetBit(itemType) + 1, or 0x21 when itemType has no bits set.
///
/// NOTE: retail RenderIcons (407546) has a special paperdoll IsThePlayer case
- /// that uses GetDIDByEnum(0x10000004, 7) + TYPE_CONTAINER for the player doll — that
+ /// that uses GetDIDByEnum(0x10000004, 7) + TYPE_CONTAINER for the player doll — that
/// path is out of scope here (paperdoll phase).
///
internal uint ResolveUnderlayDid(ItemType itemType)
@@ -97,7 +97,7 @@ internal sealed class IconComposer
uint masterDid = (uint)_dats.Portal.Db.Header.MasterMapId; // = 0x25000000
if (masterDid == 0) return;
if (!_dats.Portal.TryGet(masterDid, out var master)) return;
- if (!master.ClientEnumToID.TryGetValue(0x10000004u, out var subDid)) return; // → 0x25000008
+ if (!master.ClientEnumToID.TryGetValue(0x10000004u, out var subDid)) return; // → 0x25000008
if (_dats.Portal.TryGet(subDid, out var sub)) _underlaySubMap = sub;
}
@@ -105,7 +105,7 @@ internal sealed class IconComposer
/// Resolve the effect-overlay DID for via the EnumIDMap
/// 0x10000005 chain. index = LowestSetBit(effects)+1; if the entry is missing/zero,
/// retail falls back to index 0x21 (the solid-black tile). NOTE: the effect path has
- /// NO lsb==-1 pre-check (unlike the type underlay), so effects==0 → index 0 → miss →
+ /// NO lsb==-1 pre-check (unlike the type underlay), so effects==0 → index 0 → miss →
/// fallback. (Retail IconData::RenderIcons 0x0058d180.)
///
internal uint ResolveEffectDid(uint effects)
@@ -129,16 +129,16 @@ internal sealed class IconComposer
uint masterDid = (uint)_dats.Portal.Db.Header.MasterMapId; // = 0x25000000
if (masterDid == 0) return;
if (!_dats.Portal.TryGet(masterDid, out var master)) return;
- if (!master.ClientEnumToID.TryGetValue(0x10000005u, out var subDid)) return; // → 0x25000009
+ if (!master.ClientEnumToID.TryGetValue(0x10000005u, out var subDid)) return; // → 0x25000009
if (_dats.Portal.TryGet(subDid, out var sub)) _effectSubMap = sub;
}
///
/// Retail SurfaceWindow::ReplaceColor SURFACE overload (0x004415b0): for every
- /// pixel in that equals pure-white-opaque (RGBAColor(1,1,1,1) →
+ /// pixel in that equals pure-white-opaque (RGBAColor(1,1,1,1) →
/// 0xFFFFFFFF), copy the SAME (x,y) pixel from the source effect tile. This preserves
/// the effect tile's texture/gradient (NOT a flat color). Retail requires the source to
- /// cover the dest (it does — both are 32x32); out-of-range pixels are left unchanged.
+ /// cover the dest (it does — both are 32x32); out-of-range pixels are left unchanged.
/// Mutates in place.
///
internal static void ReplaceWhiteFromSurface(byte[] dst, int dw, int dh, byte[] src, int sw, int sh)
@@ -160,7 +160,7 @@ internal sealed class IconComposer
///
/// The decoded effect tile for (enum 0x10000005). The tile is
/// a 32x32 textured RenderSurface whose pixels ARE the per-effect coloring (blue=Magical,
- /// green=Poisoned, …; the 0x21 fallback is solid black). Retail copies it per-pixel into
+ /// green=Poisoned, …; the 0x21 fallback is solid black). Retail copies it per-pixel into
/// the icon's white pixels (gradient), so we need the whole tile, not a representative
/// color. Cached per DID.
///
@@ -224,27 +224,27 @@ internal sealed class IconComposer
/// effects==0 resolves to the 0x21 solid-black fallback tile, so pure-white pixels become
/// black (matching retail); magical items take the per-effect hue instead.
///
- public GpuTextureSlot GetIcon(ItemType itemType, uint iconId, uint underlayId, uint overlayId, uint effects)
+ public uint GetIcon(ItemType itemType, uint iconId, uint underlayId, uint overlayId, uint effects)
{
- if (iconId == 0) return GpuTextureSlot.Unassigned;
+ if (iconId == 0) return 0;
uint typeUnderlayDid = ResolveUnderlayDid(itemType);
var key = (typeUnderlayDid, iconId, underlayId, overlayId, effects);
if (_byTuple.TryGetValue(key, out var tex)) return tex;
- // Stage 1 — retail m_pDragIcon: base + custom overlay, then the effect recolor.
+ // Stage 1 — retail m_pDragIcon: base + custom overlay, then the effect recolor.
// RenderIcons retains this as a distinct Graphic because the cursor ghost must not
// carry the type/custom underlay that fills an inventory cell.
ComposedIcon? drag = GetOrCreateDragIcon(iconId, overlayId, effects);
- // Stage 2 — retail m_pIcon: type-default underlay (opaque) + custom underlay + drag.
+ // Stage 2 — retail m_pIcon: type-default underlay (opaque) + custom underlay + drag.
var layers = new List<(byte[] rgba, int w, int h)>();
AddLayer(layers, typeUnderlayDid);
AddLayer(layers, underlayId);
if (drag is not null) layers.Add((drag.Rgba, drag.Width, drag.Height));
- if (layers.Count == 0) return GpuTextureSlot.Unassigned;
+ if (layers.Count == 0) return 0;
var (rgba, w, h) = Compose(layers);
- GpuTextureSlot handle = _cache.UploadRgba8(rgba, w, h, nearest: true);
+ uint handle = _cache.UploadRgba8(rgba, w, h, nearest: true);
_byTuple[key] = handle;
return handle;
}
@@ -255,15 +255,13 @@ internal sealed class IconComposer
/// UIElement_ItemList::PrepareDragIcon obtains exactly this graphic through
/// ACCWeenieObject::GetDragIcon (0x004e2a50 / 0x0058d180).
///
- public GpuTextureSlot GetDragIcon(ItemType itemType, uint iconId, uint underlayId, uint overlayId, uint effects)
+ public uint GetDragIcon(ItemType itemType, uint iconId, uint underlayId, uint overlayId, uint effects)
{
// itemType/underlayId are deliberately unused: keeping the resolver signature identical
// to GetIcon lets every item-panel binding request the two retail siblings from one model.
_ = itemType;
_ = underlayId;
- return iconId == 0
- ? GpuTextureSlot.Unassigned
- : GetOrCreateDragIcon(iconId, overlayId, effects)?.Texture ?? GpuTextureSlot.Unassigned;
+ return iconId == 0 ? 0u : GetOrCreateDragIcon(iconId, overlayId, effects)?.Texture ?? 0u;
}
private ComposedIcon? GetOrCreateDragIcon(uint iconId, uint overlayId, uint effects)
@@ -277,11 +275,11 @@ internal sealed class IconComposer
if (dragLayers.Count == 0) return null;
var composed = Compose(dragLayers);
- // Effect recolor — ALWAYS, matching retail IconData::RenderIcons (0x0058d180):
+ // Effect recolor — ALWAYS, matching retail IconData::RenderIcons (0x0058d180):
// the effect tile (enum 0x10000005, lsb(effects)+1, fallback 0x21) is non-null
// even for effects==0 (the 0x21 SOLID-BLACK tile 0x060011C5). Retail's RenderIcons
// calls the SURFACE overload of SurfaceWindow::ReplaceColor (0x004415b0), copying
- // the textured effect tile per-pixel into the icon's pure-white pixels — so
+ // the textured effect tile per-pixel into the icon's pure-white pixels — so
// magical items take the tile's GRADIENT hue and mundane items go solid black.
// (Visually confirmed against retail 2026-06-17: the Energy Crystal's blue is a
// gradient, not a flat tint, and the no-mana scroll's edges are black.)
@@ -289,7 +287,7 @@ internal sealed class IconComposer
ReplaceWhiteFromSurface(composed.rgba, composed.w, composed.h,
tile.Rgba8, tile.Width, tile.Height);
- GpuTextureSlot texture = _cache.UploadRgba8(composed.rgba, composed.w, composed.h, nearest: true);
+ uint texture = _cache.UploadRgba8(composed.rgba, composed.w, composed.h, nearest: true);
var created = new ComposedIcon(composed.rgba, composed.w, composed.h, texture);
_dragByTuple[key] = created;
return created;
@@ -309,12 +307,12 @@ internal sealed class IconComposer
/// Retail ClientMagicSystem::CompositeSpellIcon (0x00567550): power-level
/// backing, spell art, reversed/normal recolor, then self/fellow overlay.
///
- public GpuTextureSlot GetSpellIcon(uint spellId)
+ public uint GetSpellIcon(uint spellId)
{
- if (_spellIcons.TryGetValue(spellId, out GpuTextureSlot cached)) return cached;
+ if (_spellIcons.TryGetValue(spellId, out uint cached)) return cached;
DatReaderWriter.DBObjs.SpellTable? table =
_dats.Get(0x0E00000Eu);
- if (table is null || !table.Spells.TryGetValue(spellId, out var spell)) return GpuTextureSlot.Unassigned;
+ if (table is null || !table.Spells.TryGetValue(spellId, out var spell)) return 0u;
uint power = spell.Components.Count == 0
? 0u
@@ -323,7 +321,7 @@ internal sealed class IconComposer
var layers = new List<(byte[] rgba, int w, int h)>();
AddLayer(layers, powerBacking);
AddLayer(layers, spell.Icon);
- if (layers.Count == 0) return GpuTextureSlot.Unassigned;
+ if (layers.Count == 0) return 0u;
var composed = Compose(layers);
uint tintIndex = (spell.Bitfield & DatReaderWriter.Enums.SpellIndex.Reversed) != 0
@@ -346,7 +344,7 @@ internal sealed class IconComposer
(overlay.Rgba8, overlay.Width, overlay.Height)]);
}
- GpuTextureSlot texture = _cache.UploadRgba8(composed.rgba, composed.w, composed.h, nearest: true);
+ uint texture = _cache.UploadRgba8(composed.rgba, composed.w, composed.h, nearest: true);
_spellIcons[spellId] = texture;
return texture;
}
@@ -355,11 +353,11 @@ internal sealed class IconComposer
/// Retail ClientMagicSystem::CompositeSpellComponentIcon (0x00567720).
/// Components use their raw DAT art with pure white replaced by black.
///
- public GpuTextureSlot GetSpellComponentIcon(uint iconId)
+ public uint GetSpellComponentIcon(uint iconId)
{
- if (iconId == 0u) return GpuTextureSlot.Unassigned;
- if (_componentIcons.TryGetValue(iconId, out GpuTextureSlot cached)) return cached;
- if (!TryDecode(iconId, out DecodedTexture icon)) return GpuTextureSlot.Unassigned;
+ if (iconId == 0u) return 0u;
+ if (_componentIcons.TryGetValue(iconId, out uint cached)) return cached;
+ if (!TryDecode(iconId, out DecodedTexture icon)) return 0u;
byte[] rgba = (byte[])icon.Rgba8.Clone();
for (int i = 0; i + 3 < rgba.Length; i += 4)
{
@@ -367,7 +365,7 @@ internal sealed class IconComposer
continue;
rgba[i] = rgba[i + 1] = rgba[i + 2] = 0;
}
- GpuTextureSlot texture = _cache.UploadRgba8(rgba, icon.Width, icon.Height, nearest: true);
+ uint texture = _cache.UploadRgba8(rgba, icon.Width, icon.Height, nearest: true);
_componentIcons[iconId] = texture;
return texture;
}
diff --git a/src/AcDream.App/UI/ItemDragPayload.cs b/src/AcDream.App/UI/ItemDragPayload.cs
index eb0ff2c1..f4f93234 100644
--- a/src/AcDream.App/UI/ItemDragPayload.cs
+++ b/src/AcDream.App/UI/ItemDragPayload.cs
@@ -1,25 +1,25 @@
-using AcDream.Core.Items;
+using AcDream.Core.Items;
namespace AcDream.App.UI;
///
-/// Where a dragged item came from — the retail InqDropIconInfo flag
+/// Where a dragged item came from — the retail InqDropIconInfo flag
/// distinction (flags & 0xE == 0 fresh-from-inventory vs
/// flags & 4 within-list reorder) expressed as a typed enum. The drop
/// handler maps SourceKind + target back to the fresh-vs-reorder decision.
/// Decomp anchors: gmToolbarUI 0x004bd162 / 0x004bd1af; InqDropIconInfo 230533.
///
-internal enum ItemDragSource { Inventory, ShortcutBar, Equipment, Ground }
+public enum ItemDragSource { Inventory, ShortcutBar, Equipment, Ground }
///
/// Snapshot of a drag-in-progress, taken at drag-begin (so a server move arriving
/// mid-drag can't mutate it under us). Port of retail's m_dragElement +
/// InqDropIconInfo out-params (objId/container/flags, decomp 230533).
/// SourceContainer is intentionally NOT stored: the handler resolves the
-/// LIVE container via ClientObjectTable.Get(ObjId).ContainerId at drop — the
+/// LIVE container via ClientObjectTable.Get(ObjId).ContainerId at drop — the
/// same container id retail reads off the dragged element, single source of truth.
///
-internal sealed record ItemDragPayload(
+public sealed record ItemDragPayload(
uint ObjId, // dragged weenie guid (retail itemID, +0x5FC)
ItemDragSource SourceKind, // what kind of slot it left
int SourceSlot, // the source cell's SlotIndex (retail m_lastShortcutNumDragged)
diff --git a/src/AcDream.App/UI/ItemInteractionController.cs b/src/AcDream.App/UI/ItemInteractionController.cs
index 68f6cd0e..db968d08 100644
--- a/src/AcDream.App/UI/ItemInteractionController.cs
+++ b/src/AcDream.App/UI/ItemInteractionController.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using AcDream.Core.Combat;
using AcDream.Core.Items;
using AcDream.Runtime.Gameplay;
@@ -6,14 +6,14 @@ using AcDream.Runtime.Gameplay;
namespace AcDream.App.UI;
/// Result of offering a primary click to active item-target mode.
-internal enum ItemPrimaryClickResult
+public enum ItemPrimaryClickResult
{
NotActive,
ConsumedSuccess,
ConsumedRejected,
}
-internal readonly record struct PendingBackpackPlacement(
+public readonly record struct PendingBackpackPlacement(
ulong Token,
uint ItemId,
uint ContainerId,
@@ -25,7 +25,7 @@ internal readonly record struct PendingBackpackPlacement(
/// target acquisition, and drag-out drops here instead of duplicating
/// ItemHolder::UseObject fragments in each panel.
///
-internal sealed class ItemInteractionController : IDisposable
+public sealed class ItemInteractionController : IDisposable
{
internal const string InventoryRequestBusyMessage =
"You can only move or use one item at a time";
@@ -1230,7 +1230,7 @@ internal sealed class ItemInteractionController : IDisposable
failures);
}
- internal readonly record struct AppraisalResponseAcceptance(
+ public readonly record struct AppraisalResponseAcceptance(
bool Accepted,
bool FirstResponse);
@@ -1308,7 +1308,7 @@ internal sealed class ItemInteractionController : IDisposable
{
var target = _objects.Get(targetGuid);
if (target is null)
- return false; // retail: GetWeenieObject(target) null → incompatible
+ return false; // retail: GetWeenieObject(target) null → incompatible
return ItemInteractionPolicy.IsTargetCompatible(
Snapshot(source), Snapshot(target), _playerGuid());
}
diff --git a/src/AcDream.App/UI/Layout/AppraisalUiController.cs b/src/AcDream.App/UI/Layout/AppraisalUiController.cs
index a2d2d0de..a3052bd7 100644
--- a/src/AcDream.App/UI/Layout/AppraisalUiController.cs
+++ b/src/AcDream.App/UI/Layout/AppraisalUiController.cs
@@ -1,4 +1,4 @@
-using System.Globalization;
+using System.Globalization;
using System.Text;
using AcDream.App.Spells;
using AcDream.Core.Combat;
@@ -14,7 +14,7 @@ namespace AcDream.App.UI.Layout;
/// appraisal response/subview lifecycle; the imported LayoutDesc owns all
/// chrome, geometry, fonts, and scrollbars.
///
-internal sealed class AppraisalUiController : IRetainedPanelController
+public sealed class AppraisalUiController : IRetainedPanelController
{
public const uint LayoutId = 0x2100006Bu;
public const uint RootId = 0x100005F2u;
@@ -73,8 +73,8 @@ internal sealed class AppraisalUiController : IRetainedPanelController
private readonly CreatureAppraisalRowTemplateFactory? _creatureRowTemplates;
private readonly CreatureDisplayNameResolver _creatureNames;
private readonly RetailAppraisalNameResolver _itemNames;
- private readonly Func _resolveSpellIcon;
- private readonly Func _resolveComponentIcon;
+ private readonly Func _resolveSpellIcon;
+ private readonly Func _resolveComponentIcon;
private readonly Func> _spellComponents;
private readonly Func _magicSkill;
private readonly SpellExamineComponentTemplateFactory? _spellComponentTemplates;
@@ -128,8 +128,8 @@ internal sealed class AppraisalUiController : IRetainedPanelController
CreatureAppraisalRowTemplateFactory? creatureRowTemplates,
CreatureDisplayNameResolver? creatureNames,
RetailAppraisalNameResolver? itemNames,
- Func? resolveSpellIcon,
- Func? resolveComponentIcon,
+ Func? resolveSpellIcon,
+ Func? resolveComponentIcon,
Func>? spellComponents,
Func? magicSkill,
SpellExamineComponentTemplateFactory? spellComponentTemplates)
@@ -155,8 +155,8 @@ internal sealed class AppraisalUiController : IRetainedPanelController
?? new CreatureDisplayNameResolver(
new Dictionary());
_itemNames = itemNames ?? RetailAppraisalNameResolver.Empty;
- _resolveSpellIcon = resolveSpellIcon ?? (_ => GpuTextureSlot.Unassigned);
- _resolveComponentIcon = resolveComponentIcon ?? (_ => GpuTextureSlot.Unassigned);
+ _resolveSpellIcon = resolveSpellIcon ?? (_ => 0u);
+ _resolveComponentIcon = resolveComponentIcon ?? (_ => 0u);
_spellComponents = spellComponents ?? (_ => []);
_magicSkill = magicSkill ?? (_ => 0u);
_spellComponentTemplates = spellComponentTemplates;
@@ -283,8 +283,8 @@ internal sealed class AppraisalUiController : IRetainedPanelController
CreatureAppraisalRowTemplateFactory? creatureRowTemplates = null,
CreatureDisplayNameResolver? creatureNames = null,
RetailAppraisalNameResolver? itemNames = null,
- Func? resolveSpellIcon = null,
- Func? resolveComponentIcon = null,
+ Func? resolveSpellIcon = null,
+ Func? resolveComponentIcon = null,
Func>? spellComponents = null,
Func? magicSkill = null,
SpellExamineComponentTemplateFactory? spellComponentTemplates = null)
@@ -496,7 +496,7 @@ internal sealed class AppraisalUiController : IRetainedPanelController
_characterObjectId = 0;
_spellId = 0u;
_refreshElapsed = 0;
- _spellIcon.Texture = GpuTextureSlot.Unassigned;
+ _spellIcon.Texture = 0u;
SetSpellText(_spellSchool, string.Empty);
SetSpellText(_spellMana, string.Empty);
SetSpellText(_spellDuration, string.Empty);
@@ -1037,7 +1037,7 @@ internal sealed class AppraisalUiController : IRetainedPanelController
}
}
-internal enum AppraisalView
+public enum AppraisalView
{
Item,
Creature,
diff --git a/src/AcDream.App/UI/Layout/CharacterController.cs b/src/AcDream.App/UI/Layout/CharacterController.cs
index 1074c83d..6c60dc15 100644
--- a/src/AcDream.App/UI/Layout/CharacterController.cs
+++ b/src/AcDream.App/UI/Layout/CharacterController.cs
@@ -1,4 +1,4 @@
-using System.Globalization;
+using System.Globalization;
using System.Text;
using AcDream.Core.Items;
@@ -9,7 +9,7 @@ namespace AcDream.App.UI.Layout;
/// localized report into m_pMainText; it is not a second character
/// sheet and therefore does not repeat name, level, title, or current vitals.
///
-internal static class CharacterController
+public static class CharacterController
{
public const uint LayoutId = 0x2100006Eu;
public const uint RootId = 0x10000183u;
@@ -343,7 +343,7 @@ internal static class CharacterController
/// Event-invalidated report owner for retail's character-information text.
/// Stable frames borrow the same shaped line collection.
///
-internal sealed class CharacterInformationUiController : IRetainedPanelController
+public sealed class CharacterInformationUiController : IRetainedPanelController
{
private readonly Func _data;
private readonly CharacterInfoStrings _strings;
@@ -402,7 +402,7 @@ internal sealed class CharacterInformationUiController : IRetainedPanelControlle
}
}
-internal sealed record CharacterInfoStrings(
+public sealed record CharacterInfoStrings(
string[] Birth,
string[] Played,
string DeathsNone,
diff --git a/src/AcDream.App/UI/Layout/CharacterSheet.cs b/src/AcDream.App/UI/Layout/CharacterSheet.cs
index 94a78e12..2da63597 100644
--- a/src/AcDream.App/UI/Layout/CharacterSheet.cs
+++ b/src/AcDream.App/UI/Layout/CharacterSheet.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
namespace AcDream.App.UI.Layout;
@@ -17,9 +17,9 @@ namespace AcDream.App.UI.Layout;
/// UpdateAugmentations (0x004b9000), and
/// UpdateLoad (0x004b8a20).
///
-internal sealed class CharacterSheet
+public sealed class CharacterSheet
{
- // ── Identity ──────────────────────────────────────────────────────────────
+ // ── Identity ──────────────────────────────────────────────────────────────
/// Character name (first line of the report).
public string Name { get; init; } = string.Empty;
@@ -39,8 +39,8 @@ internal sealed class CharacterSheet
/// Title string, e.g. "the Adventurer". Null = omit.
public string? Title { get; init; }
- // ── Experience / PK (gmStatManagementUI::UpdateExperience 0x004f0a70,
- // UpdatePKStatus 0x004f00a0) — the Attributes-tab header strip ──────────
+ // ── Experience / PK (gmStatManagementUI::UpdateExperience 0x004f0a70,
+ // UpdatePKStatus 0x004f00a0) — the Attributes-tab header strip ──────────
/// Total accrued experience (retail PropertyInt64 1). Header value
/// element 0x10000235 (m_pTotalXPText).
@@ -50,7 +50,7 @@ internal sealed class CharacterSheet
/// 0x10000238 (m_pXPToLevelText).
public long XpToNextLevel { get; init; }
- /// XP-to-next-level meter fill, 0..1 (retail (cur−base)/(cap−base)).
+ /// XP-to-next-level meter fill, 0..1 (retail (cur−base)/(cap−base)).
/// Drives the header meter 0x10000236 (m_pXPToLevelMeter).
public float XpFraction { get; init; }
@@ -58,13 +58,13 @@ internal sealed class CharacterSheet
/// 0x10000233 (m_pPKStatusText). Null = omit.
public string? PkStatus { get; init; }
- // ── Birth / age / deaths (UpdatePlayerBirthAgeDeaths 0x004b8cb0) ─────────
+ // ── Birth / age / deaths (UpdatePlayerBirthAgeDeaths 0x004b8cb0) ─────────
- /// Formatted birth date string (retail InqInt(0x62) → strftime).
+ /// Formatted birth date string (retail InqInt(0x62) → strftime).
/// Null = omit the birth line.
public string? BirthDate { get; init; }
- /// Formatted play-time duration (retail InqInt(0x7d) → QueryDuration).
+ /// Formatted play-time duration (retail InqInt(0x7d) → QueryDuration).
/// Null = omit the age line.
public string? PlayTime { get; init; }
@@ -77,7 +77,7 @@ internal sealed class CharacterSheet
/// Raw retail PropertyInt 0x7D seconds. Null means the quality was absent.
public int? TotalPlayTimeSeconds { get; init; }
- // ── Vitals (UpdateEnduranceInfo 0x004b8eb0) ─────────────────────────────
+ // ── Vitals (UpdateEnduranceInfo 0x004b8eb0) ─────────────────────────────
public int HealthCurrent { get; init; }
public int HealthMax { get; init; }
@@ -86,7 +86,7 @@ internal sealed class CharacterSheet
public int ManaCurrent { get; init; }
public int ManaMax { get; init; }
- // ── Innate attributes (UpdateInnateAttributeInfo 0x004b87e0) ────────────
+ // ── Innate attributes (UpdateInnateAttributeInfo 0x004b87e0) ────────────
// InqAttribute order: 1,2,4,3,5,6 = Strength, Endurance, Quickness, Coordination, Focus, Self.
public int Strength { get; init; }
@@ -96,9 +96,9 @@ internal sealed class CharacterSheet
public int Focus { get; init; }
public int Self { get; init; }
- // ── Skills (UpdateFakeSkills 0x004b8930) ────────────────────────────────
+ // ── Skills (UpdateFakeSkills 0x004b8930) ────────────────────────────────
// Character Information uses 0xB5/0xC0 for Chess/Fishing; skill credits use 0x18.
- // InqInt(0x18) = available skill credits — footer 0x10000245 in the Attributes tab.
+ // InqInt(0x18) = available skill credits — footer 0x10000245 in the Attributes tab.
public int UnspentSkillCredits { get; init; }
public int SpecializedSkillCredits { get; init; }
@@ -111,21 +111,21 @@ internal sealed class CharacterSheet
///
/// Available (unspent) skill credits shown in the Attributes tab footer State-A.
- /// Retail InqInt(0x18) — gmStatManagementUI::DisplayDefaultFooter (0x0049cde0).
+ /// Retail InqInt(0x18) — gmStatManagementUI::DisplayDefaultFooter (0x0049cde0).
/// Element 0x10000243 (footer line-1 value in the studio's 3-line layout).
///
public int SkillCredits { get; init; }
///
/// Unassigned (banked) experience points.
- /// Retail InqInt64(2) — shown in footer line-2 in State-A display.
+ /// Retail InqInt64(2) — shown in footer line-2 in State-A display.
/// Element 0x10000245 (footer line-2 value).
///
public long UnassignedXp { get; init; }
- // ── Attribute raise costs (ExperienceToAttributeLevel, gmAttributeUI::PostInit) ──
- // Retail formula for x1: ExperienceToAttributeLevel(value + 1) − xpSpent.
- // Retail formula for x10: ExperienceToAttributeLevel(value + min(10, remaining)) − xpSpent.
+ // ── Attribute raise costs (ExperienceToAttributeLevel, gmAttributeUI::PostInit) ──
+ // Retail formula for x1: ExperienceToAttributeLevel(value + 1) − xpSpent.
+ // Retail formula for x10: ExperienceToAttributeLevel(value + min(10, remaining)) − xpSpent.
// Cost 0 means the attribute is at max or not trainable. Ordered to match AttrRows:
// Strength, Endurance, Coordination, Quickness, Focus, Self, Health, Stamina, Mana.
// Source: gmAttributeUI::GetCostToRaise/GetCostToRaise10 (0x0049cb80/0x0049cc70).
@@ -150,7 +150,7 @@ internal sealed class CharacterSheet
///
public IReadOnlyList Skills { get; init; } = Array.Empty();
- // ── Augmentations (UpdateAugmentations 0x004b9000) ─────────────────────
+ // ── Augmentations (UpdateAugmentations 0x004b9000) ─────────────────────
// Retail InqInt(0x162) = AugmentationStat; string-switch 1..0xb.
/// Augmentation name from the switch in UpdateAugmentations (0x004b9000),
@@ -165,7 +165,7 @@ internal sealed class CharacterSheet
public IReadOnlyDictionary CharacterInfoProperties { get; init; }
= new Dictionary();
- // ── Burden / load (UpdateLoad 0x004b8a20) ───────────────────────────────
+ // ── Burden / load (UpdateLoad 0x004b8a20) ───────────────────────────────
// Retail InqLoad + EncumbranceCapacity(Strength, AugEncumbrance).
public int BurdenCurrent { get; init; }
@@ -173,7 +173,7 @@ internal sealed class CharacterSheet
public int EncumbranceAugmentations { get; init; }
}
-internal enum CharacterSkillAdvancementClass
+public enum CharacterSkillAdvancementClass
{
Inactive = 0,
Untrained = 1,
@@ -181,7 +181,7 @@ internal enum CharacterSkillAdvancementClass
Specialized = 3,
}
-internal sealed record CharacterSkill(
+public sealed record CharacterSkill(
uint Id,
string Name,
uint IconDid,
diff --git a/src/AcDream.App/UI/Layout/CharacterSheetProvider.cs b/src/AcDream.App/UI/Layout/CharacterSheetProvider.cs
index 56e05442..dc390f97 100644
--- a/src/AcDream.App/UI/Layout/CharacterSheetProvider.cs
+++ b/src/AcDream.App/UI/Layout/CharacterSheetProvider.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using AcDream.Core.Items;
using AcDream.Core.Player;
@@ -18,7 +18,7 @@ namespace AcDream.App.UI.Layout;
/// cited there too (gmAttributeUI::GetCostToRaise 0x0049cb80 family).
///
/// State ownership: optimistic debits go through the owning
-/// store's eventful APIs — /
+/// store's eventful APIs — /
/// (fires ObjectUpdated)
/// when the player object is in the table, else
/// /
@@ -26,9 +26,9 @@ namespace AcDream.App.UI.Layout;
/// Never write the raw property dictionaries from UI code. The next server
/// snapshot remains authoritative over every optimistic value.
///
-internal sealed class CharacterSheetProvider
+public sealed class CharacterSheetProvider
{
- /// PropertyInt64 2 = unassigned (banked) XP — CharacterSheet.UnassignedXp.
+ /// PropertyInt64 2 = unassigned (banked) XP — CharacterSheet.UnassignedXp.
private const uint UnassignedXpPropertyId = 2u;
///
@@ -48,10 +48,10 @@ internal sealed class CharacterSheetProvider
private readonly Action? _sendRaiseSkill;
private readonly Action? _sendTrainSkill;
- /// Portal SkillTable (0x0E000004) — set by the host once dats load.
+ /// Portal SkillTable (0x0E000004) — set by the host once dats load.
public DatReaderWriter.DBObjs.SkillTable? SkillTable { get; set; }
- /// Portal ExperienceTable (0x0E000018) — set by the host once dats load.
+ /// Portal ExperienceTable (0x0E000018) — set by the host once dats load.
public DatReaderWriter.DBObjs.ExperienceTable? ExperienceTable { get; set; }
public CharacterSheetProvider(
@@ -89,7 +89,7 @@ internal sealed class CharacterSheetProvider
return new ChangeBinding(this, changed);
}
- // ── Sheet assembly ─────────────────────────────────────────────────────
+ // ── Sheet assembly ─────────────────────────────────────────────────────
/// Best display name: active toon key, else the live object's name, else "Player".
public string CharacterName()
@@ -240,8 +240,8 @@ internal sealed class CharacterSheetProvider
///
/// Load the portal ExperienceTable (0x0E000018), falling back to a
- /// type scan for older or odd dat collections. Failures are logged —
- /// never silently swallowed — and leave raise costs unavailable (0).
+ /// type scan for older or odd dat collections. Failures are logged —
+ /// never silently swallowed — and leave raise costs unavailable (0).
///
public static DatReaderWriter.DBObjs.ExperienceTable? LoadExperienceTable(
IDatReaderWriter dats, Action? log = null)
@@ -275,7 +275,7 @@ internal sealed class CharacterSheetProvider
}
/// XP still needed for the next level + fill fraction of the
- /// current level band (retail (cur−base)/(cap−base); CharacterSheet.XpFraction).
+ /// current level band (retail (cur−base)/(cap−base); CharacterSheet.XpFraction).
private (long toNext, float fraction) ComputeLevelXp(int level, long totalXp)
{
var levels = ExperienceTable?.Levels;
@@ -394,7 +394,7 @@ internal sealed class CharacterSheetProvider
}
/// Cost to advance ranks along a retail
- /// cumulative-XP curve: curve[target] − xpAlreadySpent, clamped at the
+ /// cumulative-XP curve: curve[target] − xpAlreadySpent, clamped at the
/// curve end (retail GetCostToRaise/GetCostToRaise10 0x0049cb80/0x0049cc70).
private static long RaiseCostFromXpCurve(uint[]? curve, uint ranks, uint spentXp, int amount)
{
@@ -427,13 +427,13 @@ internal sealed class CharacterSheetProvider
private int VitalMax(LocalPlayerState.VitalKind kind) =>
_localPlayer.GetMaxApprox(kind) is { } max ? checked((int)Math.Min(int.MaxValue, max)) : 0;
- // ── Raise-request flow ─────────────────────────────────────────────────
+ // ── Raise-request flow ─────────────────────────────────────────────────
///
/// Send a raise/train action to the server and, when a send delegate
/// fired, optimistically apply the local effect so the sheet stays
/// current during the round trip. The next server snapshot remains
- /// authoritative (a rejected raise is corrected by the property echo —
+ /// authoritative (a rejected raise is corrected by the property echo —
/// pending/rollback ledger tracked as a follow-up issue).
///
public void HandleRaiseRequest(CharacterStatController.RaiseRequest request)
diff --git a/src/AcDream.App/UI/Layout/CharacterStatController.cs b/src/AcDream.App/UI/Layout/CharacterStatController.cs
index 6bfba90a..4ac8f737 100644
--- a/src/AcDream.App/UI/Layout/CharacterStatController.cs
+++ b/src/AcDream.App/UI/Layout/CharacterStatController.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Numerics;
using AcDream.App.UI;
@@ -6,14 +6,14 @@ using AcDream.App.UI;
namespace AcDream.App.UI.Layout;
///
-/// Controller for the Character window's Attributes tab — LayoutDesc 0x2100002E,
+/// Controller for the Character window's Attributes tab — LayoutDesc 0x2100002E,
/// whose tab-content slot 0x1000022B mounts sub-layout 0x2100002C (gmAttributeUI, root
/// type 0x1000002A) which in turn chains into the gmStatManagementUI header content.
///
/// Unlike (which targets the SEPARATE text-report
/// sub-panel 0x2100001A, gmCharacterInfoUI, by creating its runtime m_pMainText element),
/// this controller binds the real, statically-mounted header + list elements that the
-/// importer already produces — every id below is confirmed present via
+/// importer already produces — every id below is confirmed present via
/// .
///
/// Ported from gmStatManagementUI::UpdateCharacterInfo (0x004f0770) +
@@ -23,7 +23,7 @@ namespace AcDream.App.UI.Layout;
/// gmAttributeUI::PostInit (0x0049db70) + AttributeInfoRegion / Attribute2ndInfoRegion
/// (0x004f1910 / 0x004f19e0). Row icons loaded via sub-element 0x10000129 in the retail
/// dat template (each icon is a 0x06xxxxxx RenderSurface DataID from SubMap
-/// 0x25000006 / 0x25000007, spec §2).
+/// 0x25000006 / 0x25000007, spec §2).
///
/// Footer State A (nothing selected) bound from
/// DisplayDefaultFooter (0x0049cde0): title empty, line-1 value =
@@ -39,13 +39,13 @@ namespace AcDream.App.UI.Layout;
/// UIStateId.Closed (0x0B). The imported Type-12 tab owns its authored font color and
/// propagates the state to its three chrome children through PassToChildren.
///
-/// Raise buttons: 0x10000246 (×1) + 0x100005EB (×10). State "Normal" = affordable
+/// Raise buttons: 0x10000246 (×1) + 0x100005EB (×10). State "Normal" = affordable
/// (UIStateId.Normal, 0x01), state "Ghosted" = unaffordable or no selection
/// (UIStateId.Ghosted, 0x0D). Source: gmAttributeUI::AttributeInfoRegion::Update (0x004f1910).
///
-internal static class CharacterStatController
+public static class CharacterStatController
{
- // ── gmStatManagementUI header element ids (sub-layout 0x2100002C content) ──
+ // ── gmStatManagementUI header element ids (sub-layout 0x2100002C content) ──
public const uint NameId = 0x10000231u; // m_pNameText
public const uint HeritageId = 0x10000232u; // m_pHeritageText
public const uint PkStatusId = 0x10000233u; // m_pPKStatusText
@@ -64,16 +64,16 @@ internal static class CharacterStatController
public const uint ListScrollbarId = 0x1000023Eu; // m_pListBox vertical scrollbar gutter
public const uint ListDividerId = 0x1000023Fu; // bottom divider above footer
- // ── Footer STATE-A container id ──────────────────────────────────────────
+ // ── Footer STATE-A container id ──────────────────────────────────────────
// 0x10000240 is the "nothing selected" footer group. Its children (0x1000024E label row,
- // 0x10000242–0x10000245 labels+values) are the correct State-A versions with wider
+ // 0x10000242–0x10000245 labels+values) are the correct State-A versions with wider
// label widths (195px vs 145px in State B). _byId stores the LAST duplicate, which
- // is the narrower State-B/C copy — so we walk the tree to 0x10000240 and bind from there.
+ // is the narrower State-B/C copy — so we walk the tree to 0x10000240 and bind from there.
public const uint FooterStateAId = 0x10000240u; // State-A footer container (nothing selected)
public const uint FooterStateBId = 0x10000241u; // State-B footer container (row selected)
public const uint FooterStateCId = 0x10000247u; // State-C footer container (hide inactive)
- // ── Tab bar element ids (LayoutDesc 0x2100002E root) ────────────────────
+ // ── Tab bar element ids (LayoutDesc 0x2100002E root) ────────────────────
// These are imported Type-12 UIElement_Text tabs. Their Closed/Open states carry
// the caption color and PassToChildren=true; their three retained children carry
// the authored left/center/right chrome. Character and Spellbook therefore share
@@ -89,7 +89,7 @@ internal static class CharacterStatController
public const uint SkillsPageId = 0x1000022Cu;
public const uint TitlesPageId = 0x10000539u;
- // ── Footer element ids (gmStatManagementUI struct fields) ────────────────
+ // ── Footer element ids (gmStatManagementUI struct fields) ────────────────
// Source: acclient.h / DisplayDefaultFooter (0x0049cde0)
public const uint FooterTitleId = 0x1000024eu; // GetFooterTitleLabel
public const uint FooterLine1Label = 0x10000242u; // GetFooterLineOneLabel
@@ -97,26 +97,26 @@ internal static class CharacterStatController
public const uint FooterLine2Label = 0x10000244u; // GetFooterLineTwoLabel
public const uint FooterLine2Value = 0x10000245u; // GetFooterLineTwoValue
- // ── Raise button element ids ──────────────────────────────────────────────
+ // ── Raise button element ids ──────────────────────────────────────────────
// Source: gmAttributeUI::PostInit (0x0049db70); CM_Train::Event_TrainAttribute.
// Button state "Normal" (UIStateId 0x01) = affordable (green/active);
// "Ghosted" (UIStateId 0x0D) = disabled. Hidden when nothing is selected.
- public const uint RaiseOneId = 0x10000246u; // raise × 1
- public const uint RaiseTenId = 0x100005EBu; // raise × 10
+ public const uint RaiseOneId = 0x10000246u; // raise × 1
+ public const uint RaiseTenId = 0x100005EBu; // raise × 10
private static readonly Vector4 Body = new(0.92f, 0.90f, 0.82f, 1f); // parchment-white body text
private static readonly Vector4 Gold = new(1f, 0.82f, 0.36f, 1f); // section / emphasis gold
- /// Row highlight color — semi-translucent gold, matches retail
+ /// Row highlight color — semi-translucent gold, matches retail
/// UIStateId.Highlight (0x06) sprite 0x06001397 visual intent.
private static readonly Vector4 HighlightBg = new(1f, 0.75f, 0.2f, 0.25f);
private static readonly Vector4 BuffedSkillGreen = new(0.55f, 1f, 0.55f, 1f);
- // ── Row layout constants ─────────────────────────────────────────────────
+ // ── Row layout constants ─────────────────────────────────────────────────
// RowHeight 22px + IconSize 16px: retail spec (2026-06-26) says icons ~icon-height
// and rows tighter. 16px icon fits inside 22px row with 3px vertical padding each side.
// The larger row font (0x40000001, MaxCharHeight=18) is clipped to the 22px height which
- // gives a tight-but-readable line. Retail spec (2026-06-26 ref): "rows tighter, text ≈ icon height".
+ // gives a tight-but-readable line. Retail spec (2026-06-26 ref): "rows tighter, text ≈ icon height".
private const float RowHeight = 22f;
private const float IconSize = 16f;
private const float RowPadX = 4f;
@@ -146,7 +146,7 @@ internal static class CharacterStatController
Skills,
}
- internal enum RaiseTargetKind
+ public enum RaiseTargetKind
{
Attribute,
Vital,
@@ -154,7 +154,7 @@ internal static class CharacterStatController
TrainSkill,
}
- internal readonly record struct RaiseRequest(
+ public readonly record struct RaiseRequest(
RaiseTargetKind Kind,
uint StatId,
long Cost,
@@ -170,7 +170,7 @@ internal static class CharacterStatController
private sealed record SkillRowBinding(UiClickablePanel Panel, CharacterSkill Skill);
- // ── Attribute row descriptors — retail display order per spec §1 ─────────
+ // ── Attribute row descriptors — retail display order per spec §1 ─────────
private static readonly (string name, uint iconDid, uint statId)[] AttrRows = new[]
{
("Strength", 0x060002C8u, 1u),
@@ -210,7 +210,7 @@ internal static class CharacterStatController
Func data,
UiDatFont? datFont = null,
UiDatFont? rowDatFont = null,
- Func? spriteResolve = null,
+ Func? spriteResolve = null,
RaiseRequestHandler? onRaiseRequest = null,
Action? onClose = null)
{
@@ -231,35 +231,35 @@ internal static class CharacterStatController
UiElement? contentPage = FindDirectChildById(layout.Root, AttributesPageId);
// Name (18px from dat FontDid), Heritage (14px), PkStatus (14px):
- // Fix C: pass null → Label's null-guard keeps the build-time dat font.
+ // Fix C: pass null → Label's null-guard keeps the build-time dat font.
// Controllers still own the text color and the LinesProvider.
- // Name = WHITE (retail "Horan" is white — confirmed 2026-06-26).
+ // Name = WHITE (retail "Horan" is white — confirmed 2026-06-26).
Label(layout, contentPage, NameId, null, Vector4.One, () => data().Name);
Label(layout, contentPage, HeritageId, null, Body, () => CharacterIdentityText.StatHeaderLine(data()));
Label(layout, contentPage, PkStatusId, null, Body, () => data().PkStatus ?? string.Empty);
- // ── Header captions (new — retail labels above/left of each number) ──────
- // LevelCaption (0x1000023A, 16px from dat): pass null → keep build-time dat font.
+ // ── Header captions (new — retail labels above/left of each number) ──────
+ // LevelCaption (0x1000023A, 16px from dat): pass null → keep build-time dat font.
LabelTwoLine(layout, contentPage, LevelCaptionId, null, Body, "Character", "Level");
- // Level number: retail renders this as large gold centered text in the 65×50 element.
+ // Level number: retail renders this as large gold centered text in the 65×50 element.
// Fix C: the dat FontDid for the level element (0x1000023B) is now applied at build
// time when the font resolver is provided (studio path). We no longer force rowDatFont
- // here for the level — the dat's own FontDid drives the font. The Gold color is still
+ // here for the level — the dat's own FontDid drives the font. The Gold color is still
// set via LinesProvider. SYNTHESIZED elements (the 9 attribute rows built in
// BuildAttributeRows) continue to use datFont directly since they have no dat origin.
- // Source: spec §Level area (65,50) + decomp gmStatManagementUI::UpdateCharacterInfo 0x004f0770.
+ // Source: spec §Level area (65,50) + decomp gmStatManagementUI::UpdateCharacterInfo 0x004f0770.
// runtime color, dat carries none.
Label(layout, contentPage, LevelId, null, Gold, () => data().Level.ToString());
- // TotalXpLabel (16px from dat) + TotalXp (16px from dat): pass null → keep dat font.
+ // TotalXpLabel (16px from dat) + TotalXp (16px from dat): pass null → keep dat font.
LabelLeft(layout, contentPage, TotalXpLabelId, null, Body, static () => "Total Experience (XP):");
LabelRight(layout, contentPage, TotalXpId, null, Body, () => data().TotalXp.ToString("N0"));
// XP-to-level meter fill (gmStatManagementUI::UpdateExperience 0x004f0a70).
// Fix 5: child elements 0x10000237 (label) and 0x10000238 (value) are now built by
// the LayoutImporter as UiText children of the XP meter (non-Type-3 meter children
- // are explicitly built and registered in byId — see LayoutImporter.BuildWidget).
+ // are explicitly built and registered in byId — see LayoutImporter.BuildWidget).
// FindElement now returns them; the controller binds their LinesProvider.
// The importer builds them as UiText via DatWidgetFactory.BuildText, applying their
// dat-origin HJustify/VJustify/FontDid/FontColor at build time. The controller then
@@ -272,21 +272,21 @@ internal static class CharacterStatController
// Bind the dat-origin XP label (0x10000237) and value (0x10000238).
// These are now real UiText children of the meter (built by the importer).
// The retail layout places the caption + value ON TOP of the red bar
- // (ref 2026-06-26: "value … with the red fill bar behind it").
- // Source: retail spec (2026-06-26-character-window-retail-reference.md §State 1).
+ // (ref 2026-06-26: "value … with the red fill bar behind it").
+ // Source: retail spec (2026-06-26-character-window-retail-reference.md §State 1).
if (FindTextByDatId(layout, contentPage, XpNextLabelId) is UiText xpLabel)
{
if (datFont is not null) xpLabel.DatFont = datFont;
xpLabel.ClickThrough = true;
xpLabel.Centered = false; // left-align (retail: aligns with Total XP label above)
xpLabel.RightAligned = false;
- xpLabel.Padding = 0f; // avoid scroll clip — meter bar is ~13px tall
+ xpLabel.Padding = 0f; // avoid scroll clip — meter bar is ~13px tall
// Item 1: align the XP-next label's left edge to match the TotalXpLabel's
// absolute left edge. The XP-next label is a child of the meter (local coords),
- // so its Left = TotalXpLabel.Left − meter.Left. This accounts for the meter's
+ // so its Left = TotalXpLabel.Left − meter.Left. This accounts for the meter's
// horizontal offset within the panel (the meter starts to the right of the
- // "Total Experience (XP):" caption row). Source: retail spec §State 1 (the
+ // "Total Experience (XP):" caption row). Source: retail spec §State 1 (the
// "XP for next level:" caption left-aligns with "Total Experience (XP):" above).
if (FindElementByDatId(layout, contentPage, TotalXpLabelId) is { } totalXpLbl)
{
@@ -310,7 +310,7 @@ internal static class CharacterStatController
// The tab visuals are already retained in the imported LayoutDesc. Controllers
// bind only click behavior and the active Open/Closed state below.
- // ── Attribute list — 9 rows in list box 0x1000023D ────────────────────
+ // ── Attribute list — 9 rows in list box 0x1000023D ────────────────────
// Mutable selected-index box: -1 = nothing selected.
// Gather EVERY copy of the raise buttons in the tree. The raise button ids
@@ -322,7 +322,7 @@ internal static class CharacterStatController
// At bind time the tree includes all three tab pages (the page-visibility pass
// runs AFTER this). Collecting from the full tree is safe: once the page-
// visibility pass hides the inactive pages their raise buttons are invisible
- // regardless of the Visible flag we set here — but the Attributes page's
+ // regardless of the Visible flag we set here — but the Attributes page's
// buttons (which are NOT hidden by the page pass) must be explicitly hidden.
var allRaise1 = new List();
var allRaise10 = new List();
@@ -365,7 +365,7 @@ internal static class CharacterStatController
foreach (var b in allRaise1) b.Visible = false;
foreach (var b in allRaise10) b.Visible = false;
- // ── Footer state visibility ───────────────────────────────────────────
+ // ── Footer state visibility ───────────────────────────────────────────
// There are THREE footer state groups (A=0x10000240, B=0x10000241, C=0x10000247)
// all stacked at the same position within the Attributes page. _byId stores only
// the LAST copy of each id; the others live in the VISIBLE Attributes page and must
@@ -373,13 +373,13 @@ internal static class CharacterStatController
//
// WHY this cannot be done in the importer (dat state-model audit 2026-06-26):
// All three group elements have DefaultState = StatManagement_Footer_Default
- // (0x10000011) — the dat does NOT differentiate them by visibility. The parent
+ // (0x10000011) — the dat does NOT differentiate them by visibility. The parent
// element (0x1000022F) has a States map {Default, Text, Meter} with PassToChildren=
// true, but each child group also registers all three states (IncFlags=None,
- // Media=0) — meaning the state-propagation produces no media change on any group.
+ // Media=0) — meaning the state-propagation produces no media change on any group.
// Retail's gmStatManagementUI uses hardcoded element-id dispatch
// (GetChildRecursive(this, 0x10000240) for Default, 0x10000241 for Text, 0x10000247
- // for Meter) to access the right group's children at runtime — the groups themselves
+ // for Meter) to access the right group's children at runtime — the groups themselves
// are never hidden/shown via the dat state mechanism. The controller is the correct
// and only place for this visibility management. See retail decomp
// gmStatManagementUI::GetFooterTitleLabel @0x004f0170.
@@ -389,7 +389,7 @@ internal static class CharacterStatController
// selected and owns the retail raise buttons; State C stays hidden for now.
SetFooterSelected(false);
- // ── Footer State A initial binding ────────────────────────────────────
+ // ── Footer State A initial binding ────────────────────────────────────
// Walk to the State-A container directly (rather than _byId which returns the
// last duplicate) so we get the wider-label copies (195px) for the unselected state.
BindFooterDynamic(layout, datFont, data, activeTab, attrSel, skillSel, contentPage);
@@ -428,10 +428,10 @@ internal static class CharacterStatController
RetailTabBinding.SetClick(titlesTab, null);
UpdateTabStates();
- // ── Active-page selection (fixes the dark-overlay) ─────────────────────
+ // ── Active-page selection (fixes the dark-overlay) ─────────────────────
// WHY this cannot be done in the importer (dat state-model audit 2026-06-26):
// The three tab-page content areas (0x1000022B Attributes, 0x1000022C Skills,
- // 0x10000539 Titles) all have DefaultState = Undef (0) — the dat carries no
+ // 0x10000539 Titles) all have DefaultState = Undef (0) — the dat carries no
// visibility encoding for tabs. Tab visibility is managed at runtime by gmTabUI
// via SetVisible(bool) on the page containers. The controller is the correct
// and only place for initial tab-page selection.
@@ -565,7 +565,7 @@ internal static class CharacterStatController
ImportedLayout layout,
UiElement? contentPage,
UiElement? statList,
- Func? spriteResolve)
+ Func? spriteResolve)
{
if (spriteResolve is null)
return null;
@@ -624,7 +624,7 @@ internal static class CharacterStatController
private static void ConfigureSkillScrollbar(
UiScrollbar bar,
- Func spriteResolve)
+ Func spriteResolve)
{
bar.SpriteResolve = id => { var (h, w, ht) = spriteResolve(id); return (h, w, ht); };
bar.TrackSprite = ScrollTrackSprite;
@@ -649,12 +649,12 @@ internal static class CharacterStatController
: SkillContentWidth;
}
- // ── 9-row attribute list ─────────────────────────────────────────────────
+ // ── 9-row attribute list ─────────────────────────────────────────────────
private static List BuildAttributeRows(
UiElement list,
UiDatFont? datFont,
- Func? spriteResolve,
+ Func? spriteResolve,
Func data,
int[] sel,
List allRaise1,
@@ -736,7 +736,7 @@ internal static class CharacterStatController
private static List BuildSkillRows(
UiElement list,
UiDatFont? datFont,
- Func? spriteResolve,
+ Func? spriteResolve,
Func data,
int[] sel,
List allRaise1,
@@ -792,7 +792,7 @@ internal static class CharacterStatController
private static UiPanel AddSkillHeader(
UiElement list,
UiDatFont? datFont,
- Func? spriteResolve,
+ Func? spriteResolve,
float left,
float top,
float width,
@@ -878,14 +878,14 @@ internal static class CharacterStatController
: Vector4.One;
///
- /// Handles a row click: toggle (same row → deselect), else select new row.
+ /// Handles a row click: toggle (same row → deselect), else select new row.
/// Updates highlight, footer providers, and raise-button state.
///
private static void HandleRowClick(
int clickedIndex,
int[] sel,
List rows,
- Func? spriteResolve,
+ Func? spriteResolve,
Func data,
List allRaise1,
List allRaise10)
@@ -895,10 +895,10 @@ internal static class CharacterStatController
// Log for live test confirmation (user tests selection in the studio).
string rowName = GetRowName(newSel);
- Console.WriteLine($"[CharacterStat] Row click: index={clickedIndex} → selected={newSel} ({rowName})");
+ Console.WriteLine($"[CharacterStat] Row click: index={clickedIndex} → selected={newSel} ({rowName})");
// Update highlight on all rows.
- // Retail uses sprite 0x06001397 (Button state 6 — the dark horizontal bars)
+ // Retail uses sprite 0x06001397 (Button state 6 — the dark horizontal bars)
// for the selected row background. When spriteResolve is available, apply the
// sprite; otherwise fall back to the translucent gold tint.
const uint HighlightSprite = 0x06001397u;
@@ -936,7 +936,7 @@ internal static class CharacterStatController
int clickedIndex,
int[] sel,
List rows,
- Func? spriteResolve,
+ Func? spriteResolve,
Func data,
List allRaise1,
List allRaise10)
@@ -956,7 +956,7 @@ internal static class CharacterStatController
private static void ApplySkillSelectionVisuals(
int selectedIndex,
IReadOnlyList rows,
- Func? spriteResolve)
+ Func? spriteResolve)
{
for (int i = 0; i < rows.Count; i++)
{
@@ -1227,7 +1227,7 @@ internal static class CharacterStatController
private static UiClickablePanel AddRow(
UiElement list,
UiDatFont? datFont,
- Func? spriteResolve,
+ Func? spriteResolve,
float left, float top, float width, float height,
uint iconDid,
string nameText,
@@ -1317,7 +1317,7 @@ internal static class CharacterStatController
return row;
}
- // ── Footer — dynamic (State A + State B via sel[]) ────────────────────────
+ // ── Footer — dynamic (State A + State B via sel[]) ────────────────────────
///
/// Bind all 5 footer elements with providers that close over :
@@ -1361,7 +1361,7 @@ internal static class CharacterStatController
// IMPORTANT: The footer state id (0x10000240) appears once per tab-page sub-layout
// (Attributes / Skills / Titles). layout._byId stores only the LAST registered copy,
// which ends up in the LAST-imported tab page (Titles). The page-visibility pass
- // hides the Titles page → the bound footer elements would be invisible.
+ // hides the Titles page → the bound footer elements would be invisible.
//
// Fix: find State A/B inside the explicit Attributes page, not via _byId. The
// id dictionary stores the last duplicate and can point at a hidden Skills/Titles
@@ -1401,19 +1401,19 @@ internal static class CharacterStatController
// The dat title element is H=55 (the full footer box). The dat says VJustify=Center, so
// without an override the text would center vertically in the 55px box, overlapping
// line-1/line-2 below. We set VerticalJustify=Top explicitly so the text renders at the
- // top of the 55px box (y≈Padding), keeping all three footer lines non-overlapping.
- // The dat says HJustify=Center (Centered=true from BuildText) — the title is centered.
+ // top of the 55px box (y≈Padding), keeping all three footer lines non-overlapping.
+ // The dat says HJustify=Center (Centered=true from BuildText) — the title is centered.
// BackgroundSprite cleared: its full-height sprite would cover line-1/line-2.
titleEl.BackgroundSprite = 0;
titleEl.VerticalJustify = VJustify.Top; // dat says Center; override to Top (see comment above)
titleEl.OneLine = true;
}
- // Title (FooterTitle 0x1000024E, 20px from dat): pass null → keep dat font.
+ // Title (FooterTitle 0x1000024E, 20px from dat): pass null → keep dat font.
// Fix C: the dat has a 20px font for the footer title. Let it drive.
if (titleEl is not null)
{
- // DatFont: null → keep the build-time dat font (20px in studio, global fallback in live game).
- // Centered=true comes from the dat (HJustify=Center) via BuildText — not overridden here.
+ // DatFont: null → keep the build-time dat font (20px in studio, global fallback in live game).
+ // Centered=true comes from the dat (HJustify=Center) via BuildText — not overridden here.
// RightAligned stays false (BuildText default for a Center element).
titleEl.ClickThrough = true;
titleEl.LinesProvider = () =>
@@ -1440,7 +1440,7 @@ internal static class CharacterStatController
};
}
- // Footer lines (all dat-origin with their own font sizes): pass null → keep dat font.
+ // Footer lines (all dat-origin with their own font sizes): pass null → keep dat font.
var l1L = ByPos(20f, 5f, FooterLine1Label);
LabelProvider(l1L, null, Body, () =>
{
@@ -1475,7 +1475,7 @@ internal static class CharacterStatController
return cost > 0 ? cost.ToString("N0") : "Infinity!";
});
- // Line-2 elements: pass null → keep dat font.
+ // Line-2 elements: pass null → keep dat font.
var l2L = ByPos(37f, 5f, FooterLine2Label);
LabelProvider(l2L, null, Body, () =>
{
@@ -1596,7 +1596,7 @@ internal static class CharacterStatController
}
}
- // ── Helpers ──────────────────────────────────────────────────────────────
+ // ── Helpers ──────────────────────────────────────────────────────────────
private static void SetCompatibilityAnchorsAllById(
UiElement node,
@@ -1707,7 +1707,7 @@ internal static class CharacterStatController
/// renders multiple lines oldest-first (top-to-bottom), so
/// line 0 = (top) and line 1 = (bottom).
/// This replaces the single-line "Character Level" caption which truncated in the 65px element.
- /// Source: retail spec (2026-06-26-character-window-retail-reference.md §State 1 level caption).
+ /// Source: retail spec (2026-06-26-character-window-retail-reference.md §State 1 level caption).
private static void LabelTwoLine(ImportedLayout layout, uint id, UiDatFont? datFont, Vector4 color,
string line1, string line2)
=> LabelTwoLine(layout, null, id, datFont, color, line1, line2);
@@ -1719,7 +1719,7 @@ internal static class CharacterStatController
{
// Null = keep whatever the importer (dat FontDid resolver) set at build time.
if (datFont is not null) t.DatFont = datFont;
- t.Centered = false; // non-Centered → scroll/multi-line path
+ t.Centered = false; // non-Centered → scroll/multi-line path
t.RightAligned = false;
t.ClickThrough = true;
t.Padding = 1f;
@@ -1732,7 +1732,7 @@ internal static class CharacterStatController
}
/// Left-justified label (for captions that should be left-aligned, not centered).
- /// Padding=0 so a single dat-font line (≈12px) fits cleanly in a small element without
+ /// Padding=0 so a single dat-font line (≈12px) fits cleanly in a small element without
/// being clipped by the bottom-pin scroll math (top=Padding, bottom=H-Padding).
private static void LabelLeft(ImportedLayout layout, uint id, UiDatFont? datFont, Vector4 color, Func text)
=> LabelLeft(layout, null, id, datFont, color, text);
@@ -1776,8 +1776,8 @@ internal static class CharacterStatController
/// Bind a directly-located widget with a provider.
/// Used when the widget was found by subtree walk rather than FindElement.
/// Sets Padding = 0 to prevent the scroll-clip from hiding text in small
- /// (H≈17–18px) footer elements: with the default Padding=4 and a dat font line-height
- /// of ~12px the bottom-pinned baseY ends up above the top clip boundary → blank.
+ /// (H≈17–18px) footer elements: with the default Padding=4 and a dat font line-height
+ /// of ~12px the bottom-pinned baseY ends up above the top clip boundary → blank.
private static void LabelProvider(UiText? t, UiDatFont? datFont, Vector4 color, Func text)
{
if (t is null) return;
@@ -1798,7 +1798,7 @@ internal static class CharacterStatController
/// The standard returns only the LAST widget
/// registered for a given id; for elements duplicated across tab-page sub-layouts
/// (raise buttons, close buttons) we need ALL copies so that visibility changes are
- /// reflected in every page — not just the last-mounted one.
+ /// reflected in every page — not just the last-mounted one.
///
///
///
@@ -1810,14 +1810,14 @@ internal static class CharacterStatController
/// We therefore walk the tree recursively and collect every whose
/// ActiveState reflects the dat default (before our code sets it), which is not a
/// reliable discriminator. Instead, we gather ALL instances from
- /// the subtree at the known spatial position (bottom of the panel) — but positions can
+ /// the subtree at the known spatial position (bottom of the panel) — but positions can
/// overlap across pages.
///
///
///
/// The correct approach: since _byId stores only one instance per id, we use the
/// for the canonical id, then do a FULL tree walk
- /// to find ADDITIONAL instances that have identical Width×Height to
+ /// to find ADDITIONAL instances that have identical Width×Height to
/// the known button. This works because the three page copies share the same dat template
/// and thus the same geometry. Collected via reference-equality guard to avoid duplicates.
///
@@ -1833,7 +1833,7 @@ internal static class CharacterStatController
_ = layout;
// Walk the tree and collect ALL UiButton instances matching the canonical geometry.
- // The canonical copy itself will also be found — that's fine; use a HashSet to dedup.
+ // The canonical copy itself will also be found — that's fine; use a HashSet to dedup.
var seen = new HashSet(ReferenceEqualityComparer.Instance);
CollectMatchingButtons(node, targetId, seen, result);
}
diff --git a/src/AcDream.App/UI/Layout/ChatWindowController.cs b/src/AcDream.App/UI/Layout/ChatWindowController.cs
index 7d577bf8..c41561dd 100644
--- a/src/AcDream.App/UI/Layout/ChatWindowController.cs
+++ b/src/AcDream.App/UI/Layout/ChatWindowController.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Numerics;
using AcDream.App.Rendering;
@@ -10,7 +10,7 @@ using AcDream.UI.Abstractions.Panels.Chat;
namespace AcDream.App.UI.Layout;
///
-/// Binds the imported chat LayoutDesc (0x21000006) to live behavior — the acdream
+/// Binds the imported chat LayoutDesc (0x21000006) to live behavior — the acdream
/// analogue of retail ChatInterface + gmMainChatUI::PostInit @0x4ce130.
///
///
@@ -24,20 +24,20 @@ namespace AcDream.App.UI.Layout;
/// and bound in place.
///
///
-internal sealed class ChatWindowController : IRetainedWindowStateController, IRetainedPanelController
+public sealed class ChatWindowController : IRetainedWindowStateController, IRetainedPanelController
{
public const uint LayoutId = 0x21000006u;
private bool _disposed;
// Element ids from chat LayoutDesc 0x21000006 (confirmed in Task D/G1).
private const uint RootId = 0x1000000Eu;
- private const uint ResizeBarId = 0x1000000Fu; // dat top resize bar (800px — dropped; nine-slice grips replace it)
+ private const uint ResizeBarId = 0x1000000Fu; // dat top resize bar (800px — dropped; nine-slice grips replace it)
private const uint TranscriptPanelId = 0x10000010u;
- private const uint TranscriptId = 0x10000011u; // Type-12 prototype — skipped by factory
+ private const uint TranscriptId = 0x10000011u; // Type-12 prototype — skipped by factory
private const uint TrackId = 0x10000012u;
private const uint InputBarId = 0x10000013u;
private const uint MenuId = 0x10000014u;
- private const uint InputId = 0x10000016u; // Type-12 Text + Editable 0x16 → UiField
+ private const uint InputId = 0x10000016u; // Type-12 Text + Editable 0x16 → UiField
private const uint SendId = 0x10000019u;
private const uint MaxMinId = 0x1000046Fu;
@@ -48,7 +48,7 @@ internal sealed class ChatWindowController : IRetainedWindowStateController, IRe
private const uint MenuItemRow = 0x0600124Eu; // item row bg (template 0x1000001E)
private const uint MenuItemSelected = 0x0600124Du; // active channel row
- // ── Public surface ─────────────────────────────────────────────────────
+ // ── Public surface ─────────────────────────────────────────────────────
/// Root element of the imported layout (the chat window chrome).
public UiElement Root { get; private set; } = null!;
@@ -71,7 +71,7 @@ internal sealed class ChatWindowController : IRetainedWindowStateController, IRe
public RetailWindowHandle? WindowHandle { get; private set; }
public bool IsMaximized => _maximized;
- // ── Private state ──────────────────────────────────────────────────────
+ // ── Private state ──────────────────────────────────────────────────────
private ChatChannelKind _activeChannel = ChatChannelKind.Say;
@@ -86,7 +86,7 @@ internal sealed class ChatWindowController : IRetainedWindowStateController, IRe
private BitmapFont? _cachedTranscriptDebugFont;
internal int TranscriptLayoutBuildCount { get; private set; }
- // ── Channel knowledge (ported from old UiChannelMenu — gmMainChatUI::InitTalkFocusMenu @0x4cdc50) ──
+ // ── Channel knowledge (ported from old UiChannelMenu — gmMainChatUI::InitTalkFocusMenu @0x4cdc50) ──
private static readonly (string Label, ChatChannelKind? Channel)[] ChannelItems =
{
@@ -133,7 +133,7 @@ internal sealed class ChatWindowController : IRetainedWindowStateController, IRe
private bool _maximized;
private UiButton? _maxMinButton;
- // ── Factory ────────────────────────────────────────────────────────────
+ // ── Factory ────────────────────────────────────────────────────────────
///
/// Bind an imported chat layout to live behavior.
@@ -156,7 +156,7 @@ internal sealed class ChatWindowController : IRetainedWindowStateController, IRe
/// Retail dat font for transcript + input rendering.
/// Fallback debug bitmap font (used when
/// is null).
- /// Dat RenderSurface id → (GL tex handle, px width, px height).
+ /// Dat RenderSurface id → (GL tex handle, px width, px height).
/// Forwarded to and .
public static ChatWindowController? Bind(
ElementInfo rootInfo,
@@ -165,7 +165,7 @@ internal sealed class ChatWindowController : IRetainedWindowStateController, IRe
Func busProvider,
UiDatFont? datFont,
BitmapFont? debugFont,
- Func resolve)
+ Func resolve)
{
// Their parent panels must exist as real widgets in the layout tree.
var transcriptPanel = layout.FindElement(TranscriptPanelId);
@@ -177,14 +177,14 @@ internal sealed class ChatWindowController : IRetainedWindowStateController, IRe
Console.WriteLine(
$"[D.2b] ChatWindowController.Bind: missing required elements " +
$"(input={input is not null}, " +
- $"panel={transcriptPanel is not null}, bar={inputBar is not null}) — " +
+ $"panel={transcriptPanel is not null}, bar={inputBar is not null}) — " +
$"chat window will not be interactive.");
return null;
}
// LayoutDesc 0x21000006 has SEVERAL top-level elements: the gmMainChatUI window
// (RootId 0x1000000E) PLUS stray auxiliary elements that are NOT part of the docked
- // window — a separate Field+ListBox (0x1000001C/1D, the floaty scrollback), the
+ // window — a separate Field+ListBox (0x1000001C/1D, the floaty scrollback), the
// talk-focus highlight strip (0x1000001E), and a scroll-button prototype (0x10000526).
// LayoutImporter.ImportInfos wraps all top-level elements in a synthetic Type-3 root,
// so using layout.Root would render the strays overlapping the real window (the
@@ -210,9 +210,9 @@ internal sealed class ChatWindowController : IRetainedWindowStateController, IRe
transcriptPanel.Top = 0f;
transcriptPanel.Height += 9f; // dat resize-bar height (0x1000000F H=9)
- // ── Transcript ───────────────────────────────────────────────────
+ // ── Transcript ───────────────────────────────────────────────────
// The factory now builds the Type-12 transcript element (0x10000011) as a UiText.
- // Find it in the widget tree and bind the live providers — no remove/add needed.
+ // Find it in the widget tree and bind the live providers — no remove/add needed.
c.Transcript = layout.FindElement(TranscriptId) as UiText
?? throw new InvalidOperationException("chat transcript 0x10000011 not built as UiText");
c.Transcript.DatFont = datFont;
@@ -228,7 +228,7 @@ internal sealed class ChatWindowController : IRetainedWindowStateController, IRe
c.Transcript.BackgroundColor = new Vector4(0f, 0f, 0f, 0.35f); // retail translucent transcript
c.Transcript.LinesProvider = () => c.GetTranscriptLines(vm);
- // ── Input ────────────────────────────────────────────────────────
+ // ── Input ────────────────────────────────────────────────────────
// Editable/selectable/one-line semantics and state sprites came from the
// imported property/state bags. The controller supplies runtime services only.
c.Input = input;
@@ -238,9 +238,9 @@ internal sealed class ChatWindowController : IRetainedWindowStateController, IRe
c.Input.SpriteResolve = resolve;
c.Input.OnSubmit = text => ChatCommandRouter.Submit(text, vm, busProvider(), c._activeChannel);
- // ── Scrollbar — bind the factory-built Type-11 track element ────────
+ // ── Scrollbar — bind the factory-built Type-11 track element ────────
// The factory now builds the Type-11 track element (0x10000012) as a UiScrollbar
- // directly. Find it, bind it in place — no remove/add needed.
+ // directly. Find it, bind it in place — no remove/add needed.
var track = layout.FindElement(TrackId);
if (track is UiScrollbar bar)
{
@@ -253,7 +253,7 @@ internal sealed class ChatWindowController : IRetainedWindowStateController, IRe
c.Scrollbar = bar;
}
- // ── Channel menu — bind the factory-built Type-6 UiMenu ──────────
+ // ── Channel menu — bind the factory-built Type-6 UiMenu ──────────
if (layout.FindElement(MenuId) is UiMenu menu)
{
menu.DatFont = datFont; menu.Font = debugFont; menu.SpriteResolve = resolve;
@@ -268,7 +268,7 @@ internal sealed class ChatWindowController : IRetainedWindowStateController, IRe
menu.EnabledProvider = p => p is not ChatChannelKind ch || ChannelAvailable(ch);
menu.ButtonLabelProvider = () => ChannelButtonLabel(c._activeChannel);
// The widget reports the pick; the controller owns Selected. Only a talk-channel
- // payload updates the active channel + highlight — the null-payload specials are
+ // payload updates the active channel + highlight — the null-payload specials are
// deferred no-ops (see the chat re-drive deferred list) and leave selection intact.
menu.OnSelect = p =>
{
@@ -277,18 +277,18 @@ internal sealed class ChatWindowController : IRetainedWindowStateController, IRe
c.Menu = menu;
}
- // ── Send button — Enter-alternate submit trigger ──────────────────
+ // ── Send button — Enter-alternate submit trigger ──────────────────
// Retail's gmMainChatUI wires the Send button to the same ProcessCommand path.
if (layout.FindElement(SendId) is UiButton sendEl)
{
sendEl.OnClick = () => c.Input.Submit();
- // The Send sprite is a blank gold button — retail draws the caption as text.
+ // The Send sprite is a blank gold button — retail draws the caption as text.
sendEl.Label = "Send";
sendEl.LabelFont = datFont;
sendEl.LabelColor = new Vector4(1f, 0.92f, 0.72f, 1f);
}
- // ── Size the channel button to its label + reflow the input field ─
+ // ── Size the channel button to its label + reflow the input field ─
// Retail's talk-focus button autosizes to the selected channel name; the input
// field then fills the gap from the button's right edge to the Send button. The
// dat authors the button at a fixed 46px (too narrow for "Chat" once the LED +
@@ -310,12 +310,12 @@ internal sealed class ChatWindowController : IRetainedWindowStateController, IRe
ReflowInputRow();
}
- // ── Max/min toggle — gmMainChatUI::HandleMaximizeButton ──
+ // ── Max/min toggle — gmMainChatUI::HandleMaximizeButton ──
if (layout.FindElement(MaxMinId) is UiButton maxMinEl)
{
// The dat puts max/min and the scrollbar up-button at the SAME X (both
// right-anchored), so at content width they overlap. Retail shows max/min
- // just LEFT of the scrollbar column — shift it one button-width left.
+ // just LEFT of the scrollbar column — shift it one button-width left.
if (track is not null)
maxMinEl.Left = track.Left - maxMinEl.Width;
maxMinEl.ResetAnchorCapture();
@@ -326,7 +326,7 @@ internal sealed class ChatWindowController : IRetainedWindowStateController, IRe
return c;
}
- // ── Max/min implementation ─────────────────────────────────────────────
+ // ── Max/min implementation ─────────────────────────────────────────────
///
/// Attach the typed outer-frame handle after the controller's imported content
@@ -419,7 +419,7 @@ internal sealed class ChatWindowController : IRetainedWindowStateController, IRe
return null;
}
- // ── Helpers ────────────────────────────────────────────────────────────
+ // ── Helpers ────────────────────────────────────────────────────────────
///
/// Convert the ChatVM's detailed lines to the transcript's
@@ -449,7 +449,7 @@ internal sealed class ChatWindowController : IRetainedWindowStateController, IRe
}
// Word-wrap each message to the transcript's current pixel width (ports retail
- // GlyphList::Recalculate @0x473800 — break at word boundaries when the line would
+ // GlyphList::Recalculate @0x473800 — break at word boundaries when the line would
// exceed wrapWidth). The cache key re-evaluates it after window resize.
Func measure =
datFont is { } df ? s => df.MeasureWidth(s)
@@ -486,8 +486,8 @@ internal sealed class ChatWindowController : IRetainedWindowStateController, IRe
/// Greedy word-wrap: split into fragments that each fit in
/// pixels (per ), breaking at spaces.
/// A word that is itself wider than the line is broken at CHARACTER boundaries (no
- /// hyphen), packed onto the current line first — so a long unbroken token (e.g. a URL
- /// or "wwwww…") wraps instead of overflowing, and a "You say," prefix stays on the same
+ /// hyphen), packed onto the current line first — so a long unbroken token (e.g. a URL
+ /// or "wwwww…") wraps instead of overflowing, and a "You say," prefix stays on the same
/// row as the start of the message. Mirrors retail GlyphList::Recalculate's per-GlyphLine
/// emission (which breaks mid-glyph-run when a run exceeds the wrap width).
///
@@ -510,7 +510,7 @@ internal sealed class ChatWindowController : IRetainedWindowStateController, IRe
}
if (line.Length > 0 && measure(word) <= maxW)
{
- yield return line.ToString(); // word fits alone → push to a new line
+ yield return line.ToString(); // word fits alone → push to a new line
line.Clear();
line.Append(word);
continue;
@@ -532,7 +532,7 @@ internal sealed class ChatWindowController : IRetainedWindowStateController, IRe
}
///
- /// Per- text color — the EXACT retail RGBA values read from a
+ /// Per- text color — the EXACT retail RGBA values read from a
/// live retail client via cdb (the named RGBAColor constants at acclient
/// 0x81c4a8+, e.g. colorWhite/colorBrightPurple/colorLightBlue/
/// colorGreen, used by ChatInterface::BuildChatColorLookupTable @0x4f31c0).
diff --git a/src/AcDream.App/UI/Layout/CombatUiController.cs b/src/AcDream.App/UI/Layout/CombatUiController.cs
index 14c9e411..f2413e0d 100644
--- a/src/AcDream.App/UI/Layout/CombatUiController.cs
+++ b/src/AcDream.App/UI/Layout/CombatUiController.cs
@@ -1,4 +1,4 @@
-using AcDream.Core.Combat;
+using AcDream.Core.Combat;
using AcDream.Runtime.Gameplay;
using AcDream.UI.Abstractions.Panels.Settings;
@@ -16,7 +16,7 @@ namespace AcDream.App.UI.Layout;
/// ListenToElementMessage (0x004CC430), and
/// RecvNotice_SetCombatMode (0x004CC620).
///
-internal sealed class CombatUiController : IRetainedPanelController
+public sealed class CombatUiController : IRetainedPanelController
{
public const uint LayoutId = 0x21000073u;
public const uint BasicPanelId = 0x1000005Cu;
@@ -231,7 +231,7 @@ internal sealed class CombatUiController : IRetainedPanelController
}
/// Localized labels assigned by retail gmCombatUI::PostInit.
-internal sealed record CombatUiLabels(
+public sealed record CombatUiLabels(
string Speed,
string Power,
string RepeatAttacks,
diff --git a/src/AcDream.App/UI/Layout/ComponentBookTemplateFactory.cs b/src/AcDream.App/UI/Layout/ComponentBookTemplateFactory.cs
index bccea804..b37dd408 100644
--- a/src/AcDream.App/UI/Layout/ComponentBookTemplateFactory.cs
+++ b/src/AcDream.App/UI/Layout/ComponentBookTemplateFactory.cs
@@ -1,4 +1,4 @@
-using DatReaderWriter;
+using DatReaderWriter;
using AcDream.Content;
namespace AcDream.App.UI.Layout;
@@ -9,7 +9,7 @@ namespace AcDream.App.UI.Layout;
/// UIElement_ListBox::AddItemFromTemplateList used by
/// gmSpellComponentUI::UpdateComponents @ 0x0048A910.
///
-internal sealed class ComponentBookTemplateFactory
+public sealed class ComponentBookTemplateFactory
{
public const uint LayoutId = 0x21000033u;
public const uint CategoryTemplateId = 0x10000466u;
@@ -35,7 +35,7 @@ internal sealed class ComponentBookTemplateFactory
private readonly ElementInfo _categoryTemplate;
private readonly ElementInfo _componentTemplate;
- private readonly Func _resolveSprite;
+ private readonly Func _resolveSprite;
private readonly UiDatFont? _defaultFont;
private readonly IReadOnlyDictionary _fonts;
private readonly string[] _categoryNames;
@@ -43,7 +43,7 @@ internal sealed class ComponentBookTemplateFactory
public ComponentBookTemplateFactory(
ElementInfo categoryTemplate,
ElementInfo componentTemplate,
- Func resolveSprite,
+ Func resolveSprite,
UiDatFont? defaultFont,
IReadOnlyDictionary? fonts = null,
IReadOnlyList? categoryNames = null)
@@ -64,7 +64,7 @@ internal sealed class ComponentBookTemplateFactory
///
public static ComponentBookTemplateFactory? TryLoad(
IDatReaderWriter dats,
- Func resolveSprite,
+ Func resolveSprite,
UiDatFont? defaultFont,
Func? resolveFont)
{
@@ -111,7 +111,7 @@ internal sealed class ComponentBookTemplateFactory
public ComponentRow CreateComponentRow(
uint componentId,
- GpuTextureSlot iconTexture,
+ uint iconTexture,
string name,
int ownedCount,
uint desiredCount)
@@ -183,7 +183,7 @@ internal sealed class ComponentBookTemplateFactory
CaptureFonts(child, resolveFont, fonts);
}
- internal readonly record struct ComponentRow(
+ public readonly record struct ComponentRow(
UiTemplateListSlot Slot,
UiField DesiredField);
}
diff --git a/src/AcDream.App/UI/Layout/CreatureAppraisalRows.cs b/src/AcDream.App/UI/Layout/CreatureAppraisalRows.cs
index 1b0cd189..a9eec101 100644
--- a/src/AcDream.App/UI/Layout/CreatureAppraisalRows.cs
+++ b/src/AcDream.App/UI/Layout/CreatureAppraisalRows.cs
@@ -1,4 +1,4 @@
-using System.Globalization;
+using System.Globalization;
using System.Numerics;
using AcDream.Content;
using AcDream.Core.Items;
@@ -9,7 +9,7 @@ using DatReaderWriter.Types;
namespace AcDream.App.UI.Layout;
-internal enum CreatureAppraisalValueStyle
+public enum CreatureAppraisalValueStyle
{
Normal,
Positive,
@@ -17,12 +17,12 @@ internal enum CreatureAppraisalValueStyle
Incomplete,
}
-internal readonly record struct CreatureAppraisalRow(
+public readonly record struct CreatureAppraisalRow(
string Label,
string Value,
CreatureAppraisalValueStyle Style);
-internal enum CreatureAppraisalRowLayer
+public enum CreatureAppraisalRowLayer
{
Combined,
Background,
@@ -35,7 +35,7 @@ internal enum CreatureAppraisalRowLayer
/// BasicCreatureExamineUI and the two Update overloads at
/// 0x004F1D90/0x004F1E80.
///
-internal static class CreatureAppraisalRows
+public static class CreatureAppraisalRows
{
private const string Unknown = "???";
private const uint DamageRating = 0x133u;
@@ -228,20 +228,20 @@ internal static class CreatureAppraisalRows
/// Instantiates LayoutDesc 0x2100006B's InfoRegion token template
/// 0x10000166, matching UIElement_ListBox::AddItemFromTemplateList.
///
-internal sealed class CreatureAppraisalRowTemplateFactory
+public sealed class CreatureAppraisalRowTemplateFactory
{
public const uint TemplateId = 0x10000166u;
public const uint LabelId = 0x1000012Au;
public const uint ValueId = 0x1000012Bu;
private readonly ElementInfo _template;
- private readonly Func _resolveSprite;
+ private readonly Func _resolveSprite;
private readonly UiDatFont? _defaultFont;
private readonly IReadOnlyDictionary _fonts;
public CreatureAppraisalRowTemplateFactory(
ElementInfo template,
- Func resolveSprite,
+ Func resolveSprite,
UiDatFont? defaultFont,
IReadOnlyDictionary? fonts = null)
{
@@ -256,7 +256,7 @@ internal sealed class CreatureAppraisalRowTemplateFactory
public static CreatureAppraisalRowTemplateFactory? TryLoad(
IDatReaderWriter dats,
- Func resolveSprite,
+ Func resolveSprite,
UiDatFont? defaultFont,
Func? resolveFont)
{
@@ -368,7 +368,7 @@ internal sealed class CreatureAppraisalRowTemplateFactory
/// instance of the same authored template keeps label/value text above it.
/// Both lists share one pixel scroll model so their rows cannot drift.
///
-internal sealed class CreatureAppraisalLayeredList
+public sealed class CreatureAppraisalLayeredList
{
public const float TextInset = 8f;
@@ -488,7 +488,7 @@ internal sealed class CreatureAppraisalLayeredList
/// creature enum 0x10000005 through portal EnumMapper 0x2200000E and replaces
/// underscores with spaces.
///
-internal sealed class CreatureDisplayNameResolver
+public sealed class CreatureDisplayNameResolver
{
public const uint MapperDid = 0x2200000Eu;
private readonly IReadOnlyDictionary _names;
diff --git a/src/AcDream.App/UI/Layout/DatStringResolver.cs b/src/AcDream.App/UI/Layout/DatStringResolver.cs
index 10aa6f72..999d4d17 100644
--- a/src/AcDream.App/UI/Layout/DatStringResolver.cs
+++ b/src/AcDream.App/UI/Layout/DatStringResolver.cs
@@ -1,4 +1,4 @@
-using DatReaderWriter;
+using DatReaderWriter;
using AcDream.Content;
using DatReaderWriter.DBObjs;
@@ -13,7 +13,7 @@ namespace AcDream.App.UI.Layout;
/// compute_str_hash @ 0x00413110. A StringInfo's token selects one
/// localized string variant; ordinary UI labels use token zero.
///
-internal sealed class DatStringResolver
+public sealed class DatStringResolver
{
private readonly IDatReaderWriter _dats;
private readonly Dictionary _tables = new();
diff --git a/src/AcDream.App/UI/Layout/DatWidgetFactory.cs b/src/AcDream.App/UI/Layout/DatWidgetFactory.cs
index dd694b7f..2f28bb53 100644
--- a/src/AcDream.App/UI/Layout/DatWidgetFactory.cs
+++ b/src/AcDream.App/UI/Layout/DatWidgetFactory.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Linq;
using AcDream.App.UI;
@@ -18,7 +18,7 @@ namespace AcDream.App.UI.Layout;
///
///
/// The meter's back/front 3-slice sprite ids live on grandchild image elements,
-/// NOT on the meter element itself (format doc §11).
+/// NOT on the meter element itself (format doc §11).
/// walks two layers down to extract them: the two Type-3 container children
/// ordered by (back behind = lower, front
/// on top = higher), then within each container the image children that carry
@@ -28,44 +28,44 @@ namespace AcDream.App.UI.Layout;
///
///
/// The expand-detail overlay present in the front container carries ONLY named
-/// states ("HideDetail"/"ShowDetail") — no "" DirectState entry — so the
+/// states ("HideDetail"/"ShowDetail") — no "" DirectState entry — so the
/// TryGetValue("") filter in excludes it
/// automatically.
///
///
-internal static class DatWidgetFactory
+public static class DatWidgetFactory
{
///
/// Creates the for , sets its
/// rect (Left/Top/Width/Height) and Anchors, and returns it.
///
/// Resolved, merged element snapshot from the LayoutDesc importer.
- /// RenderSurface id → (GL tex handle, pixel width, pixel height).
+ /// RenderSurface id → (GL tex handle, pixel width, pixel height).
/// Returns (0,0,0) when the texture is not yet uploaded.
/// Retail UI font for the meter's "cur/max" number overlay.
- /// May be null pre-load — the meter falls back to the debug bitmap font.
- /// Optional font resolver: FontDid →
+ /// May be null pre-load — the meter falls back to the debug bitmap font.
+ /// Optional font resolver: FontDid →
/// (or null when the font can't be loaded). When non-null, any element whose
/// is non-zero gets ITS OWN dat font applied instead of
/// the shared fallback. Null = original behavior (use
/// for every element).
- /// The widget for this element. Never null — every type produces a widget.
+ /// The widget for this element. Never null — every type produces a widget.
public static UiElement? Create(ElementInfo info,
- Func resolve, UiDatFont? datFont,
+ Func resolve, UiDatFont? datFont,
Func? fontResolve = null,
Func? stringResolve = null)
{
// Retail Type 3 = UIElement_Field (reg :126190), but in acdream's CURRENT layouts
// (vitals 0x2100006C / chat 0x21000006) Type-3 elements are sprite-bearing chrome +
// containers (the 8-piece bevel corners/edges, the transcript/input panels), NOT
- // editable fields — retail draws those as inert media-bearing Fields, which our
+ // editable fields — retail draws those as inert media-bearing Fields, which our
// UiDatElement reproduces pixel-for-pixel (and without the spurious focus/edit
// affordance a UiField would add). The one true editable field, the chat input
// (0x10000016), resolves to Type 12 and is controller-placed as a UiField. So Type 3
// stays on the generic fallback here; register it as UiField only when a window
// actually carries a factory-built editable Type-3 field (and UiField grows a
// background-media draw + an opt-in editable flag at that point). UiField (the widget)
- // still ships — it just isn't wired into the factory switch yet.
+ // still ships — it just isn't wired into the factory switch yet.
// Resolve this element's own dat font if a resolver is provided and the element
// has a FontDid. Falls back to the shared datFont when not set (FontDid==0) or
// when the resolver returns null (font missing from dats).
@@ -86,11 +86,11 @@ internal static class DatWidgetFactory
// gmUIElement_*Indicator custom button classes
6 => new UiMenu(), // UIElement_Menu (reg :120163)
7 => BuildMeter(info, resolve, elementFont), // UIElement_Meter
- 0xD => new UiViewport(), // UIElement_Viewport — 3-D mini-scene blit leaf
+ 0xD => new UiViewport(), // UIElement_Viewport — 3-D mini-scene blit leaf
11 => BuildScrollbar(info, resolve), // UIElement_Scrollbar (reg :124137)
12 => BuildText(info, resolve, elementFont, stringResolve), // UIElement_Text
0x13 => new UiDialogRoot(), // ConfirmationDialog
- 0x10000031u => new UiItemList(resolve), // UIElement_ItemList — toolbar/inventory/paperdoll slots
+ 0x10000031u => new UiItemList(resolve), // UIElement_ItemList — toolbar/inventory/paperdoll slots
0x10000035u => BuildCheckbox(
info, resolve, elementFont, fontResolve, stringResolve), // UIOption_Checkbox
_ => new UiDatElement(info, resolve), // generic fallback (incl. Type 3 chrome/containers)
@@ -105,7 +105,7 @@ internal static class DatWidgetFactory
e.Width = info.Width;
e.Height = info.Height;
- // Honor the dat's draw order. ZLevel is the primary layer (higher = further BACK — e.g. the
+ // Honor the dat's draw order. ZLevel is the primary layer (higher = further BACK — e.g. the
// gmInventoryUI full-window backdrop at ZLevel 100 sits behind the ZLevel-0 panels, #145);
// ReadOrder is the within-layer tiebreaker (higher = on top). K=10000 exceeds any window's
// element count so ZLevel always dominates. Vitals (all ZLevel 0) keep ZOrder == ReadOrder.
@@ -134,7 +134,7 @@ internal static class DatWidgetFactory
///
private static UiScrollbar BuildScrollbar(
ElementInfo info,
- Func resolve)
+ Func resolve)
{
var bar = new UiScrollbar
{
@@ -302,39 +302,39 @@ internal static class DatWidgetFactory
return stateName == "Normal" ? DefaultImage(info) : 0u;
}
- // ── Meter ────────────────────────────────────────────────────────────────
+ // ── Meter ────────────────────────────────────────────────────────────────
///
/// Builds a and populates its sprite ids from the meter's
- /// child/grandchild elements (format doc §11). Two shapes are handled:
+ /// child/grandchild elements (format doc §11). Two shapes are handled:
///
///
- /// 3-slice shape (vitals meters — 2 Type-3 containers, each with 3 image grandchildren):
+ /// 3-slice shape (vitals meters — 2 Type-3 containers, each with 3 image grandchildren):
///
/// meter (Type 7)
- /// ├── back-layer container (Type 3, lower ReadOrder — drawn first / behind)
- /// │ ├── left-cap image (DirectState "" → File = back-left sprite)
- /// │ ├── center image (DirectState "" → File = back-tile sprite)
- /// │ └── right-cap image (DirectState "" → File = back-right sprite)
- /// ├── front-layer container (Type 3, higher ReadOrder — drawn on top)
- /// │ ├── left-cap image (→ front-left sprite)
- /// │ ├── center image (→ front-tile sprite)
- /// │ ├── right-cap image (→ front-right sprite)
- /// │ └── expand overlay (named "ShowDetail"/"HideDetail" only — NO DirectState — IGNORED)
- /// └── text label (Type 0) (IGNORED — Fill/Label providers bound by VitalsController)
+ /// ├── back-layer container (Type 3, lower ReadOrder — drawn first / behind)
+ /// │ ├── left-cap image (DirectState "" → File = back-left sprite)
+ /// │ ├── center image (DirectState "" → File = back-tile sprite)
+ /// │ └── right-cap image (DirectState "" → File = back-right sprite)
+ /// ├── front-layer container (Type 3, higher ReadOrder — drawn on top)
+ /// │ ├── left-cap image (→ front-left sprite)
+ /// │ ├── center image (→ front-tile sprite)
+ /// │ ├── right-cap image (→ front-right sprite)
+ /// │ └── expand overlay (named "ShowDetail"/"HideDetail" only — NO DirectState — IGNORED)
+ /// └── text label (Type 0) (IGNORED — Fill/Label providers bound by VitalsController)
///
///
///
///
- /// Single-image shape (toolbar selected-object meters 0x100001A1/0x100001A2 — 1 Type-3
+ /// Single-image shape (toolbar selected-object meters 0x100001A1/0x100001A2 — 1 Type-3
/// child, no grandchildren): the back-track sprite is on the meter element's own DirectState;
/// the fill sprite is on the single Type-3 child's own DirectState. Both are placed in the
/// TILE slot (Back/FrontTile) with left/right caps 0, so tiles
/// them across the full bar geometry (DrawMode=Normal) and clips the fill to the fraction.
/// (retail: gmToolbarUI::HandleSelectionChanged :198635, UIElement_Meter::Initialize :123328)
///
- /// meter (Type 7) [DirectState "" → back-track sprite, e.g. 0x0600193E]
- /// └── fill container (Type 3) [DirectState "" → fill sprite, e.g. 0x0600193F]
+ /// meter (Type 7) [DirectState "" → back-track sprite, e.g. 0x0600193E]
+ /// └── fill container (Type 3) [DirectState "" → fill sprite, e.g. 0x0600193F]
///
///
///
@@ -345,7 +345,7 @@ internal static class DatWidgetFactory
///
///
private static UiMeter BuildMeter(ElementInfo info,
- Func resolve, UiDatFont? datFont)
+ Func resolve, UiDatFont? datFont)
{
var m = new UiMeter
{
@@ -383,18 +383,18 @@ internal static class DatWidgetFactory
// Single-image shape used by the toolbar selected-object meters
// (health 0x100001A1, mana 0x100001A2).
// - The back-track sprite lives on the meter ELEMENT's own DirectState ("" key of
- // info.StateMedia) — not on any grandchild image. e.g. health back = 0x0600193E.
+ // info.StateMedia) — not on any grandchild image. e.g. health back = 0x0600193E.
// - The fill sprite lives on the single Type-3 child's own DirectState ("" key of
// containers[0].StateMedia). e.g. health fill = 0x0600193F.
- // The fill child has NO image grandchildren, so SliceIds would return all-zero —
+ // The fill child has NO image grandchildren, so SliceIds would return all-zero —
// read the container's StateMedia directly instead.
//
// These go in the TILE slot (not the left-cap slot): the sprites are DrawMode=Normal,
// which retail renders as "tile at native width to fill the full element geometry"
- // (format doc §6; the generic UiDatElement.OnDraw Normal path; UIElement_Meter::
+ // (format doc §6; the generic UiDatElement.OnDraw Normal path; UIElement_Meter::
// DrawChildren :123574 clips the child's FULL 140px geometry box to the fill fraction).
// With the sprite on BackLeft instead, UiMeter.DrawHBar would clamp the cap to the
- // sprite's NATIVE width (capL = min(nativeW, 140)) — leaving a right-side gap and
+ // sprite's NATIVE width (capL = min(nativeW, 140)) — leaving a right-side gap and
// mapping the fill fraction to native width when nativeW < 140. The tile slot makes
// midW = full bar width, so the back tiles across all 140px and the front clips to
// 140*fraction correctly for any native sprite width (left/right caps unused = 0).
@@ -435,7 +435,7 @@ internal static class DatWidgetFactory
}
else
{
- Console.WriteLine($"[D.2b] meter 0x{info.Id:X8}: {containers.Count} Type-3 containers but no recognized 3-slice, direct-fill, or stateful-fill shape — bar may render as solid-color fallback.");
+ Console.WriteLine($"[D.2b] meter 0x{info.Id:X8}: {containers.Count} Type-3 containers but no recognized 3-slice, direct-fill, or stateful-fill shape — bar may render as solid-color fallback.");
}
return m;
@@ -482,11 +482,11 @@ internal static class DatWidgetFactory
return (left, tile, right);
}
- // ── Text ─────────────────────────────────────────────────────────────────
+ // ── Text ─────────────────────────────────────────────────────────────────
/// Type-12 UIElement_Text: an editable field or colored-line text view,
/// selected from the canonical property bag. The element's
- /// own Direct/Normal media (if any) becomes the background sprite, drawn under the text —
+ /// own Direct/Normal media (if any) becomes the background sprite, drawn under the text —
/// so a Type-12 element that previously rendered via UiDatElement keeps its sprite. Lines
/// are bound later by the controller (LinesProvider). An unbound UiText draws nothing
/// because defaults to transparent.
@@ -497,15 +497,15 @@ internal static class DatWidgetFactory
/// that subsequently call /
/// on dat-origin elements can be simplified. Controllers that explicitly set those
/// properties after still override the build-time
- /// defaults — the build-time value is just the starting point, not a lock.
+ /// defaults — the build-time value is just the starting point, not a lock.
///
///
/// The font to seed on the widget. When a font resolver was
/// provided and the element's FontDid resolved successfully, this is that element-specific
/// font; otherwise it is the shared global fallback. Controllers that call
/// and set afterward
- /// still override this — the build-time value is just the starting point.
- private static UiElement BuildText(ElementInfo info, Func resolve,
+ /// still override this — the build-time value is just the starting point.
+ private static UiElement BuildText(ElementInfo info, Func resolve,
UiDatFont? elementFont = null,
Func? stringResolve = null)
{
@@ -547,7 +547,7 @@ internal static class DatWidgetFactory
// Apply horizontal + vertical justification from the dat at build time.
// Controllers that call FindElement and set Centered/RightAligned/VerticalJustify
- // afterward will override these — this is only the dat-driven default.
+ // afterward will override these — this is only the dat-driven default.
bool centered = info.HJustify == HJustify.Center;
bool rightAligned = info.HJustify == HJustify.Right;
var vJustify = info.VJustify switch
@@ -580,7 +580,7 @@ internal static class DatWidgetFactory
// Font color from dat property 0x1B (ColorBaseProperty).
// When present, seed DefaultColor so controllers that read it don't have to hard-code colors.
- // Controllers that supply explicit per-line colors via LinesProvider still win — this is only
+ // Controllers that supply explicit per-line colors via LinesProvider still win — this is only
// the build-time default.
if (info.FontColor.HasValue)
t.DefaultColor = info.FontColor.Value;
@@ -593,7 +593,7 @@ internal static class DatWidgetFactory
private static UiButton BuildButton(
ElementInfo info,
- Func resolve,
+ Func resolve,
UiDatFont? elementFont,
Func? fontResolve,
Func? stringResolve)
@@ -663,7 +663,7 @@ internal static class DatWidgetFactory
///
private static UiButton BuildCheckbox(
ElementInfo info,
- Func resolve,
+ Func resolve,
UiDatFont? elementFont,
Func? fontResolve,
Func? stringResolve)
diff --git a/src/AcDream.App/UI/Layout/EffectRowTemplateFactory.cs b/src/AcDream.App/UI/Layout/EffectRowTemplateFactory.cs
index 3e750c04..80aeebde 100644
--- a/src/AcDream.App/UI/Layout/EffectRowTemplateFactory.cs
+++ b/src/AcDream.App/UI/Layout/EffectRowTemplateFactory.cs
@@ -1,4 +1,4 @@
-using DatReaderWriter;
+using DatReaderWriter;
using AcDream.Content;
namespace AcDream.App.UI.Layout;
@@ -9,16 +9,16 @@ namespace AcDream.App.UI.Layout;
/// UIElement_ListBox::AddItemFromTemplateList for
/// EffectInfoRegion.
///
-internal sealed class EffectRowTemplateFactory
+public sealed class EffectRowTemplateFactory
{
private readonly ElementInfo _template;
- private readonly Func _resolveSprite;
+ private readonly Func _resolveSprite;
private readonly UiDatFont? _defaultFont;
private readonly IReadOnlyDictionary _fonts;
public EffectRowTemplateFactory(
ElementInfo template,
- Func resolveSprite,
+ Func resolveSprite,
UiDatFont? defaultFont,
IReadOnlyDictionary? fonts = null)
{
@@ -33,7 +33,7 @@ internal sealed class EffectRowTemplateFactory
public static EffectRowTemplateFactory? TryLoad(
IDatReaderWriter dats,
- Func resolveSprite,
+ Func resolveSprite,
UiDatFont? defaultFont,
Func? resolveFont)
{
@@ -57,7 +57,7 @@ internal sealed class EffectRowTemplateFactory
public EffectRow Create(
uint spellId,
- GpuTextureSlot iconTexture,
+ uint iconTexture,
string name,
string remaining)
{
@@ -90,7 +90,7 @@ internal sealed class EffectRowTemplateFactory
return new EffectRow(slot, duration, remaining);
}
- internal sealed class EffectRow
+ public sealed class EffectRow
{
private readonly UiText _duration;
private string _remaining;
diff --git a/src/AcDream.App/UI/Layout/EffectsUiController.cs b/src/AcDream.App/UI/Layout/EffectsUiController.cs
index 85a9b62f..3ab84179 100644
--- a/src/AcDream.App/UI/Layout/EffectsUiController.cs
+++ b/src/AcDream.App/UI/Layout/EffectsUiController.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
@@ -7,7 +7,7 @@ using AcDream.Core.Spells;
namespace AcDream.App.UI.Layout;
/// Retail gmEffectsUI positive/negative instance binding.
-internal sealed class EffectsUiController : IRetainedPanelController
+public sealed class EffectsUiController : IRetainedPanelController
{
public const uint LayoutId = 0x2100001Bu;
// gmPanelUI's 0x10000184/185 are panel-slot IDs, not roots in this LayoutDesc.
@@ -27,7 +27,7 @@ internal sealed class EffectsUiController : IRetainedPanelController
private readonly Spellbook _spellbook;
private readonly bool _positive;
private readonly Func _serverTime;
- private readonly Func _resolveSpellIcon;
+ private readonly Func _resolveSpellIcon;
private readonly EffectRowTemplateFactory _templates;
private readonly string _selectPrompt;
private readonly UiItemList _list;
@@ -48,7 +48,7 @@ internal sealed class EffectsUiController : IRetainedPanelController
Spellbook spellbook,
bool positive,
Func serverTime,
- Func resolveSpellIcon,
+ Func resolveSpellIcon,
EffectRowTemplateFactory templates,
string selectPrompt,
UiItemList list,
@@ -81,8 +81,8 @@ internal sealed class EffectsUiController : IRetainedPanelController
Spellbook spellbook,
bool positive,
Func serverTime,
- Func spriteResolve,
- Func resolveSpellIcon,
+ Func spriteResolve,
+ Func resolveSpellIcon,
EffectRowTemplateFactory templates,
string selectPrompt,
Action? close = null)
@@ -144,7 +144,7 @@ internal sealed class EffectsUiController : IRetainedPanelController
uint identity = enchantment.Identity;
EffectRowTemplateFactory.EffectRow row = _templates.Create(
enchantment.SpellId,
- metadata is null ? GpuTextureSlot.Unassigned : _resolveSpellIcon(enchantment.SpellId),
+ metadata is null ? 0u : _resolveSpellIcon(enchantment.SpellId),
metadata?.Name ?? $"Spell {enchantment.SpellId}",
FormatRemaining(enchantment, _serverTime()));
row.Slot.Clicked = () => Select(enchantment.SpellId);
diff --git a/src/AcDream.App/UI/Layout/ElementReader.cs b/src/AcDream.App/UI/Layout/ElementReader.cs
index 0e7f32e7..b4976b42 100644
--- a/src/AcDream.App/UI/Layout/ElementReader.cs
+++ b/src/AcDream.App/UI/Layout/ElementReader.cs
@@ -1,4 +1,4 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.Numerics;
namespace AcDream.App.UI.Layout;
@@ -53,7 +53,7 @@ public sealed class ElementInfo
///
/// Raw edge-anchor flag values from the dat (LeftEdge, TopEdge,
/// RightEdge, BottomEdge fields of ElementDesc).
- /// Values 0–4. Imported elements preserve these in ;
+ /// Values 0–4. Imported elements preserve these in ;
/// is only the compatibility projection
/// for older programmatic consumers.
///
@@ -80,7 +80,7 @@ public sealed class ElementInfo
///
/// Font dat object id inherited from the base element's Properties[0x1A]
- /// (ArrayBaseProperty → DataIdBaseProperty). 0 = none / not inherited.
+ /// (ArrayBaseProperty → DataIdBaseProperty). 0 = none / not inherited.
///
public uint FontDid;
@@ -109,7 +109,7 @@ public sealed class ElementInfo
public Vector4? FontColor;
///
- /// Sprite per state: state name → (RenderSurface file id, DrawMode int).
+ /// Sprite per state: state name → (RenderSurface file id, DrawMode int).
/// The "" key represents the unnamed DirectState (ElementDesc.StateDesc).
/// Named states use the UIStateId.ToString() value as the key
/// (e.g. "HideDetail", "ShowDetail").
@@ -220,16 +220,16 @@ public sealed class ElementInfo
/// No OpenGL, no DatReaderWriter types, no rendering dependencies beyond
/// the bit-flag enum from AcDream.App.UI.
///
-internal static class ElementReader
+public static class ElementReader
{
/// Compatibility projection from raw retail modes to the legacy
/// flags. This projection cannot represent centered
/// mode 3 or proportional mode 4 exactly. Imported DAT widgets therefore use
/// ; call this only for legacy/programmatic paths.
- /// LeftEdge dat field value (0–4).
- /// TopEdge dat field value (0–4).
- /// RightEdge dat field value (0–4).
- /// BottomEdge dat field value (0–4).
+ /// LeftEdge dat field value (0–4).
+ /// TopEdge dat field value (0–4).
+ /// RightEdge dat field value (0–4).
+ /// BottomEdge dat field value (0–4).
public static AnchorEdges ToAnchors(uint left, uint top, uint right, uint bottom)
{
var a = AnchorEdges.None;
@@ -284,7 +284,7 @@ internal static class ElementReader
X = derived.X,
Y = derived.Y,
// NOTE: 0 is the "not set, inherit from base" sentinel for Width/Height. This
- // diverges from the format doc §12 rule 2 ("derived W/H win even if zero") but is
+ // diverges from the format doc §12 rule 2 ("derived W/H win even if zero") but is
// indistinguishable for Plan 1 (all base elements are zero-size Type-12 prototypes).
// If a real zero-size derived element ever needs to override a non-zero base in
// switch Width/Height to nullable values and use presence-aware merging.
@@ -301,11 +301,11 @@ internal static class ElementReader
// HJustify/VJustify: derived wins when it carries an explicit non-Center value
// (the dat property was present and read); otherwise inherit the base prototype's value.
// Center is the default (= "not set by this element") so Center-derived never overrides
- // a non-Center base — matching the FontDid "non-zero wins" convention.
+ // a non-Center base — matching the FontDid "non-zero wins" convention.
HJustify = derived.HJustify != HJustify.Center ? derived.HJustify : base_.HJustify,
VJustify = derived.VJustify != VJustify.Center ? derived.VJustify : base_.VJustify,
// FontColor: derived wins when it has an explicit (non-null) color; otherwise inherit the base.
- // Null means "dat carried no 0x1B property" — so null-derived does NOT override a non-null base.
+ // Null means "dat carried no 0x1B property" — so null-derived does NOT override a non-null base.
FontColor = derived.FontColor ?? base_.FontColor,
// DefaultStateName: derived wins if set; otherwise inherit the base's default.
DefaultStateName = !string.IsNullOrEmpty(derived.DefaultStateName) ? derived.DefaultStateName : base_.DefaultStateName,
diff --git a/src/AcDream.App/UI/Layout/ExternalContainerController.cs b/src/AcDream.App/UI/Layout/ExternalContainerController.cs
index 7d2f1a47..d7bf4460 100644
--- a/src/AcDream.App/UI/Layout/ExternalContainerController.cs
+++ b/src/AcDream.App/UI/Layout/ExternalContainerController.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using AcDream.Core.Items;
using AcDream.Core.Selection;
@@ -9,7 +9,7 @@ namespace AcDream.App.UI.Layout;
/// Chests, corpses, and other world containers share this bottom-screen strip;
/// it is intentionally independent from the owned backpack window.
///
-internal sealed class ExternalContainerController : IItemListDragHandler, IRetainedPanelController
+public sealed class ExternalContainerController : IItemListDragHandler, IRetainedPanelController
{
public const uint LayoutId = 0x21000008u;
public const uint RootId = 0x10000063u;
@@ -31,8 +31,8 @@ internal sealed class ExternalContainerController : IItemListDragHandler, IRetai
private readonly SelectionState _selection;
private readonly ItemInteractionController _itemInteraction;
private readonly StackSplitQuantityState _stackSplitQuantity;
- private readonly Func _resolveIcon;
- private readonly Func _resolveDragIcon;
+ private readonly Func _resolveIcon;
+ private readonly Func _resolveDragIcon;
private readonly Action _sendUse;
private readonly Action _sendPutItemInContainer;
private readonly Action _sendSplitToContainer;
@@ -53,8 +53,8 @@ internal sealed class ExternalContainerController : IItemListDragHandler, IRetai
SelectionState selection,
ItemInteractionController itemInteraction,
StackSplitQuantityState stackSplitQuantity,
- Func resolveIcon,
- Func resolveDragIcon,
+ Func resolveIcon,
+ Func resolveDragIcon,
Action sendUse,
Action sendPutItemInContainer,
Action sendSplitToContainer,
@@ -128,8 +128,8 @@ internal sealed class ExternalContainerController : IItemListDragHandler, IRetai
SelectionState selection,
ItemInteractionController itemInteraction,
StackSplitQuantityState stackSplitQuantity,
- Func resolveIcon,
- Func resolveDragIcon,
+ Func resolveIcon,
+ Func resolveDragIcon,
Action sendUse,
Action sendPutItemInContainer,
Action sendSplitToContainer,
@@ -354,9 +354,9 @@ internal sealed class ExternalContainerController : IItemListDragHandler, IRetai
private UiItemSlot CreateCell(UiItemList owner, uint guid, ItemDragSource source)
{
ClientObject? item = _objects.Get(guid);
- GpuTextureSlot icon = item is null ? GpuTextureSlot.Unassigned : _resolveIcon(
+ uint icon = item is null ? 0u : _resolveIcon(
item.Type, item.IconId, item.IconUnderlayId, item.IconOverlayId, item.Effects);
- GpuTextureSlot dragIcon = item is null ? GpuTextureSlot.Unassigned : _resolveDragIcon(
+ uint dragIcon = item is null ? 0u : _resolveDragIcon(
item.Type, item.IconId, item.IconUnderlayId, item.IconOverlayId, item.Effects);
var cell = new UiItemSlot
{
diff --git a/src/AcDream.App/UI/Layout/IndicatorBarController.cs b/src/AcDream.App/UI/Layout/IndicatorBarController.cs
index 9b78e933..60e6b741 100644
--- a/src/AcDream.App/UI/Layout/IndicatorBarController.cs
+++ b/src/AcDream.App/UI/Layout/IndicatorBarController.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Linq;
using AcDream.Core.Items;
using AcDream.Core.Net;
@@ -12,7 +12,7 @@ namespace AcDream.App.UI.Layout;
/// sprites, and state names; this controller owns only live state selection and
/// authored input actions.
///
-internal sealed class IndicatorBarController : IRetainedPanelController
+public sealed class IndicatorBarController : IRetainedPanelController
{
public const uint LayoutId = 0x21000071u;
@@ -270,7 +270,7 @@ internal sealed class IndicatorBarController : IRetainedPanelController
}
}
-internal sealed record IndicatorBarBindings(
+public sealed record IndicatorBarBindings(
Spellbook Spellbook,
ClientObjectTable Objects,
Func PlayerGuid,
diff --git a/src/AcDream.App/UI/Layout/InventoryController.cs b/src/AcDream.App/UI/Layout/InventoryController.cs
index 26ab7a52..4030a4b1 100644
--- a/src/AcDream.App/UI/Layout/InventoryController.cs
+++ b/src/AcDream.App/UI/Layout/InventoryController.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Numerics;
using AcDream.App.UI;
using AcDream.Core.Items;
@@ -10,12 +10,12 @@ namespace AcDream.App.UI.Layout;
/// Binds the imported gmInventoryUI tree (LayoutDesc 0x21000023) and populates it from
/// . The acdream analogue of retail
/// gmInventoryUI/gmBackpackUI/gm3DItemsUI ::PostInit (named-retail decomp 176236/176596/176728).
-/// Container-switching is live (click a side bag → Use 0x0036 → ViewContents 0x0196 full-replace);
+/// Container-switching is live (click a side bag → Use 0x0036 → ViewContents 0x0196 full-replace);
/// drag-into-bag / wield-drop wire are later sub-phases.
///
-internal sealed class InventoryController : IItemListDragHandler, IRetainedPanelController
+public sealed class InventoryController : IItemListDragHandler, IRetainedPanelController
{
- // Element ids — spec §1 (dat dump of 0x21000022 / 0x21000021 + the *::PostInit binds).
+ // Element ids — spec §1 (dat dump of 0x21000022 / 0x21000021 + the *::PostInit binds).
public const uint ContentsGridId = 0x100001C6u; // gm3DItemsUI m_itemList ("Contents of Backpack")
public const uint ContainerListId = 0x100001CAu; // gmBackpackUI m_containerList (side-bag selector)
public const uint TopContainerId = 0x100001C9u; // gmBackpackUI m_topContainer (main-pack cell)
@@ -30,7 +30,7 @@ internal sealed class InventoryController : IItemListDragHandler, IRetainedPanel
private const uint PaperdollWindowId = 0x100001CDu;
private const uint BackpackWindowId = 0x100001CEu;
- // 3D-items grid: 192x96 → 6 cols x 3 rows of the 32x32 UIItem cell (template 0x21000037).
+ // 3D-items grid: 192x96 → 6 cols x 3 rows of the 32x32 UIItem cell (template 0x21000037).
private const int ContentsColumns = 6;
private const float ContentsCellPx = 32f; // gm3DItemsUI grid (192x96 = 6x3 of 32px)
private const float BackpackCellPx = 36f; // gmBackpackUI column cells (0x100001C9/CA = 36px)
@@ -40,8 +40,8 @@ internal sealed class InventoryController : IItemListDragHandler, IRetainedPanel
private readonly ClientObjectTable _objects;
private readonly Func _playerGuid;
- private readonly Func _iconIds;
- private readonly Func? _dragIconIds;
+ private readonly Func _iconIds;
+ private readonly Func? _dragIconIds;
private readonly Func _strength;
private readonly Func? _ownerName;
@@ -80,8 +80,8 @@ internal sealed class InventoryController : IItemListDragHandler, IRetainedPanel
ImportedLayout layout,
ClientObjectTable objects,
Func playerGuid,
- Func iconIds,
- Func? dragIconIds,
+ Func iconIds,
+ Func? dragIconIds,
Func strength,
SelectionState selection,
Func? ownerName,
@@ -169,7 +169,7 @@ internal sealed class InventoryController : IItemListDragHandler, IRetainedPanel
_topContainer.ExamineItemRequested = ExamineItem;
}
- // Burden meter: vertical 11×58 bar (gmBackpackUI m_burdenMeter, retail direction 4).
+ // Burden meter: vertical 11×58 bar (gmBackpackUI m_burdenMeter, retail direction 4).
_burdenMeter = layout.FindElement(BurdenMeterId) as UiMeter;
if (_burdenMeter is not null)
{
@@ -180,7 +180,7 @@ internal sealed class InventoryController : IItemListDragHandler, IRetainedPanel
// Captions: drive each host UiText directly with the known string (the caption
// elements resolve to UiText). "Contents of Backpack" + "%d%%" are procedural in retail
- // (gm3DItemsUI/gmBackpackUI PostInit/SetLoadLevel); "Burden" is the dat label. (Spec §5.)
+ // (gm3DItemsUI/gmBackpackUI PostInit/SetLoadLevel); "Burden" is the dat label. (Spec §5.)
AttachCaption(layout.FindElement(TitleTextId), () => "Inventory of " + OwnerName(), datFont);
AttachCaption(layout.FindElement(BurdenCaptionId), () => "Burden", datFont);
AttachCaption(layout.FindElement(ContentsCaptionId), () => "Contents of " + OpenContainerName(), datFont);
@@ -228,7 +228,7 @@ internal sealed class InventoryController : IItemListDragHandler, IRetainedPanel
ImportedLayout layout,
ClientObjectTable objects,
Func playerGuid,
- Func iconIds,
+ Func iconIds,
Func strength,
SelectionState selection,
UiDatFont? datFont,
@@ -244,7 +244,7 @@ internal sealed class InventoryController : IItemListDragHandler, IRetainedPanel
ItemInteractionController? itemInteraction = null,
Action? onClose = null,
StackSplitQuantityState? stackSplitQuantity = null,
- Func? dragIconIds = null)
+ Func? dragIconIds = null)
=> new InventoryController(layout, objects, playerGuid, iconIds, dragIconIds, strength, selection,
ownerName, datFont,
contentsEmptySprite, sideBagEmptySprite, mainPackEmptySprite,
@@ -342,7 +342,7 @@ internal sealed class InventoryController : IItemListDragHandler, IRetainedPanel
Populate();
}
- /// True if the object is in (or wielded by) the player — i.e. a rebuild is warranted.
+ /// True if the object is in (or wielded by) the player — i.e. a rebuild is warranted.
private bool Concerns(ClientObject o)
{
uint p = _playerGuid();
@@ -425,14 +425,14 @@ internal sealed class InventoryController : IItemListDragHandler, IRetainedPanel
while (_containerList.GetNumUIItems() < slots) AddEmptyCell(_containerList);
}
- // Main-pack cell: the player's own container — clicking it opens/selects the main pack.
+ // Main-pack cell: the player's own container — clicking it opens/selects the main pack.
// Retail draws a CONSTANT backpack icon here, NOT the player's body icon: IconData::RenderIcons
// (0x0058d1ee) has an IsThePlayer() branch that draws a fixed backpack with m_itemType =
// TYPE_CONTAINER. Compose that backpack base over the Container type-underlay (the player
// object's own IconId is the character body, which would render wrong here). The backpack
// RenderSurface is 0x0600127E, VISUALLY CONFIRMED at the live gate 2026-06-22 (the earlier
// 0x060011F4 from a research dat-dump of GetDIDByEnum(0x10000004,7) was a green tile, not the
- // pack — the index value was misreported). Retires AP-51.
+ // pack — the index value was misreported). Retires AP-51.
if (_topContainer is not null)
{
const uint PlayerPackBaseIcon = 0x0600127Eu; // constant main-pack backpack (visual gate)
@@ -442,7 +442,7 @@ internal sealed class InventoryController : IItemListDragHandler, IRetainedPanel
p,
_iconIds(ItemType.Container, PlayerPackBaseIcon, 0u, 0u, 0u),
dragIconTexture: _dragIconIds?.Invoke(
- ItemType.Container, PlayerPackBaseIcon, 0u, 0u, 0u));
+ ItemType.Container, PlayerPackBaseIcon, 0u, 0u, 0u) ?? 0u);
main.DragAcceptSprite = 0x060011F7u; main.DragRejectSprite = 0x060011F8u;
main.Clicked = () => OpenContainer(p);
main.DoubleClicked = () => _itemInteraction?.ActivateItem(p);
@@ -454,10 +454,10 @@ internal sealed class InventoryController : IItemListDragHandler, IRetainedPanel
RefreshBurden();
}
- /// The effective open container — the explicit one, or the player (main pack) by default.
+ /// The effective open container — the explicit one, or the player (main pack) by default.
/// Resolved live (not cached at ctor) so a late-arriving player guid is handled. The default
/// sentinel is 0; once the main pack is explicitly opened, _openContainer holds the player
- /// guid instead — both resolve here to the same main-pack container, so the paths are equivalent.
+ /// guid instead — both resolve here to the same main-pack container, so the paths are equivalent.
private static bool IsBag(ClientObject item) =>
item.ContainerTypeHint != 0u
|| item.Type.HasFlag(ItemType.Container)
@@ -468,17 +468,17 @@ internal sealed class InventoryController : IItemListDragHandler, IRetainedPanel
/// The owned destination retail PlaceInBackpack currently uses.
public uint CurrentOpenContainerId => EffectiveOpen();
- /// Add a populated cell wired to its click role: container cell → open+select,
- /// item cell → select-only.
+ /// Add a populated cell wired to its click role: container cell → open+select,
+ /// item cell → select-only.
private void AddCell(UiItemList? list, uint guid, bool isContainer, bool waiting = false)
{
if (list is null) return;
var item = _objects.Get(guid);
- GpuTextureSlot tex = item is null ? GpuTextureSlot.Unassigned
+ uint tex = item is null ? 0u
: _iconIds(item.Type, item.IconId, item.IconUnderlayId, item.IconOverlayId, item.Effects);
- GpuTextureSlot? dragTex = item is null ? null
+ uint dragTex = item is null ? 0u
: _dragIconIds?.Invoke(
- item.Type, item.IconId, item.IconUnderlayId, item.IconOverlayId, item.Effects);
+ item.Type, item.IconId, item.IconUnderlayId, item.IconOverlayId, item.Effects) ?? 0u;
var cell = new UiItemSlot { SpriteResolve = list.SpriteResolve };
cell.SetItem(guid, tex, dragIconTexture: dragTex);
cell.SetWaitingState(waiting);
@@ -520,9 +520,9 @@ internal sealed class InventoryController : IItemListDragHandler, IRetainedPanel
///
/// Bind the exact ItemList_DragOver state for this destination. A normal contents-grid
- /// insertion uses ItemSlot_DragOver_Accept (0x10000041 → green circle 0x060011F9).
+ /// insertion uses ItemSlot_DragOver_Accept (0x10000041 → green circle 0x060011F9).
/// An occupied container selector uses ItemSlot_DragOver_DropIn
- /// (0x10000046 → green arrow 0x060011F7). Retail: 0x004e3400.
+ /// (0x10000046 → green arrow 0x060011F7). Retail: 0x004e3400.
///
private void ConfigureDropFeedback(UiItemList list, UiItemSlot cell)
{
@@ -532,11 +532,11 @@ internal sealed class InventoryController : IItemListDragHandler, IRetainedPanel
cell.DragRejectSprite = 0x060011F8u;
}
- /// Set the per-cell container capacity bar — retail UIElement_UIItem::UpdateCapacityDisplay
+ /// Set the per-cell container capacity bar — retail UIElement_UIItem::UpdateCapacityDisplay
/// (0x004e16e0): visible only for a container with itemsCapacity > 0; fill =
/// GetNumContainedItems / itemsCapacity, clamped [0,1]. -1 hides the bar (non-container / unknown
/// capacity). For a CLOSED side bag the contents aren't indexed until it's opened (ViewContents),
- /// so the bar reads empty until then — faithful to retail's known-children count.
+ /// so the bar reads empty until then — faithful to retail's known-children count.
private void SetCapacityBar(UiItemSlot cell, uint containerGuid)
{
int cap = _objects.Get(containerGuid)?.ItemsCapacity ?? 0;
@@ -545,7 +545,7 @@ internal sealed class InventoryController : IItemListDragHandler, IRetainedPanel
cell.CapacityFill = Math.Clamp(n / (float)cap, 0f, 1f);
}
- // ── IItemListDragHandler (B-Drag) — drop an item to move it (optimistic + wire) ──────────────
+ // ── IItemListDragHandler (B-Drag) — drop an item to move it (optimistic + wire) ──────────────
/// Retail ItemList_BeginDrag selects an unselected item before enabling its waiting
/// mesh. Inventory items do not lift-remove (unlike the toolbar): the item stays in its slot
/// until the server confirms the eventual drop.
@@ -778,7 +778,7 @@ internal sealed class InventoryController : IItemListDragHandler, IRetainedPanel
?? dispatch();
/// True only when we KNOW the container is full (capacity known + contents indexed). A
- /// closed bag (unknown count) returns false → advisory accept; the server is authoritative.
+ /// closed bag (unknown count) returns false → advisory accept; the server is authoritative.
private bool IsContainerFull(uint container)
{
int cap = _objects.Get(container)?.ItemsCapacity ?? 0;
@@ -815,7 +815,7 @@ internal sealed class InventoryController : IItemListDragHandler, IRetainedPanel
if (guid == 0) return;
_selection.Select(guid, SelectionChangeSource.Inventory);
uint open = EffectiveOpen();
- if (guid == open) { ApplyIndicators(); return; } // already open — just move the square
+ if (guid == open) { ApplyIndicators(); return; } // already open — just move the square
uint p = _playerGuid();
_openContainer = guid;
@@ -872,7 +872,7 @@ internal sealed class InventoryController : IItemListDragHandler, IRetainedPanel
if (host is null) return;
// The caption elements (0x100001D7 "Burden", 0x100001C5 "Contents of Backpack",
- // 0x100001D8 "%") resolve to UiText (Type-0 inheriting a text base — confirmed live).
+ // 0x100001D8 "%") resolve to UiText (Type-0 inheriting a text base — confirmed live).
// Drive the host UiText DIRECTLY: it is already in the paint tree and renders, whereas
// a nested child UiText did not paint. Set it to a static centered single-line label.
if (host is UiText t)
@@ -913,7 +913,7 @@ internal sealed class InventoryController : IItemListDragHandler, IRetainedPanel
}
/// Recompute the burden fill + percent. Port of CACQualities::InqLoad
- /// (decomp 0x0058f130) → gmBackpackUI::SetLoadLevel (0x004a6ea0). currentBurden:
+ /// (decomp 0x0058f130) → gmBackpackUI::SetLoadLevel (0x004a6ea0). currentBurden:
/// player wire EncumbranceVal (PropertyInt 5) if present, else the carried-Burden sum.
private void RefreshBurden()
{
diff --git a/src/AcDream.App/UI/Layout/ItemAppraisalReport.cs b/src/AcDream.App/UI/Layout/ItemAppraisalReport.cs
index ea003944..3997e6ee 100644
--- a/src/AcDream.App/UI/Layout/ItemAppraisalReport.cs
+++ b/src/AcDream.App/UI/Layout/ItemAppraisalReport.cs
@@ -1,4 +1,4 @@
-using System.Numerics;
+using System.Numerics;
using System.Text;
namespace AcDream.App.UI.Layout;
@@ -18,7 +18,7 @@ public enum ItemAppraisalFontStyle
/// Separator inserted before an appraisal fragment. This is the retained
/// equivalent of ItemExamineUI::AddItemInfo's final argument.
///
-internal enum ItemAppraisalSeparator
+public enum ItemAppraisalSeparator
{
None,
Line,
@@ -30,7 +30,7 @@ internal enum ItemAppraisalSeparator
/// font-color index. Keeping these separate until shaping preserves style
/// when a long fragment wraps.
///
-internal readonly record struct ItemAppraisalFragment(
+public readonly record struct ItemAppraisalFragment(
string Text,
ItemAppraisalSeparator Separator,
ItemAppraisalFontStyle Style);
@@ -38,7 +38,7 @@ internal readonly record struct ItemAppraisalFragment(
///
/// Immutable item appraisal report in retail append order.
///
-internal sealed class ItemAppraisalReport
+public sealed class ItemAppraisalReport
{
public static ItemAppraisalReport Empty { get; } = new([]);
diff --git a/src/AcDream.App/UI/Layout/ItemAppraisalTextFormatter.cs b/src/AcDream.App/UI/Layout/ItemAppraisalTextFormatter.cs
index 7e3d444c..8aaa60fc 100644
--- a/src/AcDream.App/UI/Layout/ItemAppraisalTextFormatter.cs
+++ b/src/AcDream.App/UI/Layout/ItemAppraisalTextFormatter.cs
@@ -1,4 +1,4 @@
-using System.Globalization;
+using System.Globalization;
using System.Text;
using AcDream.Core.Items;
using AcDream.Core.Net.Messages;
@@ -12,7 +12,7 @@ namespace AcDream.App.UI.Layout;
/// and its Appraisal_Show* helpers. Static spell prose comes from the
/// installed portal.dat spell table, like retail's ClientMagicSystem.
///
-internal static class ItemAppraisalTextFormatter
+public static class ItemAppraisalTextFormatter
{
private static readonly (uint Requirement, uint Stat, uint Difficulty)[]
WieldRequirements =
diff --git a/src/AcDream.App/UI/Layout/ItemCooldownAssets.cs b/src/AcDream.App/UI/Layout/ItemCooldownAssets.cs
index cbef9793..76a635c1 100644
--- a/src/AcDream.App/UI/Layout/ItemCooldownAssets.cs
+++ b/src/AcDream.App/UI/Layout/ItemCooldownAssets.cs
@@ -1,4 +1,4 @@
-using AcDream.Content;
+using AcDream.Content;
using DatReaderWriter;
namespace AcDream.App.UI.Layout;
@@ -7,7 +7,7 @@ namespace AcDream.App.UI.Layout;
/// DAT-authored ten-step radial cooldown art owned by the shared retail
/// UIElement_UIItem prototype.
///
-internal readonly record struct ItemCooldownAssets(IReadOnlyList Sprites)
+public readonly record struct ItemCooldownAssets(IReadOnlyList Sprites)
{
public const uint CatalogLayoutId = 0x21000037u;
public const uint SharedItemPrototypeId = 0x1000033Eu;
diff --git a/src/AcDream.App/UI/Layout/ItemCooldownUiController.cs b/src/AcDream.App/UI/Layout/ItemCooldownUiController.cs
index 9e7a4cc2..c4ed53a6 100644
--- a/src/AcDream.App/UI/Layout/ItemCooldownUiController.cs
+++ b/src/AcDream.App/UI/Layout/ItemCooldownUiController.cs
@@ -1,4 +1,4 @@
-using AcDream.Core.Items;
+using AcDream.Core.Items;
using AcDream.Core.Spells;
namespace AcDream.App.UI.Layout;
@@ -8,7 +8,7 @@ namespace AcDream.App.UI.Layout;
/// heartbeat before drawing and gives every current and future
/// the same display projection.
///
-internal sealed class ItemCooldownUiController
+public sealed class ItemCooldownUiController
{
private readonly Spellbook _spellbook;
private readonly ClientObjectTable _objects;
diff --git a/src/AcDream.App/UI/Layout/ItemListCellTemplate.cs b/src/AcDream.App/UI/Layout/ItemListCellTemplate.cs
index 651d2a71..ea6a53e5 100644
--- a/src/AcDream.App/UI/Layout/ItemListCellTemplate.cs
+++ b/src/AcDream.App/UI/Layout/ItemListCellTemplate.cs
@@ -1,4 +1,4 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
using DatReaderWriter;
using AcDream.Content;
using DatReaderWriter.DBObjs;
@@ -13,7 +13,7 @@ namespace AcDream.App.UI.Layout;
/// shared UIItem catalog LayoutDesc, = 0x21000037] -> CreateChildElement(catalog, id); the cloned
/// prototype's ItemSlot_Empty (state 0x1000001c) media is the empty-cell background.
///
-internal static class ItemListCellTemplate
+public static class ItemListCellTemplate
{
/// The shared UIItem cell-template catalog. Hardcoded: retail resolves it via
/// GetByEnum(0x10000038,5,0x23) through a master enum-map DAT object (no code literal);
@@ -87,14 +87,14 @@ internal static class ItemListCellTemplate
// child search alone returns nothing for containers. We deliberately do NOT use the
// prototype's DirectState child overlay: on the container prototype that child is the
// open/selected-container TRIANGLE indicator (0x06005D9C), which retail draws ONLY on the
- // selected container (a deferred container-selection feature) — never as empty-cell art.
+ // selected container (a deferred container-selection feature) — never as empty-cell art.
// (Live visual gate 2026-06-22: frame-first stamped the triangle onto every empty cell.)
return FindIconEmpty(catalog, proto, new HashSet());
}
- // ── attribute 0x1000000e: stored as EnumBaseProperty in the dat (Value is the prototype id) ──
+ // ── attribute 0x1000000e: stored as EnumBaseProperty in the dat (Value is the prototype id) ──
// Note: the spec anticipated DataIdBaseProperty/ArrayBaseProperty based on the font-DID pattern,
- // but the live dat uses EnumBaseProperty.Value (uint) — confirmed by runtime reflection.
+ // but the live dat uses EnumBaseProperty.Value (uint) — confirmed by runtime reflection.
private static uint ReadCellTemplateId(ElementDesc elem)
{
uint id = ReadIdFromState(elem.StateDesc);
@@ -121,7 +121,7 @@ internal static class ItemListCellTemplate
return 0;
}
- // ── prototype media: the m_elem_Icon (0x1000033B) ItemSlot_Empty, resolved through inheritance ──
+ // ── prototype media: the m_elem_Icon (0x1000033B) ItemSlot_Empty, resolved through inheritance ──
// Find the icon child's empty media within `element`'s subtree, following BaseElement edges
// within the same catalog (cycle-guarded by `baseSeen`). The 32x32 contents prototype carries
// 0x1000033B as a direct child; the 36x36 container prototype reaches it only via an inherited
@@ -171,7 +171,7 @@ internal static class ItemListCellTemplate
return 0;
}
- // ── depth-first element search by id (LayoutImporter.FindDesc is private there) ──
+ // ── depth-first element search by id (LayoutImporter.FindDesc is private there) ──
private static ElementDesc? FindDesc(LayoutDesc ld, uint id)
{
foreach (var kv in ld.Elements)
diff --git a/src/AcDream.App/UI/Layout/JumpPowerbarController.cs b/src/AcDream.App/UI/Layout/JumpPowerbarController.cs
index 7f72ba2f..ecdbd9b4 100644
--- a/src/AcDream.App/UI/Layout/JumpPowerbarController.cs
+++ b/src/AcDream.App/UI/Layout/JumpPowerbarController.cs
@@ -1,4 +1,4 @@
-using AcDream.App.Input;
+using AcDream.App.Input;
namespace AcDream.App.UI.Layout;
@@ -13,7 +13,7 @@ namespace AcDream.App.UI.Layout;
/// RecvNotice_FinishPowerbar (0x004DA580), and
/// ClientCombatSystem::CommenceJump (0x0056AF90).
///
-internal sealed class JumpPowerbarController : IRetainedPanelController
+public sealed class JumpPowerbarController : IRetainedPanelController
{
public const uint LayoutId = 0x21000072u;
public const uint MeterId = 0x10000034u;
diff --git a/src/AcDream.App/UI/Layout/LayoutImporter.cs b/src/AcDream.App/UI/Layout/LayoutImporter.cs
index 5c6648e4..43418270 100644
--- a/src/AcDream.App/UI/Layout/LayoutImporter.cs
+++ b/src/AcDream.App/UI/Layout/LayoutImporter.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using AcDream.Content;
using DatReaderWriter;
@@ -12,7 +12,7 @@ namespace AcDream.App.UI.Layout;
/// The result of importing a retail LayoutDesc: a tree with
/// an O(1) lookup table for finding any element by its dat id.
///
-internal sealed class ImportedLayout
+public sealed class ImportedLayout
{
/// Root widget of the imported tree.
public UiElement Root { get; }
@@ -37,7 +37,7 @@ internal sealed class ImportedLayout
///
/// Pure layer ( / ):
/// converts a pre-resolved tree into a
-/// tree via . Testable without dats or OpenGL — all tests
+/// tree via . Testable without dats or OpenGL — all tests
/// in LayoutImporterTests.cs exercise this layer only.
///
///
@@ -55,9 +55,9 @@ internal sealed class ImportedLayout
/// Every other element type recurses its children generically.
///
///
-internal static class LayoutImporter
+public static class LayoutImporter
{
- // ── Pure layer ────────────────────────────────────────────────────────────
+ // ── Pure layer ────────────────────────────────────────────────────────────
///
/// Convenience for tests: attach to
@@ -68,7 +68,7 @@ internal static class LayoutImporter
public static ImportedLayout BuildFromInfos(
ElementInfo rootInfo,
IEnumerable children,
- Func resolve,
+ Func resolve,
UiDatFont? datFont,
Func? fontResolve = null,
Func? stringResolve = null)
@@ -81,15 +81,15 @@ internal static class LayoutImporter
/// Pure builder: produce the widget tree from a fully resolved
/// tree (children already attached).
///
- /// Optional per-element font resolver — FontDid →
+ /// Optional per-element font resolver — FontDid →
/// (or null if the font can't be loaded). When supplied,
/// elements with a non-zero get their own dat
/// font at build time instead of the shared fallback.
/// Null preserves the original single-font behavior for all callers that don't
- /// pass it — no behavior change for the live game path.
+ /// pass it — no behavior change for the live game path.
public static ImportedLayout Build(
ElementInfo rootInfo,
- Func resolve,
+ Func resolve,
UiDatFont? datFont,
Func? fontResolve = null,
Func? stringResolve = null)
@@ -100,7 +100,7 @@ internal static class LayoutImporter
var root = BuildWidget(rootInfo, resolve, datFont, fontResolve, stringResolve, byId);
if (root is null)
{
- Console.WriteLine($"[D.2b] LayoutImporter: root element 0x{rootInfo.Id:X8} (type {rootInfo.Type}) produced no widget — using empty container fallback.");
+ Console.WriteLine($"[D.2b] LayoutImporter: root element 0x{rootInfo.Id:X8} (type {rootInfo.Type}) produced no widget — using empty container fallback.");
root = new UiDatElement(rootInfo, resolve);
}
return new ImportedLayout(root, byId);
@@ -108,20 +108,20 @@ internal static class LayoutImporter
private static UiElement? BuildWidget(
ElementInfo info,
- Func resolve,
+ Func resolve,
UiDatFont? datFont,
Func? fontResolve,
Func? stringResolve,
Dictionary byId)
{
var w = DatWidgetFactory.Create(info, resolve, datFont, fontResolve, stringResolve);
- if (w is null) return null; // Type-12 style prototype — skip
+ if (w is null) return null; // Type-12 style prototype — skip
if (info.Id != 0) byId[info.Id] = w;
// Behavioral widgets that draw their full appearance + reproduce their dat
// sub-elements procedurally (Meter's 3-slice, Menu's label/rows, Field/Text caps,
- // Button labels, Scrollbar arrows) CONSUME their dat children — building those as
+ // Button labels, Scrollbar arrows) CONSUME their dat children — building those as
// separate widgets double-draws and lets an invisible child steal pointer/focus
// from the behavioral widget (e.g. the channel Menu's label child intercepting the
// button click). Only generic containers (UiDatElement, panels) recurse. See
@@ -152,7 +152,7 @@ internal static class LayoutImporter
// double-draw the bar art). All other child types are built normally.
//
// Safe for vitals: the health/stamina/mana meters have ONLY Type-3 slice children
- // (no text children). This loop finds nothing for them → no change to vitals.
+ // (no text children). This loop finds nothing for them → no change to vitals.
foreach (var child in info.Children)
{
if (child.Type == 3) continue; // slice containers: already consumed by BuildMeter
@@ -171,7 +171,7 @@ internal static class LayoutImporter
return w;
}
- // ── Dat shell ─────────────────────────────────────────────────────────────
+ // ── Dat shell ─────────────────────────────────────────────────────────────
///
/// Dat shell, ElementInfo half: load the layout + resolve inheritance + build the
@@ -187,7 +187,7 @@ internal static class LayoutImporter
// Collect the set of element ids that are referenced as a BaseElement by ANY
// element in THIS layout (where BaseLayoutId == layoutId). Such elements are
- // purely inheritance templates ("prototypes") — retail never instantiates them
+ // purely inheritance templates ("prototypes") — retail never instantiates them
// as live widgets. Example: the toolbar slot prototype 0x100001B2 in LayoutDesc
// 0x21000016, which all 18 slot elements inherit from and which has no own media.
//
@@ -258,7 +258,7 @@ internal static class LayoutImporter
/// state an element starts in (e.g., Normal, Minimized). It does
/// NOT encode visibility of sibling Group containers. The StateDesc's
/// contains X/Y/Width/Height/
- /// ZLevel/PassToChildren — there is no Visible flag.
+ /// ZLevel/PassToChildren — there is no Visible flag.
///
///
///
@@ -276,7 +276,7 @@ internal static class LayoutImporter
public static ImportedLayout? Import(
IDatReaderWriter dats,
uint layoutId,
- Func resolve,
+ Func resolve,
UiDatFont? datFont,
Func? fontResolve = null)
{
@@ -291,7 +291,7 @@ internal static class LayoutImporter
IDatReaderWriter dats,
uint layoutId,
uint rootElementId,
- Func resolve,
+ Func resolve,
UiDatFont? datFont,
Func? fontResolve = null)
{
@@ -301,7 +301,7 @@ internal static class LayoutImporter
return Build(rootInfo, resolve, datFont, fontResolve, strings.Resolve);
}
- // ── Inheritance resolution ────────────────────────────────────────────────
+ // ── Inheritance resolution ────────────────────────────────────────────────
/// True when a pure-container inheritor needs the mounted-base Z-layer
/// correction. Child inheritance itself is unconditional and follows retail
@@ -353,9 +353,9 @@ internal static class LayoutImporter
// The mounted slot's layer WITHIN THE FRAME is its OWN ZLevel, not the mounted
// sub-window root's. The gm*UI sub-window roots carry ZLevel 1000 (their standalone
// top-window layer); ElementReader.Merge's zero-wins-base rule made the slot (own
- // ZLevel 0) inherit that 1000, and the #145 ZOrder fold (ReadOrder − ZLevel·10000)
- // turns 1000 into ZOrder ≈ −10,000,000 — sinking the whole panel BEHIND the frame's
- // Alphablend backdrop (ZLevel 100 → ≈ −1,000,000). The backdrop then overpaints the
+ // ZLevel 0) inherit that 1000, and the #145 ZOrder fold (ReadOrder − ZLevel·10000)
+ // turns 1000 into ZOrder ≈ −10,000,000 — sinking the whole panel BEHIND the frame's
+ // Alphablend backdrop (ZLevel 100 → ≈ −1,000,000). The backdrop then overpaints the
// panel's captions/meter/cells (the wash-out bug; the paperdoll root happens to be
// ZLevel 0 so it escaped). Restore the slot's own frame-layer so the panel sits in
// FRONT of the backdrop. (B-Controller debug 2026-06-21; continuation of #145.)
@@ -452,7 +452,7 @@ internal static class LayoutImporter
if (d.StateDesc is not null)
ReadState(d.StateDesc, UiStateInfo.DirectStateId, "", info);
- // Named states (e.g. UIStateId.HideDetail → "HideDetail").
+ // Named states (e.g. UIStateId.HideDetail → "HideDetail").
foreach (var s in d.States)
ReadState(s.Value, (uint)s.Key, s.Key.ToString(), info);
@@ -464,7 +464,7 @@ internal static class LayoutImporter
/// Read the first from into
/// info.StateMedia[name], read any into
/// info.StateCursors[name], and extract the font DID from property 0x1A
- /// (ArrayBaseProperty → DataIdBaseProperty) if not yet set.
+ /// (ArrayBaseProperty → DataIdBaseProperty) if not yet set.
///
private static void ReadState(StateDesc sd, uint stateId, string name, ElementInfo info)
{
@@ -504,7 +504,7 @@ internal static class LayoutImporter
info.States[stateId] = state;
// Font DID: Properties[0x1A] is ArrayBaseProperty{ DataIdBaseProperty }.
- // Format doc §3: "ArrayBaseProperty containing ONE DataIdBaseProperty".
+ // Format doc §3: "ArrayBaseProperty containing ONE DataIdBaseProperty".
if (info.FontDid == 0 && sd.Properties is not null
&& sd.Properties.TryGetValue(0x1Au, out var raw)
&& raw is ArrayBaseProperty arr && arr.Value.Count > 0
@@ -547,15 +547,15 @@ internal static class LayoutImporter
};
}
- // ColorBaseProperty (0x1B): ARGB bytes → normalized [0,1] Vector4 (R,G,B,A).
+ // ColorBaseProperty (0x1B): ARGB bytes → normalized [0,1] Vector4 (R,G,B,A).
// Only read when not already set (first dat state wins; Merge propagates from base).
if (info.FontColor is null
&& sd.Properties.TryGetValue(0x1Bu, out var cRaw)
&& cRaw is ColorBaseProperty cProp)
{
var c = cProp.Value;
- // ColorARGB stores components as bytes (0–255); normalize to [0,1] for Vector4.
- // Alpha=0 in the dat typically means fully opaque (retail convention: 0 → 255).
+ // ColorARGB stores components as bytes (0–255); normalize to [0,1] for Vector4.
+ // Alpha=0 in the dat typically means fully opaque (retail convention: 0 → 255).
float a = c.Alpha == 0 ? 1f : c.Alpha / 255f;
info.FontColor = new System.Numerics.Vector4(c.Red / 255f, c.Green / 255f, c.Blue / 255f, a);
}
@@ -638,7 +638,7 @@ internal static class LayoutImporter
return value;
}
- // ── Prototype detection helpers ───────────────────────────────────────────
+ // ── Prototype detection helpers ───────────────────────────────────────────
///
/// Recursively walks and all its children, adding to
@@ -656,7 +656,7 @@ internal static class LayoutImporter
}
///
- /// Returns true when carries no own state media — i.e. its
+ /// Returns true when carries no own state media — i.e. its
/// StateDesc (DirectState) and States (named states) yield no
/// entries with a non-zero file id.
/// Such elements are pure inheritance templates with no rendering content.
@@ -669,7 +669,7 @@ internal static class LayoutImporter
return info.StateMedia.Count == 0;
}
- // ── Element tree search ───────────────────────────────────────────────────
+ // ── Element tree search ───────────────────────────────────────────────────
///
/// Find an by id anywhere in the top-level tree of
@@ -696,7 +696,7 @@ internal static class LayoutImporter
return null;
}
- // ── Raw-edge layout provenance ────────────────────────────────────────────
+ // ── Raw-edge layout provenance ────────────────────────────────────────────
private static void SetOriginalParentSize(ElementInfo child, float width, float height)
{
diff --git a/src/AcDream.App/UI/Layout/LinkStatusUiController.cs b/src/AcDream.App/UI/Layout/LinkStatusUiController.cs
index 03c3f75a..4d461fc1 100644
--- a/src/AcDream.App/UI/Layout/LinkStatusUiController.cs
+++ b/src/AcDream.App/UI/Layout/LinkStatusUiController.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Globalization;
using AcDream.Core.Net;
@@ -10,7 +10,7 @@ namespace AcDream.App.UI.Layout;
/// chrome; this controller owns its five-second text refresh and 120-second
/// ping cadence.
///
-internal sealed class LinkStatusUiController : IRetainedPanelController
+public sealed class LinkStatusUiController : IRetainedPanelController
{
public const uint LayoutId = 0x2100001Du;
public const uint RootId = 0x10000167u;
@@ -126,7 +126,7 @@ internal sealed class LinkStatusUiController : IRetainedPanelController
}
}
-internal sealed record LinkStatusStrings(
+public sealed record LinkStatusStrings(
string Description,
string Legend,
string DisconnectWarning,
diff --git a/src/AcDream.App/UI/Layout/MiniGameUiController.cs b/src/AcDream.App/UI/Layout/MiniGameUiController.cs
index b0e2c41c..6fcda3f3 100644
--- a/src/AcDream.App/UI/Layout/MiniGameUiController.cs
+++ b/src/AcDream.App/UI/Layout/MiniGameUiController.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
namespace AcDream.App.UI.Layout;
@@ -7,7 +7,7 @@ namespace AcDream.App.UI.Layout;
/// ghosted until a future game-state owner calls SetMiniGameActive;
/// mounting the page now keeps its window lifecycle on the shared retail host.
///
-internal sealed class MiniGameUiController : IRetainedPanelController
+public sealed class MiniGameUiController : IRetainedPanelController
{
public const uint LayoutId = 0x2100001Eu;
public const uint RootId = 0x1000016Au;
diff --git a/src/AcDream.App/UI/Layout/PaperdollClickMap.cs b/src/AcDream.App/UI/Layout/PaperdollClickMap.cs
index 99de82b3..aed5a3f5 100644
--- a/src/AcDream.App/UI/Layout/PaperdollClickMap.cs
+++ b/src/AcDream.App/UI/Layout/PaperdollClickMap.cs
@@ -1,4 +1,4 @@
-using AcDream.Core.Items;
+using AcDream.Core.Items;
using AcDream.Core.Textures;
using AcDream.Content;
using DatReaderWriter;
@@ -11,7 +11,7 @@ namespace AcDream.App.UI.Layout;
/// gmPaperDollUI::CreateClickMap @ 0x004A4850 and
/// gmPaperDollUI::GetPaperDollItemUnderMouse @ 0x004A4920.
///
-internal sealed class PaperdollClickMap
+public sealed class PaperdollClickMap
{
public const uint ClickMapEnum = 0x1000000Cu;
public const uint InterfaceEnumCategory = 7u;
diff --git a/src/AcDream.App/UI/Layout/PaperdollController.cs b/src/AcDream.App/UI/Layout/PaperdollController.cs
index 48599a29..9d84f10b 100644
--- a/src/AcDream.App/UI/Layout/PaperdollController.cs
+++ b/src/AcDream.App/UI/Layout/PaperdollController.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using AcDream.App.UI;
using AcDream.Core.Items;
@@ -10,19 +10,19 @@ namespace AcDream.App.UI.Layout;
/// Binds the 24 equip slots mounted under the paperdoll (gmPaperDollUI 0x21000024, nested in the
/// inventory frame 0x21000023) to live equipped-item data and makes them drag-drop WIELD targets.
/// The acdream analogue of gmPaperDollUI::PostInit + GetLocationInfoFromElementID (named-retail decomp
-/// 175480 / 173620). Slice 1: equip slots only — no 3D doll viewport (that's Slice 2).
+/// 175480 / 173620). Slice 1: equip slots only — no 3D doll viewport (that's Slice 2).
/// Unwield is handled by InventoryController (dragging an equipped item onto the pack grid).
///
-internal sealed class PaperdollController : IItemListDragHandler, IRetainedPanelController
+public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelController
{
public const uint DollViewportId = 0x100001D5u;
public const uint DollDragMaskId = 0x100001D6u;
- // ── Slots-toggle public surface ───────────────────────────────────────────────────────────────
+ // ── Slots-toggle public surface ───────────────────────────────────────────────────────────────
///
/// The 9 armor-slot element-ids whose Visible state the Slots button (0x100005BE) toggles.
/// Doll-view: hidden. Slot-view: shown. Source: gmPaperDollUI::ListenToElementMessage decomp
- /// 175674-175706 — these are the only 9 ids that element flips. The 12 ordinary non-armor
+ /// 175674-175706 — these are the only 9 ids that element flips. The 12 ordinary non-armor
/// lists remain visible in both views; the three Aetheria lists are independently unlock-gated.
///
public static readonly uint[] ArmorSlotElementIds =
@@ -36,7 +36,7 @@ internal sealed class PaperdollController : IItemListDragHandler, IRetainedPanel
/// Default is doll-view (SlotView == false): the 3-D character is visible,
/// the 9 armor slots are hidden. Calling Toggle() alternates between views.
///
- internal sealed class PaperdollViewState
+ public sealed class PaperdollViewState
{
public bool SlotView { get; private set; } // false = doll-view (default)
public bool DollVisible => !SlotView;
@@ -46,8 +46,8 @@ internal sealed class PaperdollController : IItemListDragHandler, IRetainedPanel
private readonly ClientObjectTable _objects;
private readonly Func _playerGuid;
- private readonly Func _iconIds;
- private readonly Func? _dragIconIds;
+ private readonly Func _iconIds;
+ private readonly Func? _dragIconIds;
private readonly ItemInteractionController _itemInteraction;
private readonly bool _ownsItemInteraction;
private readonly SelectionState _selection;
@@ -55,7 +55,7 @@ internal sealed class PaperdollController : IItemListDragHandler, IRetainedPanel
private readonly List<(EquipMask Mask, UiItemList List)> _slots = new();
private readonly List<(AetheriaUnlockState Bit, UiItemList List)> _aetheriaSlots = new();
- // ── Slots-toggle state ────────────────────────────────────────────────────────────────────────
+ // ── Slots-toggle state ────────────────────────────────────────────────────────────────────────
private readonly PaperdollViewState _viewState = new();
private readonly List _armorSlots = new();
private UiElement? _dollViewport; // UiViewport wired in Slice 3; UiElement? keeps this task independent
@@ -64,11 +64,11 @@ internal sealed class PaperdollController : IItemListDragHandler, IRetainedPanel
private PaperdollController(
ImportedLayout layout, ClientObjectTable objects, Func playerGuid,
- Func iconIds, SelectionState selection,
+ Func iconIds, SelectionState selection,
ItemInteractionController itemInteraction,
uint emptySlotSprite, UiDatFont? datFont,
PaperdollClickMap? clickMap,
- Func? dragIconIds,
+ Func? dragIconIds,
IReadOnlyDictionary? emptySlotSprites,
bool ownsItemInteraction)
{
@@ -97,10 +97,10 @@ internal sealed class PaperdollController : IItemListDragHandler, IRetainedPanel
if (list.Cell.ItemId != 0)
_itemInteraction?.ActivateItem(list.Cell.ItemId);
};
- // 3D character (the "figure" — Slice 1/2 correction: NOT a per-slot
+ // 3D character (the "figure" — Slice 1/2 correction: NOT a per-slot
// silhouette) is the doll viewport, which arrives in Slice 2 with the Slots toggle.
// Cell.SpriteResolve + the default accept/reject sprites (ItemSlot_DragOver_Accept ring
- // 0x060011F9 / reject circle 0x060011F8 — the discrete-slot frames, NOT the inventory grid's
+ // 0x060011F9 / reject circle 0x060011F8 — the discrete-slot frames, NOT the inventory grid's
// insert-arrow 0x060011F7) are already wired by DatWidgetFactory when it built the UiItemList;
// no need to re-set them here.
_slots.Add((mask, list));
@@ -115,7 +115,7 @@ internal sealed class PaperdollController : IItemListDragHandler, IRetainedPanel
_objects.Cleared += OnObjectsCleared;
_selection.Changed += OnSelectionChanged;
- // ── Slots-toggle wiring ───────────────────────────────────────────────────────────────────
+ // ── Slots-toggle wiring ───────────────────────────────────────────────────────────────────
foreach (var id in ArmorSlotElementIds)
if (layout.FindElement(id) is UiItemList armor) _armorSlots.Add(armor);
@@ -166,11 +166,11 @@ internal sealed class PaperdollController : IItemListDragHandler, IRetainedPanel
public static PaperdollController Bind(
ImportedLayout layout, ClientObjectTable objects, Func playerGuid,
- Func iconIds, SelectionState selection,
+ Func iconIds, SelectionState selection,
ItemInteractionController itemInteraction,
uint emptySlotSprite = 0u, UiDatFont? datFont = null,
PaperdollClickMap? clickMap = null,
- Func? dragIconIds = null,
+ Func? dragIconIds = null,
IReadOnlyDictionary? emptySlotSprites = null,
bool ownsItemInteraction = false)
=> new PaperdollController(
@@ -221,7 +221,7 @@ internal sealed class PaperdollController : IItemListDragHandler, IRetainedPanel
Populate();
}
- /// The object belongs to the player (wielded gear or pack contents) — so a change to it may
+ /// The object belongs to the player (wielded gear or pack contents) — so a change to it may
/// add/remove/repaint a doll slot. Player-scoped: an NPC's or vendor's wielded item (which also carries
/// CurrentlyEquippedLocation from the wire) must NOT trigger a repaint. A player-equipped item always
/// has WielderId==p (login, from CreateObject) or ContainerId==p (live/optimistic wield, set by
@@ -251,9 +251,9 @@ internal sealed class PaperdollController : IItemListDragHandler, IRetainedPanel
if ((o.CurrentlyEquippedLocation & mask) != EquipMask.None) { worn = o; break; }
if (worn is null) { list.Cell.Clear(); continue; }
- GpuTextureSlot tex = _iconIds(worn.Type, worn.IconId, worn.IconUnderlayId, worn.IconOverlayId, worn.Effects);
- GpuTextureSlot? dragTex = _dragIconIds?.Invoke(
- worn.Type, worn.IconId, worn.IconUnderlayId, worn.IconOverlayId, worn.Effects);
+ uint tex = _iconIds(worn.Type, worn.IconId, worn.IconUnderlayId, worn.IconOverlayId, worn.Effects);
+ uint dragTex = _dragIconIds?.Invoke(
+ worn.Type, worn.IconId, worn.IconUnderlayId, worn.IconOverlayId, worn.Effects) ?? 0u;
list.Cell.SetItem(worn.ObjectId, tex, dragIconTexture: dragTex);
}
ApplyAetheriaVisibility();
@@ -287,10 +287,10 @@ internal sealed class PaperdollController : IItemListDragHandler, IRetainedPanel
return EquipMask.None;
}
- // ── IItemListDragHandler ──────────────────────────────────────────────────────────────────────
+ // ── IItemListDragHandler ──────────────────────────────────────────────────────────────────────
/// Selects the wielded item before the waiting mesh appears. The item itself stays put
/// until the server confirms, like the inventory grid and unlike the toolbar's remove-on-lift.
- /// Unwield happens on DROP onto the pack grid — InventoryController.
+ /// Unwield happens on DROP onto the pack grid — InventoryController.
public void OnDragLift(UiItemList sourceList, UiItemSlot sourceCell, ItemDragPayload payload)
{
// UIElement_ItemList::ItemList_BeginDrag @ 0x004E32D0.
diff --git a/src/AcDream.App/UI/Layout/PaperdollSlotBackgrounds.cs b/src/AcDream.App/UI/Layout/PaperdollSlotBackgrounds.cs
index 7994838a..6fef9fdf 100644
--- a/src/AcDream.App/UI/Layout/PaperdollSlotBackgrounds.cs
+++ b/src/AcDream.App/UI/Layout/PaperdollSlotBackgrounds.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using AcDream.Core.Items;
using AcDream.Content;
@@ -16,7 +16,7 @@ namespace AcDream.App.UI.Layout;
/// Cell creation: UIElement_ItemList::InternalCreateItem @ 0x004E3570.
/// Prototype media: live DAT catalog LayoutDesc 0x21000037.
///
-internal static class PaperdollSlotBackgrounds
+public static class PaperdollSlotBackgrounds
{
internal readonly record struct Definition(
uint Element,
diff --git a/src/AcDream.App/UI/Layout/RadarController.cs b/src/AcDream.App/UI/Layout/RadarController.cs
index 23ec5f8c..1097691b 100644
--- a/src/AcDream.App/UI/Layout/RadarController.cs
+++ b/src/AcDream.App/UI/Layout/RadarController.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Numerics;
using AcDream.App.UI;
@@ -11,7 +11,7 @@ namespace AcDream.App.UI.Layout;
/// LayoutDesc 0x21000074 tree. Like the other gm* controllers, this class only
/// finds children by retail id and attaches live providers; it does not recreate DAT chrome.
///
-internal sealed class RadarController : IRetainedPanelController
+public sealed class RadarController : IRetainedPanelController
{
public const uint LayoutId = 0x21000074u;
/// Production layout property 0x1000002D, recovered directly from the retail DAT.
diff --git a/src/AcDream.App/UI/Layout/RadarSnapshotProvider.cs b/src/AcDream.App/UI/Layout/RadarSnapshotProvider.cs
index 995c8875..004a357b 100644
--- a/src/AcDream.App/UI/Layout/RadarSnapshotProvider.cs
+++ b/src/AcDream.App/UI/Layout/RadarSnapshotProvider.cs
@@ -1,4 +1,4 @@
-using System.Numerics;
+using System.Numerics;
using AcDream.App.World;
using AcDream.Core.Items;
using AcDream.Core.Net;
@@ -15,7 +15,7 @@ namespace AcDream.App.UI.Layout;
/// AC-specific classification and math decision, while the retained widget remains a
/// backend-only renderer.
///
-internal sealed class RadarSnapshotProvider
+public sealed class RadarSnapshotProvider
{
private static readonly Vector2 ProductionCenter = new(60f, 60f);
diff --git a/src/AcDream.App/UI/Layout/RetailAppraisalNameResolver.cs b/src/AcDream.App/UI/Layout/RetailAppraisalNameResolver.cs
index fbc79c00..7546525f 100644
--- a/src/AcDream.App/UI/Layout/RetailAppraisalNameResolver.cs
+++ b/src/AcDream.App/UI/Layout/RetailAppraisalNameResolver.cs
@@ -1,4 +1,4 @@
-using AcDream.Content;
+using AcDream.Content;
using AcDream.Core.Items;
using DatReaderWriter;
using DatReaderWriter.DBObjs;
@@ -14,7 +14,7 @@ namespace AcDream.App.UI.Layout;
/// AppraisalSystem::InqCreatureDisplayName @ 0x005B59E0 and
/// InqHeritageGroupDisplayName @ 0x005B4710.
///
-internal sealed class RetailAppraisalNameResolver
+public sealed class RetailAppraisalNameResolver
{
private const uint MaterialClientEnum = 0x10000001u;
private const uint MaterialSubEnum = 1u;
diff --git a/src/AcDream.App/UI/Layout/RetailDialogData.cs b/src/AcDream.App/UI/Layout/RetailDialogData.cs
index 82d47584..debec420 100644
--- a/src/AcDream.App/UI/Layout/RetailDialogData.cs
+++ b/src/AcDream.App/UI/Layout/RetailDialogData.cs
@@ -1,10 +1,10 @@
-namespace AcDream.App.UI.Layout;
+namespace AcDream.App.UI.Layout;
///
/// Property identifiers consumed by retail's Dialog and
/// DialogFactory implementations.
///
-internal static class RetailDialogProperty
+public static class RetailDialogProperty
{
public const uint Priority = 0x8Du;
public const uint Type = 0x8Eu;
@@ -43,7 +43,7 @@ public enum RetailDialogType : uint
/// The raw numeric keys remain visible because type-specific presenters and semantic
/// callbacks both extend the same collection in retail.
///
-internal sealed class RetailDialogData
+public sealed class RetailDialogData
{
private readonly Dictionary _values = new();
diff --git a/src/AcDream.App/UI/Layout/RetailDialogFactory.cs b/src/AcDream.App/UI/Layout/RetailDialogFactory.cs
index 56b6e3f1..f4c71c2f 100644
--- a/src/AcDream.App/UI/Layout/RetailDialogFactory.cs
+++ b/src/AcDream.App/UI/Layout/RetailDialogFactory.cs
@@ -1,11 +1,11 @@
-namespace AcDream.App.UI.Layout;
+namespace AcDream.App.UI.Layout;
///
/// Retained-mode port of retail DialogFactory @ 0x004773C0..0x00478470.
/// It owns dialog contexts, independent FIFO queue groups, nonqueued dialogs,
/// priority preemption, callback delivery, close notices, and fresh catalog roots.
///
-internal sealed class RetailDialogFactory : IDisposable
+public sealed class RetailDialogFactory : IDisposable
{
public const uint DefaultQueueKey = 2u;
public const uint NonQueuedKey = 1u;
diff --git a/src/AcDream.App/UI/Layout/RetailFpsController.cs b/src/AcDream.App/UI/Layout/RetailFpsController.cs
index c1cf3fa2..b3a299b2 100644
--- a/src/AcDream.App/UI/Layout/RetailFpsController.cs
+++ b/src/AcDream.App/UI/Layout/RetailFpsController.cs
@@ -1,4 +1,4 @@
-using System.Globalization;
+using System.Globalization;
namespace AcDream.App.UI.Layout;
@@ -13,7 +13,7 @@ namespace AcDream.App.UI.Layout;
/// string 0x0DCFFF73 supplies the two labels FPS: and DEG:;
/// retail inserts both floating-point values with two decimal places.
///
-internal sealed class RetailFpsController
+public sealed class RetailFpsController
{
public const uint LayoutId = 0x2100000Fu;
public const uint DisplayElementId = 0x10000047u;
diff --git a/src/AcDream.App/UI/Layout/RetailPanelUiController.cs b/src/AcDream.App/UI/Layout/RetailPanelUiController.cs
index 80b84812..c5e9c8d5 100644
--- a/src/AcDream.App/UI/Layout/RetailPanelUiController.cs
+++ b/src/AcDream.App/UI/Layout/RetailPanelUiController.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
namespace AcDream.App.UI.Layout;
@@ -9,7 +9,7 @@ namespace AcDream.App.UI.Layout;
/// deferred child; this owner applies the same lifecycle to registered retained
/// windows without making the toolbar authoritative for non-toolbar panels.
///
-internal sealed class RetailPanelUiController : IDisposable
+public sealed class RetailPanelUiController : IDisposable
{
public const uint RestorePreviousPropertyId = 0x10000049u;
diff --git a/src/AcDream.App/UI/Layout/RetailWindowFrame.cs b/src/AcDream.App/UI/Layout/RetailWindowFrame.cs
index f94176fe..d9b22a22 100644
--- a/src/AcDream.App/UI/Layout/RetailWindowFrame.cs
+++ b/src/AcDream.App/UI/Layout/RetailWindowFrame.cs
@@ -1,9 +1,9 @@
-using System;
+using System;
namespace AcDream.App.UI.Layout;
/// How a production retained window obtains its outer chrome.
-internal enum RetailWindowChrome
+public enum RetailWindowChrome
{
/// The imported root already is the complete retail outer frame.
Imported,
@@ -21,9 +21,9 @@ internal enum RetailWindowChrome
/// shared nine-slice wrapper, applies exact resize/opacity/visibility policy, mounts
/// the outer frame, and returns its registered typed handle.
///
-internal static class RetailWindowFrame
+public static class RetailWindowFrame
{
- internal sealed record Options
+ public sealed record Options
{
public required string WindowName { get; init; }
public RetailWindowChrome Chrome { get; init; } = RetailWindowChrome.NineSlice;
@@ -87,7 +87,7 @@ internal static class RetailWindowFrame
public static RetailWindowHandle Mount(
UiRoot root,
UiElement content,
- Func resolveChrome,
+ Func resolveChrome,
Options options)
{
ArgumentNullException.ThrowIfNull(root);
diff --git a/src/AcDream.App/UI/Layout/SelectedObjectController.cs b/src/AcDream.App/UI/Layout/SelectedObjectController.cs
index 58544f8c..224f9de8 100644
--- a/src/AcDream.App/UI/Layout/SelectedObjectController.cs
+++ b/src/AcDream.App/UI/Layout/SelectedObjectController.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Numerics;
using AcDream.App.UI;
using AcDream.Core.Items;
@@ -7,7 +7,7 @@ using AcDream.Core.Selection;
namespace AcDream.App.UI.Layout;
///
-/// Controller for the action bar's selected-object strip (ids 0x1000019E–0x100001A4).
+/// Controller for the action bar's selected-object strip (ids 0x1000019E–0x100001A4).
/// Analogue of retail gmToolbarUI::HandleSelectionChanged
/// (docs/research/named-retail/acclient_2013_pseudo_c.txt:198635) +
/// RecvNotice_UpdateObjectHealth (:196213) +
@@ -18,29 +18,29 @@ namespace AcDream.App.UI.Layout;
/// guid is provided it sets the name, flashes the selection overlay briefly, and sends
/// either QueryHealth (0x01BF) for health-bearing targets or
/// QueryItemMana (0x0263) for owned non-stack items. The Health meter
-/// becomes visible only when the server actually reports health for the selected guid —
+/// becomes visible only when the server actually reports health for the selected guid —
/// either an UpdateHealth (0x01C0) arrives (retail
-/// RecvNotice_UpdateObjectHealth → SetVisible(1)) or the value is already
+/// RecvNotice_UpdateObjectHealth → SetVisible(1)) or the value is already
/// cached. So a friendly NPC you have not assessed shows name-only (no bar), and a
-/// monster's bar appears after damage / a successful assess — matching retail.
+/// monster's bar appears after damage / a successful assess — matching retail.
///
///
///
/// Retail element roles (PostInit, :198119): m_pSelObjectField
/// is the container 0x1000019E whose SetState(0x1000000b/0c) drives a
-/// 0.25s Pause→Normal flash that cascades to the overlay child's green frame.
+/// 0.25s Pause→Normal flash that cascades to the overlay child's green frame.
/// acdream has no state-cascade / transition-animation system, so this controller drives
/// the overlay element 0x100001A0 directly and reverts it after the same
/// to reproduce the brief flash. The name element
/// 0x1000019F is bumped to the top of the strip's z-order so it draws OVER the
-/// overlay frame and the health bar (retail draws the name over the bar — see the
+/// overlay frame and the health bar (retail draws the name over the bar — see the
/// "Drudge Slinker" reference shot).
///
///
///
-internal sealed class SelectedObjectController : IRetainedPanelController
+public sealed class SelectedObjectController : IRetainedPanelController
{
- // ── Element ids (toolbar LayoutDesc 0x21000016) ─────────────────────────
+ // ── Element ids (toolbar LayoutDesc 0x21000016) ─────────────────────────
/// Selected-object container / field element id (retail m_pSelObjectField).
public const uint ContainerId = 0x1000019E;
/// Selected-object name element id (retail m_pSelObjectName, UIElement_Text).
@@ -56,24 +56,24 @@ internal sealed class SelectedObjectController : IRetainedPanelController
/// Horizontal stack quantity slider (retail m_pStackSizeSlider).
public const uint StackSizeSliderId = 0x100001A4;
- /// Selection-overlay flash duration — retail's container ObjectSelected state is a
- /// Pause(0.25s)→Normal transition (toolbar dump, element 0x1000019E).
+ /// Selection-overlay flash duration — retail's container ObjectSelected state is a
+ /// Pause(0.25s)→Normal transition (toolbar dump, element 0x1000019E).
private const double FlashSeconds = 0.25;
/// Z-order for the name so it draws OVER the overlay frame + health bar.
- /// The strip's other children sit at ReadOrder 1–4; this floats the name to the top.
+ /// The strip's other children sit at ReadOrder 1–4; this floats the name to the top.
private const int NameZOrderOnTop = 1_000_000;
- /// Z-order for the selection-flash overlay — above the health meter (so the green
+ /// Z-order for the selection-flash overlay — above the health meter (so the green
/// flash isn't hidden by the bar) but below the name (so the name stays readable).
private const int OverlayZOrder = NameZOrderOnTop - 1;
/// Height (px) of the black name band at the top of the 31px bar sprite. The name
- /// label is constrained to this band (top-aligned) so the health bar shows below it —
+ /// label is constrained to this band (top-aligned) so the health bar shows below it —
/// retail "name on the black, bar below". The bar sprite's colored region starts ~y14.
private const float NameBandHeight = 15f;
- // ── Found elements (any may be null for partial/test layouts) ───────────
+ // ── Found elements (any may be null for partial/test layouts) ───────────
private readonly UiElement? _name;
private readonly UiDatElement? _overlay;
private readonly UiMeter? _healthMeter;
@@ -81,7 +81,7 @@ internal sealed class SelectedObjectController : IRetainedPanelController
private readonly UiField? _stackSizeEntry;
private readonly UiScrollbar? _stackSizeSlider;
- // ── Captured delegates ───────────────────────────────────────────────────
+ // ── Captured delegates ───────────────────────────────────────────────────
private readonly Func _isHealthTarget;
private readonly Func _isOwnedByPlayer;
private readonly Func _resolveName;
@@ -97,7 +97,7 @@ internal sealed class SelectedObjectController : IRetainedPanelController
private readonly Action> _unsubscribeItemManaChanged;
private readonly Action> _unsubscribeObjectUpdated;
- // ── Live state (read by closures on the per-frame draw path) ────────────
+ // ── Live state (read by closures on the per-frame draw path) ────────────
private uint? _current;
private string? _currentName;
private double _flashRemaining; // > 0 while the selection overlay is flashing
@@ -143,7 +143,7 @@ internal sealed class SelectedObjectController : IRetainedPanelController
_unsubscribeItemManaChanged = unsubscribeItemManaChanged;
_unsubscribeObjectUpdated = unsubscribeObjectUpdated;
- // Find elements — silently skip absent ones (partial/test layouts).
+ // Find elements — silently skip absent ones (partial/test layouts).
_name = layout.FindElement(NameId);
_overlay = layout.FindElement(OverlayId) as UiDatElement;
_healthMeter = layout.FindElement(HealthMeterId) as UiMeter;
@@ -152,7 +152,7 @@ internal sealed class SelectedObjectController : IRetainedPanelController
_stackSizeSlider = layout.FindElement(StackSizeSliderId) as UiScrollbar;
// The selection-flash overlay must draw OVER the health meter (which spans the whole
- // strip) — otherwise the meter hides the green flash whenever a bar is visible (i.e.
+ // strip) — otherwise the meter hides the green flash whenever a bar is visible (i.e.
// for players/monsters). Float it just below the name so the name stays readable.
if (_overlay is not null) _overlay.ZOrder = OverlayZOrder;
@@ -195,7 +195,7 @@ internal sealed class SelectedObjectController : IRetainedPanelController
//
// The bar sprite (0x0600193E/F, 146x31) carries a ~14px BLACK name band across its
// TOP with the colored bar in the lower portion (confirmed from the dat). Retail
- // draws the object name in that black band with the health bar BELOW it — so the
+ // draws the object name in that black band with the health bar BELOW it — so the
// label is TOP-aligned by constraining its height to the band, not centered over the
// whole 31px strip (which overlapped the bar's middle).
if (_name is not null)
@@ -240,7 +240,7 @@ internal sealed class SelectedObjectController : IRetainedPanelController
/// Imported toolbar layout (LayoutDesc 0x21000016).
/// The single Core selected-object owner.
/// Called once with
- /// (typical host: h => Combat.HealthChanged += h) — drives meter visibility.
+ /// (typical host: h => Combat.HealthChanged += h) — drives meter visibility.
/// Returns true for guids that may show a health meter
/// (proxy for retail's IsPlayer() || pet_owner || ObjectIsAttackable()).
/// Returns retail's NAME_APPROPRIATE display name for a guid.
@@ -296,8 +296,8 @@ internal sealed class SelectedObjectController : IRetainedPanelController
_sendQueryItemMana(0);
}
- // ── 1. Clear first (retail: SetText("") + m_pSelObjectField->SetState(0)
- // + SetVisible(0) on the meters). ──────────────────────────────────────
+ // ── 1. Clear first (retail: SetText("") + m_pSelObjectField->SetState(0)
+ // + SetVisible(0) on the meters). ──────────────────────────────────────
if (selectionChanged)
{
if (_healthMeter is not null) _healthMeter.Visible = false;
@@ -319,15 +319,15 @@ internal sealed class SelectedObjectController : IRetainedPanelController
uint g = guid.Value;
- // ── 2. Name (displayed via the UiText child's LinesProvider reading _currentName). ──
+ // ── 2. Name (displayed via the UiText child's LinesProvider reading _currentName). ──
uint stackSize = _stackSize(g);
string? objectName = _resolveName(g);
_currentName = stackSize > 1u && !string.IsNullOrEmpty(objectName)
? $"{stackSize} {objectName}"
: objectName;
- // ── 3. Selection overlay: brief flash (retail container ObjectSelected
- // = Pause(0.25s)→Normal). "StackedItemSelected" for stacks. ──────────────
+ // ── 3. Selection overlay: brief flash (retail container ObjectSelected
+ // = Pause(0.25s)→Normal). "StackedItemSelected" for stacks. ──────────────
SetOverlayState(stackSize > 1u
? RetailUiStateIds.StackedItemSelected
: RetailUiStateIds.ObjectSelected);
@@ -344,9 +344,9 @@ internal sealed class SelectedObjectController : IRetainedPanelController
if (_stackSizeSlider is not null) _stackSizeSlider.Visible = true;
}
- // ── 4. Health: query, and show the meter only if real health is already known.
+ // ── 4. Health: query, and show the meter only if real health is already known.
// Otherwise the meter appears when OnHealthChanged fires for this guid
- // (retail RecvNotice_UpdateObjectHealth :196213). ──────────────────────────
+ // (retail RecvNotice_UpdateObjectHealth :196213). ──────────────────────────
if (stackSize <= 1u && _isHealthTarget(g))
{
if (selectionChanged)
@@ -378,7 +378,7 @@ internal sealed class SelectedObjectController : IRetainedPanelController
if (_flashRemaining <= 0) return;
_flashRemaining -= deltaSeconds;
if (_flashRemaining <= 0)
- SetOverlayState(UiStateInfo.DirectStateId); // flash done → overlay back to blank
+ SetOverlayState(UiStateInfo.DirectStateId); // flash done → overlay back to blank
}
private void SetOverlayState(uint state)
diff --git a/src/AcDream.App/UI/Layout/SpellExamineComponentTemplateFactory.cs b/src/AcDream.App/UI/Layout/SpellExamineComponentTemplateFactory.cs
index 768ab17f..9d592114 100644
--- a/src/AcDream.App/UI/Layout/SpellExamineComponentTemplateFactory.cs
+++ b/src/AcDream.App/UI/Layout/SpellExamineComponentTemplateFactory.cs
@@ -1,4 +1,4 @@
-using AcDream.Content;
+using AcDream.Content;
using DatReaderWriter;
namespace AcDream.App.UI.Layout;
@@ -9,19 +9,19 @@ namespace AcDream.App.UI.Layout;
/// the component icon as the template root's own UIRegion image while
/// retaining child 0x10000330 as the missing-component overlay.
///
-internal sealed class SpellExamineComponentTemplateFactory
+public sealed class SpellExamineComponentTemplateFactory
{
public const uint TemplateId = 0x1000032Eu;
public const uint MissingOverlayId = 0x10000330u;
private readonly ElementInfo _template;
- private readonly Func _resolveSprite;
+ private readonly Func _resolveSprite;
private readonly UiDatFont? _defaultFont;
private readonly IReadOnlyDictionary _fonts;
public SpellExamineComponentTemplateFactory(
ElementInfo template,
- Func resolveSprite,
+ Func resolveSprite,
UiDatFont? defaultFont,
IReadOnlyDictionary? fonts = null)
{
@@ -33,7 +33,7 @@ internal sealed class SpellExamineComponentTemplateFactory
public static SpellExamineComponentTemplateFactory? TryLoad(
IDatReaderWriter dats,
- Func resolveSprite,
+ Func resolveSprite,
UiDatFont? defaultFont,
Func? resolveFont)
{
@@ -53,7 +53,7 @@ internal sealed class SpellExamineComponentTemplateFactory
fonts);
}
- public UiElement Create(GpuTextureSlot iconTexture, bool owned)
+ public UiElement Create(uint iconTexture, bool owned)
{
ImportedLayout content = LayoutImporter.Build(
_template,
diff --git a/src/AcDream.App/UI/Layout/SpellbookRowStyle.cs b/src/AcDream.App/UI/Layout/SpellbookRowStyle.cs
index 39a9bc6b..668709fa 100644
--- a/src/AcDream.App/UI/Layout/SpellbookRowStyle.cs
+++ b/src/AcDream.App/UI/Layout/SpellbookRowStyle.cs
@@ -1,4 +1,4 @@
-using System.Numerics;
+using System.Numerics;
using AcDream.Content;
using DatReaderWriter;
@@ -9,7 +9,7 @@ namespace AcDream.App.UI.Layout;
/// list points at this prototype through ItemList attribute 0x1000000E;
/// retail clones it for every learned spell.
///
-internal readonly record struct SpellbookRowStyle(
+public readonly record struct SpellbookRowStyle(
float Width,
float Height,
float IconLeft,
diff --git a/src/AcDream.App/UI/Layout/SpellbookWindowController.cs b/src/AcDream.App/UI/Layout/SpellbookWindowController.cs
index b6461f5e..7a408d8d 100644
--- a/src/AcDream.App/UI/Layout/SpellbookWindowController.cs
+++ b/src/AcDream.App/UI/Layout/SpellbookWindowController.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
@@ -10,7 +10,7 @@ using AcDream.Content;
namespace AcDream.App.UI.Layout;
-internal enum SpellbookWindowPage { Spells, Components }
+public enum SpellbookWindowPage { Spells, Components }
///
/// Binds retail's combined spellbook/component-book LayoutDesc 0x21000034.
@@ -19,7 +19,7 @@ internal enum SpellbookWindowPage { Spells, Components }
/// two authored tabs are stateful UIElement_Text controls, matching
/// gmSpellbookUI::PostInit @ 0x0048B2B0 and the resolved retail layout.
///
-internal sealed class SpellbookWindowController : IRetainedPanelController
+public sealed class SpellbookWindowController : IRetainedPanelController
{
public const uint LayoutId = 0x21000034u;
public const uint RootId = 0x100002A8u;
@@ -50,8 +50,8 @@ internal sealed class SpellbookWindowController : IRetainedPanelController
private readonly Func _playerGuid;
private readonly IReadOnlyDictionary _components;
private readonly SelectionState _selection;
- private readonly Func _resolveSpellIcon;
- private readonly Func _resolveComponentIcon;
+ private readonly Func _resolveSpellIcon;
+ private readonly Func _resolveComponentIcon;
private readonly Func _spellLevel;
private readonly Action _selectObject;
private readonly Action _addFavorite;
@@ -86,8 +86,8 @@ internal sealed class SpellbookWindowController : IRetainedPanelController
Func playerGuid,
IReadOnlyDictionary components,
SelectionState selection,
- Func resolveSpellIcon,
- Func resolveComponentIcon,
+ Func resolveSpellIcon,
+ Func resolveComponentIcon,
Func spellLevel,
Action selectObject,
Action addFavorite,
@@ -170,8 +170,8 @@ internal sealed class SpellbookWindowController : IRetainedPanelController
Func playerGuid,
IReadOnlyDictionary components,
SelectionState selection,
- Func resolveSpellIcon,
- Func resolveComponentIcon,
+ Func resolveSpellIcon,
+ Func resolveComponentIcon,
Func spellLevel,
Action selectObject,
Action addFavorite,
diff --git a/src/AcDream.App/UI/Layout/SpellcastingUiController.cs b/src/AcDream.App/UI/Layout/SpellcastingUiController.cs
index 90ecf1ee..216338b7 100644
--- a/src/AcDream.App/UI/Layout/SpellcastingUiController.cs
+++ b/src/AcDream.App/UI/Layout/SpellcastingUiController.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Linq;
using AcDream.App.Spells;
@@ -16,7 +16,7 @@ namespace AcDream.App.UI.Layout;
/// LayoutDesc 0x21000073. Favorites remain server-persisted; casting emits one
/// request through .
///
-internal sealed class SpellcastingUiController : IRetainedPanelController
+public sealed class SpellcastingUiController : IRetainedPanelController
{
public const uint PageId = 0x10000061u;
public const uint SpellNameId = 0x1000048Bu;
@@ -42,8 +42,8 @@ internal sealed class SpellcastingUiController : IRetainedPanelController
private readonly SelectionState _selection;
private readonly ClientObjectTable _objects;
private readonly Func _playerGuid;
- private readonly Func _resolveSpellIcon;
- private readonly Func _resolveItemDragIcon;
+ private readonly Func _resolveSpellIcon;
+ private readonly Func _resolveItemDragIcon;
private readonly Action _useItem;
private readonly Action? _examineSpell;
private readonly Action? _addFavorite;
@@ -73,8 +73,8 @@ internal sealed class SpellcastingUiController : IRetainedPanelController
RuntimeSpellCastState casting,
ClientObjectTable objects,
Func playerGuid,
- Func resolveSpellIcon,
- Func resolveItemDragIcon,
+ Func resolveSpellIcon,
+ Func resolveItemDragIcon,
Action useItem,
Action? examineSpell,
SelectionState selection,
@@ -162,8 +162,8 @@ internal sealed class SpellcastingUiController : IRetainedPanelController
RuntimeSpellCastState casting,
ClientObjectTable objects,
Func playerGuid,
- Func resolveSpellIcon,
- Func resolveItemDragIcon,
+ Func resolveSpellIcon,
+ Func resolveItemDragIcon,
Action useItem,
SelectionState selection,
Action? addFavorite,
@@ -351,7 +351,7 @@ internal sealed class SpellcastingUiController : IRetainedPanelController
var slot = new UiCatalogSlot
{
EntryId = id,
- CatalogIconTexture = metadata is null ? GpuTextureSlot.Unassigned : _resolveSpellIcon(id),
+ CatalogIconTexture = metadata is null ? 0u : _resolveSpellIcon(id),
Label = metadata?.Name ?? $"Spell {id}",
SpriteResolve = list.SpriteResolve,
CatalogDragPayload = new SpellFavoriteDragPayload(tab, position, id),
@@ -530,9 +530,9 @@ internal sealed class SpellcastingUiController : IRetainedPanelController
_endowmentHost.Visible = endowment is not null;
_endowmentSlot.EntryId = _endowmentItemId;
_endowmentSlot.CatalogIconTexture = _endowmentSpellId == 0u
- ? GpuTextureSlot.Unassigned : _resolveSpellIcon(_endowmentSpellId);
+ ? 0u : _resolveSpellIcon(_endowmentSpellId);
_endowmentSlot.CatalogOverlayTexture = endowment is null
- ? GpuTextureSlot.Unassigned : _resolveItemDragIcon(endowment);
+ ? 0u : _resolveItemDragIcon(endowment);
string spellName = _spellbook.TryGetMetadata(_endowmentSpellId, out SpellMetadata metadata)
? metadata.Name : $"Spell {_endowmentSpellId}";
_endowmentSlot.Label = endowment is null
@@ -603,13 +603,13 @@ internal sealed class SpellcastingUiController : IRetainedPanelController
}
}
-internal sealed record SpellFavoriteDragPayload(int SourceTab, int SourcePosition, uint SpellId);
+public sealed record SpellFavoriteDragPayload(int SourceTab, int SourcePosition, uint SpellId);
///
/// A learned-spell shortcut carried from the spellbook. Unlike a favorite drag,
/// lifting this payload never removes anything from its source collection.
///
-internal sealed record SpellbookShortcutDragPayload(uint SpellId);
+public sealed record SpellbookShortcutDragPayload(uint SpellId);
internal static class FavoriteListExtensions
{
diff --git a/src/AcDream.App/UI/Layout/ToolbarController.cs b/src/AcDream.App/UI/Layout/ToolbarController.cs
index 56d05415..9d880d84 100644
--- a/src/AcDream.App/UI/Layout/ToolbarController.cs
+++ b/src/AcDream.App/UI/Layout/ToolbarController.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using AcDream.Core.Combat;
using AcDream.Core.Items;
@@ -8,7 +8,7 @@ using AcDream.Core.Selection;
namespace AcDream.App.UI.Layout;
///
-/// Binds the imported gmToolbarUI window (LayoutDesc 0x21000016) to live data —
+/// Binds the imported gmToolbarUI window (LayoutDesc 0x21000016) to live data —
/// the gm*UI::PostInit analogue. Finds the 18 shortcut slots (UiItemList) by id,
/// populates them from the persisted PlayerDescription shortcuts
/// (UpdateFromPlayerDesc), re-binds deferred slots when an item's CreateObject
@@ -24,7 +24,7 @@ namespace AcDream.App.UI.Layout;
/// CreateObject resolves a formerly-unknown guid.
///
///
-internal sealed class ToolbarController : IItemListDragHandler, IRetainedPanelController
+public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelController
{
// Slot element ids in slot-index order (toolbar LayoutDesc 0x21000016, pre-dump).
// Row 1 = slots 0-8 (0x100001A7..0x100001AF), Row 2 = slots 9-17 (0x100006B7..0x100006BF).
@@ -39,7 +39,7 @@ internal sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCo
// SelectedObjectController owns the health/mana meters and both stack controls,
// including retail's initial-hidden state and selection-driven visibility.
- // Four mutually-exclusive combat-mode indicator elements — exactly one visible at a time.
+ // Four mutually-exclusive combat-mode indicator elements — exactly one visible at a time.
// Index 0 = NonCombat (peace), 1 = Melee, 2 = Missile, 3 = Magic.
// Retail ref: gmToolbarUI::RecvNotice_SetCombatMode (acclient_2013_pseudo_c.txt:196632-196669)
// SetVisible's exactly one element depending on the incoming mode.
@@ -64,9 +64,9 @@ internal sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCo
private readonly ClientObjectTable _repo;
private readonly CombatState? _combatState;
private readonly ShortcutStore _store;
- private readonly Func _iconIds; // (itemType, icon, underlay, overlay, effects) → GL tex
- private readonly Func? _dragIconIds;
- private readonly Action _useItem; // guid → fire UseObject
+ private readonly Func _iconIds; // (itemType, icon, underlay, overlay, effects) → GL tex
+ private readonly Func? _dragIconIds;
+ private readonly Action _useItem; // guid → fire UseObject
private readonly Action? _sendAddShortcut;
private readonly Action? _sendRemoveShortcut; // (index)
private readonly ItemInteractionController? _itemInteraction;
@@ -83,8 +83,8 @@ internal sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCo
// Retail ref: UIElement_UIItem::SetShortcutNum (acclient_2013_pseudo_c.txt:229465);
// gmToolbarUI::RecvNotice_SetCombatMode (196610-196621) re-stamps ghosting.
// Occupancy branch (decomp 229481):
- // occupied → regular 0x10000042 / ghosted 0x10000043
- // empty → background digit 0x1000005e (stance-independent)
+ // occupied → regular 0x10000042 / ghosted 0x10000043
+ // empty → background digit 0x1000005e (stance-independent)
private uint[]? _regularDigits;
private uint[]? _ghostedDigits;
private uint[]? _emptyDigits;
@@ -94,7 +94,7 @@ internal sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCo
ImportedLayout layout,
ClientObjectTable repo,
ShortcutStore shortcuts,
- Func iconIds,
+ Func iconIds,
Action useItem,
CombatState? combatState,
uint[]? regularDigits,
@@ -110,7 +110,7 @@ internal sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCo
Func