fix(rendering): preserve opaque building coverage under detail MSAA (#473)
This commit is contained in:
parent
d35ed1aece
commit
bf23673f3d
7 changed files with 387 additions and 46 deletions
|
|
@ -41,7 +41,8 @@ remain closed. See
|
|||
|
||||
## #473 — Owner gate: exterior buildings have missing roof/wall sections
|
||||
|
||||
**Status:** IN-PROGRESS — owner visual gate FAIL, 2026-09-05.
|
||||
**Status:** IN-PROGRESS — bounded code repair complete; owner visual re-gate
|
||||
pending, 2026-09-05.
|
||||
|
||||
At clean campaign `02219318a`, Release 0 warnings / 0 errors, the owner
|
||||
reports broken exterior buildings in Holtburg and the cathedral, while
|
||||
|
|
@ -52,7 +53,12 @@ A reachable draw-state defect is now identified (packet section42): detail
|
|||
final alpha is wrongly converted into MSAA sample coverage on opaque Wb
|
||||
exteriors. The owner's 4x/A2C=true log and installed detail alpha24..46/255
|
||||
exercise that path. Interior EnvCell disables A2C. A bounded pipeline repair
|
||||
and actual exterior re-gate remain; no claim that all symptoms are fixed yet.
|
||||
now forces the shared detail-active opaque Wb command onto the non-A2C opaque
|
||||
pipeline while retaining pure Clip's AP-240 quality-selected arm. Actual
|
||||
ordered and classic grouped recording tests cover ordinary/atmospheric 1x/4x,
|
||||
including a mixed ordinary/building instance command, and the old-arm mutation
|
||||
fails 12/12 discriminators. The actual exterior re-gate remains; no claim that
|
||||
all symptoms are fixed yet.
|
||||
|
||||
Evidence: `research/2026-09-01-overhaul/owner-gate-20260905-holtburg-fail.png`;
|
||||
manual session `logs/owner-gate-20260905-02219318a/`, PID18836, verified
|
||||
|
|
|
|||
|
|
@ -278,6 +278,7 @@ builder, reducing the current active total to **160**.
|
|||
|
||||
| # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle |
|
||||
|---|---|---|---|---|---|
|
||||
| ~~AP-245~~ | **FILED AND CODE-RETIRED 2026-09-05 by #473 / Campaign OVERHAUL §42.** S5-c4's shared detail-active Wb selector reused the quality-selected opaque pipeline for `RetailSetSurfaceBlend.Opaque`. With 4x MSAA and A2C enabled, the shader's retail final detail alpha `X = a*qA*qA` therefore became sample coverage even though retail's ordinary opaque SetSurface state is ONE/ZERO without alpha test. The selector now uses the non-A2C `pipelines.Opaque` for every detail-bearing opaque command; pure Clip retains AP-240's `opaquePipeline`. A classic command may contain both ordinary and building-detail instances because detail category is per instance rather than part of `GroupKey`; such a command is correctly non-A2C as a whole while its shader still filters detail per instance. | `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.Rhi.cs` (`PipelineForMaterial`); `tests/AcDream.App.Tests/Rendering/Walk/OrderPreservingSubmitterTests.cs` | The correction is one shared material-selection arm reached by the actual ordered and classic grouped submitters. Recording tests cover ordinary/atmospheric 1x/4x descriptors, draw order/count, depth and detail pushes; detail unavailable/disabled and quality A2C=false stay unchanged. | None for the retired code divergence. The owner exterior gate remains FAIL until the repaired exact binary passes the required real re-gate; this row does not claim GPU or owner acceptance. | `D3DPolyRender::SetSurface @0x0059C4D0` opaque ONE/ZERO arm; Campaign OVERHAUL §42 |
|
||||
| AP-244 | **Filed 2026-09-05 at Campaign OVERHAUL S5-c4 fix round 1.** The new exact resolved SetSurface state is deliberately selected only when a Wb/EnvCell command has active building detail, preserving §23's bounded detail-off behavior. With detail off, Wb still collapses raw Additive and InvAlpha+Additive through `TranslucencyKind.Additive` to `SRCALPHA,ONE`; EnvCell additionally selects raw `IsAdditive` before the late `Translucent\|ClipMap` override and has no detail-off inverse pipeline. Both shader families retain the existing non-detail 0.05 discard rather than every combined ClipMap's palette/DDS reference, and fixed-function fog remains enabled for raw-Additive detail-off rows. AP-240 separately covers Wb pure-Clip placement/A2C. | `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.Rhi.cs` (`PipelineForBlend`, detail gates); `src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs` (`ResolveBatchGroupIndex`); `src/AcDream.App/Rendering/Wb/EnvCellRenderer.Rhi.cs` (detail-active exact selection versus retained range buckets); `src/AcDream.App/Rendering/Shaders/mesh_modern.frag`; `src/AcDream.App/Rendering/Shaders/mesh_atmospheric.frag` | Fix round 1 was expressly bounded to detail-active one-pass material truth. The immutable state is already present end-to-end, so a later dedicated detail-off correction can select it without DAT lookup or new ownership, but must re-gate FIFO/routing/order and visual behavior together. | Detail-off raw Additive/inverse-additive brightness, inverse EnvCell compositing, combined ClipMap cutout threshold, late Translucent override, and raw-Additive fog can differ from retail. | `D3DPolyRender::SetSurface @0x0059C4D0` lines 425083–425303; Campaign OVERHAUL S5-c4 §23.2 |
|
||||
| AP-235 | **Filed 2026-08-25 at the Campaign CT4 fix round.** Retail resolves gender display text via `AppraisalSystem::InqGenderDisplayName @0x005b47c0` and heritage via `InqHeritageGroupDisplayName @0x005b4710`, both through the static `EnumMapper::GetString(uint32_t enumValue, uint32_t queryId, PStringBase<char>*) @0x0041ac40` overload — `DBObj::GetDIDByEnum(&did, enumValue, 1)` (master map `0x25000000` → category-1 sub-map `0x25000001` → `ClientEnumToID[0x10000001]`/`[0x10000002]` → EnumMapper DIDs `0x2200000A`/`0x2200000B`) — reading each id's `IdToStringMap` entry live, with heritage ids 2/5/0xd hardcoded to `"Gharu'ndim"`/`"Umbraen"`/`"Olthoi"` in place of the raw internal names `"Gharundim"`/`"Shadowbound"`/`"OlthoiAcid"`. `CharacterIdentityText.GenderDisplayName`/`HeritageGroupDisplayName` are hardcoded C# `switch` tables instead — a mechanism divergence (compile-time constant vs. live DAT read), not a content one: `CharacterPanelLiveDatTests.GenderHeritageDisplayNameTables_MatchTheRetailEnumMapperChain` (filed the same round) walks the live EnumMapper chain and asserts every table entry byte-exact, including the two entries (10 "Penumbraen", 12 "Olthoi") the CT4 review had flagged as unverified guesses — both are correct. | `src/AcDream.App/UI/Layout/CharacterIdentityText.cs` (`GenderDisplayName`, `HeritageGroupDisplayName`); `src/AcDream.App/UI/Layout/RetailAppraisalNameResolver.cs` (`ResolveHeritage` — CT5 fix round 2026-08-25 deleted its independent re-implementation of the same 2/5/13 overrides; it now delegates straight to `CharacterIdentityText.HeritageGroupDisplayName`, so this row's divergence has exactly ONE owner, not two) | `RetailDataIdResolver.Resolve` (`src/AcDream.Content/RetailDataIdResolver.cs`) already ports the generic two-level `GetDIDByEnum` chain (used today for layout/material DIDs); unifying gender/heritage onto it needs only `Resolve(dats, enumValue: 0x10000001u/0x10000002u, enumCategory: 1u)` plus an `EnumMapper.IdToStringMap` read — a live-DAT-only path with no bespoke traversal code to write, which is why the tables stayed hardcoded this round rather than porting live-read on the spot; CT5 is the natural landing slot since it already owns this same DAT-lookup family for the Titles page | A future DAT/game update that renames or reorders a heritage/gender enum entry would silently desync acdream's hardcoded tables from retail's live text with no build-time or runtime signal — the CT5 fix round retired the second-copy drift risk (`ResolveHeritage` now reads the same single table), but the core hardcoded-vs-live-DAT divergence itself remains open | `AppraisalSystem::InqGenderDisplayName @ 0x005B47C0`; `InqHeritageGroupDisplayName @ 0x005B4710`; `EnumMapper::GetString @ 0x0041AC40`; `DBObj::GetDIDByEnum @ 0x004153A0` |
|
||||
| AP-233 | **Filed 2026-08-23 at the Holtburg windmill fix (row owed since the R1-P5 sequencer cutover).** `AnimationSequencer.BuildBlendedFrame` blends each part between `floor(FrameNumber)` and the next frame in the playback direction using the retail slerp (`SlerpRetailClient`). Retail never blends animation frames: `CPartArray::UpdateParts` applies `CSequence::get_curr_animframe` = `get_part_frame(floor(frame_number))`, holding every authored 30 fps frame for its whole interval. Since 2026-08-23 the blend holds the boundary frame at BOTH ends of a node's window — including the cyclic seam — so a cycle's last→first transition is retail's hard cut, not a blend. | `src/AcDream.Core/Physics/AnimationSequencer.cs` (`BuildBlendedFrame`); tests `AnimationSequencerTests.Advance_LinkTailDoesNotBlendIntoLinkFrame0` (#61), `Advance_CyclicSeamHoldsLastFrameInsteadOfBlendingIntoFrame0` (windmill) | The blend only smooths between authored interior frames of one node; at every seam the pose is exactly retail's held frame. Authored cycles that loop by symmetry (the Holtburg windmill's 60-frame quarter turn, `0x0300061B`) or by design read identically at the seam; link tails hold their end pose (#61). The owner chose this over dropping the blend (retail's 30 fps stepping) on 2026-08-23. | Any two adjacent authored frames that are NOT meant to be traversed smoothly (a deliberate authored pop inside a node) would be smoothed where retail pops; none known. A per-frame hitch of one held 33 ms interval at each cycle seam is the price of the cut (1.5° on the windmill). | `CPartArray::UpdateParts @0x005190F0`; `CSequence::get_curr_animframe @0x00524970`; `CSequence::get_curr_frame_number @0x005249D0` |
|
||||
|
|
|
|||
|
|
@ -696,7 +696,7 @@ Update immediately when a slice changes state. Chat is not the ledger.
|
|||
| S5-#470 | **LANDED + REVIEW-CLOSED 2026-09-05; LEAD GRAPHICAL A/B PROVISIONAL PASS.** Campaign implementation stack `15a796c3a` -> `7506e5f14` -> `b333edb4f`; route `19b44e5e3`; reviewed scratch `51f974da4` -> `2cad9c84` -> `98c004aa7`; packet §§27–30. | G4 UNPASSED | Retail/behavior pass 2/10 and production pass 4/10 closed the two evidence-only findings. Fresh campaign Release 0W/0E and focused 48/48; exact scratch hermetic 16,976/16,976 and canonical InstalledDat 386/10/1, manifests 30/30. Pinned gate `logs/selfgate-20260905-032132-s5-470-pinned-shadow-ab-r1`: retail/off -> High -> retail/off, 3/3 PNGs, exit 0, graceful, no client; visual PASS provisional. The stale recipe-8 pre-route launch is excluded and recorded in §30. |
|
||||
| S5-#469 | **LANDED + REVIEW-CLOSED 2026-09-05; LEAD GRAPHICAL TRANSITION PROVISIONAL PASS.** Contract `809887524`; reviewed scratch `c09b6cf0f`; campaign implementation `94ddde69a`; route `62efc72cb`; packet §§31–32. | G4 UNPASSED | Retail/deviation pass 1/10 and production pass 2/10 both PASS with no finding. The two atmospheric vertex receivers keep retail's authored unnormalized `uLights` direction across every shadow gate; celestial direction remains for opt-in shadow/volumetric projection; IA-24 corrected in the same commit. Exact scratch: Release 0W/0E, focused 126/126, Vulkan 2/2, hermetic 16,983/16,983, InstalledDat 386/10/1. Fresh campaign Release 0W/0E, focused 82/82, Vulkan 2/2. Gate `logs/selfgate-20260905-040449-s5-469-lighting-transition-r1`: five PNGs, active 2,500-caster/four-cascade High rows, no near-black relight, exit 0/graceful, no client; visual PASS provisional. |
|
||||
| S5-c5 | **CLOSED + LANDED 2026-09-05; LEAD GRAPHICAL SMOKE PROVISIONAL PASS.** Contract `b77989c32`; campaign `bf53e2ad6` -> `e625dc4e6` -> `1b7ee4e58`; reviewed scratch tip `158656f0d`; packet §§33–35. | G4 UNPASSED | Retail/deletion pass 1/10 PASS. Production pass 2/10 found one static-field hole in the owner guard; fix round 1 reproduced both static-owner mutations, and production pass 3/10 PASS. Exact scratch hermetic 16,921/16,921, canonical InstalledDat 368/9/1, manifests 30/30. Fresh campaign Release 0W/0E, App 146/146, Core 8/8. A stale recipe-8 preflight is excluded; a non-destructive recipe-10 bake produced 2,237,865 keys with zero failures. Corrected gate `logs/selfgate-20260905-052130-s5-c5-landed-v10`: five PNGs, exit 0/graceful, no fatal/deleted-prefix match, no client left; geometry matches S5-c4, visual PASS provisional. |
|
||||
| S5 | **OWNER VISUAL GATE FAIL — #473 exterior roof/wall sections missing (2026-09-05).** c1–c5 plus #470/#469 landed; prior self-PASS is not owner acceptance. C1 offline evidence complete; C1c test correction frozen pending review. | G4 | Packet §§7 and 12–42 bind. Owner's clean02219318a Release/recipe10 gate failed Holtburg/cathedral exteriors; Facility interiors appear correct. §42 contracts the identified detail-active opaque A2C pipeline repair. The 4x/A2C=true owner path converts low detail alpha into lost coverage; a tested repair and correctly framed exterior re-gate remain. Prior screenshots did not cover complete exteriors. After repair/reviews/re-gate: C1c review/landing, real geometry/membership hash, full-lane closure, C2 and full owner G4 remain. Never merge main before G4. |
|
||||
| S5 | **OWNER VISUAL GATE FAIL — #473 code-repaired and review-closed; exterior re-gate pending (2026-09-05).** c1–c5 plus #470/#469 landed; prior self-PASS is not owner acceptance. C1 offline evidence complete; C1c test correction frozen pending review. | G4 | Packet §§7 and 12–42 bind. §42 repairs detail-active opaque A2C coverage loss at the owner's 4x/A2C=true settings. Both independent review lenses PASS; 45 focused/46 register-inclusive/158 adjacent tests pass; restored old-arm mutation fails12/12. All52 affected base/LOD prepared payloads match fresh extraction. Correctly framed full Holtburg/cathedral exterior PNG re-gate remains; prior images were inadequate. Then C1c review/landing, real geometry/membership hash, full-lane closure, C2 and full owner G4 remain. Never merge main before G4. |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -4010,3 +4010,66 @@ PASS/FAIL; close only the lead's own session gracefully. Lead PASS remains
|
|||
provisional until the owner re-gates. Keep #473 open if rendered sections
|
||||
remain missing, even if the narrow tests pass. No merge main; C1c and all
|
||||
remaining closeout/G4 obligations resume only after this failure is handled.
|
||||
|
||||
### 42.4 Implementation return — uncommitted, pending review and real re-gate
|
||||
|
||||
At base `d35ed1aec`, the bounded production repair changes only
|
||||
`PipelineForMaterial`'s detail-active Opaque arm from `opaquePipeline` to
|
||||
`pipelines.Opaque`; the pure Clip arm and every shader, prepared-data,
|
||||
geometry, LOD, queue and recipe path are unchanged. The actual ordered and
|
||||
classic grouped recording paths now cover ordinary and atmospheric pipeline
|
||||
sets at 1x and 4x. They pin exact non-A2C/A2C descriptors, opaque depth state,
|
||||
draw order/count and detail push fields. The classic proof also includes one
|
||||
real mixed command with detail categories `[0,1]`: because category is not in
|
||||
`GroupKey`, the whole detail-bearing command uses non-A2C while the shader's
|
||||
per-instance category still controls detail sampling. Detail unavailable,
|
||||
detail disabled and quality A2C=false remain on their pre-repair selections.
|
||||
|
||||
The old-arm mutation (`Opaque => opaquePipeline`) failed all 12 selected
|
||||
ordered/classic cases. The first ordered failure expected
|
||||
`wb-mesh-opaque` at pipeline transcript position 2 but received
|
||||
`wb-mesh-opaque-a2c`; the first classic failure expected
|
||||
`wb-mesh-atmospheric-opaque-1x` at position 1 but received
|
||||
`wb-mesh-atmospheric-opaque-a2c-1x`. The mixed-command discriminator likewise
|
||||
expected non-A2C and received A2C. The one-line repair was restored and the
|
||||
complete `OrderPreservingSubmitterTests` class passed 45/45 in Release.
|
||||
`dotnet build AcDream.slnx -c Release --no-restore` then completed with zero
|
||||
warnings and zero errors. A final no-build run of that class plus the existing
|
||||
register/count guard passed 46/46. `git diff --check` is clean. No client was
|
||||
launched; JSON preflights before each build/test found no AcDream, retail or
|
||||
debugger process and ACE UDP9000 PID13340. The owner gate remains FAIL and the
|
||||
required exact-binary exterior re-gate is pending sequential review and lead
|
||||
execution.
|
||||
|
||||
### 42.5 Lead review closure — graphical acceptance still pending
|
||||
|
||||
Sequential independent OpenAI review round1 passed both lenses without
|
||||
findings: Rawls checked named SetSurface, paired PE bytes, installed detail
|
||||
alpha, callers and retained material behavior; Wegener checked production
|
||||
reachability, state/order/ownership, actual dispatcher tests and gate honesty.
|
||||
The lead independently read the complete six-file diff, verified the same
|
||||
retail byte ranges and TRX mutation failures, and ran the adjacent existing
|
||||
material suite: 158/158 passed, zero skipped. Exact result files are under
|
||||
`tests/AcDream.App.Tests/TestResults/`: `s5-473-old-arm-mutation.trx` (0/12),
|
||||
`s5-473-focused-restored-final.trx` (45/45),
|
||||
`s5-473-focused-and-register-final.trx` (46/46), and
|
||||
`s5-473-lead-adjacent-material.trx` (158/158).
|
||||
|
||||
The tested candidate App.dll and the test directory's App.dll were identical:
|
||||
6F8890AA5E7FA16641EB5E9AB8CEDAC748E9CD5084853964EDCB5512EBE48E71.
|
||||
This is pre-commit candidate identity, not the subsequent graphical binary.
|
||||
Lead independently expanded the installed A9B4/F418 base/nonzero-LOD union
|
||||
and compared fresh canonical extraction against the actual recipe10 package:
|
||||
all52 complete ObjectMeshDataSerializer payloads match, no missing or corrupt
|
||||
read and no differing payload. This rules out that package/extractor mismatch
|
||||
for these meshes, not GPU upload or final coverage. It is not a replacement
|
||||
for C1's real placed geometry/membership witness.
|
||||
|
||||
No review finding or implementation fix round was needed. Mixed classic
|
||||
commands use one non-A2C opaque pipeline for all instances if any has detail;
|
||||
only wholly unarmed commands/global detail-off preserve quality-selected A2C.
|
||||
The owner expressly authorized closing/testing. Their original process had
|
||||
already exited gracefully without lead input. Lead may now commit, rebuild
|
||||
Release with no client present and execute the correctly framed exterior gate.
|
||||
Owner FAIL remains authoritative until that rendered result is inspected;
|
||||
there is no G4 or physical-GPU PASS in this review closure.
|
||||
|
|
|
|||
|
|
@ -58,9 +58,10 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
/// <summary>
|
||||
/// The four pipeline buckets a translucency kind resolves to, independent
|
||||
/// of any live <see cref="MeshPipelineSet"/> instance. <see cref="AlphaToCoverage"/>
|
||||
/// selects between <c>Opaque</c> and <c>OpaqueAlphaToCoverage</c> uniformly
|
||||
/// for the whole submission — it never varies per command — so merge-run
|
||||
/// legality only needs to know WHICH bucket a command falls in, not which
|
||||
/// selects between <c>Opaque</c> and <c>OpaqueAlphaToCoverage</c> for the
|
||||
/// default opaque bucket. A solo detail-category command may then select
|
||||
/// its exact material pipeline (including non-A2C Opaque), so merge-run
|
||||
/// legality needs the bucket plus the existing detail boundary, not the
|
||||
/// concrete <see cref="IGpuPipeline"/> that resolves to. That is what lets
|
||||
/// <see cref="BuildOrderedMergeRuns"/> stay pure CPU logic, testable
|
||||
/// without a live GPU device — the same separation
|
||||
|
|
|
|||
|
|
@ -858,7 +858,7 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
RetailSetSurfaceMaterialState material,
|
||||
IGpuPipeline opaquePipeline) => material.Blend switch
|
||||
{
|
||||
RetailSetSurfaceBlend.Opaque => opaquePipeline,
|
||||
RetailSetSurfaceBlend.Opaque => pipelines.Opaque,
|
||||
RetailSetSurfaceBlend.StraightAlpha => material.AlphaTestEnabled
|
||||
? pipelines.AlphaBlendDepthWrite
|
||||
: pipelines.AlphaBlend,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Numerics;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Gpu;
|
||||
|
|
@ -288,58 +289,201 @@ public sealed class OrderPreservingSubmitterTests
|
|||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false, 200f / 255f)]
|
||||
[InlineData(true, 100f / 255f)]
|
||||
public void PrepareThenDraw_OrdinaryBuildingClipBuildingOrdinary_ArmsOnePassInPlace(
|
||||
bool paletted,
|
||||
float expectedReference)
|
||||
[InlineData(false, 1)]
|
||||
[InlineData(false, 4)]
|
||||
[InlineData(true, 1)]
|
||||
[InlineData(true, 4)]
|
||||
public void PrepareThenDraw_OrdinaryBuildingClipBuildingOrdinary_ArmsOnePassWithoutOpaqueCoverage(
|
||||
bool atmospheric,
|
||||
int sampleCount)
|
||||
{
|
||||
using var fx = new DispatcherFixture(detailAvailable: true, detailEnabled: true);
|
||||
foreach (bool paletted in new[] { false, true })
|
||||
{
|
||||
using var fx = new DispatcherFixture(
|
||||
detailAvailable: true,
|
||||
detailEnabled: true,
|
||||
atmospheric: atmospheric,
|
||||
sampleCount: sampleCount);
|
||||
using DrawScope draw = fx.BeginDraw();
|
||||
|
||||
OrderedDrawStream stream = StreamOf(
|
||||
MakeCommand(0),
|
||||
MakeCommand(1, detailCategory: 1),
|
||||
MakeCommand(
|
||||
2,
|
||||
translucency: TranslucencyKind.ClipMap,
|
||||
detailCategory: 1,
|
||||
materialState: RetailSetSurfaceMaterialState.Resolve(
|
||||
SurfaceType.Base1ClipMap,
|
||||
texturePresent: true,
|
||||
textureHasPalette: paletted)),
|
||||
MakeCommand(3));
|
||||
|
||||
PrepareAndDrawWhole(fx.Dispatcher, draw, stream);
|
||||
|
||||
string prefix = atmospheric ? "wb-mesh-atmospheric" : "wb-mesh";
|
||||
string suffix = sampleCount == 1 ? "-1x" : string.Empty;
|
||||
string opaque = $"{prefix}-opaque{suffix}";
|
||||
string opaqueA2c = $"{prefix}-opaque-a2c{suffix}";
|
||||
List<(GpuPushConstants Constants, int Start, int Count)> runs = DecodeRuns(fx.Device);
|
||||
Assert.Equal([(0, 1), (1, 1), (2, 1), (3, 1)],
|
||||
runs.Select(run => (run.Start, run.Count)).ToList());
|
||||
Assert.Equal((0u, 0f, 0f), DetailFields(runs[0].Constants));
|
||||
Assert.Equal((77u, 3.5f, 0f), DetailFields(runs[1].Constants));
|
||||
Assert.Equal(
|
||||
(77u, 3.5f, paletted ? 100f / 255f : 200f / 255f),
|
||||
DetailFields(runs[2].Constants));
|
||||
Assert.Equal((0u, 0f, 0f), DetailFields(runs[3].Constants));
|
||||
Assert.Equal(
|
||||
[opaque, opaqueA2c, opaque, opaqueA2c, opaqueA2c],
|
||||
fx.Device.Calls.OfType<GpuRecordedPipelineBind>()
|
||||
.Select(call => call.PipelineName)
|
||||
.ToArray());
|
||||
Assert.Equal(4, fx.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>().Count());
|
||||
|
||||
AssertOpaquePipeline(fx.Device, opaque, atmospheric, sampleCount, alphaToCoverage: false);
|
||||
AssertOpaquePipeline(fx.Device, opaqueA2c, atmospheric, sampleCount, alphaToCoverage: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false, 1)]
|
||||
[InlineData(false, 4)]
|
||||
[InlineData(true, 1)]
|
||||
[InlineData(true, 4)]
|
||||
public void ClassicGrouped_OrdinaryBuildingClipBuildingOrdinary_ArmsOnePassWithoutOpaqueCoverage(
|
||||
bool atmospheric,
|
||||
int sampleCount)
|
||||
{
|
||||
using var fx = new DispatcherFixture(
|
||||
detailAvailable: true,
|
||||
detailEnabled: true,
|
||||
atmospheric: atmospheric,
|
||||
sampleCount: sampleCount);
|
||||
using DrawScope draw = fx.BeginDraw();
|
||||
|
||||
OrderedDrawStream stream = StreamOf(
|
||||
MakeCommand(0),
|
||||
MakeCommand(1, detailCategory: 1),
|
||||
MakeCommand(
|
||||
ExecuteClassicGroups(
|
||||
fx,
|
||||
MakeClassicGroup(0, detailCategory: 0),
|
||||
MakeClassicGroup(1, detailCategory: 1),
|
||||
MakeClassicGroup(
|
||||
2,
|
||||
translucency: TranslucencyKind.ClipMap,
|
||||
detailCategory: 1,
|
||||
translucency: TranslucencyKind.ClipMap,
|
||||
materialState: RetailSetSurfaceMaterialState.Resolve(
|
||||
SurfaceType.Base1ClipMap,
|
||||
texturePresent: true,
|
||||
textureHasPalette: paletted)),
|
||||
MakeCommand(3));
|
||||
textureHasPalette: false)),
|
||||
MakeClassicGroup(3, detailCategory: 0));
|
||||
|
||||
string prefix = atmospheric ? "wb-mesh-atmospheric" : "wb-mesh";
|
||||
string suffix = sampleCount == 1 ? "-1x" : string.Empty;
|
||||
string opaque = $"{prefix}-opaque{suffix}";
|
||||
string opaqueA2c = $"{prefix}-opaque-a2c{suffix}";
|
||||
var transcript = DecodePipelineRuns(fx.Device);
|
||||
Assert.Equal([opaqueA2c, opaque, opaqueA2c, opaqueA2c],
|
||||
transcript.Select(entry => entry.Pipeline));
|
||||
Assert.All(transcript, entry => Assert.Equal(1u, entry.Draw.DrawCount));
|
||||
Assert.Equal((0u, 0f, 0f), DetailFields(transcript[0].Constants));
|
||||
Assert.Equal((77u, 3.5f, 0f), DetailFields(transcript[1].Constants));
|
||||
Assert.Equal((77u, 3.5f, 200f / 255f), DetailFields(transcript[2].Constants));
|
||||
Assert.Equal((0u, 0f, 0f), DetailFields(transcript[3].Constants));
|
||||
AssertOpaquePipeline(fx.Device, opaque, atmospheric, sampleCount, alphaToCoverage: false);
|
||||
AssertOpaquePipeline(fx.Device, opaqueA2c, atmospheric, sampleCount, alphaToCoverage: true);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false, 1)]
|
||||
[InlineData(false, 4)]
|
||||
[InlineData(true, 1)]
|
||||
[InlineData(true, 4)]
|
||||
public void ClassicGrouped_MixedOrdinaryAndBuildingInstances_UsesNonCoverageForWholeCommand(
|
||||
bool atmospheric,
|
||||
int sampleCount)
|
||||
{
|
||||
using var fx = new DispatcherFixture(
|
||||
detailAvailable: true,
|
||||
detailEnabled: true,
|
||||
atmospheric: atmospheric,
|
||||
sampleCount: sampleCount);
|
||||
using DrawScope draw = fx.BeginDraw();
|
||||
WbDrawDispatcher.InstanceGroup mixed = MakeClassicGroup(0, detailCategory: 0);
|
||||
AppendClassicInstance(mixed, index: 1, detailCategory: 1);
|
||||
|
||||
ExecuteClassicGroups(fx, mixed);
|
||||
|
||||
string prefix = atmospheric ? "wb-mesh-atmospheric" : "wb-mesh";
|
||||
string suffix = sampleCount == 1 ? "-1x" : string.Empty;
|
||||
var entry = Assert.Single(DecodePipelineRuns(fx.Device));
|
||||
Assert.Equal($"{prefix}-opaque{suffix}", entry.Pipeline);
|
||||
Assert.Equal(1u, entry.Draw.DrawCount);
|
||||
Assert.Equal((77u, 3.5f, 0f), DetailFields(entry.Constants));
|
||||
Assert.Equal([0u, 1u], mixed.DetailCategories);
|
||||
Assert.Equal(2, mixed.Matrices.Count);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false, true, true)]
|
||||
[InlineData(true, false, true)]
|
||||
[InlineData(true, true, false)]
|
||||
public void PrepareThenDraw_DetailUnavailableDisabledOrQualityA2cFalsePreservesSelection(
|
||||
bool detailAvailable,
|
||||
bool detailEnabled,
|
||||
bool alphaToCoverage)
|
||||
{
|
||||
using var fx = new DispatcherFixture(
|
||||
detailAvailable: detailAvailable,
|
||||
detailEnabled: detailEnabled,
|
||||
sampleCount: 4,
|
||||
alphaToCoverage: alphaToCoverage);
|
||||
using DrawScope draw = fx.BeginDraw();
|
||||
OrderedDrawStream stream = StreamOf(
|
||||
MakeCommand(0),
|
||||
MakeCommand(1, detailCategory: 1));
|
||||
|
||||
PrepareAndDrawWhole(fx.Dispatcher, draw, stream);
|
||||
|
||||
List<(GpuPushConstants Constants, int Start, int Count)> runs = DecodeRuns(fx.Device);
|
||||
Assert.Equal([(0, 1), (1, 1), (2, 1), (3, 1)],
|
||||
runs.Select(run => (run.Start, run.Count)).ToList());
|
||||
Assert.Equal((0u, 0f), (runs[0].Constants.TextureIndexA, runs[0].Constants.ParamA));
|
||||
Assert.Equal((77u, 3.5f), (runs[1].Constants.TextureIndexA, runs[1].Constants.ParamA));
|
||||
Assert.Equal((77u, 3.5f), (runs[2].Constants.TextureIndexA, runs[2].Constants.ParamA));
|
||||
Assert.Equal((0u, 0f), (runs[3].Constants.TextureIndexA, runs[3].Constants.ParamA));
|
||||
bool detailActive = detailAvailable && detailEnabled;
|
||||
string selected = alphaToCoverage ? "wb-mesh-opaque-a2c" : "wb-mesh-opaque";
|
||||
Assert.Equal(
|
||||
[
|
||||
"wb-mesh-opaque-1x",
|
||||
"wb-mesh-opaque-a2c-1x",
|
||||
"wb-mesh-opaque-a2c-1x",
|
||||
"wb-mesh-opaque-a2c-1x",
|
||||
"wb-mesh-opaque-a2c-1x",
|
||||
],
|
||||
["wb-mesh-opaque", selected, detailActive ? "wb-mesh-opaque" : selected],
|
||||
fx.Device.Calls.OfType<GpuRecordedPipelineBind>()
|
||||
.Select(call => call.PipelineName)
|
||||
.ToArray());
|
||||
Assert.Equal(4, fx.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>().Count());
|
||||
Assert.Equal(expectedReference, runs[2].Constants.ParamB);
|
||||
GpuPipelineDescription a2c = fx.Device.CreatedPipelines
|
||||
.Single(pipeline => pipeline.Description.Name == "wb-mesh-opaque-a2c-1x")
|
||||
.Description;
|
||||
Assert.Equal(GpuBlendMode.None, a2c.Blend);
|
||||
Assert.True(a2c.AlphaToCoverage);
|
||||
Assert.True(a2c.Depth.Test);
|
||||
Assert.True(a2c.Depth.Write);
|
||||
Assert.Equal(WorldDepthContract.WorldCompare, a2c.Depth.Compare);
|
||||
.Select(call => call.PipelineName));
|
||||
List<(GpuPushConstants Constants, int Start, int Count)> runs = DecodeRuns(fx.Device);
|
||||
Assert.Equal([(0, 1), (1, 1)], runs.Select(run => (run.Start, run.Count)));
|
||||
Assert.Equal((0u, 0f, 0f), DetailFields(runs[0].Constants));
|
||||
Assert.Equal(
|
||||
detailActive ? (77u, 3.5f, 0f) : (0u, 0f, 0f),
|
||||
DetailFields(runs[1].Constants));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false, true, true)]
|
||||
[InlineData(true, false, true)]
|
||||
[InlineData(true, true, false)]
|
||||
public void ClassicGrouped_DetailUnavailableDisabledOrQualityA2cFalsePreservesSelection(
|
||||
bool detailAvailable,
|
||||
bool detailEnabled,
|
||||
bool alphaToCoverage)
|
||||
{
|
||||
using var fx = new DispatcherFixture(
|
||||
detailAvailable: detailAvailable,
|
||||
detailEnabled: detailEnabled,
|
||||
sampleCount: 4,
|
||||
alphaToCoverage: alphaToCoverage);
|
||||
using DrawScope draw = fx.BeginDraw();
|
||||
|
||||
ExecuteClassicGroups(fx, MakeClassicGroup(0, detailCategory: 1));
|
||||
|
||||
bool detailActive = detailAvailable && detailEnabled;
|
||||
var entry = Assert.Single(DecodePipelineRuns(fx.Device));
|
||||
Assert.Equal(alphaToCoverage && !detailActive
|
||||
? "wb-mesh-opaque-a2c"
|
||||
: "wb-mesh-opaque", entry.Pipeline);
|
||||
Assert.Equal(1u, entry.Draw.DrawCount);
|
||||
Assert.Equal(
|
||||
detailActive ? (77u, 3.5f, 0f) : (0u, 0f, 0f),
|
||||
DetailFields(entry.Constants));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -832,6 +976,130 @@ public sealed class OrderPreservingSubmitterTests
|
|||
private static List<(int Start, int Count)> DecodeDrawRanges(RecordingGpuDevice device) =>
|
||||
[.. DecodeRuns(device).Select(r => (r.Start, r.Count))];
|
||||
|
||||
private static (uint TextureIndexA, float ParamA, float ParamB) DetailFields(
|
||||
GpuPushConstants constants) =>
|
||||
(constants.TextureIndexA, constants.ParamA, constants.ParamB);
|
||||
|
||||
private static void AssertOpaquePipeline(
|
||||
RecordingGpuDevice device,
|
||||
string name,
|
||||
bool atmospheric,
|
||||
int sampleCount,
|
||||
bool alphaToCoverage)
|
||||
{
|
||||
GpuPipelineDescription description = device.CreatedPipelines
|
||||
.Single(pipeline => pipeline.Description.Name == name)
|
||||
.Description;
|
||||
Assert.Equal(atmospheric ? "mesh_atmospheric" : "mesh_modern", description.Shaders.Name);
|
||||
Assert.Equal(GpuBlendMode.None, description.Blend);
|
||||
Assert.Equal(alphaToCoverage, description.AlphaToCoverage);
|
||||
Assert.Equal(sampleCount, description.SampleCount);
|
||||
Assert.True(description.Depth.Test);
|
||||
Assert.True(description.Depth.Write);
|
||||
Assert.Equal(WorldDepthContract.WorldCompare, description.Depth.Compare);
|
||||
}
|
||||
|
||||
private static WbDrawDispatcher.InstanceGroup MakeClassicGroup(
|
||||
int index,
|
||||
uint detailCategory,
|
||||
TranslucencyKind translucency = TranslucencyKind.Opaque,
|
||||
RetailSetSurfaceMaterialState? materialState = null)
|
||||
{
|
||||
var group = new WbDrawDispatcher.InstanceGroup
|
||||
{
|
||||
FirstIndex = (uint)index * 3,
|
||||
BaseVertex = index * 4,
|
||||
IndexCount = 3,
|
||||
TextureSlot = new GpuTextureSlot((uint)index),
|
||||
TextureLayer = 0,
|
||||
Translucency = translucency,
|
||||
MaterialState = materialState ?? RetailSetSurfaceMaterialState.Opaque,
|
||||
SurfaceOpacity = 1f,
|
||||
CullMode = CullMode.CounterClockwise,
|
||||
FoliageFlags = 0,
|
||||
};
|
||||
group.Matrices.Add(Matrix4x4.CreateTranslation(index, index * 2, index * 3));
|
||||
group.SubmissionOrders.Add(index);
|
||||
group.Slots.Add(0);
|
||||
group.LightSets.Add(WbDrawDispatcher.InstanceLightSet.Disabled);
|
||||
group.IndoorFlags.Add(0);
|
||||
group.DetailCategories.Add(detailCategory);
|
||||
group.Opacities.Add(1f);
|
||||
group.SelectionLighting.Add(Vector2.Zero);
|
||||
return group;
|
||||
}
|
||||
|
||||
private static void AppendClassicInstance(
|
||||
WbDrawDispatcher.InstanceGroup group,
|
||||
int index,
|
||||
uint detailCategory)
|
||||
{
|
||||
group.Matrices.Add(Matrix4x4.CreateTranslation(index, index * 2, index * 3));
|
||||
group.SubmissionOrders.Add(index);
|
||||
group.Slots.Add(0);
|
||||
group.LightSets.Add(WbDrawDispatcher.InstanceLightSet.Disabled);
|
||||
group.IndoorFlags.Add(0);
|
||||
group.DetailCategories.Add(detailCategory);
|
||||
group.Opacities.Add(1f);
|
||||
group.SelectionLighting.Add(Vector2.Zero);
|
||||
}
|
||||
|
||||
private static void ExecuteClassicGroups(
|
||||
DispatcherFixture fixture,
|
||||
params WbDrawDispatcher.InstanceGroup[] groups)
|
||||
{
|
||||
fixture.Dispatcher.BeginFrame(frameSlot: 0);
|
||||
MethodInfo execute = typeof(WbDrawDispatcher).GetMethod(
|
||||
"ExecuteClassifiedGroups",
|
||||
BindingFlags.Instance | BindingFlags.NonPublic)!;
|
||||
execute.Invoke(
|
||||
fixture.Dispatcher,
|
||||
[
|
||||
Matrix4x4.Identity,
|
||||
Vector3.Zero,
|
||||
0u,
|
||||
groups,
|
||||
WbDrawDispatcher.EntitySet.All,
|
||||
groups.Length,
|
||||
groups.Length,
|
||||
false,
|
||||
false,
|
||||
]);
|
||||
}
|
||||
|
||||
private static List<(
|
||||
string Pipeline,
|
||||
GpuPushConstants Constants,
|
||||
GpuRecordedMultiDrawIndirect Draw)> DecodePipelineRuns(RecordingGpuDevice device)
|
||||
{
|
||||
var transcript = new List<(
|
||||
string Pipeline,
|
||||
GpuPushConstants Constants,
|
||||
GpuRecordedMultiDrawIndirect Draw)>();
|
||||
IReadOnlyList<GpuRecordedCall> calls = device.Calls;
|
||||
for (int i = 0; i < calls.Count; i++)
|
||||
{
|
||||
if (calls[i] is not GpuRecordedMultiDrawIndirect draw)
|
||||
continue;
|
||||
|
||||
string? pipeline = null;
|
||||
GpuPushConstants? constants = null;
|
||||
for (int prior = i - 1; prior >= 0 && (pipeline is null || constants is null); prior--)
|
||||
{
|
||||
if (pipeline is null && calls[prior] is GpuRecordedPipelineBind bind)
|
||||
pipeline = bind.PipelineName;
|
||||
if (constants is null && calls[prior] is GpuRecordedPushConstants push)
|
||||
constants = push.Constants;
|
||||
}
|
||||
|
||||
Assert.NotNull(pipeline);
|
||||
Assert.NotNull(constants);
|
||||
transcript.Add((pipeline!, constants!.Value, draw));
|
||||
}
|
||||
|
||||
return transcript;
|
||||
}
|
||||
|
||||
private static List<(GpuPushConstants Constants, int Start, int Count)> DecodeRuns(
|
||||
RecordingGpuDevice device)
|
||||
{
|
||||
|
|
@ -892,6 +1160,7 @@ public sealed class OrderPreservingSubmitterTests
|
|||
bool atmospheric = false,
|
||||
bool receiverBindingAvailable = true,
|
||||
int sampleCount = 1,
|
||||
bool alphaToCoverage = true,
|
||||
RecordingGpuDevice? device = null)
|
||||
{
|
||||
Device = device ?? new RecordingGpuDevice();
|
||||
|
|
@ -923,6 +1192,7 @@ public sealed class OrderPreservingSubmitterTests
|
|||
new GpuTextureSlot(77), 3.5f, 0x05000001, 0x08000001, 16, 16)
|
||||
: default,
|
||||
buildingDetailEnabled: () => detailEnabled);
|
||||
Dispatcher.AlphaToCoverage = alphaToCoverage;
|
||||
if (atmospheric)
|
||||
{
|
||||
var source = new BindableAtmosphericSource(receiverBindingAvailable);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue