fix(rendering): repair EnvCell retail CLIP state
Exclude deferred EnvCell subsets from the opaque turn, preserve exact CLIP/ALPHA fixed-state groups through leaf replay, and use retail's row-3 override, blend, depth, and texture-class alpha references. Keep the existing building-detail sentinel distinct from the two CLIP references.
Bound rejected source payload, replace no-op allocation proofs with actual EnvCell and particle RHI paths, rebuild checked-in SPIR-V, and correct AP-238/AP-240 plus the S4-c2 evidence record.
Gates: Release 0W/0E; hermetic 16735/0/0; InstalledDat 255 pass/10 known fail/1 skip; shaders 32/32; focused 239/239; allocation 2/2 at 0 B. The evidence/comment repair re-ran Release 0W/0E, shaders 32/32, affected 37/37, and allocation 2/2; mesh_detail.vert.spv remained byte-identical at SHA-256 5346247ab7d606046943e19b28888c814e08dc6cb27cd9750096ac055457eb57.
Mutation proof, with each mutation restored after its named first failure:
1. Restoring the opaque predicate to !IsAdditive fails WholeLeaf_MixedCellDrawsOpaqueAtTurnThenClipAndAlphaAtDrain first at draw count: expected 1, actual 3.
2. Selecting _alphaPipeline for CLIP fails WholeLeaf_ClipDrainBindsExactStateAndTextureClassReference first at the bind sequence: expected [envcell-clip], actual [envcell-alpha].
3. Disabling CLIP depth write fails that production Theory first at Assert.True(clipPipeline.Depth.Write): expected true, actual false.
4. Swapping palette/DDS references fails the DDS row first: expected 0.784313738, actual 0.392156869; the palette row reports the inverse.
5. Mutating mesh_modern.frag from < to <= fails ClipShaders_UseGreaterEqualForThePerRangeReference first at Assert.Contains("if (color.a < alphaCutoff) discard;"): the required source spelling is absent.
6. Restoring row-3 OverrideClipmap=true fails the real-interface clip Theory first at Assert.False: expected false, actual true.
7. Deleting failed-append rollback fails the flush/end/abort rejection-storm rows first at the bounded pending-count assertion: expected 0, actual 9000.
8. Selecting _transparentDetailPipeline for a ClipMap detail contribution fails the leaf detail pin first at the bind sequence: expected second bind envcell-retail-detail-clip, actual envcell-retail-detail-alpha.
9. Resetting detail ParamB to zero fails the same detail pin first at the second pushed reference: expected 0.784313738, actual 0.
10. Classifying CLIP with exact mask equality excludes legal 0x09 and fails WholeLeaf_PositiveStippleClipMaskUsesClipPipelineAndDdsReference first at pipeline: expected envcell-clip, actual envcell-alpha.
11. Mapping the new blend to SRC_ALPHA/INVSRCALPHA fails AllRetailBlendModesAreRepresentable first at the tuple: expected (One, OneMinusSrcAlpha), actual (SrcAlpha, OneMinusSrcAlpha).
12. Restoring mesh_detail.vert's uParamB > 0.5 category predicate fails ClipShaders_UseGreaterEqualForThePerRangeReference first because vDetailCategory = uParamB == 1.0 is absent.
13. Treating every positive detail uParamB as a cutoff fails that source pin first because isRetailClipReference(uParamB) ? uParamB : 0.05 is absent.
14. Adding arbitrary 0.5 as an accepted reference to either mesh_modern.frag or mesh_detail.frag fails that source pin first at Assert.DoesNotContain("value - 0.5"); both mutations were run and reversed independently.
Retail: D3DPolyRender::SetSurface @ 0x0059c4d0; paired binary @ 0x0059c72a, 0x0059c747/0x0059c74f, 0x0059c821, 0x0059c838, 0x0059c866.
This commit is contained in:
parent
0aa166aa09
commit
252886e84f
21 changed files with 812 additions and 277 deletions
|
|
@ -330,9 +330,9 @@ research and is no longer active.
|
|||
| ~~AP-28~~ | **RETIRED 2026-08-08 (Campaign A slice A2).** The three picked AL parameters and the gain-driven eviction are both gone. `RetailSoundMixer` now carries the byte-decoded retail curve — `g = dist < 5 ? vol : 25·vol/dist²`, clamped to 1, ONE master multiply, `db = ceil(20·log10 g)`, and a hard −50 dB no-allocate floor (audible radius ≈94.2 m at unity) — with pan as retail's `−15·sin(Δbearing)` in whole decibels and a 5-metre integer deadzone. Every AL source is source-relative with `AL_ROLLOFF_FACTOR = 0` and the global distance model is `None`, so AL contributes no attenuation of its own; the old `InverseDistanceClamped` ref-2 m curve was inverse FIRST power (`2/d`), quieter than retail up close and far louder at range with no cutoff at all. Voice eviction now compares the DAT-authored float priority strictly-less in ring order per `SoundManager::PlaySoundInternal` @ `0x0054FEC0` (the row's old `FUN_00550ad0` citation was wrong — that address is inside an `IntrusiveHashTable` constructor). The residual pan-LAW approximation is AP-173; retail's own `s_bPlaySoundOnlyWhenActive` gate is TS-64. | retired | — | — | `SoundManager::GetAttenuation @ 0x00550020`; `SoundManager::PlaySoundInternal @ 0x00550170` and `@ 0x0054FEC0`; `docs/research/2026-08-08-audio-retail-soundmanager-core.md` |
|
||||
| AP-31 | Scenery placement drift + the 0xA9B1 road-edge tree — WB-upstream divergences from retail, ACCEPTED (**#49/#50**, 2026-05-11) | `src/AcDream.Core/World/SceneryGenerator.cs` (via `WbSceneryAdapter`) | Piecemeal patching against WB upstream is net-negative (the `e279c46` road-check attempt over-suppressed scenery elsewhere, reverted `677a726`); visible impact = a handful of trees a few meters off | The same WB-upstream class could hide a *larger* placement divergence elsewhere; revisit only via a coherent ACME-style per-vertex filter port | `CLandBlock::get_land_scenes`; ACME GameScene.cs:1074 per-vertex road filter |
|
||||
| ~~AP-34~~ | **RETIRED 2026-09-04 (S4-c2 fix round 1) — residuals AP-238 / AP-239 / AP-240.** Historical: a single acdream-only distance-sorted alpha queue stood in for retail's two independent per-`DrawMesh`-invocation FIFO lists (CLIP/ALPHA, capacity 3000 each) drained at retail's four normal-world `FlushAlphaList` sites (`DrawBuilding`/`DrawBlock`/`PView::DrawCells`/`RenderNormalMode`). S4-c2 (`048d5b12f`) replaced it with `RetailAlphaQueue`'s exact two-list FIFO port of `D3DPolyRender::AddMeshToAlphaList`'s append-only/capacity-drop behavior, routed by `RetailAlphaMeshRouter`'s port of `DrawMesh`'s five-row branch table, under retail's exact no-op threshold rule — no acdream-only sort remains anywhere on the alpha path. | retired | — | — | `D3DPolyRender::AddMeshToAlphaList` 0x0059C230; `D3DPolyRender::FlushAlphaList` 0x0059D2E0; `D3DPolyRender::DrawMesh` 0x0059D4A0 |
|
||||
| AP-238 | **S4-c2 (2026-09-04); NARROWED FURTHER at fix rounds 1 and 2 — renumbered from the colliding id "AP-236" (that id was already filed AND retired on `main`; ids are never reused).** `RetailAlphaQueue` is retail's own independent CLIP/ALPHA FIFO pair. Fix round 2 now retains each real EnvCell `ObjectRenderBatch`'s `TextureBatchData.RetailSurfaceMask` and routes every transparent subset independently through `RetailAlphaMeshRouter`: canonical `0x08` Base1ClipMap subsets enter CLIP, `0x02` alpha-family subsets enter ALPHA, table-Immediate subsets draw at the cell turn, and detail-active eligible subsets remain immediate with detail. A mixed cell can therefore contribute to both lists. The remaining residual is only token granularity: acdream coalesces to at most ONE token per `(cell,list)` and replays a list-filtered subset view, where retail appends one entry per surface subset. | `src/AcDream.App/Rendering/RetailPViewPassExecutor.cs` (`SubmitOrDrawTransparentCellShell`, `EnvCellAlphaDrawSource`); `src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs` / `.Rhi.cs` (`GetTransparentRoutes`, `RouteTransparentBatch`, filtered `RenderTransparentOrdered`); `src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs` (`ObjectRenderBatch.RetailSurfaceMask`); `src/AcDream.App/Rendering/RetailAlphaQueue.cs` | Per-list replay preserves the exact subset class and the queue's adjacent-only batching preserves interleaving around other sources; the approximation exists only within one cell/list when several same-list subsets are represented by one token. | At a genuine depth-overlap, collapsing multiple same-list subsets to one `(cell,list)` token can visibly change compositing if other translucent content should fall between those subsets in retail's FIFO. | `RenderDeviceD3D::DrawEnvCell` 0x0059F170; `D3DPolyRender::DrawMesh` 0x0059D4A0; `D3DPolyRender::AddMeshToAlphaList` 0x0059C230; canonical installed-DAT case `F4180104` surface `08000BFF` mask `0x08` |
|
||||
| AP-238 | **S4-c2 (2026-09-04); NARROWED FURTHER by fix rounds 1/2 and the owner-authorized repair — renumbered from the colliding id "AP-236" (that id was already filed AND retired on `main`; ids are never reused).** `RetailAlphaQueue` is retail's independent CLIP/ALPHA FIFO pair. The repaired production leaf excludes every non-additive transparent EnvCell subset from its preceding opaque draw, retains each real batch's `RetailSurfaceMask`, and routes canonical `0x08` plus legal positive-stipple `0x09` to CLIP with row-3 `overrideClipmap=false`; `0x02` routes to ALPHA. EnvCell CLIP now uses `SetSurface`'s exact `ONE/INVSRCALPHA`, depth-test/write-on state and `GREATER_EQUAL` reference (100/255 paletted, 200/255 DDS); detail-active row 1 keeps that CLIP base/reference before its detail contribution. A mixed cell can contribute to both lists without cross-feeding. The only remaining residual is token granularity: acdream coalesces to at most ONE token per `(cell,list)` and replays a list-filtered subset view, where retail appends one entry per surface subset. | `src/AcDream.App/Rendering/RetailPViewPassExecutor.cs` (`SubmitOrDrawTransparentCellShell`, `EnvCellAlphaDrawSource`); `src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs` / `.Rhi.cs` (`BatchBelongsToPass`, retained-mask state grouping, filtered `RenderTransparentOrdered`); `src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs` (`ObjectRenderBatch.RetailSurfaceMask`); `src/AcDream.App/Rendering/RetailAlphaQueue.cs` | Per-list replay preserves the exact subset class/state and the queue's adjacent-only batching preserves interleaving around other sources; the approximation exists only within one cell/list when several same-list subsets are represented by one token. | At a genuine depth-overlap, collapsing multiple same-list subsets to one `(cell,list)` token can visibly change compositing if other translucent content should fall between those subsets in retail's FIFO. | `RenderDeviceD3D::DrawEnvCell` 0x0059F170; `D3DPolyRender::DrawMesh` 0x0059D4A0; `D3DPolyRender::AddMeshToAlphaList` 0x0059C230; `D3DPolyRender::SetSurface` 0x0059C4D0 (paired binary: blend ids 2/6 @0x0059C72A, refs `0x64`/`0xC8` at globals 0x00820D8C/0x00820D90, func 7, depth-write byte 1 @0x0059C866); canonical installed-DAT case `F4180104` surface `08000BFF` mask `0x08` |
|
||||
| AP-239 | **Filed 2026-09-04 (S4-c2); renumbered from the colliding id "AP-237" at fix round 1.** `RetailAlphaMeshRouter.MaskFromTranslucencyKind` reconstructs retail's `ConstructMesh` routing mask from acdream's already-collapsed `TranslucencyKind`. A raw `Surface.Type` with Translucent(0x10) and Base1ClipMap(0x04), but no Alpha/InvAlpha/Additive bit, collapses to `AlphaBlend` and reconstructs as mask `0x02` (ALPHA), while retail retains mask `0x08` (CLIP). `GroupKey` does not retain the raw ClipMap bit required to distinguish the two origins. The original cloud example was disproven; a 2026-09-04 scan found 27 real `Type=0x14` surfaces among 6,152, including surface `0x08000015` referenced by GfxObj `0x010001EC`. | `src/AcDream.App/Rendering/RetailAlphaMeshRouter.cs` (`MaskFromTranslucencyKind`); `src/AcDream.Core/Meshing/TranslucencyKind.cs` (`FromSurfaceType`) | The affected surface keeps the same blend state, but choosing the wrong FIFO changes compositing order; at a genuine overlap that order difference can be visibly different. | A translucent+clipmap surface (e.g. GfxObj `0x010001EC`) interleaves with ALPHA-list content instead of CLIP-list content and can composite differently at a depth overlap. | `D3DPolyRender::ConstructMesh` 0x0059DFA0 (mask priority); `D3DPolyRender::SetSurface` 0x0059C4D0 |
|
||||
| AP-240 | **Filed 2026-09-04 (S4-c2 fix round 1, M2); narrowed at fix round 2.** `WbDrawDispatcher.IsOpaque(t) => t == Opaque \|\| t == ClipMap` keeps clip-mapped GfxObj/scenery/building subsets out of `_translucentDraws`/`SubmitWalkAlphaInstance`/`SubmitToAlphaQueue`; retail gives those subsets mask `0x08` and appends them to CLIP. They instead draw immediately on acdream's `OpaqueAlphaToCoverage` pipeline. Fix round 2 proves CLIP has two other production feeder families: clip-mapped mesh particles and exact-mask EnvCell subsets (canonical `F4180104` / surface `08000BFF`). The residual is therefore only ordinary GfxObj/scenery/building content, not an empty or particle-only CLIP list. | `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs` (`IsOpaque`); `src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs` (`RouteTransparentBatch`); `src/AcDream.App/Rendering/ParticleRenderer.cs`; `src/AcDream.App/Rendering/RetailAlphaMeshRouter.cs` | The cutout itself remains present, but immediate opaque-path placement differs from retail's deferred FIFO order. Changing it is a VisualMaster-era pipeline decision beyond this bounded chunk. | At an overlap with deferred CLIP content from particles or EnvCells, clip-mapped GfxObj/scenery/building edges can visibly composite in the wrong relative order. | `D3DPolyRender::ConstructMesh` 0x0059DFA0 (mask priority, row 3 → CLIP); `D3DPolyRender::DrawMesh` 0x0059D4A0; `D3DPolyRender::AddMeshToAlphaList` 0x0059C230 |
|
||||
| AP-240 | **Filed 2026-09-04 (S4-c2 fix round 1, M2); narrowed at fix round 2 and the owner-authorized repair.** `WbDrawDispatcher.IsOpaque(t) => t == Opaque \|\| t == ClipMap` keeps clip-mapped GfxObj/scenery/building subsets out of `_translucentDraws`/`SubmitWalkAlphaInstance`/`SubmitToAlphaQueue`; retail gives those subsets mask `0x08` and appends them to CLIP. They instead draw immediately on acdream's `OpaqueAlphaToCoverage` pipeline. CLIP has two genuine production feeder families outside this residual: clip-mapped mesh particles and exact-mask EnvCell subsets (canonical `F4180104` / surface `08000BFF`), with EnvCell now proven through the real leaf and exact `SetSurface` CLIP state. The residual is only ordinary GfxObj/scenery/building content; no ordinary-Wb A2C behavior changed in this repair. | `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs` (`IsOpaque`); `src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs` (`RouteTransparentBatch`); `src/AcDream.App/Rendering/ParticleRenderer.cs`; `src/AcDream.App/Rendering/RetailAlphaMeshRouter.cs` | The cutout itself remains present, but immediate opaque-path placement differs from retail's deferred FIFO order. Changing it is a VisualMaster-era pipeline decision beyond this bounded chunk. | At an overlap with deferred CLIP content from particles or EnvCells, clip-mapped GfxObj/scenery/building edges can visibly composite in the wrong relative order. | `D3DPolyRender::ConstructMesh` 0x0059DFA0 (mask priority, row 3 → CLIP); `D3DPolyRender::DrawMesh` 0x0059D4A0; `D3DPolyRender::AddMeshToAlphaList` 0x0059C230 |
|
||||
| AP-36 | Dungeon streaming gate triggers on the player's CURRENT cell being a sealed EnvCell (`CurrCell.IsEnv && !SeenOutside`), an approximation of ACE's full landblock `IsDungeon` (all-heights-zero + NumCells>0 + Buildings.Count==0). The retail BEHAVIOR (a dungeon loads no adjacent landblocks) is faithful — only the runtime TRIGGER is the cheap cell predicate instead of classifying the center landblock. **#135 pre-collapse:** at login/teleport the same collapse is triggered EARLY (the instant the streaming center is recentered onto the spawn/dest cell) via `IsSealedDungeonCell` reading the EnvCell **dat** `SeenOutside` flag — because the physics `CurrCell` is null until placement, which waits for hydration; without the early trigger the full 25×25 ocean-grid window loads then unloads (the ~30 s login FPS ramp). **#215 cell identity:** the pre-collapse/recenter decision compares the player's current `Position.objcell_id` landblock with the received destination `objcell_id`; it never reconstructs the source from XYZ because dungeon frame origins may be negative. **#145/#138 teleport-hold suppression:** during a teleport arrival HOLD the player is unplaced, so `CurrCell` is the frozen SOURCE cell, not the destination; the gate is suppressed for the hold (`DungeonStreamingGate.Compute(isTeleportHold:true)` → not-inside-dungeon) so a teleport OUT of a dungeon follows the destination (the PortalSpace observer pin) and `ExitDungeonExpand`s, instead of re-pinning streaming onto the source dungeon (which left the outdoor destination un-hydrated → 600-frame readiness timeout → force-snap to ocean — the #145 "second teleport does nothing" + #138 incomplete-world) | `src/AcDream.App/Streaming/TeleportLandblockTransition.cs` (source/destination cell-ID classification) + `src/AcDream.App/Streaming/DungeonStreamingGate.cs` (`Compute` — per-frame predicate + teleport-hold suppression) + `src/AcDream.App/World/LiveEntityHydrationPorts.cs` (`LiveEntityWorldOriginCoordinator.TryInitialize` — login pre-collapse) + `src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs` (`OnPosition` — first accepted canonical Position) + `GameWindow:AimTeleportDestination`/`IsSealedDungeonCell` (teleport pre-collapse and DAT predicate) + `src/AcDream.App/Streaming/StreamingController.cs` (collapse/expand/`PreCollapseToDungeon`) | The predicate is already computed for sun/sky gating (playerInsideCell) and exactly matches for sealed dungeons vs windowed building interiors (SeenOutside=true → not gated); no landblock re-classification needed. The dat-flag read is the same `EnvCellFlags.SeenOutside` the hydrated `ObjCell.SeenOutside` is built from (`EnvCell.cs:72`/`PhysicsDataCache.cs:224`), so the pre-collapse decision matches the eventual per-frame gate exactly. The cell-ID comparison matches retail's complete `Position` flow. | A dungeon cell that reports SeenOutside (an entrance cell open to the surface) briefly un-collapses and re-streams the window; a hypothetical windowless building back-room (IsEnv && !SeenOutside but HasBuildings) would wrongly collapse its outdoor neighbors; a sealed-dungeon entrance cell that is itself SeenOutside is simply MISSED by the early trigger and falls back to the existing late collapse (no worse than before #135) | ACE `LandblockManager.GetAdjacentIDs` (dungeons→empty) Landblock.cs:577-582; `IsDungeon` Landblock.cs:1264-1277; retail `SmartBox::TeleportPlayer` 0x00453910 |
|
||||
| AP-43 | Per-object torch (point/spot) lighting AND sun are both gated on the OBJECT's own cell via the same `IndoorObjectReceivesTorches(ParentCellId)` predicate (`(id & 0xFFFF) >= 0x0100`): indoor objects (EnvCell-parented) get torches + NO sun; outdoor objects get the SUN + ambient + NO torches. This is the faithful per-draw port of retail's `useSunlight` gate — `DrawMeshInternal` (0x0059f398) calls `minimize_object_lighting` only `if (Render::useSunlight == 0)`, and `PView::DrawCells` (0x005a4840) calls `useSunlightSet(1)` (0x005a485a) for the outdoor stage and `useSunlightSet(0)` (0x005a49f3) for the interior-cell stage. **#142 (2026-06-20):** the sun gate is now PER-INSTANCE in the shader (binding=6 `instanceIndoor[]` flag in `mesh_modern.vert`, filled by `AppendCurrentLightSet`) — it was previously a per-FRAME global keyed on the PLAYER cell (`UpdateSunFromSky`). The per-frame global is retained for sealed dungeons (correctly kills the sun frame-wide when no sky is visible). **Residual:** the `ebp_2` second seen-outside test in `CellManager::ChangePosition` (0x004559B0) is unaudited — unclear whether it changes the ambient/sun regime for a subset of cells. No observed behavioral impact in tested cells. | `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs` (`IndoorObjectReceivesTorches`, `ComputeEntityLightSet`, `AppendCurrentLightSet`, `_instIndoorSsbo`/`_indoorData`/`InstanceGroup.IndoorFlags`); `src/AcDream.App/Rendering/Shaders/mesh_modern.vert` (binding=6 `instanceIndoor[]` gate on sun loop); per-frame sun `src/AcDream.App/Rendering/WorldRenderFrameBuilder.cs` (`RuntimeWorldFrameEnvironmentPreparation.UpdateSunFromSky`) | Torches: outdoor objects never torch-lit (exact retail). Sun: indoor objects (furniture, NPCs, player in a windowed building) never sun-lit (exact retail per-stage). Ambient: per-player-cell regime unchanged (exact retail `ChangePosition`). | The `ebp_2` unaudited test in `ChangePosition` could affect a narrow class of cells (entrance cells? sub-cells with special flags?) — no symptom observed; audit it if a lighting edge case arises in an unusual cell type | `useSunlight` gate `DrawMeshInternal` 0x0059f398; `useSunlightSet` 0x0054d450; per-stage `PView::DrawCells` 0x005a4840 (`useSunlightSet(1)` 0x005a485a / `useSunlightSet(0)` 0x005a49f3); `minimize_object_lighting` 0x0054d480; `CellManager::ChangePosition` 0x004559B0 (ambient + seen_outside) |
|
||||
| AP-35 | Point/spot lights are now PER-VERTEX Gouraud (`pointContribution` ~line 153 of `mesh_modern.vert`) matching retail's `SetStaticLightingVertexColors` bake path. Half-Lambert wrap (`(1/1.5)·(N·D + 0.5·d)`) AND norm distance attenuation (`distsq>1 ? distsq·d : d`) ARE ported (A7 Fix A, `aa94ced`). Point-light sum clamped to [0,1] on its own accumulator before adding ambient+sun (A7 Fix D D-1, mirrors retail's per-vertex bake clamp). CPU oracle: `src/AcDream.Core/Lighting/LightBake.cs`, locked by `tests/AcDream.Core.Tests/Lighting/LightBakeConformanceTests.cs`. **Residual (two parts):** (a) acdream lights in-shader each frame (per-frame GPU evaluate); retail bakes into the vertex buffer ONCE — an architecture/performance difference; the wrap + norm + clamp formula is the same, but bake-once is cheaper for static geometry; (b) acdream's `SelectForObject` keeps only the 8 NEAREST reaching point/spot lights per object/cell (`MaxLightsPerObject=8`, see AP-16), whereas retail's bake sums ALL reaching static lights per vertex — a surface reached by >8 point lights is dimmer in acdream than retail's bake result (rare in practice; a room has a handful of torches) | `src/AcDream.App/Rendering/Shaders/mesh_modern.vert` (`pointContribution` ~line 153; wrap ~line 163; norm ~line 167; point-sum clamp line 210) | Per-vertex Gouraud + wrap + norm + clamp all match retail. The two residuals are: (a) per-frame GPU vs bake-once — architecture/perf only; (b) 8-light cap dimming when >8 lights reach one surface — rare. `LightInfoLoader.cs:81` folds static_light_factor 1.3 into Range | (a) A new frame-time consumer bypassing `accumulateLights` would need to replicate the wrap + norm formula; per-frame GPU re-evaluate has higher per-frame cost than bake for static geometry. (b) A densely lit scene (>8 torches reaching one wall) renders dimmer than retail — see AP-16 for the 8-cap ownership | `calc_point_light` 0x0059c8b0 (line 0x0059c9a2 ramp; 0x0059c925 wrap); `SetStaticLightingVertexColors` 0x0059cfe0; static_light_factor 0x00820e24 |
|
||||
|
|
|
|||
|
|
@ -121,6 +121,14 @@ internal enum GpuBlendMode
|
|||
/// <summary>Straight alpha: <c>SrcAlpha, OneMinusSrcAlpha</c>.</summary>
|
||||
StraightAlpha,
|
||||
|
||||
/// <summary>
|
||||
/// Premultiplied/source-over: <c>One, OneMinusSrcAlpha</c>. Retail pure
|
||||
/// Base1ClipMap surfaces select exactly this pair in
|
||||
/// <c>D3DPolyRender::SetSurface</c> @0x0059c72a when the per-entry
|
||||
/// override-clipmap byte is clear.
|
||||
/// </summary>
|
||||
PremultipliedAlpha,
|
||||
|
||||
/// <summary>Additive: <c>SrcAlpha, One</c>.</summary>
|
||||
Additive,
|
||||
|
||||
|
|
|
|||
|
|
@ -174,6 +174,7 @@ internal static class VulkanViewportMapping
|
|||
internal static (BlendFactor Source, BlendFactor Destination) BlendFactorsOf(GpuBlendMode blend) => blend switch
|
||||
{
|
||||
GpuBlendMode.StraightAlpha => (BlendFactor.SrcAlpha, BlendFactor.OneMinusSrcAlpha),
|
||||
GpuBlendMode.PremultipliedAlpha => (BlendFactor.One, BlendFactor.OneMinusSrcAlpha),
|
||||
GpuBlendMode.Additive => (BlendFactor.SrcAlpha, BlendFactor.One),
|
||||
// Retail's third mode, found at slice V4c in WbDrawDispatcher.ApplyRetailBlend.
|
||||
GpuBlendMode.InverseAlpha => (BlendFactor.OneMinusSrcAlpha, BlendFactor.SrcAlpha),
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@ public sealed unsafe partial class ParticleRenderer
|
|||
|
||||
/// <summary>
|
||||
/// The RHI arm's constructor. No GL context, no <c>Shader</c>, no
|
||||
/// <c>BindlessSupport</c>: the five pipelines compile <c>particle</c> and
|
||||
/// <c>BindlessSupport</c>: the six pipelines compile <c>particle</c> and
|
||||
/// <c>particle_mesh</c> from the committed SPIR-V, and both texture sources
|
||||
/// already hand out the device's own <c>GpuTextureSlot</c> (V4t).
|
||||
/// </summary>
|
||||
|
|
@ -267,9 +267,11 @@ public sealed unsafe partial class ParticleRenderer
|
|||
});
|
||||
|
||||
/// <summary>
|
||||
/// One mesh-particle pipeline. Same depth bracket as the billboards (see
|
||||
/// <see cref="AcDream.App.Rendering.WorldDepthContract"/> for the world
|
||||
/// <c>GL_LESS</c> citation); the winding is CW because
|
||||
/// One mesh-particle pipeline. Every variant uses retail's world
|
||||
/// <c>GL_LESS</c> compare; delayed alpha variants match the billboard
|
||||
/// no-write bracket, while row 5's opaque variant depth-writes. See
|
||||
/// <see cref="AcDream.App.Rendering.WorldDepthContract"/> for the compare
|
||||
/// citation. The winding is CW because
|
||||
/// <c>PrepareMeshPipeline</c> sets <c>glFrontFace(GL_CW)</c>, and the cull
|
||||
/// mode stays DYNAMIC because it is resolved per sub-batch from the DAT's
|
||||
/// own <c>CullMode</c>.
|
||||
|
|
|
|||
|
|
@ -263,13 +263,31 @@ internal sealed partial class RetailPViewPassExecutor : IEnvCellImmediateDrawSin
|
|||
if ((routes & EnvCellTransparentRoute.Clip) != 0)
|
||||
{
|
||||
int token = clipSource.AddPendingCellId(cellId);
|
||||
queue.TryAppend(RetailAlphaList.Clip, clipSource, token, overrideClipmap: true);
|
||||
// This fixed source is ordinary DrawMesh row 3: pure ClipMap,
|
||||
// multipass false, therefore CLIP with overrideClipmap=false.
|
||||
// If the queue drops at 3,000, retain no source-owned payload;
|
||||
// TryAppend has already registered the source for exact reset.
|
||||
if (!queue.TryAppend(
|
||||
RetailAlphaList.Clip,
|
||||
clipSource,
|
||||
token,
|
||||
overrideClipmap: false))
|
||||
{
|
||||
clipSource.RollbackPendingCellId(token);
|
||||
}
|
||||
}
|
||||
|
||||
if ((routes & EnvCellTransparentRoute.Alpha) != 0)
|
||||
{
|
||||
int token = alphaSource.AddPendingCellId(cellId);
|
||||
queue.TryAppend(RetailAlphaList.Alpha, alphaSource, token, overrideClipmap: false);
|
||||
if (!queue.TryAppend(
|
||||
RetailAlphaList.Alpha,
|
||||
alphaSource,
|
||||
token,
|
||||
overrideClipmap: false))
|
||||
{
|
||||
alphaSource.RollbackPendingCellId(token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -297,13 +315,27 @@ internal sealed partial class RetailPViewPassExecutor : IEnvCellImmediateDrawSin
|
|||
if ((routes & EnvCellTransparentRoute.Clip) != 0)
|
||||
{
|
||||
int token = clipSource.AddPendingCellId(cellId);
|
||||
queue.TryAppend(RetailAlphaList.Clip, clipSource, token, overrideClipmap: true);
|
||||
if (!queue.TryAppend(
|
||||
RetailAlphaList.Clip,
|
||||
clipSource,
|
||||
token,
|
||||
overrideClipmap: false))
|
||||
{
|
||||
clipSource.RollbackPendingCellId(token);
|
||||
}
|
||||
}
|
||||
|
||||
if ((routes & EnvCellTransparentRoute.Alpha) != 0)
|
||||
{
|
||||
int token = alphaSource.AddPendingCellId(cellId);
|
||||
queue.TryAppend(RetailAlphaList.Alpha, alphaSource, token, overrideClipmap: false);
|
||||
if (!queue.TryAppend(
|
||||
RetailAlphaList.Alpha,
|
||||
alphaSource,
|
||||
token,
|
||||
overrideClipmap: false))
|
||||
{
|
||||
alphaSource.RollbackPendingCellId(token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -327,15 +359,26 @@ internal sealed partial class RetailPViewPassExecutor : IEnvCellImmediateDrawSin
|
|||
EnvCellTransparentRoute route,
|
||||
bool detailSurfaceActive);
|
||||
|
||||
internal sealed class EnvCellAlphaDrawSource(
|
||||
RenderEnvCellsByRoute renderTransparentOrdered,
|
||||
EnvCellTransparentRoute route)
|
||||
: IRetailAlphaDrawSource
|
||||
internal sealed class EnvCellAlphaDrawSource : IRetailAlphaDrawSource
|
||||
{
|
||||
private readonly RenderEnvCellsByRoute _renderTransparentOrdered;
|
||||
private readonly EnvCellTransparentRoute _route;
|
||||
private readonly Action? _resetObserver;
|
||||
private readonly List<uint> _pendingCellIds = new();
|
||||
private readonly List<uint> _preparedCellIds = new();
|
||||
private readonly List<uint> _drawScratch = new();
|
||||
|
||||
internal EnvCellAlphaDrawSource(
|
||||
RenderEnvCellsByRoute renderTransparentOrdered,
|
||||
EnvCellTransparentRoute route,
|
||||
Action? resetObserver = null)
|
||||
{
|
||||
_renderTransparentOrdered = renderTransparentOrdered
|
||||
?? throw new ArgumentNullException(nameof(renderTransparentOrdered));
|
||||
_route = route;
|
||||
_resetObserver = resetObserver;
|
||||
}
|
||||
|
||||
/// <summary>Returns this cell's token — its index into
|
||||
/// <see cref="_pendingCellIds"/> at the moment it was added.</summary>
|
||||
internal int AddPendingCellId(uint cellId)
|
||||
|
|
@ -345,6 +388,22 @@ internal sealed partial class RetailPViewPassExecutor : IEnvCellImmediateDrawSin
|
|||
return token;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rolls back only the just-reserved tail token after queue rejection.
|
||||
/// Accepted indices therefore never move, while a rejection storm can
|
||||
/// neither grow the pending count nor force a second geometric growth
|
||||
/// beyond the 3,000-entry list's warmed capacity.
|
||||
/// </summary>
|
||||
internal void RollbackPendingCellId(int token)
|
||||
{
|
||||
if (token != _pendingCellIds.Count - 1)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Only the just-reserved EnvCell alpha token can be rolled back.");
|
||||
}
|
||||
_pendingCellIds.RemoveAt(token);
|
||||
}
|
||||
|
||||
public void PrepareAlphaDraws(ReadOnlySpan<int> tokens)
|
||||
{
|
||||
_preparedCellIds.Clear();
|
||||
|
|
@ -359,7 +418,10 @@ internal sealed partial class RetailPViewPassExecutor : IEnvCellImmediateDrawSin
|
|||
_drawScratch.Clear();
|
||||
for (int i = 0; i < drawCount; i++)
|
||||
_drawScratch.Add(_preparedCellIds[firstPreparedDraw + i]);
|
||||
renderTransparentOrdered(_drawScratch, route, detailSurfaceActive: false);
|
||||
_renderTransparentOrdered(
|
||||
_drawScratch,
|
||||
_route,
|
||||
detailSurfaceActive: false);
|
||||
}
|
||||
|
||||
public void ResetAlphaSubmissions()
|
||||
|
|
@ -367,9 +429,13 @@ internal sealed partial class RetailPViewPassExecutor : IEnvCellImmediateDrawSin
|
|||
_pendingCellIds.Clear();
|
||||
_preparedCellIds.Clear();
|
||||
_drawScratch.Clear();
|
||||
_resetObserver?.Invoke();
|
||||
}
|
||||
|
||||
internal int PendingCount => _pendingCellIds.Count;
|
||||
internal int PendingCapacity => _pendingCellIds.Capacity;
|
||||
internal int PreparedCapacity => _preparedCellIds.Capacity;
|
||||
internal int DrawCapacity => _drawScratch.Capacity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,10 @@ in flat uint vBatchFlags;
|
|||
in flat uint vDetailCategory;
|
||||
|
||||
uniform uint uTextureIndexA; // category detail texture, layer 0
|
||||
// S4-c2 F3: 100/255 or 200/255 for a pure ClipMap base/detail pair. Exact 1
|
||||
// remains mesh_detail.vert's existing building-category sentinel and must use
|
||||
// the ordinary empty-fragment cutoff. Zero is the ordinary EnvCell category.
|
||||
uniform float uParamB;
|
||||
|
||||
// SceneLighting UBO — IDENTICAL layout to mesh_modern.frag binding=1 (same
|
||||
// std140 block, same struct, same binding). Declared here ONLY for fog
|
||||
|
|
@ -50,6 +54,11 @@ vec3 applyFog(vec3 lit, vec3 worldPos) {
|
|||
|
||||
out vec4 FragColor;
|
||||
|
||||
bool isRetailClipReference(float value) {
|
||||
return abs(value - (100.0 / 255.0)) < 0.000001
|
||||
|| abs(value - (200.0 / 255.0)) < 0.000001;
|
||||
}
|
||||
|
||||
// VM2 (2026-08-22, live cdb read on the PDB-paired retail client, GUID
|
||||
// 9e847e2f-777c-4bd9-886c-22256bb87f32): retail's RenderDevice reports
|
||||
// m_caps.bCanDoSinglePassDetailing = 1 and the file-static trysinglepass = 1,
|
||||
|
|
@ -100,7 +109,8 @@ void main() {
|
|||
vec4 base = ACDREAM_SAMPLE_ARRAY(
|
||||
vBaseTextureIndex,
|
||||
vec3(vBaseUv, float(vBaseTextureLayer)));
|
||||
if (base.a < 0.05)
|
||||
float alphaCutoff = isRetailClipReference(uParamB) ? uParamB : 0.05;
|
||||
if (base.a < alphaCutoff)
|
||||
discard;
|
||||
|
||||
vec4 detail = ACDREAM_SAMPLE_ARRAY(
|
||||
|
|
|
|||
|
|
@ -38,9 +38,9 @@ layout(std430, binding = 3) readonly buffer ClipSlotBuf {
|
|||
uint instanceClipSlot[];
|
||||
};
|
||||
|
||||
// Object renderer only: 1 for a retail building shell, 0 for ordinary
|
||||
// scenery/creatures/players. EnvCellRenderer sets uParamB=0 and ignores the
|
||||
// value, but still binds one valid word because Vulkan sees this static use.
|
||||
// Exact uParamB=1 selects this per-instance building/object category. EnvCell
|
||||
// zero and its pure-ClipMap 100/255 or 200/255 references retain the bound
|
||||
// environment category; the fragment shader consumes those CLIP references.
|
||||
layout(std430, binding = 9) readonly buffer InstanceDetailCategoryBuf {
|
||||
uint instanceDetailCategory[];
|
||||
};
|
||||
|
|
@ -61,7 +61,10 @@ uniform mat4 uViewProjection;
|
|||
uniform int uDrawIDOffset;
|
||||
uniform uint uTextureIndexB; // absolute transform prefix in the shared pose arena
|
||||
uniform float uParamA; // detail UV tiling
|
||||
uniform float uParamB; // 1 = require building instance, 0 = EnvCell category
|
||||
// Exact 1 remains the existing "require building instance" sentinel. S4-c2
|
||||
// also passes 100/255 or 200/255 for an EnvCell ClipMap alpha reference; both
|
||||
// must retain the EnvCell category rather than taking the building branch.
|
||||
uniform float uParamB;
|
||||
|
||||
out vec2 vBaseUv;
|
||||
out vec2 vDetailUv;
|
||||
|
|
@ -95,7 +98,7 @@ void main() {
|
|||
vBaseTextureIndex = batch.textureIndex;
|
||||
vBaseTextureLayer = batch.textureLayer;
|
||||
vBatchFlags = batch.flags;
|
||||
vDetailCategory = uParamB > 0.5
|
||||
vDetailCategory = uParamB == 1.0
|
||||
? instanceDetailCategory[instanceIndex]
|
||||
: 1u;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,10 @@ in flat vec2 vSelectionLighting; // x=luminosity, y=diffuse
|
|||
// transparent overdraw cost otherwise)
|
||||
uniform int uRenderPass;
|
||||
uniform int uLightDebug; // #176 stripe hunt (see mesh_modern.vert) — mode 3 handled here
|
||||
// S4-c2 F3: only retail's exact pure-ClipMap alpha-test references are
|
||||
// recognized, supplied per MDI range from TextureKey.PaletteId (100/255
|
||||
// paletted, 200/255 DDS/non-paletted). Every other value keeps 0.05.
|
||||
uniform float uParamB;
|
||||
|
||||
// SceneLighting UBO — IDENTICAL layout to mesh_instanced.frag binding=1.
|
||||
struct Light {
|
||||
|
|
@ -57,15 +61,23 @@ vec3 applyFog(vec3 lit, vec3 worldPos) {
|
|||
|
||||
out vec4 FragColor;
|
||||
|
||||
bool isRetailClipReference(float value) {
|
||||
return abs(value - (100.0 / 255.0)) < 0.000001
|
||||
|| abs(value - (200.0 / 255.0)) < 0.000001;
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec4 color = ACDREAM_SAMPLE_ARRAY(vTextureIndex, vec3(vTexCoord, float(vTextureLayer)));
|
||||
float alphaCutoff = isRetailClipReference(uParamB) ? uParamB : 0.05;
|
||||
|
||||
// Two-pass alpha-test (N.5 Decision 2).
|
||||
// A.5 T20: opaque pass writes alpha as-sampled so GL_SAMPLE_ALPHA_TO_COVERAGE
|
||||
// derives the MSAA sample mask from it — ClipMap foliage edges become smooth.
|
||||
// Discard only fully-transparent (α < 0.05); the GPU handles coverage masking.
|
||||
if (uRenderPass == 0) {
|
||||
if (color.a < 0.05) discard; // opaque pass — kill truly empty only (A2C)
|
||||
// '<' spells D3DCMP_GREATEREQUAL: equality passes. Retail loads
|
||||
// 100 or 200 and func 7 at SetSurface 0x0059c73c..0x0059c838.
|
||||
if (color.a < alphaCutoff) discard;
|
||||
} else {
|
||||
// Transparent pass.
|
||||
//
|
||||
|
|
@ -84,7 +96,7 @@ void main() {
|
|||
// Keep the α<0.05 short-circuit as a fragment-cost optimization
|
||||
// (skip fully-empty pixels — saves blend bandwidth on alpha-keyed
|
||||
// sprites with large transparent margins).
|
||||
if (color.a < 0.05) discard;
|
||||
if (color.a < alphaCutoff) discard;
|
||||
}
|
||||
|
||||
// Per-vertex Gouraud lighting from the vertex shader (ambient + capped
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -231,12 +231,12 @@
|
|||
"stages": [
|
||||
{
|
||||
"stage": "vert",
|
||||
"sourceSha256": "0c1eddb0fc892cb76ef4da47f727cec2516052d63247fa2682918bfd60b84fa8",
|
||||
"sourceSha256": "2d8b5b42f053a2d42e1ff44db3e331d5bea2d3f7dd36e20d813b7ae22c033181",
|
||||
"compiled": true
|
||||
},
|
||||
{
|
||||
"stage": "frag",
|
||||
"sourceSha256": "e037fd28cf71453792c17522c97c4c7797193823ed78ca629a4331b632f177b1",
|
||||
"sourceSha256": "a5285c51e8d6a1fa91f403f8489819595fa5a20abaaaccc969f220ea2c4eb335",
|
||||
"compiled": true
|
||||
}
|
||||
]
|
||||
|
|
@ -252,7 +252,7 @@
|
|||
},
|
||||
{
|
||||
"stage": "frag",
|
||||
"sourceSha256": "12f2ceec9a8420bd273650e95251ca56c3bf3c3691fce1a539e4c5aea7abaabf",
|
||||
"sourceSha256": "d6ae09de279e814b159e7c481d6b66f3cc37c741b3d8a249fbe2bc8156e52d4e",
|
||||
"compiled": true
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -26,9 +26,11 @@ public sealed unsafe partial class EnvCellRenderer
|
|||
private readonly IWorldPassScope? _scope;
|
||||
private IGpuPipeline? _opaquePipeline;
|
||||
private IGpuPipeline? _alphaPipeline;
|
||||
private IGpuPipeline? _clipPipeline;
|
||||
private IGpuPipeline? _additivePipeline;
|
||||
private IGpuPipeline? _detailPipeline;
|
||||
private IGpuPipeline? _transparentDetailPipeline;
|
||||
private IGpuPipeline? _clipDetailPipeline;
|
||||
private readonly TerrainAtlas.RetailDetailTextureBinding _environmentDetail;
|
||||
private readonly Func<bool> _buildingDetailEnabled;
|
||||
|
||||
|
|
@ -48,7 +50,7 @@ public sealed unsafe partial class EnvCellRenderer
|
|||
|
||||
/// <summary>
|
||||
/// The RHI arm's constructor. It also completes <c>Initialize</c>'s job: the
|
||||
/// five pipelines ARE this renderer's program, so there is no second step
|
||||
/// seven pipelines ARE this renderer's program, so there is no second step
|
||||
/// and no <c>Shader</c> to hand in.
|
||||
/// </summary>
|
||||
internal EnvCellRenderer(
|
||||
|
|
@ -72,6 +74,12 @@ public sealed unsafe partial class EnvCellRenderer
|
|||
device, "envcell-opaque", GpuBlendMode.None, depthWrite: true, scope.SampleCount);
|
||||
_alphaPipeline = CreateShellPipeline(
|
||||
device, "envcell-alpha", GpuBlendMode.StraightAlpha, depthWrite: false, scope.SampleCount);
|
||||
_clipPipeline = CreateShellPipeline(
|
||||
device,
|
||||
"envcell-clip",
|
||||
GpuBlendMode.PremultipliedAlpha,
|
||||
depthWrite: true,
|
||||
scope.SampleCount);
|
||||
_additivePipeline = CreateShellPipeline(
|
||||
device, "envcell-additive", GpuBlendMode.Additive, depthWrite: false, scope.SampleCount);
|
||||
_detailPipeline = CreateShellPipeline(
|
||||
|
|
@ -92,6 +100,15 @@ public sealed unsafe partial class EnvCellRenderer
|
|||
shaderName: "mesh_detail",
|
||||
depthCompare: RetailDetailTextureContract.DetailDepthCompare(
|
||||
transparent: true));
|
||||
_clipDetailPipeline = CreateShellPipeline(
|
||||
device,
|
||||
"envcell-retail-detail-clip",
|
||||
GpuBlendMode.RetailDetail,
|
||||
depthWrite: true,
|
||||
scope.SampleCount,
|
||||
shaderName: "mesh_detail",
|
||||
depthCompare: RetailDetailTextureContract.DetailDepthCompare(
|
||||
transparent: false));
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
|
|
@ -274,12 +291,18 @@ public sealed unsafe partial class EnvCellRenderer
|
|||
MdiDrawRange drawRange = _mdiDrawRanges[drawRangeIndex];
|
||||
int groupIndex = drawRange.GroupIndex;
|
||||
CullMode cullMode = ResolveRetailCellShellCullMode(
|
||||
(CullMode)(groupIndex % 4));
|
||||
(CullMode)(groupIndex % CullGroupCount));
|
||||
|
||||
bool isAdditive = groupIndex >= 4;
|
||||
IGpuPipeline rangeBasePipeline = isAdditive
|
||||
? _additivePipeline!
|
||||
: _alphaPipeline!;
|
||||
bool isAdditive = renderPass == WbRenderPass.Transparent
|
||||
&& groupIndex >= AdditiveGroupBase
|
||||
&& groupIndex < ClipDdsGroupBase;
|
||||
bool isClip = renderPass == WbRenderPass.Transparent
|
||||
&& groupIndex >= ClipDdsGroupBase;
|
||||
IGpuPipeline rangeBasePipeline = isClip
|
||||
? _clipPipeline!
|
||||
: isAdditive
|
||||
? _additivePipeline!
|
||||
: _alphaPipeline!;
|
||||
if (renderPass == WbRenderPass.Transparent)
|
||||
{
|
||||
// Blend state is the pipeline's; switching variants mid-pass has
|
||||
|
|
@ -298,6 +321,9 @@ public sealed unsafe partial class EnvCellRenderer
|
|||
? (int)renderPass | 0x100
|
||||
: (int)renderPass;
|
||||
pushConstants.DrawIdOffset = drawRange.FirstCommand;
|
||||
pushConstants.ParamB = isClip
|
||||
? groupIndex >= ClipPalettedGroupBase ? 100f / 255f : 200f / 255f
|
||||
: 0f;
|
||||
encoder.SetPushConstants(in pushConstants);
|
||||
|
||||
// Retail DrawMesh's two-pass fallback redraws each transparent
|
||||
|
|
@ -317,7 +343,9 @@ public sealed unsafe partial class EnvCellRenderer
|
|||
pushConstants.DrawIdOffset = command;
|
||||
pushConstants.TextureIndexA = 0;
|
||||
pushConstants.ParamA = 0f;
|
||||
pushConstants.ParamB = 0f;
|
||||
pushConstants.ParamB = isClip
|
||||
? groupIndex >= ClipPalettedGroupBase ? 100f / 255f : 200f / 255f
|
||||
: 0f;
|
||||
encoder.SetPushConstants(in pushConstants);
|
||||
encoder.MultiDrawIndexedIndirect(
|
||||
commandBuffer,
|
||||
|
|
@ -325,12 +353,17 @@ public sealed unsafe partial class EnvCellRenderer
|
|||
1,
|
||||
(uint)sizeof(DrawElementsIndirectCommand));
|
||||
|
||||
BindPipelineWithMesh(encoder, _transparentDetailPipeline!, mesh);
|
||||
BindPipelineWithMesh(
|
||||
encoder,
|
||||
isClip ? _clipDetailPipeline! : _transparentDetailPipeline!,
|
||||
mesh);
|
||||
SetCullMode(encoder, cullMode);
|
||||
pushConstants.DrawIdOffset = command;
|
||||
pushConstants.TextureIndexA = _environmentDetail.TextureSlot.Index;
|
||||
pushConstants.ParamA = _environmentDetail.Tiling;
|
||||
pushConstants.ParamB = 0f;
|
||||
pushConstants.ParamB = isClip
|
||||
? groupIndex >= ClipPalettedGroupBase ? 100f / 255f : 200f / 255f
|
||||
: 0f;
|
||||
encoder.SetPushConstants(in pushConstants);
|
||||
encoder.MultiDrawIndexedIndirect(
|
||||
commandBuffer,
|
||||
|
|
@ -348,9 +381,11 @@ public sealed unsafe partial class EnvCellRenderer
|
|||
(uint)sizeof(DrawElementsIndirectCommand));
|
||||
}
|
||||
|
||||
// Retail DrawEnvCell category (2). Replay the already-filtered opaque
|
||||
// shell commands, including ClipMap built-mesh subsets. No distance
|
||||
// fade (VM1/VM2): retail's noFadeDetail gates get_alpha_for_z to the
|
||||
// Retail DrawEnvCell category (2). Replay only the already-filtered
|
||||
// opaque shell commands. Transparent ClipMap is excluded here: it
|
||||
// takes the immediate/delayed CLIP path and, when detail is active,
|
||||
// its own _clipDetailPipeline contribution. 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
|
||||
|
|
@ -368,7 +403,7 @@ public sealed unsafe partial class EnvCellRenderer
|
|||
{
|
||||
MdiDrawRange drawRange = _mdiDrawRanges[drawRangeIndex];
|
||||
CullMode cullMode = ResolveRetailCellShellCullMode(
|
||||
(CullMode)(drawRange.GroupIndex % 4));
|
||||
(CullMode)(drawRange.GroupIndex % CullGroupCount));
|
||||
SetCullMode(encoder, cullMode);
|
||||
pushConstants.DrawIdOffset = drawRange.FirstCommand;
|
||||
encoder.SetPushConstants(in pushConstants);
|
||||
|
|
@ -468,9 +503,10 @@ public sealed unsafe partial class EnvCellRenderer
|
|||
|
||||
/// <summary>
|
||||
/// Binds one category word for <c>mesh_detail.vert</c>'s statically used
|
||||
/// binding 9. EnvCell draws select their renderer-wide category via
|
||||
/// <c>uParamB=0</c>, so the value is semantically unused, but Vulkan still
|
||||
/// requires the declared descriptor to be valid.
|
||||
/// binding 9. The vertex shader treats only exact <c>uParamB=1</c> as the
|
||||
/// object/building-category sentinel; ordinary EnvCell zero and the CLIP
|
||||
/// references 100/255 and 200/255 therefore keep the environment category.
|
||||
/// Vulkan still requires the declared descriptor to be valid.
|
||||
/// </summary>
|
||||
internal static void BindEnvironmentDetailCategory(
|
||||
IGpuPassEncoder encoder,
|
||||
|
|
@ -491,11 +527,15 @@ public sealed unsafe partial class EnvCellRenderer
|
|||
_opaquePipeline = null;
|
||||
_alphaPipeline?.Dispose();
|
||||
_alphaPipeline = null;
|
||||
_clipPipeline?.Dispose();
|
||||
_clipPipeline = null;
|
||||
_additivePipeline?.Dispose();
|
||||
_additivePipeline = null;
|
||||
_detailPipeline?.Dispose();
|
||||
_detailPipeline = null;
|
||||
_transparentDetailPipeline?.Dispose();
|
||||
_transparentDetailPipeline = null;
|
||||
_clipDetailPipeline?.Dispose();
|
||||
_clipDetailPipeline = null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -142,8 +142,17 @@ public sealed partial class EnvCellRenderer :
|
|||
private readonly List<(ObjectRenderData renderData, ulong gfxObjId, int count, int offset)> _renderDrawCalls = new();
|
||||
private readonly Dictionary<ulong, List<InstanceData>> _filteredGroups = new();
|
||||
private readonly HashSet<List<InstanceData>> _filteredOwnedLists = new();
|
||||
// S4-c2 F3: four cull modes for each fixed-state family. The original
|
||||
// eight slots were non-additive/additive only; transparent replay also
|
||||
// needs distinct pure-ClipMap DDS/paletted ranges because their alpha-test
|
||||
// references differ and neither state may share an MDI call with ALPHA.
|
||||
private const int CullGroupCount = 4;
|
||||
private const int AdditiveGroupBase = 4;
|
||||
private const int ClipDdsGroupBase = 8;
|
||||
private const int ClipPalettedGroupBase = 12;
|
||||
private const int BatchGroupCount = 16;
|
||||
private readonly List<(ObjectRenderBatch batch, int instanceCount, int instanceOffset)>[] _batchesByCullGroup =
|
||||
Enumerable.Range(0, 8)
|
||||
Enumerable.Range(0, BatchGroupCount)
|
||||
.Select(_ => new List<(ObjectRenderBatch, int, int)>())
|
||||
.ToArray();
|
||||
private readonly List<int> _activeCullGroups = new(8);
|
||||
|
|
@ -1214,17 +1223,8 @@ public sealed partial class EnvCellRenderer :
|
|||
foreach (var batch in call.renderData.Batches)
|
||||
{
|
||||
// WB BaseObjectRenderManager.cs:723-731: pass-filter.
|
||||
if (renderPass != WbRenderPass.SinglePass)
|
||||
{
|
||||
if (batch.IsAdditive)
|
||||
{
|
||||
if (renderPass == WbRenderPass.Opaque) continue;
|
||||
}
|
||||
else if (!batch.IsTransparent)
|
||||
{
|
||||
if (renderPass == WbRenderPass.Transparent) continue;
|
||||
}
|
||||
}
|
||||
if (!BatchBelongsToPass(batch, renderPass))
|
||||
continue;
|
||||
|
||||
if (renderPass == WbRenderPass.Transparent
|
||||
&& transparentRoute != EnvCellTransparentRoute.All
|
||||
|
|
@ -1277,17 +1277,8 @@ public sealed partial class EnvCellRenderer :
|
|||
var call = drawCalls[callIndex];
|
||||
foreach (var batch in call.renderData.Batches)
|
||||
{
|
||||
if (renderPass != WbRenderPass.SinglePass)
|
||||
{
|
||||
if (batch.IsAdditive)
|
||||
{
|
||||
if (renderPass == WbRenderPass.Opaque) continue;
|
||||
}
|
||||
else if (!batch.IsTransparent)
|
||||
{
|
||||
if (renderPass == WbRenderPass.Transparent) continue;
|
||||
}
|
||||
}
|
||||
if (!BatchBelongsToPass(batch, renderPass))
|
||||
continue;
|
||||
|
||||
if (renderPass == WbRenderPass.Transparent
|
||||
&& transparentRoute != EnvCellTransparentRoute.All
|
||||
|
|
@ -1296,7 +1287,9 @@ public sealed partial class EnvCellRenderer :
|
|||
continue;
|
||||
}
|
||||
|
||||
int groupIndex = (int)batch.CullMode + (batch.IsAdditive ? 4 : 0);
|
||||
int groupIndex = ResolveBatchGroupIndex(
|
||||
batch,
|
||||
renderPass);
|
||||
List<(ObjectRenderBatch batch, int instanceCount, int instanceOffset)> group =
|
||||
_batchesByCullGroup[groupIndex];
|
||||
if (group.Count == 0)
|
||||
|
|
@ -1357,6 +1350,50 @@ public sealed partial class EnvCellRenderer :
|
|||
SubmitRhi(allInstances, renderPass, totalDraws, uniqueInstanceCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exact WB pass membership, with <see cref="WbRenderPass.SinglePass"/>
|
||||
/// intentionally unchanged. A non-additive transparent batch is delayed
|
||||
/// content and must never draw during the preceding opaque cell turn.
|
||||
/// </summary>
|
||||
internal static bool BatchBelongsToPass(
|
||||
ObjectRenderBatch batch,
|
||||
WbRenderPass renderPass) => renderPass switch
|
||||
{
|
||||
WbRenderPass.Opaque => !batch.IsAdditive && !batch.IsTransparent,
|
||||
WbRenderPass.Transparent => batch.IsAdditive || batch.IsTransparent,
|
||||
WbRenderPass.SinglePass => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
private static int ResolveBatchGroupIndex(
|
||||
ObjectRenderBatch batch,
|
||||
WbRenderPass renderPass)
|
||||
{
|
||||
int cull = (int)batch.CullMode;
|
||||
if ((uint)cull >= CullGroupCount)
|
||||
throw new ArgumentOutOfRangeException(nameof(batch), batch.CullMode, "Unknown cell-shell cull mode.");
|
||||
|
||||
// Preserve the old SinglePass grouping exactly: non-additive 0..3,
|
||||
// additive 4..7. Its submission path still keeps the opaque pipeline.
|
||||
if (renderPass != WbRenderPass.Transparent)
|
||||
return cull + (batch.IsAdditive ? AdditiveGroupBase : 0);
|
||||
|
||||
if (batch.IsAdditive)
|
||||
return cull + AdditiveGroupBase;
|
||||
|
||||
// Fixed state follows ConstructMesh's retained per-subset class, not
|
||||
// the DrawMesh row action. Detail-active row 1 is Immediate, but a
|
||||
// mask-0x08 subset (including legal positive-stipple 0x09) still needs
|
||||
// the CLIP base pipeline/reference. ConstructMesh's priority already
|
||||
// excludes additive/alpha-family rows before retaining this class.
|
||||
if ((batch.RetailSurfaceMask & RetailAlphaMeshRouter.MaskClipMap) == 0)
|
||||
return cull;
|
||||
|
||||
return cull + (batch.Key.PaletteId != 0
|
||||
? ClipPalettedGroupBase
|
||||
: ClipDdsGroupBase);
|
||||
}
|
||||
|
||||
internal static void AppendMdiDrawRange(
|
||||
List<MdiDrawRange> ranges,
|
||||
int groupIndex,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
using System.Numerics;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Gpu;
|
||||
using AcDream.App.Rendering.Gpu.Vk;
|
||||
|
|
@ -38,44 +40,38 @@ public sealed class EnvCellAlphaDrawSourceTests
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// A mixed cell creates one token in each list and neither set draws before
|
||||
/// the real queue drain. CLIP drains first and receives only the CLIP
|
||||
/// replay filter; ALPHA receives only ALPHA. Mutation checks: inverting the
|
||||
/// detail-off immediate predicate makes <c>Assert.Empty(log)</c> fail;
|
||||
/// removing the replay filter makes either route assertion fail.</summary>
|
||||
/// The real walk leaf executes the preceding opaque pass, dispatch, and
|
||||
/// queue drain. The opaque subset draws at the turn; CLIP and ALPHA do not.
|
||||
/// Each delayed list then replays exactly its own subset.</summary>
|
||||
[Fact]
|
||||
public void MixedCell_DefersOneTokenPerList_AndReplayIsListFiltered()
|
||||
public void WholeLeaf_MixedCellDrawsOpaqueAtTurnThenClipAndAlphaAtDrain()
|
||||
{
|
||||
using var fixture = new ProductionEnvCellFixture(
|
||||
detailSurfaceActive: false,
|
||||
0x08,
|
||||
0x02);
|
||||
EnvCellTransparentRoute routes = fixture.Renderer.GetTransparentRoutes(
|
||||
ProductionEnvCellFixture.CellId,
|
||||
detailSurfaceActive: false);
|
||||
BatchSpec.Opaque,
|
||||
BatchSpec.ClipDds,
|
||||
BatchSpec.Alpha);
|
||||
|
||||
fixture.Queue.BeginFrame();
|
||||
RetailPViewPassExecutor.DispatchTransparentCellShell(
|
||||
ProductionEnvCellFixture.CellId,
|
||||
routes,
|
||||
detailSurfaceActive: false,
|
||||
fixture.Queue,
|
||||
fixture.ClipSource,
|
||||
fixture.AlphaSource,
|
||||
fixture.ImmediateSink);
|
||||
fixture.Leaf.DrawCellShell(ProductionEnvCellFixture.CellId);
|
||||
|
||||
Assert.Equal(EnvCellTransparentRoute.Clip | EnvCellTransparentRoute.Alpha, routes);
|
||||
Assert.Empty(fixture.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>());
|
||||
GpuRecordedMultiDrawIndirect turnDraw = Assert.Single(
|
||||
fixture.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>());
|
||||
Assert.Equal(1u, turnDraw.DrawCount);
|
||||
Assert.Equal(["envcell-opaque"], DrawPipelineNames(fixture.Device));
|
||||
Assert.Equal(1, fixture.Queue.ClipCount);
|
||||
Assert.Equal(1, fixture.Queue.AlphaCount);
|
||||
Assert.False(Assert.Single(ClipEntries(fixture.Queue)).OverrideClipmap);
|
||||
|
||||
fixture.Queue.Flush(RetailAlphaFlushSite.DrawBuilding, 0f);
|
||||
fixture.Queue.EndFrame();
|
||||
|
||||
GpuRecordedMultiDrawIndirect[] draws =
|
||||
[.. fixture.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>()];
|
||||
Assert.Equal(2, draws.Length);
|
||||
Assert.Equal(3, draws.Length);
|
||||
Assert.All(draws, static draw => Assert.Equal(1u, draw.DrawCount));
|
||||
fixture.Queue.EndFrame();
|
||||
Assert.Equal(
|
||||
["envcell-opaque", "envcell-clip", "envcell-alpha"],
|
||||
DrawPipelineNames(fixture.Device));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -83,28 +79,29 @@ public sealed class EnvCellAlphaDrawSourceTests
|
|||
{
|
||||
using var fixture = new ProductionEnvCellFixture(
|
||||
detailSurfaceActive: true,
|
||||
0x08);
|
||||
EnvCellTransparentRoute routes = fixture.Renderer.GetTransparentRoutes(
|
||||
ProductionEnvCellFixture.CellId,
|
||||
detailSurfaceActive: true);
|
||||
BatchSpec.ClipDds);
|
||||
fixture.Queue.BeginFrame();
|
||||
|
||||
RetailPViewPassExecutor.DispatchTransparentCellShell(
|
||||
ProductionEnvCellFixture.CellId,
|
||||
routes,
|
||||
detailSurfaceActive: true,
|
||||
fixture.Queue,
|
||||
fixture.ClipSource,
|
||||
fixture.AlphaSource,
|
||||
fixture.ImmediateSink);
|
||||
fixture.Leaf.DrawCellShell(ProductionEnvCellFixture.CellId);
|
||||
|
||||
Assert.Equal(EnvCellTransparentRoute.Immediate, routes);
|
||||
Assert.Equal(1, fixture.ImmediateSink.DrawCount);
|
||||
Assert.True(fixture.ImmediateSink.LastDetailSurfaceActive);
|
||||
Assert.Equal(2, fixture.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>().Count());
|
||||
Assert.Equal(
|
||||
["envcell-clip", "envcell-retail-detail-clip"],
|
||||
DrawPipelineNames(fixture.Device));
|
||||
Assert.All(
|
||||
DrawPushConstants(fixture.Device),
|
||||
constants => Assert.Equal(200f / 255f, constants.ParamB));
|
||||
GpuPipelineDescription detailPipeline = fixture.Device.CreatedPipelines
|
||||
.Single(static pipeline =>
|
||||
pipeline.Description.Name == "envcell-retail-detail-clip")
|
||||
.Description;
|
||||
Assert.Equal(GpuBlendMode.RetailDetail, detailPipeline.Blend);
|
||||
Assert.True(detailPipeline.Depth.Test);
|
||||
Assert.True(detailPipeline.Depth.Write);
|
||||
Assert.Equal(GpuCompareOp.Equal, detailPipeline.Depth.Compare);
|
||||
Assert.Equal(0, fixture.Queue.PendingCount);
|
||||
fixture.Queue.EndFrame();
|
||||
Assert.Equal(1, fixture.ImmediateSink.DrawCount);
|
||||
Assert.Equal(2, fixture.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>().Count());
|
||||
}
|
||||
|
||||
/// <summary>Two cell tokens from the SAME <see cref="RetailPViewPassExecutor.EnvCellAlphaDrawSource"/>,
|
||||
|
|
@ -141,47 +138,202 @@ public sealed class EnvCellAlphaDrawSourceTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void ProductionDispatch_WarmedImmediateAndFilteredSourcesDoNotAllocate()
|
||||
public void ProductionWholeLeaf_WarmedScanSubmitRhiAndFilteredReplayDoNotAllocate()
|
||||
{
|
||||
var sink = new NoAllocRouteSink();
|
||||
var clipSource = new RetailPViewPassExecutor.EnvCellAlphaDrawSource(
|
||||
sink.Render,
|
||||
EnvCellTransparentRoute.Clip);
|
||||
var alphaSource = new RetailPViewPassExecutor.EnvCellAlphaDrawSource(
|
||||
sink.Render,
|
||||
EnvCellTransparentRoute.Alpha);
|
||||
RetailPViewPassExecutor.RenderImmediateEnvCellRoute immediate = sink.DrawImmediate;
|
||||
var queue = new RetailAlphaQueue();
|
||||
using var fixture = new ProductionEnvCellFixture(
|
||||
detailSurfaceActive: false,
|
||||
BatchSpec.Opaque,
|
||||
BatchSpec.ClipDds,
|
||||
BatchSpec.Alpha,
|
||||
ringCapacityBytes: 64 * 1024 * 1024);
|
||||
|
||||
for (int i = 0; i < 64; i++)
|
||||
RunMixed(queue, clipSource, alphaSource, immediate);
|
||||
|
||||
long before = GC.GetAllocatedBytesForCurrentThread();
|
||||
for (int i = 0; i < 1_000; i++)
|
||||
RunMixed(queue, clipSource, alphaSource, immediate);
|
||||
long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
|
||||
fixture.Device.Clear();
|
||||
fixture.Device.RecordingEnabled = false;
|
||||
long allocated = ZeroAllocationProbe.MeasureWarmed(
|
||||
fixture.RunWholeLeafFrame,
|
||||
batchSize: 256,
|
||||
warmupBatches: 2,
|
||||
samples: 4);
|
||||
|
||||
Assert.Equal(0, allocated);
|
||||
}
|
||||
|
||||
private static void RunMixed(
|
||||
RetailAlphaQueue queue,
|
||||
RetailPViewPassExecutor.EnvCellAlphaDrawSource clipSource,
|
||||
RetailPViewPassExecutor.EnvCellAlphaDrawSource alphaSource,
|
||||
RetailPViewPassExecutor.RenderImmediateEnvCellRoute immediate)
|
||||
[Theory]
|
||||
[InlineData(false, 200f / 255f)]
|
||||
[InlineData(true, 100f / 255f)]
|
||||
public void WholeLeaf_ClipDrainBindsExactStateAndTextureClassReference(
|
||||
bool paletted,
|
||||
float expectedReference)
|
||||
{
|
||||
queue.BeginFrame();
|
||||
RetailPViewPassExecutor.DispatchTransparentCellShell(
|
||||
0xF4180104u,
|
||||
EnvCellTransparentRoute.Immediate
|
||||
| EnvCellTransparentRoute.Clip
|
||||
| EnvCellTransparentRoute.Alpha,
|
||||
BatchSpec clip = paletted ? BatchSpec.ClipPaletted : BatchSpec.ClipDds;
|
||||
using var fixture = new ProductionEnvCellFixture(
|
||||
detailSurfaceActive: false,
|
||||
queue,
|
||||
clipSource,
|
||||
alphaSource,
|
||||
immediate);
|
||||
queue.EndFrame();
|
||||
clip);
|
||||
|
||||
fixture.Queue.BeginFrame();
|
||||
fixture.Leaf.DrawCellShell(ProductionEnvCellFixture.CellId);
|
||||
Assert.Empty(fixture.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>());
|
||||
Assert.False(Assert.Single(ClipEntries(fixture.Queue)).OverrideClipmap);
|
||||
fixture.Queue.EndFrame();
|
||||
|
||||
Assert.Equal(["envcell-clip"], DrawPipelineNames(fixture.Device));
|
||||
Assert.Equal(
|
||||
expectedReference,
|
||||
Assert.Single(DrawPushConstants(fixture.Device)).ParamB);
|
||||
|
||||
GpuPipelineDescription clipPipeline = fixture.Device.CreatedPipelines
|
||||
.Single(static pipeline => pipeline.Description.Name == "envcell-clip")
|
||||
.Description;
|
||||
Assert.Equal(GpuBlendMode.PremultipliedAlpha, clipPipeline.Blend);
|
||||
Assert.True(clipPipeline.Depth.Test);
|
||||
Assert.True(clipPipeline.Depth.Write);
|
||||
Assert.Equal(WorldDepthContract.WorldCompare, clipPipeline.Depth.Compare);
|
||||
Assert.False(clipPipeline.AlphaToCoverage);
|
||||
|
||||
GpuPipelineDescription alphaPipeline = fixture.Device.CreatedPipelines
|
||||
.Single(static pipeline => pipeline.Description.Name == "envcell-alpha")
|
||||
.Description;
|
||||
Assert.Equal(GpuBlendMode.StraightAlpha, alphaPipeline.Blend);
|
||||
Assert.True(alphaPipeline.Depth.Test);
|
||||
Assert.False(alphaPipeline.Depth.Write);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WholeLeaf_PositiveStippleClipMaskUsesClipPipelineAndDdsReference()
|
||||
{
|
||||
using var fixture = new ProductionEnvCellFixture(
|
||||
detailSurfaceActive: false,
|
||||
BatchSpec.ClipPositiveStippleDds);
|
||||
|
||||
fixture.Queue.BeginFrame();
|
||||
fixture.Leaf.DrawCellShell(ProductionEnvCellFixture.CellId);
|
||||
|
||||
Assert.Empty(fixture.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>());
|
||||
Assert.Equal(1, fixture.Queue.ClipCount);
|
||||
Assert.Equal(0, fixture.Queue.AlphaCount);
|
||||
Assert.False(Assert.Single(ClipEntries(fixture.Queue)).OverrideClipmap);
|
||||
|
||||
fixture.Queue.EndFrame();
|
||||
|
||||
Assert.Equal(["envcell-clip"], DrawPipelineNames(fixture.Device));
|
||||
Assert.Equal(
|
||||
200f / 255f,
|
||||
Assert.Single(DrawPushConstants(fixture.Device)).ParamB);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Vulkan has no fixed-function alpha test. The shader's discard-on-less
|
||||
/// spelling is exactly GREATER_EQUAL: equality survives. Changing either
|
||||
/// comparison to <= (strict GREATER) fails this named source+manifest pin.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ClipShaders_UseGreaterEqualForThePerRangeReference()
|
||||
{
|
||||
string root = RepositoryRoot();
|
||||
string modern = File.ReadAllText(Path.Combine(
|
||||
root, "src", "AcDream.App", "Rendering", "Shaders", "mesh_modern.frag"));
|
||||
string detail = File.ReadAllText(Path.Combine(
|
||||
root, "src", "AcDream.App", "Rendering", "Shaders", "mesh_detail.frag"));
|
||||
string detailVertex = File.ReadAllText(Path.Combine(
|
||||
root, "src", "AcDream.App", "Rendering", "Shaders", "mesh_detail.vert"));
|
||||
|
||||
Assert.Contains("if (color.a < alphaCutoff) discard;", modern, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("if (color.a <= alphaCutoff) discard;", modern, StringComparison.Ordinal);
|
||||
Assert.Contains("isRetailClipReference(uParamB) ? uParamB : 0.05", modern, StringComparison.Ordinal);
|
||||
Assert.Contains("isRetailClipReference(uParamB) ? uParamB : 0.05", detail, StringComparison.Ordinal);
|
||||
foreach (string shader in new[] { modern, detail })
|
||||
{
|
||||
Assert.Contains("abs(value - (100.0 / 255.0)) < 0.000001", shader, StringComparison.Ordinal);
|
||||
Assert.Contains("abs(value - (200.0 / 255.0)) < 0.000001", shader, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("uParamB > 0.0", shader, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("value - 0.5", shader, StringComparison.Ordinal);
|
||||
}
|
||||
Assert.Contains("if (base.a < alphaCutoff)", detail, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("if (base.a <= alphaCutoff)", detail, StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"vDetailCategory = uParamB == 1.0",
|
||||
detailVertex,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)] // Flush
|
||||
[InlineData(1)] // EndFrame
|
||||
[InlineData(2)] // AbortFrame
|
||||
public void RejectedEnvCellStorm_RollsBackPayloadAndStillResetsFirstUseSource(int completion)
|
||||
{
|
||||
const int retainedGeometricBound = 4096;
|
||||
int rejectedResetCount = 0;
|
||||
int rejectedDrawCount = 0;
|
||||
var accepted = new RetailPViewPassExecutor.EnvCellAlphaDrawSource(
|
||||
static (_, _, _) => { },
|
||||
EnvCellTransparentRoute.Clip);
|
||||
var rejected = new RetailPViewPassExecutor.EnvCellAlphaDrawSource(
|
||||
(_, _, _) => rejectedDrawCount++,
|
||||
EnvCellTransparentRoute.Clip,
|
||||
() => rejectedResetCount++);
|
||||
var unusedAlpha = new RetailPViewPassExecutor.EnvCellAlphaDrawSource(
|
||||
static (_, _, _) => { },
|
||||
EnvCellTransparentRoute.Alpha);
|
||||
var queue = new RetailAlphaQueue();
|
||||
RetailPViewPassExecutor.RenderImmediateEnvCellRoute neverImmediate =
|
||||
static (_, _, _) => throw new InvalidOperationException();
|
||||
|
||||
queue.BeginFrame();
|
||||
for (int i = 0; i < RetailAlphaQueue.ListCapacity; i++)
|
||||
{
|
||||
RetailPViewPassExecutor.DispatchTransparentCellShell(
|
||||
(uint)i,
|
||||
EnvCellTransparentRoute.Clip,
|
||||
detailSurfaceActive: false,
|
||||
queue,
|
||||
accepted,
|
||||
unusedAlpha,
|
||||
neverImmediate);
|
||||
}
|
||||
for (int i = 0; i < RetailAlphaQueue.ListCapacity * 3; i++)
|
||||
{
|
||||
RetailPViewPassExecutor.DispatchTransparentCellShell(
|
||||
0xF4180104u,
|
||||
EnvCellTransparentRoute.Clip,
|
||||
detailSurfaceActive: false,
|
||||
queue,
|
||||
rejected,
|
||||
unusedAlpha,
|
||||
neverImmediate);
|
||||
}
|
||||
|
||||
Assert.Equal(RetailAlphaQueue.ListCapacity, queue.ClipCount);
|
||||
Assert.Equal(RetailAlphaQueue.ListCapacity, accepted.PendingCount);
|
||||
Assert.Equal(0, rejected.PendingCount);
|
||||
Assert.InRange(accepted.PendingCapacity, RetailAlphaQueue.ListCapacity, retainedGeometricBound);
|
||||
Assert.InRange(rejected.PendingCapacity, 0, retainedGeometricBound);
|
||||
|
||||
switch (completion)
|
||||
{
|
||||
case 0:
|
||||
queue.Flush(RetailAlphaFlushSite.DrawBuilding, 0f);
|
||||
queue.AbortFrame();
|
||||
break;
|
||||
case 1:
|
||||
queue.EndFrame();
|
||||
break;
|
||||
case 2:
|
||||
queue.AbortFrame();
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(completion));
|
||||
}
|
||||
|
||||
Assert.Equal(1, rejectedResetCount);
|
||||
Assert.Equal(0, rejectedDrawCount);
|
||||
Assert.Equal(0, rejected.PendingCount);
|
||||
Assert.InRange(accepted.PendingCapacity, 0, retainedGeometricBound);
|
||||
Assert.InRange(accepted.PreparedCapacity, 0, retainedGeometricBound);
|
||||
Assert.InRange(accepted.DrawCapacity, 0, retainedGeometricBound);
|
||||
Assert.InRange(rejected.PendingCapacity, 0, retainedGeometricBound);
|
||||
Assert.InRange(rejected.PreparedCapacity, 0, retainedGeometricBound);
|
||||
Assert.InRange(rejected.DrawCapacity, 0, retainedGeometricBound);
|
||||
}
|
||||
|
||||
private static RetailPViewPassExecutor.EnvCellAlphaDrawSource Source(
|
||||
|
|
@ -197,6 +349,97 @@ public sealed class EnvCellAlphaDrawSourceTests
|
|||
},
|
||||
expectedRoute);
|
||||
|
||||
private static List<RetailAlphaEntry> ClipEntries(RetailAlphaQueue queue) =>
|
||||
(List<RetailAlphaEntry>)typeof(RetailAlphaQueue)
|
||||
.GetField("_clip", BindingFlags.NonPublic | BindingFlags.Instance)!
|
||||
.GetValue(queue)!;
|
||||
|
||||
private static string[] DrawPipelineNames(RecordingGpuDevice device)
|
||||
{
|
||||
var names = new List<string>();
|
||||
IReadOnlyList<GpuRecordedCall> calls = device.Calls;
|
||||
for (int i = 0; i < calls.Count; i++)
|
||||
{
|
||||
if (calls[i] is not GpuRecordedMultiDrawIndirect)
|
||||
continue;
|
||||
for (int prior = i - 1; prior >= 0; prior--)
|
||||
{
|
||||
if (calls[prior] is GpuRecordedPipelineBind bind)
|
||||
{
|
||||
names.Add(bind.PipelineName);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return [.. names];
|
||||
}
|
||||
|
||||
private static GpuPushConstants[] DrawPushConstants(RecordingGpuDevice device)
|
||||
{
|
||||
var constants = new List<GpuPushConstants>();
|
||||
IReadOnlyList<GpuRecordedCall> calls = device.Calls;
|
||||
for (int i = 0; i < calls.Count; i++)
|
||||
{
|
||||
if (calls[i] is not GpuRecordedMultiDrawIndirect)
|
||||
continue;
|
||||
for (int prior = i - 1; prior >= 0; prior--)
|
||||
{
|
||||
if (calls[prior] is GpuRecordedPushConstants push)
|
||||
{
|
||||
constants.Add(push.Constants);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return [.. constants];
|
||||
}
|
||||
|
||||
private static string RepositoryRoot()
|
||||
{
|
||||
DirectoryInfo? cursor = new(AppContext.BaseDirectory);
|
||||
while (cursor is not null && !File.Exists(Path.Combine(cursor.FullName, "AcDream.slnx")))
|
||||
cursor = cursor.Parent;
|
||||
return cursor?.FullName
|
||||
?? throw new DirectoryNotFoundException("Could not locate AcDream.slnx.");
|
||||
}
|
||||
|
||||
private readonly record struct BatchSpec(
|
||||
byte Mask,
|
||||
bool IsTransparent,
|
||||
AcDream.Core.Meshing.TranslucencyKind Translucency,
|
||||
uint PaletteId)
|
||||
{
|
||||
internal static BatchSpec Opaque { get; } = new(
|
||||
0x00,
|
||||
IsTransparent: false,
|
||||
AcDream.Core.Meshing.TranslucencyKind.Opaque,
|
||||
PaletteId: 0);
|
||||
|
||||
internal static BatchSpec ClipDds { get; } = new(
|
||||
RetailAlphaMeshRouter.MaskClipMap,
|
||||
IsTransparent: true,
|
||||
AcDream.Core.Meshing.TranslucencyKind.ClipMap,
|
||||
PaletteId: 0);
|
||||
|
||||
internal static BatchSpec ClipPaletted { get; } = new(
|
||||
RetailAlphaMeshRouter.MaskClipMap,
|
||||
IsTransparent: true,
|
||||
AcDream.Core.Meshing.TranslucencyKind.ClipMap,
|
||||
PaletteId: 0x04000001u);
|
||||
|
||||
internal static BatchSpec ClipPositiveStippleDds { get; } = new(
|
||||
RetailAlphaMeshRouter.MaskClipMap | RetailAlphaMeshRouter.MaskPositiveStipple,
|
||||
IsTransparent: true,
|
||||
AcDream.Core.Meshing.TranslucencyKind.ClipMap,
|
||||
PaletteId: 0);
|
||||
|
||||
internal static BatchSpec Alpha { get; } = new(
|
||||
RetailAlphaMeshRouter.MaskAlphaFamily,
|
||||
IsTransparent: true,
|
||||
AcDream.Core.Meshing.TranslucencyKind.AlphaBlend,
|
||||
PaletteId: 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recording-GPU fixture for the production EnvCell scan, dispatch and
|
||||
/// filtered replay path. The two retained masks enter through the real
|
||||
|
|
@ -215,9 +458,12 @@ public sealed class EnvCellAlphaDrawSourceTests
|
|||
|
||||
public ProductionEnvCellFixture(
|
||||
bool detailSurfaceActive,
|
||||
params byte[] retainedMasks)
|
||||
BatchSpec firstBatch,
|
||||
BatchSpec secondBatch = default,
|
||||
BatchSpec thirdBatch = default,
|
||||
int ringCapacityBytes = 8 * 1024 * 1024)
|
||||
{
|
||||
Device = new RecordingGpuDevice();
|
||||
Device = new RecordingGpuDevice(ringCapacityBytes);
|
||||
_frames = new GpuDeviceFrameLifetime(Device);
|
||||
_scope = new VulkanWorldPassScope(sampleCount: 1);
|
||||
_meshManager = new ObjectMeshManager(
|
||||
|
|
@ -236,17 +482,27 @@ public sealed class EnvCellAlphaDrawSourceTests
|
|||
new VertexPositionNormalTexture { Position = new Vector3(0, 1, 0) },
|
||||
],
|
||||
};
|
||||
var batches = new List<TextureBatchData>(retainedMasks.Length);
|
||||
for (int i = 0; i < retainedMasks.Length; i++)
|
||||
BatchSpec[] specs = secondBatch == default
|
||||
? [firstBatch]
|
||||
: thirdBatch == default
|
||||
? [firstBatch, secondBatch]
|
||||
: [firstBatch, secondBatch, thirdBatch];
|
||||
var batches = new List<TextureBatchData>(specs.Length);
|
||||
for (int i = 0; i < specs.Length; i++)
|
||||
{
|
||||
BatchSpec spec = specs[i];
|
||||
batches.Add(new TextureBatchData
|
||||
{
|
||||
Key = new TextureKey { SurfaceId = 0x08000BFFu + (uint)i },
|
||||
Key = new TextureKey
|
||||
{
|
||||
SurfaceId = 0x08000BFFu + (uint)i,
|
||||
PaletteId = spec.PaletteId,
|
||||
},
|
||||
TextureData = new byte[8 * 8 * 4],
|
||||
Indices = [0, 1, 2],
|
||||
IsTransparent = true,
|
||||
Translucency = AcDream.Core.Meshing.TranslucencyKind.AlphaBlend,
|
||||
RetailSurfaceMask = retainedMasks[i],
|
||||
IsTransparent = spec.IsTransparent,
|
||||
Translucency = spec.Translucency,
|
||||
RetailSurfaceMask = spec.Mask,
|
||||
CullMode = CullMode.Clockwise,
|
||||
IsCellShell = true,
|
||||
SourceSurfaceIndex = i,
|
||||
|
|
@ -291,6 +547,10 @@ public sealed class EnvCellAlphaDrawSourceTests
|
|||
},
|
||||
},
|
||||
});
|
||||
((HashSet<uint>)typeof(EnvCellRenderer)
|
||||
.GetField("_transparentCellIds", BindingFlags.NonPublic | BindingFlags.Instance)!
|
||||
.GetValue(Renderer)!)
|
||||
.Add(CellId);
|
||||
|
||||
Queue = new RetailAlphaQueue();
|
||||
ClipSource = new RetailPViewPassExecutor.EnvCellAlphaDrawSource(
|
||||
|
|
@ -299,7 +559,28 @@ public sealed class EnvCellAlphaDrawSourceTests
|
|||
AlphaSource = new RetailPViewPassExecutor.EnvCellAlphaDrawSource(
|
||||
Renderer.RenderTransparentOrdered,
|
||||
EnvCellTransparentRoute.Alpha);
|
||||
ImmediateSink = new ProductionImmediateSink(Renderer);
|
||||
var passes = new RetailPViewPassExecutor(
|
||||
new NullWorldPassSurface(),
|
||||
NullRenderFrameGlState.Instance,
|
||||
ClipFrame.NoClip(),
|
||||
terrain: null,
|
||||
Renderer,
|
||||
(WbDrawDispatcher)RuntimeHelpers.GetUninitializedObject(typeof(WbDrawDispatcher)),
|
||||
sky: null,
|
||||
particles: null,
|
||||
particleRenderer: null,
|
||||
portalDepthMask: null,
|
||||
Queue,
|
||||
(WorldRenderDiagnostics)RuntimeHelpers.GetUninitializedObject(typeof(WorldRenderDiagnostics)),
|
||||
(TerrainDrawDiagnosticsController)RuntimeHelpers.GetUninitializedObject(
|
||||
typeof(TerrainDrawDiagnosticsController)));
|
||||
Leaf = new WalkProductionLeafRenderer(
|
||||
passes,
|
||||
new RetailPViewFrameInput(),
|
||||
new ClipFrameAssembly(),
|
||||
static () => { },
|
||||
static () => { },
|
||||
static () => 0);
|
||||
|
||||
_frames.BeginFrame();
|
||||
IGpuFrame frame = _frames.CurrentFrame!;
|
||||
|
|
@ -323,7 +604,14 @@ public sealed class EnvCellAlphaDrawSourceTests
|
|||
|
||||
public RetailPViewPassExecutor.EnvCellAlphaDrawSource AlphaSource { get; }
|
||||
|
||||
public ProductionImmediateSink ImmediateSink { get; }
|
||||
public WalkProductionLeafRenderer Leaf { get; }
|
||||
|
||||
public void RunWholeLeafFrame()
|
||||
{
|
||||
Queue.BeginFrame();
|
||||
Leaf.DrawCellShell(CellId);
|
||||
Queue.EndFrame();
|
||||
}
|
||||
|
||||
private static bool DetailOn() => true;
|
||||
|
||||
|
|
@ -339,22 +627,15 @@ public sealed class EnvCellAlphaDrawSourceTests
|
|||
}
|
||||
}
|
||||
|
||||
private sealed class ProductionImmediateSink(EnvCellRenderer renderer) :
|
||||
IEnvCellImmediateDrawSink
|
||||
private sealed class NullWorldPassSurface : IWorldPassSurface
|
||||
{
|
||||
public int DrawCount { get; private set; }
|
||||
public void PrepareClipFrame() { }
|
||||
|
||||
public bool LastDetailSurfaceActive { get; private set; }
|
||||
public void EnableClipDistances() { }
|
||||
|
||||
public void DrawImmediate(
|
||||
uint cellId,
|
||||
EnvCellTransparentRoute route,
|
||||
bool detailSurfaceActive)
|
||||
{
|
||||
DrawCount++;
|
||||
LastDetailSurfaceActive = detailSurfaceActive;
|
||||
renderer.RenderTransparentOrdered([cellId], route, detailSurfaceActive);
|
||||
}
|
||||
public void DisableClipDistances() { }
|
||||
|
||||
public void ClearInteriorDepth() => throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
private sealed class NullPreparedAssetSource : IPreparedAssetSource
|
||||
|
|
@ -395,20 +676,4 @@ public sealed class EnvCellAlphaDrawSourceTests
|
|||
}
|
||||
}
|
||||
|
||||
private sealed class NoAllocRouteSink
|
||||
{
|
||||
public void Render(
|
||||
IReadOnlyList<uint> cells,
|
||||
EnvCellTransparentRoute route,
|
||||
bool detailSurfaceActive)
|
||||
{
|
||||
}
|
||||
|
||||
public void DrawImmediate(
|
||||
uint cellId,
|
||||
EnvCellTransparentRoute route,
|
||||
bool detailSurfaceActive)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,16 +74,17 @@ public sealed class GpuContractTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void BlendModesCoverEveryRetailTranslucencyKind()
|
||||
public void BlendModesCoverEveryRetailRendererState()
|
||||
{
|
||||
// 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(5, Enum.GetValues<GpuBlendMode>().Length);
|
||||
// surface's TranslucencyKind. Retail also selects ONE/INVSRCALPHA for a
|
||||
// pure Base1ClipMap with override=false (SetSurface @0x0059c72a), while
|
||||
// the detail combine is a separate renderer state. Collapsing any of
|
||||
// these modes changes the corresponding subset's composition.
|
||||
Assert.Equal(6, Enum.GetValues<GpuBlendMode>().Length);
|
||||
Assert.Contains(GpuBlendMode.None, Enum.GetValues<GpuBlendMode>());
|
||||
Assert.Contains(GpuBlendMode.StraightAlpha, Enum.GetValues<GpuBlendMode>());
|
||||
Assert.Contains(GpuBlendMode.PremultipliedAlpha, Enum.GetValues<GpuBlendMode>());
|
||||
Assert.Contains(GpuBlendMode.Additive, Enum.GetValues<GpuBlendMode>());
|
||||
Assert.Contains(GpuBlendMode.InverseAlpha, Enum.GetValues<GpuBlendMode>());
|
||||
Assert.Contains(GpuBlendMode.RetailDetail, Enum.GetValues<GpuBlendMode>());
|
||||
|
|
|
|||
|
|
@ -135,6 +135,14 @@ internal sealed class RecordingGpuDevice : IGpuDevice, IGpuPipelineFormatVariant
|
|||
/// <summary>Every recorded call, in submission order.</summary>
|
||||
public IReadOnlyList<GpuRecordedCall> Calls => _calls;
|
||||
|
||||
/// <summary>
|
||||
/// Allocation measurements may drive the real renderer/RHI submission
|
||||
/// path without allocating one diagnostic record per call. Resource,
|
||||
/// frame, ring, and encoder behavior stays identical; only call-record
|
||||
/// object construction is suppressed when this is false.
|
||||
/// </summary>
|
||||
public bool RecordingEnabled { get; set; } = true;
|
||||
|
||||
/// <summary>Backing store for ring allocations, so tests can read what a renderer wrote.</summary>
|
||||
public ReadOnlySpan<byte> RingBytes => _ring;
|
||||
|
||||
|
|
@ -410,7 +418,8 @@ internal sealed class RecordingGpuDevice : IGpuDevice, IGpuPipelineFormatVariant
|
|||
}
|
||||
|
||||
_ringCursor = aligned + (uint)byteCount;
|
||||
_calls.Add(new GpuRecordedRingAllocation(usage, byteCount, aligned));
|
||||
if (RecordingEnabled)
|
||||
_calls.Add(new GpuRecordedRingAllocation(usage, byteCount, aligned));
|
||||
return new GpuRingAllocation(RingBuffer, aligned, _ring.AsSpan((int)aligned, byteCount));
|
||||
}
|
||||
|
||||
|
|
@ -460,7 +469,8 @@ internal sealed class RecordingGpuFrame(RecordingGpuDevice device, long serial,
|
|||
"Published host writes require a host-writable storage buffer.",
|
||||
nameof(buffer));
|
||||
}
|
||||
device.Record(new GpuRecordedHostStorageVisibility(buffer.Name));
|
||||
if (device.RecordingEnabled)
|
||||
device.Record(new GpuRecordedHostStorageVisibility(buffer.Name));
|
||||
}
|
||||
|
||||
public IGpuPassEncoder BeginPass(GpuPassDescription description)
|
||||
|
|
@ -549,60 +559,97 @@ internal sealed class RecordingGpuPassEncoder(RecordingGpuDevice device, GpuPass
|
|||
throw new InvalidOperationException("Pipeline and pass colour-attachment intents must match.");
|
||||
if (pipeline.Description.ViewMask != Pass.ViewMask)
|
||||
throw new InvalidOperationException("Pipeline and pass view masks must match.");
|
||||
device.Record(new GpuRecordedPipelineBind(pipeline.Description.Name));
|
||||
if (device.RecordingEnabled)
|
||||
device.Record(new GpuRecordedPipelineBind(pipeline.Description.Name));
|
||||
}
|
||||
|
||||
public void BindStorageBuffer(uint binding, IGpuBuffer buffer, uint offsetBytes, uint sizeBytes)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(buffer);
|
||||
device.Record(new GpuRecordedStorageBind(binding, buffer.Name, offsetBytes, sizeBytes));
|
||||
if (device.RecordingEnabled)
|
||||
device.Record(new GpuRecordedStorageBind(binding, buffer.Name, offsetBytes, sizeBytes));
|
||||
}
|
||||
|
||||
public void BindUniformBuffer(uint binding, IGpuBuffer buffer, uint offsetBytes, uint sizeBytes)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(buffer);
|
||||
device.Record(new GpuRecordedUniformBind(binding, buffer.Name, offsetBytes, sizeBytes));
|
||||
if (device.RecordingEnabled)
|
||||
device.Record(new GpuRecordedUniformBind(binding, buffer.Name, offsetBytes, sizeBytes));
|
||||
}
|
||||
|
||||
public void BindVertexBuffer(uint binding, IGpuBuffer buffer, uint offsetBytes)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(buffer);
|
||||
device.Record(new GpuRecordedVertexBind(binding, buffer.Name, offsetBytes));
|
||||
if (device.RecordingEnabled)
|
||||
device.Record(new GpuRecordedVertexBind(binding, buffer.Name, offsetBytes));
|
||||
}
|
||||
|
||||
public void BindIndexBuffer(IGpuBuffer buffer, uint offsetBytes, GpuIndexType indexType)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(buffer);
|
||||
device.Record(new GpuRecordedIndexBind(buffer.Name, offsetBytes, indexType));
|
||||
if (device.RecordingEnabled)
|
||||
device.Record(new GpuRecordedIndexBind(buffer.Name, offsetBytes, indexType));
|
||||
}
|
||||
|
||||
public void SetPushConstants(in GpuPushConstants constants) =>
|
||||
device.Record(new GpuRecordedPushConstants(constants));
|
||||
public void SetPushConstants(in GpuPushConstants constants)
|
||||
{
|
||||
if (device.RecordingEnabled)
|
||||
device.Record(new GpuRecordedPushConstants(constants));
|
||||
}
|
||||
|
||||
public void SetViewport(int x, int y, int width, int height) =>
|
||||
device.Record(new GpuRecordedViewport(x, y, width, height));
|
||||
public void SetViewport(int x, int y, int width, int height)
|
||||
{
|
||||
if (device.RecordingEnabled)
|
||||
device.Record(new GpuRecordedViewport(x, y, width, height));
|
||||
}
|
||||
|
||||
public void SetScissor(int x, int y, int width, int height) =>
|
||||
device.Record(new GpuRecordedScissor(x, y, width, height));
|
||||
public void SetScissor(int x, int y, int width, int height)
|
||||
{
|
||||
if (device.RecordingEnabled)
|
||||
device.Record(new GpuRecordedScissor(x, y, width, height));
|
||||
}
|
||||
|
||||
public void SetCullMode(GpuCullMode cullMode) => device.Record(new GpuRecordedCullMode(cullMode));
|
||||
public void SetCullMode(GpuCullMode cullMode)
|
||||
{
|
||||
if (device.RecordingEnabled)
|
||||
device.Record(new GpuRecordedCullMode(cullMode));
|
||||
}
|
||||
|
||||
public void SetFrontFace(GpuFrontFace frontFace) => device.Record(new GpuRecordedFrontFace(frontFace));
|
||||
public void SetFrontFace(GpuFrontFace frontFace)
|
||||
{
|
||||
if (device.RecordingEnabled)
|
||||
device.Record(new GpuRecordedFrontFace(frontFace));
|
||||
}
|
||||
|
||||
public void SetStencil(in GpuStencilState stencil) => device.Record(new GpuRecordedStencil(stencil));
|
||||
public void SetStencil(in GpuStencilState stencil)
|
||||
{
|
||||
if (device.RecordingEnabled)
|
||||
device.Record(new GpuRecordedStencil(stencil));
|
||||
}
|
||||
|
||||
public void SetDepthWrite(bool enabled) => device.Record(new GpuRecordedDepthWrite(enabled));
|
||||
public void SetDepthWrite(bool enabled)
|
||||
{
|
||||
if (device.RecordingEnabled)
|
||||
device.Record(new GpuRecordedDepthWrite(enabled));
|
||||
}
|
||||
|
||||
public void DrawIndexed(uint indexCount, uint instanceCount, uint firstIndex, int vertexOffset, uint firstInstance) =>
|
||||
device.Record(new GpuRecordedDrawIndexed(indexCount, instanceCount, firstIndex, vertexOffset, firstInstance));
|
||||
public void DrawIndexed(uint indexCount, uint instanceCount, uint firstIndex, int vertexOffset, uint firstInstance)
|
||||
{
|
||||
if (device.RecordingEnabled)
|
||||
device.Record(new GpuRecordedDrawIndexed(indexCount, instanceCount, firstIndex, vertexOffset, firstInstance));
|
||||
}
|
||||
|
||||
public void Draw(uint vertexCount, uint instanceCount, uint firstVertex, uint firstInstance) =>
|
||||
device.Record(new GpuRecordedDraw(vertexCount, instanceCount, firstVertex, firstInstance));
|
||||
public void Draw(uint vertexCount, uint instanceCount, uint firstVertex, uint firstInstance)
|
||||
{
|
||||
if (device.RecordingEnabled)
|
||||
device.Record(new GpuRecordedDraw(vertexCount, instanceCount, firstVertex, firstInstance));
|
||||
}
|
||||
|
||||
public void MultiDrawIndexedIndirect(IGpuBuffer commands, uint offsetBytes, uint drawCount, uint strideBytes)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(commands);
|
||||
device.Record(new GpuRecordedMultiDrawIndirect(commands.Name, offsetBytes, drawCount, strideBytes));
|
||||
if (device.RecordingEnabled)
|
||||
device.Record(new GpuRecordedMultiDrawIndirect(commands.Name, offsetBytes, drawCount, strideBytes));
|
||||
}
|
||||
|
||||
public IDisposable BeginTimerScope(string scopeName)
|
||||
|
|
|
|||
|
|
@ -41,7 +41,13 @@ public sealed class VulkanShaderManifestTests
|
|||
{
|
||||
["debug_line.frag.spv"] = "02fc04880bc5eb74353566f914675244038125c71443964decdc28e8199264df",
|
||||
["debug_line.vert.spv"] = "f9c6a9b575bb07a426fb6ade677bca96a7752ca6b120e8f6451363ba73b51140",
|
||||
["mesh_modern.frag.spv"] = "b702b644862aca31ce1fb0677adc5872b39c4ea87f595a89363b44d10f2cc50e",
|
||||
// Re-pinned 2026-09-04 (Campaign OVERHAUL S4-c2 owner-authorized
|
||||
// repair): Vulkan expresses SetSurface's pure-ClipMap alpha test
|
||||
// through uParamB, recognizing only the paletted 100/255 and DDS
|
||||
// 200/255 references and using discard-on-less so equality passes.
|
||||
// The paired binary selects those globals and
|
||||
// D3DCMP_GREATEREQUAL at 0x0059c73c..0x0059c838.
|
||||
["mesh_modern.frag.spv"] = "1b3a59d0cc7c5bb6acf793d57d9673d860a7eb50bdd51770781f59feac4604a1",
|
||||
// mesh_modern.vert re-pinned 2026-09-03 (S3 review fix round 1,
|
||||
// F5): the binding=2 ClipRegionBuf (CellClip) SSBO, its
|
||||
// gl_ClipDistance loop, and the gl_PerVertex redeclaration are
|
||||
|
|
|
|||
|
|
@ -132,11 +132,14 @@ public sealed class VulkanViewportMappingTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void AllThreeRetailBlendModesAreRepresentable()
|
||||
public void AllRetailBlendModesAreRepresentable()
|
||||
{
|
||||
Assert.Equal(
|
||||
(BlendFactor.SrcAlpha, BlendFactor.OneMinusSrcAlpha),
|
||||
VulkanViewportMapping.BlendFactorsOf(GpuBlendMode.StraightAlpha));
|
||||
Assert.Equal(
|
||||
(BlendFactor.One, BlendFactor.OneMinusSrcAlpha),
|
||||
VulkanViewportMapping.BlendFactorsOf(GpuBlendMode.PremultipliedAlpha));
|
||||
Assert.Equal(
|
||||
(BlendFactor.SrcAlpha, BlendFactor.One),
|
||||
VulkanViewportMapping.BlendFactorsOf(GpuBlendMode.Additive));
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using System.Collections;
|
||||
using System.Numerics;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
|
@ -9,6 +10,7 @@ using AcDream.App.Tests.Rendering.Gpu;
|
|||
using AcDream.Content;
|
||||
using AcDream.Core.Meshing;
|
||||
using AcDream.Core.Vfx;
|
||||
using Chorizite.Core.Render.Enums;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace AcDream.App.Tests.Rendering;
|
||||
|
|
@ -189,54 +191,110 @@ public sealed class ParticleRendererRouteTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void ProductionDispatch_WarmedAppendAndImmediatePathsDoNotAllocate()
|
||||
public void ProductionImmediateMesh_WarmedDrawImmediateParticleSubmissionRhiDoesNotAllocate()
|
||||
{
|
||||
var queue = new RetailAlphaQueue();
|
||||
var sink = new NoAllocParticleSink();
|
||||
ParticleRenderer.ReserveDeferredParticleDraw reserve = sink.Reserve;
|
||||
ParticleRenderer.DrawImmediateParticle immediate = sink.DrawImmediate;
|
||||
using var device = new RecordingGpuDevice(64 * 1024 * 1024);
|
||||
using var manager = new ObjectMeshManager(
|
||||
new VulkanMeshPipelineDevice(device.Retirement),
|
||||
device,
|
||||
new NullPreparedAssetSource(),
|
||||
NullLogger<ObjectMeshManager>.Instance);
|
||||
ObjectRenderData renderData = Assert.IsType<ObjectRenderData>(manager.UploadMeshData(
|
||||
new ObjectMeshData
|
||||
{
|
||||
ObjectId = 0x010001ECu,
|
||||
Vertices =
|
||||
[
|
||||
new VertexPositionNormalTexture { Position = Vector3.Zero },
|
||||
new VertexPositionNormalTexture { Position = Vector3.UnitX },
|
||||
new VertexPositionNormalTexture { Position = Vector3.UnitY },
|
||||
],
|
||||
TextureBatches =
|
||||
{
|
||||
[(8, 8, TextureFormat.RGBA8)] =
|
||||
[
|
||||
new TextureBatchData
|
||||
{
|
||||
Key = new TextureKey { SurfaceId = 0x08000015u },
|
||||
TextureData = new byte[8 * 8 * 4],
|
||||
Indices = [0, 1, 2],
|
||||
Translucency = TranslucencyKind.Opaque,
|
||||
CullMode = DatReaderWriter.Enums.CullMode.Clockwise,
|
||||
},
|
||||
],
|
||||
},
|
||||
}));
|
||||
var adapter = (WbMeshAdapter)RuntimeHelpers.GetUninitializedObject(typeof(WbMeshAdapter));
|
||||
typeof(WbMeshAdapter).GetField(
|
||||
"_meshManager",
|
||||
BindingFlags.Instance | BindingFlags.NonPublic)!
|
||||
.SetValue(adapter, manager);
|
||||
var frames = new GpuDeviceFrameLifetime(device);
|
||||
var scope = new VulkanWorldPassScope(sampleCount: 1);
|
||||
using var renderer = new ParticleRenderer(
|
||||
device,
|
||||
frames,
|
||||
scope,
|
||||
new ParticleSystem(new EmitterDescRegistry(), new Random(42)),
|
||||
meshAdapter: adapter);
|
||||
|
||||
for (int i = 0; i < 64; i++)
|
||||
RunPair(queue, sink, reserve, immediate);
|
||||
Type keyType = typeof(ParticleRenderer).GetNestedType(
|
||||
"MeshBatchKey", BindingFlags.NonPublic)!;
|
||||
Type instanceType = typeof(ParticleRenderer).GetNestedType(
|
||||
"MeshParticleInstance", BindingFlags.NonPublic)!;
|
||||
Type drawType = typeof(ParticleRenderer).GetNestedType(
|
||||
"MeshParticleDraw", BindingFlags.NonPublic)!;
|
||||
object key = Activator.CreateInstance(
|
||||
keyType,
|
||||
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
|
||||
binder: null,
|
||||
args: [0x010001ECu, 0],
|
||||
culture: null)!;
|
||||
object instance = Activator.CreateInstance(
|
||||
instanceType,
|
||||
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
|
||||
binder: null,
|
||||
args: [Matrix4x4.Identity, 0xFFFFFFFFu, 0f, 0u],
|
||||
culture: null)!;
|
||||
object draw = Activator.CreateInstance(
|
||||
drawType,
|
||||
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
|
||||
binder: null,
|
||||
args: [key, renderData.Batches[0], instance],
|
||||
culture: null)!;
|
||||
var drawList = (IList)typeof(ParticleRenderer).GetField(
|
||||
"_meshDrawListScratch",
|
||||
BindingFlags.Instance | BindingFlags.NonPublic)!
|
||||
.GetValue(renderer)!;
|
||||
drawList.Add(draw);
|
||||
var immediate = (ParticleRenderer.DrawImmediateParticle)typeof(ParticleRenderer)
|
||||
.GetField("_drawImmediateParticle", BindingFlags.Instance | BindingFlags.NonPublic)!
|
||||
.GetValue(renderer)!;
|
||||
|
||||
long before = GC.GetAllocatedBytesForCurrentThread();
|
||||
for (int i = 0; i < 1_000; i++)
|
||||
RunPair(queue, sink, reserve, immediate);
|
||||
long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
|
||||
frames.BeginFrame();
|
||||
renderer.BeginFrame(frameSlot: 0);
|
||||
IGpuFrame frame = frames.CurrentFrame!;
|
||||
using IGpuPassEncoder pass = frame.BeginPass(GpuPassDescription.BackbufferClear(
|
||||
"s4-c2-particle-allocation",
|
||||
Vector4.Zero,
|
||||
sampleCount: 1));
|
||||
using IDisposable publication = scope.Publish(pass);
|
||||
|
||||
device.Clear();
|
||||
device.RecordingEnabled = false;
|
||||
long allocated = ZeroAllocationProbe.MeasureWarmed(
|
||||
() => immediate(
|
||||
Matrix4x4.Identity,
|
||||
ParticleSubmissionKind.Mesh,
|
||||
drawIndex: 0,
|
||||
opaqueDepthState: true),
|
||||
batchSize: 256,
|
||||
warmupBatches: 2,
|
||||
samples: 4);
|
||||
|
||||
Assert.Equal(0, allocated);
|
||||
}
|
||||
|
||||
private static void RunPair(
|
||||
RetailAlphaQueue queue,
|
||||
NoAllocParticleSink sink,
|
||||
ParticleRenderer.ReserveDeferredParticleDraw reserve,
|
||||
ParticleRenderer.DrawImmediateParticle immediate)
|
||||
{
|
||||
queue.BeginFrame();
|
||||
ParticleRenderer.DeferToRetailAlphaQueue(
|
||||
ParticleSubmissionKind.Mesh,
|
||||
TranslucencyKind.Opaque,
|
||||
0x80FFFFFFu,
|
||||
queue,
|
||||
sink,
|
||||
reserve,
|
||||
immediate,
|
||||
Matrix4x4.Identity,
|
||||
drawIndex: 0);
|
||||
ParticleRenderer.DeferToRetailAlphaQueue(
|
||||
ParticleSubmissionKind.Mesh,
|
||||
TranslucencyKind.Opaque,
|
||||
0xFFFFFFFFu,
|
||||
queue,
|
||||
sink,
|
||||
reserve,
|
||||
immediate,
|
||||
Matrix4x4.Identity,
|
||||
drawIndex: 1);
|
||||
queue.EndFrame();
|
||||
}
|
||||
|
||||
private sealed class RecordingSource : IRetailAlphaDrawSource
|
||||
{
|
||||
public int PrepareCount { get; private set; }
|
||||
|
|
@ -252,31 +310,6 @@ public sealed class ParticleRendererRouteTests
|
|||
}
|
||||
}
|
||||
|
||||
private sealed class NoAllocParticleSink : IRetailAlphaDrawSource
|
||||
{
|
||||
private int _nextToken;
|
||||
|
||||
public int Reserve() => _nextToken++;
|
||||
|
||||
public void DrawImmediate(
|
||||
Matrix4x4 viewProjection,
|
||||
ParticleSubmissionKind kind,
|
||||
int drawIndex,
|
||||
bool opaqueDepthState)
|
||||
{
|
||||
}
|
||||
|
||||
public void PrepareAlphaDraws(ReadOnlySpan<int> tokens)
|
||||
{
|
||||
}
|
||||
|
||||
public void DrawPreparedAlphaBatch(int firstPreparedDraw, int drawCount)
|
||||
{
|
||||
}
|
||||
|
||||
public void ResetAlphaSubmissions() => _nextToken = 0;
|
||||
}
|
||||
|
||||
private sealed class NullPreparedAssetSource : IPreparedAssetSource
|
||||
{
|
||||
public PreparedAssetSourceStats Stats => default;
|
||||
|
|
|
|||
|
|
@ -240,9 +240,10 @@ public sealed class RetailAlphaMeshRouterTests
|
|||
|
||||
/// <summary>
|
||||
/// <see cref="RetailAlphaMeshRouter.MaskFromTranslucencyKind"/>'s
|
||||
/// reachable mapping (opaque/ClipMap are pre-filtered upstream and never
|
||||
/// actually call this from a live site, but the function itself must
|
||||
/// still map them defensively).
|
||||
/// complete mapping. Ordinary Wb callers pre-filter opaque/ClipMap and
|
||||
/// retain AP-239's earlier subset-mask reconstruction distinction. Mesh
|
||||
/// particles, however, genuinely call this method without that ordinary-
|
||||
/// Wb pre-filter, including for opaque-classified batches.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData(TranslucencyKind.AlphaBlend, RetailAlphaMeshRouter.MaskAlphaFamily)]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue