diff --git a/docs/research/2026-08-21-retail-building-detail-texturing-pseudocode.md b/docs/research/2026-08-21-retail-building-detail-texturing-pseudocode.md
index 0bf362d9..9ea8f024 100644
--- a/docs/research/2026-08-21-retail-building-detail-texturing-pseudocode.md
+++ b/docs/research/2026-08-21-retail-building-detail-texturing-pseudocode.md
@@ -105,9 +105,17 @@ for each replayed fragment:
detail = sample(categoryTexture, baseUv * categoryTiling)
diffuseAlpha = base_subset_diffuse_alpha // 1 for opaque; the
// translucency-fade multiplier
- // for a fading subset
- // (tmpmaterial.Diffuse.a in
- // RenderMeshSubset)
+ // for a fading subset.
+ // tmpmaterial.Diffuse.a = 1f
+ // (0x0059cb99) is the
+ // burnedInStaticLights < 0 &&
+ // *(render_device+0x7e4) == 0
+ // branch in RenderMeshSubset;
+ // the other branch leaves
+ // diffuse FromVertex. Either
+ // way the opaque->1 /
+ // fading->opacity mapping
+ // still holds.
// D3DPolyRender::SetSurface (0x0059c4d0) texture-stage setup:
// stage 0 colour = MODULATE(TEXTURE, DIFFUSE) = base.rgb * diffuse.rgb
@@ -176,6 +184,18 @@ change base coverage/blend/depth behavior; it avoids making the checkbox
reorder the default transparent scene. The connected acceptance matrix must
still exercise overlapping transparent building/EnvCell surfaces.
+Also unmodelled: retail's stage-1 OUTPUT alpha — `MODULATE(TEXTURE, CURRENT)`
+(`0x0059c549`) — which for a delayed-alpha subset becomes the framebuffer
+blend weight the alpha queue composites that subset with. acdream instead
+draws the base subset with its own alpha and a second, separately blended
+draw weighted by `detail.a * diffuseAlpha` (the pipeline in
+`VulkanViewportMapping.BlendFactorsOf(GpuBlendMode.RetailDetail)`). For
+opaque subsets this is identical (both reduce to the base's own alpha
+gating nothing else downstream); on translucent building/EnvCell subsets it
+is a bounded difference in how much the SUBSEQUENT alpha-queue compositing
+sees, already covered by the existing AP-34 shared-alpha-queue divergence
+row rather than a new one.
+
## Darkening, not brightening
The earlier version of this note read the two-pass fallback's `DEST_COLOR +
diff --git a/src/AcDream.App/Rendering/RetailDetailTextureContract.cs b/src/AcDream.App/Rendering/RetailDetailTextureContract.cs
index c647d5ab..36fc600a 100644
--- a/src/AcDream.App/Rendering/RetailDetailTextureContract.cs
+++ b/src/AcDream.App/Rendering/RetailDetailTextureContract.cs
@@ -77,4 +77,33 @@ internal static class RetailDetailTextureContract
///
internal static bool IsNeutral(Vector4 detail, float opacity) =>
detail.W * opacity == 0f;
+
+ ///
+ /// Review fix (post-05970306): retail's D3D fog stage runs AFTER the
+ /// texture-stage combine, applying to the FINAL pixel, not to
+ /// detail.rgb in isolation. acdream draws the combine as two
+ /// separate passes (mesh_modern's base draw, then mesh_detail's blended
+ /// replay), so each draw fogs its OWN colour before the fixed-function
+ /// blend recombines them — this is the CPU statement of that two-draw
+ /// path: lerp(mix(base,fog,f), mix(detail,fog,f), detail.a*opacity).
+ /// It is algebraically identical to retail's single-draw
+ /// fog-after-combine order, mix(Expected(base,detail,opacity), fog,
+ /// f) — see RetailDetailTextureContractTests for the identity
+ /// pinned numerically, and mesh_detail.frag's header comment for the
+ /// derivation.
+ ///
+ internal static Vector3 ExpectedFogged(
+ Vector3 baseColour,
+ Vector4 detail,
+ float opacity,
+ Vector3 fog,
+ float fogFactor)
+ {
+ Vector3 foggedBase = Vector3.Lerp(baseColour, fog, fogFactor);
+ Vector3 foggedDetail = Vector3.Lerp(
+ new Vector3(detail.X, detail.Y, detail.Z),
+ fog,
+ fogFactor);
+ return Vector3.Lerp(foggedBase, foggedDetail, detail.W * opacity);
+ }
}
diff --git a/src/AcDream.App/Rendering/Shaders/mesh_detail.frag b/src/AcDream.App/Rendering/Shaders/mesh_detail.frag
index 7a0ec49b..380c0c22 100644
--- a/src/AcDream.App/Rendering/Shaders/mesh_detail.frag
+++ b/src/AcDream.App/Rendering/Shaders/mesh_detail.frag
@@ -4,6 +4,7 @@
in vec2 vBaseUv;
in vec2 vDetailUv;
in float vDetailOpacity;
+in vec3 vWorldPos;
in flat uint vBaseTextureIndex;
in flat uint vBaseTextureLayer;
in flat uint vBatchFlags;
@@ -11,6 +12,42 @@ in flat uint vDetailCategory;
uniform uint uTextureIndexA; // category detail texture, layer 0
+// SceneLighting UBO — IDENTICAL layout to mesh_modern.frag binding=1 (same
+// std140 block, same struct, same binding). Declared here ONLY for fog
+// (uFogParams/uFogColor/uCameraAndTime); mesh_detail never lights (no
+// uLights[]/uCellAmbient read), it only needs applyFog below to match
+// retail's fog placement exactly.
+struct Light {
+ vec4 posAndKind;
+ vec4 dirAndRange;
+ vec4 colorAndIntensity;
+ vec4 coneAngleEtc;
+};
+layout(std140, ACDREAM_UBO_SET binding = 1) uniform SceneLighting {
+ Light uLights[8];
+ vec4 uCellAmbient;
+ vec4 uFogParams;
+ vec4 uFogColor;
+ vec4 uCameraAndTime;
+};
+
+// Copied verbatim from mesh_modern.frag — same math, not "improved". Retail's
+// D3D fixed-function fog stage runs AFTER the texture-stage pipeline
+// (RenderDeviceD3D's fog render state applies to the final pixel the
+// blender produced, not to an individual texture stage's output), so the
+// detail contribution must be fogged exactly like the base pass fogs its
+// own colour, with the identical fog curve.
+vec3 applyFog(vec3 lit, vec3 worldPos) {
+ int mode = int(uFogParams.w);
+ if (mode == 0) return lit;
+ float d = length(worldPos - uCameraAndTime.xyz);
+ float fogStart = uFogParams.x;
+ float fogEnd = uFogParams.y;
+ float span = max(1e-3, fogEnd - fogStart);
+ float fog = clamp((d - fogStart) / span, 0.0, 1.0);
+ return mix(lit, uFogColor.xyz, fog);
+}
+
out vec4 FragColor;
// VM2 (2026-08-22, live cdb read on the PDB-paired retail client, GUID
@@ -37,6 +74,20 @@ out vec4 FragColor;
// that combined alpha and lets the fixed-function blend unit do the
// base*(1-a) + detail*a lerp. See
// docs/research/2026-08-22-vm2-retail-detail-path-cdb.md.
+//
+// Review fix (post-05970306): retail's texture-stage combine above happens
+// BEFORE the D3D fixed-function fog stage, not after — fog is the LAST thing
+// applied to the pixel, so it applies to the lerp's result, not to detail.rgb
+// alone. mesh_modern.frag already fogs the base colour before this replay
+// draws over it (applyFog(rgb, vWorldPos) there), so fogging detail.rgb here
+// too makes the two-draw blend collapse to retail's single-draw order:
+// (1-a)*mix(base,fog,f) + a*mix(detail,fog,f)
+// = (1-f)*[(1-a)*base + a*detail] + f*fog
+// = mix(lerp(base,detail,a), fog, f)
+// which is exactly retail's fog-after-combine pixel. Leaving detail.rgb
+// unfogged would draw detail at full saturation/brightness even at maximum
+// fog distance. RetailDetailTextureContractTests pins this identity
+// numerically (ExpectedFogged).
void main() {
// Object command replays may contain ordinary instances; only building
@@ -56,5 +107,5 @@ void main() {
uTextureIndexA,
vec3(vDetailUv, 0.0));
- FragColor = vec4(detail.rgb, detail.a * vDetailOpacity);
+ FragColor = vec4(applyFog(detail.rgb, vWorldPos), detail.a * vDetailOpacity);
}
diff --git a/src/AcDream.App/Rendering/Shaders/mesh_detail.vert b/src/AcDream.App/Rendering/Shaders/mesh_detail.vert
index ad43ece2..9c8b8dad 100644
--- a/src/AcDream.App/Rendering/Shaders/mesh_detail.vert
+++ b/src/AcDream.App/Rendering/Shaders/mesh_detail.vert
@@ -70,6 +70,8 @@ uniform float uParamB; // 1 = require building instance, 0 = EnvCell category
out vec2 vBaseUv;
out vec2 vDetailUv;
out float vDetailOpacity;
+out vec3 vWorldPos; // review fix: mesh_detail.frag needs this for applyFog,
+ // exactly like mesh_modern.vert's vWorldPos.
out flat uint vBaseTextureIndex;
out flat uint vBaseTextureLayer;
out flat uint vBatchFlags;
@@ -80,6 +82,7 @@ void main() {
int instanceIndex = transformIndex - int(uTextureIndexB);
vec4 worldPos = Instances[transformIndex].transform * vec4(aPosition, 1.0);
gl_Position = uViewProjection * worldPos;
+ vWorldPos = worldPos.xyz;
uint slot = instanceClipSlot[instanceIndex];
CellClip clip = clipRegions[slot];
diff --git a/src/AcDream.App/Rendering/Shaders/spv/mesh_detail.frag.spv b/src/AcDream.App/Rendering/Shaders/spv/mesh_detail.frag.spv
index 95f1748a..caf4e76d 100644
Binary files a/src/AcDream.App/Rendering/Shaders/spv/mesh_detail.frag.spv and b/src/AcDream.App/Rendering/Shaders/spv/mesh_detail.frag.spv differ
diff --git a/src/AcDream.App/Rendering/Shaders/spv/mesh_detail.vert.spv b/src/AcDream.App/Rendering/Shaders/spv/mesh_detail.vert.spv
index 033db8b4..03ed2c55 100644
Binary files a/src/AcDream.App/Rendering/Shaders/spv/mesh_detail.vert.spv and b/src/AcDream.App/Rendering/Shaders/spv/mesh_detail.vert.spv differ
diff --git a/src/AcDream.App/Rendering/Shaders/spv/shaders.manifest.json b/src/AcDream.App/Rendering/Shaders/spv/shaders.manifest.json
index ed1abf25..9e1b750d 100644
--- a/src/AcDream.App/Rendering/Shaders/spv/shaders.manifest.json
+++ b/src/AcDream.App/Rendering/Shaders/spv/shaders.manifest.json
@@ -231,12 +231,12 @@
"stages": [
{
"stage": "vert",
- "sourceSha256": "c955bdce56199ef2057dbaf0dc1d1f75ececac49047f57685b09b63117e921da",
+ "sourceSha256": "3b03a153a439aa54fcd0bcb575274c8e1da888b8ae85ee15fae020ccb1d3bf49",
"compiled": true
},
{
"stage": "frag",
- "sourceSha256": "c02dd48647352a87c183fe925a9590b70731c677dd942bcea71ac6ad63ff486d",
+ "sourceSha256": "e037fd28cf71453792c17522c97c4c7797193823ed78ca629a4331b632f177b1",
"compiled": true
}
]
diff --git a/src/AcDream.App/Rendering/Wb/EnvCellRenderer.Rhi.cs b/src/AcDream.App/Rendering/Wb/EnvCellRenderer.Rhi.cs
index f61a27a6..1e22472b 100644
--- a/src/AcDream.App/Rendering/Wb/EnvCellRenderer.Rhi.cs
+++ b/src/AcDream.App/Rendering/Wb/EnvCellRenderer.Rhi.cs
@@ -343,10 +343,12 @@ public sealed unsafe partial class EnvCellRenderer
}
// Retail DrawEnvCell category (2). Replay the already-filtered opaque
- // shell commands, including ClipMap built-mesh subsets, and apply the
- // 10-50 m positive-view-depth fade. The existing
- // "Building Detail Textures" option gates both this and buildings,
- // matching LScape::ChangeRegion.
+ // shell commands, including ClipMap built-mesh subsets. No distance
+ // fade (VM1/VM2): retail's noFadeDetail gates get_alpha_for_z to the
+ // immediate-polygon path only, which built meshes never reach;
+ // attenuation is the sampler's linear mip chain converging to the
+ // texture mean. The existing "Building Detail Textures" option gates
+ // both this and buildings, matching LScape::ChangeRegion.
if (renderPass == WbRenderPass.Opaque
&& detailEnabled)
{
diff --git a/tests/AcDream.App.Tests/Rendering/RetailDetailTextureContractTests.cs b/tests/AcDream.App.Tests/Rendering/RetailDetailTextureContractTests.cs
index 1e4bd303..33630fd8 100644
--- a/tests/AcDream.App.Tests/Rendering/RetailDetailTextureContractTests.cs
+++ b/tests/AcDream.App.Tests/Rendering/RetailDetailTextureContractTests.cs
@@ -103,4 +103,46 @@ public sealed class RetailDetailTextureContractTests
Assert.True(MathF.Abs(result.Y - detail.Y) < 1e-6f);
Assert.True(MathF.Abs(result.Z - detail.Z) < 1e-6f);
}
+
+ [Fact]
+ public void FogAppliedPerDrawMatchesRetailsFogAfterCombineOrder()
+ {
+ // Review fix (post-05970306): retail fogs the pixel AFTER the
+ // texture-stage combine (mix(lerp(base,detail,a), fog, f)), but
+ // acdream draws the combine as two separate passes, each fogging its
+ // own colour before the fixed-function blend recombines them
+ // (lerp(mix(base,fog,f), mix(detail,fog,f), a)). This pins that the
+ // two orders are the same pixel for arbitrary inputs — see
+ // ExpectedFogged's doc comment and mesh_detail.frag's header comment
+ // for the algebra.
+ var random = new Random(0x226226);
+ for (int i = 0; i < 200; i++)
+ {
+ var baseColour = NextColour(random);
+ var detail = new Vector4(NextColour(random), random.NextSingle());
+ float opacity = random.NextSingle();
+ var fog = NextColour(random);
+ float fogFactor = random.NextSingle();
+
+ Vector3 twoDrawOrder = RetailDetailTextureContract.ExpectedFogged(
+ baseColour, detail, opacity, fog, fogFactor);
+ Vector3 retailOrder = Vector3.Lerp(
+ RetailDetailTextureContract.Expected(baseColour, detail, opacity),
+ fog,
+ fogFactor);
+
+ Assert.True(
+ MathF.Abs(twoDrawOrder.X - retailOrder.X) < 1e-6f,
+ $"R: two-draw {twoDrawOrder.X} vs retail {retailOrder.X} (sample {i})");
+ Assert.True(
+ MathF.Abs(twoDrawOrder.Y - retailOrder.Y) < 1e-6f,
+ $"G: two-draw {twoDrawOrder.Y} vs retail {retailOrder.Y} (sample {i})");
+ Assert.True(
+ MathF.Abs(twoDrawOrder.Z - retailOrder.Z) < 1e-6f,
+ $"B: two-draw {twoDrawOrder.Z} vs retail {retailOrder.Z} (sample {i})");
+ }
+ }
+
+ private static Vector3 NextColour(Random random) =>
+ new(random.NextSingle(), random.NextSingle(), random.NextSingle());
}
diff --git a/tests/AcDream.App.Tests/Rendering/TerrainAtlasDetailTextureTests.cs b/tests/AcDream.App.Tests/Rendering/TerrainAtlasDetailTextureTests.cs
new file mode 100644
index 00000000..ca048f71
--- /dev/null
+++ b/tests/AcDream.App.Tests/Rendering/TerrainAtlasDetailTextureTests.cs
@@ -0,0 +1,173 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Reflection;
+using AcDream.App.Rendering;
+using AcDream.App.Rendering.Gpu;
+using AcDream.App.Rendering.Wb;
+using AcDream.App.Tests.Rendering.Gpu;
+using AcDream.Content;
+using DatReaderWriter;
+using DatReaderWriter.DBObjs;
+using DatReaderWriter.Enums;
+using DatReaderWriter.Lib.IO;
+using DatReaderWriter.Types;
+using Xunit;
+
+namespace AcDream.App.Tests.Rendering;
+
+///
+/// Campaign VM VM1 review fix: pins the two facts the #226 note relies on
+/// for retail's mip-driven attenuation to actually be true — TerrainAtlas
+/// .TryCreateDetailTexture (~TerrainAtlas.cs:488-556) must upload a FULL
+/// mip chain, and it must be sampled with the repeat/linear world sampler,
+/// or the "attenuation is the sampler's linear mip chain" claim in
+/// mesh_detail.vert's header comment and the VM1 commit is unverified.
+/// Drives the private method directly (reflection, same pattern as
+/// EnvCellRendererTests' private-method tests) against a synthetic
+/// PFID_A8R8G8B8 RenderSurface, so no installed DAT is required — this lane
+/// stays hermetic.
+///
+public sealed class TerrainAtlasDetailTextureTests
+{
+ private const uint SurfaceTextureId = 0x05001787u;
+ private const uint RenderSurfaceId = 0x06006D58u;
+
+ [Fact]
+ public void DetailTexture_UploadsFullMipChainAndUsesRepeatLinearSampler()
+ {
+ const int width = 4;
+ const int height = 4;
+
+ using var device = new RecordingGpuDevice();
+ device.Clear();
+
+ var dats = new FakeDetailTextureDats();
+ dats.Register(new SurfaceTexture
+ {
+ Textures = new List> { RenderSurfaceId },
+ }, SurfaceTextureId);
+ dats.Register(new RenderSurface
+ {
+ Width = width,
+ Height = height,
+ Format = PixelFormat.PFID_A8R8G8B8,
+ SourceData = new byte[width * height * 4],
+ }, RenderSurfaceId);
+
+ var terrain = new TMTerrainDesc
+ {
+ TerrainTex = new TerrainTex
+ {
+ DetailTextureId = SurfaceTextureId,
+ DetailTexTiling = 4u,
+ },
+ };
+
+ IGpuSampler sampler = device.CreateSampler(GpuSamplerDescription.WorldRepeat);
+
+ MethodInfo method = typeof(TerrainAtlas).GetMethod(
+ "TryCreateDetailTexture",
+ BindingFlags.NonPublic | BindingFlags.Static)!;
+ object? result = method.Invoke(
+ null,
+ new object[] { device, dats, sampler, terrain, "building" });
+
+ Assert.NotNull(result);
+
+ // Full mip chain: TerrainAtlas.cs sizes MipLevelCount from
+ // RhiWorldTextureArray.MipLevelsFor(decoded.Width, decoded.Height) and
+ // then calls GenerateMipChain() — not a single-level upload.
+ RecordingGpuTexture texture = Assert.Single(device.CreatedTextures);
+ Assert.Equal(width, texture.Width);
+ Assert.Equal(height, texture.Height);
+ int expectedMipLevels = RhiWorldTextureArray.MipLevelsFor(width, height);
+ Assert.True(expectedMipLevels > 1, "the test fixture must exercise a real mip chain, not a 1x1 edge case");
+ Assert.Equal(expectedMipLevels, texture.MipLevelCount);
+ Assert.True(texture.MipChainGenerated);
+
+ // Repeat/linear sampler: the exact sampler GpuBindingModel world
+ // draws use, not WorldClamp (the alpha atlas' sampler) or any
+ // point-filtered UI sampler.
+ GpuRecordedTextureRegistration registration = Assert.Single(
+ device.Calls.OfType());
+ Assert.Equal(GpuSamplerDescription.WorldRepeat, registration.Sampler);
+ Assert.Equal(GpuFilter.Linear, registration.Sampler.MinFilter);
+ Assert.Equal(GpuFilter.Linear, registration.Sampler.MagFilter);
+ Assert.Equal(GpuMipFilter.Linear, registration.Sampler.MipFilter);
+ Assert.Equal(GpuAddressMode.Repeat, registration.Sampler.AddressU);
+ Assert.Equal(GpuAddressMode.Repeat, registration.Sampler.AddressV);
+ }
+
+ ///
+ /// Minimal synthetic — same shape as the
+ /// NoopDatReaderWriter pattern already used for hermetic tests
+ /// (LiveEntityNetworkOnPositionCollapseMatrixTests), except Get
+ /// resolves from an explicit id->object map instead of always missing.
+ /// Every other member is unreachable by TryCreateDetailTexture and throws
+ /// if that assumption ever changes.
+ ///
+ private sealed class FakeDetailTextureDats : IDatReaderWriter
+ {
+ private readonly Dictionary _objects = new();
+
+ public void Register(T obj, uint id) where T : IDBObj => _objects[id] = obj;
+
+ public string SourceDirectory => string.Empty;
+ public IDatDatabase Portal => throw new NotSupportedException();
+ public IDatDatabase Cell => throw new NotSupportedException();
+ public ReadOnlyDictionary CellRegions { get; } =
+ new(new Dictionary());
+ public IDatDatabase HighRes => throw new NotSupportedException();
+ public IDatDatabase Language => throw new NotSupportedException();
+ public IDatDatabase Local => throw new NotSupportedException();
+ public ReadOnlyDictionary RegionFileMap { get; } =
+ new(new Dictionary());
+ public int PortalIteration => 0;
+ public int CellIteration => 0;
+ public int HighResIteration => 0;
+ public int LanguageIteration => 0;
+
+ public bool TryGetFileBytes(
+ uint regionId,
+ uint fileId,
+ ref byte[] bytes,
+ out int bytesRead)
+ {
+ bytesRead = 0;
+ return false;
+ }
+
+ public IEnumerable GetAllIdsOfType() where T : IDBObj =>
+ Array.Empty();
+
+ public IEnumerable ResolveId(uint id) =>
+ Array.Empty();
+
+ public bool TrySave(T obj, int iteration = 0) where T : IDBObj =>
+ throw new NotSupportedException();
+
+ public bool TrySave(uint regionId, T obj, int iteration = 0) where T : IDBObj =>
+ throw new NotSupportedException();
+
+ [return: MaybeNull]
+ public T Get(uint fileId) where T : IDBObj =>
+ _objects.TryGetValue(fileId, out IDBObj? obj) && obj is T typed ? typed : default;
+
+ public bool TryGet(uint fileId, [MaybeNullWhen(false)] out T value) where T : IDBObj
+ {
+ if (_objects.TryGetValue(fileId, out IDBObj? obj) && obj is T typed)
+ {
+ value = typed;
+ return true;
+ }
+ value = default;
+ return false;
+ }
+
+ public void Dispose()
+ {
+ }
+ }
+}
diff --git a/tests/AcDream.App.Tests/Rendering/Wb/EnvCellRendererTests.cs b/tests/AcDream.App.Tests/Rendering/Wb/EnvCellRendererTests.cs
index e6ae4cbc..4ae2c907 100644
--- a/tests/AcDream.App.Tests/Rendering/Wb/EnvCellRendererTests.cs
+++ b/tests/AcDream.App.Tests/Rendering/Wb/EnvCellRendererTests.cs
@@ -116,8 +116,11 @@ public class EnvCellRendererTests
/// pass, mesh_modern's shell pipeline and (when enabled) mesh_detail's
/// replay alike.
///
- [Fact]
- public void SubmitRhi_BindsConstantOneInstanceAlphaBeforeAnyDrawInThePass()
+ [Theory]
+ [InlineData(WbRenderPass.Opaque)]
+ [InlineData(WbRenderPass.Transparent)]
+ public void SubmitRhi_BindsConstantOneInstanceAlphaBeforeAnyDrawInThePass(
+ WbRenderPass renderPass)
{
const int instanceCount = 5;
@@ -144,7 +147,13 @@ public class EnvCellRendererTests
// Seed one real draw command, exactly what
// RenderModernMDIInternal would have built from a live landblock,
// so the test can assert an actual ordering against a real draw
- // rather than a vacuous "no draw happened" pass.
+ // rather than a vacuous "no draw happened" pass. Same command for
+ // both passes: with _buildingDetailEnabled left at its default
+ // (false), the Transparent branch's own detail-interleave logic
+ // (SubmitRhi's "detailEnabled" block) never fires, so it draws
+ // through the identical MultiDrawIndexedIndirect call the Opaque
+ // pass does — only the pipeline rebound inside the per-range loop
+ // differs, which is not what this test is pinning.
Type rendererType = typeof(EnvCellRenderer);
FieldInfo commandsField = rendererType.GetField(
"_commands", BindingFlags.NonPublic | BindingFlags.Instance)!;
@@ -184,7 +193,7 @@ public class EnvCellRendererTests
"SubmitRhi", BindingFlags.NonPublic | BindingFlags.Instance)!;
submitRhi.Invoke(
renderer,
- new object[] { allInstances, WbRenderPass.Opaque, 1, instanceCount });
+ new object[] { allInstances, renderPass, 1, instanceCount });
IReadOnlyList calls = device.Calls;
int alphaBindIndex = -1;