diff --git a/src/AcDream.App/Rendering/Wb/ManagedGLTextureArray.cs b/src/AcDream.App/Rendering/Wb/ManagedGLTextureArray.cs
index eb69e11f..0ca01a58 100644
--- a/src/AcDream.App/Rendering/Wb/ManagedGLTextureArray.cs
+++ b/src/AcDream.App/Rendering/Wb/ManagedGLTextureArray.cs
@@ -41,6 +41,16 @@ namespace AcDream.App.Rendering.Wb {
public ulong BindlessClampHandle { get; private set; }
public long TotalSizeInBytes => CalculateTotalSize();
+ ///
+ /// #105 diagnostic: staged layer updates (PBO writes + pending list) 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.
+ ///
+ public int PendingUpdateCount {
+ get { lock (_mipmapLock) { return _pendingUpdates.Count; } }
+ }
+
public ManagedGLTextureArray(OpenGLGraphicsDevice graphicsDevice, TextureFormat format, int width, int height,
int size, ILogger logger, TextureParameters? texParams = null) {
var p = texParams ?? TextureParameters.Default;
diff --git a/src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs b/src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs
index 03b7dd61..118e0134 100644
--- a/src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs
+++ b/src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs
@@ -301,6 +301,23 @@ 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.
+ ///
+ public (int PendingUpdates, int ArraysWithPending, int TotalArrays) GetPendingTextureUpdateStats() {
+ int pending = 0, arraysWith = 0, total = 0;
+ foreach (var atlasList in _globalAtlases.Values) {
+ foreach (var atlas in atlasList) {
+ total++;
+ int p = atlas.TextureArray.PendingUpdateCount;
+ if (p > 0) { arraysWith++; pending += p; }
+ }
+ }
+ return (pending, arraysWith, total);
+ }
+
///
/// Decrement reference count and unload GPU resources if no longer needed.
///
diff --git a/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs b/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs
index ecf25798..5f2e8e44 100644
--- a/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs
+++ b/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs
@@ -212,6 +212,56 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
{
_meshManager.UploadMeshData(meshData);
}
+
+ bool texProbe = AcDream.Core.Rendering.RenderingDiagnostics.ProbeTexFlushEnabled;
+ var pendingBefore = texProbe
+ ? _meshManager.GetPendingTextureUpdateStats()
+ : default;
+
+ // #105 root cause (2026-06-10): TextureAtlasManager.AddTexture only STAGES
+ // texture content (PBO write + 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 only ever reached the GPU as a side effect of PBO growth,
+ // and every layer staged after an array's LAST growth kept undefined
+ // TexStorage3D content behind a valid resident bindless handle — 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
+ // WB-equivalent position.
+ _meshManager.GenerateMipmaps();
+
+ if (texProbe)
+ EmitTexFlushProbe(pendingBefore);
+ }
+
+ // #105 apparatus state — see RenderingDiagnostics.ProbeTexFlushEnabled.
+ private int _lastTexFlushBefore = -1;
+ private int _texFlushHeartbeat;
+
+ ///
+ /// #105 apparatus: one [tex-flush] line on change of the staged-texture
+ /// pending picture (plus a ~10 s heartbeat while anything is stuck). A healthy
+ /// frame ends with after=0; before==after>0 persisting at
+ /// standstill is the white-walls mechanism live (staged uploads never applied).
+ ///
+ private void EmitTexFlushProbe((int PendingUpdates, int ArraysWithPending, int TotalArrays) before)
+ {
+ var after = _meshManager!.GetPendingTextureUpdateStats();
+ bool changed = before.PendingUpdates != _lastTexFlushBefore;
+ bool flushed = after.PendingUpdates != before.PendingUpdates;
+ bool heartbeat = after.PendingUpdates > 0 && ++_texFlushHeartbeat >= 600;
+ if (!changed && !flushed && !heartbeat) return;
+
+ _texFlushHeartbeat = 0;
+ _lastTexFlushBefore = before.PendingUpdates;
+ Console.WriteLine(
+ $"[tex-flush] before={before.PendingUpdates} after={after.PendingUpdates}" +
+ $" arrays={after.ArraysWithPending}/{after.TotalArrays}" +
+ $" (arraysBefore={before.ArraysWithPending})");
}
private void PopulateMetadata(ulong id)
diff --git a/src/AcDream.Core/Rendering/RenderingDiagnostics.cs b/src/AcDream.Core/Rendering/RenderingDiagnostics.cs
index 46eb3c64..f219fbb4 100644
--- a/src/AcDream.Core/Rendering/RenderingDiagnostics.cs
+++ b/src/AcDream.Core/Rendering/RenderingDiagnostics.cs
@@ -172,6 +172,21 @@ public static class RenderingDiagnostics
public static bool ProbeClipRouteEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_CLIPROUTE") == "1";
+ ///
+ /// #105 white-indoor-textures apparatus (2026-06-10). When true, WbMeshAdapter.Tick
+ /// emits one [tex-flush] line whenever the staged-texture-update picture changes:
+ /// pending layer updates across all shared atlases BEFORE and AFTER the per-frame
+ /// ObjectMeshManager.GenerateMipmaps() flush, plus arrays-with-pending / total-array
+ /// counts. The broken contract this pins: TextureAtlasManager.AddTexture only STAGES
+ /// pixel data (PBO + pending list); without the per-frame flush (WB GameScene.cs:975) the
+ /// data never reaches the GL texture and the batch samples undefined content behind a valid
+ /// bindless handle — the classic white walls. A healthy run shows after=0 on every
+ /// line; a stuck before==after>0 at standstill is the #105 mechanism live.
+ /// Initial state from ACDREAM_PROBE_TEXFLUSH=1.
+ ///
+ public static bool ProbeTexFlushEnabled { get; set; } =
+ Environment.GetEnvironmentVariable("ACDREAM_PROBE_TEXFLUSH") == "1";
+
///
/// Bounded-propagation port apparatus (2026-06-08). When true, PortalVisibilityBuilder.Build emits
/// one [portal-churn] summary line per call: per-cell pop count (re-pops = churn), total re-enqueues,