feat(render): add GpuBlendMode.InverseAlpha and re-scope Campaign V4c
A scouting pass over V4c stopped before writing code and reported two structural blockers. Both verified against source. The pinned V0 contract was missing a blend mode. WbDrawDispatcher.ApplyRetailBlend selects three blend functions from each DAT surface's TranslucencyKind, and InvAlpha - OneMinusSrcAlpha over SrcAlpha - had no representation. Blend is baked into the pipeline and is not dynamic, so it could not be handled at the encoder, and folding it onto StraightAlpha would have silently changed how every inverse-alpha surface composites. ParticleRenderer needs it too. The contract grows here, in one reviewed commit, rather than a slice inventing a workaround for it. Retiring V2's interim handle table turns out to be its own slice. The renderers only intern bindless handles; the raw ulong is produced by the texture caches, baked into ObjectRenderBatch, and carried by GroupKey - the bucketing key V4c is forbidden to change - and by CachedBatch, where it gates cache validity. That is now V4t, with its own pixel gate. Until it lands, the world renderers bind their existing interim tables through the encoder as ordinary storage buffers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
a475732587
commit
111e72362f
4 changed files with 65 additions and 1 deletions
|
|
@ -501,7 +501,8 @@ because sample positions are not specified across implementations.
|
|||
| **V3** | Clip-space and sRGB audit: verify every projection producer is [0,1] convention, confirm clip-plane derivation, record the sRGB swapchain decision and the depth-precision divergence class here. | pixel gate + connected lifecycle |
|
||||
| **V4a** | `TextRenderer` (three fence-buffered VBO sets → ring allocations), `BitmapFont`, `DebugLineRenderer`, the UI RenderSurface upload path, `UiViewport`'s texture handoff. | pixel gate (UI-heavy checkpoints) |
|
||||
| **V4b** | `GlobalMeshBuffer` + `ObjectMeshManager` onto `IGpuBuffer`; arena, LRU and ledger logic untouched. | pixel gate |
|
||||
| **V4c** | **The large one.** `WbDrawDispatcher` + `EnvCellRenderer`: per-frame uploads → rings, MDI brackets → pipelines + `MultiDrawIndexedIndirect`, `ClipFrame`, `SceneLightingUboBinding`, timer scopes. `RetailAlphaQueue` untouched. | pixel gate at several checkpoints + connected lifecycle |
|
||||
| **V4c** | **The large one.** `WbDrawDispatcher` + `EnvCellRenderer`: per-frame uploads → rings, MDI brackets → pipelines + `MultiDrawIndexedIndirect`, loose uniforms → push constants, timer scopes. `RetailAlphaQueue` and all bucketing untouched. **Narrowed after the V4c scouting report — see §5.3.** | pixel gate at several checkpoints + connected lifecycle |
|
||||
| **V4t** | **World texture stack** (added 2026-07-27, see §5.3): `TextureCache`, `CompositeTextureArrayCache`, `ManagedGLTextureArray`, `TerrainAtlas` and `ObjectMeshManager`'s material path onto `IGpuTexture`/`IGpuSampler`; retype `GroupKey`, `CachedBatch` and `ObjectRenderBatch` from `ulong` bindless handle to `GpuTextureSlot`; retire the interim per-renderer handle tables for V4c, V4d and V4e at once. | pixel gate |
|
||||
| **V4d** | `TerrainModernRenderer` + `TerrainAtlas`. | pixel gate |
|
||||
| **V4e** | `ParticleRenderer` (after V4c — shared alpha-queue contract). | pixel gate (particle-heavy checkpoint) |
|
||||
| **V4f** | `SkyRenderer` + weather. | pixel gate (dawn/dusk, day group pinned) |
|
||||
|
|
@ -536,6 +537,41 @@ recycling — becomes reachable. Two small, separately pixel-gated changes beat
|
|||
entangled one; separating the data-model change from the RHI plumbing change is
|
||||
precisely what de-risks V4c, the largest slice in the campaign.
|
||||
|
||||
### 5.3 Why V4c was narrowed, and where V4t came from
|
||||
|
||||
A scouting pass over V4c (2026-07-27) stopped before writing code and reported two
|
||||
structural blockers. Both were verified against source; both were real.
|
||||
|
||||
**The contract was missing a blend mode.** `WbDrawDispatcher.ApplyRetailBlend`
|
||||
(`WbDrawDispatcher.cs:3191`) selects one of *three* blend functions from each DAT
|
||||
surface's `TranslucencyKind`: `AlphaBlend` → `(SrcAlpha, OneMinusSrcAlpha)`,
|
||||
`Additive` → `(SrcAlpha, One)`, and **`InvAlpha` → `(OneMinusSrcAlpha, SrcAlpha)`**.
|
||||
The V0 contract shipped `GpuBlendMode` with only the first two. Blend is baked into
|
||||
the pipeline and is not dynamic, so this could not be worked around at the encoder;
|
||||
mapping `InvAlpha` onto `StraightAlpha` would have silently changed how every
|
||||
inverse-alpha surface composites. `ParticleRenderer` hits the same wall twice, so
|
||||
V4e was blocked on it too. Fixed by adding `GpuBlendMode.InverseAlpha` to the
|
||||
contract with a test asserting all three retail kinds are representable. This is
|
||||
the correct outcome of a pinned contract meeting reality: the contract grew, in one
|
||||
reviewed commit, rather than a slice inventing a workaround.
|
||||
|
||||
**Retiring the interim handle table is its own slice.** §5.2 assumed V4c could
|
||||
switch to the device's texture table. It cannot: the renderers do not own the
|
||||
bindless handles, they only intern them. A raw `ulong` is produced by
|
||||
`TextureCache`, `CompositeTextureArrayCache`, `ManagedGLTextureArray` and
|
||||
`TerrainAtlas`, baked into `ObjectRenderBatch`, and carried by **`GroupKey`** — the
|
||||
bucketing key V4c is explicitly forbidden to change — and by `CachedBatch`, where it
|
||||
is compared for cache validity. Switching to `GpuTextureSlot` therefore means
|
||||
porting the whole texture stack and retyping three data-model records, which is most
|
||||
of V4d and V4e plus work no slice contained. That is now **V4t**, with its own pixel
|
||||
gate. Until it lands, V4c/V4d/V4e bind their existing interim tables through the
|
||||
encoder as ordinary storage buffers at binding 9 — no new escape hatch.
|
||||
|
||||
**Also deferred to V4h:** `ClipFrame`'s region buffer (binding 2) is read by terrain
|
||||
as well, and the `SceneLighting` UBO (binding 1) by terrain and the four viewport and
|
||||
portal renderers. GL binding points are global, so the safe move while those consumers
|
||||
are still raw GL is to leave both bound as they are and convert them with the spine.
|
||||
|
||||
**Sequencing invariants.** The app ships on GL until V10. V0→V1→V2→V3→V4a…V4h
|
||||
are strictly sequential. The only permitted parallelism is V5 alongside V4d
|
||||
and/or V4f (fully disjoint files), and optionally V9's `.github`/`tools`-only
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ internal static class GlEnumMapping
|
|||
{
|
||||
GpuBlendMode.StraightAlpha => (BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha),
|
||||
GpuBlendMode.Additive => (BlendingFactor.SrcAlpha, BlendingFactor.One),
|
||||
GpuBlendMode.InverseAlpha => (BlendingFactor.OneMinusSrcAlpha, BlendingFactor.SrcAlpha),
|
||||
// GpuBlendMode.None never reaches glBlendFunc — blending is disabled instead.
|
||||
_ => throw new NotSupportedException($"No GL blend factors for {blend}."),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -120,6 +120,18 @@ internal enum GpuBlendMode
|
|||
|
||||
/// <summary>Additive: <c>SrcAlpha, One</c>.</summary>
|
||||
Additive,
|
||||
|
||||
/// <summary>
|
||||
/// Retail's inverse-alpha translucency: <c>OneMinusSrcAlpha, SrcAlpha</c>.
|
||||
///
|
||||
/// Added at slice V4c, which found that `WbDrawDispatcher.ApplyRetailBlend`
|
||||
/// selects three blend functions from each DAT surface's `TranslucencyKind`,
|
||||
/// and this third one had no representation in the V0 contract. Mapping it
|
||||
/// onto <see cref="StraightAlpha"/> would have been a retail-fidelity
|
||||
/// regression, not a simplification, so the contract grew instead.
|
||||
/// `ParticleRenderer` needs it too (slice V4e).
|
||||
/// </summary>
|
||||
InverseAlpha,
|
||||
}
|
||||
|
||||
internal enum GpuCompareOp
|
||||
|
|
|
|||
|
|
@ -71,6 +71,21 @@ public sealed class GpuContractTests
|
|||
Assert.Equal(144, GpuBindingModel.ClipRegionStrideBytes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BlendModesCoverEveryRetailTranslucencyKind()
|
||||
{
|
||||
// WbDrawDispatcher.ApplyRetailBlend selects a blend function from each DAT
|
||||
// surface's TranslucencyKind. Retail has three, and the V0 contract shipped
|
||||
// with only two — slice V4c found the gap. Mapping InvAlpha onto
|
||||
// StraightAlpha would silently change how every inverse-alpha surface
|
||||
// composites, so the contract has to carry all three.
|
||||
Assert.Equal(4, Enum.GetValues<GpuBlendMode>().Length);
|
||||
Assert.Contains(GpuBlendMode.None, Enum.GetValues<GpuBlendMode>());
|
||||
Assert.Contains(GpuBlendMode.StraightAlpha, Enum.GetValues<GpuBlendMode>());
|
||||
Assert.Contains(GpuBlendMode.Additive, Enum.GetValues<GpuBlendMode>());
|
||||
Assert.Contains(GpuBlendMode.InverseAlpha, Enum.GetValues<GpuBlendMode>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UnassignedTextureSlotIsNeverAValidIndex()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue